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.
- simulo/__init__.py +433 -0
- simulo/_client/__init__.py +6 -0
- simulo/_client/_entrypoint.py +313 -0
- simulo/_client/_mounts.py +25 -0
- simulo/_client/_runner.py +186 -0
- simulo/_client/_secure_downloads.py +1181 -0
- simulo/_client/app.py +1308 -0
- simulo/_client/asset.py +331 -0
- simulo/_client/asset_api.py +517 -0
- simulo/_client/asset_package.py +1103 -0
- simulo/_client/asset_pins.py +187 -0
- simulo/_client/builtin_aliases.py +107 -0
- simulo/_client/bundle.py +254 -0
- simulo/_client/cancel_api.py +104 -0
- simulo/_client/cli.py +9063 -0
- simulo/_client/config.py +186 -0
- simulo/_client/credentials.py +210 -0
- simulo/_client/discovery.py +214 -0
- simulo/_client/export_api.py +212 -0
- simulo/_client/export_bundle.py +296 -0
- simulo/_client/facades.py +581 -0
- simulo/_client/http.py +414 -0
- simulo/_client/identity_api.py +117 -0
- simulo/_client/install_samples.py +267 -0
- simulo/_client/jobs_api.py +224 -0
- simulo/_client/learning.py +393 -0
- simulo/_client/login.py +319 -0
- simulo/_client/mode.py +29 -0
- simulo/_client/outputs.py +116 -0
- simulo/_client/packaging.py +445 -0
- simulo/_client/preflight_api.py +186 -0
- simulo/_client/preflight_render.py +200 -0
- simulo/_client/registry.py +98 -0
- simulo/_client/runtime.py +185 -0
- simulo/_client/runtime_display.py +90 -0
- simulo/_client/seed_ref.py +76 -0
- simulo/_client/stub.py +41 -0
- simulo/_client/submit_api.py +1057 -0
- simulo/_client/templates/__init__.py +21 -0
- simulo/_client/templates/inference/app.py.tmpl +316 -0
- simulo/_client/templates/inference/simuloignore.tmpl +30 -0
- simulo/_client/templates/scenario/app.py.tmpl +93 -0
- simulo/_client/templates/scenario/simuloignore.tmpl +27 -0
- simulo/_client/templates/training/app.py.tmpl +235 -0
- simulo/_client/templates/training/simuloignore.tmpl +29 -0
- simulo/_client/view_fragment.py +21 -0
- simulo/_client/view_session_api.py +122 -0
- simulo/_client/volume.py +71 -0
- simulo/callbacks.py +274 -0
- simulo/py.typed +0 -0
- simulo-0.26.0.dist-info/METADATA +130 -0
- simulo-0.26.0.dist-info/RECORD +55 -0
- simulo-0.26.0.dist-info/WHEEL +5 -0
- simulo-0.26.0.dist-info/entry_points.txt +2 -0
- 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/
|