Add heterogeneous reacting surface boundary conditions - #1821
Conversation
4a71f17 to
5b4999a
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #1821 +/- ##
==========================================
- Coverage 61.26% 61.03% -0.23%
==========================================
Files 84 84
Lines 22330 22529 +199
Branches 3265 3286 +21
==========================================
+ Hits 13680 13751 +71
- Misses 6207 6307 +100
- Partials 2443 2471 +28 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Conflict in m_ibm.fpp: master added the alpha_q, alpha_rho_q and e_q locals for per-phase EOS evaluation, this branch added W_species and the surface-reaction locals. Both sets are kept, and both appear in the kernel's private clause - a scalar assigned in the loop but absent from that list races under OpenMP offload.
get_slug hashed the phase name, which is conventionally 'gas', so two cases with different mechanisms shared one build and the second ran against the first's species set. This branch is the first to carry two gas mechanisms: the 3D reacting mixing layer's sandiego.yaml has nine species and the carbon surface case's reduced GRI mechanism has eleven, so whichever built first decided sys_size for both, and the mixing layer wrote 34 output files where its golden has 30. Reproduced by running both cases together, which is also why each passes alone. The build already reports the mechanism by source when it prints Chemistry:; this makes the key agree with what it prints.
…ombined with W_species and Ys_s were declared dimension(num_species) outside the USING_AMD guard, while Ys_IP and Ys_g inside it carry the padded literal. Ys_g(:) = 2*Ys_s(:) - Ys_IP(:) is then a shape mismatch in any generic amdflang build, at any species count: with the literal at ten and a nine-species mechanism it is ten against nine and the compile fails. Both arrays now follow the guard, and the four whole-array assignments are pinned to 1:num_species so they do not depend on the padding happening to match. Separate from the ten-species ceiling itself, which this does not lift -- an eleven-species mechanism still needs case optimization on AMD, or MFlowCode#1848.
Lines of Code
|
|
requires me to merge #1852 before this works on amd compilers will make 'ready to review' when that merges. work on this pr can continue if needed (or ideally a follow up pr later) |
|
Review of Worth saying first, because it shapes how to read the rest: the parts most likely to be silently wrong are right. Species indexing agrees end to end - The findings below are about the guards around that, not the physics. 1.
|
| species | k (m/s) | Da | Ys_s/Ys_IP | Ys_g/Ys_IP |
|---|---|---|---|---|
| OH | 57.2 | 48 | 0.020 | -0.959 |
| O | 118.1 | 97 | 0.010 | -0.980 |
| O2 | 0.032 | 0.041 | 0.961 | +0.921 |
R1 and R2 have Ea = 0 and k ~ sqrt(T), so the radical channels are diffusion-limited at any surface temperature - this is not a high-temperature corner. sum(Ys_g) is still exactly 1 because the products compensate, so no global check catches it. The alpha-QSS clip at m_chemistry.fpp:271,288 hides the symptom, and only when reaction_substeps > 0.
Fix: clamp the mirror, or drop to first order where the mirror would leave the physical range. The second keeps the surface value exact:
if (2._wp*Ys_s(q) - Ys_IP(q) < 0._wp) then
Ys_g(q) = Ys_s(q) ! first-order ghost; the mirror would be unphysical
else
Ys_g(q) = 2._wp*Ys_s(q) - Ys_IP(q)
end ifRenormalising afterwards is not enough on its own - the sum is already 1.
2. T_g = 2*T_s - T_IP has the same problem, and it bites thermal_bc = 1 too
src/simulation/m_ibm.fpp:296 (inert) and :317 (reacting). T_g feeds alpha_rho_IP(1) = alpha_IP(1)*pres_IP*mw_g/(gas_constant*T_g) and get_mixture_energy_mass(T_g, ...). A 1200 K wall under a 2500 K flame - the carbon-combustion case - gives T_g = -100 K: negative ghost density, and NASA polynomials evaluated at negative T (the a6/T term). The Newton solve clamps T_s internally to [200, 5000]; nothing clamps the extrapolation. Same one-line treatment as above, with a floor rather than zero.
3. thermal_bc and Twall are silently ignored without chemistry
The whole block at m_ibm.fpp:288 sits inside if (chemistry .and. patch_ib(patch_id)%inj_species == 0), but m_checker.fpp:104 was loosened from if (ib .and. chemistry) to if (ib), and docs/documentation/case.md:366,409 documents them as general IB thermal boundary conditions. So an isothermal cylinder in a plain Navier-Stokes IB run validates, documents as working, and is silently adiabatic. thermal_bc is read nowhere else in src/ - I grepped. Either gate the parameters on chemistry or lift the thermal branch out of the conditional; the docs should match whichever you pick.
4. The generator mis-emits sticking and reversible surface reactions
toolchain/mfc/run/input.py:236-247. append_reaction_rate guards only on hasattr(rate, "pre_exponential_factor"). Checked against the Cantera 3.1.0 in the project venv: ct.StickingArrheniusRate(0.1, 0, 0).pre_exponential_factor returns 0.1, so a sticking reaction passes the guard and its dimensionless sticking probability is emitted as an Arrhenius A - wrong by 1e4 to 1e12. Separately reaction.reversible is never consulted, so a mechanism written with <=> (Cantera's default for interface reactions) silently loses its reverse branch, and rate.coverage_dependencies is dropped.
Your own mechanism uses irreversible => with explicit orders and no sticking, so none of this affects your results - it is a trap for the next user. The generator already raises cleanly for surface-site species and unsupported thermo; three more raises in the same style would close it.
5. Two documented device-routine portability traps
s_surface_species_residual (m_ibm.fpp:1705) and s_surface_energy_residual (:1740) are GPU_ROUTINE(parallelism='[seq]') and call get_species_mass_diffusivities_mixavg / get_mixture_thermal_conductivity_mixavg / transitively get_species_enthalpies_rt from inside. .claude/rules/common-pitfalls.md records this exactly: calling get_species_* from inside a GPU_ROUTINE rather than from the kernel gave CCE OpenMP a runtime Memory access fault by GPU node-N on the first step, while every other backend ran. The build stays clean and only a case that reaches the path shows it, so NVIDIA and CPU lanes passing is not evidence. Every existing call site (m_chemistry.fpp:409-416) evaluates these in the GPU_PARALLEL_LOOP body and passes the arrays down - same restructuring here.
(I checked and discarded the related ftn-7066 concern: num_species is an integer, parameter in the pyrometheus output, so those bounds are constants, not device globals.)
6. Newton non-convergence is a silent BC switch, and it will make the golden flaky
m_ibm.fpp:1875,1898 return converged = .false., consumed at :321 and :454, and the ghost point silently reverts to an inert zero-flux surface - no counter, no warning, no output field. Beyond the diagnostic problem, converged is a discontinuous branch driven by a strict norm_trial < norm_R with no Armijo slack, so a one-ulp difference between compilers flips a ghost point between two O(1)-different boundary conditions. tests/F52F0D4C is an Example-tolerance golden on top of GRI-11 kinetics; we have already had to skip 1D_propellant_flame, 2D_hybrid_slab and 1D_flamelet for subtler drift than this. Expect it to be flaky across lanes.
Related: the Example test is not step-capped. cases.py:3177 caps t_step_stop only if "t_step_stop" in case, and this example uses cfl_adap_dt with t_stop/t_save, so the golden is the full run - ~1e3 steps of GRI-11 with a 30-iteration Newton (12 residual evaluations each, each O(Ns^2)) per ghost point per RK stage. Several adaptive-dt examples are already in casesToSkip for this. A small step-capped dedicated test would serve better than the full-run Example golden.
7. Smaller
- No Python-side validation:
case_validator.pyis untouched, so./mfc.sh validateaccepts a badthermal_bcand a real run does a full pre_process before aborting. The IB block at lines 906-944 already validatesairfoil_id/model_id/burn_rate_exp; these belong there withPHYSICS_DOCSentries. - Build slug:
build.py:310correctly keys the gas mechanism on.source- the identical argument applies tosurface_cantera_file/surface_phase, which are not hashed at all, so two cases differing only in surface mechanism share a binary. input.py:118-122has a broadexcept Exception: if the local surface file exists but fails to parse, the error prints dim and a same-named file inMFC_MECHANISMS_DIRloads instead - silently a different mechanism.d = abs(gp%levelset)has no floor and1/dis in both residuals. The NaN is contained (every line-search comparison is false for NaN, so it exhausts and returnsconverged = .false.) but that lands in finding 6.max(d, small)is clearer than relying on NaN comparison semantics.dx = fd_eps_Y*max(abs(Ys_s(j)), 1._wp)(:1841) - mass fractions are <= 1 so themaxis always 1 and the scaling is dead.minwas probably intended.- The pivot test
pivot_value <= epsilon(1._wp)(:1935) is absolute on a matrix with O(1e7) entries; it will never fire. A relative test against the column norm would. - Convention: the scalars use second-order mirroring while the Stefan velocity is added once to
vel_g(:426), matching the first-orderv_blowconvention. If reconstruction sees the wall-normal velocity as ~v_stefan/2, the convected mass flux is half what the species BC assumed. May well be deliberate, but the two halves of one surface condition using different ghost conventions deserves a comment. get_mixture_molecular_weight(Ys_IP, ...)andXs_IPare recomputed inside every residual evaluation thoughYs_IPnever changes; the transport coefficients could be frozen at the outer Newton level too.
Suggested order
1 and 2 are the ones that produce wrong answers rather than crashes, and both are a few lines. 3 is a one-line gate. 4 is three raises matching the ones already there. 5 is the restructuring m_cbc/m_ibm already use. 6 and 7 argue together for a cheap step-capped test instead of the full-run Example golden.
Separately: your Frontier AMD CPU build failures are not yours to fix - they are the 11-species mechanism against the dimension(10) literal under the USING_AMD guard, which #1852 raises. I cherry-picked #1852 onto 94ac09d locally and the tree builds clean on MI210 with F52F0D4C passing, so that lane clears when #1852 lands. The gpu-omp [2/2] lane was a bad node (syscheck failed three times on frontier10212).
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:
If these expectations are not met, we would prefer to implement the changes ourselves rather than spend time reviewing low-effort submissions.
Acknowledgement
PR template credit: junegunn
Summary
This PR adds heterogeneous reacting surface boundary conditions for immersed
boundaries. The implementation was developed for reacting carbon-particle
simulations in which heterogeneous surface chemistry is coupled to the
compressible gas-phase species equations.
The surface treatment supports:
thermal_bc = 0)thermal_bc = 1)thermal_bc = 2)surface_cantera_fileandsurface_phaseFor a reacting surface, the species boundary condition balances diffusive
transport, Stefan mass flux, and heterogeneous surface production. One species
equation is replaced by the mass-fraction closure. When
thermal_bc = 2, thesurface temperature is included as an additional Newton unknown and the
conductive and heterogeneous reaction heat fluxes are balanced.
Motivation
The immediate application is heterogeneous oxidation/gasification of carbon
particles using an immersed-boundary representation. The implementation is
intended to remain general with respect to the number of gas species and the
Cantera surface mechanism rather than hard-coding a particular carbon
mechanism.
Testing
The implementation has been tested using an 11-species reduced GRI-based gas-phase
mechanism together with a compatible heterogeneous carbon surface mechanism, including:
Both prescribed-temperature and coupled-energy carbon cases run successfully
and produce the expected surface reaction products, oxygen consumption,
thermal field, and reacting wake.
The branch was rebased from current MFC master before the surface changes were
introduced, and ./mfc.sh precheck and the simulation build pass.