ZibinDong's picture
Publish audited ActionCodec2 release files
1bfc3f0 verified
Raw History Blame
8.6 kB
"""Dense primitive cells to zero-free sparse frames, and sparse training corpora."""
from __future__ import annotations
from collections.abc import Sequence
from dataclasses import dataclass
import numpy as np
from ..configuration import ActionCodec2Config, ConfigError, _integer
from ..primitives import PrimitiveStore
@dataclass(frozen=True)
class SparseLayout:
"""Maps dense physical cells to zero-free sparse atoms."""
bins: tuple[int, ...]
zero: tuple[int | None, ...]
def __post_init__(self) -> None:
if not self.bins or len(self.bins) != len(self.zero):
raise ValueError("bins and zero must have one entry per slot")
if len(self.bins) > 15:
raise ValueError("sparse frames support at most 15 dense slots")
for count, zero in zip(self.bins, self.zero, strict=True):
_integer(count, "bin count", positive=True)
if zero is not None:
_integer(zero, "zero index")
if count < 1 or (zero is not None and not 0 <= zero < count):
raise ValueError("every slot needs a bin and an in-range zero index")
if zero is not None and count < 2:
raise ValueError("a motion slot needs at least one nonzero primitive")
@classmethod
def from_config(cls, config: ActionCodec2Config, primitives: PrimitiveStore) -> SparseLayout:
"""Motion slots (order 1 or 2) keep their exact zero primitive; order 0 is change-coded."""
zero: list[int | None] = [None] * len(primitives.bins)
for component in config.physical.components.values():
if component.primitive_order == 0:
continue
for dimension in component.dims:
axis = np.asarray(primitives.values[dimension])
matches = np.flatnonzero(axis == 0)
if not len(matches):
raise ConfigError(f"dimension {dimension} has no exact zero primitive")
zero[dimension] = int(matches[0])
return cls(tuple(int(b) for b in primitives.bins), tuple(zero))
@property
def dims(self) -> int:
return len(self.bins)
@property
def e_dim(self) -> int:
"""Sparse slot index of the end-of-frame atom."""
return len(self.bins)
@property
def sparse_bins(self) -> tuple[int, ...]:
"""Motion slots drop their zero primitive; ``E`` has exactly one value."""
return tuple(b - (z is not None) for b, z in zip(self.bins, self.zero, strict=True)) + (1,)
@property
def native_zero(self) -> list[int]:
return [-1 if z is None else int(z) for z in self.zero]
def sparsify(self, cells: np.ndarray) -> np.ndarray:
"""Dense chunk ``(T, D)`` to sparse frames ``(T, D + 1)``."""
cells = np.asarray(cells, dtype=np.int64)
if cells.ndim != 2 or cells.shape[1] != self.dims or not len(cells):
raise ValueError(f"cells must have shape (T, {self.dims}) with T >= 1")
if np.any(cells >= np.asarray(self.bins)):
raise ValueError("cell outside the primitive range")
out = np.full((len(cells), self.dims + 1), -1, dtype=np.int64)
for d, z in enumerate(self.zero):
column = cells[:, d]
if np.any((column >= 0) != (column[0] >= 0)):
raise ValueError("a slot must be active for the whole chunk or not at all")
if column[0] < 0:
continue
if z is None:
changed = np.concatenate(([True], column[1:] != column[:-1]))
out[changed, d] = column[changed]
else:
moving = column != z
out[moving, d] = column[moving] - (column[moving] > z)
content = np.flatnonzero(np.any(out[:, : self.dims] >= 0, axis=1))
if len(content):
out[: content[-1], self.dims] = 0
return out
def densify(self, sparse: np.ndarray, active: Sequence[bool]) -> np.ndarray:
"""Inverse of :meth:`sparsify` given which dense slots are active."""
sparse = np.asarray(sparse, dtype=np.int64)
out = np.full((len(sparse), self.dims), -1, dtype=np.int64)
for d, z in enumerate(self.zero):
if not active[d]:
continue
column = sparse[:, d]
if z is None:
if column[0] < 0:
raise ValueError("an absolute slot must be stated on frame 0")
index = np.maximum.accumulate(np.where(column >= 0, np.arange(len(column)), 0))
out[:, d] = column[index]
else:
out[:, d] = np.where(column < 0, z, column + (column >= z))
return out
def sparsify_chunks(
layout: SparseLayout, chunks: Sequence[np.ndarray]
) -> tuple[np.ndarray, np.ndarray]:
"""Vectorized :meth:`SparseLayout.sparsify` over many dense chunks ``[(T_i, D)]``."""
lengths = np.asarray([len(c) for c in chunks], dtype=np.int64)
if not len(lengths) or np.any(lengths < 1):
raise ValueError("chunks must be non-empty")
starts = np.concatenate(([0], np.cumsum(lengths)))
arrays = [np.asarray(c) for c in chunks]
for values in arrays:
if values.ndim != 2 or values.shape[1] != layout.dims or values.dtype.kind not in "iu":
raise ValueError("training cells must be integer matrices matching the sparse layout")
if np.any(values < -1) or np.any(values >= np.asarray(layout.bins)):
raise ValueError("cell outside the primitive range")
if np.any((values >= 0) != (values[0] >= 0)):
raise ValueError("a slot must be active for the whole chunk or not at all")
dtype = np.int16 if max(layout.bins) <= np.iinfo(np.int16).max else np.int32
cells = np.concatenate(arrays, dtype=dtype)
if getattr(layout, "dense", False):
out = np.full((len(cells), layout.dims + 1), -1, dtype=dtype)
out[:, : layout.dims] = cells
out[:, layout.dims] = 0
out[starts[1:] - 1, layout.dims] = -1
return out, starts
chunk = np.repeat(np.arange(len(lengths)), lengths)
first = np.zeros(len(cells), dtype=bool)
first[starts[:-1]] = True
out = np.full((len(cells), layout.dims + 1), -1, dtype=dtype)
for d, z in enumerate(layout.zero):
column = cells[:, d]
active = column >= 0
if z is None:
changed = active & (first | (column != np.roll(column, 1)))
out[changed, d] = column[changed]
else:
moving = active & (column != z)
out[moving, d] = column[moving] - (column[moving] > z)
local = np.arange(len(cells)) - starts[chunk]
content = np.any(out[:, : layout.dims] >= 0, axis=1)
last = np.maximum.reduceat(np.where(content, local, -1), starts[:-1])
out[local < last[chunk], layout.dims] = 0
return out, starts
def static_frames(cells: np.ndarray, layout: SparseLayout) -> np.ndarray:
"""Frames whose motion cells are all zero and whose absolute slots repeat the last frame."""
moving = np.zeros(len(cells), dtype=bool)
for d, zero in enumerate(layout.zero):
column = cells[:, d]
if column[0] < 0:
continue
if zero is None:
moving[1:] |= column[1:] != column[:-1]
else:
moving |= column != zero
return ~moving
def compress_static(cells: np.ndarray, layout: SparseLayout, run_cap: int) -> np.ndarray:
"""Drop leading/trailing static frames and cap interior static runs at ``run_cap``."""
static = static_frames(cells, layout)
if static.all():
return cells[:1]
keep = np.zeros(len(cells), dtype=bool)
first, last = int(np.argmax(~static)), len(cells) - int(np.argmax(~static[::-1]))
keep[first:last] = True
index = np.arange(len(cells))
run_start = np.maximum.accumulate(np.where(~static, index + 1, 0))
keep &= ~static | (index - run_start < run_cap)
return cells[keep]
def training_chunks(
trajectories: Sequence[np.ndarray],
layout: SparseLayout,
*,
horizon: int,
static_run_cap: int,
dtype: np.dtype | type = np.int64,
) -> list[np.ndarray]:
"""Static-compressed cell trajectories cut into aligned ``horizon`` chunks."""
out = []
for raw in trajectories:
cells = compress_static(np.asarray(raw), layout, static_run_cap)
for start in range(0, len(cells), horizon):
piece = cells[start : start + horizon]
if len(piece) >= 2 or start == 0:
out.append(np.asarray(piece, dtype=dtype))
return out