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.
Files changed (141) hide show
  1. dam/__init__.py +69 -0
  2. dam/adapter/__init__.py +11 -0
  3. dam/adapter/base.py +182 -0
  4. dam/adapter/dataset/__init__.py +3 -0
  5. dam/adapter/dataset/source.py +279 -0
  6. dam/adapter/isaac/__init__.py +21 -0
  7. dam/adapter/isaac/filter.py +143 -0
  8. dam/adapter/lerobot/__init__.py +4 -0
  9. dam/adapter/lerobot/adapter.py +611 -0
  10. dam/adapter/lerobot/builder.py +411 -0
  11. dam/adapter/lerobot/policy.py +254 -0
  12. dam/adapter/opencv/source.py +253 -0
  13. dam/adapter/ros2/__init__.py +0 -0
  14. dam/adapter/ros2/_noop_policy.py +41 -0
  15. dam/adapter/ros2/sink.py +204 -0
  16. dam/adapter/ros2/source.py +287 -0
  17. dam/adapter/transforms.py +50 -0
  18. dam/api.py +482 -0
  19. dam/boundary/__init__.py +16 -0
  20. dam/boundary/builtin.py +45 -0
  21. dam/boundary/builtin_callbacks.py +55 -0
  22. dam/boundary/callbacks/__init__.py +70 -0
  23. dam/boundary/callbacks/_helpers.py +123 -0
  24. dam/boundary/callbacks/_registry.py +182 -0
  25. dam/boundary/callbacks/execution.py +309 -0
  26. dam/boundary/callbacks/hardware.py +466 -0
  27. dam/boundary/callbacks/kinematics.py +1002 -0
  28. dam/boundary/callbacks/ood.py +231 -0
  29. dam/boundary/constraint.py +23 -0
  30. dam/boundary/container.py +92 -0
  31. dam/boundary/graph.py +86 -0
  32. dam/boundary/list_container.py +66 -0
  33. dam/boundary/node.py +14 -0
  34. dam/boundary/single.py +52 -0
  35. dam/boundary/templates.py +75 -0
  36. dam/bus/__init__.py +148 -0
  37. dam/camera/__init__.py +3 -0
  38. dam/camera/frame_hub.py +171 -0
  39. dam/cli.py +704 -0
  40. dam/config/__init__.py +4 -0
  41. dam/config/hot_reload.py +164 -0
  42. dam/config/loader.py +27 -0
  43. dam/config/schema.py +304 -0
  44. dam/decorators.py +93 -0
  45. dam/experiments/__init__.py +15 -0
  46. dam/experiments/registry.py +576 -0
  47. dam/guard/__init__.py +5 -0
  48. dam/guard/aggregator.py +50 -0
  49. dam/guard/aggregators/__init__.py +16 -0
  50. dam/guard/aggregators/motion_qp.py +253 -0
  51. dam/guard/base.py +132 -0
  52. dam/guard/builtin/__init__.py +16 -0
  53. dam/guard/builtin/execution.py +152 -0
  54. dam/guard/builtin/hardware.py +188 -0
  55. dam/guard/builtin/motion.py +72 -0
  56. dam/guard/builtin/ood.py +1151 -0
  57. dam/guard/layer.py +33 -0
  58. dam/guard/lerobot_video_loader.py +173 -0
  59. dam/guard/ood_backend.py +423 -0
  60. dam/guard/ood_context.py +358 -0
  61. dam/guard/pipeline.py +424 -0
  62. dam/guard/stage.py +57 -0
  63. dam/guard/vision_feature_extractor.py +159 -0
  64. dam/injection/__init__.py +11 -0
  65. dam/injection/pool.py +28 -0
  66. dam/injection/resolver.py +23 -0
  67. dam/injection/static.py +30 -0
  68. dam/kinematics/resolver.py +181 -0
  69. dam/logging/__init__.py +5 -0
  70. dam/logging/console.py +45 -0
  71. dam/logging/cycle_record.py +112 -0
  72. dam/logging/loopback_writer.py +971 -0
  73. dam/preset/__init__.py +27 -0
  74. dam/preset/registry.py +232 -0
  75. dam/processor.py +300 -0
  76. dam/py.typed +0 -0
  77. dam/registry/__init__.py +3 -0
  78. dam/registry/callback.py +49 -0
  79. dam/registry/guard.py +57 -0
  80. dam/runner/__init__.py +3 -0
  81. dam/runner/base.py +469 -0
  82. dam/runner/lerobot.py +139 -0
  83. dam/runner/ros2.py +158 -0
  84. dam/runtime/__init__.py +3 -0
  85. dam/runtime/_context_state_machine.py +221 -0
  86. dam/runtime/_cycle_telemetry.py +284 -0
  87. dam/runtime/_hot_reload.py +160 -0
  88. dam/runtime/_stackfile_builder.py +304 -0
  89. dam/runtime/builtin_contexts.py +415 -0
  90. dam/runtime/context.py +216 -0
  91. dam/runtime/execution_engine.py +759 -0
  92. dam/runtime/factory.py +667 -0
  93. dam/runtime/failure_classify.py +67 -0
  94. dam/runtime/guard_runtime.py +1290 -0
  95. dam/runtime/merge_policy.py +122 -0
  96. dam/runtime/qp_solver.py +355 -0
  97. dam/runtime/slow_lane.py +177 -0
  98. dam/services/__init__.py +32 -0
  99. dam/services/api.py +156 -0
  100. dam/services/boundary_config.py +137 -0
  101. dam/services/mcap_sessions.py +1017 -0
  102. dam/services/ood_trainer.py +172 -0
  103. dam/services/replay.py +479 -0
  104. dam/services/risk_log.py +314 -0
  105. dam/services/routers/__init__.py +25 -0
  106. dam/services/routers/boundaries.py +100 -0
  107. dam/services/routers/control.py +166 -0
  108. dam/services/routers/experiments.py +74 -0
  109. dam/services/routers/mcap.py +225 -0
  110. dam/services/routers/ood.py +333 -0
  111. dam/services/routers/replay.py +125 -0
  112. dam/services/routers/risk_log.py +242 -0
  113. dam/services/routers/stackfiles.py +107 -0
  114. dam/services/routers/system.py +200 -0
  115. dam/services/routers/telemetry.py +146 -0
  116. dam/services/runtime_control.py +591 -0
  117. dam/services/serialization.py +113 -0
  118. dam/services/service_container.py +27 -0
  119. dam/services/telemetry.py +284 -0
  120. dam/services/ui/live_test.html +134 -0
  121. dam/testing/__init__.py +17 -0
  122. dam/testing/dataset_source.py +5 -0
  123. dam/testing/helpers.py +40 -0
  124. dam/testing/mocks.py +47 -0
  125. dam/testing/pipeline.py +31 -0
  126. dam/testing/safety.py +37 -0
  127. dam/testing/sim_adapters.py +83 -0
  128. dam/testing/test_loopback_writer.py +409 -0
  129. dam/types/__init__.py +18 -0
  130. dam/types/action.py +148 -0
  131. dam/types/dynamics.py +219 -0
  132. dam/types/enforcement.py +17 -0
  133. dam/types/joint_layout.py +304 -0
  134. dam/types/observation.py +130 -0
  135. dam/types/result.py +81 -0
  136. dam/types/risk.py +36 -0
  137. robot_dam-0.6.0.dist-info/METADATA +261 -0
  138. robot_dam-0.6.0.dist-info/RECORD +141 -0
  139. robot_dam-0.6.0.dist-info/WHEEL +4 -0
  140. robot_dam-0.6.0.dist-info/entry_points.txt +2 -0
  141. 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}")
@@ -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,3 @@
1
+ from dam.adapter.dataset.source import DatasetReplayPolicy, DatasetSimSource
2
+
3
+ __all__ = ["DatasetReplayPolicy", "DatasetSimSource"]
@@ -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"]