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.
Files changed (45) hide show
  1. simscope/__init__.py +6 -0
  2. simscope/__main__.py +8 -0
  3. simscope/_assets/simscope-app.css +2 -0
  4. simscope/_assets/simscope-app.js +4311 -0
  5. simscope/_assets/simscope-player.js +4325 -0
  6. simscope/_assets/simscope-web.LICENSES.txt +407 -0
  7. simscope/_icon.py +22 -0
  8. simscope/_mjviser.py +203 -0
  9. simscope/annotations.py +1132 -0
  10. simscope/cli.py +482 -0
  11. simscope/core.py +257 -0
  12. simscope/derived.py +697 -0
  13. simscope/export.py +799 -0
  14. simscope/highlights.py +947 -0
  15. simscope/importers.py +874 -0
  16. simscope/index.py +579 -0
  17. simscope/io/__init__.py +45 -0
  18. simscope/io/blockfile.py +938 -0
  19. simscope/io/cas.py +294 -0
  20. simscope/io/codecs.py +566 -0
  21. simscope/io/errors.py +9 -0
  22. simscope/io/manifest.py +358 -0
  23. simscope/io/pack.py +563 -0
  24. simscope/io/scene.py +239 -0
  25. simscope/isaaclab.py +1460 -0
  26. simscope/library.py +705 -0
  27. simscope/mujoco.py +578 -0
  28. simscope/py.typed +0 -0
  29. simscope/recorder.py +784 -0
  30. simscope/server/__init__.py +9 -0
  31. simscope/server/app.py +149 -0
  32. simscope/server/blocks.py +191 -0
  33. simscope/server/jobs.py +166 -0
  34. simscope/server/routes.py +707 -0
  35. simscope/server/security.py +218 -0
  36. simscope/server/state.py +751 -0
  37. simscope/server/static.py +84 -0
  38. simscope/transforms.py +147 -0
  39. simscope-0.1.1.dist-info/METADATA +132 -0
  40. simscope-0.1.1.dist-info/RECORD +45 -0
  41. simscope-0.1.1.dist-info/WHEEL +4 -0
  42. simscope-0.1.1.dist-info/entry_points.txt +3 -0
  43. simscope-0.1.1.dist-info/licenses/LICENSE.md +201 -0
  44. simscope-0.1.1.dist-info/licenses/THIRD_PARTY_NOTICES.md +267 -0
  45. simscope-0.1.1.dist-info/licenses/src/simscope/_assets/simscope-web.LICENSES.txt +407 -0
simscope/io/codecs.py ADDED
@@ -0,0 +1,566 @@
1
+ """Payload codecs for block files and mesh blobs.
2
+
3
+ All functions are pure. Block codecs turn an f32 ``[n, K]`` array (the frames
4
+ of one env) into a deflate-raw payload and back. The byte layout assumes a
5
+ little-endian host, which the format requires anyway.
6
+ """
7
+
8
+ import struct
9
+ import sys
10
+ import zlib
11
+ from typing import Literal
12
+
13
+ import numpy as np
14
+ import numpy.typing as npt
15
+
16
+ from simscope import core
17
+ from simscope.io import errors
18
+
19
+ if sys.byteorder != "little": # pragma: no cover
20
+ raise ImportError("simscope.io requires a little-endian host")
21
+
22
+ CODEC_F32S = 1
23
+ CODEC_Q16D = 2
24
+ BLOCK_CODEC_IDS: dict[str, int] = {"f32s": CODEC_F32S, "q16d": CODEC_Q16D}
25
+ BLOCK_CODEC_NAMES: dict[int, str] = {v: k for k, v in BLOCK_CODEC_IDS.items()}
26
+
27
+ MESH_RAW = 0
28
+ MESH_Q16 = 1
29
+ MESH_CODEC_IDS: dict[str, int] = {"raw": MESH_RAW, "q16": MESH_Q16}
30
+
31
+ DEFLATE_LEVEL = 6
32
+ _U16_MAX = 65535
33
+
34
+ BlockCodec = Literal["f32s", "q16d"]
35
+ MeshCodec = Literal["raw", "q16"]
36
+ Payload = bytes | bytearray | memoryview
37
+
38
+
39
+ def codec_id(codec: str | int) -> int:
40
+ """Resolves a block codec name or id to its numeric id.
41
+
42
+ Args:
43
+ codec: ``"f32s"``, ``"q16d"``, or the numeric id 1 or 2.
44
+
45
+ Returns:
46
+ The numeric codec id.
47
+
48
+ Raises:
49
+ errors.FormatError: If the codec is unknown.
50
+ """
51
+ if isinstance(codec, str):
52
+ if codec not in BLOCK_CODEC_IDS:
53
+ raise errors.FormatError(f"unknown block codec {codec!r}")
54
+ return BLOCK_CODEC_IDS[codec]
55
+ if codec not in BLOCK_CODEC_NAMES:
56
+ raise errors.FormatError(f"unknown block codec id {codec}")
57
+ return int(codec)
58
+
59
+
60
+ def payload_ulen(codec: str | int, n: int, k: int) -> int:
61
+ """Returns the uncompressed payload length of a block.
62
+
63
+ Args:
64
+ codec: Block codec name or id.
65
+ n: Frames in the block.
66
+ k: Floats per frame.
67
+
68
+ Returns:
69
+ ``4*K*n`` for f32s and ``8*K + 2*K*n`` for q16d. q16d is two u8
70
+ planes, so it is not ``4*K*n``.
71
+ """
72
+ if codec_id(codec) == CODEC_F32S:
73
+ return 4 * k * n
74
+ return 8 * k + 2 * k * n
75
+
76
+
77
+ def deflate(data: Payload | npt.NDArray[np.uint8]) -> bytes:
78
+ """Compresses bytes with deflate-raw at the spec level (6).
79
+
80
+ Args:
81
+ data: Bytes-like object or contiguous uint8 array.
82
+
83
+ Returns:
84
+ The raw deflate stream.
85
+ """
86
+ comp = zlib.compressobj(DEFLATE_LEVEL, zlib.DEFLATED, -15)
87
+ return comp.compress(data) + comp.flush()
88
+
89
+
90
+ def inflate(payload: Payload, ulen: int, what: str = "payload") -> bytes:
91
+ """Decompresses a deflate-raw stream and checks its length.
92
+
93
+ Args:
94
+ payload: The compressed bytes.
95
+ ulen: Expected uncompressed length.
96
+ what: Description used in error messages.
97
+
98
+ Returns:
99
+ The uncompressed bytes.
100
+
101
+ Raises:
102
+ errors.FormatError: If the stream is corrupt or has the wrong length.
103
+ """
104
+ try:
105
+ raw = zlib.decompress(payload, -15, max(ulen, 1))
106
+ except zlib.error as exc:
107
+ raise errors.FormatError(f"{what}: bad deflate stream: {exc}") from exc
108
+ if len(raw) != ulen:
109
+ raise errors.FormatError(
110
+ f"{what}: inflated to {len(raw)} bytes, expected {ulen}"
111
+ )
112
+ return raw
113
+
114
+
115
+ def shuffle(d: npt.NDArray[np.unsignedinteger]) -> npt.NDArray[np.uint8]:
116
+ """Transposes ``[n, K]`` unsigned ints to component-major byte planes.
117
+
118
+ Args:
119
+ d: C-contiguous array of shape ``[n, K]`` (u16 or u32).
120
+
121
+ Returns:
122
+ A uint8 array of shape ``[itemsize, K, n]``. Plane ``b`` holds byte
123
+ ``b`` (least significant first) of every element, laid out ``[K, n]``.
124
+ """
125
+ n, k = d.shape
126
+ b = d.view(np.uint8).reshape(n, k, d.itemsize)
127
+ return np.ascontiguousarray(b.transpose(2, 1, 0))
128
+
129
+
130
+ def unshuffle_into(
131
+ planes: npt.NDArray[np.uint8], out: npt.NDArray[np.unsignedinteger]
132
+ ) -> None:
133
+ """Reverses :func:`shuffle` into a preallocated array.
134
+
135
+ Args:
136
+ planes: uint8 array of shape ``[itemsize, K, n]``.
137
+ out: C-contiguous output of shape ``[n, K]``.
138
+ """
139
+ n, k = out.shape
140
+ out.view(np.uint8).reshape(n, k, out.itemsize)[...] = planes.transpose(
141
+ 2, 1, 0
142
+ )
143
+
144
+
145
+ def delta_encode(a: npt.NDArray[np.unsignedinteger]) -> npt.NDArray:
146
+ """Delta-codes rows along axis 0 modulo the dtype width.
147
+
148
+ Args:
149
+ a: Unsigned integer array with at least one row.
150
+
151
+ Returns:
152
+ A new array where row 0 is absolute and row ``i`` is
153
+ ``a[i] - a[i-1]``.
154
+ """
155
+ d = a.copy()
156
+ np.subtract(a[1:], a[:-1], out=d[1:])
157
+ return d
158
+
159
+
160
+ def delta_decode_inplace(a: npt.NDArray[np.unsignedinteger]) -> None:
161
+ """Prefix-sums rows along axis 0 in place, modulo the dtype width.
162
+
163
+ Args:
164
+ a: Delta-coded unsigned integer array.
165
+ """
166
+ np.cumsum(a, axis=0, dtype=a.dtype, out=a)
167
+
168
+
169
+ def _as_block(x: npt.ArrayLike) -> npt.NDArray[np.float32]:
170
+ """Validates and returns a C-contiguous float32 ``[n, K]`` array."""
171
+ a = np.ascontiguousarray(x, dtype=np.float32)
172
+ if a.ndim != 2 or a.shape[0] < 1 or a.shape[1] < 1:
173
+ raise ValueError(f"block must have shape [n>=1, K>=1], got {a.shape}")
174
+ return a
175
+
176
+
177
+ def _q16_ranges(
178
+ x: npt.NDArray[np.float32],
179
+ ) -> tuple[npt.NDArray[np.float32], npt.NDArray[np.float32]]:
180
+ """Computes per-column ``lo`` and ``step`` for 16-bit quantization."""
181
+ lo = x.min(axis=0)
182
+ hi = x.max(axis=0)
183
+ step = (hi - lo) / np.float32(_U16_MAX)
184
+ return lo, step.astype(np.float32)
185
+
186
+
187
+ def _quantize(
188
+ x: npt.NDArray[np.float32],
189
+ lo: npt.NDArray[np.float32],
190
+ step: npt.NDArray[np.float32],
191
+ ) -> npt.NDArray[np.uint16]:
192
+ """Quantizes ``x`` to u16 using per-column ``lo`` and ``step``."""
193
+ safe = np.where(step > 0, step, np.float32(1.0))
194
+ q = (x - lo) / safe
195
+ np.rint(q, out=q)
196
+ np.clip(q, 0, _U16_MAX, out=q)
197
+ return q.astype(np.uint16)
198
+
199
+
200
+ def dequantize(
201
+ q: npt.NDArray[np.uint16],
202
+ lo: npt.NDArray[np.float32],
203
+ step: npt.NDArray[np.float32],
204
+ out: npt.NDArray[np.float32] | None = None,
205
+ ) -> npt.NDArray[np.float32]:
206
+ """Computes ``lo + q * step`` in float32, one rounding per operation.
207
+
208
+ Args:
209
+ q: Quantized values, broadcastable against ``lo`` and ``step``.
210
+ lo: Per-component minimum.
211
+ step: Per-component step.
212
+ out: Optional float32 output with the shape of ``q``.
213
+
214
+ Returns:
215
+ The dequantized float32 array.
216
+ """
217
+ if out is None:
218
+ out = np.empty(q.shape, np.float32)
219
+ np.multiply(q, step, out=out, dtype=np.float32)
220
+ np.add(out, lo, out=out)
221
+ return out
222
+
223
+
224
+ def can_encode_q16d(x: npt.ArrayLike) -> bool:
225
+ """Tells whether a block can be stored with q16d.
226
+
227
+ Args:
228
+ x: Block data ``[n, K]``.
229
+
230
+ Returns:
231
+ False if any value is NaN or infinite, or a column range overflows
232
+ float32.
233
+ """
234
+ a = _as_block(x)
235
+ lo, step = _q16_ranges(a)
236
+ return bool(np.isfinite(lo).all() and np.isfinite(step).all())
237
+
238
+
239
+ def encode_block(x: npt.ArrayLike, codec: str | int) -> bytes:
240
+ """Encodes one block into its compressed payload.
241
+
242
+ Args:
243
+ x: Float32 data of shape ``[n, K]``: n frames of one env.
244
+ codec: ``"f32s"`` or ``"q16d"`` (or the numeric id).
245
+
246
+ Returns:
247
+ The deflate-raw payload.
248
+
249
+ Raises:
250
+ ValueError: If ``x`` has the wrong shape, or the codec is q16d and
251
+ ``x`` has non-finite values. Use :func:`encode_block_auto` to fall
252
+ back to f32s.
253
+ errors.FormatError: If the codec is unknown.
254
+ """
255
+ cid = codec_id(codec)
256
+ a = _as_block(x)
257
+ if cid == CODEC_F32S:
258
+ d = delta_encode(a.view(np.uint32))
259
+ return deflate(shuffle(d))
260
+ lo, step = _q16_ranges(a)
261
+ if not (np.isfinite(lo).all() and np.isfinite(step).all()):
262
+ raise ValueError("q16d cannot encode non-finite values")
263
+ q = _quantize(a, lo, step)
264
+ planes = shuffle(delta_encode(q))
265
+ comp = zlib.compressobj(DEFLATE_LEVEL, zlib.DEFLATED, -15)
266
+ return (
267
+ comp.compress(lo.tobytes())
268
+ + comp.compress(step.tobytes())
269
+ + comp.compress(planes)
270
+ + comp.flush()
271
+ )
272
+
273
+
274
+ def encode_block_auto(x: npt.ArrayLike, codec: str | int) -> tuple[int, bytes]:
275
+ """Encodes a block, falling back to f32s when q16d is unsafe.
276
+
277
+ Args:
278
+ x: Float32 data of shape ``[n, K]``.
279
+ codec: Requested codec.
280
+
281
+ Returns:
282
+ ``(codec_id, payload)`` where ``codec_id`` is the codec actually used.
283
+ """
284
+ cid = codec_id(codec)
285
+ a = _as_block(x)
286
+ if cid == CODEC_Q16D and not can_encode_q16d(a):
287
+ cid = CODEC_F32S
288
+ return cid, encode_block(a, cid)
289
+
290
+
291
+ def decode_block_into(
292
+ payload: Payload,
293
+ codec: str | int,
294
+ out: npt.NDArray[np.float32],
295
+ ) -> None:
296
+ """Decodes a block payload into a preallocated array.
297
+
298
+ Args:
299
+ payload: The compressed payload.
300
+ codec: Codec name or id from the block header.
301
+ out: C-contiguous writable float32 array of shape ``[n, K]``.
302
+
303
+ Raises:
304
+ errors.FormatError: If the payload is corrupt or the codec unknown.
305
+ """
306
+ cid = codec_id(codec)
307
+ n, k = out.shape
308
+ raw = inflate(payload, payload_ulen(cid, n, k), "block payload")
309
+ if cid == CODEC_F32S:
310
+ planes = np.frombuffer(raw, np.uint8).reshape(4, k, n)
311
+ bits = out.view(np.uint32)
312
+ unshuffle_into(planes, bits)
313
+ delta_decode_inplace(bits)
314
+ return
315
+ lo = np.frombuffer(raw, np.float32, k, 0)
316
+ step = np.frombuffer(raw, np.float32, k, 4 * k)
317
+ planes = np.frombuffer(raw, np.uint8, 2 * k * n, 8 * k).reshape(2, k, n)
318
+ q = np.empty((n, k), np.uint16)
319
+ unshuffle_into(planes, q)
320
+ delta_decode_inplace(q)
321
+ dequantize(q, lo, step, out)
322
+
323
+
324
+ def decode_block(
325
+ payload: Payload, codec: str | int, n: int, k: int
326
+ ) -> npt.NDArray[np.float32]:
327
+ """Decodes a block payload into a new array.
328
+
329
+ Args:
330
+ payload: The compressed payload.
331
+ codec: Codec name or id.
332
+ n: Frames in the block.
333
+ k: Floats per frame.
334
+
335
+ Returns:
336
+ Float32 array of shape ``[n, K]``.
337
+
338
+ Raises:
339
+ errors.FormatError: If the payload is corrupt or the codec unknown.
340
+ """
341
+ out = np.empty((n, k), np.float32)
342
+ decode_block_into(payload, codec, out)
343
+ return out
344
+
345
+
346
+ def decode_components(
347
+ payload: Payload,
348
+ codec: str | int,
349
+ n: int,
350
+ k: int,
351
+ components: slice | npt.NDArray[np.intp],
352
+ ) -> npt.NDArray[np.float32]:
353
+ """Decodes only some components of a block payload.
354
+
355
+ Payloads are component-major byte planes, so after inflating,
356
+ only the wanted planes are unshuffled and prefix-summed. Reading the 7
357
+ floats of one body out of 140 is about 20 times less work than
358
+ :func:`decode_block`. q16d quaternions are not renormalized.
359
+
360
+ Args:
361
+ payload: The compressed payload.
362
+ codec: Codec name or id from the block header.
363
+ n: Frames in the block.
364
+ k: Floats per frame of the stream.
365
+ components: The components to decode, as a slice or an index array.
366
+
367
+ Returns:
368
+ A new float32 array ``[n, c]``, ``c`` being the number of components.
369
+
370
+ Raises:
371
+ errors.FormatError: If the payload is corrupt or the codec unknown.
372
+ """
373
+ cid = codec_id(codec)
374
+ raw = inflate(payload, payload_ulen(cid, n, k), "block payload")
375
+ if cid == CODEC_F32S:
376
+ planes = np.frombuffer(raw, np.uint8).reshape(4, k, n)[:, components]
377
+ bits = np.empty((n, planes.shape[1]), np.uint32)
378
+ unshuffle_into(planes, bits)
379
+ delta_decode_inplace(bits)
380
+ return bits.view(np.float32)
381
+ lo = np.frombuffer(raw, np.float32, k, 0)[components]
382
+ step = np.frombuffer(raw, np.float32, k, 4 * k)[components]
383
+ planes = np.frombuffer(raw, np.uint8, 2 * k * n, 8 * k).reshape(2, k, n)
384
+ planes = planes[:, components]
385
+ q = np.empty((n, planes.shape[1]), np.uint16)
386
+ unshuffle_into(planes, q)
387
+ delta_decode_inplace(q)
388
+ return dequantize(q, lo, step)
389
+
390
+
391
+ # --------------------------------------------------------------------------
392
+ # Mesh blobs
393
+ # --------------------------------------------------------------------------
394
+
395
+ MESH_MAGIC = b"SSMH"
396
+ MESH_MAJOR = 1
397
+ MESH_MINOR = 0
398
+ MESH_HEADER = struct.Struct("<4sHHIIIB3xII")
399
+ MESH_FLAG_NORMALS = 1
400
+ MESH_FLAG_UVS = 2
401
+
402
+
403
+ def _mesh_codec_id(codec: str | int) -> int:
404
+ """Resolves a mesh codec name or id."""
405
+ if isinstance(codec, str):
406
+ if codec not in MESH_CODEC_IDS:
407
+ raise errors.FormatError(f"unknown mesh codec {codec!r}")
408
+ return MESH_CODEC_IDS[codec]
409
+ if codec not in MESH_CODEC_IDS.values():
410
+ raise errors.FormatError(f"unknown mesh codec id {codec}")
411
+ return int(codec)
412
+
413
+
414
+ def _q16_axes(
415
+ a: npt.NDArray[np.float32],
416
+ ) -> tuple[bytes, bytes, npt.NDArray[np.uint8]]:
417
+ """Quantizes ``[n, A]`` floats; returns lo bytes, step bytes, planes."""
418
+ n, axes = a.shape
419
+ if n == 0:
420
+ lo = np.zeros(axes, np.float32)
421
+ step = np.zeros(axes, np.float32)
422
+ return lo.tobytes(), step.tobytes(), np.empty((2, axes, 0), np.uint8)
423
+ lo, step = _q16_ranges(a)
424
+ if not (np.isfinite(lo).all() and np.isfinite(step).all()):
425
+ raise ValueError("q16 mesh cannot encode non-finite values")
426
+ q = _quantize(a, lo, step)
427
+ return lo.tobytes(), step.tobytes(), shuffle(delta_encode(q))
428
+
429
+
430
+ def _q16_axes_decode(
431
+ raw: bytes, offset: int, n: int, axes: int
432
+ ) -> tuple[npt.NDArray[np.float32], int]:
433
+ """Decodes ``lo, step, planes`` at ``offset``; returns data and new end."""
434
+ lo = np.frombuffer(raw, np.float32, axes, offset)
435
+ step = np.frombuffer(raw, np.float32, axes, offset + 4 * axes)
436
+ start = offset + 8 * axes
437
+ size = 2 * axes * n
438
+ planes = np.frombuffer(raw, np.uint8, size, start).reshape(2, axes, n)
439
+ q = np.empty((n, axes), np.uint16)
440
+ unshuffle_into(planes, q)
441
+ delta_decode_inplace(q)
442
+ return dequantize(q, lo, step), start + size
443
+
444
+
445
+ def encode_mesh(mesh: core.Mesh, codec: str | int = "raw") -> bytes:
446
+ """Encodes a mesh into a full SSMH blob (header plus payload).
447
+
448
+ Args:
449
+ mesh: The mesh to encode.
450
+ codec: ``"raw"`` (lossless, library storage) or ``"q16"`` (lossy
451
+ export; normals are dropped).
452
+
453
+ Returns:
454
+ The blob bytes.
455
+
456
+ Raises:
457
+ ValueError: If a q16 mesh has non-finite vertices.
458
+ errors.FormatError: If the codec is unknown.
459
+ """
460
+ cid = _mesh_codec_id(codec)
461
+ v = np.ascontiguousarray(mesh.vertices, dtype=np.float32)
462
+ f = np.ascontiguousarray(mesh.faces, dtype=np.uint32)
463
+ uvs = mesh.uvs
464
+ flags = MESH_FLAG_UVS if uvs is not None else 0
465
+ comp = zlib.compressobj(DEFLATE_LEVEL, zlib.DEFLATED, -15)
466
+ parts: list[bytes] = []
467
+ ulen = 0
468
+
469
+ def feed(data: bytes | npt.NDArray) -> None:
470
+ nonlocal ulen
471
+ ulen += memoryview(data).nbytes
472
+ parts.append(comp.compress(data))
473
+
474
+ if cid == MESH_RAW:
475
+ feed(v)
476
+ feed(f)
477
+ if mesh.normals is not None:
478
+ flags |= MESH_FLAG_NORMALS
479
+ feed(np.ascontiguousarray(mesh.normals, dtype=np.float32))
480
+ if uvs is not None:
481
+ feed(np.ascontiguousarray(uvs, dtype=np.float32))
482
+ else:
483
+ lo, step, planes = _q16_axes(v)
484
+ feed(lo)
485
+ feed(step)
486
+ feed(planes)
487
+ fplanes = shuffle(delta_encode(f.reshape(-1, 1)))
488
+ feed(fplanes)
489
+ if uvs is not None:
490
+ lo, step, planes = _q16_axes(
491
+ np.ascontiguousarray(uvs, dtype=np.float32)
492
+ )
493
+ feed(lo)
494
+ feed(step)
495
+ feed(planes)
496
+ parts.append(comp.flush())
497
+ payload = b"".join(parts)
498
+ header = MESH_HEADER.pack(
499
+ MESH_MAGIC,
500
+ MESH_MAJOR,
501
+ MESH_MINOR,
502
+ len(v),
503
+ len(f),
504
+ flags,
505
+ cid,
506
+ ulen,
507
+ zlib.crc32(payload),
508
+ )
509
+ return header + payload
510
+
511
+
512
+ def decode_mesh(blob: Payload) -> core.Mesh:
513
+ """Decodes a full SSMH blob into a mesh.
514
+
515
+ Args:
516
+ blob: The blob bytes (header plus payload).
517
+
518
+ Returns:
519
+ The mesh. A q16 blob has no normals.
520
+
521
+ Raises:
522
+ errors.FormatError: On a bad magic, unknown major version or codec,
523
+ CRC mismatch, or corrupt payload.
524
+ """
525
+ view = memoryview(blob)
526
+ if view.nbytes < MESH_HEADER.size:
527
+ raise errors.FormatError("mesh blob shorter than its header")
528
+ magic, major, _minor, nv, nf, flags, cid, ulen, crc = (
529
+ MESH_HEADER.unpack_from(view, 0)
530
+ )
531
+ if magic != MESH_MAGIC:
532
+ raise errors.FormatError(f"bad mesh magic {bytes(magic)!r}")
533
+ if major != MESH_MAJOR:
534
+ raise errors.FormatError(f"unknown mesh major version {major}")
535
+ if cid not in MESH_CODEC_IDS.values():
536
+ raise errors.FormatError(f"unknown mesh codec id {cid}")
537
+ payload = view[MESH_HEADER.size :]
538
+ if zlib.crc32(payload) != crc:
539
+ raise errors.FormatError("mesh payload CRC mismatch")
540
+ raw = inflate(payload, ulen, "mesh payload")
541
+ normals = uvs = None
542
+ if cid == MESH_RAW:
543
+ pos = 0
544
+ verts = np.frombuffer(raw, np.float32, 3 * nv, pos).reshape(nv, 3)
545
+ pos += 12 * nv
546
+ faces = np.frombuffer(raw, np.uint32, 3 * nf, pos).reshape(nf, 3)
547
+ pos += 12 * nf
548
+ if flags & MESH_FLAG_NORMALS:
549
+ normals = np.frombuffer(raw, np.float32, 3 * nv, pos)
550
+ normals = normals.reshape(nv, 3)
551
+ pos += 12 * nv
552
+ if flags & MESH_FLAG_UVS:
553
+ uvs = np.frombuffer(raw, np.float32, 2 * nv, pos).reshape(nv, 2)
554
+ else:
555
+ verts, pos = _q16_axes_decode(raw, 0, nv, 3)
556
+ planes = np.frombuffer(raw, np.uint8, 12 * nf, pos).reshape(
557
+ 4, 1, 3 * nf
558
+ )
559
+ pos += 12 * nf
560
+ flat = np.empty((3 * nf, 1), np.uint32)
561
+ unshuffle_into(planes, flat)
562
+ delta_decode_inplace(flat)
563
+ faces = flat.reshape(nf, 3)
564
+ if flags & MESH_FLAG_UVS:
565
+ uvs, _ = _q16_axes_decode(raw, pos, nv, 2)
566
+ return core.Mesh(verts, faces, normals, uvs)
simscope/io/errors.py ADDED
@@ -0,0 +1,9 @@
1
+ """Exceptions raised by the storage layer."""
2
+
3
+
4
+ class FormatError(ValueError):
5
+ """Raised when bytes on disk are not a simscope file.
6
+
7
+ Examples are a bad magic, an unknown major version, an unknown codec id,
8
+ a CRC mismatch, or a truncated file.
9
+ """