Repository navigation
linstabpy v1.0.0rc3
Pre-release[1.0.0rc3] - 2026-08-10
Changed
- BREAKING (default):
numerics.operator_dissipation.apply_to_resolventnow
defaults totrueinstead offalse, matchingapply_to_stability. Previously,
enabling dissipation gave a damped eigenvalue problem and a bare resolvent
operator — a combination that runs to completion and returns grid-scale sawtooth
(spurious high-wavenumber streaks, gains orders of magnitude too large) with nothing
in the log to indicate it. Configs that deliberately want an undamped resolvent must
now setapply_to_resolvent: falseexplicitly. No config shipped with the project
changes behaviour: every one that runs a resolvent either disables dissipation or sets
the switch explicitly. Both defaults now live in one place,
config.defaults.DISSIPATION_APPLY_DEFAULTS, so the driver and the preflight checks
cannot disagree about them. solver.factor_backendis now honoured as a fallback for all solver stages
(linsolve,resolvent, and the shift-invert eigen path). It was previously read by
nothing: every resolver receives a sub-config, so a key at solver level silently did
nothing — one symptom being a run intended for GPU that executed entirely on CPU. It
sits at the bottom of the precedence chain (per-stage key > environment >
solver.factor_backend> built-in default), so no configuration that resolved before
resolves differently now. Route-specific defaults (for examplestrumpack_fgmres
requiringstrumpack) are unaffected.
Added
-
Preflight and post-run advisories (
driver.preflight), reported in the run log. Each
advisory names a concrete remedy rather than only a symptom. Preflight covers:
dissipation enabled but not reaching a stage that is about to run; no dissipation at
all; dissipation enabled withepsilon = 0; a spanwise wavenumber inside the band
where grid-scale sawtooth dominates while undamped; a base flow that is not normalised
to the canonical freestream (rho = u = T = 1); a shadowedfactor_backend; and a
strided (reduced-resolution) mesh. Post-run checks flag a leading gain far above the
physical scale for these cases and a postcheck residual above tolerance. -
A run banner and closing summary in the log, reporting effective values —
including the per-stage dissipation epsilon actually applied — so a run's real
configuration and the trustworthiness of its result are visible without loading the
output archive. -
Translation of opaque factorization failures into actionable messages. A MUMPS
workspace exhaustion (INFOG(1) = -9) previously surfaced only as
SLEPc EPS.setUp failed; it now names the cause and the remedy (ICNTL(14)). -
A band-marking backend advisory.
resolvent_mapruns a mandatory band-marking
eigenvalue problem, and that eigenproblem readssolver.eigen— not
solver.resolvent. A configuration that moved only the resolvent onto a scalable
backend therefore left the eigenproblem on the default direct solver, where at full
resolution it does not fail but simply never finishes; the run looks like a slow
resolvent sweep for hours. The advisory names the three ways out (solver.factor_backend,
an explicitsolver.eigen.factor_backend, oranalysis.skip_band_marking). -
A source-wide lint (
tests/test_mpi_collective_lint.py) rejecting MPI collectives
inside rank-0-only regions, in both theif rank == 0:andif rank != 0: return
forms. Such a call deadlocks every multi-rank run while remaining invisible to the
serial test suite, which runs at size 1. The lint ships with positive controls, since
a lint never shown to fire cannot be distinguished from one that cannot fire.
Fixed
- The single-point resolvent path now honours
input_regionandoutput_region, and no
longer forces the response mask to equal the forcing mask.driver.run._stage_run_resolvent
built its own boundary-condition-only diagonal and then setC_mat = B_mat.copy(), so two
things went wrong at once and neither said anything: a config naming a forcing region ran
with no region at all, because onlysolvers.router.resolvent_mapever read those keys; and
even a correct region was copied onto the response, which turns an asymmetric problem
(restricted forcing, free response) into a different one. Both paths now call the same
_forcing_response_matshelper, so one config key means one thing everywhere, andC
defaults to the boundary-condition mask rather than toB. Runs that set no region are
unchanged:BandCare still the boundary-condition mask, asserted byte-for-byte by a
regression test.scripts/validation/song_2023/song_2023.pybuilds its masks separately and
cannot honour a region, so it now raises instead of ignoring the key. - MPI is now initialised at
MPI_THREAD_MULTIPLEon every import path, restoring the
STRUMPACK factorization backend. Whichever library callsMPI_Initfirst fixes the
thread level for the process, and it can only be requested at init — never raised
afterwards.mpi4pyrequestsMPI_THREAD_MULTIPLE, PETSc only
MPI_THREAD_FUNNELED.utils.petsc_init.init_petsc_from_argvalready imported
mpi4pyfirst, but seven modules importedpetsc4py/slepc4pyat module scope
and bypassed it, so whichever of them was imported first decided the level. When
PETSc won,MatGetFactor_aij_strumpack()failed with PETSc error 57 andSLATE requires MPI_THREAD_MULTIPLE, taking the whole STRUMPACK/GPU route out of service —
8 of 14 failures in the GPU test venv had this single cause, and none of them
reported anything about MPI. The guard is a plainimport mpi4py.MPI, chosen over a
first-party helper module because isort orders third-party imports above first-party
ones: alinstabpy.*guard could not stay abovepetsc4py, andruff --fixwould
have silently reordered the fix away.mpi4pysorts abovepetsc4pyon its own.
tests/test_mpi_thread_level.pyenforces the ordering as a source-wide AST lint plus
a runtime thread-level assertion that skips loudly without petsc4py. - The freestream-normalisation check and the MUMPS error translation were defined but
never called: both shipped with full unit-test coverage and zero call sites, so
neither ran in production. They are now invoked from the driver and the eigensolver
respectively, and a test asserts that every advisory producer is reachable from the
runtime — unit tests establish that a guard works, not that anything calls it. - ASM/BJacobi sub-preconditioner settings on the
inexactshift-invert path are now
actually applied. They were configured beforeeps.setUp(), where PETSc cannot yet
materialise sub-solvers (ierr=58), and the error was swallowed — sosub_pc_type
andpc_factor_levelswere discarded. PETSc's default sub-PC is alsoilu, which
masked the type being dropped, but the fill level was not masked: a config asking for
ILU(2) ran with ILU(0). The settings are now applied immediately aftereps.setUp(),
failures raise instead of downgrading, and the realized type and fill level are logged
rather than assumed. This mirrors whatlinalg.factorizealready did for the linsolve
path. Onlyst_solver: "inexact"reached the defect — no shipped configuration selects
it — so no previously reported result changes. - MATLAB-parity tests are opt-in. This repository ships no MATLAB reference data,
so the seven tests that compare against it calledpytest.skipwhen it was absent —
which is every machine but the two the data lives on. A run reporting
"1361 passed, 13 skipped" read as near-total coverage without disclosing that seven
of those skips could never be satisfied by anything in the repository. They now carry
amatlab_paritymarker, deselected by default, and are requested explicitly with
LINSTABPY_MATLAB_PARITY=1 pytest -m matlab_parity. When requested, a missing
reference is a failure, not a skip: a comparison you asked for that quietly does
not run is indistinguishable from one that passed. A default run now reports
6 skips, every one an absent optional backend — a true statement about the machine. - Subprocess timeouts in the test suite are now load-aware everywhere, via a single
shared helper. One test file already scaled its timeout with load average; its two
siblings used hard limits of 120 s and 60 s, leaving as little as 1.4x headroom over
the test's own runtime. On a shared or busy machine those expired and surfaced as
test failures, indistinguishable from a numerical disagreement — a contributor
running the suite with a browser open would see phantom errors. These timeouts guard
against a hang; they are not performance assertions, so the bases are deliberately
generous. The helper documents a limitation found in practice: load average counts
runnable processes and does not detect memory-bandwidth starvation, so a co-resident
memory-bound job slowed a test run ~3x while the computed multiplier was 1.17x. production_case_template.jsonc, the file the documentation tells a new user to copy
for a production run, shipped with nooperator_dissipationblock at all. A first
production run made from it was sawtooth-contaminated with nothing in the log to say
so. It now enables dissipation and documents thatepsilonmust be re-tuned at the
target resolution rather than inherited.- The Streaky-BL MATLAB parity test guarded only one of the two references it needs, so
restoring the parity bundle while the base-flow data directory was still absent turned
a clean skip into a hard error that resembled a numerical disagreement. The guard now
checks every reference and announces which one is missing. - PETSc is now imported lazily by
linalg.backends,linalg.factorize,
linalg.shifted, andsolvers.router.stable_sweep, matching the existing
utils.petsc_corepattern. Importing these modules — or the
solvers.routerpackage, which re-exportsresolvent_map— no longer pulls in
petsc4py or initializes PETSc/SLEPc; initialization now happens on first
actual solver use. This makes lightweight helpers such as
solvers.router.band_markingusable without starting PETSc. Public APIs and
numerical behavior are unchanged.
Fixed
- Restored the guarantee that importing
solvers.router.band_markingdoes not
initialize PETSc (regressed whenresolvent_mapwas added to the
solvers.routerpackage exports). - The CPU/MUMPS parity regression guard no longer fails on freshly built
environments. Its tolerance was tighter (1e-12) than both the eigensolver's own
convergence tolerance (eps_tol=1e-10) and the measured build-to-build spread
of the converged eigenvalues (~1e-8 between independently configured
PETSc/MUMPS/BLAS builds), so it could not be stably satisfied. The guard now
uses 1e-6 — far above that floor, far below any physically meaningful drift —
and the frozen reference was regenerated. Repeat runs within one build remain
bit-identical.