Metadata-Version: 2.5
Name: digiarena
Version: 0.5.0
Summary: Python SDK for DigiArena (digiarena.org): browse hosted robot-simulation scenes, deploy cloud gyms, submit to benchmarks.
Project-URL: Homepage, https://digiarena.org
Project-URL: Repository, https://github.com/Grounded-Intelligence-Initiative/digiarena-client-python
Project-URL: Documentation, https://digiarena.org/docs/api
Author-email: Xiatao Sun <sunxiatao@gmail.com>
License: Apache-2.0
License-File: LICENSE
Keywords: benchmark,gaussian-splatting,gymnasium,real2sim,robotics,simulation
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Python: >=3.11
Requires-Dist: world-engine-client[gym,img]>=0.6
Provides-Extra: gym
Provides-Extra: img
Provides-Extra: render
Requires-Dist: world-engine-client[render]>=0.6; extra == 'render'
Description-Content-Type: text/markdown

# digiarena (PyPI: `digiarena`)

Python SDK for [DigiArena](https://digiarena.org) — browse hosted
robot-simulation scenes, deploy them as cloud gyms, and submit to benchmarks.

```bash
pip install digiarena
# Camera images are rendered on your machine; add this for image observations:
pip install 'digiarena[render]'   # Chromium downloads on first use if Chrome is absent
```

Needs Python 3.11 or newer.

```python
import digiarena as da

da.login()                                   # paste an API key from digiarena.org → Settings
# Continue the exact browser attempt without resetting:
with da.attach("hosted-session-id") as env:
    obs, info = env.observe()
    print(info["episode_id"], obs["rgb"].keys())

# Or launch an independent session from an accessible scene in one call
# (env.close() also tears the session down):
with da.make("alice/kitchen_001", task="pick_mug_to_sink") as env:
    obs, info = env.reset(seed=0)
    obs, reward, terminated, truncated, info = env.step(env.action_space.sample())

# Or keep the handle for the viewer URL, logs and lifetime extension:
scenes = da.scenes.list(query="kitchen")
gym = da.deploy(scene="kitchen_001", task="pick_mug_to_sink")
env = gym.connect()                          # → world_engine_client.RemoteEnv
obs, info = env.reset(seed=0)
obs, r, term, trunc, info = env.step(action)
gym.close()
```

CLI with the same surface:

```
digiarena login
digiarena scenes list --query kitchen
digiarena deploy --scene kitchen_001
digiarena leaderboard tabletop-v1
digiarena usage
```

`da.scenes.list(query)` matches a substring of a scene's name, slug,
description or owner, ignoring case, both for public scenes and for your own
(`public=False`). Two differences remain: the public search treats `%` and `_`
as wildcards and also matches the owner's display name, while your own scenes
are matched on the text as written. The public search returns at most 100
scenes, most recently updated first. The API has no paging yet, so narrow the
query to find others. `da.scenes.get()` still reads a public scene when the
configured key has been revoked. `da.leaderboard(benchmark)` returns the
entries, highest success rate first; the benchmark id is the bare slug for
version 1 (`tabletop`) and `{slug}-v{version}` after that. A reply the SDK
cannot read raises `ApiError` with code `unexpected_reply`, and the CLI prints
it as an `error:` line.

## Layering

`digiarena` depends on and reuses
[`world-engine-client`](https://github.com/Grounded-Intelligence-Initiative/world-engine-client-python)
(`connect(token, url) → RemoteEnv`). This package covers everything from your
account to that token — scene gallery, cloud deploy, benchmark submission,
leaderboards, usage — and never reimplements the wire protocol.

- Light dependencies: numpy, Pillow (camera images) and Gymnasium (spaces),
  all via `world-engine-client`. No torch or opencv. `import digiarena` loads
  only the stdlib and numpy; Pillow and Gymnasium load on first use.
- Every call hits the public REST API (`/api/v1/*`) with
  `Authorization: Bearer da_live_...` — the SDK is a thin wrapper you could
  replace with curl.
- Typed errors with actionable "Possible fixes" (the World Engine style).
- Endpoints still in later platform milestones (benchmark submission) raise `NotImplementedYetError` with the server's own
  guidance — the SDK surface is stable ahead of them.

Scene content never transits your machine: there is no upload()/download().
Content is authored in the platform's browser editor; public scenes are used
directly via `da.deploy()` without forking. Private scenes require ownership or
platform admin access. Pass `images=False` to deploy/attach for state-only operation.

Camera images are drawn on your computer, not on the platform: the hosted session
simulates physics and sends the poses with each step, and a headless Chrome on your
machine renders the same scene the browser shows. This needs the `render` extra;
without it, asking for images fails before any request. Without a hardware GPU the
images are rendered in software, which takes seconds per step, and a
`RuntimeWarning` says so; pass `render_options={"allow_software": False}` to get an
error instead. A hardware GPU that renders below 3 steps per second gets a
`RuntimeWarning` too (see the `world-engine-client` README for the basis).

## The render cache

The scene's assets are downloaded once and kept in the render cache,
`<user cache dir>/digiarena/render/` (`~/.cache` on Linux, `~/Library/Caches`
on macOS, `%LOCALAPPDATA%` on Windows; `DIGIARENA_CACHE_DIR` moves it). The
platform sends them encrypted for your account and that scene, and they stay
encrypted on disk. Every connect asks the platform for the key again, and the
key stays in memory. A scene made private or a revoked key makes its cached
files useless at once. Files a scene has not used for 30 days are deleted,
unless the scene is pinned:

```python
da.cache.list()                      # [{scene, scope, size, last_used, pinned, ...}]
da.cache.pin("alice/kitchen_001")    # keep it however long it goes unused
da.cache.unpin("alice/kitchen_001")
da.cache.clear("alice/kitchen_001")  # or da.cache.clear() for everything
da.cache.configure(max_age_days=30, max_size_gb=None)  # 0 days: keep everything
```

```bash
digiarena cache ls
digiarena cache pin alice/kitchen_001
digiarena cache clear --all
digiarena cache config --max-age-days 0
```

A scene is listed under the name `da.deploy()` or `da.make()` was given.
`DIGIARENA_CACHE_MAX_AGE_DAYS` and `DIGIARENA_CACHE_MAX_SIZE_GB` override the
stored settings, for CI and clusters. Put the cache on a local disk: on a
network file system the client warns, and machines sharing it may download a
file twice.

The 30-day rule is applied when a session with images starts, at most once a
day. A machine that stops running such sessions keeps its encrypted files
until `digiarena cache clear --all`. `digiarena cache clear --legacy` deletes
what clients before world-engine-client 0.6 left behind: plain files in an
old `cache_dir` (name it with `--dir`), and `we_render_*` directories in the
temporary directory that nothing used for an hour.

The encryption keeps a scene from someone who can read your disk but cannot
sign in as you. It does not keep it from you: your key decrypts it, and while
a session runs the decrypted assets and the images are in the memory of your
Python process, of the headless Chrome and of the GPU.

## Remote simulation

`da.attach(session_id)` uses resume semantics and pauses the current attempt.
`env.observe()` and `env.render()` never advance physics. `env.reset(seed=...)`
starts a new episode; `env.step(action)` advances one negotiated control interval.
`env.close()` releases control; `gym.close()` tears down a launched session.
A live Python controller survives browser closure. Lost/uncertain actions are
never retried automatically: reconnect or explicitly reset to recover.

Images are RGB `uint8` arrays in `obs['rgb']`, keyed by configured camera name.
Use `cameras=['wrist']` for a subset and `env.as_gymnasium()` for the optional
Gymnasium adapter.

`env.step()` returns the task's reward. A task with stage points (a `staged`
reward) has its reward divided by the task's total points by default, so an
episode that finishes sums to 1.0. To get it in points, pass
`normalize_reward=False` to `da.make()`, `gym.connect()` or `da.attach()`:

```python
with da.make("alice/kitchen_001", task="pick_mug_to_sink", normalize_reward=False) as env:
    obs, info = env.reset(seed=0)
```

Each step then pays the points it earned, less the failure penalty on the step
that fails. With partial credit on, a step that gives partial credit back has a
negative reward, though nothing failed. A step never pays the running total
again. `info["task_progress"]["reward"]` holds the running totals in points.
After every step of an episode that started with `env.reset()`, the rewards so
far add up to `earned - penalty` of that step's info. `da.attach()` joins an
episode under way, so until the next reset its rewards leave out the points
paid before it attached. Sparse and dense rewards are the same either way. The
[`world-engine-client` README](https://github.com/Grounded-Intelligence-Initiative/world-engine-client-python#rewards)
has the details.

Runnable examples are in the repository's
[`examples/`](https://github.com/Grounded-Intelligence-Initiative/digiarena-client-python/tree/main/examples)
directory (not part of the installed package): `attach.py`, `reachable_pose.py`, and
`deploy_policy.py`. `measure_remote.py` records throughput and soak measurements.
`vla_smolvla.py` runs a real VLA (SmolVLA via LeRobot, `pip install 'lerobot[smolvla]'`)
on your machine against a remote session, and `measure_link.py` reports what your
network link does to stepping: steps per second, latency and bandwidth used, for
state only, one camera and all cameras.

To check the whole path end to end, start Simulate in the browser editor, copy
the session id from the Inspector's Python API card, and send the end effector
to a pose while watching the browser:

```bash
python examples/reachable_pose.py SESSION_ID                       # small offset from the start pose
python examples/reachable_pose.py SESSION_ID --pos 0.45 0.0 0.30   # absolute position, metres
python examples/reachable_pose.py SESSION_ID --pos 0.45 0 0.3 --rot 3.14 0 0 --state-only
digiarena attach SESSION_ID --state-only                           # inspect without stepping
```

## Development

```bash
pip install -e '.[img,gym]' pytest && python -m pytest
```

The tests use a stub control plane and a fake simulator; no platform is needed.

## Credentials

`DIGIARENA_API_KEY` env var (CI/batch) → `~/.config/digiarena/credentials`
(written by `da.login()` / `digiarena login`, mode 0600).
`DIGIARENA_API_URL` overrides the endpoint for staging deployments.
