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/__init__.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""SimRig public API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from simrig._version import __version__
|
|
8
|
+
from simrig.core import (
|
|
9
|
+
BackendInfo,
|
|
10
|
+
EnvInspectionReport,
|
|
11
|
+
ModelInspectionReport,
|
|
12
|
+
RunConfig,
|
|
13
|
+
SmokeResult,
|
|
14
|
+
TrainabilityStatus,
|
|
15
|
+
)
|
|
16
|
+
from simrig.mujoco_backend import inspect_model, list_models
|
|
17
|
+
from simrig.playground_backend import (
|
|
18
|
+
demo_policy,
|
|
19
|
+
eval_policy,
|
|
20
|
+
inspect_env,
|
|
21
|
+
list_envs,
|
|
22
|
+
smoke_env,
|
|
23
|
+
train_ppo,
|
|
24
|
+
)
|
|
25
|
+
from simrig.scaffold import new_env
|
|
26
|
+
from simrig.validate_env import EnvValidationResult, validate_env
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"__version__",
|
|
30
|
+
"BackendInfo",
|
|
31
|
+
"EnvInspectionReport",
|
|
32
|
+
"EnvValidationResult",
|
|
33
|
+
"ModelInspectionReport",
|
|
34
|
+
"RunConfig",
|
|
35
|
+
"SmokeResult",
|
|
36
|
+
"TrainabilityStatus",
|
|
37
|
+
"demo_policy",
|
|
38
|
+
"eval_policy",
|
|
39
|
+
"inspect_env",
|
|
40
|
+
"inspect_model",
|
|
41
|
+
"is_env_module_path",
|
|
42
|
+
"list_envs",
|
|
43
|
+
"list_models",
|
|
44
|
+
"LiveWebViewer",
|
|
45
|
+
"load_custom_env",
|
|
46
|
+
"load_env",
|
|
47
|
+
"new_env",
|
|
48
|
+
"smoke_env",
|
|
49
|
+
"serve_model_view",
|
|
50
|
+
"serve_policy_preview",
|
|
51
|
+
"train_ppo",
|
|
52
|
+
"validate_env",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def __getattr__(name: str) -> Any:
|
|
57
|
+
"""Lazy-load browser helpers so core imports work without numpy/mujoco."""
|
|
58
|
+
if name == "LiveWebViewer":
|
|
59
|
+
from simrig.live_view import LiveWebViewer
|
|
60
|
+
|
|
61
|
+
return LiveWebViewer
|
|
62
|
+
if name == "serve_model_view":
|
|
63
|
+
from simrig.model_view import serve_model_view
|
|
64
|
+
|
|
65
|
+
return serve_model_view
|
|
66
|
+
if name == "serve_policy_preview":
|
|
67
|
+
from simrig.preview import serve_policy_preview
|
|
68
|
+
|
|
69
|
+
return serve_policy_preview
|
|
70
|
+
if name == "is_env_module_path":
|
|
71
|
+
from simrig.custom_env import is_env_module_path
|
|
72
|
+
|
|
73
|
+
return is_env_module_path
|
|
74
|
+
if name == "load_custom_env":
|
|
75
|
+
from simrig.custom_env import load_custom_env
|
|
76
|
+
|
|
77
|
+
return load_custom_env
|
|
78
|
+
if name == "load_env":
|
|
79
|
+
from simrig.playground_backend import load_env
|
|
80
|
+
|
|
81
|
+
return load_env
|
|
82
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
simrig/_version.py
ADDED
simrig/browser_render.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Background MuJoCo frame rendering for browser viewers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import threading
|
|
6
|
+
import time
|
|
7
|
+
from typing import Any, Callable
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from simrig.rendering import CameraState, create_mujoco_renderer
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def encode_jpeg(image_module: Any, frame: np.ndarray, *, quality: int = 72) -> bytes:
|
|
15
|
+
from io import BytesIO
|
|
16
|
+
|
|
17
|
+
image = image_module.fromarray(np.asarray(frame))
|
|
18
|
+
output = BytesIO()
|
|
19
|
+
image.save(output, format="JPEG", quality=quality, optimize=True)
|
|
20
|
+
return output.getvalue()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class MujocoFramePump:
|
|
24
|
+
"""Render MuJoCo frames on a dedicated thread and serve cached JPEGs."""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
mujoco: Any,
|
|
29
|
+
model: Any,
|
|
30
|
+
mj_data: Any,
|
|
31
|
+
*,
|
|
32
|
+
width: int,
|
|
33
|
+
height: int,
|
|
34
|
+
camera: Any,
|
|
35
|
+
camera_state: CameraState,
|
|
36
|
+
image_module: Any,
|
|
37
|
+
fps: int = 24,
|
|
38
|
+
jpeg_quality: int = 72,
|
|
39
|
+
render_mode: str = "mujoco",
|
|
40
|
+
scene_lock: threading.Lock | None = None,
|
|
41
|
+
before_render: Callable[[], None] | None = None,
|
|
42
|
+
fallback_frame: Callable[[], np.ndarray] | None = None,
|
|
43
|
+
error_frame: Callable[[str], np.ndarray] | None = None,
|
|
44
|
+
) -> None:
|
|
45
|
+
self.mujoco = mujoco
|
|
46
|
+
self.model = model
|
|
47
|
+
self.mj_data = mj_data
|
|
48
|
+
self.width = width
|
|
49
|
+
self.height = height
|
|
50
|
+
self.camera = camera
|
|
51
|
+
self.camera_state = camera_state
|
|
52
|
+
self.image_module = image_module
|
|
53
|
+
self.fps = max(1, int(fps))
|
|
54
|
+
self.jpeg_quality = jpeg_quality
|
|
55
|
+
self.render_mode = render_mode.lower()
|
|
56
|
+
self.scene_lock = scene_lock or threading.Lock()
|
|
57
|
+
self.before_render = before_render
|
|
58
|
+
self.fallback_frame = fallback_frame
|
|
59
|
+
self.error_frame = error_frame
|
|
60
|
+
|
|
61
|
+
self.renderer_error: str | None = None
|
|
62
|
+
self._latest_jpeg: bytes = b""
|
|
63
|
+
self._camera_lock = threading.Lock()
|
|
64
|
+
self._running = True
|
|
65
|
+
self._thread = threading.Thread(target=self._run, name="simrig-frame-pump", daemon=True)
|
|
66
|
+
self._thread.start()
|
|
67
|
+
|
|
68
|
+
def set_camera_from_query(self, query: dict[str, list[str]]) -> None:
|
|
69
|
+
with self._camera_lock:
|
|
70
|
+
self.camera_state.update_from_query(query)
|
|
71
|
+
self.camera_state.apply(self.camera)
|
|
72
|
+
|
|
73
|
+
def get_jpeg(self) -> bytes:
|
|
74
|
+
return self._latest_jpeg
|
|
75
|
+
|
|
76
|
+
def stats(self) -> dict[str, Any]:
|
|
77
|
+
return {
|
|
78
|
+
"fps_target": self.fps,
|
|
79
|
+
"renderer_error": self.renderer_error,
|
|
80
|
+
"camera": self.camera_state.to_dict(),
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
def close(self) -> None:
|
|
84
|
+
self._running = False
|
|
85
|
+
self._thread.join(timeout=2.0)
|
|
86
|
+
|
|
87
|
+
def _run(self) -> None:
|
|
88
|
+
renderer = None
|
|
89
|
+
interval = 1.0 / self.fps
|
|
90
|
+
placeholder = np.zeros((self.height, self.width, 3), dtype=np.uint8)
|
|
91
|
+
self._latest_jpeg = encode_jpeg(self.image_module, placeholder, quality=self.jpeg_quality)
|
|
92
|
+
|
|
93
|
+
if self.render_mode == "mujoco":
|
|
94
|
+
try:
|
|
95
|
+
renderer = create_mujoco_renderer(
|
|
96
|
+
self.mujoco,
|
|
97
|
+
self.model,
|
|
98
|
+
height=self.height,
|
|
99
|
+
width=self.width,
|
|
100
|
+
)
|
|
101
|
+
except Exception as exc:
|
|
102
|
+
self.renderer_error = str(exc)
|
|
103
|
+
|
|
104
|
+
while self._running:
|
|
105
|
+
started = time.monotonic()
|
|
106
|
+
try:
|
|
107
|
+
with self.scene_lock:
|
|
108
|
+
if self.before_render is not None:
|
|
109
|
+
self.before_render()
|
|
110
|
+
if self.render_mode == "topdown":
|
|
111
|
+
if self.fallback_frame is None:
|
|
112
|
+
raise RuntimeError("topdown mode requires fallback_frame.")
|
|
113
|
+
frame = self.fallback_frame()
|
|
114
|
+
elif renderer is None:
|
|
115
|
+
message = self.renderer_error or "MuJoCo renderer unavailable."
|
|
116
|
+
if self.error_frame is None:
|
|
117
|
+
raise RuntimeError(message)
|
|
118
|
+
frame = self.error_frame(message)
|
|
119
|
+
else:
|
|
120
|
+
with self._camera_lock:
|
|
121
|
+
camera = self.camera_state.apply(self.camera)
|
|
122
|
+
renderer.update_scene(self.mj_data, camera=camera)
|
|
123
|
+
frame = renderer.render()
|
|
124
|
+
self.renderer_error = None
|
|
125
|
+
self._latest_jpeg = encode_jpeg(
|
|
126
|
+
self.image_module,
|
|
127
|
+
frame,
|
|
128
|
+
quality=self.jpeg_quality,
|
|
129
|
+
)
|
|
130
|
+
except Exception as exc:
|
|
131
|
+
self.renderer_error = str(exc)
|
|
132
|
+
if self.error_frame is not None:
|
|
133
|
+
self._latest_jpeg = encode_jpeg(
|
|
134
|
+
self.image_module,
|
|
135
|
+
self.error_frame(str(exc)),
|
|
136
|
+
quality=self.jpeg_quality,
|
|
137
|
+
)
|
|
138
|
+
elapsed = time.monotonic() - started
|
|
139
|
+
time.sleep(max(0.0, interval - elapsed))
|
|
140
|
+
|
|
141
|
+
if renderer is not None:
|
|
142
|
+
try:
|
|
143
|
+
renderer.close()
|
|
144
|
+
except Exception:
|
|
145
|
+
pass
|
simrig/browser_shell.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Shared browser viewer HTML/CSS/JS for SimRig localhost tools."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def viewer_styles(*, sidebar_width: int = 360) -> str:
|
|
7
|
+
return f"""
|
|
8
|
+
:root {{ color-scheme: dark; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }}
|
|
9
|
+
body {{ margin: 0; background: #111; color: #eee; display: grid; grid-template-columns: 1fr {sidebar_width}px; min-height: 100vh; }}
|
|
10
|
+
main {{ display: grid; place-items: stretch; background: #050505; position: relative; }}
|
|
11
|
+
#viewport {{ position: relative; width: 100%; height: 100vh; }}
|
|
12
|
+
#frame {{ width: 100%; height: 100%; object-fit: contain; background: #000; display: block; cursor: grab; user-select: none; touch-action: none; }}
|
|
13
|
+
#frame.dragging {{ cursor: grabbing; }}
|
|
14
|
+
#hint {{ position: absolute; left: 16px; bottom: 16px; color: #aaa; font-size: 12px; background: rgba(0,0,0,0.55); padding: 8px 10px; border-radius: 6px; pointer-events: none; }}
|
|
15
|
+
aside {{ padding: 16px; border-left: 1px solid #333; background: #181818; overflow: auto; max-height: 100vh; }}
|
|
16
|
+
h1 {{ font-size: 18px; margin: 0 0 12px; }}
|
|
17
|
+
label {{ display: block; margin: 10px 0 4px; color: #bbb; font-size: 13px; }}
|
|
18
|
+
input {{ width: 100%; box-sizing: border-box; background: #0d0d0d; color: #eee; border: 1px solid #444; padding: 8px; }}
|
|
19
|
+
button {{ margin: 8px 6px 0 0; padding: 8px 10px; background: #2d6cdf; color: white; border: 0; cursor: pointer; }}
|
|
20
|
+
button.secondary {{ background: #333; }}
|
|
21
|
+
pre {{ white-space: pre-wrap; background: #0d0d0d; border: 1px solid #333; padding: 10px; font-size: 12px; }}
|
|
22
|
+
.meta {{ color: #aaa; font-size: 12px; margin-bottom: 12px; }}
|
|
23
|
+
.control {{ margin: 0 0 14px; padding-bottom: 10px; border-bottom: 1px solid #2a2a2a; }}
|
|
24
|
+
.control label {{ margin-top: 0; }}
|
|
25
|
+
.control input[type=range] {{ width: 100%; }}
|
|
26
|
+
.value {{ color: #8ab4ff; font-size: 12px; margin-top: 4px; }}
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def camera_interaction_script() -> str:
|
|
31
|
+
return """
|
|
32
|
+
let cameraAzimuth = 135;
|
|
33
|
+
let cameraElevation = -20;
|
|
34
|
+
let cameraDistance = 2.4;
|
|
35
|
+
let cameraInteractive = true;
|
|
36
|
+
let dragging = false;
|
|
37
|
+
let lastX = 0;
|
|
38
|
+
let lastY = 0;
|
|
39
|
+
let cameraTimer = null;
|
|
40
|
+
|
|
41
|
+
function clamp(value, min, max) {
|
|
42
|
+
return Math.max(min, Math.min(max, value));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function pushCamera() {
|
|
46
|
+
if (!cameraInteractive) return;
|
|
47
|
+
const params = new URLSearchParams({
|
|
48
|
+
azimuth: String(cameraAzimuth),
|
|
49
|
+
elevation: String(cameraElevation),
|
|
50
|
+
distance: String(cameraDistance),
|
|
51
|
+
});
|
|
52
|
+
try {
|
|
53
|
+
await fetch('/camera?' + params.toString(), {cache: 'no-store'});
|
|
54
|
+
} catch (err) {
|
|
55
|
+
console.warn('camera update failed', err);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function scheduleCameraPush() {
|
|
60
|
+
clearTimeout(cameraTimer);
|
|
61
|
+
cameraTimer = setTimeout(pushCamera, 30);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function bindCameraControls(frame) {
|
|
65
|
+
frame.addEventListener('mousedown', (event) => {
|
|
66
|
+
if (!cameraInteractive || event.button !== 0) return;
|
|
67
|
+
dragging = true;
|
|
68
|
+
frame.classList.add('dragging');
|
|
69
|
+
lastX = event.clientX;
|
|
70
|
+
lastY = event.clientY;
|
|
71
|
+
});
|
|
72
|
+
window.addEventListener('mouseup', () => {
|
|
73
|
+
if (!dragging) return;
|
|
74
|
+
dragging = false;
|
|
75
|
+
frame.classList.remove('dragging');
|
|
76
|
+
pushCamera();
|
|
77
|
+
});
|
|
78
|
+
window.addEventListener('mousemove', (event) => {
|
|
79
|
+
if (!dragging || !cameraInteractive) return;
|
|
80
|
+
const dx = event.clientX - lastX;
|
|
81
|
+
const dy = event.clientY - lastY;
|
|
82
|
+
lastX = event.clientX;
|
|
83
|
+
lastY = event.clientY;
|
|
84
|
+
cameraAzimuth = (cameraAzimuth + dx * 0.35) % 360;
|
|
85
|
+
cameraElevation = clamp(cameraElevation - dy * 0.25, -89, 89);
|
|
86
|
+
scheduleCameraPush();
|
|
87
|
+
});
|
|
88
|
+
frame.addEventListener('wheel', (event) => {
|
|
89
|
+
if (!cameraInteractive) return;
|
|
90
|
+
event.preventDefault();
|
|
91
|
+
const scale = event.deltaY > 0 ? 1.08 : 0.92;
|
|
92
|
+
cameraDistance = clamp(cameraDistance * scale, 0.4, 12.0);
|
|
93
|
+
scheduleCameraPush();
|
|
94
|
+
}, {passive: false});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function applyCameraFromStatus(status) {
|
|
98
|
+
if (!status || !status.camera) return;
|
|
99
|
+
cameraAzimuth = status.camera.azimuth ?? cameraAzimuth;
|
|
100
|
+
cameraElevation = status.camera.elevation ?? cameraElevation;
|
|
101
|
+
cameraDistance = status.camera.distance ?? cameraDistance;
|
|
102
|
+
cameraInteractive = status.camera.interactive !== false;
|
|
103
|
+
}
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def frame_poll_script(*, poll_ms: int = 33) -> str:
|
|
108
|
+
return f"""
|
|
109
|
+
let frameTimer = null;
|
|
110
|
+
let frameUrl = null;
|
|
111
|
+
|
|
112
|
+
async function refreshFrame() {{
|
|
113
|
+
clearTimeout(frameTimer);
|
|
114
|
+
try {{
|
|
115
|
+
const res = await fetch('/frame.jpg?t=' + Date.now(), {{cache: 'no-store'}});
|
|
116
|
+
if (!res.ok) throw new Error('frame request failed');
|
|
117
|
+
const blob = await res.blob();
|
|
118
|
+
const url = URL.createObjectURL(blob);
|
|
119
|
+
frame.onload = () => {{
|
|
120
|
+
if (frameUrl) URL.revokeObjectURL(frameUrl);
|
|
121
|
+
frameUrl = url;
|
|
122
|
+
}};
|
|
123
|
+
frame.src = url;
|
|
124
|
+
}} catch (err) {{
|
|
125
|
+
console.warn('frame refresh failed', err);
|
|
126
|
+
}} finally {{
|
|
127
|
+
frameTimer = setTimeout(refreshFrame, {poll_ms});
|
|
128
|
+
}}
|
|
129
|
+
}}
|
|
130
|
+
"""
|