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%advancethat 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 anifbranch or a loop body declares included;%continueand%stepstop 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.
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 bis aHappensBeforelink:acompletes beforebbegins. 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
acceptthat waits for asendin 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.
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 = 1The 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 tokenstokens 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.
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 = 2The 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.
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 $?
2incomplete 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"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.
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.
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;
}
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
plies in0.0..1.0; - where every
pout of one decision is a constant — a literal or arithmetic over literals,1.0 - 0.3as much as0.7— they sum to1.0within1e-6; apthat is an expression over the action's features is evaluated when the decision is reached and refused then, as a typedinvalid branch weightserror naming the decision and the branch, if it is no number, no finite one, or one outside0.0..1.0, or if the holding weights sum to nothing positive; - a
pthat names a feature is typed where the annotation is written: the feature's type must beRealorInteger, sop = readyover aBoolean, aString, an enumeration or a part is a type error before anything runs, whilep = pFastover aRealattribute of the action, of the object performing it (thischain) or of aninparameter 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
Probabilitywith nop, two, an attribute it does not declare, or apthat 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).
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.
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).
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:
- the witness a
replay:<file>follows, whose recorded draws the run consumes (below); - an explicit model seed — the CLI's
-seed <n>, the REPL's%seed <n>, a conformance case's"modelSeed"; - 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.88520359371734A 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.
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.
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.
- State transitions carry no weight.
@Probabilityon atransitionis refused at lowering with a typed error; only successions out of a decision node are weighted. exploreandcheckdo 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.
%runsand-runsrun an action repeatedly; theRunSweepRPC 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
RandomFunctionsonly.min,maxandaveragefix the durations and values the four functions return; a weighted decision draws from the seed under every policy, andnormalhas nominormaxunless its deviation is zero. Exploration and the checker enumerate a weighted decision's branches whatever the policy.
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
DurationConstrainton 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'smin,maxandaveragemodes 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 whoseProbabilityBTOOP : Realthe run configurations set to1.0or0.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 anaction defthat declarespart target : <the migrated execution target>— theindividual defthe target instance became, whose slots are its attribute values — andperform action run ::> target.<the classifier behavior>, with the tool'snumberOfRunsanddurationSimulationModeas@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=clockA 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.
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.
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.0The 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: satisfiedA 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).
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: passThe 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: passA 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.
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).
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.
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.
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 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 = 30If a branch never arrives, that is a deadlock rather than a failure, and the run is reported as undecided.
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 = 15Setting 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 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.