feat(ccd): freeze dynamic-increment analysis pipeline

Replace the legacy CCD workspace with acquisition, direct-dq, original and lagged CCD contracts so the DRL-versus-constant-mean mechanism is reproducible and fail-closed.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Frank14f
2026-08-07 16:41:00 +08:00
co-authored by Cursor
parent 7323a235f4
commit b144d62920
138 changed files with 8443 additions and 11564 deletions
+157
View File
@@ -9,6 +9,7 @@ from typing import List, Tuple, Union, Optional
from . import utils
from . import preprocess as preproc
from . import compiler
from src.CCD_analysis.acquisition.solver_state import copy_ping_pong_ddf, d2q9_q_over_u0_xy
FLUID = 0b00000001
SOLID = 0b00000010
@@ -112,6 +113,10 @@ class FlowField:
self.action = np.zeros(0, dtype=self.DATA_TYPE)
self.obs = np.zeros(0, dtype=self.DATA_TYPE)
self._control_interval = None
self._last_completed_observation = None
self._last_effective_action = None
self._completed_lattice_steps = 0
self._completed_control_intervals = 0
initflow(
self.flag_gpu,
@@ -126,6 +131,23 @@ class FlowField:
cuda.memcpy_dtoh(self.flag, self.flag_gpu)
cuda.memcpy_dtoh(self.ddf, self.ddf_gpu)
def completed_flags_xy(self) -> np.ndarray:
"""Return a read-only copy of configured flags in canonical ``(NX, NY)`` order.
The solver stores flags flat with ``k = x + y * NX``. No bit is
interpreted or rewritten here, so FLUID/SOLID and auxiliary bits are
preserved exactly.
"""
flat = np.asarray(self.flag)
expected = int(self.FIELD_SHAPE[0]) * int(self.FIELD_SHAPE[1]) * int(self.FIELD_SHAPE[2])
if flat.dtype != np.dtype("uint8") or flat.ndim != 1 or flat.size != expected:
raise RuntimeError("configured solver flag storage is not canonical uint8 flat data")
if int(self.FIELD_SHAPE[2]) != 1:
raise RuntimeError("canonical CCD flag export supports completed D2 geometry only")
result = np.ascontiguousarray(flat.reshape((self.FIELD_SHAPE[1], self.FIELD_SHAPE[0])).T)
result.setflags(write=False)
return result
def add_cylinder(self, center: Tuple[float, float, float], radius: float, id_obj: Optional[int] = None):
x_c, y_c, z_c = center
@@ -224,6 +246,7 @@ class FlowField:
self.objects[id_object] = {
"type": "sensor",
"center": center,
"radius": radius,
}
self.action = np.zeros(len(self.objects), dtype=self.DATA_TYPE)
@@ -409,6 +432,133 @@ class FlowField:
cuda.memset_d32_async(self.obs_gpu, 0, self.obs.size, stream)
stream.synchronize()
state["completed_steps"] += num_steps
self._completed_lattice_steps += num_steps
def current_step_observation(self):
"""Return raw telemetry for the latest completed lattice step.
During an active split interval this is the latest synchronized step. At
a completed control boundary it is the persisted final raw step, not the
interval-averaged public ``obs``.
"""
state = self._control_interval
if state is not None and state["completed_steps"] >= 1:
return state["obs_steps"][state["completed_steps"] - 1].copy()
if self._last_completed_observation is None:
raise RuntimeError("no completed lattice step is available")
return self._last_completed_observation.copy()
def current_effective_action(self):
"""Return the latest EMA action, including at a completed boundary."""
state = self._control_interval
if state is not None and state["completed_steps"] >= 1:
return np.asarray(state["action"]).copy()
if self._last_effective_action is None:
raise RuntimeError("no completed lattice step is available")
return self._last_effective_action.copy()
def _require_completed_split_step(self):
state = self._control_interval
if state is None or state["completed_steps"] < 1:
raise RuntimeError("no completed step is available in the active control interval")
def current_step_velocity_field(self):
"""Return completed Legacy nondimensional velocity ``q/U0`` as ``(NX, NY)``."""
if self._control_interval is not None:
self._require_completed_split_step()
elif self._last_completed_observation is None:
raise RuntimeError("no completed Legacy step is available")
# run_control_segment synchronizes before returning. After its pointer swap,
# ddf_gpu is the completed state and temp_gpu is the previous/work buffer.
cuda.memcpy_dtoh(self.ddf, self.ddf_gpu)
flags = self.completed_flags_xy()
return d2q9_q_over_u0_xy(
self.ddf, int(self.FIELD_SHAPE[0]), int(self.FIELD_SHAPE[1]), flags,
float(self.field_config.velocity),
)
def current_step_velocity_probe(self, lattice_index: Tuple[int, int]):
"""Read one synchronized Legacy nondimensional ``(ux/U0, uy/U0)`` pair."""
self._require_completed_split_step()
if (not isinstance(lattice_index, tuple) or len(lattice_index) != 2
or any(type(value) is not int for value in lattice_index)):
raise ValueError("lattice_index must be an (x, y) integer tuple")
x, y = lattice_index
if not (0 <= x < self.FIELD_SHAPE[0] and 0 <= y < self.FIELD_SHAPE[1]):
raise ValueError("velocity probe is outside the lattice")
ux, uy = self.current_step_velocity_field()
return np.asarray([ux[x, y], uy[x, y]], dtype=self.DATA_TYPE)
def active_step_clock_state(self):
"""Return solver lineage during a split interval after a completed step.
The control clock is the number of fully completed control intervals; it
therefore identifies the active interval's zero-based absolute index.
"""
state = self._control_interval
if state is None or state["completed_steps"] < 1:
raise RuntimeError("active-step clocks require a split interval with a completed step")
return {
"solver_absolute_lattice_clock": int(self._completed_lattice_steps),
"solver_absolute_control_clock": int(self._completed_control_intervals),
}
def solver_clock_state(self):
"""Return public absolute solver lineage clocks at the current boundary."""
if self._control_interval is not None:
raise RuntimeError("solver clocks are boundary-safe only")
return {
"solver_absolute_lattice_clock": int(self._completed_lattice_steps),
"solver_absolute_control_clock": int(self._completed_control_intervals),
}
def full_state_checkpoint(self):
"""Capture exact restart state only at a completed control boundary."""
if self._control_interval is not None:
raise RuntimeError("full checkpoint requires a completed control boundary")
ddf = self.current_step_ddf_checkpoint()
return {
**ddf,
"action": self.action.copy(),
"last_effective_action": None if self._last_effective_action is None else self._last_effective_action.copy(),
"raw_observation": None if self._last_completed_observation is None else self._last_completed_observation.copy(),
"boundary_observation": self.obs.copy(),
"solver_absolute_lattice_clock": int(self._completed_lattice_steps),
"solver_absolute_control_clock": int(self._completed_control_intervals),
}
def restore_full_state(self, checkpoint):
"""Restore both ping-pong DDFs and solver-side boundary lifecycle state."""
if self._control_interval is not None:
raise RuntimeError("cannot restore during an active control interval")
current = np.asarray(checkpoint["current_ddf"]); temp = np.asarray(checkpoint["temp_ddf"])
action = np.asarray(checkpoint["action"]); boundary = np.asarray(checkpoint["boundary_observation"])
raw = checkpoint["raw_observation"]; effective = checkpoint["last_effective_action"]
if current.dtype != self.DATA_TYPE or temp.dtype != self.DATA_TYPE or current.shape != self.ddf.shape or temp.shape != self.ddf.shape:
raise ValueError("checkpoint DDF storage mismatch")
if action.dtype != self.DATA_TYPE or action.shape != self.action.shape or boundary.dtype != self.DATA_TYPE or boundary.shape != self.obs.shape:
raise ValueError("checkpoint action/observation mismatch")
if raw is not None and (np.asarray(raw).dtype != self.DATA_TYPE or np.asarray(raw).shape != self.obs.shape): raise ValueError("checkpoint raw observation mismatch")
if effective is not None and (np.asarray(effective).dtype != self.DATA_TYPE or np.asarray(effective).shape != self.action.shape): raise ValueError("checkpoint effective action mismatch")
cuda.memcpy_htod(self.ddf_gpu, current); cuda.memcpy_htod(self.temp_gpu, temp)
self.ddf = current.copy(); self.action = action.copy(); self.obs = boundary.copy()
self._last_completed_observation = None if raw is None else np.asarray(raw).copy()
self._last_effective_action = None if effective is None else np.asarray(effective).copy()
self._completed_lattice_steps = int(checkpoint["solver_absolute_lattice_clock"])
self._completed_control_intervals = int(checkpoint["solver_absolute_control_clock"])
cuda.memcpy_htod(self.action_gpu, self.action)
def current_step_ddf_checkpoint(self):
"""Return copies/hashes of current(completed) and temp(previous/work) buffers.
Synchronous device-to-host copies make this safe both at a completed split
step and at a completed control boundary; no solver state is modified.
"""
return copy_ping_pong_ddf(
lambda host: cuda.memcpy_dtoh(host, self.ddf_gpu),
lambda host: cuda.memcpy_dtoh(host, self.temp_gpu),
int(self.FIELD_SIZE * self.LATTICE),
)
def end_control_interval(self):
"""Publish obs once, only at the original policy-control boundary."""
@@ -423,6 +573,13 @@ class FlowField:
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])
# Persist the final lattice-step state before clearing the split lifecycle.
# The next begin_control_interval therefore starts its EMA from this exact
# action, preserving the historical uninterrupted-run semantics.
self.action = np.asarray(state["action"], dtype=self.DATA_TYPE).copy()
self._last_effective_action = self.action.copy()
self._last_completed_observation = state["obs_steps"][-1].copy()
self._completed_control_intervals += 1
self._control_interval = None
return self.obs