222 lines
9.6 KiB
Python
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())
|