3. Views for review¶
The question: a design review is tomorrow. How do reviewers read this model without reading text?
The subject is the DeepScout UAV program in examples/deepscout, the same
workspace tutorial 1 walked as data and tutorial 2 executed. Reviewers do not read its six
SysML files. They read views: an explorer tree, four diagram kinds, a
property sheet, and saved view artifacts. Every view renders the same model
object, so the views cannot disagree with the source.
You will learn how to:
open the explorer over the workspace and read relationships as tree rows.
render structure, state, action, and requirement views from one dispatcher.
follow the selection seam: tree, diagram, and inspector move together.
inspect values with first-class units, and edit with honest refusals.
save the reviewed model, and save diagrams back into it as view usages.
The first cell is the whole review surface: the tree beside the diagram, one click deep. (Widget cell: captured at landing.)
import longeron
from longeron.widgets import explore
program = longeron.load("../examples/deepscout")
ex = explore(program, layout="inline")
ex.select("Rotorcraft::QuadCopter")
assert ex.element is program.find("Rotorcraft::QuadCopter")
print("tree rows:", ex.tree.total_count)
print("selected:", ex.tree.selected[0], "->", ex.kind, "diagram beside the tree")
ex
tree rows: 1717
selected: Rotorcraft::QuadCopter -> structure diagram beside the tree
The tree shows relationships, not just parts¶
The explorer pairs a tree with a diagram pane. One click selected
QuadCopter. The diagram pane rendered its package and highlighted the
selection. In JupyterLab, explorer.explore(program) docks the same panes
as a main-area tab. layout="inline" keeps this notebook self-contained.
The tree lists relationships as rows under the element that owns them. The DeepScout program declares 23 satisfy rows, 10 connections, one allocation, and one dependency, plus its imports and one flow. A reviewer reads who satisfies what without opening a file. The tree toolbar’s link button hides the relationship rows. The next cell counts the rows, drives the toggle programmatically, and restores it.
from longeron import model as M
satisfies = [e for e in program.iter_tree() if isinstance(e, M.SatisfyUsage)]
print("satisfy rows:", len(satisfies))
print("one of them: satisfy", satisfies[0].subsets[0], "by", satisfies[0].by)
full = ex.tree.total_count
ex.tree.show_relationships = False
hidden = full - ex.tree.total_count
print(f"toggle off: {hidden} relationship rows leave the tree and the counts")
ex.tree.show_relationships = True
assert len(satisfies) == 23 # 13 rotorcraft edges, 9 tailless S&C edges, the tilt-tri's engine-out
assert hidden > 25 # satisfies, connections, allocations, imports, and more
assert ex.tree.total_count == full
satisfy rows: 23
one of them: satisfy PitchStability by FlyingWingSingle
toggle off: 90 relationship rows leave the tree and the counts
Four views, one dispatcher¶
diagrams.diagram(element) picks the view from the element’s kind:
View |
Renders |
|---|---|
structure |
packages, definitions, usages, and their edges |
state |
hierarchical states, entry markers, labeled transitions |
action |
the succession graph the interpreter executes |
requirements |
requirements, satisfy edges, and the satisfying parts |
The first three come from longeron.diagrams. The fourth is the explorer’s
requirements kind, shown two sections below. The next cell dispatches
three DeepScout elements and displays the flight state machine.
(Widget cell: captured at landing.)
from longeron import diagrams
flight = diagrams.diagram(program.find("DeepScout::FlightStates"))
plan = diagrams.diagram(program.find("DeepScout::PlanBattery"))
kit = diagrams.diagram(program.find("ScoutParts::F450Kit"))
for widget in (flight, plan, kit):
state = widget._lgn_view_state # the stamp save_view reads later
print(f"{state['element'].qualified_name:24s} -> {state['kind']} view")
flight
DeepScout::FlightStates -> state view
DeepScout::PlanBattery -> action view
ScoutParts::F450Kit -> structure view
The toolbar: fit, center, collapse, search¶
Hover over any diagram, and a toolbar slides in along the top edge. It
holds Fit, Center, and Collapse buttons plus a search box. Search
highlights every node whose name contains the text and shows a
matches/total count. Search never touches the selection, so selection
callbacks cannot fire from it. The same tool is scriptable, which the next
cell uses.
from longeron import toolbar
ex.select("Rotorcraft::QuadCopter")
finder = ex.diagram.get_tool(toolbar.DiagramSearch)
finder.query = "motor"
print(f"{finder.match_count} of {finder.total_count} nodes match 'motor'")
assert finder.match_count > 0
finder.query = "" # clearing restores every highlight
7 of 46 nodes match 'motor'
The requirements view¶
A review needs the compliance landscape: which requirements exist, and who
satisfies them. The explorer’s requirements kind projects that landscape
from the selected element’s package. It draws the requirement boxes, the
satisfy edges, and the satisfying parts. The projection is read-only. Every
element keeps its real owner, so the view never mutates the model.
(Widget cell: captured at landing.)
ex.select("Rotorcraft::QuadCopter")
ex.kind = "requirements"
satisfy_edges = [
e for e in ex.diagram.source.value.edges if "sysml-edge-satisfies" in e.properties.cssClasses
]
print("satisfy edges drawn:", len(satisfy_edges))
print("one edge:", satisfy_edges[0].source.id, "->", satisfy_edges[0].target.id)
assert len(satisfy_edges) == 13 # the Rotorcraft package's own satisfy rows
ex
satisfy edges drawn: 13
one edge: Rotorcraft::QuadCopter -> DeepScout::FlightEnvelope
The selection seam¶
This section is the canonical exposition of the seam. Tutorials 4 through 9 link here instead of re-teaching it.
Every longeron surface shares one selection contract:
Each surface exposes its selection as a widget trait.
Selecting in one surface writes the same qualified name into the others.
Each write settles at its first fixpoint, so no echo loops back.
Switching the diagram kind preserves the selection.
The next three cells prove each clause on the explorer. The tree drives the diagram. The diagram drives the tree with exactly one write in each direction. The kind switch keeps the selected element. Later tutorials attach more surfaces to the same contract: the trade plots in tutorial 4, the scoreboard in tutorial 6, and the 3D scene in tutorial 7.
ex.kind = "structure"
ex.select("Rotorcraft::QuadCopter")
assert tuple(ex.kind_switcher.options) == ("structure", "requirements")
assert tuple(ex.diagram.view.selection.ids) == ("Rotorcraft::QuadCopter",)
widget = ex.diagram
ex.select("Rotorcraft::TriCopter")
assert ex.diagram is widget # same package scope: the cached widget is reused
print("tree -> diagram:", ex.tree.selected[0], "highlighted in the", ex.kind, "view")
tree -> diagram: Rotorcraft::TriCopter highlighted in the structure view
ex.select("DeepScout::MultiRotor") # a known start, so this cell re-runs cleanly
widget = ex.diagram
tree_writes, diagram_writes = [], []
# bind each sink as a default arg: a re-run's observers keep writing to
# THEIR list, not to the rebound global
ex.tree.observe(lambda ch, sink=tree_writes: sink.append(ch["new"]), "selected")
widget.view.selection.observe(lambda ch, sink=diagram_writes: sink.append(ch["new"]), "ids")
widget.view.selection.ids = ["DeepScout::PlanBattery"] # what a browser click does
assert ex.tree.selected == ["DeepScout::PlanBattery"]
assert ex.diagram is widget # the clicked diagram is not rebuilt
assert tree_writes == [["DeepScout::PlanBattery"]] # exactly one write each way
assert diagram_writes == [("DeepScout::PlanBattery",)]
print("diagram -> tree: one write each way, no echo")
diagram -> tree: one write each way, no echo
ex.select("DeepScout::FlightStates::idle")
assert tuple(ex.kind_switcher.options) == ("structure", "state", "requirements")
ex.kind = "state" # switch the view, not the subject
assert ex.tree.selected == ["DeepScout::FlightStates::idle"]
assert tuple(ex.diagram.view.selection.ids) == ("DeepScout::FlightStates::idle",)
print("kind switch to 'state' kept the selection on 'idle'")
kind switch to 'state' kept the selection on 'idle'
The app: the no-code entry¶
longeron.widgets.open() builds the review workbench. In JupyterLab it docks a
panel into the left sidebar. Reviewers without a notebook click the
Longeron tile in Lab’s launcher instead. The tile starts one console
session and runs the same app.open() call. Both surfaces are browser-only,
so they stay prose here. (Sidebar and tile: captured at landing.)
The panel loads models through a path field with a Browse listing. A Connect-to-API fold loads from a Systems Modeling API server instead. Each model row carries an Explore button and a Score button. Explore docks the explorer you already used. Score docks the requirements scoreboard that tutorial 6 teaches.
Every button has a programmatic twin on the returned handle. The cells below drive the twins, so the whole workflow runs headless. The next cell opens the app, loads the workspace, and launches an explorer tab.
from longeron.widgets import app
application = app.open()
model = application.load_path("../examples/deepscout")
tab = application.explore_model(model)
print("layout strategy:", application.layout_strategy)
for entry in application.entries:
print(f"loaded: {entry.source} (origin: {entry.origin})")
layout strategy: inline
loaded: ../examples/deepscout (origin: dir)
The inspector: units are first-class¶
app.open() builds an item inspector and exposes it as
application.inspector. In JupyterLab it docks into the right sidebar.
Click any element in an app-launched tab, and the sheet follows: a kind
chip, a path breadcrumb, read-only property rows, and editable name,
documentation, and value fields. (Sheet: captured at landing.)
Units get three dedicated facts on the sheet:
The value field shows the magnitude with its unit symbol:
1.5 kg.The typed-by row keeps the type and names the unit beside it:
Real [kg]. The type and the unit are different facts. Both stay visible.The unit row gives the symbol and its dimension:
kg — mass, resolved from the model’s own unit table.
The next cell selects the airframe mass ceiling through the explorer tab and reads all three facts off the sheet.
tab.select("DeepScout::MultiRotor::maxTakeoffMass")
sheet = application.inspector
element = application.current_element
assert sheet.element is element # the tab selection fed the seam
symbol, unit_row = sheet._unit_facts(element) # the sheet's own formatter
print("value field:", sheet._value_field.value)
print("typed by: ", f"{element.types[0]} [{symbol}]")
print("unit row: ", unit_row)
assert sheet._value_field.value == "1.5 kg"
assert (element.types, symbol) == (["Real"], "kg")
assert unit_row == "kg \u2014 mass" # em-dash: 'kg -- mass'
value field: 1.5 kg
typed by: Real [kg]
unit row: kg — mass
Editing cascades or refuses¶
Review findings become edits. The sheet’s fields commit through
longeron.edit, and this notebook calls the same functions. Every edit
validates before it mutates. A safe edit applies and cascades. An unsafe
edit raises EditError and changes nothing. In the sheet, the refusal
lands verbatim in an error strip, and the field reverts.
edit.rename rewrites every textual reference that reaches the element:
typings, connector ends, satisfy targets, imports, and expression
references. It then re-resolves every site to prove that nothing changed
meaning. The next cell documents the quad, renames it, and watches the
satisfy row follow. The cell after that attempts a rename that collides
with a sibling name, and the model refuses it.
from longeron import edit
tracker = edit.track(model)
edit.set_doc(model, "Rotorcraft::QuadCopter", "Baseline quad-rotor configuration. Reviewed.")
renamed = edit.rename(model, "Rotorcraft::QuadCopter", "QuadRotor")
print("references rewritten:", tracker.changes[-1].detail["rewritten"])
assert model.find("Rotorcraft::QuadRotor") is renamed
assert quad_satisfy.by == "QuadRotor" # the satisfy row followed the rename
print("the satisfy row now reads:", longeron.to_sysml(quad_satisfy).strip())
references rewritten: 4
the satisfy row now reads: satisfy FlightEnvelope by QuadRotor;
from longeron.errors import EditError
try:
edit.rename(model, "Rotorcraft::QuadRotor", "TriCopter") # a sibling owns that name
except EditError as refusal:
print("refused:", refusal)
assert model.find("Rotorcraft::QuadRotor") is not None # nothing moved
edit.rename(model, "Rotorcraft::QuadRotor", "QuadCopter") # the review verdict: keep it
print("renamed back; the doc edit stays unsaved")
refused: name 'TriCopter' is already used by another member of Rotorcraft
renamed back; the doc edit stays unsaved
Value writes validate semantics¶
A reviewer types 0.42 [SI::kgg], and the write must not land.
edit.set_attribute_value resolves every unit reference against the
model’s unit table before anything mutates. An unknown symbol is refused
with a did-you-mean hint. A resolvable unit with the wrong dimension is
refused too, stating both dimensions. validate=False is the documented
escape hatch for a deliberate re-dimensioning.
Validate-on-write is a review-integrity guarantee: a value that landed is a value that resolved. The next cell aims both bad writes at the mass ceiling and proves the value never moved.
mass_qname = "DeepScout::MultiRotor::maxTakeoffMass"
for attempt in ("0.42 [SI::kgg]", "0.42 [SI::s]"):
try:
edit.set_attribute_value(model, mass_qname, attempt)
except EditError as refusal:
print(f"{attempt!r} refused:\n {refusal}")
assert model.find(mass_qname).value.expr.to_text() == "1.5 [SI::kg]" # untouched
print("the mass ceiling still reads 1.5 kg")
'0.42 [SI::kgg]' refused:
unit 'SI::kgg' does not resolve (did you mean 'SI::kg' or 'SI::g'?)
'0.42 [SI::s]' refused:
current value of 'DeepScout::MultiRotor::maxTakeoffMass' is 'kg' [mass]; 's' [s] is duration; pass validate=False to override
the mass ceiling still reads 1.5 kg
Save the review copy¶
Every edit.* call records on the model’s change tracker. In the app, a
dirty model row grows a dot, and the row’s tooltip lists the unsaved
changes. Save clears the dot.
This workspace came from a directory, so Save needs an explicit path and
writes one merged .sysml file. application.save_model(model, path=...)
writes the file, marks the tracker saved, and refreshes the row. The next
cell saves, reloads the file, and finds the review edit in the copy.
API-loaded models get Push instead of Save, which sends a commit
through client.push_commit. This notebook runs no API server, so Push
stays prose.
import tempfile
from pathlib import Path
assert tracker.dirty # the doc edit is unsaved: the model row shows the dot
review_dir = Path(tempfile.mkdtemp())
saved = application.save_model(model, path=review_dir / "deepscout_review.sysml")
assert not tracker.dirty # saved: the dot clears
reloaded = longeron.load(str(saved))
print("review note in the saved copy:", reloaded.find("Rotorcraft::QuadCopter").doc)
review note in the saved copy: Baseline quad-rotor configuration. Reviewed.
Saved views are review artifacts¶
A good diagram is worth keeping. save_view writes the current diagram
into the model itself as a SysML v2 view usage. The usage is typed by a
standard view definition and exposes the shown scope, so any SysML v2 tool
can read the recipe. Presentation details land in a JSON sidecar beside the
model sources: direction, routing, and the collapse state. restore_view
rebuilds the widget from the view usage plus the sidecar. A missing sidecar
degrades to default presentation, never to an error.
The explorer’s header has a save button, and ex.save_view is its
programmatic twin. The cell below passes an explicit sidecar path, which
keeps the shipped example directory clean. The restored widget then exports
to SVG for the review packet.
from longeron import render, views
ex.select("DeepScout::FlightStates")
ex.kind = "state"
sidecar = review_dir / "views.json"
view = ex.save_view("flight review", sidecar=sidecar)
print("view usage:", view.qualified_name, "typed by", view.types[0])
assert program.find("DeepScout::flight review") is view
restored = views.restore_view(program, view, sidecar=sidecar)
svg_path = review_dir / "flight_review.svg"
render.to_svg(restored, svg_path)
print("review packet:", svg_path.name, "--", svg_path.stat().st_size, "bytes of SVG")
view usage: DeepScout::flight review typed by StandardViewDefinitions::StateTransitionView
review packet: flight_review.svg -- 11089 bytes of SVG
Where this goes¶
Reviewers read the model through views: the tree, four diagram kinds, the property sheet, and saved view artifacts. Every view rendered one model object. Every edit validated or refused. The model stayed the single source of truth, which is the review story.
Tutorial 4 attaches the trade-study surfaces to the same selection seam. Brushing a candidate in a plot selects it everywhere, exactly like the tree click in this notebook. Tutorial 6 attaches the scoreboard, and tutorial 7 attaches the 3D scene. The seam contract stays the one this notebook proved: one write per surface, no echo, and selection survives the view switch.


