Dashboard¶
Requires the viz extra (pip install "longeron[viz]").
The mission-compromise dashboard: linked widgets over the trades.
One mission_dashboard() call composes the existing house widgets
(longeron.analysis.viz.parcoords(),
longeron.widgets.viewer3d.mesh_viewer()) with plain ipywidgets into
the single artifact that ties requirements -> architectures ->
performance -> cost. The layout is built to fit one 1080p screen
(1920x950 content area) without vertical scrolling: the design row
heights are FLOORS while the rows and plots stretch to the container
width in their design proportions (pass width_px to pin a fixed
total width instead). In a height-constrained host – JupyterLab’s
“Create New View for Output” docks the dashboard in a panel with its
own height budget – the dashboard grows to FILL that height and the
two widget rows share the surplus in their design ratio (the header
strip stays fixed), every plot re-rendering to its new box. Draggable
GUTTERS between the major sections re-balance them by pointer (plot
row vs control row, parcoords vs scatter, tab set vs the 3D side);
double-click a gutter to restore the design ratio. The ratios live on
widget traits (_gutter_class()), so a re-render keeps the user’s
layout:
a HEADER STRIP – the title, a PARETO-ONLY toggle button (see below), and the top-N slider sizing the 3D lineup;
a PLOT ROW – the parallel-coordinates widget over the cross-mission metrics PLUS a computed
MOEaxis (the weighted compromise score) side by side with a compact MOE-vs-cost scatter; brushing a parcoords axis downselects the candidate pool, and the whole table re-bakes in place as sliders move (brushes survive by axis name). The brush INTERVALS sync to the kernel by axis name too, so every recompute derives the brushed subset from the table it just baked – a toggle flip or slider move can never pair fresh rows with stale brushed row indices – and a brush that excludes everything empties the picks honestly instead of silently showing the whole pool;a CONTROL ROW – one ipywidgets
Tabnext to the 3D viewer. Tab 0 summarizes all three missions: the best compromise, the mission priority sliders (one 0-100 weight per mission) feeding the MOE, and a per-mission scorecard. Tabs 1-3 hold one mission each: its requirement-threshold sliders (defaults and constraint identities read from the model’s own requirement attributes) and its requirement margin card (green holds / red broken, threshold rows tracking the sliders live). The 3D viewer sits BESIDE the tab, so slider moves and their geometric consequences share one glance: the top-N compromises render to scale in an adaptive grid (longeron.analysis.geometry.lineup()), each cell captioned in-scene. A column of LINEUP CARDS sits between the tab and the viewer: one card per pick (rank, mix, MOE, cost, and – for front members – a one-line justification, see below). Hovering a card transiently traces that candidate’s line in the parallel coordinates, where every dominance axis is visible.
LINKED SELECTION is one state shared by every view: clicking a lineup
card – or a 3D model in the lineup, whose parts carry per-candidate
identity keys through longeron.analysis.geometry.lineup() –
selects that candidate everywhere at once. The selected card border,
its scatter halo, its parallel-coordinates line, and its 3D model all
take the same violet accent (_SEL), deliberately distinct from
the petrol-blue brush (longeron.analysis.viz.ACCENT), the
terracotta top-N rings (longeron.analysis.viz.WARM), and the
green/red verdicts, so a selection stays visible WHILE brushing.
Selection is sticky until the candidate leaves the visible pool (or the
background is clicked in 3D); hover stays transient on top of it.
The PARETO-ONLY toggle filters dominated candidates out of the linked
views. Dominance is over the study’s objective axes – cost (minimized)
and every mission’s primary metric (maximized) as currently thresholded:
candidate a is dominated when some candidate b is at least as
good on every axis and not identical on all of them, so exact ties
survive (pareto_mask(), pure and unit-tested). Candidates feasible
for no mission never join the front. The dominated set ignores the
priority WEIGHTS on purpose: re-prioritizing must never change which
designs are efficient, only which efficient design wins. The toggle
never silently drops: when the current thresholds leave NOTHING
eligible the front is empty, and the filtered views empty out with a
message instead of falling back to the whole (dominated) catalog.
The MOE-vs-cost scatter is a 2-D PROJECTION of that 4-axis dominance
space, so a front member can sit below-right of another point and still
be non-dominated: it wins on an axis the plane hides. The dashboard
says so explicitly (front_justifications(), pure and unit-tested):
every front member’s scatter tooltip and lineup card carry one line
naming hidden metrics against the points that 2-D-dominate it in the
drawn plane – either one metric that strictly beats ALL of them, or (no
single metric covers every dominator) a minimal set of metrics such that
EVERY dominator strictly trails on at least one. Non-domination over
cost plus those metrics guarantees a winning metric per dominator.
The scatter’s INK tells that same 4-axis truth: every front member wears the front accent in BOTH toggle states – FILLED when it also leads the drawn plane, an OPEN RING when its win lives on a hidden axis – and gray is reserved for dominated points (light gray: infeasible), so ‘Pareto only’ can never paint a front member as chaff (the maintainer’s third report: off-staircase front members drawn in the dominated gray). The staircase line is the PLANE’s frontier, not THE front; an in-plot legend names each ink in a few words, and a hint beside the pressed toggle says the whole filtered view is non-dominated.
All linking runs in Python via traitlets observers – the candidate table is a couple hundred rows, so every front-end stays a dumb painter per the house widget pattern.
Compromise scoring (compromise_scores(), pure and unit-tested):
each mission’s primary metric is min-max normalized over the candidates FEASIBLE for that mission (an infeasible mix’s metric is what it would score if it could fly – fiction, so it never stretches the scale);
score = sum_m weight_m * normalized_mover the missions where the candidate is feasible, MINUSweight_m * INFEASIBLE_PENALTYwhere it is not: a design that cannot fly a weighted mission actively costs its weight instead of scoring a silent zero;weights are the sliders’ 0-100 values normalized to sum one (all-zero falls back to equal weighting).
That score is the dashboard’s MOE (measure of effectiveness): the parcoords axis, the scatter’s y, and the lineup ranking are all the same number, recomputed on every slider move.
Requires the viz extra (anywidget + ipywidgets arrive with it):
pip install "longeron[viz]".
- longeron.analysis.dashboard.DEFAULT_MISSIONS: dict[str, tuple[str, str]] = {'ISR': ('ScoutMissions::IsrUav', 'stationMinutes'), 'intercept': ('ScoutMissions::InterceptUav', 'maxTargetSpeed'), 'logistics': ('ScoutMissions::LogisticsUav', 'payloadRangeKgKm')}¶
the DeepScout mission trio (
examples/deepscout/missions.sysml): name -> (assembly, metric)
- longeron.analysis.dashboard.DEFAULT_THRESHOLDS: dict[str, list[dict[str, Any]]] = {'ISR': [{'attr': 'minStationMinutes', 'constraint': 'stationReq', 'key': 'stationMinutes', 'label': 'station (min)', 'step': 5.0}], 'intercept': [{'attr': 'targetSpeed', 'constraint': 'canCatch', 'key': 'maxTargetSpeed', 'label': 'target (m/s)', 'step': 1.0}], 'logistics': [{'attr': 'minPayloadKg', 'constraint': 'payloadReq', 'key': 'payloadKg', 'label': 'payload (kg)', 'step': 0.5, 'variant_attr': ('cargo', 'payloadKg')}, {'attr': 'minDeliveryRadiusKm', 'constraint': 'radiusReq', 'key': 'deliveryRadiusKm', 'label': 'radius (km)', 'step': 1.0}]}¶
requirement-threshold sliders per mission:
keyis the achieved value (a derived metric, or a variant attribute viavariant_attr),constraintthe assert it re-arms,attrthe assembly attribute holding the model’s default – the sliders are anchored in the model
- longeron.analysis.dashboard.INFEASIBLE_PENALTY = 0.5¶
score deducted (times the mission’s weight) when a candidate cannot fly a mission at all – documented in the module docstring
- longeron.analysis.dashboard.apply_thresholds(candidates, thresholds)[source]¶
The live per-candidate mission table under the given floors (pure).
thresholdsmaps mission -> {value key -> minimum}. An equipment option is eligible when its non-threshold constraints hold (bakedok) AND every achieved value clears its floor; the best eligible option by mission metric represents the candidate. When nothing is eligible the best-by-metric option is kept so the requirement cards can show why in red – but the displayed metric is 0.0 and the mission infeasible. Rows align withcandidates.
- longeron.analysis.dashboard.compromise_scores(candidates, weights, *, penalty=0.5)[source]¶
Weighted-compromise scores (the MOE), aligned with
candidates.Each candidate mapping needs
metricandfeasiblesub-mappings keyed by the mission names inweights. Normalization bounds come from the feasible candidates per mission (a constant or empty feasible set normalizes to 1.0 for whoever is feasible); see the module docstring for the full scoring contract.
- longeron.analysis.dashboard.front_justifications(points, metrics, front)[source]¶
One-line alibis for front members drawn in a 2-D projection (pure).
frontflags non-domination over the FULL objective space;pointsare the drawn plane (x minimized, y maximized);metricshold each row’s values on the axes the plane hides (larger is better). A front member can be 2-D-dominated in the plane yet stay efficient, so every front row gets one compact line about its plane dominators (its “beaters”): the metric(s) on which it strictly beats ALL of them when such metrics exist, otherwise a greedy minimal metric set such that EVERY beater strictly trails on at least one (non-domination over cost plus these metrics guarantees each beater trails somewhere, but no single metric need cover them all). Front rows nothing 2-D-dominates are called unbeaten; rows off the front getNone. Exact plane ties never count as beaters (they overlap in the plot, so they cannot mislead).
- longeron.analysis.dashboard.mission_dashboard(source, *, missions=None, width_px=None)[source]¶
The linked mission-compromise dashboard (an ipywidgets
VBox).sourceis either a loaded model (the candidate table is baked viamission_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. Passwidth_pxto 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(itsbrushestrait carries the live brush intervals by axis name;tracedthe selected line),.scatter,.viewer,.cards,.summary,.lineup(the pick cards;hovercarries the transient parcoords line index, mirrored to.parcoords.highlight;selectedthe sticky selected card line),.splitters(the section gutters by name:rowsbetween the plot and control rows – it also fills a height-constrained host –plotsbetween parcoords and scatter,tabsbetween the tab set and the 3D side; each holds itsratiotrait, clamped to[lo, hi], withratio0the double-click reset),.data,.live(the currentapply_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, orNone), and.select(index)(drive the linked selection from Python;Noneclears).- Return type:
- longeron.analysis.dashboard.mission_dashboard_data(model, missions=None, thresholds=None)[source]¶
Bake the dashboard’s candidate table from a multi-mission model.
Candidates are the Cartesian product of the variation points SHARED by every mission study (for the UAV example: airframe, motors, props, battery, material). Per candidate and mission, EVERY equipment option is kept – its exact metric, its achieved values for the mission’s requirement thresholds, and whether its non-threshold constraints hold – so requirement sliders can re-evaluate feasibility live without re-running the interpreter (
apply_thresholds()).costis the sharedbaseCostbuild-up when the model derives one. Threshold defaults come from the subject’s own attributes (thresholdsspecs default toDEFAULT_THRESHOLDS); slider ranges from the achieved values. The returned dict carries the studies themselves so the dashboard can compute margins lazily.
- longeron.analysis.dashboard.pareto_mask(objectives, eligible=None)[source]¶
Non-dominated flags, larger-is-better on every objective axis.
ais dominated when some eligiblebis >= on every axis and differs on at least one – so exact ties survive together (the same weak-dominance convention aslongeron.analysis.trades.pareto()). Ineligible rows are flaggedFalseand never dominate anyone. Orient minimized axes (cost) by negation before calling.