Requirements scoreboard¶
Scoring runs on the interpreter alone; the widget requires the viz
extra (pip install "longeron[viz]").
The reserved attributes on a requirement (weight, utility,
measure, and the display-only unit) are documented in the module
docstring below; the unit attribute annotates the raw value in
tooltips and tables without any conversion (a units integration is
designed separately).
A MAUT scoreboard over the requirements hierarchy (spike).
Multi-attribute utility theory (MAUT) on top of the model’s requirement
usages: every leaf requirement maps a raw measured value onto a [0, 1]
utility through a declared utility shape; parents aggregate their
children’s utilities by importance weight; the root aggregate is the
design’s overall score. scoreboard() builds the
Scoreboard, whose widget() renders it as an
interactive treemap (or Voronoi) tessellation where AREA is importance
and COLOR is utility.
Weights and utility shapes live IN THE MODEL, as plain attribute usages on requirement definitions/usages (all of which the grammar parses today; typed usages inherit them from their requirement definition, own declarations override inherited ones):
requirement endurance {
attribute weight : Real = 3.0; // importance (default 1.0)
attribute utility : String = "larger-is-better";
attribute ramp0 : Real = 15.0; // utility 0 anchor
attribute ramp1 : Real = 45.0; // utility 1 anchor
attribute measure : Real = flightTime; // the raw value
attribute unit : String = "min"; // display unit (optional)
}
The optional unit attribute names the measurement unit of the raw
value, DISPLAY-ONLY: it shows after raw in the widget tooltip and
the text table (Row carries it), and the ramp/target anchors
are read in that same unit. No conversion happens anywhere – SysML
quantity values like 32.0 [SI::min] parse and evaluate to their
magnitude (the measurement reference is an annotation), and a proper
units integration (pint) is designed separately.
The shape vocabulary (UTILITY_FUNCTIONS): larger-is-better
and smaller-is-better (linear between the ramp0 -> 0 and
ramp1 -> 1 anchors, orientation validated), ramp (either
orientation), target-is-best (1 at target, falling to 0 at
limit away), and step (pass/fail). step is the DEFAULT: a
leaf requirement with no utility declaration scores 1 when its own
require constraint bodies hold (via
check_requirement()) and 0 when
they do not. Raw values come from the model too – the measure
attribute’s expression is evaluated by the interpreter in the
requirement’s own context – or are injected per call: values=
entries override by requirement qualified name, by requirement name,
and as evaluation-frame bindings for the free names inside measure
expressions and constraint bodies. That last form is the trade-study
bridge: architecture_values() turns a
Architecture into exactly such a
dict, so scoreboard(model, values=architecture_values(arch)) scores
any mix without touching the model. Python-side weights= /
utilities= keyword overrides exist for exploration; the model
remains the source of truth.
Aggregation is pluggable (AGGREGATORS or any
Aggregator callable over (weight, utility) pairs):
saw – weight-normalized simple additive weighting, the default –
min (weakest link), and geometric (weighted geometric mean).
Unmeasured leaves (no raw value, or a non-applicable requirement) carry
utility NaN and are excluded from their parent’s aggregation; a fully
unmeasured subtree aggregates to NaN.
Area semantics in the widget: a node’s area share among its siblings is
its weight’s share, recursively – so a COLLAPSED subtree (click a
group’s twist to collapse or expand it in place) renders as one cell
occupying exactly the area its leaves occupied, colored by the subtree
aggregate. Double-click zooms instead of collapsing: a group cell
re-tessellates to fill the whole canvas (a leaf zooms to its parent
group), the breadcrumb bar above the canvas walks back out (Esc steps
out one level), and max_depth windows the render depth below the
current zoom root so deep hierarchies reveal themselves level by level.
Zoom and depth window are VIEW state only – scores and aggregates are
always computed over the full tree. Hover shows qualified name,
weight and share, raw value (with its declared unit), and utility;
click writes the selected trait
(the same observer idiom as the other longeron widgets, ready for
linked selection). Group MEMBERSHIP is legible without interaction
(maintainer QA: the twist alone never said which cells belong to the
group, especially in the Voronoi): every expanded group’s perimeter –
the union of its member cells’ edges – draws as a two-tone boundary
tier (a thin dark core over the white casing, heavier when shallower),
groups with enough room pin a small name / aggregate label at
their perimeter, and hovering a group’s twist (or that label) spotlights
the group’s full extent – a translucent wash covers everything
outside the group, so its member cells pop at their exact utility
colors inside a brand-colored rim. Selection renders as an inset ring – the cell’s own
perimeter stroked wide but clipped to the cell, so it never clips at
the canvas edge nor vanishes under a neighbor’s stroke (maintainer QA:
the old centered stroke read as one stray blue line) – plus a
hue-preserving brightness/saturation lift; hatched unmeasured cells
show both. Utilities and aggregates render through ONE
consistent format everywhere (cell labels, tooltips, the text table):
percent with one decimal by default, or three-decimal floats under
value_format="float". Unmeasured cells are grey and hatched. The color
ramp is red -> yellow -> green interpolated in OKLab (perceptual, with
a monotone-ish lightness cue for red/green-weak viewers). Both
tessellations are deterministic: stable model order, and the Voronoi
iteration runs on a seeded PRNG (seed=, re-derived per zoom root so
every zoom level re-tessellates stably).
The Voronoi tessellation is computed by Kcnarf’s d3-voronoi-treemap,
vendored with its dependency closure as one inlined bundle
(longeron/_js/voronoi_treemap.bundled.js; rebuild instructions in
voronoi_treemap.VENDOR.md next to it) – all BSD-3-Clause / ISC:
d3-voronoi-treemap 1.1.2 (BSD-3-Clause), d3-voronoi-map 2.1.1
(BSD-3-Clause), d3-weighted-voronoi 1.1.3 (BSD-3-Clause), d3-hierarchy
3.1.2 (ISC), d3-array 2.12.1 (BSD-3-Clause), d3-polygon 2.0.0
(BSD-3-Clause), d3-timer 2.0.0 (BSD-3-Clause), d3-dispatch 2.0.0
(BSD-3-Clause), internmap 1.0.1 (ISC).
The widget requires the viz extra (pip install "longeron[viz]");
everything else runs on the interpreter alone.
- longeron.analysis.scoreboard.AGGREGATORS: dict[str, Aggregator] = {'geometric': <function _geometric>, 'min': <function _weakest_link>, 'saw': <function _saw>}¶
the aggregation-strategy registry
- longeron.analysis.scoreboard.UTILITY_FUNCTIONS: dict[str, Callable[[Any, Mapping[str, float]], float]] = {'larger-is-better': <function _larger_is_better>, 'ramp': <function _ramp>, 'smaller-is-better': <function _smaller_is_better>, 'step': <function _step>, 'target-is-best': <function _target_is_best>}¶
the utility-shape registry (each:
fn(raw, params) -> [0, 1])
- longeron.analysis.scoreboard.Aggregation¶
the named aggregation strategies (
AGGREGATORSmaps each to its implementation; anyAggregatorcallable is accepted too)alias of
Literal[‘saw’, ‘min’, ‘geometric’]
- class longeron.analysis.scoreboard.Aggregator(*args, **kwargs)[source]¶
Bases:
ProtocolA parent’s utility from its measured children’s
(weight, utility).The scoreboard filters unmeasured (NaN-utility) children BEFORE the call and never calls an aggregator with an empty sequence (a fully unmeasured subtree is NaN without consulting the strategy) – a custom aggregator only ever sees finite utilities in [0, 1] with non-negative weights.
- class longeron.analysis.scoreboard.Row(qname, name, depth, kind, weight, share, shape, raw, unit, utility, aggregate)[source]¶
Bases:
objectOne requirement’s line in
Scoreboard.table()(pre-order).
- class longeron.analysis.scoreboard.Scoreboard(target, *, values=None, aggregation='saw', weights=None, utilities=None, value_format='percent')[source]¶
Bases:
objectMAUT utilities and aggregates over one requirement hierarchy.
Build with
scoreboard()..scoreis the root aggregate,table()the flat pre-order rows,widget()the treemap/Voronoi view.str()renders an aligned text table.- aggregation: str¶
the aggregation strategy’s display name (an
Aggregationmember, or a custom callable’s cleaned__name__)
- widget(tessellation='treemap', *, collapsed=(), zoom_root='', max_depth=None, seed=42, width_px=960, height_px=540, value_format=None)[source]¶
The scoreboard as one interactive anywidget.
tessellationpicks"treemap"(squarified) or"voronoi"(the vendored d3-voronoi-treemap;seedmakes its iteration deterministic, re-derived per zoom root). Hover a cell for details; click to select (theselectedtrait). The navigation gestures:double-click a group cell to ZOOM into that subtree (it re-tessellates to fill the whole canvas); double-clicking a leaf zooms to its parent group. A breadcrumb bar above the canvas tracks the zoom path (hidden at the tree root): each crumb zooms back out, and Esc steps out one level. Zooming is pure navigation – it never touches the collapsed set.
the twist on a group (the small triangle) collapses or expands that group IN PLACE, at any depth: a collapsed group renders as one cell occupying its subtree’s total area, colored by the subtree aggregate.
group MEMBERSHIP affordances (hover-only – no new gestures): every expanded group’s perimeter draws as an always-on two-tone boundary tier over exactly its member cells, groups with enough room pin a
name / aggregatelabel at their perimeter, and HOVERING a group’s twist or perimeter label SPOTLIGHTS the group’s full extent (everything outside it washes out; member cells keep their exact utility colors). Selection is an inset ring plus a hue-preserving fill lift, identical across both tessellations and on hatched cells.
collapsedpre-collapses subtrees by qualified name;zoom_rootstarts zoomed into one (""is the tree root);max_depth(defaultNone= unlimited) windows the render depth below the CURRENT zoom root – deeper levels draw as aggregate cells (same visual as collapsed, without entering the collapsed set), and zooming in reveals the nextmax_depthlevels.value_formatpicks how utilities/aggregates render in cell labels and tooltips:"percent"(one decimal, e.g.61.1%) or"float"(three decimals, e.g.0.611); default: the scoreboard’s ownvalue_format(percent). All navigation state is scriptable:selected,collapsed,zoom_root,max_depthandvalue_formatare two-way traits. None of them affect scoring, which always runs over the full tree. Unmeasured leaves render HATCHED (the honest no-data state); when MORE THAN HALF of the tree’s leaves are unmeasured a one-line footer legend explains the hatching (hatched = unmeasured (n of m leaves ...)), so an all-unmeasured board never reads as broken (maintainer QA). Needs thevizextra (anywidget).- Return type:
AnyWidget
- longeron.analysis.scoreboard.Tessellation¶
the scoreboard widget’s cell tessellations: squarified treemap, or the vendored d3-voronoi-treemap
alias of
Literal[‘treemap’, ‘voronoi’]
- longeron.analysis.scoreboard.UtilityShape¶
the declared utility shapes (
UTILITY_FUNCTIONSmaps each to itsfn(raw, params) -> [0, 1]implementation)alias of
Literal[‘larger-is-better’, ‘smaller-is-better’, ‘ramp’, ‘target-is-best’, ‘step’]
- longeron.analysis.scoreboard.ValueFormat¶
how utilities/aggregates print and render:
"percent"(one decimal,61.1%) or"float"(three decimals,0.611)alias of
Literal[‘percent’, ‘float’]
- longeron.analysis.scoreboard.architecture_values(architecture)[source]¶
A
values=dict from a trade-study architecture (duck-typed).Any object with a
metricsmapping qualifies –longeron.analysis.trades.Architecturein particular, whose interpreter-exact derived metrics then override the same-named free references insidemeasureexpressions and constraint bodies:best = max( study.all_architectures(), key=lambda a: scoreboard(model, values=architecture_values(a)).score, )
- longeron.analysis.scoreboard.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).
valuesinjects raw measurements: by requirement qualified name, by requirement name, or – for plain identifiers – as evaluation-frame bindings overriding the free references insidemeasureexpressions and constraint bodies (seearchitecture_values()for the trade-study bridge).aggregationis a name fromAGGREGATORSor anyAggregator;weights/utilitiesare exploration-time overrides keyed likevalues.value_formatpicks 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: