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:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user