Files
CelerisLab/tests/validation/run_dfg_re100.py
T

222 lines
9.6 KiB
Python

"""Uniform DFG 2D-2 Re100, existing Zou-He inlet / noncanonical pressure-NEQ outlet."""
import argparse, csv, hashlib, json, math, os, time
from pathlib import Path
from tempfile import TemporaryDirectory
import numpy as np
from re100_helpers import ROOT, PIN, make_config, pressure_probes, outlet_diagnostic
from periodic import assess, compare, GATES, REFERENCE_GATES
from source_provenance import checkout_provenance
def run(a):
import CelerisLab
from CelerisLab.simulation import Simulation
from CelerisLab.lbm.descriptors import FLUID
if Path(CelerisLab.__file__).resolve() != ROOT / "src/CelerisLab/__init__.py":
raise RuntimeError("Wrong source checkout")
manifest = json.loads(
(ROOT / "tests/validation/data/dfg_re100/source_manifest.json").read_text()
)
for rel, digest in manifest["production_sha256"].items():
if hashlib.sha256((ROOT / rel).read_bytes()).hexdigest() != digest:
raise RuntimeError("Production mismatch: " + rel)
if a.output.exists() and any(a.output.iterdir()):
raise ValueError("Output must be empty/new")
a.output.mkdir(parents=True, exist_ok=True)
cfg, body, p = make_config(a.diameter, a.speed, "MRT", "neq_extrap", "zou_he_local")
cap = 5.0 if a.mode == "pilot" else 300.0
max_steps = math.ceil(cap * p["diameter"] / p["mean_speed"])
result = dict(
case="DFG 2D-2 Re100, pressure-NEQ outlet deviation",
pin=PIN,
source_experiment=manifest["experiment"],
**checkout_provenance(ROOT),
production_verified=True,
parameters=p,
mode=a.mode,
cap_tstar=cap,
inlet=cfg["method"]["inlet"],
outlet=cfg["method"]["outlet"],
canonical_outlet="nu*d_n(u)-p*n=0; NOT imposed exactly",
initialization="unchanged production channel-flow initialization, not zero initial velocity",
force_phase="batch-averaged link momentum exchange, timestamp at batch midpoint",
pressure_phase="postcollision end-sample; interpolated onto force-cycle times for comparison",
gates=GATES,
reference_gates=REFERENCE_GATES,
status="running",
history=[],
suite_validated=False,
)
reference = json.loads(
(ROOT / "tests/validation/data/dfg_re100/reference_cycle.json").read_text()
)
started = time.monotonic()
step_seconds = 0.0
healthy = True
with TemporaryDirectory() as tmp:
cp, bp = Path(tmp) / "lbm.json", Path(tmp) / "body.json"
cp.write_text(json.dumps(cfg))
bp.write_text(json.dumps(body))
with Simulation(str(cp), str(bp), device_id=0) as sim:
sim.initialize()
result["startup_seconds"] = time.monotonic() - started
fluid = (sim.field.flag.reshape(p["ny"], p["nx"]) & FLUID) != 0
np.savez_compressed(
a.output / "geometry.npz",
flag=sim.field.flag.reshape(p["ny"], p["nx"]),
x=np.arange(p["nx"]) * p["dx"],
y=(np.arange(p["ny"]) - 0.5) * p["dx"],
)
with (a.output / "history.csv").open("w") as f:
writer = None
while sim.stepper.step_count < max_steps:
begin = sim.stepper.step_count
tstar = begin * p["mean_speed"] / p["diameter"]
# Pilot exercises the expensive dense measurement path too.
width = 0.02 if a.mode == "pilot" or tstar >= 240 else 0.5
n = min(
max(1, round(width * p["diameter"] / p["mean_speed"])),
max_steps - begin,
)
impulse = np.zeros(2)
timer = time.monotonic()
done = 0
while done < n:
batch = min(100, n - done)
sim.run(batch)
impulse += sim.read_force(0).astype(np.float64) * batch
done += batch
step_seconds += time.monotonic() - timer
macro = sim.get_macroscopic()
rho, ux, uy = [
np.asarray(macro[k], np.float64).reshape(p["ny"], p["nx"])
for k in ("rho", "ux", "uy")
]
healthy = bool(
all(np.isfinite(v[fluid]).all() for v in (rho, ux, uy))
and np.isfinite(impulse).all()
and (rho[fluid] > 0).all()
)
if not healthy:
break
den = p["mean_speed"] ** 2 * p["diameter"] / 2
row = dict(
step=sim.stepper.step_count,
tstar=sim.stepper.step_count * p["mean_speed"] / p["diameter"],
tstar_force=(begin + n / 2) * p["mean_speed"] / p["diameter"],
cd=float(impulse[0] / n / den),
cl=float(impulse[1] / n / den),
mass=float(rho[fluid].sum()),
rho_min=float(rho[fluid].min()),
rho_max=float(rho[fluid].max()),
max_mach=float(
np.sqrt(3 * (ux[fluid] ** 2 + uy[fluid] ** 2)).max()
),
flux_in=float(np.sum(rho[1:-1, 2] * ux[1:-1, 2])),
flux_out=float(np.sum(rho[1:-1, -3] * ux[1:-1, -3])),
**pressure_probes(rho, fluid, p),
)
result["history"].append(row)
if (
row["max_mach"] > 0.2
or row["rho_min"] < 0.5
or row["rho_max"] > 1.5
):
healthy = False
result["safety_stop"] = "Mach > .2 or density outside [.5,1.5]"
if writer is None:
writer = csv.DictWriter(f, fieldnames=row)
writer.writeheader()
writer.writerow(row)
if not healthy:
f.flush()
break
if len(result["history"]) == 1:
print(
"READY "
+ json.dumps(
dict(
pid=os.getpid(),
step=row["step"],
tstar=row["tstar"],
all_runtime_and_macro_paths_exercised=True,
)
),
flush=True,
)
if len(result["history"]) % 100 == 0:
f.flush()
print(json.dumps(row), flush=True)
result.update(
steps=sim.stepper.step_count,
step_seconds=step_seconds,
wall_seconds=time.monotonic() - started,
)
result["measurement_seconds"] = max(
0.0,
result["wall_seconds"] - result["startup_seconds"] - step_seconds,
)
result["mlups"] = (
p["nx"] * p["ny"] * result["steps"] / max(step_seconds, 1e-30) / 1e6
)
result.update(assess(result["history"]))
if not healthy:
result["periodic"] = False
result["status"] = (
"invalid_state"
if not healthy
else (
"pilot_complete"
if a.mode == "pilot"
else (
"periodic"
if result["periodic"]
else "incomplete_periodicity"
)
)
)
if healthy:
np.savez_compressed(
a.output / "final_field.npz", rho=rho, ux=ux, uy=uy, fluid=fluid
)
result["outlet_residual"] = outlet_diagnostic(rho, ux, uy, p)
if result["cycles"]:
result["reference_comparison"] = compare(
result["cycles"][-1], reference
)
result["case_reference_pass"] = bool(
healthy
and result["periodic"]
and result.get("reference_comparison", {}).get(
"reference_agreement", False
)
)
(a.output / "summary.json").write_text(
json.dumps(result, indent=2, allow_nan=False)
)
print(
json.dumps({k: v for k, v in result.items() if k not in ("history", "cycles")}),
flush=True,
)
return (
1 if not healthy else (2 if a.mode == "full" and not result["periodic"] else 0)
)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--diameter", type=int, choices=[20, 40, 80], default=20)
parser.add_argument("--speed", type=float, choices=[0.01, 0.02], default=0.01)
parser.add_argument("--mode", choices=["pilot", "full"], default="pilot")
parser.add_argument("--output", type=Path, required=True)
a = parser.parse_args()
if not os.environ.get("CUDA_VISIBLE_DEVICES"):
raise SystemExit("Select physical GPU with CUDA_VISIBLE_DEVICES")
return run(a)
if __name__ == "__main__":
raise SystemExit(main())