From 9c72d59e9fa61d3e4f8692d6bd0cd15bbc8efcc2 Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 16 Jul 2026 02:08:29 +0000 Subject: [PATCH 1/2] eos: guard null thermo in apply_primitive_limiter_ apply_conserved_limiter_ already checks options->thermo() before reading vapor/cloud species counts; apply_primitive_limiter_ did not, so any species-free EOS (e.g. ideal-gas) hit a null-thermo dereference every time the primitive limiter ran. In release builds this doesn't throw -- it silently corrupts the tracer slice of prim with garbage, and once a column's density/pressure are driven low enough (e.g. a strong transient updraft), the corrupted state reaches the implicit vertical solver as a singular Jacobian, which then spins retrying without bound. Reproduced on a cubed-sphere Jupiter case: ideal-gas with limiter disabled loses all density/pressure flooring and crashes via a singular LU decomposition; enabling the limiter instead hits this null-thermo path and corrupts state (some columns pinned exactly at the floor, others with velocities on the order of 1e30+). An identical run using ideal-moist with trace (~1e-10) vapor species -- same forcing, same cold start, same perturbation -- has no such bug (thermo is non-null) and survives the same event cleanly, isolating the failure to this one line. Confirmed fixed by directly constructing a low-density/high-momentum cell (the exact failure shape) and calling U->W: before the fix this corrupts memory; after, density and pressure clamp to their floors as intended. Co-Authored-By: Claude Sonnet 5 --- src/eos/equation_of_state.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/eos/equation_of_state.cpp b/src/eos/equation_of_state.cpp index 8da4c514..e8d0d47f 100644 --- a/src/eos/equation_of_state.cpp +++ b/src/eos/equation_of_state.cpp @@ -174,9 +174,14 @@ void EquationOfStateImpl::apply_primitive_limiter_(torch::Tensor const& prim) { prim.masked_fill_(torch::isnan(prim), 0.); prim[IDN].clamp_min_(options->density_floor()); - int ny = options->thermo()->vapor_ids().size() + - options->thermo()->cloud_ids().size() - 1; - prim.narrow(0, ICY, ny).clamp_min_(0.); + // options->thermo() is unset for species-free EOS types (e.g. ideal-gas); + // guard it the same way apply_conserved_limiter_ already does, otherwise + // this dereferences a null thermo and corrupts prim with garbage tracers. + if (options->thermo()) { + int ny = options->thermo()->vapor_ids().size() + + options->thermo()->cloud_ids().size() - 1; + prim.narrow(0, ICY, ny).clamp_min_(0.); + } prim[IPR].clamp_min_(options->pressure_floor()); } From eb3eeb674e4c54c5696c67d0cbfbdc1c9bebdf1f Mon Sep 17 00:00:00 2001 From: Sihe Chen Date: Thu, 16 Jul 2026 10:24:46 +0800 Subject: [PATCH 2/2] Update equation_of_state.cpp --- src/eos/equation_of_state.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/eos/equation_of_state.cpp b/src/eos/equation_of_state.cpp index e8d0d47f..ac693a69 100644 --- a/src/eos/equation_of_state.cpp +++ b/src/eos/equation_of_state.cpp @@ -174,9 +174,6 @@ void EquationOfStateImpl::apply_primitive_limiter_(torch::Tensor const& prim) { prim.masked_fill_(torch::isnan(prim), 0.); prim[IDN].clamp_min_(options->density_floor()); - // options->thermo() is unset for species-free EOS types (e.g. ideal-gas); - // guard it the same way apply_conserved_limiter_ already does, otherwise - // this dereferences a null thermo and corrupts prim with garbage tracers. if (options->thermo()) { int ny = options->thermo()->vapor_ids().size() + options->thermo()->cloud_ids().size() - 1;