diff --git a/toolchain/mfc/case_validator.py b/toolchain/mfc/case_validator.py index 0eb8fc277..de2394cdd 100644 --- a/toolchain/mfc/case_validator.py +++ b/toolchain/mfc/case_validator.py @@ -81,6 +81,14 @@ "math": r"m > 0, \quad n \geq 0, \quad p \geq 0", "explanation": ("The x-direction must have cells. Cannot have z without y. Cylindrical coordinates require odd p."), }, + "check_domain_extents": { + "title": "Domain Extents Specified", + "category": "Domain and Geometry", + "math": r"m > 0 \Rightarrow x_{\mathrm{beg}}, x_{\mathrm{end}} \ \mathrm{set}", + "explanation": ( + "Every dimension that has cells needs its physical extents so that the grid can be generated. Skipped on restarts (old_grid = T), where the mesh is read from the existing grid files." + ), + }, "check_patch_within_domain": { "title": "Patch Within Domain", "category": "Domain and Geometry", @@ -330,12 +338,6 @@ def check_parameter_types(self): f"recon_type must be one of {_recon_shown}", ) - # Required domain parameters when m > 0 - m = self.get("m") - if m is not None and m > 0: - self.prohibit(not self.is_set("x_domain%beg"), "x_domain%beg must be set when m > 0") - self.prohibit(not self.is_set("x_domain%end"), "x_domain%end must be set when m > 0") - # Common Checks (All Stages) def check_simulation_domain(self): @@ -1508,6 +1510,31 @@ def check_restart(self): f"num_patches must be <= {num_patches_max} (num_patches_max in m_constants.fpp)", ) + def check_domain_extents(self): + """Checks that the physical extents of every active dimension are set (pre-process) + + The grid generator needs (xyz)_domain%beg and (xyz)_domain%end for each + dimension that has cells. On a restart (old_grid = T) the mesh is read + from the existing grid files, so the extents are neither needed nor read. + """ + if self.get("old_grid", "F") == "T": + return + + m = self.get("m", 0) + if self._is_numeric(m) and m > 0: + self.prohibit(not self.is_set("x_domain%beg"), "x_domain%beg must be set when m > 0") + self.prohibit(not self.is_set("x_domain%end"), "x_domain%end must be set when m > 0") + + n = self.get("n", 0) + if self._is_numeric(n) and n > 0: + self.prohibit(not self.is_set("y_domain%beg"), "y_domain%beg must be set when n > 0") + self.prohibit(not self.is_set("y_domain%end"), "y_domain%end must be set when n > 0") + + p = self.get("p", 0) + if self._is_numeric(p) and p > 0: + self.prohibit(not self.is_set("z_domain%beg"), "z_domain%beg must be set when p > 0") + self.prohibit(not self.is_set("z_domain%end"), "z_domain%end must be set when p > 0") + def check_qbmm_pre_process(self): """Checks QBMM constraints for pre-process""" qbmm = self.get("qbmm", "F") == "T" @@ -2499,6 +2526,7 @@ def validate_pre_process(self): """Validate pre-process-specific parameters""" self.validate_common() self.check_restart() + self.check_domain_extents() self.check_qbmm_pre_process() self.check_parallel_io_pre_process() self.check_grid_stretching() diff --git a/toolchain/mfc/params_tests/test_integration.py b/toolchain/mfc/params_tests/test_integration.py index b7f4c6113..ceefc2f89 100644 --- a/toolchain/mfc/params_tests/test_integration.py +++ b/toolchain/mfc/params_tests/test_integration.py @@ -263,5 +263,40 @@ def test_validator_log_params_match_registry(self): self.assertEqual(validator_log_params, registry_log_params) +class TestDomainExtents(unittest.TestCase): + """Tests for the pre-process domain extent requirement.""" + + # 2D case with no (xyz)_domain entries + BASE = {"m": 100, "n": 50, "num_patches": 1} + + def _errors(self, params, stage): + from ..case_validator import CaseConstraintError, CaseValidator + + validator = CaseValidator(params) + try: + validator.validate(stage) + except CaseConstraintError: + pass + return [e for e in validator.errors if "domain%" in e and "must be set" in e] + + def test_missing_extents_rejected(self): + """Every dimension with cells needs its extents when generating a grid.""" + self.assertEqual(len(self._errors(self.BASE, "pre_process")), 4) + + def test_restart_does_not_need_extents(self): + """old_grid = T reads the mesh from the grid files, so the extents are not needed.""" + params = {**self.BASE, "old_grid": "T", "old_ic": "T", "t_step_old": 7000, "num_patches": 0} + + for stage in ("pre_process", "simulation", "post_process"): + with self.subTest(stage=stage): + self.assertEqual(self._errors(params, stage), []) + + def test_extents_not_required_outside_pre_process(self): + """(xyz)_domain is a pre-process parameter only.""" + for stage in ("simulation", "post_process"): + with self.subTest(stage=stage): + self.assertEqual(self._errors(self.BASE, stage), []) + + if __name__ == "__main__": unittest.main()