Repository navigation
feat(domain): add forced Kolmogorov flow to ns-grid, with forward and chained-gradient checks - #272
Merged
Merged
Conversation
# Conflicts: # mosaic/benchmarks/problems/navier_stokes_grid/exclusions.py
This was referenced Oct 26, 2026
# Conflicts: # mosaic/tesseracts/navier-stokes-grid/xlb/tesseract_api.py
Exponax now runs with its default 2/3-rule dealiasing (#280), and the truth, its initial states and its convergence check are regenerated with it. The 512² run tracks the 1024² truth as closely as before (0.3% after one time unit, 2.3-3.2% after four), and JAX-CFD still converges to it at observed orders 1.6 and 1.9.
With its default dealiasing (#280) Exponax stays stable on both configurations of forward/kolmogorov, so the exclusion for its aliasing instability goes.
Each solver now runs on 32² to 256² at the coarse configuration's CFL number and records the wall time of one time unit on each grid, so the result shows how much accuracy each solver buys per second. The status check asks that refining from 64² to 256² converge at order 0.8 or more and that no grid from 64² up diverge, in place of the reference-to-coarse error ratio.
gradient/chain_fd_check differentiates a training-style loss through 16 chained solver calls on the Kolmogorov flow and compares the gradient with central differences. The loss is relative to its targets, so the cotangents each call receives are far smaller than in the single-call checks. A VJP that is only accurate for cotangents of order one fails it. The result has the shape of fd_check's and goes through the same status checks and plot.
…plot A perturbed run that diverges leaves NaN differences, which the result JSON stores as null. The plot passed them to math.isfinite and float and failed; it now leaves them out, as the status checks already do.
Adds an intervention record to each tesseract's metadata.mosaic block: the solver's abstraction level, its interventions (level by mechanism, necessity, and per setup the consequences, symptom and evidence), and its setup deviations. Discovery parses it into SolverSpec.record, and a test validates every record and its mosaic-intervention markers. Records for OpenFOAM, FEniCS (structural and heat), XLB and Warp-NS, checked against the current code.
Adds intervention records for deal.II, Firedrake, JAX-FEM and torch-fem (structural and heat), TopOpt.jl, Exponax, INS.jl, jax-cfd, PhiFlow and PICT, checked against the current code, with markers around their L2 and L3 code. The format check now requires a record on every tesseract.
… the RK4 momentum Resolution as in #310.
…ecks Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ks use Drop the spectrum and correlation metrics of forward/kolmogorov, which no plot or check reads, and the burned-in states that no experiment starts from. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Records the tuning that merged after the records were written (#296, #299, #301, #305, #306, #307, #308, #314) and drops what it removed (#268, #284, #296). Applies the same rules to every solver: `tuned` marks a setting picked by measuring alternatives, a setup an intervention was never measured on gets its own `unknown` entry, and settings that only keep Julia working under juliacall or only let solvers share the benchmark machine are not recorded.
Split spliced sentences, cut explanations repeated across docstrings, and use American spelling (including cell_centers). Update the docstrings of XLB, INS.jl and Warp-NS that still described the unforced solvers.
2 of 6 tasks
…ence Add the body force and drag to the ns-grid description, and list the 2D Kolmogorov flow among the converged spectral references.
dionhaefner
added a commit
that referenced
this pull request
Nov 1, 2026
## Summary Records the interventions and setup deviations of the 14 tesseracts #302 left out: deal.II, Firedrake, JAX-FEM and torch-fem (structural and heat), TopOpt.jl, Exponax, INS.jl, jax-cfd, PhiFlow and PICT. It also brings #302's records for OpenFOAM, Warp-NS, XLB and FEniCS up to date with the tuning PRs that merged after it. L2/L3 code gets `mosaic-intervention` markers, and the format check now requires a record on every tesseract. ## Type of change - [ ] New solver backend - [ ] Solver tuning / improvement - [ ] New benchmark domain - [x] Harness / infrastructure change - [x] Documentation - [ ] Bug fix ## What changes Each record starts from the intervention spike (annotations of 6517f75) and is checked against `main` at 6e850bb. Interventions the cleanup removed are dropped, for example XLA autotuning flags (#291), Exponax's dealiasing (#280), JAX-FEM's `umfpack_solver` (#286), TopOpt.jl's INP parser patch (#299) and PhiFlow's periodic CG tolerance (#296). Settings the tuning PRs introduced are recorded as tuned L0 interventions with their measurements: OpenFOAM's periodic GAMG and MPI decomposition (#306, #314), the channel CG tolerances of Warp-NS and PhiFlow (#308), torch-fem's CG tolerance (#301), deal.II heat's AMG preconditioner (#305) and INS.jl's CUDA backend (#307). Where nobody knows why something is there, the record says `necessity: unknown`; where an intervention was never measured on a setup, that setup gets its own `unknown` entry. Setup caches, warm-ups and the deal.II worker are runtime adaptations and are not recorded. The rubric in `docs/tutorial.qmd` now says what `tuned` covers (a setting picked by measuring alternatives), that runtime settings which only keep Julia working under juliacall are wrapper fixes, and that settings which only let solvers share the benchmark machine, such as JAX's GPU memory fraction, are not recorded. Highest required level per setup: | Level | Setups | | --- | --- | | L3 | PhiFlow on the periodic boxes (int64 index patch), FEniCS heat | | L2 | PhiFlow and PICT on the cylinder, Warp-NS everywhere, XLB on the periodic boxes, Exponax in 2-D | | L0 | OpenFOAM on the 2-D box and the cylinder, PICT on the 2-D box, torch-fem | | none | the rest | ## Benchmark label `benchmark:none`: YAML metadata, comments and one schema description. ## Notes - Some evidence is not public yet: PICT's outflow A/B in OpenFOAM, PhiFlow's semi-Lagrangian accuracy figure and FEniCS's adjoint-patch A/B live in the unpublished cleanup notes. - Still to record when they merge: DAFoam (#295) and any setups the Kolmogorov PRs (#272) add. - Judgment calls for review are in the first comment.
…nto feat/kolmogorov-forward # Conflicts: # mosaic/tesseracts/navier-stokes-grid/exponax/tesseract_api.py
Exponax sets its stepper's private injection term to place the force at the cell centers, jax-cfd sums the force and drag itself because sum_forcings needs jax.tree_map, and XLB applies the exact difference method by hand because its operator takes only a constant force. Extend the INS.jl rollout, Exponax mean-flow and XLB sub-stepping records to the forced runs.
# Conflicts: # mosaic/tesseracts/navier-stokes-grid/exponax/tesseract_api.py # mosaic/tesseracts/navier-stokes-grid/exponax/tesseract_config.yaml # mosaic/tesseracts/navier-stokes-grid/ins-jl/solver.jl # mosaic/tesseracts/navier-stokes-grid/ins-jl/tesseract_config.yaml # mosaic/tesseracts/navier-stokes-grid/jax-cfd/tesseract_config.yaml
Contributor
📊 View the full benchmark resultsThe rendered docs preview has every plot for this run (forward accuracy, gradients, cost, optimization) merged with existing baseline results on Mosaic statusBenchmarks: skipped by the |
Kolmogorov flow is a different problem statement from the decaying Taylor-Green box, so its experiments (forward/kolmogorov, forward/kolmogorov_laminar, gradient/chain_fd_check) now belong to a new ns-2d-kolmogorov setup instead of falling into the periodic box. Results pages then report them separately, and a solver's flags on the forced flow no longer count towards its Taylor-Green results. The forcing interventions of Exponax, jax-cfd and XLB move to the new setup, since only the forced runs need them. Interventions on the shared periodic code path apply to both setups and reuse the same reason. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 task done
…tiplier make_incompressible pads the singular periodic pressure system with a Lagrange multiplier unless the solve states its rank deficiency, which makes the system indefinite. CG is not guaranteed to converge on it, and under jit phiml returns unconverged solves silently. On the Kolmogorov flow at 256², 852 of 6400 solves to t = 4 stopped unconverged, two of three states went non-finite on CI, and the chain gradient check got a non-finite gradient. The periodic solve now passes rank_deficiency=0; make_incompressible balances the divergence to zero mean, so plain CG converges. The adjoint solve stops at the relative tolerance only, since its right-hand side scales with the cotangent and the absolute tolerance stopped it after a few iterations in the chain check. Every PhiFlow cell of forward/kolmogorov and gradient/chain_fd_check now passes, no other check changes, and the cost experiments run 1.3-7.5x faster in 2D and 1.4-16x in 3D, so the sparse-CG cost explanation no longer applies and is removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
forward/kolmogorov ran each scored time as its own call from t = 0, so every solver simulated 1 + 2 + 3 + 4 = 10 time units per state and grid to score 4. Chaining calls instead would round-trip staggered and lattice states through the cell centers between times, which damps the finest scored modes. The canonical ns-grid schema gets an optional snapshot_every input and a non-differentiable snapshots output: the velocity at the cell centers after every snapshot_every steps, 2-D periodic boxes only. Every solver that runs the Kolmogorov flow records them inside its own loop (JAX-CFD from its trajectory, Exponax, PhiFlow and XLB with a nested scan, INS.jl in its RK4 loop, PICT in a post-step hook, Warp-NS in its step loop); the VJPs skip them. OpenFOAM rejects them, as it rejects the forcing. The kernel makes one call per state and grid to the last time and scores each snapshot, so a run that diverges late keeps its earlier times. safe_apply_with_state can leave the finiteness check to the caller for that. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4 of 9 tasks
dionhaefner
added a commit
that referenced
this pull request
Nov 2, 2026
## Summary INS.jl now runs `forward/cylinder` and `optimization/drag_opt` on the DFG 2D-1 channel, so it joins the cylinder setup instead of being excluded as "not wired up". jax-cfd stays excluded; its exclusion now says why the channel isn't expressible through its API. ## Type of change - [ ] New solver backend - [x] Solver tuning / improvement - [ ] New benchmark domain - [ ] Harness / infrastructure change - [ ] Documentation - [ ] Bug fix ### For solver tuning - [x] Before/after compared: INS.jl's periodic path is unchanged (`tests/test_ins_jl_consistency.py` passes on the new image); only the cylinder cells are new - [x] No regressions to other solvers (only INS.jl's tesseract changed) - [x] Intervention record updated for every intervention added, changed or removed ## What changes - **The cylinder** is a Brinkman penalization force −χu/η, with η = dt. χ = 1 on every face touching a cell of the shared staircase. INS.jl 5.0.0 has no obstacles; a body force is its documented hook for extra physics. The drag is the penalization's integral. - **The inlet profile** is added to the state after INS.jl's boundary conditions. INS.jl's rrule gives a `DirichletBC` value no tangent, so drag_opt would get a zero gradient. - **The channel's pressure** is solved by diagonalizing INS.jl's Laplacian, which splits into an x and a y operator on this grid. `psolver_direct` needs INS.jl's CUDSS extension on the GPU, which fails to load with CUDA.jl 6. `psolver_transform` rejects the pressure outlet, and `psolver_cg` ran past 600 s where this takes 2.7 s. - **The drag_opt charts** only drew a fixed XLB/PhiFlow/Warp-NS list; INS.jl is added. - Each workaround is an L2 intervention with its measurements in `tesseract_config.yaml`. ## Benchmark label `benchmark:solver`: INS.jl's tesseract changed. ## Results On an A100, at 20 cells per diameter unless noted: | | INS.jl | Peers (#272's CI) | |---|---|---| | Cd error (gate 5%) | −2.9% (−4.5% at 10) | −1.3% to +3.0% | | Δp error | 6.0% | 3.5% to 6.8% | | Cd drift (gate 1e-3) | 2e-5 | | | drag_opt Cd / Cd₀ | 0.520, 58 iterations, 419 s | 0.523 to 0.537 | - The penalization drag agrees with the control-volume Cd to 0.04%. - The drag gradient matches a central difference to 3e-5. - The optimized profile is within 2.2% of Warp-NS's. ## Notes - **Drag gap:** INS.jl's drag sits about a point below PhiFlow and Warp-NS on the same staircase. Halving η moves Cd by only 0.1%, so the cause is still open. - **README:** line 65 ("only four of seven … a spectral projection incompatible with the geometry") is now out of date. I left it for you. - **Upstream:** INS.jl's CUDSS extension imports `CUDSS.CUDA`, which CUDSS 0.7 removed. Not reported upstream yet. - **Tests:** `tests/test_ins_jl_cylinder.py` runs INS.jl's CPU backend in a container without a GPU. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # mosaic/tesseracts/navier-stokes-grid/ins-jl/tesseract_api.py # mosaic/tesseracts/navier-stokes-grid/ins-jl/tesseract_config.yaml Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dionhaefner
force-pushed
the
feat/kolmogorov-forward
branch
from
November 2, 2026 22:00
6fd8ea2 to
cba4383
Compare
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Relevant issue or PR
All Kolmogorov-flow work in one PR: body forcing for the ns-grid solvers, the three checks and the ground truth. It replaces #267 (forcing inputs) and #310. It builds on #268 (Exponax in 2D ns-grid, including its 2D sign fix) and #317 (experiment-generated references), both now on
main. #203 (docs redesign) is now onmaintoo, with the Kolmogorov setup on its scoreboard; the protocol text of the three checks is still to be added here.The solver-in-the-loop study that led here is in draft #316 for reproduction. It is not meant to merge.
Description of changes
Three checks of the forced 2D Kolmogorov flow (force sin 4y, linear drag), the ground truth they score against, its generator, and the inputs and outputs they need. It also fixes PhiFlow's periodic pressure solve, which made PhiFlow fail both turbulent checks and run for 79 minutes on CI.
Forcing inputs. The canonical ns-grid schema gets two optional, non-differentiable inputs, allowed only on 2D periodic boxes:
Each solver applies them inside its own time step, in both apply and VJP, and maps the force to its native locations the same way it maps
v0. With both left atNone, every solver runs the code it ran before.semi_implicit_navier_stokes, averaged onto the facesv0, in the momentum rate of every RK4 stagetentative_vel_2d_kernelinjection_modewould put sin κy half a cell off)drag; the mean flow decays exactlyWith a force, XLB sizes its lattice Mach number from a bound that does not depend on the solution, so a flow that the force spins up from rest stays at the target Mach number.
Snapshots. One more optional input and a non-differentiable output, also 2D periodic boxes only:
Each solver records them inside its own loop, so its staggered or lattice state never round-trips through the cell centers between times: JAX-CFD slices its trajectory, Exponax, PhiFlow and XLB nest their scans, INS.jl records in its RK4 loop, PICT in a post-step hook, Warp-NS in its step loop. The VJPs skip them. OpenFOAM rejects them, as it rejects the forcing.
forward/kolmogorov_laminar. From rest at ν = 0.05 the flow stays a shear flow with an exact solution, u = (A/λ)(1 − e^{−λt}) sin(κy). The sweep is N ∈ {32, 64, 128} to t = 2. The check ismax_error(0.05, at=64)plusmin_order(0.8). A correct second-order scheme is off by about 1% at N = 64. A convention bug (a missing 2π/L, a flipped sign, a force half a cell off) is off by 20% or more.forward/kolmogorov. Each solver runs on a ladder of grids, 32² to 256², at the CFL number of 64² with dt = 0.01, from three turbulent states. Each grid runs once to t = 4, returns the velocity at t = 1, 2, 3 and 4 as snapshots, and is scored on 64² against the truth. From the first non-finite snapshot on, the run counts as diverged. The plot shows error against wall time per time unit. The check ismin_refinement_order(0.8): the observed order from 64² to 256² at t = 1 must be at least 0.8, and no grid from 64² up may diverge. The bound comes from the scheme's consistency, not from the other solvers.gradient/chain_fd_check. Training a model through a solver chains many calls, and its loss is normalized by the target, so each call receives a far smaller cotangent than in the single-call gradient checks. A VJP that is only accurate for large cotangents, for example because its adjoint solves stop at an absolute tolerance, passes those checks and fails this one. The check chains 16 calls of 0.1 time units at 64². It starts from a truth state plus a 1% divergence-free offset, and the loss is the mean relative squared distance to the solver's own unperturbed chain. The VJP is compared with central differences over 8 divergence-free directions at ε ∈ {1, 0.1, 0.01}. The gates aremin_cosine(0.99),max_rel_err(1e-2)andrel_err_peer_outlier(50). A correct VJP meets float32 differences to 1e-4–2e-3, and a truncated one is off by tens of percent.Ground truth. The ground truth is an Exponax run at 1024², with 2/3-rule dealiasing, from three states spun up for 100 time units. Its generator runs through #317's reference path. A 512² run differs from the truth by 0.3% at t = 1. The 0.7 MB file is documented in
references/README.md.PhiFlow's periodic pressure solve. The periodic pressure matrix is singular. Unless the solve states its rank deficiency, phiml adds a Lagrange multiplier to it, which makes the system indefinite, and CG is not guaranteed to converge on that. Under
jit, phiml returns an unconverged solve without raising. On the Kolmogorov flow at 256², 852 of 6400 solves to t = 4 stopped unconverged, 128 of them flagged as diverged with residuals up to 10⁴ times the right-hand side. Which grid and state then went non-finite varied from run to run. The solve now passesrank_deficiency=0.make_incompressiblealready balances the divergence to zero mean, so plain CG on the singular system converges, in about 375 iterations instead of 570. The adjoint solve now stops at the relative tolerance only, because its right-hand side scales with the cotangent. With the absolute tolerance, the chain check's small cotangents stopped it after 0–17 iterations. Both are recorded as interventions. The fix also makes PhiFlow's cost experiments faster, 1.3–7.5× in 2D and 1.4–16× in 3D, growing with N. All four of its ns-grid cost anomalies become ok, so their explanation, which attributed the cost to the sparse matrix-vector product, is removed.Warp-NS is excluded from the turbulent checks: its explicit-Euler, central-difference scheme is linearly unstable unless dt ≲ 4ν/u², a bound that does not shrink with the grid. OpenFOAM is excluded too: icoFoam has no source term, and OpenFOAM has no VJP.
Results (A100)
forward/kolmogorov_laminar, relative error at N = 32 / 64 / 128. All pass.forward/kolmogorov(median error at t = 1 per grid, observed order 64² → 256²) andgradient/chain_fd_check(median error and cosine at the best ε):Every solver passes both checks. Before the PhiFlow fix, its 256² run went non-finite at t = 4 from two of the three states on CI, and its chain gradient was non-finite on CI and off by 12% at the best ε on the A100.
forward/kolmogorovtook 1,027 s for all solvers together on the A100: PICT 523 s, PhiFlow 349 s, INS.jl 68 s, the others under 40 s. The last CI run took 6,600 s on its GPU, 4,759 s of it PhiFlow's. Locally, PhiFlow took 5,585 s before the fix.The chained check also catches the defect that #294 fixed. Before that fix, PICT's adjoint solves stopped at an absolute tolerance, and the check gave a median error of 0.89 at a cosine of 0.51.
Testing done
tests/test_schemas.py: the forcing inputs andsnapshot_everyare rejected outside 2D periodic boxes, andsnapshot_everymust dividesteps.tests/test_ns_grid_forcing.py(Docker, skipped without images) checks every solver image on four things: the laminar transient within 5% at N = 64, a zero force with zero drag against the unforced output, a forced VJP against central differences within 1%, and the snapshots of a forced run against separate runs that stop at their steps, to 1e-5. OpenFOAM must reject both. All 30 pass.tests/test_kolmogorov.pycovers: exact resampling of band-limited fields, the laminar solution, the vorticity identities, the forward kernel on a fake solver including a diverged grid and a run that diverges only at its last snapshot, andmin_refinement_order.tests/core/test_reference.pychecks the layout of the stored truth.tests/test_chain_fd_check.pycovers: the loss, the divergence-free directions, a correct VJP, and a VJP that drops small cotangents, which the check must flag.main.forward/kolmogoroverrors match the previous CI run's separate calls to within 1e-4 relative (PhiFlow aside).mosaic runof the ns-grid forward suite,gradient/fd_check,gradient/chain_fd_checkand the ns-3d forward suite changes no status against that CI run apart from PhiFlow's two fixes. PhiFlow with the fix also ran every suite of both problems: no other status changed, and best-ε gradient errors stayed where they were (ns-gridfd_check1.04e-5 against 1.05e-5).main. Warp-NS, PICT and PhiFlow were within 2e-7 (forward) and 2e-6 (VJP), the same asmainagainst itself.🤖 Generated with Claude Code