fix(esopull): correct init layout and pre-streaming semantics (v0.5.1)
EsoPull curved boundaries and wall BCs now use consistent backing-layout reads; InitEsoPull writes equilibrium in t=0 EsoPull layout. Cache N_OBJS after compile and atomic config header writes to avoid parallel races. Adds config screening tools, flume configs, and FP16S/EsoPull diagnosis doc. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,43 +1,37 @@
|
||||
# CelerisLab/tests/postproc/run_exp_ctrl_matrix_streakline.py
|
||||
"""Streakline post-processing for exp_ctrl_matrix cases.
|
||||
|
||||
Runs full CFD, uses Streakline.observe() in the last STREAK_WINDOW steps,
|
||||
renders streakline at the final step.
|
||||
Release from step RELEASE_START; render snapshots at SNAPSHOT_STEPS.
|
||||
Particles are cleared after each snapshot except the last so each frame
|
||||
uses a fresh ~20k-step release window.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
_REPO = Path(__file__).resolve().parents[2]
|
||||
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(_REPO / "src"))
|
||||
sys.path.insert(0, str(_REPO / "tests" / "postproc"))
|
||||
|
||||
import run_exp_ctrl_matrix_vorticity as vort
|
||||
from CelerisLab import Simulation
|
||||
from CelerisLab.common.streakline import Streakline, ReleaseConfig, IntegratorConfig
|
||||
from CelerisLab.common.preprocess import build_triangle_release_points, cylinders_from_triangle_layout
|
||||
from CelerisLab.common.streakline import IntegratorConfig, ReleaseConfig, Streakline
|
||||
|
||||
DIAMETER_CELLS = vort.DIAMETER_CELLS
|
||||
DEFAULT_OUT = _REPO / "tests" / "output" / "exp_ctrl_matrix_streak_ny300"
|
||||
STREAK_WINDOW_STEPS = 20_000
|
||||
DEFAULT_OUT = _REPO / "tests" / "output" / "exp_ctrl_matrix_streak_nx1500"
|
||||
RELEASE_START_STEP = 20_000
|
||||
SNAPSHOT_STEPS = (40_000, 60_000, 80_000, 100_000)
|
||||
CLEAR_AFTER_SNAPSHOT = frozenset({40_000, 60_000, 80_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])
|
||||
STREAK_COLOR = (0.0, 0.35, 0.95) # blue particles on white background
|
||||
|
||||
|
||||
def _apply_body_actions(
|
||||
@@ -52,13 +46,22 @@ def _apply_body_actions(
|
||||
vort._set_body_omegas(sim, w1, w2, w3)
|
||||
|
||||
|
||||
def _cylinders_from_triangle_layout(layout: dict) -> list[tuple[tuple[float, float], float]]:
|
||||
radius = float(layout["radius_lb"])
|
||||
return [
|
||||
((float(layout["x_apex"]), float(layout["y_center"])), radius),
|
||||
((float(layout["x_rear"]), float(layout["y_lower"])), radius),
|
||||
((float(layout["x_rear"]), float(layout["y_upper"])), radius),
|
||||
]
|
||||
def _render_streak(
|
||||
streak: Streakline,
|
||||
out_path: Path,
|
||||
*,
|
||||
step: int,
|
||||
) -> dict:
|
||||
info = streak.render(
|
||||
str(out_path),
|
||||
age_decay_steps=STREAK_AGE_DECAY,
|
||||
blur_sigma=STREAK_BLUR_SIGMA,
|
||||
background_color=(1.0, 1.0, 1.0),
|
||||
streak_color=STREAK_COLOR,
|
||||
)
|
||||
info["step"] = int(step)
|
||||
info["n_particles"] = int(streak.n_particles)
|
||||
return info
|
||||
|
||||
|
||||
def run_streak_case(
|
||||
@@ -68,13 +71,14 @@ def run_streak_case(
|
||||
*,
|
||||
out_dir: Path,
|
||||
total_steps: int,
|
||||
streak_window: int,
|
||||
release_start: int,
|
||||
snapshot_steps: tuple[int, ...],
|
||||
sample_every: int,
|
||||
report_every: int,
|
||||
device_id: int,
|
||||
) -> dict:
|
||||
streak_start = max(0, int(total_steps) - int(streak_window))
|
||||
compat = vort._ensure_compat_config(vort.CONFIG_PATH)
|
||||
sim = Simulation(compat)
|
||||
sim = Simulation(compat, device_id=device_id)
|
||||
layout = vort._add_triangle_cylinders(sim)
|
||||
sim.initialize()
|
||||
u_lb = float(sim.lbm_cfg.velocity)
|
||||
@@ -82,7 +86,7 @@ def run_streak_case(
|
||||
(vort.CYLINDER_DIAMETER_M / DIAMETER_CELLS)
|
||||
* (u_lb / vort.INLET_U_PHYS_M_S)
|
||||
)
|
||||
cylinders = _cylinders_from_triangle_layout(layout)
|
||||
cylinders = cylinders_from_triangle_layout(layout)
|
||||
|
||||
release_cfg = ReleaseConfig(
|
||||
mode="strip",
|
||||
@@ -95,7 +99,12 @@ def run_streak_case(
|
||||
integrator_cfg = IntegratorConfig(
|
||||
alpha_t=0.25, alpha_x=0.40, max_particle_age=None
|
||||
)
|
||||
base_release = build_release_points_for_triangle(layout)
|
||||
base_release = build_triangle_release_points(
|
||||
layout,
|
||||
nx=int(sim.lbm_cfg.nx),
|
||||
ny=int(sim.lbm_cfg.ny),
|
||||
diameter_cells=DIAMETER_CELLS,
|
||||
)
|
||||
|
||||
streak = Streakline(
|
||||
release_points=base_release,
|
||||
@@ -106,59 +115,71 @@ def run_streak_case(
|
||||
cylinders=cylinders,
|
||||
)
|
||||
|
||||
snapshot_set = set(snapshot_steps)
|
||||
print(
|
||||
f"--- {case_id} {slug} steps={total_steps} streak_window={streak_window} "
|
||||
f"(inject from step {streak_start}) ---"
|
||||
f"--- {case_id} {slug} steps={total_steps} release_from={release_start} "
|
||||
f"snapshots={list(snapshot_steps)} device={device_id} ---"
|
||||
)
|
||||
|
||||
snapshots: list[dict] = []
|
||||
|
||||
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)
|
||||
|
||||
# Feed streakline within the window
|
||||
if step >= streak_start and (step + 1) % sample_every == 0:
|
||||
current_step = step + 1
|
||||
observed = False
|
||||
if step >= release_start and current_step % sample_every == 0:
|
||||
macro = sim.get_macroscopic()
|
||||
streak.observe(ux=macro["ux"], uy=macro["uy"], step=int(step + 1))
|
||||
streak.observe(ux=macro["ux"], uy=macro["uy"], step=current_step)
|
||||
observed = True
|
||||
|
||||
if report_every > 0 and (step + 1) % report_every == 0:
|
||||
if current_step in snapshot_set:
|
||||
if streak.n_particles == 0:
|
||||
raise RuntimeError(
|
||||
f"{case_id}: no particles at step {current_step}; "
|
||||
f"check release_start/sample_every."
|
||||
)
|
||||
png = out_dir / f"streakline_{case_id}_{slug}_step{current_step:06d}.png"
|
||||
snap_info = _render_streak(streak, png, step=current_step)
|
||||
snap_info["image_path"] = str(png)
|
||||
snapshots.append(snap_info)
|
||||
print(
|
||||
f" step {step+1}/{total_steps} a=({a1:+.5f},{a2:+.5f},{a3:+.5f}) "
|
||||
f"particles={streak.n_particles}"
|
||||
f" snapshot step {current_step} particles={streak.n_particles} "
|
||||
f"-> {png.name}"
|
||||
)
|
||||
|
||||
if streak.n_particles == 0:
|
||||
raise RuntimeError(
|
||||
f"{case_id}: no particles in streak window; lower sample_every."
|
||||
)
|
||||
if current_step in CLEAR_AFTER_SNAPSHOT and current_step < total_steps:
|
||||
streak.reset()
|
||||
if observed:
|
||||
macro = sim.get_macroscopic()
|
||||
streak.observe(ux=macro["ux"], uy=macro["uy"], step=current_step)
|
||||
|
||||
if report_every > 0 and current_step % report_every == 0:
|
||||
print(
|
||||
f" step {current_step}/{total_steps} "
|
||||
f"a=({a1:+.5f},{a2:+.5f},{a3:+.5f}) particles={streak.n_particles}"
|
||||
)
|
||||
|
||||
png = out_dir / f"streakline_{case_id}_{slug}.png"
|
||||
render_info = streak.render(
|
||||
str(png),
|
||||
age_decay_steps=STREAK_AGE_DECAY,
|
||||
blur_sigma=STREAK_BLUR_SIGMA,
|
||||
background_color=(1.0, 1.0, 1.0),
|
||||
streak_color=(1.0, 0.0, 0.0),
|
||||
)
|
||||
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),
|
||||
"release_start_step": int(release_start),
|
||||
"snapshot_steps": list(snapshot_steps),
|
||||
"clear_after_snapshot": sorted(CLEAR_AFTER_SNAPSHOT),
|
||||
"sample_every": int(sample_every),
|
||||
"particle_count_final": int(streak.n_particles),
|
||||
"device_id": int(device_id),
|
||||
"release_points_dense": int(base_release.shape[0]),
|
||||
"streak_png": str(png),
|
||||
"snapshots": snapshots,
|
||||
"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={streak.n_particles}")
|
||||
return summary
|
||||
|
||||
|
||||
@@ -166,12 +187,22 @@ 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("--release-start", type=int, default=RELEASE_START_STEP)
|
||||
ap.add_argument(
|
||||
"--snapshots",
|
||||
type=str,
|
||||
default=",".join(str(s) for s in SNAPSHOT_STEPS),
|
||||
help="Comma-separated render steps, e.g. 40000,60000,100000",
|
||||
)
|
||||
ap.add_argument("--sample-every", type=int, default=STREAK_SAMPLE_EVERY)
|
||||
ap.add_argument("--report-every", type=int, default=20000)
|
||||
ap.add_argument("--device-id", type=int, default=2)
|
||||
ap.add_argument("--cases", type=str, default="")
|
||||
args = ap.parse_args()
|
||||
|
||||
snapshot_steps = tuple(
|
||||
int(s.strip()) for s in args.snapshots.split(",") if s.strip()
|
||||
)
|
||||
out_dir = Path(args.out_dir)
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
selected = (
|
||||
@@ -184,7 +215,8 @@ def main() -> int:
|
||||
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}"
|
||||
f"release from {args.release_start} | snapshots={list(snapshot_steps)} | "
|
||||
f"device={args.device_id}"
|
||||
)
|
||||
|
||||
summaries = []
|
||||
@@ -198,17 +230,24 @@ def main() -> int:
|
||||
features,
|
||||
out_dir=out_dir,
|
||||
total_steps=int(args.steps),
|
||||
streak_window=int(args.streak_window),
|
||||
release_start=int(args.release_start),
|
||||
snapshot_steps=snapshot_steps,
|
||||
sample_every=int(args.sample_every),
|
||||
report_every=int(args.report_every),
|
||||
device_id=int(args.device_id),
|
||||
)
|
||||
)
|
||||
|
||||
manifest = {
|
||||
"grid": grid,
|
||||
"steps": int(args.steps),
|
||||
"streak_window_steps": int(args.streak_window),
|
||||
"release_start_step": int(args.release_start),
|
||||
"snapshot_steps": list(snapshot_steps),
|
||||
"clear_after_snapshot": sorted(CLEAR_AFTER_SNAPSHOT),
|
||||
"sample_every": int(args.sample_every),
|
||||
"streak_color": list(STREAK_COLOR),
|
||||
"stealth_steady_omega_m_s": float(vort.STEALTH_STEADY_OMEGA_M_S),
|
||||
"device_id": int(args.device_id),
|
||||
"swap_action23_bodies": bool(vort.SWAP_ACTION23_BODIES),
|
||||
"cases": summaries,
|
||||
}
|
||||
|
||||
@@ -48,8 +48,12 @@ OMEGA_SIGN_FROM_ACTION = -1.0
|
||||
VORT_VMIN = -0.003
|
||||
VORT_VMAX = 0.003
|
||||
|
||||
STEALTH_REF_OMEGA_M_S = 0.01806
|
||||
STEALTH_STEADY_FRAC = 1.25 # s125 from steady sweep
|
||||
STEALTH_STEADY_OMEGA_M_S = STEALTH_REF_OMEGA_M_S * STEALTH_STEADY_FRAC
|
||||
|
||||
CONFIG_PATH = _REPO / "src/CelerisLab/configs/config_lbm_three_cylinder_triangle.json"
|
||||
DEFAULT_OUT = _REPO / "tests" / "output" / "exp_ctrl_matrix_vort_ny300"
|
||||
DEFAULT_OUT = _REPO / "tests" / "output" / "exp_ctrl_matrix_vort_nx1500"
|
||||
FIXED_STEPS = 100000 # keep constant while grid changes
|
||||
# Body order: 0=apex, 1=rear-lower(y_lower), 2=rear-upper(y_upper); swap action2/action3 targets.
|
||||
SWAP_ACTION23_BODIES = True
|
||||
@@ -70,8 +74,8 @@ CONTROL_CASES: List[Tuple[str, str, Dict[str, Any]]] = [
|
||||
"stealth",
|
||||
{
|
||||
"action1": {"mean": 0.0, "components": [(0.1354, 0.0, 1.600)]},
|
||||
"action2": {"mean": -0.01806, "components": [(0.1354, 0.0, 2.099)]},
|
||||
"action3": {"mean": 0.01806, "components": [(0.1354, 0.0, 1.639)]},
|
||||
"action2": {"mean": -STEALTH_STEADY_OMEGA_M_S, "components": [(0.1354, 0.0, 2.099)]},
|
||||
"action3": {"mean": STEALTH_STEADY_OMEGA_M_S, "components": [(0.1354, 0.0, 1.639)]},
|
||||
},
|
||||
),
|
||||
(
|
||||
@@ -308,43 +312,31 @@ def run_case(
|
||||
dt_phys = dx_phys * (u_lb / INLET_U_PHYS_M_S)
|
||||
cylinders = cylinders_from_triangle_layout(layout)
|
||||
|
||||
print(f"--- {case_id} {slug} steps={steps} u_lb={u_lb} dt_phys={dt_phys} batch={batch} ---")
|
||||
stream = sim.stream
|
||||
batch_size = max(1, int(batch))
|
||||
print(f"--- {case_id} {slug} steps={steps} u_lb={u_lb} dt_phys={dt_phys} ---")
|
||||
|
||||
# Main loop: precompute actions, batch-step, read forces/sensors at intervals
|
||||
for batch_start in range(0, steps, batch_size):
|
||||
batch_end = min(batch_start + batch_size, steps)
|
||||
for j in range(batch_start, batch_end):
|
||||
t_phys = j * dt_phys
|
||||
a1, a2, a3 = _actions_at_time(t_phys, features)
|
||||
w1 = _action_to_omega_lb(a1, u_lb)
|
||||
w2 = _action_to_omega_lb(a2, u_lb)
|
||||
w3 = _action_to_omega_lb(a3, u_lb)
|
||||
if SWAP_ACTION23_BODIES:
|
||||
_set_body_omegas(sim, w1, w3, w2)
|
||||
else:
|
||||
_set_body_omegas(sim, w1, w2, w3)
|
||||
for step in range(steps):
|
||||
t_phys = step * dt_phys
|
||||
a1, a2, a3 = _actions_at_time(t_phys, features)
|
||||
w1 = _action_to_omega_lb(a1, u_lb)
|
||||
w2 = _action_to_omega_lb(a2, u_lb)
|
||||
w3 = _action_to_omega_lb(a3, u_lb)
|
||||
if SWAP_ACTION23_BODIES:
|
||||
_set_body_omegas(sim, w1, w3, w2)
|
||||
else:
|
||||
_set_body_omegas(sim, w1, w2, w3)
|
||||
sim.run(1, sync_obs=False)
|
||||
|
||||
n = batch_end - batch_start
|
||||
sim.stepper.step(
|
||||
n,
|
||||
action_gpu=sim.bodies.action_gpu,
|
||||
obs_gpu=sim.bodies.obs_gpu,
|
||||
stream=stream,
|
||||
)
|
||||
|
||||
if report_every > 0 and (batch_end % report_every == 0 or batch_end == steps):
|
||||
stream.synchronize()
|
||||
if report_every > 0 and ((step + 1) % report_every == 0 or step + 1 == steps):
|
||||
sim.stream.synchronize()
|
||||
for bid in range(sim.bodies.count):
|
||||
fx = sim.bodies.read_force(bid)
|
||||
print(
|
||||
f" step={batch_end} body={bid}"
|
||||
f" step={step + 1} body={bid}"
|
||||
f" fx={float(fx[0]):+.6f} fy={float(fx[1]):+.6f}",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
stream.synchronize()
|
||||
sim.stream.synchronize()
|
||||
macro = sim.get_macroscopic()
|
||||
vort = compute_vorticity(macro["ux"], macro["uy"])
|
||||
png = out_dir / f"vorticity_{case_id}_{slug}.png"
|
||||
@@ -461,6 +453,8 @@ def main() -> int:
|
||||
"device_id": int(args.device_id),
|
||||
"vort_vmin": VORT_VMIN,
|
||||
"vort_vmax": VORT_VMAX,
|
||||
"stealth_steady_omega_m_s": STEALTH_STEADY_OMEGA_M_S,
|
||||
"stealth_steady_frac": STEALTH_STEADY_FRAC,
|
||||
"swap_action23_bodies": bool(SWAP_ACTION23_BODIES),
|
||||
"cases": summaries,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
# CelerisLab/tests/postproc/run_stealth_steady_sweep.py
|
||||
"""Steady stealth rotation sweep: vorticity + final-step streakline per speed.
|
||||
|
||||
Grid nx=1500 (see config_lbm_three_cylinder_triangle.json). Steady means
|
||||
constant surface-speed means only (no harmonic components).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import List, Tuple
|
||||
|
||||
_REPO = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(_REPO / "src"))
|
||||
sys.path.insert(0, str(_REPO / "tests" / "postproc"))
|
||||
|
||||
import run_exp_ctrl_matrix_vorticity as vort_mod
|
||||
import run_exp_ctrl_matrix_streakline as streak
|
||||
from CelerisLab import Simulation
|
||||
from CelerisLab.common.preprocess import build_triangle_release_points, cylinders_from_triangle_layout
|
||||
from CelerisLab.common.render import compute_vorticity, render_vorticity_field
|
||||
from CelerisLab.common.streakline import Streakline, IntegratorConfig, ReleaseConfig
|
||||
|
||||
STEALTH_REF_M_S = 0.01806
|
||||
# Fractions of reference stealth surface speed (action2 negative, action3 positive).
|
||||
SPEED_FRACTIONS: List[Tuple[str, float]] = [
|
||||
("s050", 0.50),
|
||||
("s075", 0.75),
|
||||
("s100", 1.00),
|
||||
("s125", 1.25),
|
||||
("s150", 1.50),
|
||||
]
|
||||
DEFAULT_OUT = _REPO / "tests" / "output" / "stealth_steady_sweep_nx1500"
|
||||
|
||||
|
||||
def _stealth_features(omega_m_s: float) -> dict:
|
||||
return {
|
||||
"action1": {"mean": 0.0, "components": []},
|
||||
"action2": {"mean": -float(omega_m_s), "components": []},
|
||||
"action3": {"mean": float(omega_m_s), "components": []},
|
||||
}
|
||||
|
||||
|
||||
def run_one(
|
||||
tag: str,
|
||||
omega_m_s: float,
|
||||
*,
|
||||
out_dir: Path,
|
||||
total_steps: int,
|
||||
streak_window: int,
|
||||
sample_every: int,
|
||||
report_every: int,
|
||||
device_id: int,
|
||||
) -> dict:
|
||||
features = _stealth_features(omega_m_s)
|
||||
slug = f"stealth_{tag}_w{omega_m_s:.5f}"
|
||||
|
||||
compat = vort_mod._ensure_compat_config(vort_mod.CONFIG_PATH)
|
||||
sim = Simulation(compat, device_id=device_id)
|
||||
layout = vort_mod._add_triangle_cylinders(sim)
|
||||
sim.initialize()
|
||||
u_lb = float(sim.lbm_cfg.velocity)
|
||||
nx = int(sim.lbm_cfg.nx)
|
||||
ny = int(sim.lbm_cfg.ny)
|
||||
dt_phys = (vort_mod.CYLINDER_DIAMETER_M / vort_mod.DIAMETER_CELLS) * (
|
||||
u_lb / vort_mod.INLET_U_PHYS_M_S
|
||||
)
|
||||
cylinders = cylinders_from_triangle_layout(layout)
|
||||
base_release = build_triangle_release_points(
|
||||
layout, nx=nx, ny=ny, diameter_cells=vort_mod.DIAMETER_CELLS
|
||||
)
|
||||
|
||||
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)
|
||||
streak_obj = Streakline(
|
||||
release_points=base_release,
|
||||
release_cfg=release_cfg,
|
||||
integrator_cfg=integrator_cfg,
|
||||
nx=nx,
|
||||
ny=ny,
|
||||
cylinders=cylinders,
|
||||
)
|
||||
|
||||
streak_start = max(0, int(total_steps) - int(streak_window))
|
||||
|
||||
print(
|
||||
f"--- {slug} omega={omega_m_s:.5f} m/s steps={total_steps} "
|
||||
f"grid={nx}x{ny} streak_from={streak_start} ---"
|
||||
)
|
||||
print(
|
||||
f" layout x_apex={layout['x_apex']:.1f} x_rear={layout['x_rear']:.1f} "
|
||||
f"release_x={base_release[0, 0]:.1f}"
|
||||
)
|
||||
|
||||
for step in range(total_steps):
|
||||
t_phys = step * dt_phys
|
||||
a1, a2, a3 = vort_mod._actions_at_time(t_phys, features)
|
||||
w1 = vort_mod._action_to_omega_lb(a1, u_lb)
|
||||
w2 = vort_mod._action_to_omega_lb(a2, u_lb)
|
||||
w3 = vort_mod._action_to_omega_lb(a3, u_lb)
|
||||
if vort_mod.SWAP_ACTION23_BODIES:
|
||||
vort_mod._set_body_omegas(sim, w1, w3, w2)
|
||||
else:
|
||||
vort_mod._set_body_omegas(sim, w1, w2, w3)
|
||||
sim.run(1)
|
||||
|
||||
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"omega_lb=({w1:+.6f},{w2:+.6f},{w3:+.6f}) "
|
||||
f"particles={streak_obj.n_particles}"
|
||||
)
|
||||
|
||||
if step >= streak_start and (step + 1) % sample_every == 0:
|
||||
macro = sim.get_macroscopic()
|
||||
streak_obj.observe(ux=macro["ux"], uy=macro["uy"], step=int(step + 1))
|
||||
|
||||
if streak_obj.n_particles == 0:
|
||||
raise RuntimeError(f"{slug}: no particles in streak window.")
|
||||
|
||||
macro = sim.get_macroscopic()
|
||||
vort_field = compute_vorticity(macro["ux"], macro["uy"])
|
||||
|
||||
vort_png = out_dir / f"vorticity_{slug}.png"
|
||||
streak_png = out_dir / f"streakline_{slug}.png"
|
||||
ckpt = out_dir / f"state_{slug}.h5"
|
||||
sim.save_checkpoint(str(ckpt))
|
||||
|
||||
vort_info = render_vorticity_field(
|
||||
vort_field,
|
||||
nx=nx,
|
||||
ny=ny,
|
||||
out_path=str(vort_png),
|
||||
cylinders=cylinders,
|
||||
vmin=vort_mod.VORT_VMIN,
|
||||
vmax=vort_mod.VORT_VMAX,
|
||||
minimal_axes=True,
|
||||
)
|
||||
streak_info = streak_obj.render(
|
||||
str(streak_png),
|
||||
age_decay_steps=streak.STREAK_AGE_DECAY,
|
||||
blur_sigma=streak.STREAK_BLUR_SIGMA,
|
||||
background_color=(1.0, 1.0, 1.0),
|
||||
streak_color=streak.STREAK_COLOR,
|
||||
)
|
||||
sim.close()
|
||||
|
||||
summary = {
|
||||
"tag": tag,
|
||||
"slug": slug,
|
||||
"omega_m_s": float(omega_m_s),
|
||||
"fraction_of_ref": float(omega_m_s / STEALTH_REF_M_S),
|
||||
"total_steps": int(total_steps),
|
||||
"streak_window_steps": int(streak_window),
|
||||
"streak_start_step": int(streak_start),
|
||||
"sample_every": int(sample_every),
|
||||
"layout": {k: float(layout[k]) for k in layout},
|
||||
"release_points": base_release.tolist(),
|
||||
"particle_count_final": int(streak_obj.n_particles),
|
||||
"vort_png": str(vort_png),
|
||||
"streak_png": str(streak_png),
|
||||
"checkpoint": str(ckpt),
|
||||
"vorticity": vort_info,
|
||||
"streakline": streak_info,
|
||||
}
|
||||
with (out_dir / f"summary_{slug}.json").open("w", encoding="utf-8") as f:
|
||||
json.dump(summary, f, indent=2)
|
||||
print(f" saved {vort_png.name} {streak_png.name} particles={streak_obj.n_particles}")
|
||||
return summary
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description="steady stealth rotation sweep")
|
||||
ap.add_argument("--out-dir", type=str, default=str(DEFAULT_OUT))
|
||||
ap.add_argument("--steps", type=int, default=vort_mod.FIXED_STEPS)
|
||||
ap.add_argument("--streak-window", type=int, default=streak.STREAK_WINDOW_STEPS)
|
||||
ap.add_argument("--sample-every", type=int, default=streak.STREAK_SAMPLE_EVERY)
|
||||
ap.add_argument("--report-every", type=int, default=20000)
|
||||
ap.add_argument("--device-id", type=int, default=0)
|
||||
ap.add_argument("--tags", type=str, default="", help="Comma tags e.g. s050,s100 or empty=all")
|
||||
args = ap.parse_args()
|
||||
|
||||
out_dir = Path(args.out_dir)
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
selected = {t.strip() for t in args.tags.split(",") if t.strip()} if args.tags else None
|
||||
|
||||
with vort_mod.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"ref_omega={STEALTH_REF_M_S} m/s"
|
||||
)
|
||||
|
||||
summaries = []
|
||||
for tag, frac in SPEED_FRACTIONS:
|
||||
if selected and tag not in selected:
|
||||
continue
|
||||
omega = STEALTH_REF_M_S * frac
|
||||
summaries.append(
|
||||
run_one(
|
||||
tag,
|
||||
omega,
|
||||
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),
|
||||
device_id=int(args.device_id),
|
||||
)
|
||||
)
|
||||
|
||||
manifest = {
|
||||
"grid": grid,
|
||||
"steps": int(args.steps),
|
||||
"stealth_ref_m_s": STEALTH_REF_M_S,
|
||||
"speed_fractions": SPEED_FRACTIONS,
|
||||
"streak_color": list(streak.STREAK_COLOR),
|
||||
"cases": summaries,
|
||||
}
|
||||
with (out_dir / "manifest.json").open("w", encoding="utf-8") as f:
|
||||
json.dump(manifest, f, indent=2)
|
||||
print(f"Manifest: {out_dir / 'manifest.json'}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user