stable-envs 0.1.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 (278) hide show
  1. stable_envs/__about__.py +9 -0
  2. stable_envs/__init__.py +91 -0
  3. stable_envs/__main__.py +3 -0
  4. stable_envs/_core/__init__.py +1 -0
  5. stable_envs/_core/cache.py +12 -0
  6. stable_envs/_core/config.py +84 -0
  7. stable_envs/_core/dynamics.py +17 -0
  8. stable_envs/_core/environ.py +166 -0
  9. stable_envs/_core/mujoco_gl.py +106 -0
  10. stable_envs/_core/physics.py +492 -0
  11. stable_envs/_core/plate_texels.py +89 -0
  12. stable_envs/_core/reset_tree.py +78 -0
  13. stable_envs/_core/space_nodes.py +10 -0
  14. stable_envs/_core/stepping.py +341 -0
  15. stable_envs/_core/style.py +169 -0
  16. stable_envs/_core/tensordicts.py +136 -0
  17. stable_envs/_core/warp_cuda.py +66 -0
  18. stable_envs/_execution/__init__.py +1 -0
  19. stable_envs/_execution/mjbatch.py +1000 -0
  20. stable_envs/_execution/mjwarp_fused.py +387 -0
  21. stable_envs/_execution/mjwarp_tree.py +522 -0
  22. stable_envs/_execution/policy.py +38 -0
  23. stable_envs/_execution/process.py +503 -0
  24. stable_envs/_execution/protocol.py +114 -0
  25. stable_envs/_execution/render_pool.py +355 -0
  26. stable_envs/_execution/requests.py +119 -0
  27. stable_envs/_execution/shared_memory.py +46 -0
  28. stable_envs/_execution/state_columns.py +52 -0
  29. stable_envs/_execution/workers.py +34 -0
  30. stable_envs/_tune.py +263 -0
  31. stable_envs/cli.py +34 -0
  32. stable_envs/doctor.py +651 -0
  33. stable_envs/env.py +1450 -0
  34. stable_envs/envs/__init__.py +5 -0
  35. stable_envs/envs/classic_control/__init__.py +9 -0
  36. stable_envs/envs/classic_control/envs.py +1101 -0
  37. stable_envs/envs/classic_control/semantics.py +215 -0
  38. stable_envs/envs/classic_control/simulation/__init__.py +0 -0
  39. stable_envs/envs/classic_control/simulation/batched.py +577 -0
  40. stable_envs/envs/classic_control/simulation/device.py +425 -0
  41. stable_envs/envs/classic_control/simulation/execution.py +31 -0
  42. stable_envs/envs/classic_control/simulation/local.py +424 -0
  43. stable_envs/envs/classic_control/simulation/process.py +50 -0
  44. stable_envs/envs/classic_control/simulation/triton_step.py +504 -0
  45. stable_envs/envs/craftax/__init__.py +5 -0
  46. stable_envs/envs/craftax/envs.py +198 -0
  47. stable_envs/envs/craftax/kernel.py +259 -0
  48. stable_envs/envs/dmcontrol/__init__.py +14 -0
  49. stable_envs/envs/dmcontrol/_aligned.py +20 -0
  50. stable_envs/envs/dmcontrol/_cells.py +95 -0
  51. stable_envs/envs/dmcontrol/_continuation.py +12 -0
  52. stable_envs/envs/dmcontrol/_delegated.py +199 -0
  53. stable_envs/envs/dmcontrol/_family.py +107 -0
  54. stable_envs/envs/dmcontrol/_first_family.py +7 -0
  55. stable_envs/envs/dmcontrol/_humanoid.py +7 -0
  56. stable_envs/envs/dmcontrol/_mass_rows.py +112 -0
  57. stable_envs/envs/dmcontrol/_model.py +235 -0
  58. stable_envs/envs/dmcontrol/_shading.py +154 -0
  59. stable_envs/envs/dmcontrol/_tendon.py +206 -0
  60. stable_envs/envs/dmcontrol/acrobot.py +15 -0
  61. stable_envs/envs/dmcontrol/ball_in_cup.py +14 -0
  62. stable_envs/envs/dmcontrol/base.py +454 -0
  63. stable_envs/envs/dmcontrol/cartpole.py +9 -0
  64. stable_envs/envs/dmcontrol/cheetah.py +29 -0
  65. stable_envs/envs/dmcontrol/finger.py +25 -0
  66. stable_envs/envs/dmcontrol/hopper.py +29 -0
  67. stable_envs/envs/dmcontrol/humanoid.py +42 -0
  68. stable_envs/envs/dmcontrol/manipulator.py +60 -0
  69. stable_envs/envs/dmcontrol/pendulum.py +6 -0
  70. stable_envs/envs/dmcontrol/point_mass.py +8 -0
  71. stable_envs/envs/dmcontrol/quadruped.py +97 -0
  72. stable_envs/envs/dmcontrol/reacher.py +62 -0
  73. stable_envs/envs/dmcontrol/simulation/__init__.py +0 -0
  74. stable_envs/envs/dmcontrol/simulation/aligned.py +460 -0
  75. stable_envs/envs/dmcontrol/simulation/batch.py +280 -0
  76. stable_envs/envs/dmcontrol/simulation/continuation.py +286 -0
  77. stable_envs/envs/dmcontrol/simulation/execution.py +100 -0
  78. stable_envs/envs/dmcontrol/simulation/factory.py +74 -0
  79. stable_envs/envs/dmcontrol/simulation/first_family.py +205 -0
  80. stable_envs/envs/dmcontrol/simulation/humanoid.py +105 -0
  81. stable_envs/envs/dmcontrol/simulation/lanes.py +170 -0
  82. stable_envs/envs/dmcontrol/simulation/mjwarp.py +623 -0
  83. stable_envs/envs/dmcontrol/simulation/mjwarp_reset.py +380 -0
  84. stable_envs/envs/dmcontrol/simulation/mjwarp_tasks.py +675 -0
  85. stable_envs/envs/dmcontrol/simulation/physics.py +292 -0
  86. stable_envs/envs/dmcontrol/simulation/reacher_mjx.py +229 -0
  87. stable_envs/envs/dmcontrol/simulation/registered.py +54 -0
  88. stable_envs/envs/dmcontrol/simulation/state.py +132 -0
  89. stable_envs/envs/dmcontrol/suite.py +945 -0
  90. stable_envs/envs/dmcontrol/swimmer.py +50 -0
  91. stable_envs/envs/dmcontrol/trackcom.py +570 -0
  92. stable_envs/envs/dmcontrol/trackcom_opengl.py +81 -0
  93. stable_envs/envs/dmcontrol/trackcom_reflection.py +724 -0
  94. stable_envs/envs/dmcontrol/trackcom_texture.py +46 -0
  95. stable_envs/envs/dmcontrol/walker.py +29 -0
  96. stable_envs/envs/dmcontrol/warp_render.py +1252 -0
  97. stable_envs/envs/fetch/__init__.py +8 -0
  98. stable_envs/envs/fetch/envs.py +986 -0
  99. stable_envs/envs/fetch/mujoco_raster.py +168 -0
  100. stable_envs/envs/fetch/opengl.py +259 -0
  101. stable_envs/envs/fetch/raster.py +1284 -0
  102. stable_envs/envs/fetch/semantics.py +367 -0
  103. stable_envs/envs/fetch/simulation/__init__.py +0 -0
  104. stable_envs/envs/fetch/simulation/adapter.py +403 -0
  105. stable_envs/envs/fetch/simulation/batch.py +541 -0
  106. stable_envs/envs/fetch/simulation/execution.py +61 -0
  107. stable_envs/envs/fetch/simulation/mjwarp.py +246 -0
  108. stable_envs/envs/fetch/simulation/observations.py +85 -0
  109. stable_envs/envs/fetch/simulation/process.py +88 -0
  110. stable_envs/envs/fetch/transparency.py +356 -0
  111. stable_envs/envs/miniworld/__init__.py +6 -0
  112. stable_envs/envs/miniworld/envs.py +204 -0
  113. stable_envs/envs/miniworld/simulation/__init__.py +0 -0
  114. stable_envs/envs/miniworld/simulation/adapter.py +532 -0
  115. stable_envs/envs/miniworld/simulation/batched.py +371 -0
  116. stable_envs/envs/miniworld/simulation/kernel_cpu.py +491 -0
  117. stable_envs/envs/miniworld/simulation/kernel_torch.py +499 -0
  118. stable_envs/envs/miniworld/simulation/kernel_triton.py +338 -0
  119. stable_envs/envs/miniworld/simulation/rng.py +78 -0
  120. stable_envs/envs/miniworld/simulation/world.py +116 -0
  121. stable_envs/envs/mshuman/__init__.py +5 -0
  122. stable_envs/envs/mshuman/assets.py +59 -0
  123. stable_envs/envs/mshuman/envs.py +127 -0
  124. stable_envs/envs/mshuman/simulation/__init__.py +0 -0
  125. stable_envs/envs/mshuman/simulation/adapter.py +115 -0
  126. stable_envs/envs/navigation/__init__.py +8 -0
  127. stable_envs/envs/navigation/envs.py +1825 -0
  128. stable_envs/envs/navigation/maze_goal.py +210 -0
  129. stable_envs/envs/navigation/navdot.py +213 -0
  130. stable_envs/envs/navigation/navdot_cpu.py +114 -0
  131. stable_envs/envs/navigation/navstep_cpu.py +200 -0
  132. stable_envs/envs/navigation/tworoom_jax.py +115 -0
  133. stable_envs/envs/ogbench/__init__.py +33 -0
  134. stable_envs/envs/ogbench/context_cache.py +199 -0
  135. stable_envs/envs/ogbench/litpass_cpu.py +422 -0
  136. stable_envs/envs/ogbench/manipulation.py +1125 -0
  137. stable_envs/envs/ogbench/manipulation_fidelity.py +118 -0
  138. stable_envs/envs/ogbench/manipulation_opengl.py +468 -0
  139. stable_envs/envs/ogbench/manipulation_opengl_pool.py +221 -0
  140. stable_envs/envs/ogbench/manipulation_raster.py +207 -0
  141. stable_envs/envs/ogbench/manipulation_renderer.py +328 -0
  142. stable_envs/envs/ogbench/manipulation_semantics.py +496 -0
  143. stable_envs/envs/ogbench/maze.py +1527 -0
  144. stable_envs/envs/ogbench/simulation/__init__.py +0 -0
  145. stable_envs/envs/ogbench/simulation/compatibility.py +677 -0
  146. stable_envs/envs/ogbench/simulation/execution.py +78 -0
  147. stable_envs/envs/ogbench/simulation/manipulation.py +545 -0
  148. stable_envs/envs/ogbench/simulation/manipulation_batch.py +481 -0
  149. stable_envs/envs/ogbench/simulation/manipulation_control.py +330 -0
  150. stable_envs/envs/ogbench/simulation/manipulation_process.py +71 -0
  151. stable_envs/envs/ogbench/simulation/manipulation_warp.py +712 -0
  152. stable_envs/envs/ogbench/simulation/maze.py +446 -0
  153. stable_envs/envs/ogbench/simulation/maze_batch.py +903 -0
  154. stable_envs/envs/ogbench/simulation/maze_process.py +32 -0
  155. stable_envs/envs/ogbench/simulation/pcg.py +46 -0
  156. stable_envs/envs/ogbench/simulation/render_state.py +295 -0
  157. stable_envs/envs/pusht/__init__.py +17 -0
  158. stable_envs/envs/pusht/draw.py +123 -0
  159. stable_envs/envs/pusht/legacy.py +522 -0
  160. stable_envs/envs/pusht/projection.py +124 -0
  161. stable_envs/envs/pusht/raster.py +1379 -0
  162. stable_envs/envs/pusht/raster_cpu.py +669 -0
  163. stable_envs/envs/pusht/raster_device.py +933 -0
  164. stable_envs/envs/pusht/render_pool.py +114 -0
  165. stable_envs/envs/pusht/semantics.py +505 -0
  166. stable_envs/envs/pusht/simulation/__init__.py +0 -0
  167. stable_envs/envs/pusht/simulation/batch.py +540 -0
  168. stable_envs/envs/pusht/simulation/contacts.py +32 -0
  169. stable_envs/envs/pusht/simulation/execution.py +70 -0
  170. stable_envs/envs/pusht/simulation/history.py +27 -0
  171. stable_envs/envs/pusht/simulation/jax_batch.py +56 -0
  172. stable_envs/envs/pusht/simulation/jax_physics.py +402 -0
  173. stable_envs/envs/pusht/simulation/numba_batch.py +199 -0
  174. stable_envs/envs/pusht/simulation/numba_broadphase.py +329 -0
  175. stable_envs/envs/pusht/simulation/numba_physics.py +662 -0
  176. stable_envs/envs/pusht/simulation/numeric.py +506 -0
  177. stable_envs/envs/pusht/simulation/process.py +149 -0
  178. stable_envs/envs/pusht/simulation/quantizer.py +64 -0
  179. stable_envs/envs/pusht/simulation/replay.py +412 -0
  180. stable_envs/envs/pusht/simulation/replay_batch.py +387 -0
  181. stable_envs/envs/pusht/simulation/reset_pool.py +183 -0
  182. stable_envs/envs/pusht/simulation/task.py +898 -0
  183. stable_envs/envs/pusht/simulation/triton_batch.py +117 -0
  184. stable_envs/envs/pusht/simulation/triton_broadphase.py +370 -0
  185. stable_envs/envs/pusht/simulation/triton_physics.py +1007 -0
  186. stable_envs/envs/roadster/__init__.py +8 -0
  187. stable_envs/envs/roadster/envs.py +273 -0
  188. stable_envs/envs/roadster/render.py +219 -0
  189. stable_envs/envs/roadster/simulation/__init__.py +0 -0
  190. stable_envs/envs/roadster/simulation/adapter.py +191 -0
  191. stable_envs/envs/roadster/simulation/base.py +408 -0
  192. stable_envs/envs/roadster/simulation/batch.py +544 -0
  193. stable_envs/envs/roadster/simulation/execution.py +36 -0
  194. stable_envs/envs/roadster/simulation/warp.py +651 -0
  195. stable_envs/envs/roadster/simulation/world.py +295 -0
  196. stable_envs/envs/roadster/source/__init__.py +18 -0
  197. stable_envs/envs/roadster/source/body_mesh.py +459 -0
  198. stable_envs/envs/roadster/source/cinematic.py +95 -0
  199. stable_envs/envs/roadster/source/control.py +137 -0
  200. stable_envs/envs/roadster/source/course.py +55 -0
  201. stable_envs/envs/roadster/source/decor.py +358 -0
  202. stable_envs/envs/roadster/source/expert.py +64 -0
  203. stable_envs/envs/roadster/source/fx.py +219 -0
  204. stable_envs/envs/roadster/source/model.py +449 -0
  205. stable_envs/envs/roadster/source/scenarios.py +245 -0
  206. stable_envs/envs/roadster/source/sim.py +362 -0
  207. stable_envs/envs/roadster/source/tasks.py +429 -0
  208. stable_envs/envs/roadster/source/textures.py +171 -0
  209. stable_envs/envs/roadster/source/util.py +45 -0
  210. stable_envs/envs/roadster/source/weather.py +170 -0
  211. stable_envs/envs/rocket/__init__.py +20 -0
  212. stable_envs/envs/rocket/legacy.py +785 -0
  213. stable_envs/envs/rocket/raster.py +184 -0
  214. stable_envs/envs/rocket/render.py +518 -0
  215. stable_envs/envs/rocket/semantics.py +551 -0
  216. stable_envs/envs/rocket/simulation/__init__.py +0 -0
  217. stable_envs/envs/rocket/simulation/actions.py +33 -0
  218. stable_envs/envs/rocket/simulation/adapter.py +298 -0
  219. stable_envs/envs/rocket/simulation/execution.py +91 -0
  220. stable_envs/envs/rocket/simulation/numba_batch.py +635 -0
  221. stable_envs/envs/rocket/simulation/numba_cuda_batch.py +375 -0
  222. stable_envs/envs/rocket/simulation/numba_cuda_physics.py +300 -0
  223. stable_envs/envs/rocket/simulation/numba_physics.py +1210 -0
  224. stable_envs/envs/rocket/simulation/numeric.py +97 -0
  225. stable_envs/envs/rocket/simulation/process.py +115 -0
  226. stable_envs/envs/rocket/simulation/replay.py +648 -0
  227. stable_envs/envs/rocket/simulation/task.py +518 -0
  228. stable_envs/envs/rocket/tiny_raster.py +1169 -0
  229. stable_envs/registry.py +282 -0
  230. stable_envs/render/__init__.py +0 -0
  231. stable_envs/render/_resources.py +272 -0
  232. stable_envs/render/cpu3d.py +1117 -0
  233. stable_envs/render/cpu_raster/__init__.py +43 -0
  234. stable_envs/render/cpu_raster/_common.py +215 -0
  235. stable_envs/render/cpu_raster/classic.py +153 -0
  236. stable_envs/render/cpu_raster/rays3d.py +470 -0
  237. stable_envs/render/cpu_raster/scene3d.py +866 -0
  238. stable_envs/render/cpu_raster/suite3d.py +388 -0
  239. stable_envs/render/gfxdraw/__init__.py +1410 -0
  240. stable_envs/render/gfxdraw/classic.py +415 -0
  241. stable_envs/render/gfxdraw/fused.py +452 -0
  242. stable_envs/render/mjwarp/__init__.py +483 -0
  243. stable_envs/render/mujoco_raster.py +1601 -0
  244. stable_envs/render/mujoco_scene.py +861 -0
  245. stable_envs/render/nvdiff.py +1120 -0
  246. stable_envs/render/primitives.py +158 -0
  247. stable_envs/render/raster2d.py +759 -0
  248. stable_envs/render/raycast3d.py +1324 -0
  249. stable_envs/render/scene.py +411 -0
  250. stable_envs/render/texture_mip.py +151 -0
  251. stable_envs/spaces.py +405 -0
  252. stable_envs/specs.py +336 -0
  253. stable_envs/testing/__init__.py +5 -0
  254. stable_envs/testing/_data.py +595 -0
  255. stable_envs/testing/_sampling.py +135 -0
  256. stable_envs/testing/conformance.py +1270 -0
  257. stable_envs/testing/corpus.py +127 -0
  258. stable_envs/testing/evidence.py +111 -0
  259. stable_envs/testing/metrics.py +93 -0
  260. stable_envs/testing/provenance.py +164 -0
  261. stable_envs/tuning.py +569 -0
  262. stable_envs-0.1.0.dist-info/METADATA +241 -0
  263. stable_envs-0.1.0.dist-info/RECORD +278 -0
  264. stable_envs-0.1.0.dist-info/WHEEL +5 -0
  265. stable_envs-0.1.0.dist-info/entry_points.txt +2 -0
  266. stable_envs-0.1.0.dist-info/licenses/LICENSE +21 -0
  267. stable_envs-0.1.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +235 -0
  268. stable_envs-0.1.0.dist-info/licenses/licenses/APACHE-2.0.txt +201 -0
  269. stable_envs-0.1.0.dist-info/licenses/licenses/Bullet-zlib.txt +14 -0
  270. stable_envs-0.1.0.dist-info/licenses/licenses/Gymnasium-MIT.txt +22 -0
  271. stable_envs-0.1.0.dist-info/licenses/licenses/Gymnasium-Robotics-MIT.txt +21 -0
  272. stable_envs-0.1.0.dist-info/licenses/licenses/LGPL-2.1.txt +501 -0
  273. stable_envs-0.1.0.dist-info/licenses/licenses/Munk2D-MIT.txt +20 -0
  274. stable_envs-0.1.0.dist-info/licenses/licenses/NumPy-BSD-3-Clause.txt +30 -0
  275. stable_envs-0.1.0.dist-info/licenses/licenses/NumPy-random-NCSA-BSD-3-Clause.txt +71 -0
  276. stable_envs-0.1.0.dist-info/licenses/licenses/OGBench-MIT.txt +21 -0
  277. stable_envs-0.1.0.dist-info/licenses/licenses/PyFlyt-MIT.txt +19 -0
  278. stable_envs-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,9 @@
1
+ from __future__ import annotations
2
+
3
+ __title__ = "stable-envs"
4
+ __version__ = "0.1.0"
5
+ __summary__ = "A batched research-environment library with explicit state, dynamics, variation spaces, and fast state-driven CPU/GPU rendering."
6
+ __author__ = "Randall Balestriero, Sami BuGhanem"
7
+ __license__ = "MIT"
8
+
9
+ __all__ = ["__title__", "__version__", "__summary__", "__author__", "__license__"]
@@ -0,0 +1,91 @@
1
+ """Batched environments with explicit state, variation and state-driven rendering.
2
+
3
+ Environment classes are attributes named by their registry key, for example ``stable_envs.PushT``.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any
9
+
10
+ from . import registry as _registry
11
+ from . import tuning
12
+ from .__about__ import __version__ # noqa: F401
13
+ from ._core.config import get_config, set # noqa: A004,F401
14
+ from .env import Env, State, StepResult, VariationLike # noqa: F401
15
+ from .registry import import_errors, info, list_envs, list_families # noqa: F401
16
+ from .registry import make as _make
17
+ from .specs import CompositeSpec, Spec # noqa: F401
18
+
19
+ # Discover available families; report missing optional dependencies through import_errors().
20
+ _registry.discover()
21
+
22
+
23
+ def make(name: str, **kwargs) -> Env:
24
+ """Construct a registered environment by class name.
25
+
26
+ ``make("CartPole", num_envs=64)`` is the same as ``stable_envs.CartPole(num_envs=64)``.
27
+
28
+ Args:
29
+ name: Class name from ``list_envs``, not a ``reference_id`` such as ``"CartPole-v1"``.
30
+ **kwargs: Forwarded to the constructor, such as ``num_envs``, ``size``, ``device`` and ``seed``.
31
+
32
+ Raises:
33
+ KeyError: If ``name`` is not registered. ``import_errors`` shows families that didn't import.
34
+ """
35
+ return _make(name, **kwargs)
36
+
37
+
38
+ def tune(env: Env, *, knobs=(), repeats: int = 5, write: bool = True, verbose: bool = True) -> dict[str, int]:
39
+ """Time the tuning knobs ``env`` reads and cache the fastest values.
40
+
41
+ Args:
42
+ env: Environment whose device, ``size`` and ``num_envs`` define the workload.
43
+ knobs: Knobs to sweep (all that ``env`` reads if empty).
44
+ repeats: Timed calls per value.
45
+ write: Save the results to the tuning cache.
46
+ verbose: Print the result table.
47
+
48
+ Returns:
49
+ A dict mapping each swept knob to its chosen value.
50
+
51
+ Raises:
52
+ ValueError: If ``knobs`` names an unknown knob.
53
+ """
54
+ from ._tune import tune as _tune
55
+
56
+ return _tune(env, knobs=knobs, repeats=repeats, write=write, echo=print if verbose else None)
57
+
58
+
59
+ def __getattr__(name: str) -> Any:
60
+ """Resolve environment classes by canonical registry name."""
61
+ if name in _registry.REGISTRY:
62
+ return _registry.REGISTRY[name]
63
+ raise AttributeError(
64
+ f"module 'stable_envs' has no attribute {name!r}. Environments are looked up by class "
65
+ f"name: snv.list_envs() shows the {len(_registry.REGISTRY)} registered and "
66
+ f"snv.import_errors() shows the families that did not import."
67
+ )
68
+
69
+
70
+ def __dir__() -> list[str]:
71
+ return sorted({*__all__, *_registry.REGISTRY})
72
+
73
+
74
+ __all__ = [
75
+ "Env",
76
+ "State",
77
+ "StepResult",
78
+ "VariationLike",
79
+ "Spec",
80
+ "CompositeSpec",
81
+ "make",
82
+ "info",
83
+ "list_envs",
84
+ "list_families",
85
+ "import_errors",
86
+ "set",
87
+ "get_config",
88
+ "tuning",
89
+ "tune",
90
+ "__version__",
91
+ ]
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1 @@
1
+ """Private shared helpers used across the stable_envs package."""
@@ -0,0 +1,12 @@
1
+ """The stable-envs cache directory, shared by the tuning cache and model downloads."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+
8
+
9
+ def cache_dir() -> Path:
10
+ """Return the ``stable-envs`` cache root: ``$XDG_CACHE_HOME/stable-envs``, or ``~/.cache/stable-envs`` when unset."""
11
+ base = os.environ.get("XDG_CACHE_HOME") or str(Path.home() / ".cache")
12
+ return Path(base) / "stable-envs"
@@ -0,0 +1,84 @@
1
+ """Global verbosity and default-device settings, and the package logger."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from loguru import logger as _loguru_logger
8
+
9
+ from . import environ as _environ
10
+
11
+ # Records carry extra["package"] == "stable_envs" for application-side filtering.
12
+ logger = _loguru_logger.bind(package="stable_envs")
13
+
14
+ _LEVELS = {
15
+ "TRACE": 5,
16
+ "DEBUG": 10,
17
+ "INFO": 20,
18
+ "SUCCESS": 25,
19
+ "WARNING": 30,
20
+ "ERROR": 40,
21
+ "CRITICAL": 50,
22
+ "SILENT": 100,
23
+ }
24
+
25
+ _DEFAULTS: dict[str, Any] = {"verbose": "WARNING", "default_device": "auto"}
26
+
27
+
28
+ def _initial() -> dict[str, Any]:
29
+ return {**_DEFAULTS, "verbose": _environ.read("STABLE_ENVS_VERBOSE")}
30
+
31
+
32
+ _CONFIG: dict[str, Any] = _initial()
33
+
34
+
35
+ def set(**kwargs: Any) -> dict[str, Any]: # noqa: A001 - the public spelling is ``snv.set``
36
+ """Update the log level or default device and return a copy of the settings.
37
+
38
+ Args:
39
+ **kwargs: ``verbose``, the lowest level logged (``"TRACE"``, ``"DEBUG"``, ``"INFO"``,
40
+ ``"SUCCESS"``, ``"WARNING"``, ``"ERROR"``, ``"CRITICAL"`` or ``"SILENT"``), and
41
+ ``default_device``, used when a constructor gets ``device=None``.
42
+
43
+ Raises:
44
+ KeyError: If any other key is passed.
45
+ ValueError: If ``verbose`` is not a known level.
46
+ """
47
+ unknown = [k for k in kwargs if k not in _DEFAULTS]
48
+ if unknown:
49
+ raise KeyError(
50
+ f"stable_envs.set: unknown config key(s) {unknown}; the keys are {sorted(_DEFAULTS)}. "
51
+ f"device, size, supersample and backend are environment constructor arguments."
52
+ )
53
+ if "verbose" in kwargs:
54
+ v = str(kwargs["verbose"]).upper()
55
+ if v not in _LEVELS:
56
+ raise ValueError(f"stable_envs.set: verbose must be one of {sorted(_LEVELS)}, got {kwargs['verbose']!r}")
57
+ kwargs["verbose"] = v
58
+ if "default_device" in kwargs and kwargs["default_device"] is not None:
59
+ kwargs["default_device"] = str(kwargs["default_device"])
60
+ _CONFIG.update(kwargs)
61
+ return dict(_CONFIG)
62
+
63
+
64
+ def get_config() -> dict[str, Any]:
65
+ """Return a copy of the global settings, ``verbose`` and ``default_device``."""
66
+ return dict(_CONFIG)
67
+
68
+
69
+ def reset_config() -> dict[str, Any]:
70
+ """Restore and return the default global settings, with verbosity from ``STABLE_ENVS_VERBOSE``."""
71
+ _CONFIG.clear()
72
+ _CONFIG.update(_initial())
73
+ return dict(_CONFIG)
74
+
75
+
76
+ def log(level: str, message: str) -> None:
77
+ """Log ``message`` through the package logger when ``level`` meets the configured verbosity.
78
+
79
+ Args:
80
+ level: A level name such as ``"DEBUG"`` or ``"WARNING"``.
81
+ message: The text to log.
82
+ """
83
+ if _LEVELS[level.upper()] >= _LEVELS[_CONFIG["verbose"]]:
84
+ logger.opt(depth=1).log(level.upper(), message)
@@ -0,0 +1,17 @@
1
+ """Detect whether an environment implements dynamics, and the error raised when it does not."""
2
+
3
+
4
+ def has_dynamics(env):
5
+ """Return whether ``env`` overrides the base ``_step``."""
6
+ base = getattr(env, "_se_dynamics_base", None)
7
+ if base is None:
8
+ return hasattr(env, "step")
9
+ return type(env)._step is not base._step
10
+
11
+
12
+ def no_dynamics(env, what):
13
+ """Raise NotImplementedError for ``what`` on a render-only environment."""
14
+ raise NotImplementedError(
15
+ f"{type(env).__name__}.{what}: full simulator semantics are not implemented for this environment; "
16
+ "it renders supplied states only. Compact renderer rows cannot substitute for State."
17
+ )
@@ -0,0 +1,166 @@
1
+ """The registry of every ``STABLE_ENVS_*`` environment variable and the one function that reads them."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import functools
6
+ import os
7
+ import re
8
+ from collections.abc import Callable
9
+ from dataclasses import dataclass
10
+ from typing import Any
11
+
12
+ PREFIX = "STABLE_ENVS_"
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class Variable:
17
+ """One environment variable: its name, value type, default, documentation and parser."""
18
+
19
+ name: str
20
+ kind: str
21
+ default: Any
22
+ doc: str
23
+ parse: Callable[[str], Any]
24
+
25
+
26
+ VARIABLES: dict[str, Variable] = {}
27
+
28
+ _SUFFIX = {"": 1, "K": 1 << 10, "M": 1 << 20, "G": 1 << 30, "T": 1 << 40}
29
+
30
+
31
+ def parse_int(text: str) -> int | None:
32
+ """Parse an integer with an optional K, M, G or T suffix (powers of 1024); ``auto`` or empty is None."""
33
+ t = text.strip().replace("_", "")
34
+ if t.lower() in ("", "auto"):
35
+ return None
36
+ m = re.fullmatch(r"(\d+)\s*([KMGT]?)I?B?", t.upper())
37
+ if m is None:
38
+ raise ValueError("expected an integer, optionally with a K, M, G or T suffix, or 'auto'")
39
+ return int(m.group(1)) * _SUFFIX[m.group(2)]
40
+
41
+
42
+ def parse_flag(text: str) -> bool:
43
+ """Parse ``1`` as True and ``0`` or empty as False."""
44
+ t = text.strip()
45
+ if t not in ("", "0", "1"):
46
+ raise ValueError("expected 1 or 0")
47
+ return t == "1"
48
+
49
+
50
+ def parse_path(text: str) -> str | None:
51
+ """Return a non-empty path string, or None when empty."""
52
+ return text.strip() or None
53
+
54
+
55
+ def register(name: str, kind: str, default: Any, doc: str, parse: Callable[[str], Any]) -> Variable:
56
+ """Add a variable to the registry; registering the same name twice keeps the first entry."""
57
+ if not name.startswith(PREFIX):
58
+ raise ValueError(f"{name} does not start with {PREFIX}")
59
+ return VARIABLES.setdefault(name, Variable(name, kind, default, doc, parse))
60
+
61
+
62
+ @functools.lru_cache(maxsize=512)
63
+ def _parsed(name: str, raw: str) -> Any:
64
+ try:
65
+ return VARIABLES[name].parse(raw)
66
+ except ValueError as e:
67
+ raise ValueError(f"{name}={raw!r}: {e}") from None
68
+
69
+
70
+ def read(name: str) -> Any:
71
+ """Return the parsed value of variable ``name``, or its default when unset.
72
+
73
+ Raises:
74
+ KeyError: ``name`` is not registered.
75
+ ValueError: The value does not parse; the message names the variable.
76
+ """
77
+ var = VARIABLES[name]
78
+ raw = os.environ.get(name)
79
+ return var.default if raw is None else _parsed(name, raw)
80
+
81
+
82
+ def variables() -> dict[str, Variable]:
83
+ """Return every registered variable, including those of modules not imported yet."""
84
+ from .. import tuning # noqa: F401 (registers the knob variables)
85
+
86
+ return dict(sorted(VARIABLES.items()))
87
+
88
+
89
+ def set_variables(environ=None) -> tuple[dict[str, str], list[str]]:
90
+ """Return the ``STABLE_ENVS_*`` variables set in ``environ`` and the names that are not registered."""
91
+ environ = os.environ if environ is None else environ
92
+ known = variables()
93
+ found = {k: v for k, v in sorted(environ.items()) if k.startswith(PREFIX)}
94
+ return found, [k for k in found if k not in known]
95
+
96
+
97
+ def parse_verbosity(text: str) -> str:
98
+ """Parse a log level name such as ``INFO`` or ``SILENT``; an empty value is ``WARNING``."""
99
+ from .config import _LEVELS
100
+
101
+ t = text.strip().upper() or "WARNING"
102
+ if t not in _LEVELS:
103
+ raise ValueError(f"expected one of {sorted(_LEVELS)}")
104
+ return t
105
+
106
+
107
+ def parse_dmc_render(text: str) -> str:
108
+ """Parse ``auto`` (or empty) or ``mjwarp``."""
109
+ t = text.strip() or "auto"
110
+ if t not in ("auto", "mjwarp"):
111
+ raise ValueError("expected auto or mjwarp")
112
+ return t
113
+
114
+
115
+ def parse_json_object(text: str) -> dict | None:
116
+ """Parse a JSON object; an empty value is None."""
117
+ import json
118
+
119
+ t = text.strip()
120
+ if not t:
121
+ return None
122
+ try:
123
+ value = json.loads(t)
124
+ except json.JSONDecodeError as e:
125
+ raise ValueError(f"expected a JSON object: {e}") from None
126
+ if not isinstance(value, dict):
127
+ raise ValueError("expected a JSON object")
128
+ return value
129
+
130
+
131
+ register(
132
+ "STABLE_ENVS_DMC_CUDA_RENDER",
133
+ "choice",
134
+ "auto",
135
+ "mjwarp makes backend='auto' fixed-camera DM Control environments on CUDA draw with MuJoCo Warp's ray tracer.",
136
+ parse_dmc_render,
137
+ )
138
+ register(
139
+ "STABLE_ENVS_DMC_WARP_OPTIONS",
140
+ "json",
141
+ None,
142
+ "A JSON object of default MuJoCo Warp render options for DM Control; render_options= overrides it.",
143
+ parse_json_object,
144
+ )
145
+ register(
146
+ "STABLE_ENVS_VERBOSE",
147
+ "level",
148
+ "WARNING",
149
+ "Default log level of the library, with the same values as snv.set(verbose=...). snv.set overrides it.",
150
+ parse_verbosity,
151
+ )
152
+ register(
153
+ "STABLE_ENVS_OGB_UNMEASURED",
154
+ "flag",
155
+ False,
156
+ "1 lets OGBench Cube and Scene cells without measured visual fidelity register, with placeholder "
157
+ "values, while measuring them. Never set it for normal use.",
158
+ parse_flag,
159
+ )
160
+ register(
161
+ "STABLE_ENVS_MSHUMAN_HOME",
162
+ "path",
163
+ None,
164
+ "Path to an existing MS-Human-700 checkout; unset downloads the pinned commit into the cache.",
165
+ parse_path,
166
+ )
@@ -0,0 +1,106 @@
1
+ """Configure GL before importing MuJoCo or dm_control.
2
+
3
+ On macOS, bind MuJoCo to its native backend before importing dm_control with
4
+ rendering disabled. This avoids GLFW initialization during discovery. Restore
5
+ ``glfw`` afterward for Gymnasium, which rejects ``cgl``. On other platforms,
6
+ default to offscreen EGL and preserve explicit settings.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import importlib
12
+ import importlib.util
13
+ import os
14
+ import sys
15
+
16
+ _DARWIN_MUJOCO = frozenset({"cgl", "glfw"})
17
+ _LINUX_MUJOCO = frozenset({"egl", "osmesa", "glfw"})
18
+ GL_VARS = ("MUJOCO_GL", "PYOPENGL_PLATFORM", "MUJOCO_EGL_DEVICE_ID")
19
+ #: The GL variables as the process received them, before configure_mujoco_gl fills defaults.
20
+ INITIAL_GL_ENV = {name: os.environ.get(name) for name in GL_VARS}
21
+
22
+ NO_RENDERER_FIX = (
23
+ "Headless Linux: install EGL (apt install libegl1; NVIDIA machines also need the driver's "
24
+ "libEGL_nvidia) and keep MUJOCO_GL=egl, or install OSMesa (apt install libosmesa6) and set "
25
+ "MUJOCO_GL=osmesa PYOPENGL_PLATFORM=osmesa. With a display, MUJOCO_GL=glfw. "
26
+ "`stable-envs doctor` tests each backend on this machine."
27
+ )
28
+
29
+
30
+ def selected_backend(environ=None, platform: str = sys.platform) -> str:
31
+ """Return the MUJOCO_GL backend MuJoCo binds when stable_envs imports it.
32
+
33
+ Unset on Linux, it follows a PYOPENGL_PLATFORM that names a MuJoCo backend, else EGL.
34
+ """
35
+ environ = os.environ if environ is None else environ
36
+ backend = environ.get("MUJOCO_GL")
37
+ if platform == "darwin":
38
+ return backend if backend in _DARWIN_MUJOCO else "cgl"
39
+ if backend is not None:
40
+ return backend
41
+ pyopengl = (environ.get("PYOPENGL_PLATFORM") or "").lower()
42
+ return pyopengl if pyopengl in _LINUX_MUJOCO else "egl"
43
+
44
+
45
+ def configure_mujoco_gl() -> str:
46
+ """Configure import-time GL selection and return the resulting environment value."""
47
+ if sys.platform == "darwin":
48
+ if "mujoco" not in sys.modules:
49
+ os.environ["MUJOCO_GL"] = selected_backend()
50
+ try:
51
+ import mujoco # noqa: F401
52
+ except Exception: # noqa: BLE001 — optional extra; discovery records the miss
53
+ pass
54
+ have_dmc = importlib.util.find_spec("dm_control") is not None
55
+ if have_dmc and "dm_control._render" not in sys.modules:
56
+ os.environ["MUJOCO_GL"] = "disable"
57
+ try:
58
+ importlib.import_module("dm_control.mujoco")
59
+ except Exception: # noqa: BLE001 — a backend that is not installed
60
+ return os.environ["MUJOCO_GL"]
61
+ # Gymnasium's OffScreenViewer only accepts glfw/egl/osmesa. dm_control is already
62
+ # imported (or absent), so restoring glfw does not re-enter GLFW init in this process.
63
+ os.environ["MUJOCO_GL"] = "glfw"
64
+ return os.environ["MUJOCO_GL"]
65
+ os.environ.setdefault("MUJOCO_GL", selected_backend())
66
+ return os.environ["MUJOCO_GL"]
67
+
68
+
69
+ def _renderer_import_error() -> Exception | None:
70
+ # mujoco swallows the ImportError that leaves it without Renderer; importing again recovers it.
71
+ for name in ("mujoco.rendering.classic.renderer", "mujoco.renderer"):
72
+ try:
73
+ importlib.import_module(name)
74
+ except ModuleNotFoundError as e:
75
+ if e.name != name:
76
+ return e
77
+ except Exception as e: # noqa: BLE001 — reported to the user
78
+ return e
79
+ return None
80
+
81
+
82
+ def mujoco_renderer(model, *, height: int, width: int):
83
+ """Construct ``mujoco.Renderer``, failing with the GL setup to change when no OpenGL context can be made."""
84
+ import mujoco
85
+
86
+ backend = os.environ.get("MUJOCO_GL", "<unset>")
87
+ renderer = getattr(mujoco, "Renderer", None)
88
+ if renderer is None:
89
+ cause = _renderer_import_error()
90
+ detail = f"MuJoCo has no OpenGL renderer with MUJOCO_GL={backend}"
91
+ else:
92
+ try:
93
+ return renderer(model, height=height, width=width)
94
+ except ValueError:
95
+ raise
96
+ except Exception as e: # noqa: BLE001 — re-raised with the fix
97
+ cause = e
98
+ detail = f"MuJoCo could not create an OpenGL context with MUJOCO_GL={backend}"
99
+ pyopengl = os.environ.get("PYOPENGL_PLATFORM")
100
+ if cause is not None and "PYOPENGL_PLATFORM" in str(cause) and pyopengl:
101
+ raise RuntimeError(
102
+ f"PYOPENGL_PLATFORM={pyopengl} conflicts with MUJOCO_GL={backend}: unset PYOPENGL_PLATFORM "
103
+ f"or set both to the same backend. {NO_RENDERER_FIX}"
104
+ ) from cause
105
+ reason = f" ({type(cause).__name__}: {cause})" if cause is not None else ""
106
+ raise RuntimeError(f"{detail}{reason}. {NO_RENDERER_FIX}") from cause