Mujoco KDL Wrapper  0.2.2
MuJoCo + KDL bridge for robot kinematics and dynamics
Loading...
Searching...
No Matches
menagerie.py
Go to the documentation of this file.
1"""Locate (and optionally fetch) MuJoCo Menagerie models.
2
3The only model source this package provides is the official MuJoCo Menagerie
4(https://github.com/google-deepmind/mujoco_menagerie). Resolution order:
5
61. ``MJ_KDL_MENAGERIE`` env var (a Menagerie checkout root), if set.
72. The local cache populated by ``mj-kdl-fetch-menagerie`` (or :func:`fetch`).
8
9Examples use this helper for the Kinova GEN3 arm and bundled assets. Both are
10resolved from explicit env overrides or the user cache.
11
12Other model sources (e.g. the ``robot_descriptions`` package, your own URDF/MJCF
13exports) are not provided here, but you can use them by pointing ``MJ_KDL_MODEL``
14/ ``MJ_KDL_GRIPPER`` (or ``MJ_KDL_MENAGERIE``) at the files yourself.
15"""
16
17from __future__ import annotations
18
19import os
20import shutil
21import subprocess
22from importlib import resources
23from pathlib import Path
24
25MENAGERIE_REPO = "https://github.com/google-deepmind/mujoco_menagerie.git"
26
27# logical name -> (Menagerie subdirectory, model file within it)
28_MODELS = {
29 "kinova_gen3": ("kinova_gen3", "gen3.xml"),
30 "robotiq_2f85": ("robotiq_2f85", "2f85.xml"),
31}
32
33
34def _cache_root() -> Path:
35 # Mirrored in src/examples/example_paths.hpp:cache_root and
36 # CMakeLists.txt's _MJ_KDL_CACHE_ROOT; keep all three in sync.
37 base = os.environ.get("XDG_CACHE_HOME") or str(Path.home() / ".cache")
38 return Path(base) / "mj_kdl_wrapper"
39
40
41def _cache_dir() -> Path:
42 return _cache_root() / "menagerie"
43
44
45def assets_cache_dir() -> Path:
46 return _cache_root() / "assets"
47
48
49def _repo_assets_dir() -> Path:
50 return Path(__file__).resolve().parents[2] / "assets"
51
52
53def model_path(name: str, *, env_var: str | None = None) -> str:
54 """Return a filesystem path to the named Menagerie model.
55
56 If ``env_var`` is set, that value is used as a user override; it must
57 point at an existing file or a RuntimeError is raised. Otherwise the
58 fetched cache is used; if it is missing, a RuntimeError explains how to
59 fetch the models.
60 """
61 if env_var:
62 override = os.environ.get(env_var)
63 if override:
64 if not Path(override).exists():
65 raise RuntimeError(f"{env_var}={override} was set but does not exist")
66 return override
67
68 try:
69 subdir, filename = _MODELS[name]
70 except KeyError:
71 raise KeyError(f"unknown Menagerie model '{name}'; known: {sorted(_MODELS)}") from None
72
73 env = os.environ.get("MJ_KDL_MENAGERIE")
74 roots = (Path(env), _cache_dir()) if env else (_cache_dir(),)
75 for root in roots:
76 candidate = root / subdir / filename
77 if candidate.exists():
78 return str(candidate)
79
80 searched = ", ".join(str(root / subdir / filename) for root in roots)
81 raise RuntimeError(
82 f"Menagerie model '{name}' was not found. Searched: {searched}. Fetch the models "
83 f"with the 'mj-kdl-fetch-menagerie' console script, set MJ_KDL_MENAGERIE to a "
84 f"MuJoCo Menagerie checkout, or set {env_var or 'the model env var'} to a model file."
85 )
86
87
88def asset_path(relative_path: str, *, env_var: str | None = None) -> str:
89 """Return a filesystem path to a bundled mj_kdl_wrapper asset.
90
91 If ``env_var`` is set, that value is used as a user override; it must
92 point at an existing file or a RuntimeError is raised.
93 """
94 if env_var:
95 override = os.environ.get(env_var)
96 if override:
97 if not Path(override).exists():
98 raise RuntimeError(f"{env_var}={override} was set but does not exist")
99 return override
100
101 relative = Path(relative_path)
102 if relative.is_absolute() or ".." in relative.parts:
103 raise ValueError(f"asset path must be relative inside assets/: {relative_path}")
104
105 try:
107 except RuntimeError:
108 pass
109
110 candidate = assets_cache_dir() / relative
111 if candidate.exists():
112 return str(candidate)
113
114 raise RuntimeError(
115 f"Asset '{relative_path}' was not found. Searched: {candidate}. Fetch bundled assets "
116 f"with 'mj-kdl-fetch-menagerie' or set {env_var or 'the asset env var'} to an asset file."
117 )
118
119
120def _run(cmd: list[str]) -> bool:
121 return subprocess.run(cmd, capture_output=True).returncode == 0
122
123
124def fetch(dest: str | os.PathLike[str] | None = None) -> dict[str, str]:
125 """Shallow-clone the full MuJoCo Menagerie into ``dest`` (default: cache).
126
127 Returns a mapping of model name -> resolved path. Requires ``git``.
128 """
129 if shutil.which("git") is None:
130 raise RuntimeError("git is required to fetch MuJoCo Menagerie")
131
132 target = Path(dest) if dest is not None else _cache_dir()
133 if not (target / ".git").exists():
134 target.parent.mkdir(parents=True, exist_ok=True)
135 shutil.rmtree(target, ignore_errors=True)
136 if not _run(["git", "clone", "--depth", "1", MENAGERIE_REPO, str(target)]):
137 raise RuntimeError(f"failed to clone {MENAGERIE_REPO}")
138
139 resolved = {}
140 for name, (subdir, filename) in _MODELS.items():
141 path = target / subdir / filename
142 if not path.exists():
143 raise RuntimeError(f"expected {path} after fetch, but it is missing")
144 resolved[name] = str(path)
146 return resolved
147
148
149def fetch_assets(dest: str | os.PathLike[str] | None = None) -> str:
150 """Copy bundled mj_kdl_wrapper assets into ``dest`` (default: cache)."""
151 target = Path(dest) if dest is not None else assets_cache_dir()
152 target.parent.mkdir(parents=True, exist_ok=True)
153 try:
154 with resources.as_file(resources.files("mj_kdl_wrapper") / "assets") as assets_src:
155 if assets_src.exists():
156 shutil.copytree(assets_src, target, dirs_exist_ok=True)
157 return str(target)
158 except ImportError:
159 pass
160 if _repo_assets_dir().exists():
161 shutil.copytree(_repo_assets_dir(), target, dirs_exist_ok=True)
162 return str(target)
163 raise RuntimeError("bundled mj_kdl_wrapper assets are missing")
164
165
166def main() -> int:
167 import argparse
168
169 parser = argparse.ArgumentParser(
170 description="Fetch the MuJoCo Menagerie models used by the mj_kdl_wrapper examples."
171 )
172 parser.add_argument(
173 "--dest",
174 default=None,
175 help="Destination directory for the Menagerie checkout (default: user cache).",
176 )
177 args = parser.parse_args()
178
179 target = Path(args.dest) if args.dest is not None else _cache_dir()
180 if (target / ".git").exists():
181 print(f"Using existing MuJoCo Menagerie checkout: {target}")
182 else:
183 print(f"Fetching MuJoCo Menagerie from {MENAGERIE_REPO}")
184 print(f"Destination: {target}")
185
186 resolved = fetch(target)
187
188 print("Resolved models:")
189 for name, path in resolved.items():
190 print(f" {name}: {path}")
191 print(f"Bundled assets: {assets_cache_dir()}")
192 print()
193 print("Model lookup uses this order:")
194 print(" 1. MJ_KDL_MENAGERIE environment variable")
195 print(f" 2. user cache: {_cache_dir()}")
196 print(f"Bundled asset lookup also uses user cache: {assets_cache_dir()}")
197 print(f"Set MJ_KDL_MENAGERIE={target} to force this checkout.")
198 return 0
199
200
201if __name__ == "__main__":
202 raise SystemExit(main())
str fetch_assets(str|os.PathLike[str]|None dest=None)
Definition menagerie.py:149
bool _run(list[str] cmd)
Definition menagerie.py:120
str model_path(str name, *, str|None env_var=None)
Definition menagerie.py:53
str asset_path(str relative_path, *, str|None env_var=None)
Definition menagerie.py:88
dict[str, str] fetch(str|os.PathLike[str]|None dest=None)
Definition menagerie.py:124