FACT / fact /data_utils.py
zhicao's picture
Add FACT training code
af8c481 verified
Raw History Blame Contribute Delete
5.48 kB
"""Timestamp alignment and delta-base EEF62 pose math shared by the FACT data path.
These helpers define the 20 Hz logical control grid (nearest-timestamp
gathering from the native-rate LeRobot parquet) and the T-Rex delta-base
action parameterisation: arm poses are expressed in the frame of the block
reference state, hand joint targets stay absolute.
"""
from __future__ import annotations
from collections.abc import Iterable
from dataclasses import dataclass
import numpy as np
def matrix_to_rotation_6d(matrix: np.ndarray) -> np.ndarray:
"""First two columns of a rotation matrix, concatenated (inverse of ``rotation_6d_to_matrix``)."""
return np.concatenate((matrix[..., :, 0], matrix[..., :, 1]), axis=-1)
@dataclass(frozen=True)
class TimestampSelection:
indices: np.ndarray
target_timestamps: np.ndarray
padding_mask: np.ndarray
alignment_error: np.ndarray
def nearest_timestamp_indices(
source_timestamps: np.ndarray,
target_timestamps: np.ndarray,
*,
tie_tolerance: float = 1e-9,
) -> TimestampSelection:
"""Map target times to source rows, choosing the earlier row on a tie."""
source = np.asarray(source_timestamps, dtype=np.float64)
target = np.asarray(target_timestamps, dtype=np.float64)
if source.ndim != 1 or source.size < 2:
raise ValueError("source timestamps must be one-dimensional with at least two rows")
if target.ndim != 1 or target.size == 0:
raise ValueError("target timestamps must be a non-empty one-dimensional array")
if not np.isfinite(source).all() or not np.isfinite(target).all():
raise ValueError("timestamps must be finite")
if not np.all(np.diff(source) > 0):
raise ValueError("source timestamps must be strictly increasing")
if not np.all(np.diff(target) > 0):
raise ValueError("target timestamps must be strictly increasing")
insertion = np.searchsorted(source, target, side="left")
lower = np.clip(insertion - 1, 0, source.size - 1)
upper = np.clip(insertion, 0, source.size - 1)
lower_error = np.abs(target - source[lower])
upper_error = np.abs(source[upper] - target)
choose_upper = upper_error < lower_error - tie_tolerance
indices = np.where(choose_upper, upper, lower).astype(np.int64)
padding = (target < source[0]) | (target > source[-1])
indices[target < source[0]] = 0
indices[target > source[-1]] = source.size - 1
return TimestampSelection(
indices=indices,
target_timestamps=target,
padding_mask=padding,
alignment_error=np.abs(source[indices] - target),
)
def uniform_target_times(
anchor_timestamp: float,
offsets: Iterable[int],
rate_hz: float,
) -> np.ndarray:
offsets_array = np.asarray(tuple(offsets), dtype=np.float64)
if offsets_array.ndim != 1 or offsets_array.size == 0:
raise ValueError("offsets must be a non-empty sequence")
if not np.all(np.diff(offsets_array) > 0):
raise ValueError("offsets must be strictly increasing")
if not np.isfinite(rate_hz) or rate_hz <= 0:
raise ValueError("rate_hz must be positive")
return float(anchor_timestamp) + offsets_array / float(rate_hz)
def rotation_6d_to_matrix(rotation_6d: np.ndarray) -> np.ndarray:
"""Convert matrix-column 6D rotations to proper rotation matrices."""
values = np.asarray(rotation_6d, dtype=np.float64)
if values.shape[-1] != 6:
raise ValueError("rotation_6d must end in six values")
first = values[..., :3]
first /= np.linalg.norm(first, axis=-1, keepdims=True).clip(min=1e-8)
second = values[..., 3:6]
second = second - np.sum(first * second, axis=-1, keepdims=True) * first
second /= np.linalg.norm(second, axis=-1, keepdims=True).clip(min=1e-8)
third = np.cross(first, second)
return np.stack((first, second, third), axis=-1)
def eef62_delta_base(reference_state: np.ndarray, absolute_targets: np.ndarray) -> np.ndarray:
"""Apply T-Rex delta-base pose math; hand targets remain absolute."""
reference = np.asarray(reference_state, dtype=np.float64)
targets = np.asarray(absolute_targets, dtype=np.float64)
if reference.shape != (62,) or targets.shape[-1] != 62:
raise ValueError("expected reference [62] and targets [...,62]")
output = np.empty_like(targets, dtype=np.float64)
for pose_slice, hand_slice in ((slice(0, 9), slice(9, 31)), (slice(31, 40), slice(40, 62))):
reference_pose = reference[pose_slice]
target_pose = targets[..., pose_slice]
reference_rotation = rotation_6d_to_matrix(reference_pose[3:9])
target_rotation = rotation_6d_to_matrix(target_pose[..., 3:9])
translation_delta = np.einsum(
"ji,...j->...i",
reference_rotation,
target_pose[..., :3] - reference_pose[:3],
)
rotation_delta = np.einsum(
"ji,...jk->...ik", reference_rotation, target_rotation
)
output[..., pose_slice] = np.concatenate(
(
translation_delta,
rotation_delta[..., :, 0],
rotation_delta[..., :, 1],
),
axis=-1,
)
output[..., hand_slice] = targets[..., hand_slice]
return output.astype(np.float32)
__all__ = [
"TimestampSelection",
"eef62_delta_base",
"matrix_to_rotation_6d",
"nearest_timestamp_indices",
"rotation_6d_to_matrix",
"uniform_target_times",
]