Skip to content

Extract m_eos and add a single EOS family registry - #1870

Open
sbryngelson wants to merge 21 commits into
MFlowCode:masterfrom
sbryngelson:module-eos
Open

Extract m_eos and add a single EOS family registry#1870
sbryngelson wants to merge 21 commits into
MFlowCode:masterfrom
sbryngelson:module-eos

Conversation

@sbryngelson

Copy link
Copy Markdown
Member

Made with Claude Code.

Summary

  • Extracts the equation-of-state machinery (s_reference_curve, f_pressure, s_compute_energy, f_bulk_modulus, s_initialize_eos_module, and friends) out of src/common/m_variables_conversion.fpp into a new leaf module src/common/m_eos.fpp (526 lines), separating per-family EOS math from the mixture-closure rules (Wood's law, six-equation mean, bubbly branch) that remain in m_variables_conversion.fpp (now 1493 lines).
  • Adds toolchain/mfc/params/eos_families.py: a single EOS_FAMILIES registry (one EosFamily entry per family — stiffened-gas, ideal-gas, Mie-Gruneisen, JWL, Vinet). From that entry, these are now generated instead of hand-maintained at each of ~16 separate sites: the Fortran eos_* constants, the physical_parameters parameter registration in definitions.py, the case validator's per-family required/optional parameter sets and initial-state coefficient calls, both f_is_state_dependent/f_has_isentropic_reference family predicates, both layers of s_initialize_eos_module, and the case-optimization any_state_dependent_eos flag.
  • What stays hand-written by design, each checked against the registry by a test that fails on disagreement: s_reference_curve's per-family case body (the actual mathematics), the Python mirror function in toolchain/mfc/eos.py, and the family's fields on physical_parameters in src/common/m_derived_types.fpp.
  • Updates docs/documentation/contributing.md's "How to Add an Equation of State" section to describe the registry as the entry point.
  • Design and implementation plans are included under docs/superpowers/ for reference/history.

This is a refactor: it does not change any CFD result. Golden test files under tests/ are untouched (git diff --stat upstream/master -- tests/ is empty).

Test plan

  • ./mfc.sh build -j 16 — exit 0
  • build/venv/bin/python -m pytest toolchain/mfc/ -q — all passing
  • ./mfc.sh test -j 16713 passed, 0 failed, 39 skipped, 752 total (bit-for-bit identical to pre-refactor goldens)
  • git diff --stat upstream/master -- tests/ — empty
  • ./mfc.sh precheck -j 12 — all 7 checks pass
  • python3 toolchain/mfc/lint_docs.py — clean

Contribution Policy

We do not accept pull requests generated primarily by AI without genuine understanding or real-world usage context.

All contributions are expected to demonstrate:

  • A clear understanding of the codebase
  • Alignment with product direction
  • Thoughtful reasoning behind changes
  • Evidence of real-world usage or hands-on experience with the problem

If these expectations are not met, we would prefer to implement the changes ourselves rather than spend time reviewing low-effort submissions.


Acknowledgement

  • I confirm this PR meets the above expectations and reflects my own understanding and real-world context.

PR template credit: junegunn

sbryngelson and others added 18 commits September 12, 2026 16:24
Task 3 of the m_eos extraction: consumers of the EOS routines now `use
m_eos` (or `use m_eos, only: ...`) directly instead of relying on
m_variables_conversion's temporary re-export. m_variables_conversion's
public list drops the moved EOS names; the m_global_parameters_common
arrays it re-exports (gammas, isentrope_n, pi_infs, isentrope_B, cvs,
qvs, qvps) are unaffected and stay out of scope.

Only use/public statements changed - no executable code was touched.
…metrically

Give s_phase_pressure_on_isentrope and s_phase_temperature the
function_name= and cray_inline=True attributes carried by every other
cross-module GPU_ROUTINE device helper, matching the bare-directive
convention already followed by m_eos's private helpers
(s_reference_curve, s_eos_coefficients, s_ode_slope, s_rk4, s_phase_c2).

Also close an allocation/deallocation asymmetry Task 4 introduced:
s_initialize_eos_module (m_eos) allocates eight fluid-property arrays,
but s_finalize_variables_conversion_module (m_variables_conversion) was
still the one freeing them. Add a public s_finalize_eos_module to
m_eos that deallocates exactly those eight arrays, shrink
m_variables_conversion's DEALLOCATE call to its own Gs_vc, and call
s_finalize_eos_module() immediately after
s_finalize_variables_conversion_module() in all three start-ups
(pre_process, simulation, post_process).
Task 6 of the m_eos extraction: contributing.md's "How to Add an
Equation of State" section and equations.md's key-source-files line
still pointed at m_variables_conversion.fpp for the EOS operators
themselves. Update both to m_eos.fpp, add a s_reference_curve table
row, and note that s_initialize_eos_module resolves constant-coefficient
families once at start-up.
Placing it last made "the first six are mechanical... the last two
are caloric" wrongly include s_reference_curve, which needs only rho
and a fluid index. Moving it to row 1 restores the six/two split and
matches "adding a second EOS means supplying these" - it's the one
operator a new family must actually add.
- Correct m_global_parameters_common.fpp ownership comment: the material
  property arrays are initialized by m_eos, not m_variables_conversion.
- Remove unused f_bulk_modulus import from m_hypoelastic.fpp.
- Move f_c2_from_coefficients to the private/family-layer row in the
  m_eos extraction design doc's tier table, matching the code and prose.
- Fix m_eos.fpp header overclaim: state direct dependencies (and build
  order) instead of falsely claiming no transitive MFC dependencies.
definitions.py stored the eos_* enum three times (names, labels, choices)
and hand-wrote three near-identical mg_*/jwl_*/vinet_* parameter blocks.
toolchain/mfc/params/eos_families.py is now the single registry those
sites derive from; it also carries each state-dependent family's
eos_coeffs field mapping (parameter, Fortran literal, or computed) for
later refactor steps to generate Fortran from.

test_eos_families.py asserts against values captured from the
unmodified tree, not the registry, to prove this is behavior-preserving.

Co-authored-by: Claude <noreply@anthropic.com>
…en_a bug

R2 of the EOS family registry collapse. The state-dependent families dict,
the optional-parameter dict, and the initial-state-inside-EOS dispatch in
case_validator.py now derive from EOS_FAMILIES instead of hard-coding the
mie_gruneisen/jwl/vinet trio in four places.

Rewiring the dispatch through the registry's coefficients_fn exposed a real
bug: the validator's Vinet path composed coefficients_from_curve(vinet_reference(...),
vinet_gruneisen) directly, silently dropping vinet_gruneisen_a, unlike
m_eos.fpp which applies a variable Gruneisen coefficient to every
state-dependent family. Added eos.vinet_coefficients (mirroring
eos_coefficients) so all three families now resolve to one direct
coefficients call.
…is live

Review of 712d1a4 found the added vinet_coefficients test called eos.py
directly and never exercised case_validator.py's dispatch -- the actual
bug site. The only existing validator-level Vinet test sets alpha_rho(1)
equal to vinet_rho0, so mu = 0 and gruneisen_a*mu is zero regardless of
gruneisen_a, making it blind to a dropped gruneisen_a in the lambda.

Added a state at mu = 0.2 with vinet_gruneisen_a = -20.0, chosen so the
variable-Gruneisen and constant-Gruneisen paths disagree on accept/reject,
not just on the computed coefficients. Verified (then reverted) that
temporarily dropping vinet_gruneisen_a from the lambda in
_check_initial_states_inside_eos makes this test fail before restoring it.
…writing them

fluid_pp(1)%eos gets a "fortran_prefix": "eos" constraint entry; generate_constants_fpp
now emits <fortran_prefix>_<name> for a compound key that carries one, instead of
skipping it (deduped across the 10 fluid_pp(f)%eos entries that share the prefix).
Deletes the hand-written eos_* block from m_constants.fpp, which already #:includes
the generated file.

test_eos_selector.py's test_fortran_and_python_enums_agree parsed m_constants.fpp
textually and would find nothing now that the constants live in the #:include'd
generated file; asserting the generator agrees with the registry it generated from
would also be circular. Replaced with two non-circular tests: the emitter produces
exactly the five expected eos_<suffix> = <value> lines, and every eos_* identifier
referenced under src/ is either generated or a known non-family symbol (eos_coeffs,
eos_coefficients, eos_rk4_steps).
…m EOS_FAMILIES

New generated_eos.fpp holds four Fypp macros derived from the registry: the two family
predicates (expression-valued, spliced with ${MACRO('i')}$) and the two layers of
s_initialize_eos_module's coefficient assignment (statement-valued, @:MACRO(i)). A Fypp
#:include is textual and adds no use, so m_eos stays a leaf module.

Which layer a field belongs to is derived, not hard-coded: a field more than one family
writes needs a case arm to pick its source, a field one family writes does not. That
yields case = {rho0, t0, gruneisen0, gruneisen_a} and unconditional = {c0, s, s2, s3,
mu_max, a, b, r1, r2, k0, k0p}, reproducing the hand-written split exactly. All three
eos_coeffs source kinds are handled: Param copies, JWL's FortranLiteral gruneisen_a, and
mu_max's Computed call, whose arguments are qualified as fluid_pp(i)%<param> and which
raises rather than guessing if it cannot resolve one.

f_has_isentropic_reference generates only the family half of its condition. The
gruneisen_a == 0._wp conjunct is a runtime value, not a family property, and stays
hand-written in m_eos.fpp; generating it away would change which fluids take the
closed-form isentrope.

The case default arm is not a family property either, so EOS_COEFF_DEFAULT /
EOS_COEFF_DEFAULTS were added to eos_families.py to say explicitly that gruneisen_a
defaults to a live 0._wp where the other dispatched fields default to dflt_real.

Verified by textual equivalence rather than by a test run: m_eos.fpp preprocessed
through Fypp before and after, with cmake's exact flags, leaves 547 statements on each
side and one difference -- case default moves from between case (eos_jwl) and
case (eos_vinet) to last, which Fortran treats identically. Both predicates expand
byte-identically. Details in the R4 report.

Made with Claude Code.
Seven tests in test_eos_families.py close the gap between the registry and the
parts of the codebase that stay hand-written by design: every registry
parameter has a physical_parameters field, every required parameter is read
by s_initialize_eos_module (accounting for generated_eos.fpp), every
coefficients_fn resolves in eos.py, every family has a s_reference_curve
case, Doxygen math symbols match a hand-verified expected map, and every
case-dispatched eos_coeffs field is assigned by every state-dependent family
(with EOS_COEFF_DEFAULTS keys checked against that same set). The last check
is duplicated as a generation-time assertion in generate_eos_fpp so a future
family that skips a case-dispatched field fails the build, not just CI.

Each test was verified to fail by breaking its target and observing the
failure before reverting.
Task R6 of the m_eos extraction. "How to Add an Equation of State" described
the pre-registry world implicitly (only the per-family operators, not how a
family's parameters get registered/validated). Add a short paragraph naming
eos_families.py as the entry point, what it generates (Fortran eos_*
constants, definitions.py registration, validator required/optional sets,
both family predicates, both s_initialize_eos_module layers), and what stays
hand-written and test-checked (s_reference_curve's case body, eos.py's
mirror function, physical_parameters' fields), plus the gruneisen_a runtime
caveat in f_has_isentropic_reference.

Made with Claude Code.
case.py held the seventeenth family list, `eos_state_dependent = {3, 4, 5}`,
feeding the any_state_dependent_eos case-optimization flag; a new family would
have left it false and aborted every case-optimized run with a "Rebuild"
message pointing at the wrong fix. It now derives from EOS_FAMILIES.

case_validator's initial-state check took only the function name from
coefficients_fn while the argument list stayed hand-written per family, ending
in an unlabelled `else` meaning Vinet. A new family fell into that `else` and
died with a TypeError. EosFamily gains an ordered `coefficients_args`; the call
is built generically, defaulting exactly the suffixes listed in `optional`. The
generated calls are argument-identical to the three they replace.

Widen the init-agreement test to optional parameters: the bug this registry
exists to prevent (Vinet ignoring vinet_gruneisen_a) was an optional one, so a
required-only sweep still missed it.

contributing.md now states the four guarded edits a new family needs and names
the two that remain, marking them as failing loudly at build.

Made with Claude Code.
Copilot AI lite review requested due to automatic review settings September 12, 2026 21:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Pull request overview

Refactors equation-of-state (EOS) plumbing by extracting EOS math into a new Fortran leaf module (m_eos) and centralizing EOS-family metadata in a single Python registry (EOS_FAMILIES) that drives code generation and validation.

Changes:

  • Extracts EOS computation/initialization into src/common/m_eos.fpp and updates Fortran call sites to initialize/finalize it.
  • Introduces toolchain/mfc/params/eos_families.py registry and extends codegen (fortran_gen.py) to emit EOS-related Fypp macros/constants from it.
  • Updates validator logic and adds tests to ensure registry ↔ generated ↔ hand-written EOS code remain consistent (including the Vinet variable-Gruneisen fix).

Reviewed changes

Copilot reviewed 38 out of 38 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
toolchain/mfc/test_eos_mie_gruneisen.py Adds regression coverage ensuring Vinet coefficients incorporate variable Grüneisen term.
toolchain/mfc/test_case_validator.py Adds validator regression test ensuring vinet_gruneisen_a participates in initial-state checks.
toolchain/mfc/params_tests/test_fortran_gen.py Updates generator expectations and adds coverage for generated EOS Fypp macros.
toolchain/mfc/params_tests/test_eos_selector.py Switches EOS constant checks to validate generator output and scans src/ for stray eos_* identifiers.
toolchain/mfc/params_tests/test_eos_families.py New test suite pinning observable behavior and cross-checking registry vs hand-written EOS touchpoints.
toolchain/mfc/params/generators/fortran_gen.py Adds fortran_prefix support for compound enum params and generates generated_eos.fpp macros/initializers.
toolchain/mfc/params/eos_families.py Adds the EOS_FAMILIES registry, coefficient-source modeling, and default rules.
toolchain/mfc/params/definitions.py Derives EOS enum names/labels/choices and parameter registration from EOS_FAMILIES; adds fortran_prefix constraint key.
toolchain/mfc/eos.py Adds vinet_coefficients mirroring Fortran’s variable-Grüneisen logic.
toolchain/mfc/case_validator.py Refactors state-dependent EOS dispatch based on EOS_FAMILIES (including initial-state coefficient calls).
toolchain/mfc/case.py Derives EOS_STATE_DEPENDENT_VALUES from registry for case-optimization flagging.
src/simulation/m_start_up.fpp Initializes/finalizes m_eos for simulation target.
src/simulation/m_riemann_solver_hypo_hlld.fpp Imports m_eos for EOS helpers now moved out of m_variables_conversion.
src/simulation/m_riemann_solver_hllc.fpp Imports m_eos for EOS helpers now moved out of m_variables_conversion.
src/simulation/m_riemann_solver_hll.fpp Imports m_eos for EOS helpers now moved out of m_variables_conversion.
src/simulation/m_rhs.fpp Imports m_eos for EOS helpers now moved out of m_variables_conversion.
src/simulation/m_reactive_burn.fpp Switches EOS calls (f_pressure, s_phase_temperature) to m_eos.
src/simulation/m_qbmm.fpp Imports m_eos for EOS helpers now moved out of m_variables_conversion.
src/simulation/m_pressure_relaxation.fpp Switches multiple EOS helpers to m_eos (pressure/coefficients/isentrope helpers).
src/simulation/m_igr.fpp Imports m_eos for EOS helpers now moved out of m_variables_conversion.
src/simulation/m_ibm.fpp Imports m_eos for EOS helpers now moved out of m_variables_conversion.
src/simulation/m_hypoelastic.fpp Switches bulk-modulus helper import to m_eos.
src/simulation/m_bubbles_EL.fpp Imports m_eos for EOS helpers now moved out of m_variables_conversion.
src/simulation/m_bubbles_EE.fpp Imports m_eos for EOS helpers now moved out of m_variables_conversion.
src/simulation/m_acoustic_src.fpp Imports m_eos for EOS helpers now moved out of m_variables_conversion.
src/pre_process/m_start_up.fpp Initializes/finalizes m_eos for pre_process target.
src/post_process/m_start_up.fpp Initializes/finalizes m_eos for post_process target.
src/post_process/m_derived_variables.fpp Imports m_eos for EOS helpers now moved out of m_variables_conversion.
src/post_process/m_data_output.fpp Imports m_eos for EOS helpers now moved out of m_variables_conversion.
src/common/m_variables_conversion.fpp Removes extracted EOS routines and delegates EOS-dependent operations to m_eos.
src/common/m_phase_change.fpp Imports m_eos for EOS helpers now moved out of m_variables_conversion.
src/common/m_global_parameters_common.fpp Updates docs comment to reflect EOS arrays now initialized in m_eos.
src/common/m_eos.fpp New leaf module containing EOS initialization, reference-curve math, and EOS thermo helpers.
src/common/m_constants.fpp Removes hand-written EOS selector constants and documents codegen source.
docs/module_categories.json Registers m_eos under Physics Models for documentation tooling.
docs/documentation/equations.md Updates key-source-file pointers to reflect EOS extraction into m_eos.
docs/documentation/contributing.md Updates “How to Add an EOS” guidance to use EOS family registry as entry point.
cmake/ParamsCodegen.cmake Adds generated_eos.fpp to enumerated generated include outputs (15 → 18).
Suppressed comments (4)

toolchain/mfc/params/generators/fortran_gen.py:1

  • The emitted_prefixes de-dupe will silently skip subsequent enum constraints that reuse the same fortran_prefix, even if their names mappings differ (only the first encountered gets emitted). Consider adding an explicit consistency check: if a prefix was already emitted, verify the incoming names dict is identical (or raise a ValueError) to prevent subtle omissions when additional fortran_prefix users are introduced.
    toolchain/mfc/params_tests/test_eos_selector.py:1
  • This regex only matches identifiers starting with lowercase eos_. Since Fortran is case-insensitive, EOS_VINET (or mixed-case) would be missed by the scan. Consider compiling with re.IGNORECASE and normalizing matches to lowercase before comparison so the test reliably catches all selector references regardless of casing.
    toolchain/mfc/params_tests/test_eos_selector.py:1
  • Filtering with if \"eos_\" in line is broader than needed and can become brittle if headers/comments ever mention eos_ (or if additional unrelated eos_* symbols are generated). Consider narrowing the filter to the exact declaration pattern being asserted (e.g., lines starting with integer, parameter :: eos_) so the test only inspects emitted constants.
    toolchain/mfc/params/generators/fortran_gen.py:1
  • If families is empty, this returns an empty string, which would produce an invalid macro body in generated_eos.fpp (blank expression). Consider returning a valid Fortran logical literal such as .false. when families is empty, so the generator remains robust if a category ever legitimately has zero families.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

args = []
for suffix in family.coefficients_args:
value = self.get(f"fluid_pp({i})%{family.prefix}_{suffix}")
args.append((value or 0.0) if suffix in optional else value)
@github-actions

Copy link
Copy Markdown

Claude Code Review

Head SHA: ab34a1a

Files changed:

  • 35
  • cmake/ParamsCodegen.cmake
  • docs/documentation/contributing.md
  • docs/documentation/equations.md
  • docs/module_categories.json
  • src/common/m_constants.fpp
  • src/common/m_eos.fpp
  • src/common/m_global_parameters_common.fpp
  • src/common/m_phase_change.fpp
  • src/common/m_variables_conversion.fpp
  • src/post_process/m_data_output.fpp

Findings:

  • src/common/m_eos.fpp:389,414: s_phase_pressure_on_isentrope and s_phase_temperature gained function_name=... and cray_inline=True on their GPU_ROUTINE directive during the move out of src/common/m_variables_conversion.fpp. The pre-move versions used plain $:GPU_ROUTINE(parallelism='[seq]') (no function_name, no cray_inline). Both routines internally call other subroutines (s_reference_curve, s_rk4), and the module's own comment on s_phase_c2 warns that a device routine calling another device subroutine is a pattern that is compiler-sensitive. This PR is otherwise presented and tested as a pure, behavior-preserving extraction (see the R1/R5 tests in test_eos_families.py), but this GPU-macro change is untested by that suite and could alter GPU inlining/offload codegen on Cray or other GPU-macro-sensitive compilers.

@github-actions

Copy link
Copy Markdown

Lines of Code

File Lines Diff
src/common/m_variables_conversion.fpp 1126 -339
src/common/m_eos.fpp 331 +331
src/common/m_constants.fpp 87 -5
src/post_process/m_start_up.fpp 789 +3
src/pre_process/m_start_up.fpp 484 +3
src/simulation/m_start_up.fpp 1261 +3
src/common/m_phase_change.fpp 285 +1
src/post_process/m_data_output.fpp 1188 +1
src/post_process/m_derived_variables.fpp 356 +1
src/simulation/m_acoustic_src.fpp 516 +1
src/simulation/m_bubbles_EE.fpp 304 +1
src/simulation/m_bubbles_EL.fpp 1632 +1
src/simulation/m_ibm.fpp 1254 +1
src/simulation/m_igr.fpp 2289 +1
src/simulation/m_qbmm.fpp 879 +1
src/simulation/m_reactive_burn.fpp 113 +1
src/simulation/m_rhs.fpp 1957 +1
src/simulation/m_riemann_solver_hll.fpp 616 +1
src/simulation/m_riemann_solver_hllc.fpp 1319 +1
src/simulation/m_riemann_solver_hypo_hlld.fpp 783 +1
Directory Lines Diff
common 10341 -12
pre_process 4516 +3
simulation 28100 +14
post_process 3393 +5
total 46350 +10

@codecov

codecov Bot commented Sep 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.10870% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 61.22%. Comparing base (dc0aec1) to head (acf936f).
⚠️ Report is 4 commits behind head on master.

Files with missing lines Patch % Lines
src/common/m_eos.fpp 94.94% 0 Missing and 9 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1870      +/-   ##
==========================================
- Coverage   61.26%   61.22%   -0.04%     
==========================================
  Files          84       85       +1     
  Lines       22330    22321       -9     
  Branches     3265     3266       +1     
==========================================
- Hits        13680    13666      -14     
- Misses       6207     6211       +4     
- Partials     2443     2444       +1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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.

2 participants