diff --git a/asynchronix/Cargo.toml b/asynchronix/Cargo.toml index a5eee42..7fc0d90 100644 --- a/asynchronix/Cargo.toml +++ b/asynchronix/Cargo.toml @@ -60,10 +60,8 @@ tracing = { version= "0.1.40", default-features = false, features=["std"], optio tracing-subscriber = { version= "0.3.18", optional = true } [dev-dependencies] -atomic-wait = "1.1" futures-util = "0.3" futures-executor = "0.3" -mio = { version = "1.0", features = ["os-poll", "net"] } tracing-subscriber = { version= "0.3.18", features=["env-filter"] } [target.'cfg(asynchronix_loom)'.dev-dependencies] diff --git a/asynchronix/examples/assembly.rs b/asynchronix/examples/assembly.rs index f5b1114..c7888f8 100644 --- a/asynchronix/examples/assembly.rs +++ b/asynchronix/examples/assembly.rs @@ -3,27 +3,29 @@ //! //! This example demonstrates in particular: //! +//! * model prototypes, //! * submodels, -//! * outputs cloning, //! * self-scheduling methods, -//! * model setup, //! * model initialization, -//! * simulation monitoring with event streams. +//! * simulation monitoring with buffered event sinks. //! //! ```text -//! ┌──────────────────────────────────────────────┐ -//! │ Assembly │ -//! │ ┌──────────┐ ┌──────────┐ │ -//! PPS │ │ │ coil currents │ │ │position -//! Pulse rate ●───────►│──►│ Driver ├───────────────►│ Motor ├──►│─────────► -//! (±freq)│ │ │ (IA, IB) │ │ │(0:199) -//! │ └──────────┘ └──────────┘ │ -//! └──────────────────────────────────────────────┘ +//! ┌────────────────────────────────────────────┐ +//! │ Assembly │ +//! │ ┌──────────┐ │ +//! PPS │ │ │ coil currents ┌─────────┐ │ +//! Pulse rate ●──────────┼──►│ Driver ├───────────────►│ │ │ +//! (±freq) │ │ │ (IA, IB) │ │ │ position +//! │ └──────────┘ │ Motor ├──┼──────────► +//! torque │ │ │ │ (0:199) +//! Load ●──────────┼──────────────────────────────►│ │ │ +//! │ └─────────┘ │ +//! └────────────────────────────────────────────┘ //! ``` use std::time::Duration; -use asynchronix::model::{Model, SetupContext}; +use asynchronix::model::{BuildContext, Model, ProtoModel}; use asynchronix::ports::{EventBuffer, Output}; use asynchronix::simulation::{Mailbox, SimInit, SimulationError}; use asynchronix::time::MonotonicTime; @@ -32,36 +34,59 @@ mod stepper_motor; pub use stepper_motor::{Driver, Motor}; -pub struct MotorAssembly { +/// A prototype for `MotorAssembly`. +pub struct ProtoMotorAssembly { pub position: Output, init_pos: u16, - load: Output, - pps: Output, } -impl MotorAssembly { +impl ProtoMotorAssembly { + /// The prototype has a public constructor. pub fn new(init_pos: u16) -> Self { Self { position: Default::default(), init_pos, - load: Default::default(), - pps: Default::default(), } } - /// Sets the pulse rate (sign = direction) [Hz] -- input port. + // Input methods are in the model itself. +} + +/// The parent model which submodels are the driver and the motor. +pub struct MotorAssembly { + /// Private output for submodel connection. + pps: Output, + /// Private output for submodel connection. + load: Output, +} + +impl MotorAssembly { + /// The model now has a module-private constructor. + fn new() -> Self { + Self { + pps: Default::default(), + load: Default::default(), + } + } + + /// Pulse rate (sign = direction) [Hz] -- input port. pub async fn pulse_rate(&mut self, pps: f64) { - self.pps.send(pps).await; + self.pps.send(pps).await } /// Torque applied by the load [N·m] -- input port. pub async fn load(&mut self, torque: f64) { - self.load.send(torque).await; + self.load.send(torque).await } } -impl Model for MotorAssembly { - fn setup(&mut self, setup_context: &SetupContext) { +impl Model for MotorAssembly {} + +impl ProtoModel for ProtoMotorAssembly { + type Model = MotorAssembly; + + fn build(self, ctx: &BuildContext) -> MotorAssembly { + let mut assembly = MotorAssembly::new(); let mut motor = Motor::new(self.init_pos); let mut driver = Driver::new(1.0); @@ -70,17 +95,20 @@ impl Model for MotorAssembly { let driver_mbox = Mailbox::new(); // Connections. - self.pps.connect(Driver::pulse_rate, &driver_mbox); - self.load.connect(Motor::load, &motor_mbox); + assembly.pps.connect(Driver::pulse_rate, &driver_mbox); + assembly.load.connect(Motor::load, &motor_mbox); driver.current_out.connect(Motor::current_in, &motor_mbox); - // Note: it is important to clone `position` from the parent to the - // submodel so that all connections made by the user to the parent model - // are preserved. Connections added after cloning are reflected in all - // clones. - motor.position = self.position.clone(); - setup_context.add_model(driver, driver_mbox, "driver"); - setup_context.add_model(motor, motor_mbox, "motor"); + // Move the prototype's output to the submodel. The `self.position` + // output can be cloned if necessary if several submodels need access to + // it. + motor.position = self.position; + + // Add the submodels to the simulation. + ctx.add_submodel(driver, driver_mbox, "driver"); + ctx.add_submodel(motor, motor_mbox, "motor"); + + assembly } } @@ -91,7 +119,7 @@ fn main() -> Result<(), SimulationError> { // Models. let init_pos = 123; - let mut assembly = MotorAssembly::new(init_pos); + let mut assembly = ProtoMotorAssembly::new(init_pos); // Mailboxes. let assembly_mbox = Mailbox::new(); diff --git a/asynchronix/examples/external_input.rs b/asynchronix/examples/external_input.rs index ec33d42..33167aa 100644 --- a/asynchronix/examples/external_input.rs +++ b/asynchronix/examples/external_input.rs @@ -1,80 +1,97 @@ -//! Example: a model that reads data from the external world. +//! Example: a model that reads data external to the simulation. //! //! This example demonstrates in particular: //! -//! * external world inputs (useful in cosimulation), +//! * processing of external inputs (useful in co-simulation), //! * system clock, //! * periodic scheduling. //! //! ```text -//! ┌────────────────────────────────┐ -//! │ Simulation │ -//! ┌────────────┐ ┌────────────┐ │ ┌──────────┐ │ -//! │ │ UDP │ │ message │ message │ │ message │ ┌─────────────┐ -//! │ UDP Client ├─────────►│ UDP Server ├──────────►├─────────►│ Listener ├─────────►├──►│ EventBuffer │ -//! │ │ message │ │ │ │ │ │ └─────────────┘ -//! └────────────┘ └────────────┘ │ └──────────┘ │ -//! └────────────────────────────────┘ +//! ┏━━━━━━━━━━━━━━━━━━━━━━━━┓ +//! ┃ Simulation ┃ +//! ┌╌╌╌╌╌╌╌╌╌╌╌╌┐ ┌╌╌╌╌╌╌╌╌╌╌╌╌┐ ┃ ┌──────────┐ ┃ +//! ┆ ┆ message ┆ ┆ message ┃ │ │ message ┃ +//! ┆ UDP Client ├╌╌╌╌╌╌╌╌►┆ UDP Server ├╌╌╌╌╌╌╌╌╌╌╌╂╌╌►│ Listener ├─────────╂─► +//! ┆ ┆ [UDP] ┆ ┆ [channel] ┃ │ │ ┃ +//! └╌╌╌╌╌╌╌╌╌╌╌╌┘ └╌╌╌╌╌╌╌╌╌╌╌╌┘ ┃ └──────────┘ ┃ +//! ┗━━━━━━━━━━━━━━━━━━━━━━━━┛ //! ``` use std::io::ErrorKind; -use std::net::UdpSocket; -use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; +use std::net::{Ipv4Addr, UdpSocket}; use std::sync::mpsc::{channel, Receiver, Sender}; -use std::sync::Arc; +use std::sync::{Arc, Condvar, Mutex}; use std::thread::{self, sleep, JoinHandle}; use std::time::Duration; -use atomic_wait::{wait, wake_one}; - -use mio::net::UdpSocket as MioUdpSocket; -use mio::{Events, Interest, Poll, Token}; - -use asynchronix::model::{Context, InitializedModel, Model, SetupContext}; +use asynchronix::model::{BuildContext, Context, InitializedModel, Model, ProtoModel}; use asynchronix::ports::{EventBuffer, Output}; use asynchronix::simulation::{Mailbox, SimInit, SimulationError}; use asynchronix::time::{AutoSystemClock, MonotonicTime}; const DELTA: Duration = Duration::from_millis(2); const PERIOD: Duration = Duration::from_millis(20); -const N: u32 = 10; -const SENDER: &str = "127.0.0.1:8000"; -const RECEIVER: &str = "127.0.0.1:9000"; +const N: usize = 10; +const SHUTDOWN_SIGNAL: &str = ""; +const SENDER: (Ipv4Addr, u16) = (Ipv4Addr::new(127, 0, 0, 1), 8000); +const RECEIVER: (Ipv4Addr, u16) = (Ipv4Addr::new(127, 0, 0, 1), 9000); -/// Model that receives external input. -pub struct Listener { +/// Prototype for the `Listener` Model. +pub struct ProtoListener { /// Received message. pub message: Output, + /// Notifier to start the UDP client. + start: Notifier, +} + +impl ProtoListener { + fn new(start: Notifier) -> Self { + Self { + message: Output::default(), + start, + } + } +} + +impl ProtoModel for ProtoListener { + type Model = Listener; + + /// Start the UDP Server immediately upon model construction. + fn build(self, _: &BuildContext) -> Listener { + let (tx, rx) = channel(); + + let external_handle = thread::spawn(move || { + Listener::listen(tx, self.start); + }); + + Listener::new(self.message, rx, external_handle) + } +} + +/// Model that asynchronously receives messages external to the simulation. +pub struct Listener { + /// Received message. + message: Output, + /// Receiver of external messages. rx: Receiver, - /// External sender. - tx: Option>, - - /// Synchronization with client. - start: Arc, - - /// Synchronization with simulation. - stop: Arc, - /// Handle to UDP Server. - external_handle: Option>, + server_handle: Option>, } impl Listener { /// Creates a Listener. - pub fn new(start: Arc) -> Self { - start.store(0, Ordering::Relaxed); - - let (tx, rx) = channel(); + pub fn new( + message: Output, + rx: Receiver, + server_handle: JoinHandle<()>, + ) -> Self { Self { - message: Output::default(), + message, rx, - tx: Some(tx), - start, - stop: Arc::new(AtomicBool::new(false)), - external_handle: None, + server_handle: Some(server_handle), } } @@ -85,82 +102,39 @@ impl Listener { } } - /// UDP server. - /// - /// Code is based on the MIO UDP example. - fn listener(tx: Sender, start: Arc, stop: Arc) { - const UDP_SOCKET: Token = Token(0); - let mut poll = Poll::new().unwrap(); - let mut events = Events::with_capacity(10); - let mut socket = MioUdpSocket::bind(RECEIVER.parse().unwrap()).unwrap(); - poll.registry() - .register(&mut socket, UDP_SOCKET, Interest::READABLE) - .unwrap(); + /// Starts the UDP server. + fn listen(tx: Sender, start: Notifier) { + let socket = UdpSocket::bind(RECEIVER).unwrap(); let mut buf = [0; 1 << 16]; // Wake up the client. - start.store(1, Ordering::Relaxed); - wake_one(&*start); + start.notify(); - 'process: loop { - // Wait for UDP packet or end of simulation. - if let Err(err) = poll.poll(&mut events, Some(Duration::from_secs(1))) { - if err.kind() == ErrorKind::Interrupted { - // Exit if simulation is finished. - if stop.load(Ordering::Relaxed) { - break 'process; - } + loop { + match socket.recv_from(&mut buf) { + Ok((packet_size, _)) => { + if let Ok(message) = std::str::from_utf8(&buf[..packet_size]) { + if message == SHUTDOWN_SIGNAL { + break; + } + // Inject external message into simulation. + if tx.send(message.into()).is_err() { + break; + } + }; + } + Err(e) if e.kind() == ErrorKind::Interrupted => { continue; } - break 'process; - } - - for event in events.iter() { - match event.token() { - UDP_SOCKET => loop { - match socket.recv_from(&mut buf) { - Ok((packet_size, _)) => { - if let Ok(message) = std::str::from_utf8(&buf[..packet_size]) { - // Inject external message into simulation. - if tx.send(message.into()).is_err() { - break 'process; - } - }; - } - Err(e) if e.kind() == ErrorKind::WouldBlock => { - break; - } - _ => { - break 'process; - } - } - }, - _ => { - panic!("Got event for unexpected token: {:?}", event); - } + _ => { + break; } } - // Exit if simulation is finished. - if stop.load(Ordering::Relaxed) { - break 'process; - } } - - poll.registry().deregister(&mut socket).unwrap(); } } impl Model for Listener { - /// Start UDP Server on model setup. - fn setup(&mut self, _: &SetupContext) { - let tx = self.tx.take().unwrap(); - let start = Arc::clone(&self.start); - let stop = Arc::clone(&self.stop); - self.external_handle = Some(thread::spawn(move || { - Self::listener(tx, start, stop); - })); - } - /// Initialize model. async fn init(self, context: &Context) -> InitializedModel { // Schedule periodic function that processes external events. @@ -174,13 +148,40 @@ impl Model for Listener { } impl Drop for Listener { - /// Notify UDP Server that simulation is over and wait for server shutdown. + /// Wait for UDP Server shutdown. fn drop(&mut self) { - self.stop.store(true, Ordering::Relaxed); - let handle = self.external_handle.take(); - if let Some(handle) = handle { - handle.join().unwrap(); - } + self.server_handle.take().map(|handle| { + let _ = handle.join(); + }); + } +} + +/// A synchronization barrier that can be unblocked by a notifier. +struct WaitBarrier(Arc<(Mutex, Condvar)>); + +impl WaitBarrier { + fn new() -> Self { + Self(Arc::new((Mutex::new(false), Condvar::new()))) + } + fn notifier(&self) -> Notifier { + Notifier(self.0.clone()) + } + fn wait(self) { + let _unused = self + .0 + .1 + .wait_while(self.0 .0.lock().unwrap(), |pending| *pending) + .unwrap(); + } +} + +/// A notifier for the associated synchronization barrier. +struct Notifier(Arc<(Mutex, Condvar)>); + +impl Notifier { + fn notify(self) { + *self.0 .0.lock().unwrap() = false; + self.0 .1.notify_one(); } } @@ -191,16 +192,17 @@ fn main() -> Result<(), SimulationError> { // Models. - // Client-server synchronization. - let start = Arc::new(AtomicU32::new(0)); + // Synchronization barrier for the UDP client. + let start = WaitBarrier::new(); - let mut listener = Listener::new(Arc::clone(&start)); + // Prototype of the listener model. + let mut listener = ProtoListener::new(start.notifier()); // Mailboxes. let listener_mbox = Mailbox::new(); // Model handles for simulation. - let mut message = EventBuffer::new(); + let mut message = EventBuffer::with_capacity(N + 1); listener.message.connect_sink(&message); // Start time (arbitrary since models do not depend on absolute time). @@ -218,32 +220,39 @@ fn main() -> Result<(), SimulationError> { // External client that sends UDP messages. let sender_handle = thread::spawn(move || { - // Wait until UDP Server is ready. - wait(&start, 0); + let socket = UdpSocket::bind(SENDER).unwrap(); + + // Wait until the UDP Server is ready. + start.wait(); for i in 0..N { - let socket = UdpSocket::bind(SENDER).unwrap(); socket.send_to(i.to_string().as_bytes(), RECEIVER).unwrap(); if i % 3 == 0 { - sleep(PERIOD * i) + sleep(PERIOD * i as u32) } } + + socket }); // Advance simulation, external messages will be collected. simu.step_by(Duration::from_secs(2))?; + // Shut down the server. + let socket = sender_handle.join().unwrap(); + socket + .send_to(SHUTDOWN_SIGNAL.as_bytes(), RECEIVER) + .unwrap(); + // Check collected external messages. let mut packets = 0_u32; for _ in 0..N { - // UDP can reorder packages, we are expecting that on not too loaded - // localhost packages would not be dropped + // Check all messages accounting for possible UDP packet re-ordering, + // but assuming no packet loss. packets |= 1 << message.next().unwrap().parse::().unwrap(); } assert_eq!(packets, u32::MAX >> 22); assert_eq!(message.next(), None); - sender_handle.join().unwrap(); - Ok(()) } diff --git a/asynchronix/examples/power_supply.rs b/asynchronix/examples/power_supply.rs index ed87703..e566f33 100644 --- a/asynchronix/examples/power_supply.rs +++ b/asynchronix/examples/power_supply.rs @@ -8,19 +8,19 @@ //! ```text //! ┌────────┐ //! │ │ -//! ┌───►│ Load ├───► Power +//! ┌──◄►│ Load ├───► Power //! │ │ │ //! │ └────────┘ //! │ //! │ ┌────────┐ //! │ │ │ -//! ├───►│ Load ├───► Power +//! ├──◄►│ Load ├───► Power //! │ │ │ //! │ └────────┘ //! │ //! │ ┌────────┐ //! ┌──────────┐ voltage► │ │ │ -//! Voltage setting ●────►│ │◄────────────┴───►│ Load ├───► Power +//! Voltage setting ●────►│ │►◄───────────┴──◄►│ Load ├───► Power //! │ Power │ ◄current │ │ //! │ supply │ └────────┘ //! │ ├───────────────────────────────► Total power diff --git a/asynchronix/examples/stepper_motor.rs b/asynchronix/examples/stepper_motor.rs index aec6832..6073374 100644 --- a/asynchronix/examples/stepper_motor.rs +++ b/asynchronix/examples/stepper_motor.rs @@ -4,14 +4,17 @@ //! //! * self-scheduling methods, //! * model initialization, -//! * simulation monitoring with event streams. +//! * simulation monitoring with buffered event sinks. //! //! ```text -//! ┌──────────┐ ┌──────────┐ -//! PPS │ │ coil currents │ │ position -//! Pulse rate ●─────────►│ Driver ├───────────────►│ Motor ├──────────► -//! (±freq) │ │ (IA, IB) │ │ (0:199) -//! └──────────┘ └──────────┘ +//! ┌──────────┐ +//! PPS │ │ coil currents ┌─────────┐ +//! Pulse rate ●─────────►│ Driver ├───────────────►│ │ +//! (±freq) │ │ (IA, IB) │ │ position +//! └──────────┘ │ Motor ├──────────► +//! torque │ │ (0:199) +//! Load ●─────────────────────────────────────►│ │ +//! └─────────┘ //! ``` use std::future::Future; @@ -136,7 +139,7 @@ impl Driver { } } - /// Sets the pulse rate (sign = direction) [Hz] -- input port. + /// Pulse rate (sign = direction) [Hz] -- input port. pub async fn pulse_rate(&mut self, pps: f64, context: &Context) { println!( "Model instance {} at time {}: setting pps: {:.2}", diff --git a/asynchronix/src/model.rs b/asynchronix/src/model.rs index cda239c..3e69508 100644 --- a/asynchronix/src/model.rs +++ b/asynchronix/src/model.rs @@ -1,19 +1,39 @@ //! Model components. //! -//! # Model trait +//! # Models and model prototypes //! -//! Every model must implement the [`Model`] trait. This trait defines -//! * a setup method, [`Model::setup()`], which main purpose is to create, -//! connect and add to the simulation bench submodels and perform other setup -//! steps, -//! * an asynchronous initialization method, [`Model::init()`], which main -//! purpose is to enable models to perform specific actions only once all -//! models have been connected and migrated to the simulation, but before the -//! simulation actually starts. +//! Every model must implement the [`Model`] trait. This trait defines an +//! asynchronous initialization method, [`Model::init`], which main purpose is +//! to enable models to perform specific actions when the simulation starts, +//! 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 +//! *model prototype*—rather than the final model. This can be done by +//! implementing the `ProtoModel`, which defines the associated model +//! type and a [`ProtoModel::build` method`] invoked when a model is added the +//! the simulation and returning the actual model instance. +//! +//! 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 +//! benefit from the use of prototype models, however, is hierarchical model +//! building. When a parent model contains sub-models, these sub-models are +//! often an implementation detail that needs not be exposed to the user. One +//! may then define a prototype model that contains all outputs and requestors +//! ports, which upon invocation of `ProtoModel::build()` are moved to the +//! appropriate sub-models (note that the `build` method also allows adding +//! sub-models to the simulation). +//! +//! Note that a trivial `ProtoModel` implementation is generated by default for +//! any object implementing the `Model` trait, where the associated +//! `ProtoModel::Model` type is the model type itself and where +//! `ProtoModel::build` simply returns the model instance. This is what makes it +//! possible to use either an explicitly-defined `ProtoModel` as argument to the +//! [`SimInit::add_model`](crate::simulation::SimInit::add_model) method, or a +//! plain `Model` type. //! //! #### Examples //! -//! A model that does not require setup and initialization can simply use the +//! A model that does not require initialization or building can simply use the //! default implementation of the `Model` trait: //! //! ``` @@ -25,27 +45,19 @@ //! impl Model for MyModel {} //! ``` //! -//! Otherwise, custom `setup()` or `init()` methods can be implemented: +//! If a default action is required during simulation initialization, the `init` +//! methods can be explicitly implemented: //! //! ``` -//! use std::future::Future; -//! use std::pin::Pin; -//! -//! use asynchronix::model::{Context, InitializedModel, Model, SetupContext}; +//! use asynchronix::model::{Context, InitializedModel, Model}; //! //! pub struct MyModel { //! // ... //! } //! impl Model for MyModel { -//! fn setup( -//! &mut self, -//! setup_context: &SetupContext) { -//! println!("...setup..."); -//! } -//! //! async fn init( //! mut self, -//! context: &Context +//! ctx: &Context //! ) -> InitializedModel { //! println!("...initialization..."); //! @@ -54,6 +66,61 @@ //! } //! ``` //! +//! Finally, if a model builder is required, the `ProtoModel` trait can be +//! explicitly implemented: +//! +//! ``` +//! use asynchronix::model::{BuildContext, InitializedModel, Model, ProtoModel}; +//! use asynchronix::ports::Output; +//! +//! /// The final model. +//! pub struct Multiplier { +//! // Private outputs and requestors stored in a form that constitutes an +//! // implementation detail and should not be exposed to the user. +//! my_outputs: Vec> +//! } +//! impl Multiplier { +//! // Private constructor: the final model is only built by the prototype +//! // model. +//! fn new( +//! value_times_1: Output, +//! value_times_2: Output, +//! value_times_3: Output, +//! ) -> Self { +//! Self { +//! my_outputs: vec![value_times_1, value_times_2, value_times_3] +//! } +//! } +//! +//! // Public inputs and repliers to be used by the user during bench +//! // construction. +//! pub async fn my_input(&mut self, my_data: usize) { +//! for (i, output) in self.my_outputs.iter_mut().enumerate() { +//! output.send(my_data*(i + 1)).await; +//! } +//! } +//! } +//! impl Model for Multiplier {} +//! +//! pub struct ProtoMultiplier { +//! // Prettyfied outputs exposed to the user. +//! pub value_times_1: Output, +//! pub value_times_2: Output, +//! pub value_times_3: Output, +//! } +//! impl ProtoModel for ProtoMultiplier { +//! type Model = Multiplier; +//! +//! fn build( +//! mut self, +//! _: &BuildContext +//! ) -> Multiplier { +//! Multiplier::new(self.value_times_1, self.value_times_2, self.value_times_3) +//! } +//! } +//! ``` +//! +//! //! # Events and queries //! //! Models can exchange data via *events* and *queries*. @@ -169,57 +236,22 @@ use std::future::Future; -pub use context::{Context, SetupContext}; +pub use context::{BuildContext, Context}; mod context; -/// Trait to be implemented by all models. +/// Trait to be implemented by simulation models. /// -/// This trait enables models to perform specific actions during setup and -/// initialization. The [`Model::setup()`] method is run only once when models -/// are being added to the simulation bench. This method allows in particular -/// sub-models to be created, connected and added to the simulation. -/// -/// The [`Model::init()`] method is run only once all models have been connected 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 the -/// beginning of the simulation. +/// This trait enables models to perform specific actions during initialization. +/// The [`Model::init()`] method is run only once all models have been connected +/// 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 +/// the beginning of the simulation. /// /// The `init` function converts the model to the opaque `InitializedModel` type /// to prevent an already initialized model from being added to the simulation /// bench. pub trait Model: Sized + Send + 'static { - /// Performs model setup. - /// - /// This method is executed exactly once for all models of the simulation - /// when the [`SimInit::add_model()`](crate::simulation::SimInit::add_model) - /// method is called. - /// - /// The default implementation does nothing. - /// - /// # Examples - /// - /// ``` - /// use std::future::Future; - /// use std::pin::Pin; - /// - /// use asynchronix::model::{InitializedModel, Model, SetupContext}; - /// - /// pub struct MyModel { - /// // ... - /// } - /// - /// impl Model for MyModel { - /// fn setup( - /// &mut self, - /// setup_context: &SetupContext - /// ) { - /// println!("...setup..."); - /// } - /// } - /// ``` - fn setup(&mut self, _: &SetupContext) {} - /// Performs asynchronous model initialization. /// /// This asynchronous method is executed exactly once for all models of the @@ -271,3 +303,36 @@ impl From for InitializedModel { InitializedModel(model) } } + +/// Trait to be implemented by model prototypes. +/// +/// This trait makes it possible to build the final model from a builder type +/// when it is added to the simulation. +/// +/// The [`ProtoModel::build()`] method consumes the prototype. It is +/// automatically called when a model or submodel prototype is added to the +/// simulation using +/// [`Simulation::add_model()`](crate::simulation::SimInit::add_model) or +/// [`BuildContext::add_submodel`]. +/// +/// The +pub trait ProtoModel: Sized { + /// Type of the model to be built. + type Model: Model; + + /// Builds the model. + /// + /// This method is invoked when the + /// [`SimInit::add_model()`](crate::simulation::SimInit::add_model) or + /// [`BuildContext::add_submodel`] method is called. + fn build(self, ctx: &BuildContext) -> Self::Model; +} + +// Every model can be used as a prototype for itself. +impl ProtoModel for M { + type Model = Self; + + fn build(self, _: &BuildContext) -> Self::Model { + self + } +} diff --git a/asynchronix/src/model/context.rs b/asynchronix/src/model/context.rs index b71742a..640babd 100644 --- a/asynchronix/src/model/context.rs +++ b/asynchronix/src/model/context.rs @@ -3,7 +3,7 @@ use std::fmt; use crate::executor::Executor; use crate::simulation::{self, LocalScheduler, Mailbox}; -use super::Model; +use super::{Model, ProtoModel}; /// A local context for models. /// @@ -95,75 +95,102 @@ impl fmt::Debug for Context { } } -/// A setup context for models. +/// Context available when building a model from a model prototype. /// -/// A `SetupContext` can be used by models during the setup stage to -/// create submodels and add them to the simulation bench. +/// A `BuildContext` can be used to add the sub-models of a hierarchical model +/// to the simulation bench. /// /// # Examples /// -/// A model that contains two connected submodels. +/// A model that multiplies its input by four using two sub-models that each +/// multiply their input by two. +/// +/// ```text +/// ┌───────────────────────────────────────┐ +/// │ MyltiplyBy4 │ +/// │ ┌─────────────┐ ┌─────────────┐ │ +/// │ │ │ │ │ │ +/// Input ●─────┼──►│ MultiplyBy2 ├──►│ MultiplyBy2 ├───┼─────► Output +/// f64 │ │ │ │ │ │ f64 +/// │ └─────────────┘ └─────────────┘ │ +/// │ │ +/// └───────────────────────────────────────┘ +/// ``` /// /// ``` /// use std::time::Duration; -/// use asynchronix::model::{Model, SetupContext}; +/// use asynchronix::model::{BuildContext, Model, ProtoModel}; /// use asynchronix::ports::Output; /// use asynchronix::simulation::Mailbox; /// /// #[derive(Default)] -/// pub struct SubmodelA { -/// out: Output, +/// struct MultiplyBy2 { +/// pub output: Output, /// } +/// impl MultiplyBy2 { +/// pub async fn input(&mut self, value: i32) { +/// self.output.send(value * 2).await; +/// } +/// } +/// impl Model for MultiplyBy2 {} /// -/// impl Model for SubmodelA {} +/// pub struct MultiplyBy4 { +/// // Private forwarding output. +/// forward: Output, +/// } +/// impl MultiplyBy4 { +/// pub async fn input(&mut self, value: i32) { +/// self.forward.send(value).await; +/// } +/// } +/// impl Model for MultiplyBy4 {} /// -/// #[derive(Default)] -/// pub struct SubmodelB {} +/// pub struct ProtoMultiplyBy4 { +/// pub output: Output, +/// } +/// impl ProtoModel for ProtoMultiplyBy4 { +/// type Model = MultiplyBy4; /// -/// impl SubmodelB { -/// pub async fn input(&mut self, value: u32) { -/// println!("Received {}", value); +/// fn build( +/// self, +/// ctx: &BuildContext) +/// -> MultiplyBy4 { +/// let mut mult = MultiplyBy4 { forward: Output::default() }; +/// let mut submult1 = MultiplyBy2::default(); +/// +/// // Move the prototype's output to the second multiplier. +/// let mut submult2 = MultiplyBy2 { output: self.output }; +/// +/// // Forward the parent's model input to the first multiplier. +/// let submult1_mbox = Mailbox::new(); +/// mult.forward.connect(MultiplyBy2::input, &submult1_mbox); +/// +/// // Connect the two multiplier submodels. +/// let submult2_mbox = Mailbox::new(); +/// submult1.output.connect(MultiplyBy2::input, &submult2_mbox); +/// +/// // Add the submodels to the simulation. +/// ctx.add_submodel(submult1, submult1_mbox, "submultiplier 1"); +/// ctx.add_submodel(submult2, submult2_mbox, "submultiplier 2"); +/// +/// mult /// } /// } /// -/// impl Model for SubmodelB {} -/// -/// #[derive(Default)] -/// pub struct Parent {} -/// -/// impl Model for Parent { -/// fn setup( -/// &mut self, -/// setup_context: &SetupContext) { -/// let mut a = SubmodelA::default(); -/// let b = SubmodelB::default(); -/// let a_mbox = Mailbox::new(); -/// let b_mbox = Mailbox::new(); -/// let a_name = setup_context.name().to_string() + "::a"; -/// let b_name = setup_context.name().to_string() + "::b"; -/// -/// a.out.connect(SubmodelB::input, &b_mbox); -/// -/// setup_context.add_model(a, a_mbox, a_name); -/// setup_context.add_model(b, b_mbox, b_name); -/// } -/// } -/// /// ``` - #[derive(Debug)] -pub struct SetupContext<'a, M: Model> { +pub struct BuildContext<'a, P: ProtoModel> { /// Mailbox of the model. - pub mailbox: &'a Mailbox, - context: &'a Context, + pub mailbox: &'a Mailbox, + context: &'a Context, executor: &'a Executor, } -impl<'a, M: Model> SetupContext<'a, M> { +impl<'a, P: ProtoModel> BuildContext<'a, P> { /// Creates a new local context. pub(crate) fn new( - mailbox: &'a Mailbox, - context: &'a Context, + mailbox: &'a Mailbox, + context: &'a Context, executor: &'a Executor, ) -> Self { Self { @@ -178,16 +205,26 @@ impl<'a, M: Model> SetupContext<'a, M> { &self.context.name } - /// Adds a new model and its mailbox to the simulation bench. + /// Adds a sub-model to the simulation bench. /// - /// The `name` argument needs not be unique (it can be an empty string) and - /// is used for convenience for model instance identification (e.g. for - /// logging purposes). - pub fn add_model(&self, model: N, mailbox: Mailbox, name: impl Into) { + /// The `name` argument needs not be unique. If an empty string is provided, + /// it is replaced by the string ``. + /// + /// The provided name is appended to that of the parent model using a dot as + /// a separator (e.g. `parent_name.child_name`) to build an identifier. This + /// identifier is used for logging or error-reporting purposes. + pub fn add_submodel( + &self, + model: S, + mailbox: Mailbox, + name: impl Into, + ) { let mut submodel_name = name.into(); - if !self.context.name().is_empty() && !submodel_name.is_empty() { - submodel_name = self.context.name().to_string() + "." + &submodel_name; - } + if submodel_name.is_empty() { + submodel_name = String::from(""); + }; + submodel_name = self.context.name().to_string() + "." + &submodel_name; + simulation::add_model( model, mailbox, diff --git a/asynchronix/src/ports.rs b/asynchronix/src/ports.rs index 17362ec..d90bdc7 100644 --- a/asynchronix/src/ports.rs +++ b/asynchronix/src/ports.rs @@ -19,18 +19,18 @@ //! //! #### Example //! -//! This example demonstrates a submodel inside a parent model. The output of -//! the submodel is a clone of the parent model output. Both outputs remain -//! therefore always connected to the same inputs. +//! 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 +//! connected to the same inputs. //! -//! For a more comprehensive example demonstrating output cloning in submodels +//! For a more comprehensive example demonstrating hierarchical model //! assemblies, see the [`assembly example`][assembly]. //! //! [assembly]: //! https://github.com/asynchronics/asynchronix/tree/main/asynchronix/examples/assembly.rs //! //! ``` -//! use asynchronix::model::{Model, SetupContext}; +//! use asynchronix::model::{BuildContext, Model, ProtoModel}; //! use asynchronix::ports::Output; //! use asynchronix::simulation::Mailbox; //! @@ -39,9 +39,9 @@ //! } //! //! impl ChildModel { -//! pub fn new() -> Self { +//! pub fn new(output: Output) -> Self { //! Self { -//! output: Default::default(), +//! output, //! } //! } //! } @@ -49,10 +49,16 @@ //! impl Model for ChildModel {} //! //! pub struct ParentModel { +//! output: Output, +//! } +//! +//! impl Model for ParentModel {} +//! +//! pub struct ProtoParentModel { //! pub output: Output, //! } //! -//! impl ParentModel { +//! impl ProtoParentModel { //! pub fn new() -> Self { //! Self { //! output: Default::default(), @@ -60,13 +66,15 @@ //! } //! } //! -//! impl Model for ParentModel { -//! fn setup(&mut self, setup_context: &SetupContext) { -//! let mut child = ChildModel::new(); -//! let child_mbox = Mailbox::new(); -//! child.output = self.output.clone(); -//! let child_name = setup_context.name().to_string() + "::child"; -//! setup_context.add_model(child, child_mbox, child_name); +//! impl ProtoModel for ProtoParentModel { +//! type Model = ParentModel; +//! +//! fn build(self, ctx: &BuildContext) -> ParentModel { +//! let mut child = ChildModel::new(self.output.clone()); +//! +//! ctx.add_submodel(child, Mailbox::new(), "child"); +//! +//! ParentModel { output: self.output } //! } //! } //! ``` diff --git a/asynchronix/src/simulation.rs b/asynchronix/src/simulation.rs index 9d8c1fb..e3c3579 100644 --- a/asynchronix/src/simulation.rs +++ b/asynchronix/src/simulation.rs @@ -146,7 +146,7 @@ use recycle_box::{coerce_box, RecycleBox}; use crate::channel::ChannelObserver; use crate::executor::{Executor, ExecutorError}; -use crate::model::{Context, Model, SetupContext}; +use crate::model::{BuildContext, Context, Model, ProtoModel}; use crate::ports::{InputFn, ReplierFn}; use crate::time::{AtomicTime, Clock, MonotonicTime}; use crate::util::seq_futures::SeqFuture; @@ -647,9 +647,9 @@ impl From for SimulationError { } /// Adds a model and its mailbox to the simulation bench. -pub(crate) fn add_model( - mut model: M, - mailbox: Mailbox, +pub(crate) fn add_model( + model: P, + mailbox: Mailbox, name: String, scheduler: Scheduler, executor: &Executor, @@ -658,9 +658,9 @@ pub(crate) fn add_model( let span = tracing::span!(target: env!("CARGO_PKG_NAME"), tracing::Level::INFO, "model", name); let context = Context::new(name, LocalScheduler::new(scheduler, mailbox.address())); - let setup_context = SetupContext::new(&mailbox, &context, executor); + let build_context = BuildContext::new(&mailbox, &context, executor); - model.setup(&setup_context); + let model = model.build(&build_context); let mut receiver = mailbox.0; let fut = async move { diff --git a/asynchronix/src/simulation/sim_init.rs b/asynchronix/src/simulation/sim_init.rs index efeacf4..c6580b3 100644 --- a/asynchronix/src/simulation/sim_init.rs +++ b/asynchronix/src/simulation/sim_init.rs @@ -3,7 +3,7 @@ use std::sync::{Arc, Mutex}; use crate::channel::ChannelObserver; use crate::executor::{Executor, SimulationContext}; -use crate::model::Model; +use crate::model::ProtoModel; use crate::time::{AtomicTime, MonotonicTime, TearableAtomicTime}; use crate::time::{Clock, NoClock}; use crate::util::priority_queue::PriorityQueue; @@ -57,13 +57,13 @@ impl SimInit { /// Adds a model and its mailbox to the simulation bench. /// - /// The `name` argument needs not be unique (it can be the empty string) and - /// is used for convenience for the model instance identification (e.g. for - /// logging purposes). - pub fn add_model( + /// The `name` argument needs not be unique. If an empty string is provided, + /// it is replaced by the string ``. This name serves an identifier + /// for logging or error-reporting purposes. + pub fn add_model( mut self, - model: M, - mailbox: Mailbox, + model: P, + mailbox: Mailbox, name: impl Into, ) -> Self { let name = name.into(); diff --git a/asynchronix/src/util/cached_rw_lock.rs b/asynchronix/src/util/cached_rw_lock.rs index 8997627..0866050 100644 --- a/asynchronix/src/util/cached_rw_lock.rs +++ b/asynchronix/src/util/cached_rw_lock.rs @@ -22,7 +22,7 @@ pub(crate) struct CachedRwLock { } impl CachedRwLock { - /// Creates a new cached read-write lock in an ulocked state. + /// Creates a new cached read-write lock in unlocked state. pub(crate) fn new(t: T) -> Self { let shared = t.clone(); Self {