simulo-interfaces 0.22.0__tar.gz → 0.24.0__tar.gz

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 (66) hide show
  1. {simulo_interfaces-0.22.0/src/simulo_interfaces.egg-info → simulo_interfaces-0.24.0}/PKG-INFO +1 -1
  2. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/pyproject.toml +1 -1
  3. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/authoring/__init__.py +16 -10
  4. simulo_interfaces-0.24.0/src/simulo/interfaces/authoring/policy_inputs.py +207 -0
  5. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/authoring/prop.py +12 -6
  6. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/authoring/record.py +35 -2
  7. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/authoring/robot.py +96 -28
  8. simulo_interfaces-0.24.0/src/simulo/interfaces/authoring/robot_config.py +82 -0
  9. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/authoring/scene.py +6 -1
  10. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/authoring/sensor.py +9 -1
  11. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/authoring/terrain.py +2 -4
  12. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/platform/__init__.py +91 -0
  13. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/platform/asset_catalog.py +2 -2
  14. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/platform/enums.py +2 -2
  15. simulo_interfaces-0.24.0/src/simulo/interfaces/platform/evaluation.py +457 -0
  16. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/platform/manifest.py +7 -6
  17. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/platform/onnx_bundle.py +192 -45
  18. simulo_interfaces-0.24.0/src/simulo/interfaces/platform/policy_checkpoint.py +89 -0
  19. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/platform/runs.py +20 -0
  20. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/platform/submit.py +13 -0
  21. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/platform/systems.py +6 -7
  22. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/runtime/__init__.py +2 -0
  23. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/runtime/components.py +3 -0
  24. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/runtime/env.py +12 -0
  25. simulo_interfaces-0.24.0/src/simulo/interfaces/runtime/policy_frames.py +42 -0
  26. simulo_interfaces-0.24.0/src/simulo/interfaces/runtime/task.py +141 -0
  27. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/runtime/trainer.py +4 -12
  28. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0/src/simulo_interfaces.egg-info}/PKG-INFO +1 -1
  29. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo_interfaces.egg-info/SOURCES.txt +5 -0
  30. simulo_interfaces-0.22.0/src/simulo/interfaces/runtime/task.py +0 -83
  31. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/MANIFEST.in +0 -0
  32. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/PYPI.md +0 -0
  33. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/setup.cfg +0 -0
  34. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/__init__.py +0 -0
  35. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/authoring/actuator.py +0 -0
  36. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/authoring/asset.py +0 -0
  37. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/authoring/checkpoint.py +0 -0
  38. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/authoring/entity.py +0 -0
  39. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/authoring/light.py +0 -0
  40. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/authoring/materials.py +0 -0
  41. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/authoring/physics.py +0 -0
  42. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/authoring/pose.py +0 -0
  43. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/authoring/primitives.py +0 -0
  44. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/authoring/visual.py +0 -0
  45. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/authoring/world.py +0 -0
  46. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/exceptions.py +0 -0
  47. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/ids.py +0 -0
  48. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/platform/_payload.py +0 -0
  49. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/platform/app.py +0 -0
  50. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/platform/asset.py +0 -0
  51. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/platform/callbacks.py +0 -0
  52. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/platform/debug.py +0 -0
  53. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/platform/domain.py +0 -0
  54. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/platform/job_events.py +0 -0
  55. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/platform/outputs.py +0 -0
  56. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/platform/preview.py +0 -0
  57. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/platform/runtime.py +0 -0
  58. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/platform/volume.py +0 -0
  59. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/py.typed +0 -0
  60. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/runtime/anomaly.py +0 -0
  61. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/runtime/player.py +0 -0
  62. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/runtime/policy.py +0 -0
  63. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/runtime/tensors.py +0 -0
  64. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo_interfaces.egg-info/dependency_links.txt +0 -0
  65. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo_interfaces.egg-info/requires.txt +0 -0
  66. {simulo_interfaces-0.22.0 → simulo_interfaces-0.24.0}/src/simulo_interfaces.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simulo-interfaces
3
- Version: 0.22.0
3
+ Version: 0.24.0
4
4
  Summary: Typed Python contracts for the Simulo cloud robotics SDK and managed simulation runtime.
5
5
  Author-email: Simulo Team <team@simulo.ai>
6
6
  License: BSD-3-Clause
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "simulo-interfaces"
7
- version = "0.22.0"
7
+ version = "0.24.0"
8
8
  description = "Typed Python contracts for the Simulo cloud robotics SDK and managed simulation runtime."
9
9
  readme = "PYPI.md"
10
10
  requires-python = ">=3.11"
@@ -1,13 +1,10 @@
1
1
  """Part C — the typed authoring surface.
2
2
 
3
- The **relocation and mirror destination** for the Simulo authoring value types.
4
- Each type here has exactly ONE definition — this one — shared by both
5
- distributions: the thin client authors these values at submit (torch-free,
6
- e.g. ``simulo.Pose(xyz=(1, 2, 3))``) and the backend executes them on the
7
- worker. There is no other copy and no re-export shim anywhere ("nuke as you
8
- go", per the owner's 2026-07-26 directive recorded in the plan): the former
9
- backend ``core`` modules for these types are deleted, so ``isinstance`` keeps
10
- working because only this definition exists.
3
+ The relocation and mirror destination for shared authoring value types.
4
+ Each value type has one definition here, used by the torch-free client when
5
+ submitting a job and by the backend when executing it. Robot configuration
6
+ records retain their existing backend import paths as re-exports of these
7
+ same objects.
11
8
 
12
9
  Relocated so far — Wave C1: ``pose`` (``Pose``), ``materials`` (``Material``,
13
10
  ``SurfaceMaterial``), ``physics`` (``Physics``, ``Collision``, ``RigidPhysics``,
@@ -53,8 +50,9 @@ clearly in a submit-only installation.
53
50
  Two relocation mechanisms select which types land here and how (see the derived
54
51
  plan for the full worked example and rejected alternatives):
55
52
 
56
- * **RELOCATE** — the class definition MOVES here; the old ``core/<module>.py``
57
- is deleted and every call site rewritten in the same PR (no re-export shims).
53
+ * **RELOCATE** — the class definition MOVES here. Dependency-bearing modules
54
+ that also exposed a shared value type can re-export its one definition for
55
+ existing callers; dependency-free modules move with their call sites.
58
56
  Reserved for types with zero import-time *or* call-time dependency on
59
57
  anything that cannot live in a zero-dependency package.
60
58
  * **MIRROR** — a ``typing.Protocol`` (named ``*Protocol``, matching the
@@ -99,6 +97,7 @@ from simulo.interfaces.authoring.physics import (
99
97
  Physics,
100
98
  RigidPhysics,
101
99
  )
100
+ from simulo.interfaces.authoring.policy_inputs import PolicyInput, PolicyInputSchema
102
101
  from simulo.interfaces.authoring.pose import Pose
103
102
  from simulo.interfaces.authoring.primitives import (
104
103
  Cone,
@@ -111,6 +110,7 @@ from simulo.interfaces.authoring.primitives import (
111
110
  from simulo.interfaces.authoring.prop import PropProtocol, PropStateProtocol
112
111
  from simulo.interfaces.authoring.record import RecordConfig
113
112
  from simulo.interfaces.authoring.robot import RobotProtocol, RobotStateProtocol
113
+ from simulo.interfaces.authoring.robot_config import ActuatorGainsConfig, ArticulationConfig, JointConfig
114
114
  from simulo.interfaces.authoring.scene import SceneGroupProtocol, SceneProtocol
115
115
  from simulo.interfaces.authoring.sensor import CameraProtocol
116
116
  from simulo.interfaces.authoring.terrain import (
@@ -175,6 +175,8 @@ __all__ = [
175
175
  "RecordConfig",
176
176
  # injected policy input (dependency-free; loading is deferred to execution)
177
177
  "PolicyCheckpoint",
178
+ "PolicyInput",
179
+ "PolicyInputSchema",
178
180
  # visual
179
181
  "VisualMarkerBase",
180
182
  "AxesMarker",
@@ -185,6 +187,10 @@ __all__ = [
185
187
  # scene (MIRROR — the concrete classes live in the backend package)
186
188
  "SceneProtocol",
187
189
  "SceneGroupProtocol",
190
+ # robot configuration (dependency-free owning definitions)
191
+ "ActuatorGainsConfig",
192
+ "ArticulationConfig",
193
+ "JointConfig",
188
194
  # robot / prop / sensor / actuator (MIRROR — the concrete classes live in
189
195
  # the backend package and explicitly inherit these Protocols)
190
196
  "RobotProtocol",
@@ -0,0 +1,207 @@
1
+ """Dependency-free declarations for raw actor and critic inputs.
2
+
3
+ Images must be batched RGB uint8 NHWC. The contract requires future trainer
4
+ and export consumers to convert the current minibatch to NCHW float and divide
5
+ by 255; ``center`` also subtracts 0.5. These transforms belong in the actor's
6
+ graph. This module checks declarations and raw values, not graph behavior.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import hashlib
12
+ import json
13
+ import re
14
+ from collections.abc import Mapping
15
+ from dataclasses import asdict, dataclass
16
+ from typing import Any, Literal
17
+
18
+
19
+ @dataclass(frozen=True, slots=True, kw_only=True)
20
+ class PolicyInput:
21
+ """One named input, with ``shape`` excluding the environment batch.
22
+
23
+ Names match ``[A-Za-z][A-Za-z0-9_]*``; shape dimensions are positive.
24
+ Tuple and list shapes are accepted and stored as tuples. Defaults are
25
+ ``dtype="float32"``, ``kind="numeric"`` and ``preprocessing="none"``.
26
+ Numeric inputs require float32, no camera and no preprocessing. Image
27
+ inputs require uint8, shape ``(H, W, 3)`` and a nonblank camera scene name:
28
+ the last segment of ``scene.add(camera, at="/World/<name>")``. Full paths
29
+ are rejected. LearningEnv setup requires that camera's purpose to be
30
+ ``"policy"``. Two policy cameras cannot share a scene name.
31
+ """
32
+
33
+ name: str
34
+ shape: tuple[int, ...]
35
+ dtype: str = "float32"
36
+ kind: Literal["numeric", "image"] = "numeric"
37
+ camera: str | None = None
38
+ preprocessing: Literal["none", "center"] = "none"
39
+
40
+ def __post_init__(self) -> None:
41
+ label = f"Input {self.name!r}"
42
+ if isinstance(self.shape, list):
43
+ object.__setattr__(self, "shape", tuple(self.shape))
44
+ if not isinstance(self.name, str) or not re.fullmatch(r"[A-Za-z][A-Za-z0-9_]*", self.name):
45
+ raise ValueError(f"{label}: name must start with a letter and contain only letters, digits or underscores.")
46
+ if (
47
+ not isinstance(self.shape, tuple)
48
+ or not self.shape
49
+ or any(type(dim) is not int or dim <= 0 for dim in self.shape)
50
+ ):
51
+ raise ValueError(f"{label}: shape must be a nonempty tuple of positive dimensions, excluding batch.")
52
+ if self.kind not in ("numeric", "image"):
53
+ raise ValueError(f"{label}: kind must be 'numeric' or 'image'.")
54
+ if self.preprocessing not in ("none", "center"):
55
+ raise ValueError(f"{label}: preprocessing must be 'none' or 'center'.")
56
+ if self.kind == "image":
57
+ if self.dtype != "uint8":
58
+ raise ValueError(f"{label}: image dtype must be uint8 RGB NHWC, got {self.dtype!r}.")
59
+ if len(self.shape) != 3 or self.shape[-1] != 3:
60
+ raise ValueError(f"{label}: image shape must be (H, W, 3) RGB NHWC, excluding batch, got {self.shape}.")
61
+ if not isinstance(self.camera, str) or not self.camera.strip():
62
+ raise ValueError(f"{label}: image inputs require a camera identity.")
63
+ if "/" in self.camera or self.camera != self.camera.strip():
64
+ raise ValueError(f"{label}: camera must be a scene name, the last segment of scene.add(..., at=...).")
65
+ elif self.dtype != "float32" or self.camera is not None or self.preprocessing != "none":
66
+ raise ValueError(f"{label}: numeric inputs require float32, no camera, and preprocessing='none'.")
67
+
68
+ def to_dict(self) -> dict[str, Any]:
69
+ """Return JSON metadata, converting the stored tuple shape to a list."""
70
+ return {**asdict(self), "shape": list(self.shape)}
71
+
72
+ @classmethod
73
+ def from_dict(cls, value: Mapping[str, Any]) -> PolicyInput:
74
+ """Parse JSON, converting list shapes to tuples; reject unknown fields."""
75
+ if not isinstance(value, Mapping):
76
+ raise ValueError("Input declaration must be an object.")
77
+ allowed = {"name", "shape", "dtype", "kind", "camera", "preprocessing"}
78
+ if set(value) - allowed or not {"name", "shape"} <= set(value):
79
+ raise ValueError(f"Input {value.get('name')!r}: declaration has missing or unknown fields.")
80
+ shape = value["shape"]
81
+ if not isinstance(shape, (list, tuple)):
82
+ raise ValueError(f"Input {value['name']!r}: shape must be an array of positive dimensions.")
83
+ return cls(**{**value, "shape": tuple(shape)})
84
+
85
+
86
+ @dataclass(frozen=True, slots=True, kw_only=True)
87
+ class PolicyInputSchema:
88
+ """Ordered actor inputs and optional separate training-only critic inputs.
89
+
90
+ ``critic=None`` means the critic uses exactly the actor declarations.
91
+ Future export and playback consumers must use only ``actor``. A critic
92
+ declaration sharing an actor name must be identical. Actor inputs omitted
93
+ from an explicit critic are not supplied to that critic. Order is part of
94
+ checkpoint compatibility, including numeric concatenation order.
95
+ """
96
+
97
+ actor: tuple[PolicyInput, ...]
98
+ critic: tuple[PolicyInput, ...] | None = None
99
+
100
+ def __post_init__(self) -> None:
101
+ for role, inputs in (("actor", self.actor), ("critic", self.critic_inputs)):
102
+ if not isinstance(inputs, tuple) or not inputs:
103
+ raise ValueError(f"{role} inputs must be a nonempty tuple of PolicyInput declarations.")
104
+ seen: set[str] = set()
105
+ for spec in inputs:
106
+ if not isinstance(spec, PolicyInput):
107
+ raise ValueError(f"{role} inputs must contain PolicyInput declarations.")
108
+ if spec.name in seen:
109
+ raise ValueError(f"{role} input {spec.name!r}: duplicate input name.")
110
+ seen.add(spec.name)
111
+ actor = {spec.name: spec for spec in self.actor}
112
+ for spec in self.critic_inputs:
113
+ if spec.name in actor and spec != actor[spec.name]:
114
+ raise ValueError(
115
+ f"critic input {spec.name!r}: declaration contradicts the actor input of the same name."
116
+ )
117
+
118
+ @property
119
+ def critic_inputs(self) -> tuple[PolicyInput, ...]:
120
+ """Effective critic declarations, defaulting to actor inputs."""
121
+ return self.actor if self.critic is None else self.critic
122
+
123
+ def to_dict(self) -> dict[str, Any]:
124
+ """Return the actor/critic schema for a checkpoint sidecar."""
125
+ return {
126
+ "actor": [spec.to_dict() for spec in self.actor],
127
+ "critic": None if self.critic is None else [spec.to_dict() for spec in self.critic],
128
+ }
129
+
130
+ @classmethod
131
+ def from_dict(cls, value: Mapping[str, Any]) -> PolicyInputSchema:
132
+ """Parse a schema without permitting undeclared metadata."""
133
+ if not isinstance(value, Mapping) or set(value) != {"actor", "critic"}:
134
+ raise ValueError("Policy input schema requires exactly actor and critic fields.")
135
+ actor, critic = value["actor"], value["critic"]
136
+ if not isinstance(actor, (list, tuple)) or (critic is not None and not isinstance(critic, (list, tuple))):
137
+ raise ValueError("Policy input schema actor and critic must be arrays (critic may be null).")
138
+ return cls(
139
+ actor=tuple(PolicyInput.from_dict(spec) for spec in actor),
140
+ critic=None if critic is None else tuple(PolicyInput.from_dict(spec) for spec in critic),
141
+ )
142
+
143
+ @property
144
+ def fingerprint(self) -> str:
145
+ """SHA-256 of ordered inputs, including camera and preprocessing.
146
+
147
+ An omitted critic and an explicitly identical critic are equivalent.
148
+ Batch size is absent, so a checkpoint can change parallelism.
149
+ """
150
+
151
+ def canonical(spec: PolicyInput) -> dict[str, Any]:
152
+ # Pin v1's subset: future defaulted fields do not invalidate hashes.
153
+ return {name: getattr(spec, name) for name in ("name", "shape", "dtype", "kind", "camera", "preprocessing")}
154
+
155
+ metadata = {
156
+ "version": "simulo.policy-input-fingerprint.v1",
157
+ "actor": [canonical(spec) for spec in self.actor],
158
+ "critic": [canonical(spec) for spec in self.critic_inputs],
159
+ }
160
+ return hashlib.sha256(json.dumps(metadata, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
161
+
162
+ def validate_values(
163
+ self,
164
+ actor: Any,
165
+ critic: Any,
166
+ *,
167
+ batch_size: int | None = None,
168
+ ) -> None:
169
+ """Check raw names, shapes, dtypes and batch size at LearningEnv._init_spaces.
170
+
171
+ Values need only ``shape`` and ``dtype`` attributes. No engine or tensor
172
+ package is imported. One declared numeric input may use the legacy
173
+ tensor return; multiple or image inputs require a named mapping. Values
174
+ must have a leading num_envs batch. Failures raise ValueError naming the
175
+ role and input. Legacy tasks with no schema are not checked by setup.
176
+ """
177
+ expected_batch = batch_size
178
+ for role, specs, values in (("actor", self.actor, actor), ("critic", self.critic_inputs, critic)):
179
+ if not isinstance(values, Mapping):
180
+ if len(specs) != 1 or specs[0].kind != "numeric":
181
+ raise ValueError(f"{role} input {specs[0].name!r}: named inputs require a mapping.")
182
+ values = {specs[0].name: values}
183
+ declared = {spec.name for spec in specs}
184
+ for name in values:
185
+ if name not in declared:
186
+ raise ValueError(f"{role} input {name!r}: unknown input name.")
187
+ for spec in specs:
188
+ if spec.name not in values:
189
+ raise ValueError(f"{role} input {spec.name!r}: missing input.")
190
+ value = values[spec.name]
191
+ shape = tuple(getattr(value, "shape", ()))
192
+ if (
193
+ len(shape) != len(spec.shape) + 1
194
+ or shape[1:] != spec.shape
195
+ or type(shape[0]) is not int
196
+ or shape[0] <= 0
197
+ ):
198
+ raise ValueError(
199
+ f"{role} input {spec.name!r}: expected batched shape (N, {spec.shape}), got {shape}."
200
+ )
201
+ if expected_batch is None:
202
+ expected_batch = shape[0]
203
+ if shape[0] != expected_batch:
204
+ raise ValueError(f"{role} input {spec.name!r}: expected batch {expected_batch}, got {shape[0]}.")
205
+ dtype = str(getattr(value, "dtype", "")).removeprefix("torch.")
206
+ if dtype != spec.dtype:
207
+ raise ValueError(f"{role} input {spec.name!r}: expected dtype {spec.dtype}, got {dtype!r}.")
@@ -154,15 +154,21 @@ class PropProtocol(Protocol):
154
154
 
155
155
  @property
156
156
  def default_pose(self) -> TensorLike:
157
- """The prop's spawn pose in the **world** frame, ``(num_envs, 7)``.
157
+ """The prop's spawn pose in the **world** frame.
158
158
 
159
- The pose the prop was authored at, already offset by each
160
- environment's own origin — so it is directly comparable with
161
- ``state.pose`` and directly usable as the base for a randomised
162
- reset. Index it with the environments being reset
163
- (``prop.default_pose[env_ids]``), perturb, and write it back with
159
+ For a prop added with ``per_environment=True`` (the default), the pose
160
+ has shape ``(num_envs, 7)`` and includes each environment's origin, so
161
+ it is directly comparable with ``state.pose`` and directly usable as
162
+ the base for a randomised reset. Index it with the environments being
163
+ reset (``prop.default_pose[env_ids]``), perturb, and write it back with
164
164
  :meth:`set_pose`.
165
165
 
166
+ For a prop added with ``per_environment=False``, this is one row
167
+ containing its authored world pose with no environment origin. A
168
+ shared prop is supported only when the scene has one environment;
169
+ scene building refuses ``per_environment=False`` for a prop when
170
+ ``num_envs > 1``.
171
+
166
172
  Raises ``RuntimeError`` until the simulation runtime has attached the
167
173
  prop.
168
174
  """
@@ -44,10 +44,13 @@ fingerprint that overrides the auto-collected one when present).
44
44
 
45
45
  from __future__ import annotations
46
46
 
47
+ import math
47
48
  import os
49
+ import sys
48
50
  from dataclasses import dataclass, field
51
+ from numbers import Real
49
52
  from pathlib import Path
50
- from typing import Any, Dict, Literal, Mapping, Optional, Sequence, Tuple, Union
53
+ from typing import Any, Dict, Literal, Mapping, Optional, Sequence, SupportsFloat, Tuple, Union
51
54
 
52
55
  # All include_* field names this config exposes. Centralizing avoids drift
53
56
  # between the dataclass declaration, _PROFILE_DEFAULTS, and resolve_includes.
@@ -192,6 +195,11 @@ class RecordConfig:
192
195
  profile precedence can distinguish "user passed False" from "default
193
196
  None". Do not edit defaults to plain ``bool`` — the silent-default
194
197
  mitigation depends on the tri-state.
198
+
199
+ ``record_all_envs`` expands numeric recording data only. Policy camera
200
+ video always uses ``env_indices`` (default: environment 0), so host frame
201
+ buffers and encoder count are bounded by the selected environments and
202
+ declared cameras, independently of the total environment count.
195
203
  """
196
204
 
197
205
  output_path: Optional[Union[str, "os.PathLike[str]", Path]] = None
@@ -253,7 +261,14 @@ class RecordConfig:
253
261
  video_fallback: Literal["software", "error"] = "software"
254
262
  video_codec: Literal["h264", "hevc", "av1"] = "h264"
255
263
  video_bitrate: Optional[str] = "8M"
256
- video_fps: Optional[int] = 30
264
+ # ``None`` leaves cadence to the frame producer. The encoder then uses its
265
+ # documented 30 fps default, but the recorder omits ``video_fps`` from
266
+ # /run/start because packet timestamps, not an encoder setting, describe
267
+ # that producer-owned cadence. Callers such as evaluation filming set the
268
+ # measured cadence explicitly.
269
+ # Values must also fit a finite IEEE-754 double because both supported
270
+ # encoders and the MCAP JSON record use that representation.
271
+ video_fps: Optional[SupportsFloat] = None
257
272
  video_gop: Optional[int] = 30
258
273
  video_storage: Literal["embedded", "sidecar"] = "embedded"
259
274
  # When True (default), the recorder writes a parallel
@@ -300,6 +315,24 @@ class RecordConfig:
300
315
  raise ValueError(f"RecordConfig.profile must be one of {VALID_PROFILES}; got {self.profile!r}")
301
316
  if self.record_every_n_steps < 1:
302
317
  raise ValueError(f"RecordConfig.record_every_n_steps must be >= 1; got {self.record_every_n_steps}")
318
+ try:
319
+ video_fps = None if self.video_fps is None else float(self.video_fps)
320
+ except (OverflowError, TypeError, ValueError):
321
+ video_fps = None
322
+ if self.video_fps is not None and (
323
+ isinstance(self.video_fps, bool)
324
+ or not isinstance(self.video_fps, Real)
325
+ or video_fps is None
326
+ or not math.isfinite(video_fps)
327
+ # 0.1 fps is a policy floor that keeps rates sensible; ffmpeg's
328
+ # rational conversion only rounds to zero far below it.
329
+ or video_fps < 0.1
330
+ or video_fps > sys.float_info.max
331
+ ):
332
+ raise ValueError(
333
+ "RecordConfig.video_fps must be a finite real number representable by the encoder and at least 0.1 "
334
+ f"when set; got {self.video_fps!r}"
335
+ )
303
336
  # env_indices: accept any sequence of ints, including a torch tensor —
304
337
  # but normalize to a tuple of ints for hashability + reproducibility.
305
338
  try:
@@ -7,7 +7,7 @@ and reset paths all reach through a live engine handle), so its
7
7
  *implementation* cannot live in a zero-dependency package. What CAN live here
8
8
  is its declared public surface: this Protocol names exactly what authoring
9
9
  code may rely on when it holds a robot — construction-time configuration,
10
- joint/body queries, command targets, state reads, and reset.
10
+ joint/body queries, command targets, default state, live state reads, and reset.
11
11
 
12
12
  The backend's concrete ``Robot`` class explicitly inherits this Protocol
13
13
  (nominal conformance). Enforcement is the standard four layers for a mirror —
@@ -23,15 +23,16 @@ The Protocol stays structural for any other implementer.
23
23
 
24
24
  ``state`` is DECLARED, as ``RobotStateProtocol`` — the grouped live-state
25
25
  contract. It is the supported, typed way to read a robot's simulation state
26
- (``robot.state.joint_positions``, ``robot.state.pose``, …): six read-only,
26
+ (``robot.state.joint_positions``, ``robot.state.pose``, …): eight read-only,
27
27
  batched members whose names are plain English rather than engine vocabulary.
28
- The members are LIVE VIEWS — each read goes straight through to the
29
- simulation's own buffers, at the same cost as reaching through the
30
- ``internals`` escape hatch, with no copy and no per-access container
31
- allocation — so reading them inside a training loop is free of hidden
32
- overhead. Unlike ``internals``, these six names ARE a compatibility promise:
33
- they keep working across engine upgrades, because the backend maps each one
34
- to whatever the engine calls it.
28
+ Six members are zero-copy live views of the simulation's buffers. The two
29
+ base-frame velocity members are different: the engine rotates the current
30
+ world-frame velocities into the base frame on every read, returning a new
31
+ tensor. That small computation and allocation are the cost of those two
32
+ accesses, and writing into either result does not reach the simulation.
33
+ Unlike ``internals``, all eight names ARE a compatibility promise: they keep
34
+ working across engine upgrades, because the backend maps each one to whatever
35
+ the engine calls it.
35
36
 
36
37
  Deliberate scoping notes, so they are not mistaken for drift (each is an
37
38
  entry in the backend conformance test's documented-omissions allowlist):
@@ -63,32 +64,37 @@ from typing import Any, Dict, List, Optional, Protocol, Tuple, runtime_checkable
63
64
 
64
65
  from simulo.interfaces.authoring.asset import AssetSource
65
66
  from simulo.interfaces.authoring.pose import Pose
67
+ from simulo.interfaces.authoring.robot_config import ArticulationConfig, JointConfig
66
68
  from simulo.interfaces.runtime.tensors import TensorLike
67
69
 
68
70
 
69
71
  @runtime_checkable
70
72
  class RobotStateProtocol(Protocol):
71
- """The live robot-state contract — batched, read-only, zero-copy.
73
+ """The live robot-state contract — batched and read-only.
72
74
 
73
75
  Obtained from a robot as ``robot.state``; never constructed directly.
74
- Each member is a live view of the simulation's own state buffers: reading
75
- one returns the current values without copying, and the same access works
76
- identically inside a training loop's hot path. All members carry the
77
- environment batch as their leading dimension (the scene's ``num_envs``).
76
+ Six members (``pose``, the two world-frame velocities, and the three
77
+ ``joint_*`` members) are zero-copy live views of the simulation's own
78
+ buffers. ``linear_velocity_in_base_frame`` and
79
+ ``angular_velocity_in_base_frame`` are computed from the current base
80
+ orientation on every read. Each of those reads returns a new tensor,
81
+ costs a small computation, and cannot modify the simulation when written
82
+ into. All members carry the environment batch as their leading dimension
83
+ (the scene's ``num_envs``).
78
84
 
79
85
  **Read a member each time you need it — do not hold the returned tensor
80
- across steps, and do not write into it.** The tensor IS the simulation's
81
- own memory: the engine refreshes most of these buffers by rebinding them
82
- to new tensors, so a tensor held across a step can silently serve stale
83
- values, and writing into one writes into the simulation. The accessor
84
- (``robot.state`` itself) may be held freely; the tensors its members
85
- return should be re-read at each use.
86
+ across steps, and do not write into it.** The six live-view tensors share
87
+ simulation memory, and writing into one writes into the simulation. The
88
+ engine can also rebind those buffers, while the base-frame members are
89
+ recomputed, so a tensor held across a step can silently serve stale values.
90
+ The accessor (``robot.state`` itself) may be held freely; the tensors its
91
+ members return should be re-read at each use.
86
92
 
87
93
  Members raise ``RuntimeError`` until the simulation runtime has attached
88
94
  the robot — read them from ``on_start`` onward. The ``state`` accessor
89
95
  itself is always available and is the same object on every access.
90
96
 
91
- These six names are a compatibility promise: they keep working across
97
+ These eight names are a compatibility promise: they keep working across
92
98
  engine upgrades. For anything not named here, the ``internals`` escape
93
99
  hatch remains available — explicitly unstable, at your own risk.
94
100
  """
@@ -111,6 +117,24 @@ class RobotStateProtocol(Protocol):
111
117
  ``[wx, wy, wz]``."""
112
118
  ...
113
119
 
120
+ @property
121
+ def linear_velocity_in_base_frame(self) -> TensorLike:
122
+ """Base linear velocity expressed in the base frame, shape
123
+ ``(num_envs, 3)`` in m/s with columns ``[vx, vy, vz]``. Available
124
+ from ``on_start`` onward. Computed from the current base orientation
125
+ on every read; each read returns a new tensor, costs a small
126
+ computation, and writing into it does not reach the simulation."""
127
+ ...
128
+
129
+ @property
130
+ def angular_velocity_in_base_frame(self) -> TensorLike:
131
+ """Base angular velocity expressed in the base frame, shape
132
+ ``(num_envs, 3)`` in rad/s with columns ``[wx, wy, wz]``. Available
133
+ from ``on_start`` onward. Computed from the current base orientation
134
+ on every read; each read returns a new tensor, costs a small
135
+ computation, and writing into it does not reach the simulation."""
136
+ ...
137
+
114
138
  @property
115
139
  def joint_positions(self) -> TensorLike:
116
140
  """Joint positions (rad or m), shape ``(num_envs, num_joints)``."""
@@ -146,7 +170,7 @@ class RobotProtocol(Protocol):
146
170
  """Model source (USD or URDF) defining the robot."""
147
171
 
148
172
  initial_pose: Pose
149
- """Pose the robot spawns at, in the world frame (identity when not given)."""
173
+ """Pose the robot spawns at (identity when not given). For a robot copied into each environment, the position is measured from that environment's origin, and default_pose reports it with the origin added. For some catalog robots, such as the cartpole, the identity pose keeps the catalog model's own spawn height; any other pose replaces it. A robot added under a Transform spawns at the Transform's position."""
150
174
 
151
175
  collision_enabled: bool
152
176
  """Whether collision detection is enabled."""
@@ -160,17 +184,21 @@ class RobotProtocol(Protocol):
160
184
  scale: Tuple[float, float, float]
161
185
  """Per-axis spawn scale factor."""
162
186
 
163
- articulation: Any
187
+ articulation: ArticulationConfig
164
188
  """Articulation physics and control configuration."""
165
189
 
166
- joint_config: Optional[List[Any]]
190
+ joint_config: Optional[List[JointConfig]]
167
191
  """Optional per-joint configurations."""
168
192
 
169
193
  disable_gravity: bool
170
194
  """If ``True``, gravity is disabled for this robot."""
171
195
 
172
196
  actuator_gains: Optional[Dict[str, Any]]
173
- """Optional mapping of actuator-group names to gain configurations."""
197
+ """Optional mapping of actuator-group names to gain configurations.
198
+
199
+ Catalog robots support only the ``"all_joints"`` key; other keys are ignored
200
+ with a warning.
201
+ """
174
202
 
175
203
  # -- Identity and attachment ------------------------------------------
176
204
 
@@ -203,9 +231,49 @@ class RobotProtocol(Protocol):
203
231
  """The grouped live robot state — the supported, typed state surface.
204
232
 
205
233
  Always available (the accessor is the same object on every access);
206
- its members read straight through to the simulation's state buffers
207
- and raise ``RuntimeError`` until the simulation runtime has attached
208
- the robot — read them from ``on_start`` onward.
234
+ six members read straight through to the simulation's state buffers,
235
+ while the two base-frame velocity members are computed into new
236
+ tensors on every read. Members raise ``RuntimeError`` until the
237
+ simulation runtime has attached the robot — read them from
238
+ ``on_start`` onward.
239
+ """
240
+ ...
241
+
242
+ @property
243
+ def default_joint_positions(self) -> TensorLike:
244
+ """Joint positions that :meth:`reset` writes, shape
245
+ ``(num_envs, num_joints)`` in rad for hinge joints and m for slider
246
+ joints. Columns use the same order as ``state.joint_positions`` and
247
+ the indices returned by :meth:`find_joints`. Available from
248
+ ``on_start`` onward. Each read returns a new tensor, so writing into
249
+ it cannot change a later reset."""
250
+ ...
251
+
252
+ @property
253
+ def default_joint_velocities(self) -> TensorLike:
254
+ """Joint velocities that :meth:`reset` writes, shape
255
+ ``(num_envs, num_joints)`` in rad/s for hinge joints and m/s for
256
+ slider joints. Columns use the same order as
257
+ ``state.joint_positions`` and the indices returned by
258
+ :meth:`find_joints`. Available from ``on_start`` onward. Each read
259
+ returns a new tensor, so writing into it cannot change a later
260
+ reset."""
261
+ ...
262
+
263
+ @property
264
+ def default_pose(self) -> TensorLike:
265
+ """Base pose that :meth:`reset` writes in the world frame.
266
+
267
+ Columns are ``[x, y, z, qw, qx, qy, qz]``: position in m followed by a
268
+ w-first unit quaternion. For a robot added with
269
+ ``per_environment=True`` (the default), the shape is ``(num_envs, 7)``
270
+ and each environment's origin is included. For a robot added with
271
+ ``per_environment=False``, this is one row containing its authored
272
+ world pose with no environment origin. A shared robot is supported
273
+ only when the scene has one environment; scene building refuses
274
+ ``per_environment=False`` for a robot when ``num_envs > 1``. Available
275
+ from ``on_start`` onward. Each read returns a new tensor, so writing
276
+ into it cannot change a later reset.
209
277
  """
210
278
  ...
211
279