Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions toolchain/mfc/case_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@

from . import eos
from .common import MFCException
from .params.definitions import CONSTRAINTS
from .params.definitions import CONSTRAINTS, NAF, NB, NIB, NSM, NTS, NUM_PATCHES_MAX
from .params.eos_families import EOS_FAMILIES
from .params.namelist_parser import get_fortran_constants, get_fortran_real_constants
from .params.namelist_parser import get_fortran_real_constants
from .state import CFG

# Above this the Ensemble-Averaged Bubble Model's O(alpha) expansion, which enters the closure as
Expand Down Expand Up @@ -790,7 +790,7 @@ def check_ibm(self):
ib and num_ibs <= 0 and not has_particle_clouds,
"num_ibs must be >= 1 when ib is enabled (or specify at least one particle_cloud with num_particles > 0)",
)
num_ib_patches_max = get_fortran_constants().get("num_ib_patches_max_namelist", 50000)
num_ib_patches_max = NIB
self.prohibit(
ib and num_ibs > num_ib_patches_max,
f"num_ibs must be <= {num_ib_patches_max} (num_ib_patches_max_namelist in m_constants.fpp)",
Expand Down Expand Up @@ -996,8 +996,8 @@ def check_ibm(self):
f"particle_cloud({i}) hemisphere shell {name}-extent must lie within {name}_domain",
)

num_ib_airfoils_max = get_fortran_constants().get("num_ib_airfoils_max", 5)
num_stl_models_max = get_fortran_constants().get("num_stl_models_max", 10)
num_ib_airfoils_max = NAF
num_stl_models_max = NSM
num_stl_models = self.get("num_stl_models", 0)
self.prohibit(
num_stl_models < 0,
Expand Down Expand Up @@ -1436,7 +1436,7 @@ def check_synthetic_turbulence(self):
num_turbulent_sources = self.get("num_turbulent_sources", 0) or 0
self.prohibit(num_turbulent_sources <= 0, "num_turbulent_sources must be > 0 when synthetic_turbulence is enabled")

num_turb_sources_max = get_fortran_constants().get("num_turb_sources_max", 10)
num_turb_sources_max = NTS
self.prohibit(
num_turbulent_sources > num_turb_sources_max,
f"num_turbulent_sources must be <= {num_turb_sources_max} (num_turb_sources_max in m_constants.fpp)",
Expand Down Expand Up @@ -2011,7 +2011,7 @@ def check_restart(self):
self.prohibit(old_grid and t_step_old is None, "old_grid requires t_step_old to be set")
self.prohibit(num_patches < 0, "num_patches must be non-negative")
self.prohibit(num_patches == 0 and t_step_old is None, "num_patches must be positive for the non-restart case")
num_patches_max = get_fortran_constants().get("num_patches_max", 1000)
num_patches_max = NUM_PATCHES_MAX
self.prohibit(
num_patches > num_patches_max,
f"num_patches must be <= {num_patches_max} (num_patches_max in m_constants.fpp)",
Expand Down Expand Up @@ -2407,7 +2407,7 @@ def check_patch_physics(self):
def check_bc_patches(self):
"""Checks boundary condition patch geometry (pre-process)"""
num_bc_patches = self.get("num_bc_patches", 0)
num_bc_patches_max = get_fortran_constants().get("num_bc_patches_max", 10)
num_bc_patches_max = NB

if num_bc_patches <= 0:
return
Expand Down
2 changes: 1 addition & 1 deletion toolchain/mfc/params/definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ def _fc(name: str, default: int) -> int:
NAF = _fc("num_ib_airfoils_max", 5) # ib_airfoil (Fortran array bound)
NSM = _fc("num_stl_models_max", 10) # stl_models (Fortran array bound)
NPB = _fc("num_particle_clouds_max", 10) # particle_cloud (Fortran array bound)
NTS = _fc("num_turb_sources_max", 10) # turb_pos, synth_L (Fortran array bound)
# Enumeration limits for families not yet converted to IndexedFamily.
# These are smaller than the Fortran array bounds to keep the registry compact.
# The CONSTRAINTS dict below uses the Fortran constants for validation.
Expand Down Expand Up @@ -859,7 +860,6 @@ def _load():
_r("num_turbulent_sources", INT, {"synthetic_turbulence"})
_r("synth_U_inf", REAL, {"synthetic_turbulence"}, math=r"\f$U_\infty\f$")
NSS = _fc("num_synth_shells_max", 50)
NTS = _fc("num_turb_sources_max", 10)
for i in range(1, NSS + 1):
_r(f"synth_n_waves_per_shell({i})", INT, {"synthetic_turbulence"})
_r(f"synth_k_shell({i})", REAL, {"synthetic_turbulence"}, math=r"\f$k_s\f$")
Expand Down
48 changes: 48 additions & 0 deletions toolchain/mfc/params_tests/test_definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

import ast
import inspect
import pathlib
import re
import unittest

from ..params import REGISTRY, definitions
Expand Down Expand Up @@ -194,3 +196,49 @@ def test_fallbacks_match_m_constants(self):
for call in calls:
name, default = (ast.literal_eval(arg) for arg in call.args)
self.assertEqual(default, constants[name], f"_fc('{name}', {default}) fallback differs from m_constants.fpp")

def test_each_constant_is_resolved_once(self):
"""No constant should be resolved by two _fc() calls.

A second _fc() for the same name is a second place to edit. _load()
had its own NTS = _fc("num_turb_sources_max", 10) shadowing the
module-level one, so importing NTS elsewhere would not have given a
single source of truth.
"""
calls = [node for node in ast.walk(ast.parse(inspect.getsource(definitions))) if isinstance(node, ast.Call) and getattr(node.func, "id", None) == "_fc"]
names = [ast.literal_eval(call.args[0]) for call in calls]
duplicated = sorted({name for name in names if names.count(name) > 1})
self.assertEqual(duplicated, [], f"resolved by more than one _fc() call: {duplicated}")


class TestFortranConstantGetFallbacks(unittest.TestCase):
"""The same rule for get_fortran_constants().get(name, default) anywhere in the toolchain.

definitions.py resolves its constants through _fc(), which the test above
guards. Callers that re-resolve a constant with their own .get() default
are a second, unguarded copy of the same number: case_validator.py capped
num_ibs at 50000 while m_constants.fpp said 810000, and allowed 1000
num_patches while the Fortran array holds 10 -- too strict in one
direction and, worse, too permissive in the other. Those defaults are only
reached when src/ is absent, as in a Homebrew layout, so nothing at
development time notices they have drifted.

Prefer importing the already-resolved constant from definitions.py. This
test allows a .get() default only when it still matches the Fortran.
"""

def test_get_fallbacks_match_m_constants(self):
constants = get_fortran_constants()
self.assertTrue(constants, "m_constants.fpp could not be parsed")

toolchain = pathlib.Path(__file__).resolve().parents[2]
pattern = re.compile(r"""get_fortran_constants\(\)\.get\(\s*["'](\w+)["']\s*,\s*(\d+)\s*\)""")

stale = []
for path in sorted(toolchain.rglob("*.py")):
for name, default in pattern.findall(path.read_text(encoding="utf-8", errors="ignore")):
if name in constants and int(default) != constants[name]:
rel = path.relative_to(toolchain)
stale.append(f"{rel}: .get('{name}', {default}) != {constants[name]} in m_constants.fpp")

self.assertEqual(stale, [], "stale Fortran-constant fallbacks:\n " + "\n ".join(stale))
Loading