diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2cfed61..2d1eb50 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -135,6 +135,16 @@ jobs: env: MIRIFLAGS: -Zmiri-strict-provenance -Zmiri-disable-isolation -Zmiri-num-cpus=4 + - name: Run cargo miri example5 (single-threaded executor) + run: cargo miri run --example observables + env: + MIRIFLAGS: -Zmiri-strict-provenance -Zmiri-disable-isolation -Zmiri-num-cpus=1 + + - name: Run cargo miri example5 (multi-threaded executor) + run: cargo miri run --example observables + env: + MIRIFLAGS: -Zmiri-strict-provenance -Zmiri-disable-isolation -Zmiri-num-cpus=4 + lints: name: Lints runs-on: ubuntu-latest diff --git a/README.md b/README.md index abc5fc8..c80d351 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,8 @@ The [API] documentation is relatively exhaustive and includes a practical overview which should provide all necessary information to get started. More fleshed out examples can also be found in the dedicated -[directory](asynchronix/examples). +[simulator](asynchronix/examples) and [utilities](asynchronix-util/examples) +directories. [API]: https://docs.rs/asynchronix @@ -183,4 +184,4 @@ This software is licensed under the [Apache License, Version 2.0](LICENSE-APACHE Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be -dual licensed as above, without any additional terms or conditions. \ No newline at end of file +dual licensed as above, without any additional terms or conditions. diff --git a/asynchronix-util/README.md b/asynchronix-util/README.md index 4bdbc8d..85f3163 100644 --- a/asynchronix-util/README.md +++ b/asynchronix-util/README.md @@ -1,3 +1,5 @@ # Utilities for model-building -TODO: add documentation +This crate contains utilities used for model and simulation bench development. + + diff --git a/asynchronix-util/examples/observables.rs b/asynchronix-util/examples/observables.rs new file mode 100644 index 0000000..1949b98 --- /dev/null +++ b/asynchronix-util/examples/observables.rs @@ -0,0 +1,310 @@ +//! Example: processor with observable states. +//! +//! This example demonstrates in particular: +//! +//! * the use of observable states, +//! * state machine with delays. +//! +//! ```text +//! ┌───────────┐ +//! Switch ON/OFF ●────►│ ├────► Mode +//! │ Processor │ +//! Process data ●────►│ ├────► Value +//! │ │ +//! │ ├────► House Keeping +//! └───────────┘ +//! ``` + +use std::time::Duration; + +use asynchronix::model::{Context, InitializedModel, Model}; +use asynchronix::ports::{EventBuffer, Output}; +use asynchronix::simulation::{AutoActionKey, Mailbox, SimInit, SimulationError}; +use asynchronix::time::MonotonicTime; +use asynchronix_util::observables::{Observable, ObservableState, ObservableValue}; + +/// House keeping TM. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Hk { + pub voltage: f64, + pub current: f64, +} + +impl Default for Hk { + fn default() -> Self { + Self { + voltage: 0.0, + current: 0.0, + } + } +} + +/// Processor mode ID. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ModeId { + Off, + Idle, + Processing, +} + +/// Processor state. +pub enum State { + Off, + Idle, + Processing(AutoActionKey), +} + +impl Default for State { + fn default() -> Self { + State::Off + } +} + +impl Observable for State { + fn observe(&self) -> ModeId { + match *self { + State::Off => ModeId::Off, + State::Idle => ModeId::Idle, + State::Processing(_) => ModeId::Processing, + } + } +} + +/// Processor model. +pub struct Processor { + /// Mode output. + pub mode: Output, + + /// Calculated value output. + pub value: Output, + + /// HK output. + pub hk: Output, + + /// Internal state. + state: ObservableState, + + /// Accumulator. + acc: ObservableValue, + + /// Electrical data. + elc: ObservableValue, +} + +impl Processor { + /// Create a new processor. + pub fn new() -> Self { + let mode = Output::new(); + let value = Output::new(); + let hk = Output::new(); + Self { + mode: mode.clone(), + value: value.clone(), + hk: hk.clone(), + state: ObservableState::new(mode), + acc: ObservableValue::new(value), + elc: ObservableValue::new(hk), + } + } + + /// Switch processor ON/OFF. + pub async fn switch_power(&mut self, on: bool) { + if on { + self.state.set(State::Idle).await; + self.elc + .set(Hk { + voltage: 5.5, + current: 0.1, + }) + .await; + self.acc.set(0).await; + } else { + self.state.set(State::Off).await; + self.elc.set(Hk::default()).await; + self.acc.set(0).await; + } + } + + /// Process data for dt milliseconds. + pub async fn process(&mut self, dt: u64, context: &Context) { + if matches!(self.state.observe(), ModeId::Idle | ModeId::Processing) { + self.state + .set(State::Processing( + context + .scheduler + .schedule_keyed_event( + Duration::from_millis(dt), + Self::finish_processing, + (), + ) + .unwrap() + .into_auto(), + )) + .await; + self.elc.modify(|hk| hk.current = 1.0).await; + } + } + + /// Finish processing. + async fn finish_processing(&mut self) { + self.state.set(State::Idle).await; + self.acc.modify(|a| *a += 1).await; + self.elc.modify(|hk| hk.current = 0.1).await; + } +} + +impl Model for Processor { + /// Propagate all internal states. + async fn init(mut self, _: &Context) -> InitializedModel { + self.state.propagate().await; + self.acc.propagate().await; + self.elc.propagate().await; + self.into() + } +} + +fn main() -> Result<(), SimulationError> { + // --------------- + // Bench assembly. + // --------------- + + // Models. + let mut proc = Processor::new(); + + // Mailboxes. + let proc_mbox = Mailbox::new(); + + // Model handles for simulation. + let mut mode = EventBuffer::new(); + let mut value = EventBuffer::new(); + let mut hk = EventBuffer::new(); + + proc.mode.connect_sink(&mode); + proc.value.connect_sink(&value); + proc.hk.connect_sink(&hk); + let proc_addr = proc_mbox.address(); + + // Start time (arbitrary since models do not depend on absolute time). + let t0 = MonotonicTime::EPOCH; + + // Assembly and initialization. + let mut simu = SimInit::new().add_model(proc, proc_mbox, "proc").init(t0)?; + + // ---------- + // Simulation. + // ---------- + + // Initial state. + expect( + &mut mode, + Some(ModeId::Off), + &mut value, + Some(0), + &mut hk, + 0.0, + 0.0, + ); + + // Switch processor on. + simu.process_event(Processor::switch_power, true, &proc_addr)?; + expect( + &mut mode, + Some(ModeId::Idle), + &mut value, + Some(0), + &mut hk, + 5.5, + 0.1, + ); + + // Trigger processing. + simu.process_event(Processor::process, 100, &proc_addr)?; + expect( + &mut mode, + Some(ModeId::Processing), + &mut value, + None, + &mut hk, + 5.5, + 1.0, + ); + + // All data processed. + simu.step_by(Duration::from_millis(101))?; + expect( + &mut mode, + Some(ModeId::Idle), + &mut value, + Some(1), + &mut hk, + 5.5, + 0.1, + ); + + // Trigger long processing. + simu.process_event(Processor::process, 100, &proc_addr)?; + expect( + &mut mode, + Some(ModeId::Processing), + &mut value, + None, + &mut hk, + 5.5, + 1.0, + ); + + // Trigger short processing, it cancels the previous one. + simu.process_event(Processor::process, 10, &proc_addr)?; + expect( + &mut mode, + Some(ModeId::Processing), + &mut value, + None, + &mut hk, + 5.5, + 1.0, + ); + + // Wait for short processing to finish, check results. + simu.step_by(Duration::from_millis(11))?; + expect( + &mut mode, + Some(ModeId::Idle), + &mut value, + Some(2), + &mut hk, + 5.5, + 0.1, + ); + + // Wait long enough, no state change as the long processing has been + // cancelled. + simu.step_by(Duration::from_millis(100))?; + assert_eq!(mode.next(), None); + assert_eq!(value.next(), None); + assert_eq!(hk.next(), None); + + Ok(()) +} + +// Check observable state. +fn expect( + mode: &mut EventBuffer, + mode_ex: Option, + value: &mut EventBuffer, + value_ex: Option, + hk: &mut EventBuffer, + voltage_ex: f64, + current_ex: f64, +) { + assert_eq!(mode.next(), mode_ex); + assert_eq!(value.next(), value_ex); + let hk_value = hk.next().unwrap(); + assert!(same(hk_value.voltage, voltage_ex)); + assert!(same(hk_value.current, current_ex)); +} + +// Compare two voltages or currents. +fn same(a: f64, b: f64) -> bool { + (a - b).abs() < 1e-12 +} diff --git a/asynchronix-util/src/observables.rs b/asynchronix-util/src/observables.rs index 45bb28a..6fee335 100644 --- a/asynchronix-util/src/observables.rs +++ b/asynchronix-util/src/observables.rs @@ -1,3 +1,9 @@ +//! Observable states. +//! +//! This module contains types used to implement states automatically propagated +//! to output on change. +//! + use std::ops::Deref; use asynchronix::ports::Output; diff --git a/asynchronix/examples/stepper_motor.rs b/asynchronix/examples/stepper_motor.rs index 6073374..52af7d7 100644 --- a/asynchronix/examples/stepper_motor.rs +++ b/asynchronix/examples/stepper_motor.rs @@ -56,15 +56,8 @@ impl Motor { /// For the sake of simplicity, we do as if the rotor rotates /// instantaneously. If the current is too weak to overcome the load or when /// attempting to move to an opposite phase, the position remains unchanged. - pub async fn current_in(&mut self, current: (f64, f64), context: &Context) { + pub async fn current_in(&mut self, current: (f64, f64)) { assert!(!current.0.is_nan() && !current.1.is_nan()); - println!( - "Model instance {} at time {}: setting currents: {:.2} and {:.2}", - context.name(), - context.scheduler.time(), - current.0, - current.1 - ); let (target_phase, abs_current) = match (current.0 != 0.0, current.1 != 0.0) { (false, false) => return, @@ -88,16 +81,9 @@ impl Motor { } /// Torque applied by the load [N·m] -- input port. - pub fn load(&mut self, torque: f64, context: &Context) { + pub fn load(&mut self, torque: f64) { assert!(torque >= 0.0); - println!( - "Model instance {} at time {}: setting load: {:.2}", - context.name(), - context.scheduler.time(), - torque - ); - self.torque = torque; } } @@ -141,13 +127,6 @@ impl Driver { /// 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}", - context.name(), - context.scheduler.time(), - pps - ); - let pps = pps.signum() * pps.abs().clamp(Self::MIN_PPS, Self::MAX_PPS); if pps == self.pps { return; @@ -172,12 +151,6 @@ impl Driver { _: (), context: &'a Context, ) -> impl Future + Send + 'a { - println!( - "Model instance {} at time {}: sending pulse", - context.name(), - context.scheduler.time() - ); - async move { let current_out = match self.next_phase { 0 => (self.current, 0.0),