Source code for embodichain_models.dynamics.rigidformer.supervised
# ----------------------------------------------------------------------------
# Copyright (c) 2021-2026 DexForce Technology Co., Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ----------------------------------------------------------------------------
"""Shifted author supervision and valid-anchor loss reduction for unified SL."""
from __future__ import annotations
from collections.abc import Mapping
from typing import Any
import torch
from torch import nn
from torch.nn import functional as F
from embodichain.learning.sl import LossResult, SupervisedBatch
__all__ = ["rigidformer_batch", "RigidFormerObjective"]
[docs]
def rigidformer_batch(
values: Mapping[str, Any], *, metadata: Mapping[str, Any] | None = None
) -> SupervisedBatch:
"""Separate shifted conditioning history and next-frame supervision.
Args:
values: Author batch including an aligned sequence of at least 3 frames.
metadata: Optional immutable data and sampling provenance.
Returns:
Supervision for frames 2..T-1 with causal conditioning through T-2.
Earlier targets also occur as subsequent teacher-forcing inputs.
"""
positions = values["obj_positions"]
if positions.ndim != 5 or positions.shape[1] < 3:
raise ValueError("Supervision requires obj_positions [B,T>=3,O,P,3]")
inputs = {k: v for k, v in values.items() if isinstance(v, torch.Tensor)}
inputs["obj_positions"] = positions[:, :-1]
inputs["valid_vtx_mask"] = values["valid_vtx_mask"][:, :-1]
return SupervisedBatch(
inputs=inputs, targets=positions[:, 2:], metadata=dict(metadata or {})
)
[docs]
class RigidFormerObjective:
"""Reproduce the author's four anchor-only smooth-L1 losses.
Args:
weights: Position/acceleration before/after Kabsch weights.
time_step_scale: Divide errors by step_size squared before smooth-L1.
augmentation: Optional author geometry augmentation, applied in FP32
before the network's numerical context.
"""
[docs]
def __init__(
self,
weights: Mapping[str, float] | None = None,
*,
time_step_scale: bool = True,
augmentation: Any = None,
) -> None:
self.weights = {
"position_before_icp": 10.0,
"position_after_icp": 10.0,
"acceleration_before_icp": 1.0,
"acceleration_after_icp": 1.0,
**dict(weights or {}),
}
if set(self.weights) != {
"position_before_icp",
"position_after_icp",
"acceleration_before_icp",
"acceleration_after_icp",
} or any(
not torch.isfinite(torch.tensor(v)) or v < 0 for v in self.weights.values()
):
raise ValueError(
"Only the four finite nonnegative author anchor loss weights are supported"
)
self.time_step_scale = time_step_scale
self.augmentation = augmentation
[docs]
def compute_loss(self, model: nn.Module, batch: SupervisedBatch) -> LossResult:
"""Compute one local anchor mean; the shared learner owns DDP reduction.
Args:
model: Native RigidFormer model.
batch: Shifted conditioning and owned target geometry.
Returns:
Weighted scalar loss, diagnostic terms and valid-anchor count.
"""
values = dict(batch.inputs)
history = values["obj_positions"]
target = batch.targets
if self.augmentation is not None:
raw = dict(values)
raw["obj_positions"] = torch.cat((history[:, :2], target), dim=1)
raw["valid_vtx_mask"] = torch.cat(
(values["valid_vtx_mask"], values["valid_vtx_mask"][:, -1:]), dim=1
)
# The reference augments FP32 geometry before entering model autocast.
with torch.autocast(history.device.type, enabled=False):
augmented = rigidformer_batch(self.augmentation(raw, split="train"))
values = dict(augmented.inputs)
history, target = values["obj_positions"], augmented.targets
if target.shape != history[:, 1:].shape or batch.masks:
raise ValueError(
"Target layout must align with shifted author history; masks use geometry fields"
)
# The last frame is never a conditioning input. Fill it without labels.
values["obj_positions"] = torch.cat(
(history, torch.zeros_like(history[:, -1:])), dim=1
)
values["valid_vtx_mask"] = torch.cat(
(values["valid_vtx_mask"], values["valid_vtx_mask"][:, -1:]), dim=1
)
# The native network centers its own input in-place when configured;
# its position outputs use that centered frame, while target is owned
# separately and remains in world coordinates.
reference = getattr(model, "reference_config", {})
centered = reference.get("data", {}).get("center_scene_by_t0", False)
centers = None
if centered:
positions = values.get("obj_positions_input", values["obj_positions"])
centers = torch.stack(
[
positions[i, 0][values["valid_vtx_mask"][i, 0]].mean(dim=0)
for i in range(positions.shape[0])
]
)
predictions = model(values)
b, t, o, p, _ = target.shape
anchors = values["anchor_idx"][:, None, :, :, None].expand(b, t, o, -1, 3)
def at_anchor(value: torch.Tensor) -> torch.Tensor:
return torch.gather(value, 3, anchors)
gt_pos = at_anchor(target)
if centers is not None:
gt_pos = gt_pos - centers[:, None, None, None, :]
gt_acc = at_anchor(target - 2 * history[:, 1:] + history[:, :-1])
errors = {
"position_before_icp": at_anchor(predictions["obj_positions_norm"])
- gt_pos,
"position_after_icp": at_anchor(predictions["obj_positions_icp_norm"])
- gt_pos,
"acceleration_before_icp": predictions[
"obj_accelerations_no_icp_norm_anchor"
]
- gt_acc,
"acceleration_after_icp": at_anchor(predictions["obj_accelerations_norm"])
- gt_acc,
}
mask = (
values["valid_obj_mask"][:, None, :, None]
.expand(b, t, o, anchors.shape[3])
.float()
)
count = mask.sum()
if count <= 0:
raise ValueError("At least one valid supervised anchor is required")
scale = (
values["skip_step_size"][:, None, None, None, None].float().square()
if self.time_step_scale
else 1.0
)
terms = {
name: (
F.smooth_l1_loss(
error / scale, torch.zeros_like(error), reduction="none"
)
.sum(-1)
.float()
* mask
).sum()
/ (count + 1e-8)
for name, error in errors.items()
}
loss = sum(terms[name] * weight for name, weight in self.weights.items())
return LossResult(loss=loss, metrics=terms, weight=float(count.detach()))