Query Cookbook¶
Every recipe in this chapter runs against one model,
examples/cookbook.sysml, and every output shown
is what the sysml binary printed. The model is a small observatory:
package Cookbook {
private import DocumentQueries::*;
private import KerML::Root::Element;
private import ScalarValues::*;
part def Subsystem {
attribute mass : Real;
assert constraint massKnown { mass > 0.0 }
}
part def OpticalSubsystem :> Subsystem;
part def MirrorAssembly :> OpticalSubsystem {
attribute :>> mass = 10.0;
assert constraint lightweight { mass < 5.0 }
}
metadata def Critical;
port def OpticalPort;
port def DataPort;
part telescope {
part primaryMirror : MirrorAssembly {
@Critical;
port opticalOut : OpticalPort;
}
part instrumentCluster : Subsystem {
attribute redefines mass = 4.5;
port opticalIn : OpticalPort;
port dataOut : DataPort;
}
part mountControl : Subsystem {
attribute redefines mass = 15.0;
port dataIn : DataPort;
}
connection opticalPath connect primaryMirror.opticalOut to instrumentCluster.opticalIn;
connection dataPath connect instrumentCluster.dataOut to mountControl.dataIn;
}
part def Computer;
part scienceComputer : Computer;
allocation processing allocate telescope.instrumentCluster to scienceComputer;
requirement massRequirement;
part observatory {
satisfy massRequirement by telescope;
}
verification def MassTest;
verification massVerification : MassTest {
objective {
verify massRequirement;
}
}
// ... the recipe queries below ...
requirement def PointingRequirement {
doc /* The telescope holds a target within the stated accuracy. */
}
requirement <'REQ-2'> pointingRequirement : PointingRequirement {
doc /* The telescope points at a target to within 2 arcseconds. */
requirement <'REQ-2.1'> slewRequirement {
doc /* The mount reaches a new target within 60 seconds. */
}
requirement <'REQ-2.2'> trackingRequirement {
doc /* The mount tracks a target for 30 minutes without drift. */
}
}
verification def PointingTest;
verification pointingVerification : PointingTest {
objective {
verify pointingRequirement;
}
}
// ... and the coverage recipes ...
}
Each recipe is a calc def specializing DocumentQueries::Query declared in
the same package. Run one with:
A binding whose expression is a name binds the element it denotes; anything else is evaluated as an expression (strings in quotes, numbers as literals).
Anatomy of a query¶
calc def MassTable :> Query {
in root : Element; // entry parameters, bound by the caller
Project( // operations compose inside-out
source = PartsByMass(root = root), // ... and queries invoke queries
properties = ("name", "mass", "qualifiedName")
)
}
- A query is a
calc defspecializingDocumentQueries::Query. - Its
inparameters are the entry bindings a caller supplies — an element, a string, a number, a boolean, or a sequence of them. - Its body is one expression composing the library operations;
sourcearguments chain them, innermost first. A name in an argument reads the query's parameter of that name, or binds the model element it refers to (OwnedElements(source = telescope)starts from that part), just as a%run-querybinding does. The element is checked against the parameter's type when the query is planned. - A query can invoke another query by name, with its own bindings. Invocation is dependency-ordered and cycle-checked, with depth and count budgets.
Results are ordered element sequences. Order is the model's declaration order
until an OrderBy says otherwise, and elements are deduplicated by identity,
so a query is deterministic by construction.
Parameter defaults¶
An in parameter may declare a default, and a caller that leaves it unbound
gets that default — from %run-query, -run-query, RunDocumentQuery, a
document's content block, or another query's invocation alike:
calc def HeavySubsystems :> Query {
in root : Element = telescope; // a name binds the element it refers to
in threshold : String default "10"; // anything else is evaluated
WhereFeature(
source = Descendants(source = root, maxDepth = 3),
'feature' = "mass", operator = ">=", value = threshold
)
}
calc def LightSubsystems :> HeavySubsystems {
in redefines threshold default "5"; // a redefining default wins
}
- A default follows the binding rule of
%run-query <p>=<expr>: a default that names a model element binds that element; any other default is an expression. The rule applies wherever a value is expected, soin roots : Element[0..*] = (telescope, groundStation);binds both elements, a list may mix element names with parameters and query invocations, andin candidates : Element[0..*] = OwnedElements(source = telescope);starts the traversal from that part. - An expression default is evaluated once per query execution, before any row is
produced, in the scope of the query that declared it — it may name that
query's other parameters (
in candidates : Element[0..*] = OwnedElements(source = root);) or invoke another query, within the usual visit and invocation budgets. Defaults are filled in parameter order after the explicit bindings, so a default may read a parameter bound explicitly or defaulted before it; one that reads a later, still unbound parameter fails as a missing binding. - Defaults are inherited:
LightSubsystemskeepsroot = telescopefromHeavySubsystems, and its ownthresholddefault replaces the inherited one. The nearest default along the redefinition chain wins. - A default is checked against the parameter's type and multiplicity exactly
like an explicit binding, and an explicit binding always overrides the
default. What the default's text already settles is refused when the query is
planned, as
document-query-default-typeordocument-query-default-multiplicitynaming the parameter: a literal or named element of the wrong type, and a list or invocation whose size cannot fit (in source : Element = (telescope, groundStation);). A named element is never a data value —= labelis refused for aStringparameter even whenlabelis aStringattribute — except an enumeration literal, which is a value of its enumeration:in hue : Color = Color::red;binds the literal, and a literal of another enumeration is refused. What only the values decide — a parameter reference whose multiplicity is not known statically — is checked when the default is evaluated, with the failures an explicit binding gets. - A default the plan cannot represent (a form the query expression language has
no operation for) is a planning error naming the parameter, reported with the
other
document-query-*diagnostics rather than at execution time.
Collection¶
Direct children: OwnedElements¶
$ sysml cookbook.sysml -run-query "Cookbook::Children root=Cookbook::telescope"
✓ Query Cookbook::Children returned 5 rows
Row 1: Cookbook::telescope::primaryMirror
Row 2: Cookbook::telescope::instrumentCluster
Row 3: Cookbook::telescope::mountControl
Row 4: Cookbook::telescope::opticalPath
Row 5: Cookbook::telescope::dataPath
Everything the element owns is returned — here the three parts and the two connections, in declaration order. Filter afterwards to narrow.
Descendants to a depth: Descendants¶
calc def AllParts :> Query {
in root : Element;
WhereType(
source = Descendants(source = root, maxDepth = 10),
type = "PartUsage"
)
}
$ sysml cookbook.sysml -run-query "Cookbook::AllParts root=Cookbook::telescope"
✓ Query Cookbook::AllParts returned 5 rows
Row 1: Cookbook::telescope::primaryMirror
Row 2: Cookbook::telescope::instrumentCluster
Row 3: Cookbook::telescope::mountControl
Row 4: Cookbook::telescope::opticalPath
Row 5: Cookbook::telescope::dataPath
maxDepth bounds the walk; each level is visited in declaration order. Omit
it (or pass null) to walk the whole subtree — Ancestors likewise walks to
the root when unbounded.
Note that the connections are still here: a connection usage is a
PartUsage in the SysML metamodel (its metaclass conforms to it). Use
a feature or name filter, or type = "ConnectionUsage", to separate them —
see Type filters.
Ancestors: Ancestors¶
$ sysml cookbook.sysml -run-query "Cookbook::Enclosing leaf=Cookbook::telescope::primaryMirror::opticalOut"
✓ Query Cookbook::Enclosing returned 2 rows
Row 1: Cookbook::telescope::primaryMirror
Row 2: Cookbook::telescope
Owners are returned nearest-first, up to maxDepth levels.
Elements by qualified name: Named¶
calc def NamedParts :> Query {
WhereType(
source = Descendants(source = Named(qualifiedName = ("Cookbook::telescope", "Cookbook::Traceability"))),
type = "PartUsage"
)
}
$ sysml cookbook.sysml -run-query "Cookbook::NamedParts"
✓ Query Cookbook::NamedParts returned 12 rows
Row 1: Cookbook::telescope::primaryMirror
Row 2: Cookbook::telescope::instrumentCluster
...
Row 6: Cookbook::Traceability::gimbal
...
A query parameter must be bound to a feature, so a walk rooted at a package
or a definition has nothing to bind root to. Named resolves qualified
names — spelled as strings, like the types WhereType takes — to the elements
they name, in the order given, and any element may be named, a package or
definition included. A name that resolves to nothing, or to more than one
element, fails the query with the name quoted rather than returning fewer rows.
The SysML v1 migration roots every table scope this way.
Type filters¶
WhereType keeps elements whose metamodel type matches — "PartUsage",
"ConnectionUsage", "RequirementUsage", "AttributeUsage", "PortUsage",
"PartDefinition" and so on — including metaclass conformance, so
type = "Usage" keeps every kind of usage. A metamodel type name means the
metaclass even when an element of the model bears the same name; qualify the
element's name to mean the element. Several names keep the elements of any of
them: type = ("PartUsage", "PortUsage"). A name that is neither a
known metamodel type nor resolvable in the model is a typed
unknown-classification error rather than a silently-empty result.
calc def Connections :> Query {
in root : Element;
WhereType(
source = Descendants(source = root, maxDepth = 10),
type = "ConnectionUsage"
)
}
$ sysml cookbook.sysml -run-query "Cookbook::Connections root=Cookbook::telescope"
✓ Query Cookbook::Connections returned 2 rows
Row 1: Cookbook::telescope::opticalPath
Row 2: Cookbook::telescope::dataPath
To select by a model-defined classification — "every part typed by
Subsystem" — filter on what distinguishes those elements instead: a
metadata annotation (below) or a characteristic
attribute (property filters).
Metadata filters¶
WhereMetadata keeps elements annotated with a metadata definition, matching
specializations of it too; several names keep the elements annotated with any
of them. The model marks primaryMirror with @Critical:
calc def CriticalParts :> Query {
in root : Element;
WhereMetadata(
source = AllParts(root = root),
'metadata' = "Cookbook::Critical"
)
}
$ sysml cookbook.sysml -run-query "Cookbook::CriticalParts root=Cookbook::telescope"
✓ Query Cookbook::CriticalParts returned 1 row
Row 1: Cookbook::telescope::primaryMirror
('metadata' is quoted because metadata is a SysML keyword.)
Name filters¶
WhereName compares each element's effective name against a value:
calc def MirrorParts :> Query {
in root : Element;
WhereName(
source = AllParts(root = root),
operator = "contains",
value = "Mirror"
)
}
$ sysml cookbook.sysml -run-query "Cookbook::MirrorParts root=Cookbook::telescope"
✓ Query Cookbook::MirrorParts returned 1 row
Row 1: Cookbook::telescope::primaryMirror
Text operators: =/==, !=/<>, contains, startsWith, endsWith
(also spelled starts-with/ends-with), and matches with a regular
expression.
WhereText applies the same operators to a projected table's cells — the
query form of a table's search box:
calc def HeavyMirrorRows :> Query {
in root : Element;
WhereText(
source = MassTable(root = root),
columns = ("name", "qualifiedName"),
operator = "contains",
value = "Mirror"
)
}
A row is kept when one of the named columns — every projected column when
columns is omitted — holds a value whose text satisfies the comparison. A
value is compared as plain text — an element by its effective name, a number
in base 10, a boolean as true/false, a quantity with its unit — each value
of a multi-valued cell on its own; an empty cell matches nothing. The rows keep
their projected columns and widths, and their nesting as any filter does: a row
whose ancestors it dropped nests under the row before it. A column the source does
not project, or a matches pattern that is not a valid regular expression, is
a typed error naming it.
Property filters¶
WhereFeature compares an attribute's constant value. The comparison is
typed: numbers compare numerically (<, <=, >, >= and equality, with
* accepted as infinity), booleans by equality, strings with the text
operators above, and an element-valued feature — a verdict's assertion, a
RelatedColumn list, an attribute whose value names an enumeration literal or
a part — as the name it prints by, with the text operators, or as its qualified
name when the value written is qualified (holds ::). An element without the attribute simply does not match; a
property no element in the source has is a typed unknown-property error.
calc def HeavyParts :> Query {
in root : Element;
in threshold : String;
WhereFeature(
source = AllParts(root = root),
'feature' = "mass",
operator = ">=",
value = threshold
)
}
$ sysml cookbook.sysml -run-query "Cookbook::HeavyParts root=Cookbook::telescope threshold=\"10\""
✓ Query Cookbook::HeavyParts returned 2 rows
Row 1: Cookbook::telescope::primaryMirror
Row 2: Cookbook::telescope::mountControl
Two details worth noting: value is always written as a string and parsed by
the operator's type, and primaryMirror matches through its definition —
MirrorAssembly fixes mass = 10.0, and the usage inherits it.
The built-in properties of the projection table are features
too, so feature = "shortName" with startsWith selects the requirements
whose identifier shares a prefix, and feature = "documentation" with
contains selects the elements whose doc text mentions a word — any one of
an element's several bodies matching is enough.
Quantities¶
An attribute declared with a unit — attribute :>> mass = 2290000 [kg]; — is
a quantity: a magnitude carried with its unit, never a bare number. The
filter's value is a bare number, and it compares against the magnitude in
the attribute's own unit: mass >= "1000000" matches 2290000 [kg], and
would match 1500000 [g] too, because the threshold is read in each element's
unit. Choose the threshold for the unit your model declares, or normalize the
unit in the model. A value carrying a unit of its own ("1000 [kg]") is
not a number and is refused as a typed invalid-argument error.
calc def HeavyStages :> Query {
in root : Element;
WhereFeature(
source = WhereType(source = Descendants(source = root, maxDepth = 1), type = "PartUsage"),
'feature' = "mass",
operator = ">=",
value = "1000000"
)
}
$ sysml units.sysml -run-query "UnitsRepro::HeavyStages root=UnitsRepro::rocket"
✓ Query UnitsRepro::HeavyStages returned 1 row
Row 1: UnitsRepro::rocket::s1
The attribute compared need not be a literal: a mass declared as dryMass +
propellantMass is evaluated for each element before the comparison, as
derived values describes. Filtering and sorting then follow
the same commensurability rules as literal quantities.
Sorting¶
OrderBy sorts by a property with every policy explicit — there are no
defaults to guess:
direction:"ascending"or"descending".missing: where elements without the property go —"first","last", or"error"to refuse them.multiple: which value to sort by when the property has several —"first","last", or"error".
The sort is stable, so equal keys keep their declaration order. Mixing
incomparable value types across elements is a typed invalid-order error.
Quantities sort by converted magnitude when their units are commensurable:
500000 [g] orders below 119000 [kg]. Two Integer magnitudes compare
exactly, so neighbours a Real cannot tell apart (9007199254740993 [kg]
against 9007199254740992 [kg]) keep their order. Quantities of different dimensions
(2290000 [kg] against 42 [m]) are not ordered — that is an
invalid-order error naming both units, never a silent comparison of the
bare magnitudes.
calc def PartsByMass :> Query {
in root : Element;
OrderBy(
source = AllParts(root = root),
property = "mass",
direction = "descending",
missing = "last",
multiple = "error"
)
}
$ sysml cookbook.sysml -run-query "Cookbook::PartsByMass root=Cookbook::telescope"
✓ Query Cookbook::PartsByMass returned 5 rows
Row 1: Cookbook::telescope::mountControl
Row 2: Cookbook::telescope::primaryMirror
Row 3: Cookbook::telescope::instrumentCluster
Row 4: Cookbook::telescope::opticalPath
Row 5: Cookbook::telescope::dataPath
The connections have no mass, so missing = "last" places them after the
sorted parts.
Projection¶
Project turns elements into rows of named, typed cells — what a document
table renders. Beyond the model's own attributes, these built-in properties
are always projectable:
| Property | Value |
|---|---|
name |
The effective name |
declaredName |
The declared name, absent when the name is derived |
shortName |
The effective short name (<'HLR-R001'>), absent when the element has none |
declaredShortName |
The declared short name, absent when the short name is derived |
documentation |
The body of each doc comment in declaration order, delimiters and indentation removed; absent when undocumented |
qualifiedName, @id |
The fully-qualified name |
owner |
The owner's qualified name |
@type |
The metamodel type (PartUsage, ...) |
type |
The declared type's qualified name |
general |
The types a definition specializes or a usage is typed by, in declaration order, each the element itself — printed by name and, in HTML, linked and carrying its qualified name in data-element; absent when it specializes none |
isAbstract |
Boolean |
isIndividual |
Boolean: whether a definition or usage carries the individual modifier |
multiplicityLower, multiplicityUpper |
Integers, * as unbounded |
satisfiedRequirement |
The requirement a satisfy usage names, or the usage itself when it declares the requirement |
satisfyingFeature |
The feature named by a satisfy usage's by clause; a feature chain (by v.heater) reports the feature the chain ends at |
For example, the model's satisfy massRequirement by telescope; reports
satisfyingFeature = Cookbook::telescope and
satisfiedRequirement = Cookbook::massRequirement. Selecting these properties
on SatisfyRequirementUsage elements shows which parts satisfy which
requirements. A satisfy with no by clause has no satisfyingFeature.
calc def MassTable :> Query {
in root : Element;
Project(
source = PartsByMass(root = root),
properties = ("name", "mass", "qualifiedName")
)
}
$ sysml cookbook.sysml -run-query "Cookbook::MassTable root=Cookbook::telescope"
✓ Query Cookbook::MassTable returned 5 rows
Columns: name, mass, qualifiedName
Row 1: Cookbook::telescope::mountControl
name = "mountControl"
mass = 15.0
qualifiedName = "Cookbook::telescope::mountControl"
Row 2: Cookbook::telescope::primaryMirror
name = "primaryMirror"
mass = 10.0
qualifiedName = "Cookbook::telescope::primaryMirror"
Row 3: Cookbook::telescope::instrumentCluster
name = "instrumentCluster"
mass = 4.5
qualifiedName = "Cookbook::telescope::instrumentCluster"
Row 4: Cookbook::telescope::opticalPath
name = "opticalPath"
mass = (none)
qualifiedName = "Cookbook::telescope::opticalPath"
Row 5: Cookbook::telescope::dataPath
name = "dataPath"
mass = (none)
qualifiedName = "Cookbook::telescope::dataPath"
A cell for a property the element lacks is empty ((none) in the CLI's row
listing, an empty table cell in a document). A feature whose declared value
names an element of the model — an enumeration literal, a part, a unit — holds
that element, printed by name like general, whether the value is written on
the feature or bound by a redefinition (attribute :>> beam = Beam::'650mm';);
a name the model does not resolve stays the text as written.
Quantity cells¶
A quantity-valued attribute projects as the runtime prints it — magnitude,
then the unit in brackets — in the CLI row listing, a Markdown cell (brackets
escaped as \[kg\], so they read as text) and an HTML cell, whose
<span class="sysml-value" data-value-kind="quantity"> also carries
data-magnitude and data-unit apart. The unit is the one the model spelt
(kg, km/h), not a reduction to base units.
part def Stage {
attribute mass :> ISQ::mass;
}
part def FirstStage :> Stage {
attribute :>> mass = 2290000 [kg];
}
part def UpperStage :> Stage {
attribute :>> mass = 119000 [kg];
}
part rocket {
part s1 : FirstStage;
part s2 : UpperStage;
}
calc def Masses :> Query {
in root : Element;
Project(
source = WhereType(source = Descendants(source = root, maxDepth = 1), type = "PartUsage"),
properties = ("name", "mass")
)
}
$ sysml units.sysml -run-query "UnitsRepro::Masses root=UnitsRepro::rocket"
✓ Query UnitsRepro::Masses returned 2 rows
Columns: name, mass
Row 1: UnitsRepro::rocket::s1
name = "s1"
mass = 2290000 [kg]
Row 2: UnitsRepro::rocket::s2
name = "s2"
mass = 119000 [kg]
Derived values¶
A value that is a literal, a quantity, or an expression over those alone
(2 [kg] * 3 is 6 [kg], 1 [km] + 500 [m] is 1.5 [km]) is folded once,
statically, when the model is analysed. A value written over other features
— the common shape in a mass or power budget — is instead evaluated by the
runtime as seen from the row's element: each leaf is read through the
redefinition chain of that concrete carrier, so a type-level :>> and a
usage-level :>> both win over the definition's own value, a default applies
where nothing binds the feature, and a feature chain (s1.mass) reads the
owned part's value. Arithmetic, comparisons, conditionals and the library
functions the runtime provides (sum, size, indexing with #, ->collect)
all apply, with the runtime's rules: units are kept and converted, Integer
stays Integer, and a collection-valued attribute projects as one value per
element.
part def Stage {
attribute dryMass :> ISQ::mass;
attribute propellantMass :> ISQ::mass;
attribute mass :> ISQ::mass = dryMass + propellantMass;
}
part def FirstStage :> Stage {
attribute :>> dryMass default = 130000 [kg];
attribute :>> propellantMass = 2160000 [kg];
}
part def Vehicle {
part s1 : FirstStage;
part s2 : FirstStage {
attribute :>> dryMass = 120000 [kg];
}
attribute liftoffMass :> ISQ::mass = s1.mass + s2.mass;
}
part rocket : Vehicle;
$ sysml derived.sysml -run-query "DerivedRepro::Masses root=DerivedRepro::Vehicle"
✓ Query DerivedRepro::Masses returned 2 rows
Columns: name, dryMass, mass
Row 1: DerivedRepro::Vehicle::s1
name = "s1"
dryMass = 130000 [kg]
mass = 2290000 [kg]
Row 2: DerivedRepro::Vehicle::s2
name = "s2"
dryMass = 120000 [kg]
mass = 2280000 [kg]
$ sysml derived.sysml -run-query "DerivedRepro::Vehicles root=DerivedRepro"
✓ Query DerivedRepro::Vehicles returned 1 row
Columns: name, liftoffMass
Row 1: DerivedRepro::rocket
name = "rocket"
liftoffMass = 4570000 [kg]
The query and the REPL agree: rocket.s1.mass prints = 2290000 [kg] too.
Note that s2's dryMass overrides a default =; a value written with a
plain = is fixed for every redefinition, and the analyser refuses the
override before any query runs.
What the runtime cannot turn into a value is reported, never guessed:
- A leaf unbound anywhere in the carrier's chain (an abstract
attribute mass :> ISQ::mass;that nothing ever binds) makes the derived value absent — an empty cell, as for a value-less feature — andWhereFeaturedoes not match it,OrderByplaces it by itsmissingpolicy. - A value that genuinely depends on the model running — an
inparameter of a calculation, an action's state, a non-constant function — or that the runtime rejects — a cycle (a = b; b = a;), operands of different dimensions (mass + length), a result no cell can hold such as a part (attribute heart = engine.core;) — is a typedunevaluable-featureerror naming the query, the property, the row element and the runtime's reason. A table never shows a wrong number or a silently empty cell for a value the model does declare, and a??default does not cover it — the feature is present, not absent.
The roll-up a library writes over a possibly empty collection evaluates as
written: sum over no quantities is the zero of the collection's declared
kind, in its coherent SI unit, so mass + sum(subcomponents.totalMass) on a
component whose subcomponents : MassedComponent [*] default null holds
nothing is mass (100 [kg] + 0 [kg]), and a default null collection holds
the parts that subset it (part b1 : Bolt :> subcomponents;, or with
subsets) — the default is only its value where nothing populates it — so
the sum rolls up through them recursively. The kind is the collection's
declared one, and survives a select, reject or collect that leaves
nothing: a collection typed Real[*], or an empty one mapped through an
untyped body parameter (->collect { in x; x * x }), still sums to the
number 0, and 10 [kg] + 0 [m] or 10 [kg] + 5 remain the
incommensurable units error above.
A value written with = holds for as long as the object does, not just on
the first read: it is derived from what the object holds now. When a run
assigns a feature the expression read (assign a := 9;), when a binding
propagates a new value into it, or when a default null collection is
superseded by a part that subsets it, the derived value is dropped and
derived again the next time it is read — through a part or a binding the
expression read through, and on through the values that read it. Nothing is
recomputed until something asks, and a value a run assigned is never
recomputed: assign d := 100; fixes d whatever a does afterwards, while
a dd = d + 1 beside it keeps following d. A probe or transaction that
wrote such a feature is rolled back with the values that read it.
The parameters of a calculation or action usage are the boundary. They are
bound once, when the invocation starts, and stay bound while its outputs are
read — an assignment to a feature an in named does not rebind it for a
later output read of the same invocation. Only the = value of an object's
own feature follows what it read.
Computed columns¶
A projection may also derive columns: each Column entry supplies exactly one
of expression, cell, or path. An expression is evaluated once per row
over the row element's declared features; a cell receives the row as a typed
parameter; a path reads a dotted member path from the row. Arithmetic (+,
-, *, /), string concatenation with + and ?? defaults for absent
values are supported:
calc def MassBudget :> Query {
in root : Element;
Project(
source = PartsByMass(root = root),
properties = ("name", "mass"),
columns = (
Column(name = "massLbs", cell = { in row : Subsystem; (row.mass ?? 0.0) * 2.2 }),
Column(name = "label", expression = "part: " + Element::name)
)
)
}
$ sysml cookbook.sysml -run-query "Cookbook::MassBudget root=Cookbook::telescope"
✓ Query Cookbook::MassBudget returned 5 rows
Columns: name, mass, massLbs, label
Row 1: Cookbook::telescope::mountControl
name = "mountControl"
mass = 15.0
massLbs = 33.0
label = "part: mountControl"
...
Feature references name the declaring definition (Subsystem::mass,
Element::name); a row element that lacks the feature makes the expression
fail with a typed error naming the query, column and row — unless a ??
default covers it, which is why MassBudget's massLbs defaults to 0.0
for the two connections in its results. Computed names join the projection:
OrderBy can sort by them and a table's groupBy can group by them.
Every built-in property is reachable the same way — Element::shortName,
Element::declaredShortName and Element::documentation included — so
(Element::shortName ?? "—") + ": " + Element::name labels a row by its
identifier. A column holds as many values as the feature it reads declares:
Element::documentation is [0..*], so an element carrying two doc bodies
fills the cell with both in order (comma-joined in a document table) and one
carrying none leaves it empty, while a feature declared without a multiplicity
is one value per row — a row binding two fails the column with a typed
column-cardinality error naming the declared bound, and a row binding none
with column-absent unless ?? supplies a default — a value, or null to
leave that row's cell empty (Stage::mass ?? null). Operators always take one
value per operand, so Element::documentation + "." over two bodies fails.
A column expression may also be a feature chain — 'Monte Carlo'.runs,
stat.runs, outer.inner.value — reading a feature of a member nested in
the row element. Each segment names a member of the element the previous one
reached: the row's own members answer first, then inherited ones, and the
last segment reads that member's feature. A segment that is not a basic name
is quoted, as in the notation. In a properties/property string a feature
whose own name contains a period is read by that name first, the path only
the fallback. A row lacking a segment entirely makes the
path absent on that row — an empty cell, or a ?? default — and the path is
an unknown-property error only when no row reaches it; a member the path
finds that declares no value is an empty cell, and a multi-valued member
fills the cell with all of its values — more than its multiplicity admits
fails the column as a direct feature column does. The same path works as a properties
or property string ("stat.runs"), and OrderBy sorts by it. This is how
an individual's nested usage — an analysis the migrator writes, for instance —
contributes a column:
analysis def 'Template Group 1 Monte Carlo' {
out runs : ScalarValues::Natural;
out mean : ScalarValues::Real;
}
individual part def 'template Group 11' :> 'Template Group 1' {
analysis 'Monte Carlo' : 'Template Group 1 Monte Carlo' {
out :>> runs = 5;
out :>> mean = 18.0;
}
}
calc def RunCounts :> Query {
in root : Element;
Project(
source = Descendants(source = root, maxDepth = 1),
properties = ("name"),
columns = (Column(name = "runs", path = "'Monte Carlo'.runs"))
)
}
$ sysml cookbook.sysml -run-query "Cookbook::RunCounts root=Cookbook::Results"
✓ Query Cookbook::RunCounts returned 3 rows
Columns: name, runs
Row 1: Cookbook::Results::'template Group 11'
name = "template Group 11"
runs = 5
...
Quantities take part in column arithmetic with the runtime's rules, so a
column keeps its unit: Stage::mass * 2 is 4580000 [kg], Stage::mass /
1000 is 2290 [kg], Stage::mass / Stage::length is 54523.8… [kg/m],
and a ratio of like quantities (Stage::length / Stage::length) is a bare
number. Adding or subtracting quantities converts the right operand into the
left operand's unit (1 [km] + 500 [m] is 1.5 [km]); operands of
different dimensions — Stage::mass +
Stage::length, or a quantity plus a bare number — are a typed
column-incommensurable error naming the column, the row and both units.
Query invokes query¶
MassTable above already shows it: PartsByMass(root = root) invokes the
other query with its own bindings, and AllParts invokes Children's
sibling the same way. Factoring collection into one base query and deriving
filtered/sorted/projected variants from it is the intended style. The engine
compiles the invocation graph up front: an unknown name, a cycle, or blowing
the depth/count budget is a typed error at that point.
Relationship traversal¶
RelatedElements walks one named relationship kind from each source element:
RelatedElements(
source = <elements>,
relationshipKind = "<kind>", // specialization, subsetting, redefinition,
// typing, connection, allocation,
// satisfaction, verification,
// derivation or refinement
direction = "<direction>", // outgoing or incoming
maxDepth = <n> // omit, or null, for no bound
)
Direction is from the relationship's own point of view — outgoing follows
it as declared, incoming follows it backwards. Traversal is breadth-first
to maxDepth (unbounded when omitted or null), deduplicated, in
declaration order, and bounded by a visit budget so a pathological model
terminates with a typed error rather than hanging. The budget pays only for
the elements reached: the edge table a relationship kind reads is built once
per model (again after an edit), from every declaration in the workspace, and is not charged to it —
so a matrix over a large model costs what its rows relate to, not the model's size.
Connections¶
Connection edges run port to port — traverse from the connector's endpoint, not from the part that owns it:
calc def ConnectedTo :> Query {
in origin : Element;
RelatedElements(
source = origin,
relationshipKind = "connection",
direction = "outgoing",
maxDepth = 1
)
}
$ sysml cookbook.sysml -run-query "Cookbook::ConnectedTo origin=Cookbook::telescope::primaryMirror::opticalOut"
✓ Query Cookbook::ConnectedTo returned 1 row
Row 1: Cookbook::telescope::instrumentCluster::opticalIn
outgoing follows connect A to B from A's endpoint to B's; incoming
follows it the other way. Untyped connect clauses carry connection edges
too.
Allocations¶
allocate X to Y is outgoing from X, incoming to Y:
calc def AllocatedTargets :> Query {
in origin : Element;
RelatedElements(
source = origin,
relationshipKind = "allocation",
direction = "outgoing",
maxDepth = 1
)
}
$ sysml cookbook.sysml -run-query "Cookbook::AllocatedTargets origin=Cookbook::telescope::instrumentCluster"
✓ Query Cookbook::AllocatedTargets returned 1 row
Row 1: Cookbook::scienceComputer
Satisfy relationships¶
satisfy R by P points from the satisfying element to the requirement, so
"who satisfies this requirement" is an incoming traversal from the
requirement:
calc def SatisfiedBy :> Query {
in req : Element;
RelatedElements(
source = req,
relationshipKind = "satisfaction",
direction = "incoming",
maxDepth = 1
)
}
$ sysml cookbook.sysml -run-query "Cookbook::SatisfiedBy req=Cookbook::massRequirement"
✓ Query Cookbook::SatisfiedBy returned 1 row
Row 1: Cookbook::telescope
Verify relationships¶
Likewise, "which verifications cover this requirement" is incoming from the
requirement; the result is the verification usage whose objective verifys
it:
calc def VerifiedBy :> Query {
in req : Element;
RelatedElements(
source = req,
relationshipKind = "verification",
direction = "incoming",
maxDepth = 1
)
}
$ sysml cookbook.sysml -run-query "Cookbook::VerifiedBy req=Cookbook::massRequirement"
✓ Query Cookbook::VerifiedBy returned 1 row
Row 1: Cookbook::massVerification
Derive relationships¶
A requirement derivation is a connection conforming to the domain library's
RequirementDerivation::Derivation — typed by it, or written with the
#derivation semantic metadata. The cookbook model derives three requirements
from massRequirement, one of them at second hand:
requirement mirrorMassRequirement;
requirement segmentMassRequirement;
requirement instrumentMassRequirement;
connection deriveMirrorMass : RequirementDerivation::Derivation
connect massRequirement to mirrorMassRequirement;
#RequirementDerivation::derivation connection deriveInstrumentMass
connect massRequirement to instrumentMassRequirement;
#RequirementDerivation::derivation connection deriveSegmentMass
connect mirrorMassRequirement to segmentMassRequirement;
The derivation kind runs from the original requirement to each derived one,
so the requirements derived from an original — transitively, to maxDepth —
are an outgoing traversal, and the original(s) a derived requirement traces
back to are an incoming one:
calc def DerivedFrom :> Query {
in req : Element;
RelatedElements(
source = req,
relationshipKind = "derivation",
direction = "outgoing",
maxDepth = 2
)
}
$ sysml cookbook.sysml -run-query "Cookbook::DerivedFrom req=Cookbook::massRequirement"
✓ Query Cookbook::DerivedFrom returned 3 rows
Row 1: Cookbook::mirrorMassRequirement
Row 2: Cookbook::instrumentMassRequirement
Row 3: Cookbook::segmentMassRequirement
Which end is the original is read from the derivation itself: an end
subsetting originalRequirements or tagged #original is the original, one
subsetting derivedRequirements or tagged #derive is derived, and a
connection typed by a connection def specializing Derivation inherits the
roles its definition's ends state. An end that states no role takes the one
left over: it is the original when no other end is, and derived otherwise —
so connect (a, b, c) with no stated roles derives b and c from a, and
an unmarked end beside an #original end is derived. A connection def
specializing Derivation whose ends are typed by requirement definitions —
the form the v1 migrator writes — relates those definitions the same way,
through the ends it inherits from a general definition as well as its own; an
end that redefines an inherited end keeps that end's role and, when it declares
no type, its type. A plain connection between two requirements is not a
derivation.
Refine relationships¶
A refinement is a dependency annotated @ModelingMetadata::Refinement,
as a prefix (#refinement dependency ...) or in its body ({ @Refinement; }).
The cookbook model states one from a part definition to the requirement it
refines:
#ModelingMetadata::refinement dependency mirrorRefinesMass
from MirrorAssembly to mirrorMassRequirement;
The refinement kind runs from each client of the dependency to each of its
suppliers, so "what refines this requirement" is an incoming traversal
from the requirement:
calc def RefinedBy :> Query {
in req : Element;
RelatedElements(
source = req,
relationshipKind = "refinement",
direction = "incoming",
maxDepth = 1
)
}
$ sysml cookbook.sysml -run-query "Cookbook::RefinedBy req=Cookbook::mirrorMassRequirement"
✓ Query Cookbook::RefinedBy returned 1 row
Row 1: Cookbook::MirrorAssembly
A dependency with several clients or suppliers relates every client to every
supplier. A dependency without the Refinement metadata states no refinement
edge.
Specialization (and the other structural kinds)¶
specialization, subsetting, redefinition and typing traverse the
declaration hierarchy. Incoming specialization from a general type finds what
specializes it, transitively to maxDepth:
calc def Specializers :> Query {
in general : Element;
RelatedElements(
source = general,
relationshipKind = "specialization",
direction = "incoming",
maxDepth = 2
)
}
$ sysml cookbook.sysml -run-query "Cookbook::Specializers general=Cookbook::Subsystem"
✓ Query Cookbook::Specializers returned 2 rows
Row 1: Cookbook::OpticalSubsystem
Row 2: Cookbook::MirrorAssembly
Traversal results are elements like any others — feed them into Project for
a traceability table, as the worked example does for its
requirement section.
Coverage¶
RelatedElements answers "what satisfies this requirement"; a traceability
report also has to answer "which requirements does nothing satisfy".
WhereRelated is that filter: it keeps each source row by whether at least
one element is reachable from it over a relationship kind, and takes the same
relationshipKind, direction and maxDepth as RelatedElements — every
kind it accepts, the same typed errors for an unknown kind or direction, the
same edge tables and visit budget:
WhereRelated(
source = <elements>,
relationshipKind = "<kind>",
direction = "<direction>",
maxDepth = <n>,
exists = true // keep rows with a related element (default),
// false keeps the rows with none
)
The rows to check are the requirements under a root. WhereType matches
nested requirement usages as well as top-level ones, and — because a
satisfy usage is a RequirementUsage in the metamodel — the satisfaction
assertions too, so the base recipe subtracts those with Except:
calc def Requirements :> Query {
in root : Element;
Except(
source = Union(
source = WhereType(source = Descendants(source = root, maxDepth = 10), type = "RequirementDefinition"),
other = WhereType(source = Descendants(source = root, maxDepth = 10), type = "RequirementUsage")
),
exclude = WhereType(source = Descendants(source = root, maxDepth = 10), type = "SatisfyRequirementUsage")
)
}
calc def UnsatisfiedRequirements :> Query {
in root : Element;
WhereRelated(
source = Requirements(root = root),
relationshipKind = "satisfaction",
direction = "incoming",
maxDepth = 1,
exists = false
)
}
The model's pointingRequirement (declared at the end of the package with
its definition and two nested requirements) is verified by
pointingVerification but satisfied by nothing, and neither are its
children or the three requirements derived from massRequirement:
$ sysml cookbook.sysml -run-query "Cookbook::UnsatisfiedRequirements root=Cookbook"
✓ Query Cookbook::UnsatisfiedRequirements returned 7 rows
Row 1: Cookbook::PointingRequirement
Row 2: Cookbook::pointingRequirement
Row 3: Cookbook::mirrorMassRequirement
Row 4: Cookbook::segmentMassRequirement
Row 5: Cookbook::instrumentMassRequirement
Row 6: Cookbook::pointingRequirement::slewRequirement
Row 7: Cookbook::pointingRequirement::trackingRequirement
UnverifiedRequirements is the same recipe with
relationshipKind = "verification"; rooted at the package it also finds the
nested Traceability package's downlinkRequirement, which the
traceability matrix below shows with no verifier:
$ sysml cookbook.sysml -run-query "Cookbook::UnverifiedRequirements root=Cookbook"
✓ Query Cookbook::UnverifiedRequirements returned 7 rows
Row 1: Cookbook::PointingRequirement
Row 2: Cookbook::mirrorMassRequirement
Row 3: Cookbook::segmentMassRequirement
Row 4: Cookbook::instrumentMassRequirement
Row 5: Cookbook::pointingRequirement::slewRequirement
Row 6: Cookbook::pointingRequirement::trackingRequirement
Row 7: Cookbook::Traceability::downlinkRequirement
Rows keep their order and any projected columns, so WhereRelated composes
with Project and OrderBy like the other filters. Omitting exists
keeps the covered rows instead; maxDepth bounds how far the walk looks
for a related element, and each element it reaches charges the visit budget.
Set operations: Except and Union¶
Except(source, exclude) keeps the rows of source not among exclude, in
source order; Union(source, other) is every row of source followed by the
rows of other not already present. Both emit each row once and identify a row the
way RelatedElements de-duplicates: a model element by its declaration, an
object the session holds by the object itself, a verdict by its assertion and
the object it was checked on, a state by its object, machine and state path, and
an event by its place in the trace — so Verdicts, States and Events tables
can be combined too.
Combining the two coverage queries gives the requirements with a gap of
either kind, and subtracting one from the other the requirements with exactly
one:
calc def UncoveredRequirements :> Query {
in root : Element;
Union(
source = UnsatisfiedRequirements(root = root),
other = UnverifiedRequirements(root = root)
)
}
calc def VerifiedButUnsatisfied :> Query {
in root : Element;
Except(
source = UnsatisfiedRequirements(root = root),
exclude = UnverifiedRequirements(root = root)
)
}
$ sysml cookbook.sysml -run-query "Cookbook::UncoveredRequirements root=Cookbook"
✓ Query Cookbook::UncoveredRequirements returned 8 rows
Row 1: Cookbook::PointingRequirement
Row 2: Cookbook::pointingRequirement
Row 3: Cookbook::mirrorMassRequirement
Row 4: Cookbook::segmentMassRequirement
Row 5: Cookbook::instrumentMassRequirement
Row 6: Cookbook::pointingRequirement::slewRequirement
Row 7: Cookbook::pointingRequirement::trackingRequirement
Row 8: Cookbook::Traceability::downlinkRequirement
$ sysml cookbook.sysml -run-query "Cookbook::VerifiedButUnsatisfied root=Cookbook"
✓ Query Cookbook::VerifiedButUnsatisfied returned 1 row
Row 1: Cookbook::pointingRequirement
Except keeps the source's projected columns; Union requires both inputs
to be unprojected or to project the same columns, and is a typed error
otherwise, since its rows share one table.
Requirement hierarchy¶
A requirement tree is the requirement definitions and usages under a root —
Requirements above — in hierarchy order. Descendants visits level by
level, so sort by qualifiedName: an element's qualified name prefixes its
children's, which puts each requirement directly above the ones nested in
it. Projecting shortName, name and documentation gives the table a
document renders:
calc def RequirementTree :> Query {
in root : Element;
Project(
source = OrderBy(
source = Requirements(root = root),
property = "qualifiedName",
direction = "ascending",
missing = "last",
multiple = "error"
),
properties = ("shortName", "name", "documentation")
)
}
$ sysml cookbook.sysml -run-query "Cookbook::RequirementTree root=Cookbook::pointingRequirement"
✓ Query Cookbook::RequirementTree returned 2 rows
Columns: shortName, name, documentation
Row 1: Cookbook::pointingRequirement::slewRequirement
shortName = "REQ-2.1"
name = "slewRequirement"
documentation = "The mount reaches a new target within 60 seconds."
Row 2: Cookbook::pointingRequirement::trackingRequirement
shortName = "REQ-2.2"
name = "trackingRequirement"
documentation = "The mount tracks a target for 30 minutes without drift."
Rooted at the package, the same query lists PointingRequirement, the
Traceability package's three requirements, massRequirement and its three
derived requirements, and pointingRequirement with its two children beneath
it.
The requirements example renders such a tree
as the last table of its report, requirements.md.
Nesting rows: Tree¶
Sorting by qualified name puts children under their parents but leaves every
row at the margin. Tree arranges the rows as a containment tree instead:
each row nests under the nearest row containing it — its nearest owner among
the rows, or the individual whose part it is — in pre-order, at a depth the
renderers indent by (Markdown with a ↳ marker, HTML with the row's
data-depth; see hierarchical rows):
calc def RequirementOutline :> Query {
in root : Element;
Project(
source = Tree(source = Requirements(root = root)),
properties = ("shortName", "name")
)
}
A row whose containing element is not among the rows nests under the nearest
one that is, so a filtered tree stays compact. A row the source repeats is
kept, each occurrence nesting where the first does. ancestors adds rows for the
elements containing the source rows — Descendants of a scope, or the scope
itself as the root — as intermediate levels wherever a source row nests under
them, and nowhere else — over unprojected rows only, since an ancestor has no
cells to show: Tree(source = Project(…), ancestors = …) is refused with the
typed error projected-ancestors. Project, the filters, Except and Union carry the
depths through; OrderBy keeps each row's depth but not the pre-order, so sort
before nesting.
Traceability matrix¶
RelatedElements answers one requirement at a time. To put every requirement
in one table with its satisfiers and verifiers beside it, derive the columns
from the relationships instead: a RelatedColumn(name, relationshipKind,
direction, maxDepth, aggregate = "list", targets) entry of columns
traverses the named relationship from each row's element — the same kinds,
directions and depth bound as RelatedElements — and fills a cell with what
it reaches. The aggregate chooses the cell's shape: "list" (the default)
holds the related elements, "count" how many there are, "any" whether
there is at least one — an existence test that stops at the first element it
reaches. targets, when given, keeps only the reached elements among them:
a dependency matrix whose columns are one query and whose rows are another
is Project(source = <rows>, columns = (RelatedColumn(..., targets = <columns>))).
The cookbook model's Traceability package holds three requirements, a
spacecraft whose parts satisfy them and three verification cases, two of
which verify the pointing requirement and none the downlink one:
calc def TraceMatrix :> Query {
in root : Element;
Project(
source = WhereType(
source = Descendants(source = root, maxDepth = 1),
type = "RequirementUsage"
),
properties = ("shortName", "name"),
columns = (
RelatedColumn(name = "satisfiedBy", relationshipKind = "satisfaction", direction = "incoming", maxDepth = 1),
RelatedColumn(name = "verifiedBy", relationshipKind = "verification", direction = "incoming", maxDepth = 1),
RelatedColumn(
name = "verifications",
relationshipKind = "verification",
direction = "incoming",
maxDepth = 1,
aggregate = "count"
)
)
)
}
$ sysml cookbook.sysml -run-query "Cookbook::TraceMatrix root=Cookbook::Traceability"
✓ Query Cookbook::TraceMatrix returned 3 rows
Columns: shortName, name, satisfiedBy, verifiedBy, verifications
Row 1: Cookbook::Traceability::pointingRequirement
shortName = "TR-1"
name = "pointingRequirement"
satisfiedBy = Cookbook::Traceability::gimbal
verifiedBy = [Cookbook::Traceability::pointingTest, Cookbook::Traceability::pointingAnalysis]
verifications = 2
Row 2: Cookbook::Traceability::thermalRequirement
shortName = "TR-2"
name = "thermalRequirement"
satisfiedBy = Cookbook::Traceability::radiator
verifiedBy = Cookbook::Traceability::thermalTest
verifications = 1
Row 3: Cookbook::Traceability::downlinkRequirement
shortName = "TR-3"
name = "downlinkRequirement"
satisfiedBy = Cookbook::Traceability::transmitter
verifiedBy = (none)
verifications = 0
A list cell is genuinely multi-valued: the report brackets several elements
and prints (none) for an empty cell, a document table renders each element
as it renders a multi-valued documentation projection (comma-joined in
Markdown, one linked value each in HTML), and the gRPC run_query response
carries every element. The elements keep the traversal's order, so the
verification declared first comes first.
Related columns join the projection like computed ones: OrderBy sorts by
them, a table's groupBy groups by them, and WhereFeature filters on them.
A list cell's elements sort as their qualified names and compare as the name
they print by, or as their qualified names against a qualified value, so
WhereFeature(feature = "satisfiedBy", operator = "endsWith", value = "::gimbal")
and WhereFeature(feature = "satisfiedBy", operator = "=", value = "gimbal")
both keep the requirements the gimbal satisfies and OrderBy(property =
"satisfiedBy", multiple = "first") sorts by each row's first satisfier.
Uncovered requirements are the rows whose count is zero:
calc def Unverified :> Query {
in root : Element;
WhereFeature(
source = TraceMatrix(root = root),
'feature' = "verifications",
operator = "=",
value = "0"
)
}
$ sysml cookbook.sysml -run-query "Cookbook::Unverified root=Cookbook::Traceability"
✓ Query Cookbook::Unverified returned 1 row
Columns: shortName, name, satisfiedBy, verifiedBy, verifications
Row 1: Cookbook::Traceability::downlinkRequirement
shortName = "TR-3"
name = "downlinkRequirement"
satisfiedBy = Cookbook::Traceability::transmitter
verifiedBy = (none)
verifications = 0
A relationship kind or direction RelatedElements would refuse is refused
here too, with the same typed error naming the column; so is an aggregate
other than the three above. The traceability example
renders such a matrix as a document table beside the requirement list and
the requirements' verdicts.
Objects the session holds¶
Every recipe so far reads the model: its elements and what they declare. A
query can also read the objects a session holds — the ones -instantiate
(or %instantiate in the REPL) created — with the same operations. A binding
written as a usage's name binds the object the session holds under that name
while it holds one, and the element otherwise; #2 binds an object by the id
the instantiation report printed, and telescope.primaryMirror a nested object
by its path. Over an object, OwnedElements and Descendants are the objects
it holds as its parts, Ancestors the objects holding it, WhereType tests
the object's types, and WhereFeature, Project and OrderBy read the values
the object holds now — after a run changed them, not the declared defaults.
An object's name is its path from the object it was bound through
(primaryMirror, wheels[2] for the second of a collection), its
qualifiedName the whole path (Cookbook::telescope.primaryMirror), and the
report prints its id beside each row.
calc def HeldParts :> Query {
in root : Element;
Project(
source = WhereType(source = Descendants(source = root, maxDepth = 2), type = "Subsystem"),
properties = ("qualifiedName", "mass")
)
}
$ sysml cookbook.sysml -instantiate Cookbook::telescope -run-query "Cookbook::HeldParts root=telescope"
✓ Created instance of Cookbook::telescope
ID: 1
Use %features Cookbook::telescope to inspect
✓ Query Cookbook::HeldParts returned 3 rows
Columns: qualifiedName, mass
Row 1: Cookbook::telescope.primaryMirror (#2)
qualifiedName = "Cookbook::telescope.primaryMirror"
mass = 10.0
Row 2: Cookbook::telescope.instrumentCluster (#4)
qualifiedName = "Cookbook::telescope.instrumentCluster"
mass = 4.5
Row 3: Cookbook::telescope.mountControl (#7)
qualifiedName = "Cookbook::telescope.mountControl"
mass = 15.0
Without -instantiate the same invocation binds the element telescope and
returns its three declared subsystems, as the recipes above do.
Objects(type = "<type>") enumerates every object the session holds that is of
the type — the objects bound at the top and every object they hold, each under
its path — and needs no binding at all:
calc def HeldSubsystems :> Query {
OrderBy(
source = Project(source = Objects(type = "Subsystem"), properties = ("qualifiedName", "mass")),
property = "mass",
direction = "descending",
missing = "last",
multiple = "first"
)
}
$ sysml cookbook.sysml -instantiate Cookbook::telescope -run-query "Cookbook::HeldSubsystems"
✓ Created instance of Cookbook::telescope
ID: 1
Use %features Cookbook::telescope to inspect
✓ Query Cookbook::HeldSubsystems returned 3 rows
Columns: qualifiedName, mass
Row 1: Cookbook::telescope.mountControl (#7)
qualifiedName = "Cookbook::telescope.mountControl"
mass = 15.0
Row 2: Cookbook::telescope.primaryMirror (#2)
qualifiedName = "Cookbook::telescope.primaryMirror"
mass = 10.0
Row 3: Cookbook::telescope.instrumentCluster (#4)
qualifiedName = "Cookbook::telescope.instrumentCluster"
mass = 4.5
A session holding no object returns no rows from Objects; outside any
session the operation is refused with a typed error, and RelatedElements is
refused over an object row — Which query is
which draws these boundaries.
A document renders the same way: -instantiate <name> -render-document <doc>
creates the object first, and every table or list whose query is bound to that
usage's name, or enumerates Objects, renders the objects by path. In HTML each
such row carries its data-object="#<id>" beside the data-element of the
usage it stands for, and an object-valued cell is a span.sysml-object. See
Rendering a document over objects.
Which constraints and requirements hold¶
Verdicts(source = <rows>) checks the object behind each row — the object the
session holds when the binding is one, the row's declared object otherwise —
and returns one row per assertion about it or about the objects it holds:
every assert constraint, every requirement the object carries, every
satisfy whose subject it is, and the verification cases that verify those
requirements. Each row is a verdict: its verdict is holds, violated
or undecided, its path names the object the assertion was checked on
(Cookbook::telescope.primaryMirror), its kind is constraint,
requirement, satisfaction or verification, and its reason explains a
violation or why nothing could be decided. The row still stands for the
assertion element, so name, qualifiedName, WhereName and WhereType
read the constraint or requirement itself; on a violated row condition is the
condition that came out false, as written.
The cookbook's Subsystem asserts massKnown { mass > 0.0 }, and the
MirrorAssembly redefining mass = 10.0 also asserts lightweight { mass < 5.0 }:
calc def Checks :> Query {
in root : Element;
Project(source = Verdicts(source = root), properties = ("path", "name", "verdict", "reason"))
}
$ sysml cookbook.sysml -run-query "Cookbook::Checks root=telescope"
✓ Query Cookbook::Checks returned 6 rows
Columns: path, name, verdict, reason
Row 1: satisfy massRequirement by telescope on Cookbook::telescope: undecided
path = "Cookbook::telescope"
name = (none)
verdict = "undecided"
reason = "satisfaction satisfy massRequirement by telescope: no condition to evaluate"
Row 2: verification Cookbook::massVerification on Cookbook::telescope: undecided
path = "Cookbook::telescope"
name = "massVerification"
verdict = "undecided"
reason = "the case body bound no VerdictKind value"
Row 3: assert constraint massKnown on Cookbook::telescope.primaryMirror: holds
path = "Cookbook::telescope.primaryMirror"
name = "massKnown"
verdict = "holds"
reason = (none)
Row 4: assert constraint lightweight on Cookbook::telescope.primaryMirror: violated
path = "Cookbook::telescope.primaryMirror"
name = "lightweight"
verdict = "violated"
reason = "constraint lightweight: assertion evaluated to false: mass < 5.0"
Row 5: assert constraint massKnown on Cookbook::telescope.instrumentCluster: holds
...
Row 6: assert constraint massKnown on Cookbook::telescope.mountControl: holds
...
Rows come in the order the objects are walked — the root first, then each
part in declaration order — with the assertions on one object together. The
massRequirement has no require constraint, so satisfying it decides
nothing, and its verification case binds no verdict; both are undecided with
the reason saying so. Written over the element telescope, the query checks
the declared object — definition defaults and :>> redefinitions — exactly as
the derived mass recipes above read it. With -instantiate Cookbook::telescope
the same binding is the held object and the verdicts are about its values
now, so a run that changed mass changes the table.
kind = "constraint" (or requirement, satisfaction, verification)
keeps one kind of assertion; the default "all" keeps every kind. To list
only what fails, filter on the verdict:
calc def Violated :> Query {
in root : Element;
WhereFeature(source = Verdicts(source = root), 'feature' = "verdict", operator = "=", value = "violated")
}
$ sysml cookbook.sysml -run-query "Cookbook::Violated root=telescope"
✓ Query Cookbook::Violated returned 1 row
Row 1: assert constraint lightweight on Cookbook::telescope.primaryMirror: violated
OrderBy(property = "verdict") sorts the table by outcome, WhereFeature on
path or kind narrows it, and Project reads any verdict property beside
the assertion's own (shortName, documentation) — assertion and carrier
project the assertion element and the object it was checked on themselves.
A verdict row's
verification property lists the outcomes (pass, fail, inconclusive,
error) of the verification cases that verify its requirement — on a
requirement or satisfaction row — while a verification row carries one
case's own outcome as its verdict.
Two things a verdict table refuses rather than approximates. Verdicts over a
row that is not an object — a package, an attribute usage — is a typed error
naming the element, as -validate=<object> is. And when the object graph
cannot be walked whole (a part that holds another of its own type without
end, or one that exceeds the materialization budget), the query fails with an
incomplete-validation error instead of returning a table missing rows;
Ancestors, Descendants and OwnedElements are likewise refused over
verdict rows, which are assertions checked on an object, not elements owning
others.
In a document, a Verdicts table renders each cell as
<assertion> on <path>: <verdict>; in HTML a verdict cell is a
span.sysml-verdict carrying data-verdict, data-path and, for an object
the session holds, data-object, beside the data-element of the assertion.
Where the objects stand and what they did¶
Objects the session holds may be running: an object whose type exhibits a
state machine starts it when the object is created, and %send and
%advance (or -state with -advance on the command line) drive it. Three
operations read the run. States(source = <rows>) answers the state each
object's machine is in now — one row per active leaf state, so a parallel
state contributes a row per region; InState(name = "<state>") is the
inverse, the held objects whose machine is in that state; and
Events(source, kind, since, before) reads the trace the session records as
rows. All three read the session as Objects does, and are refused with the
same no-runtime error where there is none;
Which query is which has
the boundaries in full.
The cookbook model's Dome exhibits a DomeControl machine: closed until
an Open arrives, then open with two regions — pointing, which slews on a
Slew(azimuth) signal, and shutter, which takes a second to reach opened
— and back to closed on Close:
calc def DomeStates :> Query {
in root : Element;
Project(source = States(source = root), properties = ("machine", "statePath", "region", "enclosing"))
}
calc def Opened :> Query {
Project(source = InState(name = "open"), properties = ("qualifiedName"))
}
calc def Accepted :> Query {
in root : Element;
Project(
source = Events(source = root, kind = "accept", since = 1 [s], before = 2.5 [s]),
properties = ("time", "event", "payload")
)
}
Sending a signal needs the prompt, so this recipe runs there. %trace on
first, since Events reads the trace the session records and refuses
(no-trace) in one that records none — and %trace off discards it:
$ sysml docs/manual/examples/cookbook.sysml
sysml> %trace on
sysml> %instantiate dome
✓ Created instance of Cookbook::dome
ID: 1
sysml> %instantiate spareDome
✓ Created instance of Cookbook::spareDome
ID: 3
sysml> %state control dome
sysml> %send Open
sysml> %advance 1
sysml> %send Slew(azimuth = 120.0)
sysml> %advance 1
sysml> %send Open to spareDome
sysml> %advance 0.5
sysml> %send Close to spareDome
sysml> %advance 0.5
Which state is #1.control in? — one row per active leaf, with the
region each runs in and the composite state enclosing both:
sysml> %run-query DomeStates root=#1
✓ Query Cookbook::DomeStates returned 2 rows
Columns: machine, statePath, region, enclosing
Row 1: #1.control in open.slewing
machine = "control"
statePath = "open.slewing"
region = "pointing"
enclosing = "open"
Row 2: #1.control in open.opened
machine = "control"
statePath = "open.opened"
region = "shutter"
enclosing = "open"
A state row's name is the leaf's own (slewing), statePath its name
qualified by the states enclosing it, object and path the object, and the
row answers the state declaration's own properties too, so WhereName and
WhereFeature on any of them narrow the table and OrderBy orders it. A row
prints as <object>.<machine> in <statePath>, the object as the binding named
it. States over an object exhibiting no state machine is a typed
no-state-machine error, not an empty table.
Which objects are in open? — spareDome opened at t = 2 and closed
again at t = 2.5, so only dome is:
sysml> %run-query Opened
✓ Query Cookbook::Opened returned 1 row
Columns: qualifiedName
Row 1: Cookbook::dome (#1)
qualifiedName = "Cookbook::dome"
The name is a leaf or a state enclosing one, by name or dotted path
(open.slewing), and each object is one row however many of its leaves match;
a name no held object's machine declares is a typed unknown-state error.
The rows are object rows, so Descendants, WhereFeature and Verdicts
read them as above.
What did #1 accept between t = 1 [s] and t = 2.5 [s]? — the interval
is inclusive at since and exclusive at before, so the Open accepted at
t = 0 is out, the Slew at t = 1 in, and for spareDome the Open at
t = 2 is in while the Close at t = 2.5 is out:
sysml> %run-query Accepted root=#1
✓ Query Cookbook::Accepted returned 1 row
Columns: time, event, payload
Row 1: t=1 Cookbook::dome.control: accept Slew
time = 1.0 [s]
event = "Slew"
payload = "azimuth = 120.0"
sysml> %run-query Accepted root=spareDome
✓ Query Cookbook::Accepted returned 1 row
Columns: time, event, payload
Row 1: t=2 Cookbook::spareDome.control: accept Open
time = 2.0 [s]
event = "Open"
payload = (none)
kind names the records to keep — accept, send, transition, entry,
exit, do, choice (a due order or region order the run drew, with
alternatives and taken) or guard (one it could not evaluate), several
separated by commas, all by default — and a source left out reads every
object's records. since and before take a duration or a bare number of
the clock's seconds; a bound that is not a duration (1 [m]), or an interval
with before at or before since, is a typed invalid-interval error. Each
row's time is the instant read from the runtime clock, state, from and
to the states an entry, exit or transition touched, target the object a
send was addressed to, and text the line %trace prints — the rows are the
record it prints from, so the two never disagree. The rows come in the order
the run made them; OrderBy(property = "time", direction = "descending", ...)
reverses it.
In a document, a state cell renders as <path>.<machine> in <statePath> and
an event cell as t=<instant> <path>.<machine>: <text>; in HTML they are a
span.sysml-state with data-machine, data-state and data-region, and a
span.sysml-event with data-event-kind and data-time, each carrying the
object's data-object.