simrig 0.2.2__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.
- simrig/__init__.py +82 -0
- simrig/_version.py +3 -0
- simrig/browser_render.py +145 -0
- simrig/browser_shell.py +130 -0
- simrig/cli.py +412 -0
- simrig/core.py +144 -0
- simrig/custom_env.py +109 -0
- simrig/huggingface.py +78 -0
- simrig/io.py +49 -0
- simrig/live_view.py +553 -0
- simrig/model_view.py +944 -0
- simrig/mujoco_backend.py +197 -0
- simrig/paths.py +54 -0
- simrig/playground_backend.py +603 -0
- simrig/presets.py +93 -0
- simrig/preview.py +956 -0
- simrig/rendering.py +127 -0
- simrig/scaffold.py +127 -0
- simrig/three_scene.py +107 -0
- simrig/validate_env.py +211 -0
- simrig-0.2.2.dist-info/METADATA +238 -0
- simrig-0.2.2.dist-info/RECORD +26 -0
- simrig-0.2.2.dist-info/WHEEL +5 -0
- simrig-0.2.2.dist-info/entry_points.txt +2 -0
- simrig-0.2.2.dist-info/licenses/LICENSE +21 -0
- simrig-0.2.2.dist-info/top_level.txt +1 -0
simrig/mujoco_backend.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""MuJoCo model discovery and inspection backend."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from simrig.core import BackendInfo, ModelInspectionReport, TrainabilityStatus
|
|
9
|
+
from simrig.paths import find_menagerie
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _import_mujoco():
|
|
13
|
+
try:
|
|
14
|
+
import mujoco # type: ignore
|
|
15
|
+
except ImportError as exc:
|
|
16
|
+
raise RuntimeError(
|
|
17
|
+
"MuJoCo is not installed. Install SimRig with the mujoco extra or "
|
|
18
|
+
"install the `mujoco` package."
|
|
19
|
+
) from exc
|
|
20
|
+
return mujoco
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def backend_info() -> BackendInfo:
|
|
24
|
+
try:
|
|
25
|
+
mujoco = _import_mujoco()
|
|
26
|
+
except RuntimeError as exc:
|
|
27
|
+
return BackendInfo(name="mujoco", available=False, detail=str(exc))
|
|
28
|
+
version = getattr(mujoco, "__version__", None)
|
|
29
|
+
return BackendInfo(name="mujoco", available=True, version=version)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def list_models(menagerie: Path | str | None = None) -> list[dict[str, Any]]:
|
|
33
|
+
"""List model directories in a MuJoCo Menagerie checkout."""
|
|
34
|
+
root = find_menagerie(menagerie)
|
|
35
|
+
entries: list[dict[str, Any]] = []
|
|
36
|
+
skip = {".git", ".github", "assets", "test", "opensource"}
|
|
37
|
+
for directory in sorted(path for path in root.iterdir() if path.is_dir()):
|
|
38
|
+
if directory.name.startswith(".") or directory.name in skip:
|
|
39
|
+
continue
|
|
40
|
+
xmls = sorted(directory.glob("*.xml"))
|
|
41
|
+
if not xmls:
|
|
42
|
+
continue
|
|
43
|
+
scenes = sorted(directory.glob("scene*.xml"))
|
|
44
|
+
mjx_xmls = sorted(directory.glob("*mjx*.xml"))
|
|
45
|
+
entries.append(
|
|
46
|
+
{
|
|
47
|
+
"name": directory.name,
|
|
48
|
+
"path": str(directory),
|
|
49
|
+
"scene_xmls": [str(path.relative_to(root)) for path in scenes],
|
|
50
|
+
"xmls": [str(path.relative_to(root)) for path in xmls],
|
|
51
|
+
"mjx_xmls": [str(path.relative_to(root)) for path in mjx_xmls],
|
|
52
|
+
"has_readme": (directory / "README.md").is_file(),
|
|
53
|
+
"has_license": (directory / "LICENSE").is_file(),
|
|
54
|
+
}
|
|
55
|
+
)
|
|
56
|
+
return entries
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def resolve_model_path(model_or_xml: str | Path, menagerie: Path | str | None = None) -> Path:
|
|
60
|
+
"""Resolve a model directory name, relative path, or XML path."""
|
|
61
|
+
raw = Path(model_or_xml).expanduser()
|
|
62
|
+
if raw.is_file():
|
|
63
|
+
return raw.resolve()
|
|
64
|
+
if raw.is_dir():
|
|
65
|
+
scene = _preferred_scene(raw)
|
|
66
|
+
if scene is None:
|
|
67
|
+
raise FileNotFoundError(f"No scene*.xml or *.xml found in {raw}")
|
|
68
|
+
return scene.resolve()
|
|
69
|
+
|
|
70
|
+
root = find_menagerie(menagerie)
|
|
71
|
+
candidate = root / str(model_or_xml)
|
|
72
|
+
if candidate.is_file():
|
|
73
|
+
return candidate.resolve()
|
|
74
|
+
if candidate.is_dir():
|
|
75
|
+
scene = _preferred_scene(candidate)
|
|
76
|
+
if scene is not None:
|
|
77
|
+
return scene.resolve()
|
|
78
|
+
|
|
79
|
+
# Accept relative XML paths inside Menagerie, e.g. unitree_g1/scene.xml.
|
|
80
|
+
relative_candidate = root / raw
|
|
81
|
+
if relative_candidate.is_file():
|
|
82
|
+
return relative_candidate.resolve()
|
|
83
|
+
|
|
84
|
+
raise FileNotFoundError(f"Could not resolve model or XML: {model_or_xml}")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def inspect_model(
|
|
88
|
+
model_or_xml: str | Path,
|
|
89
|
+
*,
|
|
90
|
+
menagerie: Path | str | None = None,
|
|
91
|
+
steps: int = 25,
|
|
92
|
+
noise_scale: float = 0.25,
|
|
93
|
+
) -> ModelInspectionReport:
|
|
94
|
+
"""Compile and briefly step a MuJoCo model."""
|
|
95
|
+
path = resolve_model_path(model_or_xml, menagerie)
|
|
96
|
+
mujoco = _import_mujoco()
|
|
97
|
+
warnings: list[str] = []
|
|
98
|
+
errors: list[str] = []
|
|
99
|
+
notes: list[str] = []
|
|
100
|
+
compiled = False
|
|
101
|
+
stepped = False
|
|
102
|
+
model = None
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
model = mujoco.MjModel.from_xml_path(str(path))
|
|
106
|
+
compiled = True
|
|
107
|
+
except Exception as exc: # MuJoCo raises several native exception types.
|
|
108
|
+
errors.append(str(exc))
|
|
109
|
+
return ModelInspectionReport(
|
|
110
|
+
name=path.stem,
|
|
111
|
+
path=str(path),
|
|
112
|
+
backend="mujoco",
|
|
113
|
+
status=TrainabilityStatus.FAILED,
|
|
114
|
+
compiled=False,
|
|
115
|
+
stepped=False,
|
|
116
|
+
errors=errors,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
data = mujoco.MjData(model)
|
|
120
|
+
try:
|
|
121
|
+
for index in range(max(0, steps)):
|
|
122
|
+
_bounded_ctrl_noise(mujoco, model, data, index, noise_scale)
|
|
123
|
+
mujoco.mj_step(model, data)
|
|
124
|
+
stepped = True
|
|
125
|
+
except Exception as exc:
|
|
126
|
+
errors.append(str(exc))
|
|
127
|
+
|
|
128
|
+
for warning_index, count in enumerate(data.warning.number):
|
|
129
|
+
if count:
|
|
130
|
+
try:
|
|
131
|
+
name = mujoco.mjtWarning(warning_index).name
|
|
132
|
+
except Exception:
|
|
133
|
+
name = f"warning_{warning_index}"
|
|
134
|
+
warnings.append(f"{name}: count={int(count)}")
|
|
135
|
+
|
|
136
|
+
has_mjx_hint = "mjx" in path.name.lower() or any(
|
|
137
|
+
"mjx" in sibling.name.lower() for sibling in path.parent.glob("*.xml")
|
|
138
|
+
)
|
|
139
|
+
has_freejoint = _has_freejoint(mujoco, model)
|
|
140
|
+
if model.nu <= 0:
|
|
141
|
+
notes.append("Model has no actuators; it can be inspected but is not directly controllable.")
|
|
142
|
+
if not has_mjx_hint:
|
|
143
|
+
notes.append("No MJX-named XML variant found near this model.")
|
|
144
|
+
notes.append(
|
|
145
|
+
"Raw MuJoCo model inspection does not prove trainability; a task env still defines observations, rewards, resets, and termination."
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
status = TrainabilityStatus.SIMULATABLE if compiled and stepped and not errors else TrainabilityStatus.INSPECTABLE
|
|
149
|
+
return ModelInspectionReport(
|
|
150
|
+
name=path.parent.name if path.parent.name else path.stem,
|
|
151
|
+
path=str(path),
|
|
152
|
+
backend="mujoco",
|
|
153
|
+
status=status,
|
|
154
|
+
compiled=compiled,
|
|
155
|
+
stepped=stepped,
|
|
156
|
+
bodies=int(model.nbody),
|
|
157
|
+
joints=int(model.njnt),
|
|
158
|
+
dofs=int(model.nv),
|
|
159
|
+
actuators=int(model.nu),
|
|
160
|
+
sensors=int(model.nsensor),
|
|
161
|
+
keyframes=int(model.nkey),
|
|
162
|
+
has_freejoint=has_freejoint,
|
|
163
|
+
has_mjx_hint=has_mjx_hint,
|
|
164
|
+
warnings=warnings,
|
|
165
|
+
errors=errors,
|
|
166
|
+
notes=notes,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _preferred_scene(directory: Path) -> Path | None:
|
|
171
|
+
for pattern in ("scene_mjx.xml", "scene.xml", "scene*.xml", "*.xml"):
|
|
172
|
+
matches = sorted(directory.glob(pattern))
|
|
173
|
+
if matches:
|
|
174
|
+
return matches[0]
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _bounded_ctrl_noise(mujoco: Any, model: Any, data: Any, index: int, noise: float) -> None:
|
|
179
|
+
for actuator_index in range(model.nu):
|
|
180
|
+
ctrlrange = model.actuator_ctrlrange[actuator_index]
|
|
181
|
+
if model.actuator_ctrllimited[actuator_index]:
|
|
182
|
+
center = 0.5 * (ctrlrange[1] + ctrlrange[0])
|
|
183
|
+
radius = 0.5 * (ctrlrange[1] - ctrlrange[0])
|
|
184
|
+
else:
|
|
185
|
+
center = 0.0
|
|
186
|
+
radius = 1.0
|
|
187
|
+
data.ctrl[actuator_index] = center + radius * noise * (
|
|
188
|
+
2 * mujoco.mju_Halton(index + 1, actuator_index + 2) - 1
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _has_freejoint(mujoco: Any, model: Any) -> bool:
|
|
193
|
+
for joint_index in range(model.njnt):
|
|
194
|
+
if model.jnt_type[joint_index] == mujoco.mjtJoint.mjJNT_FREE:
|
|
195
|
+
return True
|
|
196
|
+
return False
|
|
197
|
+
|
simrig/paths.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Path helpers for SimRig projects and external model checkouts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
PROJECT_DIRS = ("runs", "reports", "artifacts", "envs", "configs")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def ensure_project_dirs(root: Path | str = ".") -> list[Path]:
|
|
13
|
+
base = Path(root)
|
|
14
|
+
paths = [base / name for name in PROJECT_DIRS]
|
|
15
|
+
for path in paths:
|
|
16
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
17
|
+
return paths
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def menagerie_candidates(extra: Path | str | None = None) -> list[Path]:
|
|
21
|
+
candidates: list[Path] = []
|
|
22
|
+
if extra is not None:
|
|
23
|
+
candidates.append(Path(extra).expanduser())
|
|
24
|
+
env_path = os.environ.get("MUJOCO_MENAGERIE_PATH")
|
|
25
|
+
if env_path:
|
|
26
|
+
candidates.append(Path(env_path).expanduser())
|
|
27
|
+
cwd = Path.cwd()
|
|
28
|
+
candidates.extend(
|
|
29
|
+
[
|
|
30
|
+
cwd / "mujoco_menagerie",
|
|
31
|
+
cwd.parent / "mujoco_menagerie",
|
|
32
|
+
Path.home() / "Desktop" / "mujoco_menagerie",
|
|
33
|
+
]
|
|
34
|
+
)
|
|
35
|
+
deduped: list[Path] = []
|
|
36
|
+
seen: set[Path] = set()
|
|
37
|
+
for candidate in candidates:
|
|
38
|
+
resolved = candidate.resolve()
|
|
39
|
+
if resolved not in seen:
|
|
40
|
+
deduped.append(resolved)
|
|
41
|
+
seen.add(resolved)
|
|
42
|
+
return deduped
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def find_menagerie(extra: Path | str | None = None) -> Path:
|
|
46
|
+
for candidate in menagerie_candidates(extra):
|
|
47
|
+
if (candidate / "README.md").is_file() and any(candidate.glob("*/scene*.xml")):
|
|
48
|
+
return candidate
|
|
49
|
+
searched = "\n".join(str(path) for path in menagerie_candidates(extra))
|
|
50
|
+
raise FileNotFoundError(
|
|
51
|
+
"Could not find a MuJoCo Menagerie checkout. Set MUJOCO_MENAGERIE_PATH "
|
|
52
|
+
f"or pass --menagerie. Searched:\n{searched}"
|
|
53
|
+
)
|
|
54
|
+
|