Skip to content
Open
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
44 changes: 44 additions & 0 deletions examples/2D_bc_patch_geometry/README.md
Original file line number Diff line number Diff line change
@@ -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` |
Comment on lines +27 to +30

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.
99 changes: 99 additions & 0 deletions examples/2D_bc_patch_geometry/case.py
Original file line number Diff line number Diff line change
@@ -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))
14 changes: 14 additions & 0 deletions toolchain/mfc/case_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2217,6 +2217,20 @@ 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
Comment on lines +2224 to +2225
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")
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:
self.prohibit(radius is not None, f"Line Segment Patch {i} can't have radius defined")
Expand Down
Loading