simulo-interfaces 0.23.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 (65) hide show
  1. {simulo_interfaces-0.23.0/src/simulo_interfaces.egg-info → simulo_interfaces-0.24.0}/PKG-INFO +1 -1
  2. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/pyproject.toml +1 -1
  3. {simulo_interfaces-0.23.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.23.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/authoring/prop.py +12 -6
  6. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/authoring/record.py +5 -0
  7. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/authoring/robot.py +21 -9
  8. simulo_interfaces-0.24.0/src/simulo/interfaces/authoring/robot_config.py +82 -0
  9. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/authoring/scene.py +6 -1
  10. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/authoring/sensor.py +9 -1
  11. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/platform/asset_catalog.py +2 -2
  12. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/platform/evaluation.py +9 -3
  13. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/platform/onnx_bundle.py +192 -45
  14. simulo_interfaces-0.24.0/src/simulo/interfaces/platform/policy_checkpoint.py +89 -0
  15. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/platform/runs.py +9 -0
  16. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/platform/submit.py +3 -0
  17. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/platform/systems.py +6 -7
  18. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/runtime/__init__.py +2 -0
  19. simulo_interfaces-0.24.0/src/simulo/interfaces/runtime/policy_frames.py +42 -0
  20. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/runtime/task.py +30 -1
  21. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0/src/simulo_interfaces.egg-info}/PKG-INFO +1 -1
  22. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo_interfaces.egg-info/SOURCES.txt +4 -0
  23. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/MANIFEST.in +0 -0
  24. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/PYPI.md +0 -0
  25. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/setup.cfg +0 -0
  26. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/__init__.py +0 -0
  27. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/authoring/actuator.py +0 -0
  28. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/authoring/asset.py +0 -0
  29. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/authoring/checkpoint.py +0 -0
  30. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/authoring/entity.py +0 -0
  31. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/authoring/light.py +0 -0
  32. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/authoring/materials.py +0 -0
  33. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/authoring/physics.py +0 -0
  34. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/authoring/pose.py +0 -0
  35. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/authoring/primitives.py +0 -0
  36. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/authoring/terrain.py +0 -0
  37. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/authoring/visual.py +0 -0
  38. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/authoring/world.py +0 -0
  39. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/exceptions.py +0 -0
  40. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/ids.py +0 -0
  41. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/platform/__init__.py +0 -0
  42. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/platform/_payload.py +0 -0
  43. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/platform/app.py +0 -0
  44. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/platform/asset.py +0 -0
  45. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/platform/callbacks.py +0 -0
  46. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/platform/debug.py +0 -0
  47. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/platform/domain.py +0 -0
  48. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/platform/enums.py +0 -0
  49. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/platform/job_events.py +0 -0
  50. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/platform/manifest.py +0 -0
  51. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/platform/outputs.py +0 -0
  52. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/platform/preview.py +0 -0
  53. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/platform/runtime.py +0 -0
  54. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/platform/volume.py +0 -0
  55. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/py.typed +0 -0
  56. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/runtime/anomaly.py +0 -0
  57. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/runtime/components.py +0 -0
  58. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/runtime/env.py +0 -0
  59. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/runtime/player.py +0 -0
  60. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/runtime/policy.py +0 -0
  61. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/runtime/tensors.py +0 -0
  62. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo/interfaces/runtime/trainer.py +0 -0
  63. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo_interfaces.egg-info/dependency_links.txt +0 -0
  64. {simulo_interfaces-0.23.0 → simulo_interfaces-0.24.0}/src/simulo_interfaces.egg-info/requires.txt +0 -0
  65. {simulo_interfaces-0.23.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.23.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.23.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
  """
@@ -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
@@ -0,0 +1,82 @@
1
+ """Dependency-free robot physics and joint-control configuration."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import List, Optional, Tuple
5
+
6
+
7
+ @dataclass
8
+ class ActuatorGainsConfig:
9
+ """Configuration for an actuator group.
10
+
11
+ Actuators control named groups of joints.
12
+ Use this to configure control properties per actuator group.
13
+
14
+ Attributes:
15
+ stiffness: Position gain (P in PD control). Set to 0 for effort control.
16
+ damping: Velocity gain (D in PD control). Set to 0 for effort control.
17
+ """
18
+
19
+ stiffness: float = 0.0
20
+ damping: float = 0.0
21
+
22
+
23
+ @dataclass
24
+ class JointConfig:
25
+ """Configuration for a single joint.
26
+
27
+ Stored-USD robots receive force drives on drive-less joints. Only non-None
28
+ gains and limits override existing values; zero is an explicit override.
29
+
30
+ Attributes:
31
+ name: Joint name
32
+ type: Joint type (revolute, prismatic, fixed, etc.)
33
+ limits: (lower, upper) position limits in radians or meters
34
+ effort_limit: Maximum torque/force in N·m or N. Stored USD joints use
35
+ this value when non-None; otherwise they keep the USD/PhysX limit.
36
+ Only newly created drives get a finite fallback of 1000.
37
+ velocity_limit: Maximum velocity in rad/s or m/s. Stored USD joints use
38
+ this value when non-None; otherwise they keep the USD/PhysX limit.
39
+ Only newly created drives get a finite fallback of 100.
40
+ damping: Velocity gain; None keeps task actuator_gains or USD gains.
41
+ stiffness: Position gain; None keeps task actuator_gains or USD gains.
42
+ armature: Armature inertia; not applied by the stored-USD builder.
43
+ """
44
+
45
+ name: str = ""
46
+ type: str = "revolute"
47
+ limits: Optional[Tuple[float, float]] = None
48
+ effort_limit: Optional[float] = None
49
+ velocity_limit: Optional[float] = None
50
+ damping: Optional[float] = None
51
+ stiffness: Optional[float] = None
52
+ armature: float = 0.0
53
+
54
+
55
+ @dataclass
56
+ class ArticulationConfig:
57
+ """Configuration for robot articulation physics and control.
58
+
59
+ For stored USDs, only non-None solver, self-collision and root-fixing
60
+ settings override USD-authored values or PhysX defaults. Declaring joint
61
+ defaults alone leaves those physics settings intact. Sleep and stabilization
62
+ thresholds are not applied by the stored-USD builder.
63
+
64
+ Attributes:
65
+ solver_position_iterations: Position solver iteration count
66
+ solver_velocity_iterations: Velocity solver iteration count
67
+ sleep_threshold: Velocity threshold for sleep
68
+ stabilization_threshold: Stabilization threshold
69
+ enable_self_collision: Whether to enable self-collision
70
+ default_joint_positions: Default joint positions (rad or m)
71
+ default_joint_velocities: Default joint velocities (rad/s or m/s)
72
+ fix_root_link: Whether to create a fixed joint from the root link to the world
73
+ """
74
+
75
+ solver_position_iterations: Optional[int] = None
76
+ solver_velocity_iterations: Optional[int] = None
77
+ sleep_threshold: float = 0.005
78
+ stabilization_threshold: float = 0.001
79
+ enable_self_collision: Optional[bool] = None
80
+ default_joint_positions: Optional[List[float]] = None
81
+ default_joint_velocities: Optional[List[float]] = None
82
+ fix_root_link: Optional[bool] = None
@@ -77,6 +77,9 @@ class SceneGroupProtocol(Protocol):
77
77
 
78
78
  ``per_environment=True`` (the default) clones the item per environment
79
79
  when the scene has more than one environment.
80
+ Shared robots and props are supported only when the scene has one
81
+ environment. Transform children are not replicated; add robots and
82
+ props directly to a multi-environment scene.
80
83
  """
81
84
  ...
82
85
 
@@ -116,7 +119,9 @@ class SceneProtocol(Protocol):
116
119
 
117
120
  ``per_environment=True`` (the default) clones the item per environment
118
121
  when ``num_envs > 1``; pass ``per_environment=False`` for shared items
119
- like ground planes and global lights.
122
+ like ground planes and global lights. Shared robots and props are
123
+ supported only when ``num_envs == 1``. Transform children are not
124
+ replicated; add robots and props directly to a multi-environment scene.
120
125
  """
121
126
  ...
122
127
 
@@ -32,7 +32,7 @@ documented-omissions allowlist):
32
32
  implementation; giving them contract homes is a later, separate decision.
33
33
  """
34
34
 
35
- from typing import Any, List, Optional, Protocol, runtime_checkable
35
+ from typing import Any, List, Literal, Optional, Protocol, runtime_checkable
36
36
 
37
37
  from simulo.interfaces.runtime.tensors import TensorLike
38
38
 
@@ -88,6 +88,14 @@ class CameraProtocol(SensorProtocol, Protocol):
88
88
  height: int
89
89
  """Image height in pixels."""
90
90
 
91
+ purpose: Literal["observer", "policy"]
92
+ """Camera label, default "observer"; only "observer" and "policy" are valid.
93
+
94
+ Construction rejects other values with ValueError. LearningEnv setup
95
+ requires "policy" for declared image cameras. Rendering and recording
96
+ consumers are implemented in later milestones.
97
+ """
98
+
91
99
  data_types: List[str]
92
100
  """Data types to capture (e.g. ``"rgb"``, ``"depth"``, ``"normals"``,
93
101
  ``"semantic"``)."""
@@ -695,7 +695,7 @@ ASSET_NOT_FOUND_CODE = "asset_not_found"
695
695
  # plane); this docstring is the drift anchor for a future reconciliation
696
696
  # where the control plane imports these directly instead of re-declaring
697
697
  # them. The thin client's `simulo asset publish`/`validate` error
698
- # rendering (`_client/cli.py::_friendly_asset_error`) matches these exact
698
+ # rendering (`_client/commands/assets.py::_friendly_asset_error`) matches these exact
699
699
  # strings, never a hand-typed literal.
700
700
  # --------------------------------------------------------------------------
701
701
 
@@ -727,7 +727,7 @@ ASSET_UPLOAD_INCOMPLETE_CODE = "asset_upload_incomplete"
727
727
  # "mirrored, not imported" precedent above), so this is a value-only
728
728
  # duplication, drift-anchored by this docstring. The thin client's
729
729
  # `simulo asset get`/`deprecate`/`delete` error rendering
730
- # (`_client/cli.py::_friendly_asset_error`) matches these exact strings so
730
+ # (`_client/commands/assets.py::_friendly_asset_error`) matches these exact strings so
731
731
  # the server's already user-language message renders verbatim, never a
732
732
  # code-prefixed wire dump.
733
733
  # --------------------------------------------------------------------------
@@ -4,9 +4,10 @@ An eval job runs a trained checkpoint on numbered episodes and judges each
4
4
  one with the eval file's ``@app.success`` function. The runtime returns the
5
5
  report as a plain ``dict`` of :class:`EvaluationReport` shape and registers it
6
6
  as a ``report`` output; the client renders it, the Console reads it from the
7
- job result, and the control plane checks its schema version. Every episode's
8
- detail goes in ``episodes.csv`` (:func:`episodes_csv_columns`), so the report
9
- stays small whatever the episode count.
7
+ job result, and the control plane validates it before accepting completion.
8
+ Every episode's detail goes in ``episodes.csv``
9
+ (:func:`episodes_csv_columns`), so the report stays small whatever the episode
10
+ count.
10
11
 
11
12
  The comparison block (``--compare``) is part of v1 from the start, so comparing
12
13
  two checkpoints never needs a new schema version. Additive-only: a breaking
@@ -300,6 +301,11 @@ class EvaluationComparison(TypedDict):
300
301
  """The ``--compare`` checkpoint."""
301
302
  compared_result: EvaluationResult
302
303
  """The ``--compare`` checkpoint's own RESULT."""
304
+ task_changed: NotRequired[bool]
305
+ """Whether the evaluation task differs from the ``--compare`` checkpoint's training task.
306
+
307
+ Omitted by reports written before this additive v1 field existed.
308
+ """
303
309
  improved: int
304
310
  """Episodes that failed with ``compared_with`` and succeeded with the ``--policy`` checkpoint."""
305
311
  regressed: int