"""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())