From c28d4b0ec749ed1ca1911329862dd86347b9f809 Mon Sep 17 00:00:00 2001 From: Brandon Hung Date: Fri, 24 Oct 2025 15:09:19 -0400 Subject: [PATCH 01/11] Adding the ability to pull a mesh from a remote github repository --- judo/__init__.py | 3 + judo/models/xml/fr3_components/assets.xml | 58 ------ judo/models/xml/fr3_components/fr3.xml | 116 ------------ .../xml/fr3_components/params_and_default.xml | 77 -------- judo/models/xml/fr3_pick.xml | 17 -- judo/tasks/fr3_pick.py | 7 +- judo/utils/assets.py | 175 +++++++++++++++++- pyproject.toml | 3 +- tests/test_visualizer.py | 2 + 9 files changed, 184 insertions(+), 274 deletions(-) delete mode 100644 judo/models/xml/fr3_components/assets.xml delete mode 100644 judo/models/xml/fr3_components/fr3.xml delete mode 100644 judo/models/xml/fr3_components/params_and_default.xml delete mode 100644 judo/models/xml/fr3_pick.xml diff --git a/judo/__init__.py b/judo/__init__.py index 66b0ff63..0a53a293 100644 --- a/judo/__init__.py +++ b/judo/__init__.py @@ -4,3 +4,6 @@ PACKAGE_ROOT = Path(__file__).resolve().parent MODEL_PATH = PACKAGE_ROOT / "models" +MESHES_PATH = MODEL_PATH / "meshes" +XML_PATH = MODEL_PATH / "xml" +DESCRIPTION_CACHE_DIR = "~/.cache/judo_descriptions_cache" diff --git a/judo/models/xml/fr3_components/assets.xml b/judo/models/xml/fr3_components/assets.xml deleted file mode 100644 index ffd0b90d..00000000 --- a/judo/models/xml/fr3_components/assets.xml +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/judo/models/xml/fr3_components/fr3.xml b/judo/models/xml/fr3_components/fr3.xml deleted file mode 100644 index d8ae83de..00000000 --- a/judo/models/xml/fr3_components/fr3.xml +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/judo/models/xml/fr3_components/params_and_default.xml b/judo/models/xml/fr3_components/params_and_default.xml deleted file mode 100644 index 494af3a9..00000000 --- a/judo/models/xml/fr3_components/params_and_default.xml +++ /dev/null @@ -1,77 +0,0 @@ - - - diff --git a/judo/models/xml/fr3_pick.xml b/judo/models/xml/fr3_pick.xml deleted file mode 100644 index bb8e3403..00000000 --- a/judo/models/xml/fr3_pick.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/judo/tasks/fr3_pick.py b/judo/tasks/fr3_pick.py index 9c022cf2..f2a88b82 100644 --- a/judo/tasks/fr3_pick.py +++ b/judo/tasks/fr3_pick.py @@ -7,12 +7,11 @@ import mujoco import numpy as np -from judo import MODEL_PATH from judo.gui import slider from judo.tasks.base import Task, TaskConfig +from judo.utils.assets import retrieve_description_path_from_remote from judo.utils.fields import np_1d_field -XML_PATH = str(MODEL_PATH / "xml/fr3_pick.xml") QPOS_HOME = np.array( [ 0.7, 0, 0.02, 1, 0, 0, 0, # object @@ -104,8 +103,10 @@ class FR3PickConfig(TaskConfig): class FR3Pick(Task[FR3PickConfig]): """Defines the FR3 pick task.""" - def __init__(self, model_path: str = XML_PATH, sim_model_path: str | None = None) -> None: + def __init__(self, model_path: str | None = None, sim_model_path: str | None = None) -> None: """Initializes the LEAP cube rotation task.""" + if model_path is None: + model_path = retrieve_description_path_from_remote("fr3", force=False) + "/fr3_pick.xml" super().__init__(model_path, sim_model_path=sim_model_path) self.reset_command = np.array([0, 0, 0, -1.57079, 0, 1.57079, -0.7853, 0.0]) diff --git a/judo/utils/assets.py b/judo/utils/assets.py index 527d4fc2..ec4952fa 100644 --- a/judo/utils/assets.py +++ b/judo/utils/assets.py @@ -1,11 +1,21 @@ # Copyright (c) 2025 Robotics and AI Institute LLC. All rights reserved. - +import logging import os +import shutil import time import zipfile +from dataclasses import dataclass from pathlib import Path import requests +from git import ( + GitCommandError, + InvalidGitRepositoryError, + NoSuchPathError, + Repo, +) + +from judo import DESCRIPTION_CACHE_DIR def acquire_lock(lock_path: Path, timeout: int = 60, poll_interval: float = 0.1) -> None: @@ -52,7 +62,7 @@ def download_and_extract_meshes( return # fetch latest release info - print("Mesh assets not detected! Downloading assets now...") + logging.info("Mesh assets not detected! Downloading assets now...") if tag is None: api_url = f"https://api.github.com/repos/{repo}/releases/latest" else: @@ -87,3 +97,164 @@ def download_and_extract_meshes( finally: release_lock(lock_path) + + +@dataclass +class RepoInfo: + """Information required to clone a description from a repository.""" + + url: str + description_path: str + remote_model_path: str | None = None + remote_meshes_path: str | None = None + remote_xml_path: str | None = None + commit_hash: str | None = None + + +KNOWN_DESCRIPTIONS = { + "spot": RepoInfo( + url="", commit_hash=None, description_path="spot", remote_meshes_path="spot/assets", remote_xml_path="spot" + ), + "fr3": RepoInfo( + url="https://github.com/bhung-bdai/judo_descriptions_test.git", + commit_hash="c50e2f3f5d5b151a7a8dd046630235e3733b0689", + description_path="judo_descriptions_test", + remote_model_path="franka_fr3", + ), + "leap": RepoInfo( + url="https://github.com/deepmind/mujoco_menagerie.git", + commit_hash="1f2300db40caa341786bffdad5ee02966c5f8d0a", + description_path="mujoco_menagerie", + remote_meshes_path="leap_hand", + remote_xml_path="leap_hand", + ), + "dynamixel_2r": RepoInfo( + url="https://github.com/deepmind/mujoco_menagerie.git", + commit_hash="1f2300db40caa341786bffdad5ee02966c5f8d0a", + description_path="mujoco_menagerie", + remote_meshes_path="dynamixel_2r/assets", + remote_xml_path="dynamixel_2r", + ), +} + + +def delete_assets_cache(cache_dir: Path | None = None) -> None: + """Deletes the assets cache.""" + if cache_dir is None: + cache_dir = Path(os.environ.get("JUDO_DESCRIPTION_CACHE_DIR", DESCRIPTION_CACHE_DIR)).expanduser() + if not cache_dir.exists(): + raise FileNotFoundError(f"Cache directory {cache_dir} does not exist!") + shutil.rmtree(cache_dir) + + +def get_description_keys() -> list[str]: + """Returns a list of the keys of the known descriptions.""" + return list(KNOWN_DESCRIPTIONS.keys()) + + +def add_known_description(description_name: str, description_info: RepoInfo) -> None: + """Adds a new description via the RepoInfo to the known descriptions dictionary.""" + KNOWN_DESCRIPTIONS[description_name] = description_info + + +def get_description(description_info: RepoInfo, force: bool = False) -> Repo: + """ + Clone a description from the provided information and return the path to the cloned repository. + + Args: + description_info: The information about the description to clone. + force: Forcefully redownload the description, regardless of whether or not it already exists. + + Returns: + The path to the cloned description repository. + """ + assert isinstance(description_info, RepoInfo), "description_info must be a RepoInfo object!" + description_url = description_info.url + description_path = Path(description_info.description_path) + commit_hash = description_info.commit_hash + + # Find the directory hosting the cache + cache_dir = Path(os.environ.get("JUDO_DESCRIPTION_CACHE_DIR", DESCRIPTION_CACHE_DIR)).expanduser() + if commit_hash: + description_path = description_path / commit_hash + cache_path = cache_dir / description_path + + description_clone = None + if force: + if cache_path.exists(): + shutil.rmtree(cache_path) + cache_path.mkdir(parents=True, exist_ok=True) + logging.info(f"Cloning description from {description_url} into {cache_path}") + description_clone = Repo.clone_from(description_url, cache_path) + else: + try: + description_clone = Repo(cache_path) + except (InvalidGitRepositoryError, NoSuchPathError, GitCommandError): + # Deletes the existing cache path for this repo if it exists and recreates it + if cache_path.exists(): + shutil.rmtree(cache_path) + cache_path.mkdir(parents=True, exist_ok=True) + logging.info(f"Cloning description from {description_url} into {cache_path}") + description_clone = Repo.clone_from(description_url, cache_path) + + if description_clone is None: + raise FileNotFoundError(f"Failed to clone description from {description_url} into {cache_path}") + + if commit_hash: + try: + description_clone.git.checkout(commit_hash) + except GitCommandError: + description_clone.git.fetch("origin") + description_clone.git.checkout(commit_hash) + + logging.info(f"Retrieved description from {description_url} to {description_clone.working_dir}") + return description_clone + + +def retrieve_description_path_from_remote(description_name: str, force: bool = False) -> str: + """Gets the path to a description from a remote repository, downloaded and stored in cache.""" + try: + description_info = KNOWN_DESCRIPTIONS[description_name] + except KeyError as e: + raise ValueError(f"Description {description_name} not found in {get_description_keys()}!") from e + description_clone = get_description(description_info, force=force) + return f"{description_clone.working_dir}/{description_info.remote_model_path}" + + +# def import_description_assets( +# description_name: str, local_meshes_path: str = MESHES_PATH, local_xml_path: str = XML_PATH +# ) -> None: +# """ +# Imports assets from a description into the local assets path. +# """ +# try: +# description_info = KNOWN_DESCRIPTIONS[description_name] +# except KeyError: +# raise ValueError(f"Description {description_name} not found in known descriptions!") +# description_clone = get_description(description_info) + +# # Copy in the meshes from the description meshes, if it is not None +# if description_info.remote_meshes_path is not None: +# mesh_path = Path(local_meshes_path) +# if not mesh_path.exists(): +# mesh_path.mkdir(parents=True, exist_ok=True) +# mesh_path = mesh_path / description_name +# if not mesh_path.exists(): +# mesh_path.mkdir(parents=True, exist_ok=True) + +# # Copy meshes from the description location into the local meshes path +# remote_path = Path(description_clone.working_dir) +# remote_meshes_path = remote_path / description_info.remote_meshes_path +# if not remote_meshes_path.exists(): +# raise FileNotFoundError(f"Mesh directory {remote_meshes_path} not found!") +# shutil.copytree(remote_meshes_path, mesh_path, dirs_exist_ok=True) + +# # Copy xmls from the description XML location to the local XML path +# if description_info.remote_xml_path is not None: +# xml_path = Path(local_xml_path) +# if not xml_path.exists(): +# xml_path.mkdir(parents=True, exist_ok=True) +# remote_xml_path = remote_path / description_info.remote_xml_path +# if not remote_xml_path.exists(): +# raise FileNotFoundError(f"XML directory {remote_xml_path} not found!") +# shutil.copytree(remote_xml_path, xml_path, dirs_exist_ok=True) diff --git a/pyproject.toml b/pyproject.toml index afac3772..608034d0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,7 @@ classifiers = [ dependencies = [ "dora-utils", # dora + qol utils for writing nodes + "GitPython >=3.1.18", # required to clone description assets from remote repositories "mujoco", "numpy", "pillow", # for displaying app logo @@ -178,4 +179,4 @@ judo-rai = { path = ".", editable = true } [tool.pixi.environments] default = { solve-group = "default" } docs = { features = ["docs"], solve-group = "default" } -dev = { features = ["dev", "docs"], solve-group = "default" } \ No newline at end of file +dev = { features = ["dev", "docs"], solve-group = "default" } diff --git a/tests/test_visualizer.py b/tests/test_visualizer.py index bfc74a7d..7686c259 100644 --- a/tests/test_visualizer.py +++ b/tests/test_visualizer.py @@ -4,6 +4,7 @@ import numpy as np import pytest from trimesh.creation import box +from trimesh.visual import TextureVisuals from viser import ViserServer from judo import MODEL_PATH @@ -80,6 +81,7 @@ def test_set_mesh_color() -> None: expected_int = rgba_float_to_int(rgba) # Check a few face colors (all should be set to the same integer color) assert mesh.visual is not None, "Mesh visual is None" + assert isinstance(mesh.visual, TextureVisuals), "Mesh visual does not have a material" assert np.array_equal(mesh.visual.material.main_color, expected_int), ( "set_mesh_color did not set the color correctly" ) From fba05704a0729e54f9d452e56e21a220ae6e6b21 Mon Sep 17 00:00:00 2001 From: Brandon Hung Date: Fri, 24 Oct 2025 16:54:27 -0400 Subject: [PATCH 02/11] Loads the default task and caches it locally --- judo/cli.py | 4 ++ judo/tasks/caltech_leap_cube.py | 11 +++-- judo/tasks/cartpole.py | 8 ++-- judo/tasks/cylinder_push.py | 8 ++-- judo/tasks/leap_cube.py | 11 +++-- judo/tasks/leap_cube_down.py | 11 +++-- judo/utils/assets.py | 85 ++++++++++++--------------------- 7 files changed, 64 insertions(+), 74 deletions(-) diff --git a/judo/cli.py b/judo/cli.py index f726171f..2d5007ee 100644 --- a/judo/cli.py +++ b/judo/cli.py @@ -9,6 +9,8 @@ from hydra.core.config_store import ConfigStore from omegaconf import DictConfig +from judo.utils.assets import retrieve_description_path_from_remote + # suppress annoying warning from hydra warnings.filterwarnings( "ignore", @@ -27,6 +29,8 @@ @hydra.main(config_path=str(CONFIG_PATH), config_name="judo_default", version_base="1.3") def main_app(cfg: DictConfig) -> None: """Main function to run judo via a hydra configuration yaml file.""" + # Include a download for the default asset + retrieve_description_path_from_remote(cfg["task"], force=False) run(cfg) diff --git a/judo/tasks/caltech_leap_cube.py b/judo/tasks/caltech_leap_cube.py index 66f1775a..d7405245 100644 --- a/judo/tasks/caltech_leap_cube.py +++ b/judo/tasks/caltech_leap_cube.py @@ -4,12 +4,10 @@ import numpy as np -from judo import MODEL_PATH from judo.gui import slider from judo.tasks.leap_cube import LeapCube, LeapCubeConfig +from judo.utils.assets import retrieve_description_path_from_remote -XML_PATH = str(MODEL_PATH / "xml/caltech_leap_cube.xml") -SIM_XML_PATH = str(MODEL_PATH / "xml/caltech_leap_cube_sim.xml") QPOS_HOME = np.array( [ 0.11, 0.005, 0.04, 1.0, 0.0, 0.0, 0.0, # cube @@ -31,8 +29,13 @@ class CaltechLeapCubeConfig(LeapCubeConfig): class CaltechLeapCube(LeapCube): """Defines the LEAP cube rotation task.""" - def __init__(self, model_path: str = XML_PATH, sim_model_path: str = SIM_XML_PATH) -> None: + def __init__(self, model_path: str | None = None, sim_model_path: str | None = None) -> None: """Initializes the LEAP cube rotation task.""" + leap_cube_path = retrieve_description_path_from_remote("caltech_leap_cube", force=False) + if model_path is None: + model_path = f"{leap_cube_path}/caltech_leap_cube.xml" + if sim_model_path is None: + sim_model_path = f"{leap_cube_path}/caltech_leap_cube_sim.xml" super(LeapCube, self).__init__(model_path, sim_model_path=sim_model_path) self.goal_pos = np.array([0.11, 0.005, 0.03]) self.goal_quat = np.array([1.0, 0.0, 0.0, 0.0]) diff --git a/judo/tasks/cartpole.py b/judo/tasks/cartpole.py index f18ed68a..9260857e 100644 --- a/judo/tasks/cartpole.py +++ b/judo/tasks/cartpole.py @@ -6,14 +6,12 @@ import mujoco import numpy as np -from judo import MODEL_PATH from judo.tasks.base import Task, TaskConfig from judo.tasks.cost_functions import ( quadratic_norm, smooth_l1_norm, ) - -XML_PATH = str(MODEL_PATH / "xml/cartpole.xml") +from judo.utils.assets import retrieve_description_path_from_remote @dataclass @@ -31,8 +29,10 @@ class CartpoleConfig(TaskConfig): class Cartpole(Task[CartpoleConfig]): """Defines the cartpole balancing task.""" - def __init__(self, model_path: str = XML_PATH, sim_model_path: str | None = None) -> None: + def __init__(self, model_path: str | None = None, sim_model_path: str | None = None) -> None: """Initializes the cartpole task.""" + if model_path is None: + model_path = f"{retrieve_description_path_from_remote('cartpole', force=False)}/cartpole.xml" super().__init__(model_path, sim_model_path=sim_model_path) self.reset() diff --git a/judo/tasks/cylinder_push.py b/judo/tasks/cylinder_push.py index 7ad54043..fe0c0c62 100644 --- a/judo/tasks/cylinder_push.py +++ b/judo/tasks/cylinder_push.py @@ -6,14 +6,12 @@ import mujoco import numpy as np -from judo import MODEL_PATH from judo.gui import slider from judo.tasks.base import Task, TaskConfig from judo.tasks.cost_functions import quadratic_norm +from judo.utils.assets import retrieve_description_path_from_remote from judo.utils.fields import np_1d_field -XML_PATH = str(MODEL_PATH / "xml/cylinder_push.xml") - @slider("w_pusher_proximity", 0.0, 5.0, 0.1) @dataclass @@ -39,8 +37,10 @@ class CylinderPushConfig(TaskConfig): class CylinderPush(Task[CylinderPushConfig]): """Defines the cylinder push balancing task.""" - def __init__(self, model_path: str = XML_PATH, sim_model_path: str | None = None) -> None: + def __init__(self, model_path: str | None = None, sim_model_path: str | None = None) -> None: """Initializes the cylinder push task.""" + if model_path is None: + model_path = f"{retrieve_description_path_from_remote('cylinder_push', force=False)}/cylinder_push.xml" super().__init__(model_path, sim_model_path=sim_model_path) self.reset() diff --git a/judo/tasks/leap_cube.py b/judo/tasks/leap_cube.py index 6f26df61..e961663a 100644 --- a/judo/tasks/leap_cube.py +++ b/judo/tasks/leap_cube.py @@ -6,13 +6,11 @@ import mujoco import numpy as np -from judo import MODEL_PATH from judo.gui import slider from judo.tasks.base import Task, TaskConfig +from judo.utils.assets import retrieve_description_path_from_remote from judo.utils.math_utils import quat_diff, quat_diff_so3 -XML_PATH = str(MODEL_PATH / "xml/leap_cube.xml") -SIM_XML_PATH = str(MODEL_PATH / "xml/leap_cube_sim.xml") QPOS_HOME = np.array( [ 0.0, 0.03, 0.1, 1.0, 0.0, 0.0, 0.0, # cube @@ -37,8 +35,13 @@ class LeapCubeConfig(TaskConfig): class LeapCube(Task[LeapCubeConfig]): """Defines the LEAP cube rotation task.""" - def __init__(self, model_path: str = XML_PATH, sim_model_path: str | None = SIM_XML_PATH) -> None: + def __init__(self, model_path: str | None = None, sim_model_path: str | None = None) -> None: """Initializes the LEAP cube rotation task.""" + leap_cube_path = retrieve_description_path_from_remote("leap_cube", force=False) + if model_path is None: + model_path = f"{leap_cube_path}/leap_cube.xml" + if sim_model_path is None: + sim_model_path = f"{leap_cube_path}/leap_cube_sim.xml" super().__init__(model_path, sim_model_path=sim_model_path) self.goal_pos = np.array([0.0, 0.03, 0.1]) self.goal_quat = np.array([1.0, 0.0, 0.0, 0.0]) diff --git a/judo/tasks/leap_cube_down.py b/judo/tasks/leap_cube_down.py index 673ccd55..f6289c1e 100644 --- a/judo/tasks/leap_cube_down.py +++ b/judo/tasks/leap_cube_down.py @@ -4,12 +4,10 @@ import numpy as np -from judo import MODEL_PATH from judo.gui import slider from judo.tasks.leap_cube import LeapCube, LeapCubeConfig +from judo.utils.assets import retrieve_description_path_from_remote -XML_PATH = str(MODEL_PATH / "xml/leap_cube_palm_down.xml") -SIM_XML_PATH = str(MODEL_PATH / "xml/leap_cube_palm_down_sim.xml") QPOS_HOME = np.array( [ -0.04, -0.035, -0.065, 1.0, 0.0, 0.0, 0.0, # cube @@ -33,8 +31,13 @@ class LeapCubeDownConfig(LeapCubeConfig): class LeapCubeDown(LeapCube): """Defines the LEAP cube with palm down rotation task.""" - def __init__(self, model_path: str = XML_PATH, sim_model_path: str = SIM_XML_PATH) -> None: + def __init__(self, model_path: str | None = None, sim_model_path: str | None = None) -> None: """Initializes the LEAP cube rotation task.""" + leap_cube_path = retrieve_description_path_from_remote("leap_cube_palm_down", force=False) + if model_path is None: + model_path = f"{leap_cube_path}/leap_cube_palm_down.xml" + if sim_model_path is None: + sim_model_path = f"{leap_cube_path}/leap_cube_palm_down_sim.xml" super(LeapCube, self).__init__(model_path, sim_model_path=sim_model_path) self.goal_pos = np.array([-0.04, -0.035, -0.065]) self.goal_quat = np.array([1.0, 0.0, 0.0, 0.0]) diff --git a/judo/utils/assets.py b/judo/utils/assets.py index ec4952fa..fc73c085 100644 --- a/judo/utils/assets.py +++ b/judo/utils/assets.py @@ -112,28 +112,44 @@ class RepoInfo: KNOWN_DESCRIPTIONS = { - "spot": RepoInfo( - url="", commit_hash=None, description_path="spot", remote_meshes_path="spot/assets", remote_xml_path="spot" + # "spot": RepoInfo( + # url="", commit_hash=None, description_path="spot", remote_meshes_path="spot/assets", remote_xml_path="spot" + # ), + "cylinder_push": RepoInfo( + url="https://github.com/bhung-bdai/judo_descriptions_test.git", + commit_hash=None, + description_path="judo_descriptions_test", + remote_model_path="cylinder_push", + ), + "cartpole": RepoInfo( + url="https://github.com/bhung-bdai/judo_descriptions_test.git", + commit_hash=None, + description_path="judo_descriptions_test", + remote_model_path="cartpole", ), "fr3": RepoInfo( url="https://github.com/bhung-bdai/judo_descriptions_test.git", - commit_hash="c50e2f3f5d5b151a7a8dd046630235e3733b0689", + commit_hash=None, description_path="judo_descriptions_test", remote_model_path="franka_fr3", ), - "leap": RepoInfo( - url="https://github.com/deepmind/mujoco_menagerie.git", - commit_hash="1f2300db40caa341786bffdad5ee02966c5f8d0a", - description_path="mujoco_menagerie", - remote_meshes_path="leap_hand", - remote_xml_path="leap_hand", + "leap_cube": RepoInfo( + url="https://github.com/bhung-bdai/judo_descriptions_test.git", + commit_hash=None, + description_path="judo_descriptions_test", + remote_model_path="leap_hand", + ), + "leap_cube_palm_down": RepoInfo( + url="https://github.com/bhung-bdai/judo_descriptions_test.git", + commit_hash=None, + description_path="judo_descriptions_test", + remote_model_path="leap_hand", ), - "dynamixel_2r": RepoInfo( - url="https://github.com/deepmind/mujoco_menagerie.git", - commit_hash="1f2300db40caa341786bffdad5ee02966c5f8d0a", - description_path="mujoco_menagerie", - remote_meshes_path="dynamixel_2r/assets", - remote_xml_path="dynamixel_2r", + "caltech_leap_cube": RepoInfo( + url="https://github.com/bhung-bdai/judo_descriptions_test.git", + commit_hash=None, + description_path="judo_descriptions_test", + remote_model_path="leap_hand", ), } @@ -219,42 +235,3 @@ def retrieve_description_path_from_remote(description_name: str, force: bool = F raise ValueError(f"Description {description_name} not found in {get_description_keys()}!") from e description_clone = get_description(description_info, force=force) return f"{description_clone.working_dir}/{description_info.remote_model_path}" - - -# def import_description_assets( -# description_name: str, local_meshes_path: str = MESHES_PATH, local_xml_path: str = XML_PATH -# ) -> None: -# """ -# Imports assets from a description into the local assets path. -# """ -# try: -# description_info = KNOWN_DESCRIPTIONS[description_name] -# except KeyError: -# raise ValueError(f"Description {description_name} not found in known descriptions!") -# description_clone = get_description(description_info) - -# # Copy in the meshes from the description meshes, if it is not None -# if description_info.remote_meshes_path is not None: -# mesh_path = Path(local_meshes_path) -# if not mesh_path.exists(): -# mesh_path.mkdir(parents=True, exist_ok=True) -# mesh_path = mesh_path / description_name -# if not mesh_path.exists(): -# mesh_path.mkdir(parents=True, exist_ok=True) - -# # Copy meshes from the description location into the local meshes path -# remote_path = Path(description_clone.working_dir) -# remote_meshes_path = remote_path / description_info.remote_meshes_path -# if not remote_meshes_path.exists(): -# raise FileNotFoundError(f"Mesh directory {remote_meshes_path} not found!") -# shutil.copytree(remote_meshes_path, mesh_path, dirs_exist_ok=True) - -# # Copy xmls from the description XML location to the local XML path -# if description_info.remote_xml_path is not None: -# xml_path = Path(local_xml_path) -# if not xml_path.exists(): -# xml_path.mkdir(parents=True, exist_ok=True) -# remote_xml_path = remote_path / description_info.remote_xml_path -# if not remote_xml_path.exists(): -# raise FileNotFoundError(f"XML directory {remote_xml_path} not found!") -# shutil.copytree(remote_xml_path, xml_path, dirs_exist_ok=True) From 821df5fee85d6dd4244c438688268b3cd9fed97d Mon Sep 17 00:00:00 2001 From: Brandon Hung Date: Fri, 24 Oct 2025 17:20:27 -0400 Subject: [PATCH 03/11] Remove bad meshes --- judo/__init__.py | 3 - judo/controller/__init__.py | 15 +- judo/models/xml/README.md | 10 - .../xml/caltech_leap_components/assets.xml | 25 -- .../xml/caltech_leap_components/leap_rh.xml | 269 ------------------ .../params_and_default.xml | 186 ------------ .../params_and_default_sim.xml | 186 ------------ judo/models/xml/caltech_leap_cube.xml | 33 --- judo/models/xml/caltech_leap_cube_sim.xml | 33 --- judo/models/xml/cartpole.xml | 45 --- judo/models/xml/cylinder_push.xml | 45 --- judo/models/xml/leap_components/assets.xml | 14 - .../xml/leap_components/cube_pips_coll.xml | 39 --- .../xml/leap_components/cube_pips_vis.xml | 37 --- judo/models/xml/leap_components/leap_hand.xml | 260 ----------------- .../leap_components/params_and_default.xml | 164 ----------- .../params_and_default_sim.xml | 164 ----------- judo/models/xml/leap_cube.xml | 17 -- judo/models/xml/leap_cube_palm_down.xml | 17 -- judo/models/xml/leap_cube_palm_down_sim.xml | 17 -- judo/models/xml/leap_cube_sim.xml | 17 -- tests/test_visualizer.py | 5 +- 22 files changed, 11 insertions(+), 1590 deletions(-) delete mode 100644 judo/models/xml/README.md delete mode 100644 judo/models/xml/caltech_leap_components/assets.xml delete mode 100644 judo/models/xml/caltech_leap_components/leap_rh.xml delete mode 100644 judo/models/xml/caltech_leap_components/params_and_default.xml delete mode 100644 judo/models/xml/caltech_leap_components/params_and_default_sim.xml delete mode 100644 judo/models/xml/caltech_leap_cube.xml delete mode 100644 judo/models/xml/caltech_leap_cube_sim.xml delete mode 100644 judo/models/xml/cartpole.xml delete mode 100644 judo/models/xml/cylinder_push.xml delete mode 100644 judo/models/xml/leap_components/assets.xml delete mode 100644 judo/models/xml/leap_components/cube_pips_coll.xml delete mode 100644 judo/models/xml/leap_components/cube_pips_vis.xml delete mode 100644 judo/models/xml/leap_components/leap_hand.xml delete mode 100644 judo/models/xml/leap_components/params_and_default.xml delete mode 100644 judo/models/xml/leap_components/params_and_default_sim.xml delete mode 100644 judo/models/xml/leap_cube.xml delete mode 100644 judo/models/xml/leap_cube_palm_down.xml delete mode 100644 judo/models/xml/leap_cube_palm_down_sim.xml delete mode 100644 judo/models/xml/leap_cube_sim.xml diff --git a/judo/__init__.py b/judo/__init__.py index 0a53a293..aed54c88 100644 --- a/judo/__init__.py +++ b/judo/__init__.py @@ -3,7 +3,4 @@ from pathlib import Path PACKAGE_ROOT = Path(__file__).resolve().parent -MODEL_PATH = PACKAGE_ROOT / "models" -MESHES_PATH = MODEL_PATH / "meshes" -XML_PATH = MODEL_PATH / "xml" DESCRIPTION_CACHE_DIR = "~/.cache/judo_descriptions_cache" diff --git a/judo/controller/__init__.py b/judo/controller/__init__.py index 82c841d3..7196a908 100644 --- a/judo/controller/__init__.py +++ b/judo/controller/__init__.py @@ -1,6 +1,6 @@ # Copyright (c) 2025 Robotics and AI Institute LLC. All rights reserved. -from judo import MODEL_PATH +# from judo import MODEL_PATH from judo.controller.controller import Controller, ControllerConfig from judo.controller.overrides import ( set_default_caltech_leap_cube_overrides, @@ -10,13 +10,14 @@ set_default_leap_cube_down_overrides, set_default_leap_cube_overrides, ) -from judo.utils.assets import download_and_extract_meshes -download_and_extract_meshes( - extract_root=str(MODEL_PATH), - repo="bdaiinstitute/judo", - asset_name="meshes.zip", -) +# from judo.utils.assets import download_and_extract_meshes + +# download_and_extract_meshes( +# extract_root=str(MODEL_PATH), +# repo="bdaiinstitute/judo", +# asset_name="meshes.zip", +# ) set_default_caltech_leap_cube_overrides() set_default_cartpole_overrides() diff --git a/judo/models/xml/README.md b/judo/models/xml/README.md deleted file mode 100644 index dc4bbbbf..00000000 --- a/judo/models/xml/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# Model Description Sources - -All model descriptions in this directory were either created at RAI or taken and modified from an external source. The table below documents the origin and license of each model description. - -| Model Name | Source | License | Notes | -|---------------|---------------------------------------------------------------------------------------------------------------------------------------------|-------------|-----------------------------------------------------------------| -| cylinder_push | Created at RAI | MIT | N/A | -| cartpole | Created at RAI | MIT | N/A | -| fr3_pick | [mujoco_menagerie – franka_fr3](https://github.com/google-deepmind/mujoco_menagerie/tree/main/franka_fr3) | Apache-2.0 | Modified | -| leap_cube | [mujoco_menagerie – leap_hand](https://github.com/google-deepmind/mujoco_menagerie/tree/main/leap_hand) | MIT | Modified. Includes all derivative `leap_cube` XMLs in this repo | diff --git a/judo/models/xml/caltech_leap_components/assets.xml b/judo/models/xml/caltech_leap_components/assets.xml deleted file mode 100644 index 46604981..00000000 --- a/judo/models/xml/caltech_leap_components/assets.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/judo/models/xml/caltech_leap_components/leap_rh.xml b/judo/models/xml/caltech_leap_components/leap_rh.xml deleted file mode 100644 index 6c455f9e..00000000 --- a/judo/models/xml/caltech_leap_components/leap_rh.xml +++ /dev/null @@ -1,269 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/judo/models/xml/caltech_leap_components/params_and_default.xml b/judo/models/xml/caltech_leap_components/params_and_default.xml deleted file mode 100644 index 9f3a4433..00000000 --- a/judo/models/xml/caltech_leap_components/params_and_default.xml +++ /dev/null @@ -1,186 +0,0 @@ - - - diff --git a/judo/models/xml/caltech_leap_components/params_and_default_sim.xml b/judo/models/xml/caltech_leap_components/params_and_default_sim.xml deleted file mode 100644 index 62ea2772..00000000 --- a/judo/models/xml/caltech_leap_components/params_and_default_sim.xml +++ /dev/null @@ -1,186 +0,0 @@ - - - diff --git a/judo/models/xml/caltech_leap_cube.xml b/judo/models/xml/caltech_leap_cube.xml deleted file mode 100644 index 20d6e812..00000000 --- a/judo/models/xml/caltech_leap_cube.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/judo/models/xml/caltech_leap_cube_sim.xml b/judo/models/xml/caltech_leap_cube_sim.xml deleted file mode 100644 index 99148c1d..00000000 --- a/judo/models/xml/caltech_leap_cube_sim.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/judo/models/xml/cartpole.xml b/judo/models/xml/cartpole.xml deleted file mode 100644 index 967d91c2..00000000 --- a/judo/models/xml/cartpole.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/judo/models/xml/cylinder_push.xml b/judo/models/xml/cylinder_push.xml deleted file mode 100644 index 1e75e2fb..00000000 --- a/judo/models/xml/cylinder_push.xml +++ /dev/null @@ -1,45 +0,0 @@ - - diff --git a/judo/models/xml/leap_components/assets.xml b/judo/models/xml/leap_components/assets.xml deleted file mode 100644 index 55461f7e..00000000 --- a/judo/models/xml/leap_components/assets.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/judo/models/xml/leap_components/cube_pips_coll.xml b/judo/models/xml/leap_components/cube_pips_coll.xml deleted file mode 100644 index c4098be4..00000000 --- a/judo/models/xml/leap_components/cube_pips_coll.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/judo/models/xml/leap_components/cube_pips_vis.xml b/judo/models/xml/leap_components/cube_pips_vis.xml deleted file mode 100644 index acafcdc8..00000000 --- a/judo/models/xml/leap_components/cube_pips_vis.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/judo/models/xml/leap_components/leap_hand.xml b/judo/models/xml/leap_components/leap_hand.xml deleted file mode 100644 index 9fb80bfb..00000000 --- a/judo/models/xml/leap_components/leap_hand.xml +++ /dev/null @@ -1,260 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/judo/models/xml/leap_components/params_and_default.xml b/judo/models/xml/leap_components/params_and_default.xml deleted file mode 100644 index 03eb572c..00000000 --- a/judo/models/xml/leap_components/params_and_default.xml +++ /dev/null @@ -1,164 +0,0 @@ - - - diff --git a/judo/models/xml/leap_components/params_and_default_sim.xml b/judo/models/xml/leap_components/params_and_default_sim.xml deleted file mode 100644 index c561c810..00000000 --- a/judo/models/xml/leap_components/params_and_default_sim.xml +++ /dev/null @@ -1,164 +0,0 @@ - - - diff --git a/judo/models/xml/leap_cube.xml b/judo/models/xml/leap_cube.xml deleted file mode 100644 index aaa6c2e2..00000000 --- a/judo/models/xml/leap_cube.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/judo/models/xml/leap_cube_palm_down.xml b/judo/models/xml/leap_cube_palm_down.xml deleted file mode 100644 index 98b69e9b..00000000 --- a/judo/models/xml/leap_cube_palm_down.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/judo/models/xml/leap_cube_palm_down_sim.xml b/judo/models/xml/leap_cube_palm_down_sim.xml deleted file mode 100644 index 9c2a9511..00000000 --- a/judo/models/xml/leap_cube_palm_down_sim.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/judo/models/xml/leap_cube_sim.xml b/judo/models/xml/leap_cube_sim.xml deleted file mode 100644 index 1389982f..00000000 --- a/judo/models/xml/leap_cube_sim.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/tests/test_visualizer.py b/tests/test_visualizer.py index 7686c259..bb032ff8 100644 --- a/tests/test_visualizer.py +++ b/tests/test_visualizer.py @@ -7,7 +7,8 @@ from trimesh.visual import TextureVisuals from viser import ViserServer -from judo import MODEL_PATH +# from judo import MODEL_PATH +from judo.utils.assets import retrieve_description_path_from_remote from judo.visualizers.model import ( ViserMjModel, add_box, @@ -27,7 +28,7 @@ # Create a global ViserServer instance for use by the tests. viser_server = ViserServer() -model_path = str(MODEL_PATH / "xml/cylinder_push.xml") +model_path = f"{retrieve_description_path_from_remote('cylinder_push', force=False)}/cylinder_push.xml" spec = mujoco.MjSpec.from_file(model_path) model = spec.compile() From 520b5914e662bd61342b19150f3ddb6c0e002ccb Mon Sep 17 00:00:00 2001 From: Brandon Hung Date: Fri, 24 Oct 2025 17:32:04 -0400 Subject: [PATCH 04/11] Add resolution for race condition which can happen when switching between XML elements of the same name --- judo/app/visualization.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/judo/app/visualization.py b/judo/app/visualization.py index 866168fd..fcec6429 100644 --- a/judo/app/visualization.py +++ b/judo/app/visualization.py @@ -432,6 +432,8 @@ def _remove_gui_elements(self) -> None: else: v.remove() self.viser_model.remove() + # Ensure the previous GUI elements are totally removed before setting up the new ones + self.server.flush() def cleanup(self) -> None: """Cleanup the visualization node.""" From 87926e8db1827259c36b436dd5104aeb3939f116 Mon Sep 17 00:00:00 2001 From: Brandon Hung Date: Fri, 24 Oct 2025 17:40:55 -0400 Subject: [PATCH 05/11] Added updated pixi lock --- judo/controller/__init__.py | 9 ----- pixi.lock | 73 ++++++++++++++++++++++++++++++++++++- tests/test_visualizer.py | 1 - 3 files changed, 72 insertions(+), 11 deletions(-) diff --git a/judo/controller/__init__.py b/judo/controller/__init__.py index 7196a908..6481a809 100644 --- a/judo/controller/__init__.py +++ b/judo/controller/__init__.py @@ -1,6 +1,5 @@ # Copyright (c) 2025 Robotics and AI Institute LLC. All rights reserved. -# from judo import MODEL_PATH from judo.controller.controller import Controller, ControllerConfig from judo.controller.overrides import ( set_default_caltech_leap_cube_overrides, @@ -11,14 +10,6 @@ set_default_leap_cube_overrides, ) -# from judo.utils.assets import download_and_extract_meshes - -# download_and_extract_meshes( -# extract_root=str(MODEL_PATH), -# repo="bdaiinstitute/judo", -# asset_name="meshes.zip", -# ) - set_default_caltech_leap_cube_overrides() set_default_cartpole_overrides() set_default_cylinder_push_overrides() diff --git a/pixi.lock b/pixi.lock index 6d557191..be62e29f 100644 --- a/pixi.lock +++ b/pixi.lock @@ -43,6 +43,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/3f/d7/11e00e53ef9fb475ee80a9327af61a491d9f4ec10356be4a23dd6ff4ee47/embreex-2.17.7.post6-cp313-cp313-manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e7/98/87b5946356095738cb90a6df7b35ff69ac5750f6e783d5fbcc5cb3b6cbd7/etils-1.13.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/47/71/70db47e4f6ce3e5c37a607355f80da8860a33226be640226ac52cb05ef2e/fsspec-2025.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/01/61/d4b89fec821f72385526e1b9d9a3a0385dda4a72b206d28049e2c7cd39b8/gitpython-3.1.45-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7f/ba/de3630757c7d7fc2086aaf3994926d6b869d31586e4d0c14f1666af31b93/glfw-2.9.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl @@ -83,6 +85,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/e4/82/08e4076df538fb56caa1d489588d880ec7c52d8273a606bb54d660528f7c/scipy-1.16.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/dd/87/1017c31e52370b2b79e4d29e07cbb590ab9e5e58cf7e2bdfe363765d6251/shapely-2.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3a/83/4f5b250220e1a5acd31345a5ec1c95a7769725d0d8135276f399f44062f8/svg_path-7.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/56/b3/23eec760215910609914dd99aba23ce1c72a3bcbe046ee44f45adf740452/tifffile-2025.8.28-py3-none-any.whl @@ -128,6 +131,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/99/d6/aa06d1060e6526f57d0920ccc5554d83de7dc5cd07d1ea4f2be53b40bc05/embreex-2.17.7.post6-cp313-cp313-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e7/98/87b5946356095738cb90a6df7b35ff69ac5750f6e783d5fbcc5cb3b6cbd7/etils-1.13.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/47/71/70db47e4f6ce3e5c37a607355f80da8860a33226be640226ac52cb05ef2e/fsspec-2025.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/01/61/d4b89fec821f72385526e1b9d9a3a0385dda4a72b206d28049e2c7cd39b8/gitpython-3.1.45-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/21/71/13dd8a8d547809543d21de9438a3a76a8728fc7966d01ad9fb54599aebf5/glfw-2.9.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-macosx_10_6_intel.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl @@ -168,6 +173,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/93/0b/b5c99382b839854a71ca9482c684e3472badc62620287cbbdab499b75ce6/scipy-1.16.1-cp313-cp313-macosx_10_14_x86_64.whl - pypi: https://files.pythonhosted.org/packages/71/8e/2bc836437f4b84d62efc1faddce0d4e023a5d990bbddd3c78b2004ebc246/shapely-2.1.1-cp313-cp313-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3a/83/4f5b250220e1a5acd31345a5ec1c95a7769725d0d8135276f399f44062f8/svg_path-7.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/56/b3/23eec760215910609914dd99aba23ce1c72a3bcbe046ee44f45adf740452/tifffile-2025.8.28-py3-none-any.whl @@ -213,6 +219,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/78/62/0ffc083b1d5f7c95eb03b260d2622dfacb70d1153011d216260b71c8e7d9/dora_utils-0.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e7/98/87b5946356095738cb90a6df7b35ff69ac5750f6e783d5fbcc5cb3b6cbd7/etils-1.13.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/47/71/70db47e4f6ce3e5c37a607355f80da8860a33226be640226ac52cb05ef2e/fsspec-2025.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/01/61/d4b89fec821f72385526e1b9d9a3a0385dda4a72b206d28049e2c7cd39b8/gitpython-3.1.45-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f8/a2/45e6dceec1e0a0ffa8dd3c0ecf1e11d74639a55186243129160c6434d456/glfw-2.9.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl @@ -253,6 +261,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/eb/e5/69ab2771062c91e23e07c12e7d5033a6b9b80b0903ee709c3c36b3eb520c/scipy-1.16.1-cp313-cp313-macosx_12_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/12/a2/12c7cae5b62d5d851c2db836eadd0986f63918a91976495861f7c492f4a9/shapely-2.1.1-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3a/83/4f5b250220e1a5acd31345a5ec1c95a7769725d0d8135276f399f44062f8/svg_path-7.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/56/b3/23eec760215910609914dd99aba23ce1c72a3bcbe046ee44f45adf740452/tifffile-2025.8.28-py3-none-any.whl @@ -300,6 +309,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/78/62/0ffc083b1d5f7c95eb03b260d2622dfacb70d1153011d216260b71c8e7d9/dora_utils-0.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e7/98/87b5946356095738cb90a6df7b35ff69ac5750f6e783d5fbcc5cb3b6cbd7/etils-1.13.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/47/71/70db47e4f6ce3e5c37a607355f80da8860a33226be640226ac52cb05ef2e/fsspec-2025.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/01/61/d4b89fec821f72385526e1b9d9a3a0385dda4a72b206d28049e2c7cd39b8/gitpython-3.1.45-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/70/7f2f052ca20c3b69892818f2ee1fea53b037ea9145ff75b944ed1dc4ff82/glfw-2.9.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl @@ -340,6 +351,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/21/12/c0efd2941f01940119b5305c375ae5c0fcb7ec193f806bd8f158b73a1782/scipy-1.16.1-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/12/d9/6d13b8957a17c95794f0c4dfb65ecd0957e6c7131a56ce18d135c1107a52/shapely-2.1.1-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3a/83/4f5b250220e1a5acd31345a5ec1c95a7769725d0d8135276f399f44062f8/svg_path-7.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/56/b3/23eec760215910609914dd99aba23ce1c72a3bcbe046ee44f45adf740452/tifffile-2025.8.28-py3-none-any.whl @@ -410,6 +422,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/47/71/70db47e4f6ce3e5c37a607355f80da8860a33226be640226ac52cb05ef2e/fsspec-2025.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3a/34/2b07b72bee02a63241d654f5d8af87a2de977c59638eec41ca356ab915cd/furo-2025.7.19-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/01/61/d4b89fec821f72385526e1b9d9a3a0385dda4a72b206d28049e2c7cd39b8/gitpython-3.1.45-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7f/ba/de3630757c7d7fc2086aaf3994926d6b869d31586e4d0c14f1666af31b93/glfw-2.9.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl @@ -467,6 +481,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/e4/82/08e4076df538fb56caa1d489588d880ec7c52d8273a606bb54d660528f7c/scipy-1.16.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/dd/87/1017c31e52370b2b79e4d29e07cbb590ab9e5e58cf7e2bdfe363765d6251/shapely-2.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl @@ -538,6 +553,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/47/71/70db47e4f6ce3e5c37a607355f80da8860a33226be640226ac52cb05ef2e/fsspec-2025.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3a/34/2b07b72bee02a63241d654f5d8af87a2de977c59638eec41ca356ab915cd/furo-2025.7.19-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/01/61/d4b89fec821f72385526e1b9d9a3a0385dda4a72b206d28049e2c7cd39b8/gitpython-3.1.45-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/21/71/13dd8a8d547809543d21de9438a3a76a8728fc7966d01ad9fb54599aebf5/glfw-2.9.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-macosx_10_6_intel.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl @@ -595,6 +612,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/93/0b/b5c99382b839854a71ca9482c684e3472badc62620287cbbdab499b75ce6/scipy-1.16.1-cp313-cp313-macosx_10_14_x86_64.whl - pypi: https://files.pythonhosted.org/packages/71/8e/2bc836437f4b84d62efc1faddce0d4e023a5d990bbddd3c78b2004ebc246/shapely-2.1.1-cp313-cp313-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl @@ -666,6 +684,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/47/71/70db47e4f6ce3e5c37a607355f80da8860a33226be640226ac52cb05ef2e/fsspec-2025.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3a/34/2b07b72bee02a63241d654f5d8af87a2de977c59638eec41ca356ab915cd/furo-2025.7.19-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/01/61/d4b89fec821f72385526e1b9d9a3a0385dda4a72b206d28049e2c7cd39b8/gitpython-3.1.45-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f8/a2/45e6dceec1e0a0ffa8dd3c0ecf1e11d74639a55186243129160c6434d456/glfw-2.9.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl @@ -723,6 +743,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/eb/e5/69ab2771062c91e23e07c12e7d5033a6b9b80b0903ee709c3c36b3eb520c/scipy-1.16.1-cp313-cp313-macosx_12_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/12/a2/12c7cae5b62d5d851c2db836eadd0986f63918a91976495861f7c492f4a9/shapely-2.1.1-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl @@ -796,6 +817,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/47/71/70db47e4f6ce3e5c37a607355f80da8860a33226be640226ac52cb05ef2e/fsspec-2025.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3a/34/2b07b72bee02a63241d654f5d8af87a2de977c59638eec41ca356ab915cd/furo-2025.7.19-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/01/61/d4b89fec821f72385526e1b9d9a3a0385dda4a72b206d28049e2c7cd39b8/gitpython-3.1.45-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/70/7f2f052ca20c3b69892818f2ee1fea53b037ea9145ff75b944ed1dc4ff82/glfw-2.9.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl @@ -853,6 +876,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/21/12/c0efd2941f01940119b5305c375ae5c0fcb7ec193f806bd8f158b73a1782/scipy-1.16.1-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/12/d9/6d13b8957a17c95794f0c4dfb65ecd0957e6c7131a56ce18d135c1107a52/shapely-2.1.1-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl @@ -932,6 +956,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/e7/98/87b5946356095738cb90a6df7b35ff69ac5750f6e783d5fbcc5cb3b6cbd7/etils-1.13.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/47/71/70db47e4f6ce3e5c37a607355f80da8860a33226be640226ac52cb05ef2e/fsspec-2025.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3a/34/2b07b72bee02a63241d654f5d8af87a2de977c59638eec41ca356ab915cd/furo-2025.7.19-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/01/61/d4b89fec821f72385526e1b9d9a3a0385dda4a72b206d28049e2c7cd39b8/gitpython-3.1.45-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7f/ba/de3630757c7d7fc2086aaf3994926d6b869d31586e4d0c14f1666af31b93/glfw-2.9.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl @@ -978,6 +1004,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/e4/82/08e4076df538fb56caa1d489588d880ec7c52d8273a606bb54d660528f7c/scipy-1.16.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/dd/87/1017c31e52370b2b79e4d29e07cbb590ab9e5e58cf7e2bdfe363765d6251/shapely-2.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl @@ -1043,6 +1070,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/e7/98/87b5946356095738cb90a6df7b35ff69ac5750f6e783d5fbcc5cb3b6cbd7/etils-1.13.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/47/71/70db47e4f6ce3e5c37a607355f80da8860a33226be640226ac52cb05ef2e/fsspec-2025.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3a/34/2b07b72bee02a63241d654f5d8af87a2de977c59638eec41ca356ab915cd/furo-2025.7.19-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/01/61/d4b89fec821f72385526e1b9d9a3a0385dda4a72b206d28049e2c7cd39b8/gitpython-3.1.45-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/21/71/13dd8a8d547809543d21de9438a3a76a8728fc7966d01ad9fb54599aebf5/glfw-2.9.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-macosx_10_6_intel.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl @@ -1089,6 +1118,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/93/0b/b5c99382b839854a71ca9482c684e3472badc62620287cbbdab499b75ce6/scipy-1.16.1-cp313-cp313-macosx_10_14_x86_64.whl - pypi: https://files.pythonhosted.org/packages/71/8e/2bc836437f4b84d62efc1faddce0d4e023a5d990bbddd3c78b2004ebc246/shapely-2.1.1-cp313-cp313-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl @@ -1154,6 +1184,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/e7/98/87b5946356095738cb90a6df7b35ff69ac5750f6e783d5fbcc5cb3b6cbd7/etils-1.13.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/47/71/70db47e4f6ce3e5c37a607355f80da8860a33226be640226ac52cb05ef2e/fsspec-2025.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3a/34/2b07b72bee02a63241d654f5d8af87a2de977c59638eec41ca356ab915cd/furo-2025.7.19-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/01/61/d4b89fec821f72385526e1b9d9a3a0385dda4a72b206d28049e2c7cd39b8/gitpython-3.1.45-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f8/a2/45e6dceec1e0a0ffa8dd3c0ecf1e11d74639a55186243129160c6434d456/glfw-2.9.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl @@ -1200,6 +1232,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/eb/e5/69ab2771062c91e23e07c12e7d5033a6b9b80b0903ee709c3c36b3eb520c/scipy-1.16.1-cp313-cp313-macosx_12_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/12/a2/12c7cae5b62d5d851c2db836eadd0986f63918a91976495861f7c492f4a9/shapely-2.1.1-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl @@ -1267,6 +1300,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/e7/98/87b5946356095738cb90a6df7b35ff69ac5750f6e783d5fbcc5cb3b6cbd7/etils-1.13.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/47/71/70db47e4f6ce3e5c37a607355f80da8860a33226be640226ac52cb05ef2e/fsspec-2025.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3a/34/2b07b72bee02a63241d654f5d8af87a2de977c59638eec41ca356ab915cd/furo-2025.7.19-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/01/61/d4b89fec821f72385526e1b9d9a3a0385dda4a72b206d28049e2c7cd39b8/gitpython-3.1.45-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/70/7f2f052ca20c3b69892818f2ee1fea53b037ea9145ff75b944ed1dc4ff82/glfw-2.9.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl @@ -1313,6 +1348,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/21/12/c0efd2941f01940119b5305c375ae5c0fcb7ec193f806bd8f158b73a1782/scipy-1.16.1-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/12/d9/6d13b8957a17c95794f0c4dfb65ecd0957e6c7131a56ce18d135c1107a52/shapely-2.1.1-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl @@ -1933,6 +1969,35 @@ packages: - pygments>=2.7 - accessible-pygments>=0.0.5 requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl + name: gitdb + version: 4.0.12 + sha256: 67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf + requires_dist: + - smmap>=3.0.1,<6 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/01/61/d4b89fec821f72385526e1b9d9a3a0385dda4a72b206d28049e2c7cd39b8/gitpython-3.1.45-py3-none-any.whl + name: gitpython + version: 3.1.45 + sha256: 8908cb2e02fb3b93b7eb0f2827125cb699869470432cc885f019b8fd0fccff77 + requires_dist: + - gitdb>=4.0.1,<5 + - typing-extensions>=3.10.0.2 ; python_full_version < '3.10' + - coverage[toml] ; extra == 'test' + - ddt>=1.1.1,!=1.4.3 ; extra == 'test' + - mock ; python_full_version < '3.8' and extra == 'test' + - mypy ; extra == 'test' + - pre-commit ; extra == 'test' + - pytest>=7.3.1 ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-instafail ; extra == 'test' + - pytest-mock ; extra == 'test' + - pytest-sugar ; extra == 'test' + - typing-extensions ; python_full_version < '3.11' and extra == 'test' + - sphinx>=7.1.2,<7.2 ; extra == 'doc' + - sphinx-rtd-theme ; extra == 'doc' + - sphinx-autodoc-typehints ; extra == 'doc' + requires_python: '>=3.7' - pypi: https://files.pythonhosted.org/packages/21/71/13dd8a8d547809543d21de9438a3a76a8728fc7966d01ad9fb54599aebf5/glfw-2.9.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-macosx_10_6_intel.whl name: glfw version: 2.9.0 @@ -2168,9 +2233,10 @@ packages: - pypi: ./ name: judo-rai version: 0.0.4 - sha256: d4ac7cd64855d3dfdd89ad3fd54ad4706bc4095e670c15f516043b856aa37df3 + sha256: 3ab39d2dd098aafb4162eb27fd25175d942bebb9b20b088f031f539dfba3c757 requires_dist: - dora-utils + - gitpython>=3.1.18 - mujoco - numpy - pillow @@ -4236,6 +4302,11 @@ packages: version: 1.17.0 sha256: 4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*' +- pypi: https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl + name: smmap + version: 5.0.2 + sha256: b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e + requires_python: '>=3.7' - pypi: https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl name: sniffio version: 1.3.1 diff --git a/tests/test_visualizer.py b/tests/test_visualizer.py index bb032ff8..986e1f35 100644 --- a/tests/test_visualizer.py +++ b/tests/test_visualizer.py @@ -7,7 +7,6 @@ from trimesh.visual import TextureVisuals from viser import ViserServer -# from judo import MODEL_PATH from judo.utils.assets import retrieve_description_path_from_remote from judo.visualizers.model import ( ViserMjModel, From f7a904f14adb8de461f637455f6b8b42f056e780 Mon Sep 17 00:00:00 2001 From: Brandon Hung Date: Wed, 29 Oct 2025 16:49:06 -0400 Subject: [PATCH 06/11] Added function to return the model instead of the path to the repo --- judo/cli.py | 2 +- judo/tasks/caltech_leap_cube.py | 8 +++--- judo/tasks/cartpole.py | 5 ++-- judo/tasks/cylinder_push.py | 5 ++-- judo/tasks/fr3_pick.py | 5 ++-- judo/tasks/leap_cube.py | 8 +++--- judo/tasks/leap_cube_down.py | 8 +++--- judo/utils/assets.py | 51 +++++++++++++++++++++++++++------ tests/test_visualizer.py | 4 +-- 9 files changed, 67 insertions(+), 29 deletions(-) diff --git a/judo/cli.py b/judo/cli.py index 2d5007ee..c7d732d5 100644 --- a/judo/cli.py +++ b/judo/cli.py @@ -29,7 +29,7 @@ @hydra.main(config_path=str(CONFIG_PATH), config_name="judo_default", version_base="1.3") def main_app(cfg: DictConfig) -> None: """Main function to run judo via a hydra configuration yaml file.""" - # Include a download for the default asset + # Include a download for the default asset to cache the description repository retrieve_description_path_from_remote(cfg["task"], force=False) run(cfg) diff --git a/judo/tasks/caltech_leap_cube.py b/judo/tasks/caltech_leap_cube.py index d7405245..5f07e4cc 100644 --- a/judo/tasks/caltech_leap_cube.py +++ b/judo/tasks/caltech_leap_cube.py @@ -6,7 +6,7 @@ from judo.gui import slider from judo.tasks.leap_cube import LeapCube, LeapCubeConfig -from judo.utils.assets import retrieve_description_path_from_remote +from judo.utils.assets import retrieve_model_from_remote QPOS_HOME = np.array( [ @@ -31,11 +31,11 @@ class CaltechLeapCube(LeapCube): def __init__(self, model_path: str | None = None, sim_model_path: str | None = None) -> None: """Initializes the LEAP cube rotation task.""" - leap_cube_path = retrieve_description_path_from_remote("caltech_leap_cube", force=False) + default_model_path, default_sim_model_path = retrieve_model_from_remote("caltech_leap_cube", force=False) if model_path is None: - model_path = f"{leap_cube_path}/caltech_leap_cube.xml" + model_path = default_model_path if sim_model_path is None: - sim_model_path = f"{leap_cube_path}/caltech_leap_cube_sim.xml" + sim_model_path = default_sim_model_path super(LeapCube, self).__init__(model_path, sim_model_path=sim_model_path) self.goal_pos = np.array([0.11, 0.005, 0.03]) self.goal_quat = np.array([1.0, 0.0, 0.0, 0.0]) diff --git a/judo/tasks/cartpole.py b/judo/tasks/cartpole.py index 9260857e..2d9c3839 100644 --- a/judo/tasks/cartpole.py +++ b/judo/tasks/cartpole.py @@ -11,7 +11,7 @@ quadratic_norm, smooth_l1_norm, ) -from judo.utils.assets import retrieve_description_path_from_remote +from judo.utils.assets import retrieve_model_from_remote @dataclass @@ -31,8 +31,9 @@ class Cartpole(Task[CartpoleConfig]): def __init__(self, model_path: str | None = None, sim_model_path: str | None = None) -> None: """Initializes the cartpole task.""" + default_model_path, _ = retrieve_model_from_remote("cartpole", force=False) if model_path is None: - model_path = f"{retrieve_description_path_from_remote('cartpole', force=False)}/cartpole.xml" + model_path = default_model_path super().__init__(model_path, sim_model_path=sim_model_path) self.reset() diff --git a/judo/tasks/cylinder_push.py b/judo/tasks/cylinder_push.py index fe0c0c62..1861dda7 100644 --- a/judo/tasks/cylinder_push.py +++ b/judo/tasks/cylinder_push.py @@ -9,7 +9,7 @@ from judo.gui import slider from judo.tasks.base import Task, TaskConfig from judo.tasks.cost_functions import quadratic_norm -from judo.utils.assets import retrieve_description_path_from_remote +from judo.utils.assets import retrieve_model_from_remote from judo.utils.fields import np_1d_field @@ -39,8 +39,9 @@ class CylinderPush(Task[CylinderPushConfig]): def __init__(self, model_path: str | None = None, sim_model_path: str | None = None) -> None: """Initializes the cylinder push task.""" + default_model_path, _ = retrieve_model_from_remote("cylinder_push", force=False) if model_path is None: - model_path = f"{retrieve_description_path_from_remote('cylinder_push', force=False)}/cylinder_push.xml" + model_path = default_model_path super().__init__(model_path, sim_model_path=sim_model_path) self.reset() diff --git a/judo/tasks/fr3_pick.py b/judo/tasks/fr3_pick.py index f2a88b82..b5fe199d 100644 --- a/judo/tasks/fr3_pick.py +++ b/judo/tasks/fr3_pick.py @@ -9,7 +9,7 @@ from judo.gui import slider from judo.tasks.base import Task, TaskConfig -from judo.utils.assets import retrieve_description_path_from_remote +from judo.utils.assets import retrieve_model_from_remote from judo.utils.fields import np_1d_field QPOS_HOME = np.array( @@ -105,8 +105,9 @@ class FR3Pick(Task[FR3PickConfig]): def __init__(self, model_path: str | None = None, sim_model_path: str | None = None) -> None: """Initializes the LEAP cube rotation task.""" + default_model_path, _ = retrieve_model_from_remote("fr3", force=False) if model_path is None: - model_path = retrieve_description_path_from_remote("fr3", force=False) + "/fr3_pick.xml" + model_path = default_model_path super().__init__(model_path, sim_model_path=sim_model_path) self.reset_command = np.array([0, 0, 0, -1.57079, 0, 1.57079, -0.7853, 0.0]) diff --git a/judo/tasks/leap_cube.py b/judo/tasks/leap_cube.py index e961663a..4e81a5bb 100644 --- a/judo/tasks/leap_cube.py +++ b/judo/tasks/leap_cube.py @@ -8,7 +8,7 @@ from judo.gui import slider from judo.tasks.base import Task, TaskConfig -from judo.utils.assets import retrieve_description_path_from_remote +from judo.utils.assets import retrieve_model_from_remote from judo.utils.math_utils import quat_diff, quat_diff_so3 QPOS_HOME = np.array( @@ -37,11 +37,11 @@ class LeapCube(Task[LeapCubeConfig]): def __init__(self, model_path: str | None = None, sim_model_path: str | None = None) -> None: """Initializes the LEAP cube rotation task.""" - leap_cube_path = retrieve_description_path_from_remote("leap_cube", force=False) + default_model_path, default_sim_model_path = retrieve_model_from_remote("leap_cube", force=False) if model_path is None: - model_path = f"{leap_cube_path}/leap_cube.xml" + model_path = default_model_path if sim_model_path is None: - sim_model_path = f"{leap_cube_path}/leap_cube_sim.xml" + sim_model_path = default_sim_model_path super().__init__(model_path, sim_model_path=sim_model_path) self.goal_pos = np.array([0.0, 0.03, 0.1]) self.goal_quat = np.array([1.0, 0.0, 0.0, 0.0]) diff --git a/judo/tasks/leap_cube_down.py b/judo/tasks/leap_cube_down.py index f6289c1e..773a54e3 100644 --- a/judo/tasks/leap_cube_down.py +++ b/judo/tasks/leap_cube_down.py @@ -6,7 +6,7 @@ from judo.gui import slider from judo.tasks.leap_cube import LeapCube, LeapCubeConfig -from judo.utils.assets import retrieve_description_path_from_remote +from judo.utils.assets import retrieve_model_from_remote QPOS_HOME = np.array( [ @@ -33,11 +33,11 @@ class LeapCubeDown(LeapCube): def __init__(self, model_path: str | None = None, sim_model_path: str | None = None) -> None: """Initializes the LEAP cube rotation task.""" - leap_cube_path = retrieve_description_path_from_remote("leap_cube_palm_down", force=False) + default_model_path, default_sim_model_path = retrieve_model_from_remote("leap_cube_palm_down", force=False) if model_path is None: - model_path = f"{leap_cube_path}/leap_cube_palm_down.xml" + model_path = default_model_path if sim_model_path is None: - sim_model_path = f"{leap_cube_path}/leap_cube_palm_down_sim.xml" + sim_model_path = default_sim_model_path super(LeapCube, self).__init__(model_path, sim_model_path=sim_model_path) self.goal_pos = np.array([-0.04, -0.035, -0.065]) self.goal_quat = np.array([1.0, 0.0, 0.0, 0.0]) diff --git a/judo/utils/assets.py b/judo/utils/assets.py index fc73c085..063f079c 100644 --- a/judo/utils/assets.py +++ b/judo/utils/assets.py @@ -99,57 +99,62 @@ def download_and_extract_meshes( release_lock(lock_path) -@dataclass +@dataclass(frozen=True) class RepoInfo: """Information required to clone a description from a repository.""" url: str description_path: str - remote_model_path: str | None = None - remote_meshes_path: str | None = None - remote_xml_path: str | None = None + remote_model_path: str + remote_model_file: str # File name representing the model + remote_sim_file: str | None = None # Optional simulation model in case it's not the same as the model commit_hash: str | None = None KNOWN_DESCRIPTIONS = { - # "spot": RepoInfo( - # url="", commit_hash=None, description_path="spot", remote_meshes_path="spot/assets", remote_xml_path="spot" - # ), "cylinder_push": RepoInfo( url="https://github.com/bhung-bdai/judo_descriptions_test.git", commit_hash=None, description_path="judo_descriptions_test", remote_model_path="cylinder_push", + remote_model_file="cylinder_push.xml", ), "cartpole": RepoInfo( url="https://github.com/bhung-bdai/judo_descriptions_test.git", commit_hash=None, description_path="judo_descriptions_test", remote_model_path="cartpole", + remote_model_file="cartpole.xml", ), "fr3": RepoInfo( url="https://github.com/bhung-bdai/judo_descriptions_test.git", commit_hash=None, description_path="judo_descriptions_test", remote_model_path="franka_fr3", + remote_model_file="franka_fr3.xml", ), "leap_cube": RepoInfo( url="https://github.com/bhung-bdai/judo_descriptions_test.git", commit_hash=None, description_path="judo_descriptions_test", remote_model_path="leap_hand", + remote_model_file="leap_cube.xml", ), "leap_cube_palm_down": RepoInfo( url="https://github.com/bhung-bdai/judo_descriptions_test.git", commit_hash=None, description_path="judo_descriptions_test", remote_model_path="leap_hand", + remote_model_file="leap_cube_palm_down.xml", + remote_sim_file="leap_cube_palm_down_sim.xml", ), "caltech_leap_cube": RepoInfo( url="https://github.com/bhung-bdai/judo_descriptions_test.git", commit_hash=None, description_path="judo_descriptions_test", remote_model_path="leap_hand", + remote_model_file="caltech_leap_cube.xml", + remote_sim_file="caltech_leap_cube_sim.xml", ), } @@ -228,10 +233,40 @@ def get_description(description_info: RepoInfo, force: bool = False) -> Repo: def retrieve_description_path_from_remote(description_name: str, force: bool = False) -> str: - """Gets the path to a description from a remote repository, downloaded and stored in cache.""" + """Gets the path to a description from a remote repository, downloaded and stored in cache. + + Args: + description_name: The name of the description to retrieve. + force: Forcefully redownload the description, regardless of whether or not it already exists. + + Returns: + The path to the description repository in the local cache. + """ try: description_info = KNOWN_DESCRIPTIONS[description_name] except KeyError as e: raise ValueError(f"Description {description_name} not found in {get_description_keys()}!") from e description_clone = get_description(description_info, force=force) return f"{description_clone.working_dir}/{description_info.remote_model_path}" + + +def retrieve_model_from_remote(description_name: str, force: bool = False) -> tuple[str, str | None]: + """Gets the path to a model (and simulation file, if available) in a remote description, stored in cache. + + Args: + description_name: The name of the description to retrieve. + force: Forcefully redownload the description, regardless of whether or not it already exists. + + Returns: + The path to the description repository in the local cache. + """ + try: + description_info = KNOWN_DESCRIPTIONS[description_name] + except KeyError as e: + raise ValueError(f"Description {description_name} not found in {get_description_keys()}!") from e + description_model_path = retrieve_description_path_from_remote(description_name, force=force) + description_model_file = f"{description_model_path}/{description_info.remote_model_file}" + + if description_info.remote_sim_file is not None: + return (description_model_file, f"{description_model_path}/{description_info.remote_sim_file}") + return (description_model_file, None) diff --git a/tests/test_visualizer.py b/tests/test_visualizer.py index 986e1f35..67143ae2 100644 --- a/tests/test_visualizer.py +++ b/tests/test_visualizer.py @@ -7,7 +7,7 @@ from trimesh.visual import TextureVisuals from viser import ViserServer -from judo.utils.assets import retrieve_description_path_from_remote +from judo.utils.assets import retrieve_model_from_remote from judo.visualizers.model import ( ViserMjModel, add_box, @@ -27,7 +27,7 @@ # Create a global ViserServer instance for use by the tests. viser_server = ViserServer() -model_path = f"{retrieve_description_path_from_remote('cylinder_push', force=False)}/cylinder_push.xml" +model_path, _ = retrieve_model_from_remote("cylinder_push", force=False) spec = mujoco.MjSpec.from_file(model_path) model = spec.compile() From cea54f5a8b1d3084c6b5f9fbf1b56c22551a1442 Mon Sep 17 00:00:00 2001 From: Brandon Hung Date: Wed, 29 Oct 2025 16:51:46 -0400 Subject: [PATCH 07/11] Typing fix and removed the asset downloading --- .github/workflows/test.yml | 4 +- judo/utils/assets.py | 84 -------------------------------------- 2 files changed, 2 insertions(+), 86 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a06ff2ec..67ee12c4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -77,8 +77,8 @@ jobs: run: | python - <<'PY' import os - from judo.utils.assets import download_and_extract_meshes - download_and_extract_meshes(extract_root=os.environ["MODEL_DIR"]) + from judo.utils.assets import retrieve_model_from_remote + retrieve_model_from_remote("cylinder_push", force=True) PY - name: Show cache outcome diff --git a/judo/utils/assets.py b/judo/utils/assets.py index 063f079c..83310bf5 100644 --- a/judo/utils/assets.py +++ b/judo/utils/assets.py @@ -2,12 +2,9 @@ import logging import os import shutil -import time -import zipfile from dataclasses import dataclass from pathlib import Path -import requests from git import ( GitCommandError, InvalidGitRepositoryError, @@ -18,87 +15,6 @@ from judo import DESCRIPTION_CACHE_DIR -def acquire_lock(lock_path: Path, timeout: int = 60, poll_interval: float = 0.1) -> None: - """Acquire a lock by creating a lock file atomically. - - Raises TimeoutError if the lock can't be acquired in `timeout` seconds. - """ - start = time.time() - while True: - try: - # open with O_CREAT | O_EXCL to create file atomically, fail if exists - os.open(lock_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY) - return # acquired - except FileExistsError: - if time.time() - start > timeout: - raise TimeoutError(f"Timeout waiting for lock file {lock_path}") from None - time.sleep(poll_interval) - - -def release_lock(lock_path: Path) -> None: - """Remove the lock file to release the lock.""" - try: - lock_path.unlink() - except FileNotFoundError: - pass - - -def download_and_extract_meshes( - extract_root: str, - repo: str = "bdaiinstitute/judo", - asset_name: str = "meshes.zip", - tag: str | None = None, -) -> None: - """Downloads meshes.zip from the latest public GitHub release and extracts it.""" - extract_path = Path(extract_root).expanduser() - meshes_path = extract_path / "meshes" - lock_path = extract_path / ".meshes_download.lock" - - try: - acquire_lock(lock_path) # prevent race conditions resulting in multiple downloads - - # case: meshes already extracted - if meshes_path.exists(): - return - - # fetch latest release info - logging.info("Mesh assets not detected! Downloading assets now...") - if tag is None: - api_url = f"https://api.github.com/repos/{repo}/releases/latest" - else: - api_url = f"https://api.github.com/repos/{repo}/releases/tags/{tag}" - response = requests.get(api_url) - response.raise_for_status() - release_data = response.json() - - # get the download URL for meshes.zip - asset_url = None - for asset in release_data.get("assets", []): - if asset["name"] == asset_name: - asset_url = asset["browser_download_url"] - break - if asset_url is None: - raise ValueError(f"{asset_name} not found in latest release of {repo}.") - - # download and extract - zip_path = meshes_path.with_suffix(".zip") - meshes_path.mkdir(parents=True, exist_ok=True) - with requests.get(asset_url, stream=True) as r: - r.raise_for_status() - with open(zip_path, "wb") as f: - for chunk in r.iter_content(chunk_size=8192): - f.write(chunk) - - # extract the zip file - with zipfile.ZipFile(zip_path, "r") as zip_ref: - zip_ref.extractall(extract_path) - if zip_path.exists(): - zip_path.unlink() # remove the zip file after extraction - - finally: - release_lock(lock_path) - - @dataclass(frozen=True) class RepoInfo: """Information required to clone a description from a repository.""" From d960e923e3daa99ba2043131829b3b8150e7a50e Mon Sep 17 00:00:00 2001 From: Brandon Hung Date: Wed, 29 Oct 2025 17:20:25 -0400 Subject: [PATCH 08/11] moved docstring --- judo/utils/assets.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/judo/utils/assets.py b/judo/utils/assets.py index 3d96b78a..f336f8f7 100644 --- a/judo/utils/assets.py +++ b/judo/utils/assets.py @@ -95,8 +95,7 @@ def add_known_description(description_name: str, description_info: RepoInfo) -> def get_description(description_info: RepoInfo, force: bool = False) -> Repo: - """ - Clone a description from the provided information and return the path to the cloned repository. + """Clone a description from the provided information and return the path to the cloned repository. Args: description_info: The information about the description to clone. From 2df24bd5e5540845d758f8c4b46436adcda90d8d Mon Sep 17 00:00:00 2001 From: Brandon Hung Date: Wed, 29 Oct 2025 17:22:58 -0400 Subject: [PATCH 09/11] try to fix the workflow --- .github/workflows/test.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 67ee12c4..5433117f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -66,7 +66,6 @@ jobs: id: meshcache uses: actions/cache@v4 with: - path: ${{ steps.model.outputs.model_dir }} key: ${{ runner.os }}-${{ matrix.os }}-${{ matrix.python-version }}-judo_meshes - name: Download meshes (cache miss) From d058119d1cb73a49b969f49750ff9727e199bc34 Mon Sep 17 00:00:00 2001 From: Brandon Hung Date: Wed, 29 Oct 2025 17:28:46 -0400 Subject: [PATCH 10/11] Testing --- .github/workflows/test.yml | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5433117f..67759c4b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -62,29 +62,16 @@ jobs: echo "MODEL_DIR=$MODEL_DIR" >> "$GITHUB_ENV" echo "model_dir=$MODEL_DIR" >> "$GITHUB_OUTPUT" - - name: Restore mesh cache - id: meshcache - uses: actions/cache@v4 - with: - key: ${{ runner.os }}-${{ matrix.os }}-${{ matrix.python-version }}-judo_meshes - - - name: Download meshes (cache miss) - if: steps.meshcache.outputs.cache-hit != 'true' + - name: Download meshes env: MODEL_DIR: ${{ steps.model.outputs.model_dir }} GITHUB_TOKEN: ${{ github.token }} run: | python - <<'PY' import os - from judo.utils.assets import retrieve_model_from_remote - retrieve_model_from_remote("cylinder_push", force=True) + from judo.utils.assets import download_and_extract_meshes + download_and_extract_meshes(extract_root=os.environ["MODEL_DIR"]) PY - - name: Show cache outcome - run: | - echo "::notice::mesh cache hit = ${{ steps.meshcache.outputs.cache-hit }}" - echo "::notice::MODEL_DIR = ${{ steps.model.outputs.model_dir }}" - ls -R "${{ steps.model.outputs.model_dir }}" | head - - name: Test with pytest run: pytest -rsx From bdf5f7e96abe188da5a7f07d6a284ed677254f3f Mon Sep 17 00:00:00 2001 From: Brandon Hung Date: Wed, 29 Oct 2025 17:30:48 -0400 Subject: [PATCH 11/11] Testing again --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 67759c4b..8641f9ef 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -69,8 +69,8 @@ jobs: run: | python - <<'PY' import os - from judo.utils.assets import download_and_extract_meshes - download_and_extract_meshes(extract_root=os.environ["MODEL_DIR"]) + from judo.utils.assets import retrieve_model_from_remote + retrieve_model_from_remote("cylinder_push", force=True) PY - name: Test with pytest