1
0
forked from ROMEO/nexosim

Merge pull request #81 from asynchronics/feature/release_cleanups

Improve documentation, fix README example
This commit is contained in:
Serge Barral 2025-01-21 02:17:06 +01:00 committed by GitHub
commit 2a4b389977
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 505 additions and 335 deletions

View File

@ -90,20 +90,21 @@ asynchronix = "0.2.4"
// Input ●─────►│ multiplier 1 ├─────►│ multiplier 2 ├─────► Output // Input ●─────►│ multiplier 1 ├─────►│ multiplier 2 ├─────► Output
// │ │ │ │ // │ │ │ │
// └──────────────┘ └──────────────┘ // └──────────────┘ └──────────────┘
use nexosim::model::{Model, Output};
use nexosim::simulation::{Mailbox, SimInit};
use nexosim::time::{MonotonicTime, Scheduler};
use std::time::Duration; use std::time::Duration;
use nexosim::model::{Context, Model};
use nexosim::ports::{EventSlot, Output};
use nexosim::simulation::{Mailbox, SimInit};
use nexosim::time::MonotonicTime;
// A model that doubles its input and forwards it with a 1s delay. // A model that doubles its input and forwards it with a 1s delay.
#[derive(Default)] #[derive(Default)]
pub struct DelayedMultiplier { pub struct DelayedMultiplier {
pub output: Output<f64>, pub output: Output<f64>,
} }
impl DelayedMultiplier { impl DelayedMultiplier {
pub fn input(&mut self, value: f64, scheduler: &Scheduler<Self>) { pub fn input(&mut self, value: f64, ctx: &mut Context<Self>) {
scheduler ctx.schedule_event(Duration::from_secs(1), Self::send, 2.0 * value)
.schedule_event(Duration::from_secs(1), Self::send, 2.0 * value)
.unwrap(); .unwrap();
} }
async fn send(&mut self, value: f64) { async fn send(&mut self, value: f64) {
@ -124,28 +125,32 @@ multiplier1
.connect(DelayedMultiplier::input, &multiplier2_mbox); .connect(DelayedMultiplier::input, &multiplier2_mbox);
// Keep handles to the main input and output. // Keep handles to the main input and output.
let mut output_slot = multiplier2.output.connect_slot().0; let mut output_slot = EventSlot::new();
multiplier2.output.connect_sink(&output_slot);
let input_address = multiplier1_mbox.address(); let input_address = multiplier1_mbox.address();
// Instantiate the simulator // Instantiate the simulator
let t0 = MonotonicTime::EPOCH; // arbitrary start time let t0 = MonotonicTime::EPOCH; // arbitrary start time
let mut simu = SimInit::new() let mut simu = SimInit::new()
.add_model(multiplier1, multiplier1_mbox) .add_model(multiplier1, multiplier1_mbox, "multiplier 1")
.add_model(multiplier2, multiplier2_mbox) .add_model(multiplier2, multiplier2_mbox, "multiplier 2")
.init(t0); .init(t0)?
.0;
// Send a value to the first multiplier. // Send a value to the first multiplier.
simu.send_event(DelayedMultiplier::input, 3.5, &input_address); simu.process_event(DelayedMultiplier::input, 3.5, &input_address)?;
// Advance time to the next event. // Advance time to the next event.
simu.step(); simu.step()?;
assert_eq!(simu.time(), t0 + Duration::from_secs(1)); assert_eq!(simu.time(), t0 + Duration::from_secs(1));
assert_eq!(output_slot.take(), None); assert_eq!(output_slot.next(), None);
// Advance time to the next event. // Advance time to the next event.
simu.step(); simu.step()?;
assert_eq!(simu.time(), t0 + Duration::from_secs(2)); assert_eq!(simu.time(), t0 + Duration::from_secs(2));
assert_eq!(output_slot.take(), Some(14.0)); assert_eq!(output_slot.next(), Some(14.0));
Ok(())
``` ```
# Implementation notes # Implementation notes

View File

@ -29,7 +29,6 @@ keywords = [
] ]
[features] [features]
# gRPC service.
server = [ server = [
"dep:bytes", "dep:bytes",
"dep:ciborium", "dep:ciborium",

View File

@ -79,8 +79,8 @@ impl Executor {
/// Spawns a task which output will never be retrieved. /// Spawns a task which output will never be retrieved.
/// ///
/// Note that spawned tasks are not executed until [`run()`](Executor::run) /// Note that spawned tasks are not executed until [`run`](Executor::run) is
/// is called. /// called.
#[allow(unused)] #[allow(unused)]
pub(crate) fn spawn<T>(&self, future: T) -> Promise<T::Output> pub(crate) fn spawn<T>(&self, future: T) -> Promise<T::Output>
where where
@ -95,8 +95,8 @@ impl Executor {
/// Spawns a task which output will never be retrieved. /// Spawns a task which output will never be retrieved.
/// ///
/// Note that spawned tasks are not executed until [`run()`](Executor::run) /// Note that spawned tasks are not executed until [`run`](Executor::run) is
/// is called. /// called.
pub(crate) fn spawn_and_forget<T>(&self, future: T) pub(crate) fn spawn_and_forget<T>(&self, future: T)
where where
T: Future + Send + 'static, T: Future + Send + 'static,

View File

@ -186,8 +186,8 @@ impl Executor {
/// Spawns a task and returns a promise that can be polled to retrieve the /// Spawns a task and returns a promise that can be polled to retrieve the
/// task's output. /// task's output.
/// ///
/// Note that spawned tasks are not executed until [`run()`](Executor::run) /// Note that spawned tasks are not executed until [`run`](Executor::run) is
/// is called. /// called.
pub(crate) fn spawn<T>(&self, future: T) -> Promise<T::Output> pub(crate) fn spawn<T>(&self, future: T) -> Promise<T::Output>
where where
T: Future + Send + 'static, T: Future + Send + 'static,
@ -215,8 +215,8 @@ impl Executor {
/// This is mostly useful to avoid undue reference counting for futures that /// This is mostly useful to avoid undue reference counting for futures that
/// return a `()` type. /// return a `()` type.
/// ///
/// Note that spawned tasks are not executed until [`run()`](Executor::run) /// Note that spawned tasks are not executed until [`run`](Executor::run) is
/// is called. /// called.
pub(crate) fn spawn_and_forget<T>(&self, future: T) pub(crate) fn spawn_and_forget<T>(&self, future: T)
where where
T: Future + Send + 'static, T: Future + Send + 'static,

View File

@ -59,7 +59,7 @@ impl Executor {
/// Spawns a task and returns a promise that can be polled to retrieve the /// Spawns a task and returns a promise that can be polled to retrieve the
/// task's output. /// task's output.
/// ///
/// Note that spawned tasks are not executed until [`run()`](Executor::run) /// Note that spawned tasks are not executed until [`run`](Executor::run)
/// is called. /// is called.
pub(crate) fn spawn<T>(&self, future: T) -> Promise<T::Output> pub(crate) fn spawn<T>(&self, future: T) -> Promise<T::Output>
where where
@ -91,7 +91,7 @@ impl Executor {
/// This is mostly useful to avoid undue reference counting for futures that /// This is mostly useful to avoid undue reference counting for futures that
/// return a `()` type. /// return a `()` type.
/// ///
/// Note that spawned tasks are not executed until [`run()`](Executor::run) /// Note that spawned tasks are not executed until [`run`](Executor::run)
/// is called. /// is called.
pub(crate) fn spawn_and_forget<T>(&self, future: T) pub(crate) fn spawn_and_forget<T>(&self, future: T)
where where

View File

@ -39,9 +39,9 @@
//! * _output ports_, which are instances of the [`Output`](ports::Output) type //! * _output ports_, which are instances of the [`Output`](ports::Output) type
//! and can be used to broadcast a message, //! and can be used to broadcast a message,
//! * _requestor ports_, which are instances of the //! * _requestor ports_, which are instances of the
//! [`Requestor`](ports::Requestor) type and can be used to broadcast a //! [`Requestor`](ports::Requestor) or [`UniRequestor`](ports::UniRequestor)
//! message and receive an iterator yielding the replies from all connected //! types and can be used to broadcast a message and receive an iterator
//! replier ports, //! yielding the replies from all connected replier ports,
//! * _input ports_, which are synchronous or asynchronous methods that //! * _input ports_, which are synchronous or asynchronous methods that
//! implement the [`InputFn`](ports::InputFn) trait and take an `&mut self` //! implement the [`InputFn`](ports::InputFn) trait and take an `&mut self`
//! argument, a message argument, and an optional //! argument, a message argument, and an optional
@ -54,19 +54,27 @@
//! are referred to as *requests* and *replies*. //! are referred to as *requests* and *replies*.
//! //!
//! Models must implement the [`Model`](model::Model) trait. The main purpose of //! Models must implement the [`Model`](model::Model) trait. The main purpose of
//! this trait is to allow models to specify //! this trait is to allow models to specify a
//! * a `setup()` method that is called once during model addtion to simulation, //! [`Model::init`](model::Model::init) method that is guaranteed to run once
//! this method allows e.g. creation and interconnection of submodels inside //! and only once when the simulation is initialized, _i.e._ after all models
//! the model, //! have been connected but before the simulation starts.
//! * 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 //! The [`Model::init`](model::Model::init) methods has a default
//! that do not require setup and initialization can simply implement the trait //! implementations, so models that do not require setup and initialization can
//! with a one-liner such as `impl Model for MyModel {}`. //! simply implement the trait with a one-liner such as `impl Model for MyModel
//! {}`.
//! //!
//! #### A simple model //! More complex models can be built with the [`ProtoModel`](model::ProtoModel)
//! trait. The [`ProtoModel::build`](model::ProtoModel::build) method makes it
//! possible to:
//!
//! * build the final [`Model`](model::Model) from a builder (the *model prototype*),
//! * perform possibly blocking actions when the model is added to the
//! simulation rather than when the simulation starts, such as establishing a
//! network connection or configuring hardware devices,
//! * connect submodels and add them to the simulation.
//!
//! ### A simple model
//! //!
//! Let us consider for illustration a simple model that forwards its input //! 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 //! after multiplying it by 2. This model has only one input and one output
@ -98,7 +106,7 @@
//! impl Model for Multiplier {} //! impl Model for Multiplier {}
//! ``` //! ```
//! //!
//! #### A model using the local context //! ### A model using the local context
//! //!
//! Models frequently need to schedule actions at a future time or simply get //! 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 //! access to the current simulation time. To do so, input and replier methods
@ -141,7 +149,7 @@
//! [`Address`](simulation::Mailbox)es pointing to that mailbox. //! [`Address`](simulation::Mailbox)es pointing to that mailbox.
//! //!
//! Addresses are used among others to connect models: each output or requestor //! 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 //! 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 //! the corresponding input or replier port method and the address of the
//! targeted model. //! targeted model.
//! //!
@ -230,7 +238,7 @@
//! //!
//! // Pick an arbitrary simulation start time and build the simulation. //! // Pick an arbitrary simulation start time and build the simulation.
//! let t0 = MonotonicTime::EPOCH; //! let t0 = MonotonicTime::EPOCH;
//! let mut simu = SimInit::new() //! let (mut simu, scheduler) = SimInit::new()
//! .add_model(multiplier1, multiplier1_mbox, "multiplier1") //! .add_model(multiplier1, multiplier1_mbox, "multiplier1")
//! .add_model(multiplier2, multiplier2_mbox, "multiplier2") //! .add_model(multiplier2, multiplier2_mbox, "multiplier2")
//! .add_model(delay1, delay1_mbox, "delay1") //! .add_model(delay1, delay1_mbox, "delay1")
@ -245,27 +253,27 @@
//! The simulation can be controlled in several ways: //! The simulation can be controlled in several ways:
//! //!
//! 1. by advancing time, either until the next scheduled event with //! 1. by advancing time, either until the next scheduled event with
//! [`Simulation::step()`](simulation::Simulation::step), or until a specific //! [`Simulation::step`](simulation::Simulation::step), until a specific
//! deadline with //! deadline with
//! [`Simulation::step_until()`](simulation::Simulation::step_until). //! [`Simulation::step_until`](simulation::Simulation::step_until), or
//! until there are no more scheduled events with
//! [`Simulation::step_unbounded`](simulation::Simulation::step_unbounded).
//! 2. by sending events or queries without advancing simulation time, using //! 2. by sending events or queries without advancing simulation time, using
//! [`Simulation::process_event()`](simulation::Simulation::process_event) or //! [`Simulation::process_event`](simulation::Simulation::process_event) or
//! [`Simulation::send_query()`](simulation::Simulation::process_query), //! [`Simulation::send_query`](simulation::Simulation::process_query),
//! 3. by scheduling events, using for instance //! 3. by scheduling events with a [`Scheduler`](simulation::Scheduler).
//! [`Scheduler::schedule_event()`](simulation::Scheduler::schedule_event).
//! //!
//! When initialized with the default clock, the simulation will run as fast as //! When initialized with the default clock, the simulation will run as fast as
//! possible, without regard for the actual wall clock time. Alternatively, the //! possible, without regard for the actual wall clock time. Alternatively, the
//! simulation time can be synchronized to the wall clock time using //! simulation time can be synchronized to the wall clock time using
//! [`SimInit::set_clock()`](simulation::SimInit::set_clock) and providing a //! [`SimInit::set_clock`](simulation::SimInit::set_clock) and providing a
//! custom [`Clock`](time::Clock) type or a readily-available real-time clock //! custom [`Clock`](time::Clock) type or a readily-available real-time clock
//! such as [`AutoSystemClock`](time::AutoSystemClock). //! such as [`AutoSystemClock`](time::AutoSystemClock).
//! //!
//! Simulation outputs can be monitored using [`EventSlot`](ports::EventSlot)s //! Simulation outputs can be monitored using [`EventSlot`](ports::EventSlot)s,
//! and [`EventBuffer`](ports::EventBuffer)s, which can be connected to any //! [`EventBuffer`](ports::EventBuffer)s, or any implementer of the
//! model's output port. While an event slot only gives access to the last value //! [`EventSink`](ports::EventSink) trait, connected to one or several model
//! sent from a port, an event stream is an iterator that yields all events that //! output ports.
//! were sent in first-in-first-out order.
//! //!
//! This is an example of simulation that could be performed using the above //! This is an example of simulation that could be performed using the above
//! bench assembly: //! bench assembly:
@ -373,20 +381,20 @@
//! processed in any order by `B` and `C`, it is guaranteed that `B` will //! processed in any order by `B` and `C`, it is guaranteed that `B` will
//! process `M1` before `M3`. //! process `M1` before `M3`.
//! //!
//! The first guarantee (and only the first) also extends to events scheduled //! Both guarantees also extend to same-time events scheduled from the global
//! from a simulation with a //! [`Scheduler`](simulation::Scheduler), *i.e.* the relative ordering of events
//! [`Scheduler::schedule_*()`](simulation::Scheduler::schedule_event) method: //! scheduled for the same time is preserved and warranties 1 and 2 above
//! if the scheduler contains several events to be delivered at the same time to //! accordingly hold (assuming model `A` stands for the scheduler). Likewise,
//! the same model, these events will always be processed in the order in which //! the relative order of same-time events self-scheduled by a model using its
//! they were scheduled. //! [`Context`](model::Context) is preserved.
//! //!
//! [actor_model]: https://en.wikipedia.org/wiki/Actor_model //! [actor_model]: https://en.wikipedia.org/wiki/Actor_model
//! [pony]: https://www.ponylang.io/ //! [pony]: https://www.ponylang.io/
//! //!
//! //!
//! # Feature flags //! # Cargo feature flags
//! //!
//! ## Tracing support //! ## Tracing
//! //!
//! The `tracing` feature flag provides support for the //! The `tracing` feature flag provides support for the
//! [`tracing`](https://docs.rs/tracing/latest/tracing/) crate and can be //! [`tracing`](https://docs.rs/tracing/latest/tracing/) crate and can be
@ -409,32 +417,50 @@
//! nexosim = { version = "0.3.0-beta.0", features = ["server"] } //! nexosim = { version = "0.3.0-beta.0", features = ["server"] }
//! ``` //! ```
//! //!
//! See the [`registry`] and [`server`] modules for more information.
//!
//! Front-end usage documentation will be added upon release of the NeXosim
//! Python client.
//!
//!
//! # Other resources //! # Other resources
//! //!
//! ## Other examples //! ## Other examples
//! //!
//! The [`examples`][gh_examples] directory in the main repository contains more //! Several [`examples`][gh_examples] are available that contain more fleshed
//! fleshed out examples that demonstrate various capabilities of the simulation //! out examples and demonstrate various capabilities of the simulation
//! framework. //! framework.
//! //!
//! [gh_examples]: //! [gh_examples]:
//! https://github.com/asynchronics/nexosim/tree/main/nexosim/examples //! https://github.com/asynchronics/nexosim/tree/main/nexosim/examples
//! //!
//! ## Modules documentation
//! //!
//! While the above overview does cover the basic concepts, more information is //! ## Other features and advanced topics
//! available in the documentation of the different modules: //!
//! While the above overview does cover most basic concepts, more information is
//! available in the modules' documentation:
//!
//! * the [`model`] module provides more details about models, **model
//! prototypes** and **hierarchical models**; be sure to check as well the
//! documentation of [`model::Context`] for topics such as **self-scheduling**
//! methods and **event cancellation**,
//! * the [`ports`] module discusses in more details model ports and simulation
//! endpoints, as well as the ability to **modify and filter messages**
//! exchanged between ports; it also provides
//! [`EventSource`](ports::EventSource) and
//! [`QuerySource`](ports::QuerySource) objects which can be connected to
//! models just like [`Output`](ports::Output) and
//! [`Requestor`](ports::Requestor) ports, but for use as simulation
//! endpoints.
//! * the [`registry`] and [`server`] modules make it possible to manage and
//! monitor a simulation locally or remotely from a NeXosim Python client,
//! * the [`simulation`] module discusses **mailbox capacity** and pathological
//! situations that may lead to a **deadlock**,
//! * the [`time`] module introduces the [`time::MonotonicTime`] monotonic
//! timestamp object and **simulation clocks**.
//! * the [`tracing`] module discusses time-stamping and filtering of `tracing`
//! events.
//! //!
//! * 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)] #![warn(missing_docs, missing_debug_implementations, unreachable_pub)]
#![cfg_attr(docsrs, feature(doc_auto_cfg, doc_cfg_hide))] #![cfg_attr(docsrs, feature(doc_auto_cfg, doc_cfg_hide))]
#![cfg_attr(docsrs, doc(cfg_hide(feature = "dev-hooks")))] #![cfg_attr(docsrs, doc(cfg_hide(feature = "dev-hooks")))]
@ -458,4 +484,5 @@ pub mod server;
pub mod tracing; pub mod tracing;
#[cfg(feature = "dev-hooks")] #[cfg(feature = "dev-hooks")]
#[doc(hidden)]
pub mod dev_hooks; pub mod dev_hooks;

View File

@ -5,30 +5,30 @@
//! Every model must implement the [`Model`] trait. This trait defines an //! Every model must implement the [`Model`] trait. This trait defines an
//! asynchronous initialization method, [`Model::init`], which main purpose is //! asynchronous initialization method, [`Model::init`], which main purpose is
//! to enable models to perform specific actions when the simulation starts, //! to enable models to perform specific actions when the simulation starts,
//! i.e. after all models have been connected and added to the simulation. //! *i.e.* after all models have been connected and added to the simulation.
//! //!
//! It is frequently convenient to expose to users a model builder type—called a //! It is frequently convenient to expose to users a model builder type—called a
//! *model prototype*—rather than the final model. This can be done by //! *model prototype*—rather than the final model. This can be done by
//! implementing the [`ProtoModel`] trait, which defines the associated model //! implementing the [`ProtoModel`] trait, which defines the associated model
//! type and a [`ProtoModel::build`] method invoked when a model is added the //! type and a [`ProtoModel::build`] method invoked when a model is added the
//! the simulation and returning the actual model instance. //! simulation.
//! //!
//! Prototype models can be used whenever the Rust builder pattern is helpful, //! Prototype models can be used whenever the Rust builder pattern is helpful,
//! for instance to set optional parameters. One of the use-cases that may //! for instance to set optional parameters. One of the use-cases that may
//! benefit from the use of prototype models is hierarchical model building. //! benefit from the use of prototype models is hierarchical model building.
//! When a parent model contains submodels, these submodels are often an //! When a parent model contains submodels, these submodels are often an
//! implementation detail that needs not be exposed to the user. One may then //! implementation detail that needs not be exposed to the user. One may then
//! define a prototype model that contains all outputs and requestors ports. //! define a prototype model that contains all outputs and requestors ports,
//! Upon invocation of [`ProtoModel::build`], the ports are moved to the //! while the model itself contains the input and replier ports. Upon invocation
//! appropriate submodels and those submodels are added to the simulation. //! of [`ProtoModel::build`], the exit ports are moved to the model or its
//! submodels, and those submodels are added to the simulation.
//! //!
//! Note that a trivial [`ProtoModel`] implementation is generated by default //! Note that a trivial [`ProtoModel`] implementation is generated by default
//! for any object implementing the [`Model`] trait, where the associated //! for any object implementing the [`Model`] trait, where the associated
//! [`ProtoModel::Model`] type is the model type itself and where //! [`ProtoModel::Model`] type is the model type itself. This is what makes it
//! [`ProtoModel::build`] simply returns the model instance. This is what makes //! possible to use either an explicitly-defined [`ProtoModel`] as argument to
//! it possible to use either an explicitly-defined [`ProtoModel`] as argument //! the [`SimInit::add_model`](crate::simulation::SimInit::add_model) method, or
//! to the [`SimInit::add_model`](crate::simulation::SimInit::add_model) method, //! a plain [`Model`] type.
//! or a plain [`Model`] type.
//! //!
//! #### Examples //! #### Examples
//! //!
@ -66,7 +66,9 @@
//! ``` //! ```
//! //!
//! Finally, if a model builder is required, the [`ProtoModel`] trait can be //! Finally, if a model builder is required, the [`ProtoModel`] trait can be
//! explicitly implemented: //! explicitly implemented. Note that the [`ProtoModel`] contains all output and
//! requestor ports, while the associated [`Model`] contains all input and
//! replier methods.
//! //!
//! ``` //! ```
//! use nexosim::model::{BuildContext, InitializedModel, Model, ProtoModel}; //! use nexosim::model::{BuildContext, InitializedModel, Model, ProtoModel};
@ -117,120 +119,81 @@
//! } //! }
//! ``` //! ```
//! //!
//! # Hierarchical models
//! //!
//! # Events and queries //! Hierarchical models are models build from a prototype, which prototype adds
//! submodels to the simulation within its [`ProtoModel::build`] method. From a
//! formal point of view, however, hierarchical models are just regular models
//! implementing the [`Model`] trait, as are their submodels.
//! //!
//! Models can exchange data via *events* and *queries*.
//!
//! Events are send-and-forget messages that can be broadcast from an *output
//! port* to an arbitrary number of *input ports* with a matching event type.
//!
//! Queries actually involve two messages: a *request* that can be broadcast
//! from a *requestor port* to an arbitrary number of *replier ports* with a
//! matching request type, and a *reply* sent in response to such request. The
//! response received by a requestor port is an iterator that yields as many
//! items (replies) as there are connected replier ports.
//!
//!
//! ### Output and requestor ports
//!
//! Output and requestor ports can be added to a model using composition, adding
//! [`Output`](crate::ports::Output) and [`Requestor`](crate::ports::Requestor)
//! objects as members. They are parametrized by the event, request and reply
//! types.
//!
//! Models are expected to expose their output and requestor ports as public
//! members so they can be connected to input and replier ports when assembling
//! the simulation bench.
//! //!
//! #### Example //! #### Example
//! //!
//! ``` //! This example demonstrates a child model inside a parent model, where the
//! use nexosim::model::Model; //! parent model simply forwards input data to the child and the child in turn
//! use nexosim::ports::{Output, Requestor}; //! sends the data to the output exposed by the parent's prototype.
//! //!
//! pub struct MyModel { //! For a more comprehensive example demonstrating hierarchical model
//! pub my_output: Output<String>, //! assemblies, see the [`assembly`][assembly] example.
//! pub my_requestor: Requestor<u32, bool>, //!
//! [assembly]:
//! https://github.com/asynchronics/nexosim/tree/main/nexosim/examples/assembly.rs
//!
//! ```
//! use nexosim::model::{BuildContext, Model, ProtoModel};
//! use nexosim::ports::Output;
//! use nexosim::simulation::Mailbox;
//!
//! pub struct ParentModel {
//! // Private internal port connected to the submodel.
//! to_child: Output<u64>,
//! } //! }
//! impl MyModel { //! impl ParentModel {
//! // ... //! async fn input(&mut self, my_data: u64) {
//! } //! // Forward to the submodel.
//! impl Model for MyModel {} //! self.to_child.send(my_data).await;
//! ```
//!
//!
//! ### Input and replier ports
//!
//! Input ports and replier ports are methods that implement the
//! [`InputFn`](crate::ports::InputFn) or [`ReplierFn`](crate::ports::ReplierFn)
//! traits with appropriate bounds on their argument and return types.
//!
//! In practice, an input port method for an event of type `T` may have any of
//! the following signatures, where the futures returned by the `async` variants
//! must implement `Send`:
//!
//! ```ignore
//! fn(&mut self) // argument elided, implies `T=()`
//! fn(&mut self, T)
//! fn(&mut self, T, &mut Context<Self>)
//! async fn(&mut self) // argument elided, implies `T=()`
//! async fn(&mut self, T)
//! async fn(&mut self, T, &mut Context<Self>)
//! where
//! Self: Model,
//! T: Clone + Send + 'static,
//! R: Send + 'static,
//! ```
//!
//! The context argument is useful for methods that need access to the
//! simulation time or that need to schedule an action at a future date.
//!
//! A replier port for a request of type `T` with a reply of type `R` may in
//! turn have any of the following signatures, where the futures must implement
//! `Send`:
//!
//! ```ignore
//! async fn(&mut self) -> R // argument elided, implies `T=()`
//! async fn(&mut self, T) -> R
//! async fn(&mut self, T, &mut Context<Self>) -> R
//! where
//! Self: Model,
//! T: Clone + Send + 'static,
//! R: Send + 'static,
//! ```
//!
//! Output and replier ports will normally be exposed as public methods so they
//! can be connected to input and requestor ports when assembling the simulation
//! bench. However, input ports may instead be defined as private methods if
//! they are only used by the model itself to schedule future actions (see the
//! [`Context`] examples).
//!
//! Changing the signature of an input or replier port is not considered to
//! alter the public interface of a model provided that the event, request and
//! reply types remain the same.
//!
//! #### Example
//!
//! ```
//! use nexosim::model::{Context, Model};
//!
//! pub struct MyModel {
//! // ...
//! }
//! impl MyModel {
//! pub fn my_input(&mut self, input: String, cx: &mut Context<Self>) {
//! // ...
//! }
//! pub async fn my_replier(&mut self, request: u32) -> bool { // context argument elided
//! // ...
//! # unimplemented!()
//! } //! }
//! } //! }
//! impl Model for MyModel {} //! impl Model for ParentModel {}
//! ```
//! //!
//! pub struct ProtoParentModel {
//! pub output: Output<u64>,
//! }
//! impl ProtoModel for ProtoParentModel {
//! type Model = ParentModel;
//!
//! fn build(self, cx: &mut BuildContext<Self>) -> ParentModel {
//! // Move the output to the child model.
//! let child = ChildModel { output: self.output };
//! let mut parent = ParentModel {
//! to_child: Output::default(),
//! };
//!
//! let child_mailbox = Mailbox::new();
//!
//! // Establish an internal Parent -> Child connection.
//! parent
//! .to_child
//! .connect(ChildModel::input, child_mailbox.address());
//!
//! // Add the child model to the simulation.
//! cx.add_submodel(child, child_mailbox, "child");
//!
//! parent
//! }
//! }
//!
//! struct ChildModel {
//! output: Output<u64>,
//! }
//! impl ChildModel {
//! async fn input(&mut self, my_data: u64) {
//! self.output.send(my_data).await;
//! }
//! }
//! impl Model for ChildModel {}
//!
//! ```
use std::future::Future; use std::future::Future;
pub use context::{BuildContext, Context}; pub use context::{BuildContext, Context};
@ -240,7 +203,7 @@ mod context;
/// Trait to be implemented by simulation models. /// Trait to be implemented by simulation models.
/// ///
/// This trait enables models to perform specific actions during initialization. /// This trait enables models to perform specific actions during initialization.
/// The [`Model::init()`] method is run only once all models have been connected /// The [`Model::init`] method is run only once all models have been connected
/// and migrated to the simulation bench, but before the simulation actually /// and migrated to the simulation bench, but before the simulation actually
/// starts. A common use for `init` is to send messages to connected models at /// starts. A common use for `init` is to send messages to connected models at
/// the beginning of the simulation. /// the beginning of the simulation.
@ -253,7 +216,7 @@ pub trait Model: Sized + Send + 'static {
/// ///
/// This asynchronous method is executed exactly once for all models of the /// This asynchronous method is executed exactly once for all models of the
/// simulation when the /// simulation when the
/// [`SimInit::init()`](crate::simulation::SimInit::init) method is called. /// [`SimInit::init`](crate::simulation::SimInit::init) method is called.
/// ///
/// The default implementation simply converts the model to an /// The default implementation simply converts the model to an
/// `InitializedModel` without any side effect. /// `InitializedModel` without any side effect.
@ -290,7 +253,7 @@ pub trait Model: Sized + Send + 'static {
/// ///
/// A model can be converted to an `InitializedModel` using the `Into`/`From` /// A model can be converted to an `InitializedModel` using the `Into`/`From`
/// traits. The implementation of the simulation guarantees that the /// traits. The implementation of the simulation guarantees that the
/// [`Model::init()`] method will never be called on a model after conversion to /// [`Model::init`] method will never be called on a model after conversion to
/// an `InitializedModel`. /// an `InitializedModel`.
#[derive(Debug)] #[derive(Debug)]
pub struct InitializedModel<M: Model>(pub(crate) M); pub struct InitializedModel<M: Model>(pub(crate) M);
@ -301,18 +264,16 @@ impl<M: Model> From<M> for InitializedModel<M> {
} }
} }
/// Trait to be implemented by model prototypes. /// Trait to be implemented by simulation model prototypes.
/// ///
/// This trait makes it possible to build the final model from a builder type /// This trait makes it possible to build the final model from a builder type
/// when it is added to the simulation. /// when it is added to the simulation.
/// ///
/// The [`ProtoModel::build()`] method consumes the prototype. It is /// The [`ProtoModel::build`] method consumes the prototype. It is
/// automatically called when a model or submodel prototype is added to the /// automatically called when a model or submodel prototype is added to the
/// simulation using /// simulation using
/// [`Simulation::add_model()`](crate::simulation::SimInit::add_model) or /// [`Simulation::add_model`](crate::simulation::SimInit::add_model) or
/// [`BuildContext::add_submodel`]. /// [`BuildContext::add_submodel`].
///
/// The
pub trait ProtoModel: Sized { pub trait ProtoModel: Sized {
/// Type of the model to be built. /// Type of the model to be built.
type Model: Model; type Model: Model;
@ -320,8 +281,8 @@ pub trait ProtoModel: Sized {
/// Builds the model. /// Builds the model.
/// ///
/// This method is invoked when the /// This method is invoked when the
/// [`SimInit::add_model()`](crate::simulation::SimInit::add_model) or /// [`SimInit::add_model`](crate::simulation::SimInit::add_model) or
/// [`BuildContext::add_submodel`] method is called. /// [`BuildContext::add_submodel`] method are called.
fn build(self, cx: &mut BuildContext<Self>) -> Self::Model; fn build(self, cx: &mut BuildContext<Self>) -> Self::Model;
} }

View File

@ -1,63 +1,182 @@
//! Model ports for event and query broadcasting. //! Ports for event and query broadcasting.
//! //!
//! Models typically contain [`Output`] and/or [`Requestor`] ports, exposed as //!
//! public member variables. Output ports broadcast events to all connected //! # Events and queries
//! input ports, while requestor ports broadcast queries to, and retrieve //!
//! replies from, all connected replier ports. //! Models can exchange data via *events* and *queries*.
//!
//! Events are send-and-forget messages that can be broadcast from an [`Output`]
//! port or [`EventSource`] to an arbitrary number of *input ports* or
//! [`EventSink`]s with a matching event type.
//!
//! Queries actually involve two messages: a *request* that can be broadcast
//! from a [`Requestor`] port, a [`UniRequestor`] port or a [`QuerySource`] to
//! an arbitrary number of *replier ports* with a matching request type, and a
//! *reply* sent in response to such request. The response received by a
//! [`Requestor`] port is an iterator that yields as many items (replies) as
//! there are connected replier ports, while a [`UniRequestor`] received exactly
//! one reply.
//!
//! # Model ports
//!
//! ## Input and replier ports
//!
//! Input ports and replier ports are methods that implement the [`InputFn`] or
//! [`ReplierFn`] traits.
//!
//! In practice, an input port method for an event of type `T` may have any of
//! the following signatures, where the futures returned by the `async` variants
//! must implement `Send`:
//!
//! ```ignore
//! fn(&mut self) // argument elided, implies `T=()`
//! fn(&mut self, T)
//! fn(&mut self, T, &mut Context<Self>)
//! async fn(&mut self) // argument elided, implies `T=()`
//! async fn(&mut self, T)
//! async fn(&mut self, T, &mut Context<Self>)
//! where
//! Self: Model,
//! T: Clone + Send + 'static,
//! R: Send + 'static,
//! ```
//!
//! The context argument is useful for methods that need access to the
//! simulation time or that need to schedule an action at a future date.
//!
//! A replier port for a request of type `T` with a reply of type `R` may in
//! turn have any of the following signatures, where the futures must implement
//! `Send`:
//!
//! ```ignore
//! async fn(&mut self) -> R // argument elided, implies `T=()`
//! async fn(&mut self, T) -> R
//! async fn(&mut self, T, &mut Context<Self>) -> R
//! where
//! Self: Model,
//! T: Clone + Send + 'static,
//! R: Send + 'static,
//! ```
//!
//! Note that, due to type resolution ambiguities, non-async methods are not
//! allowed for replier ports.
//!
//! Input and replier ports will normally be exposed as public methods by a
//! [`Model`](crate::model::Model) so they can be connected to output and
//! requestor ports when assembling the simulation bench. However, input ports
//! may (and should) be defined as private methods if they are only used
//! internally by the model, for instance to schedule future actions on itself.
//!
//! Changing the signature of a public input or replier port is not considered
//! to alter the public interface of a model provided that the event, request
//! and reply types remain the same. In particular, adding a context argument or
//! changing a regular method to an `async` method will not cause idiomatic user
//! code to miscompile.
//!
//! #### Basic example
//!
//! ```
//! use nexosim::model::{Context, Model};
//!
//! pub struct MyModel {
//! // ...
//! }
//! impl MyModel {
//! pub fn my_input(&mut self, input: String, cx: &mut Context<Self>) {
//! // ...
//! }
//! pub async fn my_replier(&mut self, request: u32) -> bool { // context argument elided
//! // ...
//! # unimplemented!()
//! }
//! }
//! impl Model for MyModel {}
//! ```
//!
//! ## Output and requestor ports
//!
//! Output and requestor ports can be added to a model using composition, adding
//! [`Output`], [`Requestor`] or [`UniRequestor`] objects as members. They are
//! parametrized by the event type, or by the request and reply types.
//!
//! Output ports broadcast events to all connected input ports, while requestor
//! ports broadcast queries to, and retrieve replies from, all connected replier
//! ports.
//! //!
//! On the surface, output and requestor ports only differ in that sending a //! On the surface, output and requestor ports only differ in that sending a
//! query from a requestor port also returns an iterator over the replies from //! query from a requestor port also returns an iterator over the replies from
//! all connected ports. Sending a query is more costly, however, because of the //! all connected ports (or a single reply in the case of [`UniRequestor`]).
//! need to wait until all connected models have processed the query. In //! Sending a query is more costly, however, because of the need to wait until
//! contrast, since events are buffered in the mailbox of the target model, //! the connected model(s) have processed the query. In contrast, since events
//! sending an event is a fire-and-forget operation. For this reason, output //! are buffered in the mailbox of the target model, sending an event is a
//! ports should generally be preferred over requestor ports when possible. //! fire-and-forget operation. For this reason, output ports should generally be
//! preferred over requestor ports when possible.
//! //!
//! `Output` and `Requestor` ports are clonable. Their clones are shallow //! Models (or model prototypes, as appropriate) are expected to expose their
//! copies, meaning that any modification of the ports connected to one clone is //! output and requestor ports as public members so they can be connected to
//! immediately reflected in other clones. //! input and replier ports when assembling the simulation bench. Internal ports
//! used by hierarchical models to communicate with submodels are an exception
//! to this rule and are typically private.
//! //!
//! #### Example //! #### Basic example
//! //!
//! This example demonstrates two submodels inside a parent model. The output of //! ```
//! the submodel and of the main model are clones and remain therefore always //! use nexosim::model::Model;
//! connected to the same inputs. //! use nexosim::ports::{Output, Requestor};
//! //!
//! For a more comprehensive example demonstrating hierarchical model //! pub struct MyModel {
//! assemblies, see the [`assembly example`][assembly]. //! pub my_output: Output<String>,
//! pub my_requestor: Requestor<u32, bool>,
//! }
//! impl MyModel {
//! // ...
//! }
//! impl Model for MyModel {}
//! ```
//! //!
//! [assembly]: //! #### Example with cloned ports
//! https://github.com/asynchronics/nexosim/tree/main/nexosim/examples/assembly.rs //!
//! [`Output`] and [`Requestor`] ports are clonable. The clones are shallow
//! copies, meaning that any modification of the ports connected to one instance
//! is immediately reflected by its clones.
//!
//! Clones of output and requestor ports should be used with care: even though
//! they uphold the usual [ordering
//! guaranties](crate#message-ordering-guarantees), their use can lead to
//! somewhat surprising message orderings.
//!
//! For instance, in the below [hierarchical
//! model](crate::model#hierarchical-models), while message `M0` is guaranteed
//! to reach the cloned output first, the relative arrival order of messages
//! `M1` and `M2` forwarded by the submodels to the cloned output is not
//! guaranteed.
//! //!
//! ``` //! ```
//! use nexosim::model::{BuildContext, Model, ProtoModel}; //! use nexosim::model::{BuildContext, Model, ProtoModel};
//! use nexosim::ports::Output; //! use nexosim::ports::Output;
//! use nexosim::simulation::Mailbox; //! use nexosim::simulation::Mailbox;
//! //!
//! pub struct ChildModel { //! pub struct ParentModel {
//! pub output: Output<u64>, //! output: Output<String>,
//! to_child1: Output<String>,
//! to_child2: Output<String>,
//! } //! }
//! impl ParentModel {
//! pub async fn trigger(&mut self) {
//! // M0 is guaranteed to reach the cloned output first.
//! self.output.send("M0".to_string()).await;
//! //!
//! impl ChildModel { //! // M1 and M2 are forwarded by `Child1` and `Child2` to the cloned output
//! pub fn new(output: Output<u64>) -> Self { //! // but may reach it in any relative order.
//! Self { //! self.to_child1.send("M1".to_string()).await;
//! output, //! self.to_child2.send("M2".to_string()).await;
//! }
//! } //! }
//! } //! }
//!
//! impl Model for ChildModel {}
//!
//! pub struct ParentModel {
//! output: Output<u64>,
//! }
//!
//! impl Model for ParentModel {} //! impl Model for ParentModel {}
//! //!
//! pub struct ProtoParentModel { //! pub struct ProtoParentModel {
//! pub output: Output<u64>, //! pub output: Output<String>,
//! } //! }
//!
//! impl ProtoParentModel { //! impl ProtoParentModel {
//! pub fn new() -> Self { //! pub fn new() -> Self {
//! Self { //! Self {
@ -65,20 +184,86 @@
//! } //! }
//! } //! }
//! } //! }
//!
//! impl ProtoModel for ProtoParentModel { //! impl ProtoModel for ProtoParentModel {
//! type Model = ParentModel; //! type Model = ParentModel;
//!
//! fn build(self, cx: &mut BuildContext<Self>) -> ParentModel { //! fn build(self, cx: &mut BuildContext<Self>) -> ParentModel {
//! let mut child = ChildModel::new(self.output.clone()); //! let child1 = ChildModel { output: self.output.clone() };
//! let child2 = ChildModel { output: self.output.clone() };
//! let mut parent = ParentModel {
//! output: self.output,
//! to_child1: Output::default(),
//! to_child2: Output::default(),
//! };
//! //!
//! cx.add_submodel(child, Mailbox::new(), "child"); //! let child1_mailbox = Mailbox::new();
//! let child2_mailbox = Mailbox::new();
//! parent
//! .to_child1
//! .connect(ChildModel::input, child1_mailbox.address());
//! parent
//! .to_child2
//! .connect(ChildModel::input, child2_mailbox.address());
//! //!
//! ParentModel { output: self.output } //! cx.add_submodel(child1, child1_mailbox, "child1");
//! cx.add_submodel(child2, child2_mailbox, "child2");
//!
//! parent
//! } //! }
//! } //! }
//!
//! pub struct ChildModel {
//! pub output: Output<String>,
//! }
//! impl ChildModel {
//! async fn input(&mut self, msg: String) {
//! self.output.send(msg).await;
//! }
//! }
//! impl Model for ChildModel {}
//! ``` //! ```
//!
//! # Simulation endpoints
//!
//! Simulation endpoints can be seen as entry and exit ports for a simulation
//! bench.
//!
//! [`EventSource`] and [`QuerySource`] objects are similar to [`Output`] and
//! [`Requestor`] ports, respectively. They can be connected to models and can
//! be used to send events or queries to such models via
//! [`Action`](crate::simulation::Action)s.
//!
//! Objects implementing the [`EventSink`] trait, such as [`EventSlot`] and
//! [`EventBuffer`], are in turn similar to input ports. They can be connected
//! to model outputs and collect events sent by such models.
//!
//!
//! # Connections
//!
//! Model ports can be connected to other model ports and to simulation
//! endpoints using the `*connect` family of methods exposed by [`Output`],
//! [`Requestor`], [`UniRequestor`], [`EventSource`] and [`QuerySource`].
//!
//! Regular connections between two ports are made with the appropriate
//! `connect` method (for instance [`Output::connect`]). Such connections
//! broadcast events or requests from a sender port to one or several receiver
//! ports, cloning the event or request if necessary.
//!
//! Sometimes, however, it may be necessary to also map the event or request to
//! another type so it can be understood by the receiving port. While this
//! translation could be done by a model placed between the two ports, the
//! `map_connect` methods (for instance [`Output::map_connect`]) provide a
//! lightweight and computationally efficient alternative. These methods take a
//! mapping closure as argument which maps outgoing messages, and in the case of
//! requestors, a mapping closure which maps replies.
//!
//! Finally, it is sometime necessary to only forward messages or requests that
//! satisfy specific criteria. For instance, a model of a data bus may be
//! connected to many models (the "peripherals"), but its messages are usually
//! only addressed to selected models. The `filter_map_connect` methods (for
//! instance [`Output::filter_map_connect`]) enable this use-case by accepting a
//! closure that inspects the messages and determines whether they should be
//! forwarded, possibly after being mapped to another type.
//!
mod input; mod input;
mod output; mod output;
mod sink; mod sink;

View File

@ -9,25 +9,20 @@ use super::markers;
/// A function, method or closures that can be used as an *input port*. /// A function, method or closures that can be used as an *input port*.
/// ///
/// This trait is in particular implemented for any function or method with the /// This trait is in particular implemented for any function or method with the
/// following signature, where it is implicitly assumed that the function /// following signature, where the futures returned by the `async` variants must
/// implements `Send + 'static`: /// implement `Send`:
/// ///
/// ```ignore /// ```ignore
/// FnOnce(&mut M, T) /// fn(&mut M) // argument elided, implies `T=()`
/// FnOnce(&mut M, T, &mut Context<M>) /// fn(&mut M, T)
/// fn(&mut M, T, &mut Context<M>)
/// async fn(&mut M) // argument elided, implies `T=()`
/// async fn(&mut M, T) /// async fn(&mut M, T)
/// async fn(&mut M, T, &mut Context<M>) /// async fn(&mut M, T, &mut Context<M>)
/// where /// where
/// M: Model /// M: Model,
/// ``` /// T: Clone + Send + 'static,
/// /// R: Send + 'static,
/// It is also implemented for the following signatures when `T=()`:
///
/// ```ignore
/// FnOnce(&mut M)
/// async fn(&mut M)
/// where
/// M: Model
/// ``` /// ```
pub trait InputFn<'a, M: Model, T, S>: Send + 'static { pub trait InputFn<'a, M: Model, T, S>: Send + 'static {
/// The `Future` returned by the asynchronous method. /// The `Future` returned by the asynchronous method.
@ -121,22 +116,16 @@ where
/// A function, method or closure that can be used as a *replier port*. /// A function, method or closure that can be used as a *replier port*.
/// ///
/// This trait is in particular implemented for any function or method with the /// This trait is in particular implemented for any function or method with the
/// following signature, where it is implicitly assumed that the function /// following signature, where the returned futures must implement `Send`:
/// implements `Send + 'static`:
/// ///
/// ```ignore /// ```ignore
/// async fn(&mut M) -> R // argument elided, implies `T=()`
/// async fn(&mut M, T) -> R /// async fn(&mut M, T) -> R
/// async fn(&mut M, T, &mut Context<M>) -> R /// async fn(&mut M, T, &mut Context<M>) -> R
/// where /// where
/// M: Model /// M: Model,
/// ``` /// T: Clone + Send + 'static,
/// /// R: Send + 'static,
/// It is also implemented for the following signatures when `T=()`:
///
/// ```ignore
/// async fn(&mut M) -> R
/// where
/// M: Model
/// ``` /// ```
pub trait ReplierFn<'a, M: Model, T, R, S>: Send + 'static { pub trait ReplierFn<'a, M: Model, T, R, S>: Send + 'static {
/// The `Future` returned by the asynchronous method. /// The `Future` returned by the asynchronous method.

View File

@ -22,7 +22,7 @@ pub trait EventSinkWriter<T>: Clone + Send + Sync + 'static {
/// An iterator over collected events with the ability to pause and resume event /// An iterator over collected events with the ability to pause and resume event
/// collection. /// collection.
/// ///
/// An `EventSinkStream` will typically be implemented on an `EventSink` for /// An `EventSinkStream` will typically be implemented on an [`EventSink`] for
/// which it will constitute a draining iterator. /// which it will constitute a draining iterator.
pub trait EventSinkStream: Iterator { pub trait EventSinkStream: Iterator {
/// Starts or resumes the collection of new events. /// Starts or resumes the collection of new events.

View File

@ -12,7 +12,8 @@ struct Inner<T> {
buffer: Mutex<VecDeque<T>>, buffer: Mutex<VecDeque<T>>,
} }
/// An [`EventSink`] and [`EventSinkStream`] with a bounded size. /// An iterator implementing [`EventSink`] and [`EventSinkStream`], backed by a
/// fixed-capacity buffer.
/// ///
/// If the maximum capacity is exceeded, older events are overwritten. Events /// If the maximum capacity is exceeded, older events are overwritten. Events
/// are returned in first-in-first-out order. Note that even if the iterator /// are returned in first-in-first-out order. Note that even if the iterator

View File

@ -10,7 +10,8 @@ struct Inner<T> {
slot: Mutex<Option<T>>, slot: Mutex<Option<T>>,
} }
/// An [`EventSink`] and [`EventSinkStream`] that only keeps the last event. /// An iterator implementing [`EventSink`] and [`EventSinkStream`] that only
/// keeps the last event.
/// ///
/// Once the value is read, the iterator will return `None` until a new value is /// Once the value is read, the iterator will return `None` until a new value is
/// received. If the slot contains a value when a new value is received, the /// received. If the slot contains a value when a new value is received, the

View File

@ -188,13 +188,13 @@ impl<T: Clone + Send + 'static> fmt::Debug for EventSource<T> {
} }
} }
/// A request source port. /// A query source port.
/// ///
/// The `QuerySource` port is similar to an /// The `QuerySource` port is similar to an
/// [`Requestor`](crate::ports::Requestor) port in that it can send events to /// [`Requestor`](crate::ports::Requestor) port in that it can send requests to
/// connected input ports. It is not meant, however, to be instantiated as a /// connected replier ports and receive replies. It is not meant, however, to be
/// member of a model, but rather as a simulation monitoring endpoint /// instantiated as a member of a model, but rather as a simulation monitoring
/// instantiated during bench assembly. /// endpoint instantiated during bench assembly.
pub struct QuerySource<T: Clone + Send + 'static, R: Send + 'static> { pub struct QuerySource<T: Clone + Send + 'static, R: Send + 'static> {
broadcaster: QueryBroadcaster<T, R>, broadcaster: QueryBroadcaster<T, R>,
} }

View File

@ -1,7 +1,8 @@
//! Registry for sinks and sources. //! Registry for sinks and sources.
//! //!
//! This module provides the `EndpointRegistry` object which associates each //! This module provides the `EndpointRegistry` object which associates each
//! event sink, event source and query source to a unique name. //! event sink, event source and query source in a simulation bench to a unique
//! name.
mod event_sink_registry; mod event_sink_registry;
mod event_source_registry; mod event_source_registry;
@ -15,8 +16,7 @@ pub(crate) use event_sink_registry::EventSinkRegistry;
pub(crate) use event_source_registry::EventSourceRegistry; pub(crate) use event_source_registry::EventSourceRegistry;
pub(crate) use query_source_registry::QuerySourceRegistry; pub(crate) use query_source_registry::QuerySourceRegistry;
/// A registry that holds all sources and sinks meant to be accessed through /// A registry that holds the sources and sinks of a simulation bench.
/// bindings or remote procedure calls.
#[derive(Default, Debug)] #[derive(Default, Debug)]
pub struct EndpointRegistry { pub struct EndpointRegistry {
pub(crate) event_sink_registry: EventSinkRegistry, pub(crate) event_sink_registry: EventSinkRegistry,

View File

@ -50,7 +50,7 @@ impl fmt::Debug for EventSinkRegistry {
/// A type-erased `EventSinkStream`. /// A type-erased `EventSinkStream`.
pub(crate) trait EventSinkStreamAny: Send + 'static { pub(crate) trait EventSinkStreamAny: Send + 'static {
/// Human-readable name of the event type, as returned by /// Human-readable name of the event type, as returned by
/// `any::type_name()`. /// `any::type_name`.
fn event_type_name(&self) -> &'static str; fn event_type_name(&self) -> &'static str;
/// Starts or resumes the collection of new events. /// Starts or resumes the collection of new events.

View File

@ -92,7 +92,7 @@ pub(crate) trait EventSourceAny: Send + Sync + 'static {
) -> Result<(Action, ActionKey), DeserializationError>; ) -> Result<(Action, ActionKey), DeserializationError>;
/// Human-readable name of the event type, as returned by /// Human-readable name of the event type, as returned by
/// `any::type_name()`. /// `any::type_name`.
fn event_type_name(&self) -> &'static str; fn event_type_name(&self) -> &'static str;
} }

View File

@ -68,11 +68,11 @@ pub(crate) trait QuerySourceAny: Send + Sync + 'static {
) -> Result<(Action, Box<dyn ReplyReceiverAny>), DeserializationError>; ) -> Result<(Action, Box<dyn ReplyReceiverAny>), DeserializationError>;
/// Human-readable name of the request type, as returned by /// Human-readable name of the request type, as returned by
/// `any::type_name()`. /// `any::type_name`.
fn request_type_name(&self) -> &'static str; fn request_type_name(&self) -> &'static str;
/// Human-readable name of the reply type, as returned by /// Human-readable name of the reply type, as returned by
/// `any::type_name()`. /// `any::type_name`.
fn reply_type_name(&self) -> &'static str; fn reply_type_name(&self) -> &'static str;
} }

View File

@ -31,7 +31,7 @@ impl ControllerService {
/// that event as well as all other events scheduled for the same time. /// that event as well as all other events scheduled for the same time.
/// ///
/// Processing is gated by a (possibly blocking) call to /// Processing is gated by a (possibly blocking) call to
/// [`Clock::synchronize()`](crate::time::Clock::synchronize) on the /// [`Clock::synchronize`](crate::time::Clock::synchronize) on the
/// configured simulation clock. This method blocks until all newly /// configured simulation clock. This method blocks until all newly
/// processed events have completed. /// processed events have completed.
pub(crate) fn step(&mut self, _request: StepRequest) -> StepReply { pub(crate) fn step(&mut self, _request: StepRequest) -> StepReply {
@ -59,7 +59,7 @@ impl ControllerService {
/// Iteratively advances the simulation time until the specified deadline, /// Iteratively advances the simulation time until the specified deadline,
/// as if by calling /// as if by calling
/// [`Simulation::step()`](crate::simulation::Simulation::step) repeatedly. /// [`Simulation::step`](crate::simulation::Simulation::step) repeatedly.
/// ///
/// This method blocks until all events scheduled up to the specified target /// This method blocks until all events scheduled up to the specified target
/// time have completed. The simulation time upon completion is equal to the /// time have completed. The simulation time upon completion is equal to the

View File

@ -13,10 +13,10 @@
//! 2. connection of the models' output/requestor ports to input/replier ports //! 2. connection of the models' output/requestor ports to input/replier ports
//! using the [`Address`]es of the target models, //! using the [`Address`]es of the target models,
//! 3. instantiation of a [`SimInit`] simulation builder and migration of all //! 3. instantiation of a [`SimInit`] simulation builder and migration of all
//! models and mailboxes to the builder with [`SimInit::add_model()`], //! models and mailboxes to the builder with [`SimInit::add_model`],
//! 4. initialization of a [`Simulation`] instance with [`SimInit::init()`], //! 4. initialization of a [`Simulation`] instance with [`SimInit::init`],
//! possibly preceded by the setup of a custom clock with //! possibly preceded by the setup of a custom clock with
//! [`SimInit::set_clock()`], //! [`SimInit::set_clock`],
//! 5. discrete-time simulation, which typically involves scheduling events and //! 5. discrete-time simulation, which typically involves scheduling events and
//! incrementing simulation time while observing the models outputs. //! incrementing simulation time while observing the models outputs.
//! //!
@ -42,8 +42,8 @@
//! //!
//! The default capacity should prove a reasonable trade-off in most cases, but //! The default capacity should prove a reasonable trade-off in most cases, but
//! for situations where it is not appropriate, it is possible to instantiate //! for situations where it is not appropriate, it is possible to instantiate
//! mailboxes with a custom capacity by using [`Mailbox::with_capacity()`] //! mailboxes with a custom capacity by using [`Mailbox::with_capacity`] instead
//! instead of [`Mailbox::new()`]. //! of [`Mailbox::new`].
//! //!
//! ## Avoiding deadlocks //! ## Avoiding deadlocks
//! //!
@ -68,14 +68,13 @@
//! //!
//! The second scenario is rare in well-behaving models and if it occurs, it is //! The second scenario is rare in well-behaving models and if it occurs, it is
//! most typically at the very beginning of a simulation when models //! most typically at the very beginning of a simulation when models
//! simultaneously and mutually send events during the call to //! simultaneously and mutually send events during the call to [`Model::init`].
//! [`Model::init()`](crate::model::Model::init). If such a large amount of //! If such a large amount of events is deemed normal behavior, the issue can be
//! events is deemed normal behavior, the issue can be remedied by increasing //! remedied by increasing the capacity of the saturated mailboxes.
//! the capacity of the saturated mailboxes.
//! //!
//! Any deadlocks will be reported as an [`ExecutionError::Deadlock`] error, //! Deadlocks are reported as [`ExecutionError::Deadlock`] errors, which
//! which identifies all involved models and the amount of unprocessed messages //! identify all involved models and the count of unprocessed messages (events
//! (events or requests) in their mailboxes. //! or requests) in their mailboxes.
mod mailbox; mod mailbox;
mod scheduler; mod scheduler;
mod sim_init; mod sim_init;
@ -118,7 +117,7 @@ thread_local! { pub(crate) static CURRENT_MODEL_ID: Cell<ModelId> = const { Cell
/// Simulation environment. /// Simulation environment.
/// ///
/// A `Simulation` is created by calling /// A `Simulation` is created by calling
/// [`SimInit::init()`](crate::simulation::SimInit::init) on a simulation /// [`SimInit::init`](crate::simulation::SimInit::init) on a simulation
/// initializer. It contains an asynchronous executor that runs all simulation /// initializer. It contains an asynchronous executor that runs all simulation
/// models added beforehand to [`SimInit`]. /// models added beforehand to [`SimInit`].
/// ///
@ -126,31 +125,31 @@ thread_local! { pub(crate) static CURRENT_MODEL_ID: Cell<ModelId> = const { Cell
/// simulation time. The scheduling queue can be accessed from the simulation /// simulation time. The scheduling queue can be accessed from the simulation
/// itself, but also from models via the optional [`&mut /// itself, but also from models via the optional [`&mut
/// Context`](crate::model::Context) argument of input and replier port methods. /// Context`](crate::model::Context) argument of input and replier port methods.
/// Likewise, simulation time can be accessed with the [`Simulation::time()`] /// Likewise, simulation time can be accessed with the [`Simulation::time`]
/// method, or from models with the /// method, or from models with the
/// [`Context::time()`](crate::simulation::Context::time) method. /// [`Context::time`](crate::simulation::Context::time) method.
/// ///
/// Events and queries can be scheduled immediately, *i.e.* for the current /// Events and queries can be scheduled immediately, *i.e.* for the current
/// simulation time, using [`process_event()`](Simulation::process_event) and /// simulation time, using [`process_event`](Simulation::process_event) and
/// [`send_query()`](Simulation::process_query). Calling these methods will /// [`send_query`](Simulation::process_query). Calling these methods will block
/// block until all computations triggered by such event or query have /// until all computations triggered by such event or query have completed. In
/// completed. In the case of queries, the response is returned. /// the case of queries, the response is returned.
/// ///
/// Events can also be scheduled at a future simulation time using one of the /// Events can also be scheduled at a future simulation time using one of the
/// [`schedule_*()`](Scheduler::schedule_event) method. These methods queue an /// [`schedule_*`](Scheduler::schedule_event) method. These methods queue an
/// event without blocking. /// event without blocking.
/// ///
/// Finally, the [`Simulation`] instance manages simulation time. A call to /// Finally, the [`Simulation`] instance manages simulation time. A call to
/// [`step()`](Simulation::step) will: /// [`step`](Simulation::step) will:
/// ///
/// 1. increment simulation time until that of the next scheduled event in /// 1. increment simulation time until that of the next scheduled event in
/// chronological order, then /// chronological order, then
/// 2. call [`Clock::synchronize()`](crate::time::Clock::synchronize) which, unless the /// 2. call [`Clock::synchronize`] which, unless the simulation is configured to
/// simulation is configured to run as fast as possible, blocks until the /// run as fast as possible, blocks until the desired wall clock time, and
/// desired wall clock time, and finally /// finally
/// 3. run all computations scheduled for the new simulation time. /// 3. run all computations scheduled for the new simulation time.
/// ///
/// The [`step_until()`](Simulation::step_until) method operates similarly but /// The [`step_until`](Simulation::step_until) method operates similarly but
/// iterates until the target simulation time has been reached. /// iterates until the target simulation time has been reached.
pub struct Simulation { pub struct Simulation {
executor: Executor, executor: Executor,
@ -216,15 +215,14 @@ impl Simulation {
/// that event as well as all other events scheduled for the same time. /// that event as well as all other events scheduled for the same time.
/// ///
/// Processing is gated by a (possibly blocking) call to /// Processing is gated by a (possibly blocking) call to
/// [`Clock::synchronize()`](crate::time::Clock::synchronize) on the configured /// [`Clock::synchronize`] on the configured simulation clock. This method
/// simulation clock. This method blocks until all newly processed events /// blocks until all newly processed events have completed.
/// have completed.
pub fn step(&mut self) -> Result<(), ExecutionError> { pub fn step(&mut self) -> Result<(), ExecutionError> {
self.step_to_next(None).map(|_| ()) self.step_to_next(None).map(|_| ())
} }
/// Iteratively advances the simulation time until the specified deadline, /// Iteratively advances the simulation time until the specified deadline,
/// as if by calling [`Simulation::step()`] repeatedly. /// as if by calling [`Simulation::step`] repeatedly.
/// ///
/// This method blocks until all events scheduled up to the specified target /// This method blocks until all events scheduled up to the specified target
/// time have completed. The simulation time upon completion is equal to the /// time have completed. The simulation time upon completion is equal to the
@ -240,7 +238,7 @@ impl Simulation {
} }
/// Iteratively advances the simulation time, as if by calling /// Iteratively advances the simulation time, as if by calling
/// [`Simulation::step()`] repeatedly. /// [`Simulation::step`] repeatedly.
/// ///
/// This method blocks until all events scheduled have completed. /// This method blocks until all events scheduled have completed.
pub fn step_unbounded(&mut self) -> Result<(), ExecutionError> { pub fn step_unbounded(&mut self) -> Result<(), ExecutionError> {

View File

@ -8,7 +8,7 @@ use crate::model::Model;
/// A mailbox is an entity associated to a model instance that collects all /// A mailbox is an entity associated to a model instance that collects all
/// messages sent to that model. The size of its internal buffer can be /// messages sent to that model. The size of its internal buffer can be
/// optionally specified at construction time using /// optionally specified at construction time using
/// [`with_capacity()`](Mailbox::with_capacity). /// [`with_capacity`](Mailbox::with_capacity).
pub struct Mailbox<M: Model>(pub(crate) Receiver<M>); pub struct Mailbox<M: Model>(pub(crate) Receiver<M>);
impl<M: Model> Mailbox<M> { impl<M: Model> Mailbox<M> {
@ -58,7 +58,7 @@ impl<M: Model> fmt::Debug for Mailbox<M> {
/// For the sake of convenience, methods that require an address by value will /// For the sake of convenience, methods that require an address by value will
/// typically also accept an `&Address` or an `&Mailbox` since these references /// typically also accept an `&Address` or an `&Mailbox` since these references
/// implement the `Into<Address>` trait, automatically invoking /// implement the `Into<Address>` trait, automatically invoking
/// `Address::clone()` or `Mailbox::address()` as appropriate. /// `Address::clone` or `Mailbox::address` as appropriate.
pub struct Address<M: Model>(pub(crate) Sender<M>); pub struct Address<M: Model>(pub(crate) Sender<M>);
impl<M: Model> Clone for Address<M> { impl<M: Model> Clone for Address<M> {
@ -80,8 +80,7 @@ impl<M: Model> From<&Address<M>> for Address<M> {
impl<M: Model> From<&Mailbox<M>> for Address<M> { impl<M: Model> From<&Mailbox<M>> for Address<M> {
/// Converts a [Mailbox] reference into an [`Address`]. /// Converts a [Mailbox] reference into an [`Address`].
/// ///
/// This calls [`Mailbox::address()`] on the mailbox and returns the /// This calls [`Mailbox::address`] on the mailbox and returns the address.
/// address.
#[inline] #[inline]
fn from(s: &Mailbox<M>) -> Address<M> { fn from(s: &Mailbox<M>) -> Address<M> {
s.address() s.address()

View File

@ -25,7 +25,9 @@ use crate::{time::TearableAtomicTime, util::sync_cell::SyncCell};
const GLOBAL_SCHEDULER_ORIGIN_ID: usize = 0; const GLOBAL_SCHEDULER_ORIGIN_ID: usize = 0;
/// A global scheduler. /// A global simulation scheduler.
///
/// A `Scheduler` can be `Clone`d and sent to other threads.
#[derive(Clone)] #[derive(Clone)]
pub struct Scheduler(GlobalScheduler); pub struct Scheduler(GlobalScheduler);
@ -40,9 +42,6 @@ impl Scheduler {
/// Returns the current simulation time. /// Returns the current simulation time.
/// ///
/// Beware that, if the scheduler runs in a separate thread as the
/// simulation, the time may change concurrently.
///
/// # Examples /// # Examples
/// ///
/// ``` /// ```
@ -200,9 +199,9 @@ impl fmt::Debug for Scheduler {
/// Managed handle to a scheduled action. /// Managed handle to a scheduled action.
/// ///
/// An `AutoActionKey` is a managed handle to a scheduled action that cancels /// An `AutoActionKey` is a managed handle to a scheduled action that cancels
/// action on drop. /// its associated action on drop.
#[derive(Debug)] #[derive(Debug)]
#[must_use = "managed action key shall be used"] #[must_use = "dropping this key immediately cancels the associated action"]
pub struct AutoActionKey { pub struct AutoActionKey {
is_cancelled: Arc<AtomicBool>, is_cancelled: Arc<AtomicBool>,
} }
@ -295,6 +294,12 @@ impl Error for SchedulingError {}
/// A possibly periodic, possibly cancellable action that can be scheduled or /// A possibly periodic, possibly cancellable action that can be scheduled or
/// processed immediately. /// processed immediately.
///
/// `Actions` can be created from an [`EventSource`](crate::ports::EventSource)
/// or [`QuerySource`](crate::ports::QuerySource). They can be used to schedule
/// events and requests with [`Scheduler::schedule`], or to process events and
/// requests immediately with
/// [`Simulation::process`](crate::simulation::Simulation::process).
pub struct Action { pub struct Action {
inner: Box<dyn ActionInner>, inner: Box<dyn ActionInner>,
} }

View File

@ -151,7 +151,7 @@ impl SimInit {
} }
/// Builds a simulation initialized at the specified simulation time, /// Builds a simulation initialized at the specified simulation time,
/// executing the [`Model::init()`](crate::model::Model::init) method on all /// executing the [`Model::init`](crate::model::Model::init) method on all
/// model initializers. /// model initializers.
/// ///
/// The simulation object and its associated scheduler are returned upon /// The simulation object and its associated scheduler are returned upon

View File

@ -1,4 +1,4 @@
//! Simulation time and scheduling. //! Simulation time and clocks.
//! //!
//! This module provides most notably: //! This module provides most notably:
//! //!

View File

@ -10,7 +10,7 @@ use crate::time::MonotonicTime;
/// as-fast-as-possible and real-time clocks. /// as-fast-as-possible and real-time clocks.
/// ///
/// A clock can be associated to a simulation prior to initialization by calling /// A clock can be associated to a simulation prior to initialization by calling
/// [`SimInit::set_clock()`](crate::simulation::SimInit::set_clock). /// [`SimInit::set_clock`](crate::simulation::SimInit::set_clock).
pub trait Clock: Send { pub trait Clock: Send {
/// Blocks until the deadline. /// Blocks until the deadline.
fn synchronize(&mut self, deadline: MonotonicTime) -> SyncStatus; fn synchronize(&mut self, deadline: MonotonicTime) -> SyncStatus;
@ -107,7 +107,7 @@ impl SystemClock {
/// The provided reference time may lie in the past or in the future. /// The provided reference time may lie in the past or in the future.
/// ///
/// Note that, even though the wall clock reference is specified with the /// Note that, even though the wall clock reference is specified with the
/// (non-monotonic) system clock, the [`synchronize()`](Clock::synchronize) /// (non-monotonic) system clock, the [`synchronize`](Clock::synchronize)
/// method will still use the system's _monotonic_ clock. This constructor /// method will still use the system's _monotonic_ clock. This constructor
/// makes a best-effort attempt at synchronizing the monotonic clock with /// makes a best-effort attempt at synchronizing the monotonic clock with
/// the non-monotonic system clock _at construction time_, but this /// the non-monotonic system clock _at construction time_, but this
@ -164,8 +164,8 @@ impl Clock for SystemClock {
/// monotonic clock. /// monotonic clock.
/// ///
/// This clock is similar to [`SystemClock`] except that the first call to /// This clock is similar to [`SystemClock`] except that the first call to
/// [`synchronize()`](Clock::synchronize) never blocks and implicitly defines /// [`synchronize`](Clock::synchronize) never blocks and implicitly defines the
/// the reference time. In other words, the clock starts running on its first /// reference time. In other words, the clock starts running on its first
/// invocation. /// invocation.
#[derive(Copy, Clone, Debug, Default)] #[derive(Copy, Clone, Debug, Default)]
pub struct AutoSystemClock { pub struct AutoSystemClock {

View File

@ -69,7 +69,7 @@
//! 2024-09-10T14:39:24.670921Z INFO my_simulation: something happened outside the simulation //! 2024-09-10T14:39:24.670921Z INFO my_simulation: something happened outside the simulation
//! ``` //! ```
//! //!
//! Alternatively, `SimulationTime::with_system_timer_always()` can be used to //! Alternatively, `SimulationTime::with_system_timer_always` can be used to
//! always prepend the system time even for simulation events: //! always prepend the system time even for simulation events:
//! //!
//! ```text //! ```text