From 1abd8ed8d4427fe7dcf1286afcbe8a1d7fb39e5f Mon Sep 17 00:00:00 2001 From: "Rohan P. Singh" Date: Sat, 2 May 2026 16:03:35 -0400 Subject: [PATCH 1/2] Add H1 mimic env for mocap reference tracking --- .gitignore | 3 + envs/__init__.py | 4 +- envs/common/utils.py | 28 ++++ envs/h1/__init__.py | 3 +- envs/h1/configs/mimic.yaml | 72 +++++++++ envs/h1/gen_xml.py | 120 ++++++++++++++ envs/h1/h1_mimic.py | 318 +++++++++++++++++++++++++++++++++++++ envs/h1/mimic_reference.py | 244 ++++++++++++++++++++++++++++ models/terrains/hfield.png | Bin 0 -> 4676 bytes pyproject.toml | 1 + run_experiment.py | 2 + tasks/mimic_task.py | 303 +++++++++++++++++++++++++++++++++++ uv.lock | 256 +++++++++++++++++++++++++++-- 13 files changed, 1340 insertions(+), 14 deletions(-) create mode 100644 envs/common/utils.py create mode 100644 envs/h1/configs/mimic.yaml create mode 100644 envs/h1/h1_mimic.py create mode 100644 envs/h1/mimic_reference.py create mode 100644 models/terrains/hfield.png create mode 100644 tasks/mimic_task.py diff --git a/.gitignore b/.gitignore index 386c84a..4061d94 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ __pycache__* *.egg-info *.#~ logs* + +# Mocap reference clips +data/mocap_data/ diff --git a/envs/__init__.py b/envs/__init__.py index fb2af98..ef9c1b9 100644 --- a/envs/__init__.py +++ b/envs/__init__.py @@ -5,7 +5,7 @@ """ from envs.cartpole import CartpoleEnv -from envs.h1 import H1Env, H1WalkEnv +from envs.h1 import H1Env, H1MimicEnv, H1WalkEnv from envs.jvrc import JvrcStepEnv, JvrcWalkEnv # Registry of all available environments @@ -15,6 +15,7 @@ "jvrc_step": (JvrcStepEnv, "jvrc"), "h1": (H1Env, "h1"), "h1_walk": (H1WalkEnv, "h1"), + "h1_mimic": (H1MimicEnv, "h1"), "cartpole": (CartpoleEnv, "cartpole"), } @@ -23,6 +24,7 @@ "JvrcStepEnv", "H1Env", "H1WalkEnv", + "H1MimicEnv", "CartpoleEnv", "ENVIRONMENTS", ] diff --git a/envs/common/utils.py b/envs/common/utils.py new file mode 100644 index 0000000..f53ac33 --- /dev/null +++ b/envs/common/utils.py @@ -0,0 +1,28 @@ +from pathlib import Path + +import pandas as pd + + +def get_project_root() -> Path: + return Path(__file__).parent.parent.parent + + +def parse_mocap_csv(filepath, usecols): + """Process a mocap CSV file exported from Motive/NatNet format. + + Returns a DataFrame with columns: + Frame, Time, RotX, RotY, RotZ, RotW, PosX, PosY, PosZ + """ + with open(filepath) as file: + lines = file.readlines() + + for i, line in enumerate(lines): + if line.strip().startswith("Frame"): + header_index = i + break + else: + raise ValueError("Could not find header row starting with 'Frame'.") + + df = pd.read_csv(filepath, skiprows=header_index, usecols=usecols) + df.columns = ["Frame", "Time", "RotX", "RotY", "RotZ", "RotW", "PosX", "PosY", "PosZ"] + return df diff --git a/envs/h1/__init__.py b/envs/h1/__init__.py index 14657d3..ead02b3 100644 --- a/envs/h1/__init__.py +++ b/envs/h1/__init__.py @@ -1,4 +1,5 @@ from envs.h1.h1_env import H1Env +from envs.h1.h1_mimic import H1MimicEnv from envs.h1.h1_walk import H1WalkEnv -__all__ = ["H1Env", "H1WalkEnv"] +__all__ = ["H1Env", "H1WalkEnv", "H1MimicEnv"] diff --git a/envs/h1/configs/mimic.yaml b/envs/h1/configs/mimic.yaml new file mode 100644 index 0000000..8adaea1 --- /dev/null +++ b/envs/h1/configs/mimic.yaml @@ -0,0 +1,72 @@ +sim_dt: 0.001 +control_dt: 0.025 +obs_history_len: 1 +action_smoothing: 0.5 + +xml_export_path: /tmp/mjcf-export + +ctrllimited: false +jointlimited: false +reduced_xml: true + +init_noise: 3 + +# Half-sitting pose for all 19 actuated joints (legs, waist, arms) +half_sitting_pose: [0, 0, -0.2, 0.6, -0.4, + 0, 0, -0.2, 0.6, -0.4, + 0, + 0, 0, 0, 0, + 0, 0, 0, 0] + +pdgains: + "left_hip_yaw": [100, 10] + "left_hip_roll": [100, 10] + "left_hip_pitch": [100, 10] + "left_knee": [100, 10] + "left_ankle": [20, 4] + "right_hip_yaw": [100, 10] + "right_hip_roll": [100, 10] + "right_hip_pitch": [100, 10] + "right_knee": [100, 10] + "right_ankle": [20, 4] + "torso": [40, 4] + "left_shoulder_pitch": [20, 2] + "left_shoulder_roll": [20, 2] + "left_shoulder_yaw": [20, 2] + "left_elbow": [20, 2] + "right_shoulder_pitch": [20, 2] + "right_shoulder_roll": [20, 2] + "right_shoulder_yaw": [20, 2] + "right_elbow": [20, 2] + +# Mocap clip selection +mimic: + # Path is resolved relative to the project root. + motion_pkl: data/mocap_data/rohan/ImitationBox_01/reference_motion.pkl + object_csv: data/mocap_data/rohan/ImitationBox_01/object_motion.csv + mode: RETURN_TO_ORIGIN # one of BOX_PICKUP, BOX_DROPOFF, RETURN_TO_ORIGIN + +uneven_terrain: + enable: true + +observation_noise: + enabled: true + multiplier: 1.0 + type: "uniform" + scales: + root_orient: 0.05 + root_ang_vel: 0.05 + motor_pos: 0.02 + motor_vel: 0.05 + motor_tau: 5.0 + +perturbation: + enable: true + bodies: ["pelvis", "torso_link"] + force_magnitude: 10 + torque_magnitude: 2 + interval: 5 + +dynamics_randomization: + enable: true + interval: 0.5 diff --git a/envs/h1/gen_xml.py b/envs/h1/gen_xml.py index 46edc85..f2eeaad 100644 --- a/envs/h1/gen_xml.py +++ b/envs/h1/gen_xml.py @@ -5,6 +5,7 @@ import models H1_DESCRIPTION_PATH = os.path.join(os.path.dirname(models.__file__), "mujoco_menagerie/unitree_h1/scene.xml") +TERRAINS_PATH = os.path.join(os.path.dirname(models.__file__), "terrains/hfield.png") LEG_JOINTS = [ "left_hip_yaw", @@ -47,6 +48,103 @@ def create_rangefinder_array(mjcf_model, num_rows=4, num_cols=4, spacing=0.4): return mjcf_model +def create_hfield(mjcf_model): + mjcf_model.asset.add( + "hfield", + name="hf1", + size="2.5 2.5 0.08 0.01", + file=TERRAINS_PATH, + ) + mjcf_model.worldbody.add("body", name="hfield", pos=[0, 0, -0.1]) + mjcf_model.find("body", "hfield").add( + "geom", + name="hfield", + type="hfield", + dclass="collision", + condim="3", + conaffinity="15", + hfield="hf1", + friction=".8 .1 .1", + ) + return mjcf_model + + +def create_mocap_bodies(mjcf_model, body_names): + """Add mocap-target bodies for visualizing the reference motion. + + Each target is a single sphere marker (not a geometry-faithful clone of + the source body). Geometry-faithful cloning is brittle when source geoms + inherit type/size from default classes (e.g. the H1 menagerie ankle). + """ + for bn in body_names: + b = mjcf_model.find("body", bn) + if b is None: + continue + attr = b.get_attributes() + attr["name"] = "mocap_" + b.name + attr["mocap"] = "true" + mocap_b = mjcf_model.worldbody.add("body") + mocap_b.set_attributes(**attr) + mocap_b.add("inertial", pos="0 0 0", mass="0") + mocap_b.add("geom", dclass="mocap", type="sphere", size="0.04") + return mjcf_model + + +def add_movable_box(mjcf_model, name): + body = mjcf_model.worldbody.add("body", name=name, pos=[1.5, -0.8, 0.81], euler=[0, 0, 1.57]) + body.add("inertial", pos="0 0 0", mass="1.36", diaginertia="0.01655043 0.01566667 0.00995043") + body.add("joint", type="free") + for s in ["visual", "collision"]: + geom0 = body.add("geom", name=name + "-geom0-" + s, size=[0.20, 0.1, 0.01], pos=[0, 0, 0.1]) + geom1 = body.add("geom", name=name + "-geom1-" + s, size=[0.18, 0.1, 0.1], pos=[0, 0, 0]) + for geom in [geom0, geom1]: + geom.dclass = mjcf_model.default.default["h1"].default[s] + geom.type = "box" + geom.rgba = [1, 0, 0, 1] + body.add("site", name="box", size="0.01", pos=[0, 0, 0]) + + # Two fixed tables for pickup / dropoff + body = mjcf_model.worldbody.add("body", name="table1", pos=[1.5, -0.8, 0.65]) + for s in ["visual", "collision"]: + body.add( + "geom", + name="table1-" + s, + dclass=s, + size="0.25 0.25 0.05", + type="box", + pos=[0, 0, 0], + rgba="0.18 0. 0.30 1", + density="0.001", + ) + body.add("site", name="table1", size="0.01", pos=[0, 0, 0.05]) + + body = mjcf_model.worldbody.add("body", name="table2", pos=[0.47, -2.7, 0.65]) + for s in ["visual", "collision"]: + body.add( + "geom", + name="table2-" + s, + dclass=s, + size="0.25 0.25 0.05", + type="box", + pos=[0, 0, 0], + rgba="0.18 0. 0.30 1", + density="0.001", + ) + body.add("site", name="table2", size="0.01", pos=[0, 0, 0.05]) + return mjcf_model + + +def add_hand_sites(mjcf_model): + """Add hand sites at the end-effector of each elbow link. + + The H1 menagerie model has no hand bodies; we approximate hand position + with sites offset from the elbow link, mirroring the internal mocap rig. + """ + mjcf_model.find("body", "left_elbow_link").add("site", name="left_hand", pos="0.2605 0 -0.0185", size="0.01") + mjcf_model.find("body", "right_elbow_link").add("site", name="right_hand", pos="0.2605 0 -0.0185", size="0.01") + return mjcf_model + + def remove_joints_and_actuators(mjcf_model, config): # remove joints for limb in config["unused_joints"]: @@ -98,6 +196,28 @@ def builder(export_path, config): # set name of freejoint mjcf_model.find("body", "pelvis").freejoint.name = "root" + # add hand sites (for tasks that need end-effector targets) + if config.get("hand_sites", False): + mjcf_model = add_hand_sites(mjcf_model) + + # add a movable box and surrounding tables + if "movable_box" in config and config["movable_box"]: + mjcf_model = add_movable_box(mjcf_model, config["movable_box"]) + + # add mocap bodies (visualization of reference motion) + if config.get("mocap_bodies"): + mocap_default = mjcf_model.default.default["h1"].add("default", dclass="mocap") + mocap_default.geom.contype = "0" + mocap_default.geom.conaffinity = "0" + mocap_default.geom.group = "3" + mocap_default.geom.rgba = "0 1 0 0.5" + mocap_default.geom.density = "0" + mjcf_model = create_mocap_bodies(mjcf_model, config["mocap_bodies"]) + + # add an uneven heightfield underneath the robot + if config.get("hfield", False): + mjcf_model = create_hfield(mjcf_model) + # add rangefinder if "rangefinder" in config: if config["rangefinder"]: diff --git a/envs/h1/h1_mimic.py b/envs/h1/h1_mimic.py new file mode 100644 index 0000000..c03219a --- /dev/null +++ b/envs/h1/h1_mimic.py @@ -0,0 +1,318 @@ +"""H1 humanoid mocap-imitation environment. + +Subclasses H1BaseEnv but overrides: + * `_build_xml` — adds movable box, mocap-vis bodies, optional hfield + * `_setup_robot` — actuates all 19 joints (legs + waist + arms) and adds box DOFs + * `_setup_spaces` — 19-D action, observation = robot state (62) + clocks (4) + box rel pose (9) + * `step`/`reset_model` — phase-driven episode + reference state initialization + * `render`/`viewer_setup` — mocap-body visualization of the reference clip +""" + +from __future__ import annotations + +import collections +import contextlib +import os + +import numpy as np +import transforms3d as tf3 + +from envs.common import robot_interface +from envs.common.utils import get_project_root +from robots.robot_base import RobotBase +from tasks.mimic_task import MimicTask + +from .gen_xml import ARM_JOINTS, LEG_JOINTS, WAIST_JOINTS, builder +from .h1_base import H1BaseEnv +from .mimic_reference import Mode, load_reference + + +class H1MimicEnv(H1BaseEnv): + """H1 humanoid imitating a mocap clip while interacting with a box.""" + + # Bodies the env visualizes against the reference clip. + _MOCAP_BODIES = [ + "box", + "pelvis", + "torso_link", + "left_elbow_link", + "right_elbow_link", + "left_knee_link", + "left_ankle_link", + "right_knee_link", + "right_ankle_link", + ] + + def _get_default_config_path(self) -> str: + return os.path.join(os.path.dirname(os.path.realpath(__file__)), "configs/mimic.yaml") + + def _build_xml(self) -> str: + export_dir = self._get_xml_export_dir("h1_mimic") + path_to_xml = os.path.join(export_dir, "h1.xml") + if not os.path.exists(path_to_xml): + builder( + export_dir, + config={ + "unused_joints": [], + "rangefinder": False, + "raisedplatform": False, + "ctrllimited": self.cfg.ctrllimited, + "jointlimited": self.cfg.jointlimited, + "minimal": self.cfg.reduced_xml, + "movable_box": "box", + "mocap_bodies": self._MOCAP_BODIES, + "hand_sites": True, + "hfield": getattr(self.cfg, "uneven_terrain", None) is not None and self.cfg.uneven_terrain.enable, + }, + ) + return path_to_xml + + def _setup_robot(self) -> None: + control_dt = self.cfg.control_dt + + # Actual robot is ~7kg heavier + self.model.body("pelvis").mass = 8.89 + self.model.body("torso_link").mass = 21.289 + + # Actuate all 19 joints + self.leg_names = LEG_JOINTS + self.waist_names = WAIST_JOINTS + self.arm_names = ARM_JOINTS + self.actuators = LEG_JOINTS + WAIST_JOINTS + ARM_JOINTS + + gains_dict = self.cfg.pdgains.to_dict() + kp, kd = zip(*[gains_dict[jn] for jn in self.actuators], strict=True) + pdgains = np.array([kp, kd]) + + # Half-sitting pose (19 entries) + self.half_sitting_pose = list(self.cfg.half_sitting_pose) + assert len(self.half_sitting_pose) == len(self.actuators), ( + f"half_sitting_pose has {len(self.half_sitting_pose)} entries but " + f"{len(self.actuators)} actuated joints are configured" + ) + + # Nominal pose: root (7) + joints (19) + box (7) + base_position = [0, 0, 0.98] + base_orientation = [1, 0, 0, 0] + box_position = [1.5, -0.8, 0.81] + box_orientation = list(tf3.euler.euler2quat(0, 0, 1.57)) + self.nominal_pose = base_position + base_orientation + self.half_sitting_pose + box_position + box_orientation + + self.interface = robot_interface.RobotInterface(self.model, self.data, self.RFOOT_BODY, self.LFOOT_BODY, None) + + # Load mocap clip + cfg_mimic = self.cfg.mimic + motion_pkl = get_project_root() / cfg_mimic.motion_pkl + object_csv = get_project_root() / cfg_mimic.object_csv + mode = Mode[cfg_mimic.mode] + self.reference = load_reference(motion_pkl, object_csv, mode) + + # Setup task + self.task = MimicTask( + client=self.interface, + dt=control_dt, + root_body="pelvis", + rhand_body="right_elbow_link", + lhand_body="left_elbow_link", + rfoot_body=self.RFOOT_BODY, + lfoot_body=self.LFOOT_BODY, + object_body="box", + track_joint_names=self.actuators, + track_body_names=[ + "torso_link", + "left_elbow_link", + "right_elbow_link", + "left_ankle_link", + "right_ankle_link", + ], + reference=self.reference, + ) + # Initial periods so calc_reward() is callable before reset + self.task.reset(iter_count=0) + + self.robot = RobotBase(pdgains, control_dt, self.interface, self.task) + + def _setup_spaces(self) -> None: + action_space_size = len(self.actuators) + self.action_space = np.zeros(action_space_size) + self.prev_prediction = np.zeros(action_space_size) + + self.base_obs_len = self._get_robot_state_len() + self._get_num_external_obs() + self.observation_space = np.zeros(self.base_obs_len * self.history_len) + + self._setup_obs_normalization() + + def _get_robot_state_len(self) -> int: + # root_r(1) + root_p(1) + root_ang_vel(3) + motor_pos(19) + motor_vel(19) + motor_tau(19) + return 1 + 1 + 3 + 3 * len(self.actuators) + + def _get_num_external_obs(self) -> int: + # mimic clock (2) + gait clock (2) + rel obj pos (3) + rel obj rot top-2-rows (6) + return 2 + 2 + 3 + 6 + + def _setup_obs_normalization(self) -> None: + n_jnt = len(self.actuators) + obs_mean = np.concatenate( + ( + np.zeros(5), + np.array(self.half_sitting_pose), + np.zeros(n_jnt), + np.zeros(n_jnt), + np.zeros(2), + np.zeros(2), + np.zeros(9), + ) + ) + obs_std = np.concatenate( + ( + np.array([0.2, 0.2, 1, 1, 1]), + 0.5 * np.ones(n_jnt), + 4 * np.ones(n_jnt), + 100 * np.ones(n_jnt), + np.ones(2), + np.ones(2), + np.ones(9), + ) + ) + self.obs_mean = np.tile(obs_mean, self.history_len) + self.obs_std = np.tile(obs_std, self.history_len) + + def _get_external_state(self) -> np.ndarray: + # Clocks + phi_mimic = 2 * np.pi * self.task.mimic_phase / self.task.mimic_period + phi_gait = 2 * np.pi * self.task.gait_phase / self.task.gait_period + clocks = np.array([np.sin(phi_mimic), np.cos(phi_mimic), np.sin(phi_gait), np.cos(phi_gait)]) + + # Relative pose of the observation target (box for pickup, table2 otherwise) + cfg_mimic = self.cfg.mimic + obs_obj_name = "box" if cfg_mimic.mode == "BOX_PICKUP" else "table2" + root_pose = self.interface.get_object_affine_by_name("pelvis", "OBJ_BODY") + obj_pose = self.interface.get_object_affine_by_name(obs_obj_name, "OBJ_SITE") + rel = np.linalg.inv(root_pose).dot(obj_pose) + rel_obj_pos = rel[:3, 3].flatten() + rel_obj_rot = rel[:2, :3].flatten() + + ext = np.concatenate([clocks, rel_obj_pos, rel_obj_rot]) + + # Optional observation noise on relative object pose + if hasattr(self.cfg, "observation_noise") and self.cfg.observation_noise.enabled: + level = self.cfg.observation_noise.multiplier + noise_type = self.cfg.observation_noise.type + noise = ( + (lambda x, n: np.random.uniform(-x, x, n)) + if noise_type == "uniform" + else (lambda x, n: np.random.randn(n) * x) + ) + ext[4:7] += noise(0.01 * level, 3) + ext[7:13] += noise(0.001 * level, 6) + return ext + + def step(self, action: np.ndarray): + targets = self._action_smoothing * action + (1 - self._action_smoothing) * self.prev_prediction + offsets = np.asarray(self._get_action_offsets()) + + rewards, done = self.robot.step(targets, offsets) + + self.task.mimic_phase += 1 + self.task.gait_phase += 1 + if self.task.mimic_phase >= self.task.mimic_period: + obs = self.reset_model(phase=0) + return obs, sum(rewards.values()), done, rewards + if self.task.gait_phase >= self.task.gait_period: + self.task.gait_phase = 0 + + obs = self.get_obs() + self.prev_prediction = action + + # Domain randomization (matching the BaseHumanoidEnv schedule) + if self.dynrand_interval > 0 and np.random.randint(self.dynrand_interval) == 0: + self._randomize_dynamics() + if self.perturb_interval > 0 and np.random.randint(self.perturb_interval) == 0: + self._apply_perturbation() + + return obs, sum(rewards.values()), done, rewards + + def reset_model(self, phase: int | None = None) -> np.ndarray: + # Heightfield height jitter (only applied if hfield exists) + if getattr(self.cfg, "uneven_terrain", None) is not None and self.cfg.uneven_terrain.enable: + with contextlib.suppress(KeyError): + self.model.body("hfield").pos[2] = np.random.uniform(-0.05, -0.08) + + self.task.reset(iter_count=self.robot.iteration_count) + if phase is not None: + self.task.mimic_phase = phase + + idx = self._reference_idx() + self._put_robot_in_scene(idx) + + self.prev_prediction = np.zeros_like(self.prev_prediction) + self.observation_history = collections.deque(maxlen=self.history_len) + return self.get_obs() + + def _reference_idx(self) -> int: + n = self.reference["root_pose"].shape[0] + return int((self.task.mimic_phase / self.task.mimic_period) * n) + + def _put_robot_in_scene(self, idx: int) -> None: + init_qpos = np.array(self.nominal_pose, dtype=float) + init_qvel = np.zeros(self.interface.nv()) + + # Robot joints from reference clip + for jn in self.actuators: + qp = self.interface.get_jnt_qposadr_by_name(jn)[0] + qv = self.interface.get_jnt_qveladr_by_name(jn)[0] + init_qpos[qp] = self.reference["joint_position"][jn][idx] + init_qvel[qv] = self.reference["joint_velocity"][jn][idx] + + # Robot root from reference (mocap quat is [x, y, z, w]; mujoco wants [w, x, y, z]) + ref_root_pose = self.reference["root_pose"][idx] + root_jnt_adr = self.model.body("pelvis").jntadr[0] + qpos_adr = self.model.joint(root_jnt_adr).qposadr[0] + init_qpos[qpos_adr : qpos_adr + 7] = ref_root_pose[[0, 1, 2, 6, 3, 4, 5]] + + # Object root from reference + ref_obj_pose = self.reference["object_pose"][idx] + box_jnt_adr = self.model.body("box").jntadr[0] + box_qpos_adr = self.model.joint(box_jnt_adr).qposadr[0] + init_qpos[box_qpos_adr : box_qpos_adr + 7] = ref_obj_pose[[0, 1, 2, 6, 3, 4, 5]] + + self.set_state(init_qpos, init_qvel) + for _ in range(3): + self.interface.step() + + def render(self): + if self.viewer is None: + super().render() + self.viewer.opt.geomgroup[2] = 1 # show collision + self.viewer.opt.geomgroup[3] = 1 # show mocap + return + + idx = self._reference_idx() + ref_root = self.reference["root_pose"][idx][[0, 1, 2, 6, 3, 4, 5]] + ref_root_mat = tf3.affines.compose(ref_root[:3], tf3.quaternions.quat2mat(ref_root[3:]), np.ones(3)) + bodies: dict[str, tuple[np.ndarray, np.ndarray]] = {} + for b, v in self.reference["relative_link_pose"].items(): + rel_pose = v[idx][[0, 1, 2, 6, 3, 4, 5]] + rel_pose_mat = tf3.affines.compose(rel_pose[:3], tf3.quaternions.quat2mat(rel_pose[3:]), np.ones(3)) + global_pose_mat = ref_root_mat.dot(rel_pose_mat) + bodies[b] = (global_pose_mat[:3, 3], tf3.quaternions.mat2quat(global_pose_mat[:3, :3])) + bodies["pelvis"] = (ref_root[:3], ref_root[3:]) + obj_root = self.reference["object_pose"][idx][[0, 1, 2, 6, 3, 4, 5]] + bodies["box"] = (obj_root[:3], obj_root[3:]) + + for i in range(self.model.nbody): + body = self.model.body(i) + mocapid = body.mocapid[0] if hasattr(body.mocapid, "__len__") else body.mocapid + if mocapid != -1: + name = body.name[len("mocap_") :] + if name in bodies: + p, q = bodies[name] + self.data.mocap_pos[mocapid] = p + self.data.mocap_quat[mocapid] = q + super().render() + + def viewer_setup(self): + super().viewer_setup() + self.viewer.cam.distance = 8 + self.viewer.cam.lookat[2] = 1.5 + self.viewer.cam.lookat[0] = 1.0 diff --git a/envs/h1/mimic_reference.py b/envs/h1/mimic_reference.py new file mode 100644 index 0000000..b198cc9 --- /dev/null +++ b/envs/h1/mimic_reference.py @@ -0,0 +1,244 @@ +"""Reference motion loader for H1 mimic environment. + +Loads a pickled mocap clip plus an object-motion CSV and applies the same set +of fixes used by the internal mocap rig (manual fix-ups for bad mocap frames, +synthesized return-to-origin segment, derived hand poses, and force schedule). +""" + +from __future__ import annotations + +import pickle +from enum import Enum, auto +from pathlib import Path + +import numpy as np +import transforms3d as tf3 +from scipy.spatial.transform import Rotation, Slerp + +from envs.common.utils import parse_mocap_csv + +# Joint name suffix used by the mocap dictionary; not present in the menagerie XML. +_MOCAP_JOINT_SUFFIX = "_joint" + + +class Mode(Enum): + BOX_PICKUP = auto() + BOX_DROPOFF = auto() + RETURN_TO_ORIGIN = auto() + + def get_bounds(self): + if self.name == "BOX_PICKUP": + return [1000, 2400] + if self.name == "BOX_DROPOFF": + return [2400, 3700] + if self.name == "RETURN_TO_ORIGIN": + return [3700, 5500] + raise ValueError(self.name) + + +def _strip_joint_suffix(d: dict) -> dict: + """Rewrite mocap dict keys to drop the `_joint` suffix to match XML names.""" + return {(k[: -len(_MOCAP_JOINT_SUFFIX)] if k.endswith(_MOCAP_JOINT_SUFFIX) else k): v for k, v in d.items()} + + +def load_reference(motion_pkl: str | Path, object_csv: str | Path, mode: Mode) -> dict: + """Load and preprocess a mocap clip + object motion CSV. + + Returns a dict keyed by: + - "root_pose" : (N, 7) [x, y, z, qx, qy, qz, qw] + - "joint_position" : dict[xml_joint_name, (N,)] + - "joint_velocity" : dict[xml_joint_name, (N,)] + - "relative_link_pose": dict[body_name, (N, 7)] + - "object_pose" : (N, 7) [x, y, z, qx, qy, qz, qw] + - "force_lhand", "force_rhand", "force_box", "force_lfoot", "force_rfoot": (N,) + - "distance_left_hand_to_target", "distance_right_hand_to_target": (N,) + - "dt": float + """ + with open(motion_pkl, "rb") as f: + pkl_data = pickle.load(f) + + # Manually fix arm/return frames (caused by bad mocap data) + jnts = [ + "_shoulder_pitch_joint", + "_shoulder_roll_joint", + "_shoulder_yaw_joint", + "_elbow_joint", + ] + for i, j in zip(range(3300, 3700), range(1950, 2350), strict=False): + q1 = pkl_data["root_pose"][i][3:] + q2 = pkl_data["root_pose"][j][3:] + new_q = Rotation.from_euler( + "xyz", + [ + *Rotation.from_quat(q2).as_euler("xyz")[:2], + Rotation.from_quat(q1).as_euler("xyz")[2], + ], + ).as_quat() + pkl_data["root_pose"][i][3:] = new_q + pkl_data["root_pose"][i][:2] = pkl_data["root_pose"][3300][:2] + pkl_data["root_pose"][i][2] = pkl_data["root_pose"][j][2] + for attr in ["joint_position", "joint_velocity", "relative_link_pose"]: + for key in pkl_data[attr]: + if attr == "relative_link_pose" and key.endswith("_elbow_link"): + continue + if attr == "joint_position": + if key in ["right" + jn for jn in jnts]: + continue + if key in ["left" + jn for jn in jnts]: + continue + pkl_data[attr][key][i] = pkl_data[attr][key][j] + + # Mirror left elbow pose into right elbow for the same range + for i in range(3300, 3700): + left_pose = pkl_data["relative_link_pose"]["left_elbow_link"][i] + p, q = left_pose[:3], left_pose[3:] + pkl_data["relative_link_pose"]["right_elbow_link"][i][0] = p[0] + pkl_data["relative_link_pose"]["right_elbow_link"][i][1] = -p[1] + pkl_data["relative_link_pose"]["right_elbow_link"][i][2] = p[2] + rot = Rotation.from_quat(q) + R_reflect = np.diag([1, -1, 1]) + R_mirrored = R_reflect @ rot.as_matrix() @ R_reflect + pkl_data["relative_link_pose"]["right_elbow_link"][i][3:] = Rotation.from_matrix(R_mirrored).as_quat() + for jn in jnts: + pkl_data["joint_position"]["right" + jn][i] = pkl_data["joint_position"]["left" + jn][i] + pkl_data["joint_velocity"]["right" + jn][i] = pkl_data["joint_velocity"]["left" + jn][i] + if jn in ("_shoulder_roll_joint", "_shoulder_yaw_joint"): + pkl_data["joint_position"]["right" + jn][i] = -pkl_data["joint_position"]["left" + jn][i] + pkl_data["joint_velocity"]["right" + jn][i] = -pkl_data["joint_velocity"]["left" + jn][i] + + # Synthesize a 'return to origin' segment from frame 3700 to 5500 + start_idx, end_idx = 3700, 5000 + s = pkl_data["root_pose"][start_idx, :2] + e = pkl_data["root_pose"][0, :2] + pkl_data["root_pose"] = pkl_data["root_pose"][:start_idx] + slerp = Slerp( + [0, 1], + Rotation.from_quat( + [ + pkl_data["root_pose"][start_idx - 1, 3:], + pkl_data["root_pose"][1000, 3:], + ] + ), + ) + for i in range(start_idx, end_idx): + root_xy = s + (e - s) * (i - start_idx) / (end_idx - start_idx) + root_z = [1.05] + root_q = pkl_data["root_pose"][start_idx - 1, 3:] + if i > 4700: + j = (i - 4700) / (end_idx - 4700) + root_q = slerp(j).as_quat() + v = np.concatenate((root_xy, root_z, root_q)) + pkl_data["root_pose"] = np.vstack([pkl_data["root_pose"], v]) + pkl_data["root_pose"] = np.vstack([pkl_data["root_pose"], np.tile(pkl_data["root_pose"][-1], (500, 1))]) + ref_idx = 1000 + for f in ["joint_position", "joint_velocity", "relative_link_pose"]: + for key in pkl_data[f]: + pkl_data[f][key] = pkl_data[f][key][:start_idx] + len_diff = len(pkl_data["root_pose"]) - len(pkl_data[f][key]) + if f == "relative_link_pose": + reps = np.tile(pkl_data[f][key][ref_idx], (len_diff, 1)) + else: + reps = np.tile(pkl_data[f][key][ref_idx], len_diff) + pkl_data[f][key] = np.concatenate((pkl_data[f][key], reps)) + + # Object motion (CSV in mm; rotated -90 deg around z to match world frame) + df = parse_mocap_csv(object_csv, range(9)) + pose_array = df[["PosX", "PosY", "PosZ", "RotX", "RotY", "RotZ", "RotW"]].to_numpy() + pose_array = np.vstack([pose_array, np.tile(pose_array[-1], (len(pkl_data["root_pose"]) - len(pose_array), 1))]) + rot = Rotation.from_euler("z", -90, degrees=True) + rotated_positions = rot.apply(pose_array[:, :3]) + rotated_quaternions = ( + rot * Rotation.from_quat(pose_array[:, 3:]) * Rotation.from_euler("z", 90, degrees=True) + ).as_quat() + pkl_data["object_pose"] = np.zeros_like(pose_array) + pkl_data["object_pose"][:, :3] = rotated_positions / 1000 + pkl_data["object_pose"][:, 3:] = rotated_quaternions + pkl_data["object_pose"][:, 0] -= 0.1 + pkl_data["object_pose"][:, 2] -= 0.1 + + # Lock the object to the midpoint of both hands during carry + hands_offset = tf3.affines.compose([0.2605, 0, -0.0185], np.eye(3), np.ones(3)) + for idx in range(2100, 3400): + ref_root = pkl_data["root_pose"][idx][[0, 1, 2, 6, 3, 4, 5]] + ref_root_mat = tf3.affines.compose(ref_root[:3], tf3.quaternions.quat2mat(ref_root[3:]), np.ones(3)) + rel_pose = pkl_data["relative_link_pose"]["right_elbow_link"][idx][[0, 1, 2, 6, 3, 4, 5]] + rel_pose_mat = tf3.affines.compose(rel_pose[:3], tf3.quaternions.quat2mat(rel_pose[3:]), np.ones(3)) + p1 = ref_root_mat.dot(rel_pose_mat.dot(hands_offset))[:3, 3] + rel_pose = pkl_data["relative_link_pose"]["left_elbow_link"][idx][[0, 1, 2, 6, 3, 4, 5]] + rel_pose_mat = tf3.affines.compose(rel_pose[:3], tf3.quaternions.quat2mat(rel_pose[3:]), np.ones(3)) + p2 = ref_root_mat.dot(rel_pose_mat.dot(hands_offset))[:3, 3] + pkl_data["object_pose"][idx, :3] = (p1 + p2) / 2 + pkl_data["object_pose"][3400:] = pkl_data["object_pose"][3400 - 1] + + # Synthetic force schedule + n = len(pkl_data["root_pose"]) + pkl_data["force_rfoot"] = np.zeros(n) + pkl_data["force_lfoot"] = np.zeros(n) + pkl_data["force_rhand"] = np.zeros(n) + pkl_data["force_lhand"] = np.zeros(n) + pkl_data["force_box"] = np.ones(n) + for frc in ["lhand", "rhand"]: + pkl_data["force_" + frc][2100:3400] = 1 + pkl_data["force_box"][2100:3400] = 0 + + # Synthesize hand-link poses from elbow-link poses + a fixed offset + hand_in_elbow = np.array([0.2605, 0, -0.0185, 0, 0, 0, 1]) + for idx in range(n): + for s_side in ["right", "left"]: + elbow_in_body = pkl_data["relative_link_pose"][s_side + "_elbow_link"][idx] + R_elbow = Rotation.from_quat(elbow_in_body[3:]).as_matrix() + hand_in_body_t = R_elbow.dot(hand_in_elbow[:3]) + elbow_in_body[:3] + pkl_data["relative_link_pose"][s_side + "_hand_link"][idx] = np.concatenate( + (hand_in_body_t, elbow_in_body[3:]) + ) + + # Hand-to-target distances (target expressed in box frame) + pkl_data["distance_right_hand_to_target"] = np.zeros(n) + pkl_data["distance_left_hand_to_target"] = np.zeros(n) + targets = { + "right_hand": np.array([0.2, 0, 0]), + "left_hand": np.array([-0.2, 0, 0]), + } + for idx in range(n): + R_root = Rotation.from_quat(pkl_data["root_pose"][idx][3:]) + t_root = pkl_data["root_pose"][idx][:3] + R_obj = Rotation.from_quat(pkl_data["object_pose"][idx][3:]) + t_obj = pkl_data["object_pose"][idx][:3] + for s_side in ["right", "left"]: + t_hand_local = pkl_data["relative_link_pose"][s_side + "_hand_link"][idx][:3] + hand_in_world = R_root.apply(t_hand_local) + t_root + target_in_world = R_obj.apply(targets[s_side + "_hand"]) + t_obj + pkl_data["distance_" + s_side + "_hand_to_target"][idx] = np.linalg.norm(hand_in_world - target_in_world) + pkl_data["distance_right_hand_to_target"][2000:3400] = 0 + pkl_data["distance_left_hand_to_target"][2000:3400] = 0 + + # Zero out roll/pitch on feet rotations (keep yaw only) + for bn in ["right_ankle_link", "left_ankle_link"]: + ref_q = Rotation.from_quat(pkl_data["relative_link_pose"][bn][:, 3:]) + root_q = Rotation.from_quat(pkl_data["root_pose"][: len(ref_q.as_quat()), 3:]) + rotw = (root_q * ref_q).as_euler("xyz") + rotw[:, 0] = 0 + rotw[:, 1] = 0 + rot_new = Rotation.from_euler("xyz", rotw) + pkl_data["relative_link_pose"][bn][:, 3:] = (root_q.inv() * rot_new).as_quat() + + # Lift the root height slightly to avoid floor penetration + pkl_data["root_pose"][:, 2] += 0.05 + + # Slice out the requested mode segment + start_idx, end_idx = mode.get_bounds() + for attr in pkl_data: + if attr == "dt": + continue + v = pkl_data[attr] + if isinstance(v, np.ndarray): + pkl_data[attr] = v[start_idx:end_idx] + elif isinstance(v, dict): + for key in v: + v[key] = v[key][start_idx:end_idx] + + # Rewrite joint dict keys to drop the "_joint" suffix to match XML + pkl_data["joint_position"] = _strip_joint_suffix(pkl_data["joint_position"]) + pkl_data["joint_velocity"] = _strip_joint_suffix(pkl_data["joint_velocity"]) + + return pkl_data diff --git a/models/terrains/hfield.png b/models/terrains/hfield.png new file mode 100644 index 0000000000000000000000000000000000000000..b1dff851a118487982b971f3b7e1c6c1c0f3e69a GIT binary patch literal 4676 zcmV-K61(k*P)UTrz84+R&y@Fl?F2Al{?ulKRkeqWSuq&Oyx=Tf?v~YMvHAf+^@Xf!C+X| zlDl@pg)7aD6c&zzo&1~0(3Y|i87SUqb?co7R|J%B-!vdYH6I9f=5syZsmTCg0Sd-p{P z89{2Xm%yGSFzE^oB@Lh7)EcU~j-R?wF&|tChx#o>8{e&^Riu72IMCS2A#vg7#mR)E$DuRATXNc{H{WzNX4=22z^#%v;;74{%(G+sctP{2?N-7QiM&zmn@D$h(S#Fw*dMc&ug*901H~Hd<<+T{jEc^op z6V2FbUkS5S*ag+jFhABv96{6IQ2874e@<)3ii?>=+3M1#RYmM~Xk%0$87>EPzH1L;B@-Ly_ zB}Q%ZQckhg8rT{>dmQ|R6U$>`S!98=XqyL=;9V20U`)-pyU2H!U}o1m?gg(RUi`=2 zQQd$t+sw-Y%4y zrryZH;VRX0RaXbW_aXPI2zpDKh6qiIVyeWjef?L(&*A*#K zcft6icqx;hQ4=m^I&y1PX8z~#BH7sso^_7gKo8>;F0AG)*;Mw=Os!RK=!x36nfkJU zef^z&D%LnAb$DKm{S<7{2Yv5Y2nsys>=F67dkV9n!ImudN*}F~b8zV?+QHwUJj&o2 zX=Aw~`du9%fl^orb1V8Vmx51(>iR6yuOvPR`*CCXgbIxEuVr#x+t-eUFhHH>*vad7 z7n{LIO6W0wDre!bbQ9kN$5K3$E?k&t!PcuD9$Ld#DC$#{gxxY)6Lt{b+PCmk!JinR+F{Ht6Se*oyj*5k;7S{wzRIakj1I#SD495%8my|5dgWHQ zhsY`9r>KdH3ImwwZC`SE06rd>7{7KoW<<}ubf!Ot7jL5QU4^*E74`A9T0J7j(_?LR zI2~o#A zQ;53yd7T7|!_D5I`|O-Fh@);g&UH*y_G9_(JBrO9WCZcZ;Zh1)g7+8F1ovDsOayU9DQ!Jd@< zF&zTxe#?`yYb2=*0+_kJ-z4zX!{YG13?Bbx*S5ur!nexFGx{c= zD$aD%r6S1XeiY|Lc87+kxaggAAxHQ{bjtqCWJSYJQ7ZiOza;6Q{Gr#b1|gTa zmQSOW;kfg!^F~sJGeyGVW+6|phGX?XUO9P-k0WPuoMF+6!2!G@>TNui51Snp=n1+W zV)LU#p=mkj>Lb&(mXG%GHK5ofu(}0*l#XU!C9Qpx7^l3^D#GgjW`tH$F+9^O^1(Pl zcwBavX{X}q#hEk%#`trf-P`a3tqufuYuw_;2-)>Dp}Q->D_8#(!e1hdz5W=^O=_Qo zcinQtxl-o_!!(OfP@b=?j~m_~OmdxksDXtB)Fw`NGRq6ppoX7?bl@7=ModK-mI`@6 zO2&>MC)Lo6tb}tApwX+N3Gg+R5eu)BHpI*l9qs?25-(d^(Gq&gm{|MDkQgX6dAi5WU7;g4RC$De5&r!Z}ukl}aSw*A8$h%~|R#=6~=OFD`?O zcgP;CZ&fu4)QS{lMe|ZrTkr3KzpE6TjvgqpIyRIy(|G5?K|ODmBp=UHUCc|3b#o!l zgB1+YNZ)W~#37Ku0Cw)vW=gcp1S})GLbk)Web;Fo}AZ zlDTSCqlXSqAiZgfJn&{j~yDmt$gx zLgVh{@z=Pp2PQ{5^6R}NsnkF7UMy(bU4)RcA^^&gw~(Za&Ejo$^A_l^@Z#3wrtdnJ zVcsvAYN_WEKaY+qUIzW=B@4kpn`GlY-V|6HZ}|C(H-wL|CerMTiBECzs3xIipO7%m zl)-^&$(-NS&uIE;0ZJ##%s70los$oT$cy8zxYz!WwA~Sisv9;7piFu$_O})av+m$V zY*U3TJs9BI?xB4MUhqn!U9&tid(^K=`4rhA!jhg6(*>cVFW|oACZlJ_3&wjjNDUXNfc%r1vd{Q!*j)BI^L53-2 zMJ{|uxt=Wca|e%5R;87{#!3DhxKmkN6Snl3>9L{7a=HDoE?C8?oNs$CAh}D~7O8{- zcD=KroJ1Ae@JaEAp1UTt^^UWX4DM##cI&3Yz9{ASUH@Kx-T6rv%Fc8wv6F3$!shz! zMvYk%PRHF(YD=>zXo=66Qv}cbs2X)3!zVx=0eq_tmTa;Ij9z;cB6ZP4+7JTx*2T!Izh{82w3n!?L8Auv{z~72XdnfyhV2R3M^#f3`cw;O725e z^?Ph@^?fV_k6+hY#=#yZ?!>4RELZh6yV& z0!lI2t|<#DFJ*eGM?0fUe4}yS8{KBgerSA*?JL!Dy$nO5=E6Kk69>Pn?6=rOuFsQi z1|fw_lNN86`frp#l8sv3gA4Cy-ZprMkLID7^&03eaKe+iEQ;Q5$f(6m_Ku1jj2K?~ zd7iW(xqkseRHbEyo5PN(+zJh-3Y!)^vFQ7ZeSAffrWIt%J5@ymGC)d4e*SYC~{H=(`7>f-kQ}5g4%nd zuRm_5)$GlAulRyyImgV!*1 zCP!4EqiPAK$amS{qCLWY250$ICgbGhv=R=Tpk`xM^f7~E_cNM9w@}`&N_@B{ z5QS{tV9FjRb>d8OE=0rb%x2Ij$Nsh=@D0gTN05#XeQnuQ+s8jnX(n>3VZ|PZ2REMj z;xIjE7FMWoeG-ClmrT*|Yv<~T-X6BlNOZ`=`AqqrvH=p742=JrNB@a7F`*R8^E?tL zg$6o>)2g%cWafD_7o)}Ux-n5pvR&FnSASL|@F>=D=5o zK|w;AFm}?82wXHz1??8|>gEEKJDIy}w#g*~hIcWnjxHBcH*>L%gaX9}i zquA6Io_OiJMHIuD8&!SPJ%$+|2*lha0cr z5hiFi#jt^$8M1<$3?0559k^A<4+>z_n@OnZ+L3mnO5|0Tw1qYU#XT!{hL~=hi}*&K z40$e2Ubryivfl$&*QrzyVZo7g%1Y69a)91UMyn)ikdNZ zX{24bt3jC#%>4zwUFTRxUe{O_{hh$%+%O0nWx9KwQ7IxS)F!=Qm|z?;!VW@esE@b7-Aj9g#-W7Fb2ki&IC7m^5KPgVt>NV z>N_K4)mER^ri-6P8gh&|+vrDr5#F`DXE{QS(E+3TVP&rvYs5h!A4=OT!Ld}=$%~^h zuTu+C3b}}w30q}j6EULyN&p=NjNl>+jkL;|D;)YaQEQvtoxOrdnRuo3LJm>UXwmUl zbX2$-&yLqLvXLzjeH-0vos4kZrHP$Y&qTA@_zNGUkN*F;tECo>lyrdr0000=2.37.0", "intel-openmp>=2025.2.1", "mujoco==3.4.0", + "pandas>=2.0", "ray==2.40.0", "tensorboard>=2.20.0", "torch==2.5.1", diff --git a/run_experiment.py b/run_experiment.py index 5ae2b4f..6763293 100644 --- a/run_experiment.py +++ b/run_experiment.py @@ -93,6 +93,8 @@ def import_env(env_name_str): from envs.h1 import H1Env as Env elif env_name_str == "h1_walk": from envs.h1 import H1WalkEnv as Env + elif env_name_str == "h1_mimic": + from envs.h1 import H1MimicEnv as Env elif env_name_str == "cartpole": from envs.cartpole import CartpoleEnv as Env else: diff --git a/tasks/mimic_task.py b/tasks/mimic_task.py new file mode 100644 index 0000000..d26635d --- /dev/null +++ b/tasks/mimic_task.py @@ -0,0 +1,303 @@ +"""Mocap-imitation task for the H1 humanoid. + +Tracks reference root pose, joint positions/velocities, body poses, and +contact forces against a preloaded mocap clip. All joint names used here +are XML-style (no `_joint` suffix); the reference loader rewrites mocap +keys accordingly. +""" + +from __future__ import annotations + +import numpy as np +import transforms3d as tf3 +from scipy.spatial.transform import Rotation + +from tasks.base_task import BaseTask + +LEG_JOINTS = [ + "left_hip_yaw", + "left_hip_roll", + "left_hip_pitch", + "left_knee", + "left_ankle", + "right_hip_yaw", + "right_hip_roll", + "right_hip_pitch", + "right_knee", + "right_ankle", +] +WAIST_JOINTS = ["torso"] +ARM_JOINTS = [ + "left_shoulder_pitch", + "left_shoulder_roll", + "left_shoulder_yaw", + "left_elbow", + "right_shoulder_pitch", + "right_shoulder_roll", + "right_shoulder_yaw", + "right_elbow", +] + + +class MimicTask(BaseTask): + """Reference-tracking task for an H1 carrying a box.""" + + def __init__( + self, + client=None, + dt: float = 0.025, + root_body: str = "pelvis", + rhand_body: str = "right_elbow_link", + lhand_body: str = "left_elbow_link", + rfoot_body: str = "right_ankle_link", + lfoot_body: str = "left_ankle_link", + object_body: str = "box", + track_joint_names: list[str] | None = None, + track_body_names: list[str] | None = None, + reference: dict | None = None, + ): + self._client = client + self._control_dt = dt + + self.reference_motion = reference or {} + + self.root_body_name = root_body + self.rhand_body_name = rhand_body + self.lhand_body_name = lhand_body + self.rfoot_body_name = rfoot_body + self.lfoot_body_name = lfoot_body + self.object_body_name = object_body + + self.track_joint_names = list(track_joint_names) if track_joint_names else [] + self.track_body_names = list(track_body_names) if track_body_names else [] + + # Phase / period are set in reset() + self.mimic_phase = 0 + self.gait_phase = 0 + self.mimic_period = 1 + self.gait_period = 1 + + # Tracking errors (filled in calc_reward; consumed by done()) + self.root_track_error = 0.0 + self.body_track_error = 0.0 + self.object_track_error = 0.0 + self.joint_track_error = 0.0 + self.hand_track_error = 0.0 + self.force_track_error = 0.0 + + def step(self) -> None: + # Phase advancement is handled at the env level so the env can detect + # clip-end and trigger a reset; nothing to do here. + return + + def calc_reward(self, prev_torque, prev_action, action) -> dict[str, float]: + idx = self._current_idx() + dt = self.reference_motion["dt"] + ref_xy_vel = np.linalg.norm( + (self.reference_motion["root_pose"][idx] - self.reference_motion["root_pose"][idx - 1])[:2] / dt + ) + mimic_flag = ref_xy_vel < 0.15 + + if mimic_flag: + self.track_joint_names = LEG_JOINTS + WAIST_JOINTS + ARM_JOINTS + self.track_body_names = [ + "torso_link", + "left_elbow_link", + "right_elbow_link", + "left_ankle_link", + "right_ankle_link", + ] + else: + self.track_joint_names = WAIST_JOINTS + ARM_JOINTS + self.track_body_names = ["torso_link", "left_elbow_link", "right_elbow_link"] + + # Current motor positions/velocities + motor_pos = [self._client.get_act_joint_position(jn + "_motor") for jn in self.track_joint_names] + motor_vel = [self._client.get_act_joint_velocity(jn + "_motor") for jn in self.track_joint_names] + + # Current relative body poses (in root frame) + root_pose = self._client.get_object_affine_by_name(self.root_body_name, "OBJ_BODY") + rel_link_poses = [] + for body_name in self.track_body_names: + body_pose = self._client.get_object_affine_by_name(body_name, "OBJ_BODY") + rel_link_poses.append(np.linalg.inv(root_pose).dot(body_pose)) + + thresh = 10.0 + lhand_frc = int(self._client.get_interaction_force(self.lhand_body_name, self.object_body_name) > thresh) + rhand_frc = int(self._client.get_interaction_force(self.rhand_body_name, self.object_body_name) > thresh) + box_frc = int( + self._client.get_interaction_force(self.object_body_name, "table1") + + self._client.get_interaction_force(self.object_body_name, "table2") + > thresh / 2 + ) + + # Reference at current phase + ref_root_pose = self.reference_motion["root_pose"][idx][[0, 1, 2, 6, 3, 4, 5]] + ref_root_pose = tf3.affines.compose(ref_root_pose[:3], tf3.quaternions.quat2mat(ref_root_pose[3:]), np.ones(3)) + ref_motor_pos = np.array([self.reference_motion["joint_position"][jn][idx] for jn in self.track_joint_names]) + ref_motor_vel = np.array([self.reference_motion["joint_velocity"][jn][idx] for jn in self.track_joint_names]) + ref_rel_link_poses = [] + for body_name in self.track_body_names: + ref_link_pose = self.reference_motion["relative_link_pose"][body_name][idx][[0, 1, 2, 6, 3, 4, 5]] + ref_rel_link_poses.append( + tf3.affines.compose(ref_link_pose[:3], tf3.quaternions.quat2mat(ref_link_pose[3:]), np.ones(3)) + ) + + ref_lhand_frc = self.reference_motion["force_lhand"][idx] + ref_rhand_frc = self.reference_motion["force_rhand"][idx] + ref_box_frc = self.reference_motion["force_box"][idx] + + # Joint tracking error + motor_vel = 0.1 * np.array(motor_vel) + ref_motor_vel = 0.1 * ref_motor_vel + self.joint_track_error = np.linalg.norm( + np.concatenate((ref_motor_pos, ref_motor_vel)) - np.concatenate((motor_pos, motor_vel)) + ) + + # Body tracking error + link_positions = np.array(rel_link_poses)[:, :3, 3].flatten() + link_rotations = np.array(rel_link_poses)[:, :3, :3].flatten() + ref_link_positions = np.array(ref_rel_link_poses)[:, :3, 3].flatten() + ref_link_rotations = np.array(ref_rel_link_poses)[:, :3, :3].flatten() + body_pos_track_error = np.linalg.norm(ref_link_positions - link_positions) + body_rot_track_error = np.linalg.norm(ref_link_rotations - link_rotations) + self.body_track_error = body_pos_track_error + 0.1 * body_rot_track_error + + # Root tracking error + root_position = root_pose[:3, 3].flatten() + root_rotation = root_pose[:3, :3].flatten() + ref_root_position = ref_root_pose[:3, 3].flatten() + ref_root_rotation = ref_root_pose[:3, :3].flatten() + self.root_track_error = np.linalg.norm(root_position - ref_root_position) + 0.1 * np.linalg.norm( + root_rotation - ref_root_rotation + ) + + # Object tracking error (in root frame) + obj_pose = self._client.get_object_affine_by_name(self.object_body_name, "OBJ_BODY") + rel_obj_pose = np.linalg.inv(root_pose).dot(obj_pose) + ref_obj_pose_v = self.reference_motion["object_pose"][idx][[0, 1, 2, 6, 3, 4, 5]] + ref_obj_pose = tf3.affines.compose(ref_obj_pose_v[:3], tf3.quaternions.quat2mat(ref_obj_pose_v[3:]), np.ones(3)) + ref_rel_obj_pose = np.linalg.inv(ref_root_pose).dot(ref_obj_pose) + self.object_track_error = np.linalg.norm(rel_obj_pose[:3, 3] - ref_rel_obj_pose[:3, 3]) + 0.1 * np.linalg.norm( + rel_obj_pose[:2, :3].flatten() - ref_rel_obj_pose[:2, :3].flatten() + ) + + # Hand tracking error (target expressed in box frame) + target_positions = { + "right_hand": np.array([0.2, 0, 0]), + "left_hand": np.array([-0.2, 0, 0]), + } + box_p = self._client.get_object_xpos_by_name("box", "OBJ_BODY") + box_q = self._client.get_object_xquat_by_name("box", "OBJ_BODY") + # MuJoCo quat is [w, x, y, z]; scipy expects [x, y, z, w] + box_rot = Rotation.from_quat([box_q[1], box_q[2], box_q[3], box_q[0]]).as_matrix() + hands_in_box = [] + for hand in ["right_hand", "left_hand"]: + p = self._client.get_object_xpos_by_name(hand, "OBJ_SITE") + hand_in_box_frame = box_rot.T.dot(p - np.array(box_p)) + hands_in_box.append(np.linalg.norm(hand_in_box_frame - target_positions[hand])) + + ref_rhand_dist = self.reference_motion["distance_right_hand_to_target"][idx] + ref_lhand_dist = self.reference_motion["distance_left_hand_to_target"][idx] + self.hand_track_error = np.linalg.norm(np.array([ref_rhand_dist, ref_lhand_dist]) - np.array(hands_in_box)) + + # Force tracking error + self.force_track_error = np.linalg.norm( + np.array([lhand_frc, rhand_frc, box_frc]) - np.array([ref_lhand_frc, ref_rhand_frc, ref_box_frc]) + ) + + # Joint range / torque penalties + current_pose = np.array(self._client.get_act_joint_positions()) + l_oo, u_oo = self.get_out_of_limit_joints(current_pose, reduction=10) + joint_range_error = float(np.sum(l_oo) + np.sum(u_oo)) + tau_error = np.linalg.norm(self._client.get_act_joint_torques()) + + return { + "root_track_score": 0.2 * np.exp(-0.5 * self.root_track_error**2), + "body_track_score": 0.15 * np.exp(-10.0 * self.body_track_error**2), + "joint_track_score": 0.15 * np.exp(-0.5 * self.joint_track_error**2), + "object_track_score": 0.1 * np.exp(-4.0 * self.object_track_error**2), + "hand_track_score": 0.2 * np.exp(-10.0 * self.hand_track_error**2), + "force_track_score": 0.1 * np.exp(-100.0 * self.force_track_error**2), + "joint_range_score": 0.05 * np.exp(-20.0 * joint_range_error**2), + "joint_torque_score": 0.05 * np.exp(-5e-5 * tau_error**2), + } + + def done(self) -> bool: + current_pose = np.array(self._client.get_act_joint_positions()) + l_oo, u_oo = self.get_out_of_limit_joints(current_pose, reduction=5) + joint_near_limit = bool(np.any(l_oo > 0)) or bool(np.any(u_oo > 0)) + + root_jnt_adr = self._client.model.body(self.root_body_name).jntadr[0] + root_qpos_adr = self._client.model.joint(root_jnt_adr).qposadr[0] + qpos = self._client.get_qpos()[root_qpos_adr : root_qpos_adr + 7] + bad_contact = self._check_bad_collisions(self._client.model, self._client.data) + + terminate = { + "qpos[2]_ll": qpos[2] < 0.6, + "qpos[2]_ul": qpos[2] > 1.4, + "faraway_root": self.root_track_error > 0.4, + "faraway_bodies": self.body_track_error > 0.8, + "bad_contact": bad_contact, + "joint_near_limit": joint_near_limit, + } + return any(terminate.values()) + + def reset(self, iter_count: int = 0) -> None: + dt = self.reference_motion["dt"] + clip_frames = len(self.reference_motion["root_pose"]) + clip_duration_s = round(clip_frames * dt, 5) + self.mimic_period = clip_duration_s * (1 / self._control_dt) + + if iter_count < 5000: + p = [0.2, 0.8] + else: + p = [0.8, 0.2] + self.mimic_phase = int( + np.random.choice( + [0, np.random.randint(int(0.8 * self.mimic_period), int(self.mimic_period))], + p=p, + ) + ) + + # Bipedal gait clock (independent of mocap clock) + swing_duration = 0.4 + stance_duration = 0.1 + total_duration = 2 * (swing_duration + stance_duration) + self.gait_period = float(np.floor(total_duration * (1 / self._control_dt))) + self.gait_phase = int(np.random.randint(0, int(self.gait_period))) + + def _current_idx(self) -> int: + n = self.reference_motion["root_pose"].shape[0] + return int((self.mimic_phase / self.mimic_period) * n) + + def get_out_of_limit_joints(self, current_pose, reduction: int = 10): + llimit, ulimit = self._client.get_act_joint_ranges()[0], self._client.get_act_joint_ranges()[1] + llimit = llimit + np.deg2rad(reduction) + ulimit = ulimit - np.deg2rad(reduction) + out_of_llimit = -np.clip(current_pose - llimit, a_min=-np.inf, a_max=0) + out_of_ulimit = np.clip(current_pose - ulimit, a_min=0, a_max=np.inf) + return out_of_llimit, out_of_ulimit + + @staticmethod + def _check_bad_collisions(model, data) -> bool: + allowed = { + "world", + "right_elbow_link", + "left_elbow_link", + "right_ankle_link", + "left_ankle_link", + "box", + "table1", + "table2", + "hfield", + } + for i in range(data.ncon): + c = data.contact[i] + geom1_body = model.body(model.geom_bodyid[c.geom1]).name + geom2_body = model.body(model.geom_bodyid[c.geom2]).name + if geom1_body not in allowed or geom2_body not in allowed: + return True + if geom1_body.endswith("elbow_link") and geom2_body != "box": + return True + return False diff --git a/uv.lock b/uv.lock index 4b4db73..cfca571 100644 --- a/uv.lock +++ b/uv.lock @@ -2,9 +2,18 @@ version = 1 revision = 3 requires-python = ">=3.10" resolution-markers = [ - "python_full_version >= '3.13'", - "python_full_version == '3.12.*'", - "python_full_version == '3.11.*'", + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version < '3.11'", ] @@ -383,6 +392,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f7/02/6e639e90f181dc9127046e00d0528f9f7ad12d428972e3a5378b9aefdb0b/glfw-2.10.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", hash = "sha256:7916034efa867927892635733a3b6af8cd95ceb10566fd7f1e0d2763c2ee8b12", size = 243525, upload-time = "2025-09-12T08:54:34.006Z" }, { url = "https://files.pythonhosted.org/packages/84/06/cb588ca65561defe0fc48d1df4c2ac12569b81231ae4f2b52ab37007d0bd/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-win32.whl", hash = "sha256:6c9549da71b93e367b4d71438798daae1da2592039fd14204a80a1a2348ae127", size = 552685, upload-time = "2025-09-12T08:54:35.723Z" }, { url = "https://files.pythonhosted.org/packages/86/27/00c9c96af18ac0a5eac2ff61cbe306551a2d770d7173f396d0792ee1a59e/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-win_amd64.whl", hash = "sha256:6292d5d6634d668cd23d337e6089491d3945a9aa4ac6e1667b0003520d7caa51", size = 559466, upload-time = "2025-09-12T08:54:37.661Z" }, + { url = "https://files.pythonhosted.org/packages/b3/87/de0b33f6f00687499ca1371f22aa73396341b85bf88f1a284f9da8842493/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.py39.py310.py311.py312.py313.py314-none-macosx_10_6_intel.whl", hash = "sha256:2aab89d2d9535635ba011fc7303390685169a1aa6731ad580d08d043524b8899", size = 105326, upload-time = "2026-01-28T05:57:56.083Z" }, + { url = "https://files.pythonhosted.org/packages/b6/a6/6ea2f73ad4474896d9e38b3ffbe6ffd5a802c738392269e99e8c6621a461/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.py39.py310.py311.py312.py313.py314-none-macosx_11_0_arm64.whl", hash = "sha256:23936202a107039b5372f0b88ae1d11080746aa1c78910a45d4a0c4cf408cfaa", size = 102180, upload-time = "2026-01-28T05:57:57.787Z" }, + { url = "https://files.pythonhosted.org/packages/58/19/d81b19e8261b9cb51b81d1402167791fef81088dfe91f0c4e9d136fdc5ca/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.py39.py310.py311.py312.py313.py314-none-manylinux2014_aarch64.whl", hash = "sha256:7be06d0838f61df67bd54cb6266a6193d54083acb3624ff3c3812a6358406fa4", size = 230038, upload-time = "2026-01-28T05:57:59.105Z" }, + { url = "https://files.pythonhosted.org/packages/e2/fa/b035636cd82198b97b51a93efe9cfc4343d6b15cefbd336a3f2be871d848/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.py39.py310.py311.py312.py313.py314-none-manylinux2014_x86_64.whl", hash = "sha256:91d36b3582a766512eff8e3b5dcc2d3ffcbf10b7cf448551085a08a10f1b8244", size = 241983, upload-time = "2026-01-28T05:58:00.352Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b4/f7b6cc022dd7c68b6c702d19da5d591f978f89c958b9bd3090615db0c739/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.py39.py310.py311.py312.py313.py314-none-manylinux_2_28_aarch64.whl", hash = "sha256:27c9e9a2d5e1dc3c9e3996171d844d9df9a5a101e797cb94cce217b7afcf8fd9", size = 231053, upload-time = "2026-01-28T05:58:01.683Z" }, + { url = "https://files.pythonhosted.org/packages/5a/3f/efeb7c6801c46e11bd666a5180f0d615f74f72264212f74f39586c6fda9d/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.py39.py310.py311.py312.py313.py314-none-manylinux_2_28_x86_64.whl", hash = "sha256:ce6724bb7cb3d0543dcba17206dce909f94176e68220b8eafee72e9f92bcf542", size = 243522, upload-time = "2026-01-28T05:58:03.517Z" }, + { url = "https://files.pythonhosted.org/packages/cf/b9/b04c3aa0aad2870cfe799f32f8b59789c98e1816bbce9e83f4823c5b840b/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.py39.py310.py311.py312.py313.py314-none-win32.whl", hash = "sha256:fca724a21a372731edb290841edd28a9fb1ee490f833392752844ac807c0086a", size = 552682, upload-time = "2026-01-28T05:58:05.649Z" }, + { url = "https://files.pythonhosted.org/packages/bd/e1/6d6816b296a529ac9b897ad228b1e084eb1f92319e96371880eebdc874a6/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.py39.py310.py311.py312.py313.py314-none-win_amd64.whl", hash = "sha256:823c0bd7770977d4b10e0ed0aef2f3682276b7c88b8b65cfc540afce5951392f", size = 559464, upload-time = "2026-01-28T05:58:07.261Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a8/d4dab8a58fc2e6981fc7a58c4e56ba9d777fb24931cec6a22152edbb3540/glfw-2.10.0-py2.py3-none-macosx_10_6_intel.whl", hash = "sha256:a0d1f29f206219cc291edfb6cace663a86da2470632551c998e3db82d48ea177", size = 105288, upload-time = "2026-03-10T17:21:19.929Z" }, + { url = "https://files.pythonhosted.org/packages/14/61/68d35e001872a7705112418da236fa2418d4f2e5419f8b2837f9b81bb3da/glfw-2.10.0-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:d28d6f3ef217e64e35dc6fd0a7acb4cec9bfe7cd14dd9b35a7228a87002de154", size = 102139, upload-time = "2026-03-10T17:21:21.645Z" }, + { url = "https://files.pythonhosted.org/packages/4e/e1/ca5984081aaae07c9d371cb11dc4e4ff603510678ed9b73e58b6c351fe63/glfw-2.10.0-py2.py3-none-manylinux2014_aarch64.whl", hash = "sha256:f968b522bb6a0e04aaf4dcac30a476d7229308bb2bac406a60587debb5a61e29", size = 229998, upload-time = "2026-03-10T17:21:23.549Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c4/82ac75fdcfba2896da7a573c0fc7f8ceb8f77ead6866d500d06c32f1c464/glfw-2.10.0-py2.py3-none-manylinux2014_x86_64.whl", hash = "sha256:68cf3752bdadb6f4bc0a876247c28c88c7251ac39f8af076ed938fdfd71e72dd", size = 241944, upload-time = "2026-03-10T17:21:26.102Z" }, + { url = "https://files.pythonhosted.org/packages/e3/96/9f691823cca5eb6a08f346bd0ff03b78032db9370b509a1e9c8976fb20a5/glfw-2.10.0-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:44d98de5dbf8f727e0cb29f9b29d29528ea7570f2e6f42f8430a69df05f12b48", size = 231009, upload-time = "2026-03-10T17:21:28.481Z" }, + { url = "https://files.pythonhosted.org/packages/3f/93/977b9e679e356871d428ae7a1139ec767dd5177bed58a6344b4d2199e00f/glfw-2.10.0-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:cca5158d62189e08792b1ae54f92307a282921a0e7783315b467e21b0a381c88", size = 243480, upload-time = "2026-03-10T17:21:30.538Z" }, + { url = "https://files.pythonhosted.org/packages/f9/bd/cea9569c8f2188b0a104472951420434a3e1f5cf26f5836ef9d7227a1a30/glfw-2.10.0-py2.py3-none-win32.whl", hash = "sha256:5e024509989740e8e7b86cc4aab508195495f79879072b0e1f68bd036a2916ad", size = 552641, upload-time = "2026-03-10T17:21:32.653Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9b/4366ad3e1c0688146c70aa6143584d6a8d88583b9390f106250e25a3d5cd/glfw-2.10.0-py2.py3-none-win_amd64.whl", hash = "sha256:7f787ee8645781f10e8800438ce4357ab38c573ffb191aba380c1e72eba6311c", size = 559423, upload-time = "2026-03-10T17:21:34.766Z" }, ] [[package]] @@ -617,6 +642,8 @@ dependencies = [ { name = "imageio", extra = ["ffmpeg"] }, { name = "intel-openmp" }, { name = "mujoco" }, + { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "pandas", version = "3.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "ray" }, { name = "tensorboard" }, { name = "torch" }, @@ -637,6 +664,7 @@ requires-dist = [ { name = "imageio", extras = ["ffmpeg"], specifier = ">=2.37.0" }, { name = "intel-openmp", specifier = ">=2025.2.1" }, { name = "mujoco", specifier = "==3.4.0" }, + { name = "pandas", specifier = ">=2.0" }, { name = "ray", specifier = "==2.40.0" }, { name = "tensorboard", specifier = ">=2.20.0" }, { name = "torch", specifier = "==2.5.1" }, @@ -979,9 +1007,18 @@ name = "networkx" version = "3.5" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.13'", - "python_full_version == '3.12.*'", - "python_full_version == '3.11.*'", + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] sdist = { url = "https://files.pythonhosted.org/packages/6c/4f/ccdb8ad3a38e583f214547fd2f7ff1fc160c43a75af88e6aec213404b96a/networkx-3.5.tar.gz", hash = "sha256:d4c6f9cf81f52d69230866796b82afbccdec3db7ae4fbd1b65ea750feed50037", size = 2471065, upload-time = "2025-05-29T11:35:07.804Z" } wheels = [ @@ -1067,9 +1104,18 @@ name = "numpy" version = "2.3.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.13'", - "python_full_version == '3.12.*'", - "python_full_version == '3.11.*'", + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] sdist = { url = "https://files.pythonhosted.org/packages/d0/19/95b3d357407220ed24c139018d2518fab0a61a948e68286a25f1a4d049ff/numpy-2.3.3.tar.gz", hash = "sha256:ddc7c39727ba62b80dfdbedf400d1c10ddfa8eefbd7ec8dcb118be8b56d31029", size = 20576648, upload-time = "2025-09-09T16:54:12.543Z" } wheels = [ @@ -1267,6 +1313,144 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] +[[package]] +name = "pandas" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "python-dateutil", marker = "python_full_version < '3.11'" }, + { name = "pytz", marker = "python_full_version < '3.11'" }, + { name = "tzdata", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/f7/f425a00df4fcc22b292c6895c6831c0c8ae1d9fac1e024d16f98a9ce8749/pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c", size = 11555763, upload-time = "2025-09-29T23:16:53.287Z" }, + { url = "https://files.pythonhosted.org/packages/13/4f/66d99628ff8ce7857aca52fed8f0066ce209f96be2fede6cef9f84e8d04f/pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a", size = 10801217, upload-time = "2025-09-29T23:17:04.522Z" }, + { url = "https://files.pythonhosted.org/packages/1d/03/3fc4a529a7710f890a239cc496fc6d50ad4a0995657dccc1d64695adb9f4/pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1", size = 12148791, upload-time = "2025-09-29T23:17:18.444Z" }, + { url = "https://files.pythonhosted.org/packages/40/a8/4dac1f8f8235e5d25b9955d02ff6f29396191d4e665d71122c3722ca83c5/pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838", size = 12769373, upload-time = "2025-09-29T23:17:35.846Z" }, + { url = "https://files.pythonhosted.org/packages/df/91/82cc5169b6b25440a7fc0ef3a694582418d875c8e3ebf796a6d6470aa578/pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250", size = 13200444, upload-time = "2025-09-29T23:17:49.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/ae/89b3283800ab58f7af2952704078555fa60c807fff764395bb57ea0b0dbd/pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4", size = 13858459, upload-time = "2025-09-29T23:18:03.722Z" }, + { url = "https://files.pythonhosted.org/packages/85/72/530900610650f54a35a19476eca5104f38555afccda1aa11a92ee14cb21d/pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826", size = 11346086, upload-time = "2025-09-29T23:18:18.505Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790, upload-time = "2025-09-29T23:18:30.065Z" }, + { url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831, upload-time = "2025-09-29T23:38:56.071Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267, upload-time = "2025-09-29T23:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281, upload-time = "2025-09-29T23:18:56.834Z" }, + { url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453, upload-time = "2025-09-29T23:19:09.247Z" }, + { url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361, upload-time = "2025-09-29T23:19:25.342Z" }, + { url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702, upload-time = "2025-09-29T23:19:38.296Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, + { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" }, + { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" }, + { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" }, + { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" }, + { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" }, + { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" }, + { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" }, + { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" }, + { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" }, + { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" }, + { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" }, + { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" }, + { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" }, + { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" }, + { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" }, + { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" }, + { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" }, + { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" }, + { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" }, + { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" }, + { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" }, + { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" }, + { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, +] + +[[package]] +name = "pandas" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "numpy", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "python-dateutil", marker = "python_full_version >= '3.11'" }, + { name = "tzdata", marker = "(python_full_version >= '3.11' and sys_platform == 'emscripten') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/99/b342345300f13440fe9fe385c3c481e2d9a595ee3bab4d3219247ac94e9a/pandas-3.0.2.tar.gz", hash = "sha256:f4753e73e34c8d83221ba58f232433fca2748be8b18dbca02d242ed153945043", size = 4645855, upload-time = "2026-03-31T06:48:30.816Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/35/6411db530c618e0e0005187e35aa02ce60ae4c4c4d206964a2f978217c27/pandas-3.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a727a73cbdba2f7458dc82449e2315899d5140b449015d822f515749a46cbbe0", size = 10326926, upload-time = "2026-03-31T06:46:08.29Z" }, + { url = "https://files.pythonhosted.org/packages/c4/d3/b7da1d5d7dbdc5ef52ed7debd2b484313b832982266905315dad5a0bf0b1/pandas-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dbbd4aa20ca51e63b53bbde6a0fa4254b1aaabb74d2f542df7a7959feb1d760c", size = 9926987, upload-time = "2026-03-31T06:46:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/52/77/9b1c2d6070b5dbe239a7bc889e21bfa58720793fb902d1e070695d87c6d0/pandas-3.0.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:339dda302bd8369dedeae979cb750e484d549b563c3f54f3922cb8ff4978c5eb", size = 10757067, upload-time = "2026-03-31T06:46:14.903Z" }, + { url = "https://files.pythonhosted.org/packages/20/17/ec40d981705654853726e7ac9aea9ddbb4a5d9cf54d8472222f4f3de06c2/pandas-3.0.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:61c2fd96d72b983a9891b2598f286befd4ad262161a609c92dc1652544b46b76", size = 11258787, upload-time = "2026-03-31T06:46:17.683Z" }, + { url = "https://files.pythonhosted.org/packages/90/e3/3f1126d43d3702ca8773871a81c9f15122a1f412342cc56284ffda5b1f70/pandas-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c934008c733b8bbea273ea308b73b3156f0181e5b72960790b09c18a2794fe1e", size = 11771616, upload-time = "2026-03-31T06:46:20.532Z" }, + { url = "https://files.pythonhosted.org/packages/2e/cf/0f4e268e1f5062e44a6bda9f925806721cd4c95c2b808a4c82ebe914f96b/pandas-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:60a80bb4feacbef5e1447a3f82c33209c8b7e07f28d805cfd1fb951e5cb443aa", size = 12337623, upload-time = "2026-03-31T06:46:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/44/a0/97a6339859d4acb2536efb24feb6708e82f7d33b2ed7e036f2983fcced82/pandas-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:ed72cb3f45190874eb579c64fa92d9df74e98fd63e2be7f62bce5ace0ade61df", size = 9897372, upload-time = "2026-03-31T06:46:26.703Z" }, + { url = "https://files.pythonhosted.org/packages/8f/eb/781516b808a99ddf288143cec46b342b3016c3414d137da1fdc3290d8860/pandas-3.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:f12b1a9e332c01e09510586f8ca9b108fd631fd656af82e452d7315ef6df5f9f", size = 9154922, upload-time = "2026-03-31T06:46:30.284Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b0/c20bd4d6d3f736e6bd6b55794e9cd0a617b858eaad27c8f410ea05d953b7/pandas-3.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:232a70ebb568c0c4d2db4584f338c1577d81e3af63292208d615907b698a0f18", size = 10347921, upload-time = "2026-03-31T06:46:33.36Z" }, + { url = "https://files.pythonhosted.org/packages/35/d0/4831af68ce30cc2d03c697bea8450e3225a835ef497d0d70f31b8cdde965/pandas-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:970762605cff1ca0d3f71ed4f3a769ea8f85fc8e6348f6e110b8fea7e6eb5a14", size = 9888127, upload-time = "2026-03-31T06:46:36.253Z" }, + { url = "https://files.pythonhosted.org/packages/61/a9/16ea9346e1fc4a96e2896242d9bc674764fb9049b0044c0132502f7a771e/pandas-3.0.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aff4e6f4d722e0652707d7bcb190c445fe58428500c6d16005b02401764b1b3d", size = 10399577, upload-time = "2026-03-31T06:46:39.224Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a8/3a61a721472959ab0ce865ef05d10b0d6bfe27ce8801c99f33d4fa996e65/pandas-3.0.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef8b27695c3d3dc78403c9a7d5e59a62d5464a7e1123b4e0042763f7104dc74f", size = 10880030, upload-time = "2026-03-31T06:46:42.412Z" }, + { url = "https://files.pythonhosted.org/packages/da/65/7225c0ea4d6ce9cb2160a7fb7f39804871049f016e74782e5dade4d14109/pandas-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f8d68083e49e16b84734eb1a4dcae4259a75c90fb6e2251ab9a00b61120c06ab", size = 11409468, upload-time = "2026-03-31T06:46:45.2Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/46e7c76032639f2132359b5cf4c785dd8cf9aea5ea64699eac752f02b9db/pandas-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:32cc41f310ebd4a296d93515fcac312216adfedb1894e879303987b8f1e2b97d", size = 11936381, upload-time = "2026-03-31T06:46:48.293Z" }, + { url = "https://files.pythonhosted.org/packages/7b/8b/721a9cff6fa6a91b162eb51019c6243b82b3226c71bb6c8ef4a9bd65cbc6/pandas-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:a4785e1d6547d8427c5208b748ae2efb64659a21bd82bf440d4262d02bfa02a4", size = 9744993, upload-time = "2026-03-31T06:46:51.488Z" }, + { url = "https://files.pythonhosted.org/packages/d5/18/7f0bd34ae27b28159aa80f2a6799f47fda34f7fb938a76e20c7b7fe3b200/pandas-3.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:08504503f7101300107ecdc8df73658e4347586db5cfdadabc1592e9d7e7a0fd", size = 9056118, upload-time = "2026-03-31T06:46:54.548Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ca/3e639a1ea6fcd0617ca4e8ca45f62a74de33a56ae6cd552735470b22c8d3/pandas-3.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b5918ba197c951dec132b0c5929a00c0bf05d5942f590d3c10a807f6e15a57d3", size = 10321105, upload-time = "2026-03-31T06:46:57.327Z" }, + { url = "https://files.pythonhosted.org/packages/0b/77/dbc82ff2fb0e63c6564356682bf201edff0ba16c98630d21a1fb312a8182/pandas-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d606a041c89c0a474a4702d532ab7e73a14fe35c8d427b972a625c8e46373668", size = 9864088, upload-time = "2026-03-31T06:46:59.935Z" }, + { url = "https://files.pythonhosted.org/packages/5c/2b/341f1b04bbca2e17e13cd3f08c215b70ef2c60c5356ef1e8c6857449edc7/pandas-3.0.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:710246ba0616e86891b58ab95f2495143bb2bc83ab6b06747c74216f583a6ac9", size = 10369066, upload-time = "2026-03-31T06:47:02.792Z" }, + { url = "https://files.pythonhosted.org/packages/12/c5/cbb1ffefb20a93d3f0e1fdcda699fb84976210d411b008f97f48bf6ce27e/pandas-3.0.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5d3cfe227c725b1f3dff4278b43d8c784656a42a9325b63af6b1492a8232209e", size = 10876780, upload-time = "2026-03-31T06:47:06.205Z" }, + { url = "https://files.pythonhosted.org/packages/98/fe/2249ae5e0a69bd0ddf17353d0a5d26611d70970111f5b3600cdc8be883e7/pandas-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c3b723df9087a9a9a840e263ebd9f88b64a12075d1bf2ea401a5a42f254f084d", size = 11375181, upload-time = "2026-03-31T06:47:09.383Z" }, + { url = "https://files.pythonhosted.org/packages/de/64/77a38b09e70b6464883b8d7584ab543e748e42c1b5d337a2ee088e0df741/pandas-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a3096110bf9eac0070b7208465f2740e2d8a670d5cb6530b5bb884eca495fd39", size = 11928899, upload-time = "2026-03-31T06:47:12.686Z" }, + { url = "https://files.pythonhosted.org/packages/5e/52/42855bf626868413f761addd574acc6195880ae247a5346477a4361c3acb/pandas-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:07a10f5c36512eead51bc578eb3354ad17578b22c013d89a796ab5eee90cd991", size = 9746574, upload-time = "2026-03-31T06:47:15.64Z" }, + { url = "https://files.pythonhosted.org/packages/88/39/21304ae06a25e8bf9fc820d69b29b2c495b2ae580d1e143146c309941760/pandas-3.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:5fdbfa05931071aba28b408e59226186b01eb5e92bea2ab78b65863ca3228d84", size = 9047156, upload-time = "2026-03-31T06:47:18.595Z" }, + { url = "https://files.pythonhosted.org/packages/72/20/7defa8b27d4f330a903bb68eea33be07d839c5ea6bdda54174efcec0e1d2/pandas-3.0.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:dbc20dea3b9e27d0e66d74c42b2d0c1bed9c2ffe92adea33633e3bedeb5ac235", size = 10756238, upload-time = "2026-03-31T06:47:22.012Z" }, + { url = "https://files.pythonhosted.org/packages/e9/95/49433c14862c636afc0e9b2db83ff16b3ad92959364e52b2955e44c8e94c/pandas-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b75c347eff42497452116ce05ef461822d97ce5b9ff8df6edacb8076092c855d", size = 10408520, upload-time = "2026-03-31T06:47:25.197Z" }, + { url = "https://files.pythonhosted.org/packages/3b/f8/462ad2b5881d6b8ec8e5f7ed2ea1893faa02290d13870a1600fe72ad8efc/pandas-3.0.2-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1478075142e83a5571782ad007fb201ed074bdeac7ebcc8890c71442e96adf7", size = 10324154, upload-time = "2026-03-31T06:47:28.097Z" }, + { url = "https://files.pythonhosted.org/packages/0a/65/d1e69b649cbcddda23ad6e4c40ef935340f6f652a006e5cbc3555ac8adb3/pandas-3.0.2-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5880314e69e763d4c8b27937090de570f1fb8d027059a7ada3f7f8e98bdcb677", size = 10714449, upload-time = "2026-03-31T06:47:30.85Z" }, + { url = "https://files.pythonhosted.org/packages/47/a4/85b59bc65b8190ea3689882db6cdf32a5003c0ccd5a586c30fdcc3ffc4fc/pandas-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b5329e26898896f06035241a626d7c335daa479b9bbc82be7c2742d048e41172", size = 11338475, upload-time = "2026-03-31T06:47:34.026Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c4/bc6966c6e38e5d9478b935272d124d80a589511ed1612a5d21d36f664c68/pandas-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:81526c4afd31971f8b62671442a4b2b51e0aa9acc3819c9f0f12a28b6fcf85f1", size = 11786568, upload-time = "2026-03-31T06:47:36.941Z" }, + { url = "https://files.pythonhosted.org/packages/e8/74/09298ca9740beed1d3504e073d67e128aa07e5ca5ca2824b0c674c0b8676/pandas-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:7cadd7e9a44ec13b621aec60f9150e744cfc7a3dd32924a7e2f45edff31823b0", size = 10488652, upload-time = "2026-03-31T06:47:40.612Z" }, + { url = "https://files.pythonhosted.org/packages/bb/40/c6ea527147c73b24fc15c891c3fcffe9c019793119c5742b8784a062c7db/pandas-3.0.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:db0dbfd2a6cdf3770aa60464d50333d8f3d9165b2f2671bcc299b72de5a6677b", size = 10326084, upload-time = "2026-03-31T06:47:43.834Z" }, + { url = "https://files.pythonhosted.org/packages/95/25/bdb9326c3b5455f8d4d3549fce7abcf967259de146fe2cf7a82368141948/pandas-3.0.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0555c5882688a39317179ab4a0ed41d3ebc8812ab14c69364bbee8fb7a3f6288", size = 9914146, upload-time = "2026-03-31T06:47:46.67Z" }, + { url = "https://files.pythonhosted.org/packages/8d/77/3a227ff3337aa376c60d288e1d61c5d097131d0ac71f954d90a8f369e422/pandas-3.0.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:01f31a546acd5574ef77fe199bc90b55527c225c20ccda6601cf6b0fd5ed597c", size = 10444081, upload-time = "2026-03-31T06:47:49.681Z" }, + { url = "https://files.pythonhosted.org/packages/15/88/3cdd54fa279341afa10acf8d2b503556b1375245dccc9315659f795dd2e9/pandas-3.0.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:deeca1b5a931fdf0c2212c8a659ade6d3b1edc21f0914ce71ef24456ca7a6535", size = 10897535, upload-time = "2026-03-31T06:47:53.033Z" }, + { url = "https://files.pythonhosted.org/packages/06/9d/98cc7a7624f7932e40f434299260e2917b090a579d75937cb8a57b9d2de3/pandas-3.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f48afd9bb13300ffb5a3316973324c787054ba6665cda0da3fbd67f451995db", size = 11446992, upload-time = "2026-03-31T06:47:56.193Z" }, + { url = "https://files.pythonhosted.org/packages/9a/cd/19ff605cc3760e80602e6826ddef2824d8e7050ed80f2e11c4b079741dc3/pandas-3.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6c4d8458b97a35717b62469a4ea0e85abd5ed8687277f5ccfc67f8a5126f8c53", size = 11968257, upload-time = "2026-03-31T06:47:59.137Z" }, + { url = "https://files.pythonhosted.org/packages/db/60/aba6a38de456e7341285102bede27514795c1eaa353bc0e7638b6b785356/pandas-3.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:b35d14bb5d8285d9494fe93815a9e9307c0876e10f1e8e89ac5b88f728ec8dcf", size = 9865893, upload-time = "2026-03-31T06:48:02.038Z" }, + { url = "https://files.pythonhosted.org/packages/08/71/e5ec979dd2e8a093dacb8864598c0ff59a0cee0bbcdc0bfec16a51684d4f/pandas-3.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:63d141b56ef686f7f0d714cfb8de4e320475b86bf4b620aa0b7da89af8cbdbbb", size = 9188644, upload-time = "2026-03-31T06:48:05.045Z" }, + { url = "https://files.pythonhosted.org/packages/f1/6c/7b45d85db19cae1eb524f2418ceaa9d85965dcf7b764ed151386b7c540f0/pandas-3.0.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:140f0cffb1fa2524e874dde5b477d9defe10780d8e9e220d259b2c0874c89d9d", size = 10776246, upload-time = "2026-03-31T06:48:07.789Z" }, + { url = "https://files.pythonhosted.org/packages/a8/3e/7b00648b086c106e81766f25322b48aa8dfa95b55e621dbdf2fdd413a117/pandas-3.0.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae37e833ff4fed0ba352f6bdd8b73ba3ab3256a85e54edfd1ab51ae40cca0af8", size = 10424801, upload-time = "2026-03-31T06:48:10.897Z" }, + { url = "https://files.pythonhosted.org/packages/da/6e/558dd09a71b53b4008e7fc8a98ec6d447e9bfb63cdaeea10e5eb9b2dabe8/pandas-3.0.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d888a5c678a419a5bb41a2a93818e8ed9fd3172246555c0b37b7cc27027effd", size = 10345643, upload-time = "2026-03-31T06:48:13.7Z" }, + { url = "https://files.pythonhosted.org/packages/be/e3/921c93b4d9a280409451dc8d07b062b503bbec0531d2627e73a756e99a82/pandas-3.0.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b444dc64c079e84df91baa8bf613d58405645461cabca929d9178f2cd392398d", size = 10743641, upload-time = "2026-03-31T06:48:16.659Z" }, + { url = "https://files.pythonhosted.org/packages/56/ca/fd17286f24fa3b4d067965d8d5d7e14fe557dd4f979a0b068ac0deaf8228/pandas-3.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4544c7a54920de8eeacaa1466a6b7268ecfbc9bc64ab4dbb89c6bbe94d5e0660", size = 11361993, upload-time = "2026-03-31T06:48:19.475Z" }, + { url = "https://files.pythonhosted.org/packages/e4/a5/2f6ed612056819de445a433ca1f2821ac3dab7f150d569a59e9cc105de1d/pandas-3.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:734be7551687c00fbd760dc0522ed974f82ad230d4a10f54bf51b80d44a08702", size = 11815274, upload-time = "2026-03-31T06:48:22.695Z" }, + { url = "https://files.pythonhosted.org/packages/00/2f/b622683e99ec3ce00b0854bac9e80868592c5b051733f2cf3a868e5fea26/pandas-3.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:57a07209bebcbcf768d2d13c9b78b852f9a15978dac41b9e6421a81ad4cdd276", size = 10888530, upload-time = "2026-03-31T06:48:25.806Z" }, + { url = "https://files.pythonhosted.org/packages/cb/2b/f8434233fab2bd66a02ec014febe4e5adced20e2693e0e90a07d118ed30e/pandas-3.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:5371b72c2d4d415d08765f32d689217a43227484e81b2305b52076e328f6f482", size = 9455341, upload-time = "2026-03-31T06:48:28.418Z" }, +] + [[package]] name = "pillow" version = "11.3.0" @@ -1490,6 +1674,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" }, ] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "pytz" +version = "2026.1.post1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/56/db/b8721d71d945e6a8ac63c0fc900b2067181dbb50805958d4d4661cf7d277/pytz-2026.1.post1.tar.gz", hash = "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1", size = 321088, upload-time = "2026-03-03T07:47:50.683Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/99/781fe0c827be2742bcc775efefccb3b048a3a9c6ce9aec0cbf4a101677e5/pytz-2026.1.post1-py2.py3-none-any.whl", hash = "sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a", size = 510489, upload-time = "2026-03-03T07:47:49.167Z" }, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -1842,9 +2047,18 @@ name = "scipy" version = "1.16.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.13'", - "python_full_version == '3.12.*'", - "python_full_version == '3.11.*'", + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ { name = "numpy", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, @@ -1922,6 +2136,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, ] +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + [[package]] name = "sympy" version = "1.13.1" @@ -2118,6 +2341,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] +[[package]] +name = "tzdata" +version = "2026.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, +] + [[package]] name = "umf" version = "0.11.0" From 62e1c4b7fc4f7af6f2048025c098c4ebe3920052 Mon Sep 17 00:00:00 2001 From: "Rohan P. Singh" Date: Sat, 2 May 2026 21:05:15 -0400 Subject: [PATCH 2/2] Add H1 mimic leg-imitation projector for PPO --imitate --- envs/h1/h1_mimic.py | 125 ++++++++++++++++++++++++++++++++++++++++++++ tasks/mimic_task.py | 11 ++-- 2 files changed, 132 insertions(+), 4 deletions(-) diff --git a/envs/h1/h1_mimic.py b/envs/h1/h1_mimic.py index c03219a..d2262a3 100644 --- a/envs/h1/h1_mimic.py +++ b/envs/h1/h1_mimic.py @@ -15,10 +15,12 @@ import os import numpy as np +import torch import transforms3d as tf3 from envs.common import robot_interface from envs.common.utils import get_project_root +from rl.algos.imitation import ImitationQuery from robots.robot_base import RobotBase from tasks.mimic_task import MimicTask @@ -316,3 +318,126 @@ def viewer_setup(self): self.viewer.cam.distance = 8 self.viewer.cam.lookat[2] = 1.5 self.viewer.cam.lookat[0] = 1.0 + + def imitation_projector(self): + """Build the leg-imitation projector for the H1 walking expert. + + Slices the 19-joint mimic obs down to the 10-leg walker layout, + appends a synthetic walker command (INPLACE + zero velocity ref), + and masks samples to those whose gait phase falls within one full + leg swing+stance window. + """ + n_legs = len(self.leg_names) + n_act = len(self.actuators) + + # Per-timestep mimic obs layout (must match _get_robot_state + + # _get_external_state above): + # [0..5) root_r, root_p, root_ang_vel(3) + # [5..5+n_act) motor_pos + # [5+n_act..5+2*n_act) motor_vel + # [5+2*n_act..5+3*n_act) motor_tau + # [robot_state_len..) mimic_clock(2), gait_clock(2), obj_pos(3), obj_rot(6) + robot_state_len = self._get_robot_state_len() + motor_pos_start = 5 + motor_vel_start = motor_pos_start + n_act + motor_tau_start = motor_pos_start + 2 * n_act + gait_clock_start = robot_state_len + 2 # past mimic_clock + + # Indices into one mimic timestep that map to one walker timestep. + # Walker order: root(5), legs_pos(10), legs_vel(10), legs_tau(10), gait_clock(2). + per_ts_indices = ( + list(range(0, 5)) + + list(range(motor_pos_start, motor_pos_start + n_legs)) + + list(range(motor_vel_start, motor_vel_start + n_legs)) + + list(range(motor_tau_start, motor_tau_start + n_legs)) + + list(range(gait_clock_start, gait_clock_start + 2)) + ) + + # INPLACE mode + zero velocity reference: tells the walker to "walk in + # place" for the leg-imitation signal during the box-manip task. + inplace_command = np.concatenate([np.array([0.0, 1.0, 0.0]), np.zeros(3)]) + + # Imitation phase window: one full leg swing + half-stance buffer on + # either side, derived from the task's gait timing. + s = self.task._stance_duration + w = self.task._swing_duration + t = self.task._total_duration + phi_lo = (s / 2) / t + phi_hi = (s / 2 + w + s / 2) / t + + return _MimicLegImitationProjector( + per_ts_indices=per_ts_indices, + per_ts_mimic_dim=self.base_obs_len, + history_len=self.history_len, + gait_clock_offset=gait_clock_start, + synthetic_command=inplace_command, + action_indices=np.arange(n_legs, dtype=np.int64), + phi_window=(phi_lo, phi_hi), + ) + + +class _MimicLegImitationProjector: + """Projects mimic obs into the walker-expert obs space. + + The projector is layout-stateless: it captures all index/shape constants + at construction and operates purely on the obs batch tensor. Both envs + share the same per-step gait clock layout (sin/cos of `2*pi*phi`), so we + read the current gait phase out of the most-recent history chunk + (chunk 0 — `appendleft` puts the latest state at the front). + """ + + def __init__( + self, + per_ts_indices: list[int], + per_ts_mimic_dim: int, + history_len: int, + gait_clock_offset: int, + synthetic_command: np.ndarray, + action_indices: np.ndarray, + phi_window: tuple[float, float], + ): + self._per_ts_mimic_dim = per_ts_mimic_dim + self._history_len = history_len + self._phi_lo, self._phi_hi = phi_window + + # Absolute index of the gait_clock sin/cos in the latest chunk. + self._gait_sin_idx = gait_clock_offset + self._gait_cos_idx = gait_clock_offset + 1 + + # Pre-build the projection index list for every chunk in the history. + per_ts = torch.tensor(per_ts_indices, dtype=torch.long) + offsets = torch.arange(history_len, dtype=torch.long) * per_ts_mimic_dim + self._select_idx = (offsets[:, None] + per_ts[None, :]).reshape(-1) + + cmd = torch.tensor(synthetic_command, dtype=torch.float32) + self._synthetic_command = cmd.repeat(history_len) + + self._action_indices = torch.tensor(action_indices, dtype=torch.long) + + def __call__(self, obs_batch: torch.Tensor) -> ImitationQuery: + device = obs_batch.device + + # Mask: gait phase of the most recent timestep within the imitation window. + sin_phi = obs_batch[:, self._gait_sin_idx] + cos_phi = obs_batch[:, self._gait_cos_idx] + phi = torch.atan2(sin_phi, cos_phi) / (2 * torch.pi) + phi = phi % 1.0 + sample_mask = (phi >= self._phi_lo) & (phi <= self._phi_hi) + + select_idx = self._select_idx.to(device) + active = obs_batch[sample_mask] + sliced = active.index_select(dim=1, index=select_idx) + # Interleave per-timestep walker chunks with appended command per chunk. + n_active = sliced.shape[0] + per_ts_walker_minus_cmd = sliced.shape[1] // self._history_len + cmd_per_ts = self._synthetic_command.shape[0] // self._history_len + sliced = sliced.reshape(n_active, self._history_len, per_ts_walker_minus_cmd) + cmd_chunks = self._synthetic_command.to(device).reshape(self._history_len, cmd_per_ts) + cmd_chunks = cmd_chunks.unsqueeze(0).expand(n_active, -1, -1) + expert_obs = torch.cat([sliced, cmd_chunks], dim=2).reshape(n_active, -1) + + return ImitationQuery( + expert_obs=expert_obs, + sample_mask=sample_mask, + action_indices=self._action_indices.to(device), + ) diff --git a/tasks/mimic_task.py b/tasks/mimic_task.py index d26635d..4c09195 100644 --- a/tasks/mimic_task.py +++ b/tasks/mimic_task.py @@ -77,6 +77,12 @@ def __init__( self.mimic_period = 1 self.gait_period = 1 + # Bipedal gait timing (seconds). Exposed as attributes so the env's + # leg-imitation projector can derive its phase window from them. + self._swing_duration = 0.4 + self._stance_duration = 0.1 + self._total_duration = 2 * (self._swing_duration + self._stance_duration) + # Tracking errors (filled in calc_reward; consumed by done()) self.root_track_error = 0.0 self.body_track_error = 0.0 @@ -261,10 +267,7 @@ def reset(self, iter_count: int = 0) -> None: ) # Bipedal gait clock (independent of mocap clock) - swing_duration = 0.4 - stance_duration = 0.1 - total_duration = 2 * (swing_duration + stance_duration) - self.gait_period = float(np.floor(total_duration * (1 / self._control_dt))) + self.gait_period = float(np.floor(self._total_duration * (1 / self._control_dt))) self.gait_phase = int(np.random.randint(0, int(self.gait_period))) def _current_idx(self) -> int: