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
+60 -17
View File
@@ -111,6 +111,7 @@ class FlowField:
self.objects = {}
self.action = np.zeros(0, dtype=self.DATA_TYPE)
self.obs = np.zeros(0, dtype=self.DATA_TYPE)
self._control_interval = None
initflow(
self.flag_gpu,
@@ -337,26 +338,55 @@ class FlowField:
# if type == "taylor":
# self.vortex_config[6] =
def run(self, num_steps: int, action_target: np.ndarray):
if (
action_target.size != len(self.objects)
or action_target.dtype != self.DATA_TYPE
):
def _validate_run(self, num_steps: int, action_target: np.ndarray):
if type(num_steps) is not int or num_steps < 1:
raise ValueError("num_steps must be a positive integer")
if action_target.size != len(self.objects) or action_target.dtype != self.DATA_TYPE:
raise ValueError("action data type or size does not match the objects.")
elif len(self.objects) == 0:
if len(self.objects) == 0:
raise ValueError("No objects have been added to the flow field.")
weight = 0.1
stream = cuda.Stream()
action_pinned = cuda.pagelocked_empty_like(self.action)
action_pinned[:] = self.action
obs_pinned = cuda.pagelocked_empty_like(self.obs)
def run(self, num_steps: int, action_target: np.ndarray):
"""Advance one complete legacy interval (original public behavior)."""
if self._control_interval is not None:
raise RuntimeError("run is unavailable while a control interval is active")
self.begin_control_interval(num_steps, action_target)
self.run_control_segment(num_steps)
self.end_control_interval()
def begin_control_interval(self, total_steps: int, action_target: np.ndarray):
"""Start one policy interval that may be split only to read/save fields."""
if self._control_interval is not None:
raise RuntimeError("a control interval is already active")
self._validate_run(total_steps, action_target)
self.error_flag[0] = 0
cuda.memcpy_htod(self.error_flag_gpu, self.error_flag)
self.obs[:] = 0
for i in range(num_steps):
action_pinned = (1 - weight) * action_pinned + weight * action_target
cuda.memcpy_htod_async(self.action_gpu, action_pinned, stream)
action = cuda.pagelocked_empty_like(self.action)
action[:] = self.action
self._control_interval = {
"total_steps": total_steps,
"completed_steps": 0,
"target": action_target.copy(),
"action": action,
"obs_steps": cuda.pagelocked_empty((total_steps, self.obs.size), dtype=self.DATA_TYPE),
"stream": cuda.Stream(),
}
def run_control_segment(self, num_steps: int):
"""Advance part of the active interval without resetting smoothing or obs."""
state = self._control_interval
if state is None:
raise RuntimeError("no control interval is active")
if type(num_steps) is not int or num_steps < 1:
raise ValueError("num_steps must be a positive integer")
if state["completed_steps"] + num_steps > state["total_steps"]:
raise ValueError("segment exceeds the active control interval")
stream = state["stream"]
start = state["completed_steps"]
for local_step in range(num_steps):
state["action"] = 0.9 * state["action"] + 0.1 * state["target"]
cuda.memcpy_htod_async(self.action_gpu, state["action"], stream)
self.step(
self.flag_gpu,
self.ddf_gpu,
@@ -375,13 +405,26 @@ class FlowField:
stream=stream,
)
self.ddf_gpu, self.temp_gpu = self.temp_gpu, self.ddf_gpu
cuda.memcpy_dtoh_async(obs_pinned, self.obs_gpu, stream)
cuda.memcpy_dtoh_async(state["obs_steps"][start + local_step], self.obs_gpu, stream)
cuda.memset_d32_async(self.obs_gpu, 0, self.obs.size, stream)
self.obs += obs_pinned
stream.synchronize()
self.obs = (self.obs / num_steps).astype(self.DATA_TYPE)
state["completed_steps"] += num_steps
def end_control_interval(self):
"""Publish obs once, only at the original policy-control boundary."""
state = self._control_interval
if state is None:
raise RuntimeError("no control interval is active")
if state["completed_steps"] != state["total_steps"]:
raise RuntimeError("cannot end a control interval before its boundary")
self.obs[:] = 0
for step_obs in state["obs_steps"]:
self.obs += step_obs
self.obs = (self.obs / state["total_steps"]).astype(self.DATA_TYPE)
cuda.memcpy_dtoh(self.error_flag, self.error_flag_gpu)
self.last_error_flag = int(self.error_flag[0])
self._control_interval = None
return self.obs
def has_numeric_error(self) -> bool:
return bool(self.last_error_flag != 0)