Modes and state machines
Most controllers are modal: idle or running, charging or discharging, nominal or in a fault they
never leave. magscript declares the modes with an enum and runs one arm per mode with a match.
This guide builds the controller in examples/modes.
Name the modes
Section titled “Name the modes”An enum is a closed set of names, declared once in the file that owns it:
enum Mode { Idle, Running, Fault,}A variant such as Mode::Idle is a value. It can be:
- an input or an output;
- a parameter;
- a probe;
- a
const; - the initial value of a
fby.
An enum supports only == and !=.
Match on the mode
Section titled “Match on the mode”A match lists the targets every arm drives and runs one arm per variant:
model Ctl { k: f64, lim: f64, mode_ic: Mode,}
@probe(mode)step Ctl(cmd: bool, e: f64) -> (out: f64) { let mode: Mode = self.mode_ic fby next; match mode -> (u: f64, next: Mode) { Mode::Idle => { u = 0.0; next = if cmd { Mode::Running } else { Mode::Idle }; } Mode::Running => { let i: f64 = 0.0 fby (i + dt * e); u = self.k * i; next = if e > self.lim { Mode::Fault } else if not cmd { Mode::Idle } else { Mode::Running }; } Mode::Fault => { u = 0.0; next = Mode::Fault; } } out = u;}
params Ctl(k: f64 = 2.0, lim: f64 = 10.0, mode_ic: Mode = Mode::Idle) { Self { k, lim, mode_ic }}The model has three parts:
modeis a memory that starts atmode_icand takesnexton each tick.- The
matchdeclares the targetsuandnext, and each arm assigns both.nextis the transition, an ordinaryifover the inputs. out = uafter the match drives the output.
An arm is a scope. The fby in Running belongs to that arm and advances only while it runs.
Leave Running and return, and i resumes where it stopped instead of restarting at zero.
Run it
Section titled “Run it”$ magnet simulate --dt 0.1 --input data/commands.csvtick,t,out,probe.mode0,0,0,Idle1,0.1,0,Idle2,0.2,0,Running3,0.30000000000000004,0.2,Running4,0.4,0.4,Running5,0.5,0,Idle6,0.6000000000000001,0,Idle7,0.7000000000000001,0.6000000000000001,Running8,0.8,0,Fault9,0.9,0,FaultOn tick 7 the controller resumes at 0.6, one step past tick 4, because i held through the idle
ticks. Tick 8 feeds an error of 20.0, and the fault latches because no transition leaves Fault.
The initial mode is a parameter, so --param mode_ic=Fault starts the run latched.
What the checker enforces
Section titled “What the checker enforces”The checker reports each of these at the line that caused it:
- The scrutinee is an enum value and every variant has exactly one arm. A missing arm is named,
as in
`Mode` has no arm for `Mode::Fault`. - Every target is assigned exactly once in every arm.
- An output is never assigned inside an arm. Drive it from a target after the match.
- An arm’s
letis not visible outside the arm. - Equations inside an arm are ordered by dependency, and a target read in its own arm outside a
fbyis an algebraic loop. - An instance is stepped at most once on every path through the tick.
Modes across models
Section titled “Modes across models”A mode another model needs is an ordinary output. examples/bms has:
- a state machine that outputs its
BmsMode; - a charger that matches on it;
- a contactor that closes on
mode != BmsMode::Standby.
An enum in another file is reached with use crate::state_machine::BmsMode;. Two files that each
declare a Mode declare two types.
Generated code
Section titled “Generated code”The match becomes Rust’s match over an enum, with the targets declared before it:
#[repr(C)]#[derive(Clone, Copy, PartialEq, Eq)]pub enum Mode { Idle, Running, Fault,}
pub struct Ctl { mode: Mode, running_i: f64,}
fn step(&mut self, ctx: &Ctx, params: &Self::Parameters, (cmd, e): Self::Inputs) -> Self::Outputs { let mode: Mode = self.mode; let out: f64; let next: Mode; match mode { Mode::Idle => { out = 0.0; next = if cmd { Mode::Running } else { Mode::Idle }; } Mode::Running => { let i: f64 = self.running_i; out = params.k * i; next = if e > params.lim { Mode::Fault } else if !cmd { Mode::Idle } else { Mode::Running }; self.running_i = i + ctx.dt_secs_f64() * e; } Mode::Fault => { out = 0.0; next = Mode::Fault; } } self.mode = next; out}running_i is the arm’s i, written only at the end of its arm. The simulation harness prints and
reads a mode by its variant name, so a CSV says Running, not 1.
Not supported yet
Section titled “Not supported yet”Each of these is a named error today:
- A
_arm. Adding a variant should fail at every match that has not handled it. - A
matchinside an arm. - A
matchas an expression. - Variants with payloads.
- A rated instance stepped inside an arm.
- An
inlineinstance stepped in more than one arm. restarton a transition.- Same-tick transitions.
- Hierarchical machines.