Skip to content

JWL equation of state: single-material and multi-fluid mixture implementation#1586

Draft
fahnab666 wants to merge 22 commits into
MFlowCode:masterfrom
fahnab666:jwl-upstream-rebase
Draft

JWL equation of state: single-material and multi-fluid mixture implementation#1586
fahnab666 wants to merge 22 commits into
MFlowCode:masterfrom
fahnab666:jwl-upstream-rebase

Conversation

@fahnab666

@fahnab666 fahnab666 commented Jun 12, 2026

Copy link
Copy Markdown

PR Description

Implements the Jones-Wilkins-Lee (JWL) equation of state in MFC for
detonation-products modeling and five-equation multi-fluid JWL/ideal-gas
mixtures.

This PR adds:

  • Per-fluid EOS selection through fluid_pp(i)%eos.
  • JWL pressure recovery, energy inversion, and sound-speed support.
  • GPU-resident JWL material parameter tables.
  • JWL-aware total-energy reconstruction in HLL/HLLC paths.
  • A contact-restoration repair for the five-equation HLLC JWL path.
  • Validation artifacts for exact-reference, invariant, and stress-test cases.

Closes #TBD.

Type of Change

  • New feature
  • Bug fix / numerical correctness fix
  • Tests or validation artifacts
  • Documentation update

Implemented Capabilities

Capability Status Evidence / Notes
Single-material JWL EOS Implemented Pure-JWL Riemann cases compare against exact/sampled JWL references.
Five-equation JWL/ideal-gas mixtures Implemented Well-posed compressed-air interface compares against an exact two-material Riemann solver.
Per-fluid EOS selection Implemented fluid_pp(i)%eos = 1 selects stiffened gas; fluid_pp(i)%eos = 2 selects JWL.
JWL pressure recovery Implemented Uses the Mie-Gruneisen form with the JWL cold curve and thermal contribution.
JWL energy inversion Implemented Recovers specific internal energy from (rho, p) for JWL reconstruction and initialization.
JWL sound speed Implemented Uses the Rocflu/Stanley-style Mie-Gruneisen sound-speed relation.
HLL/HLLC JWL energy reconstruction Implemented Replaces the ideal-gas total-energy reconstruction when a JWL material is active.
Five-equation HLLC contact repair Implemented Initializes alpha_rho_L(:) and alpha_rho_R(:) before JWL energy reconstruction.
GPU parameter availability Implemented JWL parameter arrays are available on the device for GPU-resident EOS evaluation.
Six-equation JWL/air Not validated Early-time smoke checks exist; longer final-time exact-interface runs remain under-debug.
TNT-air blast Stress evidence Useful positivity/symmetry stress test, not a headline quantitative validation yet.

JWL EOS Form

The pressure is modeled as a Mie-Gruneisen EOS with a two-exponential JWL cold
curve:

p = p_cold(rho) + omega*(rho/rho0)*e

where

p_cold(rho) =
    A*(1 - omega/(R1*V))*exp(-R1*V)
  + B*(1 - omega/(R2*V))*exp(-R2*V)

V = rho0/rho

The sound speed follows the Mie-Gruneisen/Rocflu-style form used by the
reference solver:

c^2 = dp_cold/drho + (omega/rho0)*e + (omega/rho0)*(p/rho)

This avoids explicit temperature evaluation in the hot EOS path.

EOS Selection

Each material selects its EOS with fluid_pp(i)%eos.

Value EOS Required parameters
1 Stiffened gas Existing stiffened-gas material parameters
2 JWL fluid_pp(i)%jwl_A, jwl_B, jwl_R1, jwl_R2, jwl_omega, jwl_rho0, and mixture parameters when applicable

Below-cold-curve states require careful interpretation. If a requested state
has p < p_cold(rho), exact round-trip identity is not generally expected once
the implementation applies the non-negative-energy or temperature floor. These
states should be described as cold-floor-sensitive rather than as ordinary
positive-temperature JWL states.

Validation Evidence Tracker

Tier Test family Reference / metric Current result PR verdict
Core EOS formula and round-trip checks (rho, p) -> e -> p' residuals Formula path checked against reference behavior Pass / core evidence
Core Pure 1D JWL Riemann Exact/sampled JWL Riemann solution MFC profiles overlap reference with expected shock/contact smearing Pass / core evidence
Core Five-equation JWL/air exact interface Exact two-material Riemann solver Monotone L1 convergence on the well-posed compressed-air case Pass / core evidence
Core Smooth JWL entropy wave Periodic return / consistency check Useful smooth-code verification with documented order behavior Pass / core evidence
Core 2D quasi-1D JWL 1D exact slice plus y-invariance Zero y-deviation; 2D reproduces 1D behavior Pass / core evidence
Supporting JWL mixture closure formulas Analytic closure residuals Near machine-precision residuals in closure checks Supporting evidence
Supporting Shyue water-air Exact stiffened-gas two-material regression All checks pass; validates multi-fluid infrastructure, not JWL directly Supporting evidence
Stress 2D TNT-air blast Positivity, symmetry, radial profile Stable circular blast morphology; no direct spherical reference yet Stress only
Stress Ambient-pressure JWL/air mixing Known extreme pressure-ratio behavior Useful limitation study Do not use as headline validation
Exploratory Kamm/Tarver/PBX cases Literature-style comparisons Needs documented metrics and pass/fail tables Follow-up
Under-debug Six-equation JWL/air Exact two-material final-time comparison Longer runs show air-pressure collapse and grid-independent error Not validated

Reviewer-Facing Summary

The reviewable feature is the five-equation JWL foundation: single-material JWL,
JWL/ideal-gas five-equation mixtures, EOS conversion routines, and JWL-aware
HLL/HLLC reconstruction. The strongest validation stack is exact-reference JWL
Riemann testing, exact two-material JWL/air convergence, smooth EOS
code-verification, and 2D quasi-1D invariance.

The six-equation JWL/air model is intentionally not claimed as complete in this
PR. Existing six-equation results are useful diagnostics, but longer final-time
comparisons still fail and should remain future work.

PR Checklist

  • Added JWL EOS capability.
  • Added or updated tests/validation artifacts for new behavior.
  • Documented EOS selection and JWL material requirements.
  • Separated exact-reference validation from qualitative stress tests.
  • Replace #TBD with the issue number before opening the PR.
  • Confirm whether the orphan jwl_contact_blend parameter should be removed before review.
  • Confirm CPU validation numbers from the final branch after source cleanup.
  • Confirm GPU results match CPU results on NVIDIA or AMD hardware.
  • Trigger AI/code review after the PR is opened:
    • @coderabbitai review for incremental review.
    • @coderabbitai full review for full review from scratch.
    • /review for Qodo review.
    • /improve for Qodo suggestions.
    • @claude full review or label claude-full-review for Claude review.

Known Limitations and Follow-Up

  • Remove or consume the jwl_contact_blend parameter if it remains unused in
    solver logic.
  • Add explicit pass/fail reports before promoting Kamm/Tarver/PBX cases from
    exploratory/supporting evidence to validation evidence.
  • Run an axisymmetric or 3D spherical blast case before claiming direct
    Kingery-Bulmash blast validation.
  • Continue six-equation JWL/air debugging separately from this five-equation
    PR foundation.

Fahad Nabid added 3 commits June 12, 2026 12:08
Add Jones-Wilkins-Lee equation of state with four mixture closures
(isobaric, Kuhl-Khasainov, p-T equilibrium, Rocflu blend) via
jwl_mix_type. New shared m_jwl module, per-fluid eos selector,
GPU-resident parameter arrays, example cases, and inverse-function
test suite.
Without this, the device copy of jwl_mix_type was uninitialized; non-zero
mixture closures (Kuhl, p-T equil, Rocflu) silently executed the wrong
branch on GPU.
… rocflu fix

- s_initialize_jwl_module: add f_is_default sentinel checks on all JWL
  parameters, positivity checks on R1/R2/omega/rho0, n_jwl>1 guard;
  upgrade cv check with f_is_default; use local use statements
  (m_mpi_common, m_helper_basic) instead of module-level import
- s_jwl_sound_speed_squared: refactor to reuse s_jwl_pcold /
  s_jwl_dpcold_drho helpers instead of inlining the same algebra
- s_jwl_ptequil_pressure_er/energy_pr: add 1e-12 early-exit to both
  bisection loops; add bracket sign check in energy_pr fallback
- s_jwl_rocflu_pressure_er/energy_pr: widen high-Y guard to
  1-1e-4 (symmetric with low-Y guard) to close the 0.99 discontinuity
- m_variables_conversion: remove dead s_mpi_allreduce_integer_sum import
- tests: add golden files for jwl_single_material_shocktube,
  1D/2D jwl_mixture_test
@fahnab666 fahnab666 force-pushed the jwl-upstream-rebase branch from 73700a5 to d83068f Compare June 13, 2026 02:05
Fahad Nabid added 2 commits June 13, 2026 00:27
…s (5-eq model)

Wire the JWL EOS into the solver hot path for the five-equation model
(model_eqns = 2, Allaire et al. JCP 2002). All changes are guarded by
jwl_idx > 0 and model_eqns == model_eqns_5eq so non-JWL cases are
unaffected.

m_variables_conversion:
- s_convert_conservative_to_primitive_variables: after s_compute_pressure
  gives the stiffened-gas pressure, override it with s_jwl_mix_pressure_er
  using Y = alpha_rho(jwl_idx)/rho and alpha = alpha(jwl_idx). Without
  this, the solver uses the wrong EOS to recover pressure from conserved
  variables, giving thermodynamically inconsistent primitive fields.
- s_convert_primitive_to_conservative_variables: add JWL branch that
  computes E = rho*e_mix(rho,p,Y,alpha) + 0.5*rho|u|^2 + qv via
  s_jwl_mix_energy_pr instead of the stiffened-gas E = gamma*p + pi_inf
  formula, keeping prim->cons consistent with cons->prim.
- s_compute_speed_of_sound: add JWL branch using
  s_jwl_mixture_sound_speed_squared (frozen mass-weighted rule). Accepts
  an optional alpha_rho_j argument for the correct Y_j = alpha_rho_j/rho;
  falls back to the rho0 proxy alpha_j*rho0/rho when not supplied (e.g.
  avg-state calls).
- Export s_jwl_mixture_sound_speed_squared via the module public list.

m_riemann_solver_hll / m_riemann_solver_hllc:
- Add use m_jwl to both modules.
- In every E_L/E_R reconstruction block: when jwl_idx > 0 and 5-eq,
  compute e_L/e_R from pressure via s_jwl_mix_energy_pr and form
  E = rho*e + 0.5*rho|u|^2 instead of the stiffened-gas gamma*p + pi_inf
  formula. This keeps the Riemann wave-speed estimates thermodynamically
  consistent with the JWL EOS for both HLL and HLLC solvers.
- In m_riemann_solver_hll: pass alpha_rho_j to both s_compute_speed_of_sound
  calls so the sound speed uses the actual phasic partial density for Y_j
  rather than the rho0 proxy.
Convert the 1D JWL CJ products example from the slow six-equation pTg relaxation setup to a cheap five-equation JWL mixture smoke case. The case now uses fixed timestepping, HLL, periodic boundaries, and an 8-step stop so it runs in seconds instead of spending minutes in relaxation/adaptive CFL work.

Finish the JWL sound-speed and energy consistency wiring by passing actual JWL partial densities through the HLL sound-speed calls, using JWL inverse energy during primitive/flux conversion, and adding guards for invalid JWL pressure, energy, and RK energy updates.

Add the generated 52A7542F golden test files for the updated jwl_cj_products case. Validation before push: ./mfc.sh build -j 8, ./mfc.sh test --generate --no-build -f 52A7542F -t 52A7542F -j 1, and the commit-hook precheck all passed.
@fahnab666 fahnab666 force-pushed the jwl-upstream-rebase branch from 0998f4b to cdb34a7 Compare June 13, 2026 09:03
Fahad Nabid added 3 commits June 13, 2026 16:03
… case to 5-eq

s_compute_speed_of_sound used 'present(alpha_rho_j) .and. alpha_rho_j == alpha_rho_j'
in a single expression. Fortran does not guarantee short-circuit evaluation of .and.,
so gfortran evaluated alpha_rho_j (null pointer) even when it was absent, causing a
SIGSEGV in every HLLC test. Fix: separate into a nested if-block, compute the
rho0-proxy Y_j first, then conditionally override when alpha_rho_j is present and
not NaN.

Also converts examples/1D_jwl_cj_products from the expensive 6-equation pTg relaxation
model (model_eqns=3, relax_model=6) to the 5-equation model (model_eqns=2) with the
isobaric JWL closure, restoring physical CJ detonation parameters (D=6900 m/s,
CR=1.80) that keep the initial pressure (34.5 GPa) above the JWL cold curve (7.1 GPa).

All four JWL tests now pass: 52A7542F, E3460991, DB95E4F7, 25B9827F.
… guard

LF solver (riemann_solver=5) was missing the JWL E_L/E_R reconstruction
block present in HLL and HLLC, producing completely wrong energy fluxes
for any JWL case with the Lax-Friedrichs solver. Add the s_jwl_mix_energy_pr
block and alpha_rho_j argument to both sound-speed calls, matching the HLL
pattern. Add use m_jwl to the module.

All four HLLC model branches were calling s_compute_speed_of_sound without
the optional alpha_rho_j argument. This caused the JWL mixture sound speed
to use the rho0-proxy mass fraction Y_j = alpha_j*rho0/rho instead of the
correct Y_j = alpha_rho_j/rho, giving wrong wave-speed estimates for all
HLLC JWL runs. Fix all 12 call sites; the 5eq and bubbles sections use
qL/R_prim_rsx_vf(*,max(1,jwl_idx)) directly; the 4eq section uses
alpha_rho_L/R(max(1,jwl_idx)) which is already populated there.

Add a runtime guard in s_initialize_jwl_module that aborts if fluid_pp%eos=2
is used with model_eqns != 2. JWL is only wired into the five-equation
prim/cons conversion and Riemann solver paths; using it with any other
model_eqns silently falls through to the stiffened-gas path.

Regenerate tests/DB95E4F7 golden files (1D jwl_mixture_test, HLLC) since
the corrected alpha_rho_j changes the numerical output.
…construction

The JWL mixture pressure inversion was applied only in the field-conversion path, so probe output and pre_process fell back to the stiffened-gas formula and reported the wrong pressure. Move the inversion into s_compute_pressure behind new optional jwl_Y/jwl_alpha arguments so every caller shares one correct code path.

Also fix two numerical bugs: s_jwl_rocflu_sound_speed_squared omitted the omega-density term in its derivative, giving an incorrect sound speed in the blended region; and the HLL and LF JWL energy reconstruction double-counted qv.

Regenerate the four JWL golden files to match the corrected output.
@fahnab666 fahnab666 force-pushed the jwl-upstream-rebase branch from c7db505 to 87a0281 Compare June 14, 2026 22:17
@sbryngelson

Copy link
Copy Markdown
Member

Maintainer review — full issue list

Thanks for this. The physics is genuinely thoughtful — four mixture closures, exact inverses for the linear ones, fail-fast parameter validation in init, and an honest validation tracker that separates what's verified from what isn't. That part I'm not asking you to change.

The engineering, however, isn't yet at MFC standards. Below is the complete list of what needs to be addressed before this can merge, grouped by severity, with the reason each one matters. Numbering is for ease of reference in replies.


Blocking — correctness & maintainability

1. Six near-identical copies of the JWL energy-reconstruction block.
The same ~14-line block (Y_L/Y_R clamp → alpha_jwl_*s_jwl_mix_energy_prE_* = rho*e_jwl + 0.5*rho*vel_rms) appears 4× in src/simulation/m_riemann_solver_hllc.fpp (~lines 245, 632, 908, 1364) and again in m_riemann_solver_hll.fpp and m_riemann_solver_lf.fpp.
Why it matters: any future fix or sign correction will land in one copy and silently miss the other five. This is the single highest-risk maintenance hazard in the PR. Collapse it into one #:def fpp macro or a shared subroutine and call it from all six sites.

2. HLLC and HLL/LF feed the sound-speed routine different inputs for the same physics.
HLLC passes the bounded primitives jwl_Y/jwl_alpha into s_compute_speed_of_sound; HLL, LF, m_data_output.fpp, and m_time_steppers.fpp instead pass alpha_rho_j = q_prim_vf(max(1, jwl_idx))%sf(...).
Why it matters: the mass fraction driving the sound speed comes from a different source depending on which Riemann solver is selected, so the same physical state produces different wave speeds. There is no physical reason for this divergence — it reads like two iterations of the same idea that never got unified. Pick one input path and use it everywhere.

3. max(1, jwl_idx) indexing hack.
Appears in m_riemann_solver_hll.fpp, m_riemann_solver_lf.fpp, m_data_output.fpp, m_time_steppers.fpp:

alpha_rho_j = q_prim_vf(max(1, jwl_idx))%sf(j, k, l)

Why it matters: when no JWL fluid is present (jwl_idx == 0) this silently reads fluid 1 and passes a meaningless value into the sound-speed call on every non-JWL run, relying on present()/NaN guards in the callee to ignore it. That's a band-aid for not guarding at the call site. Guard the call site instead so non-JWL paths never construct the argument.

4. Runtime branch added to the hottest kernel in every build.
Every flux loop in all three Riemann solvers now evaluates if (jwl_idx > 0 .and. model_eqns == model_eqns_5eq). jwl_idx is a runtime device scalar, not a compile-time constant.
Why it matters: this is contrary to MFC's compile-time case-specialization philosophy — the branch is not optimized away, so every simulation (including the 99% that never touch JWL) pays a predicated branch per face in the hottest part of the code. Gate the JWL path behind an fpp/case-optimization flag so non-JWL builds compile it out entirely.

5. Per-face 60-iteration bisection with transcendentals, on GPU.
s_jwl_ptequil_pressure_er / s_jwl_ptequil_energy_pr (m_jwl.fpp ~321, ~376) run a fixed 60-iteration bisection with exp() calls inside, invoked from the energy-reconstruction path that executes on every Riemann face. The convergence test rarely triggers early, so it's effectively always 60 iterations.
Why it matters: on GPU this is a serial 60-iteration transcendental loop per thread with heavy warp divergence — a severe throughput problem for an exascale target. Either provide a convergence/performance benchmark justifying it, or gate jwl_mix_type = 2/3 as host-only/experimental until it's optimized.

6. Silent NaN/floor squashing masks divergence.
Both s_jwl_mix_pressure_er and s_jwl_mix_energy_pr end with:

if (pres /= pres) pres = sgm_eps
if (pres < sgm_eps) pres = sgm_eps

and s_compute_speed_of_sound (m_variables_conversion.fpp ~307) has:

if (c2 /= c2) c2 = jwl_omegas(jwl_idx)*max(pres, 1._wp)/max(rho, sgm_eps)
c2 = max(c2, jwl_omegas(jwl_idx)*max(pres, 1._wp)/max(rho, sgm_eps))

Why it matters: a diverged bisection or a NaN pressure gets converted to a 1e-16 floor and the solver keeps running with garbage instead of aborting — a classic silent-failure mode that makes bugs nearly impossible to localize. Additionally max(pres, 1._wp) hardcodes a 1 Pa dimensional floor, and the unconditional max(c2, ...) raises every sound speed to at least the products-gas value, biasing wave speeds in air-dominated cells. Replace silent squashing with debug-mode assertions; remove the hardcoded 1._wp.


Blocking — validation & test integration

7. The unit test exercises a Python re-implementation, not the compiled Fortran.
toolchain/mfc/test/test_jwl_inverse.py (665 lines) is a standalone Python translation of the EOS routines.
Why it matters: it can pass while the Fortran is wrong (or vice versa) — it cannot catch a Fortran/Python divergence, which is exactly the kind of bug it appears to be guarding against. It provides false confidence. Either drive the compiled Fortran, or clearly relabel it as a reference oracle (and wire it into CI discovery so it actually runs).

8. Golden cases not wired into the test registry, generated on a dirty tree.
The four golden case dirs (tests/25B9827F, 52A7542F, DB95E4F7, E3460991) appear with no corresponding change to the test-case generator in the diff, and their golden-metadata.txt records generation from /Users/fahadnabid/.../MFC on jwl-upstream-rebase (dirty).
Why it matters: if they aren't produced by a registered case spec, the suite will never exercise them (or will flag them stale), and goldens generated from an uncommitted working tree are not reproducible. Wire the cases into the generator and regenerate goldens on a clean checkout.

9. Promote validation tiers only with reports. The tracker lists Kamm/Tarver/PBX as exploratory and TNT-air as stress-only.
Why it matters: fine to keep them out of scope, but the PR should not present any of them as validation. Please keep the headline claim scoped to the exact-reference five-equation stack, which is the part that's actually backed.


Non-blocking — quality & style

10. Hardcoded air defaults as non-sentinel values, triple-duplicated.
In src/simulation/m_global_parameters.fpp, src/pre_process/m_global_parameters.fpp, src/post_process/m_global_parameters.fpp:

fluid_pp(i)%jwl_air_e0    = 2.5575e5_wp
fluid_pp(i)%jwl_air_rho0  = 1._wp
fluid_pp(i)%jwl_air_gamma = 0.4_wp

Why it matters: every neighboring field defaults to dflt_real so a forgotten input fails fast; these instead silently inject a specific air model. Make them dflt_real with validation (or document them as deliberate), and factor the block into one shared init helper instead of three copies that will drift.

11. Dead/over-broad public API.
s_jwl_pcold, s_jwl_sound_speed_squared, s_jwl_energy_pr, s_jwl_kuhl_pressure_er, s_jwl_kuhl_energy_pr, s_jwl_kuhl_sound_speed_squared are in the module's public list and re-exported again through m_variables_conversion, but are only ever reached internally via the dispatchers.
Why it matters: gratuitous API surface invites accidental external coupling and obscures the real interface. Make them private; only the s_jwl_mix_*, the two sound-speed dispatchers, jwl_idx, and init/finalize need to be public, and they shouldn't be re-exported a second time.

12. ;-packed multi-statement lines.
The p-T-equilibrium routines pack 3–4 statements per line, e.g.:

rj = max(Y_s*rho_s/a_lo, sgm_eps); ra = max((1._wp - Y_s)*rho_s/(1._wp - a_lo), sgm_eps); V = rho0/rj

Why it matters: MFC is one-statement-per-line throughout; this hurts readability and makes git blame/diffs useless. Unpack them.

13. Unnamed magic thresholds.
Guards mix sgm_eps (1e-16), 1.e-12_wp, and a 1.e-4_wp pure-fluid cutoff in the Rocflu routines (s_jwl_rocflu_pressure_er / _energy_pr / _sound_speed_squared).
Why it matters: the 1.e-4_wp is a physical threshold with no name and no rationale in-code. Give it a named constant and a one-line justification.

14. Possibly-negative denominator flooring in the Kuhl closure.
s_jwl_kuhl_* divides by R_mix via max(rho_safe*R_mix, sgm_eps), but R_mix can be negative depending on the air_gamma/omega combination.
Why it matters: max(., sgm_eps) on a negative value clamps it to +1e-16, flipping the sign of the denominator and silently producing wrong (not just floored) results. Check R_mix > 0 explicitly or document why it can't be negative.

15. Energy floor ignores the cold-curve offset.
The inverse routines floor specific internal energy at max(e, 0._wp).
Why it matters: for JWL the physically meaningful floor is the cold-curve energy, not 0, so the round-trip pressure_er(energy_pr(p)) == p breaks near the floor. The Python unit test only samples e ≥ 0 states, so it won't catch this. At minimum document it as a deliberate safety net.

16. LLM-style verbosity vs MFC norms.
m_jwl.fpp carries multi-sentence justification comments inline (e.g. the Wood's-rule block, the reader-addressed "present() guards must be separate if-blocks because Fortran does not short-circuit…") and defensive boilerplate at the end of every dispatcher.
Why it matters: it's far more heavily commented than the rest of the codebase, which makes it read as out-of-place and harder to skim. Trim to MFC's comment density.

17. Leftover dev cruft in .typos.toml.
The PR adds exclusions for _jwl_verification/ and _jwl_organization/, directories that aren't part of the PR.
Why it matters: dangling references to scratch dirs are confusing and will rot. Remove them.


Summary

The closures are the strong part — leave them. The asks, in priority order: de-dup the six energy blocks (1), unify the sound-speed input path (2, 3), gate the JWL branch at compile time (4), justify-or-host-gate the bisection (5), replace silent floors with assertions (6), and fix the test/golden story (7, 8). The rest are cleanup. Happy to discuss any of these.

Fahad Nabid added 5 commits June 15, 2026 00:22
… input

Factor the six duplicated JWL energy-reconstruction blocks into a single
JWL_RECONSTRUCT_ENERGY fypp macro and apply it only on the live
five-equation paths (HLLC general-5eq, HLL, LF). Statically dead JWL
branches in the 6eq/4eq/bubbles loops are deleted in favor of plain
stiffened-gas energy. Unify the JWL sound-speed input on the bounded
(jwl_Y, jwl_alpha) pair, drop the redundant alpha_rho_j optional from
s_compute_speed_of_sound, and remove the max(1, jwl_idx) indexing hack
from all callers (m_data_output, m_time_steppers). Numerics preserved;
all four JWL golden cases pass.
Replace the fixed bisection in s_jwl_ptequil_pressure_er/energy_pr with
the Illinois (modified false-position) method: derivative-free, superlinear,
and bracket-preserving, so it converges to the same root in far fewer
iterations (cutting GPU warp-divergence cost) without risking a wrong root.

Remove the silent NaN/floor squashing flagged in review: drop the
unconditional c2 max-bias and the hardcoded 1 Pa proxy in the JWL
sound-speed dispatch (kept only the standard sqrt-argument floor), and the
redundant post-call NaN/floor masks in the mixture pressure/energy
dispatchers (each closure already floors its own output). A genuinely bad
value now surfaces instead of being hidden as plausible-but-wrong. All four
JWL golden cases pass.
Make internal m_jwl routines private and drop the m_variables_conversion
re-export, pulling jwl_idx directly into m_data_output and m_time_steppers.
Consolidate fluid-parameter validation into s_initialize_jwl_module (now
covering jwl_E0 and the air parameters) and remove the duplicate s_check_jwl
from m_checker_common. Name the Rocflu pure-phase mass-fraction cutoff,
guard R_mix positivity in the Kuhl closure, unpack semicolon-packed
statements, document the energy floor, and trim verbose docstrings.
Drop the scratch-dir entries from .typos.toml.
Comment thread src/simulation/m_riemann_solver_hll.fpp Outdated
Comment thread src/simulation/m_riemann_solver_lf.fpp Outdated
@danieljvickers

Copy link
Copy Markdown
Member

I unsure why you created the file test_jwl_inverse.py, but this is a significant deviation from how MFC handles testing in general. If you want to add a unit testing framework, we will need a significant PR just for the testing framework itself. It would be unsustainable to add the framework as you have implemented it here, as it would bloat the code significantly.

Also, for your tests added, you add 3 1D cases and a 2D case. It is not inherently bad to add them, but a feature like this really requires a 3D example to be added with comparison to some sort of example. I know that I have seen comparisons of your code to published examples, and those comparisons should make it into the readme if you are going to provide a readme.

The only concern I have about the fortran code outside of the minor comments that I left are with the size of the jwl module. I need to actually take a look at it in an IDE where I have access to grep and other tools, but it looks like some redundant code that we should consider parsing down.

Fahad Nabid and others added 8 commits June 15, 2026 13:55
…son data

Remove test_jwl_inverse.py: per-function Python unit tests are a deviation from
MFC's golden-file regression model and belong in a separate unified-framework PR,
not bolted onto a feature. Drop the 1D cj_products and 1D mixture_test examples
(and their golden files), keeping the 1D single-material shock tube and the 2D
mixture/IBM case. Add the quantitative convergence and error tables (exact-Riemann
L1 norms, multi-closure grid convergence, 2D y-invariance) to the kept READMEs so
the verification results are actually present alongside the cases.
Per review, the JWL five-equation energy path was nested inside the non-MHD
else branch (a Y_jwl guard plus the macro's own if/else), adding an extra
if-tree layer. Hoist it to an else-if at the same level as the relativity and
MHD branches in the HLL, LF, and HLLC solvers, with the standard stiffened-gas
energy in the final else. JWL_RECONSTRUCT_ENERGY now emits only the JWL branch
body (the de-duplicated inverse-EOS calls), so the macro still removes the
cross-solver duplication without imposing the nesting.
…cell cutoff

Replace the false-position root-find, which could overshoot the (0,1)
alpha bracket and produce NaNs, with an in-bracket bisection. Skip the
solve for near-pure cells, where the equilibrium root sits within ~(1-Y)
of the bracket end and is ill-conditioned, evaluating the single-phase
EOS directly below jwl_pure_cutoff.
…OS inputs

Match the HLL averaged-state JWL sound-speed inputs to HLLC's symmetric
form, replacing the density-weighted Y and right-state-only alpha. Remove
the hardcoded 1 Pa pressure floor in the mixture-EOS inversion; the
closures already floor their own output. Document fluid_pp%eos and the
per-fluid jwl_* parameters in the case guide.
The eos_idxs table duplicated fluid_pp%eos and was GPU-resident and per-case allocated, yet no kernel ever read it; the only consumer was a startup validation check. Drop the array across declaration, allocation, host/device copies, and deallocation, and validate fluid_pp%eos directly in the init loop. Shrinks dead device state in the shared variables-conversion module with zero numerical change (JWL 4-closure benchmark and exact-Riemann star state bit-identical; both JWL golden tests pass).
# Conflicts:
#	src/simulation/m_riemann_solver_hll.fpp
#	src/simulation/m_riemann_solver_hllc.fpp
#	src/simulation/m_riemann_solver_lf.fpp
@sbryngelson

Copy link
Copy Markdown
Member

Automated re-review (run on behalf of @sbryngelson)

🤖 This is an automated re-review against the current head. It re-checks the 17 items from the prior maintainer review by reading the actual diff (not commit messages) and looks for regressions from the revision churn (22 commits, two master merges). CI is fully green across all NVHPC CPU/GPU compilers, Intel, GNU, Convergence, and Verrou FP-stability.

Nice work on the revision — most of the prior review is genuinely addressed in the code, not just the commit log. The energy-reconstruction macro de-duplicates correctly, the sound-speed inputs are unified, the indexing hack and Python re-implementation test are gone, the defaults are sentinel-ized through a shared helper, the public API is tight, and eos_idxs is removed with no dangling references. The qv double-count fix and the Rocflu omega-term fix are present and look correct.

The following items are still open and should each be resolved before merge.

Resolved

For the record, these are confirmed fixed in the current diff: de-duplicated JWL_RECONSTRUCT_ENERGY macro (#1); unified (jwl_Y, jwl_alpha) sound-speed inputs across hllc/hll/lf (#2); removal of max(1, jwl_idx) (#3); deleted test_jwl_inverse.py (#7); dflt_real air defaults via s_assign_jwl_fluid_defaults (#10); narrowed public list with no re-export (#11); named jwl_pure_cutoff constant (#13); documented energy floor (#15); .typos.toml cleanup (#17); plus the validation-tier wording (#9) and most of the style pass (#12, #16).

Still open

A. Golden-case provenance and registration.
tests/25B9827F and tests/E3460991 carry golden-metadata.txt recording generation on a dirty tree (on jwl-upstream-rebase (dirty)), the personal branch name, and four hardcoded absolute paths (/Users/fahadnabid/Documents/default/MFC/...). There is also no change to the test-case registry (tests/cases.py) in the diff, so it cannot be confirmed that these cases are actually generated and exercised by the suite — they may be orphan golden files. Regenerate the goldens from a clean checkout via the case generator, and include the registry entry that produces these hashes.

B. Hot-path JWL branch is a runtime test, not compile-time gated.
The live five-equation path is still else if (jwl_idx > 0 .and. model_eqns == model_eqns_5eq) evaluated per face in all three Riemann solvers (and else if (jwl_idx > 0) in s_compute_speed_of_sound, m_data_output, m_time_steppers). The statically-dead 6eq/4eq/bubbles copies were correctly removed, but the live branch was not moved behind an fpp / case-optimization parameter, so the commit description "compile-time gated" is inaccurate. Either gate it at compile time consistent with MFC's case-specialization model, or adjust the description to match what the code does.

C. jwl_mix_type = 2 per-cell root-find cost.
The p-T-equilibrium closure still contains two do it = 1, 60 bisection loops with exp() calls inside the loop body (m_jwl.fpp, ~lines 320–338 and ~392–407). The default isobaric closure is correctly closed-form and jwl_pure_cutoff skips near-pure cells, but mixed cells under jwl_mix_type = 2 pay the full 60-iteration transcendental cost per face on GPU. Green CI does not measure this (the regression cases use the cheap closures). Please either provide a throughput measurement for mode 2 or document it as an experimental/host-preferred path; the revision should not be described as having reduced this cost, since the loops are unchanged.

D. Residual silent mask with a misleading comment.
s_compute_speed_of_sound now has c = sqrt(max(c2, sgm_eps)) with a comment stating a genuinely bad c2 (e.g. NaN) propagates so the failure surfaces. max(NaN, sgm_eps) is compiler/flag-dependent and frequently returns the finite argument, so a NaN sound speed can be silently floored to sqrt(sgm_eps) rather than surfacing. Either make the NaN handling explicit (self-compare check) or correct the comment so it does not claim a guarantee the code does not provide.

E. Unconditionally-passed Y_jwl can be uninitialized.
In m_data_output.fpp and m_time_steppers.fpp, Y_jwl is assigned only inside if (jwl_idx > 0) but is passed by keyword on every call:

if (jwl_idx > 0) then
    Y_jwl = min(max(q_prim_vf(jwl_idx)%sf(j, k, l)/max(rho, sgm_eps), 0._wp), 1._wp)
end if
call s_compute_speed_of_sound(..., jwl_Y=Y_jwl)

When jwl_idx == 0 this passes an uninitialized GPU-private variable. The callee only reads it under its own jwl_idx > 0 guard so behavior is currently correct, but this is undefined-behavior-flavored and -Wuninitialized/sanitizers will flag it. Initialize Y_jwl = 1._wp unconditionally.

F. Kuhl R_mix guard is a magnitude floor, not a sign check.
The Kuhl closure uses max(rho_safe*R_mix, sgm_eps), which clamps a negative R_mix to +sgm_eps and flips the denominator sign rather than failing. Inputs are validated positive in init so this cannot trigger in practice, but the prior request was an explicit R_mix > 0 check; please either add the sign check or document why R_mix is provably positive.

G. Remaining ;-packed statements.
Two multi-statement lines remain in init code (m_jwl.fpp ~700, ~704): jwl_cv_prod = 0._wp; jwl_cv_air = 0._wp and jwl_cv_air = fluid_pp(i)%cv; exit. Unpack to one statement per line.

H. 3D example with published comparison.
Per @danieljvickers' note, a feature of this scope warrants a 3D case compared against a published reference, and the code-vs-literature comparisons that exist should be included in the README alongside the cases. This is still outstanding.


Once A–H are addressed, this looks ready for a final human pass. The physics and the structural refactor are in good shape.

🤖 Generated by Claude Code at @sbryngelson's request. A human maintainer will make the final call.

@sbryngelson

Copy link
Copy Markdown
Member

Automated re-review — additional code-quality items (run on behalf of @sbryngelson)

🤖 Follow-up to the previous automated pass. Those items (A–H) still stand. This pass covers MFC-specific code-quality expectations not yet raised, each verified against the current diff. Items continue the same lettering.

I. Closure test coverage.
The PR ships four mixture closures (jwl_mix_type 0/1/2/3 = isobaric / Kuhl / p-T-equilibrium / Rocflu), but only the isobaric default is exercised. Neither 1D_jwl_single_material_shocktube/case.py nor 2D_jwl_mixture_test/case.py sets jwl_mix_type, so both run mode 0, and the two golden cases (tests/25B9827F, tests/E3460991) inherit that. The Kuhl, p-T-equilibrium, and Rocflu closures have no automated regression coverage — the only evidence they work is the hand-written convergence table in 2D_jwl_mixture_test/README.md. Please add a golden case per closure, or gate the uncovered closures off as experimental until they have coverage. Shipping selectable GPU code paths that CI never executes is the largest remaining quality gap.

J. Single-precision (wp = real32) portability.
The p-T-equilibrium bisection convergence tests use fixed relative tolerances below single-precision epsilon (~1.2e-7):

! m_jwl.fpp:331
if (abs(f_m) <= 1.e-12_wp*max(abs(pres), sgm_eps)) exit
! m_jwl.fpp:400
if (abs(g_m) <= 1.e-12_wp*p_s) exit

In a single-precision build these can never be satisfied, so both do it = 1, 60 loops always run the full 60 iterations on the hot path. Relatedly, the pure-cell shortcut Y >= 1._wp - 1.e-12_wp collapses to Y >= 1._wp in single precision, silently shifting the branch threshold. Scale these tolerances with epsilon(1._wp) (and add a convergence-failure diagnostic if the bracket never tightens).

K. EOS selectors are bare magic numbers.
fluid_pp%eos is compared against literal 1/2, and jwl_mix_type against literal 1/2/3, throughout (m_jwl.fpp:667,703,710, m_variables_conversion.fpp), including in the abort message text ("use 1 for stiffened gas or 2 for JWL"). MFC's surrounding hot-path code uses named constants (model_eqns_5eq, etc. from m_constants). Define named parameters (e.g. eos_stiffened_gas, eos_jwl, jwl_mix_isobaric, …) and use them in the comparisons and messages.

L. Validation lives outside the checker convention.
All JWL parameter validation is in s_initialize_jwl_module; nothing was added to any m_checker* module, which is where MFC validates case inputs. The aborts themselves are correct (standard s_mpi_abort with informative messages — good), but they're in the wrong stage. Compounding this, the comment added in m_global_parameters_common.fpp states the air parameters "are validated in m_checker_common," which is not true — no such check exists. Move the validation to the checker stage, or at minimum correct the stale comment.

M. Missing artifacts for a feature of this scope.

  • No benchmark case under benchmarks/, despite the change touching the Riemann hot path and adding a 60-iteration root-find.
  • No theory/equations docs page — only the case.md parameter table. 2D_jwl_mixture_test/README.md references a root README-JWL-EOS.md that is not committed (dangling reference). Either commit that file or drop the reference.

N. Incomplete/inconsistent Doxygen.
New routines have !> summary lines but no !! @param/!! @return blocks. The new jwl_Y/jwl_alpha args on s_compute_pressure are added with no !< documentation, while the same args on s_compute_speed_of_sound do have !< field comments — inconsistent. Bring the new public-interface docs up to the codebase's Doxygen level.

O. Cold-curve duplication across closures.
The cold-curve expression A*(1 - omega*rho1/(R1*rho0))*exp(...) + B*... is recomputed verbatim in s_jwl_pressure_er, s_jwl_energy_pr, and the Kuhl/p-T/Rocflu variants, even though an s_jwl_pcold helper exists and is used elsewhere. Routing all callers through the helper would remove the duplication and shrink the module (this is the concrete form of the module-size concern raised earlier in the thread).

P. present() of optionals inside GPU [seq] routines.
s_compute_speed_of_sound relies on present() of optional arguments inside an offloaded [seq] routine, with NaN-self-compare guards. The non-short-circuit caveat is documented in comments, but present() on optionals inside device routines has historically been fragile across NVHPC versions. CI is green today; please confirm this is intentional and considered portable, since it's an easy source of a future silent device/host divergence.


For completeness, these were checked and are fine: fluid_pp%eos defaults to 1 so existing cases are unaffected; toolchain registration is complete via params/definitions.py and descriptions.py (this layout has no case_dicts.py); no TODO/FIXME left in the new source; and the PR correctly fixes several neighboring 5.e-15.e-1_wp suffixes on lines it touches.

🤖 Generated by Claude Code at @sbryngelson's request. A human maintainer will make the final call.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants