301 lines
9.7 KiB
Python
301 lines
9.7 KiB
Python
# CelerisLab/tests/postproc/run_kan99b_streakline.py
|
|
"""Kan99b streakline demo using the new Streakline class (online mode only).
|
|
|
|
Usage::
|
|
|
|
python tests/run_kan99b_streakline.py --domain M --re 100 --alpha 1.0
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import tempfile
|
|
from dataclasses import dataclass
|
|
from typing import Tuple
|
|
|
|
import numpy as np
|
|
|
|
from CelerisLab import Simulation
|
|
from CelerisLab.common.streakline import Streakline, ReleaseConfig, IntegratorConfig
|
|
|
|
_REPO = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
|
_DEFAULT_LBM = os.path.join(_REPO, "src", "CelerisLab", "configs", "config_lbm.json")
|
|
|
|
U_INF = 0.03
|
|
D_LATTICE = 30.0
|
|
R_LATTICE = 15.0
|
|
KAN99B_ST_REF = 0.1655
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class DomainSpec:
|
|
key: str
|
|
nx: int
|
|
ny: int
|
|
center: Tuple[float, float]
|
|
|
|
|
|
def _domain_specs() -> dict:
|
|
return {
|
|
"S": DomainSpec("S", 1081, 481, (360.0, 240.0)),
|
|
"M": DomainSpec("M", 1351, 601, (450.0, 300.0)),
|
|
"L": DomainSpec("L", 1801, 721, (600.0, 360.0)),
|
|
}
|
|
|
|
|
|
def _load_json(path: str) -> dict:
|
|
with open(path, "r", encoding="utf-8") as f:
|
|
return json.load(f)
|
|
|
|
|
|
def _write_json(path: str, payload: dict) -> None:
|
|
with open(path, "w", encoding="utf-8") as f:
|
|
json.dump(payload, f, indent=2)
|
|
|
|
|
|
def _nu_from_re(reynolds: float) -> float:
|
|
return U_INF * D_LATTICE / float(reynolds)
|
|
|
|
|
|
def _omega_body(alpha: float) -> float:
|
|
return 2.0 * float(alpha) * U_INF / D_LATTICE
|
|
|
|
|
|
def _build_cfg(base_cfg: dict, *, nx: int, ny: int, re: float, inlet_scheme: str) -> dict:
|
|
cfg = json.loads(json.dumps(base_cfg))
|
|
cfg["grid"]["nx"] = int(nx)
|
|
cfg["grid"]["ny"] = int(ny)
|
|
cfg["grid"]["nz"] = 1
|
|
cfg["physics"]["velocity"] = float(U_INF)
|
|
cfg["physics"]["viscosity"] = float(_nu_from_re(re))
|
|
cfg["physics"]["rho"] = 1.0
|
|
cfg["method"]["collision"] = "MRT"
|
|
cfg["method"]["streaming"] = "double_buffer"
|
|
cfg["method"]["store_precision"] = "FP32"
|
|
cfg["method"]["ddf_shifting"] = False
|
|
cfg["method"]["les"]["enabled"] = False
|
|
cfg["method"]["inlet"]["profile"] = "uniform"
|
|
cfg["method"]["inlet"]["scheme"] = str(inlet_scheme)
|
|
cfg["method"]["outlet"]["mode"] = "neq_extrap"
|
|
cfg["method"]["y_wall_bc"] = "free_slip"
|
|
return cfg
|
|
|
|
|
|
def _build_simulation(
|
|
*, domain: DomainSpec, re: float, alpha: float, inlet_scheme: str
|
|
) -> Simulation:
|
|
base_cfg = _load_json(_DEFAULT_LBM)
|
|
cfg = _build_cfg(
|
|
base_cfg, nx=domain.nx, ny=domain.ny, re=re, inlet_scheme=inlet_scheme
|
|
)
|
|
body_doc = {
|
|
"objects": [
|
|
{
|
|
"type": "cylinder",
|
|
"center": [float(domain.center[0]), float(domain.center[1])],
|
|
"radius": float(R_LATTICE),
|
|
"omega": float(_omega_body(alpha)),
|
|
}
|
|
]
|
|
}
|
|
tmpd = tempfile.mkdtemp(prefix="celeris_streakline_")
|
|
lbm_tmp = os.path.join(tmpd, "config_lbm.json")
|
|
body_tmp = os.path.join(tmpd, "config_body.json")
|
|
_write_json(lbm_tmp, cfg)
|
|
_write_json(body_tmp, body_doc)
|
|
sim = Simulation(lbm_config_path=lbm_tmp, body_config_path=body_tmp)
|
|
sim.bodies.get(0).state.omega = np.float32(_omega_body(alpha))
|
|
sim.initialize()
|
|
return sim
|
|
|
|
|
|
def _default_base_release_points(center: Tuple[float, float]) -> np.ndarray:
|
|
x_rel = float(center[0] - 6.0 * D_LATTICE)
|
|
y0 = float(center[1])
|
|
return np.array(
|
|
[
|
|
[x_rel, y0 - 18.0],
|
|
[x_rel, y0 - 6.0],
|
|
[x_rel, y0 + 6.0],
|
|
[x_rel, y0 + 18.0],
|
|
],
|
|
dtype=np.float64,
|
|
)
|
|
|
|
|
|
# ---- Sampling plan helper (kept as a local utility, not part of the library) ----
|
|
def _estimate_sampling_plan(
|
|
*,
|
|
st_ref: float,
|
|
diameter: float,
|
|
u_ref: float,
|
|
snapshots_per_period: float = 24.0,
|
|
periods: int = 5,
|
|
) -> dict:
|
|
period_steps = float(diameter) / (float(st_ref) * float(u_ref))
|
|
save_every = int(
|
|
max(20, round(period_steps / snapshots_per_period / 10.0) * 10)
|
|
)
|
|
n_snapshots = int(max(20, round(float(periods) * float(snapshots_per_period))))
|
|
return {
|
|
"st_ref": float(st_ref),
|
|
"period_steps_est": float(period_steps),
|
|
"save_every_recommended": int(save_every),
|
|
"snapshot_count_recommended": int(n_snapshots),
|
|
}
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser(description="Kan99b streakline demo")
|
|
ap.add_argument("--domain", default="M", choices=("S", "M", "L"))
|
|
ap.add_argument("--re", type=float, default=100.0)
|
|
ap.add_argument("--alpha", type=float, default=1.0)
|
|
ap.add_argument("--inlet-scheme", default="regularized",
|
|
choices=("regularized", "zou_he_local"))
|
|
ap.add_argument("--start-step", type=int, default=60_000)
|
|
ap.add_argument("--sample-every", type=int, default=0,
|
|
help="0 uses recommended value.")
|
|
ap.add_argument("--n-snapshots", type=int, default=0,
|
|
help="0 uses recommended value.")
|
|
ap.add_argument("--release-mode", default="strip",
|
|
choices=("point", "line", "strip"))
|
|
ap.add_argument("--line-span", type=float, default=0.0)
|
|
ap.add_argument("--line-count", type=int, default=1)
|
|
ap.add_argument("--downstream-count", type=int, default=5)
|
|
ap.add_argument("--downstream-spacing", type=float, default=1.0)
|
|
ap.add_argument("--inject-per-seed", type=int, default=2)
|
|
ap.add_argument("--alpha-t", type=float, default=0.2)
|
|
ap.add_argument("--alpha-x", type=float, default=0.4)
|
|
ap.add_argument("--diffusion-coeff", type=float, default=0.0)
|
|
ap.add_argument(
|
|
"--out-dir",
|
|
type=str,
|
|
default=os.path.join(
|
|
_REPO, "tests", "output", "streakline", "kan99b_k2"
|
|
),
|
|
)
|
|
args = ap.parse_args()
|
|
|
|
domain = _domain_specs()[args.domain]
|
|
out_dir = os.path.abspath(args.out_dir)
|
|
os.makedirs(out_dir, exist_ok=True)
|
|
|
|
plan = _estimate_sampling_plan(
|
|
st_ref=KAN99B_ST_REF, diameter=D_LATTICE, u_ref=U_INF
|
|
)
|
|
sample_every = (
|
|
int(args.sample_every)
|
|
if int(args.sample_every) > 0
|
|
else int(plan["save_every_recommended"])
|
|
)
|
|
n_snapshots = (
|
|
int(args.n_snapshots)
|
|
if int(args.n_snapshots) > 0
|
|
else int(plan["snapshot_count_recommended"])
|
|
)
|
|
|
|
release_cfg = ReleaseConfig(
|
|
mode=args.release_mode,
|
|
line_span=float(args.line_span),
|
|
line_count=max(1, int(args.line_count)),
|
|
downstream_count=max(1, int(args.downstream_count)),
|
|
downstream_spacing=float(args.downstream_spacing),
|
|
inject_per_seed=max(1, int(args.inject_per_seed)),
|
|
)
|
|
integrator_cfg = IntegratorConfig(
|
|
alpha_t=float(args.alpha_t),
|
|
alpha_x=float(args.alpha_x),
|
|
diffusion_coeff=float(args.diffusion_coeff),
|
|
)
|
|
base_release = _default_base_release_points(domain.center)
|
|
|
|
streak = Streakline(
|
|
release_points=base_release,
|
|
release_cfg=release_cfg,
|
|
integrator_cfg=integrator_cfg,
|
|
nx=domain.nx,
|
|
ny=domain.ny,
|
|
cylinders=[(domain.center, R_LATTICE)],
|
|
)
|
|
|
|
sim = _build_simulation(
|
|
domain=domain,
|
|
re=float(args.re),
|
|
alpha=float(args.alpha),
|
|
inlet_scheme=args.inlet_scheme,
|
|
)
|
|
|
|
# Burn-in phase: step the simulation but don't feed streakline
|
|
print(f"Burning-in {args.start_step} steps ...")
|
|
sim.run(int(args.start_step))
|
|
|
|
# Sampling phase: step and feed velocity frames to streakline
|
|
target_last = int(args.start_step) + sample_every * (n_snapshots - 1)
|
|
frames_collected = 0
|
|
print(
|
|
f"Sampling every {sample_every} steps for {n_snapshots} frames "
|
|
f"(up to step {target_last})..."
|
|
)
|
|
while int(sim.stepper.step_count) < target_last:
|
|
sim.step(1)
|
|
step = int(sim.stepper.step_count)
|
|
if (step - int(args.start_step)) % sample_every != 0:
|
|
continue
|
|
macro = sim.get_macroscopic()
|
|
streak.observe(ux=macro["ux"], uy=macro["uy"], step=step)
|
|
frames_collected += 1
|
|
if frames_collected >= n_snapshots:
|
|
break
|
|
|
|
sim.close()
|
|
|
|
render_info = streak.render(
|
|
os.path.join(out_dir, "streakline.png"),
|
|
age_decay_steps=integrator_cfg.age_decay_steps,
|
|
blur_sigma=1.2,
|
|
)
|
|
|
|
meta = {
|
|
"case": {
|
|
"domain": args.domain,
|
|
"re": float(args.re),
|
|
"alpha": float(args.alpha),
|
|
"inlet_scheme": args.inlet_scheme,
|
|
"collision": "MRT",
|
|
},
|
|
"sampling_estimate": plan,
|
|
"sampling_used": {
|
|
"start_step": int(args.start_step),
|
|
"sample_every": int(sample_every),
|
|
"n_snapshots": int(n_snapshots),
|
|
"frames_collected": frames_collected,
|
|
},
|
|
"release": {
|
|
"config": {
|
|
"mode": args.release_mode,
|
|
"line_span": float(args.line_span),
|
|
"line_count": max(1, int(args.line_count)),
|
|
"downstream_count": max(1, int(args.downstream_count)),
|
|
"downstream_spacing": float(args.downstream_spacing),
|
|
"inject_per_seed": max(1, int(args.inject_per_seed)),
|
|
},
|
|
"base_points": base_release.tolist(),
|
|
},
|
|
"diagnostics": {"n_particles_final": int(streak.n_particles)},
|
|
"render": render_info,
|
|
}
|
|
_write_json(os.path.join(out_dir, "streakline_meta.json"), meta)
|
|
|
|
print(f"Recommended sample_every: {plan['save_every_recommended']} steps")
|
|
print(f"Recommended snapshots: {plan['snapshot_count_recommended']}")
|
|
print(f"Frames collected: {frames_collected}")
|
|
print(f"Final particles: {streak.n_particles}")
|
|
print(f"Output image: {render_info['image_path']}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|