simulo 0.34.0__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.34.0/src/simulo.egg-info → simulo-0.35.0}/PKG-INFO +5 -5
  2. {simulo-0.34.0 → simulo-0.35.0}/PYPI.md +1 -1
  3. {simulo-0.34.0 → simulo-0.35.0}/pyproject.toml +6 -5
  4. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/__init__.py +6 -1
  5. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/_runner.py +15 -1
  6. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/app.py +53 -60
  7. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/asset.py +2 -2
  8. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/asset_pins.py +21 -2
  9. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/cli.py +10 -7
  10. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/cli_rendering.py +10 -0
  11. simulo-0.35.0/src/simulo/_client/cli_runtime_limit.py +130 -0
  12. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/commands/recordings.py +43 -12
  13. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/commands/run.py +88 -26
  14. simulo-0.35.0/src/simulo/_client/commands/systems.py +93 -0
  15. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/credentials.py +6 -1
  16. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/http.py +66 -31
  17. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/jobs_api.py +6 -1
  18. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/login.py +20 -0
  19. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/preflight_api.py +9 -0
  20. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/preview_report.py +7 -3
  21. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/submit_api.py +16 -1
  22. simulo-0.35.0/src/simulo/_client/system_requests.py +82 -0
  23. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/templates/training/eval.py.tmpl +1 -1
  24. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/templates/training/play.py.tmpl +1 -1
  25. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/templates/training/preview.py.tmpl +1 -1
  26. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/templates/training/train.py.tmpl +1 -1
  27. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/viewstream_policy.py +21 -15
  28. {simulo-0.34.0 → simulo-0.35.0/src/simulo.egg-info}/PKG-INFO +5 -5
  29. {simulo-0.34.0 → simulo-0.35.0}/src/simulo.egg-info/SOURCES.txt +2 -0
  30. {simulo-0.34.0 → simulo-0.35.0}/src/simulo.egg-info/requires.txt +2 -2
  31. simulo-0.34.0/src/simulo/_client/commands/systems.py +0 -132
  32. {simulo-0.34.0 → simulo-0.35.0}/MANIFEST.in +0 -0
  33. {simulo-0.34.0 → simulo-0.35.0}/setup.cfg +0 -0
  34. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/__init__.py +0 -0
  35. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/_arguments.py +0 -0
  36. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/_mounts.py +0 -0
  37. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/_secure_downloads.py +0 -0
  38. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/asset_api.py +0 -0
  39. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/asset_package.py +0 -0
  40. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/builtin_aliases.py +0 -0
  41. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/bundle.py +0 -0
  42. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/cancel_api.py +0 -0
  43. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/cli_arguments.py +0 -0
  44. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/cli_context.py +0 -0
  45. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/cli_downloads.py +0 -0
  46. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/cli_errors.py +0 -0
  47. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/cli_job_reporting.py +0 -0
  48. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/cli_listing.py +0 -0
  49. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/cli_text.py +0 -0
  50. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/cli_waiting.py +0 -0
  51. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/commands/__init__.py +0 -0
  52. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/commands/assets.py +0 -0
  53. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/commands/auth.py +0 -0
  54. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/commands/create.py +0 -0
  55. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/commands/jobs.py +0 -0
  56. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/commands/policies.py +0 -0
  57. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/config.py +0 -0
  58. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/discovery.py +0 -0
  59. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/evaluation_report.py +0 -0
  60. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/export_api.py +0 -0
  61. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/facades.py +0 -0
  62. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/identity_api.py +0 -0
  63. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/install_samples.py +0 -0
  64. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/learning.py +0 -0
  65. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/mode.py +0 -0
  66. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/onnx_bundle.py +0 -0
  67. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/outputs.py +0 -0
  68. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/packaging.py +0 -0
  69. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/policies_api.py +0 -0
  70. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/preflight_render.py +0 -0
  71. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/recording_export.py +0 -0
  72. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/registry.py +0 -0
  73. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/repeatable_episodes.py +0 -0
  74. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/runtime.py +0 -0
  75. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/runtime_display.py +0 -0
  76. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/stub.py +0 -0
  77. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/templates/__init__.py +0 -0
  78. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/templates/training/simuloignore.tmpl +0 -0
  79. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/templates/training/task.py.tmpl +0 -0
  80. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/training_algorithm.py +0 -0
  81. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/view_fragment.py +0 -0
  82. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/view_session_api.py +0 -0
  83. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/_client/volume.py +0 -0
  84. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/callbacks.py +0 -0
  85. {simulo-0.34.0 → simulo-0.35.0}/src/simulo/py.typed +0 -0
  86. {simulo-0.34.0 → simulo-0.35.0}/src/simulo.egg-info/dependency_links.txt +0 -0
  87. {simulo-0.34.0 → simulo-0.35.0}/src/simulo.egg-info/entry_points.txt +0 -0
  88. {simulo-0.34.0 → 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.34.0
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,7 +31,7 @@ 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.26,>=0.25.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
@@ -40,7 +40,7 @@ Requires-Dist: simulo-viz<0.2,>=0.1.0; python_version >= "3.12" and extra == "vi
40
40
  Provides-Extra: assets
41
41
  Requires-Dist: usd-core<27,>=26.5; extra == "assets"
42
42
  Provides-Extra: dev
43
- Requires-Dist: black==26.5.1; extra == "dev"
43
+ Requires-Dist: black==26.10.1; extra == "dev"
44
44
  Requires-Dist: isort==8.0.1; extra == "dev"
45
45
  Requires-Dist: ruff==0.15.21; extra == "dev"
46
46
  Requires-Dist: mypy==2.3.0; extra == "dev"
@@ -124,7 +124,7 @@ simulo policy get policy_x:best # download a verified checkpoint
124
124
  simulo export policy_x:best # export a verified ONNX bundle
125
125
  simulo recordings # download MCAP rollout recordings
126
126
  simulo outputs # retrieve other saved outputs
127
- simulo view # view a running --viewstream job in 3D
127
+ simulo view # view a running job in 3D (live view is on by default)
128
128
  simulo cancel # stop a queued or running job
129
129
  ```
130
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.34.0"
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.25.0,<0.26",
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
@@ -105,7 +105,7 @@ assets = ["usd-core>=26.5,<27"]
105
105
  # `pytest-timeout` is pinned EXACT because it enforces the suite's hang bound;
106
106
  # changing the plugin under an unchanged timeout would change that guard's behavior.
107
107
  dev = [
108
- "black==26.5.1",
108
+ "black==26.10.1",
109
109
  "isort==8.0.1",
110
110
  "ruff==0.15.21",
111
111
  "mypy==2.3.0",
@@ -194,6 +194,7 @@ markers = [
194
194
  "isaac: requires NVIDIA Isaac Sim / IsaacLab (skipped on dev machines and in PR CI)",
195
195
  "gpu: requires a CUDA GPU (skipped on dev machines and in PR CI)",
196
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",
197
198
  ]
198
199
 
199
200
  # --- Formatters / linters --------------------------------------------------
@@ -316,8 +316,13 @@ def __getattr__(name: str) -> object:
316
316
  Fires only for attributes missing from the module dict — i.e. never for the
317
317
  real platform exports above. Delegates the learning names to the thin
318
318
  client's internal resolver (discovery stand-in vs. real
319
- ``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``.
320
321
  """
322
+ if name == "__version__":
323
+ from importlib.metadata import version
324
+
325
+ return version("simulo")
321
326
  if name in _LEARNING_NAMES:
322
327
  return _resolve_learning(name)
323
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``."""
@@ -50,12 +50,12 @@ from simulo._client import config, credentials, registry
50
50
  from simulo._client._runner import JobFailedError, JobHandle, JobResultUnavailable
51
51
  from simulo._client.repeatable_episodes import requested_repeatable_episodes
52
52
  from simulo._client.runtime import DEFAULT_RUNTIME
53
+ from simulo._client.system_requests import effective_resources, validate_resources
53
54
  from simulo._client.training_algorithm import validate_training_algorithm
54
55
  from simulo.interfaces.ids import JobId, parse_job_public_id
55
56
  from simulo.interfaces.platform import JobCallbackProtocol, JobType, normalize_job_tags
56
57
  from simulo.interfaces.platform.app import Mount
57
58
  from simulo.interfaces.platform.enums import SystemType
58
- from simulo.interfaces.platform.systems import SYSTEM_SPECS
59
59
 
60
60
  if TYPE_CHECKING: # typing-only; avoids a module-scope dependency on the HTTP client
61
61
  from simulo._client.preflight_api import PreflightOutcome
@@ -564,6 +564,7 @@ def _spawn_cloud(
564
564
  app_name: Optional[str] = None,
565
565
  job_type: JobType,
566
566
  tags: Sequence[str] = (),
567
+ job_args: Optional[Mapping[str, Any]] = None,
567
568
  ) -> JobHandle:
568
569
  """Tar the bundle, preflight it, upload it, create the job, and return a
569
570
  cloud JobHandle.
@@ -627,7 +628,7 @@ def _spawn_cloud(
627
628
  job-create call's top-level ``assets`` field, never ``args``.
628
629
  """
629
630
  from simulo._client.asset_pins import (
630
- DEFAULT_ASSET_RUNTIME,
631
+ asset_runtime_for_runtime_id,
631
632
  collect_asset_refs,
632
633
  resolve_and_pin_assets,
633
634
  )
@@ -658,15 +659,12 @@ def _spawn_cloud(
658
659
  package_id = str(package_id)
659
660
  source_digest = str(manifest.get("source_digest", ""))
660
661
  args = dict(manifest.get("args") or {})
661
- # The requested GPU system tier (system-tier-catalog-1362, M50-persistence)
662
- # — already validated at `@app.job(system=...)` decoration time and
663
- # recorded on THIS job's own manifest entry as `resources["system"]`
664
- # (the tier's wire value, e.g. "tier1"; see `decorate()`'s
665
- # `resources["system"] = system.value`). Read from the already-parsed
666
- # manifest rather than threaded in as a new parameter, since the value
667
- # 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.
668
665
  job_entry = manifest.get("jobs", {}).get(job_name) or {}
669
- system = (job_entry.get("resources") or {}).get("system")
666
+ allocation = effective_resources(job_entry.get("resources") or {})
667
+ system = allocation.pop("system", None)
670
668
  manifest_version = manifest.get("manifest_version")
671
669
  if not isinstance(manifest_version, int) or isinstance(manifest_version, bool):
672
670
  raise RuntimeError(
@@ -719,6 +717,8 @@ def _spawn_cloud(
719
717
  input_checkpoint=input_checkpoint,
720
718
  viewstream=viewstream is True,
721
719
  assets=[] if not asset_refs else None,
720
+ **({"system": system} if system is not None else {}),
721
+ **allocation,
722
722
  )
723
723
  deferred_stdout, deferred_stderr = _handle_preflight_outcome(outcome, job_name=job_name)
724
724
 
@@ -728,13 +728,15 @@ def _spawn_cloud(
728
728
  # package is uploaded — so a missing/unpinned/unvalidated ref fails in seconds.
729
729
  # Returns the top-level `assets: [{ref, digest}]` for create_job; `[]`
730
730
  # when the app mounts no registry assets (no resolve call is made then).
731
- asset_pins = resolve_and_pin_assets(
732
- client,
733
- collect_asset_refs(manifest),
734
- runtime=DEFAULT_ASSET_RUNTIME,
735
- frozen=frozen_assets,
736
- strict=strict_assets,
737
- )
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
+ )
738
740
 
739
741
  client.create_package(
740
742
  package_id=package_id,
@@ -748,6 +750,7 @@ def _spawn_cloud(
748
750
  package_id=package_id,
749
751
  job_name=job_name,
750
752
  args=args,
753
+ runtime_id=str(manifest["runtime_id"]),
751
754
  viewstream=viewstream,
752
755
  recording_copies=recording_copies,
753
756
  repeatable_episodes=repeatable_episodes,
@@ -761,6 +764,7 @@ def _spawn_cloud(
761
764
  job_type=job_type,
762
765
  tags=tags,
763
766
  manifest_version=manifest_version,
767
+ **allocation,
764
768
  )
765
769
  # A submitted entrypoint can call ``spawn()`` more than once. The
766
770
  # resolved notice describes the run-level opt-in, so print it once only
@@ -794,6 +798,8 @@ def _spawn_cloud(
794
798
  base_url=base_url,
795
799
  token=token,
796
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,
797
803
  input_checkpoint=input_checkpoint,
798
804
  compare_checkpoint=compare_checkpoint,
799
805
  deferred_stdout=deferred_stdout,
@@ -916,6 +922,12 @@ class JobFunction:
916
922
  app_name=self._app.name,
917
923
  job_type=self._spec.job_type,
918
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,
919
931
  )
920
932
 
921
933
  if _SPAWN_RECORDER is None:
@@ -1302,6 +1314,8 @@ class App:
1302
1314
  type: Union[JobType, str],
1303
1315
  callbacks: Sequence[JobCallbackProtocol] = ...,
1304
1316
  system: Optional[SystemType] = ...,
1317
+ gpus: Optional[int] = ...,
1318
+ gpus_per_node: Optional[int] = ...,
1305
1319
  timeout: Optional[int] = ...,
1306
1320
  retries: int = ...,
1307
1321
  **extra: Any,
@@ -1315,6 +1329,8 @@ class App:
1315
1329
  type: Union[JobType, str],
1316
1330
  callbacks: Sequence[JobCallbackProtocol] = ...,
1317
1331
  system: Optional[SystemType] = ...,
1332
+ gpus: Optional[int] = ...,
1333
+ gpus_per_node: Optional[int] = ...,
1318
1334
  timeout: Optional[int] = ...,
1319
1335
  retries: int = ...,
1320
1336
  **extra: Any,
@@ -1327,6 +1343,8 @@ class App:
1327
1343
  type: Optional[Union[JobType, str]] = None,
1328
1344
  callbacks: Sequence[JobCallbackProtocol] = (),
1329
1345
  system: Optional[SystemType] = None,
1346
+ gpus: Optional[int] = None,
1347
+ gpus_per_node: Optional[int] = None,
1330
1348
  timeout: Optional[int] = None,
1331
1349
  retries: int = 0,
1332
1350
  **extra: Any,
@@ -1344,23 +1362,16 @@ class App:
1344
1362
  pass it alongside keyword options).
1345
1363
  callbacks: Lifecycle callbacks from :mod:`simulo.callbacks`, recorded
1346
1364
  into the package manifest and honored at execution.
1347
- system: The GPU system tier to request, as a
1348
- :class:`simulo.SystemType` member (e.g.
1349
- ``simulo.SystemType.TIER_1``), recorded in the job's resource
1350
- spec as ``resources["system"]`` (the tier's wire value, e.g.
1351
- ``"tier1"``). Validated HERE, at decoration time, against the
1352
- published catalog: a value that is not a ``SystemType`` member
1353
- (a raw string such as ``"tier1"`` included) raises
1354
- ``TypeError``, and a tier the platform cannot provision yet
1355
- raises ``ValueError``. ``simulo systems`` lists every tier and
1356
- which are available today. The selection is recorded on the
1357
- job AND scheduled against: the control plane filters a tiered
1358
- worker's claim to its own tier plus untiered work, so the tier
1359
- decides which GPU model runs the job. It is not a priority
1360
- setting. ``simulo systems`` reports this package's catalog,
1361
- not which tiers your platform has deployed. Naming a tier it
1362
- has no fleet for is refused when you submit, and the error
1363
- 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.
1364
1375
  timeout: Requested runtime limit for one managed execution, in
1365
1376
  seconds (a positive integer; validated HERE, at decoration
1366
1377
  time — a zero, negative, or non-integer value raises
@@ -1385,7 +1396,7 @@ class App:
1385
1396
  ValueError: ``timeout`` is not ``None`` and is not a positive
1386
1397
  integer. ``True`` and ``False`` are not accepted as timeouts.
1387
1398
  Also raised when ``system`` names a tier the platform cannot
1388
- provision yet (``SYSTEM_SPECS[system].available`` is false).
1399
+ provision, or a GPU count/layout is unsupported by the built-in policy.
1389
1400
  JobDeclarationError: ``produces=`` or ``consumes=`` was passed. Outputs are
1390
1401
  not declared on the decorator. Callbacks declare their own
1391
1402
  supported outputs. Return small JSON values from the job, or use a named
@@ -1427,28 +1438,12 @@ class App:
1427
1438
  "@app.job(system=simulo.SystemType.TIER_1). Run `simulo systems` to list the "
1428
1439
  "tiers and which are available today. Remove gpu=."
1429
1440
  )
1430
- if system is not None:
1431
- # Static lookups against the published catalog — no fleet state
1432
- # is consulted. A raw string is refused even when it spells a
1433
- # valid wire value, so the one accepted spelling is the enum.
1434
- if not isinstance(system, SystemType):
1435
- raise JobDeclarationError(
1436
- "@app.job(system=...) must be a simulo.SystemType member, e.g. "
1437
- f"system=simulo.SystemType.TIER_1; got {system!r}. Run `simulo systems` to "
1438
- "list the tiers."
1439
- )
1440
- spec = SYSTEM_SPECS[system]
1441
- if not spec.available:
1442
- available = ", ".join(
1443
- f"SystemType.{tier.name} ({entry.gpu_model})"
1444
- for tier, entry in SYSTEM_SPECS.items()
1445
- if entry.available
1446
- )
1447
- raise ValueError(
1448
- f"@app.job(system=SystemType.{system.name}) ({spec.gpu_model}) is not available on "
1449
- f"the platform yet. Available today: {available}. Run `simulo systems` for the "
1450
- "full catalog."
1451
- )
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)
1452
1447
 
1453
1448
  def decorate(func: F) -> JobFunction:
1454
1449
  location = Path(func.__code__.co_filename).name
@@ -1466,9 +1461,7 @@ class App:
1466
1461
  f"{location}: Job {func.__name__!r} has invalid type={type!r}. Valid job types: {valid}. "
1467
1462
  'Example: @app.job(type="train").'
1468
1463
  ) from None
1469
- resources: dict[str, Any] = {}
1470
- if system is not None:
1471
- resources["system"] = system.value
1464
+ resources = dict(requested_resources)
1472
1465
  resources.update(extra)
1473
1466
  policy_params = [
1474
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]:
@@ -41,14 +41,12 @@ wheel's own bundled resources.
41
41
  saved sign-in state; a failed clone is removed before it can appear at that path.
42
42
 
43
43
  ``simulo systems [--json]`` lists the published GPU system tiers (``SystemType`` /
44
- ``SYSTEM_SPECS`` from ``simulo-interfaces``) that ``@app.job(system=...)`` accepts —
44
+ ``SYSTEM_TIERS`` from ``simulo-interfaces``) that ``@app.job(system=...)`` accepts —
45
45
  table or ``--json``. This reads only that static, in-package catalog: no client
46
- construction, no credential resolution, no network call. The listing is descriptive
47
- rather than a live capability check: it reports what the catalog publishes, not which
48
- tiers this platform has actually deployed, and the table's own footer says so. The
49
- requested tier IS scheduled against — ``jobs/repo.py``'s claim predicate filters a
50
- tiered worker to its own tier plus untiered work — so this listing is not a
51
- record-only surface.
46
+ construction, no credential resolution, no network call. It shows each tier's memory
47
+ class, enabled GPU models, and static planning estimates for enabled tiers. It is not a
48
+ live fleet or price check; the server can apply additional deployment checks when a job
49
+ is submitted.
52
50
 
53
51
  **Auth** (``simulo login`` / ``logout`` / ``whoami``) drives the OAuth 2.0 +
54
52
  PKCE loopback flow and manages
@@ -108,6 +106,8 @@ import os
108
106
  import sys
109
107
  from typing import Optional, Sequence
110
108
 
109
+ from simulo._client import http
110
+
111
111
 
112
112
  def _resolve_version() -> str:
113
113
  """Resolve the installed ``simulo`` distribution version for ``--version``.
@@ -209,6 +209,9 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
209
209
  # interpreter shutdown, where a closed reader fails outside any handler.
210
210
  sys.stdout.flush()
211
211
  sys.stderr.flush()
212
+ except (http.InsecureTransportError, http.InsecureBearerError) as exc:
213
+ print(f"error: {_safe_cli_error_text(exc)}", file=sys.stderr)
214
+ return 1
212
215
  except KeyboardInterrupt:
213
216
  # Last-resort safety net: every dispatched command already has its own
214
217
  # clean handling (``_observe_handler`` for the observe verbs,
@@ -170,6 +170,16 @@ def _relative_time(value: Any) -> str:
170
170
  return f"on {_short_date(value)}"
171
171
 
172
172
 
173
+ def _runtime_limit_text(runtime_budget_s: int) -> str:
174
+ """Render an exact server-resolved runtime budget in a compact unit."""
175
+ if runtime_budget_s % 60 == 0:
176
+ quantity = runtime_budget_s // 60
177
+ unit = "minute" if quantity == 1 else "minutes"
178
+ return f"{quantity} {unit}"
179
+ unit = "second" if runtime_budget_s == 1 else "seconds"
180
+ return f"{runtime_budget_s} {unit}"
181
+
182
+
173
183
  def _print_table(headers: tuple[str, ...], rows: Sequence[tuple[str, ...]]) -> None:
174
184
  """The compact whitespace-aligned table used by CLI list commands
175
185
  already render — factored out here as a shared helper for the newer
@@ -0,0 +1,130 @@
1
+ """Best-effort runtime guidance based on a saved source job's measured progress."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ import shlex
7
+ from dataclasses import dataclass
8
+ from datetime import datetime
9
+ from typing import Any, Optional
10
+
11
+ from simulo._client import http, preview_report
12
+ from simulo._client._runner import JobHandle
13
+ from simulo._client.cli_rendering import _runtime_limit_text
14
+ from simulo._client.jobs_api import JobsApiClient
15
+ from simulo._client.policies_api import PoliciesApiClient
16
+ from simulo.interfaces.ids import parse_checkpoint_ref, policy_id_for_job
17
+ from simulo.interfaces.platform.runs import JOB_SCOPE_ORG
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class CheckpointProgress:
22
+ iteration: int
23
+ iterations_per_minute: Optional[float] = None
24
+
25
+
26
+ def _checkpoint(client: PoliciesApiClient, ref: str) -> tuple[dict[str, Any], dict[str, Any]]:
27
+ policy_id, kind = parse_checkpoint_ref(ref)
28
+ policy = client.get_policy(str(policy_id))
29
+ checkpoints = policy.get("checkpoints")
30
+ if isinstance(checkpoints, list):
31
+ for checkpoint in checkpoints:
32
+ if isinstance(checkpoint, dict) and checkpoint.get("kind") == kind:
33
+ return policy, checkpoint
34
+ return policy, {}
35
+
36
+
37
+ def checkpoint_progress(handle: JobHandle, ref: str, *, measure_rate: bool = False) -> Optional[CheckpointProgress]:
38
+ """Read the selected checkpoint once; unavailable measurements leave submission intact."""
39
+ assert handle.base_url is not None
40
+ client = PoliciesApiClient(handle.base_url, token=handle.token)
41
+ progress: Optional[CheckpointProgress] = None
42
+ try:
43
+ policy, checkpoint = _checkpoint(client, ref)
44
+ iteration = checkpoint.get("iteration")
45
+ if type(iteration) is not int or iteration < 0:
46
+ return None
47
+ progress = CheckpointProgress(iteration)
48
+ if not measure_rate or not isinstance(policy.get("job_public_id"), str):
49
+ return progress
50
+ source = JobsApiClient(handle.base_url, token=handle.token).get_job(
51
+ policy["job_public_id"], scope=JOB_SCOPE_ORG
52
+ )
53
+ return CheckpointProgress(iteration, _source_rate(client, source, checkpoint))
54
+ except (ValueError, http.HttpError):
55
+ return progress
56
+
57
+
58
+ def _timestamp(value: Any) -> Optional[datetime]:
59
+ if not isinstance(value, str):
60
+ return None
61
+ try:
62
+ parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
63
+ except ValueError:
64
+ return None
65
+ return parsed if parsed.tzinfo is not None else None
66
+
67
+
68
+ def _source_rate(client: PoliciesApiClient, source: dict[str, Any], checkpoint: dict[str, Any]) -> Optional[float]:
69
+ """Retries and unresolved starting checkpoints cannot give a reliable single-job rate."""
70
+ if source.get("attempts", 1) != 1:
71
+ return None
72
+ started, saved = _timestamp(source.get("started_at")), _timestamp(checkpoint.get("updated_at"))
73
+ if started is None or saved is None or saved <= started:
74
+ return None
75
+ start_iteration = 0
76
+ if source.get("input_checkpoint"):
77
+ _, starting_checkpoint = _checkpoint(client, source["input_checkpoint"])
78
+ candidate = starting_checkpoint.get("iteration")
79
+ if type(candidate) is not int or candidate < 0:
80
+ return None
81
+ start_iteration = candidate
82
+ iterations = checkpoint["iteration"] - start_iteration
83
+ return iterations * 60 / (saved - started).total_seconds() if iterations > 0 else None
84
+
85
+
86
+ def runtime_limit_lines(handle: JobHandle, progress: Optional[CheckpointProgress]) -> list[str]:
87
+ """Estimate remaining iterations only when both a target and measured rate are known."""
88
+ budget = handle.runtime_budget_s
89
+ if budget is None:
90
+ return []
91
+ lines = [
92
+ f"Runtime limit: {_runtime_limit_text(budget)}. "
93
+ "The platform uses the lower of your organization's cap and any @app.job(timeout=...) limit. "
94
+ f"To raise your organization's cap, email {preview_report.SUPPORT_EMAIL}."
95
+ ]
96
+ total = handle.job_args.get("max_iterations")
97
+ if (
98
+ progress is None
99
+ or progress.iterations_per_minute is None
100
+ or type(total) is not int
101
+ or total <= progress.iteration
102
+ ):
103
+ return lines
104
+ minutes = (total - progress.iteration) / progress.iterations_per_minute
105
+ lines.append(
106
+ f"Estimated remaining training time: {_runtime_limit_text(math.ceil(minutes) * 60)} "
107
+ f"at the source job's rate of {progress.iterations_per_minute:.3g} iterations/min. "
108
+ "Measured from source job start to checkpoint save; startup time can differ."
109
+ )
110
+ if minutes * 60 > budget:
111
+ lines.append("Warning: the requested iterations are unlikely to finish within the runtime limit.")
112
+ return lines
113
+
114
+
115
+ def runtime_recovery_lines(client: JobsApiClient, handle: JobHandle, app_path: str) -> list[str]:
116
+ """A failed follow still reports its outcome if this best-effort read fails."""
117
+ job_ref = str(handle.public_id or handle.job_id)
118
+ try:
119
+ record = client.get_job(job_ref)
120
+ except http.HttpError:
121
+ return []
122
+ if record.get("status") != "timed_out" or record.get("status_reason") != "org_runtime_limit_exceeded":
123
+ return []
124
+ checkpoint = f"{policy_id_for_job(job_ref)}:latest"
125
+ return [
126
+ "Runtime limit reached.",
127
+ f"Check for a saved checkpoint: simulo policy get {checkpoint}",
128
+ f"To continue, use simulo run {shlex.quote(app_path)} --from {checkpoint} "
129
+ "with the original training arguments and a higher --max-iterations total.",
130
+ ]