1
0
forked from ROMEO/nexosim

Introduce ProtoModel trait, remove Model::setup

The external_input example has been as well adapted and (at least
temporarily) simplifiedi/modified to remove the dependencies on
`atomic_wait` and `mio`.
This commit is contained in:
Serge Barral
2024-11-04 00:00:50 +01:00
parent 8f7057689c
commit 039fefad47
11 changed files with 462 additions and 314 deletions

View File

@ -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<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 {
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<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) {
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<Self>) {
impl Model for MotorAssembly {}
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 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();

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:
//!
//! * 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 = "<SHUTDOWN>";
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<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.
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.
external_handle: Option<JoinHandle<()>>,
server_handle: Option<JoinHandle<()>>,
}
impl Listener {
/// Creates a Listener.
pub fn new(start: Arc<AtomicU32>) -> Self {
start.store(0, Ordering::Relaxed);
let (tx, rx) = channel();
pub fn new(
message: Output<String>,
rx: Receiver<String>,
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<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();
/// Starts the UDP server.
fn listen(tx: Sender<String>, 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<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.
async fn init(self, context: &Context<Self>) -> InitializedModel<Self> {
// 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<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.
// 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::<u8>().unwrap();
}
assert_eq!(packets, u32::MAX >> 22);
assert_eq!(message.next(), None);
sender_handle.join().unwrap();
Ok(())
}

View File

@ -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

View File

@ -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<Self>) {
println!(
"Model instance {} at time {}: setting pps: {:.2}",