Skip to content

Commit d8d6042

Browse files
committed
test: add cell-shift mode + 3D convergence cases
Cell-shift mode in convergence runner: T = K*h per resolution, compare q(T) against np.roll(q(0), +K) in each dim (analytical solution for periodic linear advection with v=1). Cost is O(1) in N (Nt = K*c/CFL independent of resolution) instead of period mode's O(N) Nt. Forces num_ranks=1 for unambiguous Fortran-order data layout. Migrate 1D/2D advection schemes (WENO1/3/5, MUSCL2, TENO5) to cell-shift mode with K=1, expected_order = scheme_order + 1. WENO7/TENO7 stay in period mode since cell-shift signal hits machine precision at typical N (h^8 < 1e-15 at N=64). Add 3D advection case + 5 cell-shift cases (skip WENO7/TENO7). Empirical rates land cleanly across dimensionalities: WENO5 1D 6.00 2D 6.00 3D 6.00 (need >= 5.7) WENO3 1D 2.50 2D 2.49 3D 2.47 (need >= 2.2; effective 1.5 at extrema) WENO1 1D 2.00 2D 1.99 3D 1.97 (need >= 1.8) MUSCL2 1D 3.00 2D 3.01 3D 3.02 (need >= 2.7; unlimited central) TENO5 1D 6.00 2D 5.99 3D 5.99 (need >= 5.7) WENO7 1D 6.97 2D 6.86 (period mode; need >= 6.5) TENO7 1D 6.97 2D 6.86 (period mode; need >= 6.5) Wall: 1D 2.7 min, 2D 15.5 min (WENO7/TENO7 period dominates), 3D 14.5 min.
1 parent ba116db commit d8d6042

5 files changed

Lines changed: 247 additions & 75 deletions

File tree

examples/1D_euler_convergence/case.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
parser.add_argument("--no-mapped", action="store_true", help="Disable mapped WENO")
2727
parser.add_argument("--muscl-lim", type=int, default=0, help="MUSCL limiter: 0=unlimited 1=minmod ... (default: 0)")
2828
parser.add_argument("--time-stepper", type=int, default=3, help="Time stepper: 1=Euler 2=RK2 3=RK3 (default: 3)")
29+
parser.add_argument("--t-end", type=float, default=None, help="Override total simulation time (default: 1.0 = one period)")
2930
args = parser.parse_args()
3031

3132
gamma = 1.4
@@ -37,8 +38,8 @@
3738
# c_sound = sqrt(gamma * p / rho) = sqrt(gamma) for p=1, rho=1
3839
c_max = math.sqrt(gamma) + 1.0 # acoustic + convective
3940
dt = args.cfl * dx / c_max
40-
T_end = 1.0
41-
Nt = max(4, math.ceil(T_end / dt))
41+
T_end = args.t_end if args.t_end is not None else 1.0
42+
Nt = max(1, math.ceil(T_end / dt))
4243
dt = T_end / Nt
4344

4445
if args.muscl:

examples/2D_advection_convergence/case.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
parser.add_argument("--no-mapped", action="store_true", help="Disable mapped WENO")
3131
parser.add_argument("--muscl-lim", type=int, default=0, help="MUSCL limiter: 0=unlimited 1=minmod ... (default: 0)")
3232
parser.add_argument("--time-stepper", type=int, default=3, help="Time stepper: 1=Euler 2=RK2 3=RK3 (default: 3)")
33+
parser.add_argument("--t-end", type=float, default=None, help="Override total simulation time (default: 0.5 = one diagonal period)")
3334
args = parser.parse_args()
3435

3536
gamma = 1.4
@@ -41,8 +42,8 @@
4142
# Max wave speed: |u| + c with c = sqrt(gamma * p / rho_min). rho_min = 1 - 0.2.
4243
c_max = math.sqrt(gamma / 0.8) + 1.0
4344
dt = args.cfl * dx / c_max
44-
T_end = 0.5 # one full diagonal period: phase shift = 2*T_end = 1.0
45-
Nt = max(4, math.ceil(T_end / dt))
45+
T_end = args.t_end if args.t_end is not None else 0.5
46+
Nt = max(1, math.ceil(T_end / dt))
4647
dt = T_end / Nt
4748

4849
if args.muscl:
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
#!/usr/bin/env python3
2+
"""
3+
3D single-fluid Euler convergence case (smooth diagonal advection).
4+
5+
Density wave rho = 1 + 0.2*sin(2*pi*(x+y+z)) with uniform velocity (1, 1, 1)
6+
on the unit cube with periodic BCs. Pressure p = 1 throughout. The wave
7+
phase x+y+z - 3t returns to x+y+z after T = 1/3.
8+
9+
Usually invoked from the convergence test framework with --t-end set so
10+
T = K / N for some integer K (cell-shift mode), allowing analytical
11+
comparison via integer-cell np.roll of the IC.
12+
"""
13+
14+
import argparse
15+
import json
16+
import math
17+
18+
parser = argparse.ArgumentParser(description="3D Euler diagonal-advection convergence case")
19+
parser.add_argument("--mfc", type=json.loads, default="{}", metavar="DICT")
20+
parser.add_argument("-N", type=int, default=32, help="Grid points per dim (default: 32)")
21+
parser.add_argument("--order", type=int, default=5, help="WENO order: 1, 3, 5, or 7")
22+
parser.add_argument("--muscl", action="store_true", help="Use MUSCL-2 instead of WENO")
23+
parser.add_argument("--teno", action="store_true", help="Use TENO instead of WENO")
24+
parser.add_argument("--teno-ct", type=float, default=1e-6, help="TENO CT threshold (default: 1e-6)")
25+
parser.add_argument("--cfl", type=float, default=0.4, help="CFL number (default: 0.4; use 0.005 for WENO7/TENO7)")
26+
parser.add_argument("--no-mapped", action="store_true", help="Disable mapped WENO")
27+
parser.add_argument("--muscl-lim", type=int, default=0, help="MUSCL limiter: 0=unlimited 1=minmod ... (default: 0)")
28+
parser.add_argument("--time-stepper", type=int, default=3, help="Time stepper: 1=Euler 2=RK2 3=RK3 (default: 3)")
29+
parser.add_argument("--t-end", type=float, default=None, help="Override total simulation time (default: 1/3 = one diagonal period)")
30+
args = parser.parse_args()
31+
32+
gamma = 1.4
33+
N = args.N
34+
m = N - 1
35+
L = 1.0
36+
dx = L / N
37+
38+
c_max = math.sqrt(gamma / 0.8) + 1.0
39+
dt = args.cfl * dx / c_max
40+
T_end = args.t_end if args.t_end is not None else 1.0 / 3.0
41+
Nt = max(1, math.ceil(T_end / dt))
42+
dt = T_end / Nt
43+
44+
if args.muscl:
45+
scheme_params = {
46+
"recon_type": 2,
47+
"muscl_order": 2,
48+
"muscl_lim": args.muscl_lim,
49+
}
50+
else:
51+
scheme_params = {
52+
"recon_type": 1,
53+
"weno_order": args.order,
54+
"weno_eps": 1.0e-40,
55+
"weno_Re_flux": "F",
56+
"weno_avg": "F",
57+
"mapped_weno": "F" if (args.order == 1 or args.no_mapped or args.teno) else "T",
58+
"null_weights": "F",
59+
"mp_weno": "F",
60+
"teno": "T" if args.teno else "F",
61+
**({"teno_CT": args.teno_ct} if args.teno else {}),
62+
}
63+
64+
print(
65+
json.dumps(
66+
{
67+
"run_time_info": "F",
68+
"x_domain%beg": 0.0,
69+
"x_domain%end": L,
70+
"y_domain%beg": 0.0,
71+
"y_domain%end": L,
72+
"z_domain%beg": 0.0,
73+
"z_domain%end": L,
74+
"m": m,
75+
"n": m,
76+
"p": m,
77+
"dt": dt,
78+
"t_step_start": 0,
79+
"t_step_stop": Nt,
80+
"t_step_save": Nt,
81+
"num_patches": 1,
82+
"model_eqns": 2,
83+
"alt_soundspeed": "F",
84+
"num_fluids": 1,
85+
"mpp_lim": "F",
86+
"mixture_err": "F",
87+
"time_stepper": args.time_stepper,
88+
"riemann_solver": 2,
89+
"wave_speeds": 1,
90+
"avg_state": 2,
91+
"bc_x%beg": -1,
92+
"bc_x%end": -1,
93+
"bc_y%beg": -1,
94+
"bc_y%end": -1,
95+
"bc_z%beg": -1,
96+
"bc_z%end": -1,
97+
"format": 1,
98+
"precision": 2,
99+
"prim_vars_wrt": "T",
100+
"parallel_io": "F",
101+
"patch_icpp(1)%geometry": 9,
102+
"patch_icpp(1)%x_centroid": 0.5,
103+
"patch_icpp(1)%y_centroid": 0.5,
104+
"patch_icpp(1)%z_centroid": 0.5,
105+
"patch_icpp(1)%length_x": L,
106+
"patch_icpp(1)%length_y": L,
107+
"patch_icpp(1)%length_z": L,
108+
"patch_icpp(1)%vel(1)": 1.0,
109+
"patch_icpp(1)%vel(2)": 1.0,
110+
"patch_icpp(1)%vel(3)": 1.0,
111+
"patch_icpp(1)%pres": 1.0,
112+
"patch_icpp(1)%alpha_rho(1)": "1.0 + 0.2 * sin(2.0 * pi * (x / lx + y / ly + z / lz))",
113+
"patch_icpp(1)%alpha(1)": 1.0,
114+
"fluid_pp(1)%gamma": 1.0 / (gamma - 1.0),
115+
"fluid_pp(1)%pi_inf": 0.0,
116+
**scheme_params,
117+
}
118+
)
119+
)

toolchain/mfc/test/cases.py

Lines changed: 88 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -12,41 +12,56 @@
1212
# the filter handle (`./mfc.sh test --only Convergence`); convergence cases
1313
# are skipped by default.
1414

15-
# 1D Euler advection (rho = 1 + 0.2*sin(2*pi*x), u=1, p=1, T=1):
16-
# WENO5/TENO5 use CFL=0.02 so RK3 temporal floor is below O(h^5) at N>=128.
17-
# WENO7/TENO7 cap at N=128 and use CFL=0.005 (machine-precision floor near N=512).
18-
# WENO3-JS degrades to 2nd order at smooth extrema (Henrick et al. 2005).
19-
# MUSCL2 uses unlimited slope (limiters clip to 1st order at smooth extrema).
15+
# Advection convergence cases use cell-shift mode by default: T = K*h per
16+
# resolution, compare q(T) to np.roll(q(0), -K) per dim. Spatial error scales
17+
# as T*h^p = h^(p+1), so measured rate = p+1 (p = scheme order). Wins ~10-100x
18+
# vs running a full period since Nt = K*c/CFL is independent of N.
19+
# WENO7/TENO7 stay in period mode: at typical N their cell-shift error hits
20+
# machine precision (h^8 < 1e-15 at N=64) before any rate signal develops.
2021
_CONS_VARS_1D = [("density", 1), ("x-momentum", 2), ("energy", 3)]
21-
_RES_1D_DEFAULT = [64, 128, 256, 512, 1024]
22+
_CONS_VARS_2D = [("density", 1), ("energy", 4)]
23+
_CONS_VARS_3D = [("density", 1), ("energy", 5)]
24+
25+
# (label, extra_args, expected_order, tol, resolutions)
26+
# expected_order = scheme order p + 1 in cell-shift mode (T scales with h).
27+
# WENO3-JS reduces to 2 at smooth extrema → cell-shift expected = 3.
28+
# MUSCL2 unlimited central → effective order 2 → cell-shift expected = 3.
2229
_CONVERGENCE_1D_SCHEMES = [
23-
("WENO5", ["--order", "5", "--cfl", "0.02"], 5, 0.2, 128, 512),
24-
("WENO3", ["--order", "3", "--cfl", "0.02"], 2, 0.2, 256, None),
25-
("WENO1", ["--order", "1", "--cfl", "0.02"], 1, 0.05, 128, None),
26-
("MUSCL2", ["--muscl", "--muscl-lim", "0", "--cfl", "0.02"], 2, 0.1, 128, None),
27-
("TENO5", ["--order", "5", "--teno", "--teno-ct", "1e-6", "--cfl", "0.02"], 5, 0.2, 128, 512),
28-
("WENO7", ["--order", "7", "--cfl", "0.005"], 7, 0.5, 64, 128),
29-
("TENO7", ["--order", "7", "--teno", "--teno-ct", "1e-9", "--cfl", "0.005"], 7, 0.5, 64, 128),
30+
("WENO5", ["--order", "5", "--cfl", "0.02"], 6, 0.3, [32, 64, 128]),
31+
("WENO3", ["--order", "3", "--cfl", "0.02"], 2.5, 0.3, [64, 128, 256]),
32+
("WENO1", ["--order", "1", "--cfl", "0.02"], 2, 0.2, [64, 128, 256]),
33+
("MUSCL2", ["--muscl", "--muscl-lim", "0", "--cfl", "0.02"], 3, 0.3, [64, 128, 256]),
34+
("TENO5", ["--order", "5", "--teno", "--teno-ct", "1e-6", "--cfl", "0.02"], 6, 0.3, [32, 64, 128]),
35+
]
36+
# WENO7/TENO7 in 1D: period mode (full period T=1.0, see 1D case.py).
37+
_CONVERGENCE_1D_PERIOD_SCHEMES = [
38+
("WENO7", ["--order", "7", "--cfl", "0.005"], 7, 0.5, [64, 128]),
39+
("TENO7", ["--order", "7", "--teno", "--teno-ct", "1e-9", "--cfl", "0.005"], 7, 0.5, [64, 128]),
3040
]
3141

32-
# 2D diagonal advection (rho = 1 + 0.2 sin(2pi(x+y)), v=(1,1), period T=0.5):
33-
# linear primitives mean no covariance floor — clean asymptotic rates for all
34-
# WENO/MUSCL/TENO orders. WENO3 reduces to 2nd at smooth extrema (Henrick 2005).
35-
# 4 ranks (2x2 decomp): per-rank stencil constraint n+1 >= 5*weno_order/2.
36-
# WENO5/TENO5 use [64, 96, 128] (33>=25 at N=64). WENO7/TENO7 use [80, 96]
37-
# (41>=35 at N=80) and CFL=0.005 to push RK3 temporal error below O(h^7).
38-
# (label, extra_args, expected_order, tol, resolutions)
39-
_CONS_VARS_2D = [("density", 1), ("energy", 4)]
4042
_CONVERGENCE_2D_SCHEMES = [
41-
("WENO5", ["--order", "5", "--cfl", "0.02"], 5, 0.3, [64, 96, 128]),
42-
("WENO3", ["--order", "3", "--cfl", "0.02"], 2, 0.3, [32, 64, 128]),
43-
("WENO1", ["--order", "1", "--cfl", "0.02"], 1, 0.2, [32, 64, 128]),
44-
("MUSCL2", ["--muscl", "--muscl-lim", "0", "--cfl", "0.02"], 2, 0.1, [32, 64, 128]),
45-
("TENO5", ["--order", "5", "--teno", "--teno-ct", "1e-6", "--cfl", "0.02"], 5, 0.3, [64, 96, 128]),
43+
("WENO5", ["--order", "5", "--cfl", "0.02"], 6, 0.3, [32, 64, 96]),
44+
("WENO3", ["--order", "3", "--cfl", "0.02"], 2.5, 0.3, [32, 64, 128]),
45+
("WENO1", ["--order", "1", "--cfl", "0.02"], 2, 0.2, [32, 64, 128]),
46+
("MUSCL2", ["--muscl", "--muscl-lim", "0", "--cfl", "0.02"], 3, 0.3, [32, 64, 128]),
47+
("TENO5", ["--order", "5", "--teno", "--teno-ct", "1e-6", "--cfl", "0.02"], 6, 0.3, [32, 64, 96]),
48+
]
49+
_CONVERGENCE_2D_PERIOD_SCHEMES = [
4650
("WENO7", ["--order", "7", "--cfl", "0.005"], 7, 0.5, [80, 96]),
4751
("TENO7", ["--order", "7", "--teno", "--teno-ct", "1e-9", "--cfl", "0.005"], 7, 0.5, [80, 96]),
4852
]
4953

54+
# 3D diagonal advection: only cell-shift mode (period T=1/3 with N^3 cells
55+
# would dominate CI even at N=64). WENO7/TENO7 skipped — at N=64 with K=1
56+
# the spatial error signal is below machine precision.
57+
_CONVERGENCE_3D_SCHEMES = [
58+
("WENO5", ["--order", "5", "--cfl", "0.02"], 6, 0.3, [32, 64]),
59+
("WENO3", ["--order", "3", "--cfl", "0.02"], 2.5, 0.3, [32, 64]),
60+
("WENO1", ["--order", "1", "--cfl", "0.02"], 2, 0.2, [32, 64]),
61+
("MUSCL2", ["--muscl", "--muscl-lim", "0", "--cfl", "0.02"], 3, 0.3, [32, 64]),
62+
("TENO5", ["--order", "5", "--teno", "--teno-ct", "1e-6", "--cfl", "0.02"], 6, 0.3, [32, 64]),
63+
]
64+
5065
# Sod L1 self-convergence: any conservative monotone scheme converges at L1
5166
# rate ~1 (Godunov). SUPERBEE is over-compressive; min_N=128 skips its
5267
# pre-asymptotic point.
@@ -71,41 +86,41 @@
7186
("RK3", ["--order", "5", "--time-stepper", "3"], 3, 0.3, [0.50, 0.25]),
7287
]
7388

74-
# 2D diagonal advection (smooth density wave; no covariance floor): exercises
75-
# WENO7 / TENO7 in 2D. CFL=0.005 keeps RK3 temporal error below O(h^7).
76-
# With 4 ranks (2x2 decomp) the per-rank stencil constraint n+1 >= 5*weno_order=35
77-
# sets min_N=70 (rounded up to 80). N capped at 96 to keep CI cost reasonable:
78-
# 7th-order error scaling between N=80 and N=96 is ~3.6x, well above the
79-
# machine-precision floor and clean for rate measurement.
80-
_RES_2D_ADV_DEFAULT = [80, 96]
81-
_CONVERGENCE_2D_ADV_SCHEMES = [
82-
("WENO7", ["--order", "7", "--cfl", "0.005"], 7, 0.5, 80, 96),
83-
("TENO7", ["--order", "7", "--teno", "--teno-ct", "1e-9", "--cfl", "0.005"], 7, 0.5, 80, 96),
84-
]
85-
8689

8790
def add_convergence_cases(cases):
8891
num_ranks = 4
8992

90-
for label, extra_args, expected, tol, min_N, max_N in _CONVERGENCE_1D_SCHEMES:
93+
def _adv_spec(case_path, ndim, cons_vars, extra_args, expected, tol, resolutions, time_mode):
94+
# cell_shift forces num_ranks=1 inside the runner; period mode keeps the suite default.
95+
return {
96+
"runner": f"{ndim}d_advection",
97+
"case_path": case_path,
98+
"extra_args": extra_args,
99+
"expected_order": expected,
100+
"tol": tol,
101+
"resolutions": resolutions,
102+
"ndim": ndim,
103+
"domain_len": 1.0,
104+
"cons_vars": cons_vars,
105+
"primary_idx": 1,
106+
"num_ranks": num_ranks,
107+
"time_mode": time_mode,
108+
"cell_shift": 1,
109+
}
110+
111+
for label, extra_args, expected, tol, resolutions in _CONVERGENCE_1D_SCHEMES:
91112
cases.append(
92113
define_convergence_case(
93114
f"Convergence -> 1D -> {label}",
94-
spec={
95-
"runner": "1d_advection",
96-
"case_path": "examples/1D_euler_convergence/case.py",
97-
"extra_args": extra_args,
98-
"expected_order": expected,
99-
"tol": tol,
100-
"resolutions": _RES_1D_DEFAULT,
101-
"min_N": min_N,
102-
"max_N": max_N,
103-
"ndim": 1,
104-
"domain_len": 1.0,
105-
"cons_vars": _CONS_VARS_1D,
106-
"primary_idx": 1,
107-
"num_ranks": num_ranks,
108-
},
115+
spec=_adv_spec("examples/1D_euler_convergence/case.py", 1, _CONS_VARS_1D, extra_args, expected, tol, resolutions, "cell_shift"),
116+
ppn=1,
117+
)
118+
)
119+
for label, extra_args, expected, tol, resolutions in _CONVERGENCE_1D_PERIOD_SCHEMES:
120+
cases.append(
121+
define_convergence_case(
122+
f"Convergence -> 1D -> {label}",
123+
spec=_adv_spec("examples/1D_euler_convergence/case.py", 1, _CONS_VARS_1D, extra_args, expected, tol, resolutions, "period"),
109124
ppn=num_ranks,
110125
)
111126
)
@@ -114,23 +129,28 @@ def add_convergence_cases(cases):
114129
cases.append(
115130
define_convergence_case(
116131
f"Convergence -> 2D -> {label}",
117-
spec={
118-
"runner": "2d_advection",
119-
"case_path": "examples/2D_advection_convergence/case.py",
120-
"extra_args": extra_args,
121-
"expected_order": expected,
122-
"tol": tol,
123-
"resolutions": resolutions,
124-
"ndim": 2,
125-
"domain_len": 1.0,
126-
"cons_vars": _CONS_VARS_2D,
127-
"primary_idx": 1,
128-
"num_ranks": num_ranks,
129-
},
132+
spec=_adv_spec("examples/2D_advection_convergence/case.py", 2, _CONS_VARS_2D, extra_args, expected, tol, resolutions, "cell_shift"),
133+
ppn=1,
134+
)
135+
)
136+
for label, extra_args, expected, tol, resolutions in _CONVERGENCE_2D_PERIOD_SCHEMES:
137+
cases.append(
138+
define_convergence_case(
139+
f"Convergence -> 2D -> {label}",
140+
spec=_adv_spec("examples/2D_advection_convergence/case.py", 2, _CONS_VARS_2D, extra_args, expected, tol, resolutions, "period"),
130141
ppn=num_ranks,
131142
)
132143
)
133144

145+
for label, extra_args, expected, tol, resolutions in _CONVERGENCE_3D_SCHEMES:
146+
cases.append(
147+
define_convergence_case(
148+
f"Convergence -> 3D -> {label}",
149+
spec=_adv_spec("examples/3D_advection_convergence/case.py", 3, _CONS_VARS_3D, extra_args, expected, tol, resolutions, "cell_shift"),
150+
ppn=1,
151+
)
152+
)
153+
134154
for label, extra_args, expected, tol, min_N in _CONVERGENCE_SOD_SCHEMES:
135155
cases.append(
136156
define_convergence_case(
@@ -1747,6 +1767,7 @@ def foreach_example():
17471767
"1D_advection_convergence",
17481768
"1D_sod_convergence",
17491769
"2D_advection_convergence",
1770+
"3D_advection_convergence",
17501771
"2D_zero_circ_vortex_analytical",
17511772
"3D_TaylorGreenVortex_analytical",
17521773
"3D_IGR_TaylorGreenVortex_nvidia",

0 commit comments

Comments
 (0)