Skip to content

Latest commit

 

History

History
2184 lines (1902 loc) · 114 KB

File metadata and controls

2184 lines (1902 loc) · 114 KB

6. Behavior: actions and state machines

Actions and state machines are executed, not just parsed. A debugger steps through them, and the non-interactive -action and -state flags run them to completion and report the values they produce. A behavior can be performed by an object, in which case the messages it sends are routed over that object's connections.

Action execution (step-by-step):

sysml> action SimpleWorkflow {
  ...>     attribute result = 0;
  ...>     first start;
  ...>     then action compute { assign result := 42; }
  ...>     then done;
  ...> }
✓ action SimpleWorkflow

sysml> %action SimpleWorkflow
✓ Started action executor for "SimpleWorkflow"
  State: Running
  Tokens: 1

Use %step to advance, %tokens to inspect, %continue to run to completion

sysml> %step
✓ Step complete
  State: Running
  Tokens: 1

sysml> %tokens
Active tokens (1):
  Token 1 @ compute
  Values:
    result = 0

sysml> %continue
✓ Action completed
  Final state: Completed
  Results:
    result = 42

State machine execution:

A machine completes when a transition reaches done, the terminal state the standard library provides for every state machine. Entering it runs the exit actions, and then the machine reports itself completed. With orthogonal regions, each region has its own done, and the machine completes only once every region has reached it. A done written inside a composite state's body ends that state, not the machine: the composite completes, and its transitions with no trigger (transition first outer then next;) fire, exactly as a plain state's do when its do behavior ends. A completed composite with no such transition stays active, and the machine runs on until its own top-level regions reach done. Only the Kernel Semantic Library's run-to-completion defaults are implemented: a machine that redefines isRunToCompletion or runToCompletionScope away from them is refused when it is lowered, naming the feature and the value written, rather than run as if the default held.

sysml> state TrafficLight {
  ...>     entry; then idle;
  ...>     state idle;
  ...>     state green;
  ...>     accept after 25 [SI::s] then yellow;
  ...>     state yellow;
  ...>     accept after 5 [SI::s] then red;
  ...>     state red;
  ...>     accept after 30 [SI::s] then done;
  ...>     succession first idle then green;
  ...> }
✓ state TrafficLight

(The first state is not named start: every state inherits start and done from the library's StateAction, so declaring a state of that name is reported as a duplicate of the inherited member.)

A transition written without transition … first, as the three accept after … then … lines above are, leaves the state declared right before it in the same body (SysML v2 §7.18.3): accept after 25 [SI::s] then yellow; leaves green because state green; precedes it. Several such transitions in a row all leave the same state, and the shorthand takes the same triggers (accept Signal, accept after, accept at, accept when), guards (if …) and effects (do …) as the full form. It has to follow the state it leaves directly, so write it in the body that declares that state, not inside the state's own body; written first in a body, or after a member that is not a state, it is reported.

sysml> %state TrafficLight
✓ Started state machine executor for "TrafficLight"
  Current state: idle
  Time: 0.0
  Events: 1

Use %events to see queue, %current for state, %advance <time> to step

sysml> %advance 25
✓ Advanced to 25.0 (2 event(s) processed)
  Current state: yellow
  Last event at: 25.0
  Remaining events: 1
  Waiting on the clock:
    t=30.0: state machine TrafficLight, time -> red

sysml> %current
Current state: yellow
Time: 25.0
Last event at: 25.0
Execution state: Suspended

sysml> %advance 5
✓ Advanced to 30.0 (1 event(s) processed)
  Current state: red
  Last event at: 30.0
  Remaining events: 1
  Waiting on the clock:
    t=60.0: state machine TrafficLight, time -> done

sysml> %advance 30
✓ Advanced to 60.0 (1 event(s) processed)
  Current state: done
  Last event at: 60.0
  Remaining events: 0

✓ State machine completed (a transition reached `done`)

Choosing the starting state. Written right after the body's entry action, the shorthand is an entry transition instead: it names the state the body starts in. entry; then idle; above always starts in idle; with a guard, entry; if cold then heating; if not cold then idle;, the alternatives are tried in the order written each time the body is entered — when the machine starts, and again whenever a transition enters the composite state whose body it is — and the first whose guard holds is entered. An unguarded then s; among them is the alternative taken when it is reached. The entry action itself runs first, so a guard reads what it assigned. When alternatives are written and no guard holds, the machine has nowhere to start and reports it as an error (no entry transition holds). An entry transition chooses by its guard alone: one written with a trigger or an effect, or one reaching something other than a state, is reported. A state usage typed by a definition (or a definition specializing another) that writes entry transitions of its own starts by those alone, the inherited ones being replaced just as its own entry behavior replaces the inherited one; a usage writing none starts where its definition says.

state def Heater {
    attribute cold : Boolean = true;
    entry;
    if cold then heating;
    if not cold then idle;
    state heating;
    state idle;
}

Sending a signal. A transition that waits on an accept is driven from the prompt with %send, which puts the signal on the runtime's message bus exactly as a send from an action would, so nothing has to be written in the model just to fire it:

sysml> package Lamps {
  ...>     private import ScalarValues::*;
  ...>     attribute def go;
  ...>     attribute def Dim { attribute level : Integer; }
  ...>     state def Lamp {
  ...>         attribute brightness : Integer = 0;
  ...>         entry; then off;
  ...>         state off;
  ...>         transition off_on first off accept go then on;
  ...>         state on;
  ...>         transition on_dim first on accept d : Dim do assign brightness := d.level then dimmed;
  ...>         state dimmed;
  ...>     }
  ...>     part def Bulb { exhibit state lamp : Lamp; }
  ...>     part bulb : Bulb;
  ...> }
✓ package Lamps

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

sysml> %state bulb
✓ Debugging state machine "lamp" exhibited by object #1 of "Lamps::bulb"
  Current state: off
  Time: 0.0
  Events: 0

Use %events to see queue, %current for state, %advance <time> to step

sysml> %send go
✓ Sent go to object #1 of "Lamps::bulb"
  Accepted by state machine "Lamp" in state off: transition off_on fires on it

Use %step or %advance <time> to dispatch it

sysml> %events
Signals in flight: 1
  go
Use %advance <time> to process next event

sysml> %advance 1
✓ Advanced to 1.0 (1 event(s) processed)
  Current state: on
  Last event at: 0.0
  Remaining events: 0

sysml> %send Dim(level=3+4)
✓ Sent Dim(level=7) to object #1 of "Lamps::bulb"
  Accepted by state machine "Lamp" in state on: transition on_dim fires on it

Use %step or %advance <time> to dispatch it

sysml> %step
✓ Event dispatched
  Current state: dimmed
  Time: 1.0
  Events: 0

sysml> %send go
error: object #1 of "Lamps::bulb" accepts no signal go now: state machine "Lamp" in state dimmed

Without to <object>, the signal goes to the object whose machine the %state session is debugging, or failing that to the object an %action <name> <object> session performs its action on behalf of (%send go to bulb names it explicitly, and is the form to use when no session is active, or the %action session performs on behalf of no object; the object is any object reference, to #1 or to rack.lamp included). Payload features are written <parameter>=<expression> as for %invoke, and are checked against the signal's declaration: %send Dim(lvl=1) is refused because Dim carries no lvl. A signal nothing in the machine's current state accepts is refused up front, with the state named, rather than queued to be silently dropped — and so is one whose every triggered transition is held back by its guard, decided as the dispatch would decide it, with the payload bound: with transition on_dim first on accept d : Dim if d.level > 0 ..., %send Dim(level=0) is refused while %send Dim(level=3) is in flight. A guard that cannot be evaluated is a %send error. If the state or the data a guard reads changes between the send and the dispatch, the %step or %advance that drops the signal says so.

One clock. Simulation time belongs to the runtime the session executes in, not to any one behavior: every action and state machine the session runs — the ones %action and %state debug, the ones instantiated objects exhibit, the ones a body performs — reads and waits on the same clock, which starts at 0.0 and counts in SI::s. An action body waits on it as a transition does: accept after 5 [SI::s] parks the token until the clock has moved five seconds past the moment the accept was reached, and accept at t (a Time::TimeInstantValue) until the clock reads t — an instant already passed is due at once. A duration or instant that is not a time (accept after 5 [SI::m]) is refused when the accept is reached, as it is in a transition. %step does not move the clock, so a token waiting only on time is reported as such, with what would move it; %advance <time> moves the clock of the session's runtime by <time> seconds, and everything due along the way runs, whichever debugger it belongs to:

sysml> package Timed {
  ...>     item def Ping;
  ...>     action pinger {
  ...>         attribute count = 0;
  ...>         first start;
  ...>         then action wait accept after 5 [SI::s];
  ...>         then action tick assign count := count + 1;
  ...>         then send new Ping() to listener;
  ...>         then done;
  ...>     }
  ...>     state listener {
  ...>         entry; then idle;
  ...>         state idle;
  ...>         transition idle_pinged first idle accept Ping then pinged;
  ...>         state pinged;
  ...>     }
  ...> }
✓ package Timed

sysml> %action Timed::pinger
✓ Started action executor for "Timed::pinger"
  State: Running
  Tokens: 1

Use %step to advance, %tokens to inspect, %continue to run to completion

sysml> %step
✓ Step complete
  State: Running
  Tokens: 1

sysml> %step
Nothing to step: the action waits on the clock, which %step does not move
  Token 1: accept after waiting since step 2 for the clock to reach t=5.0
  Use %advance 5.0 to move the clock from t=0.0 to the earliest wait

sysml> %state Timed::listener
✓ Started state machine executor for "Timed::listener"
  Current state: idle
  Time: 0.0
  Events: 0

Use %events to see queue, %current for state, %advance <time> to step

sysml> %advance 2
✓ Advanced to 2.0 (0 event(s) processed)
  Current state: idle
  Last event at: 0.0
  Remaining events: 0
  Action state: Waiting
  Tokens: 1
  Waiting on the clock:
    t=5.0: action pinger, accept after waiting since step 2 for the clock to reach t=5.0

sysml> %advance 3
✓ Advanced to 5.0 (1 event(s) processed)
  Current state: pinged
  Last event at: 5.0
  Remaining events: 0
  Action state: Completed
  Tokens: 0
  Action steps taken: 4

✓ Action completed
  Results:
    count = 1

An advance runs what comes due in the order it comes due; at one instant, each executor runs its own work in the order it always has (a machine dispatches its due event and runs its do behavior, an action moves its tokens), and which executor goes first when several are due at the same instant is a choice point (below). Once the definite work at an instant has settled, the change conditions state machines and action tokens watch (accept when) are polled, so a condition another executor has just made true fires at that instant — also for the executor driving the clock itself, and whatever timer it has waiting beside the condition. Which watcher polls first is the same choice point, as what one does on its condition is what the next sees. The advance stops early, saying so and how to raise the bound, when it exhausts the event, do-step or step budget (environment); a wait due after the deadline stays queued and is listed under Waiting on the clock; an advance with nothing waiting just moves the clock. %continue runs an action to completion on its own, moving the clock to each of its waits as it reaches them, and moving with it every other behavior of the same runtime that comes due.

Reading the clock. A body reads the clock through the standard library's own form: every occurrence has a localClock (Occurrences::Occurrence::localClock, the Clocks::universalClock unless the model binds another), and the clock's currentTime is the instant the runtime's clock stands at — so assign started := localClock.currentTime; stamps a Real attribute with the simulation time in seconds, and assign elapsed := localClock.currentTime - started; measures the time a stretch of the flow took. this.localClock.currentTime and part.localClock.currentTime read the same clock through another object; a part holding no object has no clock to read, and the read is empty. The attribute is one -observe/%runs table beside clock — the total of a run — so a workflow that times one of its stretches reports it per run. The clock is read only: an assignment to currentTime is refused with a clock's currentTime advances with the run and is not assigned, since %advance and the waits move it.

What a state's behaviors may do. A state's entry, do and exit behaviors are actions, and their bodies may hold whatever an action body holds: a flow of nodes joined by successions (first start; then … or, with one node no succession leads to, the flow starts there), forks, joins and decisions, timed and signal accepts, sends, nested action nodes with flows of their own, and typed usages with pin bindings (do action poll : Poll { inout n = ticks; }). A body stating no flow still runs its statements in declaration order. A braced block without the keyword — entry { … }, do { … }, exit { … }, and a transition's do { … } — is one anonymous action with that body, the same as entry action { … }: an attribute declared inside the block is local to it and shadows the state's, and a terminate; in it ends the whole block (below). The three behaviors differ in when they run: entry and exit are performed whole at the instant the state is entered or left (as is a transition's do effect), so a body of theirs that waits on the clock is refused with state behavior waits for the clock; the do behavior runs while the state is active, one statement per round — each statement of a for or while iteration and of a nested block or branch its own, one step of a flow the body states, each of its tokens one node — and may wait. An accept after in a do body parks it on the shared clock and %advance moves it; an accept Sig parks it until a matching signal is sent — %send Sig takes it though no transition fires on it, reporting that the do behavior goes on. A do behavior performs once — when its body ends, the state has completed and a completion transition out of it, if any, fires — and leaving the state for any other reason abandons what is left of it: the statements after the one it last ran do not run, its waits leave the clock, and an inout pin writes its value back to the bound attribute only when the performance ends (an inout pin valued by an enumeration literal or another constant, inout mode = Mode::idle, starts from that value and writes back nowhere). Poll below counts once at t=3.0, the state is left at t=10.0, and ticks reads 1:

sysml> package Watch {
  ...>     private import ScalarValues::*;
  ...>     action def Poll {
  ...>         inout n : Integer;
  ...>         action wait accept after 3 [SI::s];
  ...>         then assign n := n + 1;
  ...>     }
  ...>     part def Watcher {
  ...>         attribute ticks : Integer = 0;
  ...>         exhibit state m {
  ...>             entry; then watching;
  ...>             state watching { do action poll : Poll { inout n = ticks; } }
  ...>             transition first watching accept after 10 [SI::s] then finished;
  ...>             state finished;
  ...>         }
  ...>     }
  ...>     part w : Watcher;
  ...> }
✓ package Watch

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

sysml> %state Watch::w
✓ Debugging state machine "m" exhibited by object #1 of "Watch::w"
  Current state: watching
  Time: 0.0
  Events: 1

Use %events to see queue, %current for state, %advance <time> to step

sysml> %advance 20
✓ Advanced to 20.0 (1 event(s) processed)
  Current state: finished
  Last event at: 10.0
  Remaining events: 0
  Do behavior actions run: 1

sysml> %features Watch::w
Instance: Watch::w (ID: 1)
Features:
  ticks = 1
…
Behaviors:
  m: exhibited state machine, current state finished
…

(The stand for the library-declared features of the part and of the machine — subparts, isSolid, transitions, … — which %features lists after the model's own; see your first model.)

Had the poll waited 30 [SI::s] instead, the exit at t=10.0 would have cancelled it and ticks would still read 0. When the states of two orthogonal regions both have a do action due at one instant, which acts first in the round is a choice point (choice do round at t=2.0: states left, right react), explored like any other (below). A do body that binds an in pin to nothing, or to a feature the state does not declare, is refused when the behavior starts, naming the pin.

Ending a state machine with terminate. A transition whose target is a terminate action usage — transition first watching accept Abort then stop; action stop terminate;, the spelling §7.18.3 gives "to immediately terminate the containing state performance" — ends the machine's performance where the transition arrives, short of any final state. The transition itself runs as any transition does: its source is exited (the source's exit behavior runs, since that state is left) and its do effect runs, and a terminate usage declared inside a composite state entered on the way runs that composite's entry behavior. Then nothing else happens: no other state is exited, no other exit behavior runs, every do behavior still under way — the sibling region's, the enclosing composite's, the machine's own — is abandoned where it stands, and no state is active. A choice, junction or join whose way on leads into the usage ends the machine the same way once the compound transition completes; a fork's branches enter states, so one leading into a terminate usage is refused. The debugger reports it apart from completion, and %current shows no state and the data as it stood:

sysml> package Guard {
  ...>     private import ScalarValues::*;
  ...>     attribute def Abort;
  ...>     state def Sentry {
  ...>         attribute log : String = "";
  ...>         entry; then watching;
  ...>         state watching {
  ...>             do { assign log := log + "watch;"; accept after 5 [SI::s]; assign log := log + "watched;"; }
  ...>             exit { assign log := log + "watching(exit);"; }
  ...>         }
  ...>         transition first watching accept Abort do assign log := log + "abort;" then stop;
  ...>         action stop terminate;
  ...>     }
  ...> }
✓ package Guard

sysml> %state Guard::Sentry
✓ Started state machine executor for "Guard::Sentry"
  Current state: watching
  Time: 0.0
  Events: 0

sysml> %send Abort
✓ Sent Abort to state machine "Guard::Sentry"
  Accepted by state machine "Sentry" in state watching: transition watching -> stop fires on it

sysml> %advance 1
✓ Advanced to 1.0 (1 event(s) processed)
  Current state: <none>
  Last event at: 0.0
  Remaining events: 0
  Do behavior actions run: 1

✓ State machine terminated (a `terminate` ended its performance short of a final state; no state is active)

sysml> %current
Current state: <none>
Time: 1.0
Last event at: 0.0
Execution state: Terminated

State data:
  log = "watch;watching(exit);abort;"

The do behavior's watched; never ran: its wait was abandoned with the machine. From the command line (sysml -state Guard::Sentry -advance 1 …) the run ends with the same State machine terminated line and reports state as <none>, and an exploration or a check sees the run as terminated (Outcome.Terminated, its final state empty) rather than as having reached done, so a model that can end either way has two outcomes. %trace records the transition into the usage, terminate stop, and each do behavior abandoned. A terminate written inside a state's entry, do or exit body is something else — it ends that behavior only, the whole braced block where the body is one (do { assign d := 1; terminate; assign d := 9; } leaves d at 1) (below).

Action debugging commands:

  • %action <name> [<object>] — Start an action debugging session, optionally performed by an instantiated object
  • %step — Advance all tokens one step; a token waiting only on the clock is reported with the %advance that would move it
  • %continue — Run to completion, or to the first breakpoint hit
  • %tokens — Show active tokens with data
  • %break <node> — Set breakpoint on a named node, one an if branch or a loop body declares included; %continue and %step stop when a token reaches it, or before a body performs it
  • %stop — Stop debugging

State machine debugging commands:

  • %state <name> [<object>] — Start a state machine debugging session; naming an instantiated object runs the machine on behalf of that object, so what it sends routes over that object's connections. Naming the machine the object exhibits attaches to its running machine instead (see below)
  • %send <signal>[(<p>=<expr>, ...)] [to <object>] — Send a signal to an object over the runtime's message bus, for a machine it exhibits or an action it performs to take; by default to the object being debugged
  • %events — Show event queue and signals in flight
  • %current — Show current state, stack, data
  • %advance <time> — Advance the runtime's simulation clock by <time> seconds, running every state event, action token, change-condition poll and do behavior due along the way, in every debugging session of the runtime
  • %stop — Stop debugging

For complete workflows, see examples/action-executor-demo.sysml, examples/orthogonal-regions-demo.sysml and examples/pseudostates-demo.sysml.

When a model has more than one valid run

A behavior is a set of performances under a partial order, not a program with one next instruction. The KerML Kernel Semantic Library orders three things and nothing else:

  • Successions. succession first a then b is a HappensBefore link: a completes before b begins. A fork's branches all follow the fork; a join follows every branch into it.
  • Send before accept. A message is accepted after it was sent, so an accept that waits for a send in another branch follows that send.
  • Ancestor priority. When a substate's transition and its enclosing state's are both enabled by one event, the innermost fires — SysML v2/KerML order, not a pick. A deferred event (the defer <event>; extension) is ordered the same way: while a state that defers it is active, the event reaches only a transition whose source is that state or one nested in it; a transition in an enclosing state or a sibling region waits until the deferring state is exited, and the event is then dispatched, ahead of later arrivals.

Everything else two performances could do in either order, they may: which of two fork branches steps first (token interleaving), which of two holding guards a decision follows (overlapping guards), which of two transitions out of one state fires on one event (competing transitions), which of two orthogonal regions reacts first to an event both accept (region order), which of two holding guards a choice pseudostate follows — its guards are read on arrival, after the transition into it has run its effect (choice branch) —, whose value stands when two branches assign one feature in one step (same-step writes), and which of two executors due at one instant of the shared clock — an action token and a state transition, two state machines, two actions — runs first (due order). A model with any of these has several valid runs, and a run that took one of them is not wrong for it — but a tool that showed only that run, and called its result the outcome, would be. The rest of this section is how the executor keeps that honest: it reports every such pick as a choice point, lets you take another one (seed:<n>), lets you see them all (explore), and lets a test state the whole set of outcomes it admits.

The examples below are one fixture from the conformance suite, three branches writing one feature between a fork and a join, action_explore_three_writers.sysml:

package test {
	private import ScalarValues::*;

	action race {
		attribute x : Integer = 0;
		attribute aRan : Boolean = false;
		attribute bRan : Boolean = false;
		attribute cRan : Boolean = false;

		first start;
		fork split;
		action a { assign x := 1; assign aRan := true; }
		action b { assign x := 2; assign bRan := true; }
		action c { assign x := 3; assign cRan := true; }
		join sync;
		done;

		succession first start then split;
		succession first split then a;
		succession first split then b;
		succession first split then c;
		succession first a then sync;
		succession first b then sync;
		succession first c then sync;
		succession first sync then done;
	}
}

The library fixes that split precedes each branch, that sync follows all three, and that each branch runs once; it does not fix the order of the three writes to x. Six orders, three values, all valid.

Reading a choice point

Where the executor has to pick, it follows one fixed rule — reverse token order, first holding guard, first declared transition, the executor started last first, so a run replays exactly — and records the pick rather than passing it off as the only outcome. A plain run ends with a count:

$ sysml -action test::race action_explore_three_writers.sysml
✓ package test
✓ Started action executor for "test::race"
  State: Running
  Tokens: 1
✓ Action completed
  Final state: Completed
  2 choice points; %trace on to see them
  Results:
    aRan = true
    bRan = true
    cRan = true
    x = 1

The same line closes %step, %continue and %advance in the REPL, and reads 2 choice points without the hint once %trace on is showing them. Under -trace (or %trace on) each choice is a choice line naming what was open, every alternative, and the one taken:

$ sysml -trace -action test::race action_explore_three_writers.sysml

[trace] step 2: token 2@a, token 3@b, token 4@c

[trace] choice step 3: writes x := 1 by token 2, x := 2 by token 3, x := 3 by token 4 (unordered; x := 1 by token 2 stood)
[trace] choice step 3: tokens 2@a, 3@b, 4@c (unordered; took 4@c first)
[trace] step 3: token 2@sync, token 3@sync, token 4@sync
[trace] step 4: token 5@done
[trace] step 5: no active tokens

tokens 2@a, 3@b, 4@c names the tokens by id and node; took 4@c first is the reverse-order rule. The write line lists each token's last write to x and which one stood — x = 1, because token 2 stepped last. The other kinds read the same way: a decision with two holding guards is choice step 2: decision select branches 1->warn, 2->alarm hold (unordered; took 1->warn), two transitions out of one state enabled by one event are choice state idle on accept Go: transitions 1->left, 2->right (unordered; took 1->left), and two regions reacting to one event are choice on accept Go: next a1(exit), b1(exit) (unordered; took a1(exit) first) — each firing is drawn a unit at a time, its source's exit, its effect, its target's entry, so two firings may interleave; reverse and declared take the regions whole in declaration order and report each pick, and seed:<n> may take b1(exit) first. Entering a state of two regions draws the order of their entries the same way, choice entering work: next left(entry), right(entry) (unordered; took left(entry) first), a fork's branches under fork <name> and the regions a state leaves under exiting <state>; the fixed policies take declaration order there too, so a model that ran before these draws were recorded runs the same and gains only the choice lines — and two executors due at one instant of the clock are choice at t=5.0: due action watcher, state machine blinking of object #1 (unordered; ran state machine blinking of object #1 first). One executor alone due at an instant is not a choice and is not reported, so a model with a single behavior runs and traces exactly as it did before the clock was shared. Over gRPC and Connect the same choice is an informational diagnostic with code choice-point and the message choice point: step 3: tokens 2@a, 3@b, 4@c (unordered; took 4@c first), placed at the node, decision, feature or state that made it — a finding about the run, never an error.

Reporting never changes the run. Once a guard or transition holds, the ones after it are read in a preview that is undone, and one that cannot be evaluated there — a division by zero, say — is not an alternative and not an error: a guard with no result is not true, so its branch is not taken. It is counted beside the choices (1 guard not evaluable), traced as unevaluable guard step 2: decision select branch 2->alarm: division by zero (not selected), and carried as a diagnostic with code guard-unevaluable. The first guard read is the run's own, and its failure fails the run as it always has. And the case the library does order is not reported: a substate's transition outranking its enclosing state's makes no choice line under any policy.

Taking another linearization: seed:<n>

The fixed rule is one scheduling policy, named reverse, and the executor can be told to resolve every choice point under another. declared takes tokens in the order they were spawned, guards and transitions in declaration order and executors due together in the order they were started; seed:<n> draws each pick from a pseudo-random sequence the non-negative integer n fixes, so seed:1 replays the same run every time and on every platform while seed:2 may take another linearization:

$ sysml -schedule seed:1 -action test::race action_explore_three_writers.sysml
✓ package test
✓ Started action executor for "test::race"
  State: Running
  Tokens: 1
✓ Action completed
  Final state: Completed
  2 choice points; %trace on to see them
  Results:
    aRan = true
    bRan = true
    cRan = true
    x = 2

The policy is spelled the same everywhere — sysml -schedule for -action, -state and -analysis (a calc's body performs nothing, so -calc has no choice to make), %schedule seed:7 in the REPL for the runs started after it (a debugging session under way keeps the policy it started with, and its own notes, budget and calc memo, while another run is driven in between), a schedule field on the gRPC execution requests, and a schedule pin on a conformance case — and it changes only which alternative each choice takes. Every choice point the run reaches is still reported, and each took … is what the named policy took. Another linearization can reach other choice points — which tokens are steppable in a step depends on the order the earlier ones moved — so the count is not fixed across policies, only the reporting is. A guard the policy picks past the first was only previewed, so the run reads it once more for real before taking its branch (the trace shows that reading), as a transition's guard is always read again as it fires. An unknown spelling — random, seed without a number, seed:-1 — is refused before anything runs rather than falling back to the default.

Use a seed when one other linearization is what you want: to reproduce a run a colleague saw, to check a fix against the order that exposed the bug, or to pin a conformance case to a linearization other than the default's. It shows one run per seed, and says nothing about the runs no seed you tried happened to take.

Seeing the whole outcome set: explore

explore replays the behavior once per linearization. The first run records the alternative taken at each choice point; each later run is a fresh executor of the same loaded model — no object, message, clock, calc memo or note carries over — that follows a recorded prefix and takes an untried alternative at its end, until every choice sequence is spent or a budget is hit. The runs vary every choice point of the first run once, earliest first, before any is varied twice, so a choice met early is varied by the second run however many choices follow it:

$ sysml -schedule explore -action test::race action_explore_three_writers.sysml
✓ package test
✓ explored test::race: 3 outcomes
outcome                                      | linearizations | witness
---------------------------------------------+----------------+------------------------------------------------------------------
aRan = true; bRan = true; cRan = true; x = 1 | 2              | step 3: 3@b first of 2@a, 3@b, 4@c; step 4: 4@c first of 2@a, 4@c
aRan = true; bRan = true; cRan = true; x = 2 | 2              | step 3: 2@a first of 2@a, 3@b, 4@c; step 4: 4@c first of 3@b, 4@c
aRan = true; bRan = true; cRan = true; x = 3 | 2              | step 3: 2@a first of 2@a, 3@b, 4@c; step 4: 3@b first of 3@b, 4@c
complete (6 runs)

Runs that agree on what the conformance harness compares — an action's outputs; a state machine's final state, states visited and values; an analysis case's outputs and verdicts — are one outcome, and the table has one sorted row per distinct outcome: the outcome, how many linearizations reached it, and the choice sequence of one witness run (3@b first of 2@a, 3@b, 4@c is the first pick, then 4@c first of 2@a, 4@c among the two that remained). Six linearizations, three outcomes, two each; complete (6 runs) says every choice sequence was tried. Under explore an action step is one token advancing one node — not, as under the fixed policies, every steppable token moving once — so the picks fall in consecutive steps and a branch of several nodes can run ahead of, or be overtaken by, a concurrent one at each of them. A complete exploration therefore covers every interleaving of the nodes the library leaves unordered, at body granularity: the statements of one body run without interruption. A run that fails under some order is an outcome of its own (error: …), not the end of the exploration; a behavior with no choice point explores in exactly one run (no choice points in the witness column); the same model explores to the same table every time. With -trace, the table is followed by the trace of each outcome's witness run (trace of outcome 1's witness (run 4):). With -json, each check carries outcomes (values, linearizations, witness) and exploration (complete, runs, budgetsHit) beside the table's lines.

A state's do behavior is stepped the same way under explore and check: one token at a time — each due do behavior moves one token, then the machine dispatches the event at the head of its pool. Under the fixed policies (reverse, declared, seed:<n>) a do behavior's flow instead advances every steppable token once a round, and the machine dispatches only between rounds. The run a fixed policy makes — the whole round, then the dispatch — is therefore an interleaving with a value under reverse that check does not table. The fixed-policy check enumeration does not contain the interleaving where a transition interrupts a do behavior mid-round; the runtime does support that interruption. A check that reaches such a state — a machine owing a dispatch after a do step that left a token able to act standing, one ready beside the token moved or one its move freed, where a fixed policy's round would have moved it too — therefore does not report exhaustive: its verdict is no violation within bounds (or divergent, when the schedules it did search disagree) with not enumerated: do round before dispatch naming the run it left out, and the standing is bounded. Whether the dispatch waits for the round or cuts it becomes a recorded choice point — drawn per token move of the do flow — as the one site of the region-order scheduling work still open (design note); until then, run a fixed policy beside the checker when a do behavior loops through timed waits. The witnesses such a check writes replay as any other: the search is short of a run, not wrong about the ones it made.

The order of executors due at one instant of the clock is explored like any other choice: sysml -schedule explore -instantiate Demo::beacon -action Demo::watcher -state "Demo::Beacon::blinking Demo::beacon" -advance 5 starts every behavior named on one clock in each run, advances it, and tables the joint outcome — each behavior's observables under its name — once per order the due executors can run in, the witness naming which ran first (t=5.0: action watcher first of state machine blinking of object #1, action watcher).

Each run creates its objects afresh, so the object a behavior runs on is named as something a run can build: a declaration to instantiate (Demo::beacon), or a path from one into a part it holds. sysml -schedule explore -state "Comms::Ground::listen Comms::pair.ground" -state "Comms::Craft::modes Comms::pair.craft" -advance 5 instantiates Comms::pair once per run and runs each machine on the part the path reaches, so the pair's connector carries the ground's ping to the craft and the craft's frames back, and the table is of the assembly, not of a part alone. -instantiate Comms::pair gives every run the assembly instead, and a machine named alone attaches to the run's object exhibiting it. An id the report printed (#2) names an object of the session, which no run sees, and is refused (Objects an exploration runs on).

The budget is 1024 runs and 64 choice points per run unless explore:runs=N,depth=D says otherwise, and hitting it is never silent:

$ sysml -schedule explore:runs=3 -action test::race action_explore_three_writers.sysml
✓ package test
? explored test::race: 2 outcomes
outcome                                      | linearizations | witness
---------------------------------------------+----------------+------------------------------------------------------------------
aRan = true; bRan = true; cRan = true; x = 2 | 1              | step 3: 2@a first of 2@a, 3@b, 4@c; step 4: 4@c first of 3@b, 4@c
aRan = true; bRan = true; cRan = true; x = 3 | 2              | step 3: 2@a first of 2@a, 3@b, 4@c; step 4: 3@b first of 3@b, 4@c
incomplete: runs budget 3 hit after 3 runs
$ echo $?
2

incomplete names each budget hit (runs before depth), the table is what was reached so far and no more, the check is unresolved (?) and the exit status is 2 — the status of a run that decided nothing, as for an unevaluable verdict. Raise the budget it names (explore:runs=4096, explore:depth=128, or both) and run again; a model whose exploration stays incomplete at any budget you can afford has more linearizations than a table can carry, and a seed is the way to look at some of them. The two budgets bound different things: a choice point met past the depth budget takes its first alternative in every run and is never varied, however many runs remain, so a run of more choice points than depth — the witness column lists every one its run met — needs depth raised to at least that many before more runs can help; within depth, a runs budget of one more than the first run's choice points varies each of them at least once.

The same spelling explores over the wire, where the response carries outcomes and an exploration status (wire contract), and from every client (clients). The REPL refuses it, because its %action and %state debuggers step one run and an exploration replays from the start:

sysml> %schedule explore
error: explore replays a behavior from the start once per linearization, which %action and %state, stepping one run, cannot do: run `sysml -schedule explore -action <name>` (or -state, -analysis, -calc), or a request with schedule "explore"

Running one witness again

A witness column is a run you can run again. replay:<file> reads a file of choice lines — each spelled as the table spells them, one per line or joined by ; , ending at the first blank line (a trace body written after it is ignored, so a file the model checkers write serves as it stands) — and resolves the run's choice points in that order, then falls back to reverse once the file's moves are spent. Save the x = 1 witness above to x1.trace and the run reproduces that row:

$ sysml -schedule replay:x1.trace -action test::race action_explore_three_writers.sysml
✓ package test
✓ Started action executor for "test::race"

  Results:
    aRan = true
    bRan = true
    cRan = true
    x = 1
  standing: value (observed: 1 run under replay:x1.trace)

A replay follows its witness or says which move it could not follow, rather than quietly running another linearization: a move whose pick is not among the alternatives the run offered, one whose step the run has already passed, or one left over when the run ends, is replay refused: move <n> (<the choice>): <what the run faced instead>, and the check it was part of is not covered. A file that spells no choice (a pick not among its own alternatives, a line in no known form, nothing at all) is refused as the policy is parsed, before anything runs; a header of no choice points, as the checker writes for a run that met none, follows the one run there is.

A witness the smt engine writes opens with the values it chose for the action's free inputs, one input <feature> = <value> line each — input limit = -1, input mode = Modes::Mode::fast for an enumeration, input rate = 1/3 for a real, input limit = null for a feature declared [0..1] the solver left without a value — ahead of its choice lines. The replay pins each named feature at that value before the run starts, as an argument the invocation passes is pinned and before any default the model gives it, then follows the moves; a file without input lines is the format it always was and replays as before. An input line naming a feature the action does not have, or one it cannot set, is refused naming the feature, and the check it was part of is not covered. The policy is accepted wherever a policy is — -schedule, %schedule (the debuggers step one run, which is what a replay is), a conformance case's schedule pin — except over the wire, where a request carries no file of the caller's and "replay:…" is INVALID_ARGUMENT.

Writing a test that admits several outcomes

A conformance case (see the conformance README) that pins one outcome of a model with choice points pins the default policy's linearization, which is fine when that is what you mean. When the model admits several, say so with three things beside the .sysml:

outcomes + admissible in the .expected.json — the complete set of admissible outcomes, each written in full (an outcome is never "anything"), and the title of the section of the semantic oracle that derives the set from the library. This is the fixture's own action_explore_three_writers.expected.json:

{
	"type": "action",
	"trace": true,
	"outcomes": [
		{
			"outputs": {
				"x": {"type": "Integer", "value": 1},
				"aRan": {"type": "Boolean", "value": true},
				"bRan": {"type": "Boolean", "value": true},
				"cRan": {"type": "Boolean", "value": true}
			}
		},
		{
			"outputs": {
				"x": {"type": "Integer", "value": 2},
				"aRan": {"type": "Boolean", "value": true},
				"bRan": {"type": "Boolean", "value": true},
				"cRan": {"type": "Boolean", "value": true}
			}
		},
		{
			"outputs": {
				"x": {"type": "Integer", "value": 3},
				"aRan": {"type": "Boolean", "value": true},
				"bRan": {"type": "Boolean", "value": true},
				"cRan": {"type": "Boolean", "value": true}
			}
		}
	],
	"admissible": "Three concurrent writers of one feature: six orders, three values"
}

outcomes replaces outputs (or finalState, stateVisits, performers for a state case); a case may not have both, may not list one outcome, and must cite a section the oracle has. The harness checks the default run's outcome is exactly one listed member, then explores the case and fails on a listed outcome no linearization reached (admissible outcome 2 of 3 is unreachable), on a reached outcome the list omits, and on a budget hit — telling you to raise it with "exploreBudget": {"runs": N, "depth": D} in the same file. So the list is exact, not a lower bound: the three outcomes above are exactly what the six runs of the table reach.

A .trace.order file — the partial order the library does fix, as earlier < later constraints over trace labels: the first trace entry mentioning a comes strictly before the first mentioning b (blank lines and # comments are skipped). The harness checks the trace against them beside the exact golden, so a fixture states what must hold without pinning what may vary. action_explore_three_writers.trace.order:

# The fork precedes every branch; the join waits for all three.
split < a
split < b
split < c
a < sync
b < sync
c < sync

A .trace.golden — with "trace": true, the default policy's trace is recorded as usual, so the linearization the default takes is still pinned exactly (deterministic replay is a feature) while the outcomes say it is one of three. The suite also runs every case under declared and seed:1, and a case with outcomes gets a .declared.trace.golden and a .seed-1.trace.golden of its own.

Which openness a fixture makes observable, and which it leaves to a single-outcome case, is recorded per fixture in the semantic oracle; the compliance table names the code and tests behind each surface above.

When a model states its own odds

Every choice point above is scheduling nondeterminism: the library leaves the order open and gives no alternative a likelihood, so the honest report is the set. A model migrated from a simulation tool often means something else by a branch — the acquisition succeeds seven times in ten — and by a duration — the settle takes between one and eighty seconds — and SysML v2 has no notation for either. OpenSysML supplies one as an extension library, in standard SysML v2 that any other tool reads as ordinary metadata and a function call: two library packages, Stochastic and RandomFunctions under internal/workspace/libs/stdlib/OpenSysML Libraries/, both marked NON-NORMATIVE, with the vendored OMG files untouched. What they state is modeled randomness, and the runtime keeps it apart from the scheduling kind: a weighted branch is drawn by its weights, a scheduling choice never is, and the two draw from two independent streams, fixed by two independent seeds.

package MC {
	private import ScalarValues::*;
	private import ISQ::*;
	private import SI::*;
	private import Stochastic::*;
	private import RandomFunctions::*;

	action def Route {
		attribute taken : Integer = 0;
		attribute d : Real = uniform(0.0, 10.0);
		first start;
		then decide select;
		first select then fast { @Probability { p = 0.7; } }
		first select then slow { @Probability { p = 0.3; } }
		action fast { assign taken := 1; }
		then wait;
		action slow { assign taken := 2; }
		then wait;
		action wait accept after uniform(1, 80) [s];
		then done;
	}
	action route : Route;
}

A weighted branch: @Probability { p = … }

Stochastic::Probability is a metadata definition with one attribute, p : Real, applied to a succession out of a decision node from within the succession's body. The rules the lowering enforces, each violation a typed error before anything runs:

  • every succession out of one decision carries a Probability, or none does — a decision with three branches of which two are weighted is refused naming the unweighted one;
  • each p lies in 0.0..1.0;
  • where every p out of one decision is a constant — a literal or arithmetic over literals, 1.0 - 0.3 as much as 0.7 — they sum to 1.0 within 1e-6; a p that is an expression over the action's features is evaluated when the decision is reached and refused then, as a typed invalid branch weights error naming the decision and the branch, if it is no number, no finite one, or one outside 0.0..1.0, or if the holding weights sum to nothing positive;
  • a p that names a feature is typed where the annotation is written: the feature's type must be Real or Integer, so p = ready over a Boolean, a String, an enumeration or a part is a type error before anything runs, while p = pFast over a Real attribute of the action, of the object performing it (this chain) or of an in parameter is a weight the run reads when the decision is reached — so one object's attributes weight the decisions of the behaviors it performs, and two objects of the same type with different attribute values take different odds;
  • a Probability with no p, two, an attribute it does not declare, or a p that is not a number is refused naming what is wrong, and one written on an action body instead of a succession is refused rather than ignored.

A branch whose guard does not hold at the decision is out of the draw, and the weights of the branches that do hold are renormalized among themselves: a 0.7 branch guarded by if ready against a 0.3 branch unguarded is the 0.3 branch alone when ready is false, and a decision at which no holding branch weighs more than zero is refused. A Probability on a state transition is refused with a typed lowering error: weighted transitions are not in this release (see Known limitations).

A random value: RandomFunctions

Four functions return a draw from a stated distribution, each a scalar over scalars:

Function Draws Requires
uniform(lo, hi) a Real uniformly over [lo, hi) lo <= hi
uniformInteger(lo, hi) an Integer uniformly over lo..hi, both inclusive lo <= hi
triangular(lo, mode, hi) a Real from the triangular distribution peaking at mode lo <= mode <= hi, lo < hi
normal(mean, sd) a finite Real from the normal distribution; sd = 0.0 is mean sd >= 0.0

A call whose arguments break the requirement — uniform(80, 1), a negative sd, a bound that is not finite — is refused with a typed error, random function arguments bound no distribution, naming the call, not clamped. The functions take and return scalars, so a random quantity is a draw given a unit — accept after uniform(1, 80) [s] — and a quantity passed as a bound (uniform(1 [s], 80 [s])) is refused by the function's typing. A random accept after is evaluated once, when the wait is set up, and the instant it is due at stands while the token waits: the duration is not redrawn as the clock is polled.

Draw policies: min, max, average and random

How a RandomFunctions call resolves is a setting of the run, not of the model: -draws <policy> at the command line and %draws <policy> at the prompt. random, the default, draws each call from the model seed. min, max and average resolve each call to the least, greatest or mean value of its distribution instead, so a duration written uniform(1, 80) [s] is 1 [s], 80 [s] or 40.5 [s]:

Function min max average
uniform(lo, hi) lo hi (lo + hi) / 2
uniformInteger(lo, hi) lo hi the midpoint, a half rounded toward hi
triangular(lo, mode, hi) lo hi (lo + mode + hi) / 3
normal(mean, sd) refused refused mean

A run whose only randomness is its durations is therefore deterministic under a fixed policy and needs no seed: -runs 20 -draws max runs the action twenty times and every row is the same, which is how a workflow's longest and shortest paths are read off. normal has no least or greatest value, so a run that calls it under min or max stops with a typed error naming the call and the policy; its average is its mean. Weighted decisions are not durations: they draw from the seed under every policy — -draws max -seed 7 fixes the durations and randomizes the branches — and an unseeded one takes its most probable branch as it does under random. The policy applies to the debugger's session as well, so %draws max before %action steps through the longest durations. Every witness the checker writes records a fixed policy as a draws by <policy> line ahead of its draws, and replay:<file> runs under the recorded policy, refusing a recorded draw the policy could not have made (a witness that names a fixed policy and records no draw leaves them to the policy). A simulation tool's duration simulation mode is this knob; see Behaviors migrated from SysML v1.

The clock the durations run on is a setting of the run too. By default it is continuous: a wait comes due exactly when it ends. -clock-step <seconds> at the command line and %clock-step <seconds> at the prompt make it tick instead, as a simulation tool's fixed-step clock does, so a wait comes due at the first tick not before its end — under a step of 1, a wait of 2.3 [s] set at t=0 comes due at t=3.0 — and a workflow's total is a whole number of steps. 0 restores the continuous clock. A witness of a stepped run records clock steps by <seconds> and replays on it; a migrated run configuration records the tool's step, which -compare-results applies (see Comparing a migrated configuration).

Seeds: where the draws come from

A run that reaches a weighted decision or a RandomFunctions call needs a model seed. Given none, it is refused — modeled randomness needs a seed: uniform(0.0, 10.0) draws a random value; seed the run, as -seed <n> or %seed <n>, or schedule it under seed:<n> — rather than drawing an unrepeatable value that a later run could not reproduce — unless the run is under a fixed draw policy, whose calls draw nothing. The model seed comes from the first of:

  1. the witness a replay:<file> follows, whose recorded draws the run consumes (below);
  2. an explicit model seed — the CLI's -seed <n>, the REPL's %seed <n>, a conformance case's "modelSeed";
  3. the scheduling seed, when the run is under seed:<n> and no model seed is set.

So -schedule seed:3 -seed 7 shuffles the tokens from 3 and draws the model's values from 7; -schedule declared -seed 7 and -schedule seed:1 -seed 7 make the same draws in two token orders; and -schedule seed:7 alone uses 7 for both, each from a stream of its own derived from it, so the token order and the values are still two knobs. Under declared and reverse with no seed, a weighted decision is not refused: it takes the most probable branch (the first declared among equals), so an unseeded run stays deterministic, and only a call that must draw a value refuses.

The trace shows each draw as it is made and the weighted decision as a choice point, with the weights of the branches that hold, the unit draw and what it selected:

$ sysml -trace -action MC::route -seed 7 mc.sysml

[trace] draw uniform(0.0, 10.0) = 7.74817894359002

[trace] choice step 2: decision select branches 1->fast p=0.7, 2->slow p=0.3 hold (weighted; drew 0.33557143536732337, took 1->fast)

[trace] draw uniform(1, 80) = 47.88520359371734

Replaying the draws

A witness records every draw a run made, as draw <call> = <value> lines among its choice lines — draw uniform(0.0, 10.0) = 7.74817894359002 — and a weighted choice line carries the weights and the draw that selected the branch after the branch taken — step 2: decision select -> 2->slow among 1->fast p=0.7, 2->slow p=0.3 drew 0.7748; a branch explore enumerated rather than drew ends at the weights. replay:<file> consumes them in order: each call the run makes takes the next recorded draw instead of drawing, and the run is refused, as any unfollowable move is, when a draw is missing (the run draws once more than the file recorded), left over (the file recorded a draw the run never made), made by another call than the one recorded (uniform(1, 80) where the file says uniform(0.0, 10.0)), or written as no value the call can draw (draw uniform(0.0, 1.0) = 2.0: outside the bounds, of the wrong kind, off the mean of a normal with zero deviation). A weighted choice line is followed only where it fits the decision the run faces: the same branches weighed the same as the model now weighs them, and a recorded draw that is a unit draw in [0, 1) selecting the branch the line took — a line that weighs a branch otherwise, lists another set of branches, or records a draw that would select the other branch (… 2->slow … drew 0.1 where 1->fast p=0.7 comes first) is refused naming what the run faced. A replay's rollback — a probe the checker makes, a %step taken back — restores the draw position with the choice position, so a run stepped and re-stepped consumes each draw once.

Under explore and check

explore enumerates a weighted decision as it enumerates every other branch choice: every branch is a linearization, and the outcome table lists what each reaches. check searches them all the same way. The weights do not prune the search — a branch of probability 0.01 is a schedule, and a violation on it is a violation — and, in this release, are not accumulated into a probability per outcome either (see Known limitations). The random values along a linearization come from the model seed as in any run, so an exploration needs one when the model draws.

Many runs: %runs and -runs

A stochastic model is a question about a distribution, not a value. %runs <n> <seed> <action> [<observable>...] at the prompt and -runs <n> -seed <s> -observe <feature> at the command line run the action to completion n times, each run on a fresh context with a model seed of its own derived from <seed> and the run number — so run 3 of seed 7 is the same run on every platform, and can be repeated alone with %seed <its seed> — and table what each run's named features, and clock, the simulation time it completed at, came to. Without observables every feature the action holds and the clock are tabled; clock names the clock only, so a feature of that name is not reported; a part or item the action holds exactly one of is tabled through its attributes (target.total), so an action that performs a behavior on an object it declares reports what the object came to. A feature a run left without a value (an attribute t : Real [0..1]; no statement of that run assigned) is a blank cell of its row, outside the summary, and an observable no completed run gave a value is refused. Under %draws min, max or average the seed is left out (%runs 20 Sys::align clock, -runs 20 -draws max), since the runs draw nothing at random; under random a seedless %runs is refused naming the seed and %draws. Below the table each numeric observable is summarised over the runs that completed: the minimum, mean and maximum, the nearest-rank p50 and p90, and a histogram; a non-numeric observable is counted by value.

$ sysml -action MC::route -runs 8 -seed 7 -observe taken -observe clock mc.sysml
✓ package MC
runs MC::route — 8 run(s), seed 7
run | taken | clock                  | time
----+-------+------------------------+--------
1   | 1     | 45.771104597451966 [s] | 5.056ms
2   | 1     | 18.029229676573745 [s] | 6.089ms
3   | 1     | 36.432448124734556 [s] | 5.166ms
4   | 2     | 35.38279454977086 [s]  | 5.826ms
5   | 1     | 33.085779305138026 [s] | 5.177ms
6   | 2     | 31.25968702920801 [s]  | 6.824ms
7   | 1     | 72.07463187296344 [s]  | 6.696ms
8   | 1     | 75.88725335563454 [s]  | 4.860ms
taken: 8 run(s), min 1, mean 1.25, max 2, p50 1, p90 2
  1 ###############      6
  2 #####                2
clock: 8 run(s), min 18.029229676573745 [s], mean 43.490366063934395 [s], max 75.88725335563454 [s], p50 35.38279454977086 [s], p90 75.88725335563454 [s]
  18.03..25.26 [s] ###                  1

  standing: table (observed: 8 rows)

The runs are the rows of a sweep plan with no range, so they run %jobs/-jobs at a time, a run that fails is a numbered row with its error under the table rather than an abort, and the plan is bounded by OPENSYSML_MAX_SWEEP_RUNS as any sweep is. The scheduling policy is the second knob here too: every run resolves its concurrency choices under -schedule alike, and a replay:<file> policy, which is one run, is refused with -runs.

Known limitations of modeled randomness

  • State transitions carry no weight. @Probability on a transition is refused at lowering with a typed error; only successions out of a decision node are weighted.
  • explore and check do not accumulate probability. The outcome table and the checker's verdict enumerate the weighted branches as branches; the probability of an outcome (the product of the weights along its linearization's decision picks) and the probability mass of the schedules reaching a violation are not reported.
  • Random functions are scalar. A bound given as a quantity is refused; write the unit on the draw (uniform(1, 80) [s]).
  • Weights are drawn among the branches that hold. A decision whose guards leave exactly one weighted branch holding takes it with probability one, whatever its p; the sum-to-one rule is checked over the branches as written.
  • Monte Carlo runs are a REPL and CLI operation. %runs and -runs run an action repeatedly; the RunSweep RPC and the service clients take ranges and samples but no run count, and an external engine put a Monte Carlo answers with a claim, not the table of runs.
  • A draw policy resolves RandomFunctions only. min, max and average fix the durations and values the four functions return; a weighted decision draws from the seed under every policy, and normal has no min or max unless its deviation is zero. Exploration and the checker enumerate a weighted decision's branches whatever the policy.

Behaviors migrated from SysML v1

The v1 migration writes a v1 activity as an action def and a v1 state machine as a state def in exactly the forms this chapter uses, so a migrated behavior runs under the same debugger, seed and %runs as one written by hand. Two v1 idioms land on the machinery above:

  • A DurationConstraint on a call action ([1s..80s]) becomes a wait the token takes before it — accept after 3.0 [SI::s] for a point interval, accept after RandomFunctions::uniform(1.0, 80.0) [SI::s] for a proper one — so a workflow's duration is a draw from the model seed, as under a v1 tool's random duration mode. The tool's min, max and average modes are the draw policy of the run, -draws/%draws, which each migrated run configuration records.
  • «Probability» on the edges out of a decision becomes @Stochastic::Probability { p = … } on each succession, when every edge carries one: a constant for a numeric tag, and for a tag naming a property of the activity or of its context block — a v1 analysis block whose ProbabilityBTOOP : Real the run configurations set to 1.0 or 0.0 — a reference to the migrated attribute (p = ProbabilityBTOOP;, p = 1.0 - ProbabilityBTOOP;), read when the decision is reached from the object the behavior runs on. A decision whose guards are opaque English ([Align BTO]) is written unguarded, and the runtime draws its branch with the model seed.
  • A run configuration (SimulationProfile:SimulationConfig) becomes an action def that declares part target : <the migrated execution target> — the individual def the target instance became, whose slots are its attribute values — and perform action run ::> target.<the classifier behavior>, with the tool's numberOfRuns and durationSimulationMode as @Simulation::Configuration { runs = …; draws = …; } metadata; so running the configuration runs the behavior on an object holding that configuration's property values, its probabilities included. See Run configurations.

The workflow's total duration is the clock at the end of the run, which %runs reports when no observable is named:

%runs 100 1 Model::Mission::'Acquire Target'::'Acquire Target - Logical'

A migrated configuration is run by its generated name with the count and policy it records, and -compare-results sets its runs beside the snapshots the tool stored of its own (Comparing a migrated configuration with the tool's results):

sysml tmt.sysml -action "Flows::'Acq Time Group0'" -runs 6 -seed 1 -draws random -observe clock
sysml tmt.sysml -compare-results tmt.results.json -seed 1 -observe Time_Acq_Total=clock

A v1 opaque action that reads the tool's time variable (Time_Acq_Total = simtime) reads the clock: assign this.Time_Acq_Total := localClock.currentTime;, so the attribute the workflow times is one -observe this.Time_Acq_Total tables per run beside clock. Names in the body resolve through the swimlane the action sits in (this.tcs.i for a partition representing the part tcs), so a workflow whose actions read its performer's features is run through the performer: -action "'Observatory' 'Acquire Target'". A body the opaque-language subset does not read stays a comment, and the report names the token it refused.

An object runs the behaviors its type exhibits

A type that exhibits a state machine or performs an action binds that behavior to every object of the type: instantiating an object gives it an execution of its own, tied to its identity. Two objects of the same type run two independent machines, each with its own current state, event queue and feature values, and an assignment in a behavior body writes the feature value of the object performing it.

sysml> part def Monitor {
  ...>     attribute count = 0;
  ...>     exhibit state modes {
  ...>         entry; then idle;
  ...>         state idle { entry action bump { assign count := count + 1; } }
  ...>         accept after 10 [SI::s] then awake;
  ...>         state awake { entry action mark { assign count := count + 10; } }
  ...>     }
  ...>     action bumpBy { in n; action apply { assign count := count + n; } first apply; then done; }
  ...> }
✓ part def Monitor

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

sysml> %state Monitor
✓ Debugging state machine "modes" exhibited by object #1 of "Monitor"
  Current state: idle
  Time: 0.0
  Events: 1

Use %events to see queue, %current for state, %advance <time> to step

sysml> %step
✓ Event dispatched
  Current state: awake
  Time: 10.0
  Events: 0

sysml> %features Monitor
Instance: Monitor (ID: 1)
Features:
  count = 11
…
Behaviors:
  modes: exhibited state machine, current state awake
…
  bumpBy: action, not running

%instantiate started the machine, and %state Monitor attached the debugger to that object's machine rather than to a detached run of the usage. %step, %advance, %current and %events therefore drive that machine, and %features shows the values its entry actions wrote: 1 from idle, then 10 more from awake once the timer fired. The machine and the operation are not values the object holds, so they are listed under Behaviors: with what the object is doing with each: the exhibited machine's current state is the one %current reports, and bumpBy, which the type declares but does not perform, is not running.

The two-argument form does the same when the machine it names is the one the object exhibits: %state Monitor::modes Monitor attaches to the running machine and says so in a note: line, rather than performing modes a second time against the same feature values (which would run its entry actions again, leaving count at 2 instead of 1). Only a machine the object does not exhibit — one it merely performs — is started as a detached performance by that form. When the object exhibits one definition as several usages (exhibit state front : Blink; exhibit state rear : Blink;), naming the definition names no one machine, so %state Blink lamp refuses and names Lamp::front and Lamp::rear to name instead.

Naming the machine alone attaches the same way when one held object exhibits it: %state modes (or %state Monitor::modes) after %instantiate Monitor drives that object's running machine, so what its do and entry actions write shows up in %features Monitor. It never performs the machine detached from its object: with no object exhibiting it (%state modes before any %instantiate), or several (a second Monitor held as a part of another object), %state refuses and names the objects — or, before any exists, the type exhibiting the machine — and you name one with %state <object> or %state <machine> <object>. Only a state def no type exhibits is started as a detached performance by that form.

The object can also be a part reached through composition, or an id. With part def Driver { part r : Monitor; }, part driver : Driver; and %instantiate driver, %state driver.r debugs the nested part's own machine, and %state #2 the same by the id %features driver prints for it (r = Instance(ID: 2)). A path that stops short of an object says which segment failed, in the words every command uses for an object reference:

sysml> %state driver.x
error: driver has no feature "x" (its features are r, and 13 more the library declares)

Naming a usage whose definition alone was instantiated is reported as such, with what to instantiate instead. With part monitor : Monitor; declared:

sysml> %instantiate Monitor
sysml> %state Monitor::modes monitor
error: no instance of the usage "monitor": object #1 of "Monitor" is of its definition "Monitor", not of the usage — use %instantiate monitor to create the usage's object, or name Monitor to address it

When a machine starts, and how far it runs. The object's feature values are built and its constant defaults evaluated first, so an entry action sees the declared initial values. The machine is then initialized and run until it is quiescent: no event is due at the current time, no do action can run, and no message is in flight. A machine waiting on a timer or an accept is quiescent, and advancing time is what lets it proceed. Objects that signal one another are run together until they all settle, within the event and do-step budgets described in reference/environment.md; an exchange that never settles reports a budget error rather than hanging. Instantiating the same name twice creates a second object with its own identity and its own machines: %instantiate reports the new object, and the name then refers to it, while the first object keeps running and is still addressed by its id (%state #1, %invoke #1 bumpBy n=4, %features #1; see addressing an object). A %state or %action session started on the first object stays with it — it now knows the object as #1 — and it ends only if that object is later dropped. A machine a nested part exhibits is debugged by naming that part through its owner, %state Monitor.sensor or %state #1.sensor. An exhibited machine with no initial state is reported as such. A performed action that declares no flow has nothing to step, but the object is still created. A performed action waiting at an accept is also quiescent, and a message sent later by a sibling object wakes it up — as does one sent from the prompt: %send reaches an action the object performs (a top-level perform, or one nested in it) whose token is parked at a matching accept, no machine needed, and reports the accept that takes it:

sysml> package Q {
  ...>     private import ScalarValues::*;
  ...>     attribute def Go { attribute n : Integer; }
  ...>     action def Main {
  ...>         out total : Integer = 0;
  ...>         first start;
  ...>         then action w1 accept g : Go;
  ...>         then action a1 assign total := total + g.n;
  ...>         then done;
  ...>     }
  ...>     part def PD { perform action main : Main; }
  ...>     part pd : PD;
  ...> }

sysml> %instantiate Q::pd
✓ Created instance of Q::pd

sysml> %send Go(n=7) to Q::pd
✓ Sent Go(n=7) to object #1 of "Q::pd"
  Accepted by performed action "main" waiting at accept g

Open a %state or %action session, then %advance <time> dispatches it

sysml> %action Q::Main
✓ Started action executor for "Q::Main"
  State: Running
  Tokens: 1

sysml> %advance 0
✓ Advanced to 0.0 (0 event(s) processed)
  Action state: Waiting
  Tokens: 1
  Action steps taken: 5

sysml> %eval Q::pd.main.total
✓ Q::pd.main.total
  = 7

The message is in flight until the object's behaviors next run, and only a debugging session drives the runtime: %advance of any session of it — here a standalone %action Q::Main, which performs on behalf of no object and so takes nothing addressed to pd; a %state on a sibling object would do as well — moves the action past its accept, after which %eval Q::pd.main.total reads 7. With no session open, %send says so; with one open that is not what takes the signal, it says Use %advance <time> to dispatch it, as a %step of that session dispatches only what that session's own behavior accepts. An object performing an action nothing is parked at for the signal is refused up front, with the action's standing (performed action "main" waiting at accept g of type Go, or completed), as a machine in a state accepting nothing is; an object that neither exhibits a machine nor performs an action is refused too. With an %action Main Q::pd session open, a bare %send Go(n=7) goes to that object — the very one the session materialized, across an unrelated declaration that leaves the session running. The session's own executor is a fresh performance of Main beside the object's running one, not that one: both perform on behalf of the object, so a Go in flight for it is taken by whichever of the two is at its accept when the object's behaviors run, and %send reports each that is parked for it now. A bare %send in an %action session performing on behalf of no object is refused: there is no object to address.

Editing the model while an object runs. Submitting an unrelated declaration keeps the object (its identity survives the rebuilt analysis) but not the execution it was running. An execution belongs to the graph, names and message bus of the analysis it started in, so the object's behaviors are restarted from their initial states in the rebuilt analysis, and any values the discarded run wrote are dropped with it. The restart is reported (note: the exhibited state machine modes of object #1 was restarted from its initial state because the model was rebuilt), and a %state session follows the object onto its restarted machine, so a restarted behavior exchanges messages with objects instantiated after the edit in the usual way. Redeclaring what the object runs (its type's features, or the body of a machine or action it runs) produces a different object, so the original is dropped with a stated reason and %instantiate creates a new one.

Invoking an operation. %invoke <object> <op> [<p>=<expr>] runs an action owned by the object's type, performed by that object — named as %state names one, so %invoke driver.r bumpBy n=4 and %invoke #2 bumpBy n=4 reach a nested part:

sysml> %invoke Monitor bumpBy n=4
✓ Invoked bumpBy on object #1 of "Monitor"

sysml> %features Monitor
Instance: Monitor (ID: 1)
Features:
  count = 15
…
Behaviors:
  modes: exhibited state machine, current state awake
…
  bumpBy: action, not running

Each argument is written as <parameter>=<expression>. An unbound parameter, an argument that names no parameter, and an operation the type does not own are each reported as errors. A calc or constraint cannot yet be invoked this way.

Running an analysis case

An analysis case is a case, a case is a calculation, and a calculation is an action (SysML v2 §7.22, §7.21, §7.19), so running one is performing an action whose result is its out and return parameters. %analysis and -analysis run a case the way %calc invokes a calc: the case's subject is an in parameter the usage binds (subject s = ship;) or the object named after the case supplies, the other in parameters take arguments in parentheses, positionally or by name, or their declared defaults, and the report lists what the case computed with its units and, after that, the verdict of its objective.

package test {
    private import ScalarValues::*;
    part def Ship { attribute cost : Real = 5.0; }
    part ship : Ship;

    analysis steps {
        subject s = ship;
        action a {
            out p : Real;
            assign p := s.cost + 1.0;
        }
        then action b {
            in q : Real = a.p;
            out w : Real;
            assign w := q * 10.0;
        }
        out total : Real = b.w + a.p;
        return : Real = b.w;
    }
}
$ sysml -analysis test::steps steps.sysml
✓ package test
✓ test::steps
  total = 66.0
  result = 60.0

The body's action steps run through the same executor %action debugs: then sequences them, each later step reading an earlier one's output by step.pin, and a body that states no succession performs its steps in declaration order, as a calc body does. A nested analysis usage is a step too; one that binds no subject of its own runs on the enclosing case's subject, and the enclosing case reads its outputs as features of it (out total : Real = inner.m * 2.0;). The out parameters and return are evaluated in the case's frame after the steps complete, so they may read the subject, the in parameters, the steps' outputs and call calc defs. %trace on shows the order: the case is entered, its subject bound, each step performed as an action node, then return and every out evaluated.

The objective is a requirement the case frames; it is evaluated, not executed. After the body runs, its require and assume constraints are checked against the case's results by the same engine %requirement uses, and the verdict is printed beside them: satisfied, not satisfied with the condition that failed, or undecided with the reason a condition could not be evaluated (a feature the condition reads has no value). An assert constraint { ... } inside the body is checked the same way, against the values the run bound once its steps have completed. %optimize still asks a solver which values would make the objectives best; %analysis reports what they are for the values the model has — and for a trade study, which of the listed alternatives the model selects.

An objective typed by a requirement definition binds that definition's subject as a requirement usage does, in every spelling — objective : MassLimit { subject = ship; }, subject s = ship; or subject :>> s = ship; — and the binding may read the case's subject, its in parameters and locals, and its steps' outputs — a nested case's (subject = inner.picked;) or an action's (subject = weigh.m;). The case's own result, named or not, is readable by its qualified name as the OMG examples write it — subject = MassCase::result; in the objective, MassCase::result < limit in an assert constraint, inner.result from the case performing inner as a step. The qualifier names whose result it is: MassCase::result (or Cases::Case::result) is the running case's, while a sibling usage's light::result is the sibling's own run, never the running case's value. An objective that binds no subject takes the value the library states for it — in an analysis case its default, the case's result (Cases::Case::obj declares subject subj default Case::result, SysML v2 §7.22); in a verification case the case's own subject, which the library binds rather than defaults. So an objective typed by MassLimit in an analysis case that returns a Ship checks the ship returned, while in a case that returns a Real it is undecided, saying so: subject s defaults to the case's result (Cases::Case::obj): type mismatch: 1000.0 (a Real) is not a Ship. The result must also fit the subject's multiplicity: one Ship for a subject pair : Ship[2] is undecided as a multiplicity violation (an objective redeclaring the subject without one, subject :>> pair;, keeps the [2]). A case that returns nothing leaves such a subject unbound, and the verdict says to bind it or return a result. Bound either way, an object is held as a value of the subject, so one declared a Ship and bound to a subject t : Tanker is a Tanker for the conditions, its cargo answering t.cargo, exactly as a requirement usage's subject = ship; holds it; a value the subject cannot hold at all (a Buoy for a Tanker) is refused as a type mismatch before any condition is read, and an expression yielding more or fewer values than the subject declares (one Ship for a subject pair : Ship[2], or none) as a multiplicity violation, just as the default is. The object a satisfaction assertion supplies with by is held to the subject's declaration the same way: classified by its type, refused as a type mismatch where it cannot be, and as a multiplicity violation where one object is too few.

requirement def MassLimit {
    subject s : Ship;
    attribute limit : Real = 2000.0;
    require constraint { s.hullMass < limit }
}
analysis def MassCheck {
    subject ship : Ship;
    objective : MassLimit { subject = ship; }
    return r : Real = ship.hullMass;
}
analysis light : MassCheck { subject ship = O::ship; }
$ sysml -analysis O::light mass.sysml
✓ package O
✓ O::light
  r = 1000.0
  objective obj: satisfied

A case usage owned by a part definition (part def Holder { analysis inner : CostAnalysis { subject s = h; } part h : Ship; }) is a feature of every object of that type, as a calc usage owned by a part is: holder.inner.total runs the case on first read and keeps the result until a value it depends on changes, and an attribute redefined from an analysis output (attribute :>> fuelEconomy = cityAnalysis.fuelEconomyResult;) evaluates through the same path.

What stops a case is reported as an error naming it, never as a silent empty result: a subject nothing binds (analysis An::CostAnalysis: s subject is unbound: bind it (subject s = ) or run it on an object), an in parameter with neither argument nor default, a step that fails, a body that deadlocks or exhausts the step budget, and a case whose body runs itself. A case that recurses without bound — through a nested analysis step that performs its own definition, or a calc def through a calc usage member typed by itself — hits the calc depth limit, and the error collapses the repeated frames to one line as -calc's does: analysis An::rec: node again: analysis An::Rec::again: … 9999 frames: node again: calc recursion limit exceeded: calc An::Rec::again nested 10000 deep (unbounded recursion?; raise OPENSYSML_MAX_CALC_DEPTH to allow more).

Verification cases

A verification case body uses the same grammar and runs the same way: %analysis and -analysis accept a verification def or verification usage, bind its subject and in parameters as they bind an analysis case's, run its body over the same action graph, and check its objective and assert constraints afterwards. Beside those verdicts they report the VerdictKind the body produced, which is what running the body answered:

verification def SpeedCheck {
    subject lander : Lander;
    objective { verify touchdown; require constraint { lander.verticalSpeed <= 1.5 } }
    VerificationCases::PassIf(lander.verticalSpeed <= 1.5)
}
verification checkSlow : SpeedCheck { subject lander = L::slowLander; }
$ sysml -analysis L::checkSlow landing.sysml
✓ package L
✓ L::checkSlow
  result = VerdictKind::pass
  objective obj: satisfied
  ✓ Verification L::checkSlow verdict: pass

The two verdicts are independent: an objective stating no condition of its own — objective { verify touchdown; } alone — stays undecided and leaves the case unresolved, while the body verdict beside it still reports what the body answered.

A verification case's objective checks the case's subject, not its verdict: the library binds it so (VerificationCases::VerificationCase::obj redefines Cases::Case::obj with subject subj = VerificationCase::subj, SysML v2 §7.23), where an analysis case's objective defaults to the result. An objective typed by a requirement definition therefore evaluates that definition's conditions against the verification subject, whatever the requirement calls it — subject lander : Lander in the requirement below receives the scout the case is run on. The objective's own subject :>> subj; states no value: it only keeps the subject the first parameter, as a usage's owned parameters redefine its definition's by position, ahead of the in limit = limit; that binds the requirement's input. Because the library states the subject binding with =, not default, a usage cannot rebind it: objective : SoftLanding { subject lander = other; } is refused as overriding a fixed value.

requirement def SoftLanding {
    subject lander : Lander;
    in attribute limit : Real default = 1.5;
    require constraint { lander.touchdownSpeed <= limit }
}
verification def TouchdownCheck {
    subject lander : Lander;
    in attribute limit : Real = 1.5;
    objective : SoftLanding { subject :>> subj; in limit = limit; }
    VerificationCases::PassIf(lander.touchdownSpeed <= limit)
}
verification checkScout : TouchdownCheck { subject lander = L::scout; }
$ sysml -analysis L::checkScout landing.sysml
✓ package L
✓ L::checkScout
  result = VerdictKind::pass
  objective obj: satisfied
  ✓ Verification L::checkScout verdict: pass

A requirement whose subject the verification subject cannot be — subject rover : Rover checked against a Lander — leaves the objective undecided, naming both (subject subj is bound to the case's subject (VerificationCases::VerificationCase::obj): type mismatch: Lander #1 (scout) is not a Rover), and a verification whose own subject nothing binds is an error naming that subject, as for any case.

A body whose result is a VerificationCases::PassIf(...) call is pass or fail as that library calculation computes it; one binding verdict to a VerdictKind literal reports that literal; one producing no verdict value is inconclusive; and one whose run could not be carried out is error carrying the same message the run failed with. Each nested verification step is reported on its own line, marked (subcase), since the library states no roll-up of a subcase's verdict into its parent's.

%requirement, %satisfy, -requirement and -satisfy report those verdicts beside their own, and their own verdict is unchanged: the requirement engine still decides whether the requirement is satisfied, and a failing verification body does not make a satisfied requirement violated.

%sweep runs the case once per value of a range rather than once, and %samples <n> <seed> draws that many values from it instead — the same tables -sweep and -samples print. Each row is an ordinary run of the case with the swept parameter bound to that row's value, so it reports what that run computed and how long it took:

%instantiate An::ship
%sweep An::CostAnalysis An::ship limit=10.0..30.0:10.0
sweep An::CostAnalysis — 3 run(s)
limit | total | verdict                   | time
------+-------+---------------------------+--------
10.0  | 12.0  | affordable: not satisfied | 0.510ms
20.0  | 12.0  | affordable: satisfied     | 0.022ms
30.0  | 12.0  | affordable: satisfied     | 0.012ms

%samples 3 42 An::CostAnalysis An::ship limit=10.0..30.0
samples An::CostAnalysis — 3 run(s), seed 42
limit              | total | verdict                   | time
-------------------+-------+---------------------------+--------
26.509450139960897 | 12.0  | affordable: satisfied     | 0.016ms
10.856399027228605 | 12.0  | affordable: not satisfied | 0.015ms
25.521460994239078 | 12.0  | affordable: satisfied     | 0.013ms

The endpoints are expressions evaluated where the session evaluates one, units included, and a calc is swept the same way (%sweep An::Sum(2.0) b=0.0..10.0:2.5). The values a range produces are typed by the parameter it sweeps, not by how its endpoints are spelled: limit : Real swept over 10..30:10 is bound to 10.0, 20.0, 30.0, an Integer parameter swept over 1.0..3.0:1.0 to 1, 2, 3, and a range an Integer parameter cannot take — a fractional endpoint or step — is refused before any run rather than failing row by row, as is a range over a Boolean, String, enumeration or non-scalar parameter. A range read as reals takes an Integer endpoint or step only where a Real holds it without rounding (every Integer up to 2⁵³ in magnitude does), and steps only where the reals tell its rows apart, so no two rows bind one value: a Real parameter swept from 2⁶⁰ to 2⁶⁰+3 is refused, not collapsed onto one row. A parameter declaring no type takes the range as written, and the table says so. Sampling follows the same type — an Integer parameter draws Integers inclusively, a Real one draws reals in [<from>, <to>), however the endpoints are written. Several ranges run their cartesian product, a failed run is a row of the table rather than the end of it, and reference/repl-commands.md states each refusal. Sampling is uniform over the range — the bundled library defines no probability distribution, so a distribution asked for by name is refused naming what is missing.

Every row is a run of its own: it gets a fresh context, instantiates the case's subject there, and no row sees a value another row wrote. The arguments are evaluated once, at the prompt, and their values carried into every row — %sweep An::Price(base = ship.cost) n=1..4 reads ship.cost as the session holds it, a run's writes included, not as the declaration would make it; an argument naming an object binds, in each row, the object the row makes for it under the rule below, so a row's writes through it stay in the row. Rows run %jobs at a time and the table comes out in range order whatever order they finish in, so the table is the same at any count — only the time column, which is each row's own wall time, varies. A sweep on an object the session holds, as An::ship above, runs each row on a fresh An::Ship made from the same declaration, not on the held object, and the held object is untouched afterwards. That stands for the held object exactly while it is as its declaration made it — an object reached through a feature of another, fleet.flagship, is made again by instantiating fleet's declaration and walking to its flagship — and while every behavior its type exhibits or performs is still as its start left it, as it is fresh from %instantiate. Once the object is not as its declaration made it — named by #<id>, a feature of it written by a run, its state machine moved by a %send or an %advance, its performed action gone past a wait — each row runs instead on a copy of it: an image of the held object and everything it holds, taken once when the sweep begins and made afresh in every row's context under the same identities, so a row reads the written feature, the current state and the parked action as the session holds them, writes only its own copy, and the held object is untouched afterwards. An object destroyed, or one whose state no image can carry into a fresh context — a body paused mid-statement, such as a do action waiting at an accept, whose continuation points into the running model — is refused naming the reason rather than run on shared state; %instantiate it afresh and sweep that. An argument naming a held object is carried the same way, the row's own copy bound in place of it, and refused the same way when no copy can be made.

Trade studies

A trade study is an analysis case the library defines (TradeStudies::TradeStudy, SysML v2 §7.22): its subject studyAlternatives : Anything[1..*] lists the alternatives, its evaluationFunction is a calc scoring one of them, and its tradeStudyObjective — a MinimizeObjective or MaximizeObjective — states which score is best. The library writes the rest itself: the objective's best is alternatives->minimize {in x; eval(x)} (or maximize), its requirement is eval(selectedAlternative) == best, and the case returns studyAlternatives->selectOne {in ref a {} tradeStudyObjective(selectedAlternative = a)}. A model supplies the alternatives, the scoring calc and the direction:

package Trade {
    private import ScalarValues::*;
    private import TradeStudies::*;
    part def Engine { attribute mass : Real; }
    part a : Engine { attribute :>> mass = 30.0; }
    part b : Engine { attribute :>> mass = 10.0; }
    part c : Engine { attribute :>> mass = 10.0; }
    analysis lightest : TradeStudy {
        subject : Engine[1..*] = (a, b, c);
        objective : MinimizeObjective;
        calc :>> evaluationFunction {
            in part e :>> alternative : Engine;
            return :>> result : Real = e.mass;
        }
        return part :>> selectedAlternative : Engine;
    }
}

%analysis and -analysis run it as they run any case — nothing about the run is specific to trade studies. The evaluationFunction is a calc held as a value, bound into the objective's in calc :>> eval and applied by eval(x) inside the library's minimize/maximize and selectOne bodies; the objective's require constraint is inherited from the library and checked as any objective's is. What is new in the report is the evaluations the run made: each application of the case's own calc as a value, in subject order, with what it computed and whether its alternative is the one the case returned:

$ sysml -analysis Trade::lightest trade.sysml
✓ package Trade
✓ Trade::lightest
  selectedAlternative = Trade::b (object #2)
  objective tradeStudyObjective: satisfied
  evaluationFunction(Trade::a (object #1)) = 30.0
  evaluationFunction(Trade::b (object #2)) = 10.0 [selected]
  evaluationFunction(Trade::c (object #3)) = 10.0 [tied]

b and c score the same. The library's selectOne is select {…}#(1), the first element the predicate holds for, so b is the pick and the objective is satisfied — and c is marked [tied] so the tie is visible rather than a silent first-wins. %trace on shows the same order: the subject bound, minimize applying evaluationFunction to each alternative in turn, best read, then selectOne checking each alternative's eval(selectedAlternative) == best.

An evaluationFunction that fails for one alternative — a division by zero, a feature with no value — is an evaluation reported with its error, the alternatives before it keep their values, nothing is selected, and the objective is undecided naming the failure; the run fails with the same message. One declared without a body (abstract calc evaluationFunction, or a redefinition whose nested rollup calcs bind no result) fails the same way at the first alternative, naming the calc that has no return expression, so a study that states no way to score its alternatives is never answered with a fabricated pick. A subject listing no alternative, or one redeclared as Engine[1] and bound to several, is a multiplicity violation against the library's [1..*] before any alternative is evaluated; a subject restating no multiplicity (subject : Engine = (a, b);) inherits [1..*] from studyAlternatives (KerML §7.3.4.5) and runs.

Swept (%sweep, -sweep, -samples), each row carries that run's evaluations beside its outputs and verdict, so a table shows where the pick changes as a parameter moves — and a row whose run failed keeps the evaluations it made before failing:

%sweep Trade::weighted powerWeight=0.0..1.0:0.5
sweep Trade::weighted — 3 run(s)
powerWeight | selectedAlternative        | verdict                        | evaluations                                                                                                            | time
------------+---------------------------+--------------------------------+------------------------------------------------------------------------------------------------------------------------+--------
0.0         | Trade::light (object #2)  | tradeStudyObjective: satisfied | evaluationFunction(Trade::strong (object #1)) = -30.0; evaluationFunction(Trade::light (object #2)) = -10.0 [selected] | 3.053ms
0.5         | Trade::strong (object #1) | tradeStudyObjective: satisfied | evaluationFunction(Trade::strong (object #1)) = 120.0 [selected]; evaluationFunction(Trade::light (object #2)) = 15.0  | 0.215ms
1.0         | Trade::strong (object #1) | tradeStudyObjective: satisfied | evaluationFunction(Trade::strong (object #1)) = 270.0 [selected]; evaluationFunction(Trade::light (object #2)) = 40.0  | 0.153ms

The run and %optimize answer different questions. The run is the model's own answer: it evaluates every alternative the subject lists and reports the one the library's selectOne picks, with no solver. %optimize asks a solver for the best values a case's conditions admit over a continuous domain — it is for an objective whose eval states an expression over the case's parameters, not for choosing among listed alternatives — and on a trade study whose objective applies the case's evaluationFunction it refuses, pointing here:

%optimize Trade::lightest
error: analysis lightest: objective tradeStudyObjective not optimizable: it applies the calculation `evaluationFunction` to each alternative the case's subject lists, a choice among listed alternatives rather than an optimum over a continuous domain (run the trade study as an analysis (`-analysis`, `%analysis` or RunAnalysis), which evaluates every alternative and reports the one selected) at trade.sysml:9:9

The evaluations cross -json as each check's evaluations and the gRPC API as RunAnalysisResponse.evaluations and each SweepRow.evaluations under the case_evaluations capability, so a client reads the same per-alternative table (reference/wire-contract.md).

Running a continuous model

The analysis library StateSpaceRepresentation (SysML v2 Domain Libraries, Analysis/) declares a state-space protocol: StateSpace, Input and Output are vector quantities, ContinuousStateSpaceDynamics is an action with a stateSpace, an input, an output, a getDerivative and a getOutput, and DiscreteStateSpaceDynamics has a getDifference in place of the derivative. An action specializing either is run by time-stepping. The bundled StateSpaceIntegration library (an OpenSysML extension, not part of the OMG release) adds what the protocol leaves open: FixedStepDynamics states the timeStep, an optional stopTime and a time the runner writes; Euler and RK4 are the integrators; ZeroCrossing is the event a guard raises when it changes sign.

package Decay {
    private import ScalarValues::*;
    private import SI::*;
    private import VectorFunctions::*;
    private import StateSpaceRepresentation::*;
    private import StateSpaceIntegration::*;

    attribute def DecayState :> StateSpace;
    attribute def DecayInput :> Input;
    attribute def DecayOutput :> Output;

    action decay : ContinuousStateSpaceDynamics, FixedStepDynamics {
        attribute rate : Real = 0.5;
        in :>> input : DecayInput = 0 [m] * VectorOf((0.0));
        :>> stateSpace : DecayState = 1 [m] * VectorOf((1.0));
        :>> timeStep = 0.1 [s];
        :>> stopTime = 2 [s];

        calc :>> getDerivative {
            in input : DecayInput;
            in stateSpace : DecayState;
            return : StateDerivative = (0.0 - rate) * stateSpace / 1 [s];
        }
        calc :>> getOutput {
            in input : DecayInput;
            in stateSpace : DecayState;
            return : DecayOutput = 2.0 * stateSpace;
        }
    }
}

The action is run as any action is — -action, %action, ExecuteAction. Each step advances the runtime's clock by timeStep, computes the next state from getDerivative, writes stateSpace, time and output, and the run ends when the next step would pass stopTime. The action's outputs are the final values:

$ sysml -action Decay::decay -trace decay.sysml
✓ package Decay
[trace] state: decay t=0.0 x=⟨1.0⟩ [m] y=⟨2.0⟩ [m]
[trace] state: decay t=0.1 x=⟨0.9512294270833334⟩ [m] y=⟨1.9024588541666667⟩ [m]
[trace] state: decay t=0.2 x=⟨0.9048374229492866⟩ [m] y=⟨1.8096748458985732⟩ [m]
...
    output = ⟨0.7357589222950793⟩ [m]
    stateSpace = ⟨0.36787946114753967⟩ [m]
    time = 2.0 [s]

-trace (and %trace on) records one state: <action> t=<instant> x=<state> y=<output> line per step, the initial sample first, in the same trace the actions and states around it write to. The final state is within 3e-8 of the closed form e^-1 = 0.36787944…: the model above binds no integrator, and a model that binds none steps by classical fourth-order Runge–Kutta (RK4). To choose, bind getNextState's integrate to one of the two the runtime provides:

calc :>> getNextState {
    calc :>> integrate : Euler;
}

Forward Euler advances by the derivative at the start of the step; the same system ends at 0.3585, first-order error visible against RK4. A getNextState the model bodies itself is applied as written instead. DiscreteStateSpaceDynamics steps the same way with no integrator at all: each step adds getDifference(input, stateSpace) to the state.

The clock is the runtime's one clock, so a state machine exhibited beside the dynamics sees the same time: its accept after/at triggers fire in step order, and a state's entry action can read dynamics.output as the state the last step wrote. When a step and a timed trigger fall due at one instant, which runs first is a due order choice point the scheduling policy decides (see When a model has more than one valid run), never an implicit order; explore and the checker enumerate both. A model that states no stopTime steps as far as the clock is driven — %advance 5 steps it to 5 s; run to completion alone it stops at the step budget.

A guard raises an event when it crosses zero. The dynamics declare an event occurrence of a type specializing ZeroCrossing, bind its guard — any expression over the state, the input and time — and, to end the dynamics at the crossing, set terminal:

part def Lander {
    action def Touchdown :> ZeroCrossing;

    perform action fall : ContinuousStateSpaceDynamics, FixedStepDynamics {
        :>> stateSpace : FallState = VectorOf((19.0, 0.0));
        :>> timeStep = 0.5 [s];
        event occurrence touchdown : Touchdown {
            :>> guard = stateSpace.elements#(1);
            :>> terminal = true;
        }
        // getDerivative, getOutput, input as above
    }

    state flight {
        entry; then falling;
        state falling;
        accept Touchdown then landed;
        transition first falling accept after 10 [s] then lost;
        state lost;
        state landed;
    }
}

After each step every guard is evaluated; one that changed sign since the previous step, or has just reached exactly zero, posts an event of its type — once: a guard resting at zero raises no more — which the machine's accept Touchdown takes as it takes any signal, and the trace records event: zero crossing touchdown of fall (t=2.0). Two performances of the same dynamics each post their own event, so a machine accepting fall.touchdown takes its performer's crossing. The crossing is located to the end of the step that detected it: the event carries that step's instant and the state the machine reads is the post-step state, so a crossing is reported up to one timeStep late and a guard that crosses and crosses back inside one step is not seen — choose the step for the guard as much as for the integration.

A shape the runner cannot run truthfully is an error naming the action and the member at fault, never a wrong result: a stateSpace, input, derivative or output that is not a vector or is not bound; a getDerivative, getDifference or getOutput left abstract; an integrate bound to a calc other than Euler or RK4; a timeStep that is absent, zero, negative or not a duration; a state that leaves a step non-finite.

For all of this section run end to end on one model — an analysis with its objective, a verification case with its body verdict, a sweep and a sample, two trade studies and an action waiting on the clock a state machine also runs on, from the command line, the REPL and Python — see examples/analysis-demo.

Token-flow patterns

Each model below is in examples/action-executor-demo.sysml, and the output shown is from sysml -action ActionExecutorDemo::<name> examples/action-executor-demo.sysml.

Sequential: start → action → done

The sequential flow below uses standard implicit succession notation:

action sequential {
    attribute result : Integer = 0;

    first start;
    then action compute { assign result := 42 * 2; }
    then done;
}

A single token is created at start, moves to compute, which runs its body, and is consumed at done:

$ sysml -action ActionExecutorDemo::sequential examples/action-executor-demo.sysml
✓ Action completed
  Final state: Completed
  Results:
    result = 84

Fork and join: parallel paths

fork and join are action node literals, so this action is standard notation.

action forkJoin {
    attribute task1 : Integer = 0;
    attribute task2 : Integer = 0;
    attribute task3 : Integer = 0;

    first start;
    fork split;
    action left { assign task1 := 10; }
    action middle { assign task2 := 20; }
    action right { assign task3 := 30; }
    join sync;

    succession first start then split;
    succession first split then left;
    succession first split then middle;
    succession first split then right;
    succession first left then sync;
    succession first middle then sync;
    succession first right then sync;
    succession first sync then done;
}

fork places a token on each outgoing succession. join is an AND-join: it waits for a token on every incoming succession before a single token continues. A fork duplicates control, not values: all three branches are steps of the same performance, so every assignment is visible when it completes.

$ sysml -action ActionExecutorDemo::forkJoin examples/action-executor-demo.sysml
✓ Action completed
  Final state: Completed
  Results:
    task1 = 10
    task2 = 20
    task3 = 30

If a branch never arrives, that is a deadlock rather than a failure, and the run is reported as undecided.


Decision and else: conditional branching

decide with a guarded branch and an else branch is standard notation. The OpenSysML spelling decision is the one that produces a warning.

action conditional {
    attribute x : Integer = 15;
    attribute taken : Integer = 0;

    first start;
    action pathA { assign taken := 1; }
    action pathB { assign taken := 2; }

    succession first start then check;
    succession first pathA then done;
    succession first pathB then done;

    decide check;
    if x > 10 then pathA;
    else pathB;
}

decide evaluates its guards in the order written, with the action's features in scope, and takes the first guard that holds. The else branch is taken when no guard holds. With x = 15:

$ sysml -action ActionExecutorDemo::conditional examples/action-executor-demo.sysml
✓ Action completed
  Final state: Completed
  Results:
    taken = 1
    x = 15

Setting x to 5 gives taken = 2. The state-machine counterparts (orthogonal regions, choice and junction) appear in examples/orthogonal-regions-demo.sysml and examples/pseudostates-demo.sysml, and every case the executors are tested against lives under internal/exec/runtime/testdata/conformance/.

Terminate: ending an action early

terminate ends the performance it is written in, keeping whatever it assigned so far. As a node of the flow — then terminate;, or a named terminate action usage reached by a succession (then stop; action stop terminate;) — it ends the action, so nodes after it do not run and a forked sibling branch still running is dropped, an accept it never received included. A terminate action usage's body may declare pins and statements, which run before it ends the action — a terminate; among them ends the usage's own performance there, and the usage still ends the action; a flow of its own (first, successions) it may not state. As a statement of a nested action node's body it ends only that node: the rest of the body is skipped, the node's own fork branches are dropped, and the parent continues along the node's succession with the values the node assigned before it ended. terminate <name>; names an action node of the flow it is in or of a flow around it — the node itself (action c1 { terminate c1; }), the node whose body it runs in, or a sibling node still running — and ends every performance of that node still going on, the earliest begun first, and every one a token of the flow is parked at without having begun: a forked sibling the schedule has not reached yet, or an accept still waiting for its signal. Such a performance ends there, its body never run, and the flow goes on along the node's succession — which is how §7.17.10's MonitoredActivity works, its waitForTimeOut branch terminating performCriticalActivity whatever that has done so far. -trace writes every dropped token and every performance ended this way (ended waiting, ended before it began).

action bounded {
    out attribute x : Integer = 0;
    out attribute y : Integer = 0;

    first start;
    then action c1 {
        assign x := 1;
        terminate;
        assign x := 2;
    }
    then action c2 { assign y := 3; }
    then done;
}

Here c1 ends after its first assignment, and c2 still runs: x = 1, y = 3.

terminate <occurrence>; ends an object instead: terminate this; in an action a part performs ends the part (an exhibited or performed behavior is a performance the object owns, so this there is the object), and a feature chain or an expression evaluating to an object ends that object — terminate sub.worker; from a controller's action. The object's lifetime ends at the statement, its portions with it, and every behavior it performs or exhibits ends where it stands: an action of it keeps what it assigned and drops its tokens, a state machine it exhibits is terminated with no state exited and its do behaviors abandoned, while an action of another object runs on. %instances lists the object as ended and %features shows the behaviors it exhibited or performed as terminated. Terminating a value that is no occurrence, a chain that names no object, an object already destroyed or a performance that already ended is reported (occurrence cannot be terminated, performance already ended), never ignored; and a behavior started for an object that ended is refused. Inside a state's entry, do or exit body a terminate ends that behavior — the containing action of the statement — so the rest of the body does not run, the state stays active and the machine keeps dispatching; a braced body (entry { assign e := 1; terminate; assign e := 9; }, the same do { … }, exit { … } and a transition's do { … }) is one anonymous action, so the block is the action it ends and the statements after the terminate do not run either, while a named action beside the block (entry action first { … }) still does; a transition to a terminate action ends the machine instead (above). A calculation is pure and refuses terminate as it refuses send.

A run that stops early, whether through deadlock or by hitting a budget, is reported as an undecided check rather than a failure. The budgets are documented in reference/environment.md.


Next: 7. Saving, and converting to RDF.