CelerisLab/tests/run_exp_ctrl_matrix_streakline.py

234 lines
8.0 KiB
Python

# CelerisLab/tests/run_exp_ctrl_matrix_streakline.py
"""Streakline (final-step particle cloud) for exp_ctrl_matrix cases.
Runs full CFD to FIXED_STEPS, injects particles only in the last STREAK_WINDOW steps,
renders streakline at the final step (not path history).
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
import numpy as np
_REPO = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(_REPO / "src"))
sys.path.insert(0, str(_REPO / "tests"))
import run_exp_ctrl_matrix_vorticity as vort
from CelerisLab import Simulation
from CelerisLab.common.streakline import (
FlowFrame,
IntegratorConfig,
ReleaseConfig,
advance_particles_between_frames,
build_release_points,
cylinders_from_triangle_layout,
render_streakline_density,
)
DIAMETER_CELLS = vort.DIAMETER_CELLS
DEFAULT_OUT = _REPO / "tests" / "output" / "exp_ctrl_matrix_streak_ny300"
STREAK_WINDOW_STEPS = 20_000
STREAK_SAMPLE_EVERY = 50
STREAK_AGE_DECAY = 100_000.0
STREAK_BLUR_SIGMA = 0.8
def build_release_points_for_triangle(layout: dict) -> np.ndarray:
release_x = min(layout["x_apex"], layout["x_rear"]) - 4.0 * DIAMETER_CELLS
y_low_edge = layout["y_lower"] - layout["radius_lb"]
y_high_edge = layout["y_upper"] + layout["radius_lb"]
ys = np.linspace(y_low_edge, y_high_edge, 4, dtype=np.float64)
return np.column_stack([np.full(4, release_x, dtype=np.float64), ys])
def _apply_body_actions(sim: Simulation, a1: float, a2: float, a3: float, u_lb: float) -> None:
w1 = vort._action_to_omega_lb(a1, u_lb)
w2 = vort._action_to_omega_lb(a2, u_lb)
w3 = vort._action_to_omega_lb(a3, u_lb)
if vort.SWAP_ACTION23_BODIES:
vort._set_body_omegas(sim, w1, w3, w2)
else:
vort._set_body_omegas(sim, w1, w2, w3)
def run_streak_case(
case_id: str,
slug: str,
features: dict,
*,
out_dir: Path,
total_steps: int,
streak_window: int,
sample_every: int,
report_every: int,
) -> dict:
streak_start = max(0, int(total_steps) - int(streak_window))
compat = vort._ensure_compat_config(vort.CONFIG_PATH)
sim = Simulation(compat)
layout = vort._add_triangle_cylinders(sim)
sim.initialize()
u_lb = float(sim.lbm_cfg.velocity)
dt_phys = (vort.CYLINDER_DIAMETER_M / DIAMETER_CELLS) * (u_lb / vort.INLET_U_PHYS_M_S)
cylinders = cylinders_from_triangle_layout(layout)
release_cfg = ReleaseConfig(
mode="strip",
line_count=1,
line_span=0.0,
downstream_count=5,
downstream_spacing=1.0,
inject_per_seed=1,
)
integrator_cfg = IntegratorConfig(alpha_t=0.25, alpha_x=0.40, max_particle_age=None)
base_release = build_release_points_for_triangle(layout)
release_points = build_release_points(base_release, release_cfg)
particles = np.empty((0, 2), dtype=np.float64)
ages = np.empty((0,), dtype=np.float64)
prev_frame: FlowFrame | None = None
print(
f"--- {case_id} {slug} steps={total_steps} streak_window={streak_window} "
f"(inject from step {streak_start}) ---"
)
for step in range(total_steps):
t_phys = step * dt_phys
a1, a2, a3 = vort._actions_at_time(t_phys, features)
_apply_body_actions(sim, a1, a2, a3, u_lb)
sim.run(1)
if step >= streak_start and (step + 1) % sample_every == 0:
macro = sim.get_macroscopic()
curr_frame = FlowFrame(
step=int(step + 1),
ux=np.asarray(macro["ux"], dtype=np.float64),
uy=np.asarray(macro["uy"], dtype=np.float64),
)
new_particles = np.repeat(release_points, release_cfg.inject_per_seed, axis=0)
if particles.size:
particles = np.vstack([particles, new_particles])
ages = np.concatenate([ages, np.zeros(new_particles.shape[0], dtype=np.float64)])
else:
particles = new_particles.copy()
ages = np.zeros(new_particles.shape[0], dtype=np.float64)
if prev_frame is not None and particles.size:
particles, ages, _, _ = advance_particles_between_frames(
particles,
ages,
prev_frame,
curr_frame,
nx=int(sim.lbm_cfg.nx),
ny=int(sim.lbm_cfg.ny),
cfg=integrator_cfg,
cylinders=cylinders,
)
prev_frame = curr_frame
if report_every > 0 and (step + 1) % report_every == 0:
print(
f" step {step+1}/{total_steps} a=({a1:+.5f},{a2:+.5f},{a3:+.5f}) "
f"particles={particles.shape[0]}"
)
if particles.size == 0:
raise RuntimeError(f"{case_id}: no particles in streak window; lower sample_every.")
png = out_dir / f"streakline_{case_id}_{slug}.png"
render_info = render_streakline_density(
particles,
ages,
nx=int(sim.lbm_cfg.nx),
ny=int(sim.lbm_cfg.ny),
out_path=str(png),
cylinders=cylinders,
age_decay_steps=STREAK_AGE_DECAY,
blur_sigma=STREAK_BLUR_SIGMA,
minimal_axes=True,
background_color=(1.0, 1.0, 1.0),
streak_color=(1.0, 0.0, 0.0),
show_release_points=False,
)
sim.close()
summary = {
"case_id": case_id,
"slug": slug,
"total_steps": int(total_steps),
"streak_window_steps": int(streak_window),
"streak_start_step": int(streak_start),
"sample_every": int(sample_every),
"particle_count_final": int(particles.shape[0]),
"release_points_dense": int(release_points.shape[0]),
"streak_png": str(png),
"swap_action23_bodies": bool(vort.SWAP_ACTION23_BODIES),
"render": render_info,
}
with (out_dir / f"summary_{case_id}_{slug}.json").open("w", encoding="utf-8") as f:
json.dump(summary, f, indent=2)
print(f" saved {png} particles={particles.shape[0]}")
return summary
def main() -> int:
ap = argparse.ArgumentParser(description="exp_ctrl_matrix streakline batch")
ap.add_argument("--out-dir", type=str, default=str(DEFAULT_OUT))
ap.add_argument("--steps", type=int, default=vort.FIXED_STEPS)
ap.add_argument("--streak-window", type=int, default=STREAK_WINDOW_STEPS)
ap.add_argument("--sample-every", type=int, default=STREAK_SAMPLE_EVERY)
ap.add_argument("--report-every", type=int, default=20000)
ap.add_argument("--cases", type=str, default="")
args = ap.parse_args()
out_dir = Path(args.out_dir)
out_dir.mkdir(parents=True, exist_ok=True)
selected = {c.strip() for c in args.cases.split(",") if c.strip()} if args.cases else None
with vort.CONFIG_PATH.open("r", encoding="utf-8") as f:
grid = json.load(f)["grid"]
print(
f"Output: {out_dir} | grid={grid['nx']}x{grid['ny']} | steps={args.steps} | "
f"streak last {args.streak_window} steps, sample_every={args.sample_every}"
)
summaries = []
for case_id, slug, features in vort.CONTROL_CASES:
if selected and case_id not in selected:
continue
summaries.append(
run_streak_case(
case_id,
slug,
features,
out_dir=out_dir,
total_steps=int(args.steps),
streak_window=int(args.streak_window),
sample_every=int(args.sample_every),
report_every=int(args.report_every),
)
)
manifest = {
"grid": grid,
"steps": int(args.steps),
"streak_window_steps": int(args.streak_window),
"sample_every": int(args.sample_every),
"swap_action23_bodies": bool(vort.SWAP_ACTION23_BODIES),
"cases": summaries,
}
manifest_path = out_dir / "manifest.json"
with manifest_path.open("w", encoding="utf-8") as f:
json.dump(manifest, f, indent=2)
print(f"Manifest: {manifest_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())