Notation gallery — every implemented SysML v2 glyph¶
One section per implemented family of the SysML v2 graphical notation (spec 8.2.3 and the notation-table figures). Each section shows the spec’s ground-truth crop (when present on this machine), the exact SysML source, the live interactive diagram, and a compact assert cell that fails loudly if the glyph regresses. Page numbers are the spec’s printed page numbers.
How to review in JupyterLab:
Compare each diagram against the spec crop above it.
Click nodes and edges to check the SELECTED look too: selection is a color change only — hollow glyph bodies (triangles, diamonds, circles) stay white, filled glyphs (diamonds, arrowheads, balls, badges, pins) follow the selection color, and stroke widths never fatten.
Pan/zoom in on the small endpoint glyphs (shaft adornments, pins, membership circles); the toolbar’s search box highlights elements by name without touching the selection.
Tick the review checklist at the bottom as you go.
Layout runs in the browser (elkjs via the vendored ipyelk), so this
notebook executes headlessly and the diagrams lay themselves out when a
frontend attaches. Spec crops are looked up under the
directory named by the SYSML_SPEC_PAGES environment variable and
degrade gracefully when it is unset.
import os
from pathlib import Path
from ipyelk.elements import Port
from IPython.display import Image, display
import longeron
from longeron import diagrams
_spec_dir = os.environ.get("SYSML_SPEC_PAGES")
SPEC_PAGES = Path(_spec_dir) if _spec_dir else None
def spec_crop(*names: str, width: int = 460) -> None:
"""Show ground-truth crops from the spec PDF when available."""
for name in names:
p = SPEC_PAGES / name if SPEC_PAGES else None
display(Image(filename=str(p), width=width)) if p and p.exists() else print(
f"spec crop not available: {name}"
)
def walk(root):
"""The ELK node and all its descendants."""
yield root
for child in root.children:
yield from walk(child)
def node(widget, node_id):
"""The diagram node whose id (= qualified name) matches."""
return next(n for n in walk(widget.source.value) if n.id == node_id)
def nodes(widget, css):
"""All diagram nodes whose cssClasses carry the fragment."""
return [n for n in walk(widget.source.value) if css in (n.properties.cssClasses or "")]
def edges(widget, css):
"""All edges (any depth) whose cssClasses carry the fragment."""
return [e for n in walk(widget.source.value) for e in n.edges if css in e.properties.cssClasses]
def edge(widget, css):
"""The first edge whose cssClasses carry the fragment."""
return edges(widget, css)[0]
1. Specialization family (printed pp. 36–37)¶
The whole family draws a solid line into a closed hollow triangle at the general / definition end; the relationships are told apart only by the shaft adornment tight behind the head (never by keyword labels):
Textual |
Adornment |
|---|---|
subclassification |
none |
feature typing |
two dots straddling the shaft (a colon) |
subsetting |
none |
redefinition |
one perpendicular bar tick |
reference subsetting |
2×2 dots (a double colon) |
spec_crop(
"spec-p67-subclassification.png",
"spec-p67-part-defined-by-part-definition.png",
"spec-p68-subsetting.png",
"spec-p68-redefinition.png",
)
spec crop not available: spec-p67-subclassification.png
spec crop not available: spec-p67-part-defined-by-part-definition.png
spec crop not available: spec-p68-subsetting.png
spec crop not available: spec-p68-redefinition.png
m1a = longeron.loads("""
package Specializations {
part def Machine;
part def Vehicle :> Machine; // subclassification: plain hollow triangle
part car : Vehicle; // feature typing: colon dots
part vehicle;
part truck :> vehicle; // subsetting: plain hollow triangle
part pool;
part spare ::> pool; // reference subsetting: 2x2 colon dots
part def V { part engine; }
part tuned : V { part engine :>> engine; } // redefinition: bar tick
}
""")
w1a = diagrams.structure_diagram(m1a)
w1a
forms = {
kind: edge(w1a, f"sysml-edge-{kind}").properties.shape.end
for kind in ("specializes", "typed", "subsets", "redefines", "references")
}
assert forms == {
"specializes": "generalization",
"typed": "generalization-colon",
"subsets": "generalization",
"redefines": "generalization-tick",
"references": "generalization-dcolon",
}, forms
1b. Satisfy longhand — «satisfy requirement» box + reference subsetting (printed p. 133)¶
A named satisfy usage draws as the «satisfy requirement» box, joined to the satisfied requirement by a reference-subsetting edge (the 2×2 colon dots behind the hollow triangle).
spec_crop("spec-p164-satisfy-requirement-longhand-notation-with-explicit-referenc.png")
spec crop not available: spec-p164-satisfy-requirement-longhand-notation-with-explicit-referenc.png
m1b = longeron.loads("""
package Requirements { requirement requirement1; }
package System {
part sys {
satisfy requirement satisfy1 references Requirements::requirement1;
}
}
""")
w1b = diagrams.structure_diagram(m1b)
w1b
assert node(w1b, "System::sys::satisfy1").labels[0].text == "«satisfy requirement»"
ref = edge(w1b, "sysml-edge-references")
assert ref.properties.shape.end == "generalization-dcolon"
assert (ref.source.id, ref.target.id) == ("System::sys::satisfy1", "Requirements::requirement1")
2. Membership (printed pp. 26, 38)¶
2a. Feature membership — composite and referential diamonds (printed p. 38)¶
Definition-level membership edges (composition="defs", the default):
a filled diamond at the whole end for composite members, a hollow
diamond for referential (ref) members — role name on the line, the
member’s multiplicity at the part end.
spec_crop(
"spec-p69-feature-membership-iscomposite-true.png",
"spec-p69-feature-membership-iscomposite-false.png",
)
spec crop not available: spec-p69-feature-membership-iscomposite-true.png
spec crop not available: spec-p69-feature-membership-iscomposite-false.png
m2a = longeron.loads("""
package Memberships {
part def Wheel;
part def Driver;
part def Car {
part wheels : Wheel [4]; // composite: filled diamond, [4] at the part end
ref part driver : Driver; // referential: hollow diamond
}
}
""")
w2a = diagrams.structure_diagram(m2a, composition="defs")
w2a
comp = edge(w2a, "sysml-edge-member")
assert comp.properties.shape.start == "composition"
assert {"wheels", "[4]"} <= {label.text for label in comp.labels}
assert edge(w2a, "sysml-edge-refmember").properties.shape.start == "aggregation"
2b. Owned membership — the circle-plus presentation (printed p. 26)¶
membership="edges" swaps package nesting for the spec’s ALTERNATIVE
owned-membership presentation: members become sibling nodes joined to
their owning namespace by a solid edge with a true circle-plus
(cross strokes spanning the full diameter) at the owning end.
spec_crop("spec-p57-membership-owned-member.png")
spec crop not available: spec-p57-membership-owned-member.png
m2b = longeron.loads("package Package0 { package Package1 { part def X; } }")
w2b = diagrams.structure_diagram(m2b, membership="edges")
w2b
owned = edges(w2b, "sysml-edge-owned")
assert {(e.source.id, e.target.id) for e in owned} == {
("Package0", "Package0::Package1"),
("Package0::Package1", "Package0::Package1::X"),
}
assert all(e.properties.shape.start == "owned-circle-plus" for e in owned)
2c. Unowned membership (alias) — hollow circle (printed p. 26)¶
An alias draws a solid line with a small hollow circle at the referencing namespace end and the alias name as the edge label.
spec_crop("spec-p57-membership-unowned-member-with-alias-name.png")
spec crop not available: spec-p57-membership-unowned-member-with-alias-name.png
m2c = longeron.loads("""
package Lib { part def Target; }
package App { alias T for Lib::Target; }
""")
w2c = diagrams.structure_diagram(m2c)
w2c
al = edge(w2c, "sysml-edge-alias")
assert al.properties.shape.start == "alias-circle" and al.labels[0].text == "T"
assert (al.source.id, al.target.id) == ("App", "Lib::Target")
3. Connectors (printed pp. 19, 66–67, 79, 132)¶
3a. Plain connect — no endpoint glyphs, cross multiplicities (printed p. 66)¶
A binary connection is a plain solid line; cross multiplicities render near the ends they constrain.
spec_crop("spec-p97-connection.png")
spec crop not available: spec-p97-connection.png
m3a = longeron.loads("""
package Connections {
part def S {
part a;
part b;
connect [1] a to [0..2] b;
}
}
""")
w3a = diagrams.structure_diagram(m3a)
w3a
conn = edge(w3a, "sysml-edge-connect")
assert conn.properties.shape is None # no endpoint glyphs on a plain connect
placements = {lb.text: lb.layoutOptions["elk.edgeLabels.placement"] for lb in conn.labels}
assert placements == {"[1]": "TAIL", "[0..2]": "HEAD"}
3b. Connection with direction indication (printed p. 66)¶
When the connection’s definition declares directed ends
(sourceEnd / targetEnd), the connector line grows an open-V head at
the target end and carries the name : Type label.
spec_crop(
"spec-p96-connection-definition-with-direction-indication.png",
"spec-p97-connection-with-direction-indication.png",
)
spec crop not available: spec-p96-connection-definition-with-direction-indication.png
spec crop not available: spec-p97-connection-with-direction-indication.png
m3b = longeron.loads("""
package Directed {
part def Part1;
part def Part2;
connection def ConnectionDef2 {
end [1..1] part sourceEnd : Part1;
end [1..*] part targetEnd : Part2;
}
part part1 : Part1;
part part2 : Part2;
connection connection2 : ConnectionDef2 connect part1 to part2;
}
""")
w3b = diagrams.structure_diagram(m3b)
w3b
d = edge(w3b, "sysml-edge-directed")
assert d.properties.shape.end == "arrow" # open-V head at the target end
assert d.labels[0].text == "connection2 : ConnectionDef2"
3c. N-ary connection — junction dot (printed p. 66)¶
connect (a, b, c) with three or more ends meets at a small filled
junction dot, the connection’s name : Type label beside it.
spec_crop("spec-p97-connection-n-ary-with-3-ends.png")
spec crop not available: spec-p97-connection-n-ary-with-3-ends.png
m3c = longeron.loads("""
package Nary {
part def ConnectionDef1;
part part1;
part part2;
part part3;
connection connection1 : ConnectionDef1 connect (part1, part2, part3);
}
""")
w3c = diagrams.structure_diagram(m3c)
w3c
j = nodes(w3c, "sysml-connjunction")[0]
assert j.labels[0].text == "connection1 : ConnectionDef1"
# spokes anchor on the dot's invisible center ports (in/out), so every
# line radiates from the junction itself -- never scattered boundary points
spokes = [
e
for e in edges(w3c, "sysml-edge-connect")
if any(e.source is p or e.target is p for p in j.ports)
]
assert len(spokes) == 3
3d. Proxy connection — dot on the shallowest drawn ancestor (printed p. 67)¶
A connector end naming an undrawn nested feature
(connect part2.part4 to ...) draws the spec’s small filled proxy dot on
the border of the shallowest drawn ancestor, labeled with the residual
path (.part4) inside the box, adjacent to the dot — exactly where
the spec figure writes it — never an edge into the definition’s member
box.
spec_crop("spec-p98-proxy-connection.png")
spec crop not available: spec-p98-proxy-connection.png
m3d = longeron.loads("""
package Proxies {
part def Part2 { part part4; }
part def Part3 { part part5; }
part part1 {
part part2 : Part2;
part part3 : Part3;
connect part2.part4 to part3.part5;
}
}
""")
w3d = diagrams.structure_diagram(m3d)
w3d
p2 = node(w3d, "Proxies::part1::part2")
proxy = next(p for p in p2.ports if "sysml-port-proxy" in p.properties.cssClasses)
assert proxy.labels[0].text == ".part4" and proxy.properties.shape.use == "port-proxy"
# the residual-path label reads INSIDE the part box (spec p. 67 figure)
assert p2.layoutOptions["elk.portLabels.placement"] == "INSIDE"
assert "PORT_LABELS" in p2.layoutOptions["nodeSize.constraints"]
3e. Binding connector — the = glyph (printed p. 67)¶
A binding connector is a plain solid line with an = riding
mid-span, no endpoint glyphs.
Reading the spec crop below: the = on the bottom edge is the
binding; the diamonds belong to the surrounding figure (composite
membership of the parts), not to the binding notation.
spec_crop("spec-p98-binding-connection.png")
spec crop not available: spec-p98-binding-connection.png
m3e = longeron.loads("""
package Bindings {
part original;
part alias_part;
binding bind original = alias_part;
}
""")
w3e = diagrams.structure_diagram(m3e)
w3e
b = edge(w3e, "sysml-edge-binding")
assert [label.text for label in b.labels] == ["="] and b.properties.shape is None
3f. Dependency — binary and n-ary (printed p. 19)¶
Dependencies draw dashed open-V edges client → supplier with the
optional (name) label; an n-ary dependency radiates dashed links from a
small filled junction dot (client links plain, supplier links arrowed).
spec_crop("spec-p50-dependency.png", "spec-p50-dependency-nary.png")
spec crop not available: spec-p50-dependency.png
spec crop not available: spec-p50-dependency-nary.png
m3f = longeron.loads("""
package Dependencies {
part a;
part b;
part c;
part s;
dependency Uses from a to s;
dependency Multi from b, c to s;
}
""")
w3f = diagrams.structure_diagram(m3f)
w3f
deps = edges(w3f, "sysml-edge-dependency")
assert len(deps) == 2 and all(e.properties.shape.end == "arrow" for e in deps)
assert nodes(w3f, "sysml-junction")[0].labels[0].text == "(Multi)"
assert len(edges(w3f, "sysml-edge-depclient")) == 2 # unarrowed client links
3g. Satisfy shorthand — «satisfy» keyword edge (printed p. 132)¶
The anonymous shorthand satisfy R by sys; draws a solid line with an
open-V arrow and the «satisfy» keyword from the satisfying element to the
requirement.
spec_crop("spec-p163-satisfy-requirement-shorthand-notation.png")
spec crop not available: spec-p163-satisfy-requirement-shorthand-notation.png
m3g = longeron.loads("""
package Satisfies {
requirement requirement1;
part sys;
satisfy requirement1 by sys;
}
""")
w3g = diagrams.structure_diagram(m3g)
w3g
sat = edge(w3g, "sysml-edge-satisfies")
assert sat.labels[0].text == "«satisfy»" and sat.properties.shape.end == "arrow"
assert (sat.source.id, sat.target.id) == ("Satisfies::sys", "Satisfies::requirement1")
3h. Allocation — «allocate» keyword edge + «allocation» box (printed p. 79)¶
Anonymous allocate a to b draws the «allocate» keyword arrow; a
named allocation usage draws the «allocation» box form instead.
spec_crop("spec-p110-allocation.png", "spec-p110-allocation-definition.png")
spec crop not available: spec-p110-allocation.png
spec crop not available: spec-p110-allocation-definition.png
m3h = longeron.loads("""
package Allocations {
part part1;
part part2;
allocate part1 to part2; // keyword edge
allocation def AllocationDef1;
part part3;
part part4;
allocation allocation1 : AllocationDef1 // box form
allocate part3 to part4;
}
""")
w3h = diagrams.structure_diagram(m3h)
w3h
alc = edge(w3h, "sysml-edge-allocate")
assert alc.labels[0].text == "«allocate»" and alc.properties.shape.end == "arrow"
assert node(w3h, "Allocations::allocation1").labels[0].text == "«allocation»"
4. Flow connections (printed pp. 77, 81)¶
4a. Pin form — undrawn ports (printed p. 81)¶
A flow between features that are not drawn as port squares runs pin-to-pin: a small square source-output pin, a small square target-input pin with a filled arrowhead tight against it, and the payload item labeled near each end.
spec_crop("spec-p112-flow.png")
spec crop not available: spec-p112-flow.png
m4a = longeron.loads("""
package Flows {
item def Item1;
action def A { in x : Item1; out y : Item1; }
action action1 : A;
action action2 : A;
flow of Item1 from action1.y to action2.x;
}
""")
w4a = diagrams.structure_diagram(m4a)
w4a
flow = edge(w4a, "sysml-edge-flow")
assert (flow.properties.shape.start, flow.properties.shape.end) == (
"flow-source-pin",
"flow-target-pin",
)
ends = [lb.layoutOptions["elk.edgeLabels.placement"] for lb in flow.labels if lb.text == "Item1"]
assert sorted(ends) == ["HEAD", "TAIL"] # payload labeled near both ends
4b. Port-attached form — filled head only (printed p. 77)¶
When the flow’s ends resolve to drawn boundary port squares, the port
already is the pin: the line runs square-to-square — straight between
the facing borders — and keeps only the small filled arrowhead at the
target port. The payload label (item1 : Item1, Table 14) rides the line
near each end; port labels live inside the part bodies, like the spec’s
figure. The receiving side uses the spec’s own conjugation pattern
(pc : ~Pout — p. 77 receives on the ~ end), so its square shows the
in arrow the flow enters through.
spec_crop("spec-p108-interface-as-node-with-flow.png")
spec crop not available: spec-p108-interface-as-node-with-flow.png
m4b = longeron.loads("""
package PortFlows {
item def Item1;
port def Pout { out item y : Item1; }
part part0 {
part part1 { port po : Pout; }
part part2 { port pc : ~Pout; }
flow of item1 : Item1 from part1.po to part2.pc;
}
}
""")
w4b = diagrams.structure_diagram(m4b)
w4b
pflow = edge(w4b, "sysml-edge-portflow")
assert pflow.properties.shape.end == "flow-arrow" and pflow.properties.shape.start is None
assert isinstance(pflow.source, Port) and isinstance(pflow.target, Port) # square-to-square
# the payload item label rides the line near BOTH ends (Table 14 flow row)
assert [label.text for label in pflow.labels] == ["item1 : Item1", "item1 : Item1"]
# the receiving square shows the flow entering: in-arrow on its west side
pc = next(p for p in node(w4b, "PortFlows::part0::part2").ports if p.id.endswith("::pc"))
assert pc.properties.shape.use == "port-in-west"
# and every port label sits INSIDE its part body (spec p. 108 figure)
for name in ("part1", "part2"):
owner = node(w4b, f"PortFlows::part0::{name}")
assert owner.layoutOptions["elk.portLabels.placement"] == "INSIDE"
5. Ports (printed pp. 59, 62, 75–76)¶
Port usages render as the spec’s small squares on the owning box’s
border with the name : Type label inside the box, next to the
square (where the spec’s part figures write it). The direction arrow
inside the square derives from the port definition’s directed features
(all in, all out, mixed → double-headed); a plain square draws no
arrow. The arrow orients relative to the node interior — an in
arrow points into the box from whatever border the square rides.
Conjugation is textual (~Pin) and flips the arrow both ways (spec
7.12.3): pc : ~Pout receives, pd : ~Pin sends. Interfaces attach
square-to-square.
spec_crop("spec-p90-part-with-ports.png", "spec-p93-port.png", "spec-p106-interface.png")
spec crop not available: spec-p90-part-with-ports.png
spec crop not available: spec-p93-port.png
spec crop not available: spec-p106-interface.png
m5 = longeron.loads("""
package Ports {
item def Item1;
port def Pin { in item x : Item1; }
port def Pout { out item y : Item1; }
port def Pio { in item a : Item1; out item b : Item1; }
port def Plain;
part part0 {
part part1 { port po : Pout; port p4 : Plain; }
part part2 { port pi : Pin; port pc : ~Pout; port pd : ~Pin; port pio : Pio; }
interface if1 connect part1.po to part2.pc;
}
}
""")
w5 = diagrams.structure_diagram(m5)
w5
squares = {p.id.split("::")[-1]: p for n in walk(w5.source.value) for p in n.ports if p.id}
arrows = {
name: p.properties.shape.use if p.properties.shape else None for name, p in squares.items()
}
assert arrows == {
"po": "port-out-east", # out arrows LEAVE the box through its east border
"p4": None,
"pi": "port-in-west", # in arrows ENTER the box through its west border
"pc": "port-in-west", # conjugation textual, arrow flipped out -> in
"pd": "port-out-east", # ... and in -> out
"pio": "port-inout-east",
}
assert squares["pc"].labels[0].text == "pc : ~Pout"
assert squares["pd"].labels[0].text == "pd : ~Pin"
iface = edge(w5, "sysml-edge-connect")
assert iface.source is squares["po"] and iface.target is squares["pc"] # square-to-square
assert iface.labels[0].text == "if1"
6. Behavior views (printed pp. 90–92, 97, 115–116, 227)¶
6a. Action flow — control glyphs, badges, dashed successions (printed pp. 92, 97, 227)¶
The succession control-flow graph the interpreter executes: start
filled dot, done bullseye, fork/join thick filled bars,
decision/merge empty rhombi, accept/send standard rounded action
boxes with a filled top-left badge. Successions render dashed; guarded
branches carry their [guard] label. The decision below has three ways
out and the merge three ways in — each fan converges on a single anchor
point of the glyph (fork/join bars deliberately distribute along the
bar instead).
spec_crop(
"spec-p123-actions-with-control-nodes.png",
"spec-p128-accept-action.png",
"spec-p128-send-action.png",
)
spec crop not available: spec-p123-actions-with-control-nodes.png
spec crop not available: spec-p128-accept-action.png
spec crop not available: spec-p128-send-action.png
m6a = longeron.loads("""
package Behaviors {
item def Go;
action def Flow {
action prep { assign x := 1; }
fork f;
action a;
action b;
join j;
decide d;
merge g;
action rx accept go : Go;
action tx send new Go() via ch;
action c;
first start then prep;
first prep then f;
first f then a;
first f then b;
first a then j;
first b then j;
first j then d;
first d if x > 0 then rx;
first d if x < 0 then c;
first d then g;
first rx then tx;
first tx then g;
first c then g;
first g then done;
}
}
""")
w6a = diagrams.action_diagram(m6a.find("Behaviors::Flow"))
w6a
by_id = {n.id: n for n in walk(w6a.source.value) if n.id}
fork = by_id["Behaviors::Flow::f"]
assert (fork.width, fork.height) == (6, 40) and "sysml-ctrl-bar" in fork.properties.cssClasses
decision = by_id["Behaviors::Flow::d"]
assert type(decision.properties.shape).__name__ == "Diamond"
assert [p.layoutOptions["elk.port.side"] for p in decision.ports] == ["WEST", "EAST"] # anchors
assert len(edges(w6a, "sysml-edge-guarded")) == 2 # [x > 0] and [x < 0]
assert by_id["Behaviors::Flow::rx"].labels[0].properties.shape.use == "accept-badge"
assert by_id["Behaviors::Flow::tx"].labels[0].properties.shape.use == "send-badge"
assert "glyph-core" in nodes(w6a, "sysml-final")[0].properties.shape.use # done bullseye
assert nodes(w6a, "sysml-marker") # start dot
6b. Terminate — circle-X (printed p. 227)¶
A terminate; statement draws the spec’s circle with an inscribed X.
spec_crop("spec-p258.png", width=700) # the behavior-glyph BNF page (start/done/terminate/...)
spec crop not available: spec-p258.png
m6b = longeron.loads("""
package Behaviors2 {
item def Go;
action def Abort {
action warn send new Go() via ch;
terminate;
}
}
""")
w6b = diagrams.action_diagram(m6b.find("Behaviors2::Abort"))
w6b
term = nodes(w6b, "sysml-terminate")[0]
assert "glyph-x" in term.properties.shape.use # the inscribed X
6c. Swim lanes — «performer» (printed p. 90)¶
action_diagram(..., lanes=True) derives dashed-boundary «performer»
swim lanes from perform targets (perform station.a1 lands in lane
station), ordered left-to-right; start/done markers stay outside.
spec_crop("spec-p121-perform-actions-swimlanes.png")
spec crop not available: spec-p121-perform-actions-swimlanes.png
m6c = longeron.loads("""
package Swimlanes {
part station { action a1; action a4; }
part rover { action a2; action a3; }
action def Swim {
perform station.a1;
perform rover.a2;
perform rover.a3;
perform station.a4;
}
}
""")
w6c = diagrams.action_diagram(m6c.find("Swimlanes::Swim"), lanes=True)
w6c
lanes = nodes(w6c, "sysml-lane")
assert {lane.labels[1].text for lane in lanes} == {"station", "rover"}
assert all(lane.labels[0].text == "«performer»" for lane in lanes)
assert w6c.source.value.layoutOptions["elk.partitioning.activate"] == "true"
6d. State machine — transitions + typed submachine expansion (printed pp. 115–116)¶
Hierarchical states with entry markers and trigger [guard] / effect
transition labels. A state usage typed by a state def (state x : Inner;) expands into the definition’s submachine with instance-qualified
ids (bound by submachine_depth).
spec_crop(
"spec-p146-state-with-graphical-compartment-with-standard-state-transit.png",
"spec-p147-transition.png",
)
spec crop not available: spec-p146-state-with-graphical-compartment-with-standard-state-transit.png
spec crop not available: spec-p147-transition.png
m6d = longeron.loads("""
package States {
state def Inner {
entry; then a;
state a;
transition first a accept go then b;
state b;
}
state def Outer {
entry; then x;
state x : Inner;
transition first x accept quit then off;
state off;
}
}
""")
w6d = diagrams.state_diagram(m6d.find("States::Outer"))
w6d
x = node(w6d, "States::Outer::x")
assert {"States::Outer::x::a", "States::Outer::x::b"} <= {n.id for n in walk(x) if n.id}
assert any("sysml-marker" in (n.properties.cssClasses or "") for n in walk(x)) # entry markers
texts = [lb.text for e in edges(w6d, "sysml-edge-transition") for lb in (e.labels or [])]
assert "go" in texts and "quit" in texts # trigger labels
7. Annotations (printed pp. 20–21, 24, 157–158)¶
Every package box carries the spec’s folder tab riding its top-left
(printed p. 24). annotations=True additionally draws comment/doc
notes — the folded-corner box joined by a dashed anchor line with no
endpoint glyph (printed pp. 20–21) — and «@Type» metadata adornments
on annotated nodes (printed p. 157).
spec_crop(
"spec-p55-package-name-in-body.png",
"spec-p51-comment.png",
"spec-p189-annotation-metadata.png",
)
spec crop not available: spec-p55-package-name-in-body.png
spec crop not available: spec-p51-comment.png
spec crop not available: spec-p189-annotation-metadata.png
m7 = longeron.loads("""
package Annotated {
metadata def Safety;
part def Pump {
attribute pressure : Real;
}
@Safety about Pump;
comment about Pump /* Centrifugal, oil-free. */
part pump : Pump {
doc /* The unit under review. */
}
}
""")
w7 = diagrams.structure_diagram(m7, annotations=True)
w7
from longeron.render import _note_path_d
assert node(w7, "Annotated").labels[0].properties.shape.use == "package-tab" # folder tab
notes = nodes(w7, "sysml-note")
assert {n.labels[0].text for n in notes} == {"«comment»", "«doc»"}
# the UML note silhouette: cut corner PLUS the crease 'L' outlining the
# fold triangle — one explicit path shared with the headless renderer
for n in notes:
assert n.properties.shape.type == "node:path"
assert n.properties.shape.use == _note_path_d(n.width, n.height)
anchors = edges(w7, "sysml-edge-anchor")
assert len(anchors) == 2 and all(e.properties.shape is None for e in anchors) # dashed, no glyph
assert node(w7, "Annotated::Pump").labels[0].text == "«@Safety»" # metadata adornment
8. Portions, actors, stakeholders (printed p. 52; errata N17)¶
8a. Portion membership — the notched ball (printed p. 52)¶
timeslice / snapshot usages draw a solid line with a filled ball,
open-V notch on the line side, at the whole-occurrence end — replacing
the plain typing edge. The «individual» / «timeslice» / «snapshot»
keywords ride the boxes.
spec_crop(
"spec-p83-time-slices-snapshots-and-portion-membership.png",
"spec-p83-individual-occurrence.png",
)
spec crop not available: spec-p83-time-slices-snapshots-and-portion-membership.png
spec crop not available: spec-p83-individual-occurrence.png
m8a = longeron.loads("""
package Portions {
individual part def Rover;
individual rover : Rover;
timeslice t1 : Rover;
snapshot s1 : Rover;
}
""")
w8a = diagrams.structure_diagram(m8a)
w8a
portions = edges(w8a, "sysml-edge-portion")
assert {(e.source.id, e.target.id) for e in portions} == {
("Portions::t1", "Portions::Rover"),
("Portions::s1", "Portions::Rover"),
}
assert all(e.properties.shape.end == "portion-ball" for e in portions)
assert node(w8a, "Portions::t1").labels[0].text == "«timeslice»"
assert node(w8a, "Portions::Rover").labels[0].text == "«individual part def»"
8b. Actors and stakeholders — stick figure and keyword box¶
Actor usages draw the spec’s classic stick figure by default (BNF
printed p.244; crop gt-actor.png): head circle + limbs line art in the
usage palette, name below the figure — the «actor» keyword is omitted
because the figure IS the stereotype. actor_style="box" keeps the
errata-N17 «actor» keyword-box alternative (right). Stakeholders stay
«stakeholder» keyword boxes in both styles — the spec reserves the
figure for actors.
import ipywidgets as W
m8b = longeron.loads("""
package Actors {
part def Person;
use case def Deliver {
subject route;
actor driver : Person;
}
requirement def Comfort {
stakeholder owner : Person;
}
}
""")
w8b = diagrams.structure_diagram(m8b) # actors draw the stick figure by default
w8b_box = diagrams.structure_diagram(m8b, actor_style="box") # «actor» keyword-box alternative
W.HBox([w8b, w8b_box], layout=W.Layout(align_items="stretch"))
driver = node(w8b, "Actors::Deliver::driver") # present in the built tree, by qname
assert "sysml-actor" in (driver.properties.cssClasses or "") # the stick-figure node class
assert driver.properties.shape.use.startswith("<circle") # head circle + limbs line art
assert [lbl.text for lbl in driver.labels] == ["driver : Person"] # name below, no «actor» row
assert node(w8b_box, "Actors::Deliver::driver").labels[0].text == "«actor»" # box fallback
owner = node(w8b, "Actors::Comfort::owner")
assert owner.labels[0].text == "«stakeholder»" # stakeholders stay keyword boxes
8c. View usages — «view» keyword box¶
A saved diagram’s recipe — a view usage, written by
view persistence (longeron.views;
design: docs/design/view-persistence.md) — is a model element too, so it draws where it
lives: the generic usage box with the «view» keyword. The spec
defines no dedicated view-usage glyph (views are diagram
configuration, §7.26), so the keyword box IS the spec
presentation; the view’s own members (expose, filter,
render) configure the view and draw nothing themselves.
m8c = longeron.loads("""
package Rig {
part def Axle { part hub : Hub [2]; }
part def Hub;
part axle : Axle;
view 'axle structure' : StandardViewDefinitions::InterconnectionView {
expose Rig::**;
render Views::asInterconnectionDiagram;
}
}
""")
w8c = diagrams.structure_diagram(m8c)
w8c
view_box = node(w8c, "Rig::axle structure")
assert "sysml-usage" in (view_box.properties.cssClasses or "") # rounded usage box
stereo = [
lbl.text for lbl in view_box.labels if "sysml-stereotype" in (lbl.properties.cssClasses or "")
]
assert stereo == ["«view»"] # the keyword row
assert view_box.children == [] # expose/filter/render configure; they never draw
9. Edge routing styles (toolbar)¶
SysML tools (and the spec’s figures) mix straight and orthogonal
connectors. Every widget’s toolbar carries a routing button (share-alt
icon) that cycles orthogonal → polyline → splines and re-lays the
live diagram out; the choice persists per widget on the tool’s routing
trait. Headless renders take the same choice as the routing= kwarg.
The option lands on the root and every compound node (ELK does not
inherit it through INCLUDE_CHILDREN). Endpoint glyphs auto-orient, so
the hollow triangles stay aligned on diagonal polyline shafts.
from longeron.toolbar import EdgeRoutingTool
w9 = diagrams.structure_diagram(m1a, routing="polyline")
tool = w9.get_tool(EdgeRoutingTool)
assert tool.routing == "POLYLINE" # seeded by the kwarg, persisted per widget
assert w9.source.value.layoutOptions["elk.edgeRouting"] == "POLYLINE"
tool.ui.click() # cycle: polyline -> splines
assert tool.routing == "SPLINES"
tool.routing = "orthogonal" # the trait normalizes and re-applies
assert w9.source.value.layoutOptions["elk.edgeRouting"] == "ORTHOGONAL"
w9
10. Compartments — labeled sections + selectable rows (printed pp. 46, 60, 199)¶
A definition/usage node is its name compartment plus a compartment stack (spec 8.2.3.6, printed p.199): every labeled compartment opens with a full-width separator rule and its italic name — ‘attributes’ (printed p.46), ‘parts’ (p.60), ‘directed features’ (p.62), the constraint compartments (p.127), … And every row is a first-class selectable element: it carries the projected element’s qualified name as its id, so clicking a row selects that attribute exactly like clicking a node (hover shows the pointer cursor; the tree and inspector follow).
spec_crop("spec-p77-attributes-compartment.png", "spec-p91-parts-compartment.png")
spec crop not available: spec-p77-attributes-compartment.png
spec crop not available: spec-p91-parts-compartment.png
m10 = longeron.loads("""
package Comp {
part def Battery {
attribute capacity : Real = 5200.0;
attribute cells : Integer = 3;
}
part def Drone {
attribute mass : Real;
part battery : Battery [2];
constraint massLimit { mass < 2.0 }
}
}
""")
w10a = diagrams.structure_diagram(m10)
w10a
battery = node(w10a, "Comp::Battery")
headers = [
lab.text[2:] # strip the fold twist the header affordance prepends
for lab in battery.labels
if "sysml-comp-label" in (lab.properties.cssClasses or "")
]
assert headers == ["attributes"] # the labeled compartment replaces the row blob
row = next(lab for lab in battery.labels if (lab.text or "").startswith("capacity"))
assert row.id == "Comp::Battery::capacity" # the row IS the attribute usage
assert row.properties.selectable is True # click -> the same selection seam as nodes
drone = node(w10a, "Comp::Drone")
drone_headers = [
lab.text[2:] for lab in drone.labels if "sysml-comp-label" in (lab.properties.cssClasses or "")
]
assert drone_headers == ["attributes", "constraints"] # spec-ordered stack
print("compartments OK")
compartments OK
10b. Parts as rows — the collapsed presentation (printed p.60)¶
Nested parts legally render either as drawn nested boxes (the
default — required where children anchor edges) or as textual
name : Type rows in a parts compartment. parts="rows" picks the
collapsed textual presentation; the rows keep full element identity.
w10b = diagrams.structure_diagram(m10, parts="rows")
w10b
drone_b = node(w10b, "Comp::Drone")
assert drone_b.children == [] # collapsed: no nested battery box
prow = next(lab for lab in drone_b.labels if lab.text == "battery : Battery [2]")
assert prow.id == "Comp::Drone::battery" and prow.properties.selectable is True
assert "parts" in [
lab.text[2:] # strip the fold twist the header affordance prepends
for lab in drone_b.labels
if "sysml-comp-label" in (lab.properties.cssClasses or "")
]
print("collapsed parts presentation OK")
collapsed parts presentation OK
Review checklist¶
Tick each glyph after checking both its resting and SELECTED look:
Specialization family (§1)
[ ] Subclassification
:>— plain hollow triangle, solid line[ ] Feature typing
:— colon dots behind the head[ ] Subsetting
:>— plain hollow triangle[ ] Redefinition
:>>— perpendicular bar tick[ ] Reference subsetting
::>— 2×2 colon dots[ ] «satisfy requirement» box + reference-subsetting edge (longhand)
Membership (§2)
[ ] Composite membership — filled diamond, role name,
[4]at the part end[ ] Referential membership — hollow diamond
[ ] Owned membership — true circle-plus at the owning end (
membership="edges")[ ] Alias — hollow circle at the referencing end, alias name label
Connectors (§3)
[ ] Plain
connect— no endpoint glyphs, cross multiplicities at the ends[ ] Directed connection — open-V head,
name : Typelabel[ ] N-ary connection — filled junction dot, three spokes
[ ] Proxy connection — filled dot on the drawn ancestor,
.part4label INSIDE the box[ ] Binding —
=riding the line mid-span[ ] Dependency — dashed open-V,
(Uses)label[ ] N-ary dependency — junction dot, plain client links
[ ] Satisfy shorthand — «satisfy» keyword edge
[ ] Allocate — «allocate» keyword edge
[ ] Named allocation — «allocation» box
Flows (§4)
[ ] Pin form — square pins straddling both borders, filled head at the target pin
[ ] Port-attached form — square-to-square, straight, filled head at the target square,
item1 : Item1riding the line
Ports (§5)
[ ] Direction squares — in / out / inout arrows, plain square without
[ ] Arrows read against the node interior —
inpoints INTO the box on any side[ ] Labels INSIDE the part body, next to the square
[ ] Conjugated ports —
~Poutreceives (in arrow),~Pinsends (out arrow)[ ] Interface — attaches square-to-square
Behavior views (§6)
[ ] Start dot, done bullseye
[ ] Fork and join bars (edges distribute along the bar)
[ ] Decision and merge rhombi — fans converge on single anchor points
[ ] Accept and send badge boxes
[ ] Successions dashed, guards labeled
[x > 0][ ] Terminate — circle-X
[ ] Swim lanes — dashed «performer» containers, left-to-right
[ ] State machine — entry marker, trigger-labeled transitions
[ ] Typed submachine expansion (
x : Inneropens up)
Annotations (§7)
[ ] Package folder tab
[ ] Comment/doc notes — folded corner WITH the crease ‘L’, dashed glyph-free anchors
[ ] Metadata adornment — «@Safety»
Portions and actors (§8)
[ ] Portion membership — filled notched ball at the whole end
[ ] «individual» / «timeslice» / «snapshot» keywords
[ ] «actor» / «stakeholder» keyword boxes
Toolbar (§9)
[ ] Routing button (
share-alt) cycles orthogonal → polyline → splines and re-lays out[ ] Markers/adornments stay attached and aligned on diagonal polyline shafts
[ ] The choice persists per widget;
routing=kwarg seeds it
Compartments (§10)
[ ] Separator rule spans the box edge-to-edge above each italic header
[ ] ‘attributes’ / ‘constraints’ / ‘parts’ headers stack in spec order
[ ] Rows hover with a pointer cursor and select in their own right (accent color)
[ ]
parts="rows"collapses nested boxes to rows; the default stays nested


























