Skip to main content

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:

  1. Checked and unchecked verdicts are never averaged. A verdict a stored predicate computed and one a runner asserted are different kinds of number.
  2. 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.
  3. 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.
  4. Comparisons are paired and come back as an effect, not a ranking. Two policies that ran different suites raise rather than being compared.
  5. Determinism is checked, not assumedreport.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)

Source distribution for simhub 0.1.0
File Size Uploaded
simhub-0.1.0.tar.gz 190.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for simhub 0.1.0
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page