重构body api,性能分析,项目整理

This commit is contained in:
Frank14f
2026-05-31 01:42:58 +08:00
parent 4758eb3215
commit 2e052480c2
64 changed files with 5196 additions and 1693 deletions
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())
+300
View File
@@ -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