Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Running Your Model

The previous chapters taught you how to write a SysML v2 model: parts, actions, states, expressions, constraints, requirements, verification cases, metadata, and views. Everything so far has been structural – it describes what your coffee machine is. Even the state machine in Chapter 8 was a description, not an execution. Even the constraints in Chapter 9 were inert.

This chapter is about the other half. A SysML v2 model can also run. The same attribute waterTemp : Real that was a static description in Chapter 5 becomes a value that changes over time. The same state heating that was a static node in Chapter 8 becomes a state the machine actually enters and leaves. The same constraint that was a static obligation in Chapter 9 becomes a verdict that flips when a value moves.

This is not a separate language. There is no “simulation SysML” to learn. The constructs you already know describe the dynamic behaviour. What this chapter teaches is how to read your own model once it starts running, and the handful of authoring patterns that make a model run cleanly.

A note on scope. The concepts here are language: the run-as-occurrence framing, the state-space pattern, binding override, and the verdict stream are things any SysML v2 author should understand. The mechanics – which solver picks up your ODE, what the tool’s run surface looks like, how a trace is encoded, which commands to type – are sysml-rs’s runtime, and their canonical documentation is the sysml-rs portal: the runtime and CLI workflows. Blockquotes in this chapter mark the boundary.

16.A What It Means for a SysML v2 Model to Run

Start with the distinction that everything else depends on:

the model     a file (or workspace) of declarations. Has a tree shape,
              cross-references, types. Has no time and no current value.

a run of it   one trajectory through that model. Has a clock, a current
              value for every observable, a history, and a current state
              for every state machine.

The model is not the run. The model is what you wrote. A run is one execution of it. One model has many possible runs.

The language already has vocabulary for this, in KerML rather than SysML. A definition classifies; the thing classified is an occurrence, and clause 9.2.4.2.13 of the KerML specification puts it plainly: “An Occurrence is Anything that happens over time and space (the four physical dimensions). Occurrences can be portions of another Occurrence within time and space, including slices in time, leading to snapshots that take zero time.”

That sentence is the whole abstraction. A run is an occurrence. A tick is a snapshot of it. Asking “what is waterTemp?” of the model is a category error; you ask it of a snapshot.

Authors coming from static tools blur the two. They edit the model when they want to change one run; they talk about the answer when there are many. Hold the distinction and three later patterns become obvious: overriding a binding (§16.C) changes the run, not the model; verification (§16.D) checks a run; a Monte Carlo sweep is N runs over one model.

A run you can read

Here is a discrete run, end to end. The model is a three-state machine whose states write to shared attributes on entry – nothing in it is written for simulation; entry; then idle;, accept startGrind, and entry action are the Chapter 8 constructs:

state def GrinderController {
    doc /* Controls the burr grinder.
    * Writes: grindWeight, grinderRunning
    */

    entry; then idle;

    state idle {
        entry action { grinderRunning = 0; }
    }

    state grinding {
        entry action { grinderRunning = 1; grindWeight = 0; }
    }

    state dosed {
        entry action { grinderRunning = 0; grindWeight = 18.5; }
    }

    transition idle_to_grinding first idle accept startGrind then grinding;
    transition grinding_to_dosed first grinding accept doseReached then dosed;
    transition dosed_to_idle first dosed accept clearDose then idle;
}

Drive a run of it with the event sequence startGrind, doseReached, clearDose and read what comes back as a run rather than as a diagram. The run starts in idle – that is the entry; then idle; succession resolved. At each point, the set of available transitions is the set enabled from the state the run is currently in – not from the state definition, from the run. startGrind moves it to grinding, whose entry action sets grinderRunning to 1 and grindWeight to 0; doseReached moves it to dosed, where grindWeight becomes 18.5; clearDose returns it to idle. That pair of numbers – grindWeight was 0 at one tick and 18.5 at the next – exists nowhere in the model. It belongs to this run. The run is a reading of the Chapter 8 declarations.

Whatever tool you use, a run offers roughly four operations, and everything later in this chapter is built on them: step it forward one tick; run it until a horizon, a guard, or you stop it; reset it, returning time to t=0 and every observable to its declared initial value; and fork it, so two runs share a past and diverge from here.

sysml-rs extends the spec here. The specification defines the elements of a model and their semantics. It does not prescribe a run loop or a session abstraction, and it defines no command-line interface. Step, run, reset and fork are sysml-rs’s session API; another conformant tool will do these things under other names, or not run models at all. How to start, drive, and inspect a run in sysml-rs is documented on the portal: the runtime and CLI workflows.

16.B ODE-Bearing Models – The Language Story

The biggest reason a model has to run rather than just be analyzed is that it contains differential equations. Boiler water does not jump from 22 degrees to 92; it warms along a thermal curve. Conductor current does not appear instantaneously; it rises along an inductor curve.

SysML v2 has a standard-library answer, and it needs no metadata and no tool-specific annotation. The shape is:

part def <Thing> {
    attribute  <parameters>              // constants of the run
    out attribute <stateVar> default <x0>   // integrated over time

    action def <Dynamics> :> ContinuousStateSpaceDynamics {
        calc def <D> :> GetDerivative { return d<stateVar>dt = <rhs>; }
        calc def <O> :> GetOutput     { return <name>      = <expr>; }
        event occurrence <e> : ZeroCrossingEventDef;
    }
}

Here is that shape as a real, running file – a vessel heated at a constant rate past boiling:

private import StateSpaceRepresentation::*;

part def HeatedVessel {
    // Parameters
    attribute heaterRate : Real default 5.0;
    attribute boilingPoint : Real default 100.0;

    // ODE state variable
    out attribute temperature : Real default 20.0;

    action def HeatingDynamics :> ContinuousStateSpaceDynamics {
        // Derivative: dT/dt = heaterRate
        calc def TemperatureDerivative :> GetDerivative {
            return dTdt = heaterRate;
        }

        // Algebraic output: overshoot above boiling point
        calc def OvershootOutput :> GetOutput {
            return overshoot = temperature - boilingPoint;
        }

        event occurrence boilingReached : ZeroCrossingEventDef;
    }
}

Four things in that file carry the meaning, and each has a name:

  • An out attribute on the part is a state variable: a value the run integrates rather than one you assign. Its default is the initial condition.
  • A calc def specializing GetDerivative is a state equation. Its body is the right-hand side of dx/dt = f(x, u).
  • A calc def specializing GetOutput is an output equation: an algebraic function of the state, computed each tick, never integrated. y = g(x, u).
  • An action def specializing ContinuousStateSpaceDynamics is the envelope that holds them and marks the part’s behaviour as continuous.

All four come from StateSpaceRepresentation, a standard library package, and the specification devotes clause 9.4.4 to it. Its overview is worth quoting for one warning it contains: state-space representation describes “a set of state variables whose evolution by a state equation (note that this is a different conception of ‘state’ than used in the behavioral state modeling constructs described in 7.18)”. A state variable and a state machine state are different things. temperature is the former; heating is the latter. This chapter uses both, and they meet in the hybrid pattern below.

ZeroCrossingEventDef is also from that package. Clause 9.4.4.2 explains why it exists: solvers, “especially variable-step ones, need to identify such points for precise integration”, and an implementation “may notify zero-crossings with these event occurrences”. Declaring one says this run has a boundary the integrator should locate, not fire this callback.

Tool note: name your return variable after the state. sysml-rs matches a derivative to the state variable it integrates by name, so write return dwaterTempdt for waterTemp, and pick state names where none is a substring of another. The matching rule and its edge cases are documented on the portal under the runtime.

The coffee-machine boiler

Cast in the running example, with the constraints from Chapter 9 alongside:

package BoilerHybrid {
    private import ScalarValues::*;
    private import StateSpaceRepresentation::*;

    part def Boiler {
        attribute heaterPower : Real default 1500.0;
        attribute ambientTemp : Real default 20.0;
        attribute thermalMass : Real default 1500.0;
        attribute heatLossCoeff : Real default 4.5;
        attribute readyTemp : Real default 92.0;

        out attribute waterTemp : Real default 22.0;

        action def BoilerFieldDynamics :> ContinuousStateSpaceDynamics {
            calc def WaterTempDerivative :> GetDerivative {
                return dwaterTempdt =
                    (heaterPower - heatLossCoeff * (waterTemp - ambientTemp))
                    / thermalMass;
            }
            calc def HeadroomOutput :> GetOutput {
                return headroom = readyTemp - waterTemp;
            }
            event occurrence readyReached : ZeroCrossingEventDef;
        }
    }

    constraint def HeaterRated {
        heaterPower > 0.0 and heaterPower <= 2000.0
    }
    constraint def PositiveThermalMass {
        thermalMass > 0.0
    }
    constraint def TempInRange {
        waterTemp >= 0.0 and waterTemp <= 105.0
    }
}

You can read the derivative’s value at any state by hand, which is the cheapest way to sanity-check a right-hand side before running anything. Cold, at 22 degrees, (1500.0 - 4.5 * (22.0 - 20.0)) / 1500.0 is 0.994 – near full rate. At temperature, 92 degrees, (1500.0 - 4.5 * (92.0 - 20.0)) / 1500.0 is 0.784 – losses biting. The derivative falls as the water warms, which is what a first-order thermal lag should do. That check costs nothing and catches sign errors and misplaced parentheses, and any expression evaluator will do it – yours, or the one described in the portal’s CLI workflows.

When the ODE depends on a state machine

Real systems are hybrid: an ODE describes the continuous evolution, and a state machine selects which regime applies. A boiler is the textbook case. The state variable waterTemp is one variable; whether the heater is driving it is a state-machine question.

You compose the two by writing them next to each other and letting the guards read the state variable:

state def BoilerMode {
    in attribute waterTemp : Real;
    in attribute readyTemp : Real;
    in attribute standbyTemp : Real;

    state idle;
    state heating;

    entry; then heating;

    transition heating_to_idle
        first heating
        accept when waterTemp >= readyTemp
        then idle;

    transition idle_to_heating
        first idle
        accept when waterTemp <= standbyTemp
        then heating;
}

Two constructs are doing the work. The in attribute declarations are how the state machine names the continuous values it depends on; they are bound to the part’s state variables of the same name when the model is assembled. And accept when <expression> is a change trigger: the transition fires when the expression becomes true, rather than on a received event. A change trigger over a state variable is exactly the zero crossing that ZeroCrossingEventDef announces.

The same pattern scales. A full-size hybrid model – a reciprocating pump, say – gives its cycle state machine in attributes for each continuous value it needs (stroke, velocity, exposure), and every cycle transition is a located crossing of one of them:

transition compress_to_discharge
    first compress
    accept when velocity < 0.0
    then discharge;

Its dynamics envelope supplies one :> GetDerivative calc per state variable and a handful of :> GetOutput calcs for the shared algebraic chain. The distribution of labour is the general one: derivatives say how the state moves, outputs say what it means, guards say when the regime changes.

sysml-rs extends the spec here. Given a calc def :> GetDerivative inside an action def :> ContinuousStateSpaceDynamics, sysml-rs auto-detects the ODE and picks an integrator; when a change trigger mentions a state variable it locates the crossing with sub-tick precision rather than at the next discrete step. Neither is authored. Solver selection and how continuous runs are driven are documented on the portal under the runtime.

A note on what’s gone. Older sysml-rs material and example models declared ODEs with @ToolVariable { derivative = "..." } and @ToolExecution { toolName = "builtin:ode-rk45" }. All of that is removed. Those two metadata definitions are real – they are the whole content of the standard library’s AnalysisTooling package, and they mean “dispatch this action to an external tool” – but they are not how you write an ODE. If you find a model that still uses them for one, rewrite it with the state-space pattern above: AnalysisTooling makes a model tool-specific, StateSpaceRepresentation does not.

16.C One Model, Many Runs: Overriding a Binding

You have a boiler model. Someone asks: what if the water came in at 110 degrees, could that happen? You would like to answer without editing the file, because editing means committing to a value, re-checking everything, and telling everyone who depended on the old one.

What you want is to override a binding for one run:

same model + binding B1  ->  run 1  ->  answer 1
same model + binding B2  ->  run 2  ->  answer 2

The language framing is already familiar. Every calc def is an equation. Every in binding is an input substitution. Every :>> redefinition is a substitution you wrote into the source. What is new at run time is only that the substitution does not require editing the source.

With the boiler model from §16.B, a run at declared values satisfies all three constraints: HeaterRated, PositiveThermalMass, and TempInRange all pass. Re-evaluate the same file with the single binding waterTemp = 110.0 overridden, and TempInRangewaterTemp >= 0.0 and waterTemp <= 105.0 – fails while the other two still pass. Two answers, one model, nothing edited. That is the whole idea, and it scales in three directions you may already use:

  • What-if analysis overrides one binding and re-evaluates interactively.
  • Trade studies sweep a binding across a range and re-evaluate at each point.
  • Monte Carlo samples a binding from a distribution and re-evaluates across N draws.

All three are one override repeated. Think of every calc as an equation, every binding as an input, and every override as a substitution, and it does not matter whether the override comes from a UI knob, an API call, or a sweep script.

sysml-rs extends the spec here. The override mechanism – a flag on the constraint checker, an interactive equations workbench that lists the model’s equations and exposes their bindings as editable fields, and the sweep APIs – is tooling, and all of its entry points go through the same path, so they agree. See the portal’s runtime and CLI workflows pages.

16.D Sensing What Happened – Verdicts and Traces

The run finished. The boiler heated, the mode flipped, a constraint changed verdict. The question that matters is rarely “what was the value at tick 35” but “why”.

Verdicts as live observations

Chapter 9 introduced assert constraint, which produces a verdict at evaluation time. Chapter 10 aggregated verdicts into verification outcomes. Both treated a verdict as a discrete artifact: you evaluated, you got an answer.

In a run it is not discrete. A constraint over a state variable is re-decided as the run advances, so one constraint becomes a stream of verdicts, each carrying the operand values that were live when it fired. The TempInRange failure in §16.C is one sample from that stream: the sample where waterTemp was 110.0.

The verdict is four-valued, not two. The specification’s VerdictKind (clause 9.2.17.2.2) enumerates exactly pass, fail, inconclusive and error, and inconclusive is not failure: it means the constraint could not be decided. That happens whenever an operand has no value, which is common mid-authoring:

constraint def ReadyTempSane {
    readyTemp <= 100.0
}
constraint def PumpPressureOk {
    pumpPressure >= 9.0        // pumpPressure is not declared anywhere
}

Evaluate these and the honest reading is “one passed, none failed, one inconclusive”: ReadyTempSane passes, and PumpPressureOk cannot be decided because pumpPressure names nothing. A tool that scored the second as a failure would send you hunting for a physics bug that is really a missing declaration; one that scored it as a pass would be lying. When you consume verdicts programmatically, insist on the four-valued kind, not a boolean.

sysml-rs extends the spec here. That an asserted constraint yields a four-valued verdict is spec (Chapter 10). How often it is evaluated is not. sysml-rs re-evaluates every asserted constraint on every tick of a run, because constraints are cheap and a dense verdict stream makes a better trace; another tool might evaluate once at the end, or only when asked. The verdict wire format and the verification commands are on the portal under the runtime and CLI workflows.

From a verdict to a verification case

A verification case rolls the stream up into one verdict for a requirement. The idiom is the one from Chapter 10, and it reads the run’s values as free names:

part def Boiler {
    out attribute waterTemp : Real default 22.0;
}

requirement def ReachesBrewTemp {
    require constraint atTemperature {
        waterTemp >= 92.0
    }
}

verification def BrewTempCase {
    subject boiler;
    objective brewTempObjective {
        verify requirement brewTempCheck : ReachesBrewTemp;
    }
}

Run against the declared initial condition, the boiler has not heated yet, so BrewTempCase fails. Override the binding to waterTemp = 95.0, as in §16.C, and the same case passes. Note that waterTemp in atTemperature is a free name resolved from the run, not a feature of the requirement. That is what makes the same requirement reusable across runs – and it is why an un-run model reports the constraint as inconclusive (waterTemp has no value yet) rather than issuing a verdict. Inconclusive, again, is the right answer there.

Causal trace

Every value change in a run has a cause. A constraint flipped because a state variable crossed a threshold; the variable crossed because the integrator stepped a derivative; the derivative was non-zero because a transition switched the active regime; the transition fired because a change trigger became true. Those are causal links, and a runtime can record them and walk them backwards.

A useful cause is often a *non-*event. Guard a brew transition on a value another subsystem writes:

transition standby_to_brewing
    first standby
    accept startBrew
    if machineReady > 0
    then brewing;

Drive the brew controller with startBrew on its own and it does not move: machineReady is still 0, because nothing has run the boiler. “The brew did not start because machineReady was 0” is the answer you want, and it is only available because the guard names a shared attribute rather than testing an anonymous expression.

Which is the whole language-level lesson: the trace is only as good as the identifiers in your model. A trace that says “heating_to_idle fired because waterTemp reached readyTemp” is readable; “t_3 changed attr_17” is not, and no tooling can repair it. The cost of skipping a name is visible immediately. Write your constraints in the anonymous nested form, constraint { boilerTemp < 100 }, and the verdicts come back with nothing in the name column:

[PASS] : machineReady > 0
[FAIL] : waterPressure >= 8 and waterPressure <= 10

Ten constraints, ten blank labels, three failures you now have to locate by reading expressions. Name your transitions, name your constraints, name the return variable of every calc. That is not decoration.

Breakpoints

A breakpoint stops a run at an event so you can look at it. The events worth stopping on are the ones the language already names:

  • entering or leaving a state
  • a transition firing
  • an action being invoked
  • a constraint flipping verdict
  • a state variable crossing a threshold

You do not author breakpoints inside the model; they are decorations on a run, set from outside, and another tool reading the same model will not see them. So the authoring work that makes breakpoints useful is the work that makes traces useful: clear state names, named transitions, named constraints.

sysml-rs extends the spec here. The causal recorder, its query shape, and the breakpoint variants are runtime features, not specification. They are documented on the portal under the runtime.

How These Patterns Compose

The four sections stand alone but interlock:

  • A model with an ODE (§16.B) needs a run (§16.A) to evolve at all.
  • A run can be re-asked with a different binding (§16.C), which is how you get from one trajectory to a trade study.
  • A run emits a verdict stream (§16.D), and a causal trace walks back from any verdict to its cause.
  • A verification case (Chapter 10) rolls the stream into one answer about a requirement.

The boiler exercises all four. WaterTempDerivative evolves waterTemp; BoilerMode selects whether the heater is driving it; overriding heaterPower answers “what if the element were bigger”; TempInRange says whether the water stayed safe; BrewTempCase says whether the requirement held. You did not learn a new language for any of it – you wrote out attribute, state, calc def ... :> GetDerivative, accept when, and constraint def. The same declarations that were a static description in earlier chapters are a running system here. The runtime only reads them.

Where the Mechanics Live

sysml-rs tooling – documented on the portal. Everything behind the blockquotes in this chapter – sessions and the run loop, solver auto-selection, binding overrides and sweeps, the verdict wire format, causal-trace queries, breakpoints, and the command surface that drives them – is product documentation, and its canonical home is the sysml-rs portal: the runtime and CLI workflows.

If you are writing a model you intend to run, you should not need to reach for those. Occurrence and snapshot, state variable and state, derivative and output, binding and override, verdict stream and trace – that vocabulary is enough.