Crate simrs

source · []
Expand description

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());
    }));
}

Structs

This struct exposes only immutable access to the simulation clock. The clock itself is owned by the scheduler, while others can obtain ClockRef to read the current simulation time.

A type-safe identifier of a component. This is an analogue of Key used specifically for components.

Container holding type-erased components.

Entry type stored in the scheduler, including the event value, component ID, and the time when it is supposed to occur.

Executor is used for simple execution of an entire simulation.

Abstraction over VecDeque that allows to limit the capacity of the queue. This means that push operations can fail. By default, the capacity is equal to usize::MAX, which makes unlimited in practice.

A type-safe key used to fetch values from the value store.

Binary heap implementation of Queue.

Error return when an attempt to push an element to a queue fails due to the queue having reached its capacity.

A type-safe identifier of a queue. This is an analogue of Key used specifically for queues.

Scheduler is used to keep the current time and information about the upcoming events.

Simulation struct that puts different parts of the simulation together.

State of a simulation holding all queues and arbitrary values in a store value.

Traits

Interface of a simulation component.

Simulation execution trait.

Trait implemented by the queues used in the simulation.