simulo 0.6.0__tar.gz → 0.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. simulo-0.7.0/PKG-INFO +190 -0
  2. simulo-0.7.0/README.md +158 -0
  3. simulo-0.7.0/pyproject.toml +107 -0
  4. simulo-0.7.0/src/simulo/__init__.py +154 -0
  5. simulo-0.7.0/src/simulo/_client/__init__.py +6 -0
  6. simulo-0.7.0/src/simulo/_client/_entrypoint.py +104 -0
  7. simulo-0.7.0/src/simulo/_client/_mounts.py +25 -0
  8. simulo-0.7.0/src/simulo/_client/_runner.py +171 -0
  9. simulo-0.7.0/src/simulo/_client/app.py +440 -0
  10. simulo-0.7.0/src/simulo/_client/asset.py +119 -0
  11. simulo-0.7.0/src/simulo/_client/bundle.py +157 -0
  12. simulo-0.7.0/src/simulo/_client/cli.py +1076 -0
  13. simulo-0.7.0/src/simulo/_client/config.py +136 -0
  14. simulo-0.7.0/src/simulo/_client/credentials.py +175 -0
  15. simulo-0.7.0/src/simulo/_client/discovery.py +214 -0
  16. simulo-0.7.0/src/simulo/_client/http.py +191 -0
  17. simulo-0.7.0/src/simulo/_client/jobs_api.py +160 -0
  18. simulo-0.7.0/src/simulo/_client/learning.py +189 -0
  19. simulo-0.7.0/src/simulo/_client/login.py +282 -0
  20. simulo-0.7.0/src/simulo/_client/manifest.py +56 -0
  21. simulo-0.7.0/src/simulo/_client/mode.py +29 -0
  22. simulo-0.7.0/src/simulo/_client/packaging.py +283 -0
  23. simulo-0.7.0/src/simulo/_client/registry.py +85 -0
  24. simulo-0.7.0/src/simulo/_client/runtime.py +189 -0
  25. simulo-0.7.0/src/simulo/_client/stub.py +41 -0
  26. simulo-0.7.0/src/simulo/_client/submit_api.py +291 -0
  27. simulo-0.7.0/src/simulo/_client/volume.py +52 -0
  28. simulo-0.7.0/src/simulo/callbacks.py +249 -0
  29. simulo-0.7.0/src/simulo.egg-info/PKG-INFO +190 -0
  30. simulo-0.7.0/src/simulo.egg-info/SOURCES.txt +72 -0
  31. simulo-0.7.0/src/simulo.egg-info/entry_points.txt +2 -0
  32. simulo-0.7.0/src/simulo.egg-info/requires.txt +15 -0
  33. simulo-0.7.0/src/simulo.egg-info/top_level.txt +1 -0
  34. simulo-0.7.0/tests/test_callbacks_manifest.py +182 -0
  35. simulo-0.7.0/tests/test_cartpole_e2e.py +203 -0
  36. simulo-0.7.0/tests/test_cartpole_eval_e2e.py +280 -0
  37. simulo-0.7.0/tests/test_cartpole_eval_lean_submit.py +166 -0
  38. simulo-0.7.0/tests/test_cartpole_lean_submit.py +92 -0
  39. simulo-0.7.0/tests/test_cartpole_resume_e2e.py +244 -0
  40. simulo-0.7.0/tests/test_cli.py +252 -0
  41. simulo-0.7.0/tests/test_cli_auth_commands.py +114 -0
  42. simulo-0.7.0/tests/test_cli_env_preset_error.py +85 -0
  43. simulo-0.7.0/tests/test_cli_models.py +794 -0
  44. simulo-0.7.0/tests/test_cli_recordings.py +462 -0
  45. simulo-0.7.0/tests/test_cli_run_cloud.py +152 -0
  46. simulo-0.7.0/tests/test_cloud_submit_e2e.py +253 -0
  47. simulo-0.7.0/tests/test_config.py +121 -0
  48. simulo-0.7.0/tests/test_credentials.py +313 -0
  49. simulo-0.7.0/tests/test_discovery.py +77 -0
  50. simulo-0.7.0/tests/test_discovery_transitive.py +83 -0
  51. simulo-0.7.0/tests/test_fake_control_plane.py +155 -0
  52. simulo-0.7.0/tests/test_hello_submit.py +106 -0
  53. simulo-0.7.0/tests/test_http.py +136 -0
  54. simulo-0.7.0/tests/test_jobs_api.py +271 -0
  55. simulo-0.7.0/tests/test_jobs_cli.py +487 -0
  56. simulo-0.7.0/tests/test_learning_surface.py +450 -0
  57. simulo-0.7.0/tests/test_learning_typing.py +83 -0
  58. simulo-0.7.0/tests/test_local_stream_e2e.py +365 -0
  59. simulo-0.7.0/tests/test_login.py +360 -0
  60. simulo-0.7.0/tests/test_manifest.py +37 -0
  61. simulo-0.7.0/tests/test_manifest_pip_dependencies.py +112 -0
  62. simulo-0.7.0/tests/test_namespace_coexistence_thin.py +24 -0
  63. simulo-0.7.0/tests/test_no_stray_namespace_init.py +32 -0
  64. simulo-0.7.0/tests/test_packaging.py +229 -0
  65. simulo-0.7.0/tests/test_process_retry_e2e.py +150 -0
  66. simulo-0.7.0/tests/test_protocol_conformance.py +53 -0
  67. simulo-0.7.0/tests/test_spawn_cloud.py +247 -0
  68. simulo-0.7.0/tests/test_submit_api.py +279 -0
  69. simulo-0.7.0/tests/test_submit_end_to_end.py +250 -0
  70. simulo-0.7.0/tests/test_submit_handle.py +142 -0
  71. simulo-0.7.0/tests/test_torch_free_import.py +62 -0
  72. simulo-0.6.0/LICENSE +0 -29
  73. simulo-0.6.0/PKG-INFO +0 -81
  74. simulo-0.6.0/README.md +0 -38
  75. simulo-0.6.0/cli/src/simulo_cli/__init__.py +0 -1
  76. simulo-0.6.0/cli/src/simulo_cli/__main__.py +0 -6
  77. simulo-0.6.0/cli/src/simulo_cli/api/__init__.py +0 -1
  78. simulo-0.6.0/cli/src/simulo_cli/api/client.py +0 -98
  79. simulo-0.6.0/cli/src/simulo_cli/auth/__init__.py +0 -1
  80. simulo-0.6.0/cli/src/simulo_cli/auth/credentials.py +0 -57
  81. simulo-0.6.0/cli/src/simulo_cli/cli.py +0 -33
  82. simulo-0.6.0/cli/src/simulo_cli/commands/__init__.py +0 -1
  83. simulo-0.6.0/cli/src/simulo_cli/commands/auth.py +0 -272
  84. simulo-0.6.0/cli/src/simulo_cli/commands/org.py +0 -29
  85. simulo-0.6.0/cli/src/simulo_cli/config.py +0 -138
  86. simulo-0.6.0/pyproject.toml +0 -185
  87. simulo-0.6.0/sdk/src/simulo/__init__.py +0 -162
  88. simulo-0.6.0/sdk/src/simulo/backend/__init__.py +0 -1
  89. simulo-0.6.0/sdk/src/simulo/backend/isaaclab/__init__.py +0 -5
  90. simulo-0.6.0/sdk/src/simulo/backend/isaaclab/asset_registry.py +0 -365
  91. simulo-0.6.0/sdk/src/simulo/backend/isaaclab/backend.py +0 -227
  92. simulo-0.6.0/sdk/src/simulo/backend/isaaclab/scene_builder.py +0 -2237
  93. simulo-0.6.0/sdk/src/simulo/backend/isaaclab/stream_adapter.py +0 -182
  94. simulo-0.6.0/sdk/src/simulo/core/__init__.py +0 -88
  95. simulo-0.6.0/sdk/src/simulo/core/actuator.py +0 -314
  96. simulo-0.6.0/sdk/src/simulo/core/asset.py +0 -63
  97. simulo-0.6.0/sdk/src/simulo/core/controller.py +0 -1012
  98. simulo-0.6.0/sdk/src/simulo/core/entity.py +0 -180
  99. simulo-0.6.0/sdk/src/simulo/core/env.py +0 -553
  100. simulo-0.6.0/sdk/src/simulo/core/light.py +0 -84
  101. simulo-0.6.0/sdk/src/simulo/core/materials.py +0 -61
  102. simulo-0.6.0/sdk/src/simulo/core/math_utils.py +0 -74
  103. simulo-0.6.0/sdk/src/simulo/core/observations.py +0 -481
  104. simulo-0.6.0/sdk/src/simulo/core/physics.py +0 -154
  105. simulo-0.6.0/sdk/src/simulo/core/player.py +0 -604
  106. simulo-0.6.0/sdk/src/simulo/core/policy.py +0 -245
  107. simulo-0.6.0/sdk/src/simulo/core/pose.py +0 -99
  108. simulo-0.6.0/sdk/src/simulo/core/primitives.py +0 -132
  109. simulo-0.6.0/sdk/src/simulo/core/rewards.py +0 -798
  110. simulo-0.6.0/sdk/src/simulo/core/robot.py +0 -1041
  111. simulo-0.6.0/sdk/src/simulo/core/scene.py +0 -462
  112. simulo-0.6.0/sdk/src/simulo/core/sensor.py +0 -923
  113. simulo-0.6.0/sdk/src/simulo/core/task.py +0 -215
  114. simulo-0.6.0/sdk/src/simulo/core/terminations.py +0 -326
  115. simulo-0.6.0/sdk/src/simulo/core/terrain.py +0 -380
  116. simulo-0.6.0/sdk/src/simulo/core/trainer.py +0 -437
  117. simulo-0.6.0/sdk/src/simulo/core/visual.py +0 -302
  118. simulo-0.6.0/sdk/src/simulo/core/world.py +0 -138
  119. simulo-0.6.0/sdk/src/simulo/py.typed +0 -0
  120. simulo-0.6.0/sdk/src/simulo/scenario.py +0 -196
  121. simulo-0.6.0/sdk/src/simulo/streaming/__init__.py +0 -31
  122. simulo-0.6.0/sdk/src/simulo/streaming/adapter.py +0 -209
  123. simulo-0.6.0/sdk/src/simulo/streaming/config.py +0 -90
  124. simulo-0.6.0/sdk/src/simulo/streaming/types.py +0 -103
  125. simulo-0.6.0/simulo.egg-info/PKG-INFO +0 -81
  126. simulo-0.6.0/simulo.egg-info/SOURCES.txt +0 -59
  127. simulo-0.6.0/simulo.egg-info/entry_points.txt +0 -2
  128. simulo-0.6.0/simulo.egg-info/requires.txt +0 -23
  129. simulo-0.6.0/simulo.egg-info/top_level.txt +0 -2
  130. {simulo-0.6.0 → simulo-0.7.0}/setup.cfg +0 -0
  131. {simulo-0.6.0/cli/src/simulo_cli → simulo-0.7.0/src/simulo}/py.typed +0 -0
  132. {simulo-0.6.0 → simulo-0.7.0/src}/simulo.egg-info/dependency_links.txt +0 -0
simulo-0.7.0/PKG-INFO ADDED
@@ -0,0 +1,190 @@
1
+ Metadata-Version: 2.4
2
+ Name: simulo
3
+ Version: 0.7.0
4
+ Summary: Simulo thin client — the Modal-inspired, torch-free user-facing SDK. Provides simulo.App / Runtime / Volume / Asset, @app.job, the source packager, and the `simulo` CLI. Depends only on simulo-interfaces.
5
+ Author-email: Simulo Team <team@simulo.ai>
6
+ License: BSD-3-Clause
7
+ Project-URL: Homepage, https://simulo.ai
8
+ Project-URL: Source, https://github.com/simulo-org/simulo-platform
9
+ Keywords: robotics,simulation,reinforcement-learning,thin-client,simulo
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: BSD License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Typing :: Typed
17
+ Requires-Python: >=3.11
18
+ Description-Content-Type: text/markdown
19
+ Requires-Dist: simulo-interfaces<0.2,>=0.1
20
+ Provides-Extra: dev
21
+ Requires-Dist: black>=24.0; extra == "dev"
22
+ Requires-Dist: isort>=5.13; extra == "dev"
23
+ Requires-Dist: ruff>=0.5; extra == "dev"
24
+ Requires-Dist: mypy>=1.10; extra == "dev"
25
+ Requires-Dist: pytest>=8.2; extra == "dev"
26
+ Requires-Dist: build>=1.2; extra == "dev"
27
+ Requires-Dist: twine>=5.1; extra == "dev"
28
+ Provides-Extra: release
29
+ Requires-Dist: commitizen>=3.27; extra == "release"
30
+ Requires-Dist: build>=1.2; extra == "release"
31
+ Requires-Dist: twine>=5.1; extra == "release"
32
+
33
+ # simulo
34
+
35
+ The Simulo **thin client** — the Modal-inspired, user-facing SDK you install with
36
+ `pip install simulo`.
37
+
38
+ This distribution is **platform-only and torch-free**. It provides the cloud/packaging
39
+ surface and depends only on [`simulo-interfaces`](../simulo-interfaces):
40
+
41
+ - `simulo.App`, `simulo.Runtime`, `simulo.Volume`, `simulo.Asset`
42
+ - `@app.job` and `@app.local_entrypoint`
43
+ - `simulo.callbacks` (`ResumableCheckpoint`, `CommitVolume`, `DebugOnAnomaly`)
44
+ - the source **packager** and the `simulo run` CLI (submit)
45
+
46
+ Heavy imports (`torch`, Isaac, …) never live in this package. They are written inside the
47
+ user's `runtime.imports()` guard and resolved only during **execution** — by the
48
+ `simulo-backend` runner today (the local-simulation stand-in for the cloud worker), and by
49
+ the cloud worker eventually.
50
+
51
+ ## Submit vs. execute
52
+
53
+ Submit and execute are cleanly separated:
54
+
55
+ | Step | Who | What it does |
56
+ |------|-----|--------------|
57
+ | **Submit** | the user (`simulo run app.py`) | runs `@app.local_entrypoint` in discovery mode (torch-free) → `spawn()`/`submit()` **write the package** (manifest + `source/`) to disk — and, in cloud mode, upload it + create a job on the control plane. NEVER trains, NEVER invokes the backend. |
58
+ | **Execute** | `simulo-backend run-package` (worker/system launcher; the **sole executor**) | imports the packaged source in execution mode and calls the `@app.job` — the real training. Invoked directly for the local simulation, and by `simulo-backend worker` in the cloud. |
59
+
60
+ ## Offline vs. cloud mode
61
+
62
+ The SAME `simulo run` serves both; the presence of a cloud target selects the mode:
63
+
64
+ | Mode | When | What `spawn()` does |
65
+ |------|------|---------------------|
66
+ | **Offline (local disk)** | no credentials, no `SIMULO_API_URL`/`SIMULO_ENV` — or `SIMULO_SUBMIT=local` forces it | writes the package to disk; you execute it yourself with `simulo-backend run-package` |
67
+ | **Cloud** | logged in (`simulo login`), or `SIMULO_API_URL`/`SIMULO_ENV` names a target | ALSO tars + uploads the package (`POST /v1/packages` + archive PUT) and creates a job (`POST /v1/jobs`); `simulo run` then follows the job's logs to completion by default (`--detach` to skip), and `JobHandle.get()` polls the platform for the real result |
68
+
69
+ Cloud quickstart (staging):
70
+
71
+ ```bash
72
+ SIMULO_ENV=staging simulo login # OAuth PKCE via browser loopback -> ~/.simulo/credentials (0600)
73
+ simulo run examples/hello/app.py # uploads, creates the job, follows its logs
74
+ simulo result # the job's result JSON
75
+ ```
76
+
77
+ A worker must be polling the control plane to execute cloud jobs — on a GPU box:
78
+
79
+ ```bash
80
+ SIMULO_API_URL=https://api.staging.simulo.ai SIMULO_WORKER_TOKEN=<token> \
81
+ simulo-backend worker # claim -> download -> run-package -> stream logs -> complete
82
+ ```
83
+
84
+ The full demo script (staging AND the zero-AWS local stack) is
85
+ `context/verification-steps/cloud-submit-demo-runbook.md`; the automated full-stack
86
+ proof is `tests/integration/` at the repo root (markers `cp_integration` / `cloud_e2e`).
87
+
88
+ This mirrors Modal: the runtime imports the module and calls the function; it does **not**
89
+ run `python app.py`. The job body the author wrote runs unchanged (source-preserving).
90
+
91
+ ## Namespace
92
+
93
+ `simulo` is a **hybrid namespace**. The thin client is the only distribution that ships
94
+ `simulo/__init__.py`; it calls `pkgutil.extend_path` so the PEP 420 namespace portions
95
+ shipped by `simulo-backend` (`simulo.core`, `simulo.backend`, `simulo.recording`) and
96
+ `simulo-interfaces` (`simulo.interfaces`) coexist under the same top-level `simulo`.
97
+
98
+ ## Modes
99
+
100
+ A single mode-aware package, driven by `SIMULO_MODE`:
101
+
102
+ | Mode | `runtime.imports()` |
103
+ |------|---------------------|
104
+ | `discovery` (default) | installs a stub import finder, records deferred top-level imports, runs no job body — the torch-free **submit** path |
105
+ | `execution` | `contextlib.nullcontext()` — real imports; set explicitly by the `simulo-backend` runner when it **executes** a package |
106
+
107
+ Discovery is the default so any bare `import` of a user app stays torch-free; `simulo
108
+ run` submits on that default. Execution is only ever set by the sole executor
109
+ (`simulo-backend run-package`).
110
+
111
+ ## One user command — submit
112
+
113
+ `simulo run` is the canonical — and only — submit command:
114
+
115
+ ```bash
116
+ simulo run app.py --num-envs 4096 --max-iterations 200
117
+ ```
118
+
119
+ It runs the `@app.local_entrypoint` in discovery mode (torch-free). Its `spawn()` /
120
+ `submit()` calls **write the package** — a content-addressed `<digest>/source/` tree plus
121
+ `<digest>/simulo.manifest.json` — and print where it landed. The manifest records the
122
+ entrypoint, runtime image + env, jobs (resources/timeout/retries/resume), volumes, the
123
+ submitted `args`, and the deferred `remote_imports` (e.g. `["torch"]`). `spawn()` returns
124
+ a `JobHandle` exposing `.package_path` / `.job_id`; it does **not** return a result —
125
+ submit never trains.
126
+
127
+ `simulo run` imports the file as a non-`__main__` module and invokes the registered
128
+ entrypoint explicitly (argparse is built from its signature; `--num-envs` maps to the
129
+ `num_envs` parameter). A direct `python app.py` does **not** submit:
130
+ `@app.local_entrypoint` detects the `__main__` run, prints the exact equivalent
131
+ `simulo run` command to stderr, and exits 2 — so a script wrapping the `python` form
132
+ fails loudly instead of silently not submitting.
133
+
134
+ ## Execute (the sole executor)
135
+
136
+ Submit does not train. Executing a package is a separate step owned entirely by the
137
+ `simulo-backend` runner — a worker/system launcher surfaced as a CLI today only for the
138
+ local simulation:
139
+
140
+ ```bash
141
+ simulo-backend run-package <package> --job train
142
+ ```
143
+
144
+ It imports the same packaged source in execution mode and calls the `@app.job` body —
145
+ resolving the real `torch` / Isaac imports there. (Eventually the cloud worker invokes it
146
+ and `JobHandle.get()` streams the result back; today `get()` raises a friendly
147
+ `JobResultUnavailable` pointing at the package + the execute command.) Executing locally
148
+ therefore needs the GPU runtime installed (`pip install simulo-backend`). See
149
+ [`examples/cartpole`](examples/cartpole/) for a full, GPU-proven submit → execute walkthrough.
150
+
151
+ ## Observe — `simulo jobs` / `simulo logs` / `simulo result`
152
+
153
+ Once a package executes, the thin client observes it **over HTTP only** — it never reads
154
+ the executor's run store from disk. Submit/observe and execute behave as if on **two
155
+ separate machines**: eventually a cloud event triggers execution and the client streams
156
+ from the real control plane; locally, *you* are that cloud event, and a deliberately-thin
157
+ stdlib server stands in for the control plane. The wire contract (job-record schema, the
158
+ four `GET /v1/jobs*` endpoints, byte-offset log semantics) is the durable piece — it
159
+ lives in `simulo.interfaces.platform.runs` and survives the move to the real cloud.
160
+
161
+ The two-terminal story on one box:
162
+
163
+ ```bash
164
+ # Terminal B — the "pretend cloud" (needs simulo-backend installed):
165
+ simulo-backend serve # local stand-in for the cloud jobs API (127.0.0.1)
166
+ simulo-backend run-package <package> --job train # you are the cloud event; prints SIMULO_JOB_ID=<id> first
167
+
168
+ # Terminal A — the user's machine (torch-free thin client, HTTP only):
169
+ simulo run app.py --num-envs 64 # submit: writes the package (never executes)
170
+ simulo logs <job-id> --follow # stream live training output; exits 0/1 with the job
171
+ simulo result <job-id> # the job's result JSON (404->error until success)
172
+ simulo jobs # table of runs: id, job, status, started, exit
173
+ ```
174
+
175
+ Notes:
176
+
177
+ - The **job id is printed by `run-package` at execution** (first line,
178
+ `SIMULO_JOB_ID=...`), not at submit — a run only exists once execution starts.
179
+ - `simulo logs <id> --follow` on a job that does not exist *yet* waits for it to appear,
180
+ so you can arm the tail in terminal A before triggering `run-package` in terminal B.
181
+ - Reconnecting is just re-reading: log chunks are addressed by **byte offset**, so a new
182
+ `simulo logs <id>` replays the full stream from 0.
183
+ - `SIMULO_API_URL` points the client at a different API (default
184
+ `http://127.0.0.1:8765`); `SIMULO_API_TOKEN`, when set, is sent as a bearer token.
185
+ With credentials saved by `simulo login`, their token + recorded `api_base_url`
186
+ are used instead. Bearer over plaintext `http://` is refused for any
187
+ non-loopback host (the https-when-token guard).
188
+ - `simulo recordings <job-id> [name|--all] [-o PATH]` downloads a job's MCAP
189
+ recording(s) via a presigned URL (`GET /v1/jobs/{id}/recordings`) — open them in
190
+ Lichtblick or read them with the `mcap` library.
simulo-0.7.0/README.md ADDED
@@ -0,0 +1,158 @@
1
+ # simulo
2
+
3
+ The Simulo **thin client** — the Modal-inspired, user-facing SDK you install with
4
+ `pip install simulo`.
5
+
6
+ This distribution is **platform-only and torch-free**. It provides the cloud/packaging
7
+ surface and depends only on [`simulo-interfaces`](../simulo-interfaces):
8
+
9
+ - `simulo.App`, `simulo.Runtime`, `simulo.Volume`, `simulo.Asset`
10
+ - `@app.job` and `@app.local_entrypoint`
11
+ - `simulo.callbacks` (`ResumableCheckpoint`, `CommitVolume`, `DebugOnAnomaly`)
12
+ - the source **packager** and the `simulo run` CLI (submit)
13
+
14
+ Heavy imports (`torch`, Isaac, …) never live in this package. They are written inside the
15
+ user's `runtime.imports()` guard and resolved only during **execution** — by the
16
+ `simulo-backend` runner today (the local-simulation stand-in for the cloud worker), and by
17
+ the cloud worker eventually.
18
+
19
+ ## Submit vs. execute
20
+
21
+ Submit and execute are cleanly separated:
22
+
23
+ | Step | Who | What it does |
24
+ |------|-----|--------------|
25
+ | **Submit** | the user (`simulo run app.py`) | runs `@app.local_entrypoint` in discovery mode (torch-free) → `spawn()`/`submit()` **write the package** (manifest + `source/`) to disk — and, in cloud mode, upload it + create a job on the control plane. NEVER trains, NEVER invokes the backend. |
26
+ | **Execute** | `simulo-backend run-package` (worker/system launcher; the **sole executor**) | imports the packaged source in execution mode and calls the `@app.job` — the real training. Invoked directly for the local simulation, and by `simulo-backend worker` in the cloud. |
27
+
28
+ ## Offline vs. cloud mode
29
+
30
+ The SAME `simulo run` serves both; the presence of a cloud target selects the mode:
31
+
32
+ | Mode | When | What `spawn()` does |
33
+ |------|------|---------------------|
34
+ | **Offline (local disk)** | no credentials, no `SIMULO_API_URL`/`SIMULO_ENV` — or `SIMULO_SUBMIT=local` forces it | writes the package to disk; you execute it yourself with `simulo-backend run-package` |
35
+ | **Cloud** | logged in (`simulo login`), or `SIMULO_API_URL`/`SIMULO_ENV` names a target | ALSO tars + uploads the package (`POST /v1/packages` + archive PUT) and creates a job (`POST /v1/jobs`); `simulo run` then follows the job's logs to completion by default (`--detach` to skip), and `JobHandle.get()` polls the platform for the real result |
36
+
37
+ Cloud quickstart (staging):
38
+
39
+ ```bash
40
+ SIMULO_ENV=staging simulo login # OAuth PKCE via browser loopback -> ~/.simulo/credentials (0600)
41
+ simulo run examples/hello/app.py # uploads, creates the job, follows its logs
42
+ simulo result # the job's result JSON
43
+ ```
44
+
45
+ A worker must be polling the control plane to execute cloud jobs — on a GPU box:
46
+
47
+ ```bash
48
+ SIMULO_API_URL=https://api.staging.simulo.ai SIMULO_WORKER_TOKEN=<token> \
49
+ simulo-backend worker # claim -> download -> run-package -> stream logs -> complete
50
+ ```
51
+
52
+ The full demo script (staging AND the zero-AWS local stack) is
53
+ `context/verification-steps/cloud-submit-demo-runbook.md`; the automated full-stack
54
+ proof is `tests/integration/` at the repo root (markers `cp_integration` / `cloud_e2e`).
55
+
56
+ This mirrors Modal: the runtime imports the module and calls the function; it does **not**
57
+ run `python app.py`. The job body the author wrote runs unchanged (source-preserving).
58
+
59
+ ## Namespace
60
+
61
+ `simulo` is a **hybrid namespace**. The thin client is the only distribution that ships
62
+ `simulo/__init__.py`; it calls `pkgutil.extend_path` so the PEP 420 namespace portions
63
+ shipped by `simulo-backend` (`simulo.core`, `simulo.backend`, `simulo.recording`) and
64
+ `simulo-interfaces` (`simulo.interfaces`) coexist under the same top-level `simulo`.
65
+
66
+ ## Modes
67
+
68
+ A single mode-aware package, driven by `SIMULO_MODE`:
69
+
70
+ | Mode | `runtime.imports()` |
71
+ |------|---------------------|
72
+ | `discovery` (default) | installs a stub import finder, records deferred top-level imports, runs no job body — the torch-free **submit** path |
73
+ | `execution` | `contextlib.nullcontext()` — real imports; set explicitly by the `simulo-backend` runner when it **executes** a package |
74
+
75
+ Discovery is the default so any bare `import` of a user app stays torch-free; `simulo
76
+ run` submits on that default. Execution is only ever set by the sole executor
77
+ (`simulo-backend run-package`).
78
+
79
+ ## One user command — submit
80
+
81
+ `simulo run` is the canonical — and only — submit command:
82
+
83
+ ```bash
84
+ simulo run app.py --num-envs 4096 --max-iterations 200
85
+ ```
86
+
87
+ It runs the `@app.local_entrypoint` in discovery mode (torch-free). Its `spawn()` /
88
+ `submit()` calls **write the package** — a content-addressed `<digest>/source/` tree plus
89
+ `<digest>/simulo.manifest.json` — and print where it landed. The manifest records the
90
+ entrypoint, runtime image + env, jobs (resources/timeout/retries/resume), volumes, the
91
+ submitted `args`, and the deferred `remote_imports` (e.g. `["torch"]`). `spawn()` returns
92
+ a `JobHandle` exposing `.package_path` / `.job_id`; it does **not** return a result —
93
+ submit never trains.
94
+
95
+ `simulo run` imports the file as a non-`__main__` module and invokes the registered
96
+ entrypoint explicitly (argparse is built from its signature; `--num-envs` maps to the
97
+ `num_envs` parameter). A direct `python app.py` does **not** submit:
98
+ `@app.local_entrypoint` detects the `__main__` run, prints the exact equivalent
99
+ `simulo run` command to stderr, and exits 2 — so a script wrapping the `python` form
100
+ fails loudly instead of silently not submitting.
101
+
102
+ ## Execute (the sole executor)
103
+
104
+ Submit does not train. Executing a package is a separate step owned entirely by the
105
+ `simulo-backend` runner — a worker/system launcher surfaced as a CLI today only for the
106
+ local simulation:
107
+
108
+ ```bash
109
+ simulo-backend run-package <package> --job train
110
+ ```
111
+
112
+ It imports the same packaged source in execution mode and calls the `@app.job` body —
113
+ resolving the real `torch` / Isaac imports there. (Eventually the cloud worker invokes it
114
+ and `JobHandle.get()` streams the result back; today `get()` raises a friendly
115
+ `JobResultUnavailable` pointing at the package + the execute command.) Executing locally
116
+ therefore needs the GPU runtime installed (`pip install simulo-backend`). See
117
+ [`examples/cartpole`](examples/cartpole/) for a full, GPU-proven submit → execute walkthrough.
118
+
119
+ ## Observe — `simulo jobs` / `simulo logs` / `simulo result`
120
+
121
+ Once a package executes, the thin client observes it **over HTTP only** — it never reads
122
+ the executor's run store from disk. Submit/observe and execute behave as if on **two
123
+ separate machines**: eventually a cloud event triggers execution and the client streams
124
+ from the real control plane; locally, *you* are that cloud event, and a deliberately-thin
125
+ stdlib server stands in for the control plane. The wire contract (job-record schema, the
126
+ four `GET /v1/jobs*` endpoints, byte-offset log semantics) is the durable piece — it
127
+ lives in `simulo.interfaces.platform.runs` and survives the move to the real cloud.
128
+
129
+ The two-terminal story on one box:
130
+
131
+ ```bash
132
+ # Terminal B — the "pretend cloud" (needs simulo-backend installed):
133
+ simulo-backend serve # local stand-in for the cloud jobs API (127.0.0.1)
134
+ simulo-backend run-package <package> --job train # you are the cloud event; prints SIMULO_JOB_ID=<id> first
135
+
136
+ # Terminal A — the user's machine (torch-free thin client, HTTP only):
137
+ simulo run app.py --num-envs 64 # submit: writes the package (never executes)
138
+ simulo logs <job-id> --follow # stream live training output; exits 0/1 with the job
139
+ simulo result <job-id> # the job's result JSON (404->error until success)
140
+ simulo jobs # table of runs: id, job, status, started, exit
141
+ ```
142
+
143
+ Notes:
144
+
145
+ - The **job id is printed by `run-package` at execution** (first line,
146
+ `SIMULO_JOB_ID=...`), not at submit — a run only exists once execution starts.
147
+ - `simulo logs <id> --follow` on a job that does not exist *yet* waits for it to appear,
148
+ so you can arm the tail in terminal A before triggering `run-package` in terminal B.
149
+ - Reconnecting is just re-reading: log chunks are addressed by **byte offset**, so a new
150
+ `simulo logs <id>` replays the full stream from 0.
151
+ - `SIMULO_API_URL` points the client at a different API (default
152
+ `http://127.0.0.1:8765`); `SIMULO_API_TOKEN`, when set, is sent as a bearer token.
153
+ With credentials saved by `simulo login`, their token + recorded `api_base_url`
154
+ are used instead. Bearer over plaintext `http://` is refused for any
155
+ non-loopback host (the https-when-token guard).
156
+ - `simulo recordings <job-id> [name|--all] [-o PATH]` downloads a job's MCAP
157
+ recording(s) via a presigned URL (`GET /v1/jobs/{id}/recordings`) — open them in
158
+ Lichtblick or read them with the `mcap` library.
@@ -0,0 +1,107 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "simulo"
7
+ version = "0.7.0"
8
+ description = "Simulo thin client — the Modal-inspired, torch-free user-facing SDK. Provides simulo.App / Runtime / Volume / Asset, @app.job, the source packager, and the `simulo` CLI. Depends only on simulo-interfaces."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = { text = "BSD-3-Clause" }
12
+ authors = [{ name = "Simulo Team", email = "team@simulo.ai" }]
13
+ keywords = ["robotics", "simulation", "reinforcement-learning", "thin-client", "simulo"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: BSD License",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Typing :: Typed",
22
+ ]
23
+ # Torch-free by construction. The thin client orchestrates packaging + discovery
24
+ # and depends ONLY on the contract layer; NO torch, NO simulo-backend. Heavy
25
+ # imports live inside the user's `runtime.imports()` guard (resolved in the
26
+ # cloud / by the backend runner), never in this distribution.
27
+ dependencies = ["simulo-interfaces>=0.1,<0.2"]
28
+
29
+ [project.optional-dependencies]
30
+ dev = ["black>=24.0", "isort>=5.13", "ruff>=0.5", "mypy>=1.10", "pytest>=8.2", "build>=1.2", "twine>=5.1"]
31
+ # Mirrors simulo-backend's [release] extra. `dev` above carries build/twine
32
+ # but deliberately NOT commitizen — the release skill (.claude/skills/release/
33
+ # SKILL.md) installs THIS extra specifically so `cz` is available without
34
+ # also pulling in a second copy of build/twine.
35
+ release = ["commitizen>=3.27", "build>=1.2", "twine>=5.1"]
36
+
37
+ [project.scripts]
38
+ simulo = "simulo._client.cli:main"
39
+
40
+ [project.urls]
41
+ Homepage = "https://simulo.ai"
42
+ Source = "https://github.com/simulo-org/simulo-platform"
43
+
44
+ # The thin client is the ONE distribution that ships `src/simulo/__init__.py`.
45
+ # That __init__ calls pkgutil.extend_path so the PEP 420 namespace portions
46
+ # shipped by simulo-backend (simulo.core/backend/recording) and simulo-interfaces
47
+ # (simulo.interfaces) union into the same `simulo` package. `namespaces = true`
48
+ # keeps setuptools from choking on the shared top-level name.
49
+ [tool.setuptools.packages.find]
50
+ where = ["src"]
51
+ namespaces = true
52
+
53
+ [tool.setuptools.package-data]
54
+ "simulo" = ["py.typed"]
55
+
56
+ [tool.mypy]
57
+ python_version = "3.11"
58
+ mypy_path = ["src"]
59
+ namespace_packages = true
60
+ explicit_package_bases = true
61
+ strict = true
62
+
63
+ [tool.pytest.ini_options]
64
+ testpaths = ["tests"]
65
+ addopts = "--import-mode=importlib"
66
+ markers = [
67
+ "isaac: requires NVIDIA Isaac Sim / IsaacLab (skipped on dev machines and in PR CI)",
68
+ "gpu: requires a CUDA GPU (skipped on dev machines and in PR CI)",
69
+ ]
70
+
71
+ [tool.isort]
72
+ profile = "black"
73
+ line_length = 120
74
+ known_first_party = ["simulo"]
75
+
76
+ [tool.ruff]
77
+ line-length = 120
78
+ src = ["src"]
79
+
80
+ [tool.black]
81
+ line-length = 120
82
+ target-version = ["py311"]
83
+
84
+ # --- Commitizen ------------------------------------------------------------
85
+ # Path B release flow (see `.claude/skills/release/SKILL.md`): `cz bump` here
86
+ # creates the bump commit + tag locally; `.github/workflows/release.yml`
87
+ # publishes to PyPI on tag push. version_provider = "pep621" reads/writes
88
+ # [project].version directly; there is no in-tree __version__ mirror to keep
89
+ # in sync (version_files = []).
90
+ #
91
+ # The FIRST release (0.7.0) cannot come from `cz bump` — 0.1.0 -> 0.7.0 is not
92
+ # a conventional-commit increment, and cz's first-run changelog would sweep
93
+ # in repo-wide history that predates this distribution's split from
94
+ # simulo-backend. It is a hand-written bump commit per the release skill's
95
+ # First-release appendix. 0.7.0 (not 0.2.0) is deliberate: it must exceed the
96
+ # legacy thick-client `simulo` package's last version (0.6.0) already
97
+ # published to PyPI under this same project name, so `pip install simulo`
98
+ # resolves to the new thin client, never the old thick client.
99
+ [tool.commitizen]
100
+ name = "cz_conventional_commits"
101
+ version_provider = "pep621"
102
+ version_files = []
103
+ tag_format = "simulo-v$version"
104
+ update_changelog_on_bump = true
105
+ changelog_incremental = true
106
+ major_version_zero = true
107
+ bump_message = "chore(release): bump simulo to v$new_version"
@@ -0,0 +1,154 @@
1
+ """Simulo thin client — the user-facing, torch-free SDK surface.
2
+
3
+ This is the ONE Simulo distribution that ships ``simulo/__init__.py``. The
4
+ ``extend_path`` call below unions every ``simulo/`` directory on ``sys.path`` into
5
+ this package's ``__path__`` so the PEP 420 namespace portions shipped by sibling
6
+ distributions (``simulo.core`` / ``simulo.backend`` / ``simulo.recording`` from
7
+ ``simulo-backend``; ``simulo.interfaces`` from ``simulo-interfaces``) resolve
8
+ alongside the clean top-level ``simulo.App`` re-exported here.
9
+
10
+ It MUST run before the re-exports below, because importing ``simulo._client.app``
11
+ pulls in ``simulo.interfaces`` (a sibling distribution).
12
+
13
+ Two kinds of name live on ``simulo``:
14
+
15
+ * **Platform** names — ``App / Runtime / Volume / Asset / callbacks`` — are **real,
16
+ eager** attributes bound below. They are always torch-free.
17
+ * **Learning** names — ``Task / Scene / Robot / LearningEnv / RLTrainer / …`` — are
18
+ resolved **lazily and mode-aware** through the PEP 562 ``__getattr__`` below,
19
+ which delegates to :mod:`simulo._client.learning`. At submit (discovery mode)
20
+ they resolve to lean stand-ins so the user app imports without the heavy runtime;
21
+ only the backend runner (execution mode) resolves them to ``simulo.core``. Because
22
+ the platform names are real attributes, ``__getattr__`` fires *only* for the
23
+ learning names.
24
+ """
25
+
26
+ import pkgutil as _pkgutil
27
+ from typing import TYPE_CHECKING
28
+
29
+ __path__ = _pkgutil.extend_path(__path__, __name__)
30
+
31
+ from simulo import callbacks
32
+ from simulo._client.app import App
33
+ from simulo._client.asset import Asset
34
+ from simulo._client.learning import LEARNING_NAMES as _LEARNING_NAMES
35
+ from simulo._client.learning import resolve as _resolve_learning
36
+ from simulo._client.runtime import Runtime
37
+ from simulo._client.volume import Volume
38
+
39
+ # Public surface = eager platform exports (real attributes; ``__getattr__`` never
40
+ # fires for these) + the lazily-resolved learning names. Kept a static literal so
41
+ # tooling recognises the re-exports; a test guards it against ``LEARNING_NAMES`` drift.
42
+ __all__ = [
43
+ # platform (eager, real)
44
+ "App",
45
+ "Runtime",
46
+ "Volume",
47
+ "Asset",
48
+ "callbacks",
49
+ # learning (lazy, mode-aware)
50
+ "Entity",
51
+ "LearningEnv",
52
+ "Light",
53
+ "Material",
54
+ "ObservationComponent",
55
+ "Physics",
56
+ "Player",
57
+ "Policy",
58
+ "Pose",
59
+ "RLPlayer",
60
+ "RLTrainer",
61
+ "RecordConfig",
62
+ "RewardComponent",
63
+ "Robot",
64
+ "Scenario",
65
+ "Scene",
66
+ "Task",
67
+ "Terrain",
68
+ "TerminationCondition",
69
+ "Trainer",
70
+ "Visual",
71
+ ]
72
+
73
+
74
+ def __getattr__(name: str) -> object:
75
+ """PEP 562 hook: lazily resolve the mode-aware learning names.
76
+
77
+ Fires only for attributes missing from the module dict — i.e. never for the
78
+ real platform exports above. Delegates the learning names to
79
+ :func:`simulo._client.learning.resolve` (discovery stand-in vs. real
80
+ ``simulo.core`` class, by mode); everything else is a genuine ``AttributeError``.
81
+ """
82
+ if name in _LEARNING_NAMES:
83
+ return _resolve_learning(name)
84
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
85
+
86
+
87
+ def __dir__() -> list[str]:
88
+ """Expose only the public surface to ``dir(simulo)``.
89
+
90
+ That is ``__all__`` (eager platform exports + learning names) unioned with
91
+ :data:`LEARNING_NAMES`. Private module globals (``_pkgutil``,
92
+ ``_resolve_learning``, ``_LEARNING_NAMES``, the ``TYPE_CHECKING`` import, …)
93
+ are deliberately filtered out so tab-completion shows the user-facing API only.
94
+ """
95
+ return sorted(set(__all__) | set(_LEARNING_NAMES))
96
+
97
+
98
+ if TYPE_CHECKING:
99
+ # Static aliases so a type checker sees the learning names with precise types
100
+ # (PEP 562 ``__getattr__`` would otherwise type them as ``Any``). The rule that
101
+ # this block MUST follow — and that the mypy positive test in
102
+ # ``tests/test_learning_typing.py`` locks — is:
103
+ #
104
+ # * **Subclassable** names (used as ``class X(simulo.Task)``) alias their
105
+ # ``interfaces.runtime`` Protocol, so the subclass is checked against the
106
+ # contract (e.g. ``TaskProtocol``).
107
+ # * **Instantiated / value-config** names (used as ``simulo.LearningEnv(task=...)``,
108
+ # ``simulo.RLTrainer(env=...)``, ``simulo.Scene(...)``) alias ``Any``. Their
109
+ # concrete type is the heavy ``simulo.core`` class, available only on the
110
+ # worker — aliasing them to a Protocol would make mypy reject instantiation
111
+ # ("Cannot instantiate protocol class").
112
+ from typing import Any
113
+
114
+ from simulo.interfaces.runtime import (
115
+ ObservationComponentProtocol,
116
+ PlayerProtocol,
117
+ PolicyProtocol,
118
+ RewardComponentProtocol,
119
+ ScenarioProtocol,
120
+ TaskProtocol,
121
+ TerminationConditionProtocol,
122
+ )
123
+
124
+ # Subclassable names alias their Protocol so ``class X(simulo.Task)`` is checked
125
+ # against ``TaskProtocol``.
126
+ Task = TaskProtocol
127
+ Scenario = ScenarioProtocol
128
+ Policy = PolicyProtocol
129
+ Player = PlayerProtocol
130
+ ObservationComponent = ObservationComponentProtocol
131
+ RewardComponent = RewardComponentProtocol
132
+ TerminationCondition = TerminationConditionProtocol
133
+ # Instantiated / value-config names: concrete type is the heavy ``simulo.core``
134
+ # (or ``simulo.recording``, for ``RecordConfig``) class, available only on the
135
+ # worker; alias ``Any`` so mypy accepts ``simulo.Name(...)``.
136
+ # ``LearningEnv`` is here — ``simulo.LearningEnv(task=...)`` is instantiated, so
137
+ # it must NOT alias ``LearningEnvProtocol`` (mypy would reject the call).
138
+ # ``RLPlayer``/``RecordConfig`` are instantiated too (``simulo.RLPlayer(env=...)``,
139
+ # ``simulo.RecordConfig(output_path=...)``), so they alias ``Any`` — only the
140
+ # subclassable ``Player`` base aliases ``PlayerProtocol``.
141
+ LearningEnv = Any
142
+ Scene = Any
143
+ Robot = Any
144
+ RLTrainer = Any
145
+ Trainer = Any
146
+ RLPlayer = Any
147
+ RecordConfig = Any
148
+ Pose = Any
149
+ Terrain = Any
150
+ Light = Any
151
+ Entity = Any
152
+ Material = Any
153
+ Physics = Any
154
+ Visual = Any
@@ -0,0 +1,6 @@
1
+ """Internal implementation package for the Simulo thin client.
2
+
3
+ Nothing here is part of the public API. Users import from the top-level
4
+ ``simulo`` namespace (``simulo.App`` / ``simulo.Runtime`` / ``simulo.callbacks``);
5
+ these modules are the private machinery behind those re-exports.
6
+ """