feat(ccd): add dual-clock field sampling

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Frank14f
2026-07-22 14:07:05 +08:00
co-authored by Cursor
parent c9ce3e6dcb
commit 8d760145de
8 changed files with 489 additions and 27 deletions
+42 -5
View File
@@ -17,6 +17,7 @@ import os
import sys
import time
from collections import deque
from pathlib import Path
import numpy as np
@@ -36,6 +37,7 @@ from CCD_analysis.utils.cfd_interface import (
calc_lag, calc_dtw_sim,
)
from CCD_analysis.utils.resampling import analyze_harmonics, gen_target_states_at
from CCD_analysis.utils.dual_clock import DualClockCollector, field_steps_from_interval
DATA_TYPE = np.float32
L0 = 20.0
@@ -44,9 +46,17 @@ FIFO_LEN = 150
CONV_LEN = 36
def run_single(scene_name: str, device_id: int, n_steps: int) -> dict:
def run_single(
scene_name: str,
device_id: int,
n_steps: int,
*,
field_interval: int | None = None,
output_dir: str | None = None,
) -> dict:
cfg = get_scene(scene_name)
out_dir = data_dir_for_scene(scene_name)
out_dir = output_dir or data_dir_for_scene(scene_name)
Path(out_dir).mkdir(parents=True, exist_ok=True)
u0 = cfg["u0"]
si = cfg["sample_interval"]
ac_scale = cfg["action_scale"]
@@ -152,6 +162,15 @@ def run_single(scene_name: str, device_id: int, n_steps: int) -> dict:
obs = np.zeros(s_dim, dtype=np.float32)
sens_c, forc_c, act_c, rew_c, sim_c = [], [], [], [], []
collector = None
if field_interval is not None:
collector = DualClockCollector(
ff,
control_interval=si,
control_count=n_steps,
u0=u0,
field_steps=field_steps_from_interval(si * n_steps, field_interval),
)
for step in range(n_steps):
action, _ = model.predict(obs, deterministic=True)
@@ -163,7 +182,10 @@ def run_single(scene_name: str, device_id: int, n_steps: int) -> dict:
temp_a[3:6] = omega
ff.context.push()
ff.run(si, temp_a)
if collector is None:
ff.run(si, temp_a)
else:
collector.run_interval(step, temp_a)
ff.context.pop()
obs_slice = ff.obs.copy()[0:12]
@@ -219,6 +241,8 @@ def run_single(scene_name: str, device_id: int, n_steps: int) -> dict:
np.savez(os.path.join(out_dir, "controlled.npz"),
sensors=sens_arr, forces=forc_arr, actions=act_arr,
rewards=np.array(rew_c, dtype=np.float32))
if collector is not None:
collector.save(Path(out_dir) / "fields.npz")
save_vorticity_png(os.path.join(out_dir, "vorticity_controlled.png"),
vorticity_from_ddf(ff, u0=u0),
@@ -229,7 +253,13 @@ def run_single(scene_name: str, device_id: int, n_steps: int) -> dict:
avg_sim = float(np.mean(sim_c[-tail:])) if sim_c else 0.0
print(f" reward={avg_reward:.4f} similarity={avg_sim:.4f}")
result = {"scene": scene_name, "similarity": avg_sim, "avg_reward": avg_reward}
result = {
"scene": scene_name,
"similarity": avg_sim,
"avg_reward": avg_reward,
"control_interval": si,
"field_interval": field_interval,
}
with open(os.path.join(out_dir, "result.json"), "w") as f:
json.dump(result, f, indent=2)
@@ -245,6 +275,10 @@ def main():
help="Diameter shortcut (0.75, 1.0, 1.5)")
ap.add_argument("--device", type=int, default=2)
ap.add_argument("--steps", type=int, default=200)
ap.add_argument("--field-interval", type=int, default=None,
help="Save fields every N absolute lattice steps")
ap.add_argument("--output-dir", type=str, default=None,
help="Optional isolated output directory")
args = ap.parse_args()
if args.diameter is not None:
@@ -257,7 +291,10 @@ def main():
return 1
t0 = time.time()
r = run_single(scene_name, args.device, args.steps)
r = run_single(
scene_name, args.device, args.steps,
field_interval=args.field_interval, output_dir=args.output_dir,
)
print(f"Done in {time.time()-t0:.1f}s: sim={r['similarity']:.4f}")