1
0
forked from ROMEO/nexosim

Merge pull request #56 from asynchronics/document-utils

Document observable states
This commit is contained in:
Jauhien Piatlicki
2024-11-14 00:20:13 +01:00
committed by GitHub
6 changed files with 334 additions and 32 deletions

View File

@ -135,6 +135,16 @@ jobs:
env: env:
MIRIFLAGS: -Zmiri-strict-provenance -Zmiri-disable-isolation -Zmiri-num-cpus=4 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: lints:
name: Lints name: Lints
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@ -49,7 +49,8 @@ The [API] documentation is relatively exhaustive and includes a practical
overview which should provide all necessary information to get started. overview which should provide all necessary information to get started.
More fleshed out examples can also be found in the dedicated 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 [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 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 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. dual licensed as above, without any additional terms or conditions.

View File

@ -1,3 +1,5 @@
# Utilities for model-building # Utilities for model-building
TODO: add documentation This crate contains utilities used for model and simulation bench development.

View File

@ -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<ModeId> 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<ModeId>,
/// Calculated value output.
pub value: Output<u16>,
/// HK output.
pub hk: Output<Hk>,
/// Internal state.
state: ObservableState<State, ModeId>,
/// Accumulator.
acc: ObservableValue<u16>,
/// Electrical data.
elc: ObservableValue<Hk>,
}
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<Self>) {
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<Self>) -> InitializedModel<Self> {
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<ModeId>,
mode_ex: Option<ModeId>,
value: &mut EventBuffer<u16>,
value_ex: Option<u16>,
hk: &mut EventBuffer<Hk>,
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
}

View File

@ -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 std::ops::Deref;
use asynchronix::ports::Output; use asynchronix::ports::Output;

View File

@ -56,15 +56,8 @@ impl Motor {
/// For the sake of simplicity, we do as if the rotor rotates /// 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 /// instantaneously. If the current is too weak to overcome the load or when
/// attempting to move to an opposite phase, the position remains unchanged. /// attempting to move to an opposite phase, the position remains unchanged.
pub async fn current_in(&mut self, current: (f64, f64), context: &Context<Self>) { pub async fn current_in(&mut self, current: (f64, f64)) {
assert!(!current.0.is_nan() && !current.1.is_nan()); 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) { let (target_phase, abs_current) = match (current.0 != 0.0, current.1 != 0.0) {
(false, false) => return, (false, false) => return,
@ -88,16 +81,9 @@ impl Motor {
} }
/// Torque applied by the load [N·m] -- input port. /// Torque applied by the load [N·m] -- input port.
pub fn load(&mut self, torque: f64, context: &Context<Self>) { pub fn load(&mut self, torque: f64) {
assert!(torque >= 0.0); assert!(torque >= 0.0);
println!(
"Model instance {} at time {}: setting load: {:.2}",
context.name(),
context.scheduler.time(),
torque
);
self.torque = torque; self.torque = torque;
} }
} }
@ -141,13 +127,6 @@ impl Driver {
/// 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!(
"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); let pps = pps.signum() * pps.abs().clamp(Self::MIN_PPS, Self::MAX_PPS);
if pps == self.pps { if pps == self.pps {
return; return;
@ -172,12 +151,6 @@ impl Driver {
_: (), _: (),
context: &'a Context<Self>, context: &'a Context<Self>,
) -> impl Future<Output = ()> + Send + 'a { ) -> impl Future<Output = ()> + Send + 'a {
println!(
"Model instance {} at time {}: sending pulse",
context.name(),
context.scheduler.time()
);
async move { async move {
let current_out = match self.next_phase { let current_out = match self.next_phase {
0 => (self.current, 0.0), 0 => (self.current, 0.0),