From d477a13ee6a42b8f173a3e4521aeb9f889d089c2 Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Sat, 12 Sep 2026 17:17:39 -0400 Subject: [PATCH 1/2] Refuse a boundary-condition patch geometry the dimensionality never applies s_apply_boundary_patches dispatches by dimensionality -- geometry 1 in 2D, geometry 2 or 3 in 3D -- and has no else. A geometry belonging to the other case falls straight through: the patch is never applied, nothing is printed, and the face silently keeps whatever bc_[xyz] gave it. A nozzle cut into a no-slip wall with a 3D geometry in a 2D case therefore stays a solid wall. The run completes, writes output, and the jet has a velocity of exactly zero for all time with no indication anything was ignored. That is how it was found. examples/2D_bc_patch_geometry carries the case with a GEOMETRY knob: geometry 1 passes as before, GEOMETRY=3 now reports patch_bc(1)%geometry must be 1 (line segment) in 2D; geometry 3 is never applied where it used to pass and then do nothing. The 3D direction is symmetric and is refused the same way. Validator only; all 182 example cases still validate. --- examples/2D_bc_patch_geometry/README.md | 44 +++++++++++ examples/2D_bc_patch_geometry/case.py | 99 +++++++++++++++++++++++++ toolchain/mfc/case_validator.py | 11 +++ 3 files changed, 154 insertions(+) create mode 100644 examples/2D_bc_patch_geometry/README.md create mode 100644 examples/2D_bc_patch_geometry/case.py diff --git a/examples/2D_bc_patch_geometry/README.md b/examples/2D_bc_patch_geometry/README.md new file mode 100644 index 000000000..d4a68a71e --- /dev/null +++ b/examples/2D_bc_patch_geometry/README.md @@ -0,0 +1,44 @@ +# Boundary-condition patch geometry has to match the dimensionality + +`s_apply_boundary_patches` (`src/pre_process/m_boundary_conditions.fpp`) dispatches by dimensionality: + +```fortran +if (p > 0) then ! 3D + if (patch_bc(i)%geometry == 2) call s_circle_bc(i, bc_type) + else if (patch_bc(i)%geometry == 3) call s_rectangle_bc(i, bc_type) +else if (n > 0) then ! 2D + if (patch_bc(i)%geometry == 1) call s_line_segment_bc(i, bc_type) +``` + +There is no `else`. A geometry belonging to the other dimensionality falls straight through: **the patch is +never applied, nothing is printed, and the face silently keeps whatever `bc_[xyz]` gave it.** + +That is quiet in the worst way. A nozzle cut into a no-slip wall with a 3D geometry in a 2D case simply stays +a solid wall — the run completes, writes output, and the jet has a velocity of exactly zero for all time with +no indication anything was ignored. + +## Running it + +``` +./mfc.sh validate examples/2D_bc_patch_geometry/case.py # geometry 1, valid in 2D +GEOMETRY=3 ./mfc.sh validate examples/2D_bc_patch_geometry/case.py # a 3D geometry in a 2D case +``` + +| | before | after | +| --- | --- | --- | +| `GEOMETRY=1` | passes | passes | +| `GEOMETRY=3` | **passes, then does nothing at run time** | `patch_bc(1)%geometry must be 1 (line segment) in 2D; geometry 3 is never applied` | + +The 3D direction is symmetric: geometry 1 in a 3D case is equally ignored, and is now equally refused. + +## A second trap in the same corner + +The case carries a second initial-condition patch a few cells thick at the inlet, and it is load-bearing: +the Dirichlet buffer is filled by pre_process from the **initial condition at the boundary face**. A domain +initialised at rest stores rest in that buffer, and the "inflow" then delivers zero for all time — again with +no warning. This is not what the validator change addresses; it is noted here because the two failures look +identical from the outside, and knowing that saves working out which one is in play. + +## Scope + +Validator only; no source change and no golden files. All 182 example cases still validate. diff --git a/examples/2D_bc_patch_geometry/case.py b/examples/2D_bc_patch_geometry/case.py new file mode 100644 index 000000000..28e0dc830 --- /dev/null +++ b/examples/2D_bc_patch_geometry/case.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +"""A 2D boundary-condition patch, with the geometry the dimensionality actually supports. + +`s_apply_boundary_patches` dispatches by dimensionality: geometry 1 (line segment) in 2D, geometry 2 (circle) +or 3 (rectangle) in 3D. A geometry belonging to the other case falls straight through the dispatch -- the +patch is never applied, and the face silently keeps whatever `bc_[xyz]` gave it. + +Set GEOMETRY=3 to see the validator refuse what used to pass quietly: + + python3 case.py # geometry 1, valid in 2D + GEOMETRY=3 python3 case.py # a 3D geometry in a 2D case +""" + +import json +import os + +gamma, rho, U, Ma = 1.4, 1.0, 1.0, 0.6 +cs = U / Ma +P = rho * cs**2 / gamma +D, L, H = 1.0, 8.0, 6.0 +dx = D / 20 +N, NY = int(L / dx), int(H / dx) + +case = { + "run_time_info": "T", + "parallel_io": "T", + "prim_vars_wrt": "T", + "format": "silo", + "precision": "double", + "x_domain%beg": 0.0, + "x_domain%end": L, + "y_domain%beg": -0.5 * H, + "y_domain%end": 0.5 * H, + "m": N - 1, + "n": NY - 1, + "p": 0, + "cyl_coord": "F", + "dt": 0.2 * dx / (U + cs), + "t_step_start": 0, + "t_step_stop": 100, + "t_step_save": 100, + "num_patches": 2, + "num_fluids": 1, + "model_eqns": "5eq", + "alt_soundspeed": "F", + "mpp_lim": "F", + "mixture_err": "T", + "time_stepper": "rk3", + "weno_order": 5, + "weno_eps": 1.0e-10, + "weno_avg": "T", + "avg_state": "arithmetic", + "mapped_weno": "T", + "null_weights": "F", + "mp_weno": "F", + "riemann_solver": "hllc", + "low_Mach": 2, + "wave_speeds": "direct", + "viscous": "F", + "fd_order": 4, + "patch_icpp(1)%geometry": 3, + "patch_icpp(1)%x_centroid": 0.5 * L, + "patch_icpp(1)%y_centroid": 0.0, + "patch_icpp(1)%length_x": L, + "patch_icpp(1)%length_y": H, + "patch_icpp(1)%vel(1)": 0.0, + "patch_icpp(1)%vel(2)": 0.0, + "patch_icpp(1)%pres": P, + "patch_icpp(1)%alpha_rho(1)": rho, + "patch_icpp(1)%alpha(1)": 1.0, + # The Dirichlet buffer is filled by pre_process from the initial condition *at the boundary face*, so the + # inflow state has to be present there. A face initialised at rest stores rest and delivers nothing. + "patch_icpp(2)%geometry": 3, + "patch_icpp(2)%alter_patch(1)": "T", + "patch_icpp(2)%x_centroid": 2 * dx, + "patch_icpp(2)%y_centroid": 0.0, + "patch_icpp(2)%length_x": 4 * dx, + "patch_icpp(2)%length_y": D, + "patch_icpp(2)%vel(1)": U, + "patch_icpp(2)%vel(2)": 0.0, + "patch_icpp(2)%pres": P, + "patch_icpp(2)%alpha_rho(1)": rho, + "patch_icpp(2)%alpha(1)": 1.0, + "fluid_pp(1)%gamma": 1.0 / (gamma - 1.0), + "fluid_pp(1)%eos": "ideal_gas", + # a wall with a nozzle cut into it + "bc_x%beg": -16, + "num_bc_patches": 1, + "patch_bc(1)%dir": 1, + "patch_bc(1)%loc": -1, + "patch_bc(1)%type": -17, + "patch_bc(1)%geometry": int(os.environ.get("GEOMETRY", 1)), + "patch_bc(1)%centroid(2)": 0.0, + "patch_bc(1)%length(2)": D, + "bc_x%end": -8, + "bc_y%beg": -8, + "bc_y%end": -8, +} +print(json.dumps(case, indent=4)) diff --git a/toolchain/mfc/case_validator.py b/toolchain/mfc/case_validator.py index 1da790a90..804cb8a73 100644 --- a/toolchain/mfc/case_validator.py +++ b/toolchain/mfc/case_validator.py @@ -2217,6 +2217,17 @@ def check_bc_patches(self): if geometry is None: continue + # s_apply_boundary_patches dispatches by dimensionality: geometry 1 in 2D, 2 or 3 in 3D. A + # geometry that belongs to the other case falls through the dispatch, the patch is never applied, + # and the face silently keeps whatever bc_[xyz] gave it -- a nozzle cut into a wall simply stays a + # wall, with no warning and a jet that never starts. + p = self.get("p", 0) or 0 + n = self.get("n", 0) or 0 + if p > 0: + self.prohibit(geometry not in (2, 3), f"patch_bc({i})%geometry must be 2 (circle) or 3 (rectangle) in 3D; " f"geometry {geometry} is never applied") + elif n > 0: + self.prohibit(geometry != 1, f"patch_bc({i})%geometry must be 1 (line segment) in 2D; " f"geometry {geometry} is never applied") + # Line Segment BC (geometry = 1) if geometry == 1: self.prohibit(radius is not None, f"Line Segment Patch {i} can't have radius defined") From 844cb0d1f9c590539c2137a707433babd2d1252b Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Sat, 12 Sep 2026 22:01:38 -0500 Subject: [PATCH 2/2] Refuse a boundary-condition patch geometry in 1D, where none is ever applied --- toolchain/mfc/case_validator.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/toolchain/mfc/case_validator.py b/toolchain/mfc/case_validator.py index 804cb8a73..2d1b096ca 100644 --- a/toolchain/mfc/case_validator.py +++ b/toolchain/mfc/case_validator.py @@ -2227,6 +2227,9 @@ def check_bc_patches(self): self.prohibit(geometry not in (2, 3), f"patch_bc({i})%geometry must be 2 (circle) or 3 (rectangle) in 3D; " f"geometry {geometry} is never applied") elif n > 0: self.prohibit(geometry != 1, f"patch_bc({i})%geometry must be 1 (line segment) in 2D; " f"geometry {geometry} is never applied") + else: + # 1D enters neither branch of the dispatch, so every geometry is ignored, not just a mismatched one. + self.prohibit(True, f"patch_bc({i})%geometry cannot be used in 1D; boundary-condition patches are only applied in 2D and 3D") # Line Segment BC (geometry = 1) if geometry == 1: