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
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,15 @@ timestep against the stability limit, so refining a shipped demo's mesh
blows up silently at step 17; and `BoundaryFaceConfig.velocity` is a
validated, documented configuration field that no engine code reads.
See `docs/planning/roadmap.md`'s own Stage 9 section for the criteria
and the measurements. It is placed before Better Numerics by dependency,
and the measurements. **TASK-052 has landed** -- a sealed box now
conserves a purely advected tracer exactly, where it lost 14.27% before,
and the lid-driven cavity's own error against Ghia, Ghia & Shin (1982)
*fell at every resolution* (9x9 0.1433 -> 0.1292, 13x13 0.0874 ->
0.0766, 17x17 0.0578 -> 0.0524), which is independent evidence the
change was physics rather than a re-fitted tolerance. Its own demo is
`uv run python -m pyflow run --demos sealed_box`. TASK-053 (a failed
frame failing the run) and TASK-054 (the timestep stability warning)
are drafted and not yet built. It is placed before Better Numerics by dependency,
not preference -- Stage 10's own Rayleigh-Bénard criterion measures
convection between heated walls, which is not meaningful while those
walls leak.
Expand Down
4 changes: 1 addition & 3 deletions adr/ADR-009-pressure-coupling-dt.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ TASK-021 (Stage 3, done 2026-08-23) gave `PressureCoupling` this
abstract method:

```python
def correct(
self, provisional_velocity: VectorField
) -> tuple[VectorField, ScalarField]: ...
def correct(self, provisional_velocity: VectorField) -> tuple[VectorField, ScalarField]: ...
```

No timestep. That was sufficient through Stage 3, where no concrete
Expand Down
27 changes: 27 additions & 0 deletions docs/architecture/icds.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,17 @@ limit (`docs/handbook/numerical-methods/advection.md`,
`time-integration.md`). Appropriate for MVP correctness validation, not
for accuracy-sensitive production use.

**Boundary treatment, corrected 2026-09-12 (TASK-052, Stage 9):** a
genuine boundary face's *transporting* velocity is resolved from that
face's own boundary condition, through the same
`boundary_normal_velocity` function `GreenGaussDivergence` resolves it
through -- not from the cell inside the face, which is what this scheme
did until then, and which let material cross solid walls at up to 42% of
a flow's own peak speed. The transported field's own boundary value is a
separate question, answered the same way it always was (a Dirichlet
condition's prescribed value at inflow, zero-order extrapolation at
outflow or under Neumann).

**Limitations:** first-order accuracy only; smooths sharp gradients more
than a user comparing against a higher-order reference might expect. The
artificial diffusivity this amounts to is roughly $\rho |u| \Delta x / 2$
Expand Down Expand Up @@ -458,6 +469,22 @@ surface, which no Stage 5 task discharged.
**Expected behaviour:** each condition type supplies the face value
(Dirichlet), face gradient (Neumann), or wrapped-neighbour reference
(periodic) the interior advection/diffusion schemes need at that face.
**Since TASK-052 (Stage 9, 2026-09-12) it also supplies the face's own
normal *velocity*** -- the number every operator that transports across
that face resolves through `boundary_normal_velocity`
(`src/pyflow/engine/numerics/boundary_condition.py`). A Dirichlet
condition with no per-field override for the velocity field prescribes
`0.0` there, a no-penetration wall; it used to fall through to
`scalar_value`, a transported scalar's boundary value, which is not a
velocity.

**A fifth requirement, still open: `BoundaryFaceConfig.velocity` reaches
no scheme.** It is validated (mutual exclusivity with `pressure`, zero
net flux) and then read by nothing -- the per-component channel
`field_values["velocity.0"]`/`["velocity.1"]` is what a real
configuration uses and what the engine reads. Recorded 2026-09-12 as
Stage 9 Completion Criterion 3's remaining half rather than fixed in
the same change as the defect above.

**Limitations:** limited to simple, axis-aligned domain edges -- internal
boundaries and arbitrary-geometry surfaces are explicitly future work,
Expand Down
45 changes: 45 additions & 0 deletions docs/implementation/golden-demos.md
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,51 @@ scene (`tests/integration/test_playback_cli.py::
test_play_renders_both_arrows_and_a_panel_for_smoke_transport`), not
only that the CLI exits cleanly.

## Sealed Box

TASK-052's own golden demo, and Stage 9's -- the visible form of the
defect that opened that stage. A tracer stirred by a real, solved
lid-driven flow inside a closed no-slip cavity has nowhere to go, and
must still be entirely inside the cavity however long it is stirred.

**The second demo, after Multi-Field Plume, that exists because an audit
found something wrong rather than because a task built a capability.**
Before TASK-052, `FirstOrderUpwindAdvection` took a boundary face's
transporting velocity from the cell inside it rather than from what the
configuration prescribed there, so material crossed solid walls at up to
42% of the flow's own peak speed -- 14.27% of a purely advected tracer
lost over 400 steps on `smoke_transport.yaml`'s own geometry, whose mesh,
timestep, viscosity and moving lid this demo shares.

"Working" means, concretely:

- the demo *is* `examples/golden-demos/sealed_box.yaml`, run via
`uv run python -m pyflow run --config examples/golden-demos/sealed_box.yaml`
(or `--demos sealed_box`);
- **every wall face carries exactly zero advective flux, while the cells
against those walls are still moving** -- both halves, checked through
`bootstrap()` on the committed config
(`tests/golden/test_sealed_box.py`). The second half is what stops the
first being satisfied by a flow that simply stopped;
- the tracer is genuinely carried by the recirculating flow, the same
"bootstrap at two frame counts, compare" shape
`test_smoke_transport.py` already established;
- it runs headlessly via `--backend offscreen`, same as every other demo.

**This demo checks the wall, not the domain integral, and that is a real
constraint rather than a preference.** Diffusion to a zero-valued wall
removes tracer legitimately, so the integral is not exactly constant
here; a scenario asserting it was would be wrong. Switching diffusion off
entirely would make exact conservation the right claim, and
`FieldConfig` rejects a non-positive `diffusion_coefficient` -- a
rejection that is itself a bound Stage 6 acceptance criterion
(`tests/features/field_declaration.feature`). Relaxing it for one demo's
convenience would reopen another stage's criterion, so exact
conservation is proven at the engine level instead, against a purely
advective fixture: `tests/features/boundary_velocity.feature`'s own
sealed-domain scenario, which measures `+0.000000000000%` against a
pre-fix `-4.54%`.

## Future Demos

Add an entry here when a new capability is implemented, per
Expand Down
Loading
Loading