Skip to content

TASK-054: warn when the configured timestep exceeds the stability limit - #94

Merged
AdamClemens merged 2 commits into
mainfrom
feat/task-054-timestep-stability-warning
Sep 13, 2026
Merged

TASK-054: warn when the configured timestep exceeds the stability limit#94
AdamClemens merged 2 commits into
mainfrom
feat/task-054-timestep-stability-warning

Conversation

@AdamClemens

Copy link
Copy Markdown
Owner

Closes Stage 9's third finding (F3): stable_timestep() has existed since
Stage 5 and no live run reached it.

A 64x64 lid-driven cavity at the shipped timestep: 0.008 is 2.05x its own
stability limit and diverges at step 17, with nothing said beforehand:

16x16 dt=0.008 stable=0.01562 ratio=0.51  max|u|=0.721    ran 150 steps
32x32 dt=0.008 stable=0.00781 ratio=1.02  max|u|=0.875    ran 150 steps
48x48 dt=0.008 stable=0.00521 ratio=1.54  max|u|=0.918    ran 150 steps
64x64 dt=0.008 stable=0.00391 ratio=2.05  max|u|=6.4e+04  diverged at step 17

What this does

build_simulation_state now computes the limit for the mesh it is about to
run on and warns when the configured timestep exceeds it, naming both numbers
and their ratio.

It lives in simulation_run.py rather than bootstrap.py so run, record
and resume all get it -- record is the path a long unattended run uses.

Non-fatal by design: stable_timestep's 0.25 safety factor is conservative,
and 32x32 at ratio 1.02 demonstrably runs to completion. A gate would reject
runs that work.

The fifth test exists because mutation testing found the fourth missing

_characteristic_velocity reads only what the configuration prescribes --
simulation.velocity and the velocity.* entries of each boundary face's
field_values. That prefix filter is load-bearing and was untested:
replacing name.startswith("velocity.") with an unconditional True left
all eleven tests passing, while letting a declared scalar's wall value
(temperature: 300.0) be read as a speed of 300 -- shrinking the limit by
two orders of magnitude and firing this warning on configurations that are
perfectly stable.

That is the same scalar/velocity conflation TASK-052 fixed one layer down,
reappearing in new code that reads the same mapping.
test_a_declared_scalars_wall_value_is_not_read_as_a_speed closes it and
fails under that mutation.

Verification

By hand on all three subcommands against a real 64x64 configuration:

configured numerics.timestep 0.008 exceeds this mesh's own stability limit
0.0039062 (2.05x) -- the run may diverge

Silent at the shipped 16x16 (0.51x). make ci green locally: 1219 passed,
20 skipped, all 17 structural checks.

Stage 9 stays open

Not closed at 7-of-7. TASK-055 is drafted to carry Criterion 3's remaining
half -- BoundaryFaceConfig.velocity is still validated for mutual
exclusivity and zero net flux, then read by no engine code -- and Criterion
6's documentation grep. It carries an open design question (wire the field,
or reject it) that wants a maintainer's answer before implementation rather
than the easier reading picked silently.

🤖 Generated with Claude Code

AdamClemens and others added 2 commits September 13, 2026 13:08
`stable_timestep()` has existed since Stage 5 and no live run reached it.
A 64x64 lid-driven cavity at the shipped `timestep: 0.008` is 2.05x its own
stability limit and diverges at step 17, with nothing said beforehand.

`build_simulation_state` now computes the limit for the mesh it is about to
run on and warns when the configured timestep exceeds it, naming both numbers
and their ratio. It lives in `simulation_run.py` rather than `bootstrap.py`
so `run`, `record` and `resume` all get it -- `record` is the path a long
unattended run uses.

Non-fatal by design: `stable_timestep`'s 0.25 safety factor is conservative
and 32x32 at ratio 1.02 demonstrably runs to completion.

`_characteristic_velocity` reads only what the configuration prescribes --
`simulation.velocity` and the `velocity.*` entries of each boundary face's
`field_values`. The `velocity.` prefix filter is load-bearing and was
untested until mutation testing found it: replacing it with an unconditional
`True` left all eleven tests passing, while letting a declared scalar's wall
value (`temperature: 300.0`) be read as a speed of 300, shrinking the limit
by two orders of magnitude. That is the same scalar/velocity conflation
TASK-052 fixed one layer down, reappearing in new code reading the same
mapping. `test_a_declared_scalars_wall_value_is_not_read_as_a_speed` closes
it and fails under that mutation.

Verified by hand on all three subcommands at 64x64:

    configured numerics.timestep 0.008 exceeds this mesh's own stability
    limit 0.0039062 (2.05x)

and silent at the shipped 16x16 (0.51x).

Stage 9 stays `opened` rather than closing at 7-of-7: TASK-055 is drafted to
carry Criterion 3's remaining half (`BoundaryFaceConfig.velocity` is still
validated and then ignored) and Criterion 6's documentation grep. It carries
an open design question -- wire the field or reject it -- that wants a
maintainer's answer before implementation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Merge Gate criterion 2, run as a grep rather than remembered, on the
branch that was already green.

**Dates.** TASK-054's commit landed 2026-09-13T12:08 UTC, and the entry
dated itself the 12th -- the local wall clock at drafting time, which is
exactly the failure `check_dates.py`'s own docstring now warns about
("date a change by its UTC commit time, not by the wall clock you are
looking at"). The rule was written yesterday after CI rejected the
opposite error; this is the same mistake in the other direction. TASK-054's
status, TASK-055's drafted date, and the test-count date move to the 13th.
The maintainer's warn-rather-than-reject call stays on the 12th: that
decision really was taken then.

**Two claims this branch made false and left standing.**

`stable_timestep` being engine code no live run reaches was stated in two
places outside TASK-054's own entry -- Stage 5's Criterion 12 verdict and
Stage 10's Design Question Two -- both in the present tense, both now
wrong. Amended rather than deleted: the Stage 5 verdict keeps what it said
on 2026-08-29 and records that the one gap it chose to note rather than
file has since been closed. Stage 10's question now says what TASK-054
deliberately did not do (derive the timestep) instead of restating a gap
that is gone.

**And one error of my own.** TASK-054's Purpose said Stage 5's verdict
"noted and filed" the gap. The verdict says the opposite in as many words
-- noted *rather than* filed as a violation -- and the distinction is the
whole point of the surrounding paragraph, which argues that a gap recorded
against no criterion is how it survives. Corrected to what the source
actually says, with the reasoning Stage 5 gave, and with why the outcome
was still a user's run blowing up with nothing said.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@AdamClemens
AdamClemens merged commit c76bb12 into main Sep 13, 2026
2 checks passed
@AdamClemens
AdamClemens deleted the feat/task-054-timestep-stability-warning branch September 13, 2026 16:39
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