Widgets

longeron.widgets is the way into the house widgets. The catalog below re-exports the canonical entry point of every interactive front-end, so one import surface covers them all:

from longeron.widgets import explore, mission_dashboard

The catalog is lazy (PEP 562). import longeron.widgets loads no widget toolkit, and each entry imports its home module on first access. If an entry’s extra is missing, the entry raises MissingExtraError with the exact install command when you reach for it.

The package is also the shared toolkit for widget authors and the mandatory home for every new widget. New widgets land here as submodules, beside the resident homes (longeron.widgets.explorer, longeron.widgets.app, longeron.widgets.inspector, longeron.widgets.replay, longeron.widgets.viewer3d, longeron.widgets.mission3d, longeron.widgets.graph3d, longeron.widgets.time). The pre-0.12 homes (longeron.explorer, longeron.inspector, longeron.app, longeron.analysis.viewer3d, plus replay_widget on longeron.replay and mission_viewer on longeron.analysis.mission3d) remain importable as deprecated aliases that warn once and will be removed in a future release.

The catalog

Entry

What it is

Extras

Taught in

explore

The model explorer: a tree navigator beside a diagram pane.

replay (explorer for Lab docking)

Tutorial 3

Explorer

The explorer widget class that explore builds.

replay

Tutorial 3

ModelTree

The explorer’s tree engine: disclosure rows, kind badges, live filter.

replay

Tutorial 3

ModelApp

The review workbench class that open builds.

replay

Tutorial 3

open

The review workbench: model list, explorer tabs, item inspector.

replay (explorer for Lab docking)

Tutorial 3

Inspector

The property sheet; open builds one as app.inspector.

replay

Tutorial 3

diagram

The diagram dispatcher: picks the view from the element’s kind.

vendored ipyelk

Tutorial 3

structure_diagram

Parts, ports, and connections as an interactive ELK diagram.

vendored ipyelk

Tutorial 4

state_diagram

A state machine as an interactive ELK diagram.

vendored ipyelk

Tutorial 2

action_diagram

An action’s control flow as an interactive ELK diagram.

vendored ipyelk

Tutorial 3 (via diagram)

replay_widget

Simulate an element and replay the run over its diagram.

replay, vendored ipyelk, node

Replay widget reference

scoreboard

The MAUT requirements scoreboard: area is importance, color is utility.

viz (the widget; scoring needs none)

Tutorial 6

mission_dashboard

The linked mission-compromise dashboard.

viz

Tutorial 4

grand_dashboard

The grand tour: diagram, CAD, scoreboard, sizing, consistency, and the mission globe on one surface.

viz, mdao, smt, vendored ipyelk

Tutorial 9

mesh_viewer

Baked geometry meshes in a three.js canvas, at true scale.

viz

Tutorial 4

mission_viewer

Fly a mission track on a Cesium globe.

viz

Tutorial 7

graph_viewer

The RDF projection as an interactive 3D force graph.

rdf, viz

Tutorial 8

Clock

The shared playhead for one linked group of time-aware views.

none

Time seam reference

Timebase

One recording, many views: a trace plus its optional mission binding.

none

Time seam reference

link_time

Wire time-aware views to one clock: the temporal link_selection.

none

Time seam reference

time_scrubber

The standalone transport bar: play/pause, rate, the shared time axis.

replay

Time seam reference

The pip extras install as pip install "longeron[replay,viz]" (or any subset). The vendored ipyelk installs as pip install -e vendor/ipyelk from a repo checkout. replay_widget also needs a node executable on PATH for the baked SVG.

The widget layer: longeron’s interactive front-ends, in one place.

This package has three jobs. It is the shared toolkit for widget authors (the anywidget conventions the house front-ends follow: baked JSON traitlets, kernel-side computation, on-demand rendering). It is the mandatory home for every new widget. And it is the catalog of the house widgets: every canonical entry point, re-exported under one roof, so longeron.widgets is the one import to learn.

The catalog is lazy (PEP 562). import longeron.widgets loads no widget toolkit; each entry imports its home module on first attribute access. If an entry’s extra is missing, the access (or the call, for homes that guard at call time) raises MissingExtraError with the exact install command.

Loss tolerance (the sync discipline every widget follows). Comm messages are fire-and-forget: under load the channel drops them (jupyter-server’s iopub rate limiter, websocket reconnects mid-burst), and trait sync only sends CHANGES, so a dropped update stays wrong forever unless the widget’s protocol heals it. The kernel is the source of truth; front-ends reconcile. Three tiers, matched to the state a widget mirrors:

  • Baked idempotent payloads (every widget): kernel -> front-end state rides absolute JSON traitlets (spec_json, czml_json, timeline_json…), never deltas, so any later push heals any earlier drop. This is the house convention already – keep it.

  • Single-shot interaction traits (picks, selections, splitter ratios, tool toggles): a drop loses one gesture and the user’s retry is the retransmit; the kernel-side handler must therefore be idempotent and order-independent. No extra machinery.

  • Live bidirectional seams (the time seam’s time / playing / rate: high-rate reports racing kernel seeks): generation stamps + acknowledged reports + full-state re-pushes + a trailing-edge verify, via longeron.widgets._seam (kernel mixin SeamHost, front-end lgnSeam). A new widget with kernel-mirrored state that either side can write while the other is also writing MUST ride this seam client; see the module’s docstring for the protocol and the CI anatomy that mandated it.

The entries (tutorial numbers refer to Tutorials):

  • explore – the model explorer: a tree navigator beside a diagram pane (tutorial 3).

  • Explorer – the explorer widget class that explore builds (tutorial 3).

  • ModelTree – the explorer’s tree engine: disclosure rows, kind badges, live filter (tutorial 3).

  • ModelApp – the review workbench class that open builds (tutorial 3).

  • open – the review workbench: model list, explorer tabs, item inspector (tutorial 3).

  • Inspector – the property sheet; open builds one as app.inspector (tutorial 3).

  • diagram – the diagram dispatcher: picks the view from the element’s kind (tutorial 3).

  • structure_diagram – parts, ports, and connections as an interactive ELK diagram (tutorial 4).

  • state_diagram – a state machine as an interactive ELK diagram (tutorial 2).

  • action_diagram – an action’s control flow as an interactive ELK diagram (tutorial 3 reaches it through diagram).

  • replay_widget – simulate an element and replay the run over its diagram (the replay reference).

  • scoreboard – the MAUT requirements scoreboard: area is importance, color is utility (tutorial 6).

  • mission_dashboard – the linked mission-compromise dashboard (tutorial 4).

  • grand_dashboard – the grand tour: diagram, CAD, scoreboard, sizing, consistency, and the mission globe on one surface (tutorial 9).

  • mesh_viewer – baked geometry meshes in a three.js canvas, at true scale (tutorial 4).

  • mission_viewer – fly a mission track on a Cesium globe (tutorial 7).

  • graph_viewer – the RDF projection as an interactive 3D force graph (tutorial 8).

  • Clock – the shared playhead for one linked group of time-aware views (the time-seam reference).

  • Timebase – one recording, many views: a trace plus its optional mission binding, aligned on one axis (the time-seam reference).

  • link_time – wire time-aware views to one clock, the temporal link_selection (the time-seam reference).

  • time_scrubber – the standalone transport bar: play/pause, rate, the shared time axis (the time-seam reference).

Current resident modules:

The pre-0.12 homes (longeron.explorer, longeron.inspector, longeron.app, longeron.analysis.viewer3d, plus replay_widget on longeron.replay and mission_viewer on longeron.analysis.mission3d) remain importable as deprecated aliases that warn once and will be removed in a future release.

class longeron.widgets.Clock(span=(0.0, 0.0), *, step_mode=False, rate=1.0, t=None)[source]

Bases: object

The shared playhead for one linked group of views.

t is the playhead in axis units (sim seconds, or the step index when step_mode), playing says someone is animating, rate is axis units per wall second (1.0 = real time; negative plays backwards, as Cesium’s shuttle ring does), and span is the (t0, t1) window seeks clamp into. The clock owns no wall-clock timer: views animate, the clock holds state and fans it out through plain callbacks, so the core package stays dependency-free.

The no-echo discipline is the selection seam’s, restated for floats: a seek() within 1e-3 of the current t does not fan out, playing/rate coalesce on equality, and every subscriber applies the same rule before writing back, so each write settles at its first fixpoint. Linking is explicit and scoped (link_time()); two dashboards in one notebook keep two clocks.

property span: tuple[float, float]

The (t0, t1) window; seeks clamp into it.

property step_mode: bool

True when the axis is a step index, not seconds.

property t: float

The playhead, in axis units. Assigning delegates to seek.

property playing: bool

True while some view animates. Assigning plays or pauses.

property rate: float

Axis units per wall second. Assigning delegates to set_rate.

seek(t)[source]

Move the playhead: clamp into the span, coalesce, fan out.

Return type:

None

play()[source]

Mark the group playing (idempotent; fans out on the flip).

Return type:

None

pause()[source]

Mark the group paused (idempotent; fans out on the flip).

Return type:

None

set_rate(rate)[source]

Change the playback rate (coalesces equal values).

Return type:

None

observe(callback)[source]

Subscribe to changes; returns the matching unobserve.

callback receives a traitlets-shaped change dict: {"name", "old", "new", "owner"} with name one of "t", "playing", "rate".

Return type:

Callable[[], None]

class longeron.widgets.Explorer(model, *, tree=None, layout='auto', mode='tab-after', structure_scope='package', height='600px')[source]

Bases: HBox

Tree navigator (left) + applicable-kind diagram pane (right).

Build one with explore(). The public knobs:

  • tree – the tree engine, any TreeView (default ModelTree; its selected / query traits are the headless automation surface);

  • kind_switcher – the toggle buttons offering the applicable diagram kinds for the current selection;

  • diagram – the currently displayed diagram widget;

  • select() – programmatic selection by qualified name or element;

  • save_view() (and the header’s save_button) – save the current diagram as a SysML v2 view usage plus sidecar entry (longeron.views);

  • element / kind – the current selection and view;

  • layout_strategy – the resolved layout ("inline" or "lab"; see explore());

  • dock_mode – how the lab layout docks into the shell (default "tab-after"; see explore()).

The panes are built ONCE; the layout strategy only composes them: inline puts them side by side in this HBox (28%/72%), lab docks them as a resizable JupyterLab split panel (lab_panel) and leaves a small placeholder in the cell output.

Diagrams are cached per (scope, kind): re-selecting inside the same package reuses the SAME widget (the browser keeps its layout), so a selection change costs one trait write, not a diagram rebuild.

property element: Element | None

The currently selected model element (None before a selection).

property kind: Literal['structure', 'state', 'action', 'requirements']

The active diagram kind (one of DIAGRAM_KINDS).

property diagram: Any

The diagram widget currently SHOWN in the right pane.

Built widgets stay in the diagram box as persistent children – their browser views must survive re-shows (see _show()) – so the shown one is the child whose display is not none.

select(target)[source]

Select by tree node id / qualified name, or by element.

Return type:

None

save_view(name=None, *, sidecar=None)[source]

Save the current pane’s diagram as a SysML v2 view usage.

The chrome affordance behind the header’s save button. The currently shown diagram widget carries its own root and kind (the longeron.views seam), so this is a thin capture: longeron.views.save_view() appends the typed view usage – with a recursive expose of the shown scope and the standard render reference – to the scope’s owning package, and the live presentation (direction, routing, collapse state) lands in the sidecar entry. sidecar is a path, None to auto-discover the workspace sidecar next to the model’s sources (skipped silently for in-memory models), or False to skip the sidecar write. The model text itself is NOT rewritten here: export the model (longeron.save() / to_sysml) or push it over the API to materialize the change. Returns the view usage element; the tree refreshes so the saved view appears.

Return type:

Element

refresh()[source]

Re-read the model after an edit: rebuild the tree, re-render.

The app calls this on its launched explorers after a longeron.edit rename or value edit (the tree’s labels, qualified-name node ids, and the diagrams’ drawn labels may all be stale). The tree payload rebuilds wholesale (_tree_data() + set_nodes – the cheapest correct hook); the resolver is rebuilt too, because renames invalidate its resolution caches. Every CACHED diagram is dropped and the current selection re-rendered through the normal _show path; the previously built widgets stay in the diagram box as hidden children – removing them would zombie their live browser views (see _show()) – but the cache drop guarantees a stale diagram is never shown again. Selection is preserved by element IDENTITY (its qualified name may have changed); an element that left the tree falls back to the root.

Return type:

None

class longeron.widgets.Inspector(app, *, layout=None, activate=False)[source]

Bases: VBox

The property sheet widget (module docstring for the full tour).

Built for one ModelApp (which exposes it as app.inspector); attaches to the app’s selection seam in the constructor and never detaches – it lives exactly as long as the app. layout defaults to the app’s own resolved strategy; activate reveals the right sidebar on dock (default False: the tab is one click away, and auto-expanding a collapsed sidebar reshapes the user’s layout uninvited).

The automation surface (tests, notebooks): element is what the sheet shows; _name_field / _doc_field / _value_field commit like the user’s Enter/blur when assigned.

property element: Element | None

The element the sheet currently shows (None before any).

show_element(element)[source]

Render the sheet for element (the seam callback).

Return type:

None

reveal()[source]

Expand the right sidebar and select the Inspector tab.

The sweeper clicks the inspector’s own sidebar tab (the same gesture a user makes; JupyterLab’s shell.expand_right alone would expand whatever tab was last current). A no-op inline. The APP calls this once per session, on the first app-launched selection (longeron.widgets.app.ModelApp._reveal_inspector_once(); open(reveal_inspector=False) disables it) – every LATER selection deliberately leaves the user’s layout alone.

Return type:

None

class longeron.widgets.ModelApp(*, layout='auto', activate=True, inspector=True, reveal_inspector=True)[source]

Bases: VBox

The sidebar workbench widget. Build one with open().

The programmatic surface mirrors every UI affordance (the notebook and test automation path): load_path(), add_model(), connect_api() / fetch_api_model(), explore_model(), scoreboard_model(), save_model(), push_model(), close_model() – plus the inspector seam documented in the module docstring (current_model / on_model_selected(), current_element / on_element_selected()).

property current_model: Model | None

The most recently loaded/selected model (None before any).

property current_element: Element | None

The most recently selected element in any app-launched tab.

on_model_selected(callback)[source]

Call callback(model_or_none) on every current-model change.

Return type:

None

on_element_selected(callback)[source]

Call callback(element) on every current-element change.

Return type:

None

property entries: tuple[ModelEntry, ...]

The loaded models’ source records, in load order.

property models: tuple[Model, ...]

The loaded models, in load order.

property explorers: tuple[Explorer, ...]

Every explorer this app launched or adopted (in wiring order).

Launched: explore_model() (a row’s Explore button, a notebook cell). Adopted: a DIRECT longeron.widgets.explorer. explore() call made while this app was the kernel’s most recent one (_adopt_explorer()) – its tree selections feed the inspector seam exactly like a launched tab’s.

load_path(path=None)[source]

Load the file or directory at path (default: the path field).

Files go through longeron.load() (.sysml/.kerml parse, .json import); directories merge every .sysml under them (load_dir()). The loaded model joins the list (replacing a previous load of the same source) and becomes current_model.

Return type:

Model

load_selected()[source]

Load every FILE selected in the browse listing, one entry each.

The multi-select path (ctrl/cmd-click in the listing, then the Load selected button): every picked file: row loads through load_path() and becomes its own models-list entry; directory rows in the selection are ignored (descending is a single-pick gesture). One busy strip covers the whole batch.

Return type:

list[Model]

add_model(model, *, source=None)[source]

Adopt an in-memory model (origin "text"; Save disabled).

Return type:

Model

close_model(model)[source]

Drop the model’s row (launched tabs stay; they own their views).

Return type:

None

refresh_explorers(model)[source]

Refresh every explorer in explorers showing model.

The bounded blast radius of a model edit (module docstring): launched AND adopted explorers rebuild their tree payload and the selection’s diagram (longeron.widgets.explorer.Explorer. refresh()); explorers the app never saw (constructed before it opened) and scoreboard tabs are left alone.

Return type:

None

select_element(element)[source]

Select element programmatically (the seam’s write half).

Routes through the most recently launched explorer of the element’s model when one exists – the tree reveals the element and the diagram highlights it, and the explorer’s own selection hook feeds the seam back. Without an explorer the seam updates directly, so the inspector still follows.

Return type:

None

explore_model(model)[source]

Launch an explorer tab (inline widget headless) for the model.

The explorer docks through its own idempotent identity (one tab per model, replaced on relaunch). Its tree selection feeds the inspector seam (_adopt_explorer()): every selection in the tab updates current_element (and current_model).

Return type:

Explorer

scoreboard_model(model)[source]

Launch a requirements scoreboard tab; returns the widget.

Raises AnalysisError when the model has no requirement usages (the row button is pre-disabled by the same test). Cell clicks in the tab feed the inspector seam through the widget’s selected trait.

Return type:

Any

save_model(model, path=None)[source]

Write the model back to its source (or path: save-as).

Single-file models write back to their file. Directory-loaded models write back FILE BY FILE: every tracked edit (longeron.edit) maps to the source file its top-level member was loaded from, and only files whose regenerated content differs from disk are rewritten (longeron.export.save_workspace()); an edit that cannot be mapped – or a top-level member with no recorded source file – refuses the save with nothing written. In-memory and API models need an explicit path (one merged file). API models push instead: push_model().

Return type:

Path

push_model(model, message='')[source]

Push an API-loaded model back as a commit (client.push_commit).

Return type:

dict[str, Any]

connect_api(url=None, token=None, *, client=None)[source]

Connect to a Systems Modeling API server and list its projects.

url/token default to the fold’s fields; client injects a pre-built Client-compatible object (the in-process test idiom). A bearer token rides an Authorization header (the Flexo JWT convention). Returns the connected client; the project picker fills on success.

Return type:

Any

fetch_api_model()[source]

Fetch the picked project/commit into the model list.

Return type:

Model

class longeron.widgets.ModelTree(nodes=(), **kwargs)[source]

Bases: AnyWidget

The built-in TreeView engine (a self-contained anywidget).

Disclosure rows, kind badges, filter, keyboard navigation. Pure presentation over TreeNode dicts – it holds no model references, only ids (qualified names). selected is the two-way selection trait (at most one id); setting it from Python reveals the node in the browser (ancestors expand, the row scrolls into view). query live-filters the tree exactly like the diagram toolbar’s search (case-insensitive substring over label and qualified name); match_count / total_count mirror its matches/total counter and are computed kernel-side too, so headless tests see the same numbers the browser shows. show_relationships (default True) is the tree-toolbar toggle’s trait: False hides every kind='relationship' row – they drop out of the rendered tree AND out of both counts – so kernels and notebooks can drive the toggle programmatically.

selected

selected node ids; [] = no selection

query

live filter text; empty shows the whole tree

show_relationships

whether relationship rows are shown (and counted)

match_count

how many visible nodes match the query

total_count

how many nodes the tree shows

set_nodes(nodes)[source]

Replace the tree’s contents (the browser re-indexes and re-renders).

Return type:

None

on_select(callback)[source]

Call callback with the selected ids on every selection change.

Return type:

None

reveal(node_id)[source]

Expand the node’s ancestors and scroll it into view (browser-side).

Return type:

None

filter(text)[source]

Live-filter the tree; returns the kernel-side match count.

Return type:

int

class longeron.widgets.Timebase(timeline, track=None, seconds_per_step=None)[source]

Bases: object

One recording, many views: a trace plus its optional mission binding, aligned on one axis.

timeline is the recorded truth (longeron.replay); track is the optional globe binding, built FROM that timeline (longeron.analysis.mission3d.track_from_timeline()), so the two views replay one execution. The shared axis is the timeline’s own: sim seconds for a timed trace (track seconds are then the same numbers, the 1:1 mapping the design verified), or the step index in step mode.

Step-only traces have no time axis, so a step-mode timebase refuses a track unless seconds_per_step states one (a scalar, or a per-step sequence/mapping – see step_seconds()); the same value must then have built the track. Stated durations count as first-class; only the synthesized gaps show up in synthetic_intervals(), which is what the scrubber labels.

property step_mode: bool

True when the shared axis is the step index.

property span: tuple[float, float]

The shared axis window: (t_start, t_end), or (0, n_steps - 1) in step mode.

seconds_at(t)[source]

Track seconds at axis position t (identity when timed).

Return type:

float

axis_at(s)[source]

Axis position at track second s (the seconds_at inverse).

Return type:

float

events_at(t0, t1)[source]

The fired transitions inside [t0, t1] (axis units, closed).

Return type:

list[FiredTransition]

env_at(t)[source]

The telemetry row at t: the last scalar-env snapshot at or before it (step semantics, like the tracks); {} before the first.

Return type:

dict[str, Any]

phase_at(t)[source]

The (phase, qname) of the track segment under t (axis units); None without a track or outside every segment. The final segment includes its end instant.

Return type:

tuple[str, str] | None

synthetic_intervals()[source]

The axis intervals whose seconds were synthesized (merged runs of unstated step durations); [] for timed traces and for step-only timebases that state no seconds axis at all.

Return type:

list[tuple[float, float]]

longeron.widgets.action_diagram(action, *, lanes=None, toolbar=True, routing='orthogonal', direction='right', max_label_width=480.0, height=None)[source]

The succession control-flow graph the interpreter executes.

Successions render dashed with open-V arrows and the behavior nodes use the spec glyphs (spec 8.2.3 printed p.227-228; figures pp.90-92): start = filled dot, done = bullseye, terminate = circle-X, fork/join = thick filled bar, decision/merge = empty rhombus, accept/send = the standard rounded action box with a filled top-left badge. Control glyphs carry single convergence anchors: every incoming edge joins at one point and every outgoing edge leaves from one point (fork/join bars excepted – their edges distribute along the bar, which is the bar’s semantic).

lanes (default off) partitions the flow into dashed-boundary «performer» swim lanes (spec “Perform Actions Swimlanes”, printed p.90): pass a mapping of lane title -> step names, or True to derive lanes from perform targets (perform part1.action1 lands in lane part1). Lanes are content-sized dashed containers ordered left-to-right via ELK layer partitioning – an honest approximation of the spec’s full-height, shared-boundary lanes. Steps in no lane stay outside (like the spec’s start/done markers). toolbar=False keeps ipyelk’s stock toolbar; routing picks the edge routing style (orthogonal / polyline / splines); direction the layout flow ("right", the flow-reading default, or "down"). max_label_width caps compartment-row display width exactly like structure_diagram() (behavior boxes carry no rows today, so the cap is future-proofing); height pins the widget’s rendered height to a CSS length exactly like structure_diagram() (default: the 400px-floor bare-cell behavior).

Return type:

Diagram

longeron.widgets.diagram(element, **kwargs)[source]

Pick a view by element kind: state machines, actions, else structure.

Return type:

Diagram

longeron.widgets.explore(model, *, tree=None, layout='auto', mode='tab-after', structure_scope='package', height='600px')[source]

Explore model: a tree navigator beside a diagram pane.

Keyword arguments reach Explorer (spelled out here so the vocabularies typecheck at the call site):

  • layout"auto" (the default: dock into JupyterLab when ipylab is installed and a Lab frontend is detected, else render inline), "inline" (a plain side-by-side HBox; works everywhere – nbclient, VS Code, docs), or "lab" (require the ipylab docking; raises MissingExtraError unless the explorer extra is installed);

  • mode – how the lab layout docks into the shell, passed straight through to JupyterLab ("tab-after", "tab-before", "split-right", "split-left", "split-top", "split-bottom", …). The default "tab-after" opens the explorer as its own full-width main-area tab WITHOUT stealing focus or width from the notebook; choose a split-* mode (or drag the tab) to see both at once. Ignored by the inline layout. Re-running the cell – or restarting the kernel and running all cells – REPLACES the model’s docked panel instead of accumulating copies (see the module docstring);

  • tree – a custom TreeView engine (default ModelTree);

  • structure_scope"package" (the default) scopes the structure view to the selection’s owning package so relationship edges to siblings stay visible; "element" scopes it to the selected namespace itself;

  • height – the explorer’s inline CSS height: the tree pane and the diagram pane both honor it (default "600px"; the diagram area takes the pane minus the kind-switcher header). The lab layout ignores it – the docked panel fills its tab, and the dock’s split handles own the sizing.

Return type:

Explorer

longeron.widgets.grand_dashboard(model, sizing=None, *, assembly='Rotorcraft::QuadCopter', states='DeepScout::FlightStates', sizer='ScoutSizing::IsrPrime', station_requirement='ScoutSizing::IsrStation', station_var='stationMinutes', loiter_var='loiterSpeed', what_if_station=420.0, values=None, waypoints=((33.7813, -84.3833, 350.0), (33.7885, -84.3785, 390.0), (33.79, -84.3695, 380.0), (33.7838, -84.369, 360.0), (33.777, -84.3825, 350.0)), events=(2.0, 'launch', 6.0, 'airborne', 150.0, 'low_battery', 10.0), ground_alt=300.0, imagery='satellite')[source]

The grand-tour dashboard (an ipywidgets VBox) – one call.

model carries the drone: its structure feeds the diagram, its interpreted M0 population sizes the 3D mesh, its requirement hierarchy is the scoreboard, and its states machine flies the Cesium mission over waypoints. sizing (default: model itself) carries the continuous side: sizer becomes the OpenMDAO problem behind the loiter slider, and station_requirement the Z3 consistency cards – the what-if card demands station_var >= what_if_station with loiter_var freed, an impossible floor whose UNSAT core names the binding constraints. values injects extra measured scoreboard bindings (e.g. performance measures computed through the interpreter); the live occlusion and disc-overlap measures are merged on top.

See the module docstring for the pane list and the wiring map. The returned layout exposes every piece for scripting and tests: .diagram, .viewer, .board (+ .scoreboard, the current Scoreboard), .elevation / .azimuth / .readout / .report, .loiter / .optimize / .problem / .optimum, .smt_sat / .smt_what_if, .mission / .track, .mesh / .part_map / .camera, .header, .config_view (the ConfigViewBinding behind the config-keyed 3D pane), and .unlink (drops the diagram <-> 3D binding).

Return type:

Any

longeron.widgets.graph_viewer(model_or_graph, *, namespaces=None, families=None, literals=False, external=False, isolated=True, seed=7, iterations=60, node_cap=5000, width_px=760, height_px=520)[source]

Explore a model’s RDF projection as an interactive 3D graph.

model_or_graph is a Model or a graph already built with longeron.rdf.to_graph() (pass the latter to keep evaluated=True literals in the hover payloads). namespaces / families / literals / external / isolated select the initial view exactly as in graph_view(); the in-scene panel (or widget.filter(...)) changes them later, re-layouting kernel-side on every change. seed and iterations steer the deterministic spring_layout() embedding; the layered dag_layout() embedding ships alongside it and the in-scene slider morphs between the two without kernel round trips.

Views larger than node_cap nodes keep the node_cap highest-degree nodes and say so in an in-scene notice: rendering is instanced and stays fluid into five figures, but the exact O(n^2) layout is the honest ceiling, so the cap protects the kernel rather than the GPU.

The widget’s selected trait (qualified names, two-way) plus on_select(callback) form the same selection contract the explorer’s tree exposes: clicks land in the kernel, kernel assignments drive the in-scene emphasis (and an eased camera fly-to), and counts / layout_seconds report the current view’s size and layout cost. focus(id, k=...) / unfocus() isolate a neighborhood kernel-side, and export_html(path) writes the current view as a self-contained standalone page.

Return type:

AnyWidget

Wire time-aware views to one clock (the temporal link_selection).

Each view is any widget with a time trait on the clock’s axis: the replay player, the mission viewer, the scrubber, or a future subscriber. The adapter observes the trait into Clock.seek() and fans clock changes back, both sides under the 1e-3 coalescing tolerance, so scrubbing one view scrubs them all and no write echoes. Views that also carry playing and rate traits (the scrubber; the mission viewer’s Cesium bridge) get those wired the same way, and the clock’s current state fans out to every view at link time.

The one non-identity mapping is the globe under a step-mode clock: steps are not seconds, so the binding is REFUSED unless seconds_per_step opts in (a scalar, or a per-step sequence/mapping matching the track’s own build – see step_seconds()); the adapter then maps step positions through the stated durations, scales rate to track seconds per wall second, and sizes the viewer’s drift tolerance to match.

A view holds ONE time link; linking it again replaces the previous adapter. Returns an idempotent unlink() that detaches every adapter, mirroring link_selection.

Return type:

Callable[[], None]

longeron.widgets.mesh_viewer(mesh, mesh_b=None, *, label='', label_b='', width_px=760, height_px=430)[source]

View one baked mesh dict, or two side by side at true scale.

mesh/mesh_b come from longeron.analysis.geometry (or any producer of the same schema). The canvas fills the notebook cell’s width; width_px/height_px set its aspect ratio (and the fallback width when the host width cannot be measured). Drag to orbit, shift-drag or right-drag to pan, scroll to zoom, double-click to re-fit. Assign a new JSON string to the returned widget’s mesh_json to swap the scene in place – e.g. from an observe handler on another widget.

Linked selection: widget.highlight(keys) pops the parts whose identity key (the key stamped by longeron.analysis.geometry.tag_parts(), else the part name) is in keys and dims the rest; widget.highlight() clears. A plain click on a part reports its key on the picked_json traitlet. See longeron.analysis.link.link_selection() for wiring both to a diagram.

Return type:

AnyWidget

longeron.widgets.mission_dashboard(source, *, missions=None, width_px=None)[source]

The linked mission-compromise dashboard (an ipywidgets VBox).

source is either a loaded model (the candidate table is baked via mission_dashboard_data(), a half-minute of interpreter time) or an already-prepared data dict from that function. By default the layout is FLUID: rows and plots stretch to the container width in their design proportions while the row heights hold the one-screen FLOOR, so the dashboard fills any screen without ever needing vertical scroll at 1080p. The floor is not a cap: docked in a height-constrained host (JupyterLab’s “Create New View for Output”) the dashboard grows to fill the host’s height and the two widget rows share the surplus. Draggable gutters between the major sections re-balance them (double-click resets); their ratios are persisted widget traits. Pass width_px to pin a fixed total width instead (see the module docstring for the layout).

The returned layout exposes its pieces for scripting and tests: .sliders (mission -> priority IntSlider), .requirements (mission -> key -> threshold FloatSlider), .top_n, .pareto_toggle (dominated-candidate filter), .pareto_hint (the one-line all-non-dominated hint beside the pressed toggle), .tabs (summary + one tab per mission), .parcoords (its brushes trait carries the live brush intervals by axis name; traced the selected line), .scatter, .viewer, .cards, .summary, .lineup (the pick cards; hover carries the transient parcoords line index, mirrored to .parcoords.highlight; selected the sticky selected card line), .splitters (the section gutters by name: rows between the plot and control rows – it also fills a height-constrained host – plots between parcoords and scatter, tabs between the tab set and the 3D side; each holds its ratio trait, clamped to [lo, hi], with ratio0 the double-click reset), .data, .live (the current apply_thresholds() table), .front (per-candidate non-dominated flags), .pool (the candidate indices currently in view – EMPTY when the toggle is on and nothing is eligible), .picks (the current top-N candidate indices), .scores (the MOE per candidate), .selected (the selected candidate index, or None), and .select(index) (drive the linked selection from Python; None clears).

Return type:

Any

longeron.widgets.mission_viewer(track, *, mesh=None, model_scale=1.0, label=None, height_px=480, imagery='satellite', ion_token='')[source]

Fly track on a Cesium globe in the notebook.

The viewer starts paused at the track epoch with the camera tracking the drone; Cesium’s native timeline and animation dial play, pause, scrub, and re-speed the mission. Pass mesh (a longeron.analysis.geometry mesh dict) to fly the airframe’s own geometry as a glTF model at model_scale times true size, flown with the multirotor attitude (yaw along the track heading, props level in vertical phases, the track’s tilt_deg forward tilt in cruise); without a mesh the drone is a point. imagery picks the tokenless base: 'satellite' (Esri World Imagery, the default), 'plain' (a neutral dark globe, no tiles), or 'osm' (OpenStreetMap streets); ion_token upgrades to Cesium World Terrain + imagery regardless. Click the drone (or a waypoint pin) to report its CZML id on picked_json; drive or observe the playhead through the bidirectional time trait. Assign a new JSON string to czml_json to swap the mission in place.

Return type:

AnyWidget

longeron.widgets.open(*, layout='auto', activate=True, inspector=True, reveal_inspector=True)[source]

Open the longeron model app (module docstring for the full tour).

  • layout"auto" (the default: dock into the JupyterLab LEFT sidebar when ipylab is installed and a Lab frontend is detected, else render inline), "inline" (the same widget in the cell output; works everywhere), or "lab" (require the sidebar docking; raises MissingExtraError unless the explorer extra is installed);

  • activate – reveal the sidebar panel once it attaches (the sweeper clicks the app’s own tab; JupyterLab does not activate left-area additions itself);

  • inspector – also build the item inspector (longeron.widgets.inspector), docked into the RIGHT sidebar under the lab layout (collapsed until clicked) and exposed as app.inspector everywhere. False skips it;

  • reveal_inspector – reveal the docked inspector ONCE, on the first element selection an app-launched tab feeds through the seam (so users see where selections land); every later selection leaves the layout alone. False keeps the inspector fully collapsed until its tab is clicked.

Re-running open() – or restarting the kernel and re-running – REPLACES the docked panel instead of stacking a second one; the fresh app starts with an empty model list (the returned handle owns the models).

Return type:

ModelApp

longeron.widgets.replay_widget(interpreter, element, events=None, *, inputs=None, width_px=760, kind=None, timeline=None)[source]

Simulate element and replay it over its diagram.

kind picks the view and recorder: "state" (record_timeline() over the state diagram) or "action" (record_action_timeline() over the action diagram). The default (None) auto-detects: elements whose kind is "action" replay as actions, everything else as a state machine.

timeline skips the recording and replays a PREBUILT Timeline instead, so one recording can feed this widget, the mission globe, and the time seam’s scrubber (see longeron.widgets.time); it excludes events/inputs. The widget’s bidirectional time trait is its seam surface: a kernel-side write seeks the playhead (stopping any front-end playback first), and the front-end reports the playhead at ~4 Hz while playing – longeron.widgets.link_time() subscribes it to a shared clock.

Needs the replay extra (anywidget) plus the diagram toolchain (vendored ipyelk and a node executable, as for render.to_svg).

Return type:

AnyWidget

longeron.widgets.scoreboard(model_or_element, values=None, aggregation='saw', *, weights=None, utilities=None, value_format='percent')[source]

MAUT-score the requirement hierarchy under model_or_element.

The scope’s root requirement usages become the top level (several roots aggregate under one synthetic root; requirement definitions contribute their attributes through typing – pass a definition itself to score it directly). values injects raw measurements: by requirement qualified name, by requirement name, or – for plain identifiers – as evaluation-frame bindings overriding the free references inside measure expressions and constraint bodies (see architecture_values() for the trade-study bridge). aggregation is a name from AGGREGATORS or any Aggregator; weights/utilities are exploration-time overrides keyed like values. value_format picks ONE consistent rendering for utilities/aggregates everywhere they display (str()’s table, the widget’s cell labels and tooltips): "percent" (the default; one decimal, 61.1%) or "float" (three decimals, 0.611). Scoreboard.table() always carries the raw floats.

Return type:

Scoreboard

longeron.widgets.state_diagram(machine, *, submachine_depth=None, toolbar=True, routing='orthogonal', direction='right', max_label_width=480.0, height=None)[source]

A hierarchical state machine: states, entry markers, transitions.

A state usage typed by a state def (state swap : ToteSwap;) is expanded into the definition’s full submachine – states, entry marker, transitions – the same member view the interpreter executes (StateMachine descends through members_of). Expansion is recursive and cycle-safe: a definition reached again through its own submachine draws as a collapsed leaf.

submachine_depth bounds how many typing hops to expand: None (the default) is unlimited, 0 draws typed states as plain leaves (the pre-0.8 behavior). Plain nested states are always shown. toolbar=False keeps ipyelk’s stock toolbar; routing picks the edge routing style (orthogonal / polyline / splines); direction the layout flow ("right" or "down"); max_label_width caps compartment-row display width exactly like structure_diagram() (state boxes carry no rows today, so the cap is future-proofing); height pins the widget’s rendered height to a CSS length exactly like structure_diagram() (default: the 400px-floor bare-cell behavior).

Expanded substate ids are instance-qualified (…::swapSource::swap::evaluating) so they stay unique per expansion site, selectable in the browser (the resolver walks typing hops), and exactly what longeron.replay records: two usages of one definition never share a replay key.

Return type:

Diagram

longeron.widgets.structure_diagram(element, *, show_attributes=True, show_relationships=True, composition='defs', membership='nested', annotations=False, actor_style='figure', parts='nested', levels=None, folded=None, toolbar=True, routing='orthogonal', direction='right', max_label_width=480.0, height=None)[source]

Containment structure with specialization/typing/connection edges.

composition="defs" (the default) draws definition-level membership edges – a filled diamond at the whole end for composite part/item members, a hollow diamond for referential (ref) members, role name on the line, multiplicity at the part end – per the SysML v2 Parts notation; composition="none" suppresses them. Flow / binding / dependency / satisfy / alias / portion notation is always drawn when both ends resolve to drawn nodes (see the module docstring).

membership="nested" (the default) draws each package’s owned members NESTED inside its box – the spec’s primary presentation and exactly the pre-0.8 output. membership="edges" draws the spec’s ALTERNATIVE presentation instead (printed p.26, errata E18): packages do not swallow their drawn members – every member becomes a SIBLING node and a solid owned-membership edge runs from the owning package, carrying a true circle-plus at the owning end. (Siblings keep ELK’s layered layout stable: an edge between a package and a node nested inside it is the ancestor<->descendant case the layout mishandles.) Membership edges are containment presentation, not relationship edges, so show_relationships=False keeps them.

Port usages owned by a drawn definition/usage box render as the spec’s boundary squares (10x10, straddling the border, name : Type label INSIDE the box next to the square – where the spec’s part figures write it – direction arrow inside the square when the port definition’s directed features agree on one); interface / connection / binding / flow ends then attach square-to-square, and connector ends naming UNDRAWN nested features draw the spec’s proxy dot on the shallowest drawn ancestor (printed p.67). Only nodes that own drawn ports opt into ELK port handling – everything else keeps the exact pre-port layout path.

annotations=True (default off, to keep existing diagrams uncluttered) additionally draws comment/documentation notes – the folded-corner box with a dashed anchor line (no endpoint glyph) to each annotated element (spec printed pp.20-21) – and «@Type» / «#keyword» metadata adornments on annotated nodes.

actor_style="figure" (the default) draws actor usages as the spec’s stick figure (BNF printed p.244) – head, body, arms, legs in the usage palette, name below the figure, no «actor» stereotype (the figure IS the stereotype); actor_style="box" keeps the «actor» keyword-box alternative (errata N17), which also shows compartments. Stakeholders always draw the «stakeholder» box – the spec reserves the figure for actors.

Textual members group into the spec’s LABELED compartments (8.2.3.6 printed p.199): every compartment opens with a full-width separator rule and its italic name – ‘attributes’ (printed p.46), ‘enums’ (p.48), ‘directed features’ (p.62; ‘parameters’ on action/calc boxes, p.91), the constraint compartments (p.127), ‘subject’, and so on – replacing the earlier unlabeled row blob. Every row is a first-class SELECTABLE projection of its model element: it carries the element’s qualified name as its id, clicking it in the browser feeds on_select() exactly like a node click, and kernel-side selection writes light it up.

parts picks the presentation of nested usages (both are legal spec notation; the option only chooses): "nested" (the default) draws them as nested boxes – the graphical compartment, required where children anchor edges (connections, flows, proxies) – while "rows" is the COLLAPSED presentation: parts, items, the occurrence family, actions, states, requirements, named satisfies and allocations, actors, stakeholders and views render as textual name : Type rows in their spec compartments (‘parts’ printed p.60, ‘items’ p.57, ‘actions’ p.89, ‘states’ p.117, …). Edges that would anchor on the collapsed children are not drawn – the textual presentation trades them for compactness.

levels names individual nodes (qualified names, or elements -> "partial" / "collapsed") whose rendition starts below the expanded default – the state behind the toolbar’s collapse button (which CYCLES the selected node: expanded -> partial -> collapsed -> expanded, each click one step less detail) and the level() kernel API (see CollapseTool). "partial" is the per-node version of parts="rows": the node’s rowable members become textual rows. "collapsed" is the smallest legal rendition: the name compartment alone – kind chip + name, no compartment stack, no drawn children (boundary port squares stay: they are border interface points, the classic black-box view); a collapsed PACKAGE likewise draws its folder box alone, whatever the membership mode. folded names per-node FOLDED compartments (qualified name -> compartment names): a folded compartment keeps its header – with the closed twist – and drops its rows while the node stays at its level (the header row’s click affordance in the browser; the fold() kernel API).

How collapse composes, level x presentation:

  • edges – connector-family edges (connections, bindings, interfaces, flows, allocates) that anchored on a shrunken node’s children re-anchor as the spec’s proxy dots on the node itself (printed p.67) at BOTH shrunken levels; connectors living entirely inside one shrunken node are part of the collapsed content and are not drawn; the specialization/typing family from undrawn children is not drawn (at partial, the rows’ : Type text carries it) – all exactly as under the diagram-wide parts="rows";

  • parts="rows" – every node is already textual, so "partial" changes nothing there and the toolbar cycle skips it (expanded -> collapsed -> expanded); "collapsed" and folded work unchanged;

  • folds – independent of the level: they apply to whatever compartments the node currently shows (attributes at expanded, parts rows at partial, none at collapsed) and are remembered through level changes.

toolbar=False keeps ipyelk’s stock text-button toolbar instead of the compact icon+search one (longeron.toolbar).

routing picks the ELK edge routing style – "orthogonal" (the default), "polyline" or "splines" – for headless renders and the initial widget; the toolbar’s routing button cycles it live. direction picks the layout flow – "right" (left-to-right, the default) or "down" (top-to-bottom); the toolbar’s orientation button toggles it live.

max_label_width caps how wide a compartment row may draw, in px (default 480): longer rows – calculation/expression attributes are the usual offenders – are end-ellipsized with the FULL text on the row’s hover tooltip, so one absurd expression no longer makes the whole node absurd. None lifts the cap (every row at full width).

height pins the widget’s rendered height to a CSS length (e.g. "480px") so inline compositions can match a neighbor exactly – tutorial 7 sits a diagram beside a 650px 3D viewer in an HBox. The default None keeps the bare-cell behavior: content-driven height with a 400px minimum floor. An explicit height always wins, even below that floor.

Return type:

Diagram

longeron.widgets.time_scrubber(timebase, *, width_px=760)[source]

The standalone transport bar for a recording.

A play/pause button, a rate select, a slim slider over the timebase’s span with tick marks at the recorded transition instants (a density band above ~100 events), the mission phase bands where a track binding exists, a readout clock, and the scalar-telemetry line that follows the playhead. Step-mode recordings read step k / N; where a seconds axis was stated per step the stated seconds show plainly and the synthesized segments carry an explicit (xN s) tag plus a striped band – a fabricated second is always displayed as fabricated.

The scrubber is one subscriber among equals: pass it to link_time() beside the replay player and the mission viewer. While playing it animates locally at the shared rate and syncs its time trait at ~4 Hz, exactly like its peers.

Needs the replay extra (anywidget).

Return type:

AnyWidget