simulo 0.26.0__py3-none-any.whl

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 (55) hide show
  1. simulo/__init__.py +433 -0
  2. simulo/_client/__init__.py +6 -0
  3. simulo/_client/_entrypoint.py +313 -0
  4. simulo/_client/_mounts.py +25 -0
  5. simulo/_client/_runner.py +186 -0
  6. simulo/_client/_secure_downloads.py +1181 -0
  7. simulo/_client/app.py +1308 -0
  8. simulo/_client/asset.py +331 -0
  9. simulo/_client/asset_api.py +517 -0
  10. simulo/_client/asset_package.py +1103 -0
  11. simulo/_client/asset_pins.py +187 -0
  12. simulo/_client/builtin_aliases.py +107 -0
  13. simulo/_client/bundle.py +254 -0
  14. simulo/_client/cancel_api.py +104 -0
  15. simulo/_client/cli.py +9063 -0
  16. simulo/_client/config.py +186 -0
  17. simulo/_client/credentials.py +210 -0
  18. simulo/_client/discovery.py +214 -0
  19. simulo/_client/export_api.py +212 -0
  20. simulo/_client/export_bundle.py +296 -0
  21. simulo/_client/facades.py +581 -0
  22. simulo/_client/http.py +414 -0
  23. simulo/_client/identity_api.py +117 -0
  24. simulo/_client/install_samples.py +267 -0
  25. simulo/_client/jobs_api.py +224 -0
  26. simulo/_client/learning.py +393 -0
  27. simulo/_client/login.py +319 -0
  28. simulo/_client/mode.py +29 -0
  29. simulo/_client/outputs.py +116 -0
  30. simulo/_client/packaging.py +445 -0
  31. simulo/_client/preflight_api.py +186 -0
  32. simulo/_client/preflight_render.py +200 -0
  33. simulo/_client/registry.py +98 -0
  34. simulo/_client/runtime.py +185 -0
  35. simulo/_client/runtime_display.py +90 -0
  36. simulo/_client/seed_ref.py +76 -0
  37. simulo/_client/stub.py +41 -0
  38. simulo/_client/submit_api.py +1057 -0
  39. simulo/_client/templates/__init__.py +21 -0
  40. simulo/_client/templates/inference/app.py.tmpl +316 -0
  41. simulo/_client/templates/inference/simuloignore.tmpl +30 -0
  42. simulo/_client/templates/scenario/app.py.tmpl +93 -0
  43. simulo/_client/templates/scenario/simuloignore.tmpl +27 -0
  44. simulo/_client/templates/training/app.py.tmpl +235 -0
  45. simulo/_client/templates/training/simuloignore.tmpl +29 -0
  46. simulo/_client/view_fragment.py +21 -0
  47. simulo/_client/view_session_api.py +122 -0
  48. simulo/_client/volume.py +71 -0
  49. simulo/callbacks.py +274 -0
  50. simulo/py.typed +0 -0
  51. simulo-0.26.0.dist-info/METADATA +130 -0
  52. simulo-0.26.0.dist-info/RECORD +55 -0
  53. simulo-0.26.0.dist-info/WHEEL +5 -0
  54. simulo-0.26.0.dist-info/entry_points.txt +2 -0
  55. simulo-0.26.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,21 @@
1
+ """Starter-app templates shipped INSIDE the ``simulo`` wheel.
2
+
3
+ The repo's ``demos/`` tree lives outside the packaged ``src/`` layout and never
4
+ reaches a real ``pip install simulo`` user — this package is the fix. Each
5
+ subdirectory (``training/``, ``inference/``, ``scenario/``) holds a ``--type``
6
+ choice for ``simulo create``: an ``app.py.tmpl`` + a ``simuloignore.tmpl``.
7
+
8
+ The ``.tmpl`` suffix is load-bearing, not cosmetic: these files contain
9
+ ``import torch`` and other content that is not a valid, importable module on
10
+ its own, so the suffix keeps ruff / black / isort / mypy --strict / pytest
11
+ collection off them (all of those tools glob ``*.py``). ``simulo create``
12
+ reads them via ``importlib.resources.files(__name__)`` (never ``__file__`` path
13
+ math, which breaks once this package is zipped/installed) and substitutes the
14
+ single ``{{APP_NAME}}`` token with ``str.replace`` — this is not a template
15
+ engine.
16
+
17
+ This ``__init__.py`` itself exists so ``importlib.resources.files(__name__)``
18
+ resolves this templates directory as a real package (required for
19
+ ``setuptools.packages.find`` to discover it, and for ``package-data`` in
20
+ ``pyproject.toml`` to ship the ``.tmpl`` files inside it).
21
+ """
@@ -0,0 +1,316 @@
1
+ """{{APP_NAME}} — a Simulo train -> evaluate -> infer app (scaffolded by `simulo create`).
2
+
3
+ Three ``@app.job`` functions sharing two volumes (checkpoints + reports) — a full
4
+ policy lifecycle in one app, self-contained: ``train`` produces its own checkpoint,
5
+ so ``evaluate`` / ``rollout`` are never stranded waiting on another app.
6
+ ``--job`` picks the stage and the CLI maps the remaining ``--flags`` onto that
7
+ job's own parameters. Submit ONE stage per ``simulo run`` (each stage is a
8
+ separate command; run them in order)::
9
+
10
+ simulo run app.py --job train --num-envs 512 --max-iterations 20
11
+ simulo run app.py --job evaluate --num-episodes 10 --num-rounds 5
12
+ simulo run app.py --job rollout --num-steps 200
13
+
14
+ WHERE TO EDIT
15
+ -------------
16
+ * ``{{APP_NAME}}Task`` — ``build()`` the scene and write the observation / reward /
17
+ termination logic for your own robot and task.
18
+ * ``train`` / ``evaluate`` / ``rollout`` — hyperparameters, metrics, what gets
19
+ recorded; each job's parameters ARE its CLI flags (``num_envs`` -> ``--num-envs``).
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import math
25
+ import os
26
+ from typing import Any, Tuple
27
+
28
+ import simulo
29
+
30
+ # The cartpole robot — a global-catalog asset. Swap the ref for your own robot
31
+ # (``simulo asset publish`` your own USD/URDF, or browse the catalog with
32
+ # ``simulo asset list --global``).
33
+ cartpole = simulo.Asset.from_registry("simulo/robot/cartpole:v1")
34
+
35
+ # Two named, durable volumes: one for the trained checkpoints (the skrl checkpoint
36
+ # and the exported TorchScript policy), one for the JSON reports + MCAP recording.
37
+ checkpoints = simulo.Volume.from_name("{{APP_NAME}}-checkpoints", create_if_missing=True)
38
+ reports = simulo.Volume.from_name("{{APP_NAME}}-reports", create_if_missing=True)
39
+
40
+ # Advanced: pick a different Simulo runtime — App("name", runtime=simulo.Runtime.from_registry("simulo/gpu-rl:2026.06")). See the Runtimes docs.
41
+ app = simulo.App("{{APP_NAME}}", mounts={"/checkpoints": checkpoints, "/reports": reports})
42
+
43
+ # The ONE module-level heavy import, deferred under the runtime guard so submit
44
+ # records it as a remote import instead of resolving it.
45
+ with app.runtime.imports():
46
+ import torch # noqa: F401 (resolved only in execution mode, on the worker)
47
+
48
+ # Stable filenames inside the checkpoint volume, shared across the three jobs.
49
+ _CHECKPOINT_FILE = "{{APP_NAME}}_final.pt"
50
+ _POLICY_FILE = "{{APP_NAME}}_policy.pt"
51
+
52
+
53
+ @app.runtime.torch_jit
54
+ def _compute_rewards(
55
+ rew_scale_alive: float,
56
+ rew_scale_terminated: float,
57
+ rew_scale_pole_pos: float,
58
+ rew_scale_cart_vel: float,
59
+ rew_scale_pole_vel: float,
60
+ pole_pos: torch.Tensor,
61
+ pole_vel: torch.Tensor,
62
+ cart_pos: torch.Tensor,
63
+ cart_vel: torch.Tensor,
64
+ reset_terminated: torch.Tensor,
65
+ ) -> torch.Tensor:
66
+ """JIT-compiled reward kernel. EDIT: score your own task here."""
67
+ pole_pos = pole_pos.squeeze()
68
+ pole_vel = pole_vel.squeeze()
69
+ cart_pos = cart_pos.squeeze()
70
+ cart_vel = cart_vel.squeeze()
71
+ reset_terminated = reset_terminated.squeeze()
72
+
73
+ rew_alive = rew_scale_alive * (1.0 - reset_terminated.float())
74
+ rew_termination = rew_scale_terminated * reset_terminated.float()
75
+ rew_pole_pos = rew_scale_pole_pos * torch.square(pole_pos)
76
+ rew_cart_vel = rew_scale_cart_vel * torch.abs(cart_vel)
77
+ rew_pole_vel = rew_scale_pole_vel * torch.abs(pole_vel)
78
+
79
+ reward: torch.Tensor = rew_alive + rew_termination + rew_pole_pos + rew_cart_vel + rew_pole_vel
80
+ return reward.view(-1)
81
+
82
+
83
+ class {{APP_NAME}}Task(simulo.Task):
84
+ """Balance a pole on a cart. EDIT: replace with your own robot and task.
85
+
86
+ Observation (4-dim): pole angle, pole angular velocity, cart position, cart
87
+ velocity. Action (1-dim): scaled horizontal force on the cart.
88
+ """
89
+
90
+ observation_dim = 4
91
+ action_dim = 1
92
+
93
+ episode_length_s = 5.0
94
+ action_scale = 100.0 # [N]
95
+
96
+ max_cart_pos = 3.0 # [m]
97
+ initial_pole_angle_range = (-0.25, 0.25) # fraction of pi [rad]
98
+
99
+ rew_scale_alive = 1.0
100
+ rew_scale_terminated = -2.0
101
+ rew_scale_pole_pos = -1.0
102
+ rew_scale_cart_vel = -0.01
103
+ rew_scale_pole_vel = -0.005
104
+
105
+ # Framework-injected at runtime by ``simulo.core.Task`` / ``LearningEnv``
106
+ # (declared here only so the type checker sees the names the methods read;
107
+ # the annotations are PEP 563 strings and never shadow the inherited values).
108
+ device: str
109
+ max_episode_length: int
110
+ episode_length_buf: torch.Tensor
111
+ reset_terminated: torch.Tensor
112
+
113
+ def build(self, scene: simulo.Scene) -> None:
114
+ # EDIT: swap the ground / light / robot below for your own scene.
115
+ scene.add(simulo.Terrain.plane(name="ground"), at="/", per_environment=False)
116
+ scene.add(
117
+ simulo.Light.dome(name="light", intensity=2000.0, color=(0.75, 0.75, 0.75)),
118
+ at="/",
119
+ per_environment=False,
120
+ )
121
+ self.robot = simulo.Robot(asset=cartpole, initial_pose=simulo.Pose.identity())
122
+ scene.add(self.robot, at="/World/Robot")
123
+
124
+ def on_start(self, env: simulo.LearningEnv) -> None:
125
+ self._cart_dof_idx = self.robot.find_joints("slider_to_cart")
126
+ self._pole_dof_idx = self.robot.find_joints("cart_to_pole")
127
+ # robot.state is the supported, typed way to read live state (robot.internals
128
+ # is the unstable engine escape hatch — see the Scene, Robot & World docs).
129
+ self._joint_pos = self.robot.state.joint_positions
130
+ self._joint_vel = self.robot.state.joint_velocities
131
+
132
+ def get_observations(self) -> torch.Tensor:
133
+ # EDIT: return your own observation vector.
134
+ pole_idx = self._pole_dof_idx[0]
135
+ cart_idx = self._cart_dof_idx[0]
136
+ pole_pos = self._joint_pos[:, pole_idx].view(-1, 1)
137
+ pole_vel = self._joint_vel[:, pole_idx].view(-1, 1)
138
+ cart_pos = self._joint_pos[:, cart_idx].view(-1, 1)
139
+ cart_vel = self._joint_vel[:, cart_idx].view(-1, 1)
140
+ return torch.cat((pole_pos, pole_vel, cart_pos, cart_vel), dim=-1)
141
+
142
+ def get_rewards(self) -> torch.Tensor:
143
+ return _compute_rewards(
144
+ self.rew_scale_alive,
145
+ self.rew_scale_terminated,
146
+ self.rew_scale_pole_pos,
147
+ self.rew_scale_cart_vel,
148
+ self.rew_scale_pole_vel,
149
+ self._joint_pos[:, self._pole_dof_idx[0]],
150
+ self._joint_vel[:, self._pole_dof_idx[0]],
151
+ self._joint_pos[:, self._cart_dof_idx[0]],
152
+ self._joint_vel[:, self._cart_dof_idx[0]],
153
+ self.reset_terminated,
154
+ )
155
+
156
+ def get_dones(self) -> Tuple[torch.Tensor, torch.Tensor]:
157
+ # EDIT: your own termination condition.
158
+ self._joint_pos = self.robot.state.joint_positions
159
+ self._joint_vel = self.robot.state.joint_velocities
160
+ pole_idx = self._pole_dof_idx[0]
161
+ cart_idx = self._cart_dof_idx[0]
162
+ truncated = self.episode_length_buf >= self.max_episode_length - 1
163
+ cart_out = torch.abs(self._joint_pos[:, cart_idx]) > self.max_cart_pos
164
+ pole_fallen = torch.abs(self._joint_pos[:, pole_idx]) > math.pi / 2
165
+ terminated = cart_out | pole_fallen
166
+ return terminated, truncated
167
+
168
+ def apply_actions(self, actions: torch.Tensor) -> None:
169
+ self.robot.set_joint_effort_target(self.action_scale * actions, joint_ids=self._cart_dof_idx)
170
+
171
+ def reset_idx(self, env_ids: torch.Tensor) -> None:
172
+ num_resets = len(env_ids)
173
+ if num_resets == 0:
174
+ return
175
+ self.robot.reset(env_ids)
176
+ pole_idx = self._pole_dof_idx[0]
177
+ # robot.state has no default-joint-value equivalent, so this stays on the
178
+ # internals escape hatch (there is nothing unstable about reading it here,
179
+ # just no supported, typed name for it yet).
180
+ joint_pos = self.robot.internals.default_joint_pos[env_ids].clone()
181
+ random_angles = torch.empty(num_resets, device=self.device).uniform_(
182
+ self.initial_pole_angle_range[0] * math.pi,
183
+ self.initial_pole_angle_range[1] * math.pi,
184
+ )
185
+ joint_pos[:, pole_idx] += random_angles
186
+ # set_joint_state writes both positions and velocities through the engine's
187
+ # own command path (Articulation.write_joint_state_to_sim ->
188
+ # write_joint_{position,velocity}_to_sim), which updates robot.state's
189
+ # backing buffers in place AND pushes to the physics view in the same call.
190
+ # self._joint_pos / self._joint_vel are the SAME objects as those buffers
191
+ # (the hold-safety contract on core/robot.py), so they are already current
192
+ # after this call — no separate write into either tensor is needed, and
193
+ # robot.state's contract is never write into a member's tensor directly.
194
+ joint_vel = self.robot.internals.default_joint_vel[env_ids]
195
+ self.robot.set_joint_state(joint_pos, velocities=joint_vel, env_ids=env_ids)
196
+
197
+
198
+ def _make_env(num_envs: int) -> Any:
199
+ """Construct the shared environment (same sim settings across all three jobs)."""
200
+ return simulo.LearningEnv(
201
+ task={{APP_NAME}}Task(),
202
+ num_envs=num_envs,
203
+ device="cuda",
204
+ dt=1.0 / 120.0,
205
+ physics_steps_per_action=2,
206
+ env_spacing=4.0,
207
+ headless=True,
208
+ seed=42,
209
+ )
210
+
211
+
212
+ # system= picks a GPU tier from the published catalog; run `simulo systems` to see them.
213
+ @app.job(
214
+ system=simulo.SystemType.TIER_1,
215
+ timeout=8 * 60 * 60,
216
+ retries=2,
217
+ callbacks=[simulo.callbacks.ResumableCheckpoint(every=50)],
218
+ )
219
+ def train(num_envs: int = 512, max_iterations: int = 20) -> dict[str, Any]:
220
+ """Train a policy, then save the checkpoint AND export a TorchScript policy.
221
+
222
+ Saves two artefacts to the checkpoint volume: the skrl checkpoint (consumed by
223
+ ``evaluate``) and a deterministic TorchScript policy (consumed by ``rollout``
224
+ via ``simulo.RLPlayer``).
225
+ """
226
+ env = _make_env(num_envs)
227
+ trainer = simulo.RLTrainer(env=env, algorithm="PPO", device="cuda", seed=42)
228
+
229
+ stats = trainer.train(max_iterations=max_iterations)
230
+
231
+ checkpoint = f"{checkpoints.path}/{_CHECKPOINT_FILE}"
232
+ policy_path = f"{checkpoints.path}/{_POLICY_FILE}"
233
+ trainer.save(checkpoint)
234
+ trainer.export_policy(policy_path)
235
+
236
+ # Close the trainer before the environment so skrl releases its resources first.
237
+ trainer.close()
238
+ env.close()
239
+
240
+ return {"checkpoint": checkpoint, "policy": policy_path, "num_envs": num_envs, **stats}
241
+
242
+
243
+ @app.job(system=simulo.SystemType.TIER_1, timeout=2 * 60 * 60)
244
+ def evaluate(num_episodes: int = 10, num_rounds: int = 5) -> dict[str, Any]:
245
+ """Evaluate the saved checkpoint over several rounds; aggregate with numpy."""
246
+ import json
247
+
248
+ import numpy as np
249
+
250
+ checkpoint = f"{checkpoints.path}/{_CHECKPOINT_FILE}"
251
+ reward_target = float(os.environ.get("SIMULO_EVAL_REWARD_TARGET", "75.0"))
252
+
253
+ env = _make_env(num_envs=64)
254
+ trainer = simulo.RLTrainer(env=env, algorithm="PPO", device="cuda", seed=42)
255
+
256
+ round_rewards: list[float] = []
257
+ round_lengths: list[float] = []
258
+ for _ in range(num_rounds):
259
+ metrics = trainer.evaluate(checkpoint=checkpoint, num_episodes=num_episodes)
260
+ round_rewards.append(float(metrics["mean_reward"]))
261
+ round_lengths.append(float(metrics["mean_length"]))
262
+
263
+ trainer.close()
264
+ env.close()
265
+
266
+ rewards = np.array(round_rewards, dtype=np.float64)
267
+ lengths = np.array(round_lengths, dtype=np.float64)
268
+ report = {
269
+ "checkpoint": checkpoint,
270
+ "num_rounds": num_rounds,
271
+ "num_episodes": num_episodes,
272
+ "reward_target": reward_target,
273
+ "mean_reward": float(rewards.mean()),
274
+ "std_reward": float(rewards.std()),
275
+ "mean_length": float(lengths.mean()),
276
+ "success_rate": float((rewards >= reward_target).mean()),
277
+ }
278
+
279
+ report_path = f"{reports.path}/eval_report.json"
280
+ with open(report_path, "w") as fh:
281
+ json.dump(report, fh, indent=2)
282
+ print(f"[{{APP_NAME}}] Wrote eval report to {report_path}")
283
+
284
+ return {"report": report_path, **report}
285
+
286
+
287
+ @app.job(system=simulo.SystemType.TIER_1, timeout=1 * 60 * 60)
288
+ def rollout(num_steps: int = 200) -> dict[str, Any]:
289
+ """Inference path: play the exported TorchScript policy and record it to MCAP.
290
+
291
+ No trainer and no skrl here — ``simulo.RLPlayer`` auto-detects the TorchScript
292
+ policy ``train`` exported and drives it directly. ``record=simulo.RecordConfig(...)``
293
+ captures the rollout to an MCAP flight recording in the report volume.
294
+ """
295
+ import json
296
+
297
+ policy_path = f"{checkpoints.path}/{_POLICY_FILE}"
298
+ mcap_path = f"{reports.path}/rollout.mcap"
299
+
300
+ env = _make_env(num_envs=1)
301
+ # A TorchScript checkpoint makes the player run the policy directly — trainer-free.
302
+ player = simulo.RLPlayer(env=env, checkpoint=policy_path, device="cuda")
303
+ record = simulo.RecordConfig(output_path=mcap_path, policy_checkpoint=policy_path, robot_model="{{APP_NAME}}")
304
+ stats = player.play(num_steps=num_steps, record=record)
305
+
306
+ player.close()
307
+ env.close()
308
+
309
+ summary = {"policy": policy_path, "mcap": mcap_path, **stats}
310
+
311
+ summary_path = f"{reports.path}/rollout_summary.json"
312
+ with open(summary_path, "w") as fh:
313
+ json.dump(summary, fh, indent=2)
314
+ print(f"[{{APP_NAME}}] Wrote rollout summary to {summary_path}")
315
+
316
+ return {"summary": summary_path, **summary}
@@ -0,0 +1,30 @@
1
+ # Gitignore-style excludes for `simulo run` / `simulo create --type inference`'s
2
+ # {{APP_NAME}}. Applied on top of the packager's own built-in excludes (assets/,
3
+ # datasets/, checkpoints/, outputs/, logs/, .git/, __pycache__/, .simulo/, and
4
+ # *.usd(a|c)/*.pt/*.pth/*.onnx/*.mcap/*.mp4/*.pyc) — the extra patterns below cover
5
+ # things that default set does not.
6
+
7
+ # Trained checkpoints and exported policies (already written to a Volume via
8
+ # checkpoints.path / reports.path — never need to be part of the packaged source
9
+ # bundle).
10
+ *.pt
11
+ *.pth
12
+ *.ckpt
13
+
14
+ # Python virtual environments.
15
+ .venv/
16
+ venv/
17
+ env/
18
+
19
+ # Local secrets / environment overrides — never bundle these.
20
+ .env
21
+ .env.*
22
+
23
+ # Editor / OS cruft.
24
+ .DS_Store
25
+ *.swp
26
+
27
+ # Data blobs.
28
+ *.npy
29
+ *.npz
30
+ data/
@@ -0,0 +1,93 @@
1
+ """{{APP_NAME}} — a Simulo scenario app (scaffolded by `simulo create`).
2
+
3
+ A headless scene with NO learning: build a world, step it, and shut it down.
4
+ Submit it with::
5
+
6
+ simulo run app.py --num-steps 1000 --num-envs 4
7
+
8
+ The CLI maps each ``--flag`` onto ``simulate``'s own parameters — no extra
9
+ boilerplate; run with no flags to use the defaults. Submitting only
10
+ writes/uploads the job — it does NOT run the scene on this machine. Once logged
11
+ in (``simulo login``), the SAME command uploads and runs this on the Simulo
12
+ cloud instead, following its logs automatically.
13
+
14
+ WHERE TO EDIT
15
+ -------------
16
+ * ``{{APP_NAME}}Scenario.build`` — populate the scene: terrain, lights, robots, assets.
17
+ * ``on_start`` / ``on_step`` / ``on_shutdown`` — your own simulation logic.
18
+ * ``simulate`` (the ``@app.job``) — simulation length and what gets returned;
19
+ its parameters ARE the app's CLI flags (``num_steps`` -> ``--num-steps``).
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from typing import Any
25
+
26
+ import simulo
27
+
28
+ # The cartpole robot — a global-catalog asset. Swap the ref for your own robot
29
+ # (``simulo asset publish`` your own USD/URDF, or browse the catalog with
30
+ # ``simulo asset list --global``).
31
+ cartpole = simulo.Asset.from_registry("simulo/robot/cartpole:v1")
32
+
33
+
34
+ class {{APP_NAME}}Scenario(simulo.Scenario): # -> ScenarioProtocol at submit, real class on worker
35
+ """A minimal scene: a ground plane, a light, and a robot. EDIT: build your own.
36
+
37
+ Critical rule: everything a Scenario does must be expressible with
38
+ ``simulo.Terrain`` / ``simulo.Light`` / ``simulo.Robot`` / ``simulo.Asset`` —
39
+ no ``torch`` here (this class is defined, but never instantiated, at submit).
40
+ """
41
+
42
+ def build(self, scene: simulo.Scene) -> None:
43
+ # EDIT: swap the ground / light / robot below for your own scene.
44
+ scene.add(simulo.Terrain.plane(name="ground"), at="/", per_environment=False)
45
+ scene.add(
46
+ simulo.Light.dome(name="light", intensity=2000.0, color=(0.75, 0.75, 0.75)),
47
+ at="/",
48
+ per_environment=False,
49
+ )
50
+ self.robot = simulo.Robot(asset=cartpole, initial_pose=simulo.Pose.identity())
51
+ scene.add(self.robot, at="/World/Robot")
52
+
53
+ def on_start(self) -> None:
54
+ """Called once after build() and runtime initialization. EDIT: setup here."""
55
+
56
+ def on_step(self) -> None:
57
+ """Called every simulation step. EDIT: your per-step logic here."""
58
+
59
+ def on_shutdown(self) -> None:
60
+ """Called during cleanup. EDIT: final logging / saving here."""
61
+
62
+
63
+ # Advanced: pick a different Simulo runtime — App("name", runtime=simulo.Runtime.from_registry("simulo/gpu-rl:2026.06")). See the Runtimes docs.
64
+ app = simulo.App("{{APP_NAME}}")
65
+
66
+
67
+ # system= picks a GPU tier from the published catalog; run `simulo systems` to see them.
68
+ @app.job(system=simulo.SystemType.TIER_1, timeout=30 * 60)
69
+ def simulate(num_steps: int = 1000, num_envs: int = 4) -> dict[str, Any]:
70
+ """Run the scenario headless on the worker. EDIT: device / step count / env count.
71
+
72
+ Args:
73
+ num_steps: Number of simulation steps to run.
74
+ num_envs: Number of parallel environments (robots added in ``build`` are
75
+ automatically replicated across all of them).
76
+
77
+ Returns:
78
+ A JSON-serialisable dict summarising the run.
79
+ """
80
+ # simulo.run is execution-only (it resolves to an inert stand-in at
81
+ # submit and to the real simulo.scenario.run on the worker); job bodies
82
+ # never run at submit, so it needs no extra import here. Keep this call
83
+ # INSIDE the job body: the identical line at MODULE scope would silently
84
+ # do nothing at submit (an inert stand-in, no error) yet launch a real
85
+ # simulation at import time on the worker.
86
+ simulo.run(
87
+ {{APP_NAME}}Scenario,
88
+ device="cuda",
89
+ headless=True,
90
+ max_steps=num_steps,
91
+ num_envs=num_envs,
92
+ )
93
+ return {"steps": num_steps, "num_envs": num_envs}
@@ -0,0 +1,27 @@
1
+ # Gitignore-style excludes for `simulo run` / `simulo create --type scenario`'s
2
+ # {{APP_NAME}}. Applied on top of the packager's own built-in excludes (assets/,
3
+ # datasets/, checkpoints/, outputs/, logs/, .git/, __pycache__/, .simulo/, and
4
+ # *.usd(a|c)/*.pt/*.pth/*.onnx/*.mcap/*.mp4/*.pyc) — the extra patterns below cover
5
+ # things that default set does not.
6
+
7
+ # Any saved state this scenario writes locally.
8
+ *.pt
9
+ *.ckpt
10
+
11
+ # Python virtual environments.
12
+ .venv/
13
+ venv/
14
+ env/
15
+
16
+ # Local secrets / environment overrides — never bundle these.
17
+ .env
18
+ .env.*
19
+
20
+ # Editor / OS cruft.
21
+ .DS_Store
22
+ *.swp
23
+
24
+ # Data blobs.
25
+ *.npy
26
+ *.npz
27
+ data/