1
0
forked from ROMEO/nexosim
2024-11-15 16:52:24 +01:00

453 lines
18 KiB
Rust

//! A high-performance, discrete-event computation framework for system
//! simulation.
//!
//! Asynchronix is a developer-friendly, yet highly optimized software simulator
//! able to scale to very large simulation with complex time-driven state
//! machines.
//!
//! It promotes a component-oriented architecture that is familiar to system
//! engineers and closely resembles [flow-based programming][FBP]: a model is
//! essentially an isolated entity with a fixed set of typed inputs and outputs,
//! communicating with other models through message passing via connections
//! defined during bench assembly. Unlike in conventional flow-based
//! programming, request-reply patterns are also possible.
//!
//! Asynchronix leverages asynchronous programming to perform
//! auto-parallelization in a manner that is fully transparent to model authors
//! and users, achieving high computational throughput on large simulation
//! benches by means of a custom multi-threaded executor.
//!
//!
//! [FBP]: https://en.wikipedia.org/wiki/Flow-based_programming
//!
//! # A practical overview
//!
//! Simulating a system typically involves three distinct activities:
//!
//! 1. the design of simulation models for each sub-system,
//! 2. the assembly of a simulation bench from a set of models, performed by
//! inter-connecting model ports,
//! 3. the execution of the simulation, managed through periodical increments of
//! the simulation time and by exchange of messages with simulation models.
//!
//! The following sections go through each of these activities in more details.
//!
//! ## Authoring models
//!
//! Models can contain four kinds of ports:
//!
//! * _output ports_, which are instances of the [`Output`](ports::Output) type
//! and can be used to broadcast a message,
//! * _requestor ports_, which are instances of the
//! [`Requestor`](ports::Requestor) type and can be used to broadcast a
//! message and receive an iterator yielding the replies from all connected
//! replier ports,
//! * _input ports_, which are synchronous or asynchronous methods that
//! implement the [`InputFn`](ports::InputFn) trait and take an `&mut self`
//! argument, a message argument, and an optional
//! [`&mut Context`](model::Context) argument,
//! * _replier ports_, which are similar to input ports but implement the
//! [`ReplierFn`](ports::ReplierFn) trait and return a reply.
//!
//! Messages that are broadcast by an output port to an input port are referred
//! to as *events*, while messages exchanged between requestor and replier ports
//! are referred to as *requests* and *replies*.
//!
//! Models must implement the [`Model`](model::Model) trait. The main purpose of
//! this trait is to allow models to specify
//! * a `setup()` method that is called once during model addtion to simulation,
//! this method allows e.g. creation and interconnection of submodels inside
//! the model,
//! * an `init()` method that is guaranteed to run once and only once when the
//! simulation is initialized, _i.e._ after all models have been connected but
//! before the simulation starts.
//!
//! The `setup()` and `init()` methods have default implementations, so models
//! that do not require setup and initialization can simply implement the trait
//! with a one-liner such as `impl Model for MyModel {}`.
//!
//! #### A simple model
//!
//! Let us consider for illustration a simple model that forwards its input
//! after multiplying it by 2. This model has only one input and one output
//! port:
//!
//! ```text
//! ┌────────────┐
//! │ │
//! Input ●───────►│ Multiplier ├───────► Output
//! f64 │ │ f64
//! └────────────┘
//! ```
//!
//! `Multiplier` could be implemented as follows:
//!
//! ```
//! use asynchronix::model::Model;
//! use asynchronix::ports::Output;
//!
//! #[derive(Default)]
//! pub struct Multiplier {
//! pub output: Output<f64>,
//! }
//! impl Multiplier {
//! pub async fn input(&mut self, value: f64) {
//! self.output.send(2.0 * value).await;
//! }
//! }
//! impl Model for Multiplier {}
//! ```
//!
//! #### A model using the local context
//!
//! Models frequently need to schedule actions at a future time or simply get
//! access to the current simulation time. To do so, input and replier methods
//! can take an optional argument that gives them access to a local context.
//!
//! To show how the local context can be used in practice, let us implement
//! `Delay`, a model which simply forwards its input unmodified after a 1s
//! delay:
//!
//! ```
//! use std::time::Duration;
//! use asynchronix::model::{Context, Model};
//! use asynchronix::ports::Output;
//!
//! #[derive(Default)]
//! pub struct Delay {
//! pub output: Output<f64>,
//! }
//! impl Delay {
//! pub fn input(&mut self, value: f64, cx: &mut Context<Self>) {
//! cx.schedule_event(Duration::from_secs(1), Self::send, value).unwrap();
//! }
//!
//! async fn send(&mut self, value: f64) {
//! self.output.send(value).await;
//! }
//! }
//! impl Model for Delay {}
//! ```
//!
//! ## Assembling simulation benches
//!
//! A simulation bench is a system of inter-connected models that have been
//! migrated to a simulation.
//!
//! The assembly process usually starts with the instantiation of models and the
//! creation of a [`Mailbox`](simulation::Mailbox) for each model. A mailbox is
//! essentially a fixed-capacity buffer for events and requests. While each
//! model has only one mailbox, it is possible to create an arbitrary number of
//! [`Address`](simulation::Mailbox)es pointing to that mailbox.
//!
//! Addresses are used among others to connect models: each output or requestor
//! port has a `connect()` method that takes as argument a function pointer to
//! the corresponding input or replier port method and the address of the
//! targeted model.
//!
//! Once all models are connected, they are added to a
//! [`SimInit`](simulation::SimInit) instance, which is a builder type for the
//! final [`Simulation`](simulation::Simulation).
//!
//! The easiest way to understand the assembly step is with a short example. Say
//! that we want to assemble the following system from the models implemented
//! above:
//!
//! ```text
//! ┌────────────┐
//! │ │
//! ┌──►│ Delay ├──┐
//! ┌────────────┐ │ │ │ │ ┌────────────┐
//! │ │ │ └────────────┘ │ │ │
//! Input ●──►│ Multiplier ├───┤ ├──►│ Delay ├──► Output
//! │ │ │ ┌────────────┐ │ │ │
//! └────────────┘ │ │ │ │ └────────────┘
//! └──►│ Multiplier ├──┘
//! │ │
//! └────────────┘
//! ```
//!
//! Here is how this could be done:
//!
//! ```
//! # mod models {
//! # use std::time::Duration;
//! # use asynchronix::model::{Context, Model};
//! # use asynchronix::ports::Output;
//! # #[derive(Default)]
//! # pub struct Multiplier {
//! # pub output: Output<f64>,
//! # }
//! # impl Multiplier {
//! # pub async fn input(&mut self, value: f64) {
//! # self.output.send(2.0 * value).await;
//! # }
//! # }
//! # impl Model for Multiplier {}
//! # #[derive(Default)]
//! # pub struct Delay {
//! # pub output: Output<f64>,
//! # }
//! # impl Delay {
//! # pub fn input(&mut self, value: f64, cx: &mut Context<Self>) {
//! # cx.schedule_event(Duration::from_secs(1), Self::send, value).unwrap();
//! # }
//! # async fn send(&mut self, value: f64) { // this method can be private
//! # self.output.send(value).await;
//! # }
//! # }
//! # impl Model for Delay {}
//! # }
//! use std::time::Duration;
//! use asynchronix::ports::EventSlot;
//! use asynchronix::simulation::{Mailbox, SimInit};
//! use asynchronix::time::MonotonicTime;
//!
//! use models::{Delay, Multiplier};
//!
//! // Instantiate models.
//! let mut multiplier1 = Multiplier::default();
//! let mut multiplier2 = Multiplier::default();
//! let mut delay1 = Delay::default();
//! let mut delay2 = Delay::default();
//!
//! // Instantiate mailboxes.
//! let multiplier1_mbox = Mailbox::new();
//! let multiplier2_mbox = Mailbox::new();
//! let delay1_mbox = Mailbox::new();
//! let delay2_mbox = Mailbox::new();
//!
//! // Connect the models.
//! multiplier1.output.connect(Delay::input, &delay1_mbox);
//! multiplier1.output.connect(Multiplier::input, &multiplier2_mbox);
//! multiplier2.output.connect(Delay::input, &delay2_mbox);
//! delay1.output.connect(Delay::input, &delay2_mbox);
//!
//! // Keep handles to the system input and output for the simulation.
//! let mut output_slot = EventSlot::new();
//! delay2.output.connect_sink(&output_slot);
//! let input_address = multiplier1_mbox.address();
//!
//! // Pick an arbitrary simulation start time and build the simulation.
//! let t0 = MonotonicTime::EPOCH;
//! let mut simu = SimInit::new()
//! .add_model(multiplier1, multiplier1_mbox, "multiplier1")
//! .add_model(multiplier2, multiplier2_mbox, "multiplier2")
//! .add_model(delay1, delay1_mbox, "delay1")
//! .add_model(delay2, delay2_mbox, "delay2")
//! .init(t0)?;
//!
//! # Ok::<(), asynchronix::simulation::SimulationError>(())
//! ```
//!
//! ## Running simulations
//!
//! The simulation can be controlled in several ways:
//!
//! 1. by advancing time, either until the next scheduled event with
//! [`Simulation::step()`](simulation::Simulation::step), or until a specific
//! deadline with
//! [`Simulation::step_until()`](simulation::Simulation::step_until).
//! 2. by sending events or queries without advancing simulation time, using
//! [`Simulation::process_event()`](simulation::Simulation::process_event) or
//! [`Simulation::send_query()`](simulation::Simulation::process_query),
//! 3. by scheduling events, using for instance
//! [`Scheduler::schedule_event()`](simulation::Scheduler::schedule_event).
//!
//! When initialized with the default clock, the simulation will run as fast as
//! possible, without regard for the actual wall clock time. Alternatively, the
//! simulation time can be synchronized to the wall clock time using
//! [`SimInit::set_clock()`](simulation::SimInit::set_clock) and providing a
//! custom [`Clock`](time::Clock) type or a readily-available real-time clock
//! such as [`AutoSystemClock`](time::AutoSystemClock).
//!
//! Simulation outputs can be monitored using [`EventSlot`](ports::EventSlot)s
//! and [`EventBuffer`](ports::EventBuffer)s, which can be connected to any
//! model's output port. While an event slot only gives access to the last value
//! sent from a port, an event stream is an iterator that yields all events that
//! were sent in first-in-first-out order.
//!
//! This is an example of simulation that could be performed using the above
//! bench assembly:
//!
//! ```
//! # mod models {
//! # use std::time::Duration;
//! # use asynchronix::model::{Context, Model};
//! # use asynchronix::ports::Output;
//! # #[derive(Default)]
//! # pub struct Multiplier {
//! # pub output: Output<f64>,
//! # }
//! # impl Multiplier {
//! # pub async fn input(&mut self, value: f64) {
//! # self.output.send(2.0 * value).await;
//! # }
//! # }
//! # impl Model for Multiplier {}
//! # #[derive(Default)]
//! # pub struct Delay {
//! # pub output: Output<f64>,
//! # }
//! # impl Delay {
//! # pub fn input(&mut self, value: f64, cx: &mut Context<Self>) {
//! # cx.schedule_event(Duration::from_secs(1), Self::send, value).unwrap();
//! # }
//! # async fn send(&mut self, value: f64) { // this method can be private
//! # self.output.send(value).await;
//! # }
//! # }
//! # impl Model for Delay {}
//! # }
//! # use std::time::Duration;
//! # use asynchronix::ports::EventSlot;
//! # use asynchronix::simulation::{Mailbox, SimInit};
//! # use asynchronix::time::MonotonicTime;
//! # use models::{Delay, Multiplier};
//! # let mut multiplier1 = Multiplier::default();
//! # let mut multiplier2 = Multiplier::default();
//! # let mut delay1 = Delay::default();
//! # let mut delay2 = Delay::default();
//! # let multiplier1_mbox = Mailbox::new();
//! # let multiplier2_mbox = Mailbox::new();
//! # let delay1_mbox = Mailbox::new();
//! # let delay2_mbox = Mailbox::new();
//! # multiplier1.output.connect(Delay::input, &delay1_mbox);
//! # multiplier1.output.connect(Multiplier::input, &multiplier2_mbox);
//! # multiplier2.output.connect(Delay::input, &delay2_mbox);
//! # delay1.output.connect(Delay::input, &delay2_mbox);
//! # let mut output_slot = EventSlot::new();
//! # delay2.output.connect_sink(&output_slot);
//! # let input_address = multiplier1_mbox.address();
//! # let t0 = MonotonicTime::EPOCH;
//! # let mut simu = SimInit::new()
//! # .add_model(multiplier1, multiplier1_mbox, "multiplier1")
//! # .add_model(multiplier2, multiplier2_mbox, "multiplier2")
//! # .add_model(delay1, delay1_mbox, "delay1")
//! # .add_model(delay2, delay2_mbox, "delay2")
//! # .init(t0)?
//! # .0;
//! // Send a value to the first multiplier.
//! simu.process_event(Multiplier::input, 21.0, &input_address)?;
//!
//! // The simulation is still at t0 so nothing is expected at the output of the
//! // second delay gate.
//! assert!(output_slot.next().is_none());
//!
//! // Advance simulation time until the next event and check the time and output.
//! simu.step()?;
//! assert_eq!(simu.time(), t0 + Duration::from_secs(1));
//! assert_eq!(output_slot.next(), Some(84.0));
//!
//! // Get the answer to the ultimate question of life, the universe & everything.
//! simu.step()?;
//! assert_eq!(simu.time(), t0 + Duration::from_secs(2));
//! assert_eq!(output_slot.next(), Some(42.0));
//!
//! # Ok::<(), asynchronix::simulation::SimulationError>(())
//! ```
//!
//! # Message ordering guarantees
//!
//! The Asynchronix runtime is based on the [actor model][actor_model], meaning
//! that every simulation model can be thought of as an isolated entity running
//! in its own thread. While in practice the runtime will actually multiplex and
//! migrate models over a fixed set of kernel threads, models will indeed run in
//! parallel whenever possible.
//!
//! Since Asynchronix is a time-based simulator, the runtime will always execute
//! tasks in chronological order, thus eliminating most ordering ambiguities
//! that could result from parallel execution. Nevertheless, it is sometimes
//! possible for events and queries generated in the same time slice to lead to
//! ambiguous execution orders. In order to make it easier to reason about such
//! situations, Asynchronix provides a set of guarantees about message delivery
//! order. Borrowing from the [Pony][pony] programming language, we refer to
//! this contract as *causal messaging*, a property that can be summarized by
//! these two rules:
//!
//! 1. *one-to-one message ordering guarantee*: if model `A` sends two events or
//! queries `M1` and then `M2` to model `B`, then `B` will always process
//! `M1` before `M2`,
//! 2. *transitivity guarantee*: if `A` sends `M1` to `B` and then `M2` to `C`
//! which in turn sends `M3` to `B`, even though `M1` and `M2` may be
//! processed in any order by `B` and `C`, it is guaranteed that `B` will
//! process `M1` before `M3`.
//!
//! The first guarantee (and only the first) also extends to events scheduled
//! from a simulation with a
//! [`Scheduler::schedule_*()`](simulation::Scheduler::schedule_event) method:
//! if the scheduler contains several events to be delivered at the same time to
//! the same model, these events will always be processed in the order in which
//! they were scheduled.
//!
//! [actor_model]: https://en.wikipedia.org/wiki/Actor_model
//! [pony]: https://www.ponylang.io/
//!
//!
//! # Feature flags
//!
//! ## Tracing support
//!
//! The `tracing` feature flag provides support for the
//! [`tracing`](https://docs.rs/tracing/latest/tracing/) crate and can be
//! activated in `Cargo.toml` with:
//!
//! ```toml
//! [dependencies]
//! asynchronix = { version = "0.3", features = ["tracing"] }
//! ```
//!
//! See the [`tracing`] module for more information.
//!
//!
//! # Other resources
//!
//! ## Other examples
//!
//! The [`examples`][gh_examples] directory in the main repository contains more
//! fleshed out examples that demonstrate various capabilities of the simulation
//! framework.
//!
//! [gh_examples]:
//! https://github.com/asynchronics/asynchronix/tree/main/asynchronix/examples
//!
//! ## Modules documentation
//!
//! While the above overview does cover the basic concepts, more information is
//! available in the documentation of the different modules:
//!
//! * the [`model`] module provides more details about the signatures of input
//! and replier port methods and discusses model initialization in the
//! documentation of [`model::Model`] and self-scheduling methods as well as
//! scheduling cancellation in the documentation of [`model::Context`],
//! * the [`simulation`] module discusses how the capacity of mailboxes may
//! affect the simulation, how connections can be modified after the
//! simulation was instantiated, and which pathological situations can lead to
//! a deadlock,
//! * the [`time`] module discusses in particular the monotonic timestamp format
//! used for simulations ([`time::MonotonicTime`]).
#![warn(missing_docs, missing_debug_implementations, unreachable_pub)]
#![cfg_attr(docsrs, feature(doc_auto_cfg, doc_cfg_hide))]
#![cfg_attr(docsrs, doc(cfg_hide(feature = "dev-hooks")))]
pub(crate) mod channel;
pub(crate) mod executor;
mod loom_exports;
pub(crate) mod macros;
pub mod model;
pub mod ports;
pub mod simulation;
pub mod time;
pub(crate) mod util;
#[cfg(feature = "grpc")]
pub mod grpc;
#[cfg(feature = "grpc")]
pub mod registry;
#[cfg(feature = "tracing")]
pub mod tracing;
#[cfg(feature = "dev-hooks")]
pub mod dev_hooks;