Skip to content

SystemDefiner consistency overhaul: module split, cross-reference cascades, validator - #3

Merged
JScholz-tech merged 21 commits into
mainfrom
fix/systemdefiner-consistency
Jul 15, 2026
Merged

SystemDefiner consistency overhaul: module split, cross-reference cascades, validator#3
JScholz-tech merged 21 commits into
mainfrom
fix/systemdefiner-consistency

Conversation

@JScholz-tech

Copy link
Copy Markdown
Owner

Summary

A config-mutation audit of the SystemDefiner found 12 classes of silent inconsistency: mutations that leave config.yaml valid — it loads and solves — but internally wrong, so scenario/MC parameters silently retarget, TCs go stale, and element edits orphan stored values. This PR restructures the app and fixes all of them, plus a 13th found during verification.

Note: this branch was cut from feat/composition-sunburst, so its commits (sunburst composition view, CSV flow-data import, extensible hierarchy depth, T16 golden) are included here as the base.

Restructuring (move-only, pinned by tests)

  • main.py (3,119 lines) split into routers/ (one APIRouter per domain) + forms.py, cascades.py, health.py, scenario_params.py, deps.py. Route table pinned byte-identical by a route-inventory test.

Consistency fixes

  • New consistency.py: iter_flow_pointers() — single registry of every scalar flow-ID pointer field (new schema fields register once, all cascades pick them up) — and check_config_consistency(), merged into the overview health report and asserted clean after every mutation route in the test suite.
  • Renumber remaps scenario/MC parameter names (exact flow IDs and embedded P{pid}_… / TC_E{n}_{ff}_{tt} / TC_Cap_{pid} names, FlowCap cap_tc_id), renames _N-suffixed duplicate-edge flow IDs, and is order-independent (two-pass rename).
  • Process delete purges scenario/MC entries naming the deleted process, so a reused ID can't inherit them.
  • DSM_Component spare-part flows included in every rename/purge/renumber cascade (previously dangled).
  • Flow rewire moves TC ownership to the new source (stale TCs used to silently override later edits via duplicate derived names) and drops stranded flow data/compositions.
  • Convention flow IDs (F_<from>_<to>[_N]) validated against endpoints on create/edit.
  • Element rename/reorder/delete cascades through every element-keyed dict and remaps positional E{n} parameter names (template carries per-row original names).
  • Import: no BOM wipe without a BOM sheet, stale-reference purge, duplicate-ID rejection (Excel + YAML paths).
  • scenario_engine.check_mass_balance indexed by process ID, not list position; ID gaps no longer silently drop flows (same fix as plotting/validation.py, 332b047).
  • Storage: saves normalize (processes sorted by ID, orphan BOM/IS pruned); folder name is authoritative on load — a hand-copied study carrying another study's internal name: used to have every save written into the other study's folder (found live in heatpumps_1).
  • Hardening: gap-tolerant DSM form parsers, hierarchy-cycle guard (used to hang four pages), scenario-name URL validation, anchored dynamic-TC field parsing.

Tests

  • 04_tests/app: 150 → 183 tests. New: route-inventory + full-page round trip, one mutation test per audit finding, round-trip over every tracked case study (normalization introduces no new errors, save/load idempotent).
  • Full suite: 399 passed including golden regression T01–T16 (T01/T04 re-pinned after schema-default normalization; all arrays verified bit-identical).

Known follow-ups (not in this PR)

  • The checker now flags real pre-existing damage in local user studies (heatpumps*, DGAW, Lexmark TC_Cap_02 no-ops) — fix via UI.
  • Element-name-based parameter IDs (instead of positional E{n}) would remove that bug class entirely but needs an Excel-template migration.

🤖 Generated with Claude Code

JScholz-tech and others added 21 commits July 13, 2026 15:58
The composition/hierarchy editors were hard-capped at 4 depth levels
("layers"), even though the data model, YAML/Excel serialization, and the
engine were already depth-agnostic. Lift the cap so users can add/remove
hierarchy levels ("columns") as well as element branches ("rows").

- config_schema: pad_level_names no longer truncates to 4 — accepts any
  number of levels (min 2), backfilling defaults only when too few.
- main.py: _rules_to_paths pads to the actual deepest path; elements_save
  collects level-name and path-cell fields tolerantly (dynamic count).
- elements.html: dynamic level-name editors with + Add Level / remove
  controls; path table columns, preview, and JS derive depth from the DOM;
  init grows columns to fit a stored hierarchy deeper than its named levels.
- compositions/tc_edit_dynamic/initial_stock_edit/bom_edit: the shared
  grouped-matrix widget now derives its column count from the level list /
  deepest path instead of a hardcoded 4.

Golden regression unaffected (engine untouched). Verified via HTTP
round-trip: a 6-level hierarchy with a 5-deep branch saves and reloads.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Flow Data editor rendered every input flow expanded with an eager
Chart.js build, and points could only be entered one row at a time — slow
for studies with many flows or long timelines.

- Accordion: each flow collapses to a one-line header (name, id, P→P, live
  point count). Charts build lazily on first expand, so a study with many
  flows loads instantly. Adds Expand-all / Collapse-all; first flow opens
  by default.
- CSV import: a per-flow "Import CSV" button parses a "year,value" file
  client-side and replaces that flow's points (then Save persists them via
  the existing POST — no new route). Tolerates a header row, ';'/tab
  delimiters, and European comma decimals.

Verified: flow_data page renders with the new controls; parseCsv unit-tested
against header/comma-decimal/semicolon/junk-row cases.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add plot_flow_composition_sunburst() as a supplementary hierarchical view
alongside the stacked-bar plot_flow_composition(). The element hierarchy is
drawn as concentric rings — total-mass element at the centre, top-level
elements in the first ring, sub-elements outward — with a lighter
"Remaining X" wedge for any untracked residual so children sum to their
parent (branchvalues="total"). Nodes whose children exceed them are expanded
to fit rather than raising.

Reuses build_element_children_map() and flow.Values[year, element]; Plotly is
already a dependency. Follows the module convention (renders via fig.show(),
not exported from plotting/__init__). Surfaced in section 2.4 of
00_BioDYM_Workflow and the Composition tab of the Voila dashboard (aggregates
all flows at the final year by default; flow_id/year selectable).

Verified against a solved T01 system (correct nesting, no branchvalue
violations) and full pytest (356 passed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The sunburst used fig.show(), which clashed with the sibling FigureWidget
from plot_flow_composition when both rendered in the same ipywidgets Output
(Voila dashboard) — breaking the existing stacked-bar composition figure.

Switch plot_flow_composition_sunburst to the same FigureWidget + display()
mechanism as plot_flow_composition, and add live controls:
- Flow dropdown ("All flows" or a single flow)
- Year slider
Both update the figure in place via batch_update (trace arrays swapped),
so node count can change between selections. Export button now exports the
current view. Returns None (display-only) to match the module convention;
use _build_composition_sunburst_figure for a static figure in tests.

Verified: interactive construct, per-flow/per-year builds, and the
FigureWidget trace-swap update; full pytest 356 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Set the sunburst figure height to 750px (from Plotly's ~450 default) and
bump the base font to 14px so the rings and labels are easier to read.
Width stays responsive (100% of the container in VoilA).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New tutorial case study T16_Complex_Flow_Composition exercising the
extensible composition hierarchy and the sunburst view together:

- Six named levels (Device → Module → Component → Material → Substance →
  Element) with a genuinely 6-deep element tree, e.g.
  Laptop → Mainboard → Electronics → Metals → Precious_Metals → Gold and
  Laptop → Battery → Cells → Active_Material → Lithium_Compound → Lithium
  (22 elements, 12 hierarchy rules).
- Two input flows (Laptop, Smartphone) sharing the tree but differing in
  their top-level module split, so the sunburst's Flow dropdown is meaningful.

Golden-pinned like T01–T15 (glob T[0-9][0-9]_*): reference npz added, solver
converges, golden suite green (17 passed). Verified the sunburst renders 6
rings for both flows with correct deep-chain masses and no branchvalue
violations.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
plot_total_mass_balance_error indexed inflows/outflows by process ID
(flow.P_Start/P_End) but stock dS by ProcessList position. When a study's
processes are not stored in ascending-ID order (e.g. P12 defined before
P11), a process's flows were paired with a different process's stock,
producing a phantom mass-balance error on the mislabelled bar.

Index all three arrays and the per-process loop consistently by p.ID,
sized to the highest process ID. No-op for studies already in ID order.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
flow_edit_save updated from_process/to_process but never re-derived the
F_<from>_<to> ID, so editing a flow's endpoints left a stale ID that
disguised the real topology and could hide duplicate edges (a stray
F_09_17 wired 12->17 masked a second P12->P17 edge).

Re-derive the convention ID on endpoint change via
_next_flow_id/_rename_flow_id: collision-safe (a second edge into the
same pair becomes F_.._2, surfacing duplicates) and leaving deliberately
custom IDs untouched. Adds three route tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e restructuring

Safety net for the upcoming main.py module split: the exact ordered route
table, the symbols other code imports from systemdefiner.main, and a study
built through every editor whose pages must all render.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…r modules

Move-only: deps (templates/Jinja env), forms (form+YAML parsing), cascades
(rename/purge/delete/renumber), health (_model_health), scenario_params
(parameter catalog), and one APIRouter per domain under routers/. main.py
keeps the app factory, exception handlers, router includes in the original
declaration order, and backwards-compatible re-exports. Route table pinned
byte-identical by test_route_inventory; all 150 app tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…stry

New consistency.py: iter_flow_pointers() is the single enumeration of every
scalar flow-ID pointer (FOMP/LFG/FlowCap outflows + the DSM_Component
spare-part flows the cascades previously missed); check_config_consistency()
reports dangling/retargeted scenario+MC parameter names, stale element keys,
TC ownership mismatches and duplicate derived TC names, duplicate
process/flow/element IDs, flow-ID/endpoint drift and duplicate edges,
stranded flow data/compositions on non-input flows, config-wide static TC
sums, and hierarchy defects (unknown members, multi-parent, cycles).

_model_health merges the report into the overview page. All 16 tutorial
studies are clean; the checker surfaces real pre-existing damage in user
studies (drifted flow IDs, orphaned MC params, stale element keys).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- _rename_flow_id/_purge_flow_references/_compact_process_ids now iterate
  consistency.iter_flow_pointers, covering the DSM_Component spare-part
  flow fields that were previously left dangling on rename/rewire/delete.
- processes/renumber remaps scenario + MC parameter names: exact flow IDs
  via the rename map, embedded process IDs in P{pid:02d}_*, TC_E{n}_ff_tt,
  TC_ff_tt and TC_Cap_{pid} names, and auto-derived FlowCap cap_tc_id keys.
  Previously all of these silently dangled (or retargeted) after renumber.
- Renumber's flow-ID rename now includes _N duplicate-edge suffixes
  (F_02_06_2 no longer drifts) and uses a two-pass park-then-assign scheme
  so the outcome cannot depend on flow list order; genuine collisions get
  a suffix via _next_flow_id instead of being silently skipped.
- _delete_process_cascade drops scenario/MC entries whose names embed the
  deleted process, so a later process reusing the freed ID cannot inherit
  another process's modifications.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nge, import

- flow edit: TC ownership moves to the new source process on rewire
  (previously the stale TC exported under the new endpoints' names and
  silently overrode later edits); flow data/compositions are dropped when
  a rewire moves the flow off an Input source.
- flows new/edit: convention-shaped IDs (F_<a>_<b>[_N]) are rejected when
  they don't match the actual endpoints — fake convention names disguised
  topology and hid duplicate edges.
- process edit: leaving BOM_Assembler drops the process's bom_assembly
  entry (mirrors the initial-stock cleanup); leaving Input drops
  flow data/compositions on its outgoing flows (they were invisible in the
  editors but still prescribed the flows in the engine).
- import: a workbook without a BOM sheet no longer wipes existing BOM
  config; preserved TCs/refs pointing at vanished flows/processes are
  purged (they exported junk TC_E*_00_00 parameters); duplicate
  process/flow/element IDs are rejected with a 422 on both the Excel and
  YAML import paths.
- tests: fixtures that wired flows inconsistently with their convention
  IDs (F_01_02 wired 0->1) renumbered to match.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… config

The elements editor previously rewrote only cfg.model.elements: renaming an
element orphaned every value keyed by the old name (TC values, dynamic TC
points, compositions, BOM fractions, initial-stock compositions, DSM
component lifetimes/elements, expected inflow compositions, flow_data
element) — the engine silently read them as 0 — and reordering retargeted
positional E{n} scenario/MC names at a different element.

Each editor row now carries a hidden element_{i}_orig field (renumbered by
the same client JS as the visible input). elements_save builds a rename map
and removal set from (orig, new) pairs, rewrites keys everywhere via
consistency._element_key_sites, drops keys of removed elements (incl. DSM
component rows tracking them), remaps E{n} positions and embedded element
names in TC_E*/P*_IS_E* scenario+MC parameter ids, and applies the same
mapping to the hierarchy path cells submitted in the same form. The
element_{idx} scan is now gap-tolerant like the other row parsers. Posts
without _orig fields keep the old replace-only behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Same defect fixed in plotting/validation.py (332b047), missed in the
scenario_engine copy: dS was accumulated at the ProcessList position while
flows were accumulated at the process ID, so a non-ID-ordered process list
(which the SystemDefiner produces routinely — process_new appends a
gap-filling ID at the end) attributed stock changes to the wrong process
and reported phantom imbalances. Flows touching IDs >= len(ProcessList)
were silently dropped from the balance, hiding real imbalances in gapped
studies. Arrays are now sized by the highest process/flow-endpoint ID,
indexed by ID throughout, and gap rows are masked out.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…orphan BOM

save_case_study now sorts cfg.processes by ID (process_new appends
gap-filling IDs at the list end, and several consumers historically assumed
list position == process ID) and drops bom_assembly entries whose process
is gone or no longer a BOM_Assembler, mirroring the existing
initial-stock prune. Tracked case-study YAMLs normalize lazily on their
next save.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- _parse_dsm / _parse_dsm_component now collect row indices with tolerant
  regex scans (like _parse_lfg/_parse_flowcap) instead of breaking at the
  first missing index — a client-side row removal without reindexing no
  longer silently drops every row past the gap; per-category component
  lifetime overrides key on the component's actual DOM index.
- _rules_to_paths guards against hierarchy cycles with a visited set —
  a cyclic parent/child pair used to hang every page rendering the matrix
  (elements, compositions, initial stock, dynamic TCs).
- scenario names may not contain slashes (they are URL path segments; a
  slash made the scenario unreachable and undeletable).
- dynamic TC form parsing is anchored on the outgoing-flow IDs and known
  element names instead of a generic regex, so flow IDs containing
  _idx_/_year_ can no longer mis-bucket values.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…test

load_case_study only setdefault'ed the internal YAML name, so a hand-copied
study carrying another study's 'name:' (found live: heatpumps_1 contains
'name: tracer') had every save silently written to the OTHER study's
folder. The folder name now overrides the stored name on load.

New round-trip test: every tracked case study loads, saves (applying the
new normalization), reloads with no new consistency errors, and save/load
is idempotent — this is what surfaced the stale-name bug.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment/docstring-only: the code<->paper notation tables and inline
references now point at the renumbered sections of
bioDYM_mathematical_formulas.md (e.g. DSM §5-6 -> §3.2-3.3, hierarchy
§2.6 -> §6.2). No runtime changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
load_dsm_from_yaml now emits the per-process 'components' list (element,
mean lifetime, spare-part in/outflow IDs, optional per-category lifetime
overrides), matching what load_dsm_parameters merges from the
3_1_DSM_Components sheet — previously DSM_Component studies only worked
when loaded from Excel.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The two tutorial configs were re-saved through the SystemDefiner, which
added new schema defaults (mc_seed, solver_strict/max_iterations,
sankey_title, expected_inflow_composition, refs) and reordered keys —
no parameter values changed. References regenerated via
generate_references.py; all flow/stock arrays are bit-identical to the
previous pins, only the embedded config hash differs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@JScholz-tech
JScholz-tech merged commit a0fc292 into main Jul 15, 2026
1 check passed
@JScholz-tech
JScholz-tech deleted the fix/systemdefiner-consistency branch July 15, 2026 12:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant