Skip to content

Add failing test to illustrate failure of convergence#77

Draft
maresb wants to merge 3 commits into
dgilford:masterfrom
maresb:convergence-failure
Draft

Add failing test to illustrate failure of convergence#77
maresb wants to merge 3 commits into
dgilford:masterfrom
maresb:convergence-failure

Conversation

@maresb

@maresb maresb commented Mar 11, 2025

Copy link
Copy Markdown
Collaborator

The data for this failure comes from climatological sensitivity analysis when looking at Francine with the following parameters:

time=datetime.datetime(2024, 9, 11, 18, 0, tzinfo=TzInfo(UTC))
wind_speed_kn=80.0
lat=28.6
lon=-92.1
delta_sst=-1.6
dayofyear=263
year=1994

@maresb
maresb marked this pull request as draft March 12, 2025 19:20
maresb added a commit that referenced this pull request Jul 14, 2026
The argmax of the running CAPE can land on a level with TVRDIF <= 0
(the trapezoid into it nets positive whenever the level below is more
buoyant than it is negative), and the PINB/PAT/TOB formulas then
interpolate between two same-sign values, extrapolating outside their
interval: wrong LNB/TOB and a spurious PAT term. Measured on 800 real
columns: 1.25% of cape() calls. Back up one level in that case (the
level below is provably positive, so the crossing formulas apply), and
skip the residual-area block entirely when no sign change exists.

With the guard, all 24 measured hazard cases agree with a crossing-level
reference implementation to <= 1.3e-3 J/kg, and the 1995 marginal
profile now matches it exactly (74.378263 m/s). Issue #77 (Francine)
converges: VMAX=83.849841, IFL=1, map continuous at the former jump.

Also adapt test_run_sample.py to the intentional LNB-selection change:
the old rtol=1e-13 pins cannot hold (summation order moved, and ~1-2.5%
of columns change on purpose, up to 14.4 m/s on this sample). Compare
NaN/flag patterns exactly (small allowance for log-derived variables
where vmax crosses 0), the bulk at per-variable noise scales, and bound
the intentional tail. Re-pin the .nc outputs to retire this scaffolding.

Generated-with: Claude Fable 5
maresb added a commit that referenced this pull request Jul 14, 2026
…vels

The continuous running work integral peaks at buoyancy zero crossings,
between grid levels. Ranking candidates by their level values alone can
mis-rank two buoyant layers whose level values are within a partial
area of each other, returning the smaller layer's CAPE and the wrong
LNB, and leaving a small residual discontinuity (bounded by the partial
area, ~few J/kg, i.e. ~0.02 hPa in the pressure map - far below the
0.5 hPa stopping tolerance, but avoidable). Rank each level by its
running CAPE plus the residual partial area to the interpolated
crossing above it.

This supersedes the previous back-up-one-level guard: levels with
TVRDIF <= 0 carry no partial area and are strictly dominated by the
crossing candidate of the buoyant layer below, so the winner always
admits the crossing formulas.

Verified on 2,400 cape() calls over 800 real columns against the
previous commit: the new value is never smaller (min diff exactly 0),
only 5 calls change (max +2.06 J/kg), with LNB moving to the truly
dominant layer on those; flags identical. Issue #77 and both 1995
profiles unchanged; full test suite and doctests pass.

Generated-with: Claude Fable 5
maresb added a commit that referenced this pull request Jul 15, 2026
Fixes the solver non-convergence of issue #77 at its root. pcmin.m's
CAPE rule -- integrate the signed buoyancy work to the HIGHEST grid
level with positive buoyancy, then clamp at zero -- is a discontinuous
function of the inputs: an arbitrarily small buoyancy sign change at a
marginal level aloft toggles the intervening negative area all at once,
jumping CAPE by O(100 J/kg) and the pressure map by ~0.8 hPa. When such
a jump straddles the map's diagonal, no fixed point exists and the
iteration locks into a bit-exact period-2 cycle (the Francine/issue-77
sounding and the historical 1995 marginal profile).

cape() now takes CAPE as the maximum of the same running signed work
integral (same trapezoid increments, same interpolated zero-crossing
partial areas) over all candidate outflow levels, and the LNB as the
level attaining it. The maximum is continuous in the inputs (Berge),
needs no clamp (the launch level is a zero-work candidate), and equals
the legacy value exactly on single-crossing buoyancy profiles. The
period-2 rescue is removed: with a continuous map the cycles it patched
over cannot occur, and both historical failing soundings now converge
through the normal path in a handful of iterations (regression-pinned
in tests/test_marginal_convergence.py, including the exact PR #77
Francine column).

Deliberately NOT bit-identical to pcmin.m -- an intentional convention
correction, validated in discontinuity_analysis/: on the 7,093-column
2024 sample, zero flag changes, 98.1% of VMAX within 0.1 m/s, and a
1.9% tail (up to 7.9 m/s) confined to marginal multi-layer profiles on
which the legacy value was discontinuity-adjacent. AGENTS.md's
bit-identity policy gains the corresponding carve-out.

The fuzz test's false invariant (IFL != 1 implies NaN outputs) is also
corrected: flags tripped by the environmental-CAPE call persist (issue
#78) and may accompany finite outputs.

Generated-with: Claude Fable 5
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