diff --git a/src/post_process/m_mpi_proxy.fpp b/src/post_process/m_mpi_proxy.fpp index d9beac5347..7649747b72 100644 --- a/src/post_process/m_mpi_proxy.fpp +++ b/src/post_process/m_mpi_proxy.fpp @@ -85,6 +85,14 @@ contains call MPI_BCAST(${VAR}$, 1, MPI_LOGICAL, 0, MPI_COMM_WORLD, ierr) #:endfor + ! wall-velocity members consumed by s_slip_wall/s_no_slip_wall on all ranks + #:for DIM in ['x', 'y', 'z'] + #:for DIR in [1, 2, 3] + call MPI_BCAST(bc_${DIM}$%vb${DIR}$, 1, mpi_p, 0, MPI_COMM_WORLD, ierr) + call MPI_BCAST(bc_${DIM}$%ve${DIR}$, 1, mpi_p, 0, MPI_COMM_WORLD, ierr) + #:endfor + #:endfor + ! manual: cfl_dt (runtime-computed logical), bc_io (BC-file existence) call MPI_BCAST(cfl_dt, 1, MPI_LOGICAL, 0, MPI_COMM_WORLD, ierr) call MPI_BCAST(bc_io, 1, MPI_LOGICAL, 0, MPI_COMM_WORLD, ierr) diff --git a/src/pre_process/m_mpi_proxy.fpp b/src/pre_process/m_mpi_proxy.fpp index bf6e3d1180..8e733765f3 100644 --- a/src/pre_process/m_mpi_proxy.fpp +++ b/src/pre_process/m_mpi_proxy.fpp @@ -39,16 +39,28 @@ contains call MPI_BCAST(${VAR}$, 1, MPI_LOGICAL, 0, MPI_COMM_WORLD, ierr) #:endfor - ! manual: bc_x/y/z and domain bounds (registered as struct members, not in NAMELIST_VARS) + ! manual: domain bounds and wall temperatures (REAL struct members, not in NAMELIST_VARS) #:for VAR in [ 'x_domain%beg', 'x_domain%end', 'y_domain%beg', & & 'y_domain%end', 'z_domain%beg', 'z_domain%end', & - & 'bc_x%beg', 'bc_x%end', 'bc_y%beg', 'bc_y%end', & - & 'bc_z%beg', 'bc_z%end', 'bc_x%Twall_in', 'bc_x%Twall_out', & + & 'bc_x%Twall_in', 'bc_x%Twall_out', & & 'bc_y%Twall_in', 'bc_y%Twall_out', 'bc_z%Twall_in', & & 'bc_z%Twall_out'] call MPI_BCAST(${VAR}$, 1, mpi_p, 0, MPI_COMM_WORLD, ierr) #:endfor + ! manual: BC type codes (INTEGER struct members) + #:for VAR in [ 'bc_x%beg', 'bc_x%end', 'bc_y%beg', 'bc_y%end', 'bc_z%beg', 'bc_z%end'] + call MPI_BCAST(${VAR}$, 1, MPI_INTEGER, 0, MPI_COMM_WORLD, ierr) + #:endfor + + ! wall-velocity members consumed by s_slip_wall/s_no_slip_wall on all ranks + #:for DIM in ['x', 'y', 'z'] + #:for DIR in [1, 2, 3] + call MPI_BCAST(bc_${DIM}$%vb${DIR}$, 1, mpi_p, 0, MPI_COMM_WORLD, ierr) + call MPI_BCAST(bc_${DIM}$%ve${DIR}$, 1, mpi_p, 0, MPI_COMM_WORLD, ierr) + #:endfor + #:endfor + ! manual: cfl_dt (runtime-computed logical), bc_io (BC-file existence) call MPI_BCAST(cfl_dt, 1, MPI_LOGICAL, 0, MPI_COMM_WORLD, ierr) call MPI_BCAST(bc_io, 1, MPI_LOGICAL, 0, MPI_COMM_WORLD, ierr) diff --git a/toolchain/mfc/params/definitions.py b/toolchain/mfc/params/definitions.py index 1ba1ed965d..3edb09e758 100644 --- a/toolchain/mfc/params/definitions.py +++ b/toolchain/mfc/params/definitions.py @@ -842,14 +842,13 @@ def _load(): _r(f"{px}sph_har_coeff({ll},{mm})", REAL) # fluid_pp (10 fluids) + # Members present in physical_parameters: gamma, pi_inf, Re, cv, qv, qvp, G. + # mul0/ss/pv/gamma_v/M_v/mu_v/k_v/cp_v/D_v were removed from the Fortran type + # by upstream #1085/#1093 — they must NOT be registered (namelist read would crash). for f in range(1, NF + 1): px = f"fluid_pp({f})%" for a, sym in [("gamma", r"\f$\gamma_k\f$"), ("pi_inf", r"\f$\pi_{\infty,k}\f$"), ("cv", r"\f$c_{v,k}\f$"), ("qv", r"\f$q_{v,k}\f$"), ("qvp", r"\f$q'_{v,k}\f$")]: _r(f"{px}{a}", REAL, math=sym) - _r(f"{px}mul0", REAL, {"viscosity"}, math=r"\f$\mu_{l,k}\f$") - _r(f"{px}ss", REAL, {"surface_tension"}, math=r"\f$\sigma_k\f$") - for a in ["pv", "gamma_v", "M_v", "mu_v", "k_v", "cp_v", "D_v"]: - _r(f"{px}{a}", REAL, {"bubbles"}) _r(f"{px}G", REAL, {"elasticity"}, math=r"\f$G_k\f$") _r(f"{px}Re(1)", REAL, {"viscosity"}, math=r"\f$\mathrm{Re}_k\f$ (shear)") _r(f"{px}Re(2)", REAL, {"viscosity"}, math=r"\f$\mathrm{Re}_k\f$ (bulk)") @@ -1049,11 +1048,16 @@ def _load(): _r(f"simplex_params%perturb_vel_offset({d},{j})", REAL) # lag_params (Lagrangian bubbles) + # Members present in bubbles_lagrange_parameters: solver_approach, cluster_type, + # pressure_corrector, smooth_type, heatTransfer_model, massTransfer_model, + # write_bubbles, write_bubbles_stats, nBubs_glb, epsilonb, charwidth, valmaxvoid. + # T0/Thost/c0/rho0/x0 were removed from the Fortran type by upstream #1085/#1093 + # — they must NOT be registered (namelist read would crash). for a in ["heatTransfer_model", "massTransfer_model", "pressure_corrector", "write_bubbles", "write_bubbles_stats"]: _r(f"lag_params%{a}", LOG, {"bubbles"}) for a in ["solver_approach", "cluster_type", "smooth_type", "nBubs_glb"]: _r(f"lag_params%{a}", INT, {"bubbles"}) - for a in ["epsilonb", "valmaxvoid", "charwidth", "c0", "rho0", "T0", "x0", "Thost"]: + for a in ["epsilonb", "valmaxvoid", "charwidth"]: _r(f"lag_params%{a}", REAL, {"bubbles"}) # chem_params diff --git a/toolchain/mfc/params/descriptions.py b/toolchain/mfc/params/descriptions.py index 016fd8cde8..79cc4089f4 100644 --- a/toolchain/mfc/params/descriptions.py +++ b/toolchain/mfc/params/descriptions.py @@ -323,15 +323,6 @@ (r"fluid_pp\((\d+)\)%qv", "Heat of formation for fluid {0}"), (r"fluid_pp\((\d+)\)%qvp", "Heat of formation prime for fluid {0}"), (r"fluid_pp\((\d+)\)%Re\((\d+)\)", "Reynolds number component {1} for fluid {0}"), - (r"fluid_pp\((\d+)\)%mul0", "Reference liquid viscosity for fluid {0}"), - (r"fluid_pp\((\d+)\)%ss", "Surface tension for fluid {0}"), - (r"fluid_pp\((\d+)\)%pv", "Vapor pressure for fluid {0}"), - (r"fluid_pp\((\d+)\)%gamma_v", "Specific heat ratio of vapor phase for fluid {0}"), - (r"fluid_pp\((\d+)\)%M_v", "Molecular weight of vapor phase for fluid {0}"), - (r"fluid_pp\((\d+)\)%mu_v", "Viscosity of vapor phase for fluid {0}"), - (r"fluid_pp\((\d+)\)%k_v", "Thermal conductivity of vapor phase for fluid {0}"), - (r"fluid_pp\((\d+)\)%cp_v", "Specific heat capacity (const. pressure) of vapor for fluid {0}"), - (r"fluid_pp\((\d+)\)%D_v", "Vapor mass diffusivity for fluid {0}"), (r"fluid_pp\((\d+)\)%non_newtonian", "Enable Herschel-Bulkley non-Newtonian viscosity for fluid {0}"), (r"fluid_pp\((\d+)\)%K", "HB consistency index for fluid {0}"), (r"fluid_pp\((\d+)\)%nn", "HB flow behavior index for fluid {0}"), @@ -500,11 +491,6 @@ (r"lag_params%epsilonb", "Standard deviation scaling for Gaussian smoothing"), (r"lag_params%charwidth", "Domain virtual depth for 2D simulations"), (r"lag_params%valmaxvoid", "Maximum permitted void fraction"), - (r"lag_params%T0", "Initial bubble temperature"), - (r"lag_params%Thost", "Host fluid temperature"), - (r"lag_params%c0", "Initial sound speed"), - (r"lag_params%rho0", "Initial density"), - (r"lag_params%x0", "Initial bubble position"), (r"lag_params%(\w+)", "Lagrangian tracking parameter: {0}"), # chem_params patterns - specific fields first (r"chem_params%diffusion", "Enable species diffusion for chemistry"), diff --git a/toolchain/mfc/params/generators/fortran_gen.py b/toolchain/mfc/params/generators/fortran_gen.py index d1bd45477e..14e0f472a6 100644 --- a/toolchain/mfc/params/generators/fortran_gen.py +++ b/toolchain/mfc/params/generators/fortran_gen.py @@ -235,7 +235,11 @@ def generate_case_opt_decls_fpp() -> str: _STRUCT_ROOTS = frozenset({"bc_x", "bc_y", "bc_z", "x_domain", "y_domain", "z_domain", "x_output", "y_output", "z_output"}) # Variables excluded from broadcast generation (derived post-broadcast or non-namelist). -_BCAST_EXCLUDE = frozenset({"muscl_eps"}) +# muscl_eps was previously excluded here on the assumption that it was derived +# post-broadcast, but the derivation only fires under f_is_default(muscl_eps), +# and default values are assigned on rank 0 only. Every multi-rank MUSCL run +# therefore had rank-divergent muscl_eps. Removed from exclusion to fix the bug. +_BCAST_EXCLUDE: frozenset = frozenset() # Post-process scalars that are namelist-bound but consumed on rank 0 only (reading/init). # Broadcasting them would be harmless but changes the existing call set, which we preserve. @@ -318,15 +322,19 @@ def _emit_bcast_group(lines: List[str], vars_list: List[str], mpi_type: str) -> def _emit_fluid_pp(lines: List[str], target: str) -> None: """Emit the fluid_pp(i) member-loop broadcast block. - Members broadcast: the 13 REAL members (EOS core plus the Herschel-Bulkley - non-Newtonian set) and the LOGICAL non_newtonian flag, matching the manual - lists this generator replaces. Sim additionally: Re(1) with count=2. + Walks the registry for every fluid_pp member, emitting the MPI datatype that + matches each member's registered ParamType (the Herschel-Bulkley merge added + a LOGICAL member, so the datatype must come from the registry, not be assumed + REAL). Sim additionally broadcasts Re(1) with count=2 (kept sim-only to + preserve the historical call set; pre/post never consumed Re). + mul0/ss/pv/gamma_v/M_v/mu_v/k_v/cp_v/D_v were removed from the Fortran type by + upstream #1085/#1093 and are no longer registered. """ - fp_real_members = ["gamma", "pi_inf", "G", "cv", "qv", "qvp", "K", "nn", "tau0", "hb_m", "mu_min", "mu_max", "mu_bulk"] + fp_members = sorted(k.split("%", 1)[1] for k in REGISTRY.all_params if k.startswith("fluid_pp(1)%") and not k.startswith("fluid_pp(1)%Re(")) lines.append(" do i = 1, num_fluids_max") - for mem in fp_real_members: - lines.append(f" call MPI_BCAST(fluid_pp(i)%{mem}, 1, mpi_p, 0, MPI_COMM_WORLD, ierr)") - lines.append(" call MPI_BCAST(fluid_pp(i)%non_newtonian, 1, MPI_LOGICAL, 0, MPI_COMM_WORLD, ierr)") + for mem in fp_members: + ptype = REGISTRY.all_params[f"fluid_pp(1)%{mem}"].param_type + lines.append(f" call MPI_BCAST(fluid_pp(i)%{mem}, 1, {_mpi_type_for(ptype)}, 0, MPI_COMM_WORLD, ierr)") if target == "sim": lines.append(" call MPI_BCAST(fluid_pp(i)%Re(1), 2, mpi_p, 0, MPI_COMM_WORLD, ierr)") lines.append(" end do") @@ -347,14 +355,13 @@ def _emit_bub_pp(lines: List[str]) -> None: def _emit_lag_params(lines: List[str]) -> None: """Emit the lag_params member broadcast block (sim-only, under bubbles_lagrange guard). - Subset of lag_params members that are actually broadcast: the fields that appear in the - existing simulation m_mpi_proxy.fpp lag_params block. The registry has additional - members (T0, Thost, c0, rho0, x0) that are not broadcast (rank-0-only). + All registered lag_params members are broadcast. T0/Thost/c0/rho0/x0 were removed + from the Fortran type by upstream #1085/#1093 and are no longer in the registry. """ - # Hardcoded broadcast subset — matches the existing sim m_mpi_proxy exactly. - lag_log = ["heatTransfer_model", "massTransfer_model", "pressure_corrector", "write_bubbles", "write_bubbles_stats"] - lag_int = ["cluster_type", "nBubs_glb", "smooth_type", "solver_approach"] - lag_real = ["charwidth", "epsilonb", "valmaxvoid"] + # Walk the registry for lag_params members, split by type. + lag_log = sorted(k.split("%", 1)[1] for k in REGISTRY.all_params if k.startswith("lag_params%") and REGISTRY.all_params[k].param_type == ParamType.LOG) + lag_int = sorted(k.split("%", 1)[1] for k in REGISTRY.all_params if k.startswith("lag_params%") and REGISTRY.all_params[k].param_type in (ParamType.INT, ParamType.ANALYTIC_INT)) + lag_real = sorted(k.split("%", 1)[1] for k in REGISTRY.all_params if k.startswith("lag_params%") and REGISTRY.all_params[k].param_type in _REAL_TYPES) lines.append(" if (bubbles_lagrange) then") for mem in sorted(lag_log): lines.append(f" call MPI_BCAST(lag_params%{mem}, 1, MPI_LOGICAL, 0, MPI_COMM_WORLD, ierr)") diff --git a/toolchain/mfc/params_tests/test_fortran_gen.py b/toolchain/mfc/params_tests/test_fortran_gen.py index e4fe751f7c..0dc208608c 100644 --- a/toolchain/mfc/params_tests/test_fortran_gen.py +++ b/toolchain/mfc/params_tests/test_fortran_gen.py @@ -387,8 +387,9 @@ def test_generate_bcast_fpp_case_opt_guard_sim(): assert "num_fluids" in guard_body assert "mapped_weno" in guard_body - # muscl_eps must NOT appear anywhere (it is excluded — derived post-broadcast) - assert "muscl_eps" not in out + # muscl_eps IS inside the case-opt guard (it is sim-only, non-CASE_OPT_PARAM, + # so it appears in the real-scalars section, which is outside the case-opt block) + assert "call MPI_BCAST(muscl_eps, 1, mpi_p, 0, MPI_COMM_WORLD, ierr)" in out def test_generate_bcast_fpp_case_opt_not_in_pre_post(): @@ -504,8 +505,6 @@ def test_generate_bcast_fpp_excludes_manual_residue(): assert "m_glb" not in out, f"{target}: m_glb should be manual" assert "n_glb" not in out, f"{target}: n_glb should be manual" assert "p_glb" not in out, f"{target}: p_glb should be manual" - # muscl_eps is excluded (derived post-broadcast) - assert "muscl_eps" not in out, f"{target}: muscl_eps should be excluded" sim = generate_bcast_fpp("sim") # shear_stress, bulk_stress, bodyForces are derived (non-namelist) @@ -530,3 +529,95 @@ def test_generate_bcast_fpp_bad_target(): with pytest.raises(ValueError, match="Unknown target"): generate_bcast_fpp("bad") + + +def test_generate_bcast_fpp_muscl_eps_now_broadcast(): + """muscl_eps is broadcast for sim (latent-bug fix: derivation is rank-0-only). + + Previously excluded via _BCAST_EXCLUDE; every multi-rank MUSCL run had + rank-divergent muscl_eps because f_is_default() only fires on rank 0. + """ + from mfc.params.generators.fortran_gen import generate_bcast_fpp + + sim = generate_bcast_fpp("sim") + assert "call MPI_BCAST(muscl_eps, 1, mpi_p, 0, MPI_COMM_WORLD, ierr)" in sim + + # muscl_eps is sim-only; must not appear in pre/post + pre = generate_bcast_fpp("pre") + post = generate_bcast_fpp("post") + assert "muscl_eps" not in pre + assert "muscl_eps" not in post + + +def test_generate_bcast_fpp_fluid_pp_registry_walk(): + """fluid_pp emitter walks registry; no dead members (mul0/ss/pv/gamma_v/M_v/mu_v/k_v/cp_v/D_v removed upstream). + + Re(1) count=2 is sim-only; all other registered REAL members appear in all three + targets. + """ + from mfc.params.generators.fortran_gen import generate_bcast_fpp + + sim = generate_bcast_fpp("sim") + pre = generate_bcast_fpp("pre") + post = generate_bcast_fpp("post") + + # All registered members appear in every target + for mem in ("gamma", "pi_inf", "cv", "qv", "qvp", "G"): + for out, t in [(sim, "sim"), (pre, "pre"), (post, "post")]: + assert f"fluid_pp(i)%{mem}" in out, f"{t}: fluid_pp(i)%{mem} missing" + + # Re(1) count=2 is sim-only + assert "fluid_pp(i)%Re(1)" in sim + assert "fluid_pp(i)%Re(1)" not in pre + assert "fluid_pp(i)%Re(1)" not in post + + # Dead members must not appear + for dead in ("mul0", "ss", "pv", "gamma_v", "M_v", "mu_v", "k_v", "cp_v", "D_v"): + for out, t in [(sim, "sim"), (pre, "pre"), (post, "post")]: + assert f"fluid_pp(i)%{dead}" not in out, f"{t}: dead member fluid_pp(i)%{dead} present" + + +def test_generate_bcast_fpp_fluid_pp_member_datatypes(): + # The HB merge added a LOGICAL fluid_pp member; datatypes must come from the + # registry, not be assumed REAL. + from mfc.params.generators.fortran_gen import generate_bcast_fpp + + sim = generate_bcast_fpp("sim") + assert "call MPI_BCAST(fluid_pp(i)%non_newtonian, 1, MPI_LOGICAL," in sim + assert "call MPI_BCAST(fluid_pp(i)%tau0, 1, mpi_p," in sim + assert "non_newtonian, 1, mpi_p" not in sim + + +def test_generate_bcast_fpp_lag_params_registry_walk(): + """lag_params emitter walks registry; no dead members (T0/Thost/c0/rho0/x0 removed upstream).""" + from mfc.params.generators.fortran_gen import generate_bcast_fpp + + sim = generate_bcast_fpp("sim") + + # All registered members must appear + for mem in ("solver_approach", "cluster_type", "smooth_type", "nBubs_glb"): + assert f"lag_params%{mem}" in sim, f"lag_params%{mem} missing from sim" + for mem in ("heatTransfer_model", "massTransfer_model", "pressure_corrector", "write_bubbles", "write_bubbles_stats"): + assert f"lag_params%{mem}" in sim, f"lag_params%{mem} missing from sim" + for mem in ("epsilonb", "charwidth", "valmaxvoid"): + assert f"lag_params%{mem}" in sim, f"lag_params%{mem} missing from sim" + + # Dead members must not appear + for dead in ("T0", "Thost", "c0", "rho0", "x0"): + assert f"lag_params%{dead}" not in sim, f"dead member lag_params%{dead} present in sim" + + +def test_mpi_proxy_residue_pins_wall_velocity_and_bc_datatypes(): + """The vb/ve wall-velocity broadcasts and integer BC datatypes live in + hand-written residue (not codegen); pin them so an edit or merge conflict + that drops them fails loudly.""" + import pathlib + + root = pathlib.Path(__file__).resolve().parents[3] + for target in ("pre_process", "post_process"): + src = (root / "src" / target / "m_mpi_proxy.fpp").read_text() + assert "bc_${DIM}$%vb${DIR}$" in src, f"{target}: vb broadcasts missing" + assert "bc_${DIM}$%ve${DIR}$" in src, f"{target}: ve broadcasts missing" + assert "'bc_x%beg', 'bc_x%end', 'bc_y%beg', 'bc_y%end', 'bc_z%beg', 'bc_z%end']" in src + seg = src.split("'bc_z%beg', 'bc_z%end']", 1)[1] + assert "MPI_INTEGER" in seg.split("#:endfor")[0], f"{target}: BC codes not MPI_INTEGER"