test: integrate periodic Taylor-Green AMR assessment

This commit is contained in:
TRON committed 2026-10-18 00:24:50 -07:00
1 parent 1f263b9e46
commit 9805383f9c
6 files changed
+764

No files matched your search

+124
View File
@@ -0,0 +1,124 @@
"""Frozen assessment of the complete 16-case matrix, independent of GPU launch."""
import argparse
import json
import math
from pathlib import Path
from oracle import TIMES, LIMITS, U
# Numerical floors reflect finite-Mach/FP32 and equilibrium startup, not observed runs.
FLOORS = dict(velocity_l2=0.002, pressure_l2=0.02, energy_relative=0.004)
BASELINE_FACTOR = 2.0
REFINEMENT_FACTOR = 0.9
def assess(records):
indexed = {}
for r in records:
key = (r["re_L"], r["n"], r["depth"])
if key in indexed:
raise ValueError(f"duplicate case {key}")
indexed[key] = r
expected = {(re, n, 1) for re in (100, 1000) for n in (32, 64, 128, 256)}
expected |= {(re, n, d) for re in (100, 1000) for n in (32, 64) for d in (2, 3)}
missing = sorted(expected - indexed.keys())
checks = {
"complete_matrix": not missing,
"no_extra_cases": set(indexed) <= expected,
}
identities = {json.dumps(r["source"], sort_keys=True) for r in records}
checks["same_source_and_harness"] = len(identities) == 1
checks["clean_source"] = bool(records) and all(
not r["source"]["dirty"] for r in records
)
for key, r in indexed.items():
label = str(key)
rows = r["rows"]
checks[label + " complete"] = r["completed"] and len(rows) == len(TIMES)
checks[label + " scales"] = (
r["U"] == U and abs(r["viscosity"] - U * r["n"] / r["re_L"]) < 1e-15
)
checks[label + " times"] = len(rows) == len(TIMES) and all(
abs(v["tstar"] - t) < 1e-12 for v, t in zip(rows, TIMES)
)
checks[label + " absolute"] = bool(rows) and all(
0 <= row[k] <= limit for row in rows for k, limit in LIMITS.items()
)
comparisons = []
def worst(key, metric):
values = [r[metric] for r in indexed.get(key, {}).get("rows", [])[1:]]
return max(values) if values and all(math.isfinite(v) for v in values) else None
# Diagnostics survive failed screens and partial pilot matrices. Missing or
# nonfinite operands are explicit nulls; they cannot acquire a passing gate.
for re in (100, 1000):
for depth in (2, 3):
for n in (32, 64):
if (re, n, depth) not in indexed:
continue
for metric, floor in FLOORS.items():
amr = worst((re, n, depth), metric)
coarse = worst((re, n, 1), metric)
fine = worst((re, n * 2 ** (depth - 1), 1), metric)
ok = (
amr is not None
and coarse is not None
and amr <= max(BASELINE_FACTOR * coarse, floor)
)
comparisons.append(
dict(
re_L=re,
n=n,
depth=depth,
metric=metric,
amr=amr,
uniform_coarse=coarse,
uniform_finest=fine,
amr_to_finest=(
None
if amr is None or fine is None
else amr / max(fine, 1e-30)
),
passed=ok,
)
)
checks[f"baseline {re}/{n}/{depth}/{metric}"] = ok
for metric, floor in FLOORS.items():
low = worst((re, 32, depth), metric)
high = worst((re, 64, depth), metric)
checks[f"refinement {re}/{depth}/{metric}"] = (
low is not None
and high is not None
and high <= max(REFINEMENT_FACTOR * low, floor)
)
for metric, floor in FLOORS.items():
low = worst((re, 32, 1), metric)
high = worst((re, 256, 1), metric)
checks[f"uniform refinement {re}/{metric}"] = (
low is not None
and high is not None
and high <= max(REFINEMENT_FACTOR * low, floor)
)
return dict(
passed=all(checks.values()),
missing=missing,
checks=checks,
comparisons=comparisons,
floors=FLOORS,
baseline_factor=BASELINE_FACTOR,
refinement_factor=REFINEMENT_FACTOR,
scope="2D periodic smooth-decay AMR verification only; no turbulence, LES, wall or Jet certification",
)
if __name__ == "__main__":
p = argparse.ArgumentParser(description=__doc__)
p.add_argument("files", type=Path, nargs="+")
p.add_argument("--output", type=Path, required=True)
a = p.parse_args()
result = assess([json.loads(path.read_text()) for path in a.files])
a.output.write_text(json.dumps(result, indent=2, allow_nan=False) + "\n")
print(json.dumps(result, indent=2))
raise SystemExit(0 if result["passed"] else 1)
+103
View File
@@ -0,0 +1,103 @@
"""CPU-only 2D Taylor–Green reference and composite-leaf diagnostics."""
import numpy as np
CX = np.array([0, 1, -1, 0, 0, 1, -1, 1, -1])
CY = np.array([0, 0, 0, 1, -1, 1, -1, -1, 1])
W = np.array([4 / 9] + [1 / 9] * 4 + [1 / 36] * 4)
U = 0.03
HORIZON = 0.48 # t* = t U/L; exactly 16*N base steps
TIMES = [0.0, 0.12, 0.24, 0.36, 0.48]
# Frozen screening ceilings; baseline-relative qualification is in assess.py.
LIMITS = dict(
velocity_l2=0.05, pressure_l2=0.15, energy_relative=0.05, mass_relative=1e-5
)
def exact(x, y, t, length, viscosity, amplitude=U):
k = 2 * np.pi / length
a = amplitude * np.exp(-2 * viscosity * k * k * t)
# TensorMesh phase: negative cos(x)sin(y), positive sin(x)cos(y).
ux = -a * np.cos(k * x) * np.sin(k * y)
uy = a * np.sin(k * x) * np.cos(k * y)
p = -0.25 * a * a * (np.cos(2 * k * x) + np.cos(2 * k * y))
return ux, uy, p # kinematic pressure, mean zero
def centers(shape, level=0):
# Campaign deliberately uses level-wide union layouts, all origins zero.
y, x = np.indices(shape, dtype=float)
h = 2.0 ** (-level)
return h * (x + 0.5), h * (y + 0.5)
def initial(shape, level, length, viscosity):
x, y = centers(shape, level)
ux, uy, p = exact(x, y, 0.0, length, viscosity)
rho = 1 + 3 * p # cs^2=1/3, rho0=1
cu = CX[:, None, None] * ux + CY[:, None, None] * uy
# Exact FP64 weights; public shifted initializer encodes before FP32 rounding.
return (
W[:, None, None]
* rho
* (1 + 3 * cu + 4.5 * cu * cu - 1.5 * (ux * ux + uy * uy))
)
def rectangles(n, depth):
if n < 32 or n % 32 or depth not in (2, 3):
raise ValueError("AMR requires N >= 32 divisible by 32 and depth 2 or 3")
# Broad off-axis rectangle cuts nonzero velocity and both gradients.
out = [[(n // 8, n // 8, 7 * n // 8, 3 * n // 4)]]
if depth == 3:
# L1 index coordinates; global physical bounds (3N/8,N/4,3N/4,5N/8).
out.append([(3 * n // 4, n // 2, 3 * n // 2, 5 * n // 4)])
return out
def metrics(arrays, masks, t, length, viscosity, initial_mass=None):
accum = []
for level, (g, mask) in enumerate(zip(arrays, masks)):
if g.shape != (9, *mask.shape):
raise ValueError("population/mask shape mismatch")
values = g[:, mask].astype(float)
if not np.isfinite(values).all():
raise FloatingPointError("nonfinite leaf populations")
rho = values.sum(axis=0)
if (rho <= 0).any():
raise FloatingPointError("nonpositive leaf density")
ux, uy = CX @ values / rho, CY @ values / rho
x, y = centers(mask.shape, level)
ex, ey, ep = exact(x[mask], y[mask], t, length, viscosity)
area = np.full(rho.size, 0.25**level)
accum.append((rho, ux, uy, (rho - 1) / 3, ex, ey, ep, area))
rho, ux, uy, p, ex, ey, ep, area = [np.concatenate(v) for v in zip(*accum)]
volume = area.sum()
if not np.isclose(volume, length**2, rtol=0, atol=1e-10):
raise ValueError("leaf partition does not cover domain once")
mass = float(area @ rho)
# Remove one global pressure gauge, never one gauge per level.
p = p - area @ p / volume
ep = ep - area @ ep / volume
energy = float(area @ (ux * ux + uy * uy) / 2 / volume)
exact_energy = U * U / 4 * np.exp(-4 * viscosity * (2 * np.pi / length) ** 2 * t)
return dict(
tstar=t * U / length,
mass=mass,
mass_relative=(
0.0 if initial_mass is None else abs(mass - initial_mass) / initial_mass
),
velocity_l2=float(
np.sqrt(
area @ ((ux - ex) ** 2 + (uy - ey) ** 2) / (area @ (ex * ex + ey * ey))
)
),
pressure_l2=float(np.sqrt(area @ ((p - ep) ** 2) / (area @ (ep * ep)))),
energy=energy,
exact_energy=exact_energy,
energy_relative=abs(energy - exact_energy) / exact_energy,
rho_min=float(rho.min()),
rho_max=float(rho.max()),
max_speed=float(np.max(np.hypot(ux, uy))),
leaf_cells=int(len(rho)),
)
+205
View File
@@ -0,0 +1,205 @@
"""One bounded GPU case. No production edits, plots, checkpoints or field dumps."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import subprocess
import sys
import time
import numpy as np
from oracle import U, TIMES, LIMITS, initial, metrics, rectangles
def provenance(repo):
git = lambda *a: subprocess.check_output(
["git", "-C", str(repo), *a], text=True
).strip()
source_hash = hashlib.sha256()
for path in sorted((repo / "src").rglob("*")):
if path.is_file() and "__pycache__" not in path.parts and path.suffix != ".pyc":
source_hash.update(
str(path.relative_to(repo)).encode() + b"\0" + path.read_bytes() + b"\0"
)
return dict(
revision=git("rev-parse", "HEAD"),
dirty=bool(git("status", "--porcelain")),
source_tree_sha256=source_hash.hexdigest(),
harness_sha256={
p.name: hashlib.sha256(p.read_bytes()).hexdigest()
for p in Path(__file__).parent.glob("*.py")
},
)
def config(n, re):
from CelerisLab.config import LBMConfig
cfg = LBMConfig(
nx=n,
ny=n,
viscosity=U * n / re,
velocity=0.0,
rho=1.0,
lattice_model="D2Q9",
collision="SRT",
streaming="double_buffer",
store_precision="FP32",
ddf_shifting=True,
les_enabled=False,
)
cfg.validate()
return cfg
def workload(masks, base_steps):
cells = [int(np.count_nonzero(m)) for m in masks]
hops = [base_steps * 2**level for level in range(len(masks))]
updates = [count * hop for count, hop in zip(cells, hops)]
return dict(
active_cells_by_level=cells,
level_hops=hops,
active_collision_updates_by_level=updates,
active_collision_updates=sum(updates),
root_intervals=base_steps,
excludes="ghost streaming, halo copies, transfers, initialization and diagnostics",
)
def main():
p = argparse.ArgumentParser(description=__doc__)
p.add_argument("--repo", type=Path, required=True)
p.add_argument("--re", type=int, choices=[100, 1000], required=True)
p.add_argument("--n", type=int, choices=[32, 64, 128, 256], required=True)
p.add_argument("--depth", type=int, choices=[1, 2, 3], required=True)
p.add_argument("--output", type=Path, required=True)
args = p.parse_args()
if not os.environ.get("CUDA_VISIBLE_DEVICES"):
p.error("Explicit physical CUDA_VISIBLE_DEVICES selection required")
repo = args.repo.resolve()
sys.path.insert(0, str(repo / "src"))
import CelerisLab
if Path(CelerisLab.__file__).resolve() != repo / "src/CelerisLab/__init__.py":
raise RuntimeError("Wrong imported source checkout")
from CelerisLab.cuda.context import CudaContext
from CelerisLab.lbm.blocks import BlockLayout2D
from CelerisLab.lbm.block_stepper import PeriodicBlockStepper
from CelerisLab.lbm.refinement_stepper import FixedPatchStepper
from CelerisLab.lbm.refinement import ACTIVE
from CelerisLab.lbm.hierarchy import build_hierarchy, leaf_partition
cfg = config(args.n, args.re)
refinements = [] if args.depth == 1 else rectangles(args.n, args.depth)
h = build_hierarchy(cfg, refinements)
np.testing.assert_array_equal(leaf_partition(h), np.ones((args.n, args.n)))
masks = [k == ACTIVE for k in h.kinds]
report = dict(
schema=1,
source=provenance(repo),
n=args.n,
re_L=args.re,
depth=args.depth,
refinements=refinements,
U=U,
mach=U * np.sqrt(3),
viscosity=cfg.viscosity,
tau_by_level=[0.5 + 3 * c.viscosity for c in h.configs],
initialization="physical FP64 equilibrium G at actual cell centers; rho=1+3p",
cuda_visible_devices=os.environ["CUDA_VISIBLE_DEVICES"],
device_id=0,
limits=LIMITS,
workload=workload(masks, round(TIMES[-1] * args.n / U)),
rows=[],
completed=False,
)
args.output.parent.mkdir(parents=True, exist_ok=True)
save = lambda: args.output.write_text(
json.dumps(report, indent=2, allow_nan=False) + "\n"
)
save()
started = time.monotonic()
with CudaContext(0) as ctx:
if args.depth == 1:
layout = BlockLayout2D.build(args.n, args.n, 8, 8, periodic=(True, True))
run = PeriodicBlockStepper(cfg, layout, context=ctx)
else:
run = FixedPatchStepper(
cfg,
context=ctx,
coarse_boundary="periodic",
refinements=refinements,
level_blocks=[(8, 8)] * args.depth,
)
with run:
run.initialize(
*[
initial(m.shape, level, args.n, cfg.viscosity)
for level, m in enumerate(masks)
]
)
initial_mass = None
for tstar in TIMES:
step = round(tstar * args.n / U)
run.step(step - run.step_count)
arrays = (run.download(),) if args.depth == 1 else run.download()
row = metrics(arrays, masks, step, args.n, cfg.viscosity, initial_mass)
row["step"] = step
if initial_mass is None:
initial_mass = row["mass"]
report["rows"].append(row)
save()
print(
json.dumps(
dict(
event="PROGRESS",
n=args.n,
re_L=args.re,
depth=args.depth,
**row,
)
),
flush=True,
)
if step == 0:
# Exercise real kernels/halos/transfers on every level before READY.
# This is the first measured trajectory step, not discarded warmup.
run.step(1)
probe = (run.download(),) if args.depth == 1 else run.download()
metrics(probe, masks, 1, args.n, cfg.viscosity, initial_mass)
report["ready_after_step"] = 1
save()
print(
json.dumps(
dict(
event="READY",
step=1,
n=args.n,
re_L=args.re,
depth=args.depth,
workload=report["workload"],
)
),
flush=True,
)
report.update(
completed=True,
seconds=time.monotonic() - started,
screening_pass=all(
row[key] <= limit for row in report["rows"] for key, limit in LIMITS.items()
),
)
save()
print(
json.dumps(
{k: report[k] for k in ("n", "re_L", "depth", "screening_pass", "seconds")}
),
flush=True,
)
return 0 if report["screening_pass"] else 1
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,170 @@
"""Standalone CPU oracle/geometry checks. Set PYTHONPATH=<checkout>/src."""
import unittest
import numpy as np
from oracle import CX, CY, U, exact, centers, initial, rectangles, metrics
from run_case import config
class OracleTests(unittest.TestCase):
def test_phase_pressure_and_decay(self):
u, v, p = exact(0.0, 8.0, 0.0, 32.0, 0.01)
self.assertAlmostEqual(u, -U)
self.assertAlmostEqual(v, 0.0)
self.assertAlmostEqual(p, 0.0)
self.assertAlmostEqual(exact(0.0, 0.0, 0.0, 32.0, 0.01)[2], -U * U / 2)
a = np.array(exact(3.0, 5.0, 0.0, 32.0, 0.01))
b = np.array(exact(3.0, 5.0, 20.0, 32.0, 0.01))
f = np.exp(-2 * 0.01 * (2 * np.pi / 32) ** 2 * 20)
np.testing.assert_allclose(b, a * np.array([f, f, f * f]), atol=1e-17)
def test_navier_stokes_residual(self):
# Independent finite-difference derivatives catch sign/phase mistakes.
x = np.array([2.7, 8.1, 13.3])
y = np.array([3.2, 7.4, 21.6])
t = 10.0
h = 0.002
n = 32.0
nu = 0.01
f = lambda x, y, t: np.array(exact(x, y, t, n, nu))
z = f(x, y, t)
dx = (f(x + h, y, t) - f(x - h, y, t)) / (2 * h)
dy = (f(x, y + h, t) - f(x, y - h, t)) / (2 * h)
dt = (f(x, y, t + h) - f(x, y, t - h)) / (2 * h)
lap = (
f(x + h, y, t) + f(x - h, y, t) + f(x, y + h, t) + f(x, y - h, t) - 4 * z
) / h**2
self.assertLess(np.max(np.abs(dx[0] + dy[1])), 1e-11)
residual = (
dt[:2]
+ z[0] * dx[:2]
+ z[1] * dy[:2]
+ np.array([dx[2], dy[2]])
- nu * lap[:2]
)
self.assertLess(np.max(np.abs(residual)), 1e-10)
def test_equilibrium_moments_and_metrics(self):
n = 32
nu = 0.01
g = initial((n, n), 0, n, nu)
rho = g.sum(axis=0)
x, y = centers((n, n))
u, v, p = exact(x, y, 0, n, nu)
np.testing.assert_allclose(rho, 1 + 3 * p, atol=1e-15)
np.testing.assert_allclose(np.einsum("q,qyx->yx", CX, g) / rho, u, atol=1e-15)
np.testing.assert_allclose(np.einsum("q,qyx->yx", CY, g) / rho, v, atol=1e-15)
m = metrics((g,), [np.ones((n, n), bool)], 0, n, nu)
for k in ("velocity_l2", "pressure_l2", "energy_relative"):
self.assertLess(m[k], 1e-11)
with self.assertRaises(FloatingPointError):
g[:, 2, 2] = np.nan
metrics((g,), [np.ones((n, n), bool)], 0, n, nu)
def test_geometry_cell_centers_and_scaling(self):
from CelerisLab.lbm.hierarchy import build_hierarchy, leaf_partition
from CelerisLab.lbm.refinement import ACTIVE
for n in (32, 64):
for re in (100, 1000):
for depth in (2, 3):
cfg = config(n, re)
h = build_hierarchy(cfg, rectangles(n, depth))
np.testing.assert_array_equal(leaf_partition(h), np.ones((n, n)))
for level, cfg_l in enumerate(h.configs):
self.assertAlmostEqual(
cfg_l.viscosity, cfg.viscosity * 2**level
)
self.assertLessEqual(cfg_l.omega, cfg_l.omega_max)
x, y = centers(h.kinds[level].shape, level)
self.assertEqual(x[0, 0], 0.5 / 2**level)
self.assertEqual(x[0, -1], n - 0.5 / 2**level)
masks = [k == ACTIVE for k in h.kinds]
g = [
initial(k.shape, l, n, cfg.viscosity)
for l, k in enumerate(h.kinds)
]
result = metrics(g, masks, 0, n, cfg.viscosity)
self.assertLess(result["velocity_l2"], 1e-12)
self.assertLess(result["pressure_l2"], 1e-10)
self.assertLess(result["energy_relative"], 0.002)
def test_complete_assessment_and_corruptions(self):
from copy import deepcopy
from assess import assess
from oracle import TIMES
records = []
for re in (100, 1000):
cases = [(n, 1) for n in (32, 64, 128, 256)]
cases += [(n, d) for n in (32, 64) for d in (2, 3)]
for n, d in cases:
rows = [
dict(
tstar=t,
velocity_l2=0.0,
pressure_l2=0.0,
energy_relative=0.0,
mass_relative=0.0,
)
for t in TIMES
]
records.append(
dict(
re_L=re,
n=n,
depth=d,
U=U,
viscosity=U * n / re,
source=dict(revision="synthetic", dirty=False),
completed=True,
rows=rows,
)
)
self.assertTrue(assess(records)["passed"])
bad = deepcopy(records)
bad[-1]["rows"][-1]["pressure_l2"] = 0.5
failed = assess(bad)
self.assertFalse(failed["passed"])
self.assertEqual(len(failed["comparisons"]), 24)
partial = assess(records[0:1] + records[4:5])
self.assertFalse(partial["passed"])
self.assertEqual(len(partial["comparisons"]), 3)
self.assertIsNone(partial["comparisons"][0]["uniform_finest"])
bad = deepcopy(records)
bad[-1]["rows"][-1]["pressure_l2"] = float("nan")
self.assertFalse(assess(bad)["passed"])
bad = deepcopy(records)
bad[-1]["source"]["dirty"] = True
self.assertFalse(assess(bad)["passed"])
bad = deepcopy(records)
bad[-1]["rows"][-1]["tstar"] = 0.49
self.assertFalse(assess(bad)["passed"])
with self.assertRaises(ValueError):
assess(records + [records[0]])
def test_workload(self):
from run_case import workload
from CelerisLab.lbm.hierarchy import build_hierarchy
from CelerisLab.lbm.refinement import ACTIVE
cases = [(n, 1) for n in (32, 64, 128, 256)]
cases += [(n, d) for n in (32, 64) for d in (2, 3)]
costs = []
for n, d in cases:
h = build_hierarchy(config(n, 100), [] if d == 1 else rectangles(n, d))
costs.append(workload([k == ACTIVE for k in h.kinds], 16 * n))
self.assertEqual(sum(c["active_collision_updates"] for c in costs), 384270336)
self.assertEqual(sum(c["root_intervals"] for c in costs), 10752)
self.assertEqual(costs[-1]["active_collision_updates"], 50987008)
def test_incomplete_assessment_fails(self):
from assess import assess
self.assertFalse(assess([])["passed"])
self.assertEqual(len(assess([])["missing"]), 16)
if __name__ == "__main__":
unittest.main()