4 releases

0.2.0 Jun 21, 2022
0.1.2 Mar 28, 2021
0.1.1 Dec 4, 2020
0.1.0 Nov 9, 2020

#144 in Simulation

MIT/Apache

49KB
1K SLoC

SimRs

This is an experimental discrete event simulation library I use in my research project. I'm not sure at the moment how much value it has for others but I found the things I did here interesting, so maybe someone else will as well.

Visit https://docs.rs/simrs/0.1.2/simrs/ for more information and documentation.

If you have any comments, suggestions, or criticism, don't hesitate to reach out by either contacting me directly or opening an issue.


lib.rs:

General purpose simulation library that provides the mechanisms such as: scheduler, state, queues, etc.

NOTE: This is all experimental right now.

The key public types are State, Scheduler, Components, and Simulation. These, along with user-defined simulation components (structs implementing the Component trait), are the simulation building blocks. Let's first explain each of the above, and then look at examples.

State

A simulation must have the ability to mutate its state. This functionality is fully delegated to the State struct. It can store, remove, and modify values of arbitrary types T: 'static. It also allows us to create queues that can be used to move data between components.

Value Store

State exposes several simple functions to insert, access, modify, and remove values. Existing values are manipulated using special type-safe keys that are generated and returned when inserting the values.

let mut state = State::default();
let key = state.insert(7);
assert_eq!(state.remove(key), Some(7));

Note that the following will fail to compile because of incompatible types:

let mut state = State::default();
let int_key = state.insert(7);
let str_key = state.insert("str");
let v: i32 = state.get(str_key);

Queues

Queues work very similar to storing values but have a different user-facing interface. The access is also done through a key type. However, a different type QueueId is used for clarity.

let mut state = State::default();
let queue_id = state.add_queue(Fifo::default());
state.send(queue_id, 1);
assert_eq!(state.len(queue_id), 1);
assert_eq!(state.recv(queue_id), Some(1));
assert_eq!(state.recv(queue_id), None);

Additionally, a bounded queue is available, which will return an error if the size reached the capacity.

let mut state = State::default();
let queue_capacity = 1;
let queue_id = state.add_queue(Fifo::bounded(queue_capacity));
assert!(state.send(queue_id, 1).is_ok());
assert_eq!(state.len(queue_id), 1);
assert!(!state.send(queue_id, 2).is_ok());
assert_eq!(state.len(queue_id), 1);

Components

The Components structure is a container for all registered components. Similarly to values and queues in the state, components are identified by ComponentId.

struct SomeComponent {
    // ...
}
#[derive(Debug)]
enum SomeEvent {
    A,
    B,
    C,
}
impl Component for SomeComponent {
    type Event = SomeEvent;
    fn process_event(
        &self,
        self_id: ComponentId<Self::Event>,
        event: &Self::Event,
        scheduler: &mut Scheduler,
        state: &mut State,
    ) {
        // Do some work...
    }
}

let mut components = Components::default();
let component_id = components.add_component(SomeComponent::new());

Scheduler

The scheduler's main functionality is to keep track of the simulation time and the future events. Events are scheduled to run on a specific component at a specified time interval. Because the events are type-erased, it's up to the component to downcast the event. To make it easy, each component gets a blanket implementation of an internal trait that does that automatically. It is all encapsulated in the Components container, as shown in the below example:

let mut components = Components::default();
let mut scheduler = Scheduler::default();
let mut state = State::default();
let component_id = components.add_component(SomeComponent::new());
scheduler.schedule(
    Duration::from_secs(1), // schedule 1 second from now
    component_id,
    SomeEvent::A,
);
let event_entry = scheduler.pop().unwrap();
components.process_event_entry(event_entry, &mut scheduler, &mut state);

Simulation

Simulation takes aggregates everything under one structure and provides some additional functions. See the example below.

Example


#[derive(Debug)]
struct Product;

struct Producer {
    outgoing: QueueId<Fifo<Product>>,
    consumer: ComponentId<ConsumerEvent>,
    produced_count: Key<usize>,
}

struct Consumer {
    incoming: QueueId<Fifo<Product>>,
    working_on: Key<Option<Product>>,
}

#[derive(Debug)]
struct ProducerEvent;

#[derive(Debug)]
enum ConsumerEvent {
    Received,
    Finished,
}

impl Producer {
    fn produce(&self) -> Product {
        Product
    }
    fn interval(&self) -> Duration {
        Duration::from_secs(1)
    }
}

impl Consumer {
    fn interval(&self) -> Duration {
        Duration::from_secs(1)
    }
    fn log(&self, product: Product) {
        println!("{:?}", product)
    }
}

impl Component for Producer {
    type Event = ProducerEvent;

    fn process_event(
        &self,
        self_id: ComponentId<ProducerEvent>,
        _event: &ProducerEvent,
        scheduler: &mut Scheduler,
        state: &mut State,
    ) {
        let count = *state.get(self.produced_count).unwrap();
        if count < 10 {
            let _ = state.send(self.outgoing, self.produce());
            scheduler.schedule(self.interval(), self_id, ProducerEvent);
            scheduler.schedule(Duration::default(), self.consumer, ConsumerEvent::Received);
            *state.get_mut(self.produced_count).unwrap() = count + 1;
        }
    }
}

impl Component for Consumer {
    type Event = ConsumerEvent;

    fn process_event(
        &self,
        self_id: ComponentId<ConsumerEvent>,
        event: &ConsumerEvent,
        scheduler: &mut Scheduler,
        state: &mut State,
    ) {
        let busy = state.get(self.working_on).is_some();
        match event {
            ConsumerEvent::Received => {
                if busy {
                    if let Some(product) = state.recv(self.incoming) {
                        state.get_mut(self.working_on).map(|w| *w = Some(product));
                        scheduler.schedule(self.interval(), self_id, ConsumerEvent::Finished);
                    }
                }
            }
            ConsumerEvent::Finished => {
                let product = state.get_mut(self.working_on).unwrap().take().unwrap();
                self.log(product);
                if state.len(self.incoming) > 0 {
                    scheduler.schedule(Duration::default(), self_id, ConsumerEvent::Received);
                }
            }
        }
    }
}

fn main() {
    let mut simulation = Simulation::default();
    let queue = simulation.add_queue(Fifo::default());
    let working_on = simulation.state.insert::<Option<Product>>(None);
    let consumer = simulation.add_component(Consumer {
        incoming: queue,
        working_on,
    });
    let produced_count = simulation.state.insert(0_usize);
    let producer = simulation.add_component(Producer {
        outgoing: queue,
        consumer,
        produced_count,
    });
    simulation.schedule(Duration::new(0, 0), producer, ProducerEvent);
    // simulation.schedule(Duration::new(0, 0), consumer, ProducerEvent);
    // The above would fail with:                         ^^^^^^^^^^^^^ expected enum `ConsumerEvent`, found struct `ProducerEvent`
    simulation.execute(Executor::unbound().side_effect(|sim| {
        println!("{:?}", sim.scheduler.time());
    }));
}

No runtime deps