Skip to content

Commit 259fd3a

Browse files
Spencer Bryngelsonclaude
andcommitted
Fix review findings: num_ibs bound, index error messages, frozen IndexedFamily
- Add num_ibs <= 1000 check in case_validator (matches num_patches_max) - Fix _family_attr_error to distinguish invalid index from invalid attr - Make IndexedFamily a frozen dataclass - Fix overstated docstring and clarify tags field - Add iteration consistency and zero-index error tests Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 4ce4604 commit 259fd3a

6 files changed

Lines changed: 66 additions & 13 deletions

File tree

toolchain/mfc/case_validator.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,7 @@ def check_ibm(self):
560560

561561
self.prohibit(ib and n <= 0, "Immersed Boundaries do not work in 1D (requires n > 0)")
562562
self.prohibit(ib and num_ibs <= 0, "num_ibs must be >= 1 when ib is enabled")
563+
self.prohibit(ib and num_ibs > 1000, "num_ibs must be <= 1000 (num_patches_max in m_constants.fpp)")
563564
self.prohibit(not ib and num_ibs > 0, "num_ibs is set, but ib is not enabled")
564565
self.prohibit(ib_state_wrt and not ib, "ib_state_wrt requires ib to be enabled")
565566

toolchain/mfc/params/definitions.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1137,9 +1137,9 @@ def _load():
11371137
_r(f"bub_pp%{a}", REAL, {"bubbles"}, math=sym)
11381138

11391139
# patch_ib (immersed boundaries) — registered as indexed family for O(1) lookup.
1140-
# max_index is intentionally None (unlimited): the Fortran side allocates
1141-
# 1:num_ibs and the case_validator enforces num_ibs >= 1 when ib is enabled.
1142-
# Indices beyond num_ibs are harmless (ignored at runtime).
1140+
# max_index is None so the parameter registry stays compact (no enumeration).
1141+
# The Fortran-side upper bound (num_patches_max = 1000 in m_constants.fpp) is
1142+
# enforced by the case_validator check on num_ibs, not by max_index here.
11431143
_ib_tags = {"ib"}
11441144
_ib_attrs: Dict[str, tuple] = {}
11451145
for a in ["geometry", "moving_ibm"]:

toolchain/mfc/params/registry.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,20 +76,21 @@ def _resolve_family(
7676
return entry if entry is not None else None
7777

7878

79-
@dataclass
79+
@dataclass(frozen=True)
8080
class IndexedFamily:
8181
"""
8282
Template for an indexed parameter family like patch_ib(N)%attr.
8383
84-
Instead of registering patch_ib(1)%geometry through patch_ib(1000000)%geometry
85-
individually (~30M entries), we store one template with the attribute definitions
86-
and validate parameter names via pattern matching.
84+
Instead of registering every index individually (e.g., patch_ib(1)%geometry
85+
through patch_ib(N)%geometry for all attributes), we store one template and
86+
validate parameter names via pattern matching.
8787
8888
Attributes:
8989
base_name: Family prefix (e.g., "patch_ib")
9090
attrs: Mapping of attribute name to (ParamType, tags) — attribute names
9191
may include sub-indices like "vel(1)", "angles(3)".
92-
tags: Default tags applied to all attributes in this family.
92+
tags: Metadata-only tags for this family (not used in resolution;
93+
per-attribute tags in ``attrs`` are what get returned).
9394
max_index: Upper bound on the index (1-based). None = unlimited.
9495
"""
9596

toolchain/mfc/params/validate.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,23 +46,37 @@
4646

4747
def _family_attr_error(name: str) -> Optional[str]:
4848
"""
49-
Check if name matches a known family base but has an invalid attribute.
49+
Diagnose why a family-pattern param was rejected by the registry.
5050
51-
Returns a targeted error message if so, or None if it doesn't match
51+
Distinguishes three cases for known family bases:
52+
- Invalid index (0 or exceeding max_index)
53+
- Unknown attribute
54+
- Unknown family base (returns None to let caller handle)
55+
56+
Returns a targeted error message, or None if name doesn't match
5257
any family pattern.
5358
"""
5459
from .registry import _INDEXED_RE
5560

5661
m = _INDEXED_RE.match(name)
5762
if m is None:
5863
return None
59-
base, _, attr = m.groups()
64+
base, idx_str, attr = m.groups()
6065
fam = REGISTRY.families.get(base)
6166
if fam is None:
6267
return None
63-
# Known family, unknown attribute — provide targeted message
68+
69+
# Check if the problem is the index (attr is valid but index is out of range)
70+
idx = int(idx_str)
71+
if attr in fam.attrs:
72+
if idx < 1:
73+
return f"Invalid index {idx} for {base}: indices are 1-based (must be >= 1)"
74+
if fam.max_index is not None and idx > fam.max_index:
75+
return f"Index {idx} exceeds maximum ({fam.max_index}) for {base}"
76+
return None # Both attr and index look valid; shouldn't reach here
77+
78+
# Unknown attribute — provide targeted message
6479
valid = sorted(fam.attrs.keys())
65-
# Truncate long lists
6680
if len(valid) > 8:
6781
shown = ", ".join(valid[:8]) + f", ... ({len(valid)} total)"
6882
else:

toolchain/mfc/params_tests/test_registry.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,35 @@ def test_get_param_def_none_for_invalid(self):
168168
self.assertIsNone(reg.get_param_def("thing(1)%bogus"))
169169
self.assertIsNone(reg.get_param_def("unknown(1)%geom"))
170170

171+
def test_all_params_iteration_bounded(self):
172+
"""Iterating all_params should yield scalars + one example per family attr."""
173+
reg = ParamRegistry()
174+
reg.register(ParamDef(name="scalar1", param_type=ParamType.INT))
175+
reg.register(ParamDef(name="scalar2", param_type=ParamType.REAL))
176+
reg.register_family(
177+
IndexedFamily(
178+
base_name="thing",
179+
attrs={
180+
"geom": (ParamType.INT, {"tag1"}),
181+
"vel(1)": (ParamType.REAL, {"tag1"}),
182+
},
183+
tags={"tag1"},
184+
)
185+
)
186+
reg.freeze()
187+
188+
keys = list(reg.all_params)
189+
# 2 scalars + 2 family attrs (one example each at index=1)
190+
self.assertEqual(len(reg.all_params), 4)
191+
self.assertEqual(len(keys), 4)
192+
self.assertIn("scalar1", keys)
193+
self.assertIn("scalar2", keys)
194+
# Family examples use index=1
195+
self.assertIn("thing(1)%geom", keys)
196+
self.assertIn("thing(1)%vel(1)", keys)
197+
# Arbitrary indices should NOT appear in iteration
198+
self.assertNotIn("thing(42)%geom", keys)
199+
171200
def test_all_params_contains_family(self):
172201
"""all_params mapping should resolve family params via __contains__."""
173202
reg = self._make_registry_with_family()

toolchain/mfc/params_tests/test_validate.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,14 @@ def test_family_valid_attr_no_error(self):
9494
errors = check_unknown_params(params)
9595
self.assertEqual(errors, [])
9696

97+
def test_family_zero_index_gives_index_error(self):
98+
"""Zero index on valid attr should report index error, not attr error."""
99+
params = {"patch_ib(0)%geometry": 1}
100+
errors = check_unknown_params(params)
101+
self.assertEqual(len(errors), 1)
102+
self.assertIn("1-based", errors[0])
103+
self.assertNotIn("Unknown attribute", errors[0])
104+
97105
@unittest.skipUnless(RAPIDFUZZ_AVAILABLE, "rapidfuzz not installed")
98106
def test_similar_param_suggests_correction(self):
99107
"""Typo near valid param should suggest 'did you mean?'."""

0 commit comments

Comments
 (0)