Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/architecture/sequences.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ sequenceDiagram
Advance->>Step: step(state, velocity, numerics, dt)
Step-->>Advance: new state
Advance->>Window: simulation_fields = new state
Advance->>Viz: scalar_field_colors(new render_field, low, high, range)
Advance->>Viz: _panel_colors(new state, panel) per field_display.panels entry
Viz-->>Advance: per-cell RGBA colors
Advance->>Window: scene.remove(old object); scene.add(new object)
Window->>Hud: on_frame() -- HUD half
Expand Down
24 changes: 17 additions & 7 deletions docs/implementation/config-template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -110,19 +110,29 @@ field_display:
arrow_scale: 0.3
# Valid: true or false.
show_legend: true
# Valid: null (no live field is coloured) or the name of one field
# declared under fields: below -- the renderer never infers which one to
# show. Invalid: naming a field fields: does not declare.
render_field: null
# Valid: null (fall back to render_field's own name) or any string -- a
# human-readable legend caption, e.g. "Temperature (K)". Invalid: a non-
# string value.
# Valid: null (no caption at all) or any string -- a human-readable legend
# caption for the static scalar_pattern display only, e.g. "Distance from
# centre". A live panel (field_display.panels below) has its own, separate
# label instead. Invalid: a non-string value.
field_label: null
# Valid: null (no vector-scale HUD line at all) or any string -- what the
# arrow display represents, e.g. "Velocity". When set, the HUD states this
# label alongside arrow_scale wherever arrows are actually drawn. Invalid:
# a non-string value.
vector_label: null
# Valid: a list of live colour-mapped panel declarations, each a mapping
# with field (a non-empty string naming one field declared under fields:
# below -- the renderer never infers which one to show), mode (linear or
# equalized -- linear maps value_range onto low_color/high_color;
# equalized colours by each cell's rank among the field's current values
# instead, so peaks and valleys stay distinguishable even when both are
# numerically tiny, no range needed), value_range (a [min, max] pair,
# linear mode only), and label (null falls back to field's own name for a
# linear panel, or the constant "equalized" for an equalized one). Drawn
# left to right in list order. [] (the default) draws nothing. Invalid:
# naming a field fields: does not declare, an unrecognised mode, or a
# degenerate value_range (max <= min).
panels: []

# Valid: a list of per-field declarations, each a mapping with name (a non-
# empty string, not reused by another declaration and not one of the
Expand Down
117 changes: 116 additions & 1 deletion docs/planning/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,122 @@ This paragraph previously said `make install` and `make test` were still
expected to fail, pending `uv.lock` and a test suite (B2/C1) -- stale
since 2026-08-16 and corrected 2026-08-19. Both now succeed: `uv.lock`
is committed (B2) and `make test` runs the suite with coverage
(C1a/C1b): **1131 tests as of 2026-09-07**, up from 1052 the day before.
(C1a/C1b): **1160 tests as of 2026-09-07**, up from 1154 slightly
earlier the same day (below), then 1153, 1143, 1137, 1131, and 1052 the
day before that.

**The 6 most recent are `pyflow resume`'s own new `--config`/`config_path`
alternative** -- a further same-day user request ("do pyflow resume
from a config file and have it start from the first frame"): a second,
mutually exclusive way to call `resume` (alongside its existing
`--checkpoint`/`checkpoint_path`) that starts a brand new recording at
frame 0, a pure delegation to `record` rather than a second copy of its
logic, so a caller can use `resume` as the one command name for a
recording's whole lifecycle. 3 in `tests/unit/test_recording.py`
(behaves exactly like `record`, checked against a real `record()` call
rather than merely not raising; rejects neither `checkpoint_path` nor
`config_path` given; rejects both given), 1 in `tests/unit/test_main.py`
(dispatches `--config` to `config_path`; the old "resume has no --config
flag at all" test is retired, replaced by a rejection test for
`--checkpoint`+`--config` together, and the existing "requires
checkpoint" test renamed to "requires checkpoint or config" -- both
already covered the same argparse mutually-exclusive-group error
message, unaffected in substance by the rename), 2 in `tests/integration/
test_record_cli.py` (a real subprocess `pyflow resume --config` run,
and the same `--checkpoint`+`--config` rejection through the real CLI
rather than only in-process); 3 + 1 + 2 = 6.

**The 1 before those is a real-bug regression test, found by a user
report rather than by any check in this repository -- the panel-list
migration just below widened `overall_bounds` rightward for extra
panels but never downward for a panel's own legend and caption, so
`_add_hud`'s stats block landed at the same world-space height the
caption already occupied.** Reported as "the legends all clip over each
other" while running `examples/experiments/smoke_transport_mesh128.yaml`
-- reproduced directly (a single-panel run's own rendered frame showed
the stats block drawn over "Smoke concentration (model units)"), root-
caused by reading `_add_declared_field_transport`'s own bounds
arithmetic against `_add_hud`'s (the live-panels path no longer flows
through `_add_hud`'s generic legend-widening block at all, since panels
caption themselves directly, and nothing replaced the widening that
block used to do), and fixed by folding each panel's own legend bottom
into `overall_bounds`'s y-minimum directly, the same margin
(`_LEGEND_LABEL_MARGIN_FRACTION`) the static path's own `_add_hud` block
already uses. `test_bootstrap_panel_stats_block_does_not_overlap_the_
legend_caption` (`tests/unit/test_bootstrap.py`) reproduces the
single-panel case directly (no `.feature` file -- a rendering-layout
defect, the same "not physics" category `adr/ADR-007`'s scope excludes)
and was confirmed to fail without the fix before being trusted: reverting
just the new widening block reproduces the exact reported symptom
(stats at y=-0.06 against the legend's own bottom edge at y=-0.6).

**The 10 before that replace `render_field`/`show_equalized_panel` with
a modular `field_display.panels: list[FieldPanelConfig]`** -- a further
same-day user request ("can we have the visibility [of] each of these
plots configurable too in a modular fashion? Later we may want [to] show
different fields than concentration too"), since the pair below could
only ever show one field, optionally twice, with no way to show two
different fields side by side or toggle either panel independently. Net
+10 across two files: `tests/unit/test_bootstrap.py` (8 tests replacing
the prior 8 named just below -- panel-list construction, one/two/no
panels, different fields per panel, the equalized caption/legend-
disabled/camera-widening/rebuild cases carried forward under new names;
net +2) and `tests/unit/test_configuration.py` (12 tests replacing the
prior 4 named just below -- reading a full panel declaration, the mode
default, each rejection surface (non-list, non-mapping, empty/non-string
field, invalid mode, degenerate value_range, non-string label), and the
two retired-setting migration-rejection tests; net +8). All six golden
demos that used `render_field` (`heat_diffusion`, `heat_transport`,
`multi_field_plume`, `passive_scalar_transport`, `smoke_transport`,
`thermal_buoyancy`) and four experiment configs under `examples/
experiments/` were migrated to `panels:` in the same change, verified by
loading each directly, not merely by the test suite passing.
`tests/features/field_declaration.feature`'s own two `render_field`
scenarios were reworded to describe `field_display.panels` instead
(`adr/ADR-007`'s "the scenario is the criterion" -- the underlying
config surface genuinely changed, so the acceptance-criteria text
changes with it), with no new scenario count.

**The 6 before those are `tests/unit/test_bootstrap.py`'s own coverage of
the equalized-panel wiring in `bootstrap.py` itself**, added once the
panel's own colour math and schema field already had tests (below) but
`bootstrap.py`'s own construction/legend/per-frame-rebuild/camera-
framing code did not -- found by `make ci`'s own coverage report
showing exactly those lines missed. `test_bootstrap_without_show_
equalized_panel_adds_no_second_field_mesh`, `test_bootstrap_with_show_
equalized_panel_adds_a_second_field_mesh_shifted_right`, `test_
bootstrap_equalized_panel_legend_caption_is_just_equalized_not_the_
field_label` (guards the caption-wrap fix below), `test_bootstrap_
equalized_panel_with_legend_disabled_adds_no_equalized_legend`, `test_
bootstrap_equalized_panel_widens_the_camera_framing`, and `test_
bootstrap_equalized_panel_field_mesh_is_rebuilt_not_accumulated_across_
frames` (the one that exercises `_advance`'s own per-frame path); six
tests, one per line/branch the coverage report named missing, verified
directly against `--cov-report=term-missing` (bootstrap.py: 86% -> 100%)
rather than assumed sufficient.

**The 6 before those are not tied to any roadmap stage or task
either** -- the same rendering feature
(`field_display.show_equalized_panel`, `rank_scalar_field_colors`,
`src/pyflow/rendering/CLAUDE.md`'s own "Equalized (rank-based) field
panel" entry) added directly at a user's request while watching the
Smoke Transport demo, the same "visualisation work, not physics, not
`adr/ADR-007`-gated" category the HUD/axis-label work already occupies.
4 in `tests/unit/test_field_visualization.py` (`rank_scalar_field_
colors`'s own rank-not-magnitude, tied-value-averaging, single-cell,
and shape/dtype cases), 2 in `tests/unit/test_configuration.py`
(`show_equalized_panel`'s own read and non-bool-rejection cases);
4 + 2 = 6. **The equalized panel's own legend caption was found and
fixed in the same follow-up change that added the six `test_bootstrap.py`
cases above**: an early version captioned it `f"{field_label}
(equalized)"`, which risked exactly the wrapped-caption-drawn-over-the-
mesh defect this file's HUD history already recorded once, the moment a
real demo's own `field_label` (`smoke_transport.yaml`'s "Smoke
concentration (model units)") got long enough -- changed to the plain
constant `"equalized"` before `smoke_transport.yaml` was updated to turn
the panel on, not after.

**1131 tests as of 2026-09-07**, up from 1052 the day before.
**30 of those 79 are TASK-046/047's own windowed-replay/playback
addition** (below the `resume` breakdown); 49 are TASK-045's own, and
**16 of those 49 are TASK-045's own `resume` addition** (below); the
Expand Down
2 changes: 1 addition & 1 deletion docs/planning/status.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ pie showData
## Live repository facts

- **47** `CLAUDE.md` files
- **1131** tests collected
- **1160** tests collected
- **144** Gherkin scenarios (`tests/features/*.feature`)

## Stages
Expand Down
7 changes: 4 additions & 3 deletions examples/experiments/smoke_transport_high_res.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,12 @@ fluid:
viscosity: 0.01

field_display:
render_field: smoke
low_color: "#0a0a2a"
high_color: "#e8e8ff"
value_range: [0.0, 1.0]
field_label: Smoke concentration (model units)
panels:
- field: smoke
value_range: [0.0, 1.0]
label: Smoke concentration (model units)

rendering:
title: Smoke Transport (High Resolution)
Expand Down
7 changes: 4 additions & 3 deletions examples/experiments/smoke_transport_mesh128.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,12 @@ fluid:
viscosity: 0.01

field_display:
render_field: smoke
low_color: "#0a0a2a"
high_color: "#e8e8ff"
value_range: [0.0, 1.0]
field_label: Smoke concentration (model units)
panels:
- field: smoke
value_range: [0.0, 1.0]
label: Smoke concentration (model units)

rendering:
title: Smoke Transport (128x128)
Expand Down
7 changes: 4 additions & 3 deletions examples/experiments/smoke_transport_mesh64.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,12 @@ fluid:
viscosity: 0.01

field_display:
render_field: smoke
low_color: "#0a0a2a"
high_color: "#e8e8ff"
value_range: [0.0, 1.0]
field_label: Smoke concentration (model units)
panels:
- field: smoke
value_range: [0.0, 1.0]
label: Smoke concentration (model units)

rendering:
title: Smoke Transport (64x64)
Expand Down
7 changes: 4 additions & 3 deletions examples/experiments/smoke_transport_re1000.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,12 @@ fluid:
viscosity: 0.001

field_display:
render_field: smoke
low_color: "#0a0a2a"
high_color: "#e8e8ff"
value_range: [0.0, 1.0]
field_label: Smoke concentration (model units)
panels:
- field: smoke
value_range: [0.0, 1.0]
label: Smoke concentration (model units)

rendering:
title: Smoke Transport (Re 1000)
Expand Down
22 changes: 12 additions & 10 deletions examples/golden-demos/heat_diffusion.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
# spreads". No velocity at all (`SimulationConfig.velocity_pattern`
# unset): this demo is pure diffusion, the reading
# `docs/handbook/numerical-methods/diffusion.md` describes for a single
# mode on a periodic domain. `field_display.render_field` names the
# declared field the live colour map shows.
# mode on a periodic domain. `field_display.panels` names the declared
# field(s) the live colour map(s) show, each its own modular panel.
#
# Run it exactly the way any user would:
#
Expand Down Expand Up @@ -50,16 +50,18 @@ fields:
diffusion_coefficient: 0.05

field_display:
render_field: tracer
low_color: "#0a0a2a"
high_color: "#ff4400"
value_range: [-1.0, 1.0]
# Stage 7 (Rendering Annotations): named on screen, not just "tracer"
# (real feedback: "isn't sufficient"). Not a real thermal unit -- this
# is an anonymous diffusing scalar mode (`heat_transport.yaml` is the
# demo with a real-named `temperature` field), so the label says
# "model units" rather than implying a calibrated Kelvin value.
field_label: Diffusing scalar amplitude (model units)
panels:
- field: tracer
value_range: [-1.0, 1.0]
# Stage 7 (Rendering Annotations): named on screen, not just
# "tracer" (real feedback: "isn't sufficient"). Not a real thermal
# unit -- this is an anonymous diffusing scalar mode
# (`heat_transport.yaml` is the demo with a real-named
# `temperature` field), so the label says "model units" rather
# than implying a calibrated Kelvin value.
label: Diffusing scalar amplitude (model units)

rendering:
title: Heat Diffusion
Expand Down
15 changes: 8 additions & 7 deletions examples/golden-demos/heat_transport.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,16 @@ fields:
diffusion_coefficient: 0.05

field_display:
render_field: temperature
low_color: "#0a0a2a"
high_color: "#ff4400"
value_range: [-1.0, 1.0]
# Stage 7 (Rendering Annotations): "model units" rather than a real
# Kelvin value -- nothing calibrates this field to real temperature,
# only its name and its diffusion equation match the physics it's
# named after.
field_label: Temperature (model units)
panels:
- field: temperature
value_range: [-1.0, 1.0]
# Stage 7 (Rendering Annotations): "model units" rather than a real
# Kelvin value -- nothing calibrates this field to real temperature,
# only its name and its diffusion equation match the physics it's
# named after.
label: Temperature (model units)

rendering:
title: Heat Transport
Expand Down
24 changes: 13 additions & 11 deletions examples/golden-demos/multi_field_plume.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -118,21 +118,23 @@ fluid:
viscosity: 0.1

field_display:
low_color: "#0a0a2a"
high_color: "#ff8844"
# One field can be colour-mapped, and it is the one driving the flow.
# The other three are transported all the same -- which is why this
# demo's own report, not its rendered frame, is what demonstrates it
# (`tests/features/multi_field_plume.feature`).
render_field: temperature
low_color: "#0a0a2a"
high_color: "#ff8844"
value_range: [0.0, 1.0]
# One line at this mesh's own HUD font size, and that is a constraint
# rather than a preference: a wrapped caption's second line is drawn
# over the mesh's bottom row (`src/pyflow/rendering/CLAUDE.md`, and
# `docs/planning/backlog.md` for the fix that is owed). The "1 of 4"
# claim this label first carried belongs in the run's own report
# anyway, which is where this demo actually demonstrates it.
field_label: Temperature (model units)
panels:
- field: temperature
value_range: [0.0, 1.0]
# One line at this mesh's own HUD font size, and that is a
# constraint rather than a preference: a wrapped caption's second
# line is drawn over the mesh's bottom row
# (`src/pyflow/rendering/CLAUDE.md`, and `docs/planning/
# backlog.md` for the fix that is owed). The "1 of 4" claim this
# label first carried belongs in the run's own report anyway,
# which is where this demo actually demonstrates it.
label: Temperature (model units)

rendering:
title: Multi-Field Plume
Expand Down
21 changes: 11 additions & 10 deletions examples/golden-demos/passive_scalar_transport.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
# `fields:` declares the blob's own name, initial condition and
# diffusivity (`FieldConfig`, `src/pyflow/configuration/schema.py`,
# TASK-042); `simulation:` names the prescribed velocity that transports
# it; `field_display.render_field` names which declared field the live
# colour map shows (there is only one here, but the renderer never
# infers that -- it must always be named). `north`/`south` are
# it; `field_display.panels` names which declared field each live colour
# map shows (there is only one panel here, but the renderer never infers
# that -- it must always be named). `north`/`south` are
# `neumann` with a zero gradient (an insulated wall) rather than
# periodic -- the prescribed velocity is purely horizontal, so nothing
# ever flows across them, but diffusion still needs *some* condition
Expand Down Expand Up @@ -48,15 +48,16 @@ simulation:
velocity: [1.0, 0.0]

field_display:
render_field: tracer
low_color: "#0a0a2a"
high_color: "#ff8c00"
value_range: [0.0, 1.0]
# Stage 7 (Rendering Annotations): real feedback on the first cut of
# this demo's own HUD -- "'Tracer' isn't sufficient." "model units":
# concentration here is a bare transported scalar, not calibrated to
# any real concentration unit.
field_label: Tracer concentration (model units)
panels:
- field: tracer
value_range: [0.0, 1.0]
# Stage 7 (Rendering Annotations): real feedback on the first cut
# of this demo's own HUD -- "'Tracer' isn't sufficient." "model
# units": concentration here is a bare transported scalar, not
# calibrated to any real concentration unit.
label: Tracer concentration (model units)

rendering:
title: Passive Scalar Transport
Expand Down
Loading
Loading