simhub
Upload reusable robots with simhub asset upload robot.urdf --env robots --name arm,
then load them in hosted projects with simhub.asset("arm").path.
See named URDF and mesh assets for dependency packaging,
project aliases, and version pinning.
The single-file house-ceiling example defines a photo-derived tracked robot base, two articulated tool arms, a domestic scene, a registered policy, a frozen evaluation and the Cycles scene in one Python file:
PYTHONPATH=simhub/src:. python examples/asbestos_ceiling.py evaluate
PYTHONPATH=simhub/src:. python examples/asbestos_ceiling.py render
PYTHONPATH=simhub/src:. python examples/asbestos_ceiling.py export-model
The example now uses simhub.particulate: two-layer moisture penetration,
contact-work-driven detachment, finite shroud capture, three fibre size bins and
conservative 3D advection/diffusion/settling. All coating, head, particle and
ventilation assumptions remain in the single file. Mass is tracked in kg of the
constituent, including constituent still bound in fragments. Hosted delivery solves the room velocity with OpenFOAM; local material-only runs
use a prescribed velocity. Neither mode resolves shroud-scale flow or predicts
calibrated asbestos clearance.
PYTHONPATH=simhub/src:. python examples/asbestos_ceiling.py material-checks
PYTHONPATH=simhub/src:. python examples/asbestos_ceiling.py diagnostics
material-checks runs controlled parameter interventions and analytic/grid/time
checks; diagnostics renders the recorded state as a scientific animation and
requires Matplotlib plus ffmpeg. New outputs default to
results/asbestos_ceiling_v3; earlier outputs remain preserved.
Evaluation retains the original design targets and reports failures without
changing the physical parameters to obtain a pass.
For other scenes, simhub.render_cycles(project_directory, trajectory_directory, output_directory, program="scene.py") invokes Blender locally. A scene program
defines build_scene(recorded, config) and returns a mapping of recorded body
names to unparented Blender objects plus an update(index, fraction) callback.
The callback can animate recorded fields and cameras; it must not rerun a policy.
Blender uses the best available Cycles GPU backend, with a CPU fallback.
Recorder.channel(name, sampler) records fixed-shape finite numeric fields
alongside body poses. simhub.SurfaceContamination and simhub.CleaningTool
provide a mass-conserving surface-transfer surrogate. The ceiling example records
remaining coating, surface and bond moisture, size-resolved airborne concentration
and seven mass inventories through this channel API, so the render and evaluation
read the same state. AirTransport, CoatingParameters, ExtractionContact,
ParticleClass and WetCoating are also exported directly from simhub. The hosted Cycles worker uses the same replay
entry point when supplied a Python bundle and params.program (use
"__init__.py" for a bundled single-file project). The worker must have Blender
and ffmpeg available; no cloud deployment is performed by the local commands.
Define an environment, a policy and a task in Python. Run one episode on your laptop; run a hundred on GPUs from the same notebook, and get the videos and the success rates back inline.
pip install simhub # the library and a client, two dependencies
pip install 'simhub[all]' # + MuJoCo, video, plots, dataset export
Two environment variables reach the hosted half, and the console prints both:
sign in, open API keys, create one, and paste the two export lines it
gives you. SIMHUB_API_URL is in there because a key on its own is half a
credential -- the console and the API are different origins behind Modal, so it
is not a thing to guess.
The whole authoring surface
Three things, and none of them is a base class you have to inherit from.
# warehouse_pick/__init__.py
import simhub
@simhub.environment("warehouse-pick")
def build(cell_seed: int = 7) -> simhub.Env:
from .env import PickEnv # heavy imports inside, always
return PickEnv(cell_seed)
@simhub.policy("scripted")
def scripted(env):
from .policies import ScriptedPick
return ScriptedPick(env)
An environment is your own class with five members — spec, reset,
step, observe, score, and a done property. A policy has spec,
reset and act. A task is data: simhub.Task(placed="eq:1", collisions="eq:0"), or a staged one that gives partial credit.
Beside the package, four keys:
# simhub.toml
name = "warehouse-pick"
image = "python-gpu" # python-cpu | python-gpu
requirements = ["mujoco==3.11.*", "gr00t"] # installed at container start
include = ["*.py", "assets/**"]
No entrypoint key and no inventory key. The package directory is the entrypoint, and what the project offers is generated by importing it — in your interpreter, so a syntax error or a missing dependency fails on your machine rather than ninety seconds into a container.
One episode, here
import simhub
simhub.run("warehouse-pick", "scripted", seed=3, out="out/one",
record=simhub.Record(video="hero"))
This is the debug loop, not the product: same rollout, same output files, one
at a time and without a GPU. Use it to find out your scene works before you
spend money running a hundred of them.
A hundred episodes, hosted
sh = simhub.connect() # SIMHUB_API_URL + SIMHUB_API_KEY
project = sh.project("./warehouse_pick") # imports locally, builds the inventory
batch = project.launch(
env="warehouse-pick",
policies=["scripted", "groot-ft"],
seeds=range(32),
task=simhub.Stages(target_lifted="gte:1", target_placed="gte:1"),
record=simhub.Record(cameras=["policy_ext", "wrist"], video="hero"),
render="cycles", # photoreal replay, in the background
)
batch # a live table, refreshing
batch.wait() # follows the server-sent events, per run; returns the batch
batch.dataframe() # one row per run
batch.videos("hero") # an inline grid of players, captioned with the verdict
batch.plot.success() # rates with intervals, checked and claimed apart
batch.cancel() # one call stops every child
The upload is invisible: your project is zipped deterministically and stored by content hash, so re-launching unchanged code uploads nothing. Nobody builds or deploys a container image to add a new simulation.
Why hosted at all
Anybody can step MuJoCo in a loop for free. The reason to reach for this is everything that is not the loop:
| what you want | why a laptop cannot |
|---|---|
| evaluate a VLA | GR00T N1.7 is 3B parameters on CUDA; pi0.5's checkpoint is gigabytes |
| a batch, not an episode | 192 episodes one after another against 16–64 at once |
| any video at all | 3.0 ms a frame on an A10G against 355 ms on CPU OSMesa |
| photoreal video | Cycles at 1080p is "under an hour" locally, minutes fanned over GPUs |
| fine-tune on what you recorded | never a laptop |
| a number worth citing next month | a result from somebody's machine has no digest, no row, and nothing to compare against |
Evaluations
An eval is a frozen set of episodes, so two policies are scored on the same problems rather than on different random draws. The suite's content digest is its version.
suite = simhub.Suite.grid("warehouse-pick/v1", cell_seed=range(8), target_seed=range(8))
ev = simhub.Eval(suite=suite, task=simhub.Task(target_placed="eq:1"),
primary="success", truncated_is="failure")
report = project.evaluate(ev, env="warehouse-pick",
policies=["scripted", "groot-ft"]).wait().report(ev)
report.table() # n, coverage, rate with its interval
report.compare("groot-ft", "scripted") # paired: effect, interval, p
report.failures(policy="groot-ft") # runs grouped by failure mode; join
# to batch.videos() on run_id for clips
Five rules the report enforces, because each is a mistake that is easy to make and expensive to publish:
- Checked and unchecked verdicts are never averaged. A verdict a stored predicate computed and one a runner asserted are different kinds of number.
- Coverage is reported before the score. A crashed run is a missing observation, not a policy failure. An eval at 80% coverage is not an eval.
- Intervals, not bare fractions. 12/20 is 0.60 with a 95% Wilson interval of [0.39, 0.78]; "8/20 to 12/20" is inside that noise.
- Comparisons are paired and come back as an effect, not a ranking. Two policies that ran different suites raise rather than being compared.
- Determinism is checked, not assumed —
report.check_determinism()re-runs one episode and asserts the metrics match.
What every run produces
The same layout whether it finished or died halfway:
trajectory/000000.npz ... every body's pose and quaternion (wxyz), per step
trajectory/meta.json body names, units, the quaternion convention, in words
video/hero.mp4 captioned with the instruction and the exact buffers
renders/<camera>/*.png when you ask for frames
metrics.json what score() reported
result.json the verdict, on both axes
checkpoints/000200.npz resume points; one is written on SIGTERM
One recording, three readers: the video, the evaluation and
simhub.datasets.lerobot(batch, ...) — which exports the same episodes in the
format GR00T and pi0.5 fine-tunes read.
Costs, and the one line that controls them
Rendering is the entire cost of a batch: a physics step is 0.08 ms and a GPU frame is 1.7–8.4 ms. So a policy declares what it reads:
simhub.PolicySpec(action_dim=7, control_hz=15.0, expects=("observation/state",))
and the rollout renders exactly that. A scripted demonstrator that never looks at an image renders no frames at all.
Checking a scene you just generated
report = simhub.check("./savant_lobby", env="savant-lobby")
report.ok, report.images # links to pictures, not numbers
Five assertions: it imports and resolves; every policy camera actually sees
something; everything rests under gravity; score() reports every metric the
spec declares; one episode completes. Non-zero exit when it does not — which is
what an agent generating scenes can branch on.
Modelling the scene in Blender
A project whose scene is easier to model than to write can build it with bpy
and hand simhub the result. Nothing about it is a special case: the contract is
still write_mjcf(out, seed, **kwargs), and the same
python_bundle -> mjcf_bundle conversion turns it into a scene.
from simhub import blender
def write_mjcf(out, seed=0, **kwargs):
import bpy # heavy import, inside
bpy.ops.wm.read_factory_settings(use_empty=True)
build_the_dock(seed) # your code, your bpy
blender.static(bpy.data.objects["plinth"]) # welded to the world
blender.body(crate, density=450, friction=0.9) # falls, collides
blender.body(ball, mass=0.35, collision=proxy) # cheap collision stand-in
blender.exclude(backdrop) # not in the simulation
return blender.export(out, name="depot")
Cameras and lights come across on their own — a Blender camera and a MuJoCo
camera share a convention, which is the one thing here needing no conversion.
Marks are stored as custom properties, so a scene keeps its meaning through a
.blend round-trip.
Two things shape how you model. Collision is convex, because MuJoCo's is:
each object collides as the hull of each of its loose parts, so a room wants to
be several objects rather than one welded mesh, and collision= takes a
low-poly proxy when a hull is wrong. Mass comes from the hull, not the
visual mesh — a hollow shell and its hull weigh different amounts and only one
of them is the physics.
Articulation is not Blender's job. A robot's joints, actuators and limits
already have a good representation in MJCF, so the Blender program builds the
set and write_mjcf attaches the arm before returning the path.
Or ship the .blend itself
A set modelled in Blender's UI rather than written as a program is a complete
input, because the marks live on the objects: blender.body and friends write
custom properties, so they survive a save.
from pathlib import Path
from simhub import blender
def write_mjcf(out, seed=0, **kwargs):
blender.open_file(Path(__file__).parent / "dock.blend")
return blender.export(out, name="dock")
include = ["*.py", "*.blend"] # the default list is text formats only
Nothing new happens to it. A .blend is a file in the project directory, so it
travels inside the ordinary python_bundle -- content-addressed, deduped, and
re-uploading nothing when it has not changed -- and the compile is the same
python_bundle -> mjcf_bundle conversion. There is no blend artifact format
and no second upload, deliberately: a fifth format would be a fifth thing to
keep in step with the other four, in exchange for behaviour that is already
here. examples/blend_file_dock is the worked
example.
One thing it is not: a .blend is one fixed layout, so it is a set rather than
a suite. Sixty-four seeds against it are sixty-four copies of one episode, and
a batch that says randomised is refused server-side for exactly that. Vary in
reset, or author the set as a program the way blender_depot does.
Without Blender on your machine
You do not need bpy locally. Listing what a project offers never imports it --
every Blender call lives inside the scene program's body, the same rule every
project follows for MuJoCo and torch -- so Project.load works on any
interpreter and the wheel is only ever installed in the image that compiles:
export SIMHUB_API_KEY=sk_...
sh = simhub.connect()
batch = sh.project("./my_scene").launch(
env="depot-settle", policies=["sweep", "simhub:hold"], seeds=range(8),
task=simhub.Conditions(settled="gte:4"),
record=simhub.Record(video="hero"),
)
batch.wait(); batch.report().table(); batch.videos()
The deployment registers python-blender as its python_bundle -> mjcf_bundle
compiler, the compile runs in an image that has Blender, and the scene lands on
the version as an ordinary mjcf_bundle. Every simulate job after that gets it
on disk; an environment that wants it declares a scene parameter and the
runner fills it in.
That indirection is not fastidiousness: bpy publishes one wheel per CPython
release and skips 3.12 entirely, so "put Blender in the worker" is not something
a 3.12 deployment can do at any price. Behind the compiler protocol the image
picks its own interpreter and nothing else has to agree.
examples/blender_depot is the worked example, end
to end.
Coupled room CFD and recorded-video artifacts
The python-multiphysics runtime adds distro OpenFOAM and official Blender to
MuJoCo on an A10G runner. Like other runtimes, it is declared by runner.toml
and registered by .github/scripts/render_runners.py for deploy.yml.
simhub.manifest(image="python-multiphysics", ...) selects it for a single-file project.
simhub.openfoam.solve_room(...) writes a reproducible room case, executes
blockMesh, checkMesh and transient pimpleFoam, and validates discrete
continuity. apply_flow(transport, "velocity.npz") transfers the solver's
oriented face fluxes to AirTransport without interpolating a nonconservative
cell-centred velocity. The current adapter supports a rectangular room with
no-slip walls, clean makeup air at y- and an outlet at y+. It does not resolve
the robot or the moving shroud; release, capture and aerosol mixing laws still
require calibration.
An environment can expose finalize_run(out, result) to produce additional
artifacts after its recorded rollout. Return file paths contained within out.
The hosted Python runner announces these using the normal artifact protocol
and remains running until the hook completes. A failed hook fails the run.
This supports evaluations and Cycles replay from the very trajectory that was
scored, without a separate manually uploaded demonstration video.
examples/asbestos_ceiling.py declares the scene, photo-derived robot,
controller, material assumptions, CFD inputs, evaluation suite and rendering
in one file. Dimensions and lift configuration are estimates from photographs;
the policy is scripted, and the material model is not a clearance prediction.
Public viewer links can open /api/shared/view?token=...&run_id=.... The page
shows the scoped run's execution status, task outcome, metrics and stored
video/artifacts without an account. The token is revocable and expires; never
substitute an organization API key in this URL.
Run pages and evaluation output
Every run has an organization-scoped console URL:
https://simhub-console.pages.dev/runs/<run_id>?org=<organization-slug>.
Open a run card to watch its recording and inspect its metrics, timing, status,
and task evaluation. Copy link preserves this address through sign-in and
refresh. The recipient needs access to that organization; no API key is included.
rollout() writes and announces evaluation.json (schema: simhub.evaluation/1)
as an ordinary artifact. It adds display metadata and the local task verdict to
the existing flat metrics.json and result.json outputs. Custom metrics need
no console changes:
spec = simhub.EnvSpec(
id="pick-place",
action_dim=7,
metrics={
"placement_error": simhub.MetricSpec(
unit="m", better="lower", required=True,
label="Placement error", description="Distance from object to target",
),
"objects_placed": simhub.MetricSpec(label="Objects placed", unit="objects"),
},
)
# Env.score() returns {"placement_error": 0.003, "objects_placed": 2}.
# simhub.rollout(...) records those values and their metadata automatically.
with simhub.connect() as sh:
evaluation = sh.run_evaluation("run_...")
print(evaluation["metrics"], evaluation["metric_specs"])
Reports include metrics for interrupted and failed episodes too. Nonfinite
numbers become JSON null and display as “Not reported”; a required metric that
is missing or null refuses a clean exit. Arrays and nested JSON statistics are
preserved. Task definitions and local_verdict describe evaluation inside the
runner. The hosted run's metrics, outcome, outcome source and checked flag remain
authoritative, including when a server task overrules the runner.
Older runs without this artifact still expose their recorded metrics. The SDK
raises a clear error for unsupported sidecar versions; the console reports that
error and keeps the server's metrics visible. Custom runner loops can build a
report with simhub.evaluation.evaluation_report() and publish it with
Emitter.evaluation(report) alongside their existing metric/result events.
Hosted model credentials
simhub model credentials set NAME securely uploads an HF token via a hidden
prompt (or --token-file / --stdin). simhub model register creates a pinned
HF policy revision bound to that credential, and
project.launch(policy_version_id=...) records it on the runs. This requires
the matching hosted API/worker/runner deployment. See
MODEL_CREDENTIALS.md for the contract and deployment steps.
Inspect a generated scene in the console
Publish a read-only, orbitable initial scene without running a policy:
import simhub
with simhub.connect() as session:
project = session.project("./warehouse_pick_cell")
preview = project.preview(env="warehouse-pick", seed=7, env_kwargs={"cell_seed": 7})
print(preview.get("console_url") or preview["artifact_id"])
The equivalent CLI entry point is simhub launch ./warehouse_pick_cell --preview --env warehouse-pick --seed 7. Preview generation is an explicit hosted compile
job and can incur compute charges. The deployment must advertise a
python_bundle -> glb compiler with the project's dependencies. The operation
constructs the selected environment and calls reset(seed); it never steps a
policy. Environments that declare a scene argument get their project's
write_mjcf() output before construction. env_kwargs go to the environment
factory, while seed controls reset and any required scene build.
The console's Scenes tab lists initial previews and compiled templates for the active environment. New SDK runs with MuJoCo recorder bindings also publish the scene immediately after reset: Runs → View scene opens that run's exact snapshot, including its seed and attempt. An older run without a snapshot says so. Merely opening a scene reads stored artifacts; it does not start compute.
Operators set SIMHUB_CONSOLE_URL to return a shareable organization-scoped
console link. The link contains no API key; viewers still need access to that
organization. Existing MJCF bundles have a limited browser fallback and an
explicit Generate full preview action when the compiler supports it.
The first version displays rigid MuJoCo geometry, authored colors, mesh normals, UV-mapped 2D textures, body selection, bounds and named perspective cameras. Procedural texture projections, heightfields, skins and deformables report coverage warnings. It displays the physics scene rather than a separately constructed Cycles set, and does not replay trajectories. GLB content is limited to 64 MiB and manifests to 2 MiB. Preview failures do not change simulation outcomes; recordings remain available from the run page.
Release files for simhub 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| simhub-0.1.0.tar.gz | 190.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| simhub-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 406.3 kB
Release files / simhub-0.1.0.tar.gz
| Download URL | simhub-0.1.0.tar.gz |
|---|---|
| Size | 190.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
64605645110b9f8265708e84c4740d6449a96336e0bf6f9a7a9c309cf17bf966
|
|
BLAKE2b-256 checksum How to use checksums |
d072ee9671ef623bae4e420c8f91fa23eb42b8ae1ca3da32277b6441afe32bf6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 18, 2026.
Transparency logRelease files / simhub-0.1.0-py3-none-any.whl
| Download URL | simhub-0.1.0-py3-none-any.whl |
|---|---|
| Size | 215.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2f265ac69d58383e3bd092c4588f030bf5bf2d78962dc5ccdc6ce59c5e07b3b4
|
|
BLAKE2b-256 checksum How to use checksums |
d03a4ae24ba4130a18b2f2d1564e8fe9e91342a9c5d351d016af336f4a3b2f4d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 18, 2026.
Transparency log