重构body api,性能分析,项目整理
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -0,0 +1,223 @@
|
||||
# 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.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
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
|
||||
|
||||
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 _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 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)
|
||||
|
||||
streak = Streakline(
|
||||
release_points=base_release,
|
||||
release_cfg=release_cfg,
|
||||
integrator_cfg=integrator_cfg,
|
||||
nx=int(sim.lbm_cfg.nx),
|
||||
ny=int(sim.lbm_cfg.ny),
|
||||
cylinders=cylinders,
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
# Feed streakline within the window
|
||||
if step >= streak_start and (step + 1) % sample_every == 0:
|
||||
macro = sim.get_macroscopic()
|
||||
streak.observe(ux=macro["ux"], uy=macro["uy"], step=int(step + 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"particles={streak.n_particles}"
|
||||
)
|
||||
|
||||
if streak.n_particles == 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 = 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),
|
||||
"sample_every": int(sample_every),
|
||||
"particle_count_final": int(streak.n_particles),
|
||||
"release_points_dense": int(base_release.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={streak.n_particles}")
|
||||
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())
|
||||
@@ -0,0 +1,475 @@
|
||||
# CelerisLab/tests/postproc/run_exp_ctrl_matrix_vorticity.py
|
||||
"""Batch vorticity images for three-cylinder control matrix (exp_ctrl_matrix.md).
|
||||
|
||||
Usage::
|
||||
|
||||
# Single case
|
||||
conda run -n pycuda_3_10 python tests/postproc/run_exp_ctrl_matrix_vorticity.py \\
|
||||
--cases C0 --batch 10 --device-id 0
|
||||
|
||||
# All cases
|
||||
conda run -n pycuda_3_10 python tests/postproc/run_exp_ctrl_matrix_vorticity.py \\
|
||||
--batch 10 --device-id 0
|
||||
|
||||
# Full 100k steps, no batching (default)
|
||||
conda run -n pycuda_3_10 python tests/postproc/run_exp_ctrl_matrix_vorticity.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Tuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
_REPO = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(_REPO / "src"))
|
||||
|
||||
from CelerisLab import Simulation
|
||||
from CelerisLab.common.render import (
|
||||
compute_vorticity,
|
||||
render_vorticity_field,
|
||||
)
|
||||
from CelerisLab.common.preprocess import cylinders_from_triangle_layout
|
||||
|
||||
INLET_U_PHYS_M_S = 0.009028
|
||||
CYLINDER_DIAMETER_M = 0.010
|
||||
CENTER_SPACING_M = 0.015
|
||||
DIAMETER_CELLS = 20.0
|
||||
RAMP_TIME_S = 5.0
|
||||
INITIAL_ACTIONS_M_S = (0.0, 0.0, 0.0)
|
||||
OMEGA_SIGN_FROM_ACTION = -1.0
|
||||
VORT_VMIN = -0.003
|
||||
VORT_VMAX = 0.003
|
||||
|
||||
CONFIG_PATH = _REPO / "src/CelerisLab/configs/config_lbm_three_cylinder_triangle.json"
|
||||
DEFAULT_OUT = _REPO / "tests" / "output" / "exp_ctrl_matrix_vort_ny300"
|
||||
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
|
||||
|
||||
# From tests/exp_ctrl_matrix.md (SIGNAL_FEATURES0 .. 6)
|
||||
CONTROL_CASES: List[Tuple[str, str, Dict[str, Any]]] = [
|
||||
(
|
||||
"C0",
|
||||
"no_ctrl",
|
||||
{
|
||||
"action1": {"mean": 0.0, "components": [(0.1354, 0.0, 1.600)]},
|
||||
"action2": {"mean": 0.0, "components": [(0.1354, 0.0, 2.099)]},
|
||||
"action3": {"mean": 0.0, "components": [(0.1354, 0.0, 1.639)]},
|
||||
},
|
||||
),
|
||||
(
|
||||
"C1",
|
||||
"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)]},
|
||||
},
|
||||
),
|
||||
(
|
||||
"C2",
|
||||
"deceit",
|
||||
{
|
||||
"action1": {"mean": 0.0, "components": [(0.1354, 0.0026, 1.600)]},
|
||||
"action2": {
|
||||
"mean": -0.008730,
|
||||
"components": [(0.1354, 0.0045, 2.099), (0.2708, 0.0010, 0.612)],
|
||||
},
|
||||
"action3": {
|
||||
"mean": 0.008730,
|
||||
"components": [(0.1354, 0.0045, 1.639), (0.2708, 0.0010, -2.962)],
|
||||
},
|
||||
},
|
||||
),
|
||||
(
|
||||
"C3",
|
||||
"deceit_multi",
|
||||
{
|
||||
"action1": {
|
||||
"mean": 0.0,
|
||||
"components": [(0.1354, 0.0029, -2.619), (0.2708, 0.0008, 2.856)],
|
||||
},
|
||||
"action2": {
|
||||
"mean": -0.0140,
|
||||
"components": [
|
||||
(0.1354, 0.0050, -0.933),
|
||||
(0.2708, 0.0010, 0.801),
|
||||
(0.1806, 0.0003, 1.854),
|
||||
],
|
||||
},
|
||||
"action3": {
|
||||
"mean": 0.014,
|
||||
"components": [
|
||||
(0.1354, 0.0050, -1.398),
|
||||
(0.2708, 0.0010, 2.208),
|
||||
(0.1806, 0.0003, 1.810),
|
||||
],
|
||||
},
|
||||
},
|
||||
),
|
||||
(
|
||||
"C4",
|
||||
"deceit_f1p5",
|
||||
{
|
||||
"action1": {"mean": 0.0, "components": [(0.2031, 0.0026, 1.600)]},
|
||||
"action2": {
|
||||
"mean": -0.008730,
|
||||
"components": [(0.2031, 0.0045, 2.099), (0.4062, 0.0010, 0.612)],
|
||||
},
|
||||
"action3": {
|
||||
"mean": 0.008730,
|
||||
"components": [(0.2031, 0.0045, 1.639), (0.4062, 0.0010, -2.962)],
|
||||
},
|
||||
},
|
||||
),
|
||||
(
|
||||
"C5",
|
||||
"deceit_multi_f1p5",
|
||||
{
|
||||
"action1": {
|
||||
"mean": 0.0,
|
||||
"components": [(0.2031, 0.0029, -2.619), (0.4062, 0.0008, 2.856)],
|
||||
},
|
||||
"action2": {
|
||||
"mean": -0.0140,
|
||||
"components": [
|
||||
(0.2031, 0.0050, -0.933),
|
||||
(0.4062, 0.0010, 0.801),
|
||||
(0.2709, 0.0003, 1.854),
|
||||
],
|
||||
},
|
||||
"action3": {
|
||||
"mean": 0.014,
|
||||
"components": [
|
||||
(0.2031, 0.0050, -1.398),
|
||||
(0.4062, 0.0010, 2.208),
|
||||
(0.2709, 0.0003, 1.810),
|
||||
],
|
||||
},
|
||||
},
|
||||
),
|
||||
(
|
||||
"C6",
|
||||
"deceit_f2",
|
||||
{
|
||||
"action1": {
|
||||
"mean": 0.0,
|
||||
"components": [(0.2708, 0.0044, -2.619), (0.8124, 0.0012, 2.856)],
|
||||
},
|
||||
"action2": {
|
||||
"mean": -0.014,
|
||||
"components": [
|
||||
(0.2708, 0.0075, -0.933),
|
||||
(0.8124, 0.0015, 0.801),
|
||||
(0.5418, 0.0005, 1.854),
|
||||
],
|
||||
},
|
||||
"action3": {
|
||||
"mean": 0.014,
|
||||
"components": [
|
||||
(0.2708, 0.0075, -1.398),
|
||||
(0.8124, 0.0015, 2.208),
|
||||
(0.5418, 0.0005, 1.810),
|
||||
],
|
||||
},
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def _ensure_compat_config(config_path: Path, preferred_scheme: str = "regularized") -> str:
|
||||
with config_path.open("r", encoding="utf-8") as f:
|
||||
cfg = json.load(f)
|
||||
method = cfg.setdefault("method", {})
|
||||
inlet = method.setdefault("inlet", {})
|
||||
outlet = method.setdefault("outlet", {})
|
||||
changed = False
|
||||
if "scheme" not in inlet:
|
||||
inlet["scheme"] = preferred_scheme
|
||||
changed = True
|
||||
if "regularized_neq_damp" not in inlet:
|
||||
inlet["regularized_neq_damp"] = 0.5
|
||||
changed = True
|
||||
if "blend_alpha" not in outlet:
|
||||
outlet["blend_alpha"] = 0.7
|
||||
changed = True
|
||||
if "backflow_clamp" not in outlet:
|
||||
outlet["backflow_clamp"] = True
|
||||
changed = True
|
||||
if not changed:
|
||||
return str(config_path)
|
||||
tmp = tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix="_compat_lbm.json", delete=False, encoding="utf-8"
|
||||
)
|
||||
with tmp:
|
||||
json.dump(cfg, tmp, indent=4)
|
||||
return tmp.name
|
||||
|
||||
|
||||
def _triangle_layout(cfg) -> dict:
|
||||
dx_phys = CYLINDER_DIAMETER_M / DIAMETER_CELLS
|
||||
spacing_lb = CENTER_SPACING_M / dx_phys
|
||||
radius_lb = DIAMETER_CELLS / 2.0
|
||||
y_center = 0.5 * (cfg.ny - 1)
|
||||
x_cluster_center = cfg.nx / 3.0
|
||||
x_apex = x_cluster_center - (math.sqrt(3.0) / 3.0) * spacing_lb
|
||||
x_rear = x_apex + (math.sqrt(3.0) / 2.0) * spacing_lb
|
||||
return {
|
||||
"x_apex": x_apex,
|
||||
"x_rear": x_rear,
|
||||
"y_center": y_center,
|
||||
"y_upper": y_center + 0.5 * spacing_lb,
|
||||
"y_lower": y_center - 0.5 * spacing_lb,
|
||||
"radius_lb": radius_lb,
|
||||
}
|
||||
|
||||
|
||||
def _add_triangle_cylinders(sim: Simulation) -> dict:
|
||||
layout = _triangle_layout(sim.lbm_cfg)
|
||||
sim.add_body("circle", center=(layout["x_apex"], layout["y_center"]),
|
||||
radius=layout["radius_lb"])
|
||||
sim.add_body("circle", center=(layout["x_rear"], layout["y_lower"]),
|
||||
radius=layout["radius_lb"])
|
||||
sim.add_body("circle", center=(layout["x_rear"], layout["y_upper"]),
|
||||
radius=layout["radius_lb"])
|
||||
return layout
|
||||
|
||||
|
||||
def _generate_signal(t_phys: float, feature: dict) -> float:
|
||||
value = float(feature["mean"])
|
||||
for freq_hz, amp, phase in feature["components"]:
|
||||
value += amp * math.cos(2.0 * math.pi * freq_hz * t_phys + phase)
|
||||
return value
|
||||
|
||||
|
||||
def _ramp_factor(elapsed_s: float) -> float:
|
||||
if elapsed_s <= 0.0:
|
||||
return 0.0
|
||||
if elapsed_s >= RAMP_TIME_S:
|
||||
return 1.0
|
||||
x = elapsed_s / RAMP_TIME_S
|
||||
return 0.5 * (1.0 - math.cos(math.pi * x))
|
||||
|
||||
|
||||
def _actions_at_time(t_phys: float, features: dict) -> Tuple[float, float, float]:
|
||||
s1 = _generate_signal(t_phys, features["action1"])
|
||||
s2 = _generate_signal(t_phys, features["action2"])
|
||||
s3 = _generate_signal(t_phys, features["action3"])
|
||||
r = _ramp_factor(t_phys)
|
||||
a1 = INITIAL_ACTIONS_M_S[0] * (1.0 - r) + s1 * r
|
||||
a2 = INITIAL_ACTIONS_M_S[1] * (1.0 - r) + s2 * r
|
||||
a3 = INITIAL_ACTIONS_M_S[2] * (1.0 - r) + s3 * r
|
||||
return a1, a2, a3
|
||||
|
||||
|
||||
def _action_to_omega_lb(action_m_s: float, u_lb: float) -> float:
|
||||
u_surf_lb = action_m_s * (u_lb / INLET_U_PHYS_M_S)
|
||||
r_lb = DIAMETER_CELLS / 2.0
|
||||
return OMEGA_SIGN_FROM_ACTION * (u_surf_lb / r_lb)
|
||||
|
||||
|
||||
def _set_body_omegas(sim: Simulation, omega0: float, omega1: float, omega2: float) -> None:
|
||||
"""Set all three body rotation speeds using new API (implicit GPU upload)."""
|
||||
sim.set_body(0, omega=omega0)
|
||||
sim.set_body(1, omega=omega1)
|
||||
sim.set_body(2, omega=omega2)
|
||||
|
||||
|
||||
def _default_steps(nx: int, u_lb: float, step_multiplier: float) -> int:
|
||||
base = int(round(2.0 * float(nx) / (3.0 * float(u_lb))))
|
||||
return int(round(base * float(step_multiplier)))
|
||||
|
||||
|
||||
def run_case(
|
||||
case_id: str,
|
||||
slug: str,
|
||||
features: dict,
|
||||
*,
|
||||
out_dir: Path,
|
||||
steps: int,
|
||||
report_every: int,
|
||||
batch: int = 1,
|
||||
device_id: int = 0,
|
||||
) -> dict:
|
||||
compat = _ensure_compat_config(CONFIG_PATH)
|
||||
sim = Simulation(compat, device_id=device_id)
|
||||
layout = _add_triangle_cylinders(sim)
|
||||
sim.initialize()
|
||||
u_lb = float(sim.lbm_cfg.velocity)
|
||||
dx_phys = CYLINDER_DIAMETER_M / DIAMETER_CELLS
|
||||
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))
|
||||
|
||||
# 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)
|
||||
|
||||
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()
|
||||
for bid in range(sim.bodies.count):
|
||||
fx = sim.bodies.read_force(bid)
|
||||
print(
|
||||
f" step={batch_end} body={bid}"
|
||||
f" fx={float(fx[0]):+.6f} fy={float(fx[1]):+.6f}",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
stream.synchronize()
|
||||
macro = sim.get_macroscopic()
|
||||
vort = compute_vorticity(macro["ux"], macro["uy"])
|
||||
png = out_dir / f"vorticity_{case_id}_{slug}.png"
|
||||
ckpt = out_dir / f"state_{case_id}_{slug}.h5"
|
||||
sim.save_checkpoint(str(ckpt))
|
||||
|
||||
render_info = render_vorticity_field(
|
||||
vort,
|
||||
nx=int(sim.lbm_cfg.nx),
|
||||
ny=int(sim.lbm_cfg.ny),
|
||||
out_path=str(png),
|
||||
cylinders=cylinders,
|
||||
vmin=VORT_VMIN,
|
||||
vmax=VORT_VMAX,
|
||||
minimal_axes=True,
|
||||
)
|
||||
sim.close()
|
||||
|
||||
summary = {
|
||||
"case_id": case_id,
|
||||
"slug": slug,
|
||||
"steps": int(steps),
|
||||
"batch": int(batch),
|
||||
"u_lb": u_lb,
|
||||
"dt_phys": dt_phys,
|
||||
"vort_png": str(png),
|
||||
"checkpoint": str(ckpt),
|
||||
"vort_range_data": [float(vort.min()), float(vort.max())],
|
||||
"vort_plot_range": [VORT_VMIN, VORT_VMAX],
|
||||
"swap_action23_bodies": bool(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}")
|
||||
return summary
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description="exp_ctrl_matrix vorticity batch")
|
||||
ap.add_argument("--out-dir", type=str, default=str(DEFAULT_OUT))
|
||||
ap.add_argument(
|
||||
"--step-multiplier",
|
||||
type=float,
|
||||
default=2.0,
|
||||
help=(
|
||||
"Steps = multiplier * round(2*nx/(3*u_lb)); "
|
||||
"use 2.0 after halving nx/ny."
|
||||
),
|
||||
)
|
||||
ap.add_argument(
|
||||
"--steps",
|
||||
type=int,
|
||||
default=FIXED_STEPS,
|
||||
help=f"Total LBM steps (default {FIXED_STEPS}).",
|
||||
)
|
||||
ap.add_argument("--report-every", type=int, default=20000)
|
||||
ap.add_argument("--cases", type=str, default="",
|
||||
help="Comma list e.g. C0,C1 or empty=all.")
|
||||
ap.add_argument(
|
||||
"--batch", type=int, default=1,
|
||||
help=(
|
||||
"Batch N steps between action uploads. Default 1 (each step). "
|
||||
"With --batch 10, actions are computed and uploaded every 10 steps. "
|
||||
"Saves kernel launch overhead at the cost of control-signal interpolation."
|
||||
),
|
||||
)
|
||||
ap.add_argument("--device-id", type=int, default=0, help="GPU device id.")
|
||||
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
|
||||
summaries = []
|
||||
|
||||
with CONFIG_PATH.open("r", encoding="utf-8") as f:
|
||||
grid_cfg = json.load(f)["grid"]
|
||||
nx = int(grid_cfg["nx"])
|
||||
ny = int(grid_cfg["ny"])
|
||||
u_lb = 0.04
|
||||
base_steps = int(round(2.0 * nx / (3.0 * u_lb)))
|
||||
steps = int(args.steps) if int(args.steps) > 0 \
|
||||
else _default_steps(nx, u_lb, args.step_multiplier)
|
||||
print(
|
||||
f"Output: {out_dir} | grid={nx}x{ny} | base_steps={base_steps} "
|
||||
f"x{args.step_multiplier} -> {steps} | batch={args.batch} | "
|
||||
f"device={args.device_id} | vort [{VORT_VMIN}, {VORT_VMAX}]"
|
||||
)
|
||||
|
||||
for case_id, slug, features in CONTROL_CASES:
|
||||
if selected and case_id not in selected:
|
||||
continue
|
||||
summaries.append(
|
||||
run_case(
|
||||
case_id,
|
||||
slug,
|
||||
features,
|
||||
out_dir=out_dir,
|
||||
steps=steps,
|
||||
report_every=int(args.report_every),
|
||||
batch=int(args.batch),
|
||||
device_id=int(args.device_id),
|
||||
)
|
||||
)
|
||||
|
||||
manifest = {
|
||||
"grid": {"nx": nx, "ny": ny},
|
||||
"base_steps": base_steps,
|
||||
"step_multiplier": float(args.step_multiplier),
|
||||
"steps": steps,
|
||||
"batch": int(args.batch),
|
||||
"device_id": int(args.device_id),
|
||||
"vort_vmin": VORT_VMIN,
|
||||
"vort_vmax": VORT_VMAX,
|
||||
"swap_action23_bodies": bool(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())
|
||||
@@ -0,0 +1,300 @@
|
||||
# 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())
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 316 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 134 KiB |
Reference in New Issue
Block a user