Skip to content

Commit 02df59d

Browse files
committed
test: report scheme spatial order (rate-1) in cell-shift mode
In cell-shift mode T = K*h scales with h, so accumulated truncation error T*h^p = h^(p+1) gives raw measured rate p+1 — one greater than the scheme's spatial order. The +1 is an artifact of scaling T with h, not an extra order of accuracy in the scheme. Subtract 1 from the displayed value so the runner reports the scheme's canonical spatial order p in both modes; the spec's expected_order field always means p directly. Output now self-documents: cell-shift: 'Fitted spatial order: 5.00 (need >= 4.7)' period: 'Fitted rate: 6.97 (need >= 6.5)'
1 parent d8d6042 commit 02df59d

2 files changed

Lines changed: 46 additions & 36 deletions

File tree

toolchain/mfc/test/cases.py

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

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.
15+
# Advection convergence cases. Cell-shift mode: T = K*h per resolution, compare
16+
# q(T) to np.roll(q(0), +K) per dim. Cost is O(1) in N (Nt = K*c/CFL independent
17+
# of resolution) — wins ~10-100x vs period mode (full advection period).
18+
# WENO7/TENO7 stay in period mode: at typical N their cell-shift signal sinks
19+
# below machine precision (h^8 < 1e-15 at N=64) before any rate develops.
20+
#
21+
# expected_order is always the scheme's spatial order p. The runner subtracts
22+
# 1 from the displayed rate in cell-shift mode (where raw rate = p+1) so the
23+
# reported "spatial order" matches expected_order in both modes.
2124
_CONS_VARS_1D = [("density", 1), ("x-momentum", 2), ("energy", 3)]
2225
_CONS_VARS_2D = [("density", 1), ("energy", 4)]
2326
_CONS_VARS_3D = [("density", 1), ("energy", 5)]
2427

2528
# (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.
29+
# WENO3-JS at smooth extrema empirically achieves ~1.5 in MFC (Henrick mapping
30+
# enabled). MUSCL2 unlimited central → effective spatial order 2.
2931
_CONVERGENCE_1D_SCHEMES = [
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]),
32+
("WENO5", ["--order", "5", "--cfl", "0.02"], 5, 0.3, [32, 64, 128]),
33+
("WENO3", ["--order", "3", "--cfl", "0.02"], 1.5, 0.3, [64, 128, 256]),
34+
("WENO1", ["--order", "1", "--cfl", "0.02"], 1, 0.2, [64, 128, 256]),
35+
("MUSCL2", ["--muscl", "--muscl-lim", "0", "--cfl", "0.02"], 2, 0.3, [64, 128, 256]),
36+
("TENO5", ["--order", "5", "--teno", "--teno-ct", "1e-6", "--cfl", "0.02"], 5, 0.3, [32, 64, 128]),
3537
]
3638
# WENO7/TENO7 in 1D: period mode (full period T=1.0, see 1D case.py).
3739
_CONVERGENCE_1D_PERIOD_SCHEMES = [
@@ -40,11 +42,11 @@
4042
]
4143

4244
_CONVERGENCE_2D_SCHEMES = [
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]),
45+
("WENO5", ["--order", "5", "--cfl", "0.02"], 5, 0.3, [32, 64, 96]),
46+
("WENO3", ["--order", "3", "--cfl", "0.02"], 1.5, 0.3, [32, 64, 128]),
47+
("WENO1", ["--order", "1", "--cfl", "0.02"], 1, 0.2, [32, 64, 128]),
48+
("MUSCL2", ["--muscl", "--muscl-lim", "0", "--cfl", "0.02"], 2, 0.3, [32, 64, 128]),
49+
("TENO5", ["--order", "5", "--teno", "--teno-ct", "1e-6", "--cfl", "0.02"], 5, 0.3, [32, 64, 96]),
4850
]
4951
_CONVERGENCE_2D_PERIOD_SCHEMES = [
5052
("WENO7", ["--order", "7", "--cfl", "0.005"], 7, 0.5, [80, 96]),
@@ -55,11 +57,11 @@
5557
# would dominate CI even at N=64). WENO7/TENO7 skipped — at N=64 with K=1
5658
# the spatial error signal is below machine precision.
5759
_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]),
60+
("WENO5", ["--order", "5", "--cfl", "0.02"], 5, 0.3, [32, 64]),
61+
("WENO3", ["--order", "3", "--cfl", "0.02"], 1.5, 0.3, [32, 64]),
62+
("WENO1", ["--order", "1", "--cfl", "0.02"], 1, 0.2, [32, 64]),
63+
("MUSCL2", ["--muscl", "--muscl-lim", "0", "--cfl", "0.02"], 2, 0.3, [32, 64]),
64+
("TENO5", ["--order", "5", "--teno", "--teno-ct", "1e-6", "--cfl", "0.02"], 5, 0.3, [32, 64]),
6365
]
6466

6567
# Sod L1 self-convergence: any conservative monotone scheme converges at L1

toolchain/mfc/test/convergence.py

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -120,15 +120,16 @@ def _print_conservation_check(all_cons_errs, var_list, tol=CONS_TOL):
120120
def _run_resolution_sweep(spec):
121121
"""1D/2D/3D resolution sweep on a smooth diagonal-advection problem.
122122
123+
`expected_order` in the spec always names the scheme's spatial order p.
123124
Two time modes:
124125
- 'period' (default): T = one full advection period; compare q(T) vs q(0).
125-
Spatial truncation accumulates over a full period, so error ~ h^p and
126-
the measured rate equals the scheme order p.
127-
- 'cell_shift': T = cell_shift * h / v; compare q(T) vs np.roll of q(0)
126+
Truncation accumulates over fixed T, so error ~ h^p and measured rate = p.
127+
- 'cell_shift': T = cell_shift * h / v; compare q(T) vs np.roll of q(0)
128128
by cell_shift cells (analytical solution for periodic linear advection
129-
with v=1 in unit domain). Cost is O(1) in N (Nt = c/CFL independent
130-
of resolution). Error scales as T*h^p = h^(p+1) so the measured rate
131-
is p+1. Forces num_ranks=1 for unambiguous Fortran-order data layout.
129+
with v=1). Cost is O(1) in N (Nt = K*c/CFL independent of resolution).
130+
Error scales as T*h^p = h^(p+1), giving raw measured rate = p+1; the
131+
runner subtracts 1 for display so the reported "spatial order" still
132+
equals p. Forces num_ranks=1 for unambiguous Fortran-order data layout.
132133
"""
133134
case_path = spec["case_path"]
134135
extra_args = list(spec.get("extra_args", []))
@@ -146,15 +147,20 @@ def _run_resolution_sweep(spec):
146147
if time_mode == "cell_shift":
147148
# Single-rank only: multi-rank concat order doesn't match (N,)*ndim reshape.
148149
num_ranks = 1
150+
# Cell-shift mode: T = K*h, so raw rate is p+1; subtract 1 for display.
151+
rate_offset = 1
152+
order_label = "spatial order"
149153
else:
150154
num_ranks = spec.get("num_ranks", 1)
155+
rate_offset = 0
156+
order_label = "rate"
151157

152158
if "min_N" in spec and spec["min_N"] is not None:
153159
resolutions = [N for N in resolutions if N >= spec["min_N"]]
154160
if "max_N" in spec and spec["max_N"] is not None:
155161
resolutions = [N for N in resolutions if N <= spec["max_N"]]
156162

157-
print(f" (need rate >= {expected_order - tol:.1f})")
163+
print(f" (need {order_label} >= {expected_order - tol:.1f})")
158164

159165
errors = []
160166
nts = []
@@ -185,16 +191,18 @@ def _run_resolution_sweep(spec):
185191
dxs = [domain_len / N for N in resolutions]
186192
rates = _pairwise_rates(errors, dxs)
187193

188-
print(f"\n {'N':>6} {'Nt':>6} {'dx':>10} {'L2 error':>14} {'rate':>8}")
189-
print(f" {'-' * 6} {'-' * 6} {'-' * 10} {'-' * 14} {'-' * 8}")
194+
col_label = order_label.replace(" ", "_")
195+
print(f"\n {'N':>6} {'Nt':>6} {'dx':>10} {'L2 error':>14} {col_label:>13}")
196+
print(f" {'-' * 6} {'-' * 6} {'-' * 10} {'-' * 14} {'-' * 13}")
190197
for i, N in enumerate(resolutions):
191-
r_str = f"{rates[i]:>8.2f}" if rates[i] is not None else f"{'---':>8}"
198+
r_str = f"{rates[i] - rate_offset:>13.2f}" if rates[i] is not None else f"{'---':>13}"
192199
print(f" {N:>6} {nts[i]:>6} {dxs[i]:>10.6f} {errors[i]:>14.6e} {r_str}")
193200

194201
if len(resolutions) > 1:
195202
overall = _fit_rate(errors, dxs)
196-
print(f"\n Fitted rate: {overall:.2f} (need >= {expected_order - tol:.1f})")
197-
rate_passed = overall >= expected_order - tol
203+
displayed = overall - rate_offset
204+
print(f"\n Fitted {order_label}: {displayed:.2f} (need >= {expected_order - tol:.1f})")
205+
rate_passed = displayed >= expected_order - tol
198206
else:
199207
rate_passed = True
200208

0 commit comments

Comments
 (0)