simhub 0.1.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 (48) hide show
  1. simhub-0.1.0/PKG-INFO +514 -0
  2. simhub-0.1.0/README.sdk.md +477 -0
  3. simhub-0.1.0/pyproject.toml +163 -0
  4. simhub-0.1.0/src/simhub/__init__.py +239 -0
  5. simhub-0.1.0/src/simhub/asset_cli.py +58 -0
  6. simhub-0.1.0/src/simhub/base.py +564 -0
  7. simhub-0.1.0/src/simhub/batch.py +334 -0
  8. simhub-0.1.0/src/simhub/blender.py +860 -0
  9. simhub-0.1.0/src/simhub/blender_replay.py +144 -0
  10. simhub-0.1.0/src/simhub/checks.py +415 -0
  11. simhub-0.1.0/src/simhub/cleaning.py +92 -0
  12. simhub-0.1.0/src/simhub/client.py +569 -0
  13. simhub-0.1.0/src/simhub/datasets.py +304 -0
  14. simhub-0.1.0/src/simhub/emit.py +283 -0
  15. simhub-0.1.0/src/simhub/env.py +264 -0
  16. simhub-0.1.0/src/simhub/errors.py +116 -0
  17. simhub-0.1.0/src/simhub/evaluation.py +117 -0
  18. simhub-0.1.0/src/simhub/hf.py +130 -0
  19. simhub-0.1.0/src/simhub/ids.py +29 -0
  20. simhub-0.1.0/src/simhub/kinematics.py +205 -0
  21. simhub-0.1.0/src/simhub/mjcf.py +665 -0
  22. simhub-0.1.0/src/simhub/model_cli.py +83 -0
  23. simhub-0.1.0/src/simhub/models.py +233 -0
  24. simhub-0.1.0/src/simhub/named_assets.py +365 -0
  25. simhub-0.1.0/src/simhub/notebook.py +237 -0
  26. simhub-0.1.0/src/simhub/objects.py +180 -0
  27. simhub-0.1.0/src/simhub/openfoam.py +231 -0
  28. simhub-0.1.0/src/simhub/particulate.py +508 -0
  29. simhub-0.1.0/src/simhub/policies.py +152 -0
  30. simhub-0.1.0/src/simhub/project.py +427 -0
  31. simhub-0.1.0/src/simhub/recording.py +686 -0
  32. simhub-0.1.0/src/simhub/registry.py +101 -0
  33. simhub-0.1.0/src/simhub/replay.py +116 -0
  34. simhub-0.1.0/src/simhub/report.py +403 -0
  35. simhub-0.1.0/src/simhub/robots.py +349 -0
  36. simhub-0.1.0/src/simhub/rollout.py +613 -0
  37. simhub-0.1.0/src/simhub/run_cli.py +229 -0
  38. simhub-0.1.0/src/simhub/runner.py +535 -0
  39. simhub-0.1.0/src/simhub/schema.py +93 -0
  40. simhub-0.1.0/src/simhub/scorer.py +481 -0
  41. simhub-0.1.0/src/simhub/session.py +507 -0
  42. simhub-0.1.0/src/simhub/single.py +205 -0
  43. simhub-0.1.0/src/simhub/spec.py +352 -0
  44. simhub-0.1.0/src/simhub/studio.py +312 -0
  45. simhub-0.1.0/src/simhub/suite.py +237 -0
  46. simhub-0.1.0/src/simhub/task.py +628 -0
  47. simhub-0.1.0/src/simhub/types.py +518 -0
  48. simhub-0.1.0/src/simhub/viewer.py +444 -0
simhub-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,514 @@
1
+ Metadata-Version: 2.5
2
+ Name: simhub
3
+ Version: 0.1.0
4
+ Summary: Define an environment, a policy and a task in Python; run them on hosted GPUs
5
+ Project-URL: Homepage, https://github.com/General-Deployment/sim
6
+ License: Proprietary
7
+ Keywords: evaluation,mujoco,reinforcement-learning,robotics,simulation
8
+ Requires-Python: >=3.9
9
+ Requires-Dist: httpx>=0.27
10
+ Requires-Dist: numpy>=1.24
11
+ Requires-Dist: pydantic>=2.7
12
+ Requires-Dist: tomli>=2.0; python_version < '3.11'
13
+ Provides-Extra: all
14
+ Requires-Dist: huggingface-hub>=0.26; extra == 'all'
15
+ Requires-Dist: imageio-ffmpeg>=0.5; extra == 'all'
16
+ Requires-Dist: ipython>=8.0; extra == 'all'
17
+ Requires-Dist: matplotlib>=3.7; extra == 'all'
18
+ Requires-Dist: mujoco==3.11.*; (python_version >= '3.10') and extra == 'all'
19
+ Requires-Dist: pandas>=2.0; extra == 'all'
20
+ Requires-Dist: pillow>=10.0; extra == 'all'
21
+ Requires-Dist: pyarrow>=15.0; extra == 'all'
22
+ Provides-Extra: dataset
23
+ Requires-Dist: pyarrow>=15.0; extra == 'dataset'
24
+ Provides-Extra: mujoco
25
+ Requires-Dist: mujoco==3.11.*; (python_version >= '3.10') and extra == 'mujoco'
26
+ Provides-Extra: notebook
27
+ Requires-Dist: ipython>=8.0; extra == 'notebook'
28
+ Requires-Dist: matplotlib>=3.7; extra == 'notebook'
29
+ Requires-Dist: pandas>=2.0; extra == 'notebook'
30
+ Requires-Dist: pillow>=10.0; extra == 'notebook'
31
+ Provides-Extra: video
32
+ Requires-Dist: imageio-ffmpeg>=0.5; extra == 'video'
33
+ Requires-Dist: pillow>=10.0; extra == 'video'
34
+ Provides-Extra: weights
35
+ Requires-Dist: huggingface-hub>=0.26; extra == 'weights'
36
+ Description-Content-Type: text/markdown
37
+
38
+ # simhub
39
+
40
+ Upload reusable robots with `simhub asset upload robot.urdf --env robots --name arm`,
41
+ then load them in hosted projects with `simhub.asset("arm").path`.
42
+ See [named URDF and mesh assets](docs/named-assets.md) for dependency packaging,
43
+ project aliases, and version pinning.
44
+
45
+ The [single-file house-ceiling example](../examples/asbestos_ceiling.py) defines a
46
+ photo-derived tracked robot base, two articulated tool arms, a domestic scene, a registered
47
+ policy, a frozen evaluation and the Cycles scene in one Python file:
48
+
49
+ ```bash
50
+ PYTHONPATH=simhub/src:. python examples/asbestos_ceiling.py evaluate
51
+ PYTHONPATH=simhub/src:. python examples/asbestos_ceiling.py render
52
+ PYTHONPATH=simhub/src:. python examples/asbestos_ceiling.py export-model
53
+ ```
54
+
55
+ The example now uses `simhub.particulate`: two-layer moisture penetration,
56
+ contact-work-driven detachment, finite shroud capture, three fibre size bins and
57
+ conservative 3D advection/diffusion/settling. All coating, head, particle and
58
+ ventilation assumptions remain in the single file. Mass is tracked in kg of the
59
+ constituent, including constituent still bound in fragments. Hosted delivery solves the room velocity with OpenFOAM; local material-only runs
60
+ use a prescribed velocity. Neither mode resolves shroud-scale flow or predicts
61
+ calibrated asbestos clearance.
62
+
63
+ ```bash
64
+ PYTHONPATH=simhub/src:. python examples/asbestos_ceiling.py material-checks
65
+ PYTHONPATH=simhub/src:. python examples/asbestos_ceiling.py diagnostics
66
+ ```
67
+
68
+ `material-checks` runs controlled parameter interventions and analytic/grid/time
69
+ checks; `diagnostics` renders the recorded state as a scientific animation and
70
+ requires Matplotlib plus ffmpeg. New outputs default to
71
+ `results/asbestos_ceiling_v3`; earlier outputs remain preserved.
72
+ Evaluation retains the original design targets and reports failures without
73
+ changing the physical parameters to obtain a pass.
74
+
75
+ For other scenes, `simhub.render_cycles(project_directory, trajectory_directory,
76
+ output_directory, program="scene.py")` invokes Blender locally. A scene program
77
+ defines `build_scene(recorded, config)` and returns a mapping of recorded body
78
+ names to unparented Blender objects plus an `update(index, fraction)` callback.
79
+ The callback can animate recorded fields and cameras; it must not rerun a policy.
80
+ Blender uses the best available Cycles GPU backend, with a CPU fallback.
81
+
82
+ `Recorder.channel(name, sampler)` records fixed-shape finite numeric fields
83
+ alongside body poses. `simhub.SurfaceContamination` and `simhub.CleaningTool`
84
+ provide a mass-conserving surface-transfer surrogate. The ceiling example records
85
+ remaining coating, surface and bond moisture, size-resolved airborne concentration
86
+ and seven mass inventories through this channel API, so the render and evaluation
87
+ read the same state. `AirTransport`, `CoatingParameters`, `ExtractionContact`,
88
+ `ParticleClass` and `WetCoating` are also exported directly from `simhub`. The hosted Cycles worker uses the same replay
89
+ entry point when supplied a Python bundle and `params.program` (use
90
+ `"__init__.py"` for a bundled single-file project). The worker must have Blender
91
+ and ffmpeg available; no cloud deployment is performed by the local commands.
92
+
93
+ Define an environment, a policy and a task in Python. Run one episode on your
94
+ laptop; run a hundred on GPUs from the same notebook, and get the videos and
95
+ the success rates back inline.
96
+
97
+ ```bash
98
+ pip install simhub # the library and a client, two dependencies
99
+ pip install 'simhub[all]' # + MuJoCo, video, plots, dataset export
100
+ ```
101
+
102
+ Two environment variables reach the hosted half, and the console prints both:
103
+ sign in, open **API keys**, create one, and paste the two `export` lines it
104
+ gives you. `SIMHUB_API_URL` is in there because a key on its own is half a
105
+ credential -- the console and the API are different origins behind Modal, so it
106
+ is not a thing to guess.
107
+
108
+ ## The whole authoring surface
109
+
110
+ Three things, and none of them is a base class you have to inherit from.
111
+
112
+ ```python
113
+ # warehouse_pick/__init__.py
114
+ import simhub
115
+
116
+ @simhub.environment("warehouse-pick")
117
+ def build(cell_seed: int = 7) -> simhub.Env:
118
+ from .env import PickEnv # heavy imports inside, always
119
+ return PickEnv(cell_seed)
120
+
121
+ @simhub.policy("scripted")
122
+ def scripted(env):
123
+ from .policies import ScriptedPick
124
+ return ScriptedPick(env)
125
+ ```
126
+
127
+ An **environment** is your own class with five members — `spec`, `reset`,
128
+ `step`, `observe`, `score`, and a `done` property. A **policy** has `spec`,
129
+ `reset` and `act`. A **task** is data: `simhub.Task(placed="eq:1",
130
+ collisions="eq:0")`, or a staged one that gives partial credit.
131
+
132
+ Beside the package, four keys:
133
+
134
+ ```toml
135
+ # simhub.toml
136
+ name = "warehouse-pick"
137
+ image = "python-gpu" # python-cpu | python-gpu
138
+ requirements = ["mujoco==3.11.*", "gr00t"] # installed at container start
139
+ include = ["*.py", "assets/**"]
140
+ ```
141
+
142
+ No entrypoint key and no inventory key. The package directory *is* the
143
+ entrypoint, and what the project offers is generated by importing it — in your
144
+ interpreter, so a syntax error or a missing dependency fails on your machine
145
+ rather than ninety seconds into a container.
146
+
147
+ ## One episode, here
148
+
149
+ ```python
150
+ import simhub
151
+
152
+ simhub.run("warehouse-pick", "scripted", seed=3, out="out/one",
153
+ record=simhub.Record(video="hero"))
154
+ ```
155
+
156
+ This is the debug loop, not the product: same `rollout`, same output files, one
157
+ at a time and without a GPU. Use it to find out your scene works before you
158
+ spend money running a hundred of them.
159
+
160
+ ## A hundred episodes, hosted
161
+
162
+ ```python
163
+ sh = simhub.connect() # SIMHUB_API_URL + SIMHUB_API_KEY
164
+ project = sh.project("./warehouse_pick") # imports locally, builds the inventory
165
+
166
+ batch = project.launch(
167
+ env="warehouse-pick",
168
+ policies=["scripted", "groot-ft"],
169
+ seeds=range(32),
170
+ task=simhub.Stages(target_lifted="gte:1", target_placed="gte:1"),
171
+ record=simhub.Record(cameras=["policy_ext", "wrist"], video="hero"),
172
+ render="cycles", # photoreal replay, in the background
173
+ )
174
+
175
+ batch # a live table, refreshing
176
+ batch.wait() # follows the server-sent events, per run; returns the batch
177
+ batch.dataframe() # one row per run
178
+ batch.videos("hero") # an inline grid of players, captioned with the verdict
179
+ batch.plot.success() # rates with intervals, checked and claimed apart
180
+ batch.cancel() # one call stops every child
181
+ ```
182
+
183
+ The upload is invisible: your project is zipped deterministically and stored by
184
+ content hash, so re-launching unchanged code uploads nothing. **Nobody builds
185
+ or deploys a container image to add a new simulation.**
186
+
187
+ ## Why hosted at all
188
+
189
+ Anybody can step MuJoCo in a loop for free. The reason to reach for this is
190
+ everything that is not the loop:
191
+
192
+ | what you want | why a laptop cannot |
193
+ | --- | --- |
194
+ | evaluate a VLA | GR00T N1.7 is 3B parameters on CUDA; pi0.5's checkpoint is gigabytes |
195
+ | a batch, not an episode | 192 episodes one after another against 16–64 at once |
196
+ | any video at all | 3.0 ms a frame on an A10G against 355 ms on CPU OSMesa |
197
+ | photoreal video | Cycles at 1080p is "under an hour" locally, minutes fanned over GPUs |
198
+ | fine-tune on what you recorded | never a laptop |
199
+ | a number worth citing next month | a result from somebody's machine has no digest, no row, and nothing to compare against |
200
+
201
+ ## Evaluations
202
+
203
+ An eval is a **frozen** set of episodes, so two policies are scored on the same
204
+ problems rather than on different random draws. The suite's content digest *is*
205
+ its version.
206
+
207
+ ```python
208
+ suite = simhub.Suite.grid("warehouse-pick/v1", cell_seed=range(8), target_seed=range(8))
209
+ ev = simhub.Eval(suite=suite, task=simhub.Task(target_placed="eq:1"),
210
+ primary="success", truncated_is="failure")
211
+
212
+ report = project.evaluate(ev, env="warehouse-pick",
213
+ policies=["scripted", "groot-ft"]).wait().report(ev)
214
+
215
+ report.table() # n, coverage, rate with its interval
216
+ report.compare("groot-ft", "scripted") # paired: effect, interval, p
217
+ report.failures(policy="groot-ft") # runs grouped by failure mode; join
218
+ # to batch.videos() on run_id for clips
219
+ ```
220
+
221
+ Five rules the report enforces, because each is a mistake that is easy to make
222
+ and expensive to publish:
223
+
224
+ 1. **Checked and unchecked verdicts are never averaged.** A verdict a stored
225
+ predicate computed and one a runner asserted are different kinds of number.
226
+ 2. **Coverage is reported before the score.** A crashed run is a missing
227
+ observation, not a policy failure. An eval at 80% coverage is not an eval.
228
+ 3. **Intervals, not bare fractions.** 12/20 is 0.60 with a 95% Wilson interval
229
+ of [0.39, 0.78]; "8/20 to 12/20" is inside that noise.
230
+ 4. **Comparisons are paired and come back as an effect, not a ranking.** Two
231
+ policies that ran different suites raise rather than being compared.
232
+ 5. **Determinism is checked, not assumed** — `report.check_determinism()`
233
+ re-runs one episode and asserts the metrics match.
234
+
235
+ ## What every run produces
236
+
237
+ The same layout whether it finished or died halfway:
238
+
239
+ ```
240
+ trajectory/000000.npz ... every body's pose and quaternion (wxyz), per step
241
+ trajectory/meta.json body names, units, the quaternion convention, in words
242
+ video/hero.mp4 captioned with the instruction and the exact buffers
243
+ renders/<camera>/*.png when you ask for frames
244
+ metrics.json what score() reported
245
+ result.json the verdict, on both axes
246
+ checkpoints/000200.npz resume points; one is written on SIGTERM
247
+ ```
248
+
249
+ One recording, three readers: the video, the evaluation and
250
+ `simhub.datasets.lerobot(batch, ...)` — which exports the same episodes in the
251
+ format GR00T and pi0.5 fine-tunes read.
252
+
253
+ ## Costs, and the one line that controls them
254
+
255
+ Rendering is the entire cost of a batch: a physics step is 0.08 ms and a GPU
256
+ frame is 1.7–8.4 ms. So a policy declares what it reads:
257
+
258
+ ```python
259
+ simhub.PolicySpec(action_dim=7, control_hz=15.0, expects=("observation/state",))
260
+ ```
261
+
262
+ and the rollout renders exactly that. A scripted demonstrator that never looks
263
+ at an image renders **no frames at all**.
264
+
265
+ ## Checking a scene you just generated
266
+
267
+ ```python
268
+ report = simhub.check("./savant_lobby", env="savant-lobby")
269
+ report.ok, report.images # links to pictures, not numbers
270
+ ```
271
+
272
+ Five assertions: it imports and resolves; every policy camera actually sees
273
+ something; everything rests under gravity; `score()` reports every metric the
274
+ spec declares; one episode completes. Non-zero exit when it does not — which is
275
+ what an agent generating scenes can branch on.
276
+
277
+ ## Modelling the scene in Blender
278
+
279
+ A project whose scene is easier to model than to write can build it with `bpy`
280
+ and hand simhub the result. Nothing about it is a special case: the contract is
281
+ still `write_mjcf(out, seed, **kwargs)`, and the same
282
+ `python_bundle -> mjcf_bundle` conversion turns it into a scene.
283
+
284
+ ```python
285
+ from simhub import blender
286
+
287
+ def write_mjcf(out, seed=0, **kwargs):
288
+ import bpy # heavy import, inside
289
+
290
+ bpy.ops.wm.read_factory_settings(use_empty=True)
291
+ build_the_dock(seed) # your code, your bpy
292
+
293
+ blender.static(bpy.data.objects["plinth"]) # welded to the world
294
+ blender.body(crate, density=450, friction=0.9) # falls, collides
295
+ blender.body(ball, mass=0.35, collision=proxy) # cheap collision stand-in
296
+ blender.exclude(backdrop) # not in the simulation
297
+
298
+ return blender.export(out, name="depot")
299
+ ```
300
+
301
+ Cameras and lights come across on their own — a Blender camera and a MuJoCo
302
+ camera share a convention, which is the one thing here needing no conversion.
303
+ Marks are stored as custom properties, so a scene keeps its meaning through a
304
+ `.blend` round-trip.
305
+
306
+ Two things shape how you model. **Collision is convex**, because MuJoCo's is:
307
+ each object collides as the hull of each of its loose parts, so a room wants to
308
+ be several objects rather than one welded mesh, and `collision=` takes a
309
+ low-poly proxy when a hull is wrong. **Mass comes from the hull**, not the
310
+ visual mesh — a hollow shell and its hull weigh different amounts and only one
311
+ of them is the physics.
312
+
313
+ Articulation is not Blender's job. A robot's joints, actuators and limits
314
+ already have a good representation in MJCF, so the Blender program builds the
315
+ *set* and `write_mjcf` attaches the arm before returning the path.
316
+
317
+ ### Or ship the `.blend` itself
318
+
319
+ A set modelled in Blender's UI rather than written as a program is a complete
320
+ input, because the marks live on the objects: `blender.body` and friends write
321
+ custom properties, so they survive a save.
322
+
323
+ ```python
324
+ from pathlib import Path
325
+ from simhub import blender
326
+
327
+ def write_mjcf(out, seed=0, **kwargs):
328
+ blender.open_file(Path(__file__).parent / "dock.blend")
329
+ return blender.export(out, name="dock")
330
+ ```
331
+
332
+ ```toml
333
+ include = ["*.py", "*.blend"] # the default list is text formats only
334
+ ```
335
+
336
+ Nothing new happens to it. A `.blend` is a file in the project directory, so it
337
+ travels inside the ordinary `python_bundle` -- content-addressed, deduped, and
338
+ re-uploading nothing when it has not changed -- and the compile is the same
339
+ `python_bundle -> mjcf_bundle` conversion. There is no `blend` artifact format
340
+ and no second upload, deliberately: a fifth format would be a fifth thing to
341
+ keep in step with the other four, in exchange for behaviour that is already
342
+ here. [`examples/blend_file_dock`](../examples/blend_file_dock) is the worked
343
+ example.
344
+
345
+ One thing it is not: a `.blend` is one fixed layout, so it is a set rather than
346
+ a suite. Sixty-four seeds against it are sixty-four copies of one episode, and
347
+ a batch that says `randomised` is refused server-side for exactly that. Vary in
348
+ `reset`, or author the set as a program the way `blender_depot` does.
349
+
350
+ ### Without Blender on your machine
351
+
352
+ You do not need `bpy` locally. Listing what a project offers never imports it --
353
+ every Blender call lives inside the scene program's body, the same rule every
354
+ project follows for MuJoCo and torch -- so `Project.load` works on any
355
+ interpreter and the wheel is only ever installed in the image that compiles:
356
+
357
+ ```bash
358
+ export SIMHUB_API_KEY=sk_...
359
+ ```
360
+
361
+ ```python
362
+ sh = simhub.connect()
363
+ batch = sh.project("./my_scene").launch(
364
+ env="depot-settle", policies=["sweep", "simhub:hold"], seeds=range(8),
365
+ task=simhub.Conditions(settled="gte:4"),
366
+ record=simhub.Record(video="hero"),
367
+ )
368
+ batch.wait(); batch.report().table(); batch.videos()
369
+ ```
370
+
371
+ The deployment registers `python-blender` as its `python_bundle -> mjcf_bundle`
372
+ compiler, the compile runs in an image that has Blender, and the scene lands on
373
+ the version as an ordinary `mjcf_bundle`. Every simulate job after that gets it
374
+ on disk; an environment that wants it declares a `scene` parameter and the
375
+ runner fills it in.
376
+
377
+ That indirection is not fastidiousness: `bpy` publishes one wheel per CPython
378
+ release and skips 3.12 entirely, so "put Blender in the worker" is not something
379
+ a 3.12 deployment can do at any price. Behind the compiler protocol the image
380
+ picks its own interpreter and nothing else has to agree.
381
+
382
+ [`examples/blender_depot`](../examples/blender_depot) is the worked example, end
383
+ to end.
384
+
385
+
386
+ ### Coupled room CFD and recorded-video artifacts
387
+
388
+ The `python-multiphysics` runtime adds distro OpenFOAM and official Blender to
389
+ MuJoCo on an A10G runner. Like other runtimes, it is declared by `runner.toml`
390
+ and registered by `.github/scripts/render_runners.py` for `deploy.yml`.
391
+ `simhub.manifest(image="python-multiphysics", ...)` selects it for a single-file project.
392
+
393
+ `simhub.openfoam.solve_room(...)` writes a reproducible room case, executes
394
+ `blockMesh`, `checkMesh` and transient `pimpleFoam`, and validates discrete
395
+ continuity. `apply_flow(transport, "velocity.npz")` transfers the solver's
396
+ oriented face fluxes to `AirTransport` without interpolating a nonconservative
397
+ cell-centred velocity. The current adapter supports a rectangular room with
398
+ no-slip walls, clean makeup air at y- and an outlet at y+. It does not resolve
399
+ the robot or the moving shroud; release, capture and aerosol mixing laws still
400
+ require calibration.
401
+
402
+ An environment can expose `finalize_run(out, result)` to produce additional
403
+ artifacts after its recorded rollout. Return file paths contained within `out`.
404
+ The hosted Python runner announces these using the normal artifact protocol
405
+ and remains running until the hook completes. A failed hook fails the run.
406
+ This supports evaluations and Cycles replay from the very trajectory that was
407
+ scored, without a separate manually uploaded demonstration video.
408
+
409
+ `examples/asbestos_ceiling.py` declares the scene, photo-derived robot,
410
+ controller, material assumptions, CFD inputs, evaluation suite and rendering
411
+ in one file. Dimensions and lift configuration are estimates from photographs;
412
+ the policy is scripted, and the material model is not a clearance prediction.
413
+
414
+ Public viewer links can open `/api/shared/view?token=...&run_id=...`. The page
415
+ shows the scoped run's execution status, task outcome, metrics and stored
416
+ video/artifacts without an account. The token is revocable and expires; never
417
+ substitute an organization API key in this URL.
418
+
419
+
420
+ ## Run pages and evaluation output
421
+
422
+ Every run has an organization-scoped console URL:
423
+ `https://simhub-console.pages.dev/runs/<run_id>?org=<organization-slug>`.
424
+ Open a run card to watch its recording and inspect its metrics, timing, status,
425
+ and task evaluation. Copy link preserves this address through sign-in and
426
+ refresh. The recipient needs access to that organization; no API key is included.
427
+
428
+ `rollout()` writes and announces `evaluation.json` (`schema: simhub.evaluation/1`)
429
+ as an ordinary artifact. It adds display metadata and the local task verdict to
430
+ the existing flat `metrics.json` and `result.json` outputs. Custom metrics need
431
+ no console changes:
432
+
433
+ ```python
434
+ spec = simhub.EnvSpec(
435
+ id="pick-place",
436
+ action_dim=7,
437
+ metrics={
438
+ "placement_error": simhub.MetricSpec(
439
+ unit="m", better="lower", required=True,
440
+ label="Placement error", description="Distance from object to target",
441
+ ),
442
+ "objects_placed": simhub.MetricSpec(label="Objects placed", unit="objects"),
443
+ },
444
+ )
445
+ # Env.score() returns {"placement_error": 0.003, "objects_placed": 2}.
446
+ # simhub.rollout(...) records those values and their metadata automatically.
447
+
448
+ with simhub.connect() as sh:
449
+ evaluation = sh.run_evaluation("run_...")
450
+ print(evaluation["metrics"], evaluation["metric_specs"])
451
+ ```
452
+
453
+ Reports include metrics for interrupted and failed episodes too. Nonfinite
454
+ numbers become JSON null and display as “Not reported”; a required metric that
455
+ is missing or null refuses a clean exit. Arrays and nested JSON statistics are
456
+ preserved. Task definitions and `local_verdict` describe evaluation inside the
457
+ runner. The hosted run's metrics, outcome, outcome source and checked flag remain
458
+ authoritative, including when a server task overrules the runner.
459
+
460
+ Older runs without this artifact still expose their recorded metrics. The SDK
461
+ raises a clear error for unsupported sidecar versions; the console reports that
462
+ error and keeps the server's metrics visible. Custom runner loops can build a
463
+ report with `simhub.evaluation.evaluation_report()` and publish it with
464
+ `Emitter.evaluation(report)` alongside their existing metric/result events.
465
+
466
+ ## Hosted model credentials
467
+
468
+ `simhub model credentials set NAME` securely uploads an HF token via a hidden
469
+ prompt (or `--token-file` / `--stdin`). `simhub model register` creates a pinned
470
+ HF policy revision bound to that credential, and
471
+ `project.launch(policy_version_id=...)` records it on the runs. This requires
472
+ the matching hosted API/worker/runner deployment. See
473
+ [MODEL_CREDENTIALS.md](MODEL_CREDENTIALS.md) for the contract and deployment steps.
474
+
475
+ ## Inspect a generated scene in the console
476
+
477
+ Publish a read-only, orbitable initial scene without running a policy:
478
+
479
+ ```python
480
+ import simhub
481
+
482
+ with simhub.connect() as session:
483
+ project = session.project("./warehouse_pick_cell")
484
+ preview = project.preview(env="warehouse-pick", seed=7, env_kwargs={"cell_seed": 7})
485
+ print(preview.get("console_url") or preview["artifact_id"])
486
+ ```
487
+
488
+ The equivalent CLI entry point is `simhub launch ./warehouse_pick_cell --preview
489
+ --env warehouse-pick --seed 7`. Preview generation is an explicit hosted compile
490
+ job and can incur compute charges. The deployment must advertise a
491
+ `python_bundle -> glb` compiler with the project's dependencies. The operation
492
+ constructs the selected environment and calls `reset(seed)`; it never steps a
493
+ policy. Environments that declare a `scene` argument get their project's
494
+ `write_mjcf()` output before construction. `env_kwargs` go to the environment
495
+ factory, while `seed` controls reset and any required scene build.
496
+
497
+ The console's **Scenes** tab lists initial previews and compiled templates for
498
+ the active environment. New SDK runs with MuJoCo recorder bindings also publish
499
+ the scene immediately after reset: **Runs → View scene** opens that run's exact
500
+ snapshot, including its seed and attempt. An older run without a snapshot says
501
+ so. Merely opening a scene reads stored artifacts; it does not start compute.
502
+
503
+ Operators set `SIMHUB_CONSOLE_URL` to return a shareable organization-scoped
504
+ console link. The link contains no API key; viewers still need access to that
505
+ organization. Existing MJCF bundles have a limited browser fallback and an
506
+ explicit **Generate full preview** action when the compiler supports it.
507
+
508
+ The first version displays rigid MuJoCo geometry, authored colors, mesh normals,
509
+ UV-mapped 2D textures, body selection, bounds and named perspective cameras.
510
+ Procedural texture projections, heightfields, skins and deformables report
511
+ coverage warnings. It displays the physics scene rather than a separately
512
+ constructed Cycles set, and does not replay trajectories. GLB content is limited
513
+ to 64 MiB and manifests to 2 MiB. Preview failures do not change simulation
514
+ outcomes; recordings remain available from the run page.