diff --git a/toolchain/mfc/case_validator.py b/toolchain/mfc/case_validator.py index 1da790a90..aef17f41a 100644 --- a/toolchain/mfc/case_validator.py +++ b/toolchain/mfc/case_validator.py @@ -1796,8 +1796,28 @@ def check_grcbc(self): if grcbc_out: # Check if EITHER beg OR end is set to -8 self.prohibit(bc_beg != -8 and bc_end != -8, f"Subsonic Outflow (grcbc_out) requires bc_{dir}%beg = -8 or bc_{dir}%end = -8") + # m_cbc.fpp relaxes the outflow toward this pressure. One branch serves the beg and end + # sides alike -- both write L(adv%end), and sign(1, cbc_loc) picks the side -- so this is + # required whichever side carries the -8: + # L(adv%end) = c*(1 - Ma)*(pres - pres_out(dir))/Del_out(dir) + # Left unset it relaxes toward an undefined target, which is silent: the boundary cell simply + # walks away, and the run aborts on ICFL tens of steps later with nothing pointing at the BC. + self.prohibit( + not self.is_set(f"bc_{dir}%pres_out"), + f"bc_{dir}%pres_out must be specified when bc_{dir}%grcbc_out is enabled", + ) if grcbc_vel_out: self.prohibit(bc_beg != -8 and bc_end != -8, f"Subsonic Outflow Velocity (grcbc_vel_out) requires bc_{dir}%beg = -8 or bc_{dir}%end = -8") + # Only the NORMAL component is read here: + # L(adv%end) += rho*c^2*(1 - Ma)*(vel(dir_idx(1)) + vel_out(dir, dir_idx(1))*sign(1, cbc_loc))/Del_out(dir) + # and dir_idx(1) is 1 for x, 2 for y, 3 for z (m_cbc.fpp:942-948). The transverse components + # are read by grcbc_in's inflow branch, not by this one, so requiring them here would reject + # configurations that run correctly. + normal = {"x": 1, "y": 2, "z": 3}[dir] + self.prohibit( + not self.is_set(f"bc_{dir}%vel_out({normal})"), + f"bc_{dir}%vel_out({normal}) must be specified when bc_{dir}%grcbc_vel_out is enabled", + ) def check_probe_output(self): """Checks probe output requirements (simulation)""" diff --git a/toolchain/mfc/test_case_validator.py b/toolchain/mfc/test_case_validator.py index d6f6d585f..253a510e1 100644 --- a/toolchain/mfc/test_case_validator.py +++ b/toolchain/mfc/test_case_validator.py @@ -529,3 +529,40 @@ def test_accepts_and_requires(self): self.assertRejects({**BASE, **self.VINET, "fluid_pp(1)%mg_s2": 0.1}, "fluid_pp(1)%mg_* are only read when") for k in ("gamma", "pi_inf"): self.assertRejects({**BASE, **self.VINET, f"fluid_pp(1)%{k}": 1.0}, f"fluid_pp(1)%{k} is not read with eos = 'vinet'") + + +class TestGrcbcOutflowTargets(ConstraintTestCase): + """grcbc_out and grcbc_vel_out relax toward targets that must actually be given. + + m_cbc.fpp computes L(adv%end) = c*(1 - Ma)*(pres - pres_out(dir))/Del_out(dir), on the beg and + end sides alike, and adds only the NORMAL velocity, vel_out(dir, dir_idx(1)), where dir_idx(1) + is 1 for x, 2 for y and 3 for z. Left unset, the relaxation pulls toward an undefined target and + the boundary cell walks away: the failure surfaces tens of steps later as an ICFL abort with + nothing naming the BC. + """ + + OUT = {"bc_x%beg": -7, "bc_x%end": -8, "bc_x%grcbc_in": "T", "bc_x%grcbc_out": "T"} + + def test_grcbc_out_requires_pres_out(self): + self.assertRejects({**BASE_2D, **self.OUT}, "bc_x%pres_out must be specified") + self.assertAccepts({**BASE_2D, **self.OUT, "bc_x%pres_out": 1.0}) + + def test_grcbc_vel_out_requires_the_normal_component(self): + p = {**BASE_2D, **self.OUT, "bc_x%pres_out": 1.0, "bc_x%grcbc_vel_out": "T"} + self.assertRejects(p, "bc_x%vel_out(1) must be specified") + self.assertAccepts({**p, "bc_x%vel_out(1)": 1.0}) + + def test_transverse_components_are_not_required(self): + """vel_out(2) is transverse at an x boundary; only grcbc_in's branch reads it, so demanding + it here would reject a case that runs correctly.""" + p = {**BASE_2D, **self.OUT, "bc_x%pres_out": 1.0, "bc_x%grcbc_vel_out": "T", "bc_x%vel_out(1)": 1.0} + self.assertAccepts(p) + self.assertAccepts( + {**p, "p": 50, "bc_z%beg": -1, "bc_z%end": -1, "z_domain%beg": 0.0, "z_domain%end": 1.0, "patch_icpp(1)%z_centroid": 0.5, "patch_icpp(1)%length_z": 1.0, "patch_icpp(1)%vel(3)": 0.0} + ) + + def test_the_required_component_follows_the_direction(self): + """dir_idx(1) is 2 at a y boundary, so it is vel_out(2) that must be given, not vel_out(1).""" + y = {**BASE_2D, "bc_y%beg": -7, "bc_y%end": -8, "bc_y%grcbc_in": "T", "bc_y%grcbc_out": "T", "bc_y%pres_out": 1.0, "bc_y%grcbc_vel_out": "T"} + self.assertRejects(y, "bc_y%vel_out(2) must be specified") + self.assertAccepts({**y, "bc_y%vel_out(2)": 0.0})