Spaces:
Running on Zero
Running on Zero
File size: 11,652 Bytes
34f3bc9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 | """Dataset Blueprint β declarative single-source-of-truth for authoring schemas.
Users describe a dataset with high-level constructs (arm DoF, parquet columns,
camera roles, annotations). `.build()` compiles these into a `DatasetSchema` β
the same internal dataclass that every downstream module (stats, merge,
preflight, adapter, transform chain, model, deploy) already consumes.
Two top-level blueprints cover every dataset we have:
SingleArmBlueprint β arm + gripper layout (oxe_auge, labutopia_level3,
future droid-family datasets).
MultiChannelBlueprint β arbitrary list of (state_col, action_col, dim,
mode) channels (robocoin's action + dedicated
gripper_open_scale_action).
Anything a DatasetSchema needs but the blueprint can't infer (e.g. canonical
dual-arm layout on a multi-channel robocoin) is passed explicitly as an
argument. The blueprint compiler is a pure function β running it twice
yields bit-identical DatasetSchema.to_dict() β which makes stats.json /
labvla_manifest.json / checkpoint compatibility self-verifying.
Normalization mode
------------------
The project-wide default is ``"mean_std"`` β z-score, set at the transform
chain level in ``policies/LabVLA/configuration_labvla.py``. ``hydrate_all``
in ``transforms/core.py`` then auto-injects per-dim ``"q01_q99"`` overrides
on the canonical gripper indices declared by ``schema.gripper_action_dims``,
so the effective policy is "arm dims β mean_std, gripper dims β q01_q99".
Schemas don't carry a per-dim normalization field; overrides flow through
``schema.gripper_action_dims`` (and ``arm_layout.gripper_indices_canonical``,
which mirrors it).
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Mapping, Optional
from .annotation_loss import AnnotationLossSpec
from .arm_layout import ArmLayoutSpec, ArmCount
from .camera_mapping import expand_camera_mapping
from .dataset_schema import DatasetSchema
# --------------------------------------------------------------------------- #
# Authoring primitives #
# --------------------------------------------------------------------------- #
@dataclass(frozen=True)
class AnnotationSpec:
"""Blueprint-level shorthand for an annotation CE loss declaration.
Compiled to `schema.annotation_loss.AnnotationLossSpec` at build() time.
"""
field: str
weight: float = 0.5
max_length: int = 32
loss_type: str = "ce_text"
def to_loss_spec(self) -> AnnotationLossSpec:
return AnnotationLossSpec(
field=self.field,
loss_type=self.loss_type,
weight=self.weight,
max_length=self.max_length,
)
@dataclass(frozen=True)
class SingleArm:
"""Single-arm spec: arm joints + one gripper channel.
The four parquet columns fully describe the per-frame layout. Gripper
state/action may live in the *same* column as arm joints (e.g. oxe_auge's
`observation.joints` holds both) β in that case the user sets the same
column name for both `joint_*` and `gripper_*`; the blueprint infers
that only one state/action key is needed.
"""
dof: int
joint_state_col: str
joint_action_col: str
gripper_state_col: str
gripper_action_col: str
arm_mode: str = "delta" # "delta" | "abs"
gripper_mode: str = "abs" # "delta" | "abs"
def __post_init__(self) -> None:
if self.dof <= 0:
raise ValueError(f"SingleArm.dof must be > 0, got {self.dof}")
for name, mode in (("arm_mode", self.arm_mode),
("gripper_mode", self.gripper_mode)):
if mode not in ("delta", "abs"):
raise ValueError(
f"SingleArm.{name} must be 'delta' or 'abs', got {mode!r}"
)
@dataclass(frozen=True)
class StateActionChannel:
"""One independent (state_col, action_col) channel with its own dim/mode.
Used by MultiChannelBlueprint for datasets whose action vector is split
across multiple parquet columns (robocoin: `action` for arm +
`gripper_open_scale_action` for gripper, with different dims and modes).
"""
state_col: str
action_col: str
dim: int
mode: str = "delta" # "delta" | "abs"
is_gripper: bool = False # marks all `dim` flat indices as gripper dims
def __post_init__(self) -> None:
if self.dim <= 0:
raise ValueError(f"StateActionChannel.dim must be > 0, got {self.dim}")
if self.mode not in ("delta", "abs"):
raise ValueError(
f"StateActionChannel.mode must be 'delta' or 'abs', got {self.mode!r}"
)
if self.is_gripper and self.mode == "delta":
raise ValueError(
f"is_gripper=True channel must use mode='abs' (gripper never delta). "
f"Got mode={self.mode!r}, state_col={self.state_col!r}"
)
# --------------------------------------------------------------------------- #
# Top-level blueprints #
# --------------------------------------------------------------------------- #
@dataclass(frozen=True)
class SingleArmBlueprint:
"""Author a single-arm dataset in ~15 lines.
Given the SingleArm spec, the blueprint derives:
state_keys = (joint_state_col,) or (joint_state_col, gripper_state_col)
state_dims = (dof,) or (dof, 1) [merged when cols are identical]
action_keys = (joint_action_col,) or (joint_action_col, gripper_action_col)
action_dims = (dof,) or (dof, 1)
delta_mask = arm_mode Γ dof + gripper_mode Γ 1
gripper_action_dims = (dof,)
arm_layout = ArmLayoutSpec(SINGLE, arm_dof=dof, gripper_index_in_raw=dof)
"""
schema_id: str
robot_type: str
arm: SingleArm
cameras: Mapping[str, str]
annotations: tuple[AnnotationSpec, ...] = ()
source_path: Optional[str] = None
# Consumed POST-build by `discover_schema` (only relaxes the reverse
# "info.json has extra cameras" check). Blueprint authoring does NOT read
# this flag β it only forwards the value into the built DatasetSchema.
allow_extra_cameras: bool = False
# Physical semantics of the gripper action channel β see
# DatasetSchema.gripper_semantic. Forwarded into the built schema; default
# None means no cross-dataset semantic guard.
gripper_semantic: Optional[str] = None
# Virtual state columns β see DatasetSchema.virtual_state_sources.
# Forwarded verbatim; validate_schema enforces the "virtual." prefix and
# membership rules on the built schema.
virtual_state_sources: Mapping[str, str] = field(default_factory=dict)
def build(self) -> DatasetSchema:
a = self.arm
# Same column for joint + gripper (e.g. oxe_auge observation.joints
# holds [j0..j6, grip] in a single column) β collapse to one key.
state_collapsed = a.joint_state_col == a.gripper_state_col
action_collapsed = a.joint_action_col == a.gripper_action_col
if state_collapsed:
state_keys = (a.joint_state_col,)
state_dims = (a.dof + 1,)
else:
state_keys = (a.joint_state_col, a.gripper_state_col)
state_dims = (a.dof, 1)
if action_collapsed:
action_keys = (a.joint_action_col,)
action_dims = (a.dof + 1,)
else:
action_keys = (a.joint_action_col, a.gripper_action_col)
action_dims = (a.dof, 1)
delta_mask = (
tuple([a.arm_mode == "delta"] * a.dof)
+ (a.gripper_mode == "delta",)
)
gripper_action_dims = (a.dof,)
arm_layout = ArmLayoutSpec(
arm_count=ArmCount.SINGLE,
arm_dof=a.dof,
gripper_index_in_raw=a.dof,
)
annotation_losses = tuple(s.to_loss_spec() for s in self.annotations)
image_mapping = expand_camera_mapping(self.cameras)
return DatasetSchema(
schema_id=self.schema_id,
robot_type=self.robot_type,
state_keys=state_keys,
action_keys=action_keys,
state_dims=state_dims,
action_dims=action_dims,
delta_mask=delta_mask,
gripper_action_dims=gripper_action_dims,
image_mapping=image_mapping,
source="manifest",
source_path=self.source_path,
allow_extra_cameras=self.allow_extra_cameras,
arm_layout=arm_layout,
annotation_losses=annotation_losses,
gripper_semantic=self.gripper_semantic,
virtual_state_sources=self.virtual_state_sources,
)
@dataclass(frozen=True)
class MultiChannelBlueprint:
"""Author a dataset whose state/action spans multiple independent channels.
Use when the robot-native layout isn't ``arm + 1 gripper`` β e.g. robocoin
merges 16+ source robots with a dedicated dual-arm `gripper_open_scale_*`
channel separate from the main `action` column.
Because `channels` may not map onto any standard ArmLayout, the caller
passes `arm_layout_canonical` explicitly (or leaves it None if the dataset
is not "arm-shaped" at all).
"""
schema_id: str
robot_type: str
channels: tuple[StateActionChannel, ...]
cameras: Mapping[str, str]
annotations: tuple[AnnotationSpec, ...] = ()
arm_layout_canonical: Optional[ArmLayoutSpec] = None
source_path: Optional[str] = None
# Consumed POST-build by `discover_schema`. See SingleArmBlueprint
# for the full note; same semantics apply here.
allow_extra_cameras: bool = False
# See SingleArmBlueprint.gripper_semantic.
gripper_semantic: Optional[str] = None
def __post_init__(self) -> None:
if not self.channels:
raise ValueError("MultiChannelBlueprint.channels must be non-empty")
def build(self) -> DatasetSchema:
state_keys = tuple(c.state_col for c in self.channels)
action_keys = tuple(c.action_col for c in self.channels)
state_dims = tuple(c.dim for c in self.channels)
action_dims = tuple(c.dim for c in self.channels)
# delta_mask: concatenate per-channel (dim Γ mode flag).
delta_mask: tuple[bool, ...] = ()
gripper_idx_list: list[int] = []
offset = 0
for c in self.channels:
delta_mask = delta_mask + tuple([c.mode == "delta"] * c.dim)
if c.is_gripper:
gripper_idx_list.extend(range(offset, offset + c.dim))
offset += c.dim
gripper_action_dims = tuple(gripper_idx_list)
annotation_losses = tuple(s.to_loss_spec() for s in self.annotations)
image_mapping = expand_camera_mapping(self.cameras)
return DatasetSchema(
schema_id=self.schema_id,
robot_type=self.robot_type,
state_keys=state_keys,
action_keys=action_keys,
state_dims=state_dims,
action_dims=action_dims,
delta_mask=delta_mask,
gripper_action_dims=gripper_action_dims,
image_mapping=image_mapping,
source="manifest",
source_path=self.source_path,
allow_extra_cameras=self.allow_extra_cameras,
arm_layout=self.arm_layout_canonical,
annotation_losses=annotation_losses,
gripper_semantic=self.gripper_semantic,
)
|