Skip to content

Latest commit

 

History

History
656 lines (551 loc) · 26.1 KB

File metadata and controls

656 lines (551 loc) · 26.1 KB

5. Expressions, calculations, constraints and requirements

This chapter describes what the runtime evaluates and how each kind of check reports its result. A constraint declared on a definition is checked against the object that carries it, so instantiate the definition first if you want a verdict about a concrete value rather than a default. When several objects the session holds carry it — two %instantiates of one name leave the first object reachable as #<id>, and a multi-valued part holds one carrier per element — the check names them (car.wheels[1], car.wheels[2], #1.wheels[1], …) and asks you to pick one, with %eval in car.wheels[2] : ... or %eval in #1 : ....

Expressions

Literals:

attribute x = 42;              // Integer
attribute y = 3.14;            // Real
attribute flag = true;         // Boolean
attribute name = "System";     // String

Operators:

attribute sum = 10 + 5;        // Arithmetic
attribute product = 3 * 7;
attribute comparison = x > 10; // Relational
attribute logic = flag and true; // Boolean

Feature References:

part def Wheel {
    attribute diameter = 16.0;
}

part Vehicle {
    part wheel : Wheel;
    attribute wheelDiameter = wheel.diameter; // Feature chain
}

Composite structures

private import ScalarValues::*;

part def Engine {
    attribute power : Real default = 200.0;
}

part def Car {
    part engine : Engine {
        :>> power = 250.0;  // Redefine nested feature
    }
}

Instantiate and inspect:

sysml> %instantiate Car
✓ Created instance of Car
  ID: 1
  Use %features Car to inspect

sysml> %features Car
Instance: Car (ID: 1)
Features:
  engine = Instance(ID: 2)
    power = 250.0
    ownedPorts = []
…
  ownedPorts = []
…

(The stand for the library features every part carries, listed after the model's own; see your first model.) The nested engine is an object of its own. Reach it by a path from the object that holds it, or by the id it was given, and read a value from it the same way:

sysml> %features Car.engine
Instance: Car.engine (ID: 2)
Features:
  power = 250.0
…

sysml> %eval in #2 : power * 2
✓ power * 2 (on #2 ID: 2)
  = 500.0

An element of a multi-valued part is picked by index counted from 1, System.wheels[3]; see addressing an object.

Multiplicity

part System {
    part sensors : Sensor[0..10];  // 0 to 10 sensors
    part wheels : Wheel[4];         // Exactly 4 wheels
}

A multi-valued feature is unique unless it says nonunique: no two of its values may be equal, whatever its order. Repeats need the keyword.

package Readings {
    private import ScalarValues::*;
    attribute channels : Integer[*] ordered = (3, 1, 2);            // fine, order kept
    attribute levels : Integer[*] = (1, 1, 2);                      // error
    attribute samples : Real[*] nonunique = (0.5, 0.5, 0.7);        // fine, three values
}
readings.sysml:4:41: error: 1 (an Integer) is written at positions 1 and 2 of a unique feature

The check reports a repeat it can decide from the text; one that only appears when the model runs — a value reaching a feature through another feature, an assign, a calc's argument or result — is refused at that write with the same wording (uniqueness violation), and the feature keeps the value it had. Nothing is dropped silently: a (1, 1, 2) is not quietly read as (1, 2). A Collections::Set or Map is the exception, since throwing away repeats is what a set does.

Casts, the unbounded value and metadata

Casts: x as T selects rather than converts. It yields x where T classifies the value x is, and the empty sequence where it does not; a sequence is cast element by element, keeping the elements T classifies in their order. An object is kept by every classifier it is an instance of, its type's generalizations included.

A scalar is of the type it is written or computed in and of that type's supertypes, never of a narrower one by the number it happens to hold: 4 is an Integer, so also a Rational, a Real and a Number; 4.0 is a Rational and not an Integer, and so is 6 / 3, since dividing two integers yields a Rational. x istype T, x hastype T, x @ T, x as T and a feature's type all judge a value by that one rule, so 4.0 istype Integer is false, 4.0 as Integer is empty, and attribute whole : Integer = 4 / 2 is refused (by the checker, where the value is a quotient; at evaluation, where a whole Real reaches an Integer feature). hastype names the value's own type alone: 4 hastype Rational is false while 4 istype Rational is true.

sysml> package Payload {
  ...>     private import ScalarValues::*;
  ...>     part def Instrument;
  ...>     part def Camera :> Instrument;
  ...>     part navCam : Camera;
  ...>     part probe : Instrument;
  ...>     ref part cameras : Camera[0..*] = (navCam, probe) as Camera;
  ...>     attribute whole : Integer[0..*] = (1.0, 2, 3.0) as Integer;
  ...>     attribute reals : Real[0..*] = (1.0, 2, 3.0) as Real;
  ...> }
✓ package Payload

sysml> %eval Payload::whole
✓ Payload::whole
  = [2]

sysml> %eval Payload::reals
✓ Payload::reals
  = [1.0, 2, 3.0]

sysml> %eval Payload::cameras
✓ Payload::cameras
  = [Instance(ID: 1)]

sysml> %eval 2.5 as ScalarValues::Integer
✓ 2.5 as ScalarValues::Integer
  = []

Declare a feature that holds a cast result [0..1] or [0..*]: a cast that selects nothing is empty, which a feature of multiplicity [1] cannot hold. The checker warns where a cast can only be empty because the operand's type and the target are unrelated.

Converting a number: the conversions are library functions, not casts. RationalFunctions::ToInteger and RealFunctions::ToInteger truncate toward zero, so pair them with RealFunctions::round, floor or ceiling where rounding is meant; IntegerFunctions::ToNatural refuses a negative integer. Declare the feature Rational or Real instead where the quotient itself is the value wanted.

sysml> package Counts {
  ...>     private import ScalarValues::*;
  ...>     attribute quotient : Rational = 6 / 3;
  ...>     attribute converted : Integer = RationalFunctions::ToInteger(6 / 3);
  ...>     attribute rounded : Integer = RealFunctions::ToInteger(RealFunctions::round(7 / 2));
  ...>     attribute count : Natural = IntegerFunctions::ToNatural(RationalFunctions::ToInteger(6 / 3));
  ...> }
✓ package Counts

sysml> %eval Counts::quotient
✓ Counts::quotient
  = 2.0

sysml> %eval Counts::converted
✓ Counts::converted
  = 2

sysml> %eval Counts::rounded
✓ Counts::rounded
  = 4

A user-declared scalar type (attribute def Even :> Integer;) marks no evaluated value, so a bare 5 as Even cannot be decided by the value and is reported rather than answered empty; a value read from a feature declared Even is kept, the declaration being what states it is one. Natural and Positive are read the same way: their bounds refuse -1 and 0, and 7 as Natural on a bare integer is reported, while n as Natural on attribute n : Natural = 7 keeps 7.

The unbounded value: * is a value of its own, not a large number. It exceeds every finite number, equals itself and prints as *; arithmetic over it is refused with an error naming the operator.

sysml> package Budget {
  ...>     private import ScalarValues::*;
  ...>     attribute passLimit : Natural = *;
  ...>     attribute withinLimit : Boolean = 40 < passLimit;
  ...> }
✓ package Budget

sysml> %eval Budget::withinLimit
✓ Budget::withinLimit
  = true

sysml> %eval * + 1
error: evaluation failed: type mismatch: operator '+' is not defined for the unbounded value '*': * + 1

Metadata: elem.metadata is the sequence of metadata annotating elem, one object per annotation in the order written — an inline @ annotation and a metadata … about elem usage declared elsewhere take their places by source position, across files in document order — each carrying the values its body binds over the defaults its metadata def declares — followed by one reflective metaobject of the element's own metaclass (KerML §8.3.4.8.15), so the metadata of an element nothing annotates is that one metaobject, not the empty sequence. The library types the sequence as Metaobject, so cast an annotation to its metadata def before reading the values it binds; x meta T is the shorthand for x.metadata as T, and the metaclass's own features (name, qualifiedName, owner, isAbstract, …) read off the metaobject through ordinary member access.

sysml> package Provenance {
  ...>     private import ScalarValues::*;
  ...>     metadata def Heritage { attribute mission : String; attribute flown : Boolean default true; }
  ...>     part def Camera;
  ...>     part navCam : Camera { @Heritage { mission = "Cassini"; } }
  ...>     part sciCam : Camera;
  ...> }
✓ package Provenance

sysml> %eval (Provenance::navCam.metadata#(1) as Provenance::Heritage).mission
✓ (Provenance::navCam.metadata#(1) as Provenance::Heritage).mission
  = "Cassini"

sysml> %eval (Provenance::navCam.metadata#(1) as Provenance::Heritage).flown
✓ (Provenance::navCam.metadata#(1) as Provenance::Heritage).flown
  = true

sysml> %eval Provenance::sciCam.metadata
✓ Provenance::sciCam.metadata
  = [meta(Provenance::sciCam : SysML::Systems::PartUsage)]

sysml> %eval Provenance::sciCam.metadata#(1).name
✓ Provenance::sciCam.metadata#(1).name
  = "sciCam"

Extents: all T (KerML's extent operator) is the ordered sequence of the instances of T the run has — every object the definition classifies, nested usages included, in declaration order; for an enumeration, its literals; for a variation, the variants it declares. It is the run's extent, so a scalar or structured data type (all Integer, all Point) is refused, since a run creates no data values to enumerate, and a filter or metadata value built on all T is diagnosed rather than evaluated.

sysml> package Fleet {
  ...>     private import ScalarValues::*;
  ...>     part def Car { attribute seats : Integer; }
  ...>     part sedan : Car { :>> seats = 5; }
  ...>     part coupe : Car { :>> seats = 2; }
  ...>     enum def Color { red; green; blue; }
  ...> }
✓ package Fleet

sysml> %eval (all Fleet::Car).seats
✓ (all Fleet::Car).seats
  = [5, 2]

sysml> %eval all Fleet::Color
✓ all Fleet::Color
  = [Color::red, Color::green, Color::blue]

sysml> %eval all Integer
error: evaluation failed: unbounded extent: Integer is a data type, whose values are not enumerated (only an enumeration's literals are)

Quantities and units

A quantity is a magnitude with a unit, 3 [SI::km], and arithmetic over quantities composes units: a product multiplies them, a quotient divides them, a power raises them. The result is reported in the coherent unit the library declares for its dimension rather than the expression it was composed by, and a prefix on an input folds into the magnitude — 3 [km] / 2 [s] is 1500.0 [SI::'m/s'], not 1.5 [km/s]. The reduction reads the library's MeasurementReferences data and nothing else: a unit's unitPowerFactors, its unitConversion, and the prefixes of SIPrefixes, so a DerivedUnit a model declares reduces the same way as one of SI's.

sysml> package Orbit {
  ...>     private import SI::*;
  ...>     private import ISQ::*;
  ...>     private import MeasurementReferences::*;
  ...>     attribute def GravParam :> Quantities::ScalarQuantityValue;
  ...>     attribute 'm³⋅s⁻²' : DerivedUnit {
  ...>         private attribute m3 : UnitPowerFactor[1] { :>> unit = SI::m; :>> exponent = 3; }
  ...>         private attribute s_2 : UnitPowerFactor[1] { :>> unit = SI::s; :>> exponent = -2; }
  ...>         attribute :>> unitPowerFactors = (m3, s_2);
  ...>     }
  ...>     calc def velocity { in a :> ISQ::acceleration; in t :> ISQ::time; return v :> ISQ::speed = a * t; }
  ...>     calc def orbital { in mu : GravParam; in r :> ISQ::length; return v :> ISQ::speed = (mu / r)^(1/2); }
  ...>     calc def perMass { in f :> ISQ::force; in m :> ISQ::mass; return a :> ISQ::acceleration = f / m; }
  ...> }
✓ package Orbit

sysml> %calc Orbit::velocity(9.80665 [SI::'m⋅s⁻²'], 311 [SI::s])
✓ Orbit::velocity(9.80665 [SI::'m⋅s⁻²'], 311 [SI::s])
  = 3049.86815 [SI::'m/s']
  standing: value (observed: 1 run under reverse)

sysml> %calc Orbit::orbital(3.986E14 [Orbit::'m³⋅s⁻²'], 6563 [SI::km])
✓ Orbit::orbital(3.986E14 [Orbit::'m³⋅s⁻²'], 6563 [SI::km])
  = 7793.229127559948 [SI::'m/s']
  standing: value (observed: 1 run under reverse)

sysml> %calc Orbit::perMass(10 [SI::N], 2 [SI::kg])
✓ Orbit::perMass(10 [SI::N], 2 [SI::kg])
  = 5.0 [SI::'m⋅s⁻²']
  standing: value (observed: 1 run under reverse)

Every verdict a check produces — a %calc value, a %constraint or %requirement pass — ends with a standing: line naming the engine that answered and how strong the evidence is: here one execution under the default reverse scheduling policy (Analysis engines).

The unit chosen is the one the library declares for the dimension, SI::'m/s' over a synonym a model declares. Where the library declares several units of one dimension that measure different kinds of quantity — J for energy and N⋅m for torque — the type of the feature the value is bound to decides: attribute work : EnergyValue = 3 [N] * 2 [m] reads 6 [SI::J], attribute torque : TorqueValue = 3 [N] * 2 [m] reads 6 [SI::'N⋅m'], and the same product bound to no quantity kind reads over the base units, 6 [SI::'kg⋅m²⋅s⁻²']. A value that reduces to dimension one is a plain number (6 [km] / 3 [km] is 2.0); a product the library declares no unit for stays over its base units (2 [kg] * 3 [K] is 6 [K*kg]); one over a dimension-one unit such as rad keeps that unit, since the reduction would lose it; and a unit written as one name (3 [km], 400 [cm]) is kept as written. The magnitude changes only by the exact scale factor of the reduction — never by the choice of spelling.

Writing a quantity to a feature typed by a quantity kind checks its reduced dimension, not its spelling: 10 [N] / 2 [kg] is admitted to an AccelerationValue, and an L·T^-1 value written to one is refused as a type mismatch naming both dimensions. See Behavior for the same rule over assign. An argument is bound to the parameter it fills, and that binding is judged like an explicit bind: sum(robots.mass) handing MassValues to RealFunctions::sum, declared over Real, draws the validation warning Bound features should have conforming types at the argument, as the SysML v2 pilot reports it.

Sets and tensors

Sets: the library declares the elements of a Collections::Set unique and unordered, so a Set holds a set: the elements it was given with every repeat dropped and no order of its own. Two sets holding the same elements are equal however they were written, and a set prints as Set{…} in a canonical order. Bag and OrderedSet keep their repeats or their order, and are sequences.

sysml> package Bands {
  ...>     private import ScalarValues::*;
  ...>     private import Collections::*;
  ...>     private import CollectionFunctions::*;
  ...>     attribute requested : Set { :>> elements = ("X", "Ka", "X", "S"); }
  ...>     attribute licensed : Set { :>> elements = ("S", "X", "Ka"); }
  ...>     attribute same : Boolean = requested == licensed;
  ...> }
✓ package Bands

sysml> %instantiate Bands::requested
✓ Created instance of Bands::requested
  ID: 1
  Use %features Bands::requested to inspect

sysml> %features Bands::requested
Instance: Bands::requested (ID: 1)
Features:
  elements = Set{"Ka", "S", "X"}

sysml> %eval Bands::same
✓ Bands::same
  = true

sysml> %eval CollectionFunctions::size(Bands::requested)
✓ CollectionFunctions::size(Bands::requested)
  = 3

Tensors: a TensorQuantityValue of any rank is built by TensorCalculations::'[' from a flat sequence of numbers and a TensorMeasurementReference whose dimensions give the shape, in row-major order with the last index varying fastest. # takes one index per dimension, counted from 1, and refuses an index outside the shape or the wrong number of them; +, - and the scalar multiplications keep the shape component by component.

sysml> package Stress {
  ...>     private import ScalarValues::*;
  ...>     private import ISQ::*;
  ...>     private import SI::*;
  ...>     private import Quantities::*;
  ...>     private import MeasurementReferences::*;
  ...>     private import TensorCalculations::*;
  ...>     attribute ref3 : TensorMeasurementReference {
  ...>         :>> dimensions = (2, 2, 2);
  ...>         :>> mRefs = (Pa, Pa, Pa, Pa, Pa, Pa, Pa, Pa);
  ...>     }
  ...>     attribute field : TensorQuantityValue = '['((1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0), ref3);
  ...>     attribute cell = field#(2, 1, 1);
  ...>     attribute rank = field.order;
  ...> }
✓ package Stress

sysml> %eval Stress::field
✓ Stress::field
  = Tensor(2, 2, 2)[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0] [Pa]

sysml> %eval Stress::cell
✓ Stress::cell
  = 5.0 [Pa]

sysml> %eval Stress::rank
✓ Stress::rank
  = 3

Calculations, constraints and requirements

Calculations:

sysml> calc distance {
  ...>     in x;
  ...>     in y;
  ...>     x * x + y * y
  ...> }
✓ calc distance

sysml> %calc distance 3 4
✓ distance(3, 4)
  = 25
  standing: value (observed: 1 run under reverse)

Library functions:

The KerML function libraries (RealFunctions::sqrt, SequenceFunctions::size, NumericalFunctions::sum, …) are ordinary library packages, and an expression reaches one of their functions by the same rule the checker applies to every name: the qualified name resolves anywhere, and the bare name resolves only where the model imports the package that declares it. Evaluation follows the checker, so a call the checker reports as an unresolved reference does not evaluate either; the error names the qualified spellings the call may have meant, and importing one of those packages makes it resolve.

sysml> package Demo {
  ...>     attribute wheels : ScalarValues::Integer[*] = (1, 2, 3, 4);
  ...>     attribute wheelCount = wheels->size();
  ...> }
3:36: error: unresolved reference: size — did you mean SequenceFunctions::size or CollectionFunctions::size?
    attribute wheelCount = wheels->size();
                                   ^~~~

sysml> %eval Demo::wheelCount
error: evaluation failed: unresolved reference: size — did you mean SequenceFunctions::size or CollectionFunctions::size?

sysml> %eval SequenceFunctions::size(Demo::wheels)
✓ SequenceFunctions::size(Demo::wheels)
  = 4

sysml> package Demo {
  ...>     private import SequenceFunctions::*;
  ...>     attribute wheels : ScalarValues::Integer[*] = (1, 2, 3, 4);
  ...>     attribute wheelCount = wheels->size();
  ...> }
✓ package Demo
note: added to the existing package Demo, replacing attribute wheels, attribute wheelCount

sysml> %eval Demo::wheelCount
✓ Demo::wheelCount
  = 4

A calc the model declares under a library function's name is what a call resolves to, even where the library is also imported. %builtins lists every function the build evaluates, each with the package an import must name for its bare name to resolve.

Calculations as values:

A calc def, a calc usage or an in calc parameter named where a value is expected is a function value: the calculation, together with whatever it closes over. It is passed as an argument, held in a feature, compared with ==, and invoked by the parameter that receives it; reading it on its own answers the function, named by its declaration.

sysml> package Gains {
  ...>     private import ScalarValues::*;
  ...>     calc def Square { in v : Real; return : Real = v * v; }
  ...>     calc def Apply { in calc f { in v : Real; return : Real; } in a : Real; return : Real = f(a); }
  ...> }
✓ package Gains

sysml> %calc Gains::Apply(Gains::Square, 3.0)
✓ Gains::Apply(Gains::Square, 3.0)
  = 9.0
  standing: value (observed: 1 run under reverse)

sysml> %eval Gains::Square
✓ Gains::Square
  = Gains::Square

A calc def is a definition, not a feature, so it is passed as an argument or referenced through a calc usage rather than bound directly as a feature's value. A nested calc closes over the features around it, and SampledFunctions::Sample from the analysis library takes a function value and tabulates it over a domain.

Collection bodies:

collect, select and reduce (ControlFunctions) take a body whose parameter is bound to each element in turn. A collect is typed by what its body returns, not by the element type of the collection it ran over, so its result can be declared with the body's type and a mismatch is reported before anything runs. A body parameter that declares no type ({ in i; i.mass }) takes the type of the collection's elements, so a member it does not have is reported as an unresolved member at validation, not left for the run to discover.

sysml> package Rollup {
  ...>     private import ScalarValues::*;
  ...>     private import ISQ::*;
  ...>     private import SI::*;
  ...>     private import ControlFunctions::*;
  ...>     part def Instrument { attribute mass : MassValue; }
  ...>     part navCam : Instrument { :>> mass = 4.0 [kg]; }
  ...>     part spectrometer : Instrument { :>> mass = 12.0 [kg]; }
  ...>     attribute masses : MassValue[0..*] = (navCam, spectrometer)->collect { in i : Instrument; i.mass };
  ...>     attribute total : MassValue = masses->reduce { in a : MassValue; in b : MassValue; a + b };
  ...> }
✓ package Rollup

sysml> %eval Rollup::total
✓ Rollup::total
  = 16.0 [kg]

sysml> package Rollup {
  ...>     attribute names : String[0..*] = (navCam, spectrometer)->collect { in i : Instrument; i.mass };
  ...> }
1:35: error: cannot bind a value of type MassValue to a feature typed by String
	attribute names : String[0..*] = (navCam, spectrometer)->collect { in i : Instrument; i.mass };
                                  ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
note: added to the existing package Rollup (its other members are kept)

Constraints:

sysml> constraint ValidSpeed {
  ...>     65 > 0 and 65 <= 120
  ...> }
✓ constraint ValidSpeed

sysml> %constraint ValidSpeed
✓ Constraint ValidSpeed passed
  standing: holds (observed: 1 run under reverse)

Requirements:

sysml> requirement SafetyReq {
  ...>     assume constraint { 65 > 0 }
  ...>     require constraint { 100 > 50 }
  ...> }
✓ requirement SafetyReq

sysml> %requirement SafetyReq
✓ Requirement SafetyReq satisfied
  standing: holds (observed: 1 run under reverse)

An object as a whole:

%constraint and %requirement answer one named condition. %validate <object> answers every assertion about an object the session holds and the objects it holds in turn — each assert constraint the carrier's type declares or inherits, each requirement usage it carries, and each satisfy assertion whose subject is in the tree — one verdict per assertion per object, then one about the object itself. The object is an object reference: the name it was instantiated under, an id, or a path into what it holds.

sysml> package Fleet {
  ...>     part def Wheel {
  ...>         attribute pressure default = 32.0;
  ...>         assert constraint pressureOk { pressure >= 30.0 }
  ...>     }
  ...>     part def Car {
  ...>         attribute mass = 1500.0;
  ...>         part wheels : Wheel[2] {
  ...>             attribute :>> pressure = 20.0;
  ...>         }
  ...>         assert constraint massOk { mass < 2000.0 }
  ...>         requirement light { require constraint { mass < 1000.0 } }
  ...>     }
  ...>     part car : Car;
  ...> }
✓ package Fleet

sysml> %instantiate Fleet::car
✓ Created instance of Fleet::car
  ID: 1
  Use %features Fleet::car to inspect

sysml> %validate car
✓ assert constraint massOk holds (on Fleet::car ID: 1)
✗ requirement light fails (on Fleet::car ID: 1)
  Required condition evaluated to false: mass < 1000.0
✗ assert constraint pressureOk fails (on Fleet::car.wheels[1] ID: 2)
  Assertion evaluated to false: pressure >= 30.0
✗ assert constraint pressureOk fails (on Fleet::car.wheels[2] ID: 3)
  Assertion evaluated to false: pressure >= 30.0
✗ Fleet::car is not valid: 3 of 4 assertions fail
  standing: violated (witnessed: 1 run under reverse)

sysml> %validate car.wheels[1]
✗ assert constraint pressureOk fails (on Fleet::car.wheels[1] ID: 2)
  Assertion evaluated to false: pressure >= 30.0
✗ Fleet::car.wheels[1] is not valid: 1 of 1 assertion fails
  standing: violated (witnessed: 1 run under reverse)

The verdicts come root first, then each held object as the walk reaches it, and each names the object it is about by the path from the one validated, a collection element by its position (wheels[2], counted from 1). The object is valid only when every assertion holds and every object it holds was reached: an assertion that could not be evaluated — a feature no value reaches — is reported as undecided with the reason, not as false, and leaves the object undecided rather than valid; so does a walk cut short by an object graph that goes on without end. An object no assertion is about decides nothing — ? Demo::crate states no assertion to validate — and is not shown valid either. A constraint declared without assert is a definition to check by name, not an assertion about the object, and is not swept. The command line makes the same check with -validate=<object>, and a script with the ValidateInstance RPC (from Python or Go).

For more examples, see examples/repl-behavioral-demo.sysml, and the expressions demo for casts, *, .metadata, function values, sets, tensors and collection bodies worked through one model.


Next: 6. Behavior: actions and state machines.