simulo 0.33.1__tar.gz → 0.35.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 (88) hide show
  1. {simulo-0.33.1/src/simulo.egg-info → simulo-0.35.0}/PKG-INFO +7 -5
  2. {simulo-0.33.1 → simulo-0.35.0}/PYPI.md +1 -1
  3. {simulo-0.33.1 → simulo-0.35.0}/pyproject.toml +7 -5
  4. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/__init__.py +12 -1
  5. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/_runner.py +15 -1
  6. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/app.py +105 -66
  7. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/asset.py +2 -2
  8. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/asset_pins.py +21 -2
  9. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/bundle.py +9 -7
  10. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/cli.py +10 -7
  11. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/cli_context.py +25 -4
  12. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/cli_job_reporting.py +62 -22
  13. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/cli_rendering.py +10 -0
  14. simulo-0.35.0/src/simulo/_client/cli_runtime_limit.py +130 -0
  15. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/cli_waiting.py +37 -25
  16. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/commands/assets.py +6 -3
  17. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/commands/jobs.py +79 -24
  18. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/commands/recordings.py +81 -42
  19. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/commands/run.py +203 -66
  20. simulo-0.35.0/src/simulo/_client/commands/systems.py +93 -0
  21. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/credentials.py +17 -2
  22. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/evaluation_report.py +45 -6
  23. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/facades.py +2 -0
  24. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/http.py +66 -31
  25. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/jobs_api.py +6 -1
  26. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/learning.py +12 -1
  27. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/login.py +20 -0
  28. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/packaging.py +27 -17
  29. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/preflight_api.py +9 -0
  30. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/preview_report.py +133 -101
  31. simulo-0.35.0/src/simulo/_client/recording_export.py +54 -0
  32. simulo-0.35.0/src/simulo/_client/repeatable_episodes.py +22 -0
  33. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/submit_api.py +52 -22
  34. simulo-0.35.0/src/simulo/_client/system_requests.py +82 -0
  35. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/templates/training/eval.py.tmpl +1 -1
  36. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/templates/training/play.py.tmpl +1 -1
  37. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/templates/training/preview.py.tmpl +1 -1
  38. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/templates/training/train.py.tmpl +1 -1
  39. simulo-0.35.0/src/simulo/_client/training_algorithm.py +11 -0
  40. simulo-0.35.0/src/simulo/_client/viewstream_policy.py +92 -0
  41. {simulo-0.33.1 → simulo-0.35.0/src/simulo.egg-info}/PKG-INFO +7 -5
  42. {simulo-0.33.1 → simulo-0.35.0}/src/simulo.egg-info/SOURCES.txt +6 -0
  43. {simulo-0.33.1 → simulo-0.35.0}/src/simulo.egg-info/requires.txt +7 -2
  44. simulo-0.33.1/src/simulo/_client/commands/systems.py +0 -132
  45. {simulo-0.33.1 → simulo-0.35.0}/MANIFEST.in +0 -0
  46. {simulo-0.33.1 → simulo-0.35.0}/setup.cfg +0 -0
  47. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/__init__.py +0 -0
  48. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/_arguments.py +0 -0
  49. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/_mounts.py +0 -0
  50. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/_secure_downloads.py +0 -0
  51. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/asset_api.py +0 -0
  52. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/asset_package.py +0 -0
  53. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/builtin_aliases.py +0 -0
  54. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/cancel_api.py +0 -0
  55. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/cli_arguments.py +0 -0
  56. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/cli_downloads.py +0 -0
  57. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/cli_errors.py +0 -0
  58. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/cli_listing.py +0 -0
  59. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/cli_text.py +0 -0
  60. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/commands/__init__.py +0 -0
  61. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/commands/auth.py +0 -0
  62. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/commands/create.py +0 -0
  63. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/commands/policies.py +0 -0
  64. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/config.py +0 -0
  65. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/discovery.py +0 -0
  66. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/export_api.py +0 -0
  67. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/identity_api.py +0 -0
  68. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/install_samples.py +0 -0
  69. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/mode.py +0 -0
  70. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/onnx_bundle.py +0 -0
  71. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/outputs.py +0 -0
  72. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/policies_api.py +0 -0
  73. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/preflight_render.py +0 -0
  74. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/registry.py +0 -0
  75. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/runtime.py +0 -0
  76. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/runtime_display.py +0 -0
  77. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/stub.py +0 -0
  78. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/templates/__init__.py +0 -0
  79. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/templates/training/simuloignore.tmpl +0 -0
  80. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/templates/training/task.py.tmpl +0 -0
  81. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/view_fragment.py +0 -0
  82. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/view_session_api.py +0 -0
  83. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/_client/volume.py +0 -0
  84. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/callbacks.py +0 -0
  85. {simulo-0.33.1 → simulo-0.35.0}/src/simulo/py.typed +0 -0
  86. {simulo-0.33.1 → simulo-0.35.0}/src/simulo.egg-info/dependency_links.txt +0 -0
  87. {simulo-0.33.1 → simulo-0.35.0}/src/simulo.egg-info/entry_points.txt +0 -0
  88. {simulo-0.33.1 → simulo-0.35.0}/src/simulo.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simulo
3
- Version: 0.33.1
3
+ Version: 0.35.0
4
4
  Summary: Python SDK and CLI for cloud-native robotics simulation, reinforcement learning, and robot policy training on managed GPUs.
5
5
  Author-email: Simulo Team <team@simulo.ai>
6
6
  License: BSD-3-Clause
@@ -9,7 +9,7 @@ Project-URL: Documentation, https://docs.simulo.ai
9
9
  Project-URL: Quickstart, https://docs.simulo.ai/getting-started/quickstart/
10
10
  Project-URL: Examples, https://github.com/simulo-org/simulo-samples
11
11
  Project-URL: Console, https://console.simulo.ai
12
- Keywords: simulo,robotics,robotics simulation,robot training,robot learning,reinforcement learning,cloud robotics,cloud simulation,GPU simulation,policy training,physical AI,embodied AI,NVIDIA Isaac Sim,Isaac Lab,OpenUSD,USD,URDF,ONNX,MCAP,Python SDK
12
+ Keywords: simulo,robotics,robotics simulation,robot training,robot learning,reinforcement learning,cloud robotics,cloud simulation,GPU simulation,policy training,physical AI,embodied AI,OpenUSD,USD,URDF,ONNX,MCAP,Python SDK
13
13
  Classifier: Development Status :: 3 - Alpha
14
14
  Classifier: Environment :: Console
15
15
  Classifier: Intended Audience :: Developers
@@ -31,14 +31,16 @@ Classifier: Topic :: System :: Distributed Computing
31
31
  Classifier: Typing :: Typed
32
32
  Requires-Python: >=3.11
33
33
  Description-Content-Type: text/markdown
34
- Requires-Dist: simulo-interfaces<0.25,>=0.24.0
34
+ Requires-Dist: simulo-interfaces<0.27,>=0.26.0
35
35
  Requires-Dist: mcap<2,>=1.3
36
36
  Requires-Dist: defusedxml>=0.7.1
37
37
  Requires-Dist: usd-core<27,>=26.5
38
+ Provides-Extra: viz
39
+ Requires-Dist: simulo-viz<0.2,>=0.1.0; python_version >= "3.12" and extra == "viz"
38
40
  Provides-Extra: assets
39
41
  Requires-Dist: usd-core<27,>=26.5; extra == "assets"
40
42
  Provides-Extra: dev
41
- Requires-Dist: black==26.5.1; extra == "dev"
43
+ Requires-Dist: black==26.10.1; extra == "dev"
42
44
  Requires-Dist: isort==8.0.1; extra == "dev"
43
45
  Requires-Dist: ruff==0.15.21; extra == "dev"
44
46
  Requires-Dist: mypy==2.3.0; extra == "dev"
@@ -122,7 +124,7 @@ simulo policy get policy_x:best # download a verified checkpoint
122
124
  simulo export policy_x:best # export a verified ONNX bundle
123
125
  simulo recordings # download MCAP rollout recordings
124
126
  simulo outputs # retrieve other saved outputs
125
- simulo view # view a running --viewstream job in 3D
127
+ simulo view # view a running job in 3D (live view is on by default)
126
128
  simulo cancel # stop a queued or running job
127
129
  ```
128
130
 
@@ -66,7 +66,7 @@ simulo policy get policy_x:best # download a verified checkpoint
66
66
  simulo export policy_x:best # export a verified ONNX bundle
67
67
  simulo recordings # download MCAP rollout recordings
68
68
  simulo outputs # retrieve other saved outputs
69
- simulo view # view a running --viewstream job in 3D
69
+ simulo view # view a running job in 3D (live view is on by default)
70
70
  simulo cancel # stop a queued or running job
71
71
  ```
72
72
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "simulo"
7
- version = "0.33.1"
7
+ version = "0.35.0"
8
8
  description = "Python SDK and CLI for cloud-native robotics simulation, reinforcement learning, and robot policy training on managed GPUs."
9
9
  readme = "PYPI.md"
10
10
  requires-python = ">=3.11"
@@ -23,8 +23,6 @@ keywords = [
23
23
  "policy training",
24
24
  "physical AI",
25
25
  "embodied AI",
26
- "NVIDIA Isaac Sim",
27
- "Isaac Lab",
28
26
  "OpenUSD",
29
27
  "USD",
30
28
  "URDF",
@@ -59,7 +57,7 @@ classifiers = [
59
57
  # mcap: used by `simulo recordings` to read back a downloaded recording and
60
58
  # verify it (message count) — MIT-licensed, deps only lz4+zstandard.
61
59
  dependencies = [
62
- "simulo-interfaces>=0.24.0,<0.25",
60
+ "simulo-interfaces>=0.26.0,<0.27",
63
61
  "mcap>=1.3,<2",
64
62
  # Hardened XML parsing for URDF ingestion (USD Asset Catalogs, PR-12 fix
65
63
  # loop 2, security NIT): stdlib xml.etree.ElementTree relies on
@@ -70,6 +68,8 @@ dependencies = [
70
68
  # featherweight promise below (that promise was about avoiding
71
69
  # usd-core's tens of MB via an opt-in extra; usd-core itself joined the
72
70
  # base footprint in 0.13.1 — see its own comment just below).
71
+ # Uncapped on purpose: the floor keeps the hardened XML parser; an upper
72
+ # bound is added only when a release breaks it.
73
73
  "defusedxml>=0.7.1",
74
74
  # usd-core: simulator-free but tens of MB — moved from the `assets`
75
75
  # extra into the base install in 0.13.1 so `pip install simulo` covers
@@ -81,6 +81,7 @@ dependencies = [
81
81
  ]
82
82
 
83
83
  [project.optional-dependencies]
84
+ viz = ["simulo-viz>=0.1.0,<0.2; python_version >= '3.12'"]
84
85
  # `assets` — kept for backward compatibility with `pip install
85
86
  # "simulo[assets]"` (the 0.13.0-era spelling): usd-core moved into the base
86
87
  # install above in 0.13.1, so this extra is now redundant (its dep is
@@ -104,7 +105,7 @@ assets = ["usd-core>=26.5,<27"]
104
105
  # `pytest-timeout` is pinned EXACT because it enforces the suite's hang bound;
105
106
  # changing the plugin under an unchanged timeout would change that guard's behavior.
106
107
  dev = [
107
- "black==26.5.1",
108
+ "black==26.10.1",
108
109
  "isort==8.0.1",
109
110
  "ruff==0.15.21",
110
111
  "mypy==2.3.0",
@@ -193,6 +194,7 @@ markers = [
193
194
  "isaac: requires NVIDIA Isaac Sim / IsaacLab (skipped on dev machines and in PR CI)",
194
195
  "gpu: requires a CUDA GPU (skipped on dev machines and in PR CI)",
195
196
  "requires_execution_backend: end-to-end test that needs a live execution backend in the loop; skipped in CPU/unit runs",
197
+ "smoke: a short fundamental-path test; the hosted smoke job in simulo-ci.yml runs only these, and local CI runs every test",
196
198
  ]
197
199
 
198
200
  # --- Formatters / linters --------------------------------------------------
@@ -129,6 +129,7 @@ from simulo.interfaces import (
129
129
  # re-exported here like every other concrete authoring type — no exclusion
130
130
  # needed any more.
131
131
  from simulo.interfaces.authoring import (
132
+ ActionChannel,
132
133
  ArrowMarker,
133
134
  AssetEntity,
134
135
  AssetSource,
@@ -143,6 +144,8 @@ from simulo.interfaces.authoring import (
143
144
  DomeLight,
144
145
  GroundPlane,
145
146
  LightConfig,
147
+ LineMarker,
148
+ ObjectVariants,
146
149
  PointMarker,
147
150
  PolicyCheckpoint,
148
151
  PolicyInput,
@@ -210,6 +213,7 @@ __all__ = [
210
213
  "Digest",
211
214
  # authoring value types (eager, real) — the rest of the vocabulary the
212
215
  # lazy resolver below does not carry
216
+ "ActionChannel",
213
217
  "Collision",
214
218
  "RigidPhysics",
215
219
  "DeformableMaterial",
@@ -219,6 +223,7 @@ __all__ = [
219
223
  "USDAsset",
220
224
  "URDFAsset",
221
225
  "LightConfig",
226
+ "ObjectVariants",
222
227
  "DistantLight",
223
228
  "DomeLight",
224
229
  "TerrainConfig",
@@ -238,6 +243,7 @@ __all__ = [
238
243
  "SphereMarker",
239
244
  "ArrowMarker",
240
245
  "PointMarker",
246
+ "LineMarker",
241
247
  # lazy, mode-aware (the learning names + save_output)
242
248
  "ActuatorGainsConfig",
243
249
  "ArticulationConfig",
@@ -310,8 +316,13 @@ def __getattr__(name: str) -> object:
310
316
  Fires only for attributes missing from the module dict — i.e. never for the
311
317
  real platform exports above. Delegates the learning names to the thin
312
318
  client's internal resolver (discovery stand-in vs. real
313
- ``simulo.core`` class, by mode); everything else is a genuine ``AttributeError``.
319
+ ``simulo.core`` class, by mode), and read ``__version__`` from installed
320
+ package metadata on demand. Unknown names raise ``AttributeError``.
314
321
  """
322
+ if name == "__version__":
323
+ from importlib.metadata import version
324
+
325
+ return version("simulo")
315
326
  if name in _LEARNING_NAMES:
316
327
  return _resolve_learning(name)
317
328
  removed_hints = {
@@ -23,7 +23,7 @@ from __future__ import annotations
23
23
 
24
24
  import time
25
25
  from pathlib import Path
26
- from typing import Any, NoReturn, Optional
26
+ from typing import Any, Mapping, NoReturn, Optional
27
27
 
28
28
  from simulo.interfaces.ids import JobId, JobPublicId
29
29
  from simulo.interfaces.platform.enums import JobStatus, JobType
@@ -91,6 +91,8 @@ class JobHandle:
91
91
  base_url: Optional[str] = None,
92
92
  token: Optional[str] = None,
93
93
  job_type: Optional[JobType] = None,
94
+ runtime_budget_s: Optional[int] = None,
95
+ job_args: Optional[Mapping[str, Any]] = None,
94
96
  input_checkpoint: Optional[str] = None,
95
97
  compare_checkpoint: Optional[str] = None,
96
98
  deferred_stdout: str = "",
@@ -104,6 +106,8 @@ class JobHandle:
104
106
  self._base_url = base_url
105
107
  self._token = token
106
108
  self._job_type = job_type
109
+ self._runtime_budget_s = runtime_budget_s
110
+ self._job_args = dict(job_args or {})
107
111
  self._input_checkpoint = input_checkpoint
108
112
  self._compare_checkpoint = compare_checkpoint
109
113
  self._deferred_stdout = deferred_stdout
@@ -149,6 +153,16 @@ class JobHandle:
149
153
  """Declared type for a cloud submission; absent on legacy local handles."""
150
154
  return self._job_type
151
155
 
156
+ @property
157
+ def runtime_budget_s(self) -> Optional[int]:
158
+ """The effective server-resolved time limit, in seconds, when served."""
159
+ return self._runtime_budget_s
160
+
161
+ @property
162
+ def job_args(self) -> Mapping[str, Any]:
163
+ """Training arguments for submit guidance, including declared defaults when known."""
164
+ return self._job_args
165
+
152
166
  @property
153
167
  def input_checkpoint(self) -> Optional[str]:
154
168
  """Policy checkpoint requested with ``simulo run --from``."""
@@ -48,12 +48,14 @@ from typing import (
48
48
 
49
49
  from simulo._client import config, credentials, registry
50
50
  from simulo._client._runner import JobFailedError, JobHandle, JobResultUnavailable
51
+ from simulo._client.repeatable_episodes import requested_repeatable_episodes
51
52
  from simulo._client.runtime import DEFAULT_RUNTIME
53
+ from simulo._client.system_requests import effective_resources, validate_resources
54
+ from simulo._client.training_algorithm import validate_training_algorithm
52
55
  from simulo.interfaces.ids import JobId, parse_job_public_id
53
56
  from simulo.interfaces.platform import JobCallbackProtocol, JobType, normalize_job_tags
54
57
  from simulo.interfaces.platform.app import Mount
55
58
  from simulo.interfaces.platform.enums import SystemType
56
- from simulo.interfaces.platform.systems import SYSTEM_SPECS
57
59
 
58
60
  if TYPE_CHECKING: # typing-only; avoids a module-scope dependency on the HTTP client
59
61
  from simulo._client.preflight_api import PreflightOutcome
@@ -131,19 +133,43 @@ def spawn_recorder() -> Iterator[list["JobHandle"]]:
131
133
  # through arbitrary user code" reason.
132
134
  # ---------------------------------------------------------------------------
133
135
 
134
- _VIEWSTREAM_REQUESTED = False
136
+ _VIEWSTREAM_REQUESTED: Optional[bool] = None
137
+ _VIEWSTREAM_NOTICE: Optional[str] = None
135
138
 
136
139
 
137
140
  @contextlib.contextmanager
138
- def viewstream_requested(enabled: bool) -> Iterator[None]:
139
- """Scope ``--viewstream`` for one ``simulo run`` submit call."""
140
- global _VIEWSTREAM_REQUESTED
141
+ def viewstream_requested(enabled: Optional[bool], *, notice: Optional[str] = None) -> Iterator[None]:
142
+ """Scope one resolved live-view request for one ``simulo run`` submit call."""
143
+ global _VIEWSTREAM_REQUESTED, _VIEWSTREAM_NOTICE
141
144
  previous = _VIEWSTREAM_REQUESTED
145
+ previous_notice = _VIEWSTREAM_NOTICE
142
146
  _VIEWSTREAM_REQUESTED = enabled
147
+ _VIEWSTREAM_NOTICE = notice
143
148
  try:
144
149
  yield
145
150
  finally:
146
151
  _VIEWSTREAM_REQUESTED = previous
152
+ _VIEWSTREAM_NOTICE = previous_notice
153
+
154
+
155
+ # ---------------------------------------------------------------------------
156
+ # 3D-recording ambient signal — `simulo run --record-3d COPIES` scopes an
157
+ # execution option without placing it in package arguments or the manifest.
158
+ # ---------------------------------------------------------------------------
159
+
160
+ _RECORDING_COPIES_REQUESTED: Optional[int] = None
161
+
162
+
163
+ @contextlib.contextmanager
164
+ def recording_copies_requested(copies: Optional[int]) -> Iterator[None]:
165
+ """Scope an optional 3D recording copy count for one submit."""
166
+ global _RECORDING_COPIES_REQUESTED
167
+ previous = _RECORDING_COPIES_REQUESTED
168
+ _RECORDING_COPIES_REQUESTED = copies
169
+ try:
170
+ yield
171
+ finally:
172
+ _RECORDING_COPIES_REQUESTED = previous
147
173
 
148
174
 
149
175
  # ---------------------------------------------------------------------------
@@ -526,7 +552,9 @@ def _spawn_cloud(
526
552
  package_path: Path,
527
553
  creds: Optional[dict[str, Any]],
528
554
  *,
529
- viewstream: bool = False,
555
+ viewstream: Optional[bool] = None,
556
+ recording_copies: Optional[int] = None,
557
+ repeatable_episodes: Optional[bool] = None,
530
558
  input_checkpoint: Optional[str] = None,
531
559
  compare_checkpoint: Optional[str] = None,
532
560
  require_success: Optional[float] = None,
@@ -536,6 +564,7 @@ def _spawn_cloud(
536
564
  app_name: Optional[str] = None,
537
565
  job_type: JobType,
538
566
  tags: Sequence[str] = (),
567
+ job_args: Optional[Mapping[str, Any]] = None,
539
568
  ) -> JobHandle:
540
569
  """Tar the bundle, preflight it, upload it, create the job, and return a
541
570
  cloud JobHandle.
@@ -599,7 +628,7 @@ def _spawn_cloud(
599
628
  job-create call's top-level ``assets`` field, never ``args``.
600
629
  """
601
630
  from simulo._client.asset_pins import (
602
- DEFAULT_ASSET_RUNTIME,
631
+ asset_runtime_for_runtime_id,
603
632
  collect_asset_refs,
604
633
  resolve_and_pin_assets,
605
634
  )
@@ -630,15 +659,12 @@ def _spawn_cloud(
630
659
  package_id = str(package_id)
631
660
  source_digest = str(manifest.get("source_digest", ""))
632
661
  args = dict(manifest.get("args") or {})
633
- # The requested GPU system tier (system-tier-catalog-1362, M50-persistence)
634
- # — already validated at `@app.job(system=...)` decoration time and
635
- # recorded on THIS job's own manifest entry as `resources["system"]`
636
- # (the tier's wire value, e.g. "tier1"; see `decorate()`'s
637
- # `resources["system"] = system.value`). Read from the already-parsed
638
- # manifest rather than threaded in as a new parameter, since the value
639
- # already lives there, keyed by this same `job_name`.
662
+ # CLI selections override this job's declared resources for this submit.
663
+ # They never enter args or rewrite the packaged manifest. Omitted options
664
+ # retain their existing wire representation for servers awaiting adoption.
640
665
  job_entry = manifest.get("jobs", {}).get(job_name) or {}
641
- system = (job_entry.get("resources") or {}).get("system")
666
+ allocation = effective_resources(job_entry.get("resources") or {})
667
+ system = allocation.pop("system", None)
642
668
  manifest_version = manifest.get("manifest_version")
643
669
  if not isinstance(manifest_version, int) or isinstance(manifest_version, bool):
644
670
  raise RuntimeError(
@@ -689,8 +715,10 @@ def _spawn_cloud(
689
715
  archive_size=len(archive_bytes),
690
716
  job_type=job_type,
691
717
  input_checkpoint=input_checkpoint,
692
- viewstream=viewstream,
718
+ viewstream=viewstream is True,
693
719
  assets=[] if not asset_refs else None,
720
+ **({"system": system} if system is not None else {}),
721
+ **allocation,
694
722
  )
695
723
  deferred_stdout, deferred_stderr = _handle_preflight_outcome(outcome, job_name=job_name)
696
724
 
@@ -700,13 +728,15 @@ def _spawn_cloud(
700
728
  # package is uploaded — so a missing/unpinned/unvalidated ref fails in seconds.
701
729
  # Returns the top-level `assets: [{ref, digest}]` for create_job; `[]`
702
730
  # when the app mounts no registry assets (no resolve call is made then).
703
- asset_pins = resolve_and_pin_assets(
704
- client,
705
- collect_asset_refs(manifest),
706
- runtime=DEFAULT_ASSET_RUNTIME,
707
- frozen=frozen_assets,
708
- strict=strict_assets,
709
- )
731
+ asset_pins: list[dict[str, str]] = []
732
+ if asset_refs:
733
+ asset_pins = resolve_and_pin_assets(
734
+ client,
735
+ asset_refs,
736
+ runtime=asset_runtime_for_runtime_id(str(manifest["runtime_id"])),
737
+ frozen=frozen_assets,
738
+ strict=strict_assets,
739
+ )
710
740
 
711
741
  client.create_package(
712
742
  package_id=package_id,
@@ -720,7 +750,10 @@ def _spawn_cloud(
720
750
  package_id=package_id,
721
751
  job_name=job_name,
722
752
  args=args,
753
+ runtime_id=str(manifest["runtime_id"]),
723
754
  viewstream=viewstream,
755
+ recording_copies=recording_copies,
756
+ repeatable_episodes=repeatable_episodes,
724
757
  input_checkpoint=input_checkpoint,
725
758
  compare_checkpoint=compare_checkpoint,
726
759
  require_success=require_success,
@@ -731,7 +764,16 @@ def _spawn_cloud(
731
764
  job_type=job_type,
732
765
  tags=tags,
733
766
  manifest_version=manifest_version,
767
+ **allocation,
734
768
  )
769
+ # A submitted entrypoint can call ``spawn()`` more than once. The
770
+ # resolved notice describes the run-level opt-in, so print it once only
771
+ # after the first successful job-create response. Do not consume it when
772
+ # create_job raises: then the run did not successfully submit anything.
773
+ global _VIEWSTREAM_NOTICE
774
+ if _VIEWSTREAM_NOTICE is not None:
775
+ print(_VIEWSTREAM_NOTICE, file=sys.stderr)
776
+ _VIEWSTREAM_NOTICE = None
735
777
 
736
778
  raw_public_id = record.get("public_id")
737
779
  try:
@@ -756,6 +798,8 @@ def _spawn_cloud(
756
798
  base_url=base_url,
757
799
  token=token,
758
800
  job_type=job_type,
801
+ runtime_budget_s=record.get("runtime_budget_s"),
802
+ job_args=job_args if job_args is not None else args,
759
803
  input_checkpoint=input_checkpoint,
760
804
  compare_checkpoint=compare_checkpoint,
761
805
  deferred_stdout=deferred_stdout,
@@ -813,6 +857,12 @@ class JobFunction:
813
857
  # job/success contract as ``simulo run``. This happens before the
814
858
  # intent marker, bundle construction, asset resolution, or network I/O.
815
859
  self._app.validate_job_submission(self._spec.name)
860
+ if self._spec.job_type is JobType.TRAIN:
861
+ parameter = inspect.signature(self._spec.fn).parameters.get("algorithm")
862
+ if "algorithm" in kwargs:
863
+ validate_training_algorithm(kwargs["algorithm"])
864
+ elif parameter is not None and parameter.default is not inspect.Parameter.empty:
865
+ validate_training_algorithm(parameter.default)
816
866
 
817
867
  # Intent-to-submit is recorded FIRST — before any packaging or network
818
868
  # I/O — so a submit that fails partway (or times out AFTER the server
@@ -861,6 +911,8 @@ class JobFunction:
861
911
  package_path,
862
912
  creds,
863
913
  viewstream=_VIEWSTREAM_REQUESTED,
914
+ recording_copies=_RECORDING_COPIES_REQUESTED,
915
+ repeatable_episodes=requested_repeatable_episodes(),
864
916
  input_checkpoint=_INPUT_CHECKPOINT_REF,
865
917
  compare_checkpoint=_COMPARE_CHECKPOINT_REF,
866
918
  require_success=_REQUIRE_SUCCESS,
@@ -870,6 +922,12 @@ class JobFunction:
870
922
  app_name=self._app.name,
871
923
  job_type=self._spec.job_type,
872
924
  tags=_JOB_TAGS_REQUESTED,
925
+ job_args={
926
+ name: parameter.default
927
+ for name, parameter in inspect.signature(self._spec.fn).parameters.items()
928
+ if parameter.default is not inspect.Parameter.empty
929
+ }
930
+ | kwargs,
873
931
  )
874
932
 
875
933
  if _SPAWN_RECORDER is None:
@@ -1256,6 +1314,8 @@ class App:
1256
1314
  type: Union[JobType, str],
1257
1315
  callbacks: Sequence[JobCallbackProtocol] = ...,
1258
1316
  system: Optional[SystemType] = ...,
1317
+ gpus: Optional[int] = ...,
1318
+ gpus_per_node: Optional[int] = ...,
1259
1319
  timeout: Optional[int] = ...,
1260
1320
  retries: int = ...,
1261
1321
  **extra: Any,
@@ -1269,6 +1329,8 @@ class App:
1269
1329
  type: Union[JobType, str],
1270
1330
  callbacks: Sequence[JobCallbackProtocol] = ...,
1271
1331
  system: Optional[SystemType] = ...,
1332
+ gpus: Optional[int] = ...,
1333
+ gpus_per_node: Optional[int] = ...,
1272
1334
  timeout: Optional[int] = ...,
1273
1335
  retries: int = ...,
1274
1336
  **extra: Any,
@@ -1281,6 +1343,8 @@ class App:
1281
1343
  type: Optional[Union[JobType, str]] = None,
1282
1344
  callbacks: Sequence[JobCallbackProtocol] = (),
1283
1345
  system: Optional[SystemType] = None,
1346
+ gpus: Optional[int] = None,
1347
+ gpus_per_node: Optional[int] = None,
1284
1348
  timeout: Optional[int] = None,
1285
1349
  retries: int = 0,
1286
1350
  **extra: Any,
@@ -1298,23 +1362,16 @@ class App:
1298
1362
  pass it alongside keyword options).
1299
1363
  callbacks: Lifecycle callbacks from :mod:`simulo.callbacks`, recorded
1300
1364
  into the package manifest and honored at execution.
1301
- system: The GPU system tier to request, as a
1302
- :class:`simulo.SystemType` member (e.g.
1303
- ``simulo.SystemType.TIER_1``), recorded in the job's resource
1304
- spec as ``resources["system"]`` (the tier's wire value, e.g.
1305
- ``"tier1"``). Validated HERE, at decoration time, against the
1306
- published catalog: a value that is not a ``SystemType`` member
1307
- (a raw string such as ``"tier1"`` included) raises
1308
- ``TypeError``, and a tier the platform cannot provision yet
1309
- raises ``ValueError``. ``simulo systems`` lists every tier and
1310
- which are available today. The selection is recorded on the
1311
- job AND scheduled against: the control plane filters a tiered
1312
- worker's claim to its own tier plus untiered work, so the tier
1313
- decides which GPU model runs the job. It is not a priority
1314
- setting. ``simulo systems`` reports this package's catalog,
1315
- not which tiers your platform has deployed. Naming a tier it
1316
- has no fleet for is refused when you submit, and the error
1317
- names the tiers it does have.
1365
+ system: Accelerator memory class as a :class:`simulo.SystemType` member.
1366
+ Omitted means Tier 1 (16 GB). Tier 2 is 24 GB, Tier 3 is 48 GB,
1367
+ and Tier 4 is 96 GB per accelerator. Hardware within a class can
1368
+ change. The built-in catalog enables Tier 1 and Tier 2 only;
1369
+ the server must also check its fleet readiness at submission.
1370
+ gpus: Total GPU count, a positive integer. Omitted means one GPU.
1371
+ Today's built-in policy permits one GPU per job.
1372
+ gpus_per_node: Optional positive integer layout constraint. All GPUs
1373
+ must run on one machine, so this must equal the total GPU count.
1374
+ It never multiplies ``gpus``. Counts reject booleans.
1318
1375
  timeout: Requested runtime limit for one managed execution, in
1319
1376
  seconds (a positive integer; validated HERE, at decoration
1320
1377
  time — a zero, negative, or non-integer value raises
@@ -1339,7 +1396,7 @@ class App:
1339
1396
  ValueError: ``timeout`` is not ``None`` and is not a positive
1340
1397
  integer. ``True`` and ``False`` are not accepted as timeouts.
1341
1398
  Also raised when ``system`` names a tier the platform cannot
1342
- provision yet (``SYSTEM_SPECS[system].available`` is false).
1399
+ provision, or a GPU count/layout is unsupported by the built-in policy.
1343
1400
  JobDeclarationError: ``produces=`` or ``consumes=`` was passed. Outputs are
1344
1401
  not declared on the decorator. Callbacks declare their own
1345
1402
  supported outputs. Return small JSON values from the job, or use a named
@@ -1381,28 +1438,12 @@ class App:
1381
1438
  "@app.job(system=simulo.SystemType.TIER_1). Run `simulo systems` to list the "
1382
1439
  "tiers and which are available today. Remove gpu=."
1383
1440
  )
1384
- if system is not None:
1385
- # Static lookups against the published catalog — no fleet state
1386
- # is consulted. A raw string is refused even when it spells a
1387
- # valid wire value, so the one accepted spelling is the enum.
1388
- if not isinstance(system, SystemType):
1389
- raise JobDeclarationError(
1390
- "@app.job(system=...) must be a simulo.SystemType member, e.g. "
1391
- f"system=simulo.SystemType.TIER_1; got {system!r}. Run `simulo systems` to "
1392
- "list the tiers."
1393
- )
1394
- spec = SYSTEM_SPECS[system]
1395
- if not spec.available:
1396
- available = ", ".join(
1397
- f"SystemType.{tier.name} ({entry.gpu_model})"
1398
- for tier, entry in SYSTEM_SPECS.items()
1399
- if entry.available
1400
- )
1401
- raise ValueError(
1402
- f"@app.job(system=SystemType.{system.name}) ({spec.gpu_model}) is not available on "
1403
- f"the platform yet. Available today: {available}. Run `simulo systems` for the "
1404
- "full catalog."
1405
- )
1441
+ if system is not None and not isinstance(system, SystemType):
1442
+ raise JobDeclarationError(
1443
+ "@app.job(system=...) must be a simulo.SystemType member, e.g. "
1444
+ f"system=simulo.SystemType.TIER_1; got {system!r}. Run `simulo systems` to list the tiers."
1445
+ )
1446
+ requested_resources = validate_resources(system, gpus, gpus_per_node)
1406
1447
 
1407
1448
  def decorate(func: F) -> JobFunction:
1408
1449
  location = Path(func.__code__.co_filename).name
@@ -1420,9 +1461,7 @@ class App:
1420
1461
  f"{location}: Job {func.__name__!r} has invalid type={type!r}. Valid job types: {valid}. "
1421
1462
  'Example: @app.job(type="train").'
1422
1463
  ) from None
1423
- resources: dict[str, Any] = {}
1424
- if system is not None:
1425
- resources["system"] = system.value
1464
+ resources = dict(requested_resources)
1426
1465
  resources.update(extra)
1427
1466
  policy_params = [
1428
1467
  name
@@ -1,7 +1,7 @@
1
1
  """``Asset`` — a read-only, registry-resolved mount (implements ``AssetProtocol``).
2
2
 
3
3
  A registry asset ref is ``[<publisher>/]<kind>/<name>[:v<N>]`` (e.g.
4
- ``robot/so-arm-100:v3``, ``simulo/world/warehouse:v2``, or the unpinned
4
+ ``robot/so-arm-100:v3``, ``simulo/world/warehouse:v3``, or the unpinned
5
5
  ``robot/forklift`` — resolved to an exact version **at submit**). The grammar
6
6
  is the ONE shared parser from ``simulo.interfaces.platform.asset_catalog`` — the
7
7
  CLI, the control plane, the worker, and the console all parse through it, so the
@@ -148,7 +148,7 @@ class Asset:
148
148
  """A read-only, registry-resolved resource mounted into running jobs.
149
149
 
150
150
  A registry ref is ``[<publisher>/]<kind>/<name>[:v<N>]`` (e.g.
151
- ``simulo/robot/cartpole:v1``); :meth:`from_registry` names one, and :attr:`path`
151
+ ``simulo/robot/cartpole:v5``); :meth:`from_registry` names one, and :attr:`path`
152
152
  resolves to its read-only mount inside a running job. ``Asset`` is also how a
153
153
  task names USD/URDF model sources to load into a scene — :meth:`usd` /
154
154
  :meth:`urdf`.
@@ -24,14 +24,33 @@ from typing import Any, Dict, List, Mapping, Optional, Sequence, TextIO
24
24
 
25
25
  from simulo._client.runtime_display import display_runtime
26
26
  from simulo._client.submit_api import AssetPinError, SubmitApiClient
27
- from simulo.interfaces.platform.asset_catalog import DEFAULT_ASSET_RUNTIME, ResolvedAssetPin, parse_asset_ref
27
+ from simulo.interfaces.platform.asset_catalog import (
28
+ DEFAULT_ASSET_RUNTIME,
29
+ ResolvedAssetPin,
30
+ parse_asset_ref,
31
+ )
32
+ from simulo.interfaces.platform.runtimes import RUNTIME_ID_BY_ASSET_RUNTIME
28
33
 
29
34
  #: Re-exported from ``simulo.interfaces.platform.asset_catalog`` (current code and tests
30
35
  #: "Runtime-string seam", 2026-07-17 — hoisted there so the CLIENT's resolve
31
36
  #: request and the VALIDATOR's finalize call can never send different runtime
32
37
  #: strings). Kept as a module attribute here too — existing call sites
33
38
  #: (``app.py``, tests) import it from this module.
34
- __all__ = ["DEFAULT_ASSET_RUNTIME", "collect_asset_refs", "resolve_and_pin_assets"]
39
+ __all__ = ["DEFAULT_ASSET_RUNTIME", "asset_runtime_for_runtime_id", "collect_asset_refs", "resolve_and_pin_assets"]
40
+
41
+
42
+ def asset_runtime_for_runtime_id(runtime_id: str) -> str:
43
+ """Return the catalog label published for a requested job runtime.
44
+
45
+ Asset validation records catalog labels while manifests request job runtime
46
+ identifiers. There is no safe label for an unlisted runtime: resolving an
47
+ asset under Isaac would make the validation result describe a different
48
+ runtime from the job that will mount it.
49
+ """
50
+ for asset_runtime, mapped_runtime_id in RUNTIME_ID_BY_ASSET_RUNTIME.items():
51
+ if runtime_id == mapped_runtime_id:
52
+ return asset_runtime
53
+ raise AssetPinError(f"runtime {runtime_id!r} has no published asset catalog mapping")
35
54
 
36
55
 
37
56
  def collect_asset_refs(manifest: Mapping[str, Any]) -> List[str]:
@@ -4,9 +4,10 @@ Shared by ``simulo run`` (the CLI) and ``JobFunction.spawn`` — the *submit* pa
4
4
  discover the app's metadata and write the package (manifest + source bundle) to
5
5
  disk. The manifest builder reads only the public ``App`` surface, so it works in
6
6
  discovery mode (where submit runs). ``ensure_bundle`` memoises the written package
7
- per *submission* — ``(app, entry_file, job_name, args)`` — so re-submitting the
8
- same file and job with the same args reuses the package, while a second spawn with
9
- a different entry file, job, or args writes a distinct package (no clobber).
7
+ per *submission* — ``(app, entry_file, job_name, args, captured assets)`` — so
8
+ re-submitting the same file and job with the same args and captured assets reuses
9
+ the package. Package identity hashes source, entry file, job, canonical args,
10
+ and the canonical asset list; changing any of those writes a distinct package.
10
11
 
11
12
  Submit NEVER executes a job and NEVER invokes the backend — it only writes the
12
13
  package. This module imports no heavy library; it stays on the torch-free side of
@@ -59,9 +60,9 @@ DEFAULT_OUT_SUBPATH = (".simulo", "packages")
59
60
  #: capture — see ``asset.py``) — included so a LATER spawn() in the same process,
60
61
  #: after user code has constructed MORE assets since an earlier spawn() with
61
62
  #: the identical ``(app, entry_file, job_name, args)``, is never served a stale cached bundle
62
- #: whose ``resources`` predates those constructions (``package_id`` itself never
63
- #: changes with the asset set — see the package-hash-invariance rule — so the
64
- #: `package_id` re-check below cannot by itself catch this staleness).
63
+ #: whose ``resources`` predates those constructions. The package id also hashes
64
+ #: resources, but a cache hit does not rebuild the manifest or recompute the id,
65
+ #: so this key must still distinguish the captured asset set.
65
66
  _BUNDLE_CACHE: dict[tuple[int, str, str, str, tuple[str, ...]], tuple[Path, str]] = {}
66
67
 
67
68
 
@@ -197,7 +198,8 @@ def ensure_bundle(
197
198
  This is the *submit* primitive: the manifest is built from the live app object
198
199
  (no second discovery import) with the submitted job's ``args`` recorded, and
199
200
  the source tree is written to ``<out>/<bundle_dir_name(package_id)>/`` — the
200
- per-submission directory keyed on source + entry file + ``job_name`` + ``args`` (named by
201
+ per-submission directory keyed on source + entry file + ``job_name`` + canonical
202
+ ``args`` + canonical asset list (named by
201
203
  the id's short, filesystem-safe form; see ``packaging.bundle_dir_name``).
202
204
  Memoised per submission (``(id(app), entry_file, job_name, canonical_json(args), sorted
203
205
  extra_assets uris)``) so re-submitting the same job with the same args AND the