Skip to content

Commit

Permalink
Merge pull request #81 from asynchronics/feature/release_cleanups
Browse files Browse the repository at this point in the history
Improve documentation, fix README example
  • Loading branch information
sbarral authored Jan 21, 2025
2 parents 4531c21 + fa8b5cf commit 2a4b389
Show file tree
Hide file tree
Showing 25 changed files with 495 additions and 325 deletions.
35 changes: 20 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,20 +90,21 @@ asynchronix = "0.2.4"
// 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 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.
#[derive(Default)]
pub struct DelayedMultiplier {
pub output: Output<f64>,
}
impl DelayedMultiplier {
pub fn input(&mut self, value: f64, scheduler: &Scheduler<Self>) {
scheduler
.schedule_event(Duration::from_secs(1), Self::send, 2.0 * value)
pub fn input(&mut self, value: f64, ctx: &mut Context<Self>) {
ctx.schedule_event(Duration::from_secs(1), Self::send, 2.0 * value)
.unwrap();
}
async fn send(&mut self, value: f64) {
Expand All @@ -124,28 +125,32 @@ multiplier1
.connect(DelayedMultiplier::input, &multiplier2_mbox);

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

// Instantiate the simulator
let t0 = MonotonicTime::EPOCH; // arbitrary start time
let mut simu = SimInit::new()
.add_model(multiplier1, multiplier1_mbox)
.add_model(multiplier2, multiplier2_mbox)
.init(t0);
.add_model(multiplier1, multiplier1_mbox, "multiplier 1")
.add_model(multiplier2, multiplier2_mbox, "multiplier 2")
.init(t0)?
.0;

// 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.
simu.step();
simu.step()?;
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.
simu.step();
simu.step()?;
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
Expand Down
1 change: 0 additions & 1 deletion nexosim/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ keywords = [
]

[features]
# gRPC service.
server = [
"dep:bytes",
"dep:ciborium",
Expand Down
8 changes: 4 additions & 4 deletions nexosim/src/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ impl Executor {

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

/// Spawns a task which output will never be retrieved.
///
/// Note that spawned tasks are not executed until [`run()`](Executor::run)
/// is called.
/// Note that spawned tasks are not executed until [`run`](Executor::run) is
/// called.
pub(crate) fn spawn_and_forget<T>(&self, future: T)
where
T: Future + Send + 'static,
Expand Down
8 changes: 4 additions & 4 deletions nexosim/src/executor/mt_executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,8 @@ impl Executor {
/// Spawns a task and returns a promise that can be polled to retrieve the
/// task's output.
///
/// Note that spawned tasks are not executed until [`run()`](Executor::run)
/// is called.
/// Note that spawned tasks are not executed until [`run`](Executor::run) is
/// called.
pub(crate) fn spawn<T>(&self, future: T) -> Promise<T::Output>
where
T: Future + Send + 'static,
Expand Down Expand Up @@ -215,8 +215,8 @@ impl Executor {
/// This is mostly useful to avoid undue reference counting for futures that
/// return a `()` type.
///
/// Note that spawned tasks are not executed until [`run()`](Executor::run)
/// is called.
/// Note that spawned tasks are not executed until [`run`](Executor::run) is
/// called.
pub(crate) fn spawn_and_forget<T>(&self, future: T)
where
T: Future + Send + 'static,
Expand Down
4 changes: 2 additions & 2 deletions nexosim/src/executor/st_executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ impl Executor {
/// Spawns a task and returns a promise that can be polled to retrieve the
/// 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.
pub(crate) fn spawn<T>(&self, future: T) -> Promise<T::Output>
where
Expand Down Expand Up @@ -91,7 +91,7 @@ impl Executor {
/// This is mostly useful to avoid undue reference counting for futures that
/// 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.
pub(crate) fn spawn_and_forget<T>(&self, future: T)
where
Expand Down
135 changes: 81 additions & 54 deletions nexosim/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@
//! * _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,
//! [`Requestor`](ports::Requestor) or [`UniRequestor`](ports::UniRequestor)
//! types 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
Expand All @@ -54,19 +54,27 @@
//! 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.
//! this trait is to allow models to specify a
//! [`Model::init`](model::Model::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 {}`.
//! The [`Model::init`](model::Model::init) methods has a 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
//! 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
//! after multiplying it by 2. This model has only one input and one output
Expand Down Expand Up @@ -98,7 +106,7 @@
//! 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
//! access to the current simulation time. To do so, input and replier methods
Expand Down Expand Up @@ -141,7 +149,7 @@
//! [`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
//! 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.
//!
Expand Down Expand Up @@ -230,7 +238,7 @@
//!
//! // Pick an arbitrary simulation start time and build the simulation.
//! let t0 = MonotonicTime::EPOCH;
//! let mut simu = SimInit::new()
//! let (mut simu, scheduler) = SimInit::new()
//! .add_model(multiplier1, multiplier1_mbox, "multiplier1")
//! .add_model(multiplier2, multiplier2_mbox, "multiplier2")
//! .add_model(delay1, delay1_mbox, "delay1")
Expand All @@ -245,27 +253,27 @@
//! 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
//! [`Simulation::step`](simulation::Simulation::step), until a specific
//! 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
//! [`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).
//! [`Simulation::process_event`](simulation::Simulation::process_event) or
//! [`Simulation::send_query`](simulation::Simulation::process_query),
//! 3. by scheduling events with a [`Scheduler`](simulation::Scheduler).
//!
//! 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
//! [`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.
//! Simulation outputs can be monitored using [`EventSlot`](ports::EventSlot)s,
//! [`EventBuffer`](ports::EventBuffer)s, or any implementer of the
//! [`EventSink`](ports::EventSink) trait, connected to one or several model
//! output ports.
//!
//! This is an example of simulation that could be performed using the above
//! bench assembly:
Expand Down Expand Up @@ -373,20 +381,20 @@
//! 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.
//! Both guarantees also extend to same-time events scheduled from the global
//! [`Scheduler`](simulation::Scheduler), *i.e.* the relative ordering of events
//! scheduled for the same time is preserved and warranties 1 and 2 above
//! accordingly hold (assuming model `A` stands for the scheduler). Likewise,
//! the relative order of same-time events self-scheduled by a model using its
//! [`Context`](model::Context) is preserved.
//!
//! [actor_model]: https://en.wikipedia.org/wiki/Actor_model
//! [pony]: https://www.ponylang.io/
//!
//!
//! # Feature flags
//! # Cargo feature flags
//!
//! ## Tracing support
//! ## Tracing
//!
//! The `tracing` feature flag provides support for the
//! [`tracing`](https://docs.rs/tracing/latest/tracing/) crate and can be
Expand All @@ -409,32 +417,50 @@
//! 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 examples
//!
//! The [`examples`][gh_examples] directory in the main repository contains more
//! fleshed out examples that demonstrate various capabilities of the simulation
//! Several [`examples`][gh_examples] are available that contain more fleshed
//! out examples and demonstrate various capabilities of the simulation
//! framework.
//!
//! [gh_examples]:
//! https://github.com/asynchronics/nexosim/tree/main/nexosim/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`]).
//!
//! ## Other features and advanced topics
//!
//! 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.
//!
#![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")))]
Expand All @@ -458,4 +484,5 @@ pub mod server;
pub mod tracing;

#[cfg(feature = "dev-hooks")]
#[doc(hidden)]
pub mod dev_hooks;
Loading

0 comments on commit 2a4b389

Please sign in to comment.