1
0
forked from ROMEO/nexosim

Merge pull request #54 from asynchronics/feature/protomodel

Introduce ProtoModel trait, remove Model::setup
This commit is contained in:
Jauhien Piatlicki 2024-11-05 23:36:51 +01:00 committed by GitHub
commit 35e7e17814
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 462 additions and 314 deletions

View File

@ -60,10 +60,8 @@ tracing = { version= "0.1.40", default-features = false, features=["std"], optio
tracing-subscriber = { version= "0.3.18", optional = true } tracing-subscriber = { version= "0.3.18", optional = true }
[dev-dependencies] [dev-dependencies]
atomic-wait = "1.1"
futures-util = "0.3" futures-util = "0.3"
futures-executor = "0.3" futures-executor = "0.3"
mio = { version = "1.0", features = ["os-poll", "net"] }
tracing-subscriber = { version= "0.3.18", features=["env-filter"] } tracing-subscriber = { version= "0.3.18", features=["env-filter"] }
[target.'cfg(asynchronix_loom)'.dev-dependencies] [target.'cfg(asynchronix_loom)'.dev-dependencies]

View File

@ -3,27 +3,29 @@
//! //!
//! This example demonstrates in particular: //! This example demonstrates in particular:
//! //!
//! * model prototypes,
//! * submodels, //! * submodels,
//! * outputs cloning,
//! * self-scheduling methods, //! * self-scheduling methods,
//! * model setup,
//! * model initialization, //! * model initialization,
//! * simulation monitoring with event streams. //! * simulation monitoring with buffered event sinks.
//! //!
//! ```text //! ```text
//! ──────────────────────────────────────────────┐ //! ┌────────────────────────────────────────────┐
//! │ Assembly │ //! │ Assembly │
//! │ ┌──────────┐ ┌──────────┐ │ //! │ ┌──────────┐ │
//! PPS │ │ │ coil currents │ │ │position //! PPS │ │ │ coil currents ┌─────────┐ │
//! Pulse rate ●───────►│──►│ Driver ├───────────────►│ Motor ├──►│─────────► //! Pulse rate ●──────────┼──►│ Driver ├───────────────►│ │ │
//! (±freq)│ │ │ (IA, IB) │ │ │(0:199) //! (±freq) │ │ │ (IA, IB) │ │ │ position
//! │ └──────────┘ └──────────┘ │ //! │ └──────────┘ │ Motor ├──┼──────────►
//! └──────────────────────────────────────────────┘ //! torque │ │ │ │ (0:199)
//! Load ●──────────┼──────────────────────────────►│ │ │
//! │ └─────────┘ │
//! └────────────────────────────────────────────┘
//! ``` //! ```
use std::time::Duration; use std::time::Duration;
use asynchronix::model::{Model, SetupContext}; use asynchronix::model::{BuildContext, Model, ProtoModel};
use asynchronix::ports::{EventBuffer, Output}; use asynchronix::ports::{EventBuffer, Output};
use asynchronix::simulation::{Mailbox, SimInit, SimulationError}; use asynchronix::simulation::{Mailbox, SimInit, SimulationError};
use asynchronix::time::MonotonicTime; use asynchronix::time::MonotonicTime;
@ -32,36 +34,59 @@ mod stepper_motor;
pub use stepper_motor::{Driver, Motor}; pub use stepper_motor::{Driver, Motor};
pub struct MotorAssembly { /// A prototype for `MotorAssembly`.
pub struct ProtoMotorAssembly {
pub position: Output<u16>, pub position: Output<u16>,
init_pos: u16, init_pos: u16,
load: Output<f64>,
pps: Output<f64>,
} }
impl MotorAssembly { impl ProtoMotorAssembly {
/// The prototype has a public constructor.
pub fn new(init_pos: u16) -> Self { pub fn new(init_pos: u16) -> Self {
Self { Self {
position: Default::default(), position: Default::default(),
init_pos, 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<f64>,
/// Private output for submodel connection.
load: Output<f64>,
}
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) { 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. /// Torque applied by the load [N·m] -- input port.
pub async fn load(&mut self, torque: f64) { pub async fn load(&mut self, torque: f64) {
self.load.send(torque).await; self.load.send(torque).await
} }
} }
impl Model for MotorAssembly { impl Model for MotorAssembly {}
fn setup(&mut self, setup_context: &SetupContext<Self>) {
impl ProtoModel for ProtoMotorAssembly {
type Model = MotorAssembly;
fn build(self, ctx: &BuildContext<Self>) -> MotorAssembly {
let mut assembly = MotorAssembly::new();
let mut motor = Motor::new(self.init_pos); let mut motor = Motor::new(self.init_pos);
let mut driver = Driver::new(1.0); let mut driver = Driver::new(1.0);
@ -70,17 +95,20 @@ impl Model for MotorAssembly {
let driver_mbox = Mailbox::new(); let driver_mbox = Mailbox::new();
// Connections. // Connections.
self.pps.connect(Driver::pulse_rate, &driver_mbox); assembly.pps.connect(Driver::pulse_rate, &driver_mbox);
self.load.connect(Motor::load, &motor_mbox); assembly.load.connect(Motor::load, &motor_mbox);
driver.current_out.connect(Motor::current_in, &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"); // Move the prototype's output to the submodel. The `self.position`
setup_context.add_model(motor, motor_mbox, "motor"); // 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. // Models.
let init_pos = 123; let init_pos = 123;
let mut assembly = MotorAssembly::new(init_pos); let mut assembly = ProtoMotorAssembly::new(init_pos);
// Mailboxes. // Mailboxes.
let assembly_mbox = Mailbox::new(); let assembly_mbox = Mailbox::new();

View File

@ -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: //! This example demonstrates in particular:
//! //!
//! * external world inputs (useful in cosimulation), //! * processing of external inputs (useful in co-simulation),
//! * system clock, //! * system clock,
//! * periodic scheduling. //! * periodic scheduling.
//! //!
//! ```text //! ```text
//! ┌────────────────────────────────┐ //! ┏━━━━━━━━━━━━━━━━━━━━━━━━┓
//! │ Simulation │ //! ┃ Simulation ┃
//! ┌────────────┐ ┌────────────┐ │ ┌──────────┐ │ //! ┌╌╌╌╌╌╌╌╌╌╌╌╌┐ ┌╌╌╌╌╌╌╌╌╌╌╌╌┐ ┃ ┌──────────┐ ┃
//! │ │ UDP │ │ message │ message │ │ message │ ┌─────────────┐ //! ┆ ┆ message ┆ ┆ message ┃ │ │ message ┃
//! │ UDP Client ├─────────►│ UDP Server ├──────────►├─────────►│ Listener ├─────────►├──►│ EventBuffer │ //! ┆ UDP Client ├╌╌╌╌╌╌╌╌►┆ UDP Server ├╌╌╌╌╌╌╌╌╌╌╌╂╌╌►│ Listener ├─────────╂─►
//! │ │ message │ │ │ │ │ │ └─────────────┘ //! ┆ ┆ [UDP] ┆ ┆ [channel] ┃ │ │ ┃
//! └────────────┘ └────────────┘ │ └──────────┘ │ //! └╌╌╌╌╌╌╌╌╌╌╌╌┘ └╌╌╌╌╌╌╌╌╌╌╌╌┘ ┃ └──────────┘ ┃
//! └────────────────────────────────┘ //! ┗━━━━━━━━━━━━━━━━━━━━━━━━┛
//! ``` //! ```
use std::io::ErrorKind; use std::io::ErrorKind;
use std::net::UdpSocket; use std::net::{Ipv4Addr, UdpSocket};
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::sync::mpsc::{channel, Receiver, Sender}; 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::thread::{self, sleep, JoinHandle};
use std::time::Duration; use std::time::Duration;
use atomic_wait::{wait, wake_one}; use asynchronix::model::{BuildContext, Context, InitializedModel, Model, ProtoModel};
use mio::net::UdpSocket as MioUdpSocket;
use mio::{Events, Interest, Poll, Token};
use asynchronix::model::{Context, InitializedModel, Model, SetupContext};
use asynchronix::ports::{EventBuffer, Output}; use asynchronix::ports::{EventBuffer, Output};
use asynchronix::simulation::{Mailbox, SimInit, SimulationError}; use asynchronix::simulation::{Mailbox, SimInit, SimulationError};
use asynchronix::time::{AutoSystemClock, MonotonicTime}; use asynchronix::time::{AutoSystemClock, MonotonicTime};
const DELTA: Duration = Duration::from_millis(2); const DELTA: Duration = Duration::from_millis(2);
const PERIOD: Duration = Duration::from_millis(20); const PERIOD: Duration = Duration::from_millis(20);
const N: u32 = 10; const N: usize = 10;
const SENDER: &str = "127.0.0.1:8000"; const SHUTDOWN_SIGNAL: &str = "<SHUTDOWN>";
const RECEIVER: &str = "127.0.0.1:9000"; 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. /// Prototype for the `Listener` Model.
pub struct Listener { pub struct ProtoListener {
/// Received message. /// Received message.
pub message: Output<String>, pub message: Output<String>,
/// 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<Self>) -> 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<String>,
/// Receiver of external messages. /// Receiver of external messages.
rx: Receiver<String>, rx: Receiver<String>,
/// External sender.
tx: Option<Sender<String>>,
/// Synchronization with client.
start: Arc<AtomicU32>,
/// Synchronization with simulation.
stop: Arc<AtomicBool>,
/// Handle to UDP Server. /// Handle to UDP Server.
external_handle: Option<JoinHandle<()>>, server_handle: Option<JoinHandle<()>>,
} }
impl Listener { impl Listener {
/// Creates a Listener. /// Creates a Listener.
pub fn new(start: Arc<AtomicU32>) -> Self { pub fn new(
start.store(0, Ordering::Relaxed); message: Output<String>,
rx: Receiver<String>,
let (tx, rx) = channel(); server_handle: JoinHandle<()>,
) -> Self {
Self { Self {
message: Output::default(), message,
rx, rx,
tx: Some(tx), server_handle: Some(server_handle),
start,
stop: Arc::new(AtomicBool::new(false)),
external_handle: None,
} }
} }
@ -85,82 +102,39 @@ impl Listener {
} }
} }
/// UDP server. /// Starts the UDP server.
/// fn listen(tx: Sender<String>, start: Notifier) {
/// Code is based on the MIO UDP example. let socket = UdpSocket::bind(RECEIVER).unwrap();
fn listener(tx: Sender<String>, start: Arc<AtomicU32>, stop: Arc<AtomicBool>) {
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();
let mut buf = [0; 1 << 16]; let mut buf = [0; 1 << 16];
// Wake up the client. // Wake up the client.
start.store(1, Ordering::Relaxed); start.notify();
wake_one(&*start);
'process: loop { 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;
}
continue;
}
break 'process;
}
for event in events.iter() {
match event.token() {
UDP_SOCKET => loop {
match socket.recv_from(&mut buf) { match socket.recv_from(&mut buf) {
Ok((packet_size, _)) => { Ok((packet_size, _)) => {
if let Ok(message) = std::str::from_utf8(&buf[..packet_size]) { if let Ok(message) = std::str::from_utf8(&buf[..packet_size]) {
if message == SHUTDOWN_SIGNAL {
break;
}
// Inject external message into simulation. // Inject external message into simulation.
if tx.send(message.into()).is_err() { if tx.send(message.into()).is_err() {
break 'process; break;
} }
}; };
} }
Err(e) if e.kind() == ErrorKind::WouldBlock => { Err(e) if e.kind() == ErrorKind::Interrupted => {
continue;
}
_ => {
break; break;
} }
_ => {
break 'process;
} }
} }
},
_ => {
panic!("Got event for unexpected token: {:?}", event);
}
}
}
// Exit if simulation is finished.
if stop.load(Ordering::Relaxed) {
break 'process;
}
}
poll.registry().deregister(&mut socket).unwrap();
} }
} }
impl Model for Listener { impl Model for Listener {
/// Start UDP Server on model setup.
fn setup(&mut self, _: &SetupContext<Self>) {
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. /// Initialize model.
async fn init(self, context: &Context<Self>) -> InitializedModel<Self> { async fn init(self, context: &Context<Self>) -> InitializedModel<Self> {
// Schedule periodic function that processes external events. // Schedule periodic function that processes external events.
@ -174,13 +148,40 @@ impl Model for Listener {
} }
impl Drop 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) { fn drop(&mut self) {
self.stop.store(true, Ordering::Relaxed); self.server_handle.take().map(|handle| {
let handle = self.external_handle.take(); let _ = handle.join();
if let Some(handle) = handle { });
handle.join().unwrap();
} }
}
/// A synchronization barrier that can be unblocked by a notifier.
struct WaitBarrier(Arc<(Mutex<bool>, 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<bool>, 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. // Models.
// Client-server synchronization. // Synchronization barrier for the UDP client.
let start = Arc::new(AtomicU32::new(0)); 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. // Mailboxes.
let listener_mbox = Mailbox::new(); let listener_mbox = Mailbox::new();
// Model handles for simulation. // Model handles for simulation.
let mut message = EventBuffer::new(); let mut message = EventBuffer::with_capacity(N + 1);
listener.message.connect_sink(&message); listener.message.connect_sink(&message);
// Start time (arbitrary since models do not depend on absolute time). // 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. // External client that sends UDP messages.
let sender_handle = thread::spawn(move || { let sender_handle = thread::spawn(move || {
// Wait until UDP Server is ready. let socket = UdpSocket::bind(SENDER).unwrap();
wait(&start, 0);
// Wait until the UDP Server is ready.
start.wait();
for i in 0..N { for i in 0..N {
let socket = UdpSocket::bind(SENDER).unwrap();
socket.send_to(i.to_string().as_bytes(), RECEIVER).unwrap(); socket.send_to(i.to_string().as_bytes(), RECEIVER).unwrap();
if i % 3 == 0 { if i % 3 == 0 {
sleep(PERIOD * i) sleep(PERIOD * i as u32)
} }
} }
socket
}); });
// Advance simulation, external messages will be collected. // Advance simulation, external messages will be collected.
simu.step_by(Duration::from_secs(2))?; 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. // Check collected external messages.
let mut packets = 0_u32; let mut packets = 0_u32;
for _ in 0..N { for _ in 0..N {
// UDP can reorder packages, we are expecting that on not too loaded // Check all messages accounting for possible UDP packet re-ordering,
// localhost packages would not be dropped // but assuming no packet loss.
packets |= 1 << message.next().unwrap().parse::<u8>().unwrap(); packets |= 1 << message.next().unwrap().parse::<u8>().unwrap();
} }
assert_eq!(packets, u32::MAX >> 22); assert_eq!(packets, u32::MAX >> 22);
assert_eq!(message.next(), None); assert_eq!(message.next(), None);
sender_handle.join().unwrap();
Ok(()) Ok(())
} }

View File

@ -8,19 +8,19 @@
//! ```text //! ```text
//! ┌────────┐ //! ┌────────┐
//! │ │ //! │ │
//! ┌──►│ Load ├───► Power //! ┌──►│ Load ├───► Power
//! │ │ │ //! │ │ │
//! │ └────────┘ //! │ └────────┘
//! │ //! │
//! │ ┌────────┐ //! │ ┌────────┐
//! │ │ │ //! │ │ │
//! ├──►│ Load ├───► Power //! ├──►│ Load ├───► Power
//! │ │ │ //! │ │ │
//! │ └────────┘ //! │ └────────┘
//! │ //! │
//! │ ┌────────┐ //! │ ┌────────┐
//! ┌──────────┐ voltage► │ │ │ //! ┌──────────┐ voltage► │ │ │
//! Voltage setting ●────►│ │◄────────────┴───►│ Load ├───► Power //! Voltage setting ●────►│ │►◄───────────┴──◄►│ Load ├───► Power
//! │ Power │ ◄current │ │ //! │ Power │ ◄current │ │
//! │ supply │ └────────┘ //! │ supply │ └────────┘
//! │ ├───────────────────────────────► Total power //! │ ├───────────────────────────────► Total power

View File

@ -4,14 +4,17 @@
//! //!
//! * self-scheduling methods, //! * self-scheduling methods,
//! * model initialization, //! * model initialization,
//! * simulation monitoring with event streams. //! * simulation monitoring with buffered event sinks.
//! //!
//! ```text //! ```text
//! ┌──────────┐ ┌──────────┐ //! ┌──────────┐
//! PPS │ │ coil currents │ │ position //! PPS │ │ coil currents ┌─────────┐
//! Pulse rate ●─────────►│ Driver ├───────────────►│ Motor ├──────────► //! Pulse rate ●─────────►│ Driver ├───────────────►│ │
//! (±freq) │ │ (IA, IB) │ │ (0:199) //! (±freq) │ │ (IA, IB) │ │ position
//! └──────────┘ └──────────┘ //! └──────────┘ │ Motor ├──────────►
//! torque │ │ (0:199)
//! Load ●─────────────────────────────────────►│ │
//! └─────────┘
//! ``` //! ```
use std::future::Future; 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<Self>) { pub async fn pulse_rate(&mut self, pps: f64, context: &Context<Self>) {
println!( println!(
"Model instance {} at time {}: setting pps: {:.2}", "Model instance {} at time {}: setting pps: {:.2}",

View File

@ -1,19 +1,39 @@
//! Model components. //! Model components.
//! //!
//! # Model trait //! # Models and model prototypes
//! //!
//! Every model must implement the [`Model`] trait. This trait defines //! Every model must implement the [`Model`] trait. This trait defines an
//! * a setup method, [`Model::setup()`], which main purpose is to create, //! asynchronous initialization method, [`Model::init`], which main purpose is
//! connect and add to the simulation bench submodels and perform other setup //! to enable models to perform specific actions when the simulation starts,
//! steps, //! i.e. after all models have been connected and added to the simulation.
//! * an asynchronous initialization method, [`Model::init()`], which main //!
//! purpose is to enable models to perform specific actions only once all //! It is frequently convenient to expose to users a model builder type—called a
//! models have been connected and migrated to the simulation, but before the //! *model prototype*—rather than the final model. This can be done by
//! simulation actually starts. //! 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 //! #### 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: //! default implementation of the `Model` trait:
//! //!
//! ``` //! ```
@ -25,27 +45,19 @@
//! impl Model for MyModel {} //! 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 asynchronix::model::{Context, InitializedModel, Model};
//! use std::pin::Pin;
//!
//! use asynchronix::model::{Context, InitializedModel, Model, SetupContext};
//! //!
//! pub struct MyModel { //! pub struct MyModel {
//! // ... //! // ...
//! } //! }
//! impl Model for MyModel { //! impl Model for MyModel {
//! fn setup(
//! &mut self,
//! setup_context: &SetupContext<Self>) {
//! println!("...setup...");
//! }
//!
//! async fn init( //! async fn init(
//! mut self, //! mut self,
//! context: &Context<Self> //! ctx: &Context<Self>
//! ) -> InitializedModel<Self> { //! ) -> InitializedModel<Self> {
//! println!("...initialization..."); //! 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<Output<usize>>
//! }
//! impl Multiplier {
//! // Private constructor: the final model is only built by the prototype
//! // model.
//! fn new(
//! value_times_1: Output<usize>,
//! value_times_2: Output<usize>,
//! value_times_3: Output<usize>,
//! ) -> 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<usize>,
//! pub value_times_2: Output<usize>,
//! pub value_times_3: Output<usize>,
//! }
//! impl ProtoModel for ProtoMultiplier {
//! type Model = Multiplier;
//!
//! fn build(
//! mut self,
//! _: &BuildContext<Self>
//! ) -> Multiplier {
//! Multiplier::new(self.value_times_1, self.value_times_2, self.value_times_3)
//! }
//! }
//! ```
//!
//!
//! # Events and queries //! # Events and queries
//! //!
//! Models can exchange data via *events* and *queries*. //! Models can exchange data via *events* and *queries*.
@ -169,57 +236,22 @@
use std::future::Future; use std::future::Future;
pub use context::{Context, SetupContext}; pub use context::{BuildContext, Context};
mod 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 /// This trait enables models to perform specific actions during initialization.
/// initialization. The [`Model::setup()`] method is run only once when models /// The [`Model::init()`] method is run only once all models have been connected
/// are being added to the simulation bench. This method allows in particular /// and migrated to the simulation bench, but before the simulation actually
/// sub-models to be created, connected and added to the simulation. /// starts. A common use for `init` is to send messages to connected models at
/// /// the beginning of 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.
/// ///
/// The `init` function converts the model to the opaque `InitializedModel` type /// The `init` function converts the model to the opaque `InitializedModel` type
/// to prevent an already initialized model from being added to the simulation /// to prevent an already initialized model from being added to the simulation
/// bench. /// bench.
pub trait Model: Sized + Send + 'static { 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<Self>
/// ) {
/// println!("...setup...");
/// }
/// }
/// ```
fn setup(&mut self, _: &SetupContext<Self>) {}
/// Performs asynchronous model initialization. /// Performs asynchronous model initialization.
/// ///
/// This asynchronous method is executed exactly once for all models of the /// This asynchronous method is executed exactly once for all models of the
@ -271,3 +303,36 @@ impl<M: Model> From<M> for InitializedModel<M> {
InitializedModel(model) 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>) -> Self::Model;
}
// Every model can be used as a prototype for itself.
impl<M: Model> ProtoModel for M {
type Model = Self;
fn build(self, _: &BuildContext<Self>) -> Self::Model {
self
}
}

View File

@ -3,7 +3,7 @@ use std::fmt;
use crate::executor::Executor; use crate::executor::Executor;
use crate::simulation::{self, LocalScheduler, Mailbox}; use crate::simulation::{self, LocalScheduler, Mailbox};
use super::Model; use super::{Model, ProtoModel};
/// A local context for models. /// A local context for models.
/// ///
@ -95,75 +95,102 @@ impl<M: Model> fmt::Debug for Context<M> {
} }
} }
/// 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 /// A `BuildContext` can be used to add the sub-models of a hierarchical model
/// create submodels and add them to the simulation bench. /// to the simulation bench.
/// ///
/// # Examples /// # 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 std::time::Duration;
/// use asynchronix::model::{Model, SetupContext}; /// use asynchronix::model::{BuildContext, Model, ProtoModel};
/// use asynchronix::ports::Output; /// use asynchronix::ports::Output;
/// use asynchronix::simulation::Mailbox; /// use asynchronix::simulation::Mailbox;
/// ///
/// #[derive(Default)] /// #[derive(Default)]
/// pub struct SubmodelA { /// struct MultiplyBy2 {
/// out: Output<u32>, /// pub output: Output<i32>,
/// } /// }
/// /// impl MultiplyBy2 {
/// impl Model for SubmodelA {} /// pub async fn input(&mut self, value: i32) {
/// /// self.output.send(value * 2).await;
/// #[derive(Default)]
/// pub struct SubmodelB {}
///
/// impl SubmodelB {
/// pub async fn input(&mut self, value: u32) {
/// println!("Received {}", value);
/// } /// }
/// } /// }
/// impl Model for MultiplyBy2 {}
/// ///
/// impl Model for SubmodelB {} /// pub struct MultiplyBy4 {
/// // Private forwarding output.
/// forward: Output<i32>,
/// }
/// impl MultiplyBy4 {
/// pub async fn input(&mut self, value: i32) {
/// self.forward.send(value).await;
/// }
/// }
/// impl Model for MultiplyBy4 {}
/// ///
/// #[derive(Default)] /// pub struct ProtoMultiplyBy4 {
/// pub struct Parent {} /// pub output: Output<i32>,
/// }
/// impl ProtoModel for ProtoMultiplyBy4 {
/// type Model = MultiplyBy4;
/// ///
/// impl Model for Parent { /// fn build(
/// fn setup( /// self,
/// &mut self, /// ctx: &BuildContext<Self>)
/// setup_context: &SetupContext<Self>) { /// -> MultiplyBy4 {
/// let mut a = SubmodelA::default(); /// let mut mult = MultiplyBy4 { forward: Output::default() };
/// let b = SubmodelB::default(); /// let mut submult1 = MultiplyBy2::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); /// // Move the prototype's output to the second multiplier.
/// let mut submult2 = MultiplyBy2 { output: self.output };
/// ///
/// setup_context.add_model(a, a_mbox, a_name); /// // Forward the parent's model input to the first multiplier.
/// setup_context.add_model(b, b_mbox, b_name); /// 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
/// } /// }
/// } /// }
/// ///
/// ``` /// ```
#[derive(Debug)] #[derive(Debug)]
pub struct SetupContext<'a, M: Model> { pub struct BuildContext<'a, P: ProtoModel> {
/// Mailbox of the model. /// Mailbox of the model.
pub mailbox: &'a Mailbox<M>, pub mailbox: &'a Mailbox<P::Model>,
context: &'a Context<M>, context: &'a Context<P::Model>,
executor: &'a Executor, executor: &'a Executor,
} }
impl<'a, M: Model> SetupContext<'a, M> { impl<'a, P: ProtoModel> BuildContext<'a, P> {
/// Creates a new local context. /// Creates a new local context.
pub(crate) fn new( pub(crate) fn new(
mailbox: &'a Mailbox<M>, mailbox: &'a Mailbox<P::Model>,
context: &'a Context<M>, context: &'a Context<P::Model>,
executor: &'a Executor, executor: &'a Executor,
) -> Self { ) -> Self {
Self { Self {
@ -178,16 +205,26 @@ impl<'a, M: Model> SetupContext<'a, M> {
&self.context.name &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 /// The `name` argument needs not be unique. If an empty string is provided,
/// is used for convenience for model instance identification (e.g. for /// it is replaced by the string `<unknown>`.
/// logging purposes). ///
pub fn add_model<N: Model>(&self, model: N, mailbox: Mailbox<N>, name: impl Into<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<S: ProtoModel>(
&self,
model: S,
mailbox: Mailbox<S::Model>,
name: impl Into<String>,
) {
let mut submodel_name = name.into(); let mut submodel_name = name.into();
if !self.context.name().is_empty() && !submodel_name.is_empty() { if submodel_name.is_empty() {
submodel_name = String::from("<unknown>");
};
submodel_name = self.context.name().to_string() + "." + &submodel_name; submodel_name = self.context.name().to_string() + "." + &submodel_name;
}
simulation::add_model( simulation::add_model(
model, model,
mailbox, mailbox,

View File

@ -19,18 +19,18 @@
//! //!
//! #### Example //! #### Example
//! //!
//! This example demonstrates a submodel inside a parent model. The output of //! This example demonstrates two submodels inside a parent model. The output of
//! the submodel is a clone of the parent model output. Both outputs remain //! the submodel and of the main model are clones and remain therefore always
//! therefore always connected to the same inputs. //! 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]. //! assemblies, see the [`assembly example`][assembly].
//! //!
//! [assembly]: //! [assembly]:
//! https://github.com/asynchronics/asynchronix/tree/main/asynchronix/examples/assembly.rs //! 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::ports::Output;
//! use asynchronix::simulation::Mailbox; //! use asynchronix::simulation::Mailbox;
//! //!
@ -39,9 +39,9 @@
//! } //! }
//! //!
//! impl ChildModel { //! impl ChildModel {
//! pub fn new() -> Self { //! pub fn new(output: Output<u64>) -> Self {
//! Self { //! Self {
//! output: Default::default(), //! output,
//! } //! }
//! } //! }
//! } //! }
@ -49,10 +49,16 @@
//! impl Model for ChildModel {} //! impl Model for ChildModel {}
//! //!
//! pub struct ParentModel { //! pub struct ParentModel {
//! output: Output<u64>,
//! }
//!
//! impl Model for ParentModel {}
//!
//! pub struct ProtoParentModel {
//! pub output: Output<u64>, //! pub output: Output<u64>,
//! } //! }
//! //!
//! impl ParentModel { //! impl ProtoParentModel {
//! pub fn new() -> Self { //! pub fn new() -> Self {
//! Self { //! Self {
//! output: Default::default(), //! output: Default::default(),
@ -60,13 +66,15 @@
//! } //! }
//! } //! }
//! //!
//! impl Model for ParentModel { //! impl ProtoModel for ProtoParentModel {
//! fn setup(&mut self, setup_context: &SetupContext<Self>) { //! type Model = ParentModel;
//! let mut child = ChildModel::new(); //!
//! let child_mbox = Mailbox::new(); //! fn build(self, ctx: &BuildContext<Self>) -> ParentModel {
//! child.output = self.output.clone(); //! let mut child = ChildModel::new(self.output.clone());
//! let child_name = setup_context.name().to_string() + "::child"; //!
//! setup_context.add_model(child, child_mbox, child_name); //! ctx.add_submodel(child, Mailbox::new(), "child");
//!
//! ParentModel { output: self.output }
//! } //! }
//! } //! }
//! ``` //! ```

View File

@ -146,7 +146,7 @@ use recycle_box::{coerce_box, RecycleBox};
use crate::channel::ChannelObserver; use crate::channel::ChannelObserver;
use crate::executor::{Executor, ExecutorError}; 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::ports::{InputFn, ReplierFn};
use crate::time::{AtomicTime, Clock, MonotonicTime}; use crate::time::{AtomicTime, Clock, MonotonicTime};
use crate::util::seq_futures::SeqFuture; use crate::util::seq_futures::SeqFuture;
@ -647,9 +647,9 @@ impl From<SchedulingError> for SimulationError {
} }
/// Adds a model and its mailbox to the simulation bench. /// Adds a model and its mailbox to the simulation bench.
pub(crate) fn add_model<M: Model>( pub(crate) fn add_model<P: ProtoModel>(
mut model: M, model: P,
mailbox: Mailbox<M>, mailbox: Mailbox<P::Model>,
name: String, name: String,
scheduler: Scheduler, scheduler: Scheduler,
executor: &Executor, executor: &Executor,
@ -658,9 +658,9 @@ pub(crate) fn add_model<M: Model>(
let span = tracing::span!(target: env!("CARGO_PKG_NAME"), tracing::Level::INFO, "model", name); 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 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 mut receiver = mailbox.0;
let fut = async move { let fut = async move {

View File

@ -3,7 +3,7 @@ use std::sync::{Arc, Mutex};
use crate::channel::ChannelObserver; use crate::channel::ChannelObserver;
use crate::executor::{Executor, SimulationContext}; use crate::executor::{Executor, SimulationContext};
use crate::model::Model; use crate::model::ProtoModel;
use crate::time::{AtomicTime, MonotonicTime, TearableAtomicTime}; use crate::time::{AtomicTime, MonotonicTime, TearableAtomicTime};
use crate::time::{Clock, NoClock}; use crate::time::{Clock, NoClock};
use crate::util::priority_queue::PriorityQueue; use crate::util::priority_queue::PriorityQueue;
@ -57,13 +57,13 @@ impl SimInit {
/// Adds a model and its mailbox to the simulation bench. /// Adds a model and its mailbox to the simulation bench.
/// ///
/// The `name` argument needs not be unique (it can be the empty string) and /// The `name` argument needs not be unique. If an empty string is provided,
/// is used for convenience for the model instance identification (e.g. for /// it is replaced by the string `<unknown>`. This name serves an identifier
/// logging purposes). /// for logging or error-reporting purposes.
pub fn add_model<M: Model>( pub fn add_model<P: ProtoModel>(
mut self, mut self,
model: M, model: P,
mailbox: Mailbox<M>, mailbox: Mailbox<P::Model>,
name: impl Into<String>, name: impl Into<String>,
) -> Self { ) -> Self {
let name = name.into(); let name = name.into();

View File

@ -22,7 +22,7 @@ pub(crate) struct CachedRwLock<T: Clone> {
} }
impl<T: Clone> CachedRwLock<T> { impl<T: Clone> CachedRwLock<T> {
/// 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 { pub(crate) fn new(t: T) -> Self {
let shared = t.clone(); let shared = t.clone();
Self { Self {