Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

action def

This notebook introduces action def; after running it you can declare a named action with typed flows, a phenomena constraint, and nest it as a step of another action.

The Chapter 3 model expresses what the toaster must accomplish and evaluates a satisfaction claim against it, but says nothing about the steps by which the system does it. Chapter 4 adds the functional layer: named actions with typed flows and the relations that constrain them, composed into an actual decomposition of ToastBread from Chapter 1. This notebook adds ApplyHeat, an action def for the heating step, and nests it inside ToastBread as its first sub-action.

action def ApplyHeat {

action def ApplyHeat opens the new action. Its parameters follow: first the inputs, then the outputs.

    in bread    : Bread;
    in energy   : ISQ::EnergyValue[0..*];
    in duration : ISQ::DurationValue[0..*];

Three typed inputs: bread, the material Chapter 1 already defines; energy, the energy supplied to the action; and duration, a signal from a control function stating how long to apply heat. No control function is modeled in this chapter, so energy and duration are declared, typed, and left unconnected to any value; duration’s own doc states its denotation directly. Both carry an explicit [0..*] multiplicity, spelled out rather than left implicit; the next few cells build the rest of ApplyHeat before explaining why.

    out toast     : Toast;
    out delivered : ISQ::EnergyValue;
    out loss      : ISQ::EnergyValue;

Three typed outputs: toast, the material Chapter 1 already defines; delivered, the energy actually delivered to the bread; and loss, the energy that is not. delivered and loss together are the two quantities the balance constraint bounds against energy.

    assert constraint balance {
        delivered >= 0.0 [SI::J] and loss >= 0.0 [SI::J] and delivered + loss <= energy
    }

The constraint relates the three energy flows without choosing a mechanism: delivered energy and loss are each non-negative, and together they can never exceed the energy supplied. Asserting the constraint, rather than leaving it a plain constraint usage that SysML only allows to hold sometimes (7.20.1), is what makes that bound actually required of every usage of ApplyHeat, not merely checkable case by case. No specific efficiency is assumed and no conversion formula is stated; a chosen heating solution characterizes its own efficiency later, once one exists.

action def ApplyHeat {
    in bread    : Bread;
    in energy   : ISQ::EnergyValue[0..*];
    in duration : ISQ::DurationValue[0..*];
    out toast     : Toast;
    out delivered : ISQ::EnergyValue;
    out loss      : ISQ::EnergyValue;
    assert constraint balance {
        delivered >= 0.0 [SI::J] and loss >= 0.0 [SI::J] and delivered + loss <= energy
    }
}

ApplyHeat states typed flows and an asserted phenomena relation, but it is still a free-floating action, not a step of anything. The next fragment nests it inside ToastBread, the action Chapter 1 declared with no body, and binds applyHeat’s own bread input to ToastBread’s bread: the one flow actually available at this level. energy stays unbound; no energy source exists anywhere in the model yet.

    first start;
    then action applyHeat : ApplyHeat {
        in bread = ToastBread::bread;
    }
    then done;

ToastBread’s own doc and its bread/toast parameters are restated unchanged; the sequence is new, and it wires the one flow this level actually has. The full increment for this notebook is ApplyHeat’s declaration together with ToastBread’s reopened body.

With the increment loaded, draw its sequence directly from the model:

Loading...

The start → applyHeat → done sequence drawn directly from the loaded model: the same nesting the text above states, shown as a flow.

The reopened ToastBread and the new ApplyHeat load without diagnostics. Nesting ApplyHeat this way is what makes ToastBread an actual decomposition, and it is why energy and duration are written with an explicit [0..*] above rather than left bare. A bare, unstated multiplicity and writing [0..*] out explicitly should mean the same thing (SysML v2.0 formal/2026-03-02 7.6.3, 7.6.4: a keyword-less in/out parameter like these already defaults to [0..*]), but the OpenSysML runtime v0.9.0 treats them differently: left implicit, any attribute of a Toaster usage that reaches ApplyHeat through ToastBread fails to evaluate; written out, exactly the same model evaluates cleanly (DEFERRED.md D-026). Spelling out the multiplicity here states the model’s real, spec-default meaning honestly and keeps it fully evaluable, working around a real inconsistency in the OpenSysML runtime without claiming anything new about energy or duration.

The next cell checks what happens when a nested step names an action definition that does not exist.

Expected error: unresolved reference: UndefinedAction

With the negative control confirmed, the next cell looks up ApplyHeat and confirms it resolves as a nested step of ToastBread, with its balance constraint present as a named member, and confirms the model stays fully evaluable: slow’s cycleTime, unrelated to ApplyHeat, still evaluates to the 200-second value Chapter 2 gave it.

action kind: actionDef
nested step kind: actionUsage
balance constraint kind: constraintUsage
slow.cycleTime: 200 [SI::s]

action def ApplyHeat { ... assert constraint balance { ... } } and ToastBread’s reopened body, printed above, loaded without error, and model.find() resolves ApplyHeat as ToastBread’s own step and its balance constraint by name, shown by the kinds printed above. model.find(...).kind reports the general constraint kind, not the asserted subtype: the printed constraintUsage is not a contradiction of assert constraint, just a coarser label than the model’s own declaration. slow.cycleTime evaluating to 200 seconds confirms the explicit [0..*] above keeps the whole model evaluable, not just ApplyHeat itself.

Try the chapter exercise in exercises/ch04/exercise.ipynb: declare a new action def (not action def Brew — Chapter 1 already declares that at package level, so redeclaring it collides) for your coffee maker’s BrewUnit, with typed in/out flows and a first/then sequence nesting its usage inside Brew’s own reopened body, mirroring ApplyHeat and ToastBread.