Skip to content

linstabpy v1.0.0rc3

Pre-release
Pre-release

Choose a tag to compare

@ricardofrantz ricardofrantz released this 08 Oct 20:46
· 258 commits to main since this release

[1.0.0rc3] - 2026-08-10

Changed

  • BREAKING (default): numerics.operator_dissipation.apply_to_resolvent now
    defaults to true instead of false, matching apply_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 set apply_to_resolvent: false explicitly. 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_backend is 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 example strumpack_fgmres
    requiring strumpack) 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 with epsilon = 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 shadowed factor_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_map runs a mandatory band-marking
    eigenvalue problem, and that eigenproblem reads solver.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 explicit solver.eigen.factor_backend, or analysis.skip_band_marking).

  • A source-wide lint (tests/test_mpi_collective_lint.py) rejecting MPI collectives
    inside rank-0-only regions, in both the if rank == 0: and if 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_region and output_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 set C_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 only solvers.router.resolvent_map ever 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_mats helper, so one config key means one thing everywhere, and C
    defaults to the boundary-condition mask rather than to B. Runs that set no region are
    unchanged: B and C are still the boundary-condition mask, asserted byte-for-byte by a
    regression test. scripts/validation/song_2023/song_2023.py builds its masks separately and
    cannot honour a region, so it now raises instead of ignoring the key.
  • MPI is now initialised at MPI_THREAD_MULTIPLE on every import path, restoring the
    STRUMPACK factorization backend. Whichever library calls MPI_Init first fixes the
    thread level for the process, and it can only be requested at init — never raised
    afterwards. mpi4py requests MPI_THREAD_MULTIPLE, PETSc only
    MPI_THREAD_FUNNELED. utils.petsc_init.init_petsc_from_argv already imported
    mpi4py first, but seven modules imported petsc4py/slepc4py at 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 and SLATE 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 plain import mpi4py.MPI, chosen over a
    first-party helper module because isort orders third-party imports above first-party
    ones: a linstabpy.* guard could not stay above petsc4py, and ruff --fix would
    have silently reordered the fix away. mpi4py sorts above petsc4py on its own.
    tests/test_mpi_thread_level.py enforces 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 inexact shift-invert path are now
    actually applied. They were configured before eps.setUp(), where PETSc cannot yet
    materialise sub-solvers (ierr=58), and the error was swallowed — so sub_pc_type
    and pc_factor_levels were discarded. PETSc's default sub-PC is also ilu, 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 after eps.setUp(),
    failures raise instead of downgrading, and the realized type and fill level are logged
    rather than assumed. This mirrors what linalg.factorize already did for the linsolve
    path. Only st_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 called pytest.skip when 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
    a matlab_parity marker, 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 no operator_dissipation block 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 that epsilon must 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, and solvers.router.stable_sweep, matching the existing
    utils.petsc_core pattern. Importing these modules — or the
    solvers.router package, which re-exports resolvent_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_marking usable without starting PETSc. Public APIs and
    numerical behavior are unchanged.

Fixed

  • Restored the guarantee that importing solvers.router.band_marking does not
    initialize PETSc (regressed when resolvent_map was added to the
    solvers.router package 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.