Skip to content

Fix missing qv term in the Lagrange bubble initial pressure - #1709

Open
sbryngelson wants to merge 2 commits into
masterfrom
fix/lagrange-bubble-qv
Open

Fix missing qv term in the Lagrange bubble initial pressure#1709
sbryngelson wants to merge 2 commits into
masterfrom
fix/lagrange-bubble-qv

Conversation

@sbryngelson

@sbryngelson sbryngelson commented Aug 8, 2026

Copy link
Copy Markdown
Member

Fixes #1706.

The defect

src/simulation/m_bubbles_EL.fpp open-codes the stiffened-gas pressure inversion and drops the qv (heat of formation) term:

call s_convert_to_mixture_variables(q_cons_vf, cell(1), cell(2), cell(3), rhol, gamma, pi_inf, qv, Re)
...
pliq = (q_cons_vf(eqn_idx%E)%sf(cell(1), cell(2), cell(3)) - dynP - pi_inf)/gamma

The canonical inversion, s_compute_pressure in m_variables_conversion.fpp:71, is

pres = (energy - dyn_p - pi_inf - qv)/gamma

The omission looks accidental rather than a deliberately different quantity: qv is an output argument of the s_convert_to_mixture_variables call on the line above, and it is never read anywhere else in that scope. It is computed and discarded.

Impact

pliq is too large by qv/gamma, and it seeds the initial bubble gas pressure:

gas_p(bub_id, 1) = pliq + 2._wp*(1._wp/Web)/bub_R0(bub_id)

So every Lagrangian bubble starts from the wrong internal pressure. This is physics, not diagnostic output.

Dormant when qv = 0, which is the default. It triggers for any case setting fluid_pp(i)%qv — the phase-change and reactive-burn configurations.

Why it survived

No test exercised the combination. Exactly one golden case sets bubbles_lagrange, and it leaves qv at zero; the cases that set qv are phase-change and reactive-burn, none of which use Lagrangian bubbles. The suite could not have caught this.

Test

Adds a 2D one-way-coupled Lagrange bubble case with fluid_pp(1)%qv = 0.01, restricted to a single configuration so it contributes one golden (F428FDC0).

The golden captures beta, the Lagrangian void fraction, alongside the conservative variables. beta follows the bubble radius, which is driven by the initial gas pressure, so the error is observable even under one-way coupling.

Verification

new case, fix reverted:   1 failed  (tolerance mismatch)
new case, fix applied:    1 passed
all bubble tests:        64 passed, 0 failed

Built with GNU 15.2.0, MPI, on macOS. The negative result is the one that matters — without it, a green suite would prove nothing here, since the pre-existing cases are all qv = 0.

No existing golden moved: the change is confined to a path only reachable with bubbles_lagrange, and no existing lag case has nonzero qv.

Behaviour change worth flagging

The correction shifts pliq by -qv/gamma, and gamma > 0 always, so the direction follows the sign of qv. For qv > 0 the initial bubble pressure drops, which brings two existing guards in the same routine within reach:

if (gas_mg(bub_id) <= 0._wp) call s_mpi_abort("The initial mass of gas inside the bubble is negative. ...")
if (pv*(massflag) > gas_p(bub_id, 1)) call s_mpi_abort("Lagrange bubble initially located in a region with pressure below the vapor pressure.")

A case with qv > 0 and Lagrangian bubbles that previously ran can now abort at startup. That is the correct outcome — it was running on an inflated bubble pressure that masked a setup the model rejects — but it converts a silently wrong answer into a hard stop, so it is a user-visible change rather than a pure numerical shift. For qv < 0 (as in the phase-change examples, which use qv = -1.167e6 for fluid 1) the pressure rises and the guards become less reachable.

gas_p also feeds the initial gas mass and the bubble natural frequency in the same routine, so both are corrected by the same change.

Adjacent issue found while reviewing

s_add_bubbles also assigns the module-level global pref from the last-added bubble's gas_p (line 383). pref is a documented user-settable case parameter that the four-equation EOS branch reads. This PR changes the value it gets overwritten with, but the overwrite itself is pre-existing and looks wrong independently. Filed as #1710 rather than addressed here.

Context

First of three PRs against #1708. The root cause is that the EOS algebra has several independent definitions, and s_compute_pressure is awkward enough to call — it demands a num_species array and an intent(inout) temperature — that call sites open-code the expression instead. This PR fixes only the bug; deduplication follows separately so that any behavior change lands in a PR whose purpose is to change behavior.


Handover notes

Context for anyone picking this up on another machine.

Branch and commits

repo    MFlowCode/MFC
branch  fix/lagrange-bubble-qv     (base: master)
  174775f  Fix missing qv term in the Lagrange bubble initial pressure
  9191c9a  Add golden for the qv_nonzero Lagrange bubble regression case

Two commits deliberately: the golden in the second is generated from a clean checkout of the first, so golden-metadata.txt records a real committed SHA rather than a dirty tree. Copilot flagged the dirty-tree provenance on the original single-commit version; that is why it is split. Preserve this if you amend — regenerate the golden only from a committed, clean tree.

Environment

./mfc.sh build -j 16          # builds pre_process, simulation, post_process
./mfc.sh test  -j 12          # full golden suite (~5 min on 18 cores)
./mfc.sh test --only F428FDC0 # this PR's case alone

Verified with GNU 15.2.0 (gfortran), MPI on, no GPU, macOS arm64. A first ./mfc.sh build bootstraps a Python venv and builds hdf5/silo; budget ~20 min.

Committing runs a pre-commit hook that executes the full CI lint-gate (7 checks). Use --no-verify only when you have already run it.

The exact defect

src/simulation/m_bubbles_EL.fpp, in s_add_bubbles:

call s_convert_to_mixture_variables(q_cons_vf, cell(1), cell(2), cell(3), rhol, gamma, pi_inf, qv, Re)
...
pliq = (q_cons_vf(eqn_idx%E)%sf(...) - dynP - pi_inf)/gamma      ! qv missing

Canonical inversion is s_compute_pressure in src/common/m_variables_conversion.fpp:
pres = (energy - dyn_p - pi_inf - qv)/gamma.

qv is an output argument of the call one line above and is read nowhere else in that scope — computed and discarded. That is the evidence the omission is accidental rather than a deliberately different quantity.

Why pliq must be the full thermodynamic pressure

Settled by internal consistency, not by the Maeda paper (which I did not read — it is paywalled JCP; docs/documentation/equations.md section 6.2 documents the model):

s_get_pinf in the same file — "Compute the bubble driving pressure p_inf", the Maeda & Colonius (2018) subgrid closure — interpolates q_prim_vf(eqn_idx%E)%sf. That slot is filled at m_variables_conversion.fpp by s_compute_pressure with qv_K passed. So at every step after t=0 the pressure driving the bubble includes qv. Initialising from an inversion that omits it left each bubble out of equilibrium with its own driving pressure by qv/gamma.

Only s_add_bubbles needs the change: the restart path (s_restart_bubbles) reads gas_p straight from the restart file rather than recomputing it.

Test case: why it is shaped the way it is

Added in toolchain/mfc/test/cases.py, inside alter_lag_bubbles, guarded to ndims == 2 and couplingMethod == 1 and adap_dt == "F" so it contributes exactly one golden (F428FDC0).

Two constraints that are easy to get wrong:

  1. The golden must capture beta. Under one-way coupling the bubbles do not feed back into the Eulerian field, so the conservative variables alone would not move. It works because beta, the Lagrangian void fraction, follows bubble radius and therefore the initial gas pressure. If you restructure the case, confirm beta is still in tests/F428FDC0/golden.txt.
  2. Do not add qv to the existing lag-bubble stack. That would churn every existing lag golden. A new label is what keeps it to one.

The case label feeds the golden UUID — crc32(sha1(str(trace))) at toolchain/mfc/test/case.py. Renaming the label changes the directory name. Avoid ! in labels (history expansion in interactive bash); qv_nonzero replaced an earlier qv!=0 for that reason.

How the fix was verified, and a trap

F428FDC0 with fix reverted:  1 failed (tolerance mismatch)
F428FDC0 with fix applied:   1 passed
all bubble tests:           64 passed, 0 failed
full suite:                627 passed, 0 failed

Trap: once the fix is committed, git stash push -- src/simulation/m_bubbles_EL.fpp has nothing to stash and silently leaves the fixed binary in place — the "reverted" run then reports a pass that looks identical to a real one. Revert with git checkout master -- src/simulation/m_bubbles_EL.fpp instead, rebuild, run, then git checkout HEAD -- <file> to restore.

This matters because no pre-existing golden covers the path: only one case sets bubbles_lagrange and it has qv = 0, while the qv /= 0 cases are phase-change and reactive-burn without Lagrangian bubbles. A green suite proves nothing here without the negative check.

Related work


Purpose in the series

A correctness fix, and the first concrete evidence for #1708.

The EOS algebra has nine hand-written copies of two expressions. Four are wrong. This is one of them, and the only one that affects physics rather than diagnostics — the others are output paths. It stands alone as a bug fix, but the reason it exists is structural: s_compute_pressure is awkward enough to call (it demands a num_species array and an intent(inout) temperature) that call sites open-code the one line instead, and then drift.

Fixing the instance without fixing the pressure is deliberate — the deduplication is tracked separately so any behaviour change lands in a PR whose stated purpose is to change behaviour.


Working conventions and hazards (shared across this series)

Collected from the work that produced #1705, #1709, #1712, #1713, #1714, #1716. Every one of these cost real time or produced a wrong result before being caught.

Testing

A regression test that cannot fail is worse than no test. Always verify the negative: revert the fix, rebuild, confirm the case fails, restore. Two ways this silently broke here:

  • git stash push -- <file> has nothing to stash once the fix is committed, so the "reverted" run tests the fixed binary and reports a pass identical to a real one. Use git checkout master -- <file>, rebuild, test, then git checkout HEAD -- <file>.
  • Picking the wrong case configuration. A probe case at model_eqns = 3 cannot detect a sound-speed defect, because the six-equation branch of s_compute_speed_of_sound touches neither H nor qv.

The golden packer discards data. toolchain/mfc/packer/pack.py treated every .dat under D/ as <x> [<y> <z>] <value> and kept only the last column of each row. Probe and integral output are multi-column time series, so most columns were never compared (#1711, fixed in #1712). Before asserting that a golden covers something, check it is actually in golden.txt.

Case labels are load-bearing. The golden UUID is crc32(sha1(str(trace))) — the label chain determines the directory name. Renaming a label renames the golden. Avoid ! in labels (history expansion in interactive bash).

Local suite runs are flaky at high -j. Non-reproducible failures appeared on several unrelated branches at -j 1216 (chemistry cases, probe cases) that passed individually and in clean reruns. Re-run before investigating.

Removing parameters or features

Deregistering a parameter breaks things that are not the source tree. Removing pref/rhoref from the registry broke the entire suite because BASE_CFG in toolchain/mfc/test/case.py set them for every case. Also check fp_stability.py, params_tests/mutation_tests.py, and lint fixtures that use real parameter names as examples.

Grep the generated artifacts, not just the sources. A stale TYPED_DECLS entry naming a deleted type survived removal and did not break the build only because the parameter had also left every target's namelist vars, so it was never emitted. Check generated_decls.fpp, generated_constants.fpp, SIM_GPU_DECL_VARS, and the MPI broadcast generators.

Dead-local tell: after removing a block, a local with exactly one remaining occurrence in its file is almost certainly its own declaration. Two occurrences often means declaration plus a private() entry.

Fortran is case-insensitive. A local pRef shadowed the module global pref in the hardcoded-IC files; the read site was spelled pref and looked like a reference to the global. It is not. Confirm scope before concluding a global is live.

GPU

A CPU test run cannot catch a missing private(). It is a silent device race. Audit by hand or by script when adding per-cell state.

Do not match GPU_PARALLEL_LOOP naivelyEND_GPU_PARALLEL_LOOP contains the same substring and will register as a loop start, producing false positives. Exclude it explicitly.

Derived-type components cannot have runtime extents. dimension(num_fluids) in a type fails to compile outside case-optimized builds, where num_fluids is a parameter.

Benchmarking

Run ./mfc.sh bench in the foreground on an idle machine. Running it in the background while a pre-commit precheck ran at -j 12 produced a bogus +50% regression on a case that executes none of the changed code. Baseline noise here is ±3.5%; run the baseline twice before trusting any delta, and sanity-check that the regressing cases actually execute the modified code.

GitHub mechanics

  • --force-with-lease needs an explicit SHA (--force-with-lease=<branch>:<sha>) when the ref has not been fetched in the current clone; the bare form fails with "stale info".
  • gh run view --log-failed can miss the real output entirely. On the Frontier jobs the failing step carried only a non-zero exit while the actual test output lived in a separate Print Logs step that succeeded. Fetch the full log.
  • gh run rerun <id> --failed refuses while the workflow is still running; retry later.
  • Frontier jobs fail spuriously with some regularity — uniform ~15-minute deaths across heterogeneous jobs, and shared-Lustre install races (file INSTALL cannot set modification time ... No such file or directory, exit 143). Check for a real error before assuming a code fault.

Copilot AI lite review requested due to automatic review settings August 8, 2026 21:11

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.

Pull request overview

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.

Fixes incorrect initial Lagrangian bubble pressure when a nonzero heat-of-formation term (qv) is configured, and adds a targeted regression test/golden to prevent recurrence.

Changes:

  • Include the missing qv term in the stiffened-gas pressure inversion used to seed Lagrangian bubble pressure.
  • Add a new lag-bubble test configuration with fluid_pp(1)%qv = 0.01 to exercise the previously untested path.
  • Add a new golden (43EA05B4) to validate the behavior.

Reviewed changes

Copilot reviewed 3 out of 4 changed files in this pull request and generated 2 comments.

File Description
src/simulation/m_bubbles_EL.fpp Fixes pressure inversion by subtracting qv in the bubble initialization path.
toolchain/mfc/test/cases.py Adds a constrained regression case that sets qv nonzero for lag bubbles.
tests/43EA05B4/golden-metadata.txt Adds metadata for the new golden tied to the new regression case.

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

Comment thread toolchain/mfc/test/cases.py Outdated
Comment thread tests/43EA05B4/golden-metadata.txt Outdated
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown

Claude Code Review

Head SHA: b3431b4

Files changed:

  • 2
  • src/simulation/m_bubbles_EL.fpp
  • toolchain/mfc/test/cases.py

Findings:

  • src/simulation/m_bubbles_EL.fpp:373: The new qv term is subtracted directly from q_cons_vf(eqn_idx%E)%sf(...), dynP, and pi_inf, all of which are energy-density quantities (energy/volume), then the whole thing is divided by gamma to get a pressure. qv (heat of formation) is a specific energy (energy/mass), so it is dimensionally inconsistent to subtract it directly — the standard stiffened-gas-with-formation-energy inversion requires the density-weighted term rhol*qv, not bare qv (i.e. pliq = (E - dynP - pi_inf - rhol*qv)/gamma). As written, this only happens to be correct if rhol is exactly 1, and will be numerically wrong for any real liquid density — silently producing an incorrect pliq for phase-change fluids with qv /= 0, which is precisely the regime the accompanying qv!=0 regression test (toolchain/mfc/test/cases.py) is meant to exercise.

s_add_bubbles open-coded the stiffened-gas pressure inversion and omitted the
qv (heat of formation) term:

    pliq = (E - dynP - pi_inf)/gamma

The canonical inversion in s_compute_pressure (m_variables_conversion.fpp:71)
is (energy - dyn_p - pi_inf - qv)/gamma. The omission is clearly accidental:
qv is an output argument of the s_convert_to_mixture_variables call one line
above and is never read anywhere else in that scope.

pliq seeds gas_p(bub_id, 1), so every Lagrangian bubble started from a wrong
internal pressure. This is physics, not diagnostics. It also propagates to the
initial gas mass and the bubble natural frequency, which are both derived from
gas_p in the same routine.

Consistency with the running solver settles which quantity pliq should be:
s_get_pinf, the Maeda and Colonius (2018) subgrid closure that supplies the
bubble driving pressure at every later step, interpolates q_prim_vf(eqn_idx%E),
and that field is filled by s_compute_pressure with qv included. Initializing
from an inversion that omits qv left each bubble out of equilibrium with its
own driving pressure by qv/gamma.

Only s_add_bubbles needs the change; the restart path reads gas_p straight from
the restart file rather than recomputing it.

Dormant when qv = 0, the default, and every existing lag-bubble test leaves it
there, which is why this survived.

Adds a 2D one-way-coupled case with qv /= 0. The golden lands in a follow-up
commit so that it is generated from a clean tree.

Fixes #1706
Generated from a clean checkout of the preceding commit so the recorded
provenance corresponds exactly to committed sources.

The golden captures beta, the Lagrangian void fraction, alongside the
conservative variables. beta follows the bubble radius, which is driven by the
initial gas pressure, so the defect is observable even under one-way coupling
where the bubbles do not feed back into the Eulerian field.
@sbryngelson

Copy link
Copy Markdown
Member Author

Both Copilot points were valid and are addressed in the force-push (b3431b49191c9a). Details, since I checked them rather than just applying them.

Label qv!=0. Correct, and worse than cosmetic. The case UUID is crc32(sha1(str(trace))) (toolchain/mfc/test/case.py:149), so the label is not merely display text — it determines the golden directory name. Separately, ! is history expansion in interactive bash, so ./mfc.sh test --only "...qv!=0..." misbehaves when pasted into a shell. Checking the conventions: = in labels has ample precedent (riemann_solver=1, weno_order=5, adap_dt=T), but qv!=0 was the only label in the entire file containing !. Renamed to qv_nonzero, which changed the UUID 43EA05B4F428FDC0; the old golden directory is removed and the new one added.

Dirty working tree. Also correct, and the recorded SHA 0c9a1d43 was not even the commit that ended up in the PR. Restructured into two commits so the provenance is real: the source fix and test case land first, then the golden is generated against that clean checkout, then committed. The metadata now reads

Git:  174775fb74510109ef017ea97c98fc9cee1eb5ca on fix/lagrange-bubble-qv (clean)

which is the parent commit in this PR.

One thing the restructure caught. After committing the fix, my original verification method silently stopped working — git stash push -- src/simulation/m_bubbles_EL.fpp had nothing to stash, so the "fix reverted" run was actually testing the fixed binary and reported a pass. Redone with git checkout master -- <file>, which reverts the source properly:

F428FDC0, fix reverted:   1 failed  (tolerance mismatch)
F428FDC0, fix applied:    1 passed
all bubble tests:        64 passed, 0 failed

Worth stating explicitly because a regression test that cannot fail is worse than no test, and the false pass looked identical to a real one.

Also updated the description with two things I had missed on the first pass: the fix can newly trigger the vapour-pressure and negative-gas-mass aborts for qv > 0 cases that previously ran, and s_add_bubbles overwrites the user-supplied global pref from the last bubble's pressure — pre-existing, now filed as #1710.

@codecov

codecov Bot commented Aug 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 60.77%. Comparing base (8dfe8c7) to head (9191c9a).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #1709   +/-   ##
=======================================
  Coverage   60.77%   60.77%           
=======================================
  Files          83       83           
  Lines       20872    20872           
  Branches     3101     3101           
=======================================
  Hits        12685    12685           
  Misses       6121     6121           
  Partials     2066     2066           

☔ 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.

bug: Lagrange bubble initial gas pressure omits the qv term from the stiffened-gas inversion

2 participants