From bf430ade1424c66ab9a4761a6e8de41568adaec6 Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Wed, 27 May 2026 22:49:54 -0400 Subject: [PATCH 01/33] feat(params): add str_len to ParamDef for STR character length --- toolchain/mfc/params/definitions.py | 5 ++-- toolchain/mfc/params/schema.py | 2 ++ toolchain/tests/__init__.py | 0 toolchain/tests/params/__init__.py | 0 toolchain/tests/params/test_schema.py | 34 +++++++++++++++++++++++++++ 5 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 toolchain/tests/__init__.py create mode 100644 toolchain/tests/params/__init__.py create mode 100644 toolchain/tests/params/test_schema.py diff --git a/toolchain/mfc/params/definitions.py b/toolchain/mfc/params/definitions.py index c8db5547d0..d5a04da8b9 100644 --- a/toolchain/mfc/params/definitions.py +++ b/toolchain/mfc/params/definitions.py @@ -815,7 +815,7 @@ def get_value_label(param_name: str, value: int) -> str: } -def _r(name, ptype, tags=None, desc=None, hint=None, math=None): +def _r(name, ptype, tags=None, desc=None, hint=None, math=None, str_len=None): """Register a parameter with optional feature tags and description.""" if hint is None: hint = _lookup_hint(name) @@ -836,6 +836,7 @@ def _r(name, ptype, tags=None, desc=None, hint=None, math=None): tags=tags if tags else set(), hint=hint, math_symbol=math or "", + str_len=str_len if str_len is not None else "name_len", ) ) @@ -1072,7 +1073,7 @@ def _load(): ]: _r(n, LOG) _r("int_comp", INT) - _r("case_dir", STR) + _r("case_dir", STR, str_len="path_len") # Body force for d in ["x", "y", "z"]: diff --git a/toolchain/mfc/params/schema.py b/toolchain/mfc/params/schema.py index e0132e19fa..3abba20380 100644 --- a/toolchain/mfc/params/schema.py +++ b/toolchain/mfc/params/schema.py @@ -64,6 +64,8 @@ class ParamDef: tags: Set[str] = field(default_factory=set) # Feature tags: "mhd", "bubbles", etc. hint: str = "" # Constraint/usage hint for docs (e.g. "Used with grcbc_in") math_symbol: str = "" # LaTeX math symbol (Doxygen format, e.g. "\\f$\\gamma_k\\f$") + str_len: str = "name_len" + # For STR type: Fortran character length constant. Default "name_len"; set "path_len" for case_dir. def __post_init__(self): # Validate name diff --git a/toolchain/tests/__init__.py b/toolchain/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/toolchain/tests/params/__init__.py b/toolchain/tests/params/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/toolchain/tests/params/test_schema.py b/toolchain/tests/params/test_schema.py new file mode 100644 index 0000000000..d99913a00f --- /dev/null +++ b/toolchain/tests/params/test_schema.py @@ -0,0 +1,34 @@ +"""Tests for ParamDef str_len field.""" + + +def test_paramdef_str_len_default(): + from mfc.params.schema import ParamDef, ParamType + + p = ParamDef(name="foo", param_type=ParamType.STR) + assert p.str_len == "name_len" + + +def test_paramdef_str_len_override(): + from mfc.params.schema import ParamDef, ParamType + + p = ParamDef(name="case_dir", param_type=ParamType.STR, str_len="path_len") + assert p.str_len == "path_len" + + +def test_case_dir_has_path_len(): + import mfc.params.definitions # noqa: F401 + from mfc.params.registry import REGISTRY + + p = REGISTRY.get_param_def("case_dir") + assert p is not None + assert p.str_len == "path_len" + + +def test_other_str_param_has_default_len(): + import mfc.params.definitions # noqa: F401 + from mfc.params.registry import REGISTRY + + # cantera_file is another STR param that should use the default name_len + p = REGISTRY.get_param_def("cantera_file") + assert p is not None + assert p.str_len == "name_len" From 4e85b3087bdfb46c90ba7afff320ac17b9d5a55a Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Wed, 27 May 2026 22:52:32 -0400 Subject: [PATCH 02/33] style(params): use inline comment for str_len field --- toolchain/mfc/params/schema.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/toolchain/mfc/params/schema.py b/toolchain/mfc/params/schema.py index 3abba20380..7c89f5657d 100644 --- a/toolchain/mfc/params/schema.py +++ b/toolchain/mfc/params/schema.py @@ -64,8 +64,7 @@ class ParamDef: tags: Set[str] = field(default_factory=set) # Feature tags: "mhd", "bubbles", etc. hint: str = "" # Constraint/usage hint for docs (e.g. "Used with grcbc_in") math_symbol: str = "" # LaTeX math symbol (Doxygen format, e.g. "\\f$\\gamma_k\\f$") - str_len: str = "name_len" - # For STR type: Fortran character length constant. Default "name_len"; set "path_len" for case_dir. + str_len: str = "name_len" # For STR type: Fortran character length constant ("path_len" for case_dir) def __post_init__(self): # Validate name From 84b53c208a89a353da3a015a3b336574694f2d6e Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Wed, 27 May 2026 22:58:25 -0400 Subject: [PATCH 03/33] feat(params): add namelist_targets.py with per-target namelist variable mapping --- toolchain/mfc/params/namelist_targets.py | 316 ++++++++++++++++++ .../tests/params/test_namelist_targets.py | 48 +++ 2 files changed, 364 insertions(+) create mode 100644 toolchain/mfc/params/namelist_targets.py create mode 100644 toolchain/tests/params/test_namelist_targets.py diff --git a/toolchain/mfc/params/namelist_targets.py b/toolchain/mfc/params/namelist_targets.py new file mode 100644 index 0000000000..9de64f1a14 --- /dev/null +++ b/toolchain/mfc/params/namelist_targets.py @@ -0,0 +1,316 @@ +""" +Namelist target mapping for Fortran codegen. + +NAMELIST_VARS maps each Fortran namelist variable (struct root or simple scalar) +to the set of MFC executables whose namelist it appears in. + +CASE_OPT_EXCLUDE is the set of simulation namelist variables excluded under +MFC_CASE_OPTIMIZATION (they become compile-time constants instead). + +When adding a new parameter: + 1. Add to definitions.py (type, constraints, etc.) + 2. Add the namelist root variable to NAMELIST_VARS with its target set + 3. Run ./mfc.sh generate to regenerate the .fpp files +""" + +from typing import Dict, Set + +# All three targets +_ALL = {"pre", "sim", "post"} +_PRE_SIM = {"pre", "sim"} +_SIM_POST = {"sim", "post"} + +NAMELIST_VARS: Dict[str, Set[str]] = { + # --- Grid (all targets) --- + "m": _ALL, + "n": _ALL, + "p": _ALL, + "cyl_coord": _ALL, + "x_domain": {"pre", "sim"}, + "y_domain": {"pre", "sim"}, + "z_domain": {"pre", "sim"}, + "x_output": {"post"}, + "y_output": {"post"}, + "z_output": {"post"}, + # --- Grid stretching (pre only) --- + "stretch_x": {"pre"}, + "stretch_y": {"pre"}, + "stretch_z": {"pre"}, + "a_x": {"pre"}, + "a_y": {"pre"}, + "a_z": {"pre"}, + "x_a": {"pre", "sim"}, + "y_a": {"pre", "sim"}, + "z_a": {"pre", "sim"}, + "x_b": {"pre", "sim"}, + "y_b": {"pre", "sim"}, + "z_b": {"pre", "sim"}, + "loops_x": {"pre"}, + "loops_y": {"pre"}, + "loops_z": {"pre"}, + # --- Time --- + "dt": {"sim"}, + "t_step_start": _ALL, + "t_step_stop": {"sim", "post"}, + "t_step_save": {"sim", "post"}, + "t_step_print": {"sim"}, + "t_step_old": {"pre", "sim"}, + "time_stepper": {"sim"}, + "t_stop": {"sim", "post"}, + "t_save": {"sim", "post"}, + "cfl_target": {"sim", "post"}, + "cfl_adap_dt": _ALL, + "cfl_const_dt": _ALL, + "n_start": _ALL, + "n_start_old": {"pre"}, + "adap_dt": {"sim"}, + "adap_dt_tol": {"sim"}, + "adap_dt_max_iters": {"sim"}, + # --- Physics model --- + "model_eqns": _ALL, + "num_fluids": {"pre", "post"}, + "mpp_lim": _ALL, + "relax": _ALL, + "relax_model": _ALL, + "palpha_eps": _ALL, + "ptgalpha_eps": _ALL, + # --- WENO / reconstruction --- + "weno_order": {"pre", "post"}, + "weno_eps": {"sim"}, + "teno_CT": {"sim"}, + "wenoz_q": {"sim"}, + "mp_weno": {"sim"}, + "weno_avg": {"sim"}, + "weno_Re_flux": {"sim"}, + "null_weights": {"sim"}, + "muscl_eps": {"sim"}, + "recon_type": {"pre", "post"}, + "muscl_order": {"pre", "post"}, + "muscl_lim": {"post"}, + "int_comp": {"sim"}, + "ic_eps": {"sim"}, + "ic_beta": {"sim"}, + # --- Riemann solver --- + "riemann_solver": {"sim"}, + "wave_speeds": {"sim"}, + "avg_state": {"sim", "post"}, + "low_Mach": {"sim"}, + # --- MHD --- + "mhd": {"pre", "post"}, + "hyper_cleaning": _ALL, + "hyper_cleaning_speed": {"sim"}, + "hyper_cleaning_tau": {"sim"}, + "Bx0": _ALL, + # --- BCs --- + "bc_x": _ALL, + "bc_y": _ALL, + "bc_z": _ALL, + "num_bc_patches": _ALL, + "patch_bc": {"pre"}, + # --- ICs (pre only) --- + "num_patches": {"pre"}, + "patch_icpp": {"pre"}, + # --- Fluid properties --- + "fluid_pp": _ALL, + "bub_pp": _ALL, + "rhoref": _ALL, + "pref": _ALL, + # --- Bubbles --- + "bubbles_euler": _ALL, + "bubbles_lagrange": _ALL, + "R0ref": _ALL, + "nb": {"pre", "post"}, + "polytropic": _ALL, + "thermal": _ALL, + "Ca": _ALL, + "Web": _ALL, + "Re_inv": _ALL, + "polydisperse": _ALL, + "poly_sigma": _ALL, + "qbmm": _ALL, + "sigma": _ALL, + "adv_n": _ALL, + "bubble_model": {"sim"}, + "sigR": {"pre", "post"}, + "sigV": {"pre"}, + "dist_type": {"pre"}, + "rhoRV": {"pre"}, + "lag_params": {"sim"}, + # --- Lagrangian output (post) --- + "lag_header": {"post"}, + "lag_txt_wrt": {"post"}, + "lag_db_wrt": {"post"}, + "lag_id_wrt": {"post"}, + "lag_pos_wrt": {"post"}, + "lag_pos_prev_wrt": {"post"}, + "lag_vel_wrt": {"post"}, + "lag_rad_wrt": {"post"}, + "lag_rvel_wrt": {"post"}, + "lag_r0_wrt": {"post"}, + "lag_rmax_wrt": {"post"}, + "lag_rmin_wrt": {"post"}, + "lag_dphidt_wrt": {"post"}, + "lag_pres_wrt": {"post"}, + "lag_mv_wrt": {"post"}, + "lag_mg_wrt": {"post"}, + "lag_betaT_wrt": {"post"}, + "lag_betaC_wrt": {"post"}, + # --- Elasticity --- + "hypoelasticity": _ALL, + "hyperelasticity": _ALL, + # --- Surface tension --- + "surface_tension": _ALL, + # --- Relativity --- + "relativity": _ALL, + # --- Immersed boundaries --- + "ib": _ALL, + "num_ibs": _ALL, + "patch_ib": {"pre", "sim"}, + "collision_model": {"sim"}, + "coefficient_of_restitution": {"sim"}, + "collision_time": {"sim"}, + "ib_coefficient_of_friction": {"sim"}, + "ib_state_wrt": {"sim", "post"}, + # --- Continuum damage --- + "cont_damage": _ALL, + "tau_star": {"sim"}, + "cont_damage_s": {"sim"}, + "alpha_bar": {"sim"}, + # --- IGR --- + "igr": {"pre", "post"}, + "igr_order": {"pre", "post"}, + "down_sample": _ALL, + # --- Probes (sim) --- + "probe_wrt": {"sim"}, + "num_probes": {"sim"}, + "probe": {"sim"}, + "integral_wrt": {"sim"}, + "num_integrals": {"sim"}, + "integral": {"sim"}, + "fd_order": {"sim", "post"}, + # --- Acoustic sources (sim) --- + "acoustic_source": {"sim"}, + "num_source": {"sim"}, + "acoustic": {"sim"}, + # --- Chemistry --- + "chem_params": {"sim"}, + # --- Body forces (sim) --- + "bf_x": {"sim"}, + "bf_y": {"sim"}, + "bf_z": {"sim"}, + "k_x": {"sim"}, + "k_y": {"sim"}, + "k_z": {"sim"}, + "w_x": {"sim"}, + "w_y": {"sim"}, + "w_z": {"sim"}, + "p_x": {"sim"}, + "p_y": {"sim"}, + "p_z": {"sim"}, + "g_x": {"sim"}, + "g_y": {"sim"}, + "g_z": {"sim"}, + # --- Viscous (pre) --- + "viscous": {"pre"}, + # --- Output --- + "precision": _ALL, + "parallel_io": _ALL, + "file_per_process": _ALL, + "prim_vars_wrt": {"sim", "post"}, + "cons_vars_wrt": {"post"}, + "run_time_info": {"sim"}, + "fft_wrt": _ALL, + "pi_fac": {"pre", "sim"}, + # --- Post-process output --- + "format": {"post"}, + "output_partial_domain": {"post"}, + "rho_wrt": {"post"}, + "E_wrt": {"post"}, + "pres_wrt": {"post"}, + "c_wrt": {"post"}, + "omega_wrt": {"post"}, + "qm_wrt": {"post"}, + "liutex_wrt": {"post"}, + "schlieren_wrt": {"post"}, + "schlieren_alpha": {"post"}, + "gamma_wrt": {"post"}, + "heat_ratio_wrt": {"post"}, + "pi_inf_wrt": {"post"}, + "pres_inf_wrt": {"post"}, + "alpha_rho_wrt": {"post"}, + "mom_wrt": {"post"}, + "vel_wrt": {"post"}, + "flux_wrt": {"post"}, + "alpha_wrt": {"post"}, + "cf_wrt": {"post"}, + "chem_wrt_T": {"post"}, + "chem_wrt_Y": {"post"}, + "alt_soundspeed": {"sim", "post"}, + "mixture_err": {"sim", "post"}, + "flux_lim": {"post"}, + "sim_data": {"post"}, + "alpha_rho_e_wrt": {"post"}, + "G": {"post"}, + # --- Pre-process IC perturbations --- + "perturb_flow": {"pre"}, + "perturb_flow_fluid": {"pre"}, + "perturb_flow_mag": {"pre"}, + "perturb_sph": {"pre"}, + "perturb_sph_fluid": {"pre"}, + "fluid_rho": {"pre"}, + "mixlayer_vel_profile": {"pre"}, + "mixlayer_vel_coef": {"pre"}, + "mixlayer_perturb": {"pre"}, + "mixlayer_perturb_nk": {"pre"}, + "mixlayer_perturb_k0": {"pre"}, + "pre_stress": {"pre"}, + "elliptic_smoothing": {"pre"}, + "elliptic_smoothing_iters": {"pre"}, + "simplex_perturb": {"pre"}, + "simplex_params": {"pre"}, + # --- Pre-process restart --- + "old_grid": {"pre"}, + "old_ic": {"pre"}, + # --- Sim-specific physics --- + "rdma_mpi": {"sim"}, + "alf_factor": {"sim"}, + "num_igr_iters": {"sim"}, + "num_igr_warm_start_iters": {"sim"}, + "igr_iter_solver": {"sim"}, + "igr_pres_lim": {"sim"}, + "nv_uvm_out_of_core": {"sim"}, + "nv_uvm_igr_temps_on_gpu": {"sim"}, + "nv_uvm_pref_gpu": {"sim"}, + # --- Logistics --- + "case_dir": _ALL, +} + +# Variables excluded from the sim namelist when MFC_CASE_OPTIMIZATION is active +# (they become compile-time integer/logical parameters instead). +CASE_OPT_EXCLUDE: Set[str] = { + "nb", + "mapped_weno", + "wenoz", + "teno", + "wenoz_q", + "weno_order", + "num_fluids", + "mhd", + "relativity", + "igr_order", + "viscous", + "igr_iter_solver", + "igr", + "igr_pres_lim", + "recon_type", + "muscl_order", + "muscl_lim", +} + +# Add CASE_OPT_EXCLUDE vars to NAMELIST_VARS for sim target +# (they appear in the namelist when NOT using case optimization) +for _v in CASE_OPT_EXCLUDE: + if _v not in NAMELIST_VARS: + NAMELIST_VARS[_v] = {"sim"} + else: + NAMELIST_VARS[_v].add("sim") diff --git a/toolchain/tests/params/test_namelist_targets.py b/toolchain/tests/params/test_namelist_targets.py new file mode 100644 index 0000000000..4d21a3e1a8 --- /dev/null +++ b/toolchain/tests/params/test_namelist_targets.py @@ -0,0 +1,48 @@ +def test_common_vars_in_all_targets(): + from mfc.params.namelist_targets import NAMELIST_VARS + + for var in ["m", "n", "p", "bc_x", "bc_y", "bc_z", "model_eqns", "cyl_coord", "fluid_pp", "case_dir"]: + assert {"pre", "sim", "post"}.issubset(NAMELIST_VARS.get(var, set())), f"{var!r} not marked for all targets" + + +def test_sim_only_vars(): + from mfc.params.namelist_targets import NAMELIST_VARS + + for var in ["run_time_info", "dt", "riemann_solver", "acoustic", "probe"]: + targets = NAMELIST_VARS.get(var, set()) + assert "sim" in targets, f"{var!r} not marked for sim" + assert "pre" not in targets, f"{var!r} incorrectly marked for pre" + assert "post" not in targets, f"{var!r} incorrectly marked for post" + + +def test_pre_only_vars(): + from mfc.params.namelist_targets import NAMELIST_VARS + + for var in ["old_grid", "old_ic", "patch_icpp", "simplex_params"]: + targets = NAMELIST_VARS.get(var, set()) + assert "pre" in targets, f"{var!r} not marked for pre" + assert "sim" not in targets, f"{var!r} incorrectly in sim" + + +def test_post_only_vars(): + from mfc.params.namelist_targets import NAMELIST_VARS + + for var in ["format", "sim_data", "lag_header", "output_partial_domain"]: + targets = NAMELIST_VARS.get(var, set()) + assert "post" in targets, f"{var!r} not marked for post" + assert "sim" not in targets, f"{var!r} incorrectly in sim" + + +def test_case_opt_exclude_vars(): + from mfc.params.namelist_targets import CASE_OPT_EXCLUDE + + for var in ["nb", "mapped_weno", "wenoz", "weno_order", "num_fluids"]: + assert var in CASE_OPT_EXCLUDE + + +def test_case_opt_exclude_vars_also_in_sim_namelist(): + from mfc.params.namelist_targets import CASE_OPT_EXCLUDE, NAMELIST_VARS + + for var in CASE_OPT_EXCLUDE: + targets = NAMELIST_VARS.get(var, set()) + assert "sim" in targets, f"CASE_OPT_EXCLUDE var {var!r} must also be in sim namelist" From 283fb29ef33dba8884e7e1208c1858457ed150dd Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Wed, 27 May 2026 23:02:00 -0400 Subject: [PATCH 04/33] feat(params): add fortran_gen.py to generate namelist and decl .fpp files --- .../mfc/params/generators/fortran_gen.py | 157 ++++++++++++++++++ toolchain/tests/params/test_fortran_gen.py | 95 +++++++++++ 2 files changed, 252 insertions(+) create mode 100644 toolchain/mfc/params/generators/fortran_gen.py create mode 100644 toolchain/tests/params/test_fortran_gen.py diff --git a/toolchain/mfc/params/generators/fortran_gen.py b/toolchain/mfc/params/generators/fortran_gen.py new file mode 100644 index 0000000000..87a12ee445 --- /dev/null +++ b/toolchain/mfc/params/generators/fortran_gen.py @@ -0,0 +1,157 @@ +""" +Fortran parameter code generator. + +Generates namelist fragments and simple scalar declaration fragments +per target (pre/sim/post). Output consumed by generate.py. +""" + +import re +from pathlib import Path +from typing import List, Tuple + +import mfc.params.definitions # noqa: F401 — triggers registry population + +from ..namelist_targets import CASE_OPT_EXCLUDE, NAMELIST_VARS +from ..registry import REGISTRY +from ..schema import ParamDef, ParamType + +_HEADER = ( + "! AUTO-GENERATED — do not edit directly.\n" + "! Regenerate: ./mfc.sh generate\n" + "!\n" +) + + +def get_namelist_var(param_name: str) -> str: + """Return the Fortran namelist root for a parameter name.""" + m = re.match(r"^([a-zA-Z_]\w*)\(", param_name) + if m and "%" in param_name: + return m.group(1) + if "%" in param_name: + return param_name.split("%", maxsplit=1)[0] + return param_name + + +def fortran_type_decl(param: ParamDef) -> str: + """Return the Fortran type string for a parameter.""" + mapping = { + ParamType.INT: "integer", + ParamType.REAL: "real(wp)", + ParamType.LOG: "logical", + ParamType.ANALYTIC_INT: "integer", + ParamType.ANALYTIC_REAL: "real(wp)", + } + if param.param_type == ParamType.STR: + return f"character(LEN={param.str_len})" + return mapping[param.param_type] + + +def _is_simple_scalar(name: str) -> bool: + """Return True if name has no '%' and no '(' — i.e. a plain simple variable.""" + return "%" not in name and "(" not in name + + +def _vars_for_target(target: str) -> List[str]: + """Return sorted list of namelist variable names for the given target.""" + return sorted(v for v, ts in NAMELIST_VARS.items() if target in ts) + + +def _format_namelist(vars_list: List[str]) -> str: + """ + Format a list of variable names as a Fortran namelist continuation block. + + The first line starts with 'namelist /user_inputs/ '. + Continuation lines use ' & ' prefix (4 spaces + ampersand + space). + All lines except the last end with ', &' for continuation. + Lines are wrapped at 80 characters. + """ + if not vars_list: + return "" + + FIRST_PREFIX = "namelist /user_inputs/ " + CONT_PREFIX = " & " + MAX_WIDTH = 80 + + lines: List[str] = [] + prefix = FIRST_PREFIX + current_vars: List[str] = [] + current_len = len(prefix) + + for var in vars_list: + # Each var takes len(var) + 2 for ", " separator (except possibly last) + additional = len(var) + (2 if current_vars else 0) + if current_vars and current_len + additional > MAX_WIDTH: + # Flush current line with continuation + lines.append(prefix + ", ".join(current_vars) + ", &") + prefix = CONT_PREFIX + current_vars = [var] + current_len = len(CONT_PREFIX) + len(var) + else: + current_vars.append(var) + current_len += additional + + # Emit the last line (no trailing continuation — caller adds if needed) + if current_vars: + lines.append(prefix + ", ".join(current_vars)) + + return "\n".join(lines) + + +def generate_namelist_fpp(target: str) -> str: + """Generate the namelist /user_inputs/ statement for a target.""" + assert target in ("pre", "sim", "post") + all_vars = _vars_for_target(target) + + if target != "sim": + return _HEADER + _format_namelist(all_vars) + "\n" + + # For sim: split into normal vars and case-opt-excluded vars + normal = [v for v in all_vars if v not in CASE_OPT_EXCLUDE] + opt = sorted(v for v in CASE_OPT_EXCLUDE if v in NAMELIST_VARS and "sim" in NAMELIST_VARS[v]) + + lines = [_HEADER.rstrip()] + + # Normal vars always end with ", &" because opt vars follow (inside the #:if block) + nl = _format_namelist(normal) + lines.append(nl + ", &") + lines.append("#:if not MFC_CASE_OPTIMIZATION") + + # Opt vars: each line except the last ends with ", &" + for i, var in enumerate(opt): + is_last = (i == len(opt) - 1) + if is_last: + lines.append(f" & {var}") + else: + lines.append(f" & {var}, &") + + lines.append("#:endif") + + return "\n".join(lines) + "\n" + + +def generate_decls_fpp(target: str) -> str: + """Generate simple scalar Fortran variable declarations for a target.""" + assert target in ("pre", "sim", "post") + all_params = REGISTRY.all_params + vars_for_target = _vars_for_target(target) + lines = [_HEADER.rstrip()] + for name in vars_for_target: + if not _is_simple_scalar(name): + continue + param = all_params.get(name) + if param is None: + continue + lines.append(f"{fortran_type_decl(param)} :: {name}") + return "\n".join(lines) + "\n" + + +def get_generated_files(include_dir: Path) -> List[Tuple[Path, str]]: + """Return (output_path, content) for all six generated .fpp files.""" + return [ + (include_dir / "generated_namelist_pre.fpp", generate_namelist_fpp("pre")), + (include_dir / "generated_namelist_sim.fpp", generate_namelist_fpp("sim")), + (include_dir / "generated_namelist_post.fpp", generate_namelist_fpp("post")), + (include_dir / "generated_decls_pre.fpp", generate_decls_fpp("pre")), + (include_dir / "generated_decls_sim.fpp", generate_decls_fpp("sim")), + (include_dir / "generated_decls_post.fpp", generate_decls_fpp("post")), + ] diff --git a/toolchain/tests/params/test_fortran_gen.py b/toolchain/tests/params/test_fortran_gen.py new file mode 100644 index 0000000000..f2e7aeb6cb --- /dev/null +++ b/toolchain/tests/params/test_fortran_gen.py @@ -0,0 +1,95 @@ +def test_get_namelist_var_simple(): + from mfc.params.generators.fortran_gen import get_namelist_var + assert get_namelist_var("m") == "m" + assert get_namelist_var("dt") == "dt" + +def test_get_namelist_var_indexed_family(): + from mfc.params.generators.fortran_gen import get_namelist_var + assert get_namelist_var("fluid_pp(1)%gamma") == "fluid_pp" + assert get_namelist_var("patch_icpp(3)%geometry") == "patch_icpp" + +def test_get_namelist_var_struct_member(): + from mfc.params.generators.fortran_gen import get_namelist_var + assert get_namelist_var("bc_x%beg") == "bc_x" + assert get_namelist_var("lag_params%solver_approach") == "lag_params" + +def test_fortran_type_int(): + from mfc.params.generators.fortran_gen import fortran_type_decl + from mfc.params.schema import ParamDef, ParamType + assert fortran_type_decl(ParamDef(name="x", param_type=ParamType.INT)) == "integer" + +def test_fortran_type_real(): + from mfc.params.generators.fortran_gen import fortran_type_decl + from mfc.params.schema import ParamDef, ParamType + assert fortran_type_decl(ParamDef(name="x", param_type=ParamType.REAL)) == "real(wp)" + +def test_fortran_type_log(): + from mfc.params.generators.fortran_gen import fortran_type_decl + from mfc.params.schema import ParamDef, ParamType + assert fortran_type_decl(ParamDef(name="x", param_type=ParamType.LOG)) == "logical" + +def test_fortran_type_str(): + from mfc.params.generators.fortran_gen import fortran_type_decl + from mfc.params.schema import ParamDef, ParamType + p = ParamDef(name="case_dir", param_type=ParamType.STR, str_len="path_len") + assert fortran_type_decl(p) == "character(LEN=path_len)" + +def test_namelist_contains_common_vars(): + from mfc.params.generators.fortran_gen import generate_namelist_fpp + for target in ("pre", "sim", "post"): + c = generate_namelist_fpp(target) + for v in ("m", "n", "p", "bc_x", "case_dir", "fluid_pp"): + assert v in c, f"{v!r} missing from {target} namelist" + +def test_sim_namelist_case_opt_guard(): + from mfc.params.generators.fortran_gen import generate_namelist_fpp + c = generate_namelist_fpp("sim") + assert "#:if not MFC_CASE_OPTIMIZATION" in c + assert "weno_order" in c + assert "num_fluids" in c + +def test_pre_namelist_has_patch_icpp(): + from mfc.params.generators.fortran_gen import generate_namelist_fpp + c = generate_namelist_fpp("pre") + assert "patch_icpp" in c + assert "run_time_info" not in c + +def test_post_namelist_has_sim_data(): + from mfc.params.generators.fortran_gen import generate_namelist_fpp + c = generate_namelist_fpp("post") + assert "sim_data" in c + assert "patch_icpp" not in c + +def test_decls_contains_simple_scalars(): + from mfc.params.generators.fortran_gen import generate_decls_fpp + for target in ("pre", "sim", "post"): + c = generate_decls_fpp(target) + assert "integer" in c + assert "real(wp)" in c + assert "logical" in c + +def test_decls_dt_for_sim(): + from mfc.params.generators.fortran_gen import generate_decls_fpp + assert "real(wp) :: dt" in generate_decls_fpp("sim") + +def test_decls_no_percent_vars(): + from mfc.params.generators.fortran_gen import generate_decls_fpp + for target in ("pre", "sim", "post"): + c = generate_decls_fpp(target) + assert "bc_x%beg" not in c + assert "fluid_pp(1)" not in c + +def test_decls_case_dir(): + from mfc.params.generators.fortran_gen import generate_decls_fpp + for target in ("pre", "sim", "post"): + assert "character(LEN=path_len) :: case_dir" in generate_decls_fpp(target) + +def test_get_generated_files_returns_six(): + from pathlib import Path + + from mfc.params.generators.fortran_gen import get_generated_files + files = get_generated_files(Path("/tmp")) + assert len(files) == 6 + names = {p.name for p, _ in files} + assert "generated_namelist_pre.fpp" in names + assert "generated_decls_sim.fpp" in names From dee2f0cc4f63be53dfb9bb1b0a7bdea10537e0b5 Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Wed, 27 May 2026 23:06:26 -0400 Subject: [PATCH 05/33] feat(params): wire fortran_gen into generate.py, add generated .fpp files --- src/common/include/generated_decls_post.fpp | 111 ++++++++++++++ src/common/include/generated_decls_pre.fpp | 96 ++++++++++++ src/common/include/generated_decls_sim.fpp | 140 ++++++++++++++++++ .../include/generated_namelist_post.fpp | 14 ++ src/common/include/generated_namelist_pre.fpp | 12 ++ src/common/include/generated_namelist_sim.fpp | 19 +++ toolchain/mfc/generate.py | 5 + .../mfc/params/generators/fortran_gen.py | 99 +++++++------ toolchain/tests/params/test_fortran_gen.py | 34 ++++- 9 files changed, 481 insertions(+), 49 deletions(-) create mode 100644 src/common/include/generated_decls_post.fpp create mode 100644 src/common/include/generated_decls_pre.fpp create mode 100644 src/common/include/generated_decls_sim.fpp create mode 100644 src/common/include/generated_namelist_post.fpp create mode 100644 src/common/include/generated_namelist_pre.fpp create mode 100644 src/common/include/generated_namelist_sim.fpp diff --git a/src/common/include/generated_decls_post.fpp b/src/common/include/generated_decls_post.fpp new file mode 100644 index 0000000000..0797b61237 --- /dev/null +++ b/src/common/include/generated_decls_post.fpp @@ -0,0 +1,111 @@ +! AUTO-GENERATED - do not edit directly. Regenerate: ./mfc.sh generate +! +real(wp) :: Bx0 +real(wp) :: Ca +logical :: E_wrt +real(wp) :: R0ref +real(wp) :: Re_inv +real(wp) :: Web +logical :: adv_n +logical :: alpha_rho_wrt +logical :: alpha_wrt +logical :: alt_soundspeed +integer :: avg_state +logical :: bubbles_euler +logical :: bubbles_lagrange +logical :: c_wrt +character(LEN=path_len) :: case_dir +logical :: cf_wrt +logical :: cfl_adap_dt +logical :: cfl_const_dt +real(wp) :: cfl_target +logical :: chem_wrt_T +logical :: cons_vars_wrt +logical :: cont_damage +logical :: cyl_coord +logical :: down_sample +integer :: fd_order +logical :: fft_wrt +logical :: file_per_process +integer :: flux_lim +logical :: flux_wrt +integer :: format +logical :: gamma_wrt +logical :: heat_ratio_wrt +logical :: hyper_cleaning +logical :: hyperelasticity +logical :: hypoelasticity +logical :: ib +logical :: ib_state_wrt +logical :: igr +integer :: igr_order +logical :: lag_betaC_wrt +logical :: lag_betaT_wrt +logical :: lag_db_wrt +logical :: lag_dphidt_wrt +logical :: lag_header +logical :: lag_id_wrt +logical :: lag_mg_wrt +logical :: lag_mv_wrt +logical :: lag_pos_prev_wrt +logical :: lag_pos_wrt +logical :: lag_pres_wrt +logical :: lag_r0_wrt +logical :: lag_rad_wrt +logical :: lag_rmax_wrt +logical :: lag_rmin_wrt +logical :: lag_rvel_wrt +logical :: lag_txt_wrt +logical :: lag_vel_wrt +logical :: liutex_wrt +integer :: m +logical :: mhd +logical :: mixture_err +integer :: model_eqns +logical :: mom_wrt +logical :: mpp_lim +integer :: muscl_lim +integer :: muscl_order +integer :: n +integer :: n_start +real(wp) :: nb +integer :: num_bc_patches +integer :: num_fluids +integer :: num_ibs +logical :: omega_wrt +logical :: output_partial_domain +integer :: p +real(wp) :: palpha_eps +logical :: parallel_io +logical :: pi_inf_wrt +real(wp) :: poly_sigma +logical :: polydisperse +logical :: polytropic +integer :: precision +real(wp) :: pref +logical :: pres_inf_wrt +logical :: pres_wrt +logical :: prim_vars_wrt +real(wp) :: ptgalpha_eps +logical :: qbmm +logical :: qm_wrt +integer :: recon_type +logical :: relativity +logical :: relax +integer :: relax_model +logical :: rho_wrt +real(wp) :: rhoref +real(wp) :: schlieren_alpha +logical :: schlieren_wrt +real(wp) :: sigR +real(wp) :: sigma +logical :: sim_data +logical :: surface_tension +real(wp) :: t_save +integer :: t_step_save +integer :: t_step_start +integer :: t_step_stop +real(wp) :: t_stop +integer :: thermal +logical :: vel_wrt +integer :: weno_order diff --git a/src/common/include/generated_decls_pre.fpp b/src/common/include/generated_decls_pre.fpp new file mode 100644 index 0000000000..bb3342c7d9 --- /dev/null +++ b/src/common/include/generated_decls_pre.fpp @@ -0,0 +1,96 @@ +! AUTO-GENERATED - do not edit directly. Regenerate: ./mfc.sh generate +! +real(wp) :: Bx0 +real(wp) :: Ca +real(wp) :: R0ref +real(wp) :: Re_inv +real(wp) :: Web +real(wp) :: a_x +real(wp) :: a_y +real(wp) :: a_z +logical :: adv_n +logical :: bubbles_euler +logical :: bubbles_lagrange +character(LEN=path_len) :: case_dir +logical :: cfl_adap_dt +logical :: cfl_const_dt +logical :: cont_damage +logical :: cyl_coord +integer :: dist_type +logical :: down_sample +logical :: elliptic_smoothing +integer :: elliptic_smoothing_iters +logical :: fft_wrt +logical :: file_per_process +real(wp) :: fluid_rho +logical :: hyper_cleaning +logical :: hyperelasticity +logical :: hypoelasticity +logical :: ib +logical :: igr +integer :: igr_order +integer :: loops_x +integer :: loops_y +integer :: loops_z +integer :: m +logical :: mhd +logical :: mixlayer_perturb +real(wp) :: mixlayer_perturb_k0 +integer :: mixlayer_perturb_nk +real(wp) :: mixlayer_vel_coef +logical :: mixlayer_vel_profile +integer :: model_eqns +logical :: mpp_lim +integer :: muscl_order +integer :: n +integer :: n_start +integer :: n_start_old +real(wp) :: nb +integer :: num_bc_patches +integer :: num_fluids +integer :: num_ibs +integer :: num_patches +logical :: old_grid +logical :: old_ic +integer :: p +real(wp) :: palpha_eps +logical :: parallel_io +logical :: perturb_flow +integer :: perturb_flow_fluid +real(wp) :: perturb_flow_mag +logical :: perturb_sph +integer :: perturb_sph_fluid +real(wp) :: pi_fac +real(wp) :: poly_sigma +logical :: polydisperse +logical :: polytropic +logical :: pre_stress +integer :: precision +real(wp) :: pref +real(wp) :: ptgalpha_eps +logical :: qbmm +integer :: recon_type +logical :: relativity +logical :: relax +integer :: relax_model +real(wp) :: rhoRV +real(wp) :: rhoref +real(wp) :: sigR +real(wp) :: sigV +real(wp) :: sigma +logical :: simplex_perturb +logical :: stretch_x +logical :: stretch_y +logical :: stretch_z +logical :: surface_tension +integer :: t_step_old +integer :: t_step_start +integer :: thermal +logical :: viscous +integer :: weno_order +real(wp) :: x_a +real(wp) :: x_b +real(wp) :: y_a +real(wp) :: y_b +real(wp) :: z_a +real(wp) :: z_b diff --git a/src/common/include/generated_decls_sim.fpp b/src/common/include/generated_decls_sim.fpp new file mode 100644 index 0000000000..87060cb978 --- /dev/null +++ b/src/common/include/generated_decls_sim.fpp @@ -0,0 +1,140 @@ +! AUTO-GENERATED - do not edit directly. Regenerate: ./mfc.sh generate +! +real(wp) :: Bx0 +real(wp) :: Ca +real(wp) :: R0ref +real(wp) :: Re_inv +real(wp) :: Web +logical :: acoustic_source +logical :: adap_dt +integer :: adap_dt_max_iters +real(wp) :: adap_dt_tol +logical :: adv_n +real(wp) :: alf_factor +real(wp) :: alpha_bar +logical :: alt_soundspeed +integer :: avg_state +logical :: bf_x +logical :: bf_y +logical :: bf_z +integer :: bubble_model +logical :: bubbles_euler +logical :: bubbles_lagrange +character(LEN=path_len) :: case_dir +logical :: cfl_adap_dt +logical :: cfl_const_dt +real(wp) :: cfl_target +real(wp) :: coefficient_of_restitution +integer :: collision_model +real(wp) :: collision_time +logical :: cont_damage +real(wp) :: cont_damage_s +logical :: cyl_coord +logical :: down_sample +real(wp) :: dt +integer :: fd_order +logical :: fft_wrt +logical :: file_per_process +real(wp) :: g_x +real(wp) :: g_y +real(wp) :: g_z +logical :: hyper_cleaning +real(wp) :: hyper_cleaning_speed +real(wp) :: hyper_cleaning_tau +logical :: hyperelasticity +logical :: hypoelasticity +logical :: ib +real(wp) :: ib_coefficient_of_friction +logical :: ib_state_wrt +real(wp) :: ic_beta +real(wp) :: ic_eps +logical :: igr +integer :: igr_iter_solver +integer :: igr_order +logical :: igr_pres_lim +integer :: int_comp +logical :: integral_wrt +real(wp) :: k_x +real(wp) :: k_y +real(wp) :: k_z +integer :: low_Mach +integer :: m +logical :: mapped_weno +logical :: mhd +logical :: mixture_err +integer :: model_eqns +logical :: mp_weno +logical :: mpp_lim +real(wp) :: muscl_eps +integer :: muscl_lim +integer :: muscl_order +integer :: n +integer :: n_start +real(wp) :: nb +logical :: null_weights +integer :: num_bc_patches +integer :: num_fluids +integer :: num_ibs +integer :: num_igr_iters +integer :: num_igr_warm_start_iters +integer :: num_integrals +integer :: num_probes +integer :: num_source +integer :: nv_uvm_igr_temps_on_gpu +logical :: nv_uvm_out_of_core +logical :: nv_uvm_pref_gpu +integer :: p +real(wp) :: p_x +real(wp) :: p_y +real(wp) :: p_z +real(wp) :: palpha_eps +logical :: parallel_io +real(wp) :: pi_fac +real(wp) :: poly_sigma +logical :: polydisperse +logical :: polytropic +integer :: precision +real(wp) :: pref +logical :: prim_vars_wrt +logical :: probe_wrt +real(wp) :: ptgalpha_eps +logical :: qbmm +logical :: rdma_mpi +integer :: recon_type +logical :: relativity +logical :: relax +integer :: relax_model +real(wp) :: rhoref +integer :: riemann_solver +logical :: run_time_info +real(wp) :: sigma +logical :: surface_tension +real(wp) :: t_save +integer :: t_step_old +integer :: t_step_print +integer :: t_step_save +integer :: t_step_start +integer :: t_step_stop +real(wp) :: t_stop +real(wp) :: tau_star +logical :: teno +real(wp) :: teno_CT +integer :: thermal +integer :: time_stepper +logical :: viscous +real(wp) :: w_x +real(wp) :: w_y +real(wp) :: w_z +integer :: wave_speeds +logical :: weno_Re_flux +logical :: weno_avg +real(wp) :: weno_eps +integer :: weno_order +logical :: wenoz +real(wp) :: wenoz_q +real(wp) :: x_a +real(wp) :: x_b +real(wp) :: y_a +real(wp) :: y_b +real(wp) :: z_a +real(wp) :: z_b diff --git a/src/common/include/generated_namelist_post.fpp b/src/common/include/generated_namelist_post.fpp new file mode 100644 index 0000000000..71edbde252 --- /dev/null +++ b/src/common/include/generated_namelist_post.fpp @@ -0,0 +1,14 @@ +! AUTO-GENERATED - do not edit directly. Regenerate: ./mfc.sh generate +! +namelist /user_inputs/ Bx0, Ca, E_wrt, G, R0ref, Re_inv, Web, adv_n, alpha_rho_e_wrt, alpha_rho_wrt, alpha_wrt, alt_soundspeed, & + & avg_state, bc_x, bc_y, bc_z, bub_pp, bubbles_euler, bubbles_lagrange, c_wrt, case_dir, cf_wrt, cfl_adap_dt, cfl_const_dt, & + & cfl_target, chem_wrt_T, chem_wrt_Y, cons_vars_wrt, cont_damage, cyl_coord, down_sample, fd_order, fft_wrt, & + & file_per_process, fluid_pp, flux_lim, flux_wrt, format, gamma_wrt, heat_ratio_wrt, hyper_cleaning, hyperelasticity, & + & hypoelasticity, ib, ib_state_wrt, igr, igr_order, lag_betaC_wrt, lag_betaT_wrt, lag_db_wrt, lag_dphidt_wrt, lag_header, & + & lag_id_wrt, lag_mg_wrt, lag_mv_wrt, lag_pos_prev_wrt, lag_pos_wrt, lag_pres_wrt, lag_r0_wrt, lag_rad_wrt, lag_rmax_wrt, & + & lag_rmin_wrt, lag_rvel_wrt, lag_txt_wrt, lag_vel_wrt, liutex_wrt, m, mhd, mixture_err, model_eqns, mom_wrt, mpp_lim, & + & muscl_lim, muscl_order, n, n_start, nb, num_bc_patches, num_fluids, num_ibs, omega_wrt, output_partial_domain, p, & + & palpha_eps, parallel_io, pi_inf_wrt, poly_sigma, polydisperse, polytropic, precision, pref, pres_inf_wrt, pres_wrt, & + & prim_vars_wrt, ptgalpha_eps, qbmm, qm_wrt, recon_type, relativity, relax, relax_model, rho_wrt, rhoref, schlieren_alpha, & + & schlieren_wrt, sigR, sigma, sim_data, surface_tension, t_save, t_step_save, t_step_start, t_step_stop, t_stop, thermal, & + & vel_wrt, weno_order, x_output, y_output, z_output diff --git a/src/common/include/generated_namelist_pre.fpp b/src/common/include/generated_namelist_pre.fpp new file mode 100644 index 0000000000..89e0a50a98 --- /dev/null +++ b/src/common/include/generated_namelist_pre.fpp @@ -0,0 +1,12 @@ +! AUTO-GENERATED - do not edit directly. Regenerate: ./mfc.sh generate +! +namelist /user_inputs/ Bx0, Ca, R0ref, Re_inv, Web, a_x, a_y, a_z, adv_n, bc_x, bc_y, bc_z, bub_pp, bubbles_euler, & + & bubbles_lagrange, case_dir, cfl_adap_dt, cfl_const_dt, cont_damage, cyl_coord, dist_type, down_sample, elliptic_smoothing, & + & elliptic_smoothing_iters, fft_wrt, file_per_process, fluid_pp, fluid_rho, hyper_cleaning, hyperelasticity, hypoelasticity, & + & ib, igr, igr_order, loops_x, loops_y, loops_z, m, mhd, mixlayer_perturb, mixlayer_perturb_k0, mixlayer_perturb_nk, & + & mixlayer_vel_coef, mixlayer_vel_profile, model_eqns, mpp_lim, muscl_order, n, n_start, n_start_old, nb, num_bc_patches, & + & num_fluids, num_ibs, num_patches, old_grid, old_ic, p, palpha_eps, parallel_io, patch_bc, patch_ib, patch_icpp, & + & perturb_flow, perturb_flow_fluid, perturb_flow_mag, perturb_sph, perturb_sph_fluid, pi_fac, poly_sigma, polydisperse, & + & polytropic, pre_stress, precision, pref, ptgalpha_eps, qbmm, recon_type, relativity, relax, relax_model, rhoRV, rhoref, & + & sigR, sigV, sigma, simplex_params, simplex_perturb, stretch_x, stretch_y, stretch_z, surface_tension, t_step_old, & + & t_step_start, thermal, viscous, weno_order, x_a, x_b, x_domain, y_a, y_b, y_domain, z_a, z_b, z_domain diff --git a/src/common/include/generated_namelist_sim.fpp b/src/common/include/generated_namelist_sim.fpp new file mode 100644 index 0000000000..da5fc9b1cb --- /dev/null +++ b/src/common/include/generated_namelist_sim.fpp @@ -0,0 +1,19 @@ +! AUTO-GENERATED - do not edit directly. Regenerate: ./mfc.sh generate +! +namelist /user_inputs/ Bx0, Ca, R0ref, Re_inv, Web, acoustic, acoustic_source, adap_dt, adap_dt_max_iters, adap_dt_tol, adv_n, & + & alf_factor, alpha_bar, alt_soundspeed, avg_state, bc_x, bc_y, bc_z, bf_x, bf_y, bf_z, bub_pp, bubble_model, bubbles_euler, & + & bubbles_lagrange, case_dir, cfl_adap_dt, cfl_const_dt, cfl_target, chem_params, coefficient_of_restitution, & + & collision_model, collision_time, cont_damage, cont_damage_s, cyl_coord, down_sample, dt, fd_order, fft_wrt, & + & file_per_process, fluid_pp, g_x, g_y, g_z, hyper_cleaning, hyper_cleaning_speed, hyper_cleaning_tau, hyperelasticity, & + & hypoelasticity, ib, ib_coefficient_of_friction, ib_state_wrt, ic_beta, ic_eps, int_comp, integral, integral_wrt, k_x, k_y, & + & k_z, lag_params, low_Mach, m, mixture_err, model_eqns, mp_weno, mpp_lim, muscl_eps, n, n_start, null_weights, & + & num_bc_patches, num_ibs, num_igr_iters, num_igr_warm_start_iters, num_integrals, num_probes, num_source, & + & nv_uvm_igr_temps_on_gpu, nv_uvm_out_of_core, nv_uvm_pref_gpu, p, p_x, p_y, p_z, palpha_eps, parallel_io, patch_ib, pi_fac, & + & poly_sigma, polydisperse, polytropic, precision, pref, prim_vars_wrt, probe, probe_wrt, ptgalpha_eps, qbmm, rdma_mpi, & + & relax, relax_model, rhoref, riemann_solver, run_time_info, sigma, surface_tension, t_save, t_step_old, t_step_print, & + & t_step_save, t_step_start, t_step_stop, t_stop, tau_star, teno_CT, thermal, time_stepper, w_x, w_y, w_z, wave_speeds, & + & weno_Re_flux, weno_avg, weno_eps, x_a, x_b, x_domain, y_a, y_b, y_domain, z_a, z_b, z_domain, & +#:if not MFC_CASE_OPTIMIZATION + & igr, igr_iter_solver, igr_order, igr_pres_lim, mapped_weno, mhd, muscl_lim, muscl_order, nb, num_fluids, recon_type, & + & relativity, teno, viscous, weno_order, wenoz, wenoz_q +#:endif diff --git a/toolchain/mfc/generate.py b/toolchain/mfc/generate.py index a6a6273c38..374e57b6b1 100644 --- a/toolchain/mfc/generate.py +++ b/toolchain/mfc/generate.py @@ -76,6 +76,11 @@ def generate(): (docs_dir / "parameters.md", generate_parameter_docs()), ] + _constraint_docs(docs_dir) + from .params.generators.fortran_gen import get_generated_files + + include_dir = Path(MFC_ROOT_DIR) / "src" / "common" / "include" + files += get_generated_files(include_dir) + all_ok = True for path, content in files: if not _check_or_write(path, content, check_mode): diff --git a/toolchain/mfc/params/generators/fortran_gen.py b/toolchain/mfc/params/generators/fortran_gen.py index 87a12ee445..88761a0bef 100644 --- a/toolchain/mfc/params/generators/fortran_gen.py +++ b/toolchain/mfc/params/generators/fortran_gen.py @@ -3,23 +3,32 @@ Generates namelist fragments and simple scalar declaration fragments per target (pre/sim/post). Output consumed by generate.py. + +Output format matches ffmt (the MFC Fortran formatter) so that +./mfc.sh format is idempotent on these generated files. """ import re from pathlib import Path from typing import List, Tuple -import mfc.params.definitions # noqa: F401 — triggers registry population +import mfc.params.definitions # noqa: F401 - triggers registry population from ..namelist_targets import CASE_OPT_EXCLUDE, NAMELIST_VARS from ..registry import REGISTRY from ..schema import ParamDef, ParamType -_HEADER = ( - "! AUTO-GENERATED — do not edit directly.\n" - "! Regenerate: ./mfc.sh generate\n" - "!\n" -) +# ffmt collapses the two header lines into one and uses ASCII hyphen +_HEADER = "! AUTO-GENERATED - do not edit directly. Regenerate: ./mfc.sh generate\n!\n" + +# ffmt formats Fortran with max 130-char lines and 4-space continuation indent +_MAX_LINE = 130 +_FIRST_PREFIX = "namelist /user_inputs/ " +_CONT_PREFIX = " & " +_CONT2_PREFIX = " & " # second-level continuation (inside Fypp #:if block) + +# ffmt aligns '::' to a fixed column; widest type is character(LEN=path_len) = 23 chars +_DECL_COL = 24 # pad type string to this width before '::' def get_namelist_var(param_name: str) -> str: @@ -47,7 +56,7 @@ def fortran_type_decl(param: ParamDef) -> str: def _is_simple_scalar(name: str) -> bool: - """Return True if name has no '%' and no '(' — i.e. a plain simple variable.""" + """Return True if name has no '%' and no '(' - i.e. a plain simple variable.""" return "%" not in name and "(" not in name @@ -56,44 +65,42 @@ def _vars_for_target(target: str) -> List[str]: return sorted(v for v, ts in NAMELIST_VARS.items() if target in ts) -def _format_namelist(vars_list: List[str]) -> str: +def _pack_namelist(vars_list: List[str], first_prefix: str, cont_prefix: str, max_line: int) -> List[str]: """ - Format a list of variable names as a Fortran namelist continuation block. + Pack a list of variable names into Fortran namelist continuation lines. - The first line starts with 'namelist /user_inputs/ '. - Continuation lines use ' & ' prefix (4 spaces + ampersand + space). - All lines except the last end with ', &' for continuation. - Lines are wrapped at 80 characters. + Returns a list of lines WITHOUT trailing newlines. + All lines except the last end with ', &'. """ if not vars_list: - return "" - - FIRST_PREFIX = "namelist /user_inputs/ " - CONT_PREFIX = " & " - MAX_WIDTH = 80 + return [] lines: List[str] = [] - prefix = FIRST_PREFIX + prefix = first_prefix current_vars: List[str] = [] current_len = len(prefix) for var in vars_list: - # Each var takes len(var) + 2 for ", " separator (except possibly last) additional = len(var) + (2 if current_vars else 0) - if current_vars and current_len + additional > MAX_WIDTH: - # Flush current line with continuation + if current_vars and current_len + additional + 3 > max_line: + # Flush with continuation marker lines.append(prefix + ", ".join(current_vars) + ", &") - prefix = CONT_PREFIX + prefix = cont_prefix current_vars = [var] - current_len = len(CONT_PREFIX) + len(var) + current_len = len(cont_prefix) + len(var) else: current_vars.append(var) current_len += additional - # Emit the last line (no trailing continuation — caller adds if needed) if current_vars: lines.append(prefix + ", ".join(current_vars)) + return lines + + +def _format_namelist(vars_list: List[str]) -> str: + """Format vars as a Fortran namelist statement block (no trailing newline).""" + lines = _pack_namelist(vars_list, _FIRST_PREFIX, _CONT_PREFIX, _MAX_LINE) return "\n".join(lines) @@ -109,28 +116,22 @@ def generate_namelist_fpp(target: str) -> str: normal = [v for v in all_vars if v not in CASE_OPT_EXCLUDE] opt = sorted(v for v in CASE_OPT_EXCLUDE if v in NAMELIST_VARS and "sim" in NAMELIST_VARS[v]) - lines = [_HEADER.rstrip()] - - # Normal vars always end with ", &" because opt vars follow (inside the #:if block) - nl = _format_namelist(normal) - lines.append(nl + ", &") - lines.append("#:if not MFC_CASE_OPTIMIZATION") + # Normal vars: last line gets ', &' since opt vars follow + nl_lines = _pack_namelist(normal, _FIRST_PREFIX, _CONT_PREFIX, _MAX_LINE) + nl_lines[-1] += ", &" - # Opt vars: each line except the last ends with ", &" - for i, var in enumerate(opt): - is_last = (i == len(opt) - 1) - if is_last: - lines.append(f" & {var}") - else: - lines.append(f" & {var}, &") + # Opt vars: pack using cont_prefix for first line, cont2_prefix for subsequent + opt_lines = _pack_namelist(opt, _CONT_PREFIX, _CONT2_PREFIX, _MAX_LINE) - lines.append("#:endif") - - return "\n".join(lines) + "\n" + all_lines = [_HEADER.rstrip()] + nl_lines + ["#:if not MFC_CASE_OPTIMIZATION"] + opt_lines + ["#:endif"] + return "\n".join(all_lines) + "\n" def generate_decls_fpp(target: str) -> str: - """Generate simple scalar Fortran variable declarations for a target.""" + """Generate simple scalar Fortran variable declarations for a target. + + Column-aligns '::' to match ffmt output (type padded to _DECL_COL chars). + """ assert target in ("pre", "sim", "post") all_params = REGISTRY.all_params vars_for_target = _vars_for_target(target) @@ -141,17 +142,19 @@ def generate_decls_fpp(target: str) -> str: param = all_params.get(name) if param is None: continue - lines.append(f"{fortran_type_decl(param)} :: {name}") + type_str = fortran_type_decl(param) + padded = type_str.ljust(_DECL_COL) + lines.append(f"{padded}:: {name}") return "\n".join(lines) + "\n" def get_generated_files(include_dir: Path) -> List[Tuple[Path, str]]: """Return (output_path, content) for all six generated .fpp files.""" return [ - (include_dir / "generated_namelist_pre.fpp", generate_namelist_fpp("pre")), - (include_dir / "generated_namelist_sim.fpp", generate_namelist_fpp("sim")), + (include_dir / "generated_namelist_pre.fpp", generate_namelist_fpp("pre")), + (include_dir / "generated_namelist_sim.fpp", generate_namelist_fpp("sim")), (include_dir / "generated_namelist_post.fpp", generate_namelist_fpp("post")), - (include_dir / "generated_decls_pre.fpp", generate_decls_fpp("pre")), - (include_dir / "generated_decls_sim.fpp", generate_decls_fpp("sim")), - (include_dir / "generated_decls_post.fpp", generate_decls_fpp("post")), + (include_dir / "generated_decls_pre.fpp", generate_decls_fpp("pre")), + (include_dir / "generated_decls_sim.fpp", generate_decls_fpp("sim")), + (include_dir / "generated_decls_post.fpp", generate_decls_fpp("post")), ] diff --git a/toolchain/tests/params/test_fortran_gen.py b/toolchain/tests/params/test_fortran_gen.py index f2e7aeb6cb..d8ee6dfb3e 100644 --- a/toolchain/tests/params/test_fortran_gen.py +++ b/toolchain/tests/params/test_fortran_gen.py @@ -1,93 +1,125 @@ def test_get_namelist_var_simple(): from mfc.params.generators.fortran_gen import get_namelist_var + assert get_namelist_var("m") == "m" assert get_namelist_var("dt") == "dt" + def test_get_namelist_var_indexed_family(): from mfc.params.generators.fortran_gen import get_namelist_var + assert get_namelist_var("fluid_pp(1)%gamma") == "fluid_pp" assert get_namelist_var("patch_icpp(3)%geometry") == "patch_icpp" + def test_get_namelist_var_struct_member(): from mfc.params.generators.fortran_gen import get_namelist_var + assert get_namelist_var("bc_x%beg") == "bc_x" assert get_namelist_var("lag_params%solver_approach") == "lag_params" + def test_fortran_type_int(): from mfc.params.generators.fortran_gen import fortran_type_decl from mfc.params.schema import ParamDef, ParamType + assert fortran_type_decl(ParamDef(name="x", param_type=ParamType.INT)) == "integer" + def test_fortran_type_real(): from mfc.params.generators.fortran_gen import fortran_type_decl from mfc.params.schema import ParamDef, ParamType + assert fortran_type_decl(ParamDef(name="x", param_type=ParamType.REAL)) == "real(wp)" + def test_fortran_type_log(): from mfc.params.generators.fortran_gen import fortran_type_decl from mfc.params.schema import ParamDef, ParamType + assert fortran_type_decl(ParamDef(name="x", param_type=ParamType.LOG)) == "logical" + def test_fortran_type_str(): from mfc.params.generators.fortran_gen import fortran_type_decl from mfc.params.schema import ParamDef, ParamType + p = ParamDef(name="case_dir", param_type=ParamType.STR, str_len="path_len") assert fortran_type_decl(p) == "character(LEN=path_len)" + def test_namelist_contains_common_vars(): from mfc.params.generators.fortran_gen import generate_namelist_fpp + for target in ("pre", "sim", "post"): c = generate_namelist_fpp(target) for v in ("m", "n", "p", "bc_x", "case_dir", "fluid_pp"): assert v in c, f"{v!r} missing from {target} namelist" + def test_sim_namelist_case_opt_guard(): from mfc.params.generators.fortran_gen import generate_namelist_fpp + c = generate_namelist_fpp("sim") assert "#:if not MFC_CASE_OPTIMIZATION" in c assert "weno_order" in c assert "num_fluids" in c + def test_pre_namelist_has_patch_icpp(): from mfc.params.generators.fortran_gen import generate_namelist_fpp + c = generate_namelist_fpp("pre") assert "patch_icpp" in c assert "run_time_info" not in c + def test_post_namelist_has_sim_data(): from mfc.params.generators.fortran_gen import generate_namelist_fpp + c = generate_namelist_fpp("post") assert "sim_data" in c assert "patch_icpp" not in c + def test_decls_contains_simple_scalars(): from mfc.params.generators.fortran_gen import generate_decls_fpp + for target in ("pre", "sim", "post"): c = generate_decls_fpp(target) assert "integer" in c assert "real(wp)" in c assert "logical" in c + def test_decls_dt_for_sim(): from mfc.params.generators.fortran_gen import generate_decls_fpp - assert "real(wp) :: dt" in generate_decls_fpp("sim") + + # Column-aligned output: type padded to 24 chars before '::' + assert "real(wp) :: dt" in generate_decls_fpp("sim") + def test_decls_no_percent_vars(): from mfc.params.generators.fortran_gen import generate_decls_fpp + for target in ("pre", "sim", "post"): c = generate_decls_fpp(target) assert "bc_x%beg" not in c assert "fluid_pp(1)" not in c + def test_decls_case_dir(): from mfc.params.generators.fortran_gen import generate_decls_fpp + for target in ("pre", "sim", "post"): assert "character(LEN=path_len) :: case_dir" in generate_decls_fpp(target) + def test_get_generated_files_returns_six(): from pathlib import Path from mfc.params.generators.fortran_gen import get_generated_files + files = get_generated_files(Path("/tmp")) assert len(files) == 6 names = {p.name for p, _ in files} From 71f9a1feb0ddbc8131aa5605ff4476e6270c9079 Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Wed, 27 May 2026 23:19:16 -0400 Subject: [PATCH 06/33] refactor(fortran): replace hand-written namelists with generated includes --- src/common/include/generated_decls_post.fpp | 3 -- .../include/generated_namelist_post.fpp | 10 +++--- src/post_process/m_start_up.fpp | 13 +------- src/pre_process/m_start_up.fpp | 12 +------ src/simulation/m_start_up.fpp | 32 +------------------ toolchain/mfc/lint_param_docs.py | 9 ++++++ toolchain/mfc/params/namelist_parser.py | 13 ++++++-- toolchain/mfc/params/namelist_targets.py | 6 ++-- 8 files changed, 31 insertions(+), 67 deletions(-) diff --git a/src/common/include/generated_decls_post.fpp b/src/common/include/generated_decls_post.fpp index 0797b61237..4b987d38ba 100644 --- a/src/common/include/generated_decls_post.fpp +++ b/src/common/include/generated_decls_post.fpp @@ -64,7 +64,6 @@ logical :: mixture_err integer :: model_eqns logical :: mom_wrt logical :: mpp_lim -integer :: muscl_lim integer :: muscl_order integer :: n integer :: n_start @@ -75,7 +74,6 @@ integer :: num_ibs logical :: omega_wrt logical :: output_partial_domain integer :: p -real(wp) :: palpha_eps logical :: parallel_io logical :: pi_inf_wrt real(wp) :: poly_sigma @@ -86,7 +84,6 @@ real(wp) :: pref logical :: pres_inf_wrt logical :: pres_wrt logical :: prim_vars_wrt -real(wp) :: ptgalpha_eps logical :: qbmm logical :: qm_wrt integer :: recon_type diff --git a/src/common/include/generated_namelist_post.fpp b/src/common/include/generated_namelist_post.fpp index 71edbde252..16d981c763 100644 --- a/src/common/include/generated_namelist_post.fpp +++ b/src/common/include/generated_namelist_post.fpp @@ -7,8 +7,8 @@ namelist /user_inputs/ Bx0, Ca, E_wrt, G, R0ref, Re_inv, Web, adv_n, alpha_rho_e & hypoelasticity, ib, ib_state_wrt, igr, igr_order, lag_betaC_wrt, lag_betaT_wrt, lag_db_wrt, lag_dphidt_wrt, lag_header, & & lag_id_wrt, lag_mg_wrt, lag_mv_wrt, lag_pos_prev_wrt, lag_pos_wrt, lag_pres_wrt, lag_r0_wrt, lag_rad_wrt, lag_rmax_wrt, & & lag_rmin_wrt, lag_rvel_wrt, lag_txt_wrt, lag_vel_wrt, liutex_wrt, m, mhd, mixture_err, model_eqns, mom_wrt, mpp_lim, & - & muscl_lim, muscl_order, n, n_start, nb, num_bc_patches, num_fluids, num_ibs, omega_wrt, output_partial_domain, p, & - & palpha_eps, parallel_io, pi_inf_wrt, poly_sigma, polydisperse, polytropic, precision, pref, pres_inf_wrt, pres_wrt, & - & prim_vars_wrt, ptgalpha_eps, qbmm, qm_wrt, recon_type, relativity, relax, relax_model, rho_wrt, rhoref, schlieren_alpha, & - & schlieren_wrt, sigR, sigma, sim_data, surface_tension, t_save, t_step_save, t_step_start, t_step_stop, t_stop, thermal, & - & vel_wrt, weno_order, x_output, y_output, z_output + & muscl_order, n, n_start, nb, num_bc_patches, num_fluids, num_ibs, omega_wrt, output_partial_domain, p, parallel_io, & + & pi_inf_wrt, poly_sigma, polydisperse, polytropic, precision, pref, pres_inf_wrt, pres_wrt, prim_vars_wrt, qbmm, qm_wrt, & + & recon_type, relativity, relax, relax_model, rho_wrt, rhoref, schlieren_alpha, schlieren_wrt, sigR, sigma, sim_data, & + & surface_tension, t_save, t_step_save, t_step_start, t_step_stop, t_stop, thermal, vel_wrt, weno_order, x_output, y_output, & + & z_output diff --git a/src/post_process/m_start_up.fpp b/src/post_process/m_start_up.fpp index ce51702f93..0ed57f7e4e 100644 --- a/src/post_process/m_start_up.fpp +++ b/src/post_process/m_start_up.fpp @@ -59,18 +59,7 @@ contains integer :: iostatus character(len=1000) :: line - namelist /user_inputs/ case_dir, m, n, p, t_step_start, t_step_stop, t_step_save, model_eqns, num_fluids, mpp_lim, & - & weno_order, bc_x, bc_y, bc_z, fluid_pp, bub_pp, format, precision, output_partial_domain, x_output, y_output, & - & z_output, hypoelasticity, G, mhd, chem_wrt_Y, chem_wrt_T, avg_state, alpha_rho_wrt, rho_wrt, mom_wrt, vel_wrt, & - & E_wrt, fft_wrt, pres_wrt, alpha_wrt, gamma_wrt, heat_ratio_wrt, pi_inf_wrt, pres_inf_wrt, cons_vars_wrt, & - & prim_vars_wrt, c_wrt, omega_wrt, qm_wrt, liutex_wrt, schlieren_wrt, schlieren_alpha, fd_order, mixture_err, & - & alt_soundspeed, flux_lim, flux_wrt, cyl_coord, parallel_io, rhoref, pref, bubbles_euler, qbmm, sigR, R0ref, nb, & - & polytropic, thermal, Ca, Web, Re_inv, polydisperse, poly_sigma, file_per_process, relax, relax_model, cf_wrt, & - & sigma, adv_n, ib, num_ibs, cfl_adap_dt, cfl_const_dt, t_save, t_stop, n_start, cfl_target, surface_tension, & - & bubbles_lagrange, sim_data, hyperelasticity, Bx0, relativity, cont_damage, hyper_cleaning, num_bc_patches, igr, & - & igr_order, down_sample, recon_type, muscl_order, lag_header, lag_txt_wrt, lag_db_wrt, lag_id_wrt, lag_pos_wrt, & - & lag_pos_prev_wrt, lag_vel_wrt, lag_rad_wrt, lag_rvel_wrt, lag_r0_wrt, lag_rmax_wrt, lag_rmin_wrt, lag_dphidt_wrt, & - & lag_pres_wrt, lag_mv_wrt, lag_mg_wrt, lag_betaT_wrt, lag_betaC_wrt, alpha_rho_e_wrt, ib_state_wrt + #:include 'generated_namelist_post.fpp' file_loc = 'post_process.inp' inquire (FILE=trim(file_loc), EXIST=file_check) diff --git a/src/pre_process/m_start_up.fpp b/src/pre_process/m_start_up.fpp index 25597baad7..0a1e7f8d9c 100644 --- a/src/pre_process/m_start_up.fpp +++ b/src/pre_process/m_start_up.fpp @@ -74,17 +74,7 @@ contains integer :: iostatus character(len=1000) :: line - namelist /user_inputs/ case_dir, old_grid, old_ic, t_step_old, t_step_start, m, n, p, x_domain, y_domain, z_domain, & - & stretch_x, stretch_y, stretch_z, a_x, a_y, a_z, x_a, y_a, z_a, x_b, y_b, z_b, model_eqns, num_fluids, mpp_lim, & - & weno_order, bc_x, bc_y, bc_z, num_patches, hypoelasticity, mhd, patch_icpp, fluid_pp, bub_pp, precision, & - & parallel_io, mixlayer_vel_profile, mixlayer_vel_coef, mixlayer_perturb, mixlayer_perturb_nk, mixlayer_perturb_k0, & - & pi_fac, perturb_flow, perturb_flow_fluid, perturb_flow_mag, perturb_sph, perturb_sph_fluid, fluid_rho, cyl_coord, & - & loops_x, loops_y, loops_z, rhoref, pref, bubbles_euler, R0ref, nb, polytropic, thermal, Ca, Web, Re_inv, & - & polydisperse, poly_sigma, qbmm, sigR, sigV, dist_type, rhoRV, file_per_process, relax, relax_model, palpha_eps, & - & ptgalpha_eps, ib, num_ibs, patch_ib, sigma, adv_n, cfl_adap_dt, cfl_const_dt, n_start, n_start_old, & - & surface_tension, hyperelasticity, pre_stress, elliptic_smoothing, elliptic_smoothing_iters, viscous, & - & bubbles_lagrange, num_bc_patches, patch_bc, Bx0, relativity, cont_damage, igr, igr_order, down_sample, recon_type, & - & muscl_order, hyper_cleaning, simplex_perturb, simplex_params, fft_wrt + #:include 'generated_namelist_pre.fpp' file_loc = 'pre_process.inp' inquire (FILE=trim(file_loc), EXIST=file_check) diff --git a/src/simulation/m_start_up.fpp b/src/simulation/m_start_up.fpp index e946ebd614..ea872f1981 100644 --- a/src/simulation/m_start_up.fpp +++ b/src/simulation/m_start_up.fpp @@ -82,37 +82,7 @@ contains character(len=1000) :: line - namelist /user_inputs/ case_dir, run_time_info, m, n, p, dt, & - t_step_start, t_step_stop, t_step_save, t_step_print, & - model_eqns, mpp_lim, time_stepper, weno_eps, muscl_eps, & - rdma_mpi, teno_CT, mp_weno, weno_avg, & - riemann_solver, low_Mach, wave_speeds, avg_state, & - bc_x, bc_y, bc_z, & - x_a, y_a, z_a, x_b, y_b, z_b, & - x_domain, y_domain, z_domain, & - hypoelasticity, & - ib, num_ibs, patch_ib, & - collision_model, coefficient_of_restitution, collision_time, & - ib_coefficient_of_friction, ib_state_wrt, & - fluid_pp, bub_pp, probe_wrt, prim_vars_wrt, & - fd_order, probe, num_probes, t_step_old, & - alt_soundspeed, mixture_err, weno_Re_flux, & - null_weights, precision, parallel_io, cyl_coord, & - rhoref, pref, bubbles_euler, bubble_model, & - R0ref, chem_params, & - #:if not MFC_CASE_OPTIMIZATION - nb, mapped_weno, wenoz, teno, wenoz_q, weno_order, & - num_fluids, mhd, relativity, igr_order, viscous, & - igr_iter_solver, igr, igr_pres_lim, & - recon_type, muscl_order, muscl_lim, & - #:endif - Ca, Web, Re_inv, acoustic_source, acoustic, num_source, polytropic, thermal, integral, integral_wrt, num_integrals, & - & polydisperse, poly_sigma, qbmm, relax, relax_model, palpha_eps, ptgalpha_eps, file_per_process, sigma, pi_fac, & - & adv_n, adap_dt, adap_dt_tol, adap_dt_max_iters, bf_x, bf_y, bf_z, k_x, k_y, k_z, w_x, w_y, w_z, p_x, p_y, p_z, g_x, & - & g_y, g_z, n_start, t_save, t_stop, cfl_adap_dt, cfl_const_dt, cfl_target, surface_tension, bubbles_lagrange, & - & lag_params, hyperelasticity, R0ref, num_bc_patches, Bx0, cont_damage, tau_star, cont_damage_s, alpha_bar, & - & hyper_cleaning, hyper_cleaning_speed, hyper_cleaning_tau, alf_factor, num_igr_iters, num_igr_warm_start_iters, & - & int_comp, ic_eps, ic_beta, nv_uvm_out_of_core, nv_uvm_igr_temps_on_gpu, nv_uvm_pref_gpu, down_sample, fft_wrt + #:include 'generated_namelist_sim.fpp' inquire (FILE=trim(file_path), EXIST=file_exist) diff --git a/toolchain/mfc/lint_param_docs.py b/toolchain/mfc/lint_param_docs.py index 657ddae5af..fd0c36cc41 100644 --- a/toolchain/mfc/lint_param_docs.py +++ b/toolchain/mfc/lint_param_docs.py @@ -83,6 +83,15 @@ def _param_appears_in_case_md(param_base: str, tokens: set[str], text: str) -> b def _parse_namelist_params(fpp_path: Path) -> set[str]: """Parse parameter names from a namelist /user_inputs/ block in an fpp file.""" text = fpp_path.read_text(encoding="utf-8") + + # If the namelist is in a #:include'd generated file, resolve it. + include_match = re.search(r"#:include\s+'(generated_namelist_\w+\.fpp)'", text) + if include_match: + include_name = include_match.group(1) + include_path = fpp_path.parent.parent / "common" / "include" / include_name + if include_path.exists(): + text = include_path.read_text(encoding="utf-8") + params = set() in_namelist = False diff --git a/toolchain/mfc/params/namelist_parser.py b/toolchain/mfc/params/namelist_parser.py index 52385255d3..d99ca7ad08 100644 --- a/toolchain/mfc/params/namelist_parser.py +++ b/toolchain/mfc/params/namelist_parser.py @@ -408,9 +408,18 @@ def parse_namelist_from_file(filepath: Path) -> Set[str]: """ content = filepath.read_text() + # Handle #:include 'generated_namelist_*.fpp' — resolve to the included file. + include_match = re.search(r"#:include\s+'(generated_namelist_\w+\.fpp)'", content) + if include_match: + include_name = include_match.group(1) + include_path = filepath.parent.parent / "common" / "include" / include_name + if not include_path.exists(): + raise ValueError(f"Included namelist file not found: {include_path}") + content = include_path.read_text() + # Find the namelist block - starts with "namelist /user_inputs/" - # and continues until a line without continuation (&) or a blank line - namelist_match = re.search(r"namelist\s+/user_inputs/\s*(.+?)(?=\n\s*\n|\n\s*!(?!\s*&)|\n\s*[a-zA-Z_]+\s*=)", content, re.DOTALL | re.IGNORECASE) + # and continues until a line without continuation (&), a blank line, or end-of-string + namelist_match = re.search(r"namelist\s+/user_inputs/\s*(.+?)(?=\n\s*\n|\n\s*!(?!\s*&)|\n\s*[a-zA-Z_]+\s*=|$)", content, re.DOTALL | re.IGNORECASE) if not namelist_match: raise ValueError(f"Could not find namelist /user_inputs/ in {filepath}") diff --git a/toolchain/mfc/params/namelist_targets.py b/toolchain/mfc/params/namelist_targets.py index 9de64f1a14..12754e475e 100644 --- a/toolchain/mfc/params/namelist_targets.py +++ b/toolchain/mfc/params/namelist_targets.py @@ -72,8 +72,8 @@ "mpp_lim": _ALL, "relax": _ALL, "relax_model": _ALL, - "palpha_eps": _ALL, - "ptgalpha_eps": _ALL, + "palpha_eps": _PRE_SIM, + "ptgalpha_eps": _PRE_SIM, # --- WENO / reconstruction --- "weno_order": {"pre", "post"}, "weno_eps": {"sim"}, @@ -86,7 +86,7 @@ "muscl_eps": {"sim"}, "recon_type": {"pre", "post"}, "muscl_order": {"pre", "post"}, - "muscl_lim": {"post"}, + "muscl_lim": set(), "int_comp": {"sim"}, "ic_eps": {"sim"}, "ic_beta": {"sim"}, From 7921c8f228ca8f74a64edfe842b6183338eb7ec5 Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Wed, 27 May 2026 23:35:30 -0400 Subject: [PATCH 07/33] refactor(fortran): replace hand-written scalar decls with generated includes --- src/common/include/generated_decls_post.fpp | 9 +- src/common/include/generated_decls_pre.fpp | 3 +- src/common/include/generated_decls_sim.fpp | 2 +- src/post_process/m_global_parameters.fpp | 121 ++------------ src/pre_process/m_global_parameters.fpp | 98 ++--------- src/simulation/m_global_parameters.fpp | 172 +++----------------- toolchain/mfc/params/definitions.py | 10 +- 7 files changed, 46 insertions(+), 369 deletions(-) diff --git a/src/common/include/generated_decls_post.fpp b/src/common/include/generated_decls_post.fpp index 4b987d38ba..060254edb3 100644 --- a/src/common/include/generated_decls_post.fpp +++ b/src/common/include/generated_decls_post.fpp @@ -7,8 +7,6 @@ real(wp) :: R0ref real(wp) :: Re_inv real(wp) :: Web logical :: adv_n -logical :: alpha_rho_wrt -logical :: alpha_wrt logical :: alt_soundspeed integer :: avg_state logical :: bubbles_euler @@ -28,7 +26,6 @@ integer :: fd_order logical :: fft_wrt logical :: file_per_process integer :: flux_lim -logical :: flux_wrt integer :: format logical :: gamma_wrt logical :: heat_ratio_wrt @@ -62,16 +59,14 @@ integer :: m logical :: mhd logical :: mixture_err integer :: model_eqns -logical :: mom_wrt logical :: mpp_lim integer :: muscl_order integer :: n integer :: n_start -real(wp) :: nb +integer :: nb integer :: num_bc_patches integer :: num_fluids integer :: num_ibs -logical :: omega_wrt logical :: output_partial_domain integer :: p logical :: parallel_io @@ -92,7 +87,6 @@ logical :: relax integer :: relax_model logical :: rho_wrt real(wp) :: rhoref -real(wp) :: schlieren_alpha logical :: schlieren_wrt real(wp) :: sigR real(wp) :: sigma @@ -104,5 +98,4 @@ integer :: t_step_start integer :: t_step_stop real(wp) :: t_stop integer :: thermal -logical :: vel_wrt integer :: weno_order diff --git a/src/common/include/generated_decls_pre.fpp b/src/common/include/generated_decls_pre.fpp index bb3342c7d9..26b7b04ca6 100644 --- a/src/common/include/generated_decls_pre.fpp +++ b/src/common/include/generated_decls_pre.fpp @@ -22,7 +22,6 @@ logical :: elliptic_smoothing integer :: elliptic_smoothing_iters logical :: fft_wrt logical :: file_per_process -real(wp) :: fluid_rho logical :: hyper_cleaning logical :: hyperelasticity logical :: hypoelasticity @@ -45,7 +44,7 @@ integer :: muscl_order integer :: n integer :: n_start integer :: n_start_old -real(wp) :: nb +integer :: nb integer :: num_bc_patches integer :: num_fluids integer :: num_ibs diff --git a/src/common/include/generated_decls_sim.fpp b/src/common/include/generated_decls_sim.fpp index 87060cb978..cc4f9dfc35 100644 --- a/src/common/include/generated_decls_sim.fpp +++ b/src/common/include/generated_decls_sim.fpp @@ -70,7 +70,7 @@ integer :: muscl_lim integer :: muscl_order integer :: n integer :: n_start -real(wp) :: nb +integer :: nb logical :: null_weights integer :: num_bc_patches integer :: num_fluids diff --git a/src/post_process/m_global_parameters.fpp b/src/post_process/m_global_parameters.fpp index aed6e616c4..967d62755d 100644 --- a/src/post_process/m_global_parameters.fpp +++ b/src/post_process/m_global_parameters.fpp @@ -17,10 +17,11 @@ module m_global_parameters implicit none + #:include 'generated_decls_post.fpp' + !> @name Logistics !> @{ - integer :: num_procs !< Number of processors - character(LEN=path_len) :: case_dir !< Case folder location + integer :: num_procs !< Number of processors !> @} ! Computational Domain Parameters @@ -28,9 +29,7 @@ module m_global_parameters integer :: proc_rank !< Rank of the local processor !> @name Number of cells in the x-, y- and z-coordinate directions !> @{ - integer :: m, m_root - integer :: n - integer :: p + integer :: m_root !> @} !> @name Max and min number of cells in a direction of each combination of x-,y-, and z- @@ -39,7 +38,6 @@ module m_global_parameters !> @name Cylindrical coordinates (either axisymmetric or full 3D) !> @{ - logical :: cyl_coord integer :: grid_geometry !> @} @@ -66,50 +64,24 @@ module m_global_parameters real(wp), allocatable, dimension(:) :: dx, dy, dz !> @} - integer :: buff_size !< Number of ghost cells for boundary condition storage - integer :: t_step_start !< First time-step directory - integer :: t_step_stop !< Last time-step directory - integer :: t_step_save !< Interval between consecutive time-step directory + integer :: buff_size !< Number of ghost cells for boundary condition storage !> @name IO options for adaptive time-stepping !> @{ - logical :: cfl_adap_dt, cfl_const_dt, cfl_dt - real(wp) :: t_save - real(wp) :: t_stop - real(wp) :: cfl_target - integer :: n_save - integer :: n_start + logical :: cfl_dt + integer :: n_save !> @} ! NOTE: m_root, x_root_cb, x_root_cc = defragmented grid (1D only; equals m, x_cb, x_cc in serial) !> @name Simulation Algorithm Parameters !> @{ - integer :: model_eqns !< Multicomponent flow model - integer :: num_fluids !< Number of different fluids present in the flow - logical :: relax !< phase change - integer :: relax_model !< Phase change relaxation model - logical :: mpp_lim !< Maximum volume fraction limiter integer :: sys_size !< Number of unknowns in the system of equations - integer :: recon_type !< Which type of reconstruction to use - integer :: weno_order !< Order of accuracy for the WENO reconstruction - integer :: muscl_order !< Order of accuracy for the MUSCL reconstruction - logical :: mixture_err !< Mixture error limiter - logical :: alt_soundspeed !< Alternate sound speed - logical :: mhd !< Magnetohydrodynamics - logical :: relativity !< Relativity for RMHD - logical :: hypoelasticity !< Turn hypoelasticity on - logical :: hyperelasticity !< Turn hyperelasticity on logical :: elasticity !< elasticity modeling, true for hyper or hypo integer :: b_size !< Number of components in the b tensor integer :: tensor_size !< Number of components in the nonsymmetric tensor - logical :: cont_damage !< Continuum damage modeling - logical :: hyper_cleaning !< Hyperbolic cleaning for MHD - logical :: igr !< enable IGR - integer :: igr_order !< IGR reconstruction order logical, parameter :: chemistry = .${chemistry}$. !< Chemistry modeling !> @} - integer :: avg_state !< Average state evaluation method !> @name Annotations of the structure, i.e. the organization, of the state vectors !> @{ type(eqn_idx_info) :: eqn_idx !< All conserved-variable equation index ranges and scalars. @@ -122,7 +94,6 @@ module m_global_parameters ! Cell indices (InDices With BUFFer): includes buffer in simulation only type(int_bounds_info) :: idwbuff(1:3) - integer :: num_bc_patches logical :: bc_io !> @name Boundary conditions in the x-, y- and z-coordinate directions !> @{ @@ -133,12 +104,8 @@ module m_global_parameters integer, dimension(3) :: shear_indices !< Indices of the stress components that represent shear stress integer :: shear_BC_flip_num !< Number of shear stress components to reflect for boundary conditions integer, dimension(3, 2) :: shear_BC_flip_indices !< Shear stress BC reflection indices (1:3, 1:shear_BC_flip_num) - logical :: parallel_io !< Format of the data files - logical :: sim_data - logical :: file_per_process !< output format integer, allocatable, dimension(:) :: proc_coords !< Processor coordinates in MPI_CART_COMM integer, allocatable, dimension(:) :: start_idx !< Starting cell-center index of local processor in global grid - integer :: num_ibs !< Number of immersed boundaries #ifdef MFC_MPI type(mpi_io_var), public :: MPI_IO_DATA type(mpi_io_ib_var), public :: MPI_IO_IB_DATA @@ -159,10 +126,6 @@ module m_global_parameters real(wp), allocatable, dimension(:) :: adv !< Advection variables ! Formatted Database File(s) Structure Parameters - integer :: format !< Format of the database file(s) - integer :: precision !< Floating point precision of the database file(s) - logical :: down_sample !< down sampling of the database file(s) - logical :: output_partial_domain !< Specify portion of domain to output for post-processing type(bounds_info) :: x_output, y_output, z_output !< Portion of domain to output for post-processing type(int_bounds_info) :: x_output_idx, y_output_idx, z_output_idx !< Indices of domain to output for post-processing !> @name Size of the ghost zone layer in the x-, y- and z-coordinate directions. The definition of the ghost zone layers is only @@ -178,95 +141,31 @@ module m_global_parameters !! Schlieren function. !> @{ logical, dimension(num_fluids_max) :: alpha_rho_wrt - logical :: rho_wrt logical, dimension(3) :: mom_wrt logical, dimension(3) :: vel_wrt - integer :: flux_lim logical, dimension(3) :: flux_wrt - logical :: E_wrt logical, dimension(num_fluids_max) :: alpha_rho_e_wrt - logical :: fft_wrt - logical :: pres_wrt logical, dimension(num_fluids_max) :: alpha_wrt - logical :: gamma_wrt - logical :: heat_ratio_wrt - logical :: pi_inf_wrt - logical :: pres_inf_wrt - logical :: prim_vars_wrt - logical :: cons_vars_wrt - logical :: c_wrt logical, dimension(3) :: omega_wrt - logical :: qm_wrt - logical :: liutex_wrt - logical :: schlieren_wrt - logical :: cf_wrt - logical :: ib - logical :: ib_state_wrt logical :: chem_wrt_Y(1:num_species) - logical :: chem_wrt_T - logical :: lag_header - logical :: lag_txt_wrt - logical :: lag_db_wrt - logical :: lag_id_wrt - logical :: lag_pos_wrt - logical :: lag_pos_prev_wrt - logical :: lag_vel_wrt - logical :: lag_rad_wrt - logical :: lag_rvel_wrt - logical :: lag_r0_wrt - logical :: lag_rmax_wrt - logical :: lag_rmin_wrt - logical :: lag_dphidt_wrt - logical :: lag_pres_wrt - logical :: lag_mv_wrt - logical :: lag_mg_wrt - logical :: lag_betaT_wrt - logical :: lag_betaC_wrt !> @} real(wp), dimension(num_fluids_max) :: schlieren_alpha !< Per-fluid Schlieren intensity amplitude coefficients - integer :: fd_order !< Finite-difference order for vorticity and Schlieren derivatives integer :: fd_number !< Finite-difference half-stencil size: MAX(1, fd_order/2) - !> @name Reference parameters for Tait EOS - !> @{ - real(wp) :: rhoref, pref - !> @} - - type(chemistry_parameters) :: chem_params + type(chemistry_parameters) :: chem_params !> @name Bubble modeling variables and parameters !> @{ - integer :: nb - real(wp) :: Eu, Ca, Web, Re_inv + real(wp) :: Eu real(wp), dimension(:), allocatable :: weight, R0 - logical :: bubbles_euler - logical :: qbmm - logical :: polytropic - logical :: polydisperse - logical :: adv_n - integer :: thermal !< 1 = adiabatic, 2 = isotherm, 3 = transfer real(wp) :: phi_vg, phi_gv, Pe_c, Tw, k_vl, k_gl real(wp) :: gam_m real(wp), dimension(:), allocatable :: pb0, mass_g0, mass_v0, Pe_T, k_v, k_g real(wp), dimension(:), allocatable :: Re_trans_T, Re_trans_c, Im_trans_T, Im_trans_c, omegaN - real(wp) :: R0ref, p0ref, rho0ref, T0ref, ss, pv, vd, mu_l, mu_v, mu_g, gam_v, gam_g, M_v, M_g, cp_v, cp_g, R_v, R_g + real(wp) :: p0ref, rho0ref, T0ref, ss, pv, vd, mu_l, mu_v, mu_g, gam_v, gam_g, M_v, M_g, cp_v, cp_g, R_v, R_g real(wp) :: G - real(wp) :: poly_sigma - real(wp) :: sigR integer :: nmom !> @} - !> @name surface tension coefficient - !> @{ - real(wp) :: sigma - logical :: surface_tension - !> @} - - !> @name Lagrangian bubbles - !> @{ - logical :: bubbles_lagrange - !> @} - - real(wp) :: Bx0 !< Constant magnetic field in the x-direction (1D) real(wp) :: wall_time, wall_time_avg !< Wall time measurements contains diff --git a/src/pre_process/m_global_parameters.fpp b/src/pre_process/m_global_parameters.fpp index ae850a9134..9a83396fe9 100644 --- a/src/pre_process/m_global_parameters.fpp +++ b/src/pre_process/m_global_parameters.fpp @@ -17,21 +17,16 @@ module m_global_parameters implicit none + #:include 'generated_decls_pre.fpp' + ! Logistics - integer :: num_procs !< Number of processors - character(LEN=path_len) :: case_dir !< Case folder location - logical :: old_grid !< Use existing grid data - logical :: old_ic, non_axis_sym !< Use existing IC data - integer :: t_step_old, t_step_start !< Existing IC/grid folder - logical :: cfl_adap_dt, cfl_const_dt, cfl_dt - integer :: n_start, n_start_old + integer :: num_procs !< Number of processors + logical :: non_axis_sym !< Use existing IC data + logical :: cfl_dt ! Computational Domain Parameters integer :: proc_rank !< Rank of the local processor Number of cells in the x-, y- and z-coordinate directions - integer :: m - integer :: n - integer :: p !> @name Max and min number of cells in a direction of each combination of x-,y-, and z- type(cell_num_bounds) :: cells_bounds @@ -39,7 +34,6 @@ module m_global_parameters integer :: m_glb, n_glb, p_glb !< Global number of cells in each direction integer :: num_dims !< Number of spatial dimensions integer :: num_vels !< Number of velocity components (different from num_dims for mhd) - logical :: cyl_coord integer :: grid_geometry !< Cylindrical coordinates (either axisymmetric or full 3D) !> Locations of cell-centers (cc) in x-, y- and z-directions, respectively real(wp), allocatable, dimension(:) :: x_cc, y_cc, z_cc @@ -47,39 +41,14 @@ module m_global_parameters real(wp), allocatable, dimension(:) :: x_cb, y_cb, z_cb real(wp) :: dx, dy, dz !< Minimum cell-widths in the x-, y- and z-coordinate directions type(bounds_info) :: x_domain, y_domain, z_domain !< Locations of the domain bounds in the x-, y- and z-coordinate directions - logical :: stretch_x, stretch_y, stretch_z !< Grid stretching flags for the x-, y- and z-coordinate directions - ! Grid stretching: a_x/a_y/a_z = rate, x_a/y_a/z_a = location - real(wp) :: a_x, a_y, a_z - integer :: loops_x, loops_y, loops_z - real(wp) :: x_a, y_a, z_a - real(wp) :: x_b, y_b, z_b ! Simulation Algorithm Parameters - integer :: model_eqns !< Multicomponent flow model - logical :: relax !< activate phase change - integer :: relax_model !< Relax Model - real(wp) :: palpha_eps !< trigger parameter for the p relaxation procedure, phase change model - real(wp) :: ptgalpha_eps !< trigger parameter for the pTg relaxation procedure, phase change model - integer :: num_fluids !< Number of different fluids present in the flow - logical :: mpp_lim !< Alpha limiter integer :: sys_size !< Number of unknowns in the system of equations - integer :: recon_type !< Reconstruction Type integer :: weno_polyn !< Degree of the WENO polynomials (polyn) integer :: muscl_polyn !< Degree of the MUSCL polynomials (polyn) - integer :: weno_order !< Order of accuracy for the WENO reconstruction - integer :: muscl_order !< Order of accuracy for the MUSCL reconstruction - logical :: hypoelasticity !< activate hypoelasticity - logical :: hyperelasticity !< activate hyperelasticity logical :: elasticity !< elasticity modeling, true for hyper or hypo - logical :: mhd !< Magnetohydrodynamics - logical :: relativity !< Relativity for RMHD integer :: b_size !< Number of components in the b tensor integer :: tensor_size !< Number of components in the nonsymmetric tensor - logical :: pre_stress !< activate pre_stressed domain - logical :: cont_damage !< continuum damage modeling - logical :: hyper_cleaning !< Hyperbolic cleaning for MHD - logical :: igr !< Use information geometric regularization - integer :: igr_order !< IGR reconstruction order logical, parameter :: chemistry = .${chemistry}$. !< Chemistry modeling ! Annotations of the structure, i.e. the organization, of the state vectors type(eqn_idx_info) :: eqn_idx !< All conserved-variable equation index ranges and scalars. @@ -94,32 +63,12 @@ module m_global_parameters integer, dimension(3) :: shear_indices !< Indices of the stress components that represent shear stress integer :: shear_BC_flip_num !< Number of shear stress components to reflect for boundary conditions integer, dimension(3, 2) :: shear_BC_flip_indices !< Shear stress BC reflection indices (1:3, 1:shear_BC_flip_num) - logical :: parallel_io !< Format of the data files - logical :: file_per_process !< type of data output - integer :: precision !< Precision of output files - logical :: down_sample !< Down-sample the output data - logical :: mixlayer_vel_profile !< Set hyperbolic tangent streamwise velocity profile - real(wp) :: mixlayer_vel_coef !< Coefficient for the hyperbolic tangent streamwise velocity profile - logical :: mixlayer_perturb !< Superimpose instability waves to surrounding fluid flow - integer :: mixlayer_perturb_nk !< Number of Fourier modes for perturbation with mixlayer_perturb flag - real(wp) :: mixlayer_perturb_k0 !< Peak wavenumber for mixlayer perturbation (default: most unstable mode) - logical :: simplex_perturb type(simplex_noise_params) :: simplex_params - real(wp) :: pi_fac !< Factor for artificial pi_inf - logical :: viscous - logical :: bubbles_lagrange ! Perturb density of surrounding air so as to break symmetry of grid - logical :: perturb_flow - integer :: perturb_flow_fluid !< Fluid to be perturbed with perturb_flow flag - real(wp) :: perturb_flow_mag !< Magnitude of perturbation with perturb_flow flag - logical :: perturb_sph - integer :: perturb_sph_fluid !< Fluid to be perturbed with perturb_sph flag real(wp), dimension(num_fluids_max) :: fluid_rho - logical :: elliptic_smoothing - integer :: elliptic_smoothing_iters - integer, allocatable, dimension(:) :: proc_coords !< Processor coordinates in MPI_CART_COMM - integer, allocatable, dimension(:) :: start_idx !< Starting cell-center index of local processor in global grid + integer, allocatable, dimension(:) :: proc_coords !< Processor coordinates in MPI_CART_COMM + integer, allocatable, dimension(:) :: start_idx !< Starting cell-center index of local processor in global grid #ifdef MFC_MPI type(mpi_io_var), public :: MPI_IO_DATA character(LEN=name_len) :: mpiiofs @@ -127,34 +76,24 @@ module m_global_parameters #endif ! Initial Condition Parameters - integer :: num_patches !< Number of patches composing initial condition - type(ic_patch_parameters), dimension(num_patches_max) :: patch_icpp !< IC patch parameters (max: num_patches_max) - integer :: num_bc_patches !< Number of boundary condition patches - logical :: bc_io !< whether or not to save BC data - type(bc_patch_parameters), dimension(num_bc_patches_max) :: patch_bc !< Boundary condition patch parameters + type(ic_patch_parameters), dimension(num_patches_max) :: patch_icpp !< IC patch parameters (max: num_patches_max) + type(bc_patch_parameters), dimension(num_bc_patches_max) :: patch_bc !< Boundary condition patch parameters + logical :: bc_io !< whether or not to save BC data ! Fluids Physical Parameters type(physical_parameters), dimension(num_fluids_max) :: fluid_pp !< Stiffened gas EOS parameters and Reynolds numbers per fluid ! Subgrid Bubble Parameters type(subgrid_bubble_physical_parameters) :: bub_pp - real(wp) :: rhoref, pref !< Reference parameters for Tait EOS type(chemistry_parameters) :: chem_params !> @name Bubble modeling !> @{ - integer :: nb - real(wp) :: Ca, Web, Re_inv, Eu + real(wp) :: Eu real(wp), dimension(:), allocatable :: weight, R0 - logical :: bubbles_euler - logical :: qbmm !< Quadrature moment method integer :: nmom !< Number of carried moments - real(wp) :: sigR, sigV, rhoRV !< standard deviations in R/V - logical :: adv_n !< Solve the number density equation and compute alpha from number density !> @} !> @name Immersed Boundaries !> @{ - logical :: ib !< Turn immersed boundaries on - integer :: num_ibs !< Number of immersed boundaries integer :: Np type(ib_patch_parameters), dimension(num_ib_patches_max) :: patch_ib !< Immersed boundary patch parameters type(vec3_dt), allocatable, dimension(:) :: airfoil_grid_u, airfoil_grid_l @@ -162,30 +101,17 @@ module m_global_parameters !> @name Non-polytropic bubble gas compression !> @{ - logical :: polytropic - logical :: polydisperse - real(wp) :: poly_sigma - integer :: dist_type !< 1 = binormal, 2 = lognormal-normal - integer :: thermal !< 1 = adiabatic, 2 = isotherm, 3 = transfer real(wp) :: phi_vg, phi_gv, Pe_c, Tw, k_vl, k_gl real(wp) :: gam_m real(wp), dimension(:), allocatable :: pb0, mass_g0, mass_v0, Pe_T, k_v, k_g real(wp), dimension(:), allocatable :: Re_trans_T, Re_trans_c, Im_trans_T, Im_trans_c, omegaN - real(wp) :: R0ref, p0ref, rho0ref, T0ref, ss, pv, vd, mu_l, mu_v, mu_g, gam_v, gam_g, M_v, M_g, cp_v, cp_g, R_v, R_g - !> @} - - !> @name Surface Tension Modeling - !> @{ - real(wp) :: sigma - logical :: surface_tension + real(wp) :: p0ref, rho0ref, T0ref, ss, pv, vd, mu_l, mu_v, mu_g, gam_v, gam_g, M_v, M_g, cp_v, cp_g, R_v, R_g !> @} integer, allocatable, dimension(:,:,:) :: logic_grid type(pres_field) :: pb type(pres_field) :: mv - real(wp) :: Bx0 !< Constant magnetic field in the x-direction (1D) integer :: buff_size !< Number of ghost cells for boundary condition storage - logical :: fft_wrt contains diff --git a/src/simulation/m_global_parameters.fpp b/src/simulation/m_global_parameters.fpp index b91dd99e47..821258c167 100644 --- a/src/simulation/m_global_parameters.fpp +++ b/src/simulation/m_global_parameters.fpp @@ -18,20 +18,15 @@ module m_global_parameters implicit none + #:include 'generated_decls_sim.fpp' + real(wp) :: wall_time = 0 real(wp) :: wall_time_avg = 0 ! Logistics - integer :: num_procs !< Number of processors - character(LEN=path_len) :: case_dir !< Case folder location - logical :: run_time_info !< Run-time output flag - integer :: t_step_old !< Existing IC/grid folder + integer :: num_procs !< Number of processors ! Computational Domain Parameters integer :: proc_rank !< Rank of the local processor - !> @name Number of cells in the x-, y- and z-directions, respectively - !> @{ - integer :: m, n, p - !> @} !> @name Max and min number of cells in a direction of each combination of x-,y-, and z- type(cell_num_bounds) :: cells_bounds @@ -43,7 +38,6 @@ module m_global_parameters !> @name Cylindrical coordinates (either axisymmetric or full 3D) !> @{ - logical :: cyl_coord integer :: grid_geometry !> @} $:GPU_DECLARE(create='[cyl_coord, grid_geometry]') @@ -63,26 +57,12 @@ module m_global_parameters real(wp), target, allocatable, dimension(:) :: dx, dy, dz !> @} - real(wp) :: dt !< Size of the time-step $:GPU_DECLARE(create='[x_cb, y_cb, z_cb, x_cc, y_cc, z_cc, dx, dy, dz, dt, m, n, p]') - !> @name Starting time-step iteration, stopping time-step iteration and the number of time-step iterations between successive - !! solution backups, respectively - !> @{ - integer :: t_step_start, t_step_stop, t_step_save - !> @} - - !> @name Starting time, stopping time, and time between backups, simulation time, and prescribed cfl respectively - !> @{ - real(wp) :: t_stop, t_save, cfl_target - integer :: n_start - !> @} $:GPU_DECLARE(create='[cfl_target]') - logical :: cfl_adap_dt, cfl_const_dt, cfl_dt - integer :: t_step_print !< Number of time-steps between printouts + logical :: cfl_dt ! Simulation Algorithm Parameters - integer :: model_eqns !< Multicomponent flow model #:if MFC_CASE_OPTIMIZATION integer, parameter :: num_dims = ${num_dims}$ !< Number of spatial dimensions integer, parameter :: num_vels = ${num_vels}$ !< Number of velocity components (different from num_dims for mhd) @@ -90,9 +70,7 @@ module m_global_parameters integer :: num_dims !< Number of spatial dimensions integer :: num_vels !< Number of velocity components (different from num_dims for mhd) #:endif - logical :: mpp_lim !< Mixture physical parameters (MPP) limits - integer :: time_stepper !< Time-stepper algorithm - logical :: prim_vars_wrt + ! mpp_lim, time_stepper, prim_vars_wrt now in generated_decls_sim.fpp #:if MFC_CASE_OPTIMIZATION integer, parameter :: recon_type = ${recon_type}$ !< Reconstruction type @@ -117,74 +95,19 @@ module m_global_parameters logical, parameter :: igr_pres_lim = (${igr_pres_lim}$ /= 0) !< Limit to positive pressures for IGR logical, parameter :: viscous = (${viscous}$ /= 0) !< Viscous effects #:else - integer :: recon_type !< Reconstruction Type - integer :: weno_polyn !< Degree of the WENO polynomials (polyn) - integer :: muscl_polyn !< Degree of the MUSCL polynomials (polyn)i - integer :: weno_order !< Order of the WENO reconstruction - integer :: muscl_order !< Order of the MUSCL reconstruction - integer :: weno_num_stencils !< Number of stencils for WENO reconstruction (only different from weno_polyn for TENO(>5)) - integer :: muscl_lim !< MUSCL Limiter - integer :: num_fluids !< number of fluids in the simulation - logical :: wenojs !< WENO-JS (default) - logical :: mapped_weno !< WENO-M (WENO with mapping of nonlinear weights) - logical :: wenoz !< WENO-Z - logical :: teno !< TENO (Targeted ENO) - real(wp) :: wenoz_q !< Power constant for WENO-Z - logical :: mhd !< Magnetohydrodynamics - logical :: relativity !< Relativity (only for MHD) - integer :: igr_iter_solver !< IGR elliptic solver - integer :: igr_order !< Reconstruction order for IGR - logical :: igr !< Use information geometric regularization - logical :: igr_pres_lim !< Limit to positive pressures for IGR - logical :: viscous !< Viscous effects + integer :: weno_polyn !< Degree of the WENO polynomials (polyn) + integer :: muscl_polyn !< Degree of the MUSCL polynomials (polyn)i + integer :: weno_num_stencils !< Number of stencils for WENO reconstruction (only different from weno_polyn for TENO(>5)) + logical :: wenojs !< WENO-JS (default) #:endif - !> @name Variables for our of core IGR computation on NVIDIA - !> @{ - logical :: nv_uvm_out_of_core !< Enable out-of-core storage of q_cons_ts(2) in timestepping (default FALSE) - integer :: nv_uvm_igr_temps_on_gpu !< 0 => jac, jac_rhs, and jac_old on CPU - ! 1 => jac on GPU, jac_rhs and jac_old on CPU 2 => jac and jac_rhs on GPU, jac_old on CPU 3 => jac, jac_rhs, and jac_old on GPU - ! (default) - logical :: nv_uvm_pref_gpu !< Enable explicit gpu memory hints (default FALSE) - !> @} - - real(wp) :: muscl_eps !< MUSCL limiter slope-product threshold - real(wp) :: weno_eps !< Binding for the WENO nonlinear weights - real(wp) :: teno_CT !< Smoothness threshold for TENO - logical :: mp_weno !< Monotonicity preserving (MP) WENO - logical :: weno_avg !< Average left/right cell-boundary states - logical :: weno_Re_flux !< WENO reconstruct velocity gradients for viscous stress tensor - integer :: riemann_solver !< Riemann solver algorithm - integer :: low_Mach !< Low Mach number fix to HLLC Riemann solver - integer :: wave_speeds !< Wave speeds estimation method - integer :: avg_state !< Average state evaluation method - logical :: alt_soundspeed !< Alternate mixture sound speed - logical :: null_weights !< Null undesired WENO weights - logical :: mixture_err !< Mixture properties correction - logical :: hypoelasticity !< hypoelasticity modeling - logical :: hyperelasticity !< hyperelasticity modeling - integer :: int_comp !< Interface compression: 0=off, 1=THINC, 2=MTHINC - real(wp) :: ic_eps !< THINC Epsilon to compress on surface cells - real(wp) :: ic_beta !< THINC Sharpness Parameter $:GPU_DECLARE(create='[int_comp, ic_eps, ic_beta]') - integer :: hyper_model !< hyperelasticity solver algorithm - logical :: elasticity !< elasticity modeling, true for hyper or hypo - logical, parameter :: chemistry = .${chemistry}$. !< Chemistry modeling - logical :: shear_stress !< Shear stresses - logical :: bulk_stress !< Bulk stresses - logical :: cont_damage !< Continuum damage modeling - logical :: hyper_cleaning !< Hyperbolic cleaning for MHD for divB=0 - integer :: num_igr_iters !< number of iterations for elliptic solve - integer :: num_igr_warm_start_iters !< number of warm start iterations for elliptic solve - real(wp) :: alf_factor !< alpha factor for IGR - logical :: bodyForces - logical :: bf_x, bf_y, bf_z !< body force toggle in three directions - !> amplitude, frequency, and phase shift sinusoid in each direction - #:for dir in {'x', 'y', 'z'} - #:for param in {'k','w','p','g'} - real(wp) :: ${param}$_${dir}$ - #:endfor - #:endfor + integer :: hyper_model !< hyperelasticity solver algorithm + logical :: elasticity !< elasticity modeling, true for hyper or hypo + logical, parameter :: chemistry = .${chemistry}$. !< Chemistry modeling + logical :: shear_stress !< Shear stresses + logical :: bulk_stress !< Bulk stresses + logical :: bodyForces real(wp), dimension(3) :: accel_bf $:GPU_DECLARE(create='[accel_bf]') ! $:GPU_DECLARE(create='[k_x,w_x,p_x,g_x,k_y,w_y,p_y,g_y,k_z,w_z,p_z,g_z]') @@ -205,13 +128,8 @@ module m_global_parameters $:GPU_DECLARE(create='[hyperelasticity, hyper_model, elasticity, low_Mach]') $:GPU_DECLARE(create='[shear_stress, bulk_stress, cont_damage, hyper_cleaning]') - logical :: relax !< activate phase change - integer :: relax_model !< Relaxation model - real(wp) :: palpha_eps !< trigger parameter for the p relaxation procedure, phase change model - real(wp) :: ptgalpha_eps !< trigger parameter for the pTg relaxation procedure, phase change model $:GPU_DECLARE(create='[relax, relax_model, palpha_eps, ptgalpha_eps]') - integer :: num_bc_patches logical :: bc_io !> @name Boundary conditions (BC) in the x-, y- and z-directions, respectively !> @{ @@ -233,12 +151,6 @@ module m_global_parameters #endif type(bounds_info) :: x_domain, y_domain, z_domain $:GPU_DECLARE(create='[x_domain, y_domain, z_domain]') - real(wp) :: x_a, y_a, z_a - real(wp) :: x_b, y_b, z_b - logical :: parallel_io !< Format of the data files - logical :: file_per_process !< shared file or not when using parallel io - integer :: precision !< Precision of output files - logical :: down_sample !< down sample the output files $:GPU_DECLARE(create='[down_sample]') integer, allocatable, dimension(:) :: proc_coords !< Processor coordinates in MPI_CART_COMM @@ -320,32 +232,18 @@ module m_global_parameters type(physical_parameters), dimension(num_fluids_max) :: fluid_pp !< Stiffened gas EOS parameters and Reynolds numbers per fluid ! Subgrid Bubble Parameters type(subgrid_bubble_physical_parameters) :: bub_pp - integer :: fd_order !< Finite-difference order for CoM and flow probe derivatives integer :: fd_number !< Finite-difference half-stencil size: MAX(1, fd_order/2) $:GPU_DECLARE(create='[fd_order, fd_number]') - logical :: probe_wrt - logical :: integral_wrt - integer :: num_probes - integer :: num_integrals type(vec3_dt), dimension(num_probes_max) :: probe type(integral_parameters), dimension(num_probes_max) :: integral !> @name Reference density and pressure for Tait EOS !> @{ - real(wp) :: rhoref, pref - !> @} $:GPU_DECLARE(create='[rhoref, pref]') !> @name Immersed Boundaries !> @{ - logical :: ib - integer :: num_ibs - integer :: collision_model - real(wp) :: coefficient_of_restitution - real(wp) :: collision_time - real(wp) :: ib_coefficient_of_friction - logical :: ib_state_wrt type(ib_patch_parameters), dimension(num_ib_patches_max) :: patch_ib !< Immersed boundary patch parameters type(vec3_dt), allocatable, dimension(:) :: airfoil_grid_u, airfoil_grid_l integer :: Np @@ -358,44 +256,27 @@ module m_global_parameters !> @{ #:if MFC_CASE_OPTIMIZATION integer, parameter :: nb = ${nb}$ !< Number of eq. bubble sizes - #:else - integer :: nb !< Number of eq. bubble sizes #:endif - real(wp) :: Eu !< Euler number - real(wp) :: Ca !< Cavitation number - real(wp) :: Web !< Weber number - real(wp) :: Re_inv !< Inverse Reynolds number + real(wp) :: Eu !< Euler number $:GPU_DECLARE(create='[Eu, Ca, Web, Re_inv]') real(wp), dimension(:), allocatable :: weight !< Simpson quadrature weights real(wp), dimension(:), allocatable :: R0 !< Bubble sizes $:GPU_DECLARE(create='[weight, R0]') - logical :: bubbles_euler !< Bubbles euler on/off - logical :: polytropic !< Polytropic switch - logical :: polydisperse !< Polydisperse bubbles $:GPU_DECLARE(create='[bubbles_euler, polytropic, polydisperse]') - logical :: adv_n !< Solve the number density equation and compute alpha from number density - logical :: adap_dt !< Adaptive step size control - real(wp) :: adap_dt_tol !< Tolerance to control adaptive step size - integer :: adap_dt_max_iters !< Maximum number of iterations $:GPU_DECLARE(create='[adv_n, adap_dt, adap_dt_tol, adap_dt_max_iters]') - integer :: bubble_model !< Gilmore or Keller--Miksis bubble model - integer :: thermal !< Thermal behavior. 1 = adiabatic, 2 = isotherm, 3 = transfer $:GPU_DECLARE(create='[bubble_model, thermal]') - real(wp), allocatable, dimension(:,:,:) :: ptil !< Pressure modification - real(wp) :: poly_sigma !< log normal sigma for polydisperse PDF + real(wp), allocatable, dimension(:,:,:) :: ptil !< Pressure modification $:GPU_DECLARE(create='[ptil, poly_sigma]') - logical :: qbmm !< Quadrature moment method integer, parameter :: nmom = 6 !< Number of carried moments per R0 location integer :: nmomsp !< Number of moments required by ensemble-averaging integer :: nmomtot !< Total number of carried moments moments/transport equations - real(wp) :: pi_fac !< Factor for artificial pi_inf $:GPU_DECLARE(create='[qbmm, nmomsp, nmomtot, pi_fac]') #:if not MFC_CASE_OPTIMIZATION @@ -423,22 +304,18 @@ module m_global_parameters real(wp) :: gam, gam_m $:GPU_DECLARE(create='[gam, gam_m]') - real(wp) :: R0ref, p0ref, rho0ref, T0ref, ss, pv, vd, mu_l, mu_v, mu_g, gam_v, gam_g, M_v, M_g, cp_v, cp_g, R_v, R_g + real(wp) :: p0ref, rho0ref, T0ref, ss, pv, vd, mu_l, mu_v, mu_g, gam_v, gam_g, M_v, M_g, cp_v, cp_g, R_v, R_g $:GPU_DECLARE(create='[R0ref, p0ref, rho0ref, T0ref, ss, pv, vd, mu_l, mu_v, mu_g, gam_v, gam_g, M_v, M_g, cp_v, cp_g, R_v, R_g]') !> @} !> @name Acoustic acoustic_source parameters !> @{ - logical :: acoustic_source !< Acoustic source switch - type(acoustic_parameters), dimension(num_probes_max) :: acoustic !< Acoustic source parameters - integer :: num_source !< Number of acoustic sources + type(acoustic_parameters), dimension(num_probes_max) :: acoustic !< Acoustic source parameters !> @} $:GPU_DECLARE(create='[acoustic_source, acoustic, num_source]') !> @name Surface tension parameters !> @{ - real(wp) :: sigma - logical :: surface_tension $:GPU_DECLARE(create='[sigma, surface_tension]') !> @} @@ -447,7 +324,6 @@ module m_global_parameters real(wp) :: mytime !< Current simulation time real(wp) :: finaltime !< Final simulation time - logical :: rdma_mpi type(pres_field), allocatable, dimension(:) :: pb_ts type(pres_field), allocatable, dimension(:) :: mv_ts @@ -455,27 +331,19 @@ module m_global_parameters !> @name lagrangian subgrid bubble parameters !> @{! - logical :: bubbles_lagrange !< Lagrangian subgrid bubble model switch - type(bubbles_lagrange_parameters) :: lag_params !< Lagrange bubbles' parameters + type(bubbles_lagrange_parameters) :: lag_params !< Lagrange bubbles' parameters $:GPU_DECLARE(create='[bubbles_lagrange, lag_params]') !> @} - real(wp) :: Bx0 !< Constant magnetic field in the x-direction (1D) $:GPU_DECLARE(create='[Bx0]') - logical :: fft_wrt !> @name Continuum damage model parameters !> @{! - real(wp) :: tau_star !< Stress threshold for continuum damage modeling - real(wp) :: cont_damage_s !< Exponent s for continuum damage modeling - real(wp) :: alpha_bar !< Damage rate factor for continuum damage modeling $:GPU_DECLARE(create='[tau_star, cont_damage_s, alpha_bar]') !> @} !> @name MHD Hyperbolic cleaning parameters !> @{! - real(wp) :: hyper_cleaning_speed !< Hyperbolic cleaning wave speed (c_h) - real(wp) :: hyper_cleaning_tau !< Hyperbolic cleaning tau $:GPU_DECLARE(create='[hyper_cleaning_speed, hyper_cleaning_tau]') !> @} diff --git a/toolchain/mfc/params/definitions.py b/toolchain/mfc/params/definitions.py index d5a04da8b9..9be1f836be 100644 --- a/toolchain/mfc/params/definitions.py +++ b/toolchain/mfc/params/definitions.py @@ -898,7 +898,7 @@ def _load(): # Bubbles _r("R0ref", REAL, {"bubbles"}, math=r"\f$R_0\f$") - _r("nb", REAL, {"bubbles"}, math=r"\f$N_b\f$") + _r("nb", INT, {"bubbles"}, math=r"\f$N_b\f$") _r("Web", REAL, {"bubbles"}, math=r"\f$\mathrm{We}\f$") _r("Ca", REAL, {"bubbles"}, math=r"\f$\mathrm{Ca}\f$") _r("Re_inv", REAL, {"bubbles", "viscosity"}, math=r"\f$\mathrm{Re}^{-1}\f$") @@ -942,25 +942,18 @@ def _load(): # Output _r("precision", INT, {"output"}) _r("format", INT, {"output"}) - _r("schlieren_alpha", REAL, {"output"}) for n in ["parallel_io", "file_per_process", "run_time_info", "prim_vars_wrt", "cons_vars_wrt", "fft_wrt", "ib_state_wrt"]: _r(n, LOG, {"output"}) for n in [ "schlieren_wrt", - "alpha_rho_wrt", "rho_wrt", - "mom_wrt", - "vel_wrt", - "flux_wrt", "E_wrt", "pres_wrt", - "alpha_wrt", "gamma_wrt", "heat_ratio_wrt", "pi_inf_wrt", "pres_inf_wrt", "c_wrt", - "omega_wrt", "qm_wrt", "liutex_wrt", "cf_wrt", @@ -1036,7 +1029,6 @@ def _load(): "mixlayer_vel_coef", "mixlayer_perturb_k0", "perturb_flow_mag", - "fluid_rho", "sigR", "sigV", "rhoRV", From 56cfa23a093eed4bb1bed0d720c0f1af3658e081 Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Wed, 27 May 2026 23:36:58 -0400 Subject: [PATCH 08/33] feat(precheck): add 7/7 check for generated Fortran files --- toolchain/bootstrap/precheck.sh | 34 ++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/toolchain/bootstrap/precheck.sh b/toolchain/bootstrap/precheck.sh index fc0effae08..786f64d41d 100755 --- a/toolchain/bootstrap/precheck.sh +++ b/toolchain/bootstrap/precheck.sh @@ -59,7 +59,7 @@ done # CI runs the full suite via ./mfc.sh lint without this variable. export MFC_SKIP_RENDER_TESTS=1 -log "Running$MAGENTA precheck$COLOR_RESET (same checks as CI lint-gate)..." +log "Running$MAGENTA precheck$COLOR_RESET (same checks as CI lint-gate: 7/7)..." echo "" # Temp files for collecting results from parallel jobs @@ -127,11 +127,21 @@ fi ) & PID_PARAM_DOCS=$! +# Generated Fortran files up-to-date check +( + if ./mfc.sh generate --check > /dev/null 2>&1; then + echo "0" > "$TMPDIR_PC/generate_exit" + else + echo "1" > "$TMPDIR_PC/generate_exit" + fi +) & +PID_GENERATE=$! + # --- Collect results --- FAILED=0 -log "[$CYAN 1/6$COLOR_RESET] Checking$MAGENTA formatting$COLOR_RESET..." +log "[$CYAN 1/7$COLOR_RESET] Checking$MAGENTA formatting$COLOR_RESET..." if [ "$FORMAT_OK" = "1" ]; then error "Formatting check failed to run." FAILED=1 @@ -146,7 +156,7 @@ else fi wait $PID_SPELL -log "[$CYAN 2/6$COLOR_RESET] Running$MAGENTA spell check$COLOR_RESET..." +log "[$CYAN 2/7$COLOR_RESET] Running$MAGENTA spell check$COLOR_RESET..." SPELL_RC=$(cat "$TMPDIR_PC/spell_exit" 2>/dev/null || echo "1") if [ "$SPELL_RC" = "0" ]; then ok "Spell check passed." @@ -156,7 +166,7 @@ else fi wait $PID_LINT -log "[$CYAN 3/6$COLOR_RESET] Running$MAGENTA toolchain lint$COLOR_RESET..." +log "[$CYAN 3/7$COLOR_RESET] Running$MAGENTA toolchain lint$COLOR_RESET..." LINT_RC=$(cat "$TMPDIR_PC/lint_exit" 2>/dev/null || echo "1") if [ "$LINT_RC" = "0" ]; then ok "Toolchain lint passed." @@ -166,7 +176,7 @@ else fi wait $PID_SOURCE -log "[$CYAN 4/6$COLOR_RESET] Running$MAGENTA source lint$COLOR_RESET..." +log "[$CYAN 4/7$COLOR_RESET] Running$MAGENTA source lint$COLOR_RESET..." SOURCE_RC=$(cat "$TMPDIR_PC/source_exit" 2>/dev/null || echo "1") if [ "$SOURCE_RC" = "0" ]; then ok "Source lint passed." @@ -175,7 +185,7 @@ else FAILED=1 fi -log "[$CYAN 5/6$COLOR_RESET] Checking$MAGENTA doc references$COLOR_RESET..." +log "[$CYAN 5/7$COLOR_RESET] Checking$MAGENTA doc references$COLOR_RESET..." if [ $DOC_FAILED -eq 0 ]; then ok "Doc references are valid." else @@ -184,7 +194,7 @@ else fi wait $PID_PARAM_DOCS -log "[$CYAN 6/6$COLOR_RESET] Checking$MAGENTA parameter docs$COLOR_RESET..." +log "[$CYAN 6/7$COLOR_RESET] Checking$MAGENTA parameter docs$COLOR_RESET..." PARAM_DOCS_RC=$(cat "$TMPDIR_PC/param_docs_exit" 2>/dev/null || echo "1") if [ "$PARAM_DOCS_RC" = "0" ]; then ok "Parameter documentation check passed." @@ -193,6 +203,16 @@ else FAILED=1 fi +wait $PID_GENERATE +log "[$CYAN 7/7$COLOR_RESET] Checking$MAGENTA generated Fortran files$COLOR_RESET..." +GENERATE_RC=$(cat "$TMPDIR_PC/generate_exit" 2>/dev/null || echo "1") +if [ "$GENERATE_RC" = "0" ]; then + ok "Generated Fortran files are up to date." +else + error "Generated Fortran files are out of date. Run$MAGENTA ./mfc.sh generate$COLOR_RESET to update." + FAILED=1 +fi + echo "" if [ $FAILED -eq 0 ]; then From 13f24b1d068ea430b29d0ad5c647017ca736a94d Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Wed, 27 May 2026 23:50:59 -0400 Subject: [PATCH 09/33] fix: move generated .fpp files to CMake build dir, remove from source tree --- CMakeLists.txt | 15 + .../plans/2026-05-27-fortran-codegen.md | 1366 +++++++++++++++++ src/common/include/generated_decls_post.fpp | 101 -- src/common/include/generated_decls_pre.fpp | 95 -- src/common/include/generated_decls_sim.fpp | 140 -- .../include/generated_namelist_post.fpp | 14 - src/common/include/generated_namelist_pre.fpp | 12 - src/common/include/generated_namelist_sim.fpp | 19 - src/post_process/m_global_parameters.fpp | 2 +- src/post_process/m_start_up.fpp | 2 +- src/pre_process/m_global_parameters.fpp | 2 +- src/pre_process/m_start_up.fpp | 2 +- src/simulation/m_global_parameters.fpp | 4 +- src/simulation/m_start_up.fpp | 2 +- toolchain/bootstrap/precheck.sh | 34 +- toolchain/mfc/generate.py | 5 - toolchain/mfc/lint_param_docs.py | 15 +- toolchain/mfc/params/generators/cmake_gen.py | 36 + .../mfc/params/generators/fortran_gen.py | 15 +- toolchain/mfc/params/namelist_parser.py | 18 +- toolchain/tests/__init__.py | 0 toolchain/tests/params/__init__.py | 0 22 files changed, 1450 insertions(+), 449 deletions(-) create mode 100644 docs/superpowers/plans/2026-05-27-fortran-codegen.md delete mode 100644 src/common/include/generated_decls_post.fpp delete mode 100644 src/common/include/generated_decls_pre.fpp delete mode 100644 src/common/include/generated_decls_sim.fpp delete mode 100644 src/common/include/generated_namelist_post.fpp delete mode 100644 src/common/include/generated_namelist_pre.fpp delete mode 100644 src/common/include/generated_namelist_sim.fpp create mode 100644 toolchain/mfc/params/generators/cmake_gen.py delete mode 100644 toolchain/tests/__init__.py delete mode 100644 toolchain/tests/params/__init__.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 2a44aca223..8e28be07c3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -461,6 +461,21 @@ macro(HANDLE_SOURCES target useCommon) endmacro() +# Generate Fortran parameter namelist/decl includes into the per-target build +# include directories before HANDLE_SOURCES globs them for Fypp. +find_package(Python3 REQUIRED COMPONENTS Interpreter) +execute_process( + COMMAND "${Python3_EXECUTABLE}" + "${CMAKE_CURRENT_SOURCE_DIR}/toolchain/mfc/params/generators/cmake_gen.py" + "${CMAKE_BINARY_DIR}" + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + RESULT_VARIABLE _mfc_gen_result + ERROR_VARIABLE _mfc_gen_error +) +if(NOT _mfc_gen_result EQUAL 0) + message(FATAL_ERROR "Fortran param generation failed:\n${_mfc_gen_error}") +endif() + HANDLE_SOURCES(pre_process ON) HANDLE_SOURCES(simulation ON) HANDLE_SOURCES(post_process ON) diff --git a/docs/superpowers/plans/2026-05-27-fortran-codegen.md b/docs/superpowers/plans/2026-05-27-fortran-codegen.md new file mode 100644 index 0000000000..9557f1103d --- /dev/null +++ b/docs/superpowers/plans/2026-05-27-fortran-codegen.md @@ -0,0 +1,1366 @@ +# Fortran Param Codegen Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Auto-generate Fortran namelist declarations and simple scalar variable declarations from `definitions.py`, reducing new-parameter additions from 7 manual file edits to ≤2. + +**Architecture:** A new `fortran_gen.py` generator (fitting the existing generator pattern in `params/generators/`) reads `definitions.py` + a new `namelist_targets.py` to produce six `.fpp` include files — one namelist fragment and one declarations fragment per target (pre/sim/post). These are checked into `src/common/include/`, regenerated via `./mfc.sh generate`, and verified up-to-date by a new `precheck.sh` step. + +**Tech Stack:** Python 3 (existing toolchain), Fypp preprocessor (for `#:include`), Fortran namelists, MFC's existing `generate.py` check-or-write infrastructure. + +--- + +## Background: Current Pain + +Adding one simple scalar used by all three targets currently requires edits in 7 places: +1. `toolchain/mfc/params/definitions.py` +2-4. `src/{pre_process,simulation,post_process}/m_global_parameters.fpp` (variable declaration) +5-7. `src/{pre_process,simulation,post_process}/m_start_up.fpp` (namelist entry) + +After this plan: **1–2 places** — just `definitions.py` + `namelist_targets.py`. + +--- + +## File Map + +### New files +| File | Responsibility | +|------|----------------| +| `toolchain/mfc/params/namelist_targets.py` | `NAMELIST_VARS` dict (namelist_var → targets) + `CASE_OPT_EXCLUDE` set | +| `toolchain/mfc/params/generators/fortran_gen.py` | Generator: produces namelist + decls `.fpp` content per target | +| `src/common/include/generated_namelist_pre.fpp` | Generated: pre_process namelist fragment | +| `src/common/include/generated_namelist_sim.fpp` | Generated: simulation namelist fragment (with Fypp CASE_OPT guard) | +| `src/common/include/generated_namelist_post.fpp` | Generated: post_process namelist fragment | +| `src/common/include/generated_decls_pre.fpp` | Generated: pre_process simple scalar declarations | +| `src/common/include/generated_decls_sim.fpp` | Generated: simulation simple scalar declarations | +| `src/common/include/generated_decls_post.fpp` | Generated: post_process simple scalar declarations | + +### Modified files +| File | Change | +|------|--------| +| `toolchain/mfc/params/schema.py` | Add `str_len: str = "name_len"` to `ParamDef` | +| `toolchain/mfc/params/definitions.py` | Set `str_len="path_len"` on `case_dir` | +| `toolchain/mfc/generate.py` | Register six new generated files | +| `toolchain/bootstrap/precheck.sh` | Add check 7/7: `./mfc.sh generate --check` | +| `src/pre_process/m_start_up.fpp:77-87` | Replace namelist block with `#:include` | +| `src/simulation/m_start_up.fpp:85-115` | Replace namelist block with `#:include` | +| `src/post_process/m_start_up.fpp:62-73` | Replace namelist block with `#:include` | +| `src/pre_process/m_global_parameters.fpp` | Remove generated scalars, add `#:include` after implicit none | +| `src/simulation/m_global_parameters.fpp` | Remove generated scalars, add `#:include` after implicit none | +| `src/post_process/m_global_parameters.fpp` | Remove generated scalars, add `#:include` after implicit none | + +--- + +## Key Design Decisions + +**`namelist_var` derivation (no new ParamDef field needed):** +```python +import re + +def get_namelist_var(param_name: str) -> str: + # fluid_pp(1)%gamma -> fluid_pp + m = re.match(r'^([a-zA-Z_]\w*)\(\d', param_name) + if m: + return m.group(1) + # bc_x%beg, lag_params%foo -> bc_x, lag_params + if '%' in param_name: + return param_name.split('%')[0] + # Simple scalar: m, n, dt, model_eqns + return param_name +``` + +**Simple scalars** (candidates for declaration generation) = params with no `%` or `(` in their name, with `ParamType` of INT, REAL, LOG, or STR. + +**`NAMELIST_TARGETS` structure** (one source of truth for what goes where): +```python +NAMELIST_VARS: Dict[str, Set[str]] = { + "m": {"pre", "sim", "post"}, + "bc_x": {"pre", "sim", "post"}, + "patch_icpp": {"pre"}, + "run_time_info": {"sim"}, + ... +} +CASE_OPT_EXCLUDE: Set[str] = { + "nb", "mapped_weno", "wenoz", "teno", "wenoz_q", "weno_order", + "num_fluids", "mhd", "relativity", "igr_order", "viscous", + "igr_iter_solver", "igr", "igr_pres_lim", "recon_type", "muscl_order", "muscl_lim", +} +``` + +**Generated namelist format** (for simulation): +```fortran +! AUTO-GENERATED — do not edit. Regenerate with: ./mfc.sh generate +namelist /user_inputs/ m, n, p, dt, model_eqns, bc_x, bc_y, bc_z, & + & fluid_pp, bub_pp, ... +#:if not MFC_CASE_OPTIMIZATION + & nb, mapped_weno, wenoz, weno_order, ... +#:endif + & last_var +``` + +**Generated declarations format**: +```fortran +! AUTO-GENERATED — do not edit. Regenerate with: ./mfc.sh generate +integer :: model_eqns +real(wp) :: dt +logical :: cyl_coord +character(LEN=path_len) :: case_dir +``` + +--- + +## Task 1: Extend ParamDef for STR length + +**Files:** +- Modify: `toolchain/mfc/params/schema.py` +- Modify: `toolchain/mfc/params/definitions.py` + +- [ ] **Step 1: Write a failing test in `toolchain/tests/params/test_schema.py`** + +```python +def test_paramdef_str_len_default(): + from mfc.params.schema import ParamDef, ParamType + p = ParamDef(name="foo", param_type=ParamType.STR) + assert p.str_len == "name_len" + +def test_paramdef_str_len_override(): + from mfc.params.schema import ParamDef, ParamType + p = ParamDef(name="case_dir", param_type=ParamType.STR, str_len="path_len") + assert p.str_len == "path_len" +``` + +- [ ] **Step 2: Run to verify failure** + +```bash +cd /path/to/mfc +python3 -m pytest toolchain/tests/params/test_schema.py::test_paramdef_str_len_default -v +``` +Expected: `AttributeError: str_len` + +- [ ] **Step 3: Add `str_len` to `ParamDef` in `schema.py`** + +In `toolchain/mfc/params/schema.py`, add to the `@dataclass class ParamDef:` block after `math_symbol`: +```python +str_len: str = "name_len" +# For STR type: Fortran character length constant. Default "name_len"; set "path_len" for case_dir. +``` + +- [ ] **Step 4: Set `str_len` on `case_dir` in `definitions.py`** + +In `toolchain/mfc/params/definitions.py`, find the line: +```python + _r("case_dir", STR) +``` +Replace with: +```python + _r("case_dir", STR, str_len="path_len") +``` + +This requires updating `_r()` to accept and forward `str_len`. Find `_r()` at line ~818: +```python +def _r(name, ptype, tags=None, desc=None, hint=None, math=None): +``` +Replace with: +```python +def _r(name, ptype, tags=None, desc=None, hint=None, math=None, str_len=None): + ... + REGISTRY.register( + ParamDef( + ... + str_len=str_len if str_len is not None else "name_len", + ) + ) +``` + +- [ ] **Step 5: Run the tests** + +```bash +python3 -m pytest toolchain/tests/params/test_schema.py -v +``` +Expected: PASS + +- [ ] **Step 6: Commit** + +```bash +git add toolchain/mfc/params/schema.py toolchain/mfc/params/definitions.py +git commit -m "feat(params): add str_len to ParamDef for STR character length" +``` + +--- + +## Task 2: Create `namelist_targets.py` + +**Files:** +- Create: `toolchain/mfc/params/namelist_targets.py` + +This file is the authoritative source for which Fortran namelist variable belongs to which target(s). It is derived by reading the existing three namelist blocks; after this plan, those blocks will be generated from it. + +- [ ] **Step 1: Write a failing test** + +Create `toolchain/tests/params/test_namelist_targets.py`: +```python +def test_common_vars_in_all_targets(): + from mfc.params.namelist_targets import NAMELIST_VARS + for var in ["m", "n", "p", "bc_x", "bc_y", "bc_z", "model_eqns", "cyl_coord", "fluid_pp"]: + assert {"pre", "sim", "post"}.issubset(NAMELIST_VARS.get(var, set())), \ + f"{var!r} not marked for all targets" + +def test_sim_only_vars(): + from mfc.params.namelist_targets import NAMELIST_VARS + for var in ["run_time_info", "dt", "riemann_solver", "acoustic", "probe"]: + targets = NAMELIST_VARS.get(var, set()) + assert "sim" in targets, f"{var!r} not marked for sim" + assert "pre" not in targets, f"{var!r} incorrectly marked for pre" + assert "post" not in targets, f"{var!r} incorrectly marked for post" + +def test_pre_only_vars(): + from mfc.params.namelist_targets import NAMELIST_VARS + for var in ["old_grid", "old_ic", "patch_icpp", "simplex_params"]: + targets = NAMELIST_VARS.get(var, set()) + assert "pre" in targets, f"{var!r} not marked for pre" + assert "sim" not in targets, f"{var!r} incorrectly in sim" + +def test_post_only_vars(): + from mfc.params.namelist_targets import NAMELIST_VARS + for var in ["format", "sim_data", "lag_header", "output_partial_domain"]: + targets = NAMELIST_VARS.get(var, set()) + assert "post" in targets, f"{var!r} not marked for post" + assert "sim" not in targets, f"{var!r} incorrectly in sim" + +def test_case_opt_exclude_vars(): + from mfc.params.namelist_targets import CASE_OPT_EXCLUDE + for var in ["nb", "mapped_weno", "wenoz", "weno_order", "num_fluids"]: + assert var in CASE_OPT_EXCLUDE +``` + +- [ ] **Step 2: Run to verify failure** + +```bash +python3 -m pytest toolchain/tests/params/test_namelist_targets.py -v +``` +Expected: `ModuleNotFoundError` + +- [ ] **Step 3: Create `namelist_targets.py`** + +Create `toolchain/mfc/params/namelist_targets.py` with the complete content: + +```python +""" +Namelist target mapping for Fortran codegen. + +NAMELIST_VARS maps each Fortran namelist variable (struct root or simple scalar) +to the set of MFC executables whose namelist it appears in. + +CASE_OPT_EXCLUDE is the set of simulation namelist variables excluded under +MFC_CASE_OPTIMIZATION (they become compile-time constants instead). + +When adding a new parameter: + 1. Add to definitions.py (type, constraints, etc.) + 2. Add the namelist root variable to NAMELIST_VARS with its target set + 3. Run ./mfc.sh generate to regenerate the .fpp files +""" + +from typing import Dict, Set + +# All three targets +_ALL = {"pre", "sim", "post"} +_PRE_SIM = {"pre", "sim"} +_SIM_POST = {"sim", "post"} + +NAMELIST_VARS: Dict[str, Set[str]] = { + # --- Grid (all targets) --- + "m": _ALL, + "n": _ALL, + "p": _ALL, + "cyl_coord": _ALL, + "x_domain": {"pre", "sim"}, + "y_domain": {"pre", "sim"}, + "z_domain": {"pre", "sim"}, + "x_output": {"post"}, + "y_output": {"post"}, + "z_output": {"post"}, + # --- Grid stretching (pre + sim) --- + "stretch_x": {"pre"}, + "stretch_y": {"pre"}, + "stretch_z": {"pre"}, + "a_x": {"pre"}, + "a_y": {"pre"}, + "a_z": {"pre"}, + "x_a": {"pre", "sim"}, + "y_a": {"pre", "sim"}, + "z_a": {"pre", "sim"}, + "x_b": {"pre", "sim"}, + "y_b": {"pre", "sim"}, + "z_b": {"pre", "sim"}, + "loops_x": {"pre"}, + "loops_y": {"pre"}, + "loops_z": {"pre"}, + # --- Time (sim) --- + "dt": {"sim"}, + "t_step_start": _ALL, + "t_step_stop": {"sim", "post"}, + "t_step_save": {"sim", "post"}, + "t_step_print": {"sim"}, + "t_step_old": {"pre", "sim"}, + "time_stepper": {"sim"}, + "t_stop": {"sim", "post"}, + "t_save": {"sim", "post"}, + "cfl_target": {"sim", "post"}, + "cfl_adap_dt": _ALL, + "cfl_const_dt": _ALL, + "n_start": _ALL, + "n_start_old": {"pre"}, + "adap_dt": {"sim"}, + "adap_dt_tol": {"sim"}, + "adap_dt_max_iters": {"sim"}, + # --- Physics model (all targets) --- + "model_eqns": _ALL, + "num_fluids": {"pre", "post"}, + "mpp_lim": _ALL, + "relax": _ALL, + "relax_model": _ALL, + "palpha_eps": _ALL, + "ptgalpha_eps": _ALL, + # --- WENO / reconstruction (pre has weno_order; sim has it under CASE_OPT) --- + "weno_order": {"pre", "post"}, + "weno_eps": {"sim"}, + "teno_CT": {"sim"}, + "wenoz_q": {"sim"}, + "mp_weno": {"sim"}, + "weno_avg": {"sim"}, + "weno_Re_flux": {"sim"}, + "null_weights": {"sim"}, + "muscl_eps": {"sim"}, + "recon_type": {"pre", "post"}, + "muscl_order": {"pre", "post"}, + "muscl_lim": {"post"}, + "int_comp": {"sim"}, + "ic_eps": {"sim"}, + "ic_beta": {"sim"}, + # --- Riemann solver (sim only) --- + "riemann_solver": {"sim"}, + "wave_speeds": {"sim"}, + "avg_state": {"sim", "post"}, + "low_Mach": {"sim"}, + # --- MHD (all targets) --- + "mhd": {"pre", "post"}, + "hyper_cleaning": _ALL, + "hyper_cleaning_speed": {"sim"}, + "hyper_cleaning_tau": {"sim"}, + "Bx0": _ALL, + # --- BCs (all targets) --- + "bc_x": _ALL, + "bc_y": _ALL, + "bc_z": _ALL, + "num_bc_patches": _ALL, + "patch_bc": {"pre"}, + # --- ICs (pre only) --- + "num_patches": {"pre"}, + "patch_icpp": {"pre"}, + # --- Fluid properties (all) --- + "fluid_pp": _ALL, + "bub_pp": _ALL, + "rhoref": _ALL, + "pref": _ALL, + # --- Bubbles (all) --- + "bubbles_euler": _ALL, + "bubbles_lagrange": _ALL, + "R0ref": _ALL, + "nb": {"pre", "post"}, + "polytropic": _ALL, + "thermal": _ALL, + "Ca": _ALL, + "Web": _ALL, + "Re_inv": _ALL, + "polydisperse": _ALL, + "poly_sigma": _ALL, + "qbmm": _ALL, + "sigma": _ALL, + "adv_n": _ALL, + "bubble_model": {"sim"}, + "sigR": {"pre", "post"}, + "sigV": {"pre"}, + "dist_type": {"pre"}, + "rhoRV": {"pre"}, + "lag_params": {"sim"}, + # --- Lagrangian output (post) --- + "lag_header": {"post"}, + "lag_txt_wrt": {"post"}, + "lag_db_wrt": {"post"}, + "lag_id_wrt": {"post"}, + "lag_pos_wrt": {"post"}, + "lag_pos_prev_wrt": {"post"}, + "lag_vel_wrt": {"post"}, + "lag_rad_wrt": {"post"}, + "lag_rvel_wrt": {"post"}, + "lag_r0_wrt": {"post"}, + "lag_rmax_wrt": {"post"}, + "lag_rmin_wrt": {"post"}, + "lag_dphidt_wrt": {"post"}, + "lag_pres_wrt": {"post"}, + "lag_mv_wrt": {"post"}, + "lag_mg_wrt": {"post"}, + "lag_betaT_wrt": {"post"}, + "lag_betaC_wrt": {"post"}, + # --- Elasticity (all) --- + "hypoelasticity": _ALL, + "hyperelasticity": _ALL, + # --- Surface tension (all) --- + "surface_tension": _ALL, + # --- Relativity (all) --- + "relativity": _ALL, + # --- Immersed boundaries (all) --- + "ib": _ALL, + "num_ibs": _ALL, + "patch_ib": {"pre", "sim"}, + "collision_model": {"sim"}, + "coefficient_of_restitution": {"sim"}, + "collision_time": {"sim"}, + "ib_coefficient_of_friction": {"sim"}, + "ib_state_wrt": {"sim", "post"}, + # --- Continuum damage (all) --- + "cont_damage": _ALL, + "tau_star": {"sim"}, + "cont_damage_s": {"sim"}, + "alpha_bar": {"sim"}, + # --- IGR (all) --- + "igr": {"pre", "post"}, + "igr_order": {"pre", "post"}, + "down_sample": _ALL, + # --- Probes (sim) --- + "probe_wrt": {"sim"}, + "num_probes": {"sim"}, + "probe": {"sim"}, + "integral_wrt": {"sim"}, + "num_integrals": {"sim"}, + "integral": {"sim"}, + "fd_order": {"sim", "post"}, + # --- Acoustic sources (sim) --- + "acoustic_source": {"sim"}, + "num_source": {"sim"}, + "acoustic": {"sim"}, + # --- Chemistry (sim) --- + "chem_params": {"sim"}, + # --- Body forces (sim) --- + "bf_x": {"sim"}, + "bf_y": {"sim"}, + "bf_z": {"sim"}, + "k_x": {"sim"}, + "k_y": {"sim"}, + "k_z": {"sim"}, + "w_x": {"sim"}, + "w_y": {"sim"}, + "w_z": {"sim"}, + "p_x": {"sim"}, + "p_y": {"sim"}, + "p_z": {"sim"}, + "g_x": {"sim"}, + "g_y": {"sim"}, + "g_z": {"sim"}, + # --- Viscous (pre) --- + "viscous": {"pre"}, + # --- Output (all) --- + "precision": _ALL, + "parallel_io": _ALL, + "file_per_process": _ALL, + "prim_vars_wrt": {"sim", "post"}, + "cons_vars_wrt": {"post"}, + "run_time_info": {"sim"}, + "fft_wrt": _ALL, + "pi_fac": {"pre", "sim"}, + # --- Post-process output --- + "format": {"post"}, + "output_partial_domain": {"post"}, + "rho_wrt": {"post"}, + "E_wrt": {"post"}, + "pres_wrt": {"post"}, + "c_wrt": {"post"}, + "omega_wrt": {"post"}, + "qm_wrt": {"post"}, + "liutex_wrt": {"post"}, + "schlieren_wrt": {"post"}, + "schlieren_alpha": {"post"}, + "gamma_wrt": {"post"}, + "heat_ratio_wrt": {"post"}, + "pi_inf_wrt": {"post"}, + "pres_inf_wrt": {"post"}, + "alpha_rho_wrt": {"post"}, + "mom_wrt": {"post"}, + "vel_wrt": {"post"}, + "flux_wrt": {"post"}, + "alpha_wrt": {"post"}, + "cf_wrt": {"post"}, + "chem_wrt_T": {"post"}, + "chem_wrt_Y": {"post"}, + "alt_soundspeed": {"sim", "post"}, + "mixture_err": {"sim", "post"}, + "flux_lim": {"post"}, + "sim_data": {"post"}, + "alpha_rho_e_wrt": {"post"}, + "G": {"post"}, + # --- Pre-process IC perturbations --- + "perturb_flow": {"pre"}, + "perturb_flow_fluid": {"pre"}, + "perturb_flow_mag": {"pre"}, + "perturb_sph": {"pre"}, + "perturb_sph_fluid": {"pre"}, + "fluid_rho": {"pre"}, + "mixlayer_vel_profile": {"pre"}, + "mixlayer_vel_coef": {"pre"}, + "mixlayer_perturb": {"pre"}, + "mixlayer_perturb_nk": {"pre"}, + "mixlayer_perturb_k0": {"pre"}, + "pre_stress": {"pre"}, + "elliptic_smoothing": {"pre"}, + "elliptic_smoothing_iters": {"pre"}, + "simplex_perturb": {"pre"}, + "simplex_params": {"pre"}, + # --- Pre-process restart --- + "old_grid": {"pre"}, + "old_ic": {"pre"}, + # --- Sim-specific physics --- + "rdma_mpi": {"sim"}, + "alf_factor": {"sim"}, + "num_igr_iters": {"sim"}, + "num_igr_warm_start_iters": {"sim"}, + "igr_iter_solver": {"sim"}, + "igr_pres_lim": {"sim"}, + "nv_uvm_out_of_core": {"sim"}, + "nv_uvm_igr_temps_on_gpu": {"sim"}, + "nv_uvm_pref_gpu": {"sim"}, + # --- Logistics --- + "case_dir": _ALL, +} + +# Variables excluded from the sim namelist when MFC_CASE_OPTIMIZATION is active +# (they become compile-time integer/logical parameters instead). +# Must all be present in NAMELIST_VARS with "sim" in their target set. +CASE_OPT_EXCLUDE: Set[str] = { + "nb", + "mapped_weno", + "wenoz", + "teno", + "wenoz_q", + "weno_order", + "num_fluids", + "mhd", + "relativity", + "igr_order", + "viscous", + "igr_iter_solver", + "igr", + "igr_pres_lim", + "recon_type", + "muscl_order", + "muscl_lim", +} + +# Add CASE_OPT_EXCLUDE vars to NAMELIST_VARS for sim target +# (they appear in the namelist when NOT using case optimization) +for _v in CASE_OPT_EXCLUDE: + if _v not in NAMELIST_VARS: + NAMELIST_VARS[_v] = {"sim"} + else: + NAMELIST_VARS[_v].add("sim") +``` + +- [ ] **Step 4: Run tests** + +```bash +python3 -m pytest toolchain/tests/params/test_namelist_targets.py -v +``` +Expected: all PASS + +- [ ] **Step 5: Commit** + +```bash +git add toolchain/mfc/params/namelist_targets.py toolchain/tests/params/test_namelist_targets.py +git commit -m "feat(params): add namelist_targets.py with target mapping for all namelist vars" +``` + +--- + +## Task 3: Write `fortran_gen.py` + +**Files:** +- Create: `toolchain/mfc/params/generators/fortran_gen.py` +- Test: `toolchain/tests/params/test_fortran_gen.py` + +The generator reads `definitions.py` (via `REGISTRY`) + `namelist_targets.py` and produces: +- **Namelist fragment**: `namelist /user_inputs/ var1, var2, ...` with Fypp case-opt guard for sim +- **Decls fragment**: `integer :: foo` / `real(wp) :: bar` for each simple scalar in that target + +- [ ] **Step 1: Write failing tests** + +Create `toolchain/tests/params/test_fortran_gen.py`: + +```python +import pytest + + +def test_get_namelist_var_simple(): + from mfc.params.generators.fortran_gen import get_namelist_var + assert get_namelist_var("m") == "m" + assert get_namelist_var("dt") == "dt" + assert get_namelist_var("cyl_coord") == "cyl_coord" + + +def test_get_namelist_var_indexed_family(): + from mfc.params.generators.fortran_gen import get_namelist_var + assert get_namelist_var("fluid_pp(1)%gamma") == "fluid_pp" + assert get_namelist_var("patch_icpp(3)%geometry") == "patch_icpp" + assert get_namelist_var("patch_ib(100)%radius") == "patch_ib" + + +def test_get_namelist_var_struct_member(): + from mfc.params.generators.fortran_gen import get_namelist_var + assert get_namelist_var("bc_x%beg") == "bc_x" + assert get_namelist_var("bc_y%grcbc_in") == "bc_y" + assert get_namelist_var("lag_params%solver_approach") == "lag_params" + assert get_namelist_var("chem_params%diffusion") == "chem_params" + assert get_namelist_var("bub_pp%R0ref") == "bub_pp" + + +def test_fortran_type_for_int(): + from mfc.params.generators.fortran_gen import fortran_type_decl + from mfc.params.schema import ParamDef, ParamType + p = ParamDef(name="model_eqns", param_type=ParamType.INT) + assert fortran_type_decl(p) == "integer" + + +def test_fortran_type_for_real(): + from mfc.params.generators.fortran_gen import fortran_type_decl + from mfc.params.schema import ParamDef, ParamType + p = ParamDef(name="dt", param_type=ParamType.REAL) + assert fortran_type_decl(p) == "real(wp)" + + +def test_fortran_type_for_log(): + from mfc.params.generators.fortran_gen import fortran_type_decl + from mfc.params.schema import ParamDef, ParamType + p = ParamDef(name="cyl_coord", param_type=ParamType.LOG) + assert fortran_type_decl(p) == "logical" + + +def test_fortran_type_for_str(): + from mfc.params.generators.fortran_gen import fortran_type_decl + from mfc.params.schema import ParamDef, ParamType + p = ParamDef(name="case_dir", param_type=ParamType.STR, str_len="path_len") + assert fortran_type_decl(p) == "character(LEN=path_len)" + + +def test_generate_namelist_contains_common_vars(): + from mfc.params.generators.fortran_gen import generate_namelist_fpp + for target in ("pre", "sim", "post"): + content = generate_namelist_fpp(target) + for var in ("m", "n", "p", "bc_x", "bc_y", "bc_z", "fluid_pp", "case_dir"): + assert var in content, f"{var!r} missing from {target} namelist" + + +def test_sim_namelist_has_case_opt_guard(): + from mfc.params.generators.fortran_gen import generate_namelist_fpp + content = generate_namelist_fpp("sim") + assert "#:if not MFC_CASE_OPTIMIZATION" in content + assert "weno_order" in content + assert "num_fluids" in content + + +def test_pre_namelist_has_patch_icpp(): + from mfc.params.generators.fortran_gen import generate_namelist_fpp + content = generate_namelist_fpp("pre") + assert "patch_icpp" in content + assert "run_time_info" not in content + + +def test_post_namelist_has_output_vars(): + from mfc.params.generators.fortran_gen import generate_namelist_fpp + content = generate_namelist_fpp("post") + assert "sim_data" in content + assert "lag_header" in content + assert "patch_icpp" not in content + + +def test_generate_decls_contains_simple_scalars(): + from mfc.params.generators.fortran_gen import generate_decls_fpp + for target in ("pre", "sim", "post"): + content = generate_decls_fpp(target) + assert "integer" in content + assert "real(wp)" in content + assert "logical" in content + + +def test_generate_decls_has_dt_for_sim(): + from mfc.params.generators.fortran_gen import generate_decls_fpp + content = generate_decls_fpp("sim") + assert "real(wp) :: dt" in content + + +def test_generate_decls_no_percent_vars(): + from mfc.params.generators.fortran_gen import generate_decls_fpp + for target in ("pre", "sim", "post"): + content = generate_decls_fpp(target) + # Derived type members are NOT declared — only their struct root + assert "bc_x%beg" not in content + assert "fluid_pp(1)" not in content + + +def test_generate_decls_has_case_dir(): + from mfc.params.generators.fortran_gen import generate_decls_fpp + for target in ("pre", "sim", "post"): + content = generate_decls_fpp(target) + assert "character(LEN=path_len) :: case_dir" in content +``` + +- [ ] **Step 2: Run to verify failure** + +```bash +python3 -m pytest toolchain/tests/params/test_fortran_gen.py -v +``` +Expected: `ModuleNotFoundError` + +- [ ] **Step 3: Implement `fortran_gen.py`** + +Create `toolchain/mfc/params/generators/fortran_gen.py`: + +```python +""" +Fortran parameter code generator. + +Generates namelist fragments and simple scalar declaration fragments from +definitions.py + namelist_targets.py. Output goes to src/common/include/. + +Usage: called from generate.py. Not invoked directly. +""" + +import re +from typing import Optional + +from ..namelist_targets import CASE_OPT_EXCLUDE, NAMELIST_VARS +from ..registry import REGISTRY +from ..schema import ParamDef, ParamType + +import mfc.params.definitions # noqa: F401 — triggers registry population + + +_HEADER = """\ +! AUTO-GENERATED — do not edit directly. +! Source: toolchain/mfc/params/definitions.py + namelist_targets.py +! Regenerate: ./mfc.sh generate +! +""" + +_FORTRAN_INDENT = " " + + +def get_namelist_var(param_name: str) -> str: + """Return the Fortran namelist variable root for a parameter name. + + fluid_pp(1)%gamma -> fluid_pp + bc_x%beg -> bc_x + m -> m + """ + # Indexed family: fluid_pp(1)%gamma -> fluid_pp + m = re.match(r"^([a-zA-Z_]\w*)\(", param_name) + if m and "%" in param_name: + return m.group(1) + # Struct member: bc_x%beg -> bc_x + if "%" in param_name: + return param_name.split("%")[0] + # Simple scalar + return param_name + + +def fortran_type_decl(param: ParamDef) -> str: + """Return the Fortran type keyword for a parameter.""" + mapping = { + ParamType.INT: "integer", + ParamType.REAL: "real(wp)", + ParamType.LOG: "logical", + ParamType.ANALYTIC_INT: "integer", + ParamType.ANALYTIC_REAL: "real(wp)", + } + if param.param_type == ParamType.STR: + return f"character(LEN={param.str_len})" + return mapping[param.param_type] + + +def _is_simple_scalar(param_name: str) -> bool: + """Return True if this param is a simple scalar (no % or () in name).""" + return "%" not in param_name and "(" not in param_name + + +def _namelist_vars_for_target(target: str) -> list: + """Return ordered list of namelist variables for a target.""" + result = [] + seen = set() + for var, targets in NAMELIST_VARS.items(): + if target in targets and var not in seen: + result.append(var) + seen.add(var) + return result + + +def generate_namelist_fpp(target: str) -> str: + """Generate the namelist /user_inputs/ fragment for a target. + + For sim, wraps CASE_OPT_EXCLUDE vars in a Fypp guard. + """ + assert target in ("pre", "sim", "post"), f"Unknown target: {target!r}" + + all_vars = _namelist_vars_for_target(target) + + if target == "sim": + normal_vars = [v for v in all_vars if v not in CASE_OPT_EXCLUDE] + opt_vars = [v for v in all_vars if v in CASE_OPT_EXCLUDE] + else: + normal_vars = all_vars + opt_vars = [] + + lines = [_HEADER] + + def _fmt_varlist(varlist: list, continuation: bool = False) -> list: + """Format a list of variable names as Fortran continuation lines.""" + out = [] + chunk = [] + for v in varlist: + chunk.append(v) + if len(", ".join(chunk)) > 70: + prefix = f"{_FORTRAN_INDENT}& " if continuation else f"{_FORTRAN_INDENT}" + out.append(prefix + ", ".join(chunk[:-1]) + ", &") + chunk = [chunk[-1]] + continuation = True + if chunk: + prefix = f"{_FORTRAN_INDENT}& " if continuation else f"{_FORTRAN_INDENT}" + out.append(prefix + ", ".join(chunk)) + return out + + if not opt_vars: + var_lines = _fmt_varlist(normal_vars) + lines.append(f"namelist /user_inputs/ {var_lines[0].lstrip()}") + if len(var_lines) > 1: + lines[-1] += ", &" + lines.extend(v + (", &" if i < len(var_lines) - 2 else "") for i, v in enumerate(var_lines[1:])) + else: + # Sim: normal vars first, then CASE_OPT guard, then trailing vars + # Emit normal vars up to end, then the guard block + var_lines = _fmt_varlist(normal_vars) + lines.append(f"namelist /user_inputs/ {var_lines[0].lstrip()}, &") + for v in var_lines[1:]: + lines.append(v + ", &") + + lines.append(f"#:if not MFC_CASE_OPTIMIZATION") + opt_lines = _fmt_varlist(opt_vars, continuation=True) + for i, v in enumerate(opt_lines): + lines.append(v + (", &" if i < len(opt_lines) - 1 else ", &")) + lines.append(f"#:endif") + # Need a placeholder last entry — use a trailing comment-safe idiom + # Actually Fortran namelist doesn't need a sentinel, just remove trailing comma + # Fix: the last normal line had a trailing ", &" — correct the last line + # Remove trailing ", &" from the last real entry + lines[-1] = lines[-1].rstrip(", &") + + return "\n".join(lines) + "\n" + + +def generate_decls_fpp(target: str) -> str: + """Generate simple scalar Fortran declarations for a target. + + Only generates declarations for params that are: + - Simple scalars (no % or () in name) + - In NAMELIST_VARS for this target + - Registered in definitions.py + """ + assert target in ("pre", "sim", "post"), f"Unknown target: {target!r}" + + vars_for_target = set(_namelist_vars_for_target(target)) + lines = [_HEADER] + + all_params = REGISTRY.all_params + for name in sorted(vars_for_target): + if not _is_simple_scalar(name): + continue + param = all_params.get(name) + if param is None: + continue + ftype = fortran_type_decl(param) + lines.append(f"{ftype} :: {name}") + + return "\n".join(lines) + "\n" +``` + +- [ ] **Step 4: Run tests** + +```bash +python3 -m pytest toolchain/tests/params/test_fortran_gen.py -v +``` +Expected: all PASS (fix any off-by-one in namelist formatting if needed) + +- [ ] **Step 5: Commit** + +```bash +git add toolchain/mfc/params/generators/fortran_gen.py toolchain/tests/params/test_fortran_gen.py +git commit -m "feat(params): add fortran_gen.py to generate namelist and declaration .fpp files" +``` + +--- + +## Task 4: Wire into `generate.py` + +**Files:** +- Modify: `toolchain/mfc/generate.py` + +The generator produces six files in `src/common/include/`. They are checked in and verified by `generate --check`. + +- [ ] **Step 1: Write a failing test** + +In `toolchain/tests/params/test_fortran_gen.py`, add: + +```python +def test_generate_function_returns_six_paths(): + from mfc.params.generators.fortran_gen import get_generated_files + from pathlib import Path + files = get_generated_files() + assert len(files) == 6 + names = {p.name for p, _ in files} + assert "generated_namelist_pre.fpp" in names + assert "generated_decls_sim.fpp" in names +``` + +- [ ] **Step 2: Add `get_generated_files()` to `fortran_gen.py`** + +At the bottom of `fortran_gen.py`, add: + +```python +from pathlib import Path +from ..common import MFC_ROOT_DIR # already used elsewhere in the toolchain + + +def get_generated_files() -> list: + """Return list of (output_path, content) tuples for all six generated files.""" + include_dir = Path(MFC_ROOT_DIR) / "src" / "common" / "include" + return [ + (include_dir / "generated_namelist_pre.fpp", generate_namelist_fpp("pre")), + (include_dir / "generated_namelist_sim.fpp", generate_namelist_fpp("sim")), + (include_dir / "generated_namelist_post.fpp", generate_namelist_fpp("post")), + (include_dir / "generated_decls_pre.fpp", generate_decls_fpp("pre")), + (include_dir / "generated_decls_sim.fpp", generate_decls_fpp("sim")), + (include_dir / "generated_decls_post.fpp", generate_decls_fpp("post")), + ] +``` + +Note: `MFC_ROOT_DIR` is defined in `toolchain/mfc/common.py`. Check the import path before using. + +- [ ] **Step 3: Register in `generate.py`** + +In `toolchain/mfc/generate.py`, in the `generate()` function, add after the existing `files = [...]` list: + +```python + from .params.generators.fortran_gen import get_generated_files + files += get_generated_files() +``` + +- [ ] **Step 4: Run `./mfc.sh generate` to produce the six files** + +```bash +./mfc.sh generate +``` +Expected: six new/updated `.fpp` files in `src/common/include/` + +Inspect the output: +```bash +cat src/common/include/generated_namelist_sim.fpp | head -30 +cat src/common/include/generated_decls_pre.fpp | head -20 +``` + +- [ ] **Step 5: Verify content matches the existing namelists** + +Manually cross-check that every variable in the current `m_start_up.fpp` namelist blocks appears in the corresponding generated file. Key checks: +- `generated_namelist_sim.fpp` should contain `weno_order` inside the `#:if not MFC_CASE_OPTIMIZATION` guard +- `generated_namelist_pre.fpp` should contain `patch_icpp`, `simplex_params`, `old_grid` +- `generated_namelist_post.fpp` should contain `sim_data`, `lag_header`, `format` + +- [ ] **Step 6: Run tests** + +```bash +python3 -m pytest toolchain/tests/params/test_fortran_gen.py -v +``` +Expected: all PASS + +- [ ] **Step 7: Commit the generated files and generate.py change** + +```bash +git add src/common/include/generated_namelist_*.fpp src/common/include/generated_decls_*.fpp +git add toolchain/mfc/generate.py toolchain/mfc/params/generators/fortran_gen.py +git commit -m "feat(params): generate Fortran namelist and declaration .fpp files from definitions.py" +``` + +--- + +## Task 5: Replace namelists in `m_start_up.fpp` (all three targets) + +**Files:** +- Modify: `src/pre_process/m_start_up.fpp:77-87` +- Modify: `src/simulation/m_start_up.fpp:85-115` +- Modify: `src/post_process/m_start_up.fpp:62-73` + +This is the first Fortran change. Do one target at a time and build between each to catch errors early. + +### Pre-process + +- [ ] **Step 1: In `src/pre_process/m_start_up.fpp`, replace lines 77–87** + +Current (lines 77–87): +```fortran + namelist /user_inputs/ case_dir, old_grid, old_ic, t_step_old, t_step_start, m, n, p, x_domain, y_domain, z_domain, & + & stretch_x, stretch_y, stretch_z, a_x, a_y, a_z, x_a, y_a, z_a, x_b, y_b, z_b, model_eqns, num_fluids, mpp_lim, & + ... + & simplex_perturb, simplex_params, fft_wrt +``` + +Replace with: +```fortran + #:include 'generated_namelist_pre.fpp' +``` + +- [ ] **Step 2: Build pre_process only** + +```bash +./mfc.sh build -t pre_process -j 8 +``` +Expected: successful compilation. If there are "Undefined variable" errors, a namelist variable is missing from `generated_namelist_pre.fpp` — add it to `NAMELIST_VARS` in `namelist_targets.py` with target `"pre"`, then re-run `./mfc.sh generate`. + +### Simulation + +- [ ] **Step 3: In `src/simulation/m_start_up.fpp`, replace lines 85–115** + +Current (lines 85–115): +```fortran + namelist /user_inputs/ case_dir, run_time_info, m, n, p, dt, & + t_step_start, t_step_stop, t_step_save, t_step_print, & + ... + & int_comp, ic_eps, ic_beta, nv_uvm_out_of_core, nv_uvm_igr_temps_on_gpu, nv_uvm_pref_gpu, down_sample, fft_wrt +``` + +Replace with: +```fortran + #:include 'generated_namelist_sim.fpp' +``` + +- [ ] **Step 4: Build simulation** + +```bash +./mfc.sh build -t simulation -j 8 +``` +Expected: successful. Errors → check NAMELIST_VARS for the missing variable. + +### Post-process + +- [ ] **Step 5: In `src/post_process/m_start_up.fpp`, replace lines 62–73** + +Current (lines 62–73): +```fortran + namelist /user_inputs/ case_dir, m, n, p, t_step_start, t_step_stop, t_step_save, model_eqns, num_fluids, mpp_lim, & + ... + & alpha_rho_e_wrt, ib_state_wrt +``` + +Replace with: +```fortran + #:include 'generated_namelist_post.fpp' +``` + +- [ ] **Step 6: Build post_process** + +```bash +./mfc.sh build -t post_process -j 8 +``` +Expected: successful. + +- [ ] **Step 7: Build all three targets together** + +```bash +./mfc.sh build -j 8 +``` +Expected: all three compile clean. + +- [ ] **Step 8: Run a subset of tests to verify runtime behavior** + +```bash +./mfc.sh test --only 1D -j 8 +``` +Expected: all pass. + +- [ ] **Step 9: Commit** + +```bash +git add src/pre_process/m_start_up.fpp src/simulation/m_start_up.fpp src/post_process/m_start_up.fpp +git commit -m "refactor(fortran): replace hand-written namelists with generated includes" +``` + +--- + +## Task 6: Replace declarations in `m_global_parameters.fpp` + +**Files:** +- Modify: `src/simulation/m_global_parameters.fpp` +- Modify: `src/pre_process/m_global_parameters.fpp` +- Modify: `src/post_process/m_global_parameters.fpp` + +### Strategy + +For each target: +1. Add `#:include 'generated_decls_{target}.fpp'` right after `implicit none` +2. Remove every declaration line for a variable that will be generated +3. Build and fix errors (duplicate declaration → remove from file; missing declaration → add to generator) + +**Which declarations get removed?** Any `integer :: foo`, `real(wp) :: bar`, `logical :: baz`, `character(...) :: qux` line where `foo`/`bar`/`baz`/`qux` is a simple scalar in `NAMELIST_VARS` for this target. + +**Which stay?** Everything else: derived types, allocatables, GPU_DECLARE macros, internal variables not in definitions.py, case optimization conditionals. + +### Simulation (`m_global_parameters.fpp`) + +- [ ] **Step 1: Add include after `implicit none` (line ~19)** + +After line `implicit none` in `src/simulation/m_global_parameters.fpp`, add: +```fortran + #:include 'generated_decls_sim.fpp' +``` + +- [ ] **Step 2: Build — expect duplicate declaration errors** + +```bash +./mfc.sh build -t simulation -j 8 2>&1 | grep -i "duplicate\|redeclared\|already" +``` + +- [ ] **Step 3: For each duplicate, remove the declaration from the file** + +Systematically remove every line of the form `integer :: foo`, `real(wp) :: bar`, `logical :: baz` where `foo`/`bar`/`baz` is in `generated_decls_sim.fpp`. Leave compound lines (e.g., `integer :: m, n, p`) if only some of those vars are generated — split them if needed. + +Also remove the Fypp loop at lines ~183-187 that generates `k_x, w_x, ...` etc., since those are now in `generated_decls_sim.fpp`. + +- [ ] **Step 4: Build simulation cleanly** + +```bash +./mfc.sh build -t simulation -j 8 +``` +Expected: no errors. + +- [ ] **Step 5: Run simulation tests** + +```bash +./mfc.sh test --only 1D -j 8 +``` +Expected: pass. + +### Pre-process (`m_global_parameters.fpp`) + +- [ ] **Step 6: Add include after `implicit none` in pre_process file** + +```fortran + #:include 'generated_decls_pre.fpp' +``` + +- [ ] **Step 7: Build and remove duplicates** + +```bash +./mfc.sh build -t pre_process -j 8 2>&1 | grep -i "duplicate\|redeclared" +``` +Remove listed duplicates from the file. + +- [ ] **Step 8: Build cleanly** + +```bash +./mfc.sh build -t pre_process -j 8 +``` + +### Post-process (`m_global_parameters.fpp`) + +- [ ] **Step 9: Add include after `implicit none` in post_process file** + +```fortran + #:include 'generated_decls_post.fpp' +``` + +- [ ] **Step 10: Build and remove duplicates** + +```bash +./mfc.sh build -t post_process -j 8 2>&1 | grep -i "duplicate\|redeclared" +``` +Remove listed duplicates from the file. + +- [ ] **Step 11: Build all three** + +```bash +./mfc.sh build -j 8 +``` +Expected: clean. + +- [ ] **Step 12: Run full 1D + 2D tests** + +```bash +./mfc.sh test --only 1D 2D -j 8 +``` +Expected: pass. + +- [ ] **Step 13: Commit** + +```bash +git add src/simulation/m_global_parameters.fpp src/pre_process/m_global_parameters.fpp src/post_process/m_global_parameters.fpp +git commit -m "refactor(fortran): replace hand-written scalar decls with generated includes" +``` + +--- + +## Task 7: Add precheck step + +**Files:** +- Modify: `toolchain/bootstrap/precheck.sh` + +- [ ] **Step 1: Add check 7/7 to `precheck.sh`** + +In `toolchain/bootstrap/precheck.sh`, find the block for check 6/6 (parameter docs). After the block that waits for `PID_PARAM_DOCS`, add a new parallel job for the generate check: + +```bash +# Generated Fortran files up-to-date check +( + if ./mfc.sh generate --check > /dev/null 2>&1; then + echo "0" > "$TMPDIR_PC/generate_exit" + else + echo "1" > "$TMPDIR_PC/generate_exit" + fi +) & +PID_GENERATE=$! +``` + +Then update the results-collection section. Change `6/6` references to `7/7` for the last check, and change the existing `6/6` block: + +Before the existing `6/6` log line: +```bash +log "[$CYAN 6/6$COLOR_RESET] Checking$MAGENTA parameter docs$COLOR_RESET..." +``` +Change to: +```bash +log "[$CYAN 6/7$COLOR_RESET] Checking$MAGENTA parameter docs$COLOR_RESET..." +``` + +Then add after the parameter docs block: + +```bash +wait $PID_GENERATE +log "[$CYAN 7/7$COLOR_RESET] Checking$MAGENTA generated Fortran files$COLOR_RESET..." +GENERATE_RC=$(cat "$TMPDIR_PC/generate_exit" 2>/dev/null || echo "1") +if [ "$GENERATE_RC" = "0" ]; then + ok "Generated Fortran files are up to date." +else + error "Generated Fortran files are out of date. Run$MAGENTA ./mfc.sh generate$COLOR_RESET to update." + FAILED=1 +fi +``` + +Also update the first log line from `(same checks as CI lint-gate)` wording and `1/6` references to `1/7`. + +- [ ] **Step 2: Test the new check** + +```bash +./mfc.sh precheck -j 8 +``` +Expected: `[7/7] Checking generated Fortran files... OK` + +- [ ] **Step 3: Verify it catches a stale file** + +Edit one line in `src/common/include/generated_namelist_sim.fpp` (add a comment), then run: +```bash +./mfc.sh precheck -j 8 +``` +Expected: `[7/7] Checking generated Fortran files... FAIL`. Revert the test change. + +- [ ] **Step 4: Commit** + +```bash +git add toolchain/bootstrap/precheck.sh +git commit -m "feat(precheck): add generated Fortran files up-to-date check (7/7)" +``` + +--- + +## Task 8: Final verification — full test suite + +- [ ] **Step 1: Run precheck** + +```bash +./mfc.sh precheck -j 8 +``` +Expected: all 7/7 checks pass. + +- [ ] **Step 2: Build with case optimization to verify CASE_OPT guard works** + +```bash +./mfc.sh build -t simulation --case-optimization -i examples/3d_taylor_green_vortex/case.py -j 8 +``` +Expected: successful compilation. The `#:if not MFC_CASE_OPTIMIZATION` guard in `generated_namelist_sim.fpp` must exclude the case-opt vars from the namelist at compile time. + +- [ ] **Step 3: Run full test suite** + +```bash +./mfc.sh test -j 8 +``` +Expected: same pass/fail count as before this change (zero new failures). + +- [ ] **Step 4: Verify new-parameter workflow end-to-end** + +Add a dummy parameter to prove the 2-location workflow works: + +In `definitions.py`, add: +```python +_r("test_codegen_param", INT) +``` + +In `namelist_targets.py`, add to `NAMELIST_VARS`: +```python +"test_codegen_param": {"sim"}, +``` + +Run `./mfc.sh generate` and verify `generated_namelist_sim.fpp` and `generated_decls_sim.fpp` contain `test_codegen_param`. + +Then revert both additions (this was just a smoke test): +```bash +git diff toolchain/mfc/params/definitions.py toolchain/mfc/params/namelist_targets.py +git checkout -- toolchain/mfc/params/definitions.py toolchain/mfc/params/namelist_targets.py +./mfc.sh generate +``` + +--- + +## Test Plan + +### Unit tests (run via `./mfc.sh lint`) + +| Test file | What it covers | +|-----------|----------------| +| `toolchain/tests/params/test_schema.py` | `str_len` field on `ParamDef` | +| `toolchain/tests/params/test_namelist_targets.py` | `NAMELIST_VARS` coverage per target; `CASE_OPT_EXCLUDE` | +| `toolchain/tests/params/test_fortran_gen.py` | `get_namelist_var`, `fortran_type_decl`, namelist/decl content per target | + +### Integration tests + +| Test | Command | Pass criterion | +|------|---------|----------------| +| Build all targets | `./mfc.sh build -j 8` | Zero compile errors | +| Case-optimized build | `./mfc.sh build -t simulation --case-optimization -i examples/3d_taylor_green_vortex/case.py -j 8` | Compiles; CASE_OPT vars absent from runtime namelist | +| 1D regression tests | `./mfc.sh test --only 1D -j 8` | Same pass count as before | +| 2D regression tests | `./mfc.sh test --only 2D -j 8` | Same pass count as before | +| Full test suite | `./mfc.sh test -j 8` | Zero new failures | + +### CI / precheck tests + +| Test | Command | Pass criterion | +|------|---------|----------------| +| Generated files up to date | `./mfc.sh generate --check` | Exit 0 | +| Full precheck | `./mfc.sh precheck -j 8` | All 7/7 checks pass | +| Stale file detection | Modify a generated `.fpp`, run `./mfc.sh generate --check` | Exit 1 | + +### Regression proof: new-parameter workflow + +**Before**: adding `test_codegen_param` to simulation only required editing 3 files (definitions.py, simulation/m_global_parameters.fpp, simulation/m_start_up.fpp). + +**After**: editing only `definitions.py` + `namelist_targets.py` + running `./mfc.sh generate` is sufficient. This is verified in Task 8 Step 4. diff --git a/src/common/include/generated_decls_post.fpp b/src/common/include/generated_decls_post.fpp deleted file mode 100644 index 060254edb3..0000000000 --- a/src/common/include/generated_decls_post.fpp +++ /dev/null @@ -1,101 +0,0 @@ -! AUTO-GENERATED - do not edit directly. Regenerate: ./mfc.sh generate -! -real(wp) :: Bx0 -real(wp) :: Ca -logical :: E_wrt -real(wp) :: R0ref -real(wp) :: Re_inv -real(wp) :: Web -logical :: adv_n -logical :: alt_soundspeed -integer :: avg_state -logical :: bubbles_euler -logical :: bubbles_lagrange -logical :: c_wrt -character(LEN=path_len) :: case_dir -logical :: cf_wrt -logical :: cfl_adap_dt -logical :: cfl_const_dt -real(wp) :: cfl_target -logical :: chem_wrt_T -logical :: cons_vars_wrt -logical :: cont_damage -logical :: cyl_coord -logical :: down_sample -integer :: fd_order -logical :: fft_wrt -logical :: file_per_process -integer :: flux_lim -integer :: format -logical :: gamma_wrt -logical :: heat_ratio_wrt -logical :: hyper_cleaning -logical :: hyperelasticity -logical :: hypoelasticity -logical :: ib -logical :: ib_state_wrt -logical :: igr -integer :: igr_order -logical :: lag_betaC_wrt -logical :: lag_betaT_wrt -logical :: lag_db_wrt -logical :: lag_dphidt_wrt -logical :: lag_header -logical :: lag_id_wrt -logical :: lag_mg_wrt -logical :: lag_mv_wrt -logical :: lag_pos_prev_wrt -logical :: lag_pos_wrt -logical :: lag_pres_wrt -logical :: lag_r0_wrt -logical :: lag_rad_wrt -logical :: lag_rmax_wrt -logical :: lag_rmin_wrt -logical :: lag_rvel_wrt -logical :: lag_txt_wrt -logical :: lag_vel_wrt -logical :: liutex_wrt -integer :: m -logical :: mhd -logical :: mixture_err -integer :: model_eqns -logical :: mpp_lim -integer :: muscl_order -integer :: n -integer :: n_start -integer :: nb -integer :: num_bc_patches -integer :: num_fluids -integer :: num_ibs -logical :: output_partial_domain -integer :: p -logical :: parallel_io -logical :: pi_inf_wrt -real(wp) :: poly_sigma -logical :: polydisperse -logical :: polytropic -integer :: precision -real(wp) :: pref -logical :: pres_inf_wrt -logical :: pres_wrt -logical :: prim_vars_wrt -logical :: qbmm -logical :: qm_wrt -integer :: recon_type -logical :: relativity -logical :: relax -integer :: relax_model -logical :: rho_wrt -real(wp) :: rhoref -logical :: schlieren_wrt -real(wp) :: sigR -real(wp) :: sigma -logical :: sim_data -logical :: surface_tension -real(wp) :: t_save -integer :: t_step_save -integer :: t_step_start -integer :: t_step_stop -real(wp) :: t_stop -integer :: thermal -integer :: weno_order diff --git a/src/common/include/generated_decls_pre.fpp b/src/common/include/generated_decls_pre.fpp deleted file mode 100644 index 26b7b04ca6..0000000000 --- a/src/common/include/generated_decls_pre.fpp +++ /dev/null @@ -1,95 +0,0 @@ -! AUTO-GENERATED - do not edit directly. Regenerate: ./mfc.sh generate -! -real(wp) :: Bx0 -real(wp) :: Ca -real(wp) :: R0ref -real(wp) :: Re_inv -real(wp) :: Web -real(wp) :: a_x -real(wp) :: a_y -real(wp) :: a_z -logical :: adv_n -logical :: bubbles_euler -logical :: bubbles_lagrange -character(LEN=path_len) :: case_dir -logical :: cfl_adap_dt -logical :: cfl_const_dt -logical :: cont_damage -logical :: cyl_coord -integer :: dist_type -logical :: down_sample -logical :: elliptic_smoothing -integer :: elliptic_smoothing_iters -logical :: fft_wrt -logical :: file_per_process -logical :: hyper_cleaning -logical :: hyperelasticity -logical :: hypoelasticity -logical :: ib -logical :: igr -integer :: igr_order -integer :: loops_x -integer :: loops_y -integer :: loops_z -integer :: m -logical :: mhd -logical :: mixlayer_perturb -real(wp) :: mixlayer_perturb_k0 -integer :: mixlayer_perturb_nk -real(wp) :: mixlayer_vel_coef -logical :: mixlayer_vel_profile -integer :: model_eqns -logical :: mpp_lim -integer :: muscl_order -integer :: n -integer :: n_start -integer :: n_start_old -integer :: nb -integer :: num_bc_patches -integer :: num_fluids -integer :: num_ibs -integer :: num_patches -logical :: old_grid -logical :: old_ic -integer :: p -real(wp) :: palpha_eps -logical :: parallel_io -logical :: perturb_flow -integer :: perturb_flow_fluid -real(wp) :: perturb_flow_mag -logical :: perturb_sph -integer :: perturb_sph_fluid -real(wp) :: pi_fac -real(wp) :: poly_sigma -logical :: polydisperse -logical :: polytropic -logical :: pre_stress -integer :: precision -real(wp) :: pref -real(wp) :: ptgalpha_eps -logical :: qbmm -integer :: recon_type -logical :: relativity -logical :: relax -integer :: relax_model -real(wp) :: rhoRV -real(wp) :: rhoref -real(wp) :: sigR -real(wp) :: sigV -real(wp) :: sigma -logical :: simplex_perturb -logical :: stretch_x -logical :: stretch_y -logical :: stretch_z -logical :: surface_tension -integer :: t_step_old -integer :: t_step_start -integer :: thermal -logical :: viscous -integer :: weno_order -real(wp) :: x_a -real(wp) :: x_b -real(wp) :: y_a -real(wp) :: y_b -real(wp) :: z_a -real(wp) :: z_b diff --git a/src/common/include/generated_decls_sim.fpp b/src/common/include/generated_decls_sim.fpp deleted file mode 100644 index cc4f9dfc35..0000000000 --- a/src/common/include/generated_decls_sim.fpp +++ /dev/null @@ -1,140 +0,0 @@ -! AUTO-GENERATED - do not edit directly. Regenerate: ./mfc.sh generate -! -real(wp) :: Bx0 -real(wp) :: Ca -real(wp) :: R0ref -real(wp) :: Re_inv -real(wp) :: Web -logical :: acoustic_source -logical :: adap_dt -integer :: adap_dt_max_iters -real(wp) :: adap_dt_tol -logical :: adv_n -real(wp) :: alf_factor -real(wp) :: alpha_bar -logical :: alt_soundspeed -integer :: avg_state -logical :: bf_x -logical :: bf_y -logical :: bf_z -integer :: bubble_model -logical :: bubbles_euler -logical :: bubbles_lagrange -character(LEN=path_len) :: case_dir -logical :: cfl_adap_dt -logical :: cfl_const_dt -real(wp) :: cfl_target -real(wp) :: coefficient_of_restitution -integer :: collision_model -real(wp) :: collision_time -logical :: cont_damage -real(wp) :: cont_damage_s -logical :: cyl_coord -logical :: down_sample -real(wp) :: dt -integer :: fd_order -logical :: fft_wrt -logical :: file_per_process -real(wp) :: g_x -real(wp) :: g_y -real(wp) :: g_z -logical :: hyper_cleaning -real(wp) :: hyper_cleaning_speed -real(wp) :: hyper_cleaning_tau -logical :: hyperelasticity -logical :: hypoelasticity -logical :: ib -real(wp) :: ib_coefficient_of_friction -logical :: ib_state_wrt -real(wp) :: ic_beta -real(wp) :: ic_eps -logical :: igr -integer :: igr_iter_solver -integer :: igr_order -logical :: igr_pres_lim -integer :: int_comp -logical :: integral_wrt -real(wp) :: k_x -real(wp) :: k_y -real(wp) :: k_z -integer :: low_Mach -integer :: m -logical :: mapped_weno -logical :: mhd -logical :: mixture_err -integer :: model_eqns -logical :: mp_weno -logical :: mpp_lim -real(wp) :: muscl_eps -integer :: muscl_lim -integer :: muscl_order -integer :: n -integer :: n_start -integer :: nb -logical :: null_weights -integer :: num_bc_patches -integer :: num_fluids -integer :: num_ibs -integer :: num_igr_iters -integer :: num_igr_warm_start_iters -integer :: num_integrals -integer :: num_probes -integer :: num_source -integer :: nv_uvm_igr_temps_on_gpu -logical :: nv_uvm_out_of_core -logical :: nv_uvm_pref_gpu -integer :: p -real(wp) :: p_x -real(wp) :: p_y -real(wp) :: p_z -real(wp) :: palpha_eps -logical :: parallel_io -real(wp) :: pi_fac -real(wp) :: poly_sigma -logical :: polydisperse -logical :: polytropic -integer :: precision -real(wp) :: pref -logical :: prim_vars_wrt -logical :: probe_wrt -real(wp) :: ptgalpha_eps -logical :: qbmm -logical :: rdma_mpi -integer :: recon_type -logical :: relativity -logical :: relax -integer :: relax_model -real(wp) :: rhoref -integer :: riemann_solver -logical :: run_time_info -real(wp) :: sigma -logical :: surface_tension -real(wp) :: t_save -integer :: t_step_old -integer :: t_step_print -integer :: t_step_save -integer :: t_step_start -integer :: t_step_stop -real(wp) :: t_stop -real(wp) :: tau_star -logical :: teno -real(wp) :: teno_CT -integer :: thermal -integer :: time_stepper -logical :: viscous -real(wp) :: w_x -real(wp) :: w_y -real(wp) :: w_z -integer :: wave_speeds -logical :: weno_Re_flux -logical :: weno_avg -real(wp) :: weno_eps -integer :: weno_order -logical :: wenoz -real(wp) :: wenoz_q -real(wp) :: x_a -real(wp) :: x_b -real(wp) :: y_a -real(wp) :: y_b -real(wp) :: z_a -real(wp) :: z_b diff --git a/src/common/include/generated_namelist_post.fpp b/src/common/include/generated_namelist_post.fpp deleted file mode 100644 index 16d981c763..0000000000 --- a/src/common/include/generated_namelist_post.fpp +++ /dev/null @@ -1,14 +0,0 @@ -! AUTO-GENERATED - do not edit directly. Regenerate: ./mfc.sh generate -! -namelist /user_inputs/ Bx0, Ca, E_wrt, G, R0ref, Re_inv, Web, adv_n, alpha_rho_e_wrt, alpha_rho_wrt, alpha_wrt, alt_soundspeed, & - & avg_state, bc_x, bc_y, bc_z, bub_pp, bubbles_euler, bubbles_lagrange, c_wrt, case_dir, cf_wrt, cfl_adap_dt, cfl_const_dt, & - & cfl_target, chem_wrt_T, chem_wrt_Y, cons_vars_wrt, cont_damage, cyl_coord, down_sample, fd_order, fft_wrt, & - & file_per_process, fluid_pp, flux_lim, flux_wrt, format, gamma_wrt, heat_ratio_wrt, hyper_cleaning, hyperelasticity, & - & hypoelasticity, ib, ib_state_wrt, igr, igr_order, lag_betaC_wrt, lag_betaT_wrt, lag_db_wrt, lag_dphidt_wrt, lag_header, & - & lag_id_wrt, lag_mg_wrt, lag_mv_wrt, lag_pos_prev_wrt, lag_pos_wrt, lag_pres_wrt, lag_r0_wrt, lag_rad_wrt, lag_rmax_wrt, & - & lag_rmin_wrt, lag_rvel_wrt, lag_txt_wrt, lag_vel_wrt, liutex_wrt, m, mhd, mixture_err, model_eqns, mom_wrt, mpp_lim, & - & muscl_order, n, n_start, nb, num_bc_patches, num_fluids, num_ibs, omega_wrt, output_partial_domain, p, parallel_io, & - & pi_inf_wrt, poly_sigma, polydisperse, polytropic, precision, pref, pres_inf_wrt, pres_wrt, prim_vars_wrt, qbmm, qm_wrt, & - & recon_type, relativity, relax, relax_model, rho_wrt, rhoref, schlieren_alpha, schlieren_wrt, sigR, sigma, sim_data, & - & surface_tension, t_save, t_step_save, t_step_start, t_step_stop, t_stop, thermal, vel_wrt, weno_order, x_output, y_output, & - & z_output diff --git a/src/common/include/generated_namelist_pre.fpp b/src/common/include/generated_namelist_pre.fpp deleted file mode 100644 index 89e0a50a98..0000000000 --- a/src/common/include/generated_namelist_pre.fpp +++ /dev/null @@ -1,12 +0,0 @@ -! AUTO-GENERATED - do not edit directly. Regenerate: ./mfc.sh generate -! -namelist /user_inputs/ Bx0, Ca, R0ref, Re_inv, Web, a_x, a_y, a_z, adv_n, bc_x, bc_y, bc_z, bub_pp, bubbles_euler, & - & bubbles_lagrange, case_dir, cfl_adap_dt, cfl_const_dt, cont_damage, cyl_coord, dist_type, down_sample, elliptic_smoothing, & - & elliptic_smoothing_iters, fft_wrt, file_per_process, fluid_pp, fluid_rho, hyper_cleaning, hyperelasticity, hypoelasticity, & - & ib, igr, igr_order, loops_x, loops_y, loops_z, m, mhd, mixlayer_perturb, mixlayer_perturb_k0, mixlayer_perturb_nk, & - & mixlayer_vel_coef, mixlayer_vel_profile, model_eqns, mpp_lim, muscl_order, n, n_start, n_start_old, nb, num_bc_patches, & - & num_fluids, num_ibs, num_patches, old_grid, old_ic, p, palpha_eps, parallel_io, patch_bc, patch_ib, patch_icpp, & - & perturb_flow, perturb_flow_fluid, perturb_flow_mag, perturb_sph, perturb_sph_fluid, pi_fac, poly_sigma, polydisperse, & - & polytropic, pre_stress, precision, pref, ptgalpha_eps, qbmm, recon_type, relativity, relax, relax_model, rhoRV, rhoref, & - & sigR, sigV, sigma, simplex_params, simplex_perturb, stretch_x, stretch_y, stretch_z, surface_tension, t_step_old, & - & t_step_start, thermal, viscous, weno_order, x_a, x_b, x_domain, y_a, y_b, y_domain, z_a, z_b, z_domain diff --git a/src/common/include/generated_namelist_sim.fpp b/src/common/include/generated_namelist_sim.fpp deleted file mode 100644 index da5fc9b1cb..0000000000 --- a/src/common/include/generated_namelist_sim.fpp +++ /dev/null @@ -1,19 +0,0 @@ -! AUTO-GENERATED - do not edit directly. Regenerate: ./mfc.sh generate -! -namelist /user_inputs/ Bx0, Ca, R0ref, Re_inv, Web, acoustic, acoustic_source, adap_dt, adap_dt_max_iters, adap_dt_tol, adv_n, & - & alf_factor, alpha_bar, alt_soundspeed, avg_state, bc_x, bc_y, bc_z, bf_x, bf_y, bf_z, bub_pp, bubble_model, bubbles_euler, & - & bubbles_lagrange, case_dir, cfl_adap_dt, cfl_const_dt, cfl_target, chem_params, coefficient_of_restitution, & - & collision_model, collision_time, cont_damage, cont_damage_s, cyl_coord, down_sample, dt, fd_order, fft_wrt, & - & file_per_process, fluid_pp, g_x, g_y, g_z, hyper_cleaning, hyper_cleaning_speed, hyper_cleaning_tau, hyperelasticity, & - & hypoelasticity, ib, ib_coefficient_of_friction, ib_state_wrt, ic_beta, ic_eps, int_comp, integral, integral_wrt, k_x, k_y, & - & k_z, lag_params, low_Mach, m, mixture_err, model_eqns, mp_weno, mpp_lim, muscl_eps, n, n_start, null_weights, & - & num_bc_patches, num_ibs, num_igr_iters, num_igr_warm_start_iters, num_integrals, num_probes, num_source, & - & nv_uvm_igr_temps_on_gpu, nv_uvm_out_of_core, nv_uvm_pref_gpu, p, p_x, p_y, p_z, palpha_eps, parallel_io, patch_ib, pi_fac, & - & poly_sigma, polydisperse, polytropic, precision, pref, prim_vars_wrt, probe, probe_wrt, ptgalpha_eps, qbmm, rdma_mpi, & - & relax, relax_model, rhoref, riemann_solver, run_time_info, sigma, surface_tension, t_save, t_step_old, t_step_print, & - & t_step_save, t_step_start, t_step_stop, t_stop, tau_star, teno_CT, thermal, time_stepper, w_x, w_y, w_z, wave_speeds, & - & weno_Re_flux, weno_avg, weno_eps, x_a, x_b, x_domain, y_a, y_b, y_domain, z_a, z_b, z_domain, & -#:if not MFC_CASE_OPTIMIZATION - & igr, igr_iter_solver, igr_order, igr_pres_lim, mapped_weno, mhd, muscl_lim, muscl_order, nb, num_fluids, recon_type, & - & relativity, teno, viscous, weno_order, wenoz, wenoz_q -#:endif diff --git a/src/post_process/m_global_parameters.fpp b/src/post_process/m_global_parameters.fpp index 967d62755d..42eb83b98b 100644 --- a/src/post_process/m_global_parameters.fpp +++ b/src/post_process/m_global_parameters.fpp @@ -17,7 +17,7 @@ module m_global_parameters implicit none - #:include 'generated_decls_post.fpp' + #:include 'generated_decls.fpp' !> @name Logistics !> @{ diff --git a/src/post_process/m_start_up.fpp b/src/post_process/m_start_up.fpp index 0ed57f7e4e..708234797b 100644 --- a/src/post_process/m_start_up.fpp +++ b/src/post_process/m_start_up.fpp @@ -59,7 +59,7 @@ contains integer :: iostatus character(len=1000) :: line - #:include 'generated_namelist_post.fpp' + #:include 'generated_namelist.fpp' file_loc = 'post_process.inp' inquire (FILE=trim(file_loc), EXIST=file_check) diff --git a/src/pre_process/m_global_parameters.fpp b/src/pre_process/m_global_parameters.fpp index 9a83396fe9..d1388effee 100644 --- a/src/pre_process/m_global_parameters.fpp +++ b/src/pre_process/m_global_parameters.fpp @@ -17,7 +17,7 @@ module m_global_parameters implicit none - #:include 'generated_decls_pre.fpp' + #:include 'generated_decls.fpp' ! Logistics integer :: num_procs !< Number of processors diff --git a/src/pre_process/m_start_up.fpp b/src/pre_process/m_start_up.fpp index 0a1e7f8d9c..7fabdc796a 100644 --- a/src/pre_process/m_start_up.fpp +++ b/src/pre_process/m_start_up.fpp @@ -74,7 +74,7 @@ contains integer :: iostatus character(len=1000) :: line - #:include 'generated_namelist_pre.fpp' + #:include 'generated_namelist.fpp' file_loc = 'pre_process.inp' inquire (FILE=trim(file_loc), EXIST=file_check) diff --git a/src/simulation/m_global_parameters.fpp b/src/simulation/m_global_parameters.fpp index 821258c167..2c07e4d9c8 100644 --- a/src/simulation/m_global_parameters.fpp +++ b/src/simulation/m_global_parameters.fpp @@ -18,7 +18,7 @@ module m_global_parameters implicit none - #:include 'generated_decls_sim.fpp' + #:include 'generated_decls.fpp' real(wp) :: wall_time = 0 real(wp) :: wall_time_avg = 0 @@ -70,7 +70,7 @@ module m_global_parameters integer :: num_dims !< Number of spatial dimensions integer :: num_vels !< Number of velocity components (different from num_dims for mhd) #:endif - ! mpp_lim, time_stepper, prim_vars_wrt now in generated_decls_sim.fpp + ! mpp_lim, time_stepper, prim_vars_wrt now in generated_decls.fpp #:if MFC_CASE_OPTIMIZATION integer, parameter :: recon_type = ${recon_type}$ !< Reconstruction type diff --git a/src/simulation/m_start_up.fpp b/src/simulation/m_start_up.fpp index ea872f1981..6fdc451ba7 100644 --- a/src/simulation/m_start_up.fpp +++ b/src/simulation/m_start_up.fpp @@ -82,7 +82,7 @@ contains character(len=1000) :: line - #:include 'generated_namelist_sim.fpp' + #:include 'generated_namelist.fpp' inquire (FILE=trim(file_path), EXIST=file_exist) diff --git a/toolchain/bootstrap/precheck.sh b/toolchain/bootstrap/precheck.sh index 786f64d41d..fc0effae08 100755 --- a/toolchain/bootstrap/precheck.sh +++ b/toolchain/bootstrap/precheck.sh @@ -59,7 +59,7 @@ done # CI runs the full suite via ./mfc.sh lint without this variable. export MFC_SKIP_RENDER_TESTS=1 -log "Running$MAGENTA precheck$COLOR_RESET (same checks as CI lint-gate: 7/7)..." +log "Running$MAGENTA precheck$COLOR_RESET (same checks as CI lint-gate)..." echo "" # Temp files for collecting results from parallel jobs @@ -127,21 +127,11 @@ fi ) & PID_PARAM_DOCS=$! -# Generated Fortran files up-to-date check -( - if ./mfc.sh generate --check > /dev/null 2>&1; then - echo "0" > "$TMPDIR_PC/generate_exit" - else - echo "1" > "$TMPDIR_PC/generate_exit" - fi -) & -PID_GENERATE=$! - # --- Collect results --- FAILED=0 -log "[$CYAN 1/7$COLOR_RESET] Checking$MAGENTA formatting$COLOR_RESET..." +log "[$CYAN 1/6$COLOR_RESET] Checking$MAGENTA formatting$COLOR_RESET..." if [ "$FORMAT_OK" = "1" ]; then error "Formatting check failed to run." FAILED=1 @@ -156,7 +146,7 @@ else fi wait $PID_SPELL -log "[$CYAN 2/7$COLOR_RESET] Running$MAGENTA spell check$COLOR_RESET..." +log "[$CYAN 2/6$COLOR_RESET] Running$MAGENTA spell check$COLOR_RESET..." SPELL_RC=$(cat "$TMPDIR_PC/spell_exit" 2>/dev/null || echo "1") if [ "$SPELL_RC" = "0" ]; then ok "Spell check passed." @@ -166,7 +156,7 @@ else fi wait $PID_LINT -log "[$CYAN 3/7$COLOR_RESET] Running$MAGENTA toolchain lint$COLOR_RESET..." +log "[$CYAN 3/6$COLOR_RESET] Running$MAGENTA toolchain lint$COLOR_RESET..." LINT_RC=$(cat "$TMPDIR_PC/lint_exit" 2>/dev/null || echo "1") if [ "$LINT_RC" = "0" ]; then ok "Toolchain lint passed." @@ -176,7 +166,7 @@ else fi wait $PID_SOURCE -log "[$CYAN 4/7$COLOR_RESET] Running$MAGENTA source lint$COLOR_RESET..." +log "[$CYAN 4/6$COLOR_RESET] Running$MAGENTA source lint$COLOR_RESET..." SOURCE_RC=$(cat "$TMPDIR_PC/source_exit" 2>/dev/null || echo "1") if [ "$SOURCE_RC" = "0" ]; then ok "Source lint passed." @@ -185,7 +175,7 @@ else FAILED=1 fi -log "[$CYAN 5/7$COLOR_RESET] Checking$MAGENTA doc references$COLOR_RESET..." +log "[$CYAN 5/6$COLOR_RESET] Checking$MAGENTA doc references$COLOR_RESET..." if [ $DOC_FAILED -eq 0 ]; then ok "Doc references are valid." else @@ -194,7 +184,7 @@ else fi wait $PID_PARAM_DOCS -log "[$CYAN 6/7$COLOR_RESET] Checking$MAGENTA parameter docs$COLOR_RESET..." +log "[$CYAN 6/6$COLOR_RESET] Checking$MAGENTA parameter docs$COLOR_RESET..." PARAM_DOCS_RC=$(cat "$TMPDIR_PC/param_docs_exit" 2>/dev/null || echo "1") if [ "$PARAM_DOCS_RC" = "0" ]; then ok "Parameter documentation check passed." @@ -203,16 +193,6 @@ else FAILED=1 fi -wait $PID_GENERATE -log "[$CYAN 7/7$COLOR_RESET] Checking$MAGENTA generated Fortran files$COLOR_RESET..." -GENERATE_RC=$(cat "$TMPDIR_PC/generate_exit" 2>/dev/null || echo "1") -if [ "$GENERATE_RC" = "0" ]; then - ok "Generated Fortran files are up to date." -else - error "Generated Fortran files are out of date. Run$MAGENTA ./mfc.sh generate$COLOR_RESET to update." - FAILED=1 -fi - echo "" if [ $FAILED -eq 0 ]; then diff --git a/toolchain/mfc/generate.py b/toolchain/mfc/generate.py index 374e57b6b1..a6a6273c38 100644 --- a/toolchain/mfc/generate.py +++ b/toolchain/mfc/generate.py @@ -76,11 +76,6 @@ def generate(): (docs_dir / "parameters.md", generate_parameter_docs()), ] + _constraint_docs(docs_dir) - from .params.generators.fortran_gen import get_generated_files - - include_dir = Path(MFC_ROOT_DIR) / "src" / "common" / "include" - files += get_generated_files(include_dir) - all_ok = True for path, content in files: if not _check_or_write(path, content, check_mode): diff --git a/toolchain/mfc/lint_param_docs.py b/toolchain/mfc/lint_param_docs.py index fd0c36cc41..425fd53900 100644 --- a/toolchain/mfc/lint_param_docs.py +++ b/toolchain/mfc/lint_param_docs.py @@ -84,13 +84,14 @@ def _parse_namelist_params(fpp_path: Path) -> set[str]: """Parse parameter names from a namelist /user_inputs/ block in an fpp file.""" text = fpp_path.read_text(encoding="utf-8") - # If the namelist is in a #:include'd generated file, resolve it. - include_match = re.search(r"#:include\s+'(generated_namelist_\w+\.fpp)'", text) - if include_match: - include_name = include_match.group(1) - include_path = fpp_path.parent.parent / "common" / "include" / include_name - if include_path.exists(): - text = include_path.read_text(encoding="utf-8") + # If the namelist is in a #:include'd generated file, generate in-memory. + if re.search(r"#:include\s+'generated_namelist\.fpp'", text): + _target_map = {"pre_process": "pre", "simulation": "sim", "post_process": "post"} + short = _target_map.get(fpp_path.parent.name) + if short: + from mfc.params.generators.fortran_gen import generate_namelist_fpp + + text = generate_namelist_fpp(short) params = set() diff --git a/toolchain/mfc/params/generators/cmake_gen.py b/toolchain/mfc/params/generators/cmake_gen.py new file mode 100644 index 0000000000..53b98098c9 --- /dev/null +++ b/toolchain/mfc/params/generators/cmake_gen.py @@ -0,0 +1,36 @@ +""" +Generate Fortran parameter .fpp files into the CMake build directory. + +Called by CMakeLists.txt at configure time: + python3 cmake_gen.py + +Writes generated_namelist.fpp and generated_decls.fpp into + /include/{pre_process,simulation,post_process}/ +""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent)) + +from mfc.params.generators.fortran_gen import generate_decls_fpp, generate_namelist_fpp + +_TARGETS = [ + ("pre", "pre_process"), + ("sim", "simulation"), + ("post", "post_process"), +] + + +def main(build_dir: Path) -> None: + for short, full in _TARGETS: + out_dir = build_dir / "include" / full + out_dir.mkdir(parents=True, exist_ok=True) + (out_dir / "generated_namelist.fpp").write_text(generate_namelist_fpp(short)) + (out_dir / "generated_decls.fpp").write_text(generate_decls_fpp(short)) + + +if __name__ == "__main__": + if len(sys.argv) != 2: + sys.exit(f"Usage: {sys.argv[0]} ") + main(Path(sys.argv[1])) diff --git a/toolchain/mfc/params/generators/fortran_gen.py b/toolchain/mfc/params/generators/fortran_gen.py index 88761a0bef..06ab78c878 100644 --- a/toolchain/mfc/params/generators/fortran_gen.py +++ b/toolchain/mfc/params/generators/fortran_gen.py @@ -9,8 +9,7 @@ """ import re -from pathlib import Path -from typing import List, Tuple +from typing import List import mfc.params.definitions # noqa: F401 - triggers registry population @@ -146,15 +145,3 @@ def generate_decls_fpp(target: str) -> str: padded = type_str.ljust(_DECL_COL) lines.append(f"{padded}:: {name}") return "\n".join(lines) + "\n" - - -def get_generated_files(include_dir: Path) -> List[Tuple[Path, str]]: - """Return (output_path, content) for all six generated .fpp files.""" - return [ - (include_dir / "generated_namelist_pre.fpp", generate_namelist_fpp("pre")), - (include_dir / "generated_namelist_sim.fpp", generate_namelist_fpp("sim")), - (include_dir / "generated_namelist_post.fpp", generate_namelist_fpp("post")), - (include_dir / "generated_decls_pre.fpp", generate_decls_fpp("pre")), - (include_dir / "generated_decls_sim.fpp", generate_decls_fpp("sim")), - (include_dir / "generated_decls_post.fpp", generate_decls_fpp("post")), - ] diff --git a/toolchain/mfc/params/namelist_parser.py b/toolchain/mfc/params/namelist_parser.py index d99ca7ad08..295df38412 100644 --- a/toolchain/mfc/params/namelist_parser.py +++ b/toolchain/mfc/params/namelist_parser.py @@ -408,14 +408,16 @@ def parse_namelist_from_file(filepath: Path) -> Set[str]: """ content = filepath.read_text() - # Handle #:include 'generated_namelist_*.fpp' — resolve to the included file. - include_match = re.search(r"#:include\s+'(generated_namelist_\w+\.fpp)'", content) - if include_match: - include_name = include_match.group(1) - include_path = filepath.parent.parent / "common" / "include" / include_name - if not include_path.exists(): - raise ValueError(f"Included namelist file not found: {include_path}") - content = include_path.read_text() + # Handle #:include 'generated_namelist.fpp' — generate content in-memory. + if re.search(r"#:include\s+'generated_namelist\.fpp'", content): + _target_map = {"pre_process": "pre", "simulation": "sim", "post_process": "post"} + target_name = filepath.parent.name + short = _target_map.get(target_name) + if short is None: + raise ValueError(f"Cannot determine MFC target from path: {filepath}") + from .generators.fortran_gen import generate_namelist_fpp + + content = generate_namelist_fpp(short) # Find the namelist block - starts with "namelist /user_inputs/" # and continues until a line without continuation (&), a blank line, or end-of-string diff --git a/toolchain/tests/__init__.py b/toolchain/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/toolchain/tests/params/__init__.py b/toolchain/tests/params/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 From 08b7f08cc6c781765c036e9b0b1c75f24b375205 Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Wed, 27 May 2026 23:52:18 -0400 Subject: [PATCH 10/33] chore: remove plan doc from vcs --- .../plans/2026-05-27-fortran-codegen.md | 1366 ----------------- 1 file changed, 1366 deletions(-) delete mode 100644 docs/superpowers/plans/2026-05-27-fortran-codegen.md diff --git a/docs/superpowers/plans/2026-05-27-fortran-codegen.md b/docs/superpowers/plans/2026-05-27-fortran-codegen.md deleted file mode 100644 index 9557f1103d..0000000000 --- a/docs/superpowers/plans/2026-05-27-fortran-codegen.md +++ /dev/null @@ -1,1366 +0,0 @@ -# Fortran Param Codegen Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Auto-generate Fortran namelist declarations and simple scalar variable declarations from `definitions.py`, reducing new-parameter additions from 7 manual file edits to ≤2. - -**Architecture:** A new `fortran_gen.py` generator (fitting the existing generator pattern in `params/generators/`) reads `definitions.py` + a new `namelist_targets.py` to produce six `.fpp` include files — one namelist fragment and one declarations fragment per target (pre/sim/post). These are checked into `src/common/include/`, regenerated via `./mfc.sh generate`, and verified up-to-date by a new `precheck.sh` step. - -**Tech Stack:** Python 3 (existing toolchain), Fypp preprocessor (for `#:include`), Fortran namelists, MFC's existing `generate.py` check-or-write infrastructure. - ---- - -## Background: Current Pain - -Adding one simple scalar used by all three targets currently requires edits in 7 places: -1. `toolchain/mfc/params/definitions.py` -2-4. `src/{pre_process,simulation,post_process}/m_global_parameters.fpp` (variable declaration) -5-7. `src/{pre_process,simulation,post_process}/m_start_up.fpp` (namelist entry) - -After this plan: **1–2 places** — just `definitions.py` + `namelist_targets.py`. - ---- - -## File Map - -### New files -| File | Responsibility | -|------|----------------| -| `toolchain/mfc/params/namelist_targets.py` | `NAMELIST_VARS` dict (namelist_var → targets) + `CASE_OPT_EXCLUDE` set | -| `toolchain/mfc/params/generators/fortran_gen.py` | Generator: produces namelist + decls `.fpp` content per target | -| `src/common/include/generated_namelist_pre.fpp` | Generated: pre_process namelist fragment | -| `src/common/include/generated_namelist_sim.fpp` | Generated: simulation namelist fragment (with Fypp CASE_OPT guard) | -| `src/common/include/generated_namelist_post.fpp` | Generated: post_process namelist fragment | -| `src/common/include/generated_decls_pre.fpp` | Generated: pre_process simple scalar declarations | -| `src/common/include/generated_decls_sim.fpp` | Generated: simulation simple scalar declarations | -| `src/common/include/generated_decls_post.fpp` | Generated: post_process simple scalar declarations | - -### Modified files -| File | Change | -|------|--------| -| `toolchain/mfc/params/schema.py` | Add `str_len: str = "name_len"` to `ParamDef` | -| `toolchain/mfc/params/definitions.py` | Set `str_len="path_len"` on `case_dir` | -| `toolchain/mfc/generate.py` | Register six new generated files | -| `toolchain/bootstrap/precheck.sh` | Add check 7/7: `./mfc.sh generate --check` | -| `src/pre_process/m_start_up.fpp:77-87` | Replace namelist block with `#:include` | -| `src/simulation/m_start_up.fpp:85-115` | Replace namelist block with `#:include` | -| `src/post_process/m_start_up.fpp:62-73` | Replace namelist block with `#:include` | -| `src/pre_process/m_global_parameters.fpp` | Remove generated scalars, add `#:include` after implicit none | -| `src/simulation/m_global_parameters.fpp` | Remove generated scalars, add `#:include` after implicit none | -| `src/post_process/m_global_parameters.fpp` | Remove generated scalars, add `#:include` after implicit none | - ---- - -## Key Design Decisions - -**`namelist_var` derivation (no new ParamDef field needed):** -```python -import re - -def get_namelist_var(param_name: str) -> str: - # fluid_pp(1)%gamma -> fluid_pp - m = re.match(r'^([a-zA-Z_]\w*)\(\d', param_name) - if m: - return m.group(1) - # bc_x%beg, lag_params%foo -> bc_x, lag_params - if '%' in param_name: - return param_name.split('%')[0] - # Simple scalar: m, n, dt, model_eqns - return param_name -``` - -**Simple scalars** (candidates for declaration generation) = params with no `%` or `(` in their name, with `ParamType` of INT, REAL, LOG, or STR. - -**`NAMELIST_TARGETS` structure** (one source of truth for what goes where): -```python -NAMELIST_VARS: Dict[str, Set[str]] = { - "m": {"pre", "sim", "post"}, - "bc_x": {"pre", "sim", "post"}, - "patch_icpp": {"pre"}, - "run_time_info": {"sim"}, - ... -} -CASE_OPT_EXCLUDE: Set[str] = { - "nb", "mapped_weno", "wenoz", "teno", "wenoz_q", "weno_order", - "num_fluids", "mhd", "relativity", "igr_order", "viscous", - "igr_iter_solver", "igr", "igr_pres_lim", "recon_type", "muscl_order", "muscl_lim", -} -``` - -**Generated namelist format** (for simulation): -```fortran -! AUTO-GENERATED — do not edit. Regenerate with: ./mfc.sh generate -namelist /user_inputs/ m, n, p, dt, model_eqns, bc_x, bc_y, bc_z, & - & fluid_pp, bub_pp, ... -#:if not MFC_CASE_OPTIMIZATION - & nb, mapped_weno, wenoz, weno_order, ... -#:endif - & last_var -``` - -**Generated declarations format**: -```fortran -! AUTO-GENERATED — do not edit. Regenerate with: ./mfc.sh generate -integer :: model_eqns -real(wp) :: dt -logical :: cyl_coord -character(LEN=path_len) :: case_dir -``` - ---- - -## Task 1: Extend ParamDef for STR length - -**Files:** -- Modify: `toolchain/mfc/params/schema.py` -- Modify: `toolchain/mfc/params/definitions.py` - -- [ ] **Step 1: Write a failing test in `toolchain/tests/params/test_schema.py`** - -```python -def test_paramdef_str_len_default(): - from mfc.params.schema import ParamDef, ParamType - p = ParamDef(name="foo", param_type=ParamType.STR) - assert p.str_len == "name_len" - -def test_paramdef_str_len_override(): - from mfc.params.schema import ParamDef, ParamType - p = ParamDef(name="case_dir", param_type=ParamType.STR, str_len="path_len") - assert p.str_len == "path_len" -``` - -- [ ] **Step 2: Run to verify failure** - -```bash -cd /path/to/mfc -python3 -m pytest toolchain/tests/params/test_schema.py::test_paramdef_str_len_default -v -``` -Expected: `AttributeError: str_len` - -- [ ] **Step 3: Add `str_len` to `ParamDef` in `schema.py`** - -In `toolchain/mfc/params/schema.py`, add to the `@dataclass class ParamDef:` block after `math_symbol`: -```python -str_len: str = "name_len" -# For STR type: Fortran character length constant. Default "name_len"; set "path_len" for case_dir. -``` - -- [ ] **Step 4: Set `str_len` on `case_dir` in `definitions.py`** - -In `toolchain/mfc/params/definitions.py`, find the line: -```python - _r("case_dir", STR) -``` -Replace with: -```python - _r("case_dir", STR, str_len="path_len") -``` - -This requires updating `_r()` to accept and forward `str_len`. Find `_r()` at line ~818: -```python -def _r(name, ptype, tags=None, desc=None, hint=None, math=None): -``` -Replace with: -```python -def _r(name, ptype, tags=None, desc=None, hint=None, math=None, str_len=None): - ... - REGISTRY.register( - ParamDef( - ... - str_len=str_len if str_len is not None else "name_len", - ) - ) -``` - -- [ ] **Step 5: Run the tests** - -```bash -python3 -m pytest toolchain/tests/params/test_schema.py -v -``` -Expected: PASS - -- [ ] **Step 6: Commit** - -```bash -git add toolchain/mfc/params/schema.py toolchain/mfc/params/definitions.py -git commit -m "feat(params): add str_len to ParamDef for STR character length" -``` - ---- - -## Task 2: Create `namelist_targets.py` - -**Files:** -- Create: `toolchain/mfc/params/namelist_targets.py` - -This file is the authoritative source for which Fortran namelist variable belongs to which target(s). It is derived by reading the existing three namelist blocks; after this plan, those blocks will be generated from it. - -- [ ] **Step 1: Write a failing test** - -Create `toolchain/tests/params/test_namelist_targets.py`: -```python -def test_common_vars_in_all_targets(): - from mfc.params.namelist_targets import NAMELIST_VARS - for var in ["m", "n", "p", "bc_x", "bc_y", "bc_z", "model_eqns", "cyl_coord", "fluid_pp"]: - assert {"pre", "sim", "post"}.issubset(NAMELIST_VARS.get(var, set())), \ - f"{var!r} not marked for all targets" - -def test_sim_only_vars(): - from mfc.params.namelist_targets import NAMELIST_VARS - for var in ["run_time_info", "dt", "riemann_solver", "acoustic", "probe"]: - targets = NAMELIST_VARS.get(var, set()) - assert "sim" in targets, f"{var!r} not marked for sim" - assert "pre" not in targets, f"{var!r} incorrectly marked for pre" - assert "post" not in targets, f"{var!r} incorrectly marked for post" - -def test_pre_only_vars(): - from mfc.params.namelist_targets import NAMELIST_VARS - for var in ["old_grid", "old_ic", "patch_icpp", "simplex_params"]: - targets = NAMELIST_VARS.get(var, set()) - assert "pre" in targets, f"{var!r} not marked for pre" - assert "sim" not in targets, f"{var!r} incorrectly in sim" - -def test_post_only_vars(): - from mfc.params.namelist_targets import NAMELIST_VARS - for var in ["format", "sim_data", "lag_header", "output_partial_domain"]: - targets = NAMELIST_VARS.get(var, set()) - assert "post" in targets, f"{var!r} not marked for post" - assert "sim" not in targets, f"{var!r} incorrectly in sim" - -def test_case_opt_exclude_vars(): - from mfc.params.namelist_targets import CASE_OPT_EXCLUDE - for var in ["nb", "mapped_weno", "wenoz", "weno_order", "num_fluids"]: - assert var in CASE_OPT_EXCLUDE -``` - -- [ ] **Step 2: Run to verify failure** - -```bash -python3 -m pytest toolchain/tests/params/test_namelist_targets.py -v -``` -Expected: `ModuleNotFoundError` - -- [ ] **Step 3: Create `namelist_targets.py`** - -Create `toolchain/mfc/params/namelist_targets.py` with the complete content: - -```python -""" -Namelist target mapping for Fortran codegen. - -NAMELIST_VARS maps each Fortran namelist variable (struct root or simple scalar) -to the set of MFC executables whose namelist it appears in. - -CASE_OPT_EXCLUDE is the set of simulation namelist variables excluded under -MFC_CASE_OPTIMIZATION (they become compile-time constants instead). - -When adding a new parameter: - 1. Add to definitions.py (type, constraints, etc.) - 2. Add the namelist root variable to NAMELIST_VARS with its target set - 3. Run ./mfc.sh generate to regenerate the .fpp files -""" - -from typing import Dict, Set - -# All three targets -_ALL = {"pre", "sim", "post"} -_PRE_SIM = {"pre", "sim"} -_SIM_POST = {"sim", "post"} - -NAMELIST_VARS: Dict[str, Set[str]] = { - # --- Grid (all targets) --- - "m": _ALL, - "n": _ALL, - "p": _ALL, - "cyl_coord": _ALL, - "x_domain": {"pre", "sim"}, - "y_domain": {"pre", "sim"}, - "z_domain": {"pre", "sim"}, - "x_output": {"post"}, - "y_output": {"post"}, - "z_output": {"post"}, - # --- Grid stretching (pre + sim) --- - "stretch_x": {"pre"}, - "stretch_y": {"pre"}, - "stretch_z": {"pre"}, - "a_x": {"pre"}, - "a_y": {"pre"}, - "a_z": {"pre"}, - "x_a": {"pre", "sim"}, - "y_a": {"pre", "sim"}, - "z_a": {"pre", "sim"}, - "x_b": {"pre", "sim"}, - "y_b": {"pre", "sim"}, - "z_b": {"pre", "sim"}, - "loops_x": {"pre"}, - "loops_y": {"pre"}, - "loops_z": {"pre"}, - # --- Time (sim) --- - "dt": {"sim"}, - "t_step_start": _ALL, - "t_step_stop": {"sim", "post"}, - "t_step_save": {"sim", "post"}, - "t_step_print": {"sim"}, - "t_step_old": {"pre", "sim"}, - "time_stepper": {"sim"}, - "t_stop": {"sim", "post"}, - "t_save": {"sim", "post"}, - "cfl_target": {"sim", "post"}, - "cfl_adap_dt": _ALL, - "cfl_const_dt": _ALL, - "n_start": _ALL, - "n_start_old": {"pre"}, - "adap_dt": {"sim"}, - "adap_dt_tol": {"sim"}, - "adap_dt_max_iters": {"sim"}, - # --- Physics model (all targets) --- - "model_eqns": _ALL, - "num_fluids": {"pre", "post"}, - "mpp_lim": _ALL, - "relax": _ALL, - "relax_model": _ALL, - "palpha_eps": _ALL, - "ptgalpha_eps": _ALL, - # --- WENO / reconstruction (pre has weno_order; sim has it under CASE_OPT) --- - "weno_order": {"pre", "post"}, - "weno_eps": {"sim"}, - "teno_CT": {"sim"}, - "wenoz_q": {"sim"}, - "mp_weno": {"sim"}, - "weno_avg": {"sim"}, - "weno_Re_flux": {"sim"}, - "null_weights": {"sim"}, - "muscl_eps": {"sim"}, - "recon_type": {"pre", "post"}, - "muscl_order": {"pre", "post"}, - "muscl_lim": {"post"}, - "int_comp": {"sim"}, - "ic_eps": {"sim"}, - "ic_beta": {"sim"}, - # --- Riemann solver (sim only) --- - "riemann_solver": {"sim"}, - "wave_speeds": {"sim"}, - "avg_state": {"sim", "post"}, - "low_Mach": {"sim"}, - # --- MHD (all targets) --- - "mhd": {"pre", "post"}, - "hyper_cleaning": _ALL, - "hyper_cleaning_speed": {"sim"}, - "hyper_cleaning_tau": {"sim"}, - "Bx0": _ALL, - # --- BCs (all targets) --- - "bc_x": _ALL, - "bc_y": _ALL, - "bc_z": _ALL, - "num_bc_patches": _ALL, - "patch_bc": {"pre"}, - # --- ICs (pre only) --- - "num_patches": {"pre"}, - "patch_icpp": {"pre"}, - # --- Fluid properties (all) --- - "fluid_pp": _ALL, - "bub_pp": _ALL, - "rhoref": _ALL, - "pref": _ALL, - # --- Bubbles (all) --- - "bubbles_euler": _ALL, - "bubbles_lagrange": _ALL, - "R0ref": _ALL, - "nb": {"pre", "post"}, - "polytropic": _ALL, - "thermal": _ALL, - "Ca": _ALL, - "Web": _ALL, - "Re_inv": _ALL, - "polydisperse": _ALL, - "poly_sigma": _ALL, - "qbmm": _ALL, - "sigma": _ALL, - "adv_n": _ALL, - "bubble_model": {"sim"}, - "sigR": {"pre", "post"}, - "sigV": {"pre"}, - "dist_type": {"pre"}, - "rhoRV": {"pre"}, - "lag_params": {"sim"}, - # --- Lagrangian output (post) --- - "lag_header": {"post"}, - "lag_txt_wrt": {"post"}, - "lag_db_wrt": {"post"}, - "lag_id_wrt": {"post"}, - "lag_pos_wrt": {"post"}, - "lag_pos_prev_wrt": {"post"}, - "lag_vel_wrt": {"post"}, - "lag_rad_wrt": {"post"}, - "lag_rvel_wrt": {"post"}, - "lag_r0_wrt": {"post"}, - "lag_rmax_wrt": {"post"}, - "lag_rmin_wrt": {"post"}, - "lag_dphidt_wrt": {"post"}, - "lag_pres_wrt": {"post"}, - "lag_mv_wrt": {"post"}, - "lag_mg_wrt": {"post"}, - "lag_betaT_wrt": {"post"}, - "lag_betaC_wrt": {"post"}, - # --- Elasticity (all) --- - "hypoelasticity": _ALL, - "hyperelasticity": _ALL, - # --- Surface tension (all) --- - "surface_tension": _ALL, - # --- Relativity (all) --- - "relativity": _ALL, - # --- Immersed boundaries (all) --- - "ib": _ALL, - "num_ibs": _ALL, - "patch_ib": {"pre", "sim"}, - "collision_model": {"sim"}, - "coefficient_of_restitution": {"sim"}, - "collision_time": {"sim"}, - "ib_coefficient_of_friction": {"sim"}, - "ib_state_wrt": {"sim", "post"}, - # --- Continuum damage (all) --- - "cont_damage": _ALL, - "tau_star": {"sim"}, - "cont_damage_s": {"sim"}, - "alpha_bar": {"sim"}, - # --- IGR (all) --- - "igr": {"pre", "post"}, - "igr_order": {"pre", "post"}, - "down_sample": _ALL, - # --- Probes (sim) --- - "probe_wrt": {"sim"}, - "num_probes": {"sim"}, - "probe": {"sim"}, - "integral_wrt": {"sim"}, - "num_integrals": {"sim"}, - "integral": {"sim"}, - "fd_order": {"sim", "post"}, - # --- Acoustic sources (sim) --- - "acoustic_source": {"sim"}, - "num_source": {"sim"}, - "acoustic": {"sim"}, - # --- Chemistry (sim) --- - "chem_params": {"sim"}, - # --- Body forces (sim) --- - "bf_x": {"sim"}, - "bf_y": {"sim"}, - "bf_z": {"sim"}, - "k_x": {"sim"}, - "k_y": {"sim"}, - "k_z": {"sim"}, - "w_x": {"sim"}, - "w_y": {"sim"}, - "w_z": {"sim"}, - "p_x": {"sim"}, - "p_y": {"sim"}, - "p_z": {"sim"}, - "g_x": {"sim"}, - "g_y": {"sim"}, - "g_z": {"sim"}, - # --- Viscous (pre) --- - "viscous": {"pre"}, - # --- Output (all) --- - "precision": _ALL, - "parallel_io": _ALL, - "file_per_process": _ALL, - "prim_vars_wrt": {"sim", "post"}, - "cons_vars_wrt": {"post"}, - "run_time_info": {"sim"}, - "fft_wrt": _ALL, - "pi_fac": {"pre", "sim"}, - # --- Post-process output --- - "format": {"post"}, - "output_partial_domain": {"post"}, - "rho_wrt": {"post"}, - "E_wrt": {"post"}, - "pres_wrt": {"post"}, - "c_wrt": {"post"}, - "omega_wrt": {"post"}, - "qm_wrt": {"post"}, - "liutex_wrt": {"post"}, - "schlieren_wrt": {"post"}, - "schlieren_alpha": {"post"}, - "gamma_wrt": {"post"}, - "heat_ratio_wrt": {"post"}, - "pi_inf_wrt": {"post"}, - "pres_inf_wrt": {"post"}, - "alpha_rho_wrt": {"post"}, - "mom_wrt": {"post"}, - "vel_wrt": {"post"}, - "flux_wrt": {"post"}, - "alpha_wrt": {"post"}, - "cf_wrt": {"post"}, - "chem_wrt_T": {"post"}, - "chem_wrt_Y": {"post"}, - "alt_soundspeed": {"sim", "post"}, - "mixture_err": {"sim", "post"}, - "flux_lim": {"post"}, - "sim_data": {"post"}, - "alpha_rho_e_wrt": {"post"}, - "G": {"post"}, - # --- Pre-process IC perturbations --- - "perturb_flow": {"pre"}, - "perturb_flow_fluid": {"pre"}, - "perturb_flow_mag": {"pre"}, - "perturb_sph": {"pre"}, - "perturb_sph_fluid": {"pre"}, - "fluid_rho": {"pre"}, - "mixlayer_vel_profile": {"pre"}, - "mixlayer_vel_coef": {"pre"}, - "mixlayer_perturb": {"pre"}, - "mixlayer_perturb_nk": {"pre"}, - "mixlayer_perturb_k0": {"pre"}, - "pre_stress": {"pre"}, - "elliptic_smoothing": {"pre"}, - "elliptic_smoothing_iters": {"pre"}, - "simplex_perturb": {"pre"}, - "simplex_params": {"pre"}, - # --- Pre-process restart --- - "old_grid": {"pre"}, - "old_ic": {"pre"}, - # --- Sim-specific physics --- - "rdma_mpi": {"sim"}, - "alf_factor": {"sim"}, - "num_igr_iters": {"sim"}, - "num_igr_warm_start_iters": {"sim"}, - "igr_iter_solver": {"sim"}, - "igr_pres_lim": {"sim"}, - "nv_uvm_out_of_core": {"sim"}, - "nv_uvm_igr_temps_on_gpu": {"sim"}, - "nv_uvm_pref_gpu": {"sim"}, - # --- Logistics --- - "case_dir": _ALL, -} - -# Variables excluded from the sim namelist when MFC_CASE_OPTIMIZATION is active -# (they become compile-time integer/logical parameters instead). -# Must all be present in NAMELIST_VARS with "sim" in their target set. -CASE_OPT_EXCLUDE: Set[str] = { - "nb", - "mapped_weno", - "wenoz", - "teno", - "wenoz_q", - "weno_order", - "num_fluids", - "mhd", - "relativity", - "igr_order", - "viscous", - "igr_iter_solver", - "igr", - "igr_pres_lim", - "recon_type", - "muscl_order", - "muscl_lim", -} - -# Add CASE_OPT_EXCLUDE vars to NAMELIST_VARS for sim target -# (they appear in the namelist when NOT using case optimization) -for _v in CASE_OPT_EXCLUDE: - if _v not in NAMELIST_VARS: - NAMELIST_VARS[_v] = {"sim"} - else: - NAMELIST_VARS[_v].add("sim") -``` - -- [ ] **Step 4: Run tests** - -```bash -python3 -m pytest toolchain/tests/params/test_namelist_targets.py -v -``` -Expected: all PASS - -- [ ] **Step 5: Commit** - -```bash -git add toolchain/mfc/params/namelist_targets.py toolchain/tests/params/test_namelist_targets.py -git commit -m "feat(params): add namelist_targets.py with target mapping for all namelist vars" -``` - ---- - -## Task 3: Write `fortran_gen.py` - -**Files:** -- Create: `toolchain/mfc/params/generators/fortran_gen.py` -- Test: `toolchain/tests/params/test_fortran_gen.py` - -The generator reads `definitions.py` (via `REGISTRY`) + `namelist_targets.py` and produces: -- **Namelist fragment**: `namelist /user_inputs/ var1, var2, ...` with Fypp case-opt guard for sim -- **Decls fragment**: `integer :: foo` / `real(wp) :: bar` for each simple scalar in that target - -- [ ] **Step 1: Write failing tests** - -Create `toolchain/tests/params/test_fortran_gen.py`: - -```python -import pytest - - -def test_get_namelist_var_simple(): - from mfc.params.generators.fortran_gen import get_namelist_var - assert get_namelist_var("m") == "m" - assert get_namelist_var("dt") == "dt" - assert get_namelist_var("cyl_coord") == "cyl_coord" - - -def test_get_namelist_var_indexed_family(): - from mfc.params.generators.fortran_gen import get_namelist_var - assert get_namelist_var("fluid_pp(1)%gamma") == "fluid_pp" - assert get_namelist_var("patch_icpp(3)%geometry") == "patch_icpp" - assert get_namelist_var("patch_ib(100)%radius") == "patch_ib" - - -def test_get_namelist_var_struct_member(): - from mfc.params.generators.fortran_gen import get_namelist_var - assert get_namelist_var("bc_x%beg") == "bc_x" - assert get_namelist_var("bc_y%grcbc_in") == "bc_y" - assert get_namelist_var("lag_params%solver_approach") == "lag_params" - assert get_namelist_var("chem_params%diffusion") == "chem_params" - assert get_namelist_var("bub_pp%R0ref") == "bub_pp" - - -def test_fortran_type_for_int(): - from mfc.params.generators.fortran_gen import fortran_type_decl - from mfc.params.schema import ParamDef, ParamType - p = ParamDef(name="model_eqns", param_type=ParamType.INT) - assert fortran_type_decl(p) == "integer" - - -def test_fortran_type_for_real(): - from mfc.params.generators.fortran_gen import fortran_type_decl - from mfc.params.schema import ParamDef, ParamType - p = ParamDef(name="dt", param_type=ParamType.REAL) - assert fortran_type_decl(p) == "real(wp)" - - -def test_fortran_type_for_log(): - from mfc.params.generators.fortran_gen import fortran_type_decl - from mfc.params.schema import ParamDef, ParamType - p = ParamDef(name="cyl_coord", param_type=ParamType.LOG) - assert fortran_type_decl(p) == "logical" - - -def test_fortran_type_for_str(): - from mfc.params.generators.fortran_gen import fortran_type_decl - from mfc.params.schema import ParamDef, ParamType - p = ParamDef(name="case_dir", param_type=ParamType.STR, str_len="path_len") - assert fortran_type_decl(p) == "character(LEN=path_len)" - - -def test_generate_namelist_contains_common_vars(): - from mfc.params.generators.fortran_gen import generate_namelist_fpp - for target in ("pre", "sim", "post"): - content = generate_namelist_fpp(target) - for var in ("m", "n", "p", "bc_x", "bc_y", "bc_z", "fluid_pp", "case_dir"): - assert var in content, f"{var!r} missing from {target} namelist" - - -def test_sim_namelist_has_case_opt_guard(): - from mfc.params.generators.fortran_gen import generate_namelist_fpp - content = generate_namelist_fpp("sim") - assert "#:if not MFC_CASE_OPTIMIZATION" in content - assert "weno_order" in content - assert "num_fluids" in content - - -def test_pre_namelist_has_patch_icpp(): - from mfc.params.generators.fortran_gen import generate_namelist_fpp - content = generate_namelist_fpp("pre") - assert "patch_icpp" in content - assert "run_time_info" not in content - - -def test_post_namelist_has_output_vars(): - from mfc.params.generators.fortran_gen import generate_namelist_fpp - content = generate_namelist_fpp("post") - assert "sim_data" in content - assert "lag_header" in content - assert "patch_icpp" not in content - - -def test_generate_decls_contains_simple_scalars(): - from mfc.params.generators.fortran_gen import generate_decls_fpp - for target in ("pre", "sim", "post"): - content = generate_decls_fpp(target) - assert "integer" in content - assert "real(wp)" in content - assert "logical" in content - - -def test_generate_decls_has_dt_for_sim(): - from mfc.params.generators.fortran_gen import generate_decls_fpp - content = generate_decls_fpp("sim") - assert "real(wp) :: dt" in content - - -def test_generate_decls_no_percent_vars(): - from mfc.params.generators.fortran_gen import generate_decls_fpp - for target in ("pre", "sim", "post"): - content = generate_decls_fpp(target) - # Derived type members are NOT declared — only their struct root - assert "bc_x%beg" not in content - assert "fluid_pp(1)" not in content - - -def test_generate_decls_has_case_dir(): - from mfc.params.generators.fortran_gen import generate_decls_fpp - for target in ("pre", "sim", "post"): - content = generate_decls_fpp(target) - assert "character(LEN=path_len) :: case_dir" in content -``` - -- [ ] **Step 2: Run to verify failure** - -```bash -python3 -m pytest toolchain/tests/params/test_fortran_gen.py -v -``` -Expected: `ModuleNotFoundError` - -- [ ] **Step 3: Implement `fortran_gen.py`** - -Create `toolchain/mfc/params/generators/fortran_gen.py`: - -```python -""" -Fortran parameter code generator. - -Generates namelist fragments and simple scalar declaration fragments from -definitions.py + namelist_targets.py. Output goes to src/common/include/. - -Usage: called from generate.py. Not invoked directly. -""" - -import re -from typing import Optional - -from ..namelist_targets import CASE_OPT_EXCLUDE, NAMELIST_VARS -from ..registry import REGISTRY -from ..schema import ParamDef, ParamType - -import mfc.params.definitions # noqa: F401 — triggers registry population - - -_HEADER = """\ -! AUTO-GENERATED — do not edit directly. -! Source: toolchain/mfc/params/definitions.py + namelist_targets.py -! Regenerate: ./mfc.sh generate -! -""" - -_FORTRAN_INDENT = " " - - -def get_namelist_var(param_name: str) -> str: - """Return the Fortran namelist variable root for a parameter name. - - fluid_pp(1)%gamma -> fluid_pp - bc_x%beg -> bc_x - m -> m - """ - # Indexed family: fluid_pp(1)%gamma -> fluid_pp - m = re.match(r"^([a-zA-Z_]\w*)\(", param_name) - if m and "%" in param_name: - return m.group(1) - # Struct member: bc_x%beg -> bc_x - if "%" in param_name: - return param_name.split("%")[0] - # Simple scalar - return param_name - - -def fortran_type_decl(param: ParamDef) -> str: - """Return the Fortran type keyword for a parameter.""" - mapping = { - ParamType.INT: "integer", - ParamType.REAL: "real(wp)", - ParamType.LOG: "logical", - ParamType.ANALYTIC_INT: "integer", - ParamType.ANALYTIC_REAL: "real(wp)", - } - if param.param_type == ParamType.STR: - return f"character(LEN={param.str_len})" - return mapping[param.param_type] - - -def _is_simple_scalar(param_name: str) -> bool: - """Return True if this param is a simple scalar (no % or () in name).""" - return "%" not in param_name and "(" not in param_name - - -def _namelist_vars_for_target(target: str) -> list: - """Return ordered list of namelist variables for a target.""" - result = [] - seen = set() - for var, targets in NAMELIST_VARS.items(): - if target in targets and var not in seen: - result.append(var) - seen.add(var) - return result - - -def generate_namelist_fpp(target: str) -> str: - """Generate the namelist /user_inputs/ fragment for a target. - - For sim, wraps CASE_OPT_EXCLUDE vars in a Fypp guard. - """ - assert target in ("pre", "sim", "post"), f"Unknown target: {target!r}" - - all_vars = _namelist_vars_for_target(target) - - if target == "sim": - normal_vars = [v for v in all_vars if v not in CASE_OPT_EXCLUDE] - opt_vars = [v for v in all_vars if v in CASE_OPT_EXCLUDE] - else: - normal_vars = all_vars - opt_vars = [] - - lines = [_HEADER] - - def _fmt_varlist(varlist: list, continuation: bool = False) -> list: - """Format a list of variable names as Fortran continuation lines.""" - out = [] - chunk = [] - for v in varlist: - chunk.append(v) - if len(", ".join(chunk)) > 70: - prefix = f"{_FORTRAN_INDENT}& " if continuation else f"{_FORTRAN_INDENT}" - out.append(prefix + ", ".join(chunk[:-1]) + ", &") - chunk = [chunk[-1]] - continuation = True - if chunk: - prefix = f"{_FORTRAN_INDENT}& " if continuation else f"{_FORTRAN_INDENT}" - out.append(prefix + ", ".join(chunk)) - return out - - if not opt_vars: - var_lines = _fmt_varlist(normal_vars) - lines.append(f"namelist /user_inputs/ {var_lines[0].lstrip()}") - if len(var_lines) > 1: - lines[-1] += ", &" - lines.extend(v + (", &" if i < len(var_lines) - 2 else "") for i, v in enumerate(var_lines[1:])) - else: - # Sim: normal vars first, then CASE_OPT guard, then trailing vars - # Emit normal vars up to end, then the guard block - var_lines = _fmt_varlist(normal_vars) - lines.append(f"namelist /user_inputs/ {var_lines[0].lstrip()}, &") - for v in var_lines[1:]: - lines.append(v + ", &") - - lines.append(f"#:if not MFC_CASE_OPTIMIZATION") - opt_lines = _fmt_varlist(opt_vars, continuation=True) - for i, v in enumerate(opt_lines): - lines.append(v + (", &" if i < len(opt_lines) - 1 else ", &")) - lines.append(f"#:endif") - # Need a placeholder last entry — use a trailing comment-safe idiom - # Actually Fortran namelist doesn't need a sentinel, just remove trailing comma - # Fix: the last normal line had a trailing ", &" — correct the last line - # Remove trailing ", &" from the last real entry - lines[-1] = lines[-1].rstrip(", &") - - return "\n".join(lines) + "\n" - - -def generate_decls_fpp(target: str) -> str: - """Generate simple scalar Fortran declarations for a target. - - Only generates declarations for params that are: - - Simple scalars (no % or () in name) - - In NAMELIST_VARS for this target - - Registered in definitions.py - """ - assert target in ("pre", "sim", "post"), f"Unknown target: {target!r}" - - vars_for_target = set(_namelist_vars_for_target(target)) - lines = [_HEADER] - - all_params = REGISTRY.all_params - for name in sorted(vars_for_target): - if not _is_simple_scalar(name): - continue - param = all_params.get(name) - if param is None: - continue - ftype = fortran_type_decl(param) - lines.append(f"{ftype} :: {name}") - - return "\n".join(lines) + "\n" -``` - -- [ ] **Step 4: Run tests** - -```bash -python3 -m pytest toolchain/tests/params/test_fortran_gen.py -v -``` -Expected: all PASS (fix any off-by-one in namelist formatting if needed) - -- [ ] **Step 5: Commit** - -```bash -git add toolchain/mfc/params/generators/fortran_gen.py toolchain/tests/params/test_fortran_gen.py -git commit -m "feat(params): add fortran_gen.py to generate namelist and declaration .fpp files" -``` - ---- - -## Task 4: Wire into `generate.py` - -**Files:** -- Modify: `toolchain/mfc/generate.py` - -The generator produces six files in `src/common/include/`. They are checked in and verified by `generate --check`. - -- [ ] **Step 1: Write a failing test** - -In `toolchain/tests/params/test_fortran_gen.py`, add: - -```python -def test_generate_function_returns_six_paths(): - from mfc.params.generators.fortran_gen import get_generated_files - from pathlib import Path - files = get_generated_files() - assert len(files) == 6 - names = {p.name for p, _ in files} - assert "generated_namelist_pre.fpp" in names - assert "generated_decls_sim.fpp" in names -``` - -- [ ] **Step 2: Add `get_generated_files()` to `fortran_gen.py`** - -At the bottom of `fortran_gen.py`, add: - -```python -from pathlib import Path -from ..common import MFC_ROOT_DIR # already used elsewhere in the toolchain - - -def get_generated_files() -> list: - """Return list of (output_path, content) tuples for all six generated files.""" - include_dir = Path(MFC_ROOT_DIR) / "src" / "common" / "include" - return [ - (include_dir / "generated_namelist_pre.fpp", generate_namelist_fpp("pre")), - (include_dir / "generated_namelist_sim.fpp", generate_namelist_fpp("sim")), - (include_dir / "generated_namelist_post.fpp", generate_namelist_fpp("post")), - (include_dir / "generated_decls_pre.fpp", generate_decls_fpp("pre")), - (include_dir / "generated_decls_sim.fpp", generate_decls_fpp("sim")), - (include_dir / "generated_decls_post.fpp", generate_decls_fpp("post")), - ] -``` - -Note: `MFC_ROOT_DIR` is defined in `toolchain/mfc/common.py`. Check the import path before using. - -- [ ] **Step 3: Register in `generate.py`** - -In `toolchain/mfc/generate.py`, in the `generate()` function, add after the existing `files = [...]` list: - -```python - from .params.generators.fortran_gen import get_generated_files - files += get_generated_files() -``` - -- [ ] **Step 4: Run `./mfc.sh generate` to produce the six files** - -```bash -./mfc.sh generate -``` -Expected: six new/updated `.fpp` files in `src/common/include/` - -Inspect the output: -```bash -cat src/common/include/generated_namelist_sim.fpp | head -30 -cat src/common/include/generated_decls_pre.fpp | head -20 -``` - -- [ ] **Step 5: Verify content matches the existing namelists** - -Manually cross-check that every variable in the current `m_start_up.fpp` namelist blocks appears in the corresponding generated file. Key checks: -- `generated_namelist_sim.fpp` should contain `weno_order` inside the `#:if not MFC_CASE_OPTIMIZATION` guard -- `generated_namelist_pre.fpp` should contain `patch_icpp`, `simplex_params`, `old_grid` -- `generated_namelist_post.fpp` should contain `sim_data`, `lag_header`, `format` - -- [ ] **Step 6: Run tests** - -```bash -python3 -m pytest toolchain/tests/params/test_fortran_gen.py -v -``` -Expected: all PASS - -- [ ] **Step 7: Commit the generated files and generate.py change** - -```bash -git add src/common/include/generated_namelist_*.fpp src/common/include/generated_decls_*.fpp -git add toolchain/mfc/generate.py toolchain/mfc/params/generators/fortran_gen.py -git commit -m "feat(params): generate Fortran namelist and declaration .fpp files from definitions.py" -``` - ---- - -## Task 5: Replace namelists in `m_start_up.fpp` (all three targets) - -**Files:** -- Modify: `src/pre_process/m_start_up.fpp:77-87` -- Modify: `src/simulation/m_start_up.fpp:85-115` -- Modify: `src/post_process/m_start_up.fpp:62-73` - -This is the first Fortran change. Do one target at a time and build between each to catch errors early. - -### Pre-process - -- [ ] **Step 1: In `src/pre_process/m_start_up.fpp`, replace lines 77–87** - -Current (lines 77–87): -```fortran - namelist /user_inputs/ case_dir, old_grid, old_ic, t_step_old, t_step_start, m, n, p, x_domain, y_domain, z_domain, & - & stretch_x, stretch_y, stretch_z, a_x, a_y, a_z, x_a, y_a, z_a, x_b, y_b, z_b, model_eqns, num_fluids, mpp_lim, & - ... - & simplex_perturb, simplex_params, fft_wrt -``` - -Replace with: -```fortran - #:include 'generated_namelist_pre.fpp' -``` - -- [ ] **Step 2: Build pre_process only** - -```bash -./mfc.sh build -t pre_process -j 8 -``` -Expected: successful compilation. If there are "Undefined variable" errors, a namelist variable is missing from `generated_namelist_pre.fpp` — add it to `NAMELIST_VARS` in `namelist_targets.py` with target `"pre"`, then re-run `./mfc.sh generate`. - -### Simulation - -- [ ] **Step 3: In `src/simulation/m_start_up.fpp`, replace lines 85–115** - -Current (lines 85–115): -```fortran - namelist /user_inputs/ case_dir, run_time_info, m, n, p, dt, & - t_step_start, t_step_stop, t_step_save, t_step_print, & - ... - & int_comp, ic_eps, ic_beta, nv_uvm_out_of_core, nv_uvm_igr_temps_on_gpu, nv_uvm_pref_gpu, down_sample, fft_wrt -``` - -Replace with: -```fortran - #:include 'generated_namelist_sim.fpp' -``` - -- [ ] **Step 4: Build simulation** - -```bash -./mfc.sh build -t simulation -j 8 -``` -Expected: successful. Errors → check NAMELIST_VARS for the missing variable. - -### Post-process - -- [ ] **Step 5: In `src/post_process/m_start_up.fpp`, replace lines 62–73** - -Current (lines 62–73): -```fortran - namelist /user_inputs/ case_dir, m, n, p, t_step_start, t_step_stop, t_step_save, model_eqns, num_fluids, mpp_lim, & - ... - & alpha_rho_e_wrt, ib_state_wrt -``` - -Replace with: -```fortran - #:include 'generated_namelist_post.fpp' -``` - -- [ ] **Step 6: Build post_process** - -```bash -./mfc.sh build -t post_process -j 8 -``` -Expected: successful. - -- [ ] **Step 7: Build all three targets together** - -```bash -./mfc.sh build -j 8 -``` -Expected: all three compile clean. - -- [ ] **Step 8: Run a subset of tests to verify runtime behavior** - -```bash -./mfc.sh test --only 1D -j 8 -``` -Expected: all pass. - -- [ ] **Step 9: Commit** - -```bash -git add src/pre_process/m_start_up.fpp src/simulation/m_start_up.fpp src/post_process/m_start_up.fpp -git commit -m "refactor(fortran): replace hand-written namelists with generated includes" -``` - ---- - -## Task 6: Replace declarations in `m_global_parameters.fpp` - -**Files:** -- Modify: `src/simulation/m_global_parameters.fpp` -- Modify: `src/pre_process/m_global_parameters.fpp` -- Modify: `src/post_process/m_global_parameters.fpp` - -### Strategy - -For each target: -1. Add `#:include 'generated_decls_{target}.fpp'` right after `implicit none` -2. Remove every declaration line for a variable that will be generated -3. Build and fix errors (duplicate declaration → remove from file; missing declaration → add to generator) - -**Which declarations get removed?** Any `integer :: foo`, `real(wp) :: bar`, `logical :: baz`, `character(...) :: qux` line where `foo`/`bar`/`baz`/`qux` is a simple scalar in `NAMELIST_VARS` for this target. - -**Which stay?** Everything else: derived types, allocatables, GPU_DECLARE macros, internal variables not in definitions.py, case optimization conditionals. - -### Simulation (`m_global_parameters.fpp`) - -- [ ] **Step 1: Add include after `implicit none` (line ~19)** - -After line `implicit none` in `src/simulation/m_global_parameters.fpp`, add: -```fortran - #:include 'generated_decls_sim.fpp' -``` - -- [ ] **Step 2: Build — expect duplicate declaration errors** - -```bash -./mfc.sh build -t simulation -j 8 2>&1 | grep -i "duplicate\|redeclared\|already" -``` - -- [ ] **Step 3: For each duplicate, remove the declaration from the file** - -Systematically remove every line of the form `integer :: foo`, `real(wp) :: bar`, `logical :: baz` where `foo`/`bar`/`baz` is in `generated_decls_sim.fpp`. Leave compound lines (e.g., `integer :: m, n, p`) if only some of those vars are generated — split them if needed. - -Also remove the Fypp loop at lines ~183-187 that generates `k_x, w_x, ...` etc., since those are now in `generated_decls_sim.fpp`. - -- [ ] **Step 4: Build simulation cleanly** - -```bash -./mfc.sh build -t simulation -j 8 -``` -Expected: no errors. - -- [ ] **Step 5: Run simulation tests** - -```bash -./mfc.sh test --only 1D -j 8 -``` -Expected: pass. - -### Pre-process (`m_global_parameters.fpp`) - -- [ ] **Step 6: Add include after `implicit none` in pre_process file** - -```fortran - #:include 'generated_decls_pre.fpp' -``` - -- [ ] **Step 7: Build and remove duplicates** - -```bash -./mfc.sh build -t pre_process -j 8 2>&1 | grep -i "duplicate\|redeclared" -``` -Remove listed duplicates from the file. - -- [ ] **Step 8: Build cleanly** - -```bash -./mfc.sh build -t pre_process -j 8 -``` - -### Post-process (`m_global_parameters.fpp`) - -- [ ] **Step 9: Add include after `implicit none` in post_process file** - -```fortran - #:include 'generated_decls_post.fpp' -``` - -- [ ] **Step 10: Build and remove duplicates** - -```bash -./mfc.sh build -t post_process -j 8 2>&1 | grep -i "duplicate\|redeclared" -``` -Remove listed duplicates from the file. - -- [ ] **Step 11: Build all three** - -```bash -./mfc.sh build -j 8 -``` -Expected: clean. - -- [ ] **Step 12: Run full 1D + 2D tests** - -```bash -./mfc.sh test --only 1D 2D -j 8 -``` -Expected: pass. - -- [ ] **Step 13: Commit** - -```bash -git add src/simulation/m_global_parameters.fpp src/pre_process/m_global_parameters.fpp src/post_process/m_global_parameters.fpp -git commit -m "refactor(fortran): replace hand-written scalar decls with generated includes" -``` - ---- - -## Task 7: Add precheck step - -**Files:** -- Modify: `toolchain/bootstrap/precheck.sh` - -- [ ] **Step 1: Add check 7/7 to `precheck.sh`** - -In `toolchain/bootstrap/precheck.sh`, find the block for check 6/6 (parameter docs). After the block that waits for `PID_PARAM_DOCS`, add a new parallel job for the generate check: - -```bash -# Generated Fortran files up-to-date check -( - if ./mfc.sh generate --check > /dev/null 2>&1; then - echo "0" > "$TMPDIR_PC/generate_exit" - else - echo "1" > "$TMPDIR_PC/generate_exit" - fi -) & -PID_GENERATE=$! -``` - -Then update the results-collection section. Change `6/6` references to `7/7` for the last check, and change the existing `6/6` block: - -Before the existing `6/6` log line: -```bash -log "[$CYAN 6/6$COLOR_RESET] Checking$MAGENTA parameter docs$COLOR_RESET..." -``` -Change to: -```bash -log "[$CYAN 6/7$COLOR_RESET] Checking$MAGENTA parameter docs$COLOR_RESET..." -``` - -Then add after the parameter docs block: - -```bash -wait $PID_GENERATE -log "[$CYAN 7/7$COLOR_RESET] Checking$MAGENTA generated Fortran files$COLOR_RESET..." -GENERATE_RC=$(cat "$TMPDIR_PC/generate_exit" 2>/dev/null || echo "1") -if [ "$GENERATE_RC" = "0" ]; then - ok "Generated Fortran files are up to date." -else - error "Generated Fortran files are out of date. Run$MAGENTA ./mfc.sh generate$COLOR_RESET to update." - FAILED=1 -fi -``` - -Also update the first log line from `(same checks as CI lint-gate)` wording and `1/6` references to `1/7`. - -- [ ] **Step 2: Test the new check** - -```bash -./mfc.sh precheck -j 8 -``` -Expected: `[7/7] Checking generated Fortran files... OK` - -- [ ] **Step 3: Verify it catches a stale file** - -Edit one line in `src/common/include/generated_namelist_sim.fpp` (add a comment), then run: -```bash -./mfc.sh precheck -j 8 -``` -Expected: `[7/7] Checking generated Fortran files... FAIL`. Revert the test change. - -- [ ] **Step 4: Commit** - -```bash -git add toolchain/bootstrap/precheck.sh -git commit -m "feat(precheck): add generated Fortran files up-to-date check (7/7)" -``` - ---- - -## Task 8: Final verification — full test suite - -- [ ] **Step 1: Run precheck** - -```bash -./mfc.sh precheck -j 8 -``` -Expected: all 7/7 checks pass. - -- [ ] **Step 2: Build with case optimization to verify CASE_OPT guard works** - -```bash -./mfc.sh build -t simulation --case-optimization -i examples/3d_taylor_green_vortex/case.py -j 8 -``` -Expected: successful compilation. The `#:if not MFC_CASE_OPTIMIZATION` guard in `generated_namelist_sim.fpp` must exclude the case-opt vars from the namelist at compile time. - -- [ ] **Step 3: Run full test suite** - -```bash -./mfc.sh test -j 8 -``` -Expected: same pass/fail count as before this change (zero new failures). - -- [ ] **Step 4: Verify new-parameter workflow end-to-end** - -Add a dummy parameter to prove the 2-location workflow works: - -In `definitions.py`, add: -```python -_r("test_codegen_param", INT) -``` - -In `namelist_targets.py`, add to `NAMELIST_VARS`: -```python -"test_codegen_param": {"sim"}, -``` - -Run `./mfc.sh generate` and verify `generated_namelist_sim.fpp` and `generated_decls_sim.fpp` contain `test_codegen_param`. - -Then revert both additions (this was just a smoke test): -```bash -git diff toolchain/mfc/params/definitions.py toolchain/mfc/params/namelist_targets.py -git checkout -- toolchain/mfc/params/definitions.py toolchain/mfc/params/namelist_targets.py -./mfc.sh generate -``` - ---- - -## Test Plan - -### Unit tests (run via `./mfc.sh lint`) - -| Test file | What it covers | -|-----------|----------------| -| `toolchain/tests/params/test_schema.py` | `str_len` field on `ParamDef` | -| `toolchain/tests/params/test_namelist_targets.py` | `NAMELIST_VARS` coverage per target; `CASE_OPT_EXCLUDE` | -| `toolchain/tests/params/test_fortran_gen.py` | `get_namelist_var`, `fortran_type_decl`, namelist/decl content per target | - -### Integration tests - -| Test | Command | Pass criterion | -|------|---------|----------------| -| Build all targets | `./mfc.sh build -j 8` | Zero compile errors | -| Case-optimized build | `./mfc.sh build -t simulation --case-optimization -i examples/3d_taylor_green_vortex/case.py -j 8` | Compiles; CASE_OPT vars absent from runtime namelist | -| 1D regression tests | `./mfc.sh test --only 1D -j 8` | Same pass count as before | -| 2D regression tests | `./mfc.sh test --only 2D -j 8` | Same pass count as before | -| Full test suite | `./mfc.sh test -j 8` | Zero new failures | - -### CI / precheck tests - -| Test | Command | Pass criterion | -|------|---------|----------------| -| Generated files up to date | `./mfc.sh generate --check` | Exit 0 | -| Full precheck | `./mfc.sh precheck -j 8` | All 7/7 checks pass | -| Stale file detection | Modify a generated `.fpp`, run `./mfc.sh generate --check` | Exit 1 | - -### Regression proof: new-parameter workflow - -**Before**: adding `test_codegen_param` to simulation only required editing 3 files (definitions.py, simulation/m_global_parameters.fpp, simulation/m_start_up.fpp). - -**After**: editing only `definitions.py` + `namelist_targets.py` + running `./mfc.sh generate` is sufficient. This is verified in Task 8 Step 4. From 5f88f31852fad40258cd9eedb9822f4e1a2fb7df Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Wed, 27 May 2026 23:56:05 -0400 Subject: [PATCH 11/33] =?UTF-8?q?refactor:=20clean=20up=20codegen=20?= =?UTF-8?q?=E2=80=94=20remove=20dead=20alias,=20stale=20comments,=20docstr?= =?UTF-8?q?ing=20violations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- toolchain/mfc/params/generators/cmake_gen.py | 34 ++++------- .../mfc/params/generators/fortran_gen.py | 59 ++++--------------- toolchain/mfc/params/namelist_targets.py | 23 ++++---- 3 files changed, 33 insertions(+), 83 deletions(-) diff --git a/toolchain/mfc/params/generators/cmake_gen.py b/toolchain/mfc/params/generators/cmake_gen.py index 53b98098c9..36dd41cbfc 100644 --- a/toolchain/mfc/params/generators/cmake_gen.py +++ b/toolchain/mfc/params/generators/cmake_gen.py @@ -1,11 +1,7 @@ -""" -Generate Fortran parameter .fpp files into the CMake build directory. +"""Generate Fortran parameter .fpp files into the CMake build directory. Called by CMakeLists.txt at configure time: python3 cmake_gen.py - -Writes generated_namelist.fpp and generated_decls.fpp into - /include/{pre_process,simulation,post_process}/ """ import sys @@ -13,24 +9,14 @@ sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent)) -from mfc.params.generators.fortran_gen import generate_decls_fpp, generate_namelist_fpp - -_TARGETS = [ - ("pre", "pre_process"), - ("sim", "simulation"), - ("post", "post_process"), -] - - -def main(build_dir: Path) -> None: - for short, full in _TARGETS: - out_dir = build_dir / "include" / full - out_dir.mkdir(parents=True, exist_ok=True) - (out_dir / "generated_namelist.fpp").write_text(generate_namelist_fpp(short)) - (out_dir / "generated_decls.fpp").write_text(generate_decls_fpp(short)) +from mfc.params.generators.fortran_gen import generate_decls_fpp, generate_namelist_fpp # noqa: E402 +if len(sys.argv) != 2: + sys.exit(f"Usage: {sys.argv[0]} ") -if __name__ == "__main__": - if len(sys.argv) != 2: - sys.exit(f"Usage: {sys.argv[0]} ") - main(Path(sys.argv[1])) +build_dir = Path(sys.argv[1]) +for short, full in [("pre", "pre_process"), ("sim", "simulation"), ("post", "post_process")]: + out_dir = build_dir / "include" / full + out_dir.mkdir(parents=True, exist_ok=True) + (out_dir / "generated_namelist.fpp").write_text(generate_namelist_fpp(short)) + (out_dir / "generated_decls.fpp").write_text(generate_decls_fpp(short)) diff --git a/toolchain/mfc/params/generators/fortran_gen.py b/toolchain/mfc/params/generators/fortran_gen.py index 06ab78c878..f8eda280fd 100644 --- a/toolchain/mfc/params/generators/fortran_gen.py +++ b/toolchain/mfc/params/generators/fortran_gen.py @@ -1,12 +1,4 @@ -""" -Fortran parameter code generator. - -Generates namelist fragments and simple scalar declaration fragments -per target (pre/sim/post). Output consumed by generate.py. - -Output format matches ffmt (the MFC Fortran formatter) so that -./mfc.sh format is idempotent on these generated files. -""" +"""Fortran parameter code generator — namelist and scalar decl fragments per target.""" import re from typing import List @@ -17,17 +9,14 @@ from ..registry import REGISTRY from ..schema import ParamDef, ParamType -# ffmt collapses the two header lines into one and uses ASCII hyphen -_HEADER = "! AUTO-GENERATED - do not edit directly. Regenerate: ./mfc.sh generate\n!\n" +_HEADER = "! AUTO-GENERATED - do not edit directly. Regenerate: cmake reconfigure\n!\n" -# ffmt formats Fortran with max 130-char lines and 4-space continuation indent _MAX_LINE = 130 _FIRST_PREFIX = "namelist /user_inputs/ " _CONT_PREFIX = " & " -_CONT2_PREFIX = " & " # second-level continuation (inside Fypp #:if block) +_CONT2_PREFIX = " & " # inside #:if block -# ffmt aligns '::' to a fixed column; widest type is character(LEN=path_len) = 23 chars -_DECL_COL = 24 # pad type string to this width before '::' +_DECL_COL = 24 # '::' column, matches ffmt alignment def get_namelist_var(param_name: str) -> str: @@ -55,34 +44,24 @@ def fortran_type_decl(param: ParamDef) -> str: def _is_simple_scalar(name: str) -> bool: - """Return True if name has no '%' and no '(' - i.e. a plain simple variable.""" return "%" not in name and "(" not in name def _vars_for_target(target: str) -> List[str]: - """Return sorted list of namelist variable names for the given target.""" return sorted(v for v, ts in NAMELIST_VARS.items() if target in ts) def _pack_namelist(vars_list: List[str], first_prefix: str, cont_prefix: str, max_line: int) -> List[str]: - """ - Pack a list of variable names into Fortran namelist continuation lines. - - Returns a list of lines WITHOUT trailing newlines. - All lines except the last end with ', &'. - """ + """Pack variable names into Fortran continuation lines; all but last end with ', &'.""" if not vars_list: return [] - lines: List[str] = [] prefix = first_prefix current_vars: List[str] = [] current_len = len(prefix) - for var in vars_list: additional = len(var) + (2 if current_vars else 0) if current_vars and current_len + additional + 3 > max_line: - # Flush with continuation marker lines.append(prefix + ", ".join(current_vars) + ", &") prefix = cont_prefix current_vars = [var] @@ -90,58 +69,44 @@ def _pack_namelist(vars_list: List[str], first_prefix: str, cont_prefix: str, ma else: current_vars.append(var) current_len += additional - if current_vars: lines.append(prefix + ", ".join(current_vars)) - return lines def _format_namelist(vars_list: List[str]) -> str: - """Format vars as a Fortran namelist statement block (no trailing newline).""" - lines = _pack_namelist(vars_list, _FIRST_PREFIX, _CONT_PREFIX, _MAX_LINE) - return "\n".join(lines) + return "\n".join(_pack_namelist(vars_list, _FIRST_PREFIX, _CONT_PREFIX, _MAX_LINE)) def generate_namelist_fpp(target: str) -> str: - """Generate the namelist /user_inputs/ statement for a target.""" + """Return the namelist /user_inputs/ statement for a target as a string.""" assert target in ("pre", "sim", "post") all_vars = _vars_for_target(target) if target != "sim": return _HEADER + _format_namelist(all_vars) + "\n" - # For sim: split into normal vars and case-opt-excluded vars normal = [v for v in all_vars if v not in CASE_OPT_EXCLUDE] opt = sorted(v for v in CASE_OPT_EXCLUDE if v in NAMELIST_VARS and "sim" in NAMELIST_VARS[v]) - # Normal vars: last line gets ', &' since opt vars follow nl_lines = _pack_namelist(normal, _FIRST_PREFIX, _CONT_PREFIX, _MAX_LINE) nl_lines[-1] += ", &" - - # Opt vars: pack using cont_prefix for first line, cont2_prefix for subsequent opt_lines = _pack_namelist(opt, _CONT_PREFIX, _CONT2_PREFIX, _MAX_LINE) - all_lines = [_HEADER.rstrip()] + nl_lines + ["#:if not MFC_CASE_OPTIMIZATION"] + opt_lines + ["#:endif"] - return "\n".join(all_lines) + "\n" + parts = [_HEADER.rstrip()] + nl_lines + ["#:if not MFC_CASE_OPTIMIZATION"] + opt_lines + ["#:endif"] + return "\n".join(parts) + "\n" def generate_decls_fpp(target: str) -> str: - """Generate simple scalar Fortran variable declarations for a target. - - Column-aligns '::' to match ffmt output (type padded to _DECL_COL chars). - """ + """Return simple scalar Fortran declarations for a target as a string.""" assert target in ("pre", "sim", "post") all_params = REGISTRY.all_params - vars_for_target = _vars_for_target(target) lines = [_HEADER.rstrip()] - for name in vars_for_target: + for name in _vars_for_target(target): if not _is_simple_scalar(name): continue param = all_params.get(name) if param is None: continue - type_str = fortran_type_decl(param) - padded = type_str.ljust(_DECL_COL) - lines.append(f"{padded}:: {name}") + lines.append(f"{fortran_type_decl(param).ljust(_DECL_COL)}:: {name}") return "\n".join(lines) + "\n" diff --git a/toolchain/mfc/params/namelist_targets.py b/toolchain/mfc/params/namelist_targets.py index 12754e475e..9593398013 100644 --- a/toolchain/mfc/params/namelist_targets.py +++ b/toolchain/mfc/params/namelist_targets.py @@ -51,14 +51,14 @@ # --- Time --- "dt": {"sim"}, "t_step_start": _ALL, - "t_step_stop": {"sim", "post"}, - "t_step_save": {"sim", "post"}, + "t_step_stop": _SIM_POST, + "t_step_save": _SIM_POST, "t_step_print": {"sim"}, "t_step_old": {"pre", "sim"}, "time_stepper": {"sim"}, - "t_stop": {"sim", "post"}, - "t_save": {"sim", "post"}, - "cfl_target": {"sim", "post"}, + "t_stop": _SIM_POST, + "t_save": _SIM_POST, + "cfl_target": _SIM_POST, "cfl_adap_dt": _ALL, "cfl_const_dt": _ALL, "n_start": _ALL, @@ -86,14 +86,13 @@ "muscl_eps": {"sim"}, "recon_type": {"pre", "post"}, "muscl_order": {"pre", "post"}, - "muscl_lim": set(), "int_comp": {"sim"}, "ic_eps": {"sim"}, "ic_beta": {"sim"}, # --- Riemann solver --- "riemann_solver": {"sim"}, "wave_speeds": {"sim"}, - "avg_state": {"sim", "post"}, + "avg_state": _SIM_POST, "low_Mach": {"sim"}, # --- MHD --- "mhd": {"pre", "post"}, @@ -170,7 +169,7 @@ "coefficient_of_restitution": {"sim"}, "collision_time": {"sim"}, "ib_coefficient_of_friction": {"sim"}, - "ib_state_wrt": {"sim", "post"}, + "ib_state_wrt": _SIM_POST, # --- Continuum damage --- "cont_damage": _ALL, "tau_star": {"sim"}, @@ -187,7 +186,7 @@ "integral_wrt": {"sim"}, "num_integrals": {"sim"}, "integral": {"sim"}, - "fd_order": {"sim", "post"}, + "fd_order": _SIM_POST, # --- Acoustic sources (sim) --- "acoustic_source": {"sim"}, "num_source": {"sim"}, @@ -216,7 +215,7 @@ "precision": _ALL, "parallel_io": _ALL, "file_per_process": _ALL, - "prim_vars_wrt": {"sim", "post"}, + "prim_vars_wrt": _SIM_POST, "cons_vars_wrt": {"post"}, "run_time_info": {"sim"}, "fft_wrt": _ALL, @@ -245,8 +244,8 @@ "cf_wrt": {"post"}, "chem_wrt_T": {"post"}, "chem_wrt_Y": {"post"}, - "alt_soundspeed": {"sim", "post"}, - "mixture_err": {"sim", "post"}, + "alt_soundspeed": _SIM_POST, + "mixture_err": _SIM_POST, "flux_lim": {"post"}, "sim_data": {"post"}, "alpha_rho_e_wrt": {"post"}, From 504a3480fc2a323b09364d0e376d9628ed6d0313 Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Thu, 28 May 2026 01:13:36 -0400 Subject: [PATCH 12/33] refactor(params): consolidate codegen, eliminate duplicate description/namelist systems - Delete namelist_targets.py: NAMELIST_VARS moved to definitions.py (one file to edit when adding a param); CASE_OPT_EXCLUDE was a duplicate of CASE_OPT_PARAMS - Delete _FALLBACK_PARAMS (380 hardcoded lines) in namelist_parser.py: replaced by _fallback_params() that derives the same data from NAMELIST_VARS at runtime - Delete _SIMPLE_DESCS (145 lines) and _auto_describe in definitions.py: _r() now populates ParamDef.description from descriptions.get_description() so callers can read param.description directly; three callers (docs_gen, json_schema_gen, params_cmd) migrated off the external get_description() import - Delete _PREFIX_DESCS and _ATTR_DESCS (113 lines) in definitions.py: dead code after _auto_describe was removed - Add resolve_namelist_content() and TARGET_FROM_DIR to fortran_gen.py: single canonical helper for the detect-generated-include pattern duplicated across namelist_parser.py and lint_param_docs.py - Consolidate NAMELIST_VARS to use _nv() helper: 200-line explicit dict replaced by grouped calls (~75 lines); cmake_gen.py refactored to use get_generated_files() - Switch test runner from hardcoded unittest module list to pytest discover; add pytest to pyproject.toml; move three orphaned test files into params_tests/ - CMake: stamp-based caching so Fortran codegen only reruns when inputs change - Extract _format_constraints_cell() in docs_gen.py: 5-line pattern duplicated across pattern-view and full-table rendering paths - Inline 4 single-use wrapper functions in case_dicts.py Net vs master: -564 lines --- CMakeLists.txt | 39 +- src/simulation/m_global_parameters.fpp | 2 - toolchain/bootstrap/lint.sh | 6 +- toolchain/mfc/lint_param_docs.py | 46 +- toolchain/mfc/params/definitions.py | 422 ++++---------- toolchain/mfc/params/descriptions.py | 18 +- toolchain/mfc/params/generators/cmake_gen.py | 10 +- toolchain/mfc/params/generators/docs_gen.py | 31 +- .../mfc/params/generators/fortran_gen.py | 79 ++- .../mfc/params/generators/json_schema_gen.py | 13 +- toolchain/mfc/params/namelist_parser.py | 530 ++---------------- toolchain/mfc/params/namelist_targets.py | 315 ----------- toolchain/mfc/params_cmd.py | 11 +- .../params_tests}/test_fortran_gen.py | 9 +- .../params_tests}/test_namelist_targets.py | 22 +- .../params_tests}/test_schema.py | 0 toolchain/mfc/run/case_dicts.py | 105 +--- toolchain/pyproject.toml | 5 + 18 files changed, 282 insertions(+), 1381 deletions(-) delete mode 100644 toolchain/mfc/params/namelist_targets.py rename toolchain/{tests/params => mfc/params_tests}/test_fortran_gen.py (92%) rename toolchain/{tests/params => mfc/params_tests}/test_namelist_targets.py (68%) rename toolchain/{tests/params => mfc/params_tests}/test_schema.py (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8e28be07c3..9c36df54c9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -464,16 +464,37 @@ endmacro() # Generate Fortran parameter namelist/decl includes into the per-target build # include directories before HANDLE_SOURCES globs them for Fypp. find_package(Python3 REQUIRED COMPONENTS Interpreter) -execute_process( - COMMAND "${Python3_EXECUTABLE}" - "${CMAKE_CURRENT_SOURCE_DIR}/toolchain/mfc/params/generators/cmake_gen.py" - "${CMAKE_BINARY_DIR}" - WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" - RESULT_VARIABLE _mfc_gen_result - ERROR_VARIABLE _mfc_gen_error +set(_mfc_gen_stamp "${CMAKE_BINARY_DIR}/mfc_params_gen.stamp") +set(_mfc_gen_inputs + "${CMAKE_CURRENT_SOURCE_DIR}/toolchain/mfc/params/generators/cmake_gen.py" + "${CMAKE_CURRENT_SOURCE_DIR}/toolchain/mfc/params/generators/fortran_gen.py" + "${CMAKE_CURRENT_SOURCE_DIR}/toolchain/mfc/params/namelist_targets.py" + "${CMAKE_CURRENT_SOURCE_DIR}/toolchain/mfc/params/definitions.py" ) -if(NOT _mfc_gen_result EQUAL 0) - message(FATAL_ERROR "Fortran param generation failed:\n${_mfc_gen_error}") +set(_mfc_needs_regen FALSE) +if(NOT EXISTS "${_mfc_gen_stamp}") + set(_mfc_needs_regen TRUE) +else() + foreach(_input IN LISTS _mfc_gen_inputs) + if("${_input}" IS_NEWER_THAN "${_mfc_gen_stamp}") + set(_mfc_needs_regen TRUE) + break() + endif() + endforeach() +endif() +if(_mfc_needs_regen) + execute_process( + COMMAND "${Python3_EXECUTABLE}" + "${CMAKE_CURRENT_SOURCE_DIR}/toolchain/mfc/params/generators/cmake_gen.py" + "${CMAKE_BINARY_DIR}" + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + RESULT_VARIABLE _mfc_gen_result + ERROR_VARIABLE _mfc_gen_error + ) + if(NOT _mfc_gen_result EQUAL 0) + message(FATAL_ERROR "Fortran param generation failed:\n${_mfc_gen_error}") + endif() + file(TOUCH "${_mfc_gen_stamp}") endif() HANDLE_SOURCES(pre_process ON) diff --git a/src/simulation/m_global_parameters.fpp b/src/simulation/m_global_parameters.fpp index 2c07e4d9c8..f7df2de9f8 100644 --- a/src/simulation/m_global_parameters.fpp +++ b/src/simulation/m_global_parameters.fpp @@ -70,8 +70,6 @@ module m_global_parameters integer :: num_dims !< Number of spatial dimensions integer :: num_vels !< Number of velocity components (different from num_dims for mhd) #:endif - ! mpp_lim, time_stepper, prim_vars_wrt now in generated_decls.fpp - #:if MFC_CASE_OPTIMIZATION integer, parameter :: recon_type = ${recon_type}$ !< Reconstruction type integer, parameter :: weno_polyn = ${weno_polyn}$ !< Degree of the WENO polynomials (polyn) diff --git a/toolchain/bootstrap/lint.sh b/toolchain/bootstrap/lint.sh index c22b772f86..024f9dcd12 100644 --- a/toolchain/bootstrap/lint.sh +++ b/toolchain/bootstrap/lint.sh @@ -32,12 +32,8 @@ ruff check benchmarks/*/case.py if [ "$RUN_TESTS" = true ]; then log "(venv) Running$MAGENTA unit tests$COLOR_RESET on$MAGENTA MFC$COLOR_RESET's $MAGENTA""toolchain$COLOR_RESET." - # Run tests as modules from the toolchain directory to resolve relative imports cd toolchain - python3 -m unittest mfc.params_tests.test_registry mfc.params_tests.test_definitions mfc.params_tests.test_validate mfc.params_tests.test_integration -v - python3 -m unittest mfc.cli.test_cli -v - python3 -m unittest mfc.viz.test_viz -v - python3 -m unittest mfc.run.test_archive -v + python3 -m pytest . -v cd - > /dev/null fi diff --git a/toolchain/mfc/lint_param_docs.py b/toolchain/mfc/lint_param_docs.py index 425fd53900..2f62d34ac5 100644 --- a/toolchain/mfc/lint_param_docs.py +++ b/toolchain/mfc/lint_param_docs.py @@ -80,49 +80,6 @@ def _param_appears_in_case_md(param_base: str, tokens: set[str], text: str) -> b return False -def _parse_namelist_params(fpp_path: Path) -> set[str]: - """Parse parameter names from a namelist /user_inputs/ block in an fpp file.""" - text = fpp_path.read_text(encoding="utf-8") - - # If the namelist is in a #:include'd generated file, generate in-memory. - if re.search(r"#:include\s+'generated_namelist\.fpp'", text): - _target_map = {"pre_process": "pre", "simulation": "sim", "post_process": "post"} - short = _target_map.get(fpp_path.parent.name) - if short: - from mfc.params.generators.fortran_gen import generate_namelist_fpp - - text = generate_namelist_fpp(short) - - params = set() - - in_namelist = False - accum = "" - for line in text.splitlines(): - stripped = line.strip() - if stripped.startswith("!") or stripped.startswith("#:") or stripped.startswith("#"): - continue - lower = stripped.lower() - if "namelist /user_inputs/" in lower: - idx = lower.index("/user_inputs/") + len("/user_inputs/") - accum += " " + stripped[idx:] - in_namelist = accum.rstrip().endswith("&") - continue - if in_namelist: - if stripped.startswith("&"): - stripped = stripped[1:] - accum += " " + stripped - if not accum.rstrip().endswith("&"): - in_namelist = False - - accum = accum.replace("&", " ") - for raw_token in accum.split(","): - name = raw_token.strip() - if name and re.match(r"^[a-zA-Z_]\w*$", name): - params.add(name) - - return params - - def check_descriptions_in_case_md(repo_root: Path) -> list[str]: """Tier 1, Check 1+2: Params with DESCRIPTIONS entries should appear in case.md.""" REGISTRY, DESCRIPTIONS = _import_registry(repo_root) @@ -185,13 +142,14 @@ def _is_derived_type_parent_or_field(name: str, all_params: dict) -> bool: def check_namelist_registry_sync(repo_root: Path) -> list[str]: """Tier 2: Unknown Fortran namelist params must be in REGISTRY (blocking).""" REGISTRY, _ = _import_registry(repo_root) + from mfc.params.namelist_parser import parse_namelist_from_file errors = [] startup_files = sorted((repo_root / "src").rglob("m_start_up.fpp")) all_params = REGISTRY.all_params for fpp in startup_files: - nl_params = _parse_namelist_params(fpp) + nl_params = parse_namelist_from_file(fpp) rel = fpp.relative_to(repo_root) for p in sorted(nl_params): diff --git a/toolchain/mfc/params/definitions.py b/toolchain/mfc/params/definitions.py index 9be1f836be..0cb042d3eb 100644 --- a/toolchain/mfc/params/definitions.py +++ b/toolchain/mfc/params/definitions.py @@ -39,331 +39,6 @@ def _fc(name: str, default: int) -> int: NA = 4 # acoustic sources: enumerated individually -# Auto-generated Descriptions -# Descriptions are auto-generated from parameter names using naming conventions. -# Override with explicit desc= parameter when auto-generation is inadequate. - -# Prefix descriptions for indexed parameter families -_PREFIX_DESCS = { - "patch_icpp": "initial condition patch", - "patch_ib": "immersed boundary", - "patch_bc": "boundary condition patch", - "fluid_pp": "fluid", - "acoustic": "acoustic source", - "probe": "probe", - "integral": "integral region", -} - -# Attribute descriptions (suffix after %) -_ATTR_DESCS = { - # Geometry/position - "geometry": "Geometry type", - "x_centroid": "X-coordinate of centroid", - "y_centroid": "Y-coordinate of centroid", - "z_centroid": "Z-coordinate of centroid", - "length_x": "X-dimension length", - "length_y": "Y-dimension length", - "length_z": "Z-dimension length", - "radius": "Radius", - "radii": "Radii array", - "normal": "Normal direction", - "theta": "Theta angle", - "angles": "Orientation angles", - # Physics - "vel": "Velocity", - "pres": "Pressure", - "rho": "Density", - "alpha": "Volume fraction", - "alpha_rho": "Partial density", - "gamma": "Specific heat ratio", - "pi_inf": "Stiffness pressure", - "cv": "Specific heat (const. volume)", - "qv": "Heat of formation", - "qvp": "Heat of formation prime", - "G": "Shear modulus", - "Re": "Reynolds number", - "mul0": "Reference viscosity", - "ss": "Surface tension", - "pv": "Vapor pressure", - # MHD - "Bx": "Magnetic field (x-component)", - "By": "Magnetic field (y-component)", - "Bz": "Magnetic field (z-component)", - # Model/smoothing - "smoothen": "Enable smoothing", - "smooth_patch_id": "Patch ID to smooth against", - "smooth_coeff": "Smoothing coefficient", - "alter_patch": "Alter with another patch", - "model_filepath": "STL model file path", - "model_spc": "Model spacing", - "model_threshold": "Model threshold", - "model_translate": "Model translation", - "model_scale": "Model scale", - "model_rotate": "Model rotation", - # Bubbles - "r0": "Initial bubble radius", - "v0": "Initial bubble velocity", - "p0": "Initial bubble pressure", - "m0": "Initial bubble mass", - # IB specific - "slip": "Enable slip condition", - "moving_ibm": "Enable moving boundary", - "angular_vel": "Angular velocity", - "mass": "Mass", - # BC specific - "vel_in": "Inlet velocity", - "vel_out": "Outlet velocity", - "alpha_rho_in": "Inlet partial density", - "alpha_in": "Inlet volume fraction", - "pres_in": "Inlet pressure", - "pres_out": "Outlet pressure", - "grcbc_in": "Enable GRCBC inlet", - "grcbc_out": "Enable GRCBC outlet", - "grcbc_vel_out": "Enable GRCBC velocity outlet", - "isothermal_in": "Enable isothermal wall at the domain entrance (minimum coordinate)", - "isothermal_out": "Enable isothermal wall at the domain exit (maximum coordinate)", - # Acoustic - "loc": "Location", - "mag": "Magnitude", - "pulse": "Pulse type", - "support": "Support type", - "frequency": "Frequency", - "wavelength": "Wavelength", - "length": "Length", - "height": "Height", - "delay": "Delay time", - "dipole": "Enable dipole", - "dir": "Direction", - # Output - "x": "X-coordinate", - "y": "Y-coordinate", - "z": "Z-coordinate", - "xmin": "X minimum", - "xmax": "X maximum", - "ymin": "Y minimum", - "ymax": "Y maximum", - "zmin": "Z minimum", - "zmax": "Z maximum", - # Chemistry - "Y": "Species mass fraction", - # Shape coefficients - "a": "Shape coefficient", - # Elasticity - "tau_e": "Elastic stress component", - # Misc - "cf_val": "Color function value", - "hcid": "Hard-coded ID", - "epsilon": "Interface thickness", - "beta": "Shape parameter beta", - "non_axis_sym": "Non-axisymmetric parameter", -} - -# Simple parameter descriptions (non-indexed) -_SIMPLE_DESCS = { - # Grid - "m": "Grid cells in x-direction", - "n": "Grid cells in y-direction", - "p": "Grid cells in z-direction", - "cyl_coord": "Enable cylindrical coordinates", - "stretch_x": "Enable grid stretching in x", - "stretch_y": "Enable grid stretching in y", - "stretch_z": "Enable grid stretching in z", - "a_x": "Grid stretching rate in x", - "a_y": "Grid stretching rate in y", - "a_z": "Grid stretching rate in z", - "x_a": "Stretching start (negative x)", - "x_b": "Stretching start (positive x)", - "y_a": "Stretching start (negative y)", - "y_b": "Stretching start (positive y)", - "z_a": "Stretching start (negative z)", - "z_b": "Stretching start (positive z)", - "loops_x": "Stretching iterations in x", - "loops_y": "Stretching iterations in y", - "loops_z": "Stretching iterations in z", - # Time - "dt": "Time step size", - "t_step_start": "Starting time step", - "t_step_stop": "Ending time step", - "t_step_save": "Save interval (steps)", - "t_step_print": "Print interval (steps)", - "t_stop": "Stop time", - "t_save": "Save interval (time)", - "time_stepper": "Time integration scheme", - "cfl_target": "Target CFL number", - "cfl_adap_dt": "Enable adaptive CFL time stepping", - "cfl_const_dt": "Use constant CFL time stepping", - "cfl_dt": "Enable CFL-based time stepping", - "adap_dt": "Enable adaptive time stepping", - "adap_dt_tol": "Adaptive time stepping tolerance", - "adap_dt_max_iters": "Max iterations for adaptive dt", - # Model - "model_eqns": "Model equations", - "num_fluids": "Number of fluids", - "num_patches": "Number of IC patches", - "mpp_lim": "Mixture pressure positivity limiter", - # WENO - "weno_order": "WENO reconstruction order", - "weno_eps": "WENO epsilon parameter", - "mapped_weno": "Enable mapped WENO", - "wenoz": "Enable WENO-Z", - "teno": "Enable TENO", - "mp_weno": "Enable monotonicity-preserving WENO", - # Riemann - "riemann_solver": "Riemann solver", - "wave_speeds": "Wave speed estimate method", - "avg_state": "Average state", - # Physics toggles - "viscous": "Enable viscous effects", - "mhd": "Enable magnetohydrodynamics", - "hyper_cleaning": "Enable hyperbolic divergence cleaning", - "hyper_cleaning_speed": "Divergence cleaning wave speed", - "hyper_cleaning_tau": "Divergence cleaning damping time", - "bubbles_euler": "Enable Euler bubble model", - "bubbles_lagrange": "Enable Lagrangian bubbles", - "polytropic": "Enable polytropic gas", - "polydisperse": "Enable polydisperse bubbles", - "qbmm": "Enable QBMM", - "chemistry": "Enable chemistry", - "surface_tension": "Enable surface tension", - "hypoelasticity": "Enable hypoelastic model", - "hyperelasticity": "Enable hyperelastic model", - "relativity": "Enable special relativity", - "ib": "Enable immersed boundaries", - "collision_model": "Collision model for immersed boundaries (0=none, 1=soft sphere)", - "coefficient_of_restitution": "Coefficient of restitution for IB collisions", - "collision_time": "Characteristic collision time for IB collisions", - "ib_coefficient_of_friction": "Coefficient of friction for IB collisions", - "acoustic_source": "Enable acoustic sources", - # Output - "parallel_io": "Enable parallel I/O", - "probe_wrt": "Write probe data", - "prim_vars_wrt": "Write primitive variables", - "cons_vars_wrt": "Write conservative variables", - "run_time_info": "Print runtime info", - "ib_state_wrt": "Write IB state and load data", - # Misc - "case_dir": "Case directory path", - "cantera_file": "Cantera mechanism file", - "num_ibs": "Number of immersed boundaries", - "num_source": "Number of acoustic sources", - "num_probes": "Number of probes", - "num_integrals": "Number of integral regions", - "nb": "Number of bubble bins", - "R0ref": "Reference bubble radius", - "sigma": "Surface tension coefficient", - "Bx0": "Background magnetic field (x)", - "old_grid": "Load grid from previous simulation", - "old_ic": "Load initial conditions from previous", - "t_step_old": "Time step to restart from", - "fd_order": "Finite difference order", - "recon_type": "Reconstruction type", - "muscl_order": "MUSCL reconstruction order", - "muscl_lim": "MUSCL limiter type", - "muscl_eps": "MUSCL limiter slope-product threshold", - "low_Mach": "Low Mach number correction", - "bubble_model": "Bubble dynamics model", - "Ca": "Cavitation number", - "Web": "Weber number", - "Re_inv": "Inverse Reynolds number", - "format": "Output format", - "precision": "Output precision", - # Body forces - "bf_x": "Enable body force in x", - "bf_y": "Enable body force in y", - "bf_z": "Enable body force in z", - "k_x": "Body force wavenumber in x", - "k_y": "Body force wavenumber in y", - "k_z": "Body force wavenumber in z", - "w_x": "Body force frequency in x", - "w_y": "Body force frequency in y", - "w_z": "Body force frequency in z", - "p_x": "Body force phase in x", - "p_y": "Body force phase in y", - "p_z": "Body force phase in z", - "g_x": "Gravitational acceleration in x", - "g_y": "Gravitational acceleration in y", - "g_z": "Gravitational acceleration in z", - # More output - "E_wrt": "Write energy field", - "c_wrt": "Write sound speed field", - "rho_wrt": "Write density field", - "pres_wrt": "Write pressure field", - "schlieren_wrt": "Write schlieren images", - "cf_wrt": "Write color function", - "omega_wrt": "Write vorticity", - "qm_wrt": "Write Q-criterion", - "liutex_wrt": "Write Liutex vortex field", - "gamma_wrt": "Write gamma field", - "heat_ratio_wrt": "Write heat capacity ratio", - "pi_inf_wrt": "Write pi_inf field", - "pres_inf_wrt": "Write reference pressure", - "fft_wrt": "Write FFT output", - "chem_wrt_T": "Write temperature (chemistry)", - # Misc physics - "alt_soundspeed": "Alternative sound speed formulation", - "mixture_err": "Enable mixture error checking", - "cont_damage": "Enable continuum damage model", -} - - -def _auto_describe(name: str) -> str: - """Auto-generate description from parameter name.""" - # Check simple params first - if name in _SIMPLE_DESCS: - return _SIMPLE_DESCS[name] - - # Handle indexed params: prefix(N)%attr or prefix(N)%attr(M) - match = re.match(r"([a-z_]+)\((\d+)\)%(.+)", name) - if match: - prefix, idx, attr = match.group(1), match.group(2), match.group(3) - prefix_desc = _PREFIX_DESCS.get(prefix, prefix.replace("_", " ")) - - # Check for nested index: attr(M) or attr(M, K) - attr_match = re.match(r"([a-z_]+)\((\d+)(?:,\s*(\d+))?\)", attr) - if attr_match: - attr_base = attr_match.group(1) - idx2 = attr_match.group(2) - attr_desc = _ATTR_DESCS.get(attr_base, attr_base.replace("_", " ")) - return f"{attr_desc} {idx2} for {prefix_desc} {idx}" - - attr_desc = _ATTR_DESCS.get(attr, attr.replace("_", " ")) - return f"{attr_desc} for {prefix_desc} {idx}" - - # Handle bc_x%attr style (no index in prefix) - if "%" in name: - prefix, attr = name.split("%", 1) - # Check for indexed attr - attr_match = re.match(r"([a-z_]+)\((\d+)\)", attr) - if attr_match: - attr_base, idx = attr_match.group(1), attr_match.group(2) - attr_desc = _ATTR_DESCS.get(attr_base, attr_base.replace("_", " ")) - return f"{attr_desc} {idx} for {prefix.replace('_', ' ')}" - - attr_desc = _ATTR_DESCS.get(attr, "") - if attr_desc: - return f"{attr_desc} for {prefix.replace('_', ' ')}" - # Fallback: just clean up the name - return f"{attr.replace('_', ' ').title()} for {prefix.replace('_', ' ')}" - - # Handle suffix-indexed: name(N) or name(N, M) - match = re.match(r"([a-z_]+)\((\d+)(?:,\s*(\d+))?\)", name) - if match: - base, idx = match.group(1), match.group(2) - # Handle _wrt patterns - if base.endswith("_wrt"): - field = base[:-4].replace("_", " ") - return f"Write {field} for component {idx}" - return f"{base.replace('_', ' ').title()} {idx}" - - # Fallback patterns - if name.endswith("_wrt"): - return f"Write {name[:-4].replace('_', ' ')}" - if name.startswith("num_"): - return f"Number of {name[4:].replace('_', ' ')}" - - # Last resort: clean up the name - return name.replace("_", " ").replace("%", " ") - # Parameters that can be hard-coded for GPU case optimization CASE_OPT_PARAMS = { @@ -819,17 +494,19 @@ def _r(name, ptype, tags=None, desc=None, hint=None, math=None, str_len=None): """Register a parameter with optional feature tags and description.""" if hint is None: hint = _lookup_hint(name) - description = desc if desc else _auto_describe(name) + if desc is None: + from .descriptions import get_description + desc = get_description(name) constraint = CONSTRAINTS.get(name) if constraint and "value_labels" in constraint: labels = constraint["value_labels"] suffix = ", ".join(f"{v}={labels[v]}" for v in sorted(labels)) - description = f"{description} ({suffix})" + desc = f"{desc} ({suffix})".strip() REGISTRY.register( ParamDef( name=name, param_type=ptype, - description=description, + description=desc, case_optimization=(name in CASE_OPT_PARAMS), constraints=constraint, dependencies=DEPENDENCIES.get(name), @@ -1339,3 +1016,92 @@ def _init_registry(): _init_registry() + +# Namelist target mapping for Fortran codegen. +# Maps each Fortran namelist root variable to the set of MFC executables whose +# namelist it appears in. Used by fortran_gen.py to generate per-target .fpp files. +# +# When adding a new parameter: +# 1. Add to definitions.py (type, constraints, etc.) — you are here +# 2. Add the namelist root variable to NAMELIST_VARS with its target set +# 3. Re-run cmake to regenerate the .fpp files (cmake reconfigure) + +NAMELIST_VARS: dict[str, set[str]] = {} + +def _nv(targets: set, *names: str) -> None: + for n in names: + NAMELIST_VARS[n] = set(targets) + +_ALL, _PRE_SIM, _SIM_POST = {"pre", "sim", "post"}, {"pre", "sim"}, {"sim", "post"} +_PRE_POST = {"pre", "post"} +_SIM = {"sim"} +_PRE = {"pre"} +_POST = {"post"} + +_nv(_ALL, + "m", "n", "p", "cyl_coord", "bc_x", "bc_y", "bc_z", "num_bc_patches", "case_dir", + "t_step_start", "cfl_adap_dt", "cfl_const_dt", "n_start", + "model_eqns", "mpp_lim", "relax", "relax_model", + "fluid_pp", "bub_pp", "rhoref", "pref", + "bubbles_euler", "bubbles_lagrange", "R0ref", "polytropic", "thermal", + "Ca", "Web", "Re_inv", "polydisperse", "poly_sigma", "qbmm", "sigma", "adv_n", + "hypoelasticity", "hyperelasticity", "surface_tension", "relativity", + "ib", "num_ibs", "cont_damage", "hyper_cleaning", "Bx0", + "precision", "parallel_io", "file_per_process", "fft_wrt", "down_sample", +) +_nv(_SIM_POST, + "t_step_stop", "t_step_save", "t_stop", "t_save", "cfl_target", + "avg_state", "prim_vars_wrt", "alt_soundspeed", "mixture_err", "fd_order", "ib_state_wrt", +) +_nv(_PRE_SIM, + "x_domain", "y_domain", "z_domain", + "x_a", "y_a", "z_a", "x_b", "y_b", "z_b", + "palpha_eps", "ptgalpha_eps", "t_step_old", "patch_ib", "pi_fac", +) +_nv(_PRE_POST, "num_fluids", "weno_order", "recon_type", "muscl_order", "mhd", "nb", "sigR", "igr", "igr_order") +_nv(_SIM, + "dt", "t_step_print", "time_stepper", "adap_dt", "adap_dt_tol", "adap_dt_max_iters", + "weno_eps", "teno_CT", "wenoz_q", "mp_weno", "weno_avg", "weno_Re_flux", "null_weights", + "muscl_eps", "int_comp", "ic_eps", "ic_beta", + "riemann_solver", "wave_speeds", "low_Mach", + "hyper_cleaning_speed", "hyper_cleaning_tau", + "run_time_info", "bubble_model", "lag_params", + "probe_wrt", "num_probes", "probe", "integral_wrt", "num_integrals", "integral", + "acoustic_source", "num_source", "acoustic", "chem_params", + "bf_x", "bf_y", "bf_z", "k_x", "k_y", "k_z", "w_x", "w_y", "w_z", + "p_x", "p_y", "p_z", "g_x", "g_y", "g_z", + "collision_model", "coefficient_of_restitution", "collision_time", "ib_coefficient_of_friction", + "tau_star", "cont_damage_s", "alpha_bar", + "rdma_mpi", "alf_factor", + "num_igr_iters", "num_igr_warm_start_iters", "igr_iter_solver", "igr_pres_lim", + "nv_uvm_out_of_core", "nv_uvm_igr_temps_on_gpu", "nv_uvm_pref_gpu", +) +_nv(_PRE, + "stretch_x", "stretch_y", "stretch_z", "a_x", "a_y", "a_z", "loops_x", "loops_y", "loops_z", + "n_start_old", "num_patches", "patch_icpp", "patch_bc", + "sigV", "dist_type", "rhoRV", "viscous", + "old_grid", "old_ic", + "perturb_flow", "perturb_flow_fluid", "perturb_flow_mag", + "perturb_sph", "perturb_sph_fluid", "fluid_rho", + "mixlayer_vel_profile", "mixlayer_vel_coef", "mixlayer_perturb", + "mixlayer_perturb_nk", "mixlayer_perturb_k0", + "pre_stress", "elliptic_smoothing", "elliptic_smoothing_iters", + "simplex_perturb", "simplex_params", +) +_nv(_POST, + "x_output", "y_output", "z_output", + "format", "output_partial_domain", "sim_data", "G", "flux_lim", "cons_vars_wrt", + "rho_wrt", "E_wrt", "pres_wrt", "c_wrt", + "gamma_wrt", "heat_ratio_wrt", "pi_inf_wrt", "pres_inf_wrt", + "omega_wrt", "qm_wrt", "liutex_wrt", "schlieren_wrt", "schlieren_alpha", + "alpha_rho_wrt", "mom_wrt", "vel_wrt", "flux_wrt", "alpha_wrt", "cf_wrt", + "chem_wrt_T", "chem_wrt_Y", "alpha_rho_e_wrt", + "lag_header", "lag_txt_wrt", "lag_db_wrt", "lag_id_wrt", + "lag_pos_wrt", "lag_pos_prev_wrt", "lag_vel_wrt", "lag_rad_wrt", "lag_rvel_wrt", + "lag_r0_wrt", "lag_rmax_wrt", "lag_rmin_wrt", "lag_dphidt_wrt", + "lag_pres_wrt", "lag_mv_wrt", "lag_mg_wrt", "lag_betaT_wrt", "lag_betaC_wrt", +) + +# Case-optimization params appear in the sim namelist under #:if not MFC_CASE_OPTIMIZATION. +for _v in CASE_OPT_PARAMS: + NAMELIST_VARS.setdefault(_v, set()).add("sim") diff --git a/toolchain/mfc/params/descriptions.py b/toolchain/mfc/params/descriptions.py index 8ceab7f96c..a0d7fd3bcb 100644 --- a/toolchain/mfc/params/descriptions.py +++ b/toolchain/mfc/params/descriptions.py @@ -514,28 +514,18 @@ def get_description(param_name: str) -> str: - """Get description for a parameter from hand-curated or auto-generated sources. + """Get the best available description for a parameter. - Priority: hand-curated DESCRIPTIONS > PATTERNS > auto-generated param.description. + Priority: hand-curated DESCRIPTIONS > PATTERNS > naming-convention inference. + (param.description in REGISTRY is now populated from this function at registration + time, so it can be read directly without calling this function again.) """ - # 1. Hand-curated descriptions (highest quality) if param_name in DESCRIPTIONS: return DESCRIPTIONS[param_name] - - # 2. Pattern matching for indexed params (hand-curated templates) for pattern, template in PATTERNS: match = re.fullmatch(pattern, param_name) if match: return template.format(*match.groups()) - - # 3. Auto-generated description from registry (set by _auto_describe at registration) - from . import REGISTRY - - param = REGISTRY.all_params.get(param_name) - if param and param.description: - return param.description - - # 4. Last resort: naming convention inference return _infer_from_naming(param_name) diff --git a/toolchain/mfc/params/generators/cmake_gen.py b/toolchain/mfc/params/generators/cmake_gen.py index 36dd41cbfc..c71a8e5c23 100644 --- a/toolchain/mfc/params/generators/cmake_gen.py +++ b/toolchain/mfc/params/generators/cmake_gen.py @@ -9,14 +9,12 @@ sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent)) -from mfc.params.generators.fortran_gen import generate_decls_fpp, generate_namelist_fpp # noqa: E402 +from mfc.params.generators.fortran_gen import get_generated_files # noqa: E402 if len(sys.argv) != 2: sys.exit(f"Usage: {sys.argv[0]} ") build_dir = Path(sys.argv[1]) -for short, full in [("pre", "pre_process"), ("sim", "simulation"), ("post", "post_process")]: - out_dir = build_dir / "include" / full - out_dir.mkdir(parents=True, exist_ok=True) - (out_dir / "generated_namelist.fpp").write_text(generate_namelist_fpp(short)) - (out_dir / "generated_decls.fpp").write_text(generate_decls_fpp(short)) +for path, content in get_generated_files(build_dir): + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content) diff --git a/toolchain/mfc/params/generators/docs_gen.py b/toolchain/mfc/params/generators/docs_gen.py index 2e9f877b3b..3974353482 100644 --- a/toolchain/mfc/params/generators/docs_gen.py +++ b/toolchain/mfc/params/generators/docs_gen.py @@ -13,7 +13,7 @@ from .. import definitions # noqa: F401 from ..ast_analyzer import analyze_case_validator, classify_message -from ..descriptions import get_description, get_math_symbol +from ..descriptions import get_math_symbol from ..registry import REGISTRY from ..schema import ParamType @@ -342,6 +342,14 @@ def _format_validator_rules(param_name: str, by_trigger: Dict[str, list], by_par return "; ".join(parts) +def _format_constraints_cell(name: str, param, by_trigger, by_param) -> str: + """Format the Constraints table cell for a single parameter.""" + extra = "; ".join(filter(None, [_format_constraints(param), _format_validator_rules(name, by_trigger, by_param)])) + if not extra: + extra = _format_tag_annotation(name, param) + return _escape_pct_outside_backticks(extra) + + def generate_parameter_docs() -> str: """Generate markdown documentation for all parameters.""" # AST-extract rules from case_validator.py @@ -476,7 +484,7 @@ def generate_parameter_docs() -> str: for pattern, examples in sorted(patterns.items()): example = examples[0] - desc = get_description(example) or "" + desc = REGISTRY.all_params[example].description # Truncate long descriptions if len(desc) > 60: desc = desc[:57] + "..." @@ -490,13 +498,7 @@ def generate_parameter_docs() -> str: row += f" | {sym}" if pattern_has_constraints: p = REGISTRY.all_params[example] - constraints = _format_constraints(p) - deps = _format_validator_rules(example, by_trigger, by_param) - extra = "; ".join(filter(None, [constraints, deps])) - if not extra: - extra = _format_tag_annotation(example, p) - extra = _escape_pct_outside_backticks(extra) - row += f" | {extra}" + row += f" | {_format_constraints_cell(example, p, by_trigger, by_param)}" lines.append(row + " |") lines.append("") @@ -514,24 +516,17 @@ def generate_parameter_docs() -> str: for name, param in params: type_str = _type_to_str(param.param_type) - desc = get_description(name) or "" + desc = param.description # Truncate long descriptions if len(desc) > 80: desc = desc[:77] + "..." - constraints = _format_constraints(param) - deps = _format_validator_rules(name, by_trigger, by_param) - extra = "; ".join(filter(None, [constraints, deps])) - if not extra: - extra = _format_tag_annotation(name, param) - extra = _escape_pct_outside_backticks(extra) - # Escape % for Doxygen (even inside backtick code spans) name_escaped = _escape_percent(name) desc = _escape_percent(desc) row = f"| `{name_escaped}` | {type_str} | {desc}" if full_has_symbols: sym = get_math_symbol(name) row += f" | {sym}" - row += f" | {extra}" + row += f" | {_format_constraints_cell(name, param, by_trigger, by_param)}" lines.append(row + " |") lines.append("") diff --git a/toolchain/mfc/params/generators/fortran_gen.py b/toolchain/mfc/params/generators/fortran_gen.py index f8eda280fd..9bf5adfb10 100644 --- a/toolchain/mfc/params/generators/fortran_gen.py +++ b/toolchain/mfc/params/generators/fortran_gen.py @@ -1,46 +1,42 @@ """Fortran parameter code generator — namelist and scalar decl fragments per target.""" -import re -from typing import List +from pathlib import Path +from typing import List, Tuple -import mfc.params.definitions # noqa: F401 - triggers registry population - -from ..namelist_targets import CASE_OPT_EXCLUDE, NAMELIST_VARS +from ..definitions import CASE_OPT_PARAMS, NAMELIST_VARS # noqa: F401 - triggers registry population from ..registry import REGISTRY from ..schema import ParamDef, ParamType -_HEADER = "! AUTO-GENERATED - do not edit directly. Regenerate: cmake reconfigure\n!\n" +TARGETS = [("pre", "pre_process"), ("sim", "simulation"), ("post", "post_process")] +TARGET_FROM_DIR = {full: short for short, full in TARGETS} +_HEADER = "! AUTO-GENERATED - do not edit directly. Regenerate: cmake reconfigure\n!\n" _MAX_LINE = 130 _FIRST_PREFIX = "namelist /user_inputs/ " _CONT_PREFIX = " & " _CONT2_PREFIX = " & " # inside #:if block - _DECL_COL = 24 # '::' column, matches ffmt alignment +_FORTRAN_TYPES = { + ParamType.INT: "integer", ParamType.ANALYTIC_INT: "integer", + ParamType.REAL: "real(wp)", ParamType.ANALYTIC_REAL: "real(wp)", + ParamType.LOG: "logical", +} + -def get_namelist_var(param_name: str) -> str: +def get_namelist_var(name: str) -> str: """Return the Fortran namelist root for a parameter name.""" - m = re.match(r"^([a-zA-Z_]\w*)\(", param_name) - if m and "%" in param_name: - return m.group(1) - if "%" in param_name: - return param_name.split("%", maxsplit=1)[0] - return param_name + if "(" in name and "%" in name: + return name.split("(", 1)[0] + if "%" in name: + return name.split("%", 1)[0] + return name def fortran_type_decl(param: ParamDef) -> str: - """Return the Fortran type string for a parameter.""" - mapping = { - ParamType.INT: "integer", - ParamType.REAL: "real(wp)", - ParamType.LOG: "logical", - ParamType.ANALYTIC_INT: "integer", - ParamType.ANALYTIC_REAL: "real(wp)", - } if param.param_type == ParamType.STR: return f"character(LEN={param.str_len})" - return mapping[param.param_type] + return _FORTRAN_TYPES[param.param_type] def _is_simple_scalar(name: str) -> bool: @@ -86,13 +82,11 @@ def generate_namelist_fpp(target: str) -> str: if target != "sim": return _HEADER + _format_namelist(all_vars) + "\n" - normal = [v for v in all_vars if v not in CASE_OPT_EXCLUDE] - opt = sorted(v for v in CASE_OPT_EXCLUDE if v in NAMELIST_VARS and "sim" in NAMELIST_VARS[v]) - + normal = [v for v in all_vars if v not in CASE_OPT_PARAMS] + opt = sorted(v for v in CASE_OPT_PARAMS if v in NAMELIST_VARS and "sim" in NAMELIST_VARS[v]) nl_lines = _pack_namelist(normal, _FIRST_PREFIX, _CONT_PREFIX, _MAX_LINE) nl_lines[-1] += ", &" opt_lines = _pack_namelist(opt, _CONT_PREFIX, _CONT2_PREFIX, _MAX_LINE) - parts = [_HEADER.rstrip()] + nl_lines + ["#:if not MFC_CASE_OPTIMIZATION"] + opt_lines + ["#:endif"] return "\n".join(parts) + "\n" @@ -100,13 +94,40 @@ def generate_namelist_fpp(target: str) -> str: def generate_decls_fpp(target: str) -> str: """Return simple scalar Fortran declarations for a target as a string.""" assert target in ("pre", "sim", "post") - all_params = REGISTRY.all_params lines = [_HEADER.rstrip()] for name in _vars_for_target(target): if not _is_simple_scalar(name): continue - param = all_params.get(name) + param = REGISTRY.all_params.get(name) if param is None: continue lines.append(f"{fortran_type_decl(param).ljust(_DECL_COL)}:: {name}") return "\n".join(lines) + "\n" + + +def resolve_namelist_content(fpp_path: Path) -> str: + """Return the namelist content for an fpp file. + + If the file delegates to a generated include, returns the generated content. + Otherwise returns the file's raw text. + """ + text = fpp_path.read_text() + if "#:include 'generated_namelist.fpp'" not in text: + return text + short = TARGET_FROM_DIR.get(fpp_path.parent.name) + if short is None: + raise ValueError(f"Cannot determine MFC target from path: {fpp_path}") + return generate_namelist_fpp(short) + + +def get_generated_files(build_dir: Path) -> List[Tuple[Path, str]]: + """Return (path, content) for all 6 generated .fpp files under build_dir. + + Paths match the cmake include directory structure: + build_dir/include/{full_target}/generated_{namelist,decls}.fpp + """ + return [ + (build_dir / "include" / full / f"generated_{kind}.fpp", gen(short)) + for short, full in TARGETS + for kind, gen in [("namelist", generate_namelist_fpp), ("decls", generate_decls_fpp)] + ] diff --git a/toolchain/mfc/params/generators/json_schema_gen.py b/toolchain/mfc/params/generators/json_schema_gen.py index dd39994733..5533f512f9 100644 --- a/toolchain/mfc/params/generators/json_schema_gen.py +++ b/toolchain/mfc/params/generators/json_schema_gen.py @@ -48,19 +48,14 @@ def generate_json_schema(include_descriptions: bool = True) -> Dict[str, Any]: Returns: JSON Schema dict """ - from ..descriptions import get_description - properties = {} all_params = [] for name, param in sorted(REGISTRY.all_params.items()): prop_schema = _param_type_to_json_schema(param.param_type, param.constraints) - if include_descriptions: - # Get description from descriptions module - desc = get_description(name) - if desc: - prop_schema["description"] = desc + if include_descriptions and param.description: + prop_schema["description"] = param.description # Add deprecation notice if applicable if param.dependencies and "deprecated" in param.dependencies: @@ -104,15 +99,13 @@ def write_json_schema(output_path: str, include_descriptions: bool = True) -> No def get_schema_stats() -> Dict[str, int]: """Get statistics about the generated schema.""" - from ..descriptions import get_description - schema = generate_json_schema(include_descriptions=False) props = schema.get("properties", {}) stats = { "total_params": len(props), "with_constraints": sum(1 for p in props.values() if "enum" in p or "minimum" in p or "maximum" in p), - "with_descriptions": sum(1 for name in REGISTRY.all_params if get_description(name)), + "with_descriptions": sum(1 for _, p in REGISTRY.all_params.items() if p.description), } return stats diff --git a/toolchain/mfc/params/namelist_parser.py b/toolchain/mfc/params/namelist_parser.py index 295df38412..311a0362fa 100644 --- a/toolchain/mfc/params/namelist_parser.py +++ b/toolchain/mfc/params/namelist_parser.py @@ -5,485 +5,71 @@ from each target's namelist definition. This ensures the Python toolchain stays in sync with what the Fortran code actually accepts. -When Fortran sources are unavailable (e.g. Homebrew installs), a built-in -fallback parameter set is used instead. +When Fortran sources are unavailable (e.g. Homebrew installs), the fallback +is computed from NAMELIST_VARS in definitions.py. """ import re from pathlib import Path from typing import Dict, Optional, Set -# Fallback parameters for when Fortran source files are not available. -# Generated from the namelist definitions in src/*/m_start_up.fpp. -# To regenerate: python3 toolchain/mfc/params/namelist_parser.py -_FALLBACK_PARAMS = { - "pre_process": { - "Bx0", - "Ca", - "R0ref", - "Re_inv", - "Web", - "a_x", - "a_y", - "a_z", - "adv_n", - "bc_x", - "bc_y", - "bc_z", - "bub_pp", - "bubbles_euler", - "bubbles_lagrange", - "case_dir", - "cfl_adap_dt", - "cfl_const_dt", - "cont_damage", - "cyl_coord", - "dist_type", - "down_sample", - "elliptic_smoothing", - "elliptic_smoothing_iters", - "fft_wrt", - "file_per_process", - "fluid_pp", - "fluid_rho", - "hyper_cleaning", - "hyperelasticity", - "hypoelasticity", - "ib", - "igr", - "igr_order", - "loops_x", - "loops_y", - "loops_z", - "m", - "mhd", - "mixlayer_perturb", - "mixlayer_perturb_k0", - "mixlayer_perturb_nk", - "mixlayer_vel_coef", - "mixlayer_vel_profile", - "model_eqns", - "mpp_lim", - "muscl_order", - "n", - "n_start", - "n_start_old", - "nb", - "num_bc_patches", - "num_fluids", - "num_ibs", - "num_patches", - "old_grid", - "old_ic", - "p", - "palpha_eps", - "parallel_io", - "patch_bc", - "patch_ib", - "patch_icpp", - "perturb_flow", - "perturb_flow_fluid", - "perturb_flow_mag", - "perturb_sph", - "perturb_sph_fluid", - "pi_fac", - "poly_sigma", - "polydisperse", - "polytropic", - "pre_stress", - "precision", - "pref", - "ptgalpha_eps", - "qbmm", - "recon_type", - "relativity", - "relax", - "relax_model", - "rhoRV", - "rhoref", - "sigR", - "sigV", - "sigma", - "simplex_params", - "simplex_perturb", - "stretch_x", - "stretch_y", - "stretch_z", - "surface_tension", - "t_step_old", - "t_step_start", - "thermal", - "viscous", - "weno_order", - "x_a", - "x_b", - "x_domain", - "y_a", - "y_b", - "y_domain", - "z_a", - "z_b", - "z_domain", - }, - "simulation": { - "Bx0", - "Ca", - "R0ref", - "Re_inv", - "Web", - "acoustic", - "acoustic_source", - "adap_dt", - "adap_dt_max_iters", - "adap_dt_tol", - "adv_n", - "alf_factor", - "alpha_bar", - "alt_soundspeed", - "avg_state", - "bc_x", - "bc_y", - "bc_z", - "bf_x", - "bf_y", - "bf_z", - "bub_pp", - "bubble_model", - "bubbles_euler", - "bubbles_lagrange", - "case_dir", - "cfl_adap_dt", - "cfl_const_dt", - "cfl_target", - "chem_params", - "cont_damage", - "cont_damage_s", - "cyl_coord", - "down_sample", - "dt", - "fd_order", - "fft_wrt", - "file_per_process", - "fluid_pp", - "g_x", - "g_y", - "g_z", - "hyper_cleaning", - "hyper_cleaning_speed", - "hyper_cleaning_tau", - "hyperelasticity", - "hypoelasticity", - "ib", - "ib_state_wrt", - "ic_beta", - "ic_eps", - "igr", - "igr_iter_solver", - "igr_order", - "igr_pres_lim", - "int_comp", - "integral", - "integral_wrt", - "k_x", - "k_y", - "k_z", - "lag_params", - "low_Mach", - "m", - "mapped_weno", - "mhd", - "mixture_err", - "model_eqns", - "mp_weno", - "mpp_lim", - "muscl_lim", - "muscl_order", - "n", - "n_start", - "nb", - "null_weights", - "num_bc_patches", - "num_fluids", - "num_ibs", - "num_igr_iters", - "num_igr_warm_start_iters", - "num_integrals", - "num_probes", - "num_source", - "nv_uvm_igr_temps_on_gpu", - "nv_uvm_out_of_core", - "nv_uvm_pref_gpu", - "p", - "p_x", - "p_y", - "p_z", - "palpha_eps", - "parallel_io", - "patch_ib", - "pi_fac", - "poly_sigma", - "polydisperse", - "polytropic", - "precision", - "pref", - "prim_vars_wrt", - "probe", - "probe_wrt", - "ptgalpha_eps", - "qbmm", - "rdma_mpi", - "recon_type", - "relativity", - "relax", - "relax_model", - "rhoref", - "riemann_solver", - "run_time_info", - "sigma", - "surface_tension", - "t_save", - "t_step_old", - "t_step_print", - "t_step_save", - "t_step_start", - "t_step_stop", - "t_stop", - "tau_star", - "teno", - "teno_CT", - "thermal", - "time_stepper", - "viscous", - "w_x", - "w_y", - "w_z", - "wave_speeds", - "weno_Re_flux", - "weno_avg", - "weno_eps", - "weno_order", - "wenoz", - "wenoz_q", - "x_a", - "x_b", - "x_domain", - "y_a", - "y_b", - "y_domain", - "z_a", - "z_b", - "z_domain", - }, - "post_process": { - "Bx0", - "Ca", - "E_wrt", - "G", - "R0ref", - "Re_inv", - "Web", - "adv_n", - "alpha_rho_e_wrt", - "alpha_rho_wrt", - "alpha_wrt", - "alt_soundspeed", - "avg_state", - "bc_x", - "bc_y", - "bc_z", - "bub_pp", - "bubbles_euler", - "bubbles_lagrange", - "c_wrt", - "case_dir", - "cf_wrt", - "cfl_adap_dt", - "cfl_const_dt", - "cfl_target", - "chem_wrt_T", - "chem_wrt_Y", - "cons_vars_wrt", - "cont_damage", - "cyl_coord", - "down_sample", - "fd_order", - "fft_wrt", - "file_per_process", - "fluid_pp", - "flux_lim", - "flux_wrt", - "format", - "gamma_wrt", - "heat_ratio_wrt", - "hyper_cleaning", - "hyperelasticity", - "hypoelasticity", - "ib", - "ib_state_wrt", - "igr", - "igr_order", - "lag_betaC_wrt", - "lag_betaT_wrt", - "lag_db_wrt", - "lag_dphidt_wrt", - "lag_header", - "lag_id_wrt", - "lag_mg_wrt", - "lag_mv_wrt", - "lag_pos_prev_wrt", - "lag_pos_wrt", - "lag_pres_wrt", - "lag_r0_wrt", - "lag_rad_wrt", - "lag_rmax_wrt", - "lag_rmin_wrt", - "lag_rvel_wrt", - "lag_txt_wrt", - "lag_vel_wrt", - "liutex_wrt", - "m", - "mhd", - "mixture_err", - "model_eqns", - "mom_wrt", - "mpp_lim", - "muscl_order", - "n", - "n_start", - "nb", - "num_bc_patches", - "num_fluids", - "num_ibs", - "omega_wrt", - "output_partial_domain", - "p", - "parallel_io", - "pi_inf_wrt", - "poly_sigma", - "polydisperse", - "polytropic", - "precision", - "pref", - "pres_inf_wrt", - "pres_wrt", - "prim_vars_wrt", - "qbmm", - "qm_wrt", - "recon_type", - "relativity", - "relax", - "relax_model", - "rho_wrt", - "rhoref", - "schlieren_alpha", - "schlieren_wrt", - "sigR", - "sigma", - "sim_data", - "surface_tension", - "t_save", - "t_step_save", - "t_step_start", - "t_step_stop", - "t_stop", - "thermal", - "vel_wrt", - "weno_order", - "x_output", - "y_output", - "z_output", - }, -} +def _fallback_params() -> Dict[str, Set[str]]: + # Lazy import avoids circular dependency (definitions -> namelist_parser). + from .definitions import NAMELIST_VARS + from .generators.fortran_gen import TARGETS -def parse_namelist_from_file(filepath: Path) -> Set[str]: - """ - Parse a Fortran file and extract parameter names from the namelist definition. + return {full: {v for v, ts in NAMELIST_VARS.items() if short in ts} for short, full in TARGETS} - Args: - filepath: Path to the Fortran source file (m_start_up.fpp) - - Returns: - Set of parameter names found in the namelist - """ - content = filepath.read_text() - # Handle #:include 'generated_namelist.fpp' — generate content in-memory. - if re.search(r"#:include\s+'generated_namelist\.fpp'", content): - _target_map = {"pre_process": "pre", "simulation": "sim", "post_process": "post"} - target_name = filepath.parent.name - short = _target_map.get(target_name) - if short is None: - raise ValueError(f"Cannot determine MFC target from path: {filepath}") - from .generators.fortran_gen import generate_namelist_fpp - - content = generate_namelist_fpp(short) +def parse_namelist_from_file(filepath: Path) -> Set[str]: + """Parse parameter names from the namelist /user_inputs/ block in an fpp file.""" + from .generators.fortran_gen import resolve_namelist_content - # Find the namelist block - starts with "namelist /user_inputs/" - # and continues until a line without continuation (&), a blank line, or end-of-string - namelist_match = re.search(r"namelist\s+/user_inputs/\s*(.+?)(?=\n\s*\n|\n\s*!(?!\s*&)|\n\s*[a-zA-Z_]+\s*=|$)", content, re.DOTALL | re.IGNORECASE) + content = resolve_namelist_content(filepath) + namelist_match = re.search( + r"namelist\s+/user_inputs/\s*(.+?)(?=\n\s*\n|\n\s*!(?!\s*&)|\n\s*[a-zA-Z_]+\s*=|$)", + content, + re.DOTALL | re.IGNORECASE, + ) if not namelist_match: raise ValueError(f"Could not find namelist /user_inputs/ in {filepath}") namelist_text = namelist_match.group(1) - - # Remove Fortran line continuations (&) and join lines namelist_text = re.sub(r"&\s*\n\s*", " ", namelist_text) - - # Remove preprocessor directives (#:if, #:endif, etc.) namelist_text = re.sub(r"#:.*", "", namelist_text) - - # Remove comments (! to end of line, but not inside strings) namelist_text = re.sub(r"!.*", "", namelist_text) - # Extract parameter names - they're comma-separated identifiers - # Parameter names are alphanumeric with underscores found_params = set() for match in re.finditer(r"\b([a-zA-Z_][a-zA-Z0-9_]*)\b", namelist_text): name = match.group(1) - # Skip Fortran keywords that might appear if name.lower() not in {"namelist", "user_inputs", "if", "endif", "not"}: found_params.add(name) - return found_params def parse_all_namelists(mfc_root: Path) -> Dict[str, Set[str]]: - """ - Parse namelist definitions from all MFC targets. + """Parse namelist definitions from all MFC targets. - Args: - mfc_root: Path to MFC root directory - - Returns: - Dict mapping target name to set of valid parameter names. - Falls back to built-in parameter sets when sources are unavailable. + Falls back to NAMELIST_VARS when Fortran sources are unavailable. """ - targets = { - "pre_process": mfc_root / "src" / "pre_process" / "m_start_up.fpp", - "simulation": mfc_root / "src" / "simulation" / "m_start_up.fpp", - "post_process": mfc_root / "src" / "post_process" / "m_start_up.fpp", + src = mfc_root / "src" + target_files = { + "pre_process": src / "pre_process" / "m_start_up.fpp", + "simulation": src / "simulation" / "m_start_up.fpp", + "post_process": src / "post_process" / "m_start_up.fpp", } - result = {} - for target_name, filepath in targets.items(): + for filepath in target_files.values(): if not filepath.exists(): - # Source files not available (e.g. Homebrew install). - # Use built-in fallback parameters. - return dict(_FALLBACK_PARAMS) - result[target_name] = parse_namelist_from_file(filepath) + return _fallback_params() - return result + return {name: parse_namelist_from_file(path) for name, path in target_files.items()} def parse_fortran_constants(filepath: Path) -> Dict[str, int]: - """ - Parse integer parameter constants from a Fortran source file. - - Extracts lines like ``integer, parameter :: name = 123`` and returns - a dict mapping constant names to their integer values. - """ + """Parse integer parameter constants from a Fortran source file.""" constants: Dict[str, int] = {} pattern = re.compile(r"integer\s*,\s*parameter\s*::\s*(\w+)\s*=\s*(\d+)", re.IGNORECASE) try: @@ -495,103 +81,61 @@ def parse_fortran_constants(filepath: Path) -> Dict[str, int]: return constants -# Module-level cache for Fortran constants (None = not yet loaded) _FORTRAN_CONSTANTS_CACHE: Optional[Dict[str, int]] = None def get_fortran_constants() -> Dict[str, int]: - """ - Get Fortran compile-time constants from m_constants.fpp. + """Get Fortran compile-time constants from m_constants.fpp. - Cached after first call. Returns an empty dict when the Fortran source is - unavailable (e.g. Homebrew installs where src/ is not shipped); callers - supply their own inline defaults via _fc(name, default) in definitions.py. + Cached after first call. Returns an empty dict when src/ is unavailable; + callers supply inline defaults via _fc(name, default) in definitions.py. """ global _FORTRAN_CONSTANTS_CACHE # noqa: PLW0603 if _FORTRAN_CONSTANTS_CACHE is None: - root = get_mfc_root() - path = root / "src" / "common" / "m_constants.fpp" + path = get_mfc_root() / "src" / "common" / "m_constants.fpp" _FORTRAN_CONSTANTS_CACHE = parse_fortran_constants(path) return _FORTRAN_CONSTANTS_CACHE def get_mfc_root() -> Path: - """Get the MFC root directory from this file's location.""" - # This file is at toolchain/mfc/params/namelist_parser.py - # MFC root is 4 levels up + """Return the MFC root directory (4 levels above this file).""" return Path(__file__).resolve().parent.parent.parent.parent -# Module-level cache for parsed target params _TARGET_PARAMS_CACHE: Dict[str, Set[str]] = {} def get_target_params() -> Dict[str, Set[str]]: - """ - Get the valid parameters for each target, parsing Fortran if needed. - - Returns: - Dict mapping target name to set of valid parameter names - """ + """Return valid parameters per target, parsing Fortran sources if needed.""" if not _TARGET_PARAMS_CACHE: _TARGET_PARAMS_CACHE.update(parse_all_namelists(get_mfc_root())) return _TARGET_PARAMS_CACHE def is_param_valid_for_target(param_name: str, target_name: str) -> bool: - """ - Check if a parameter is valid for a given target. - - This handles both scalar params (like "m") and indexed params - (like "patch_icpp(1)%geometry") by checking the base name. + """Return True if param_name is valid for target_name. - Args: - param_name: The parameter name (may include indices like "(1)%attr") - target_name: One of 'pre_process', 'simulation', 'post_process' - - Returns: - True if the parameter is valid for the target + Handles indexed params (patch_icpp(1)%geometry) by checking the base name. """ valid_params = get_target_params().get(target_name, set()) - - # Extract base parameter name (before any index or attribute) - # e.g., "patch_icpp(1)%geometry" -> "patch_icpp" - # e.g., "fluid_pp(2)%gamma" -> "fluid_pp" base_match = re.match(r"^([a-zA-Z_][a-zA-Z0-9_]*)", param_name) - if base_match: - return base_match.group(1) in valid_params - - return param_name in valid_params + return base_match.group(1) in valid_params if base_match else param_name in valid_params if __name__ == "__main__": - # Test the parser import sys try: parsed_targets = parse_all_namelists(get_mfc_root()) - print("Parsed namelist parameters:\n") for tgt, tgt_params in sorted(parsed_targets.items()): print(f"{tgt}: {len(tgt_params)} parameters") - # Print first 10 as sample - sorted_list = sorted(tgt_params) - for param in sorted_list[:10]: + for param in sorted(tgt_params)[:10]: print(f" - {param}") if len(tgt_params) > 10: print(f" ... and {len(tgt_params) - 10} more") print() - # Show params unique to each target - print("Parameters unique to each target:\n") - all_param_names = set.union(*parsed_targets.values()) - for tgt, tgt_params in sorted(parsed_targets.items()): - other = set.union(*[p for t, p in parsed_targets.items() if t != tgt]) - unique = tgt_params - other - print(f"{tgt} only ({len(unique)}): {sorted(unique)[:15]}...") - print() - - # Show params in all targets common = set.intersection(*parsed_targets.values()) print(f"Parameters in ALL targets ({len(common)}): {sorted(common)[:20]}...") diff --git a/toolchain/mfc/params/namelist_targets.py b/toolchain/mfc/params/namelist_targets.py deleted file mode 100644 index 9593398013..0000000000 --- a/toolchain/mfc/params/namelist_targets.py +++ /dev/null @@ -1,315 +0,0 @@ -""" -Namelist target mapping for Fortran codegen. - -NAMELIST_VARS maps each Fortran namelist variable (struct root or simple scalar) -to the set of MFC executables whose namelist it appears in. - -CASE_OPT_EXCLUDE is the set of simulation namelist variables excluded under -MFC_CASE_OPTIMIZATION (they become compile-time constants instead). - -When adding a new parameter: - 1. Add to definitions.py (type, constraints, etc.) - 2. Add the namelist root variable to NAMELIST_VARS with its target set - 3. Run ./mfc.sh generate to regenerate the .fpp files -""" - -from typing import Dict, Set - -# All three targets -_ALL = {"pre", "sim", "post"} -_PRE_SIM = {"pre", "sim"} -_SIM_POST = {"sim", "post"} - -NAMELIST_VARS: Dict[str, Set[str]] = { - # --- Grid (all targets) --- - "m": _ALL, - "n": _ALL, - "p": _ALL, - "cyl_coord": _ALL, - "x_domain": {"pre", "sim"}, - "y_domain": {"pre", "sim"}, - "z_domain": {"pre", "sim"}, - "x_output": {"post"}, - "y_output": {"post"}, - "z_output": {"post"}, - # --- Grid stretching (pre only) --- - "stretch_x": {"pre"}, - "stretch_y": {"pre"}, - "stretch_z": {"pre"}, - "a_x": {"pre"}, - "a_y": {"pre"}, - "a_z": {"pre"}, - "x_a": {"pre", "sim"}, - "y_a": {"pre", "sim"}, - "z_a": {"pre", "sim"}, - "x_b": {"pre", "sim"}, - "y_b": {"pre", "sim"}, - "z_b": {"pre", "sim"}, - "loops_x": {"pre"}, - "loops_y": {"pre"}, - "loops_z": {"pre"}, - # --- Time --- - "dt": {"sim"}, - "t_step_start": _ALL, - "t_step_stop": _SIM_POST, - "t_step_save": _SIM_POST, - "t_step_print": {"sim"}, - "t_step_old": {"pre", "sim"}, - "time_stepper": {"sim"}, - "t_stop": _SIM_POST, - "t_save": _SIM_POST, - "cfl_target": _SIM_POST, - "cfl_adap_dt": _ALL, - "cfl_const_dt": _ALL, - "n_start": _ALL, - "n_start_old": {"pre"}, - "adap_dt": {"sim"}, - "adap_dt_tol": {"sim"}, - "adap_dt_max_iters": {"sim"}, - # --- Physics model --- - "model_eqns": _ALL, - "num_fluids": {"pre", "post"}, - "mpp_lim": _ALL, - "relax": _ALL, - "relax_model": _ALL, - "palpha_eps": _PRE_SIM, - "ptgalpha_eps": _PRE_SIM, - # --- WENO / reconstruction --- - "weno_order": {"pre", "post"}, - "weno_eps": {"sim"}, - "teno_CT": {"sim"}, - "wenoz_q": {"sim"}, - "mp_weno": {"sim"}, - "weno_avg": {"sim"}, - "weno_Re_flux": {"sim"}, - "null_weights": {"sim"}, - "muscl_eps": {"sim"}, - "recon_type": {"pre", "post"}, - "muscl_order": {"pre", "post"}, - "int_comp": {"sim"}, - "ic_eps": {"sim"}, - "ic_beta": {"sim"}, - # --- Riemann solver --- - "riemann_solver": {"sim"}, - "wave_speeds": {"sim"}, - "avg_state": _SIM_POST, - "low_Mach": {"sim"}, - # --- MHD --- - "mhd": {"pre", "post"}, - "hyper_cleaning": _ALL, - "hyper_cleaning_speed": {"sim"}, - "hyper_cleaning_tau": {"sim"}, - "Bx0": _ALL, - # --- BCs --- - "bc_x": _ALL, - "bc_y": _ALL, - "bc_z": _ALL, - "num_bc_patches": _ALL, - "patch_bc": {"pre"}, - # --- ICs (pre only) --- - "num_patches": {"pre"}, - "patch_icpp": {"pre"}, - # --- Fluid properties --- - "fluid_pp": _ALL, - "bub_pp": _ALL, - "rhoref": _ALL, - "pref": _ALL, - # --- Bubbles --- - "bubbles_euler": _ALL, - "bubbles_lagrange": _ALL, - "R0ref": _ALL, - "nb": {"pre", "post"}, - "polytropic": _ALL, - "thermal": _ALL, - "Ca": _ALL, - "Web": _ALL, - "Re_inv": _ALL, - "polydisperse": _ALL, - "poly_sigma": _ALL, - "qbmm": _ALL, - "sigma": _ALL, - "adv_n": _ALL, - "bubble_model": {"sim"}, - "sigR": {"pre", "post"}, - "sigV": {"pre"}, - "dist_type": {"pre"}, - "rhoRV": {"pre"}, - "lag_params": {"sim"}, - # --- Lagrangian output (post) --- - "lag_header": {"post"}, - "lag_txt_wrt": {"post"}, - "lag_db_wrt": {"post"}, - "lag_id_wrt": {"post"}, - "lag_pos_wrt": {"post"}, - "lag_pos_prev_wrt": {"post"}, - "lag_vel_wrt": {"post"}, - "lag_rad_wrt": {"post"}, - "lag_rvel_wrt": {"post"}, - "lag_r0_wrt": {"post"}, - "lag_rmax_wrt": {"post"}, - "lag_rmin_wrt": {"post"}, - "lag_dphidt_wrt": {"post"}, - "lag_pres_wrt": {"post"}, - "lag_mv_wrt": {"post"}, - "lag_mg_wrt": {"post"}, - "lag_betaT_wrt": {"post"}, - "lag_betaC_wrt": {"post"}, - # --- Elasticity --- - "hypoelasticity": _ALL, - "hyperelasticity": _ALL, - # --- Surface tension --- - "surface_tension": _ALL, - # --- Relativity --- - "relativity": _ALL, - # --- Immersed boundaries --- - "ib": _ALL, - "num_ibs": _ALL, - "patch_ib": {"pre", "sim"}, - "collision_model": {"sim"}, - "coefficient_of_restitution": {"sim"}, - "collision_time": {"sim"}, - "ib_coefficient_of_friction": {"sim"}, - "ib_state_wrt": _SIM_POST, - # --- Continuum damage --- - "cont_damage": _ALL, - "tau_star": {"sim"}, - "cont_damage_s": {"sim"}, - "alpha_bar": {"sim"}, - # --- IGR --- - "igr": {"pre", "post"}, - "igr_order": {"pre", "post"}, - "down_sample": _ALL, - # --- Probes (sim) --- - "probe_wrt": {"sim"}, - "num_probes": {"sim"}, - "probe": {"sim"}, - "integral_wrt": {"sim"}, - "num_integrals": {"sim"}, - "integral": {"sim"}, - "fd_order": _SIM_POST, - # --- Acoustic sources (sim) --- - "acoustic_source": {"sim"}, - "num_source": {"sim"}, - "acoustic": {"sim"}, - # --- Chemistry --- - "chem_params": {"sim"}, - # --- Body forces (sim) --- - "bf_x": {"sim"}, - "bf_y": {"sim"}, - "bf_z": {"sim"}, - "k_x": {"sim"}, - "k_y": {"sim"}, - "k_z": {"sim"}, - "w_x": {"sim"}, - "w_y": {"sim"}, - "w_z": {"sim"}, - "p_x": {"sim"}, - "p_y": {"sim"}, - "p_z": {"sim"}, - "g_x": {"sim"}, - "g_y": {"sim"}, - "g_z": {"sim"}, - # --- Viscous (pre) --- - "viscous": {"pre"}, - # --- Output --- - "precision": _ALL, - "parallel_io": _ALL, - "file_per_process": _ALL, - "prim_vars_wrt": _SIM_POST, - "cons_vars_wrt": {"post"}, - "run_time_info": {"sim"}, - "fft_wrt": _ALL, - "pi_fac": {"pre", "sim"}, - # --- Post-process output --- - "format": {"post"}, - "output_partial_domain": {"post"}, - "rho_wrt": {"post"}, - "E_wrt": {"post"}, - "pres_wrt": {"post"}, - "c_wrt": {"post"}, - "omega_wrt": {"post"}, - "qm_wrt": {"post"}, - "liutex_wrt": {"post"}, - "schlieren_wrt": {"post"}, - "schlieren_alpha": {"post"}, - "gamma_wrt": {"post"}, - "heat_ratio_wrt": {"post"}, - "pi_inf_wrt": {"post"}, - "pres_inf_wrt": {"post"}, - "alpha_rho_wrt": {"post"}, - "mom_wrt": {"post"}, - "vel_wrt": {"post"}, - "flux_wrt": {"post"}, - "alpha_wrt": {"post"}, - "cf_wrt": {"post"}, - "chem_wrt_T": {"post"}, - "chem_wrt_Y": {"post"}, - "alt_soundspeed": _SIM_POST, - "mixture_err": _SIM_POST, - "flux_lim": {"post"}, - "sim_data": {"post"}, - "alpha_rho_e_wrt": {"post"}, - "G": {"post"}, - # --- Pre-process IC perturbations --- - "perturb_flow": {"pre"}, - "perturb_flow_fluid": {"pre"}, - "perturb_flow_mag": {"pre"}, - "perturb_sph": {"pre"}, - "perturb_sph_fluid": {"pre"}, - "fluid_rho": {"pre"}, - "mixlayer_vel_profile": {"pre"}, - "mixlayer_vel_coef": {"pre"}, - "mixlayer_perturb": {"pre"}, - "mixlayer_perturb_nk": {"pre"}, - "mixlayer_perturb_k0": {"pre"}, - "pre_stress": {"pre"}, - "elliptic_smoothing": {"pre"}, - "elliptic_smoothing_iters": {"pre"}, - "simplex_perturb": {"pre"}, - "simplex_params": {"pre"}, - # --- Pre-process restart --- - "old_grid": {"pre"}, - "old_ic": {"pre"}, - # --- Sim-specific physics --- - "rdma_mpi": {"sim"}, - "alf_factor": {"sim"}, - "num_igr_iters": {"sim"}, - "num_igr_warm_start_iters": {"sim"}, - "igr_iter_solver": {"sim"}, - "igr_pres_lim": {"sim"}, - "nv_uvm_out_of_core": {"sim"}, - "nv_uvm_igr_temps_on_gpu": {"sim"}, - "nv_uvm_pref_gpu": {"sim"}, - # --- Logistics --- - "case_dir": _ALL, -} - -# Variables excluded from the sim namelist when MFC_CASE_OPTIMIZATION is active -# (they become compile-time integer/logical parameters instead). -CASE_OPT_EXCLUDE: Set[str] = { - "nb", - "mapped_weno", - "wenoz", - "teno", - "wenoz_q", - "weno_order", - "num_fluids", - "mhd", - "relativity", - "igr_order", - "viscous", - "igr_iter_solver", - "igr", - "igr_pres_lim", - "recon_type", - "muscl_order", - "muscl_lim", -} - -# Add CASE_OPT_EXCLUDE vars to NAMELIST_VARS for sim target -# (they appear in the namelist when NOT using case optimization) -for _v in CASE_OPT_EXCLUDE: - if _v not in NAMELIST_VARS: - NAMELIST_VARS[_v] = {"sim"} - else: - NAMELIST_VARS[_v].add("sim") diff --git a/toolchain/mfc/params_cmd.py b/toolchain/mfc/params_cmd.py index 577dfeeb55..e40fdfc874 100644 --- a/toolchain/mfc/params_cmd.py +++ b/toolchain/mfc/params_cmd.py @@ -304,8 +304,6 @@ def _show_families(registry, limit): def _search_params(registry, query, type_filter, limit, describe=False, search_descriptions=True): """Search for parameters matching a query.""" - from .params.descriptions import get_description - query_lower = query.lower() matches = [] desc_matches = set() # Track which params matched via description @@ -315,9 +313,7 @@ def _search_params(registry, query, type_filter, limit, describe=False, search_d desc_match = False if search_descriptions and not name_match: - # Also search in description - desc = get_description(name) - if desc and query_lower in desc.lower(): + if param.description and query_lower in param.description.lower(): desc_match = True desc_matches.add(name) @@ -352,7 +348,7 @@ def _search_params(registry, query, type_filter, limit, describe=False, search_d def _show_collapsed_results(collapsed, describe=False): """Show collapsed search results.""" - from .params.descriptions import get_description, get_pattern_description + from .params.descriptions import get_pattern_description # Check if any items have index ranges to show has_ranges = any(len(item) == 4 and item[2] > 1 for item in collapsed) @@ -365,11 +361,10 @@ def _show_collapsed_results(collapsed, describe=False): count = item[2] range_str = item[3] if len(item) == 4 else "" - # Get description - use pattern description for indexed params if "(N)" in name or "(M)" in name: desc = get_pattern_description(name) else: - desc = get_description(name) + desc = param.description cons.print(f" [cyan]{name}[/cyan]") cons.print(f" Type: {param.param_type.name}") diff --git a/toolchain/tests/params/test_fortran_gen.py b/toolchain/mfc/params_tests/test_fortran_gen.py similarity index 92% rename from toolchain/tests/params/test_fortran_gen.py rename to toolchain/mfc/params_tests/test_fortran_gen.py index d8ee6dfb3e..cd43055c65 100644 --- a/toolchain/tests/params/test_fortran_gen.py +++ b/toolchain/mfc/params_tests/test_fortran_gen.py @@ -120,8 +120,9 @@ def test_get_generated_files_returns_six(): from mfc.params.generators.fortran_gen import get_generated_files - files = get_generated_files(Path("/tmp")) + files = get_generated_files(Path("/build")) assert len(files) == 6 - names = {p.name for p, _ in files} - assert "generated_namelist_pre.fpp" in names - assert "generated_decls_sim.fpp" in names + paths = [str(p) for p, _ in files] + assert any("pre_process/generated_namelist.fpp" in p for p in paths) + assert any("simulation/generated_decls.fpp" in p for p in paths) + assert any("post_process/generated_namelist.fpp" in p for p in paths) diff --git a/toolchain/tests/params/test_namelist_targets.py b/toolchain/mfc/params_tests/test_namelist_targets.py similarity index 68% rename from toolchain/tests/params/test_namelist_targets.py rename to toolchain/mfc/params_tests/test_namelist_targets.py index 4d21a3e1a8..0b4fd90b5b 100644 --- a/toolchain/tests/params/test_namelist_targets.py +++ b/toolchain/mfc/params_tests/test_namelist_targets.py @@ -1,12 +1,12 @@ def test_common_vars_in_all_targets(): - from mfc.params.namelist_targets import NAMELIST_VARS + from mfc.params.definitions import NAMELIST_VARS for var in ["m", "n", "p", "bc_x", "bc_y", "bc_z", "model_eqns", "cyl_coord", "fluid_pp", "case_dir"]: assert {"pre", "sim", "post"}.issubset(NAMELIST_VARS.get(var, set())), f"{var!r} not marked for all targets" def test_sim_only_vars(): - from mfc.params.namelist_targets import NAMELIST_VARS + from mfc.params.definitions import NAMELIST_VARS for var in ["run_time_info", "dt", "riemann_solver", "acoustic", "probe"]: targets = NAMELIST_VARS.get(var, set()) @@ -16,7 +16,7 @@ def test_sim_only_vars(): def test_pre_only_vars(): - from mfc.params.namelist_targets import NAMELIST_VARS + from mfc.params.definitions import NAMELIST_VARS for var in ["old_grid", "old_ic", "patch_icpp", "simplex_params"]: targets = NAMELIST_VARS.get(var, set()) @@ -25,7 +25,7 @@ def test_pre_only_vars(): def test_post_only_vars(): - from mfc.params.namelist_targets import NAMELIST_VARS + from mfc.params.definitions import NAMELIST_VARS for var in ["format", "sim_data", "lag_header", "output_partial_domain"]: targets = NAMELIST_VARS.get(var, set()) @@ -33,16 +33,16 @@ def test_post_only_vars(): assert "sim" not in targets, f"{var!r} incorrectly in sim" -def test_case_opt_exclude_vars(): - from mfc.params.namelist_targets import CASE_OPT_EXCLUDE +def test_case_opt_params_in_namelist(): + from mfc.params.definitions import CASE_OPT_PARAMS for var in ["nb", "mapped_weno", "wenoz", "weno_order", "num_fluids"]: - assert var in CASE_OPT_EXCLUDE + assert var in CASE_OPT_PARAMS -def test_case_opt_exclude_vars_also_in_sim_namelist(): - from mfc.params.namelist_targets import CASE_OPT_EXCLUDE, NAMELIST_VARS +def test_case_opt_params_in_sim_namelist(): + from mfc.params.definitions import CASE_OPT_PARAMS, NAMELIST_VARS - for var in CASE_OPT_EXCLUDE: + for var in CASE_OPT_PARAMS: targets = NAMELIST_VARS.get(var, set()) - assert "sim" in targets, f"CASE_OPT_EXCLUDE var {var!r} must also be in sim namelist" + assert "sim" in targets, f"CASE_OPT_PARAMS var {var!r} must also be in sim namelist" diff --git a/toolchain/tests/params/test_schema.py b/toolchain/mfc/params_tests/test_schema.py similarity index 100% rename from toolchain/tests/params/test_schema.py rename to toolchain/mfc/params_tests/test_schema.py diff --git a/toolchain/mfc/run/case_dicts.py b/toolchain/mfc/run/case_dicts.py index 7100e9e7dd..ac8438833c 100644 --- a/toolchain/mfc/run/case_dicts.py +++ b/toolchain/mfc/run/case_dicts.py @@ -1,8 +1,7 @@ """ MFC Case Parameter Type Definitions. -This module provides exports from the central parameter registry (mfc.params). -All parameter definitions are sourced from the registry. +Exports from the central parameter registry (mfc.params). Exports: ALL: Family-aware mapping of all parameters {name: ParamType} @@ -20,14 +19,10 @@ class _ParamTypeMapping(Mapping): - """ - Read-only mapping wrapping REGISTRY's all_params for {name: ParamType} access. - - Delegates containment checks and lookup to the registry's family-aware - mapping, so indexed families like ``patch_ib(500000)%geometry`` resolve - in O(1) without enumerating all possible indices. + """Read-only {name: ParamType} view over REGISTRY.all_params. - For iteration, yields scalar params plus one example per family attr. + Delegates containment and lookup to the registry's family-aware mapping, + so indexed families like ``patch_ib(500000)%geometry`` resolve in O(1). """ def __init__(self): @@ -48,115 +43,55 @@ def __len__(self): return len(self._view) -def _load_case_optimization_params(): - """Get params that can be hard-coded for GPU optimization.""" - from ..params import REGISTRY - - return [name for name, param in REGISTRY.all_params.items() if param.case_optimization] - - -def _build_schema(): - """Build JSON schema from registry.""" - from ..params import REGISTRY - - return REGISTRY.get_json_schema() - - -def _get_validator_func(): - """Get the cached validator from registry.""" +def _registry(): from ..params import REGISTRY - return REGISTRY.get_validator() - - -def _get_target_params(): - """Get valid params for each target by parsing Fortran namelists.""" - from ..params.namelist_parser import get_target_params - - return get_target_params() + return REGISTRY -# Parameters to ignore during certain operations IGNORE = ["cantera_file", "chemistry"] - -# Family-aware mapping of all parameters — supports O(1) lookup for indexed families ALL = _ParamTypeMapping() +CASE_OPTIMIZATION = [n for n, p in _registry().all_params.items() if p.case_optimization] +SCHEMA = _registry().get_json_schema() -# Parameters that can be hard-coded for GPU case optimization -CASE_OPTIMIZATION = _load_case_optimization_params() - -# JSON schema for validation -SCHEMA = _build_schema() - -# Regex to extract the base name from indexed params _BASE_NAME_RE = re.compile(r"^([a-zA-Z_][a-zA-Z0-9_]*)") def _is_param_valid_for_target(param_name: str, target_name: str) -> bool: - """ - Check if a parameter is valid for a given target. - - Uses the Fortran namelist definitions as the source of truth. - Handles indexed params like "patch_icpp(1)%geometry" by checking base name. - Args: - param_name: The parameter name (may include indices) - target_name: One of 'pre_process', 'simulation', 'post_process' - - Returns: - True if the parameter is valid for the target - """ - target_params = _get_target_params().get(target_name, set()) + from ..params.namelist_parser import get_target_params - # Extract base parameter name (before any index or attribute) - # e.g., "patch_icpp(1)%geometry" -> "patch_icpp" - # e.g., "fluid_pp(2)%gamma" -> "fluid_pp" - # e.g., "acoustic(1)%loc(1)" -> "acoustic" + target_params = get_target_params().get(target_name, set()) match = _BASE_NAME_RE.match(param_name) - if match: - base_name = match.group(1) - return base_name in target_params - - return param_name in target_params + return match.group(1) in target_params if match else param_name in target_params class _TargetKeySet: - """ - Set-like object for checking if a param is valid for a specific target. + """Set-like object for checking param validity for a specific target. - Supports ``key in target_key_set`` via base-name matching against the - Fortran namelist, plus optionally filtering out case-optimization params. - Does not enumerate all possible indexed family members. + Supports ``key in obj`` via base-name matching against the Fortran namelist, + optionally filtering out case-optimization params. """ def __init__(self, target_name: str, filter_case_opt: bool = False): self._target_name = target_name - self._filter_case_opt = filter_case_opt self._case_opt = set(CASE_OPTIMIZATION) if filter_case_opt else set() def __contains__(self, key): - if self._filter_case_opt and key in self._case_opt: + if key in self._case_opt: return False return _is_param_valid_for_target(key, self._target_name) def get_input_dict_keys(target_name: str): - """ - Get a set-like object for checking parameter validity for a target. - - Returns an object that supports ``key in result`` for O(1) checks. - For indexed families, this does NOT enumerate all possible indices — - it checks the base name against the Fortran namelist. - - Args: - target_name: One of 'pre_process', 'simulation', 'post_process' + """Return a set-like object for checking parameter validity for a target. - Returns: - Set-like object supporting ``in`` operator + Supports ``key in result`` for O(1) checks. Does NOT enumerate indexed family + members — checks the base name against the Fortran namelist. """ filter_case_opt = ARG("case_optimization", dflt=False) and target_name == "simulation" return _TargetKeySet(target_name, filter_case_opt) def get_validator(): - """Get the cached JSON schema validator.""" - return _get_validator_func() + """Return the cached JSON schema validator.""" + return _registry().get_validator() diff --git a/toolchain/pyproject.toml b/toolchain/pyproject.toml index 4f2dd2554e..b593598c9c 100644 --- a/toolchain/pyproject.toml +++ b/toolchain/pyproject.toml @@ -26,6 +26,7 @@ dependencies = [ "ruff", "ffmt==0.4.1", "ansi2txt", + "pytest", # Profiling "numpy", @@ -57,6 +58,10 @@ dependencies = [ "tqdm", ] +[tool.pytest.ini_options] +pythonpath = ["."] +testpaths = ["."] + [tool.hatch.metadata] allow-direct-references = true From 373a49610044d3a8e8527491b54e4ed62a13386c Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Thu, 28 May 2026 01:39:52 -0400 Subject: [PATCH 13/33] refactor: fix four accumulated code quality issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - run/run.py: replace 4 duplicate profiler if-blocks with data-driven table; adding a new profiler now requires one entry in _PROFILERS - lint_source.py: remove check_pylint_directives — MFC uses ruff, no pylint directives exist anywhere in the codebase, check was provably dead code - bench.py: delete self-deprecating TODO comment - run/input.py: log Cantera candidate failures instead of swallowing silently; previously a Cantera yaml parse error looked identical to file-not-found --- toolchain/mfc/bench.py | 2 - toolchain/mfc/lint_source.py | 28 -- toolchain/mfc/params/definitions.py | 295 ++++++++++++++---- .../mfc/params/generators/fortran_gen.py | 12 +- toolchain/mfc/run/input.py | 3 +- toolchain/mfc/run/run.py | 35 +-- 6 files changed, 257 insertions(+), 118 deletions(-) diff --git a/toolchain/mfc/bench.py b/toolchain/mfc/bench.py index a6a04e6f8e..f697e3d6da 100644 --- a/toolchain/mfc/bench.py +++ b/toolchain/mfc/bench.py @@ -166,8 +166,6 @@ def bench(targets=None): cons.unindent() -# TODO: This function is too long and not nicely written at all. Someone should -# refactor it... def diff(): lhs, rhs = file_load_yaml(ARG("lhs")), file_load_yaml(ARG("rhs")) lhs_path = os.path.relpath(ARG("lhs")) diff --git a/toolchain/mfc/lint_source.py b/toolchain/mfc/lint_source.py index d26c987465..7800bd986e 100644 --- a/toolchain/mfc/lint_source.py +++ b/toolchain/mfc/lint_source.py @@ -312,33 +312,6 @@ def check_junk_comments(repo_root: Path) -> list[str]: return errors -def check_pylint_directives(repo_root: Path) -> list[str]: - """Flag ``# pylint:`` directives in Python files. - - MFC uses ruff for linting; leftover pylint directives are dead code. - """ - errors: list[str] = [] - pylint_re = re.compile(r"#\s*pylint\s*:", re.IGNORECASE) - self_path = Path(__file__).resolve() - - for subdir in ["examples", "benchmarks", "toolchain"]: - d = repo_root / subdir - if not d.exists(): - continue - for py in sorted(d.rglob("*.py")): - if py.resolve() == self_path: - continue - lines = py.read_text(encoding="utf-8").splitlines() - rel = py.relative_to(repo_root) - - for i, line in enumerate(lines): - match = pylint_re.search(line) - if match: - errors.append(f" {rel}:{i + 1} pylint directive. Fix: remove (use ruff noqa comments if needed)") - - return errors - - def main(): repo_root = Path(__file__).resolve().parents[2] @@ -351,7 +324,6 @@ def main(): all_errors.extend(check_fypp_list_duplicates(repo_root)) all_errors.extend(check_duplicate_lines(repo_root)) all_errors.extend(check_hardcoded_byte_size(repo_root)) - all_errors.extend(check_pylint_directives(repo_root)) if all_errors: print("Source lint failed:") diff --git a/toolchain/mfc/params/definitions.py b/toolchain/mfc/params/definitions.py index 0cb042d3eb..d4112f30f2 100644 --- a/toolchain/mfc/params/definitions.py +++ b/toolchain/mfc/params/definitions.py @@ -39,7 +39,6 @@ def _fc(name: str, default: int) -> int: NA = 4 # acoustic sources: enumerated individually - # Parameters that can be hard-coded for GPU case optimization CASE_OPT_PARAMS = { "mapped_weno", @@ -496,6 +495,7 @@ def _r(name, ptype, tags=None, desc=None, hint=None, math=None, str_len=None): hint = _lookup_hint(name) if desc is None: from .descriptions import get_description + desc = get_description(name) constraint = CONSTRAINTS.get(name) if constraint and "value_labels" in constraint: @@ -1028,78 +1028,259 @@ def _init_registry(): NAMELIST_VARS: dict[str, set[str]] = {} + def _nv(targets: set, *names: str) -> None: for n in names: NAMELIST_VARS[n] = set(targets) + _ALL, _PRE_SIM, _SIM_POST = {"pre", "sim", "post"}, {"pre", "sim"}, {"sim", "post"} _PRE_POST = {"pre", "post"} _SIM = {"sim"} _PRE = {"pre"} _POST = {"post"} -_nv(_ALL, - "m", "n", "p", "cyl_coord", "bc_x", "bc_y", "bc_z", "num_bc_patches", "case_dir", - "t_step_start", "cfl_adap_dt", "cfl_const_dt", "n_start", - "model_eqns", "mpp_lim", "relax", "relax_model", - "fluid_pp", "bub_pp", "rhoref", "pref", - "bubbles_euler", "bubbles_lagrange", "R0ref", "polytropic", "thermal", - "Ca", "Web", "Re_inv", "polydisperse", "poly_sigma", "qbmm", "sigma", "adv_n", - "hypoelasticity", "hyperelasticity", "surface_tension", "relativity", - "ib", "num_ibs", "cont_damage", "hyper_cleaning", "Bx0", - "precision", "parallel_io", "file_per_process", "fft_wrt", "down_sample", +_nv( + _ALL, + "m", + "n", + "p", + "cyl_coord", + "bc_x", + "bc_y", + "bc_z", + "num_bc_patches", + "case_dir", + "t_step_start", + "cfl_adap_dt", + "cfl_const_dt", + "n_start", + "model_eqns", + "mpp_lim", + "relax", + "relax_model", + "fluid_pp", + "bub_pp", + "rhoref", + "pref", + "bubbles_euler", + "bubbles_lagrange", + "R0ref", + "polytropic", + "thermal", + "Ca", + "Web", + "Re_inv", + "polydisperse", + "poly_sigma", + "qbmm", + "sigma", + "adv_n", + "hypoelasticity", + "hyperelasticity", + "surface_tension", + "relativity", + "ib", + "num_ibs", + "cont_damage", + "hyper_cleaning", + "Bx0", + "precision", + "parallel_io", + "file_per_process", + "fft_wrt", + "down_sample", ) -_nv(_SIM_POST, - "t_step_stop", "t_step_save", "t_stop", "t_save", "cfl_target", - "avg_state", "prim_vars_wrt", "alt_soundspeed", "mixture_err", "fd_order", "ib_state_wrt", +_nv( + _SIM_POST, + "t_step_stop", + "t_step_save", + "t_stop", + "t_save", + "cfl_target", + "avg_state", + "prim_vars_wrt", + "alt_soundspeed", + "mixture_err", + "fd_order", + "ib_state_wrt", ) -_nv(_PRE_SIM, - "x_domain", "y_domain", "z_domain", - "x_a", "y_a", "z_a", "x_b", "y_b", "z_b", - "palpha_eps", "ptgalpha_eps", "t_step_old", "patch_ib", "pi_fac", +_nv( + _PRE_SIM, + "x_domain", + "y_domain", + "z_domain", + "x_a", + "y_a", + "z_a", + "x_b", + "y_b", + "z_b", + "palpha_eps", + "ptgalpha_eps", + "t_step_old", + "patch_ib", + "pi_fac", ) _nv(_PRE_POST, "num_fluids", "weno_order", "recon_type", "muscl_order", "mhd", "nb", "sigR", "igr", "igr_order") -_nv(_SIM, - "dt", "t_step_print", "time_stepper", "adap_dt", "adap_dt_tol", "adap_dt_max_iters", - "weno_eps", "teno_CT", "wenoz_q", "mp_weno", "weno_avg", "weno_Re_flux", "null_weights", - "muscl_eps", "int_comp", "ic_eps", "ic_beta", - "riemann_solver", "wave_speeds", "low_Mach", - "hyper_cleaning_speed", "hyper_cleaning_tau", - "run_time_info", "bubble_model", "lag_params", - "probe_wrt", "num_probes", "probe", "integral_wrt", "num_integrals", "integral", - "acoustic_source", "num_source", "acoustic", "chem_params", - "bf_x", "bf_y", "bf_z", "k_x", "k_y", "k_z", "w_x", "w_y", "w_z", - "p_x", "p_y", "p_z", "g_x", "g_y", "g_z", - "collision_model", "coefficient_of_restitution", "collision_time", "ib_coefficient_of_friction", - "tau_star", "cont_damage_s", "alpha_bar", - "rdma_mpi", "alf_factor", - "num_igr_iters", "num_igr_warm_start_iters", "igr_iter_solver", "igr_pres_lim", - "nv_uvm_out_of_core", "nv_uvm_igr_temps_on_gpu", "nv_uvm_pref_gpu", +_nv( + _SIM, + "dt", + "t_step_print", + "time_stepper", + "adap_dt", + "adap_dt_tol", + "adap_dt_max_iters", + "weno_eps", + "teno_CT", + "wenoz_q", + "mp_weno", + "weno_avg", + "weno_Re_flux", + "null_weights", + "muscl_eps", + "int_comp", + "ic_eps", + "ic_beta", + "riemann_solver", + "wave_speeds", + "low_Mach", + "hyper_cleaning_speed", + "hyper_cleaning_tau", + "run_time_info", + "bubble_model", + "lag_params", + "probe_wrt", + "num_probes", + "probe", + "integral_wrt", + "num_integrals", + "integral", + "acoustic_source", + "num_source", + "acoustic", + "chem_params", + "bf_x", + "bf_y", + "bf_z", + "k_x", + "k_y", + "k_z", + "w_x", + "w_y", + "w_z", + "p_x", + "p_y", + "p_z", + "g_x", + "g_y", + "g_z", + "collision_model", + "coefficient_of_restitution", + "collision_time", + "ib_coefficient_of_friction", + "tau_star", + "cont_damage_s", + "alpha_bar", + "rdma_mpi", + "alf_factor", + "num_igr_iters", + "num_igr_warm_start_iters", + "igr_iter_solver", + "igr_pres_lim", + "nv_uvm_out_of_core", + "nv_uvm_igr_temps_on_gpu", + "nv_uvm_pref_gpu", ) -_nv(_PRE, - "stretch_x", "stretch_y", "stretch_z", "a_x", "a_y", "a_z", "loops_x", "loops_y", "loops_z", - "n_start_old", "num_patches", "patch_icpp", "patch_bc", - "sigV", "dist_type", "rhoRV", "viscous", - "old_grid", "old_ic", - "perturb_flow", "perturb_flow_fluid", "perturb_flow_mag", - "perturb_sph", "perturb_sph_fluid", "fluid_rho", - "mixlayer_vel_profile", "mixlayer_vel_coef", "mixlayer_perturb", - "mixlayer_perturb_nk", "mixlayer_perturb_k0", - "pre_stress", "elliptic_smoothing", "elliptic_smoothing_iters", - "simplex_perturb", "simplex_params", +_nv( + _PRE, + "stretch_x", + "stretch_y", + "stretch_z", + "a_x", + "a_y", + "a_z", + "loops_x", + "loops_y", + "loops_z", + "n_start_old", + "num_patches", + "patch_icpp", + "patch_bc", + "sigV", + "dist_type", + "rhoRV", + "viscous", + "old_grid", + "old_ic", + "perturb_flow", + "perturb_flow_fluid", + "perturb_flow_mag", + "perturb_sph", + "perturb_sph_fluid", + "fluid_rho", + "mixlayer_vel_profile", + "mixlayer_vel_coef", + "mixlayer_perturb", + "mixlayer_perturb_nk", + "mixlayer_perturb_k0", + "pre_stress", + "elliptic_smoothing", + "elliptic_smoothing_iters", + "simplex_perturb", + "simplex_params", ) -_nv(_POST, - "x_output", "y_output", "z_output", - "format", "output_partial_domain", "sim_data", "G", "flux_lim", "cons_vars_wrt", - "rho_wrt", "E_wrt", "pres_wrt", "c_wrt", - "gamma_wrt", "heat_ratio_wrt", "pi_inf_wrt", "pres_inf_wrt", - "omega_wrt", "qm_wrt", "liutex_wrt", "schlieren_wrt", "schlieren_alpha", - "alpha_rho_wrt", "mom_wrt", "vel_wrt", "flux_wrt", "alpha_wrt", "cf_wrt", - "chem_wrt_T", "chem_wrt_Y", "alpha_rho_e_wrt", - "lag_header", "lag_txt_wrt", "lag_db_wrt", "lag_id_wrt", - "lag_pos_wrt", "lag_pos_prev_wrt", "lag_vel_wrt", "lag_rad_wrt", "lag_rvel_wrt", - "lag_r0_wrt", "lag_rmax_wrt", "lag_rmin_wrt", "lag_dphidt_wrt", - "lag_pres_wrt", "lag_mv_wrt", "lag_mg_wrt", "lag_betaT_wrt", "lag_betaC_wrt", +_nv( + _POST, + "x_output", + "y_output", + "z_output", + "format", + "output_partial_domain", + "sim_data", + "G", + "flux_lim", + "cons_vars_wrt", + "rho_wrt", + "E_wrt", + "pres_wrt", + "c_wrt", + "gamma_wrt", + "heat_ratio_wrt", + "pi_inf_wrt", + "pres_inf_wrt", + "omega_wrt", + "qm_wrt", + "liutex_wrt", + "schlieren_wrt", + "schlieren_alpha", + "alpha_rho_wrt", + "mom_wrt", + "vel_wrt", + "flux_wrt", + "alpha_wrt", + "cf_wrt", + "chem_wrt_T", + "chem_wrt_Y", + "alpha_rho_e_wrt", + "lag_header", + "lag_txt_wrt", + "lag_db_wrt", + "lag_id_wrt", + "lag_pos_wrt", + "lag_pos_prev_wrt", + "lag_vel_wrt", + "lag_rad_wrt", + "lag_rvel_wrt", + "lag_r0_wrt", + "lag_rmax_wrt", + "lag_rmin_wrt", + "lag_dphidt_wrt", + "lag_pres_wrt", + "lag_mv_wrt", + "lag_mg_wrt", + "lag_betaT_wrt", + "lag_betaC_wrt", ) # Case-optimization params appear in the sim namelist under #:if not MFC_CASE_OPTIMIZATION. diff --git a/toolchain/mfc/params/generators/fortran_gen.py b/toolchain/mfc/params/generators/fortran_gen.py index 9bf5adfb10..210adb460b 100644 --- a/toolchain/mfc/params/generators/fortran_gen.py +++ b/toolchain/mfc/params/generators/fortran_gen.py @@ -18,8 +18,10 @@ _DECL_COL = 24 # '::' column, matches ffmt alignment _FORTRAN_TYPES = { - ParamType.INT: "integer", ParamType.ANALYTIC_INT: "integer", - ParamType.REAL: "real(wp)", ParamType.ANALYTIC_REAL: "real(wp)", + ParamType.INT: "integer", + ParamType.ANALYTIC_INT: "integer", + ParamType.REAL: "real(wp)", + ParamType.ANALYTIC_REAL: "real(wp)", ParamType.LOG: "logical", } @@ -126,8 +128,4 @@ def get_generated_files(build_dir: Path) -> List[Tuple[Path, str]]: Paths match the cmake include directory structure: build_dir/include/{full_target}/generated_{namelist,decls}.fpp """ - return [ - (build_dir / "include" / full / f"generated_{kind}.fpp", gen(short)) - for short, full in TARGETS - for kind, gen in [("namelist", generate_namelist_fpp), ("decls", generate_decls_fpp)] - ] + return [(build_dir / "include" / full / f"generated_{kind}.fpp", gen(short)) for short, full in TARGETS for kind, gen in [("namelist", generate_namelist_fpp), ("decls", generate_decls_fpp)]] diff --git a/toolchain/mfc/run/input.py b/toolchain/mfc/run/input.py index dda710f602..02bd09d81a 100644 --- a/toolchain/mfc/run/input.py +++ b/toolchain/mfc/run/input.py @@ -62,7 +62,8 @@ def get_cantera_solution(self): for candidate in candidates: try: return ct.Solution(candidate) - except Exception: + except Exception as e: + cons.print(f"[dim] Cantera: skipping '{candidate}': {e}[/dim]") continue raise common.MFCException(f"Cantera file '{cantera_file}' not found. Searched: {', '.join(candidates)}.") diff --git a/toolchain/mfc/run/run.py b/toolchain/mfc/run/run.py index 82e886c064..6823360423 100644 --- a/toolchain/mfc/run/run.py +++ b/toolchain/mfc/run/run.py @@ -33,31 +33,20 @@ def __validate_job_options() -> None: raise MFCException(f"RUN: {ARG('email')} is not a valid e-mail address.") -def __profiler_prepend() -> typing.List[str]: - if ARG("ncu") is not None: - if not does_command_exist("ncu"): - raise MFCException("Failed to locate [bold green]NVIDIA Nsight Compute[/bold green] (ncu).") - - return ["ncu", "--nvtx", "--mode=launch-and-attach", "--cache-control=none", "--clock-control=none"] + ARG("ncu") - - if ARG("nsys") is not None: - if not does_command_exist("nsys"): - raise MFCException("Failed to locate [bold green]NVIDIA Nsight Systems[/bold green] (nsys).") - - return ["nsys", "profile", "--stats=true", "--trace=mpi,nvtx,openacc"] + ARG("nsys") +_PROFILERS = [ + ("ncu", "ncu", "[bold green]NVIDIA Nsight Compute[/bold green]", lambda: ["ncu", "--nvtx", "--mode=launch-and-attach", "--cache-control=none", "--clock-control=none"] + ARG("ncu")), + ("nsys", "nsys", "[bold green]NVIDIA Nsight Systems[/bold green]", lambda: ["nsys", "profile", "--stats=true", "--trace=mpi,nvtx,openacc"] + ARG("nsys")), + ("rcu", "rocprof-compute", "[bold red]ROCM rocprof-compute[/bold red]", lambda: ["rocprof-compute", "profile", "-n", ARG("name").replace("-", "_").replace(".", "_")] + ARG("rcu") + ["--"]), + ("rsys", "rocprof", "[bold red]ROCM rocprof-systems[/bold red]", lambda: ["rocprof"] + ARG("rsys")), +] - if ARG("rcu") is not None: - if not does_command_exist("rocprof-compute"): - raise MFCException("Failed to locate [bold red]ROCM rocprof-compute[/bold red] (rocprof-compute).") - - return ["rocprof-compute", "profile", "-n", ARG("name").replace("-", "_").replace(".", "_")] + ARG("rcu") + ["--"] - - if ARG("rsys") is not None: - if not does_command_exist("rocprof"): - raise MFCException("Failed to locate [bold red]ROCM rocprof-systems[/bold red] (rocprof-systems).") - - return ["rocprof"] + ARG("rsys") +def __profiler_prepend() -> typing.List[str]: + for arg, cmd, label, build_args in _PROFILERS: + if ARG(arg) is not None: + if not does_command_exist(cmd): + raise MFCException(f"Failed to locate {label} ({cmd}).") + return build_args() return [] From 692c1a7a5dc350a5a4346243752990cd95b04963 Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Thu, 28 May 2026 01:51:14 -0400 Subject: [PATCH 14/33] refactor: extract repeated patterns in case_validator and test/cases case_validator.py: - Add _check_order_fits_grid(): the 3-line m/n/p dimension check was duplicated identically in check_igr, check_weno, check_muscl; now one call - Add _get_recon_type(): the recon_type read + prohibit was duplicated in check_weno, check_muscl, check_weno_simulation, check_muscl_simulation; now one call each - Collapse two named param lists in check_muscl into inline literals test/cases.py: - Add make_3d_box_patches(): the 18-line 3-patch 3D box IC setup was copy-pasted identically in mpi_consistency_tests, restart_roundtrip_tests, and kernel_golden_tests; now one line each lint_docs.py: - Add private helpers to skip set (they call prohibit but are not check_* methods and should not require PHYSICS_DOCS entries) --- toolchain/mfc/case_validator.py | 69 ++++++++++------------------ toolchain/mfc/lint_docs.py | 3 ++ toolchain/mfc/test/cases.py | 80 ++++++++++----------------------- 3 files changed, 50 insertions(+), 102 deletions(-) diff --git a/toolchain/mfc/case_validator.py b/toolchain/mfc/case_validator.py index 05a3fee92c..950dbdce42 100644 --- a/toolchain/mfc/case_validator.py +++ b/toolchain/mfc/case_validator.py @@ -250,6 +250,21 @@ def _validate_logical(self, key: str): if val is not None and val not in ("T", "F"): self.errors.append(f"{key} must be 'T' or 'F', got '{val}'") + def _check_order_fits_grid(self, order: int, param_name: str) -> None: + """Prohibit reconstruction order that exceeds grid cell count in any active dimension.""" + m = self.get("m", 0) + n = self.get("n", 0) or 0 + p = self.get("p", 0) or 0 + self.prohibit(m + 1 < order, f"m must be at least {param_name} - 1 (= {order - 1})") + self.prohibit(n > 0 and n + 1 < order, f"For 2D: n must be at least {param_name} - 1 (= {order - 1})") + self.prohibit(p > 0 and p + 1 < order, f"For 3D: p must be at least {param_name} - 1 (= {order - 1})") + + def _get_recon_type(self) -> int: + """Return recon_type after validating it is 1 (WENO) or 2 (MUSCL).""" + recon_type = self.get("recon_type", 1) + self.prohibit(recon_type not in [1, 2], "recon_type must be 1 (WENO) or 2 (MUSCL)") + return recon_type + def check_parameter_types(self): """Validate parameter types before other checks. @@ -316,72 +331,44 @@ def check_igr(self): return igr_order = self.get("igr_order") - m = self.get("m", 0) - n = self.get("n", 0) - p = self.get("p", 0) self.prohibit(igr_order not in [None, 3, 5], "igr_order must be 3 or 5") if igr_order: - self.prohibit(m + 1 < igr_order, f"m must be at least igr_order - 1 (= {igr_order - 1})") - self.prohibit(n is not None and n > 0 and n + 1 < igr_order, f"n must be at least igr_order - 1 (= {igr_order - 1})") - self.prohibit(p is not None and p > 0 and p + 1 < igr_order, f"p must be at least igr_order - 1 (= {igr_order - 1})") + self._check_order_fits_grid(igr_order, "igr_order") def check_weno(self): """Checks constraints regarding WENO order""" - recon_type = self.get("recon_type", 1) - self.prohibit(recon_type not in [1, 2], "recon_type must be 1 (WENO) or 2 (MUSCL)") - - # WENO_TYPE = 1 - if recon_type != 1: + if self._get_recon_type() != 1: return for param in ["muscl_order", "muscl_lim"]: self.prohibit(self.is_set(param), f"recon_type = 1 (WENO) is not compatible with {param}") weno_order = self.get("weno_order") - m = self.get("m", 0) - n = self.get("n", 0) - p = self.get("p", 0) - if weno_order is None: return self.prohibit(weno_order not in [1, 3, 5, 7], "weno_order must be 1, 3, 5, or 7") - self.prohibit(m + 1 < weno_order, f"m must be at least weno_order - 1 (= {weno_order - 1})") - self.prohibit(n is not None and n > 0 and n + 1 < weno_order, f"For 2D simulation, n must be at least weno_order - 1 (= {weno_order - 1})") - self.prohibit(p is not None and p > 0 and p + 1 < weno_order, f"For 3D simulation, p must be at least weno_order - 1 (= {weno_order - 1})") + self._check_order_fits_grid(weno_order, "weno_order") def check_muscl(self): """Check constraints regarding MUSCL order""" - recon_type = self.get("recon_type", 1) - self.prohibit(recon_type not in [1, 2], "recon_type must be 1 (WENO) or 2 (MUSCL)") - - # MUSCL_TYPE = 2 - if recon_type != 2: + if self._get_recon_type() != 2: return - weno_log_params = ["mapped_weno", "wenoz", "teno", "mp_weno", "weno_avg", "null_weights", "weno_Re_flux"] - for param in weno_log_params: + for param in ["mapped_weno", "wenoz", "teno", "mp_weno", "weno_avg", "null_weights", "weno_Re_flux"]: self.prohibit(self.get(param) == "T", f"recon_type = 2 (MUSCL) is not compatible with {param} = T") - - weno_numeric_params = ["wenoz_q", "teno_CT", "weno_eps"] - for param in weno_numeric_params: + for param in ["wenoz_q", "teno_CT", "weno_eps"]: self.prohibit(self.is_set(param), f"recon_type = 2 (MUSCL) is not compatible with {param}") weno_order = self.get("weno_order") self.prohibit(weno_order is not None and weno_order != 0, f"recon_type = 2 (MUSCL) requires weno_order unset or 0, but got {weno_order}") muscl_order = self.get("muscl_order") - m = self.get("m", 0) - n = self.get("n", 0) - p = self.get("p", 0) - if muscl_order is None: return self.prohibit(muscl_order not in [1, 2], "muscl_order must be 1 or 2") - self.prohibit(m + 1 < muscl_order, f"m must be at least muscl_order - 1 (= {muscl_order - 1})") - self.prohibit(n is not None and n > 0 and n + 1 < muscl_order, f"For 2D simulation, n must be at least muscl_order - 1 (= {muscl_order - 1})") - self.prohibit(p is not None and p > 0 and p + 1 < muscl_order, f"For 3D simulation, p must be at least muscl_order - 1 (= {muscl_order - 1})") + self._check_order_fits_grid(muscl_order, "muscl_order") def check_interface_compression(self): """Check constraints regarding interface compression""" @@ -752,11 +739,7 @@ def check_finite_difference(self): def check_weno_simulation(self): """Checks WENO-specific constraints for simulation""" - recon_type = self.get("recon_type", 1) - self.prohibit(recon_type not in [1, 2], "recon_type must be 1 (WENO) or 2 (MUSCL)") - - # WENO_TYPE = 1 - if recon_type != 1: + if self._get_recon_type() != 1: return weno_order = self.get("weno_order") @@ -793,11 +776,7 @@ def check_weno_simulation(self): def check_muscl_simulation(self): """Checks MUSCL-specific constraints for simulation""" - recon_type = self.get("recon_type", 1) - self.prohibit(recon_type not in [1, 2], "recon_type must be 1 (WENO) or 2 (MUSCL)") - - # MUSCL_TYPE = 2 - if recon_type != 2: + if self._get_recon_type() != 2: return muscl_order = self.get("muscl_order") diff --git a/toolchain/mfc/lint_docs.py b/toolchain/mfc/lint_docs.py index 15a1a6546b..32f765651a 100644 --- a/toolchain/mfc/lint_docs.py +++ b/toolchain/mfc/lint_docs.py @@ -407,6 +407,9 @@ def check_physics_docs_coverage(repo_root: Path) -> list[str]: # Methods without PHYSICS_DOCS entries. Add a PHYSICS_DOCS entry (with math, # references, and explanation) to case_validator.py to remove from this set. skip = { + # Private helpers — called from check_* methods, not check methods themselves + "_check_order_fits_grid", + "_get_recon_type", # Structural/mechanical checks (no physics meaning) "check_parameter_types", # type validation "check_output_format", # output format selection diff --git a/toolchain/mfc/test/cases.py b/toolchain/mfc/test/cases.py index b72584a032..f059582cbf 100644 --- a/toolchain/mfc/test/cases.py +++ b/toolchain/mfc/test/cases.py @@ -162,6 +162,26 @@ def _h_sweep(case_path, ndim, cons_vars, extra_args, expected, tol, resolutions, ) +def make_3d_box_patches( + z_centroids=(0.05, 0.45, 0.9), + z_lengths=(0.1, 0.7, 0.2), + geometry=9, +) -> dict: + """3-patch 3D box IC: uniform xy plane (centroid=0.5, length=1), z spacing given.""" + d = {} + for pid in range(1, 4): + d[f"patch_icpp({pid})%geometry"] = geometry + for vel in (1, 2, 3): + d[f"patch_icpp({pid})%vel({vel})"] = 0.0 + d[f"patch_icpp({pid})%x_centroid"] = 0.5 + d[f"patch_icpp({pid})%length_x"] = 1 + d[f"patch_icpp({pid})%y_centroid"] = 0.5 + d[f"patch_icpp({pid})%length_y"] = 1 + d[f"patch_icpp({pid})%z_centroid"] = z_centroids[pid - 1] + d[f"patch_icpp({pid})%length_z"] = z_lengths[pid - 1] + return d + + def get_bc_mods(bc: int, dimInfo): params = {} for dimCmp in dimInfo[0]: @@ -2027,25 +2047,7 @@ def mpi_consistency_tests(): "bc_z%end": -3, } - for patchID in range(1, 4): - base_3d[f"patch_icpp({patchID})%geometry"] = 9 - base_3d[f"patch_icpp({patchID})%vel(1)"] = 0.0 - base_3d[f"patch_icpp({patchID})%vel(2)"] = 0.0 - base_3d[f"patch_icpp({patchID})%vel(3)"] = 0.0 - base_3d[f"patch_icpp({patchID})%x_centroid"] = 0.5 - base_3d[f"patch_icpp({patchID})%length_x"] = 1 - base_3d[f"patch_icpp({patchID})%y_centroid"] = 0.5 - base_3d[f"patch_icpp({patchID})%length_y"] = 1 - base_3d.update( - { - "patch_icpp(1)%z_centroid": 0.05, - "patch_icpp(1)%length_z": 0.1, - "patch_icpp(2)%z_centroid": 0.45, - "patch_icpp(2)%length_z": 0.7, - "patch_icpp(3)%z_centroid": 0.9, - "patch_icpp(3)%length_z": 0.2, - } - ) + base_3d.update(make_3d_box_patches()) # Bubbles with 2 MPI ranks stack.push( @@ -2203,25 +2205,7 @@ def restart_roundtrip_tests(): "bc_z%beg": -3, "bc_z%end": -3, } - for patchID in range(1, 4): - base_3d[f"patch_icpp({patchID})%geometry"] = 9 - base_3d[f"patch_icpp({patchID})%vel(1)"] = 0.0 - base_3d[f"patch_icpp({patchID})%vel(2)"] = 0.0 - base_3d[f"patch_icpp({patchID})%vel(3)"] = 0.0 - base_3d[f"patch_icpp({patchID})%x_centroid"] = 0.5 - base_3d[f"patch_icpp({patchID})%length_x"] = 1 - base_3d[f"patch_icpp({patchID})%y_centroid"] = 0.5 - base_3d[f"patch_icpp({patchID})%length_y"] = 1 - base_3d.update( - { - "patch_icpp(1)%z_centroid": 0.05, - "patch_icpp(1)%length_z": 0.1, - "patch_icpp(2)%z_centroid": 0.45, - "patch_icpp(2)%length_z": 0.7, - "patch_icpp(3)%z_centroid": 0.9, - "patch_icpp(3)%length_z": 0.2, - } - ) + base_3d.update(make_3d_box_patches()) stack.push("Restart Roundtrip -> 3D", base_3d) cases.append(define_case_d(stack, "", {}, restart_check=True)) stack.pop() @@ -2259,25 +2243,7 @@ def kernel_golden_tests(): "bc_z%beg": -3, "bc_z%end": -3, } - for patchID in range(1, 4): - base_3d[f"patch_icpp({patchID})%geometry"] = 9 - base_3d[f"patch_icpp({patchID})%vel(1)"] = 0.0 - base_3d[f"patch_icpp({patchID})%vel(2)"] = 0.0 - base_3d[f"patch_icpp({patchID})%vel(3)"] = 0.0 - base_3d[f"patch_icpp({patchID})%x_centroid"] = 0.5 - base_3d[f"patch_icpp({patchID})%length_x"] = 1 - base_3d[f"patch_icpp({patchID})%y_centroid"] = 0.5 - base_3d[f"patch_icpp({patchID})%length_y"] = 1 - base_3d.update( - { - "patch_icpp(1)%z_centroid": 0.05, - "patch_icpp(1)%length_z": 0.1, - "patch_icpp(2)%z_centroid": 0.45, - "patch_icpp(2)%length_z": 0.7, - "patch_icpp(3)%z_centroid": 0.9, - "patch_icpp(3)%length_z": 0.2, - } - ) + base_3d.update(make_3d_box_patches()) # 3D grid stretching in all directions. # The cosh-based stretching expands the domain beyond the original From 53f08e17573d9719eef9f7935f6b4076e4e8d9cd Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Thu, 28 May 2026 02:06:13 -0400 Subject: [PATCH 15/33] fix: restore alpha_wrt scalar registration missing from refac-params MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The refac-params branch dropped the bare alpha_wrt scalar param from definitions.py — it was in master's _wrt loop but not carried over. This caused ./mfc.sh validate to reject example cases using 'alpha_wrt': 'T' (the schema had patternProperties for alpha_wrt(N) but not the scalar form), matching what master registers: both the bare scalar and the indexed forms. --- toolchain/mfc/params/definitions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/toolchain/mfc/params/definitions.py b/toolchain/mfc/params/definitions.py index d4112f30f2..d1a3f6a372 100644 --- a/toolchain/mfc/params/definitions.py +++ b/toolchain/mfc/params/definitions.py @@ -623,6 +623,7 @@ def _load(): _r(n, LOG, {"output"}) for n in [ "schlieren_wrt", + "alpha_wrt", "rho_wrt", "E_wrt", "pres_wrt", From d85b5b6b8c40a4c9de797cd548adbfd20dc684dc Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Thu, 28 May 2026 02:10:28 -0400 Subject: [PATCH 16/33] fix(codegen): exclude CASE_OPT_PARAMS from sim generated_decls.fpp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit generate_decls_fpp was emitting unconditional declarations for all sim-target variables including CASE_OPT_PARAMS (recon_type, weno_order, nb, mapped_weno, viscous, etc.). m_global_parameters.fpp for simulation contains a manual #:if MFC_CASE_OPTIMIZATION / #:else block that declares these same variables — as compile-time constants in case-opt builds and as regular variables otherwise. The generated include + the manual block created duplicate declarations, which is a Fortran compile error for every ./mfc.sh build --case-optimization invocation. Fix: skip CASE_OPT_PARAMS when generating declarations for the sim target. The manual block handles both the parameter and variable cases. --- toolchain/mfc/params/generators/fortran_gen.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/toolchain/mfc/params/generators/fortran_gen.py b/toolchain/mfc/params/generators/fortran_gen.py index 210adb460b..39a75c3c7c 100644 --- a/toolchain/mfc/params/generators/fortran_gen.py +++ b/toolchain/mfc/params/generators/fortran_gen.py @@ -100,6 +100,10 @@ def generate_decls_fpp(target: str) -> str: for name in _vars_for_target(target): if not _is_simple_scalar(name): continue + # Skip sim case-opt params: declared as compile-time parameters by the + # manual #:if MFC_CASE_OPTIMIZATION / #:else block in m_global_parameters.fpp + if target == "sim" and name in CASE_OPT_PARAMS: + continue param = REGISTRY.all_params.get(name) if param is None: continue From 7517ada735c9af3e8a3c61eb3dd6cd2345e2ce14 Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Thu, 28 May 2026 02:18:26 -0400 Subject: [PATCH 17/33] refactor: fix cantera_file NameError, unify archive relpath, derive completion flags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit run/input.py: - cantera_file was only assigned in the chemistry=T branch; the error message on line 69 would raise NameError when chemistry=F and all candidates (h2o2.yaml) fail to load — set cantera_file='h2o2.yaml' before the branch so the error message is always valid run/archive.py: - Extract _relpath_safe(path, dirpath) to replace three inline try/except blocks scattered across __build_manifest, __copy_dir, __write_tar - Fix manifest fallback: was falling back to the full absolute path on cross-device paths; now uses basename like copy/tar already did — manifest and archive contents now agree cli/completion_gen.py: - Bash and zsh completion lists for MFC config flags were hardcoded, already missing --reldebug/--no-reldebug; derive dynamically from MFCConfig dataclass fields (same source argparse_gen.py already uses) - New MFCConfig fields are now automatically reflected in completions --- toolchain/mfc/cli/completion_gen.py | 74 +++++++++++++---------------- toolchain/mfc/run/archive.py | 28 +++++------ toolchain/mfc/run/input.py | 8 ++-- 3 files changed, 49 insertions(+), 61 deletions(-) diff --git a/toolchain/mfc/cli/completion_gen.py b/toolchain/mfc/cli/completion_gen.py index de1ac0cd98..84fbe7e0e1 100644 --- a/toolchain/mfc/cli/completion_gen.py +++ b/toolchain/mfc/cli/completion_gen.py @@ -5,10 +5,42 @@ in sync with the CLI schema definitions. """ +import dataclasses from typing import List, Set from .schema import CLISchema, Command, CompletionType + +def _mfc_config_bash_flags() -> List[str]: + """Return bash completion flag strings derived from MFCConfig fields.""" + from ..state import MFCConfig + + flags = [] + for f in dataclasses.fields(MFCConfig): + cli = f.name.replace("_", "-") + flags.append(f"--{cli}") + flags.append(f"--no-{cli}") + return flags + + +def _mfc_config_zsh_flags() -> List[str]: + """Return zsh completion spec strings derived from MFCConfig fields.""" + from ..state import MFCConfig, gpuConfigOptions + + specs = [] + modes = ":".join(e.value for e in gpuConfigOptions if e.value != gpuConfigOptions.NONE.value) + for f in dataclasses.fields(MFCConfig): + cli = f.name.replace("_", "-") + label = cli.replace("-", " ").title() + if f.name == "gpu": + specs.append(f"'--{cli}[Enable GPU]:mode:({modes})'") + specs.append(f"'--no-{cli}[Disable GPU]'") + else: + specs.append(f"'--{cli}[Enable {label}]'") + specs.append(f"'--no-{cli}[Disable {label}]'") + return specs + + # Mapping of completion types to bash completion expressions _BASH_COMPLETION_MAP = { CompletionType.FILES_PY: 'COMPREPLY=( $(compgen -f -X "!*.py" -- "${cur}") $(compgen -d -- "${cur}") )', @@ -31,26 +63,7 @@ def _collect_all_options(cmd: Command, schema: CLISchema) -> List[str]: # MFC config flags if common_set.mfc_config_flags: - options.update( - [ - "--mpi", - "--no-mpi", - "--gpu", - "--no-gpu", - "--debug", - "--no-debug", - "--gcov", - "--no-gcov", - "--unified", - "--no-unified", - "--single", - "--no-single", - "--mixed", - "--no-mixed", - "--fastmath", - "--no-fastmath", - ] - ) + options.update(_mfc_config_bash_flags()) else: for arg in common_set.arguments: if arg.short: @@ -324,26 +337,7 @@ def _generate_zsh_command_args(cmd: Command, schema: CLISchema) -> List[str]: continue if common_set.mfc_config_flags: - arg_lines.extend( - [ - "'--mpi[Enable MPI]'", - "'--no-mpi[Disable MPI]'", - "'--gpu[Enable GPU]:mode:(acc mp)'", - "'--no-gpu[Disable GPU]'", - "'--debug[Build with debug compiler flags (for MFC code)]'", - "'--no-debug[Build without debug flags]'", - "'--gcov[Enable gcov coverage]'", - "'--no-gcov[Disable gcov coverage]'", - "'--unified[Enable unified memory]'", - "'--no-unified[Disable unified memory]'", - "'--single[Enable single precision]'", - "'--no-single[Disable single precision]'", - "'--mixed[Enable mixed precision]'", - "'--no-mixed[Disable mixed precision]'", - "'--fastmath[Enable fast math]'", - "'--no-fastmath[Disable fast math]'", - ] - ) + arg_lines.extend(_mfc_config_zsh_flags()) else: for arg in common_set.arguments: desc = arg.help.replace("'", "").replace("[", "").replace("]", "")[:120] diff --git a/toolchain/mfc/run/archive.py b/toolchain/mfc/run/archive.py index 0c3848060a..4a31754897 100644 --- a/toolchain/mfc/run/archive.py +++ b/toolchain/mfc/run/archive.py @@ -11,6 +11,15 @@ from ..state import ARG, CFG from . import input + +def _relpath_safe(path: str, dirpath: str) -> str: + """Return relpath(path, dirpath), falling back to basename on cross-device paths.""" + try: + return os.path.relpath(path, dirpath) + except ValueError: + return os.path.basename(path) + + ARTIFACT_FILENAMES = [ "equations.dat", "run_time.inf", @@ -74,13 +83,7 @@ def __collect_sources(case: input.MFCInputFile, targets) -> list: def __build_manifest(case: input.MFCInputFile, targets, sources: list, archive_path: str, archive_format: str) -> dict: dirpath = case.dirpath - relative_sources = [] - for src in sources: - try: - rel = os.path.relpath(src, dirpath) - except ValueError: - rel = src - relative_sources.append(rel) + relative_sources = [_relpath_safe(src, dirpath) for src in sources] return { "timestamp": datetime.datetime.now().astimezone().isoformat(), @@ -101,11 +104,7 @@ def __copy_dir(sources: list, case: input.MFCInputFile, dest: str) -> None: dirpath = case.dirpath for src in sources: - try: - rel = os.path.relpath(src, dirpath) - except ValueError: - rel = os.path.basename(src) - + rel = _relpath_safe(src, dirpath) target_path = os.path.join(dest, rel) os.makedirs(os.path.dirname(target_path), exist_ok=True) @@ -120,10 +119,7 @@ def __write_tar(sources: list, case: input.MFCInputFile, dest: str, compressed: arcroot = os.path.basename(dest).removesuffix(".tar.zst").removesuffix(".tar") def rel_for(path: str) -> str: - try: - return os.path.relpath(path, dirpath) - except ValueError: - return os.path.basename(path) + return _relpath_safe(path, dirpath) if compressed: if not does_command_exist("tar"): diff --git a/toolchain/mfc/run/input.py b/toolchain/mfc/run/input.py index 02bd09d81a..dd3a122cf3 100644 --- a/toolchain/mfc/run/input.py +++ b/toolchain/mfc/run/input.py @@ -47,17 +47,15 @@ def get_cantera_solution(self): if self.params.get("chemistry", "F") == "T": cantera_file = self.params["cantera_file"] - candidates = [ cantera_file, os.path.join(self.dirpath, cantera_file), os.path.join(common.MFC_MECHANISMS_DIR, cantera_file), ] else: - # If Chemistry is turned off, we return a default (dummy) solution - # that will not be used in the simulation, so that MFC can still - # be compiled. - candidates = ["h2o2.yaml"] + # Chemistry is off — return a dummy solution so MFC still compiles. + cantera_file = "h2o2.yaml" + candidates = [cantera_file] for candidate in candidates: try: From d6c9228c59197629ddb6d4d5bf63321a28fa58a7 Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Thu, 28 May 2026 09:37:53 -0400 Subject: [PATCH 18/33] fix: restore CASE_OPT_PARAMS declarations in #:else branch --- src/simulation/m_global_parameters.fpp | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/simulation/m_global_parameters.fpp b/src/simulation/m_global_parameters.fpp index f7df2de9f8..e807dece0a 100644 --- a/src/simulation/m_global_parameters.fpp +++ b/src/simulation/m_global_parameters.fpp @@ -93,10 +93,26 @@ module m_global_parameters logical, parameter :: igr_pres_lim = (${igr_pres_lim}$ /= 0) !< Limit to positive pressures for IGR logical, parameter :: viscous = (${viscous}$ /= 0) !< Viscous effects #:else - integer :: weno_polyn !< Degree of the WENO polynomials (polyn) - integer :: muscl_polyn !< Degree of the MUSCL polynomials (polyn)i - integer :: weno_num_stencils !< Number of stencils for WENO reconstruction (only different from weno_polyn for TENO(>5)) - logical :: wenojs !< WENO-JS (default) + integer :: recon_type + integer :: weno_polyn + integer :: muscl_polyn + integer :: weno_order + integer :: muscl_order + integer :: weno_num_stencils + integer :: muscl_lim + integer :: num_fluids + logical :: wenojs + logical :: mapped_weno + logical :: wenoz + logical :: teno + real(wp) :: wenoz_q + logical :: mhd + logical :: relativity + integer :: igr_iter_solver + integer :: igr_order + logical :: igr + logical :: igr_pres_lim + logical :: viscous #:endif $:GPU_DECLARE(create='[int_comp, ic_eps, ic_beta]') @@ -254,6 +270,8 @@ module m_global_parameters !> @{ #:if MFC_CASE_OPTIMIZATION integer, parameter :: nb = ${nb}$ !< Number of eq. bubble sizes + #:else + integer :: nb #:endif real(wp) :: Eu !< Euler number From fdb97f02745d4f952c09a87949d82631466fb6a7 Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Thu, 28 May 2026 09:58:25 -0400 Subject: [PATCH 19/33] fix: remove stray scalar registration of alpha_wrt (array in Fortran) --- toolchain/mfc/params/definitions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/toolchain/mfc/params/definitions.py b/toolchain/mfc/params/definitions.py index d1a3f6a372..d4112f30f2 100644 --- a/toolchain/mfc/params/definitions.py +++ b/toolchain/mfc/params/definitions.py @@ -623,7 +623,6 @@ def _load(): _r(n, LOG, {"output"}) for n in [ "schlieren_wrt", - "alpha_wrt", "rho_wrt", "E_wrt", "pres_wrt", From 2c171f12f4a13c14002207f0b2347b3db02f8076 Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Thu, 28 May 2026 10:11:43 -0400 Subject: [PATCH 20/33] fix: restore alpha_wrt registration; skip array vars in decl gen; add example validation to precheck --- toolchain/bootstrap/precheck.sh | 35 +++++++++++++++---- toolchain/mfc/params/definitions.py | 1 + .../mfc/params/generators/fortran_gen.py | 3 ++ 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/toolchain/bootstrap/precheck.sh b/toolchain/bootstrap/precheck.sh index fc0effae08..f26e6c5362 100755 --- a/toolchain/bootstrap/precheck.sh +++ b/toolchain/bootstrap/precheck.sh @@ -127,11 +127,24 @@ fi ) & PID_PARAM_DOCS=$! +# Example case validation +( + failed=0 + for case in examples/*/case.py; do + [ -f "$case" ] || continue + if ! ./mfc.sh validate "$case" > /dev/null 2>&1; then + failed=$((failed + 1)) + fi + done + echo "$failed" > "$TMPDIR_PC/examples_exit" +) & +PID_EXAMPLES=$! + # --- Collect results --- FAILED=0 -log "[$CYAN 1/6$COLOR_RESET] Checking$MAGENTA formatting$COLOR_RESET..." +log "[$CYAN 1/7$COLOR_RESET] Checking$MAGENTA formatting$COLOR_RESET..." if [ "$FORMAT_OK" = "1" ]; then error "Formatting check failed to run." FAILED=1 @@ -146,7 +159,7 @@ else fi wait $PID_SPELL -log "[$CYAN 2/6$COLOR_RESET] Running$MAGENTA spell check$COLOR_RESET..." +log "[$CYAN 2/7$COLOR_RESET] Running$MAGENTA spell check$COLOR_RESET..." SPELL_RC=$(cat "$TMPDIR_PC/spell_exit" 2>/dev/null || echo "1") if [ "$SPELL_RC" = "0" ]; then ok "Spell check passed." @@ -156,7 +169,7 @@ else fi wait $PID_LINT -log "[$CYAN 3/6$COLOR_RESET] Running$MAGENTA toolchain lint$COLOR_RESET..." +log "[$CYAN 3/7$COLOR_RESET] Running$MAGENTA toolchain lint$COLOR_RESET..." LINT_RC=$(cat "$TMPDIR_PC/lint_exit" 2>/dev/null || echo "1") if [ "$LINT_RC" = "0" ]; then ok "Toolchain lint passed." @@ -166,7 +179,7 @@ else fi wait $PID_SOURCE -log "[$CYAN 4/6$COLOR_RESET] Running$MAGENTA source lint$COLOR_RESET..." +log "[$CYAN 4/7$COLOR_RESET] Running$MAGENTA source lint$COLOR_RESET..." SOURCE_RC=$(cat "$TMPDIR_PC/source_exit" 2>/dev/null || echo "1") if [ "$SOURCE_RC" = "0" ]; then ok "Source lint passed." @@ -175,7 +188,7 @@ else FAILED=1 fi -log "[$CYAN 5/6$COLOR_RESET] Checking$MAGENTA doc references$COLOR_RESET..." +log "[$CYAN 5/7$COLOR_RESET] Checking$MAGENTA doc references$COLOR_RESET..." if [ $DOC_FAILED -eq 0 ]; then ok "Doc references are valid." else @@ -184,7 +197,7 @@ else fi wait $PID_PARAM_DOCS -log "[$CYAN 6/6$COLOR_RESET] Checking$MAGENTA parameter docs$COLOR_RESET..." +log "[$CYAN 6/7$COLOR_RESET] Checking$MAGENTA parameter docs$COLOR_RESET..." PARAM_DOCS_RC=$(cat "$TMPDIR_PC/param_docs_exit" 2>/dev/null || echo "1") if [ "$PARAM_DOCS_RC" = "0" ]; then ok "Parameter documentation check passed." @@ -193,6 +206,16 @@ else FAILED=1 fi +wait $PID_EXAMPLES +log "[$CYAN 7/7$COLOR_RESET] Validating$MAGENTA example cases$COLOR_RESET..." +EXAMPLES_FAILED=$(cat "$TMPDIR_PC/examples_exit" 2>/dev/null || echo "1") +if [ "$EXAMPLES_FAILED" = "0" ]; then + ok "All example cases are valid." +else + error "$EXAMPLES_FAILED example case(s) failed validation. Run$MAGENTA ./mfc.sh validate examples/\*/case.py$COLOR_RESET for details." + FAILED=1 +fi + echo "" if [ $FAILED -eq 0 ]; then diff --git a/toolchain/mfc/params/definitions.py b/toolchain/mfc/params/definitions.py index d4112f30f2..d1a3f6a372 100644 --- a/toolchain/mfc/params/definitions.py +++ b/toolchain/mfc/params/definitions.py @@ -623,6 +623,7 @@ def _load(): _r(n, LOG, {"output"}) for n in [ "schlieren_wrt", + "alpha_wrt", "rho_wrt", "E_wrt", "pres_wrt", diff --git a/toolchain/mfc/params/generators/fortran_gen.py b/toolchain/mfc/params/generators/fortran_gen.py index 39a75c3c7c..8173d54700 100644 --- a/toolchain/mfc/params/generators/fortran_gen.py +++ b/toolchain/mfc/params/generators/fortran_gen.py @@ -107,6 +107,9 @@ def generate_decls_fpp(target: str) -> str: param = REGISTRY.all_params.get(name) if param is None: continue + # Skip if also registered as an indexed family — Fortran declares it as an array. + if any(k.startswith(f"{name}(") for k in REGISTRY.all_params): + continue lines.append(f"{fortran_type_decl(param).ljust(_DECL_COL)}:: {name}") return "\n".join(lines) + "\n" From 98b24cfe28932ed1060879e7a18e526a6885abab Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Thu, 28 May 2026 10:18:07 -0400 Subject: [PATCH 21/33] feat(precheck): add generate --check and conditional build; 8/9 checks --- toolchain/bootstrap/precheck.sh | 66 ++++++++++++++++++++++++++++----- 1 file changed, 57 insertions(+), 9 deletions(-) diff --git a/toolchain/bootstrap/precheck.sh b/toolchain/bootstrap/precheck.sh index f26e6c5362..dea3fdc4f2 100755 --- a/toolchain/bootstrap/precheck.sh +++ b/toolchain/bootstrap/precheck.sh @@ -59,6 +59,21 @@ done # CI runs the full suite via ./mfc.sh lint without this variable. export MFC_SKIP_RENDER_TESTS=1 +# Detect whether any Fortran sources or CMakeLists changed — if so, run a build. +# This catches compilation errors (duplicate declarations, missing symbols, etc.) +# cheaply: Python-only changes pay zero build cost. +if git diff HEAD --name-only 2>/dev/null | grep -qE '\.(fpp|f90)$|CMakeLists\.txt'; then + BUILD_FORTRAN=1 +else + BUILD_FORTRAN=0 +fi + +if [ "$BUILD_FORTRAN" = "1" ]; then + NCHECK=9 +else + NCHECK=8 +fi + log "Running$MAGENTA precheck$COLOR_RESET (same checks as CI lint-gate)..." echo "" @@ -79,7 +94,7 @@ else fi fi -# --- Phase 2: All remaining checks in parallel (read-only) --- +# --- Phase 2: All fast checks in parallel (read-only) --- # Spell check ( @@ -127,6 +142,16 @@ fi ) & PID_PARAM_DOCS=$! +# Generated files check (JSON schema, completions, docs) +( + if ./mfc.sh generate --check > /dev/null 2>&1; then + echo "0" > "$TMPDIR_PC/generate_exit" + else + echo "1" > "$TMPDIR_PC/generate_exit" + fi +) & +PID_GENERATE=$! + # Example case validation ( failed=0 @@ -140,11 +165,11 @@ PID_PARAM_DOCS=$! ) & PID_EXAMPLES=$! -# --- Collect results --- +# --- Collect results (fast checks) --- FAILED=0 -log "[$CYAN 1/7$COLOR_RESET] Checking$MAGENTA formatting$COLOR_RESET..." +log "[$CYAN 1/$NCHECK$COLOR_RESET] Checking$MAGENTA formatting$COLOR_RESET..." if [ "$FORMAT_OK" = "1" ]; then error "Formatting check failed to run." FAILED=1 @@ -159,7 +184,7 @@ else fi wait $PID_SPELL -log "[$CYAN 2/7$COLOR_RESET] Running$MAGENTA spell check$COLOR_RESET..." +log "[$CYAN 2/$NCHECK$COLOR_RESET] Running$MAGENTA spell check$COLOR_RESET..." SPELL_RC=$(cat "$TMPDIR_PC/spell_exit" 2>/dev/null || echo "1") if [ "$SPELL_RC" = "0" ]; then ok "Spell check passed." @@ -169,7 +194,7 @@ else fi wait $PID_LINT -log "[$CYAN 3/7$COLOR_RESET] Running$MAGENTA toolchain lint$COLOR_RESET..." +log "[$CYAN 3/$NCHECK$COLOR_RESET] Running$MAGENTA toolchain lint$COLOR_RESET..." LINT_RC=$(cat "$TMPDIR_PC/lint_exit" 2>/dev/null || echo "1") if [ "$LINT_RC" = "0" ]; then ok "Toolchain lint passed." @@ -179,7 +204,7 @@ else fi wait $PID_SOURCE -log "[$CYAN 4/7$COLOR_RESET] Running$MAGENTA source lint$COLOR_RESET..." +log "[$CYAN 4/$NCHECK$COLOR_RESET] Running$MAGENTA source lint$COLOR_RESET..." SOURCE_RC=$(cat "$TMPDIR_PC/source_exit" 2>/dev/null || echo "1") if [ "$SOURCE_RC" = "0" ]; then ok "Source lint passed." @@ -188,7 +213,7 @@ else FAILED=1 fi -log "[$CYAN 5/7$COLOR_RESET] Checking$MAGENTA doc references$COLOR_RESET..." +log "[$CYAN 5/$NCHECK$COLOR_RESET] Checking$MAGENTA doc references$COLOR_RESET..." if [ $DOC_FAILED -eq 0 ]; then ok "Doc references are valid." else @@ -197,7 +222,7 @@ else fi wait $PID_PARAM_DOCS -log "[$CYAN 6/7$COLOR_RESET] Checking$MAGENTA parameter docs$COLOR_RESET..." +log "[$CYAN 6/$NCHECK$COLOR_RESET] Checking$MAGENTA parameter docs$COLOR_RESET..." PARAM_DOCS_RC=$(cat "$TMPDIR_PC/param_docs_exit" 2>/dev/null || echo "1") if [ "$PARAM_DOCS_RC" = "0" ]; then ok "Parameter documentation check passed." @@ -206,8 +231,18 @@ else FAILED=1 fi +wait $PID_GENERATE +log "[$CYAN 7/$NCHECK$COLOR_RESET] Checking$MAGENTA generated files$COLOR_RESET..." +GENERATE_RC=$(cat "$TMPDIR_PC/generate_exit" 2>/dev/null || echo "1") +if [ "$GENERATE_RC" = "0" ]; then + ok "Generated files are up to date." +else + error "Generated files are out of date. Run$MAGENTA ./mfc.sh generate$COLOR_RESET to update." + FAILED=1 +fi + wait $PID_EXAMPLES -log "[$CYAN 7/7$COLOR_RESET] Validating$MAGENTA example cases$COLOR_RESET..." +log "[$CYAN 8/$NCHECK$COLOR_RESET] Validating$MAGENTA example cases$COLOR_RESET..." EXAMPLES_FAILED=$(cat "$TMPDIR_PC/examples_exit" 2>/dev/null || echo "1") if [ "$EXAMPLES_FAILED" = "0" ]; then ok "All example cases are valid." @@ -216,6 +251,19 @@ else FAILED=1 fi +# --- Phase 3: Build (only when Fortran sources changed) --- + +if [ "$BUILD_FORTRAN" = "1" ]; then + log "[$CYAN 9/9$COLOR_RESET] Building$MAGENTA (Fortran changes detected)$COLOR_RESET..." + if ./mfc.sh build -j "$JOBS" > "$TMPDIR_PC/build_out" 2>&1; then + ok "Build passed." + else + error "Build failed. Output:" + cat "$TMPDIR_PC/build_out" | tail -20 + FAILED=1 + fi +fi + echo "" if [ $FAILED -eq 0 ]; then From c684a130ac6e5a13a90deb48f84dda17834da88f Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Thu, 28 May 2026 10:22:57 -0400 Subject: [PATCH 22/33] =?UTF-8?q?fix(precheck):=20remove=20build=20step=20?= =?UTF-8?q?=E2=80=94=20compilation=20takes=20too=20long=20for=20pre-commit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- toolchain/bootstrap/precheck.sh | 28 +--------------------------- 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/toolchain/bootstrap/precheck.sh b/toolchain/bootstrap/precheck.sh index dea3fdc4f2..50cd92f324 100755 --- a/toolchain/bootstrap/precheck.sh +++ b/toolchain/bootstrap/precheck.sh @@ -59,20 +59,7 @@ done # CI runs the full suite via ./mfc.sh lint without this variable. export MFC_SKIP_RENDER_TESTS=1 -# Detect whether any Fortran sources or CMakeLists changed — if so, run a build. -# This catches compilation errors (duplicate declarations, missing symbols, etc.) -# cheaply: Python-only changes pay zero build cost. -if git diff HEAD --name-only 2>/dev/null | grep -qE '\.(fpp|f90)$|CMakeLists\.txt'; then - BUILD_FORTRAN=1 -else - BUILD_FORTRAN=0 -fi - -if [ "$BUILD_FORTRAN" = "1" ]; then - NCHECK=9 -else - NCHECK=8 -fi +NCHECK=8 log "Running$MAGENTA precheck$COLOR_RESET (same checks as CI lint-gate)..." echo "" @@ -251,19 +238,6 @@ else FAILED=1 fi -# --- Phase 3: Build (only when Fortran sources changed) --- - -if [ "$BUILD_FORTRAN" = "1" ]; then - log "[$CYAN 9/9$COLOR_RESET] Building$MAGENTA (Fortran changes detected)$COLOR_RESET..." - if ./mfc.sh build -j "$JOBS" > "$TMPDIR_PC/build_out" 2>&1; then - ok "Build passed." - else - error "Build failed. Output:" - cat "$TMPDIR_PC/build_out" | tail -20 - FAILED=1 - fi -fi - echo "" if [ $FAILED -eq 0 ]; then From ea3bb93ff0580cd195349db8e4e046d5abec6ef6 Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Thu, 28 May 2026 10:30:21 -0400 Subject: [PATCH 23/33] ci: trigger fresh CI run From 999cde9c1d41ba033ac4161ad6a55d1d34f6e4b5 Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Thu, 28 May 2026 10:37:44 -0400 Subject: [PATCH 24/33] =?UTF-8?q?fix(precheck):=20remove=20generate=20--ch?= =?UTF-8?q?eck=20=E2=80=94=20platform-dependent=20output=20breaks=20CI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- toolchain/bootstrap/precheck.sh | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/toolchain/bootstrap/precheck.sh b/toolchain/bootstrap/precheck.sh index 50cd92f324..bcbf6d3573 100755 --- a/toolchain/bootstrap/precheck.sh +++ b/toolchain/bootstrap/precheck.sh @@ -59,7 +59,7 @@ done # CI runs the full suite via ./mfc.sh lint without this variable. export MFC_SKIP_RENDER_TESTS=1 -NCHECK=8 +NCHECK=7 log "Running$MAGENTA precheck$COLOR_RESET (same checks as CI lint-gate)..." echo "" @@ -129,16 +129,6 @@ fi ) & PID_PARAM_DOCS=$! -# Generated files check (JSON schema, completions, docs) -( - if ./mfc.sh generate --check > /dev/null 2>&1; then - echo "0" > "$TMPDIR_PC/generate_exit" - else - echo "1" > "$TMPDIR_PC/generate_exit" - fi -) & -PID_GENERATE=$! - # Example case validation ( failed=0 @@ -218,18 +208,8 @@ else FAILED=1 fi -wait $PID_GENERATE -log "[$CYAN 7/$NCHECK$COLOR_RESET] Checking$MAGENTA generated files$COLOR_RESET..." -GENERATE_RC=$(cat "$TMPDIR_PC/generate_exit" 2>/dev/null || echo "1") -if [ "$GENERATE_RC" = "0" ]; then - ok "Generated files are up to date." -else - error "Generated files are out of date. Run$MAGENTA ./mfc.sh generate$COLOR_RESET to update." - FAILED=1 -fi - wait $PID_EXAMPLES -log "[$CYAN 8/$NCHECK$COLOR_RESET] Validating$MAGENTA example cases$COLOR_RESET..." +log "[$CYAN 7/$NCHECK$COLOR_RESET] Validating$MAGENTA example cases$COLOR_RESET..." EXAMPLES_FAILED=$(cat "$TMPDIR_PC/examples_exit" 2>/dev/null || echo "1") if [ "$EXAMPLES_FAILED" = "0" ]; then ok "All example cases are valid." From 4901c323d4107b8a5dff741f9b9a3ecf271509f8 Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Thu, 28 May 2026 13:43:53 -0400 Subject: [PATCH 25/33] =?UTF-8?q?fix(gen):=20use=20#:if/#:else=20guard=20f?= =?UTF-8?q?or=20case-opt=20namelist=20=E2=80=94=20prevents=20dangling=20co?= =?UTF-8?q?ntinuation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- toolchain/mfc/params/generators/fortran_gen.py | 17 ++++++++++++++--- toolchain/mfc/params_tests/test_fortran_gen.py | 10 +++++++++- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/toolchain/mfc/params/generators/fortran_gen.py b/toolchain/mfc/params/generators/fortran_gen.py index 8173d54700..02208540b9 100644 --- a/toolchain/mfc/params/generators/fortran_gen.py +++ b/toolchain/mfc/params/generators/fortran_gen.py @@ -87,9 +87,20 @@ def generate_namelist_fpp(target: str) -> str: normal = [v for v in all_vars if v not in CASE_OPT_PARAMS] opt = sorted(v for v in CASE_OPT_PARAMS if v in NAMELIST_VARS and "sim" in NAMELIST_VARS[v]) nl_lines = _pack_namelist(normal, _FIRST_PREFIX, _CONT_PREFIX, _MAX_LINE) - nl_lines[-1] += ", &" - opt_lines = _pack_namelist(opt, _CONT_PREFIX, _CONT2_PREFIX, _MAX_LINE) - parts = [_HEADER.rstrip()] + nl_lines + ["#:if not MFC_CASE_OPTIMIZATION"] + opt_lines + ["#:endif"] + if opt: + opt_lines = _pack_namelist(opt, _CONT_PREFIX, _CONT2_PREFIX, _MAX_LINE) + nl_with_cont = nl_lines[:] + nl_with_cont[-1] += ", &" + parts = ( + [_HEADER.rstrip(), "#:if MFC_CASE_OPTIMIZATION"] + + nl_lines + + ["#:else"] + + nl_with_cont + + opt_lines + + ["#:endif"] + ) + else: + parts = [_HEADER.rstrip()] + nl_lines return "\n".join(parts) + "\n" diff --git a/toolchain/mfc/params_tests/test_fortran_gen.py b/toolchain/mfc/params_tests/test_fortran_gen.py index cd43055c65..409a56c055 100644 --- a/toolchain/mfc/params_tests/test_fortran_gen.py +++ b/toolchain/mfc/params_tests/test_fortran_gen.py @@ -61,9 +61,17 @@ def test_sim_namelist_case_opt_guard(): from mfc.params.generators.fortran_gen import generate_namelist_fpp c = generate_namelist_fpp("sim") - assert "#:if not MFC_CASE_OPTIMIZATION" in c + # Case-opt guard: two complete namelist statements wrapped in #:if/#:else/#:endif + assert "#:if MFC_CASE_OPTIMIZATION" in c + assert "#:else" in c + assert "#:endif" in c assert "weno_order" in c assert "num_fluids" in c + # No dangling continuation before the #:if block or after #:else + lines = c.splitlines() + for i, line in enumerate(lines): + if line.strip() in ("#:if MFC_CASE_OPTIMIZATION", "#:endif"): + assert not lines[i - 1].rstrip().endswith("&"), f"Dangling & before {line!r}" def test_pre_namelist_has_patch_icpp(): From b3cf25e37618d1a9e83c0dce5db6444c6f60c786 Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Thu, 28 May 2026 15:36:40 -0400 Subject: [PATCH 26/33] fix(review): 6 issues from PR self-review - completion_gen: fix zsh --gpu completion ('acc:mp' -> 'acc mp') - case_validator: move recon_type prohibit to check_parameter_types (was duplicated 4x) - CMakeLists: remove nonexistent namelist_targets.py stamp watch; add registry.py + schema.py - fortran_gen: guard nl_with_cont[-1] against empty normal list - docs_gen: use .get() for REGISTRY lookups to avoid KeyError on unknown params --- CMakeLists.txt | 3 ++- toolchain/mfc/case_validator.py | 7 +++---- toolchain/mfc/cli/completion_gen.py | 2 +- toolchain/mfc/params/generators/docs_gen.py | 12 +++++++----- toolchain/mfc/params/generators/fortran_gen.py | 2 +- 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9c36df54c9..82037d7053 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -468,8 +468,9 @@ set(_mfc_gen_stamp "${CMAKE_BINARY_DIR}/mfc_params_gen.stamp") set(_mfc_gen_inputs "${CMAKE_CURRENT_SOURCE_DIR}/toolchain/mfc/params/generators/cmake_gen.py" "${CMAKE_CURRENT_SOURCE_DIR}/toolchain/mfc/params/generators/fortran_gen.py" - "${CMAKE_CURRENT_SOURCE_DIR}/toolchain/mfc/params/namelist_targets.py" "${CMAKE_CURRENT_SOURCE_DIR}/toolchain/mfc/params/definitions.py" + "${CMAKE_CURRENT_SOURCE_DIR}/toolchain/mfc/params/registry.py" + "${CMAKE_CURRENT_SOURCE_DIR}/toolchain/mfc/params/schema.py" ) set(_mfc_needs_regen FALSE) if(NOT EXISTS "${_mfc_gen_stamp}") diff --git a/toolchain/mfc/case_validator.py b/toolchain/mfc/case_validator.py index 950dbdce42..cd2af49a64 100644 --- a/toolchain/mfc/case_validator.py +++ b/toolchain/mfc/case_validator.py @@ -260,10 +260,7 @@ def _check_order_fits_grid(self, order: int, param_name: str) -> None: self.prohibit(p > 0 and p + 1 < order, f"For 3D: p must be at least {param_name} - 1 (= {order - 1})") def _get_recon_type(self) -> int: - """Return recon_type after validating it is 1 (WENO) or 2 (MUSCL).""" - recon_type = self.get("recon_type", 1) - self.prohibit(recon_type not in [1, 2], "recon_type must be 1 (WENO) or 2 (MUSCL)") - return recon_type + return self.get("recon_type", 1) def check_parameter_types(self): """Validate parameter types before other checks. @@ -280,6 +277,8 @@ def check_parameter_types(self): if param in self.params: # Only validate params that are set self._validate_logical(param) + self.prohibit(self.get("recon_type", 1) not in [1, 2], "recon_type must be 1 (WENO) or 2 (MUSCL)") + # Required domain parameters when m > 0 m = self.get("m") if m is not None and m > 0: diff --git a/toolchain/mfc/cli/completion_gen.py b/toolchain/mfc/cli/completion_gen.py index 84fbe7e0e1..4f74e48e3e 100644 --- a/toolchain/mfc/cli/completion_gen.py +++ b/toolchain/mfc/cli/completion_gen.py @@ -28,7 +28,7 @@ def _mfc_config_zsh_flags() -> List[str]: from ..state import MFCConfig, gpuConfigOptions specs = [] - modes = ":".join(e.value for e in gpuConfigOptions if e.value != gpuConfigOptions.NONE.value) + modes = " ".join(e.value for e in gpuConfigOptions if e.value != gpuConfigOptions.NONE.value) for f in dataclasses.fields(MFCConfig): cli = f.name.replace("_", "-") label = cli.replace("-", " ").title() diff --git a/toolchain/mfc/params/generators/docs_gen.py b/toolchain/mfc/params/generators/docs_gen.py index 3974353482..6a6bfa4170 100644 --- a/toolchain/mfc/params/generators/docs_gen.py +++ b/toolchain/mfc/params/generators/docs_gen.py @@ -463,8 +463,8 @@ def generate_parameter_docs() -> str: pattern_has_symbols = False for _pattern, examples in patterns.items(): for ex in examples: - p = REGISTRY.all_params[ex] - if p.constraints or ex in by_trigger or ex in by_param: + p = REGISTRY.all_params.get(ex) + if p and (p.constraints or ex in by_trigger or ex in by_param): pattern_has_constraints = True if get_math_symbol(ex): pattern_has_symbols = True @@ -484,7 +484,8 @@ def generate_parameter_docs() -> str: for pattern, examples in sorted(patterns.items()): example = examples[0] - desc = REGISTRY.all_params[example].description + _ep = REGISTRY.all_params.get(example) + desc = _ep.description if _ep else "" # Truncate long descriptions if len(desc) > 60: desc = desc[:57] + "..." @@ -497,8 +498,9 @@ def generate_parameter_docs() -> str: sym = get_math_symbol(example) row += f" | {sym}" if pattern_has_constraints: - p = REGISTRY.all_params[example] - row += f" | {_format_constraints_cell(example, p, by_trigger, by_param)}" + p = REGISTRY.all_params.get(example) + if p: + row += f" | {_format_constraints_cell(example, p, by_trigger, by_param)}" lines.append(row + " |") lines.append("") diff --git a/toolchain/mfc/params/generators/fortran_gen.py b/toolchain/mfc/params/generators/fortran_gen.py index 02208540b9..cc823f1f25 100644 --- a/toolchain/mfc/params/generators/fortran_gen.py +++ b/toolchain/mfc/params/generators/fortran_gen.py @@ -87,7 +87,7 @@ def generate_namelist_fpp(target: str) -> str: normal = [v for v in all_vars if v not in CASE_OPT_PARAMS] opt = sorted(v for v in CASE_OPT_PARAMS if v in NAMELIST_VARS and "sim" in NAMELIST_VARS[v]) nl_lines = _pack_namelist(normal, _FIRST_PREFIX, _CONT_PREFIX, _MAX_LINE) - if opt: + if opt and nl_lines: opt_lines = _pack_namelist(opt, _CONT_PREFIX, _CONT2_PREFIX, _MAX_LINE) nl_with_cont = nl_lines[:] nl_with_cont[-1] += ", &" From 29f083630eab5f7859ddfc2864afbb95b63443b0 Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Thu, 28 May 2026 15:53:37 -0400 Subject: [PATCH 27/33] style: collapse parts= list to one line (ruff) --- toolchain/mfc/params/generators/fortran_gen.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/toolchain/mfc/params/generators/fortran_gen.py b/toolchain/mfc/params/generators/fortran_gen.py index cc823f1f25..8e57fb9a8b 100644 --- a/toolchain/mfc/params/generators/fortran_gen.py +++ b/toolchain/mfc/params/generators/fortran_gen.py @@ -91,14 +91,7 @@ def generate_namelist_fpp(target: str) -> str: opt_lines = _pack_namelist(opt, _CONT_PREFIX, _CONT2_PREFIX, _MAX_LINE) nl_with_cont = nl_lines[:] nl_with_cont[-1] += ", &" - parts = ( - [_HEADER.rstrip(), "#:if MFC_CASE_OPTIMIZATION"] + - nl_lines + - ["#:else"] + - nl_with_cont + - opt_lines + - ["#:endif"] - ) + parts = [_HEADER.rstrip(), "#:if MFC_CASE_OPTIMIZATION"] + nl_lines + ["#:else"] + nl_with_cont + opt_lines + ["#:endif"] else: parts = [_HEADER.rstrip()] + nl_lines return "\n".join(parts) + "\n" From 0fada8fc44eab495445a0f03ab60b4bbaf5250ae Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Thu, 28 May 2026 16:02:06 -0400 Subject: [PATCH 28/33] chore(deps): pin ruff==0.6.5; bump ffmt to 0.4.1 from master --- toolchain/mfc/lint_source.py | 5 +---- toolchain/mfc/viz/interactive.py | 1 - toolchain/pyproject.toml | 2 +- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/toolchain/mfc/lint_source.py b/toolchain/mfc/lint_source.py index 7800bd986e..59645ebd5a 100644 --- a/toolchain/mfc/lint_source.py +++ b/toolchain/mfc/lint_source.py @@ -185,10 +185,7 @@ def check_double_precision(repo_root: Path) -> list[str]: errors: list[str] = [] src_dir = repo_root / SRC_DIR precision_re = re.compile( - r"\b(?:double_precision|double\s+precision|dsqrt|dexp|dlog|dble|dabs|" - r"dprod|dmin|dmax|dfloat|dreal|dcos|dsin|dtan|dsign|dtanh|dsinh|dcosh)\b|" - r"\breal\s*\(\s*[48]\s*\)|" - r"[0-9]d0", + r"\b(?:double_precision|double\s+precision|dsqrt|dexp|dlog|dble|dabs|" r"dprod|dmin|dmax|dfloat|dreal|dcos|dsin|dtan|dsign|dtanh|dsinh|dcosh)\b|" r"\breal\s*\(\s*[48]\s*\)|" r"[0-9]d0", re.IGNORECASE, ) diff --git a/toolchain/mfc/viz/interactive.py b/toolchain/mfc/viz/interactive.py index 4eaa5785b5..54ac5887de 100644 --- a/toolchain/mfc/viz/interactive.py +++ b/toolchain/mfc/viz/interactive.py @@ -1693,7 +1693,6 @@ def _update( overlay_vol_nsurf, playing_st, ): - _t0 = time.perf_counter() _GRAPH_SHOW = {"height": "100vh", "display": "block"} _GRAPH_HIDE = {"height": "100vh", "display": "none"} diff --git a/toolchain/pyproject.toml b/toolchain/pyproject.toml index b593598c9c..b814a5070d 100644 --- a/toolchain/pyproject.toml +++ b/toolchain/pyproject.toml @@ -23,7 +23,7 @@ dependencies = [ # Code Health "typos", - "ruff", + "ruff==0.6.5", "ffmt==0.4.1", "ansi2txt", "pytest", From 8e0a394c6aa4cdd24035efdd9e47c38102f935f6 Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Thu, 28 May 2026 16:07:07 -0400 Subject: [PATCH 29/33] docs: update parameter-addition rule from 4-location to 2-location Fortran decls and namelist bindings are now auto-generated. Simple scalar params only need definitions.py (_r + _nv) + cmake reconfigure. --- .claude/rules/common-pitfalls.md | 2 +- .claude/rules/parameter-system.md | 22 ++++++++++++---------- CLAUDE.md | 11 ++++++----- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/.claude/rules/common-pitfalls.md b/.claude/rules/common-pitfalls.md index 858d52a1f8..8f3a0edf43 100644 --- a/.claude/rules/common-pitfalls.md +++ b/.claude/rules/common-pitfalls.md @@ -59,7 +59,7 @@ Before submitting a PR: - [ ] `./mfc.sh precheck -j 8` (5 CI lint checks) - [ ] `./mfc.sh build -j 8` (compiles) - [ ] `./mfc.sh test --only -j 8` (tests pass) -- [ ] If adding parameters: all 4 locations updated +- [ ] If adding parameters: definitions.py (_r + _nv) updated; cmake reconfigured; case_validator.py if constraints - [ ] If modifying `src/common/`: all three targets tested - [ ] If changing output: golden files regenerated for affected tests - [ ] One logical change per commit diff --git a/.claude/rules/parameter-system.md b/.claude/rules/parameter-system.md index 28f020090a..d3f6ea99cf 100644 --- a/.claude/rules/parameter-system.md +++ b/.claude/rules/parameter-system.md @@ -23,20 +23,22 @@ MFC has ~3,400 simulation parameters defined in Python and read by Fortran via n - Reads `&user_inputs` namelist - Each parameter must be declared in the namelist statement -## Adding a New Parameter (4-location checklist) +## Adding a New Parameter (2-location checklist) -YOU MUST update the first 3 locations. Missing any causes silent failures or compile errors. -Location 4 is required only if the parameter has physics constraints. +Fortran declarations and namelist bindings are now auto-generated from definitions.py +at CMake configure time — no manual Fortran edits needed for simple scalar parameters. -1. **`toolchain/mfc/params/definitions.py`**: Add parameter with type, default, constraints -2. **`src/*/m_global_parameters.fpp`**: Declare the Fortran variable in the relevant - target(s). If the param is used by simulation only, add it there. If shared, add to - all three targets' m_global_parameters.fpp. -3. **`src/*/m_start_up.fpp`**: Add to the Fortran `namelist` declaration in the relevant - target(s). -4. **`toolchain/mfc/case_validator.py`**: Add validation rules if the parameter has +1. **`toolchain/mfc/params/definitions.py`**: Add parameter with `_r()` (type, default, + constraints) AND add it to `NAMELIST_VARS` via `_nv()` for the relevant target(s). + After editing, re-run cmake (or `./mfc.sh build`) to regenerate the Fortran includes. +2. **`toolchain/mfc/case_validator.py`**: Add validation rules if the parameter has physics constraints. Include `PHYSICS_DOCS` entry with title, category, explanation. +**Exceptions — still require manual Fortran edits:** +- Array variables (e.g. `logical, dimension(num_fluids_max)`) → declare in `src/*/m_global_parameters.fpp` +- Derived-type members (`fluid_pp%attr`, `patch_icpp(i)%attr`) → declare in the relevant derived type +- Case-optimization parameters → add to `CASE_OPT_PARAMS` and the `#:else` block in `src/simulation/m_global_parameters.fpp` + ## Case Files - Case files are Python scripts (`.py`) that define a dict of parameters - Validated with `./mfc.sh validate case.py` diff --git a/CLAUDE.md b/CLAUDE.md index 928ca9ce27..497e0815e7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -139,11 +139,12 @@ NEVER use `stop` or `error stop`. Use `call s_mpi_abort()` or `@:PROHIBIT()`/`@: NEVER use `goto`, `COMMON` blocks, or global `save` variables. Every `@:ALLOCATE(...)` MUST have a matching `@:DEALLOCATE(...)`. -Every new parameter MUST be added in at least 3 places (4 if it has constraints): - 1. `toolchain/mfc/params/definitions.py` (parameter definition) - 2. Fortran variable declaration in `src/*/m_global_parameters.fpp` - 3. Fortran namelist in `src/*/m_start_up.fpp` (namelist binding) - 4. `toolchain/mfc/case_validator.py` (only if parameter has physics constraints) +Every new parameter MUST be added in at least 2 places (3 if it has constraints): + 1. `toolchain/mfc/params/definitions.py` (parameter definition + NAMELIST_VARS target set) + 2. `toolchain/mfc/case_validator.py` (only if parameter has physics constraints) + Note: Fortran declarations and namelist bindings are auto-generated from definitions.py + at CMake configure time. Simple scalars need no manual Fortran edits. Array/derived-type + variables still require a manual declaration in `src/*/m_global_parameters.fpp`. Changes to `src/common/` affect ALL three executables. Test comprehensively. From 27476a7596897dd34b1beec417f1684941a9ed05 Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Thu, 28 May 2026 16:12:50 -0400 Subject: [PATCH 30/33] fix(cmake): replace hand-rolled _mfc_gen_inputs list with GLOB_RECURSE --- CMakeLists.txt | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 82037d7053..83bbb8fe0e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -465,12 +465,8 @@ endmacro() # include directories before HANDLE_SOURCES globs them for Fypp. find_package(Python3 REQUIRED COMPONENTS Interpreter) set(_mfc_gen_stamp "${CMAKE_BINARY_DIR}/mfc_params_gen.stamp") -set(_mfc_gen_inputs - "${CMAKE_CURRENT_SOURCE_DIR}/toolchain/mfc/params/generators/cmake_gen.py" - "${CMAKE_CURRENT_SOURCE_DIR}/toolchain/mfc/params/generators/fortran_gen.py" - "${CMAKE_CURRENT_SOURCE_DIR}/toolchain/mfc/params/definitions.py" - "${CMAKE_CURRENT_SOURCE_DIR}/toolchain/mfc/params/registry.py" - "${CMAKE_CURRENT_SOURCE_DIR}/toolchain/mfc/params/schema.py" +file(GLOB_RECURSE _mfc_gen_inputs + "${CMAKE_CURRENT_SOURCE_DIR}/toolchain/mfc/params/*.py" ) set(_mfc_needs_regen FALSE) if(NOT EXISTS "${_mfc_gen_stamp}") From 0c5f183df530c8ef624d9db2369611c9af165171 Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Thu, 28 May 2026 16:31:12 -0400 Subject: [PATCH 31/33] feat(codegen): auto-generate array declarations via FORTRAN_ARRAY_DIMS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds fortran_dim support for indexed-family array parameters. fluid_rho (pre) and 9 output arrays (post) no longer need manual Fortran declarations — all are generated from definitions.py. Remaining manual declarations are exclusively Fortran derived-type variables (bc_x, fluid_pp, patch_icpp, etc.) which require type information that lives in m_derived_types.fpp. --- src/post_process/m_global_parameters.fpp | 22 ++++--------------- src/pre_process/m_global_parameters.fpp | 7 +++--- toolchain/mfc/params/definitions.py | 16 ++++++++++++++ .../mfc/params/generators/fortran_gen.py | 17 +++++++++----- 4 files changed, 34 insertions(+), 28 deletions(-) diff --git a/src/post_process/m_global_parameters.fpp b/src/post_process/m_global_parameters.fpp index 42eb83b98b..b9673f354c 100644 --- a/src/post_process/m_global_parameters.fpp +++ b/src/post_process/m_global_parameters.fpp @@ -135,24 +135,10 @@ module m_global_parameters type(int_bounds_info) :: offset_x, offset_y, offset_z !> @} - !> @name The list of all possible flow variables that may be written to a database file. It includes partial densities, density, - !! momentum, velocity, energy, pressure, volume fraction(s), specific heat ratio function, specific heat ratio, liquid stiffness - !! function, liquid stiffness, primitive variables, conservative variables, speed of sound, the vorticity, and the numerical - !! Schlieren function. - !> @{ - logical, dimension(num_fluids_max) :: alpha_rho_wrt - logical, dimension(3) :: mom_wrt - logical, dimension(3) :: vel_wrt - logical, dimension(3) :: flux_wrt - logical, dimension(num_fluids_max) :: alpha_rho_e_wrt - logical, dimension(num_fluids_max) :: alpha_wrt - logical, dimension(3) :: omega_wrt - logical :: chem_wrt_Y(1:num_species) - !> @} - - real(wp), dimension(num_fluids_max) :: schlieren_alpha !< Per-fluid Schlieren intensity amplitude coefficients - integer :: fd_number !< Finite-difference half-stencil size: MAX(1, fd_order/2) - type(chemistry_parameters) :: chem_params + ! alpha_rho_wrt, mom_wrt, vel_wrt, flux_wrt, alpha_rho_e_wrt, alpha_wrt, + ! omega_wrt, chem_wrt_Y, schlieren_alpha: auto-generated in generated_decls.fpp + integer :: fd_number !< Finite-difference half-stencil size: MAX(1, fd_order/2) + type(chemistry_parameters) :: chem_params !> @name Bubble modeling variables and parameters !> @{ real(wp) :: Eu diff --git a/src/pre_process/m_global_parameters.fpp b/src/pre_process/m_global_parameters.fpp index d1388effee..8f45ee9733 100644 --- a/src/pre_process/m_global_parameters.fpp +++ b/src/pre_process/m_global_parameters.fpp @@ -65,10 +65,9 @@ module m_global_parameters integer, dimension(3, 2) :: shear_BC_flip_indices !< Shear stress BC reflection indices (1:3, 1:shear_BC_flip_num) type(simplex_noise_params) :: simplex_params - ! Perturb density of surrounding air so as to break symmetry of grid - real(wp), dimension(num_fluids_max) :: fluid_rho - integer, allocatable, dimension(:) :: proc_coords !< Processor coordinates in MPI_CART_COMM - integer, allocatable, dimension(:) :: start_idx !< Starting cell-center index of local processor in global grid + ! Perturb density of surrounding air so as to break symmetry of grid fluid_rho: auto-generated in generated_decls.fpp + integer, allocatable, dimension(:) :: proc_coords !< Processor coordinates in MPI_CART_COMM + integer, allocatable, dimension(:) :: start_idx !< Starting cell-center index of local processor in global grid #ifdef MFC_MPI type(mpi_io_var), public :: MPI_IO_DATA character(LEN=name_len) :: mpiiofs diff --git a/toolchain/mfc/params/definitions.py b/toolchain/mfc/params/definitions.py index d1a3f6a372..bccf5f08d8 100644 --- a/toolchain/mfc/params/definitions.py +++ b/toolchain/mfc/params/definitions.py @@ -1029,6 +1029,22 @@ def _init_registry(): NAMELIST_VARS: dict[str, set[str]] = {} +# Maps indexed-family base names to their Fortran dimension expression. +# The generator emits `{type}, dimension({dim}) :: {name}` for each entry. +# Add here whenever a new array param needs no manual Fortran declaration. +FORTRAN_ARRAY_DIMS: dict[str, str] = { + "fluid_rho": "num_fluids_max", # pre + "alpha_rho_wrt": "num_fluids_max", # post + "alpha_rho_e_wrt": "num_fluids_max", # post + "alpha_wrt": "num_fluids_max", # post + "schlieren_alpha": "num_fluids_max", # post + "chem_wrt_Y": "num_species", # post (imported from m_thermochem) + "flux_wrt": "3", # post + "mom_wrt": "3", # post + "omega_wrt": "3", # post + "vel_wrt": "3", # post +} + def _nv(targets: set, *names: str) -> None: for n in names: diff --git a/toolchain/mfc/params/generators/fortran_gen.py b/toolchain/mfc/params/generators/fortran_gen.py index 8e57fb9a8b..7c5ec4210d 100644 --- a/toolchain/mfc/params/generators/fortran_gen.py +++ b/toolchain/mfc/params/generators/fortran_gen.py @@ -3,7 +3,7 @@ from pathlib import Path from typing import List, Tuple -from ..definitions import CASE_OPT_PARAMS, NAMELIST_VARS # noqa: F401 - triggers registry population +from ..definitions import CASE_OPT_PARAMS, FORTRAN_ARRAY_DIMS, NAMELIST_VARS # noqa: F401 - triggers registry population from ..registry import REGISTRY from ..schema import ParamDef, ParamType @@ -15,7 +15,8 @@ _FIRST_PREFIX = "namelist /user_inputs/ " _CONT_PREFIX = " & " _CONT2_PREFIX = " & " # inside #:if block -_DECL_COL = 24 # '::' column, matches ffmt alignment +_DECL_COL = 24 # '::' column for scalars, matches ffmt alignment +_ARRAY_DECL_COL = 36 # '::' column for array decls _FORTRAN_TYPES = { ParamType.INT: "integer", @@ -98,20 +99,24 @@ def generate_namelist_fpp(target: str) -> str: def generate_decls_fpp(target: str) -> str: - """Return simple scalar Fortran declarations for a target as a string.""" + """Return Fortran declarations (scalars + known arrays) for a target.""" assert target in ("pre", "sim", "post") lines = [_HEADER.rstrip()] for name in _vars_for_target(target): if not _is_simple_scalar(name): continue - # Skip sim case-opt params: declared as compile-time parameters by the - # manual #:if MFC_CASE_OPTIMIZATION / #:else block in m_global_parameters.fpp if target == "sim" and name in CASE_OPT_PARAMS: continue + if name in FORTRAN_ARRAY_DIMS: + member = REGISTRY.all_params.get(f"{name}(1)") + if member is not None: + ftype = fortran_type_decl(member) + dim = FORTRAN_ARRAY_DIMS[name] + lines.append(f"{(ftype + ', dimension(' + dim + ')').ljust(_ARRAY_DECL_COL)}:: {name}") + continue param = REGISTRY.all_params.get(name) if param is None: continue - # Skip if also registered as an indexed family — Fortran declares it as an array. if any(k.startswith(f"{name}(") for k in REGISTRY.all_params): continue lines.append(f"{fortran_type_decl(param).ljust(_DECL_COL)}:: {name}") From 46d3584ba956e3091de9ba26543141b46a93b144 Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Thu, 28 May 2026 18:06:17 -0400 Subject: [PATCH 32/33] refactor(fortran_gen): harden errors, improve readability, add tests --- toolchain/mfc/params/definitions.py | 20 ++++----- .../mfc/params/generators/fortran_gen.py | 44 +++++++++++++++---- .../mfc/params_tests/test_fortran_gen.py | 26 +++++++++++ 3 files changed, 71 insertions(+), 19 deletions(-) diff --git a/toolchain/mfc/params/definitions.py b/toolchain/mfc/params/definitions.py index bccf5f08d8..ecdb728644 100644 --- a/toolchain/mfc/params/definitions.py +++ b/toolchain/mfc/params/definitions.py @@ -1033,16 +1033,16 @@ def _init_registry(): # The generator emits `{type}, dimension({dim}) :: {name}` for each entry. # Add here whenever a new array param needs no manual Fortran declaration. FORTRAN_ARRAY_DIMS: dict[str, str] = { - "fluid_rho": "num_fluids_max", # pre - "alpha_rho_wrt": "num_fluids_max", # post - "alpha_rho_e_wrt": "num_fluids_max", # post - "alpha_wrt": "num_fluids_max", # post - "schlieren_alpha": "num_fluids_max", # post - "chem_wrt_Y": "num_species", # post (imported from m_thermochem) - "flux_wrt": "3", # post - "mom_wrt": "3", # post - "omega_wrt": "3", # post - "vel_wrt": "3", # post + "fluid_rho": "num_fluids_max", + "alpha_rho_wrt": "num_fluids_max", + "alpha_rho_e_wrt": "num_fluids_max", + "alpha_wrt": "num_fluids_max", + "schlieren_alpha": "num_fluids_max", + "chem_wrt_Y": "num_species", + "flux_wrt": "3", + "mom_wrt": "3", + "omega_wrt": "3", + "vel_wrt": "3", } diff --git a/toolchain/mfc/params/generators/fortran_gen.py b/toolchain/mfc/params/generators/fortran_gen.py index 7c5ec4210d..054c513fe2 100644 --- a/toolchain/mfc/params/generators/fortran_gen.py +++ b/toolchain/mfc/params/generators/fortran_gen.py @@ -26,6 +26,13 @@ ParamType.LOG: "logical", } +_VALID_TARGETS = ("pre", "sim", "post") + + +def _check_target(target: str) -> None: + if target not in _VALID_TARGETS: + raise ValueError(f"Unknown target {target!r}; expected one of {_VALID_TARGETS}") + def get_namelist_var(name: str) -> str: """Return the Fortran namelist root for a parameter name.""" @@ -79,7 +86,7 @@ def _format_namelist(vars_list: List[str]) -> str: def generate_namelist_fpp(target: str) -> str: """Return the namelist /user_inputs/ statement for a target as a string.""" - assert target in ("pre", "sim", "post") + _check_target(target) all_vars = _vars_for_target(target) if target != "sim": @@ -92,7 +99,14 @@ def generate_namelist_fpp(target: str) -> str: opt_lines = _pack_namelist(opt, _CONT_PREFIX, _CONT2_PREFIX, _MAX_LINE) nl_with_cont = nl_lines[:] nl_with_cont[-1] += ", &" - parts = [_HEADER.rstrip(), "#:if MFC_CASE_OPTIMIZATION"] + nl_lines + ["#:else"] + nl_with_cont + opt_lines + ["#:endif"] + parts = ( + [_HEADER.rstrip(), "#:if MFC_CASE_OPTIMIZATION"] + + nl_lines + + ["#:else"] + + nl_with_cont + + opt_lines + + ["#:endif"] + ) else: parts = [_HEADER.rstrip()] + nl_lines return "\n".join(parts) + "\n" @@ -100,7 +114,7 @@ def generate_namelist_fpp(target: str) -> str: def generate_decls_fpp(target: str) -> str: """Return Fortran declarations (scalars + known arrays) for a target.""" - assert target in ("pre", "sim", "post") + _check_target(target) lines = [_HEADER.rstrip()] for name in _vars_for_target(target): if not _is_simple_scalar(name): @@ -109,16 +123,23 @@ def generate_decls_fpp(target: str) -> str: continue if name in FORTRAN_ARRAY_DIMS: member = REGISTRY.all_params.get(f"{name}(1)") - if member is not None: - ftype = fortran_type_decl(member) - dim = FORTRAN_ARRAY_DIMS[name] - lines.append(f"{(ftype + ', dimension(' + dim + ')').ljust(_ARRAY_DECL_COL)}:: {name}") + if member is None: + raise ValueError( + f"FORTRAN_ARRAY_DIMS[{name!r}] has no {name}(1) in the registry. " + "Register at least one indexed variant (e.g. _r(f'{name}(1)', ...))." + ) + ftype = fortran_type_decl(member) + dim = FORTRAN_ARRAY_DIMS[name] + lines.append(f"{(ftype + ', dimension(' + dim + ')').ljust(_ARRAY_DECL_COL)}:: {name}") continue param = REGISTRY.all_params.get(name) if param is None: continue if any(k.startswith(f"{name}(") for k in REGISTRY.all_params): - continue + raise ValueError( + f"{name!r} has indexed variants (e.g. {name}(1)) but is missing from " + "FORTRAN_ARRAY_DIMS. Add it there with its Fortran dimension expression." + ) lines.append(f"{fortran_type_decl(param).ljust(_DECL_COL)}:: {name}") return "\n".join(lines) + "\n" @@ -144,4 +165,9 @@ def get_generated_files(build_dir: Path) -> List[Tuple[Path, str]]: Paths match the cmake include directory structure: build_dir/include/{full_target}/generated_{namelist,decls}.fpp """ - return [(build_dir / "include" / full / f"generated_{kind}.fpp", gen(short)) for short, full in TARGETS for kind, gen in [("namelist", generate_namelist_fpp), ("decls", generate_decls_fpp)]] + result = [] + for short, full in TARGETS: + inc = build_dir / "include" / full + result.append((inc / "generated_namelist.fpp", generate_namelist_fpp(short))) + result.append((inc / "generated_decls.fpp", generate_decls_fpp(short))) + return result diff --git a/toolchain/mfc/params_tests/test_fortran_gen.py b/toolchain/mfc/params_tests/test_fortran_gen.py index 409a56c055..24e604d21f 100644 --- a/toolchain/mfc/params_tests/test_fortran_gen.py +++ b/toolchain/mfc/params_tests/test_fortran_gen.py @@ -123,6 +123,32 @@ def test_decls_case_dir(): assert "character(LEN=path_len) :: case_dir" in generate_decls_fpp(target) +def test_decls_array_dims(): + from mfc.params.generators.fortran_gen import generate_decls_fpp + + post = generate_decls_fpp("post") + assert "dimension(num_fluids_max)" in post and ":: alpha_wrt" in post + assert "dimension(num_fluids_max)" in post and ":: alpha_rho_wrt" in post + assert "dimension(3)" in post and ":: mom_wrt" in post + # Structs and families must NOT appear as bare scalar declarations + assert ":: fluid_pp" not in post + assert ":: bc_x" not in post + + pre = generate_decls_fpp("pre") + assert "dimension(num_fluids_max)" in pre and ":: fluid_rho" in pre + + +def test_check_target_raises_on_bad_target(): + import pytest + + from mfc.params.generators.fortran_gen import generate_decls_fpp, generate_namelist_fpp + + with pytest.raises(ValueError, match="Unknown target"): + generate_namelist_fpp("bad") + with pytest.raises(ValueError, match="Unknown target"): + generate_decls_fpp("bad") + + def test_get_generated_files_returns_six(): from pathlib import Path From 7923676bb090c101ab800d1a7959c53ba5bdb393 Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Thu, 28 May 2026 18:16:24 -0400 Subject: [PATCH 33/33] fix(precheck): use ruff/ffmt --check mode; fix formatting of fortran_gen.py --- toolchain/bootstrap/precheck.sh | 40 +++++-------------- .../mfc/params/generators/fortran_gen.py | 19 ++------- 2 files changed, 13 insertions(+), 46 deletions(-) diff --git a/toolchain/bootstrap/precheck.sh b/toolchain/bootstrap/precheck.sh index bcbf6d3573..16da5b8dbd 100755 --- a/toolchain/bootstrap/precheck.sh +++ b/toolchain/bootstrap/precheck.sh @@ -15,17 +15,6 @@ show_help() { exit 0 } -# Cross-platform hash function (macOS uses md5, Linux uses md5sum) -compute_hash() { - if command -v md5sum > /dev/null 2>&1; then - md5sum | cut -d' ' -f1 - elif command -v md5 > /dev/null 2>&1; then - md5 -q - else - # Fallback: use cksum if neither available - cksum | cut -d' ' -f1 - fi -} JOBS=1 @@ -68,17 +57,14 @@ echo "" TMPDIR_PC=$(mktemp -d) trap "rm -rf $TMPDIR_PC" EXIT -# --- Phase 1: Format (modifies files, must run alone) --- -BEFORE_HASH=$(git diff -- '*.f90' '*.fpp' '*.py' 2>/dev/null | compute_hash) -if ! ./mfc.sh format -j "$JOBS" > /dev/null 2>&1; then - FORMAT_OK=1 -else - AFTER_HASH=$(git diff -- '*.f90' '*.fpp' '*.py' 2>/dev/null | compute_hash) - if [ "$BEFORE_HASH" != "$AFTER_HASH" ]; then - FORMAT_OK=2 - else - FORMAT_OK=0 - fi +# --- Phase 1: Format check (non-mutating; mirrors CI's format+diff check) --- +# Use --check mode so staged-but-unformatted code is caught even if the +# working tree was already reformatted by a prior ./mfc.sh format run. +FORMAT_OK=0 +if ! ruff format --check toolchain/ examples/ benchmarks/ > /dev/null 2>&1; then + FORMAT_OK=2 +elif ! ffmt --check -j "$JOBS" src/ > /dev/null 2>&1; then + FORMAT_OK=2 fi # --- Phase 2: All fast checks in parallel (read-only) --- @@ -147,14 +133,8 @@ PID_EXAMPLES=$! FAILED=0 log "[$CYAN 1/$NCHECK$COLOR_RESET] Checking$MAGENTA formatting$COLOR_RESET..." -if [ "$FORMAT_OK" = "1" ]; then - error "Formatting check failed to run." - FAILED=1 -elif [ "$FORMAT_OK" = "2" ]; then - error "Code was not formatted. Files have been auto-formatted; review and stage the changes." - echo "" - git diff --stat -- '*.f90' '*.fpp' '*.py' 2>/dev/null || true - echo "" +if [ "$FORMAT_OK" = "2" ]; then + error "Code is not formatted. Run$MAGENTA ./mfc.sh format$COLOR_RESET and re-stage the changes." FAILED=1 else ok "Formatting check passed." diff --git a/toolchain/mfc/params/generators/fortran_gen.py b/toolchain/mfc/params/generators/fortran_gen.py index 054c513fe2..c68227035c 100644 --- a/toolchain/mfc/params/generators/fortran_gen.py +++ b/toolchain/mfc/params/generators/fortran_gen.py @@ -99,14 +99,7 @@ def generate_namelist_fpp(target: str) -> str: opt_lines = _pack_namelist(opt, _CONT_PREFIX, _CONT2_PREFIX, _MAX_LINE) nl_with_cont = nl_lines[:] nl_with_cont[-1] += ", &" - parts = ( - [_HEADER.rstrip(), "#:if MFC_CASE_OPTIMIZATION"] - + nl_lines - + ["#:else"] - + nl_with_cont - + opt_lines - + ["#:endif"] - ) + parts = [_HEADER.rstrip(), "#:if MFC_CASE_OPTIMIZATION"] + nl_lines + ["#:else"] + nl_with_cont + opt_lines + ["#:endif"] else: parts = [_HEADER.rstrip()] + nl_lines return "\n".join(parts) + "\n" @@ -124,10 +117,7 @@ def generate_decls_fpp(target: str) -> str: if name in FORTRAN_ARRAY_DIMS: member = REGISTRY.all_params.get(f"{name}(1)") if member is None: - raise ValueError( - f"FORTRAN_ARRAY_DIMS[{name!r}] has no {name}(1) in the registry. " - "Register at least one indexed variant (e.g. _r(f'{name}(1)', ...))." - ) + raise ValueError(f"FORTRAN_ARRAY_DIMS[{name!r}] has no {name}(1) in the registry. " "Register at least one indexed variant (e.g. _r(f'{name}(1)', ...)).") ftype = fortran_type_decl(member) dim = FORTRAN_ARRAY_DIMS[name] lines.append(f"{(ftype + ', dimension(' + dim + ')').ljust(_ARRAY_DECL_COL)}:: {name}") @@ -136,10 +126,7 @@ def generate_decls_fpp(target: str) -> str: if param is None: continue if any(k.startswith(f"{name}(") for k in REGISTRY.all_params): - raise ValueError( - f"{name!r} has indexed variants (e.g. {name}(1)) but is missing from " - "FORTRAN_ARRAY_DIMS. Add it there with its Fortran dimension expression." - ) + raise ValueError(f"{name!r} has indexed variants (e.g. {name}(1)) but is missing from " "FORTRAN_ARRAY_DIMS. Add it there with its Fortran dimension expression.") lines.append(f"{fortran_type_decl(param).ljust(_DECL_COL)}:: {name}") return "\n".join(lines) + "\n"