simscope 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- simscope/__init__.py +6 -0
- simscope/__main__.py +8 -0
- simscope/_assets/simscope-app.css +2 -0
- simscope/_assets/simscope-app.js +4311 -0
- simscope/_assets/simscope-player.js +4325 -0
- simscope/_assets/simscope-web.LICENSES.txt +407 -0
- simscope/_icon.py +22 -0
- simscope/_mjviser.py +203 -0
- simscope/annotations.py +1132 -0
- simscope/cli.py +482 -0
- simscope/core.py +257 -0
- simscope/derived.py +697 -0
- simscope/export.py +799 -0
- simscope/highlights.py +947 -0
- simscope/importers.py +874 -0
- simscope/index.py +579 -0
- simscope/io/__init__.py +45 -0
- simscope/io/blockfile.py +938 -0
- simscope/io/cas.py +294 -0
- simscope/io/codecs.py +566 -0
- simscope/io/errors.py +9 -0
- simscope/io/manifest.py +358 -0
- simscope/io/pack.py +563 -0
- simscope/io/scene.py +239 -0
- simscope/isaaclab.py +1460 -0
- simscope/library.py +705 -0
- simscope/mujoco.py +578 -0
- simscope/py.typed +0 -0
- simscope/recorder.py +784 -0
- simscope/server/__init__.py +9 -0
- simscope/server/app.py +149 -0
- simscope/server/blocks.py +191 -0
- simscope/server/jobs.py +166 -0
- simscope/server/routes.py +707 -0
- simscope/server/security.py +218 -0
- simscope/server/state.py +751 -0
- simscope/server/static.py +84 -0
- simscope/transforms.py +147 -0
- simscope-0.1.1.dist-info/METADATA +132 -0
- simscope-0.1.1.dist-info/RECORD +45 -0
- simscope-0.1.1.dist-info/WHEEL +4 -0
- simscope-0.1.1.dist-info/entry_points.txt +3 -0
- simscope-0.1.1.dist-info/licenses/LICENSE.md +201 -0
- simscope-0.1.1.dist-info/licenses/THIRD_PARTY_NOTICES.md +267 -0
- simscope-0.1.1.dist-info/licenses/src/simscope/_assets/simscope-web.LICENSES.txt +407 -0
simscope/core.py
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""Simulator-neutral data model for scenes and rollouts.
|
|
2
|
+
|
|
3
|
+
These types are the in-memory contract shared by the adapters, the storage
|
|
4
|
+
layer, the viewer, and the exporter. The on-disk encoding is ``simscope.io``.
|
|
5
|
+
|
|
6
|
+
Conventions: Z-up, right-handed, metres, seconds. A pose is
|
|
7
|
+
``[px, py, pz, qx, qy, qz, qw]`` with the quaternion in xyzw order.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import dataclasses
|
|
11
|
+
import math
|
|
12
|
+
from typing import Literal
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
import numpy.typing as npt
|
|
16
|
+
|
|
17
|
+
POSE_DIM = 7
|
|
18
|
+
"""Floats per pose: position xyz, then quaternion xyzw."""
|
|
19
|
+
|
|
20
|
+
GeomKind = Literal[
|
|
21
|
+
"box", "sphere", "capsule", "cylinder", "ellipsoid", "plane", "mesh"
|
|
22
|
+
]
|
|
23
|
+
GEOM_KINDS: tuple[GeomKind, ...] = (
|
|
24
|
+
"box",
|
|
25
|
+
"sphere",
|
|
26
|
+
"capsule",
|
|
27
|
+
"cylinder",
|
|
28
|
+
"ellipsoid",
|
|
29
|
+
"plane",
|
|
30
|
+
"mesh",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
GeomRole = Literal["visual", "collision"]
|
|
34
|
+
|
|
35
|
+
StreamKind = Literal["pose", "scalar", "vector", "arrows", "points", "polyline"]
|
|
36
|
+
STREAM_KINDS: tuple[StreamKind, ...] = (
|
|
37
|
+
"pose",
|
|
38
|
+
"scalar",
|
|
39
|
+
"vector",
|
|
40
|
+
"arrows",
|
|
41
|
+
"points",
|
|
42
|
+
"polyline",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
Vec3 = tuple[float, float, float]
|
|
46
|
+
Quat = tuple[float, float, float, float]
|
|
47
|
+
Rgba = tuple[float, float, float, float]
|
|
48
|
+
|
|
49
|
+
IDENTITY_QUAT: Quat = (0.0, 0.0, 0.0, 1.0)
|
|
50
|
+
UNIT_SCALE: Vec3 = (1.0, 1.0, 1.0)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclasses.dataclass(frozen=True, eq=False)
|
|
54
|
+
class Mesh:
|
|
55
|
+
"""A triangle mesh in its geom's local frame.
|
|
56
|
+
|
|
57
|
+
Attributes:
|
|
58
|
+
vertices: Vertex positions, shape ``[V, 3]``, dtype float32.
|
|
59
|
+
faces: Triangle vertex indices, shape ``[F, 3]``, dtype uint32.
|
|
60
|
+
normals: Optional per-vertex normals, shape ``[V, 3]``, float32.
|
|
61
|
+
uvs: Optional per-vertex texture coordinates, shape ``[V, 2]``,
|
|
62
|
+
float32.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
vertices: npt.NDArray[np.float32]
|
|
66
|
+
faces: npt.NDArray[np.uint32]
|
|
67
|
+
normals: npt.NDArray[np.float32] | None = None
|
|
68
|
+
uvs: npt.NDArray[np.float32] | None = None
|
|
69
|
+
|
|
70
|
+
def __post_init__(self) -> None:
|
|
71
|
+
"""Validates array shapes and dtypes.
|
|
72
|
+
|
|
73
|
+
Raises:
|
|
74
|
+
ValueError: If an array has the wrong shape or dtype.
|
|
75
|
+
"""
|
|
76
|
+
n_verts = _check_array("vertices", self.vertices, np.float32, 3)
|
|
77
|
+
_check_array("faces", self.faces, np.uint32, 3)
|
|
78
|
+
normals, uvs = self.normals, self.uvs
|
|
79
|
+
if (
|
|
80
|
+
normals is not None
|
|
81
|
+
and _check_array("normals", normals, np.float32, 3) != n_verts
|
|
82
|
+
):
|
|
83
|
+
raise ValueError("normals must have one row per vertex")
|
|
84
|
+
if (
|
|
85
|
+
uvs is not None
|
|
86
|
+
and _check_array("uvs", uvs, np.float32, 2) != n_verts
|
|
87
|
+
):
|
|
88
|
+
raise ValueError("uvs must have one row per vertex")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclasses.dataclass(frozen=True)
|
|
92
|
+
class Texture:
|
|
93
|
+
"""An encoded 2D image.
|
|
94
|
+
|
|
95
|
+
Attributes:
|
|
96
|
+
data: The encoded file bytes.
|
|
97
|
+
media_type: ``"image/png"`` or ``"image/jpeg"``.
|
|
98
|
+
width: Width in pixels.
|
|
99
|
+
height: Height in pixels.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
data: bytes
|
|
103
|
+
media_type: Literal["image/png", "image/jpeg"]
|
|
104
|
+
width: int
|
|
105
|
+
height: int
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclasses.dataclass(frozen=True)
|
|
109
|
+
class Material:
|
|
110
|
+
"""Surface appearance of a geom.
|
|
111
|
+
|
|
112
|
+
Attributes:
|
|
113
|
+
rgba: Base color and opacity, each in ``[0, 1]``.
|
|
114
|
+
metallic: PBR metalness in ``[0, 1]``.
|
|
115
|
+
roughness: PBR roughness in ``[0, 1]``.
|
|
116
|
+
texture: Index into ``Scene.textures``, or ``None``.
|
|
117
|
+
texrepeat: Texture repeats along u and v.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
rgba: Rgba = (0.8, 0.8, 0.8, 1.0)
|
|
121
|
+
metallic: float = 0.0
|
|
122
|
+
roughness: float = 1.0
|
|
123
|
+
texture: int | None = None
|
|
124
|
+
texrepeat: tuple[float, float] = (1.0, 1.0)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@dataclasses.dataclass(frozen=True)
|
|
128
|
+
class Body:
|
|
129
|
+
"""A rigid body that owns geoms and has one pose per frame.
|
|
130
|
+
|
|
131
|
+
Attributes:
|
|
132
|
+
name: Body name or USD path. Not required to be unique.
|
|
133
|
+
parent: Index of the parent body, or ``-1`` for a root.
|
|
134
|
+
mass: Mass in kilograms, or ``0.0`` if unknown (the world body, or
|
|
135
|
+
a simulator that does not say). Highlights weight the centre of
|
|
136
|
+
mass by it.
|
|
137
|
+
|
|
138
|
+
Raises:
|
|
139
|
+
ValueError: If ``mass`` is negative or not finite.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
name: str
|
|
143
|
+
parent: int = -1
|
|
144
|
+
mass: float = 0.0
|
|
145
|
+
|
|
146
|
+
def __post_init__(self) -> None:
|
|
147
|
+
"""Checks the mass."""
|
|
148
|
+
if not (math.isfinite(self.mass) and self.mass >= 0):
|
|
149
|
+
raise ValueError(f"body mass must be finite and >= 0: {self.mass}")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@dataclasses.dataclass(frozen=True)
|
|
153
|
+
class Geom:
|
|
154
|
+
"""A visual or collision shape attached to a body.
|
|
155
|
+
|
|
156
|
+
``kind`` and ``size`` follow MuJoCo conventions.
|
|
157
|
+
|
|
158
|
+
Attributes:
|
|
159
|
+
body: Index into ``Scene.bodies``.
|
|
160
|
+
kind: Shape kind.
|
|
161
|
+
size: Kind-specific size parameters in metres.
|
|
162
|
+
pos: Position in the body frame.
|
|
163
|
+
quat: Orientation in the body frame, xyzw.
|
|
164
|
+
scale: Component-wise scale applied to local vertices.
|
|
165
|
+
material: Index into ``Scene.materials``.
|
|
166
|
+
mesh: Index into ``Scene.meshes`` when ``kind == "mesh"``.
|
|
167
|
+
role: Whether viewers show the geom by default.
|
|
168
|
+
name: Optional geom name.
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
body: int
|
|
172
|
+
kind: GeomKind
|
|
173
|
+
size: Vec3 = (0.0, 0.0, 0.0)
|
|
174
|
+
pos: Vec3 = (0.0, 0.0, 0.0)
|
|
175
|
+
quat: Quat = IDENTITY_QUAT
|
|
176
|
+
scale: Vec3 = UNIT_SCALE
|
|
177
|
+
material: int = 0
|
|
178
|
+
mesh: int | None = None
|
|
179
|
+
role: GeomRole = "visual"
|
|
180
|
+
name: str = ""
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@dataclasses.dataclass(frozen=True, eq=False)
|
|
184
|
+
class Scene:
|
|
185
|
+
"""The static part of a rollout, shared by every frame and env.
|
|
186
|
+
|
|
187
|
+
Attributes:
|
|
188
|
+
bodies: Bodies, in the order of the pose stream.
|
|
189
|
+
geoms: Geoms attached to bodies.
|
|
190
|
+
materials: Materials referenced by geoms. Must not be empty when
|
|
191
|
+
there are geoms.
|
|
192
|
+
meshes: Meshes referenced by mesh geoms.
|
|
193
|
+
textures: Textures referenced by materials.
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
bodies: tuple[Body, ...]
|
|
197
|
+
geoms: tuple[Geom, ...] = ()
|
|
198
|
+
materials: tuple[Material, ...] = (Material(),)
|
|
199
|
+
meshes: tuple[Mesh, ...] = ()
|
|
200
|
+
textures: tuple[Texture, ...] = ()
|
|
201
|
+
|
|
202
|
+
def __post_init__(self) -> None:
|
|
203
|
+
"""Validates cross-references between scene elements.
|
|
204
|
+
|
|
205
|
+
Raises:
|
|
206
|
+
ValueError: If an index points outside its table, or a mesh geom
|
|
207
|
+
has no mesh.
|
|
208
|
+
"""
|
|
209
|
+
n_bodies = len(self.bodies)
|
|
210
|
+
for i, body in enumerate(self.bodies):
|
|
211
|
+
if not -1 <= body.parent < n_bodies or body.parent == i:
|
|
212
|
+
raise ValueError(f"body {i} has invalid parent {body.parent}")
|
|
213
|
+
for j, geom in enumerate(self.geoms):
|
|
214
|
+
if not 0 <= geom.body < n_bodies:
|
|
215
|
+
raise ValueError(f"geom {j} has invalid body {geom.body}")
|
|
216
|
+
if not 0 <= geom.material < len(self.materials):
|
|
217
|
+
raise ValueError(
|
|
218
|
+
f"geom {j} has invalid material {geom.material}"
|
|
219
|
+
)
|
|
220
|
+
if geom.kind == "mesh":
|
|
221
|
+
if geom.mesh is None or not 0 <= geom.mesh < len(self.meshes):
|
|
222
|
+
raise ValueError(f"mesh geom {j} has invalid mesh")
|
|
223
|
+
elif geom.mesh is not None:
|
|
224
|
+
raise ValueError(f"geom {j} of kind {geom.kind} has a mesh")
|
|
225
|
+
for k, material in enumerate(self.materials):
|
|
226
|
+
texture = material.texture
|
|
227
|
+
if texture is not None and not 0 <= texture < len(self.textures):
|
|
228
|
+
raise ValueError(f"material {k} has invalid texture {texture}")
|
|
229
|
+
|
|
230
|
+
@property
|
|
231
|
+
def n_bodies(self) -> int:
|
|
232
|
+
"""Number of bodies, which is the pose count per frame."""
|
|
233
|
+
return len(self.bodies)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _check_array(
|
|
237
|
+
name: str, array: npt.NDArray, dtype: type[np.generic], width: int
|
|
238
|
+
) -> int:
|
|
239
|
+
"""Checks that an array is 2D with a given width and dtype.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
name: Field name for error messages.
|
|
243
|
+
array: Array to check.
|
|
244
|
+
dtype: Required dtype.
|
|
245
|
+
width: Required size of the second axis.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
The size of the first axis.
|
|
249
|
+
|
|
250
|
+
Raises:
|
|
251
|
+
ValueError: If the shape or dtype does not match.
|
|
252
|
+
"""
|
|
253
|
+
if array.dtype != dtype:
|
|
254
|
+
raise ValueError(f"{name} must have dtype {np.dtype(dtype)}")
|
|
255
|
+
if array.ndim != 2 or array.shape[1] != width:
|
|
256
|
+
raise ValueError(f"{name} must have shape [N, {width}]")
|
|
257
|
+
return array.shape[0]
|