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/cli.py
ADDED
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
"""The ``simscope`` command line.
|
|
2
|
+
|
|
3
|
+
Commands are ``serve``, ``ls``, ``export``, ``rename``, ``pack``, ``import``,
|
|
4
|
+
``recover``, and ``info``. Everything except ``serve`` works without the
|
|
5
|
+
viewer extra. ``serve`` imports the server lazily, so the other commands
|
|
6
|
+
never load starlette, uvicorn, or watchfiles.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import dataclasses
|
|
11
|
+
import importlib
|
|
12
|
+
import json
|
|
13
|
+
import logging
|
|
14
|
+
import pathlib
|
|
15
|
+
import sys
|
|
16
|
+
import time
|
|
17
|
+
from collections.abc import Callable, Sequence
|
|
18
|
+
|
|
19
|
+
from simscope import export, importers, library
|
|
20
|
+
from simscope.io import errors, manifest
|
|
21
|
+
|
|
22
|
+
_SORTS = ("created", "name", "n_frames", "rating")
|
|
23
|
+
_RIGHT = frozenset({"FRAMES", "ENVS", "SECONDS", "RATING"})
|
|
24
|
+
_SERVER_DEPS = ("starlette", "uvicorn", "watchfiles")
|
|
25
|
+
_VIEWER_HINT = "the viewer is not installed: pip install 'simscope[viewer]'"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class UserError(Exception):
|
|
29
|
+
"""A problem the user can fix; ``main`` prints it and exits with 1."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# -- helpers --
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _library_root(path: str) -> pathlib.Path:
|
|
36
|
+
"""Returns the library folder, which must exist.
|
|
37
|
+
|
|
38
|
+
Raises:
|
|
39
|
+
UserError: If ``path`` is not a directory.
|
|
40
|
+
"""
|
|
41
|
+
root = pathlib.Path(path)
|
|
42
|
+
if not root.is_dir():
|
|
43
|
+
raise UserError(f"no library folder at {root}")
|
|
44
|
+
return root
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _check_runs(root: pathlib.Path, names: Sequence[str]) -> None:
|
|
48
|
+
"""Checks that every name is a valid, existing run of the library.
|
|
49
|
+
|
|
50
|
+
Raises:
|
|
51
|
+
UserError: On a bad name, a missing run, or a duplicate.
|
|
52
|
+
"""
|
|
53
|
+
if len(set(names)) != len(names):
|
|
54
|
+
raise UserError("duplicate run names")
|
|
55
|
+
for name in names:
|
|
56
|
+
try:
|
|
57
|
+
manifest.validate_run_name(name)
|
|
58
|
+
except ValueError as exc:
|
|
59
|
+
raise UserError(str(exc)) from exc
|
|
60
|
+
run_dir = root / "runs" / name
|
|
61
|
+
if not any(
|
|
62
|
+
(run_dir / f).is_file()
|
|
63
|
+
for f in (manifest.MANIFEST_NAME, manifest.PARTIAL_NAME)
|
|
64
|
+
):
|
|
65
|
+
raise UserError(f"no run {name!r} in {root}")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _human(n: float) -> str:
|
|
69
|
+
"""Formats a byte count, for example ``1.5 MB``."""
|
|
70
|
+
for unit in ("B", "KB", "MB", "GB"):
|
|
71
|
+
if n < 1024 or unit == "GB":
|
|
72
|
+
return f"{n:.0f} {unit}" if unit == "B" else f"{n:.1f} {unit}"
|
|
73
|
+
n /= 1024
|
|
74
|
+
raise AssertionError("unreachable") # pragma: no cover
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _table(header: Sequence[str], rows: Sequence[Sequence[str]]) -> str:
|
|
78
|
+
"""Formats rows as an aligned table; columns named in ``_RIGHT`` right."""
|
|
79
|
+
cols = list(zip(header, *rows, strict=False))
|
|
80
|
+
widths = [max(len(cell) for cell in col) for col in cols]
|
|
81
|
+
right = {i for i, h in enumerate(header) if h in _RIGHT}
|
|
82
|
+
|
|
83
|
+
def line(cells: Sequence[str]) -> str:
|
|
84
|
+
parts = [
|
|
85
|
+
c.rjust(w) if i in right else c.ljust(w)
|
|
86
|
+
for i, (c, w) in enumerate(zip(cells, widths, strict=True))
|
|
87
|
+
]
|
|
88
|
+
return " ".join(parts).rstrip()
|
|
89
|
+
|
|
90
|
+
return "\n".join([line(header), *(line(r) for r in rows)])
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# -- commands --
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _cmd_serve(args: argparse.Namespace) -> int:
|
|
97
|
+
root = _library_root(args.dir)
|
|
98
|
+
try:
|
|
99
|
+
backend = importlib.import_module("simscope.server")
|
|
100
|
+
except ModuleNotFoundError as exc:
|
|
101
|
+
if (exc.name or "").split(".")[0] not in _SERVER_DEPS:
|
|
102
|
+
raise
|
|
103
|
+
raise UserError(_VIEWER_HINT) from exc
|
|
104
|
+
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
|
105
|
+
print(
|
|
106
|
+
f"serving {root} on http://{args.host}:{args.port} (Ctrl-C to stop)",
|
|
107
|
+
flush=True,
|
|
108
|
+
)
|
|
109
|
+
backend.serve(root, host=args.host, port=args.port, author=args.author)
|
|
110
|
+
return 0
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _cmd_ls(args: argparse.Namespace) -> int:
|
|
114
|
+
root = _library_root(args.dir)
|
|
115
|
+
lib = library.Library(root)
|
|
116
|
+
try:
|
|
117
|
+
rows = lib.query(
|
|
118
|
+
tags=args.tag or (),
|
|
119
|
+
favorite=True if args.favorite else None,
|
|
120
|
+
status=args.status,
|
|
121
|
+
sort=args.sort,
|
|
122
|
+
descending=args.sort != "name",
|
|
123
|
+
limit=args.limit,
|
|
124
|
+
)
|
|
125
|
+
finally:
|
|
126
|
+
lib.close()
|
|
127
|
+
if args.json:
|
|
128
|
+
for info in rows:
|
|
129
|
+
print(json.dumps(dataclasses.asdict(info), sort_keys=True))
|
|
130
|
+
return 0
|
|
131
|
+
if not rows:
|
|
132
|
+
print("no rollouts", file=sys.stderr)
|
|
133
|
+
return 0
|
|
134
|
+
header = (
|
|
135
|
+
"NAME",
|
|
136
|
+
"STATUS",
|
|
137
|
+
"FRAMES",
|
|
138
|
+
"ENVS",
|
|
139
|
+
"SECONDS",
|
|
140
|
+
"CREATED",
|
|
141
|
+
"FAV",
|
|
142
|
+
"RATING",
|
|
143
|
+
"TAGS",
|
|
144
|
+
)
|
|
145
|
+
body = [
|
|
146
|
+
(
|
|
147
|
+
r.name,
|
|
148
|
+
r.status,
|
|
149
|
+
str(r.n_frames),
|
|
150
|
+
str(r.n_envs),
|
|
151
|
+
f"{r.n_frames * r.dt:.2f}",
|
|
152
|
+
r.created,
|
|
153
|
+
"*" if r.favorite else "",
|
|
154
|
+
"-" if r.rating is None else f"{r.rating:.1f}",
|
|
155
|
+
",".join(r.tags),
|
|
156
|
+
)
|
|
157
|
+
for r in rows
|
|
158
|
+
]
|
|
159
|
+
print(_table(header, body))
|
|
160
|
+
return 0
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _cmd_export(args: argparse.Namespace) -> int:
|
|
164
|
+
root = _library_root(args.dir)
|
|
165
|
+
_check_runs(root, args.runs)
|
|
166
|
+
layout = args.layout or ("single" if len(args.runs) == 1 else "grid")
|
|
167
|
+
if args.arrange and layout != "compare":
|
|
168
|
+
raise UserError("--arrange needs --layout compare")
|
|
169
|
+
try:
|
|
170
|
+
out = export.export_html(
|
|
171
|
+
root,
|
|
172
|
+
args.runs,
|
|
173
|
+
args.output,
|
|
174
|
+
title=args.title,
|
|
175
|
+
layout=layout,
|
|
176
|
+
arrange=args.arrange,
|
|
177
|
+
transcode=not args.no_transcode,
|
|
178
|
+
annotations=not args.no_annotations,
|
|
179
|
+
ui=args.ui,
|
|
180
|
+
envs=args.envs,
|
|
181
|
+
)
|
|
182
|
+
except ValueError as exc: # still recording, bad layout or envs
|
|
183
|
+
raise UserError(str(exc)) from exc
|
|
184
|
+
print(f"wrote {out} ({_human(out.stat().st_size)})")
|
|
185
|
+
return 0
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _cmd_rename(args: argparse.Namespace) -> int:
|
|
189
|
+
root = _library_root(args.dir)
|
|
190
|
+
try:
|
|
191
|
+
library.Library(root).rename(args.old, args.new)
|
|
192
|
+
except (ValueError, FileNotFoundError, FileExistsError, OSError) as exc:
|
|
193
|
+
raise UserError(str(exc)) from exc
|
|
194
|
+
print(f"renamed {args.old} to {args.new}")
|
|
195
|
+
return 0
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _cmd_pack(args: argparse.Namespace) -> int:
|
|
199
|
+
root = _library_root(args.dir)
|
|
200
|
+
_check_runs(root, args.runs)
|
|
201
|
+
try:
|
|
202
|
+
out = export.export_pack(
|
|
203
|
+
root,
|
|
204
|
+
args.runs,
|
|
205
|
+
args.output,
|
|
206
|
+
transcode=not args.no_transcode,
|
|
207
|
+
annotations=not args.no_annotations,
|
|
208
|
+
)
|
|
209
|
+
except ValueError as exc: # still recording
|
|
210
|
+
raise UserError(str(exc)) from exc
|
|
211
|
+
print(f"wrote {out} ({_human(out.stat().st_size)})")
|
|
212
|
+
return 0
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _tree_bytes(root: pathlib.Path) -> int:
|
|
216
|
+
"""Total size of the data folders of a library (not the index cache)."""
|
|
217
|
+
total = 0
|
|
218
|
+
for sub in ("assets", "scenes", "runs"):
|
|
219
|
+
for path in (root / sub).rglob("*"):
|
|
220
|
+
if path.is_file():
|
|
221
|
+
total += path.stat().st_size
|
|
222
|
+
return total
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _cmd_import(args: argparse.Namespace) -> int:
|
|
226
|
+
root = pathlib.Path(args.dir)
|
|
227
|
+
if root.exists() and not root.is_dir():
|
|
228
|
+
raise UserError(f"{root} is not a folder")
|
|
229
|
+
files = importers.find_importable(args.paths)
|
|
230
|
+
if not files:
|
|
231
|
+
raise UserError("nothing to import: no .rbundle or Brax HTML files")
|
|
232
|
+
lib = library.Library(root)
|
|
233
|
+
before = _tree_bytes(root)
|
|
234
|
+
started = time.perf_counter()
|
|
235
|
+
imported = failed = 0
|
|
236
|
+
try:
|
|
237
|
+
for res in importers.import_files(
|
|
238
|
+
lib,
|
|
239
|
+
files,
|
|
240
|
+
tags=args.tag or (),
|
|
241
|
+
overwrite=args.overwrite,
|
|
242
|
+
jobs=args.jobs,
|
|
243
|
+
):
|
|
244
|
+
if res.name is None:
|
|
245
|
+
failed += 1
|
|
246
|
+
print(f"skipped {res.path}: {res.error}", file=sys.stderr)
|
|
247
|
+
continue
|
|
248
|
+
imported += 1
|
|
249
|
+
print(f"{res.name} {res.n_frames} frames <- {res.path}")
|
|
250
|
+
lib.refresh()
|
|
251
|
+
finally:
|
|
252
|
+
lib.close()
|
|
253
|
+
if not imported:
|
|
254
|
+
raise UserError("nothing imported")
|
|
255
|
+
src_bytes = sum(p.stat().st_size for p, _ in files)
|
|
256
|
+
added = _tree_bytes(root) - before
|
|
257
|
+
print(
|
|
258
|
+
f"imported {imported} rollouts"
|
|
259
|
+
+ (f", skipped {failed}" if failed else "")
|
|
260
|
+
+ f" in {time.perf_counter() - started:.1f} s: input"
|
|
261
|
+
f" {_human(src_bytes)}, library grew by {_human(added)}"
|
|
262
|
+
f" ({added / max(src_bytes, 1):.0%})"
|
|
263
|
+
)
|
|
264
|
+
return 0
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _cmd_recover(args: argparse.Namespace) -> int:
|
|
268
|
+
root = _library_root(args.dir)
|
|
269
|
+
_check_runs(root, [args.run])
|
|
270
|
+
lib = library.Library(root)
|
|
271
|
+
try:
|
|
272
|
+
report = lib.recover(args.run)
|
|
273
|
+
finally:
|
|
274
|
+
lib.close()
|
|
275
|
+
if report.already_complete:
|
|
276
|
+
print(f"{report.name}: already complete, {report.n_frames} frames")
|
|
277
|
+
else:
|
|
278
|
+
print(
|
|
279
|
+
f"{report.name}: recovered {report.n_frames} frames"
|
|
280
|
+
f" (dropped {report.dropped_frames} from longer streams)"
|
|
281
|
+
)
|
|
282
|
+
return 0
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _cmd_info(args: argparse.Namespace) -> int:
|
|
286
|
+
root = _library_root(args.dir)
|
|
287
|
+
_check_runs(root, [args.run])
|
|
288
|
+
lib = library.Library(root)
|
|
289
|
+
try:
|
|
290
|
+
with lib.open(args.run) as run:
|
|
291
|
+
if run.is_recording:
|
|
292
|
+
run.refresh()
|
|
293
|
+
print(_describe(run))
|
|
294
|
+
finally:
|
|
295
|
+
lib.close()
|
|
296
|
+
return 0
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _describe(run: library.Rollout) -> str:
|
|
300
|
+
"""Formats the manifest summary of a run for ``info``."""
|
|
301
|
+
m = run.manifest
|
|
302
|
+
frames = run.n_frames
|
|
303
|
+
status = m.status
|
|
304
|
+
if run.is_recording:
|
|
305
|
+
status += f" ({frames} frames written so far)"
|
|
306
|
+
files = [p for p in run.path.rglob("*") if p.is_file()]
|
|
307
|
+
lines = [
|
|
308
|
+
("name", m.name),
|
|
309
|
+
("id", m.id),
|
|
310
|
+
("status", status),
|
|
311
|
+
("created", m.created),
|
|
312
|
+
("frames", str(frames)),
|
|
313
|
+
("envs", str(m.n_envs)),
|
|
314
|
+
("bodies", str(m.n_bodies)),
|
|
315
|
+
("dt", f"{m.dt:g} s ({1 / m.dt:.4g} Hz)"),
|
|
316
|
+
("duration", f"{frames * m.dt:.2f} s"),
|
|
317
|
+
("scene", m.scene.sha256),
|
|
318
|
+
("source", ", ".join(f"{k} {v}" for k, v in m.source.items()) or "-"),
|
|
319
|
+
("tags", ", ".join(m.tags) or "-"),
|
|
320
|
+
("on disk", _human(sum(p.stat().st_size for p in files))),
|
|
321
|
+
]
|
|
322
|
+
width = max(len(k) for k, _ in lines)
|
|
323
|
+
out = [f"{k.ljust(width)} {v}" for k, v in lines]
|
|
324
|
+
header = ("STREAM", "KIND", "SHAPE", "FILE", "SIZE")
|
|
325
|
+
rows = []
|
|
326
|
+
for name, info in m.streams.items():
|
|
327
|
+
path = run.path / info.file
|
|
328
|
+
size = path.stat().st_size if path.exists() else 0
|
|
329
|
+
rows.append(
|
|
330
|
+
(
|
|
331
|
+
name,
|
|
332
|
+
info.kind,
|
|
333
|
+
"x".join(map(str, info.item_shape)) or "scalar",
|
|
334
|
+
info.file,
|
|
335
|
+
_human(size),
|
|
336
|
+
)
|
|
337
|
+
)
|
|
338
|
+
return "\n".join([*out, "", _table(header, rows)])
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
# -- parser --
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _port(text: str) -> int:
|
|
345
|
+
"""Parses a TCP port for argparse."""
|
|
346
|
+
value = int(text)
|
|
347
|
+
if not 0 <= value <= 65535:
|
|
348
|
+
raise argparse.ArgumentTypeError(f"port out of range: {text}")
|
|
349
|
+
return value
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _positive(text: str) -> int:
|
|
353
|
+
"""Parses a positive integer for argparse."""
|
|
354
|
+
value = int(text)
|
|
355
|
+
if value < 1:
|
|
356
|
+
raise argparse.ArgumentTypeError(f"must be at least 1: {text}")
|
|
357
|
+
return value
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _env_list(text: str) -> list[int]:
|
|
361
|
+
"""Parses comma-separated env indices for argparse."""
|
|
362
|
+
try:
|
|
363
|
+
envs = [int(part) for part in text.split(",")]
|
|
364
|
+
except ValueError:
|
|
365
|
+
raise argparse.ArgumentTypeError(
|
|
366
|
+
f"expected comma-separated env indices: {text}"
|
|
367
|
+
) from None
|
|
368
|
+
if any(e < 0 for e in envs):
|
|
369
|
+
raise argparse.ArgumentTypeError(f"env indices are >= 0: {text}")
|
|
370
|
+
return envs
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
374
|
+
"""Builds the argument parser with one subparser per command."""
|
|
375
|
+
parser = argparse.ArgumentParser(
|
|
376
|
+
prog="simscope",
|
|
377
|
+
description="Record and replay robot simulation rollouts.",
|
|
378
|
+
)
|
|
379
|
+
sub = parser.add_subparsers(dest="command", required=True, metavar="CMD")
|
|
380
|
+
|
|
381
|
+
def add(name: str, func: Callable, help_: str) -> argparse.ArgumentParser:
|
|
382
|
+
p = sub.add_parser(name, help=help_, description=help_)
|
|
383
|
+
p.set_defaults(func=func)
|
|
384
|
+
p.add_argument("dir", metavar="DIR", help="the library folder")
|
|
385
|
+
return p
|
|
386
|
+
|
|
387
|
+
p = add("serve", _cmd_serve, "browse and replay rollouts in the browser")
|
|
388
|
+
p.add_argument("--host", default="127.0.0.1")
|
|
389
|
+
p.add_argument("--port", type=_port, default=8080)
|
|
390
|
+
p.add_argument("--author", help="author of new annotations")
|
|
391
|
+
|
|
392
|
+
p = add("ls", _cmd_ls, "list rollouts")
|
|
393
|
+
p.add_argument("--tag", action="append", help="require a tag (repeatable)")
|
|
394
|
+
p.add_argument(
|
|
395
|
+
"--favorite", action="store_true", help="only pinned rollouts"
|
|
396
|
+
)
|
|
397
|
+
p.add_argument("--status", help="recording, complete, or a curation status")
|
|
398
|
+
p.add_argument("--sort", choices=_SORTS, default="created")
|
|
399
|
+
p.add_argument("--limit", type=_positive, help="at most N rollouts")
|
|
400
|
+
p.add_argument("--json", action="store_true", help="one object per line")
|
|
401
|
+
|
|
402
|
+
p = add(
|
|
403
|
+
"export", _cmd_export, "export rollouts as one self-contained HTML file"
|
|
404
|
+
)
|
|
405
|
+
p.add_argument("runs", nargs="+", metavar="RUN")
|
|
406
|
+
p.add_argument("-o", "--output", required=True, metavar="OUT.html")
|
|
407
|
+
p.add_argument("--layout", choices=("single", "grid", "compare"))
|
|
408
|
+
p.add_argument(
|
|
409
|
+
"--arrange",
|
|
410
|
+
choices=export.ARRANGEMENTS,
|
|
411
|
+
help="compare panes: side (default for 2 rollouts), stack, or grid "
|
|
412
|
+
"(default for 3 or 4); needs --layout compare",
|
|
413
|
+
)
|
|
414
|
+
p.add_argument("--title")
|
|
415
|
+
p.add_argument(
|
|
416
|
+
"--ui",
|
|
417
|
+
choices=("lean", "full"),
|
|
418
|
+
default="lean",
|
|
419
|
+
help="lean: player elements; full: the whole app (default lean)",
|
|
420
|
+
)
|
|
421
|
+
p.add_argument(
|
|
422
|
+
"--envs",
|
|
423
|
+
type=_env_list,
|
|
424
|
+
metavar="I,J,...",
|
|
425
|
+
help="export only these envs (comma-separated indices)",
|
|
426
|
+
)
|
|
427
|
+
p.add_argument("--no-transcode", action="store_true")
|
|
428
|
+
p.add_argument("--no-annotations", action="store_true")
|
|
429
|
+
|
|
430
|
+
p = add("rename", _cmd_rename, "rename a rollout")
|
|
431
|
+
p.add_argument("old", metavar="OLD", help="the rollout's current name")
|
|
432
|
+
p.add_argument("new", metavar="NEW", help="its new name")
|
|
433
|
+
|
|
434
|
+
p = add("pack", _cmd_pack, "write rollouts to a .simscope pack")
|
|
435
|
+
p.add_argument("runs", nargs="+", metavar="RUN")
|
|
436
|
+
p.add_argument("-o", "--output", required=True, metavar="OUT.simscope")
|
|
437
|
+
p.add_argument("--no-transcode", action="store_true")
|
|
438
|
+
p.add_argument("--no-annotations", action="store_true")
|
|
439
|
+
|
|
440
|
+
p = add("import", _cmd_import, "import .rbundle files and Brax HTML pages")
|
|
441
|
+
p.add_argument("paths", nargs="+", metavar="PATH", help="files or folders")
|
|
442
|
+
p.add_argument("--tag", action="append", help="add a tag (repeatable)")
|
|
443
|
+
p.add_argument(
|
|
444
|
+
"--overwrite", action="store_true", help="replace an existing rollout"
|
|
445
|
+
)
|
|
446
|
+
p.add_argument("--jobs", type=_positive, help="worker processes")
|
|
447
|
+
|
|
448
|
+
p = add("recover", _cmd_recover, "repair a rollout whose recording stopped")
|
|
449
|
+
p.add_argument("run", metavar="RUN")
|
|
450
|
+
|
|
451
|
+
p = add("info", _cmd_info, "summarize a rollout")
|
|
452
|
+
p.add_argument("run", metavar="RUN")
|
|
453
|
+
return parser
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
457
|
+
"""Runs the command line.
|
|
458
|
+
|
|
459
|
+
Args:
|
|
460
|
+
argv: Arguments without the program name; ``sys.argv[1:]`` if
|
|
461
|
+
``None``.
|
|
462
|
+
|
|
463
|
+
Returns:
|
|
464
|
+
The exit status: 0 on success, 1 for a problem the user can fix (a
|
|
465
|
+
missing run, a bad name; one line on stderr), 2 for a usage error,
|
|
466
|
+
130 when interrupted.
|
|
467
|
+
"""
|
|
468
|
+
parser = _build_parser()
|
|
469
|
+
try:
|
|
470
|
+
args = parser.parse_args(argv)
|
|
471
|
+
except SystemExit as exc: # argparse exits on --help and usage errors
|
|
472
|
+
return exc.code if isinstance(exc.code, int) else 2
|
|
473
|
+
try:
|
|
474
|
+
return args.func(args)
|
|
475
|
+
except UserError as exc:
|
|
476
|
+
print(f"simscope: error: {exc}", file=sys.stderr)
|
|
477
|
+
return 1
|
|
478
|
+
except (FileNotFoundError, errors.FormatError) as exc:
|
|
479
|
+
print(f"simscope: error: {exc}", file=sys.stderr)
|
|
480
|
+
return 1
|
|
481
|
+
except KeyboardInterrupt:
|
|
482
|
+
return 130
|