Skip to content

Cut-cell normal-mode stability machinery + E4_1 alignment with Brady-Livescu 2021 - #30

Merged
pbrady merged 35 commits into
lanl:mainfrom
pbrady:e4-paper-alignment
Jul 24, 2026
Merged

Cut-cell normal-mode stability machinery + E4_1 alignment with Brady-Livescu 2021#30
pbrady merged 35 commits into
lanl:mainfrom
pbrady:e4-paper-alignment

Conversation

@pbrady

@pbrady pbrady commented Jul 24, 2026

Copy link
Copy Markdown
Member

Summary

Two bodies of work, sequential on this branch:

Plan 48 — cut-cell normal-mode stability machinery (Trefethen program). Adds a theorem-backed stability analysis of ψ-parameterized cut-cell closures to the Python pipeline:

  • gks_kreiss.py: generalized (augmented) Kreiss determinant for closures with more evolution rows than decaying modes; defective/anomalous κ made observable (was silently "stable"); structural benign-zero discrimination (conservative full-constant and Dirichlet tail-constant classes); column-equilibrated, provably bounded witness search. Fixes a pre-existing bug where L2 fed only p of r closure rows.
  • New modules: cutcell_eval.py (cached lambdified closure evaluator), reflection.py (reflection matrices R(s;ψ), per-end A_L/A_R via the mirror convention, Trefethen-1985 round-trip operator), interval_operator.py (two-cut-end interval assembly + resolvent scans), fully_discrete.py (RK4 stability-region containment, resolvent-only).
  • L8 C++ validation upgrades: embedded-shape Lua template, growth-rate metric from the L∞ trace, cut-capable dispatch, CFL knob.

E4 paper alignment. The repo's E4_1 cut construction was a one-node-downwind mis-port of the published construction (no wall row; rows centered at fluid nodes instead of (wall, f0..f3)). This branch replaces it with the Brady & Livescu 2021 (JCP 426:109794) construction exactly:

  • derive_cut_cell_paper in temo.py: wall row, paper blend rule, t+1-equation cut conservation with the wall-column identity, Dirichlet = drop wall row. All mis-ported paths deleted; E2_1/E2_2 verified bit-identical.
  • E4_1.cpp regenerated: std::array<real,4> alphas (αu04, αu14, αu24, αu25), 197/288 guard removed (it was a property of the mis-ported family's denominator; the paper family has no ψ-pole on (0,1] at feasible alphas).
  • Ground truth committed as tests/fixtures/e4_1_paper_reference.json (id=1 parameters from the paper's Appendix B text; reconstruction verified to ~1e-15 by two independent implementations).
  • The paper's published scheme passes the new machinery: half-line GKS stable on a 20-point continuum ψ sweep, dense eigenvalues negative through N=240.
  • All mis-port artifacts purged (flags, docs, test anchors). Leeward/outflow-Dirichlet probe configurations are documented as continuously ill-posed validation anchors, not design targets.

Test plan

  • Python fast suite: 1215 passed / 163 slow-skipped (slow paper anchors 9/9 with --run-slow).
  • C++: full build green; stencils label 11/11; operators 3/4 with only the pre-existing, CI-excluded t-laplacian failure (docs/CLEANUP_PLAN.md §0a).

pbrady added 30 commits July 13, 2026 19:57
New stencil_gen/cutcell_eval.py: make_cutcell_evaluator(scheme, bc) derives
the cut-cell closure once per scheme (lru_cache; mathematica path for
zeros-schemes, matching psi_sweep_group_velocity's dispatch), lambdifies
every Dirichlet-block entry over (psi, *alpha), and exposes rows() (wall
column dropped, offsets 0..T-2, row_indices 0..R-2), dims, alpha_symbols,
denominators (irreducible non-constant factors for feasibility pre-checks),
and interior(). E4_1 tests are @pytest.mark.slow (derivation ~5.5 min);
warm-cache 20-psi evaluation measured ~0.2 ms.
… gks_kreiss substrate defects

Four false-unstable mechanisms diagnosed by the blocked 1.2 attempt:
sigma_min/det divergence at large |s| for augmented m>p matrices, unbounded
NM witness runaway, near-origin anomalies routed to the zero-discrimination
classifier, and the uncovered Dirichlet tail-constant kappa=1 structural zero.
Dense eigenvalues (N=20..160) confirm E2_1 alpha=0 stable at all five psi.
Fixes the four false-positive mechanisms diagnosed by the blocked 1.2
attempt (E2_1 alpha=0 Dirichlet rows, dense-eigenvalue-confirmed stable):

(a) column-equilibrate M(s) before the SVD on the augmented m>p path
    (det-zero locations invariant; sigma_min stays O(1) at large |s|
    instead of decaying like 1/(4|s|)) and bound the witness search to
    the provable eigensolution region |s| <= S_max (sup-norm/Gershgorin
    bound, new eigensolution_s_bound): grid points beyond are masked
    unevaluated, Nelder-Mead iterates beyond are rejected (stops the
    |s| ~ 1e155 runaway/underflow);
(b) reflect NM probes at Re(s) = eps_imag (no more sub-sonic-axis
    count_mismatch anomalies from probes at Re ~ 1e-13);
(c) route near-origin anomalies (|s| < 0.1) through the structural
    benign-zero discrimination, never _classify_imag_axis;
(d) generalize the structural test: benign iff the sigma_min right
    singular vector aligns with x_const (conservative full-constant
    zero, row sums ~ 0) OR x_tail = (0_nodal, e_{kappa->1}) (Dirichlet
    tail-constant zero, tail row sums ~ 0), row-sum identity verified;
    vanishing-order fit/deflated margin cover the double zero, so the
    psi->1 row-coincidence degeneracy no longer trips sigma_tol.

Tests: the 1.2 diagnostic as regression (five psi stable, dense-eig
cross-check at N=40), equilibrated sigma O(1) at |s|=1e3, the 0.2
engineered det-zero still caught, witness |s| <= S_max, NM runaway and
axis-probe regressions, structural triage units; all 0.1-0.4 verdicts
unchanged.
New stencil_gen/reflection.py: mode_split(interior_weights,
interior_offsets, s) -> ModeSplit partitions the interior kappa roots
into rightgoing (|kappa| < 1 - tol), leftgoing (|kappa| > 1 + tol), and
borderline (unit-modulus, resolved by the Trefethen perturbation test);
enforces the exact p/p split for centered 2p+1 interiors at Re(s) > 0
and raises DefectiveKappaError on coalescing borderline (sonic) roots.

The per-root perturbation logic is factored out of
gks_kreiss._classify_imag_axis into the shared helper
gks_kreiss.perturbed_root_directions (behavior-preserving refactor)
rather than duplicated, per the item text.
New stencil_gen/fully_discrete.py: cached RK4 stability-region boundary
(Newton continuation over theta in [0, 8 pi) — arg R winds 4x around dS
by the argument principle, so the plan's naive 2 pi sweep would trace a
quarter loop and overestimate the margin; closure asserted), containment
margin min_{z in dS} sigma_min(zI - dt L) with the 1-Lipschitz-certified
margin_certified = margin - max_arc_spacing/2, eigenvalue-in-S check
(closed region), and rk4_containment_over_psi reusing the item-3.1
interval assembler at fixed N=40 plus evaluation at the worst
(psi_L, psi_R, N) triple(s) from the item-3.2 scan.  Resolvent
quantities only — no matrix powers, no expm, no stepping.

Tests cover the plan's (a) L = -I polyline-exact margin, (b) normal
advection circulant at CFL 0.8/3.0 (with the honest zero margin of the
raw circulant's boundary constant mode), (c) non-normal perturbation
shrinking the margin, boundary anchors/winding/coverage, caching, and
the E2_1 cutcell sweep wiring (E4_1 variant slow-marked).
Post-review corrections to the 4.5 findings entry and calibration script:
- Requote all three probe rows' realized psi from the _realized_psi_set
  replication at the hardcoded (rounded) coordinates, binary-verified
  against logs/geometry.csv: P_large 0.4988/0.9298, P_mid 0.0695/0.755,
  P_small 8.84e-4/0.868 (the earlier draft quoted pre-rounding search
  coordinates; C++ rates/deltas unaffected).
- Extend the part-0 scan to all 8 single-alpha +/-0.01 directions and
  correct the quoted range to 12-20/20 (zero-width conclusion unchanged).
- Fix the script docstring/comments to match what is actually quoted.

Full script rerun (exit 0) reproduces every reported figure.
Replace the mis-ported one-node-downwind E4_1 cut-cell construction with
a direct symbolic implementation of the published Brady & Livescu 2021
(JCP 426:109794) Sec 3.2/3.3 rules: new derive_cut_cell_paper builds the
5x7 block with row 0 AT the wall, rows centered (wall, f0..f3), a
transition row with no wall support, the paper blend rule
alpha_ij = psi*alphau_ij + (1-psi)*alphau_{i-1,j-1}, and the t+1-equation
cut conservation (fluid-column sums + wall-column equation
w0*B[0,0] = wu^T Bu[:,0] = -1) solved linearly with all 4 uniform alphas
surviving symbolically.

Deleted (no shims, no dual dispatch): derive_cut_cell_mathematica,
construct_cut_cell_with_free_gammas, blend_free_gammas, _safe_get,
zeroed_col_default, zeroed_col_aza, and the zeros path in
derive_cut_cell_scheme (which now forwards zeros schemes to
derive_cut_cell_paper, ending the old 2-param alpha space).
cutcell_eval and group_velocity dispatch switched to the new function.

Acceptance (new slow TestPaperE41Construction in test_e4_cut_cell.py):
blocks/dirichlet slice/weights match tests/fixtures/
e4_1_paper_reference.json at all 5 psi to <=1e-10 (observed ~5e-15);
psi=1 identity B(1) = [conserved uniform closure; interior] is an exact
symbolic zero; wall-column identity -1 exact; Taylor moments hold at the
paper row centers. E2_1/E2_2 verified bit-identical (srepr diff) and the
E2 test subsets pass unchanged; test_temo.py 127/127.

Legacy E4 slow tests anchored to the mis-port now fail as expected;
A7 overhauls them. C++ regen (E4_1.cpp) is a later item.
cutcell_eval.py: document the corrected E4_1 contract on the paper
construction — canonical alpha order alpha_0..alpha_3 = (alphau_04,
alphau_14, alphau_24, alphau_25), the C++ alpha[] contract; Dirichlet rows
at grid nodes 0..3 with integer offsets after the wall-column drop;
denominator structure (psi+1, psi+2, psi+3; psi-independent
uniform-conservation factor 66*alpha_0 - 73; one mixed cut-conservation
polynomial, degree 8 in psi) replacing the dead D6/197-288 lore.  Stale
'>170 s' derivation timing updated (~7 s with derive_cut_cell_paper).
gks_kreiss/reflection/interval_operator needed no changes — they consume
the evaluator generically (dims/alpha_symbols) and carry no 2-alpha or
old-layout assumptions.

test_cutcell_eval.py money tests (slow, fixture id=1 alphas from
tests/fixtures/e4_1_paper_reference.json):
(a) kreiss_cutcell_check stable at psi in {0.05, 0.3, 0.7}; at 0.97 the
    interior branch-point artifact (witness s=1.17600..., defective kappa,
    sigma=inf) is adjudicated via the investigation's deflated winding
    (W=0, swap-invariant G=(det M/(k1-k2))^2 on an RHP rectangle) and a
    dense fallback;
(b) dense eigenvalues of the assembled N=60 operator: max Re < 0 at all
    fixture psi (measured -1.5e-4..-1.6e-5);
(c) default 20-psi sweep runs; recorded verdict: any_unstable=True driven
    ENTIRELY by the branch-point artifact at the 6 near-one grid psi, all
    adjudicating clean; worst finite margin 2.10e-2 at psi=0.0720; no
    genuine bad-psi pocket.
Plus structure pins: denominator factor set asserted exactly, no
denominator zero on the C++ clamp range at alpha=0 or id=1; alpha-order
pin reproducing the fixture Dirichlet blocks at all fixture psi.

Focused run: tests/test_cutcell_eval.py --run-slow -k 'e4 or E4' →
19 passed; full file --run-slow 56 passed; consumer suites
(gks_kreiss/reflection/interval_operator) 192 passed, 5 skipped.
…7/288)

Regenerate src/stencils/E4_1.cpp and E4_1.t.cpp (and their
scripts/stencil_gen/output/ sources) from the paper construction
(derive_cut_cell_paper), replacing the mis-ported 2-alpha scheme.

- std::array<real, 4> alpha in the A2 canonical order
  alpha_0..3 = (alphau_04, alphau_14, alphau_24, alphau_25); the plain
  copy_zero_padded constructor now takes all four from the Lua alpha table.
- REMOVED the 197/288 constructor guard, its comment block, the
  <stdexcept> include, and the psi std::clamp in both nbs methods. A2's
  denominator structure ((psi+1)(psi+2)(psi+3), psi-independent
  66*alpha_0-73, one degree-8 mixed polynomial) has no psi-pole in (0,1)
  at feasible alpha, so no guard/clamp is emitted. A dense scan of the
  floating block over (0,1] at the id=1 alphas stays finite
  (max|coeff| = 3.09, no pole), confirming the endpoints are safe without
  the old clamp.
- Floating nbs = paper 5x7 (wall row + wall column); Dirichlet nbs =
  floating rows 1..4 (wall row dropped, wall column kept, 4x7), matching
  the operator's split of column 0 into the boundary-coupling B matrix.
- .t.cpp default alpha = fixture id=1 values; asserts the C++ nbs output
  equals the committed e4_1_paper_reference.json floating rows at psi in
  {0.3, 0.7} plus the Dirichlet block at psi=0.7, margin 1e-8.

Derivation reproduces the fixture to 2.9e-15. t-E4_1 passes; the full
stencils label (11 tests, incl. unchanged E2_1) passes.
pbrady added 5 commits July 23, 2026 20:47
Update the Python->C++ L8 bridge for the paper-aligned 4-alpha E4_1 cut
closure and remove the bridge handling built for the mis-port's 197/288
std::invalid_argument guard (the throw no longer exists in src/; grep of
src/ confirms no invalid_argument anywhere).

cpp_bridge.py: drop BridgeResult.infeasible/.reason and the _detect_infeasible
std::invalid_argument classifier; the nonzero-exit path returns a plain
failure result. brady2d_stability.py: cut-template verdict is now
growth_rate < rate_tol (no infeasible gate); layer8 result dict and the
cfl_values aggregate no longer carry infeasible/reason. _L8_CUT_SCHEME_TYPE
is unchanged (E4->make_E4_1); its comment documents the 4 free boundary
alphas (alphau_04/14/24/25) passed via params["alpha"] and that no alpha
vector is infeasible. Tests: remove TestInfeasibleDetection and the
propagation/below-floor cases; E4 cut-dispatch test now exercises 4 alphas;
new slow smoke test runs E4_1 at the paper id=1 alphas end-to-end.

Consumers of the removed fields (the two findings scripts that read
BridgeResult.infeasible/.reason) updated in lockstep so both run again:
- calibration_plan48_4_4.py: fmt()/as_dict() drop the infeasible/reason
  keys; part (d) is retargeted from the deleted 197/288 guard probe to the
  corrected 4-alpha E4_1 at the fixture id=1 alphas on the cut template
  (N in {31, 61}, CFL 0.8), with an E4_1 uncut-template uniform baseline
  and the cut-vs-uniform delta. No 197/288 / infeasible content remains.
- calibration_plan48_4_5.py: bridge_row() drops the infeasible key
  (nothing downstream read it). Verified by grep that the only remaining
  .infeasible accesses in stencil_gen are on CutCellInfeasible objects, a
  different type, not BridgeResult.

Cut rung at fixture id=1 alphas, CFL 0.8, t_final=10 (exit=0, no throw):
  N=31: cut growth_rate=+0.006461 final_linf=7.47e-3;
        uniform baseline growth_rate=-0.001489 -> delta=+0.00795
  N=61: cut growth_rate=+0.148840 final_linf=1.36e-3;
        uniform baseline growth_rate=+0.001843 -> delta=+0.14700
final_linf falls with N (converging); per plan-48 the reportable cut-cell
signal is the cut-vs-uniform delta, the template's floating-wall growth
having been divided out (template-BC issue not chased here).
Remove everything specific to the retired one-node-downwind E4_1
mis-port from the Python optimizer/sweep stack and reference docs, per
the e4-align directive (no shims, no dual dispatch):

- stencil_gen/optimizer.py: delete _E4_CLASSICAL_ALPHA1_CPP_FLOOR and
  _record_cpp_cutcell_diagnostic (the alpha[1] >= 197/288 diagnostic,
  which referenced a C++ guard that e4-align A5 already removed from
  src/), and both call sites in run_staged_optimize. Replace the
  optimizer.py:61-74 stale comment block with one sentence pointing to
  the paper construction.
- sweeps/optimize.py, sweeps/pareto.py, sweeps/bo.py: drop the
  cpp_cutcell_violates_197_288 plumbing (imports, persistence,
  docstrings) that consumed the removed diagnostic.
- tests/test_optimizer.py, tests/test_sweep_bo.py,
  tests/test_sweep_pareto.py: delete tests that exercised the removed
  flag/function directly; lightly correct TestClassicalAlphaBounds
  docstrings that stated the removed C++ guard as current fact.
  TestAlphaBasinSurvey is untouched (self-contained via monkeypatched
  run_staged_optimize, unaffected by the removal).
- scripts/stencil_gen/docs/optimization_reference.md,
  cutcell_normal_mode_reference.md: remove 197/288 and D6 lore, correct
  the DEFAULT_BOUNDS/extras-schema/basin-survey descriptions, and mark
  the legacy-closure GKS-instability finding as historical (superseded
  by e4-align, detail preserved in cutcell_trefethen_theory.md /
  e4_1_determinant_analysis.md).
- docs/reference/py-derivation.md: rewrite the E4_1 cut-cell
  construction description to the paper convention — derive_cut_cell_
  mathematica and its supporting dead code no longer exist;
  derive_cut_cell_scheme now dispatches internally to
  derive_cut_cell_paper for zeros-schemes.
- group-velocity-analysis skill: checked, does not mention 197/288 or
  old E4_1 specifics — no change needed.

Focused tests: tests/test_optimizer.py tests/test_sweep_bo.py
tests/test_sweep_pareto.py -x -q -> 175 passed, 13 skipped. Full
stencil_gen suite (non-slow): 1228 passed, 249 skipped, 1 xfailed.
Slow TestStagedClassicalAlpha::test_staged_classical_e4_single_seed
verified separately with --run-slow -> 1 passed.
Overhaul the E4_1 cut-cell test module to the published Brady & Livescu 2021
(JCP 426:109794) construction, deleting everything that pinned the removed
mis-port.

Deleted (mis-port specifics, no back-compat): the zeros={3,4} uniform hack
(TestE4UniformBoundaryWithZeros), the whole one-node-downwind cut construction
via construct_cut_cell_stencil on E4_1 (TestE4ZeroConstrainedCutCell,
TestE4CutCellConservationSolution, TestE4TEMOConstruction), the 2-parameter
alpha space + {0.1,0.7} default codegen tests (TestE4CodeGeneration,
TestE4TestFileGeneration), the infeasibility / approach-B psi(psi-1)-pole
narratives (test_e4_1_conservation_fails/with_zeros, _conservative_*,
_constant_weights_infeasible_r5, _psi_dependent_conservation_infeasible,
TestCutCellConservationAfterUniform, TestPolynomialStructure,
TestPolynomialBoundaryRows, TestFractionFreeConservation,
TestApproachAInfeasibility, TestPolynomialFullStencil), and the E4_1-specific
2-alpha assertions in TestDeriveCutCellScheme / TestE4CutCellSchemeWithZeros /
TestBuildCutCellConservationSystem.

Kept and re-anchored (construction-agnostic): the generic uniform-boundary
derivation and its conservation (TestE4UniformBoundary,
TestE4UniformConservation — shared with E2, uniform stage matches the paper);
the paper uniform stage (TestMathematicaUniformConservation); the shared
cut-cell conservation machinery via E2_1 (TestBuildCutCellConservationSystem);
and E2_1/E2_2 reproduction plus a new E4_1-forwards-to-paper dispatch check
(TestDeriveCutCellScheme).

Consolidated paper anchors into TestPaperE41Construction against
tests/fixtures/e4_1_paper_reference.json (single module-scoped
derive_cut_cell_paper fixture, folding the former duplicate TestMathematicaWorkflow):
5x7/4x7 dims, wall row 0, transition row 4 (no wall support), 4 surviving
alphas, block/dirichlet/weight match at all 5 psi, psi=1 uniform identity,
wall-column -1 identity, pole-free denominators, and q+1 Taylor moments about
the paper row centers (wall, f0..f3).

2892 -> 744 lines. Focused run: tests/test_e4_cut_cell.py --run-slow -> 38
passed. Full fast suite tests/ -> 1216 passed, 163 skipped.
Review verdict: the A0-A7 alignment is correct (details in the item
report). Construction re-derived independently from the paper PDF
(pp. 8-11): temo.derive_cut_cell_paper satisfies all paper constraints
exactly (5x7 shape, per-row Taylor accuracy, Bd,0 zero pattern, blend
rule, psi=0/psi=1 limits, t+1-equation conservation with wall column
= -1) and reproduces the FULL printed Table A.4 (35 entries) to 2.8e-14
when fed the corrupted E2_1.db alphas. Fixture id=1 alphas match the
independently extracted Appendix B text digit-for-digit. Production C++
E4_1.cpp matches Python at off-fixture psi=0.42 to 1e-15. Fast suite
1215 passed / 163 skipped; slow paper anchors 9/9.

Fixes for defects found (residual mis-port references that survived
A3/A6, per the review's zero-references gate):

- docs/reference/stencils.md: stale-as-current 2-alpha E4_1 lore
  (alpha[2], the 197/288 constructor throw, psi clamp, "re-add guards"
  workflow, 361-line guard-section test description) rewritten to the
  paper-aligned 4-alpha reality (src file is verbatim codegen output).
- stencil_gen/benchmarks/alpha_basin_survey.py: remove the dead
  cpp_cutcell_violates_197_288 plumbing (A6 removed its optimizer
  producer, so the flag could never be set); 4-column survey table.
- tests/test_optimizer.py: drop the flag-propagation test and flag
  fixtures accordingly; neutralize a 197/288 literal used as an
  arbitrary params-roundtrip value.
- scripts/stencil_gen/calibration_plan48_1_4.py: DELETE - the plan-48
  1.4 legacy-closure findings script asserts the retired 2-parameter
  zeros-path parameterization (derive_cut_cell_scheme now returns 4
  alphas, so its assert fires); wholly mis-port-specific; its numbers
  remain in the plan-48 findings log.
- docs/reference/py-brady2d.md, py-derivation.md, ONBOARDING.md: stale
  claims that the flag is "fully-computed" in optimizer.py, that
  output/E4_1.cpp diverges from src (now byte-identical), and that the
  E4_1 conservation xfail still exists (retired in A7).
@pbrady
pbrady merged commit 142ed58 into lanl:main Jul 24, 2026
4 checks passed
@pbrady
pbrady deleted the e4-paper-alignment branch July 24, 2026 21:49
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