Composites and generics
Two ways to say what a signal is beyond a number: a composite groups signals into a record, and a generic model leaves a signal’s type open and lets each instance decide it. Both reach the canvas, the CSV and the generated Rust without a second copy of anything.
Composite signals
Section titled “Composite signals”A composite declares a named record of signals, built whole and read by field, the way a Rust
struct is:
composite AttitudeState { rate: [f64; 3], angle: f64,}Fields are:
- primitives (
f64,f32,i32,bool); - fixed-size arrays;
- other composites.
Build and read
Section titled “Build and read”A literal states every field exactly once, in any order. A field read chains with indexing:
step Attitude(rate: [f64; 3], angle: f64) -> (state: AttitudeState) { state = AttitudeState { angle: angle, rate: rate };}step Consumer(state: AttitudeState) -> (spin: f64) { spin = state.rate[0] * state.angle;}A composite is a plain value, so fby can delay a whole record. It has no arithmetic, so
a + b on two composites is an error and the math happens on the fields.
At the boundary
Section titled “At the boundary”A composite may type a step’s inputs and outputs. At the CSV boundary it flattens to one column
per scalar leaf, such as state.rate[0], state.rate[1] and state.angle. @probe flattens a
composite the same way, as probe.state.rate[0].
Generated Rust gets one #[repr(C)] Copy struct per declaration, in the declaring module.
Across files
Section titled “Across files”A composite is an item, so it crosses files through use:
use crate::nav::State;reaches one in your package.use control_lib::nav::State;reaches one in a dependency.
Reading a field needs no import, because a value from a model call already carries its type.
As a parameter
Section titled “As a parameter”A model field may be a composite. It is stated as a literal in a constructor and read as
self.ic.rate:
model Holder { ic: State, d: UnitDelay<State>,}
params Holder(ic: State = State { rate: 1.5, angle: 2.5 }) { Self { ic, d: UnitDelay() }}A composite argument’s leaves are dotted as at the CSV boundary, so --param ic.rate=9.0
overrides one. UnitDelay() needs nothing stated: its ic defaults to ZERO, a record of zeros
at State.
On the canvas
Section titled “On the canvas”The declaration is text, carried as a read-only block. What you build from it is not:
- A literal is a pack block with one input per field, in declaration order.
- A field read is a field block.
Both write exactly the statement you would type. s.rate[0] and s.a.b remain text, because the
first indexes a field and the second is two reads.
Generic models
Section titled “Generic models”A model can leave a signal’s type open and let each instance decide it:
model UnitDelay<T = f64> { ic: T,}
step UnitDelay(u: T) -> (y: T) { y = self.ic fby u;}T is declared once on the model header, and the step and params items of the same name read
it. Inside the model, T stands for whatever the instance binds, whether a primitive, an array
or a composite.
An instance spells the argument, or leaves it to be inferred from what it is stepped with; a parameter no input determines takes its declared default:
model Plant { v: UnitDelay<[f64; 3]>, // delays a whole vector, whatever it is fed s: UnitDelay, // T is the type of what self.s(..) is fed; a bare literal leaves f64}Spelled arguments are never read from the call: v fed an f64 is a type error.
Every catalog block whose behaviour is per signal is declared this way. These all work on vectors and composites without a second copy:
- the delay;
- the switch;
- the integrator;
- the rate limiter.
Checked where used
Section titled “Checked where used”A generic body is a template with no bounds. It is checked once per instantiation the build
reaches, with T replaced. A body can be right at one argument and wrong at another, and the error
belongs to whoever chose the argument:
model Acc<T = f64> { ic: T }step Acc(u: T) -> (y: T) { y = self.ic fby (y + u); }
model Plant { a: Acc<bool> } // cannot instantiate `Acc<bool>`: // operator `+` requires numeric operands, found boolStating values with ZERO
Section titled “Stating values with ZERO”A literal spells one type. params UnitDelay(ic: T = 0.0) { Self { ic } } fits f64, but 0.0
does not type-check at [f64; 3]. At that instantiation the argument has no default, so
v: UnitDelay() is refused where it is written and the instance has to state its own value,
UnitDelay([0.0, 0.0, 0.0]).
ZERO is a reserved name for the zero of whatever type is expected. It is:
0.0at a float;0at ani32;falseat abool;- zeros throughout an array or composite.
A default written as ZERO is a default at every instantiation:
params UnitDelay(ic: T = ZERO) { Self { ic }}
params Plant() { Self { v: UnitDelay(), // [0.0, 0.0, 0.0] s: UnitDelay(), // 0.0 }}INFINITY and NEG_INFINITY work the same way for floats and float arrays, so a limit can
default to “unbounded” at every T:
params Integrator(ic: T = ZERO, lower_limit: T = NEG_INFINITY, upper_limit: T = INFINITY) { Self { ic, lower_limit, upper_limit }}This is why an initial condition is a parameter rather than a literal in the body. self.ic fby u
works at every T, where 0.0 fby u works only at a float.
The rules
Section titled “The rules”- Type parameters are declared on the
modelheader. Defaults come after every parameter without one. - A parameter may not share a name with a composite in scope, a Rust keyword, or a root name of
the generated crate (
Ctx,Float,Model,Vector). - An instance of a generic model is a module like any other, and
inlinesplices it as usual. - A generic step may be
extern. The crate behind it writes one impl per argument, as shown in Custom Rust blocks. - A generic body declares its state. A
fbyis the whole right-hand side of an output or an annotatedlet, and a probed local is an annotatedlet. - A generic model is not a simulation entry. Run a model that instantiates it.
Generated code
Section titled “Generated code”Each struct is spelled in the type parameters with the default carried into Rust, and there is one
impl Model per instantiation the build reached:
pub struct UnitDelayParameters<T = f64> { pub ic: T }pub struct UnitDelay<T = f64> { y: T }
impl Model for UnitDelay { … } // s: UnitDelayimpl Model for UnitDelay<Vector<f64, 3>> { … } // v: UnitDelay<[f64; 3]>Hand-written code that uses UnitDelay keeps compiling, and no trait bounds appear anywhere.