robot-dam 0.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dam/__init__.py +69 -0
- dam/adapter/__init__.py +11 -0
- dam/adapter/base.py +182 -0
- dam/adapter/dataset/__init__.py +3 -0
- dam/adapter/dataset/source.py +279 -0
- dam/adapter/isaac/__init__.py +21 -0
- dam/adapter/isaac/filter.py +143 -0
- dam/adapter/lerobot/__init__.py +4 -0
- dam/adapter/lerobot/adapter.py +611 -0
- dam/adapter/lerobot/builder.py +411 -0
- dam/adapter/lerobot/policy.py +254 -0
- dam/adapter/opencv/source.py +253 -0
- dam/adapter/ros2/__init__.py +0 -0
- dam/adapter/ros2/_noop_policy.py +41 -0
- dam/adapter/ros2/sink.py +204 -0
- dam/adapter/ros2/source.py +287 -0
- dam/adapter/transforms.py +50 -0
- dam/api.py +482 -0
- dam/boundary/__init__.py +16 -0
- dam/boundary/builtin.py +45 -0
- dam/boundary/builtin_callbacks.py +55 -0
- dam/boundary/callbacks/__init__.py +70 -0
- dam/boundary/callbacks/_helpers.py +123 -0
- dam/boundary/callbacks/_registry.py +182 -0
- dam/boundary/callbacks/execution.py +309 -0
- dam/boundary/callbacks/hardware.py +466 -0
- dam/boundary/callbacks/kinematics.py +1002 -0
- dam/boundary/callbacks/ood.py +231 -0
- dam/boundary/constraint.py +23 -0
- dam/boundary/container.py +92 -0
- dam/boundary/graph.py +86 -0
- dam/boundary/list_container.py +66 -0
- dam/boundary/node.py +14 -0
- dam/boundary/single.py +52 -0
- dam/boundary/templates.py +75 -0
- dam/bus/__init__.py +148 -0
- dam/camera/__init__.py +3 -0
- dam/camera/frame_hub.py +171 -0
- dam/cli.py +704 -0
- dam/config/__init__.py +4 -0
- dam/config/hot_reload.py +164 -0
- dam/config/loader.py +27 -0
- dam/config/schema.py +304 -0
- dam/decorators.py +93 -0
- dam/experiments/__init__.py +15 -0
- dam/experiments/registry.py +576 -0
- dam/guard/__init__.py +5 -0
- dam/guard/aggregator.py +50 -0
- dam/guard/aggregators/__init__.py +16 -0
- dam/guard/aggregators/motion_qp.py +253 -0
- dam/guard/base.py +132 -0
- dam/guard/builtin/__init__.py +16 -0
- dam/guard/builtin/execution.py +152 -0
- dam/guard/builtin/hardware.py +188 -0
- dam/guard/builtin/motion.py +72 -0
- dam/guard/builtin/ood.py +1151 -0
- dam/guard/layer.py +33 -0
- dam/guard/lerobot_video_loader.py +173 -0
- dam/guard/ood_backend.py +423 -0
- dam/guard/ood_context.py +358 -0
- dam/guard/pipeline.py +424 -0
- dam/guard/stage.py +57 -0
- dam/guard/vision_feature_extractor.py +159 -0
- dam/injection/__init__.py +11 -0
- dam/injection/pool.py +28 -0
- dam/injection/resolver.py +23 -0
- dam/injection/static.py +30 -0
- dam/kinematics/resolver.py +181 -0
- dam/logging/__init__.py +5 -0
- dam/logging/console.py +45 -0
- dam/logging/cycle_record.py +112 -0
- dam/logging/loopback_writer.py +971 -0
- dam/preset/__init__.py +27 -0
- dam/preset/registry.py +232 -0
- dam/processor.py +300 -0
- dam/py.typed +0 -0
- dam/registry/__init__.py +3 -0
- dam/registry/callback.py +49 -0
- dam/registry/guard.py +57 -0
- dam/runner/__init__.py +3 -0
- dam/runner/base.py +469 -0
- dam/runner/lerobot.py +139 -0
- dam/runner/ros2.py +158 -0
- dam/runtime/__init__.py +3 -0
- dam/runtime/_context_state_machine.py +221 -0
- dam/runtime/_cycle_telemetry.py +284 -0
- dam/runtime/_hot_reload.py +160 -0
- dam/runtime/_stackfile_builder.py +304 -0
- dam/runtime/builtin_contexts.py +415 -0
- dam/runtime/context.py +216 -0
- dam/runtime/execution_engine.py +759 -0
- dam/runtime/factory.py +667 -0
- dam/runtime/failure_classify.py +67 -0
- dam/runtime/guard_runtime.py +1290 -0
- dam/runtime/merge_policy.py +122 -0
- dam/runtime/qp_solver.py +355 -0
- dam/runtime/slow_lane.py +177 -0
- dam/services/__init__.py +32 -0
- dam/services/api.py +156 -0
- dam/services/boundary_config.py +137 -0
- dam/services/mcap_sessions.py +1017 -0
- dam/services/ood_trainer.py +172 -0
- dam/services/replay.py +479 -0
- dam/services/risk_log.py +314 -0
- dam/services/routers/__init__.py +25 -0
- dam/services/routers/boundaries.py +100 -0
- dam/services/routers/control.py +166 -0
- dam/services/routers/experiments.py +74 -0
- dam/services/routers/mcap.py +225 -0
- dam/services/routers/ood.py +333 -0
- dam/services/routers/replay.py +125 -0
- dam/services/routers/risk_log.py +242 -0
- dam/services/routers/stackfiles.py +107 -0
- dam/services/routers/system.py +200 -0
- dam/services/routers/telemetry.py +146 -0
- dam/services/runtime_control.py +591 -0
- dam/services/serialization.py +113 -0
- dam/services/service_container.py +27 -0
- dam/services/telemetry.py +284 -0
- dam/services/ui/live_test.html +134 -0
- dam/testing/__init__.py +17 -0
- dam/testing/dataset_source.py +5 -0
- dam/testing/helpers.py +40 -0
- dam/testing/mocks.py +47 -0
- dam/testing/pipeline.py +31 -0
- dam/testing/safety.py +37 -0
- dam/testing/sim_adapters.py +83 -0
- dam/testing/test_loopback_writer.py +409 -0
- dam/types/__init__.py +18 -0
- dam/types/action.py +148 -0
- dam/types/dynamics.py +219 -0
- dam/types/enforcement.py +17 -0
- dam/types/joint_layout.py +304 -0
- dam/types/observation.py +130 -0
- dam/types/result.py +81 -0
- dam/types/risk.py +36 -0
- robot_dam-0.6.0.dist-info/METADATA +261 -0
- robot_dam-0.6.0.dist-info/RECORD +141 -0
- robot_dam-0.6.0.dist-info/WHEEL +4 -0
- robot_dam-0.6.0.dist-info/entry_points.txt +2 -0
- robot_dam-0.6.0.dist-info/licenses/LICENSE +368 -0
dam/__init__.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import tomllib
|
|
2
|
+
from importlib.metadata import PackageNotFoundError
|
|
3
|
+
from importlib.metadata import version as _package_version
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from dam import testing
|
|
7
|
+
from dam.api import RunSummary, SafetyGuard, SafetyKinematicsResolver, build_runner, run, safe
|
|
8
|
+
from dam.decorators import callback, fallback, guard
|
|
9
|
+
from dam.guard.aggregator import aggregate_decisions
|
|
10
|
+
from dam.guard.base import Guard
|
|
11
|
+
from dam.guard.layer import GuardLayer
|
|
12
|
+
from dam.runner.base import BaseRunner as Runner
|
|
13
|
+
from dam.runner.base import RunnerStatus
|
|
14
|
+
from dam.types.action import ActionProposal, ValidatedAction
|
|
15
|
+
from dam.types.observation import Observation
|
|
16
|
+
from dam.types.result import GuardDecision, GuardResult
|
|
17
|
+
from dam.types.risk import CycleResult, RiskLevel
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _resolve_version() -> str:
|
|
21
|
+
version_file = Path(__file__).resolve().parents[1] / "pyproject.toml"
|
|
22
|
+
if version_file.exists():
|
|
23
|
+
with version_file.open("rb") as fh:
|
|
24
|
+
data = tomllib.load(fh)
|
|
25
|
+
version = data.get("project", {}).get("version")
|
|
26
|
+
if version:
|
|
27
|
+
return str(version)
|
|
28
|
+
try:
|
|
29
|
+
return _package_version("dam")
|
|
30
|
+
except PackageNotFoundError:
|
|
31
|
+
return "unknown"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
__version__ = _resolve_version()
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"__version__",
|
|
38
|
+
"guard",
|
|
39
|
+
"callback",
|
|
40
|
+
"fallback",
|
|
41
|
+
"Guard",
|
|
42
|
+
"aggregate_decisions",
|
|
43
|
+
"GuardLayer",
|
|
44
|
+
"GuardResult",
|
|
45
|
+
"GuardDecision",
|
|
46
|
+
"Observation",
|
|
47
|
+
"ActionProposal",
|
|
48
|
+
"ValidatedAction",
|
|
49
|
+
"RiskLevel",
|
|
50
|
+
"CycleResult",
|
|
51
|
+
"testing",
|
|
52
|
+
"build_runner",
|
|
53
|
+
"run",
|
|
54
|
+
"safe",
|
|
55
|
+
"RunSummary",
|
|
56
|
+
"SafetyGuard",
|
|
57
|
+
"SafetyKinematicsResolver",
|
|
58
|
+
"SafetyProcessorStep",
|
|
59
|
+
"Runner",
|
|
60
|
+
"RunnerStatus",
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def __getattr__(name: str): # type: ignore[no-untyped-def]
|
|
65
|
+
if name == "SafetyProcessorStep":
|
|
66
|
+
from dam.processor import SafetyProcessorStep
|
|
67
|
+
|
|
68
|
+
return SafetyProcessorStep
|
|
69
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
dam/adapter/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from dam.adapter.dataset import DatasetReplayPolicy, DatasetSimSource
|
|
2
|
+
from dam.adapter.lerobot import LeRobotAdapter, LeRobotPolicyAdapter
|
|
3
|
+
from dam.adapter.transforms import ImageNamespaceSource
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"DatasetReplayPolicy",
|
|
7
|
+
"DatasetSimSource",
|
|
8
|
+
"LeRobotAdapter",
|
|
9
|
+
"LeRobotPolicyAdapter",
|
|
10
|
+
"ImageNamespaceSource",
|
|
11
|
+
]
|
dam/adapter/base.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Abstract base classes for all DAM adapters.
|
|
2
|
+
|
|
3
|
+
Design principle: Guards NEVER import concrete adapters. They only declare parameter names
|
|
4
|
+
in their check() signature. The framework resolves and injects the correct objects from the
|
|
5
|
+
runtime pool at startup. This boundary is enforced at the import level.
|
|
6
|
+
|
|
7
|
+
Adapter roles:
|
|
8
|
+
SensorAdapter — external world → Observation
|
|
9
|
+
PolicyAdapter — Observation → ActionProposal
|
|
10
|
+
ActionAdapter — ValidatedAction → hardware
|
|
11
|
+
SimulatorAdapter — (used by L1 only, never by guards directly)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from abc import ABC, abstractmethod
|
|
17
|
+
from typing import TYPE_CHECKING, Any
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from dam.types.action import ActionProposal, ValidatedAction
|
|
21
|
+
from dam.types.observation import Observation
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SensorAdapter(ABC):
|
|
25
|
+
"""Bridges a hardware / ROS2 / serial source to DAM Observation.
|
|
26
|
+
|
|
27
|
+
Implementations: LeRobotAdapter, ROS2SourceAdapter, SerialSourceAdapter …
|
|
28
|
+
Declared in Stackfile hardware.sources; instantiated by StackfileLoader, never by user code.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def connect(self) -> None:
|
|
33
|
+
"""Open connection to the sensor / topic."""
|
|
34
|
+
...
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def read(self) -> Observation:
|
|
38
|
+
"""Read one sample and return a fully-typed Observation."""
|
|
39
|
+
...
|
|
40
|
+
|
|
41
|
+
@abstractmethod
|
|
42
|
+
def is_healthy(self) -> bool:
|
|
43
|
+
"""Return True if the sensor is reachable and data is fresh."""
|
|
44
|
+
...
|
|
45
|
+
|
|
46
|
+
@abstractmethod
|
|
47
|
+
def disconnect(self) -> None:
|
|
48
|
+
"""Close connection gracefully."""
|
|
49
|
+
...
|
|
50
|
+
|
|
51
|
+
def supported_channels(self) -> set[str]:
|
|
52
|
+
"""Return the set of observation channel names this adapter can provide.
|
|
53
|
+
|
|
54
|
+
Subclasses that support device-specific channels (e.g. servo current,
|
|
55
|
+
temperature) must override this. The default returns an empty set,
|
|
56
|
+
meaning no extra channels are available.
|
|
57
|
+
"""
|
|
58
|
+
return set()
|
|
59
|
+
|
|
60
|
+
def set_observation_channels(self, channels: list[str]) -> None:
|
|
61
|
+
"""Activate the given observation channels for reading.
|
|
62
|
+
|
|
63
|
+
Called by the factory after validating against supported_channels().
|
|
64
|
+
Subclasses that support channels must override this.
|
|
65
|
+
"""
|
|
66
|
+
raise NotImplementedError(f"{type(self).__name__} does not support observation channels")
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def camera_shapes(self) -> dict[str, tuple[int, int]]:
|
|
70
|
+
"""Return ``{camera_name: (height, width)}`` for cameras this source provides.
|
|
71
|
+
|
|
72
|
+
Populated after ``verify()`` has read a real frame. Empty when the
|
|
73
|
+
adapter has no cameras or before verify. The runtime uses this to
|
|
74
|
+
warm up downstream PyTorch models at the actual capture resolution.
|
|
75
|
+
"""
|
|
76
|
+
return {}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class PolicyAdapter(ABC):
|
|
80
|
+
"""Wraps any policy model behind a single predict() contract.
|
|
81
|
+
|
|
82
|
+
Implementations: LeRobotPolicyAdapter, RandomPolicyAdapter (testing) …
|
|
83
|
+
The adapter hides framework-specific APIs (torch tensor shapes, chunk semantics, etc.)
|
|
84
|
+
so that guards and the runtime never depend on the policy library.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
@abstractmethod
|
|
88
|
+
def initialize(self, config: dict[str, Any]) -> None:
|
|
89
|
+
"""Load weights and prepare model for inference."""
|
|
90
|
+
...
|
|
91
|
+
|
|
92
|
+
@abstractmethod
|
|
93
|
+
def predict(self, obs: Observation) -> ActionProposal:
|
|
94
|
+
"""Run a forward pass and return the next action proposal."""
|
|
95
|
+
...
|
|
96
|
+
|
|
97
|
+
@abstractmethod
|
|
98
|
+
def get_policy_name(self) -> str:
|
|
99
|
+
"""Return a human-readable identifier, e.g. 'lerobot_act_so101'."""
|
|
100
|
+
...
|
|
101
|
+
|
|
102
|
+
@abstractmethod
|
|
103
|
+
def reset(self) -> None:
|
|
104
|
+
"""Reset any internal recurrent state (called on task start)."""
|
|
105
|
+
...
|
|
106
|
+
|
|
107
|
+
def preflight(self, camera_shapes: dict[str, tuple[int, int]] | None = None) -> None: # noqa: B027
|
|
108
|
+
"""Eagerly warm up the policy at the real capture resolution.
|
|
109
|
+
|
|
110
|
+
``camera_shapes`` maps camera name → ``(height, width)`` from the
|
|
111
|
+
source adapters' first verified frames. Implementations should build
|
|
112
|
+
a dummy Observation with those exact shapes and run one forward pass
|
|
113
|
+
so PyTorch can compile its per-shape kernels before the control loop
|
|
114
|
+
starts. Default is a no-op for policies that don't need warmup.
|
|
115
|
+
"""
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class ActionAdapter(ABC):
|
|
120
|
+
"""Sends a ValidatedAction to the physical hardware or ROS2 topic.
|
|
121
|
+
|
|
122
|
+
Implementations: LeRobotAdapter, ROS2SinkAdapter …
|
|
123
|
+
Declared in Stackfile hardware.sinks; instantiated by StackfileLoader.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
@abstractmethod
|
|
127
|
+
def connect(self) -> None:
|
|
128
|
+
"""Open connection to the actuator / topic."""
|
|
129
|
+
...
|
|
130
|
+
|
|
131
|
+
@abstractmethod
|
|
132
|
+
def apply(self, action: ValidatedAction) -> None:
|
|
133
|
+
"""Send the validated action to hardware. Must be non-blocking in hot path."""
|
|
134
|
+
...
|
|
135
|
+
|
|
136
|
+
@abstractmethod
|
|
137
|
+
def emergency_stop(self) -> None:
|
|
138
|
+
"""Immediately halt all motion. Must be callable from any thread."""
|
|
139
|
+
...
|
|
140
|
+
|
|
141
|
+
@abstractmethod
|
|
142
|
+
def get_hardware_status(self) -> dict[str, Any]:
|
|
143
|
+
"""Return a dict of diagnostic information (temperatures, torques, errors)."""
|
|
144
|
+
...
|
|
145
|
+
|
|
146
|
+
@abstractmethod
|
|
147
|
+
def disconnect(self) -> None:
|
|
148
|
+
"""Stop actuator and close connection gracefully."""
|
|
149
|
+
...
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class SimulatorAdapter(ABC):
|
|
153
|
+
"""Rollout interface used exclusively by L1 (SimPreflightGuard).
|
|
154
|
+
|
|
155
|
+
Guards must NOT import this class directly — L1 receives the simulator object
|
|
156
|
+
via injection pool key 'simulator'. Other guards must not declare 'simulator'
|
|
157
|
+
in their check() signatures.
|
|
158
|
+
|
|
159
|
+
The adapter's job is to synchronise with real-world state, step forward one action,
|
|
160
|
+
and report whether the result is safe. If is_available() returns False, L1 skips
|
|
161
|
+
the sim check and returns PASS (graceful degradation — simulator is optional).
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
@abstractmethod
|
|
165
|
+
def reset(self, obs: Observation) -> None:
|
|
166
|
+
"""Synchronise the simulator state with the current real-world observation."""
|
|
167
|
+
...
|
|
168
|
+
|
|
169
|
+
@abstractmethod
|
|
170
|
+
def step(self, action: ActionProposal) -> Observation:
|
|
171
|
+
"""Apply action in simulation and return the resulting observation."""
|
|
172
|
+
...
|
|
173
|
+
|
|
174
|
+
@abstractmethod
|
|
175
|
+
def has_collision(self) -> bool:
|
|
176
|
+
"""Return True if the most recent step produced a collision."""
|
|
177
|
+
...
|
|
178
|
+
|
|
179
|
+
@abstractmethod
|
|
180
|
+
def is_available(self) -> bool:
|
|
181
|
+
"""Return False if the simulator process is unavailable; L1 will PASS gracefully."""
|
|
182
|
+
...
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""Dataset adapter source and policy for LeRobot dataset replay.
|
|
2
|
+
|
|
3
|
+
Usage
|
|
4
|
+
-----
|
|
5
|
+
Used in simulation mode when a ``simulation.dataset_repo_id`` is set in the
|
|
6
|
+
Stackfile, and by dataset-to-hardware replay templates. Replays observations
|
|
7
|
+
(joint positions + camera images) from the dataset; ``DatasetReplayPolicy``
|
|
8
|
+
can emit each recorded action through the normal guard/sink path.
|
|
9
|
+
|
|
10
|
+
The source iterates through one episode in sequence; when it reaches the end
|
|
11
|
+
it wraps around to the beginning. This gives infinite continuous replay for
|
|
12
|
+
demo and regression-testing purposes.
|
|
13
|
+
|
|
14
|
+
Initialisation
|
|
15
|
+
--------------
|
|
16
|
+
Dataset download / decoding happens in ``__init__``. This call blocks until
|
|
17
|
+
the dataset is ready (typically a few seconds for cached data, longer on first
|
|
18
|
+
download). The control loop does NOT start until the factory returns the
|
|
19
|
+
source, so blocking in ``__init__`` is acceptable and avoids partial-init race
|
|
20
|
+
conditions.
|
|
21
|
+
|
|
22
|
+
Image format
|
|
23
|
+
------------
|
|
24
|
+
LeRobot datasets store camera frames as CHW float32 tensors in [0, 1].
|
|
25
|
+
This class converts them to HWC uint8 numpy arrays ([0, 255]) to match the
|
|
26
|
+
format expected by ``Observation.images`` and the MCAP image encoder.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import logging
|
|
32
|
+
import time
|
|
33
|
+
from typing import Any
|
|
34
|
+
|
|
35
|
+
import numpy as np
|
|
36
|
+
|
|
37
|
+
from dam.adapter.base import PolicyAdapter
|
|
38
|
+
from dam.types.action import ActionProposal
|
|
39
|
+
from dam.types.observation import Observation
|
|
40
|
+
|
|
41
|
+
logger = logging.getLogger(__name__)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class DatasetSimSource:
|
|
45
|
+
"""Replay a LeRobot dataset episode as a live observation stream.
|
|
46
|
+
|
|
47
|
+
Parameters
|
|
48
|
+
----------
|
|
49
|
+
repo_id:
|
|
50
|
+
HuggingFace repo ID for the dataset (e.g. ``MikeChenYZ/soarm-fmb-v2``).
|
|
51
|
+
episode:
|
|
52
|
+
Episode index to replay (default 0).
|
|
53
|
+
hz:
|
|
54
|
+
Control frequency — used only for velocity estimation via finite
|
|
55
|
+
difference when the dataset does not include velocity data.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
repo_id: str,
|
|
61
|
+
episode: int = 0,
|
|
62
|
+
hz: float = 10.0,
|
|
63
|
+
degrees_mode: bool = True,
|
|
64
|
+
*,
|
|
65
|
+
strict: bool = False,
|
|
66
|
+
) -> None:
|
|
67
|
+
self._repo_id = repo_id
|
|
68
|
+
self._episode = episode
|
|
69
|
+
self._hz = hz
|
|
70
|
+
self._degrees_mode = degrees_mode
|
|
71
|
+
self._cursor: float = 0.0
|
|
72
|
+
self._current_frame: dict[str, Any] | None = None
|
|
73
|
+
self._strict = strict
|
|
74
|
+
self._n_joints = 6 # updated from first loaded frame
|
|
75
|
+
|
|
76
|
+
# Pre-loaded episode data: list of {"joint_positions", "images"?, "action"?}
|
|
77
|
+
self._frames: list[dict[str, Any]] = []
|
|
78
|
+
self._prev_pos: np.ndarray | None = None
|
|
79
|
+
|
|
80
|
+
logger.info("DatasetSimSource: loading %s episode %d …", repo_id, episode)
|
|
81
|
+
try:
|
|
82
|
+
self._frames, self._source_fps = self._load_episode(repo_id, episode)
|
|
83
|
+
if strict and (
|
|
84
|
+
not self._frames or any("action" not in frame for frame in self._frames)
|
|
85
|
+
):
|
|
86
|
+
raise ValueError(
|
|
87
|
+
"Dataset hardware replay requires a non-empty episode with an action per frame"
|
|
88
|
+
)
|
|
89
|
+
if self._frames:
|
|
90
|
+
self._n_joints = len(self._frames[0]["joint_positions"])
|
|
91
|
+
logger.info(
|
|
92
|
+
"DatasetSimSource: ready — %d frames, %d joints, source_fps: %.1f, cameras: %s",
|
|
93
|
+
len(self._frames),
|
|
94
|
+
self._n_joints,
|
|
95
|
+
self._source_fps,
|
|
96
|
+
sorted({k for f in self._frames[:1] for k in (f.get("images") or {})}),
|
|
97
|
+
)
|
|
98
|
+
except Exception:
|
|
99
|
+
if strict:
|
|
100
|
+
raise
|
|
101
|
+
logger.exception(
|
|
102
|
+
"DatasetSimSource: failed to load %s — falling back to random walk",
|
|
103
|
+
repo_id,
|
|
104
|
+
)
|
|
105
|
+
self._frames = []
|
|
106
|
+
self._source_fps = hz
|
|
107
|
+
|
|
108
|
+
# ── Public API ─────────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
def connect(self) -> None:
|
|
111
|
+
"""Reset playback state on fresh connection/task start."""
|
|
112
|
+
self._cursor = 0.0
|
|
113
|
+
self._current_frame = None
|
|
114
|
+
self._prev_pos = None
|
|
115
|
+
logger.info("DatasetSimSource: playback cursor reset")
|
|
116
|
+
|
|
117
|
+
def disconnect(self) -> None:
|
|
118
|
+
"""No-op for simulation."""
|
|
119
|
+
pass
|
|
120
|
+
|
|
121
|
+
def read(self) -> Observation:
|
|
122
|
+
if not self._frames:
|
|
123
|
+
return self._random_obs()
|
|
124
|
+
|
|
125
|
+
# Step-driven: advance cursor by (source_fps / runtime_hz) frames per call.
|
|
126
|
+
# This keeps replay pace proportional to the control loop rate regardless
|
|
127
|
+
# of wall-clock jitter, GC pauses, or guard latency spikes.
|
|
128
|
+
index = int(self._cursor)
|
|
129
|
+
frame = self._frames[index % len(self._frames)]
|
|
130
|
+
self._current_frame = frame
|
|
131
|
+
self._cursor += self._source_fps / self._hz
|
|
132
|
+
|
|
133
|
+
joint_pos: np.ndarray = frame["joint_positions"]
|
|
134
|
+
|
|
135
|
+
if self._degrees_mode:
|
|
136
|
+
joint_pos = joint_pos * (np.pi / 180.0)
|
|
137
|
+
|
|
138
|
+
# Velocity: dt is the real time between control cycles (1/runtime_hz),
|
|
139
|
+
# which is the interval between successive commands sent to the robot.
|
|
140
|
+
if self._prev_pos is not None:
|
|
141
|
+
dt = 1.0 / self._hz
|
|
142
|
+
velocity = (joint_pos - self._prev_pos) / dt
|
|
143
|
+
else:
|
|
144
|
+
velocity = np.zeros_like(joint_pos)
|
|
145
|
+
self._prev_pos = joint_pos
|
|
146
|
+
|
|
147
|
+
return Observation(
|
|
148
|
+
timestamp=time.monotonic(),
|
|
149
|
+
joint_positions=joint_pos.copy(),
|
|
150
|
+
joint_velocities=velocity,
|
|
151
|
+
images=frame.get("images"),
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def current_action(self) -> np.ndarray:
|
|
155
|
+
"""Return the recorded action paired with the most recently read observation."""
|
|
156
|
+
if self._current_frame is None:
|
|
157
|
+
raise RuntimeError("DatasetReplayPolicy called before the dataset source was read")
|
|
158
|
+
action = self._current_frame.get("action")
|
|
159
|
+
if action is None:
|
|
160
|
+
raise RuntimeError(
|
|
161
|
+
"Dataset has no 'action' field; hardware replay requires recorded actions"
|
|
162
|
+
)
|
|
163
|
+
action_arr = np.asarray(action, dtype=float)
|
|
164
|
+
if self._degrees_mode:
|
|
165
|
+
action_arr = action_arr * (np.pi / 180.0)
|
|
166
|
+
return action_arr.copy()
|
|
167
|
+
|
|
168
|
+
# ── Dataset loading ────────────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
def _load_episode(self, repo_id: str, episode: int) -> tuple[list[dict[str, Any]], float]:
|
|
171
|
+
"""Download + decode one episode. Returns (frames, fps)."""
|
|
172
|
+
try:
|
|
173
|
+
from lerobot.datasets.lerobot_dataset import LeRobotDataset
|
|
174
|
+
except ImportError as exc:
|
|
175
|
+
raise ImportError(
|
|
176
|
+
"lerobot is required for DatasetSimSource. Install it with: make setup-lerobot"
|
|
177
|
+
) from exc
|
|
178
|
+
|
|
179
|
+
import torch
|
|
180
|
+
from torch.utils.data import DataLoader
|
|
181
|
+
|
|
182
|
+
ds = LeRobotDataset(repo_id, episodes=[episode])
|
|
183
|
+
frames: list[dict[str, Any]] = []
|
|
184
|
+
|
|
185
|
+
# num_workers=0: single-process iteration — prevents leaked semaphore
|
|
186
|
+
# warnings from torch multiprocessing workers at shutdown.
|
|
187
|
+
# pin_memory=False: avoids CUDA page-locked memory on CPU-only setups.
|
|
188
|
+
loader = DataLoader(ds, batch_size=1, num_workers=0, pin_memory=False)
|
|
189
|
+
|
|
190
|
+
for batch in loader:
|
|
191
|
+
# DataLoader wraps each value in a batch dimension — squeeze it back
|
|
192
|
+
item = {k: (v[0] if isinstance(v, torch.Tensor) else v) for k, v in batch.items()}
|
|
193
|
+
frame: dict[str, Any] = {}
|
|
194
|
+
|
|
195
|
+
# ── Joint positions ────────────────────────────────────────
|
|
196
|
+
state = item.get("observation.state")
|
|
197
|
+
if state is None:
|
|
198
|
+
# Some datasets use different key names
|
|
199
|
+
state = item.get("state") or item.get("joints")
|
|
200
|
+
if state is not None:
|
|
201
|
+
if isinstance(state, torch.Tensor):
|
|
202
|
+
state = state.detach().cpu().numpy()
|
|
203
|
+
frame["joint_positions"] = np.asarray(state, dtype=float)
|
|
204
|
+
else:
|
|
205
|
+
frame["joint_positions"] = np.zeros(self._n_joints)
|
|
206
|
+
|
|
207
|
+
# ── Camera images ──────────────────────────────────────────
|
|
208
|
+
images: dict[str, np.ndarray] = {}
|
|
209
|
+
for key, val in item.items():
|
|
210
|
+
if not key.startswith("observation.images."):
|
|
211
|
+
continue
|
|
212
|
+
cam = key.split(".", 2)[2] # e.g. "top" or "wrist"
|
|
213
|
+
if isinstance(val, torch.Tensor):
|
|
214
|
+
# CHW float32 [0,1] → HWC uint8 [0,255]
|
|
215
|
+
t = val.detach().cpu()
|
|
216
|
+
if t.ndim == 3 and t.shape[0] in (1, 3, 4):
|
|
217
|
+
t = t.permute(1, 2, 0)
|
|
218
|
+
arr: np.ndarray[tuple[int, ...], np.dtype[np.uint8]] = (
|
|
219
|
+
(t.numpy() * 255).clip(0, 255).astype(np.uint8)
|
|
220
|
+
)
|
|
221
|
+
else:
|
|
222
|
+
arr = np.asarray(val, dtype=np.uint8)
|
|
223
|
+
if arr.ndim == 2:
|
|
224
|
+
arr = arr[:, :, np.newaxis]
|
|
225
|
+
images[cam] = arr
|
|
226
|
+
|
|
227
|
+
if images:
|
|
228
|
+
frame["images"] = images
|
|
229
|
+
|
|
230
|
+
action = item.get("action")
|
|
231
|
+
if action is not None:
|
|
232
|
+
if isinstance(action, torch.Tensor):
|
|
233
|
+
action = action.detach().cpu().numpy()
|
|
234
|
+
frame["action"] = np.asarray(action, dtype=float)
|
|
235
|
+
|
|
236
|
+
frames.append(frame)
|
|
237
|
+
|
|
238
|
+
# Explicitly delete the loader so Python can clean up its internal
|
|
239
|
+
# shared-memory / semaphore state before the resource_tracker runs.
|
|
240
|
+
del loader
|
|
241
|
+
|
|
242
|
+
return frames, float(getattr(ds, "fps", 30.0))
|
|
243
|
+
|
|
244
|
+
# ── Fallback ───────────────────────────────────────────────────────────
|
|
245
|
+
|
|
246
|
+
def _random_obs(self) -> Observation:
|
|
247
|
+
"""Random-walk fallback when dataset loading fails."""
|
|
248
|
+
if not hasattr(self, "_rng"):
|
|
249
|
+
self._rng = np.random.default_rng(42)
|
|
250
|
+
self._rng_pos = self._rng.uniform(-0.3, 0.3, size=self._n_joints)
|
|
251
|
+
delta = self._rng.normal(0.0, 0.03, size=self._n_joints)
|
|
252
|
+
self._rng_pos = np.clip(self._rng_pos + delta, -1.9, 1.9)
|
|
253
|
+
return Observation(
|
|
254
|
+
timestamp=time.monotonic(),
|
|
255
|
+
joint_positions=self._rng_pos.copy(),
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class DatasetReplayPolicy(PolicyAdapter):
|
|
260
|
+
"""Emit the action recorded alongside the source's current dataset frame."""
|
|
261
|
+
|
|
262
|
+
def __init__(self, source: DatasetSimSource) -> None:
|
|
263
|
+
self._source = source
|
|
264
|
+
|
|
265
|
+
def initialize(self, config: dict[str, Any]) -> None:
|
|
266
|
+
pass
|
|
267
|
+
|
|
268
|
+
def predict(self, obs: Observation) -> ActionProposal:
|
|
269
|
+
return ActionProposal(
|
|
270
|
+
target_joint_positions=self._source.current_action(),
|
|
271
|
+
timestamp=obs.timestamp,
|
|
272
|
+
policy_name="dataset_replay",
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
def get_policy_name(self) -> str:
|
|
276
|
+
return "dataset_replay"
|
|
277
|
+
|
|
278
|
+
def reset(self) -> None:
|
|
279
|
+
pass
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Isaac Sim 6.0 adapter — bridges DAM safety pipeline with NVIDIA Isaac Sim.
|
|
2
|
+
|
|
3
|
+
Usage (minimal):
|
|
4
|
+
|
|
5
|
+
from dam.adapter.isaac import IsaacSafetyFilter
|
|
6
|
+
|
|
7
|
+
filter = IsaacSafetyFilter("safety.yaml", task="manipulation")
|
|
8
|
+
# In your Isaac Sim step loop:
|
|
9
|
+
safe_action = filter(action_tensor, obs_tensor)
|
|
10
|
+
|
|
11
|
+
Usage (full runner with MCAP logging):
|
|
12
|
+
|
|
13
|
+
import dam
|
|
14
|
+
runner = dam.build_runner("franka_isaac.yaml")
|
|
15
|
+
runner.connect()
|
|
16
|
+
runner.start(task="pick_place")
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from dam.adapter.isaac.filter import IsaacSafetyFilter
|
|
20
|
+
|
|
21
|
+
__all__ = ["IsaacSafetyFilter"]
|