Skip to content

TASK-052: stop advection transporting through solid walls - #92

Merged
AdamClemens merged 2 commits into
mainfrom
feat/task-052-prescribed-boundary-velocity
Sep 12, 2026
Merged

TASK-052: stop advection transporting through solid walls#92
AdamClemens merged 2 commits into
mainfrom
feat/task-052-prescribed-boundary-velocity

Conversation

@AdamClemens

Copy link
Copy Markdown
Owner

Stacked on #91 (Stage 9's insertion) — merge that first.

FirstOrderUpwindAdvection took a genuine boundary face's transporting velocity from the cell inside it rather than from what the configuration prescribed there. GreenGaussDivergence meanwhile consulted the boundary condition and treated the same walls as solid — so the two operators disagreed about the domain they were solving in, and material crossed solid walls at up to 42% of the flow's own peak speed.

This falsified a use case Stage 4 wrote down for itself:

Prescribe a wall's value or its gradient, or make a boundary periodic, and have the interior schemes honour it.

The measurement

Shipped smoke_transport.yaml geometry, tracer diffusion zeroed so only advection remains. Pure advection in a closed domain must conserve exactly:

before after
Sealed box, 400 steps −14.27% +0.000000000000%

Evidence this is physics, not a re-fitted tolerance

The error against Ghia, Ghia & Shin (1982) fell at every resolution and stayed monotonic:

mesh before after
9×9 0.1433 0.1292
13×13 0.0874 0.0766
17×17 0.0578 0.0524

Both baselines are recorded rather than the old ones overwritten (Criterion 7). The 0.08 bound is deliberately unchanged — tightening it to each improvement would stop it being an independent check. Couette, Taylor-Green (both halves), kinetic-energy monotonicity and Stage 6's periodic domain integrals and decay rates were all confirmed unmoved rather than assumed so.

The fix is reuse, not new mechanism

boundary_normal_velocity is extracted from what divergence already did; advection now calls it too. Three files, no interface change, no registry change, no arity change anywhere.

A first implementation threaded a per-edge mapping from BoundaryFaceConfig.velocity through assemble_numerics and widened two registries. It was built, measured working, and abandoned when test_divergence_contract.py's linear-field exactness test turned out to be inexpressible through a per-edge scalar — a linear field's normal component varies along an edge, and a future parabolic inlet would have the same problem. That is recorded in the task entry rather than presented as the first idea.

A second defect, found by a test rather than by reading

DirichletBoundaryCondition.evaluate fell through to BoundaryFaceConfig.scalar_value for a vector field with no override — a transported scalar's boundary value, returned as a velocity. Every shipped config leaves it at 0.0, so walls were impermeable by coincidence of the defaults rather than by anything the configuration said: the same shape of accident the pre-fix divergence operator had. A new scenario failed against a fixture whose scalar boundary value was 3.0, because the wall then resolved to a normal velocity of 3.0.

One check moved, and it belonged to another stage

Stage 6's Rayleigh–Bénard onset scenario compared final vertical-velocity RMS and required a factor of two. It passed at 6.35 and now measures 1.28 — because that verdict was produced by this defect: heat and momentum were crossing that fixture's own solid walls with the Dirichlet condition re-injecting boundary-temperature fluid, a spurious heat pump.

Re-derived as a growth comparison (maintainer's call) — which is what that fixture's own docstring says it always meant to check, and which the 2026-08-31 audit found had never been implemented:

heated from below: 0.0904 → 0.1621   (+79%, still growing)
heated from above: 0.1251 → 0.1267   (+1.3%, settled)

Roughly sixty to one, against bars of 25% and 5%. Mutation-verified: inverting the buoyancy sign drops "below" to 1.012 and fails it. Recorded in Stage 6's own status table as a verdict a later stage changed, not rewritten as though it had always read that way.

Honest about what is not done

Criterion 3 is discharged only partially, and the Discharges section says so. The sweep over BoundaryFaceConfig's own fields is not built, and BoundaryFaceConfig.velocity still reaches no scheme — it is validated for mutual exclusivity and zero net flux, then read by nothing. What this task closes is the half that was actually wrong.

TASK-053 (a failed frame failing the run) and TASK-054 (the timestep stability warning) are drafted, not built — both carry Status: Not started, so Stage 9 reads as opened rather than complete.

Verification

make ci green in full: 1206 passed, 20 skipped, 156 scenarios, all 15 structural checks. All 11 golden demos re-run headlessly; sealed_box verified through pyflow run --demos sealed_box and present in the demo listing as #12; the documented recordresumeplay pipeline re-run by hand.

🤖 Generated with Claude Code

AdamClemens and others added 2 commits September 12, 2026 22:18
`FirstOrderUpwindAdvection` took a genuine 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. `GreenGaussDivergence` meanwhile consulted the
boundary condition and treated the same walls as solid, so the two
operators disagreed about the domain they were solving in.

Measured on the shipped smoke_transport geometry, diffusion zeroed so
only advection remains: a sealed box lost 14.27% of a purely advected
tracer over 400 steps. It now conserves it to +0.000000000000%.

This falsified a use case Stage 4 wrote down for itself -- "prescribe a
wall's value or its gradient, or make a boundary periodic, and have the
interior schemes honour it".

The fix is reuse, not new mechanism. `boundary_normal_velocity` is
extracted from what divergence already did, and advection now calls it
too -- three files, no interface change, no registry change, no arity
change anywhere. A first implementation threaded a per-edge mapping from
`BoundaryFaceConfig.velocity` through `assemble_numerics` and widened two
registries; it was built, measured working, and abandoned when the
divergence contract suite's linear-field exactness test turned out to be
inexpressible through a per-edge scalar. That is recorded in the task
entry rather than presented as the first idea.

A second defect fell out of writing the tests: `DirichletBoundaryCondition`
fell through to `BoundaryFaceConfig.scalar_value` for a vector field with
no override -- a transported scalar's boundary value, returned as a
velocity. Every shipped config leaves it at 0.0, so walls were
impermeable by coincidence of the defaults rather than by anything the
configuration said. A vector field with no override now resolves to 0.0.

Evidence the change is physics, not a re-fitted tolerance: the error
against Ghia, Ghia & Shin (1982) fell at every resolution and stayed
monotonic -- 9x9 0.1433 -> 0.1292, 13x13 0.0874 -> 0.0766, 17x17 0.0578
-> 0.0524. Both baselines are recorded, per Criterion 7. Couette,
Taylor-Green (both halves), kinetic-energy monotonicity and Stage 6's
periodic domain integrals and decay rates were confirmed unmoved.

One check did move, and it belonged to Stage 6. Its Rayleigh-Benard
onset scenario compared the two orientations' final vertical-velocity RMS
and required a factor of two; it passed at 6.35 and now measures 1.28.
That verdict was produced by this defect -- heat and momentum were
crossing that fixture's own solid walls, with the Dirichlet condition
re-injecting boundary-temperature fluid, a spurious heat pump. Re-derived
as a growth comparison (maintainer's call), which is what that fixture's
own docstring said it always meant to check and which separates the two
by roughly sixty to one. Mutation-verified against an inverted buoyancy
sign. Recorded in Stage 6's own status table as a verdict a later stage
changed, not rewritten as though it had always read that way.

Criterion 3 is discharged only partially and says so: the config-field
sweep is not built, and `BoundaryFaceConfig.velocity` still reaches no
scheme.

Stage 9's Golden Demo (Sealed Box) lands with it, and TASK-053/054 are
drafted. `make ci` green: 1206 passed, 156 scenarios, all 15 structural
checks. Every demo re-run headlessly and the record/resume/play pipeline
re-verified by hand.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Found by the Merge Gate's criterion 2 run as a grep rather than a diff
review, after the task's own documentation sweep had already been done
and the PR was green on both platforms. All three describe the defect as
though it were the design:

- `src/pyflow/engine/CLAUDE.md`'s `FirstOrderUpwindAdvection` entry:
  "the face-normal velocity ... or the owner's own velocity alone for a
  boundary face". Accurate until 2026-09-12, and the sentence the fix
  exists to falsify. Replaced, with the old wording kept as the
  historical note it now is.
- `advection.py`'s own inline comment on `v_avg`, which read the
  placeholder-neighbour average as the answer at a boundary face rather
  than as the input to one.
- `FirstOrderUpwindAdvection`'s class docstring, which described the
  periodic and boundary-value treatments and said nothing about the
  transporting velocity at all.

This is the shape `docs/practices.md`'s "A stage's documentation sweep is
a grep, not a diff review" names: the sweep that ran with the task added
new prose next to the contradicting old prose and did not grep for the
claim it had just made false.

`make ci` green: 1206 passed, 156 scenarios, all 15 structural checks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Base automatically changed from docs/insert-stage-9-solver-run-integrity to main September 12, 2026 22:16
@AdamClemens
AdamClemens merged commit 0c709d5 into main Sep 12, 2026
2 checks passed
@AdamClemens
AdamClemens deleted the feat/task-052-prescribed-boundary-velocity branch September 12, 2026 22:17
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