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 MOE axis (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 Tab next 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_m over the missions where the candidate is feasible, MINUS weight_m * INFEASIBLE_PENALTY where 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: key is the achieved value (a derived metric, or a variant attribute via variant_attr), constraint the assert it re-arms, attr the 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).

thresholds maps mission -> {value key -> minimum}. An equipment option is eligible when its non-threshold constraints hold (baked ok) 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 with candidates.

Return type:

list[dict[str, Any]]

longeron.analysis.dashboard.compromise_scores(candidates, weights, *, penalty=0.5)[source]

Weighted-compromise scores (the MOE), aligned with candidates.

Each candidate mapping needs metric and feasible sub-mappings keyed by the mission names in weights. 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.

Return type:

list[float]

longeron.analysis.dashboard.front_justifications(points, metrics, front)[source]

One-line alibis for front members drawn in a 2-D projection (pure).

front flags non-domination over the FULL objective space; points are the drawn plane (x minimized, y maximized); metrics hold 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 get None. Exact plane ties never count as beaters (they overlap in the plot, so they cannot mislead).

Return type:

list[str | None]

longeron.analysis.dashboard.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.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()). cost is the shared baseCost build-up when the model derives one. Threshold defaults come from the subject’s own attributes (thresholds specs default to DEFAULT_THRESHOLDS); slider ranges from the achieved values. The returned dict carries the studies themselves so the dashboard can compute margins lazily.

Return type:

dict[str, Any]

longeron.analysis.dashboard.pareto_mask(objectives, eligible=None)[source]

Non-dominated flags, larger-is-better on every objective axis.

a is dominated when some eligible b is >= on every axis and differs on at least one – so exact ties survive together (the same weak-dominance convention as longeron.analysis.trades.pareto()). Ineligible rows are flagged False and never dominate anyone. Orient minimized axes (cost) by negation before calling.

Return type:

list[bool]