Metadata-Version: 2.5
Name: simhub
Version: 0.1.0
Summary: Define an environment, a policy and a task in Python; run them on hosted GPUs
Project-URL: Homepage, https://github.com/General-Deployment/sim
License: Proprietary
Keywords: evaluation,mujoco,reinforcement-learning,robotics,simulation
Requires-Python: >=3.9
Requires-Dist: httpx>=0.27
Requires-Dist: numpy>=1.24
Requires-Dist: pydantic>=2.7
Requires-Dist: tomli>=2.0; python_version < '3.11'
Provides-Extra: all
Requires-Dist: huggingface-hub>=0.26; extra == 'all'
Requires-Dist: imageio-ffmpeg>=0.5; extra == 'all'
Requires-Dist: ipython>=8.0; extra == 'all'
Requires-Dist: matplotlib>=3.7; extra == 'all'
Requires-Dist: mujoco==3.11.*; (python_version >= '3.10') and extra == 'all'
Requires-Dist: pandas>=2.0; extra == 'all'
Requires-Dist: pillow>=10.0; extra == 'all'
Requires-Dist: pyarrow>=15.0; extra == 'all'
Provides-Extra: dataset
Requires-Dist: pyarrow>=15.0; extra == 'dataset'
Provides-Extra: mujoco
Requires-Dist: mujoco==3.11.*; (python_version >= '3.10') and extra == 'mujoco'
Provides-Extra: notebook
Requires-Dist: ipython>=8.0; extra == 'notebook'
Requires-Dist: matplotlib>=3.7; extra == 'notebook'
Requires-Dist: pandas>=2.0; extra == 'notebook'
Requires-Dist: pillow>=10.0; extra == 'notebook'
Provides-Extra: video
Requires-Dist: imageio-ffmpeg>=0.5; extra == 'video'
Requires-Dist: pillow>=10.0; extra == 'video'
Provides-Extra: weights
Requires-Dist: huggingface-hub>=0.26; extra == 'weights'
Description-Content-Type: text/markdown

# 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](docs/named-assets.md) for dependency packaging,
project aliases, and version pinning.

The [single-file house-ceiling example](../examples/asbestos_ceiling.py) 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:

```bash
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.

```bash
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.

```bash
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.

```python
# 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:

```toml
# 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

```python
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

```python
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.

```python
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 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:

```python
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

```python
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.

```python
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.

```python
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")
```

```toml
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`](../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:

```bash
export SIMHUB_API_KEY=sk_...
```

```python
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`](../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:

```python
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](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:

```python
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.
