test: integrate periodic Taylor-Green AMR assessment
This commit is contained in:
1 parent
1f263b9e46
commit
9805383f9c
6 files changed
+764
No files matched your search
@@ -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)
|
||||
@@ -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)),
|
||||
)
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user