Run contract
the universal “how a run works” — same for every simulatorSimulators are a base, you run anything
A stack is a base environment — a simulator or runtime installed, and nothing task-specific. You decide what to run on it, what to install, what to feed in, what to get out, and how much resource to use. Nothing is hardcoded: no baked-in task, no fixed metrics. You express a run either through the web Run form or a polygon.yaml in your project.
The run config (what you control)
# polygon.yaml — lives in your project, OR filled via the web Run form
base: mujoco # base image: mujoco | pybullet | python-cuda | openfoam | ...
setup: # commands run BEFORE run (install deps) — optional
- pip install mujoco imageio
run: python main.py # the command that runs YOUR code (required)
inputs: # downloaded into /work before the run — optional
- { url: "https://…/scene.xml", path: "assets/scene.xml" }
outputs: # files/dirs to collect + make downloadable — optional
- out/
- render.mp4
resources: { cpu: 4, ram_gb: 8, gpu: 1 } # requested; capped to the node's free share
display: false # true → start a headless X (Xvfb) + allow GUI/video capture
env: { FOO: "bar" }Everything is optional except base and run. Defaults: no setup, collect /outputs, a small default resource slice within the node share, display off.
Mount points (inside the container)
/work— your project (your code), and the working directory. Writable./inputs— files you asked to download, read-only./outputs— write results here; declaredoutputsare collected + made downloadable./config— materialized run config (incl./config/run.json).
The ##metric protocol (live charts, optional)
Logs always stream. If you want live charts, print a line to stdout:
print(f"##metric loss={loss} step={i}")Симолёт parses ##metric key=value [step=n] and draws it in real time. You can emit several keys per line (##metric ep_rew=231.5 success=0.8 step=40). No metrics? You still get logs plus artifacts. (You may instead write /outputs/metrics.jsonl.)
The base-image contract (how a run executes)
Every base image ships a generic entrypoint that:
cd /work.- If display is on (
display: true/$POLYGON_DISPLAY): start Xvfb/EGL so
headless GUI/render works, and export DISPLAY.
- Run setup (
bash -lc "$POLYGON_SETUP") to install your dependencies. - Materialize inputs the agent already downloaded.
- Exec the run command — the container args if given, else
$POLYGON_RUN, else a
sensible default (python main.py if present).
- Stream stdout/stderr — the agent forwards them as live logs.
- You write results to
/outputs; the agent uploads the declared outputs.
The image never runs a task pipeline of its own — it is a base plus a generic runner.
Hooks
Lifecycle hooks run around the main command, interpreted by the base's runner:
hooks.setup— install/prepare (equivalent tosetup:/POLYGON_SETUP).hooks.pre— commands run just before the run command.hooks.post— commands run after it (e.g. post-process, pack/outputs).
Streams (live telemetry, optional)
Beyond logs and metrics, a run can declare streams — named realtime channels (telemetry, frames) a client subscribes to. List them with GET /v1/jobs/{id}/streams, mint a ticket to …/subscribe, then consume …/data over SSE. A finished run also exposes a full replay timeline at GET /v1/jobs/{id}/recording.
Resource fairness
You request resources, but each node advertises only its shared slice. The scheduler filters to nodes that fit and the runner caps the container to min(requested, node share). A run can never exceed what a machine offered to the pool.
How the pieces carry it
- Web form / `polygon.yaml` → a JobSpec: base
image,command,env(incl.
POLYGON_SETUP, POLYGON_DISPLAY), workspace (your code: git / zip / inline), inputs, outputs, requires/limits.
- Orchestrator stores the bases, accepts the full spec, and schedules by resources.
- Node agent materializes the workspace, runs the base image (setup → command),
streams logs/metrics, collects + uploads the declared outputs, enforces the caps.