simulo-interfaces 0.23.0__tar.gz → 0.25.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 (69) hide show
  1. {simulo_interfaces-0.23.0/src/simulo_interfaces.egg-info → simulo_interfaces-0.25.0}/PKG-INFO +1 -1
  2. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/pyproject.toml +1 -1
  3. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo/interfaces/authoring/__init__.py +45 -14
  4. simulo_interfaces-0.25.0/src/simulo/interfaces/authoring/action_channel.py +22 -0
  5. simulo_interfaces-0.25.0/src/simulo/interfaces/authoring/policy_inputs.py +207 -0
  6. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo/interfaces/authoring/prop.py +30 -6
  7. simulo_interfaces-0.25.0/src/simulo/interfaces/authoring/properties.py +92 -0
  8. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo/interfaces/authoring/record.py +5 -0
  9. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo/interfaces/authoring/robot.py +21 -9
  10. simulo_interfaces-0.25.0/src/simulo/interfaces/authoring/robot_config.py +82 -0
  11. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo/interfaces/authoring/scene.py +28 -1
  12. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo/interfaces/authoring/sensor.py +9 -1
  13. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo/interfaces/authoring/terrain.py +43 -0
  14. simulo_interfaces-0.25.0/src/simulo/interfaces/authoring/variants.py +48 -0
  15. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo/interfaces/authoring/visual.py +90 -1
  16. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo/interfaces/platform/__init__.py +47 -2
  17. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo/interfaces/platform/asset_catalog.py +2 -2
  18. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo/interfaces/platform/evaluation.py +27 -4
  19. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo/interfaces/platform/manifest.py +6 -5
  20. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo/interfaces/platform/onnx_bundle.py +192 -45
  21. simulo_interfaces-0.25.0/src/simulo/interfaces/platform/policy_checkpoint.py +89 -0
  22. simulo_interfaces-0.25.0/src/simulo/interfaces/platform/preview.py +197 -0
  23. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo/interfaces/platform/runs.py +44 -8
  24. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo/interfaces/platform/submit.py +25 -4
  25. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo/interfaces/platform/systems.py +6 -7
  26. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo/interfaces/runtime/__init__.py +5 -1
  27. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo/interfaces/runtime/env.py +5 -0
  28. simulo_interfaces-0.25.0/src/simulo/interfaces/runtime/policy_frames.py +42 -0
  29. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo/interfaces/runtime/task.py +86 -2
  30. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo/interfaces/runtime/trainer.py +3 -1
  31. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0/src/simulo_interfaces.egg-info}/PKG-INFO +1 -1
  32. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo_interfaces.egg-info/SOURCES.txt +7 -0
  33. simulo_interfaces-0.23.0/src/simulo/interfaces/platform/preview.py +0 -85
  34. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/MANIFEST.in +0 -0
  35. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/PYPI.md +0 -0
  36. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/setup.cfg +0 -0
  37. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo/interfaces/__init__.py +0 -0
  38. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo/interfaces/authoring/actuator.py +0 -0
  39. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo/interfaces/authoring/asset.py +0 -0
  40. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo/interfaces/authoring/checkpoint.py +0 -0
  41. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo/interfaces/authoring/entity.py +0 -0
  42. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo/interfaces/authoring/light.py +0 -0
  43. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo/interfaces/authoring/materials.py +0 -0
  44. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo/interfaces/authoring/physics.py +0 -0
  45. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo/interfaces/authoring/pose.py +0 -0
  46. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo/interfaces/authoring/primitives.py +0 -0
  47. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo/interfaces/authoring/world.py +0 -0
  48. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo/interfaces/exceptions.py +0 -0
  49. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo/interfaces/ids.py +0 -0
  50. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo/interfaces/platform/_payload.py +0 -0
  51. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo/interfaces/platform/app.py +0 -0
  52. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo/interfaces/platform/asset.py +0 -0
  53. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo/interfaces/platform/callbacks.py +0 -0
  54. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo/interfaces/platform/debug.py +0 -0
  55. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo/interfaces/platform/domain.py +0 -0
  56. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo/interfaces/platform/enums.py +0 -0
  57. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo/interfaces/platform/job_events.py +0 -0
  58. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo/interfaces/platform/outputs.py +0 -0
  59. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo/interfaces/platform/runtime.py +0 -0
  60. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo/interfaces/platform/volume.py +0 -0
  61. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo/interfaces/py.typed +0 -0
  62. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo/interfaces/runtime/anomaly.py +0 -0
  63. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo/interfaces/runtime/components.py +0 -0
  64. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo/interfaces/runtime/player.py +0 -0
  65. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo/interfaces/runtime/policy.py +0 -0
  66. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo/interfaces/runtime/tensors.py +0 -0
  67. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo_interfaces.egg-info/dependency_links.txt +0 -0
  68. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.0}/src/simulo_interfaces.egg-info/requires.txt +0 -0
  69. {simulo_interfaces-0.23.0 → simulo_interfaces-0.25.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.23.0
3
+ Version: 0.25.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.23.0"
7
+ version = "0.25.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``,
@@ -20,7 +17,7 @@ decision — to resolve that exact name collision), and ``light`` (``Light``,
20
17
  ``RoughTerrain``), ``primitives`` (``PrimitiveShape``, ``Cone``,
21
18
  ``Cuboid``, ``Sphere``, ``Cylinder``, ``Transform``), ``entity`` (``Entity``,
22
19
  ``AssetEntity``), and ``visual`` (``Visual``, ``VisualMarkerBase``,
23
- ``AxesMarker``, ``SphereMarker``, ``ArrowMarker``, ``PointMarker``).
20
+ ``AxesMarker``, ``SphereMarker``, ``ArrowMarker``, ``PointMarker``, ``LineMarker``).
24
21
  Wave C4: ``record`` (``RecordConfig`` — the MCAP recording configuration;
25
22
  stdlib-only, so it classifies RELOCATE, and the backend recorder imports it
26
23
  from here), and ``world`` (``World``, ``USDWorld``): ``Terrain`` is the
@@ -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
@@ -86,11 +84,17 @@ and cross-references for cryptic engine-style names) — that table, not this
86
84
  docstring, is the source of truth for what lands here and when.
87
85
  """
88
86
 
87
+ from simulo.interfaces.authoring.action_channel import ActionChannel
89
88
  from simulo.interfaces.authoring.actuator import ActuatorProtocol
90
89
  from simulo.interfaces.authoring.asset import AssetSource, URDFAsset, USDAsset
91
90
  from simulo.interfaces.authoring.checkpoint import PolicyCheckpoint
92
91
  from simulo.interfaces.authoring.entity import AssetEntity, Entity
93
- from simulo.interfaces.authoring.light import DistantLight, DomeLight, Light, LightConfig
92
+ from simulo.interfaces.authoring.light import (
93
+ DistantLight,
94
+ DomeLight,
95
+ Light,
96
+ LightConfig,
97
+ )
94
98
  from simulo.interfaces.authoring.materials import Material, SurfaceMaterial
95
99
  from simulo.interfaces.authoring.physics import (
96
100
  Collision,
@@ -99,6 +103,7 @@ from simulo.interfaces.authoring.physics import (
99
103
  Physics,
100
104
  RigidPhysics,
101
105
  )
106
+ from simulo.interfaces.authoring.policy_inputs import PolicyInput, PolicyInputSchema
102
107
  from simulo.interfaces.authoring.pose import Pose
103
108
  from simulo.interfaces.authoring.primitives import (
104
109
  Cone,
@@ -108,10 +113,17 @@ from simulo.interfaces.authoring.primitives import (
108
113
  Sphere,
109
114
  Transform,
110
115
  )
111
- from simulo.interfaces.authoring.prop import PropProtocol, PropStateProtocol
116
+ from simulo.interfaces.authoring.prop import PropProtocol, PropStateProtocol, VariantPropProtocol
117
+ from simulo.interfaces.authoring.properties import (
118
+ ExternalForceProtocol,
119
+ PropPropertiesProtocol,
120
+ RobotPropertiesProtocol,
121
+ SceneGravityProtocol,
122
+ )
112
123
  from simulo.interfaces.authoring.record import RecordConfig
113
124
  from simulo.interfaces.authoring.robot import RobotProtocol, RobotStateProtocol
114
- from simulo.interfaces.authoring.scene import SceneGroupProtocol, SceneProtocol
125
+ from simulo.interfaces.authoring.robot_config import ActuatorGainsConfig, ArticulationConfig, JointConfig
126
+ from simulo.interfaces.authoring.scene import SceneGroupProtocol, SceneProtocol, SceneVariantsProtocol
115
127
  from simulo.interfaces.authoring.sensor import CameraProtocol
116
128
  from simulo.interfaces.authoring.terrain import (
117
129
  GroundPlane,
@@ -120,9 +132,11 @@ from simulo.interfaces.authoring.terrain import (
120
132
  Terrain,
121
133
  TerrainConfig,
122
134
  )
135
+ from simulo.interfaces.authoring.variants import ObjectVariants
123
136
  from simulo.interfaces.authoring.visual import (
124
137
  ArrowMarker,
125
138
  AxesMarker,
139
+ LineMarker,
126
140
  PointMarker,
127
141
  SphereMarker,
128
142
  Visual,
@@ -175,22 +189,39 @@ __all__ = [
175
189
  "RecordConfig",
176
190
  # injected policy input (dependency-free; loading is deferred to execution)
177
191
  "PolicyCheckpoint",
192
+ "PolicyInput",
193
+ "PolicyInputSchema",
178
194
  # visual
179
195
  "VisualMarkerBase",
180
196
  "AxesMarker",
181
197
  "SphereMarker",
182
198
  "ArrowMarker",
183
199
  "PointMarker",
200
+ "LineMarker",
184
201
  "Visual",
185
202
  # scene (MIRROR — the concrete classes live in the backend package)
203
+ "SceneGravityProtocol",
186
204
  "SceneProtocol",
187
205
  "SceneGroupProtocol",
206
+ "SceneVariantsProtocol",
207
+ # scene variants (dependency-free authoring data)
208
+ "ObjectVariants",
209
+ # robot configuration (dependency-free owning definitions)
210
+ "ActuatorGainsConfig",
211
+ "ArticulationConfig",
212
+ "JointConfig",
188
213
  # robot / prop / sensor / actuator (MIRROR — the concrete classes live in
189
214
  # the backend package and explicitly inherit these Protocols)
215
+ "ExternalForceProtocol",
216
+ "RobotPropertiesProtocol",
217
+ "PropPropertiesProtocol",
190
218
  "RobotProtocol",
191
219
  "RobotStateProtocol",
192
220
  "PropProtocol",
193
221
  "PropStateProtocol",
222
+ "VariantPropProtocol",
194
223
  "CameraProtocol",
195
224
  "ActuatorProtocol",
225
+ # action-to-joint metadata
226
+ "ActionChannel",
196
227
  ]
@@ -0,0 +1,22 @@
1
+ """Dependency-free metadata for a policy action column."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Literal
5
+
6
+ ActionKind = Literal["position", "velocity", "effort", "command"]
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class ActionChannel:
11
+ """One named policy action and the command it represents.
12
+
13
+ ``joint`` is ``None`` for a task-level command. Resolver code validates
14
+ the value against a task's action width and the engine's available joints.
15
+ """
16
+
17
+ name: str
18
+ joint: str | None
19
+ kind: ActionKind
20
+ scale: float = 1.0
21
+ offset: float = 0.0
22
+ hold_steps: int = 1
@@ -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
  """
@@ -208,3 +214,21 @@ class PropProtocol(Protocol):
208
214
  environment.
209
215
  """
210
216
  ...
217
+
218
+
219
+ @runtime_checkable
220
+ class VariantPropProtocol(PropProtocol, Protocol):
221
+ """The opt-in prop surface for a scene item with per-copy variants.
222
+
223
+ :class:`PropProtocol` remains the legacy-compatible pose contract. A task
224
+ that chooses scene variants depends on this narrower capability instead.
225
+ """
226
+
227
+ @property
228
+ def variant_index(self) -> TensorLike:
229
+ """Active option index for every environment copy, shape ``(num_envs,)``."""
230
+ ...
231
+
232
+ def set_variant(self, index: TensorLike, env_ids: Optional[TensorLike] = ...) -> None:
233
+ """Switch selected copies to an option index. Supported for pool variants."""
234
+ ...
@@ -0,0 +1,92 @@
1
+ """Optional per-copy physics capabilities, independent of the base authoring protocols.
2
+
3
+ Backends opt into each capability they implement. The frozen legacy backend
4
+ continues to satisfy RobotProtocol, PropProtocol and SceneProtocol without them.
5
+ K is the number of selected copies; B, S and J are body, shape and joint counts.
6
+ """
7
+
8
+ from typing import Optional, Protocol, runtime_checkable
9
+
10
+ from simulo.interfaces.runtime.tensors import TensorLike
11
+
12
+
13
+ @runtime_checkable
14
+ class RobotPropertiesProtocol(Protocol):
15
+ """Per-copy articulation properties supported by the engine backend."""
16
+
17
+ def set_mass(self, values: TensorLike, env_ids: Optional[TensorLike] = ...) -> None:
18
+ """Set link mass, shape ``(K, B)``, for selected copies."""
19
+ ...
20
+
21
+ def set_center_of_mass(self, values: TensorLike, env_ids: Optional[TensorLike] = ...) -> None:
22
+ """Set link centres of mass in body frames, shape ``(K, B, 3)``."""
23
+ ...
24
+
25
+ def set_friction(self, values: TensorLike, env_ids: Optional[TensorLike] = ...) -> None:
26
+ """Set shape friction, shape ``(K, S)``, for selected copies."""
27
+ ...
28
+
29
+ def set_restitution(self, values: TensorLike, env_ids: Optional[TensorLike] = ...) -> None:
30
+ """Set shape restitution, shape ``(K, S)``, for selected copies."""
31
+ ...
32
+
33
+ def set_joint_gains(
34
+ self,
35
+ stiffness: TensorLike,
36
+ damping: TensorLike,
37
+ env_ids: Optional[TensorLike] = ...,
38
+ ) -> None:
39
+ """Set joint stiffness and damping, each shape ``(K, J)``."""
40
+ ...
41
+
42
+ def set_joint_armature(self, values: TensorLike, env_ids: Optional[TensorLike] = ...) -> None:
43
+ """Set output-side joint armature, shape ``(K, J)``, for selected copies."""
44
+ ...
45
+
46
+
47
+ @runtime_checkable
48
+ class PropPropertiesProtocol(Protocol):
49
+ """Per-copy rigid-prop properties supported by the engine backend."""
50
+
51
+ def set_mass(self, values: TensorLike, env_ids: Optional[TensorLike] = ...) -> None:
52
+ """Set the prop body mass, shape ``(K, 1)``, for selected copies."""
53
+ ...
54
+
55
+ def set_center_of_mass(self, values: TensorLike, env_ids: Optional[TensorLike] = ...) -> None:
56
+ """Set the prop centre of mass in its body frame, shape ``(K, 1, 3)``."""
57
+ ...
58
+
59
+ def set_friction(self, values: TensorLike, env_ids: Optional[TensorLike] = ...) -> None:
60
+ """Set prop-shape friction, shape ``(K, S)``, for selected copies."""
61
+ ...
62
+
63
+ def set_restitution(self, values: TensorLike, env_ids: Optional[TensorLike] = ...) -> None:
64
+ """Set prop-shape restitution, shape ``(K, S)``, for selected copies."""
65
+ ...
66
+
67
+ def set_scale(self, values: TensorLike, env_ids: Optional[TensorLike] = ...) -> None:
68
+ """Set primitive-shape scale, shape ``(K, S, 3)``, for selected copies."""
69
+ ...
70
+
71
+
72
+ @runtime_checkable
73
+ class ExternalForceProtocol(Protocol):
74
+ """World-frame forces and torques for selected copies of an entity."""
75
+
76
+ def apply_external_force(
77
+ self,
78
+ forces: TensorLike,
79
+ torques: TensorLike,
80
+ env_ids: Optional[TensorLike] = ...,
81
+ ) -> None:
82
+ """Apply forces and torques, each shape ``(K, B, 3)``; props have B=1."""
83
+ ...
84
+
85
+
86
+ @runtime_checkable
87
+ class SceneGravityProtocol(Protocol):
88
+ """Per-copy gravity supported by the engine backend."""
89
+
90
+ def set_gravity(self, values: TensorLike, env_ids: Optional[TensorLike] = ...) -> None:
91
+ """Set world-frame gravity, shape ``(K, 3)``, for selected copies."""
92
+ ...
@@ -195,6 +195,11 @@ class RecordConfig:
195
195
  profile precedence can distinguish "user passed False" from "default
196
196
  None". Do not edit defaults to plain ``bool`` — the silent-default
197
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.
198
203
  """
199
204
 
200
205
  output_path: Optional[Union[str, "os.PathLike[str]", Path]] = None
@@ -64,6 +64,7 @@ from typing import Any, Dict, List, Optional, Protocol, Tuple, runtime_checkable
64
64
 
65
65
  from simulo.interfaces.authoring.asset import AssetSource
66
66
  from simulo.interfaces.authoring.pose import Pose
67
+ from simulo.interfaces.authoring.robot_config import ArticulationConfig, JointConfig
67
68
  from simulo.interfaces.runtime.tensors import TensorLike
68
69
 
69
70
 
@@ -183,17 +184,21 @@ class RobotProtocol(Protocol):
183
184
  scale: Tuple[float, float, float]
184
185
  """Per-axis spawn scale factor."""
185
186
 
186
- articulation: Any
187
+ articulation: ArticulationConfig
187
188
  """Articulation physics and control configuration."""
188
189
 
189
- joint_config: Optional[List[Any]]
190
+ joint_config: Optional[List[JointConfig]]
190
191
  """Optional per-joint configurations."""
191
192
 
192
193
  disable_gravity: bool
193
194
  """If ``True``, gravity is disabled for this robot."""
194
195
 
195
196
  actuator_gains: Optional[Dict[str, Any]]
196
- """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
+ """
197
202
 
198
203
  # -- Identity and attachment ------------------------------------------
199
204
 
@@ -257,12 +262,19 @@ class RobotProtocol(Protocol):
257
262
 
258
263
  @property
259
264
  def default_pose(self) -> TensorLike:
260
- """Base pose that :meth:`reset` writes in the world frame, shape
261
- ``(num_envs, 7)`` with columns ``[x, y, z, qw, qx, qy, qz]``:
262
- position in m followed by a w-first unit quaternion. Each
263
- environment's origin is included. Available from ``on_start``
264
- onward. Each read returns a new tensor, so writing into it cannot
265
- change a later reset."""
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.
277
+ """
266
278
  ...
267
279
 
268
280
  @property