forked from ROMEO/nexosim
Merge pull request #56 from asynchronics/document-utils
Document observable states
This commit is contained in:
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@ -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
|
||||||
|
@ -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.
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
|
||||||
|
310
asynchronix-util/examples/observables.rs
Normal file
310
asynchronix-util/examples/observables.rs
Normal 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
|
||||||
|
}
|
@ -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;
|
||||||
|
@ -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),
|
||||||
|
Reference in New Issue
Block a user