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 (AGGREGATORS maps each to its implementation; any Aggregator callable is accepted too)

alias of Literal[‘saw’, ‘min’, ‘geometric’]

class longeron.analysis.scoreboard.Aggregator(*args, **kwargs)[source]

Bases: Protocol

A 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: object

One 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: object

MAUT utilities and aggregates over one requirement hierarchy.

Build with scoreboard(). .score is 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 Aggregation member, or a custom callable’s cleaned __name__)

property score: float

The root aggregate utility (NaN when nothing is measured).

table()[source]

Flat pre-order rows: qname, weight, share, raw, utility, aggregate.

Return type:

list[Row]

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.

tessellation picks "treemap" (squarified) or "voronoi" (the vendored d3-voronoi-treemap; seed makes its iteration deterministic, re-derived per zoom root). Hover a cell for details; click to select (the selected trait). 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 / aggregate label 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.

collapsed pre-collapses subtrees by qualified name; zoom_root starts zoomed into one ("" is the tree root); max_depth (default None = 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 next max_depth levels. value_format picks 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 own value_format (percent). All navigation state is scriptable: selected, collapsed, zoom_root, max_depth and value_format are 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 the viz extra (anywidget).

Return type:

AnyWidget

payload()[source]

The widget’s node tree as plain data (the nodes_json payload).

Assign json.dumps(board.payload()) to an existing widget’s nodes_json trait to repaint it in place from a re-scored board – the live-update seam the composed dashboards use.

Return type:

dict[str, Any]

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_FUNCTIONS maps each to its fn(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 metrics mapping qualifies – longeron.analysis.trades.Architecture in particular, whose interpreter-exact derived metrics then override the same-named free references inside measure expressions and constraint bodies:

best = max(
    study.all_architectures(),
    key=lambda a: scoreboard(model, values=architecture_values(a)).score,
)
Return type:

dict[str, Any]

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). 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