From b9392aae74bf9609ac1d48ca1217ae322518b527 Mon Sep 17 00:00:00 2001 From: Xinyue-cao <648343923@qq.com> Date: Wed, 29 Oct 2025 21:40:42 +0800 Subject: [PATCH 01/14] user-in-the-box-simulator --- src/box/mian.py | 0 src/box/simulator.py | 816 ++++++++++++++++++++++++++++++++++++++ src/box/test.simulator.py | 25 ++ 3 files changed, 841 insertions(+) create mode 100644 src/box/mian.py create mode 100644 src/box/simulator.py create mode 100644 src/box/test.simulator.py diff --git a/src/box/mian.py b/src/box/mian.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/box/simulator.py b/src/box/simulator.py new file mode 100644 index 0000000000..954b06b7a5 --- /dev/null +++ b/src/box/simulator.py @@ -0,0 +1,816 @@ +import gymnasium as gym +from gymnasium import spaces +import pygame +import mujoco +import os +import numpy as np +import scipy +import matplotlib +import sys +import importlib +import shutil +import inspect +import pathlib +from datetime import datetime +import copy +from collections import defaultdict +import xml.etree.ElementTree as ET + +from stable_baselines3 import PPO #required to load a trained LLC policy in HRL approach + +from .perception.base import Perception +from .utils.rendering import Camera, Context +from .utils.functions import output_path, parent_path, is_suitable_package_name, parse_yaml, write_yaml + + +class Simulator(gym.Env): + """ + The Simulator class contains functionality to build a standalone Python package from a config file. The built package + integrates a biomechanical model, a task model, and a perception model into one simulator that implements a gym.Env + interface. + """ + + # May be useful for later, the three digit number suffix is of format X.Y.Z where X is a major version. + version = "1.1.0" + + @classmethod + def get_class(cls, *args): + """ Returns a class from given strings. The last element in args should contain the class name. """ + # TODO check for incorrect module names etc + modules = ".".join(args[:-1]) + if "." in args[-1]: + splitted = args[-1].split(".") + if modules == "": + modules = ".".join(splitted[:-1]) + else: + modules += "." + ".".join(splitted[:-1]) + cls_name = splitted[-1] + else: + cls_name = args[-1] + module = cls.get_module(modules) + return getattr(module, cls_name) + + @classmethod + def get_module(cls, *args): + """ Returns a module from given strings. """ + src = __name__.split(".")[0] + modules = ".".join(args) + return importlib.import_module(src + "." + modules) + + @classmethod + def build(cls, config): + """ Builds a simulator based on a config. The input 'config' may be a dict (parsed from YAML) or path to a YAML file + + Args: + config: + - A dict containing configuration information. See example configs in folder uitb/configs/ + - A path to a config file + """ + + # If config is a path to the config file, parse it first + if isinstance(config, str): + if not os.path.isfile(config): + raise FileNotFoundError(f"Given config file {config} does not exist") + config = parse_yaml(config) + + # Make sure required things are defined in config + assert "simulation" in config, "Simulation specs (simulation) must be defined in config" + assert "bm_model" in config["simulation"], "Biomechanical model (bm_model) must be defined in config" + assert "task" in config["simulation"], "task (task) must be defined in config" + + assert "run_parameters" in config["simulation"], "Run parameters (run_parameters) must be defined in config" + run_parameters = config["simulation"]["run_parameters"].copy() + assert "action_sample_freq" in run_parameters, "Action sampling frequency (action_sample_freq) must be defined " \ + "in run parameters" + + # Set simulator version + config["version"] = cls.version + + # Save generated simulators to uitb/simulators + if "simulator_folder" in config: + simulator_folder = os.path.normpath(config["simulator_folder"]) + else: + simulator_folder = os.path.join(output_path(), config["simulator_name"]) + + # If 'package_name' is not defined use 'simulator_name' + if "package_name" not in config: + config["package_name"] = config["simulator_name"] + if not is_suitable_package_name(config["package_name"]): + raise NameError("Package name defined in the config file (either through 'package_name' or 'simulator_name') is " + "not a suitable Python package name. Use only lower-case letters and underscores instead of " + "spaces, and the name cannot start with a number.") + + # The name used in gym has a suffix -v0 + config["gym_name"] = "uitb:" + config["package_name"] + "-v0" + + # Create a simulator in the simulator folder + cls._clone(simulator_folder, config["package_name"]) + + # Load task class + task_cls = cls.get_class("tasks", config["simulation"]["task"]["cls"]) + task_cls.clone(simulator_folder, config["package_name"], app_executable=config["simulation"]["task"].get("kwargs", {}).get("unity_executable", None)) + simulation = task_cls.initialise(config["simulation"]["task"].get("kwargs", {})) + + # Set some compiler options + # TODO: would make more sense to have a separate "environment" class / xml file that defines all these defaults, + # including e.g. cameras, lighting, etc., so that they could be easily changed. Task and biomechanical model would + # be integrated into that object + compiler_defaults = {"inertiafromgeom": "auto", "balanceinertia": "true", "boundmass": "0.001", + "boundinertia": "0.001", "inertiagrouprange": "0 1"} + compiler = simulation.find("compiler") + if compiler is None: + ET.SubElement(simulation, "compiler", compiler_defaults) + else: + compiler.attrib.update(compiler_defaults) + + # Load biomechanical model class + bm_cls = cls.get_class("bm_models", config["simulation"]["bm_model"]["cls"]) + bm_cls.clone(simulator_folder, config["package_name"]) + bm_cls.insert(simulation) + + # Add perception modules + for module_cfg in config["simulation"].get("perception_modules", []): + module_cls = cls.get_class("perception", module_cfg["cls"]) + module_kwargs = module_cfg.get("kwargs", {}) + module_cls.clone(simulator_folder, config["package_name"]) + module_cls.insert(simulation, **module_kwargs) + + # Clone also RL library files so the package will be completely standalone + rl_cls = cls.get_class("rl", config["rl"]["algorithm"]) + rl_cls.clone(simulator_folder, config["package_name"]) + + # TODO read the xml file directly from task.getroot() instead of writing it to a file first; need to input a dict + # of assets to mujoco.MjModel.from_xml_path + simulation_file = os.path.join(simulator_folder, config["package_name"], "simulation") + with open(simulation_file+".xml", 'w') as file: + simulation.write(file, encoding='unicode') + + # Initialise the simulator + model, _, _, _, _, _ = \ + cls._initialise(config, simulator_folder, {**run_parameters, "build": True}) + + # Now that simulator has been initialised, everything should be set. Now we want to save the xml file again, but + # mujoco only is able to save the latest loaded xml file (which is either the task or bm model xml files which are + # are read in their __init__ functions), hence we need to read the file we've generated again before saving the + # modified model + mujoco.MjModel.from_xml_path(simulation_file+".xml") + mujoco.mj_saveLastXML(simulation_file+".xml", model) + + # Save the modified model also as binary for faster loading + mujoco.mj_saveModel(model, simulation_file+".mjcf", None) + + # Input built time into config + config["built"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + # Save config + write_yaml(config, os.path.join(simulator_folder, "config.yaml")) + + return simulator_folder + + @classmethod + def _clone(cls, simulator_folder, package_name): + """ Create a folder for the simulator being built, and copy or create relevant files. + + Args: + simulator_folder: Location of the simulator. + package_name: Name of the simulator (which is a python package). + """ + + # Create the folder + dst = os.path.join(simulator_folder, package_name) + os.makedirs(dst, exist_ok=True) + + # Copy simulator + src = pathlib.Path(inspect.getfile(cls)) + shutil.copyfile(src, os.path.join(dst, src.name)) + + # Create __init__.py with env registration + with open(os.path.join(dst, "__init__.py"), "w") as file: + file.write("from .simulator import Simulator\n\n") + file.write("from gymnasium.envs.registration import register\n") + file.write("import pathlib\n\n") + file.write("module_folder = pathlib.Path(__file__).parent\n") + file.write("simulator_folder = module_folder.parent\n") + file.write("kwargs = {'simulator_folder': simulator_folder}\n") + file.write("register(id=f'{module_folder.stem}-v0', entry_point=f'{module_folder.stem}.simulator:Simulator', kwargs=kwargs)\n") + + # Copy utils + shutil.copytree(os.path.join(parent_path(src), "utils"), os.path.join(simulator_folder, package_name, "utils"), + dirs_exist_ok=True) + # Copy train + shutil.copytree(os.path.join(parent_path(src), "train"), os.path.join(simulator_folder, package_name, "train"), + dirs_exist_ok=True) + # Copy test + shutil.copytree(os.path.join(parent_path(src), "test"), os.path.join(simulator_folder, package_name, "test"), + dirs_exist_ok=True) + + @classmethod + def _initialise(cls, config, simulator_folder, run_parameters): + """ Initialise a simulator -- i.e., create a MjModel, MjData, and initialise all necessary variables. + + Args: + config: A config dict. + simulator_folder: Location of the simulator. + run_parameters: Important run time variables that may also be used to override parameters. + """ + + # Get task class and kwargs + task_cls = cls.get_class("tasks", config["simulation"]["task"]["cls"]) + task_kwargs = config["simulation"]["task"].get("kwargs", {}) + + # Get bm class and kwargs + bm_cls = cls.get_class("bm_models", config["simulation"]["bm_model"]["cls"]) + bm_kwargs = config["simulation"]["bm_model"].get("kwargs", {}) + + # Initialise perception modules + perception_modules = {} + for module_cfg in config["simulation"].get("perception_modules", []): + module_cls = cls.get_class("perception", module_cfg["cls"]) + module_kwargs = module_cfg.get("kwargs", {}) + perception_modules[module_cls] = module_kwargs + + # Get simulation file + simulation_file = os.path.join(simulator_folder, config["package_name"], "simulation") + + # Load the mujoco model; try first with the binary model (faster, contains some parameters that may be lost when + # re-saving xml files like body mass). For some reason the binary model fails to load in some situations (like + # when the simulator has been built on a different computer) + # TODO loading from binary disabled, weird problems (like a body not found from model when loaded from binary, but + # found correctly when model loaded from xml) + # try: + # model = mujoco.MjModel.from_binary_path(simulation_file + ".mjcf") + # except: # TODO what was the exception type + model = mujoco.MjModel.from_xml_path(simulation_file + ".xml") + + # Initialise MjData + data = mujoco.MjData(model) + + # Add frame skip and dt to run parameters + run_parameters["frame_skip"] = int(1 / (model.opt.timestep * run_parameters["action_sample_freq"])) + run_parameters["dt"] = model.opt.timestep*run_parameters["frame_skip"] + + # Initialise a rendering context, required for e.g. some vision modules + run_parameters["rendering_context"] = Context(model, + max_resolution=run_parameters.get("max_resolution", [1280, 960])) + + # Initialise callbacks + callbacks = {} + for cb in run_parameters.get("callbacks", []): + callbacks[cb["name"]] = cls.get_class(cb["cls"])(cb["name"], **cb["kwargs"]) + + # Now initialise the actual classes; model and data are input to the inits so that stuff can be modified if needed + # (e.g. move target to a specific position wrt to a body part) + task = task_cls(model, data, **{**task_kwargs, **callbacks, **run_parameters}) + bm_model = bm_cls(model, data, **{**bm_kwargs, **callbacks, **run_parameters}) + perception = Perception(model, data, bm_model, perception_modules, {**callbacks, **run_parameters}) + + return model, data, task, bm_model, perception, callbacks + + @classmethod + def get(cls, simulator_folder, render_mode="rgb_array", render_mode_perception="embed", render_show_depths=False, run_parameters=None, use_cloned=True): + """ Returns a Simulator that is located in given folder. + + Args: + simulator_folder: Location of the simulator. + render_mode: Whether render() will return a single rgb array (render_mode="rgb_array"), + a list of rgb arrays (render_mode="rgb_array_list"; + adapted from https://github.com/openai/gym/blob/master/gym/wrappers/render_collection.py), + or None while the frames in a separate PyGame window are updated directly when calling + step() or reset() (render_mode="human"; + adapted from https://github.com/openai/gym/blob/master/gym/wrappers/human_rendering.py)). + render_mode_perception: Whether images of visual perception modules should be directly embedded into main camera view ("embed"), stored as separate videos ("separate"), or not used at all [which allows to watch vision in Unity Editor if debug mode is enabled/standalone app is disabled] (None) + render_show_depths: Whether depth images of visual perception modules should be included in rendering. + run_parameters: Can be used to override parameters during run time. + use_cloned: Can be useful for debugging. Set to False to use original files instead of the ones that have been + cloned/copied during building phase. + """ + + # Read config file + config_file = os.path.join(simulator_folder, "config.yaml") + try: + config = parse_yaml(config_file) + except: + raise FileNotFoundError(f"Could not open file {config_file}") + + # Make sure the simulator has been built + if "built" not in config: + raise RuntimeError("Simulator has not been built") + + # Make sure simulator_folder is in path (used to import gen_cls_cloned) + if simulator_folder not in sys.path: + sys.path.insert(0, simulator_folder) + + # Get Simulator class + gen_cls_cloned = getattr(importlib.import_module(config["package_name"]), "Simulator") + if hasattr(gen_cls_cloned, "version"): + _legacy_mode = False + gen_cls_cloned_version = gen_cls_cloned.version.split("-v")[-1] + else: + _legacy_mode = True + gen_cls_cloned_version = gen_cls_cloned.id.split("-v")[-1] #deprecated + if use_cloned: + gen_cls = gen_cls_cloned + else: + gen_cls = cls + gen_cls_version = gen_cls.version.split("-v")[-1] + + if gen_cls_version.split(".")[0] > gen_cls_cloned_version.split(".")[0]: + raise RuntimeError( + f"""Severe version mismatch. The simulator '{config["simulator_name"]}' has version {gen_cls_cloned_version}, while your uitb package has version {gen_cls_version}.\nTo run with version {gen_cls_cloned_version}, set 'use_cloned=True'.""") + elif gen_cls_version.split(".")[1] > gen_cls_cloned_version.split(".")[1]: + print( + f"""WARNING: Version mismatch. The simulator '{config["simulator_name"]}' has version {gen_cls_cloned_version}, while your uitb package has version {gen_cls_version}.\nTo run with version {gen_cls_version}, set 'use_cloned=True'.""") + + if _legacy_mode: + _simulator = gen_cls(simulator_folder, run_parameters=run_parameters) + else: + try: + _simulator = gen_cls(simulator_folder, render_mode=render_mode, render_mode_perception=render_mode_perception, render_show_depths=render_show_depths, + run_parameters=run_parameters) + except TypeError: + _simulator = gen_cls(simulator_folder, render_mode=render_mode, render_show_depths=render_show_depths, + run_parameters=run_parameters) + + # Return Simulator object + return _simulator + + def __init__(self, simulator_folder, render_mode="rgb_array", render_mode_perception="embed", render_show_depths=False, run_parameters=None): + """ Initialise a new `Simulator`. + + Args: + simulator_folder: Location of a simulator. + render_mode: Whether render() will return a single rgb array (render_mode="rgb_array"), + a list of rgb arrays (render_mode="rgb_array_list"; + adapted from https://github.com/openai/gym/blob/master/gym/wrappers/render_collection.py), + or None while the frames in a separate PyGame window are updated directly when calling + step() or reset() (render_mode="human"; + adapted from https://github.com/openai/gym/blob/master/gym/wrappers/human_rendering.py)). + render_mode_perception: Whether images of visual perception modules should be directly embedded into main camera view ("embed"), stored as separate videos ("separate"), or not used at all [which allows to watch vision in Unity Editor if debug mode is enabled/standalone app is disabled] (None) + render_show_depths: Whether depth images of visual perception modules should be included in rendering. + run_parameters: Can be used to override parameters during run time. + """ + + # Make sure simulator exists + if not os.path.exists(simulator_folder): + raise FileNotFoundError(f"Simulator folder {simulator_folder} does not exists") + self._simulator_folder = simulator_folder + + # Read config + self._config = parse_yaml(os.path.join(self._simulator_folder, "config.yaml")) + + # Get run parameters: these parameters can be used to override parameters used during training + self._run_parameters = self._config["simulation"]["run_parameters"].copy() + self._run_parameters.update(run_parameters or {}) + + # Initialise simulation + self._model, self._data, self.task, self.bm_model, self.perception, self.callbacks = \ + self._initialise(self._config, self._simulator_folder, self._run_parameters) + + # Set action space TODO for now we assume all actuators have control signals between [-1, 1] + self.action_space = self._initialise_action_space() + + # Set observation space + self.observation_space = self._initialise_observation_space() + + # Collect some episode statistics + self._episode_statistics = {"length (seconds)": 0, "length (steps)": 0, "reward": 0} + + # Initialise viewer + self._GUI_camera = Camera(self._run_parameters["rendering_context"], self._model, self._data, camera_id='for_testing', + dt=self._run_parameters["dt"]) + + self._render_mode = render_mode + self._render_mode_perception = render_mode_perception #whether perception camera views should be directly embedded into camera view of camera_id ("embed"), stored in self._render_stack_perception ("separate"), or not used at all "separate"), or not used at all [which allows to watch vision in Unity Editor if debug mode is enabled/standalone app is disabled] (None) + self._render_stack = [] #only used if render_mode == "rgb_array_list" + self._render_stack_perception = defaultdict(list) #only used if render_mode == "rgb_array_list" and self._render_mode_perception == "separate" + self._render_stack_pop = True #If True, clear the render stack after .render() is called. + self._render_stack_clean_at_reset = True #If True, clear the render stack when .reset() is called. + self._render_show_depths = render_show_depths #If True, depth images of visual perception modules are included in GUI rendering. + self._render_screen_size = None #only used if render_mode == "human" + self._render_window = None #only used if render_mode == "human" + self._render_clock = None #only used if render_mode == "human" + + if 'llc' in self.config: #if HRL approach is used + llc_simulator_folder = os.path.join(output_path(), self.config["llc"]["simulator_name"]) + if llc_simulator_folder not in sys.path: + sys.path.insert(0, llc_simulator_folder) + if not os.path.exists(llc_simulator_folder): + raise FileNotFoundError(f"Simulator folder {llc_simulator_folder} does not exists") + llccheckpoint_dir = os.path.join(llc_simulator_folder, 'checkpoints') + # Load policy TODO should create a load method for uitb.rl.BaseRLModel + print(f'Loading model: {os.path.join(llccheckpoint_dir, self.config["llc"]["checkpoint"])}\n') + self.llc_model = PPO.load(os.path.join(llccheckpoint_dir, self.config["llc"]["checkpoint"])) + self.action_space = self._initialise_HRL_action_space() + self._max_steps = self.config["llc"]["llc_ratio"] + self._dwell_threshold = int(0.5*self._max_steps) + self._target_radius = 0.05 + self._independent_dofs = [] + self._independent_joints = [] + joints = self.config["llc"]["joints"] + for joint in joints: + joint_id = mujoco.mj_name2id(self._model, mujoco.mjtObj.mjOBJ_JOINT, joint) + if self._model.jnt_type[joint_id] not in [mujoco.mjtJoint.mjJNT_HINGE, mujoco.mjtJoint.mjJNT_SLIDE]: + raise NotImplementedError(f"Only 'hinge' and 'slide' joints are supported, joint " + f"{joint} is of type {mujoco.mjtJoint(self._model.jnt_type[joint_id]).name}") + self._independent_dofs.append(self._model.jnt_qposadr[joint_id]) + self._independent_joints.append(joint_id) + self._jnt_range = self._model.jnt_range[self._independent_joints] + + + #To normalize joint ranges for llc + def _normalise_qpos(self, qpos): + # Normalise to [0, 1] + qpos = (qpos - self._jnt_range[:, 0]) / (self._jnt_range[:, 1] - self._jnt_range[:, 0]) + # Normalise to [-1, 1] + qpos = (qpos - 0.5) * 2 + return qpos + + def _initialise_action_space(self): + """ Initialise action space. """ + num_actuators = self.bm_model.nu + self.perception.nu + actuator_limits = np.ones((num_actuators,2)) * np.array([-1.0, 1.0]) + return spaces.Box(low=np.float32(actuator_limits[:, 0]), high=np.float32(actuator_limits[:, 1])) + + def _initialise_HRL_action_space(self): + bm_nu = self.bm_model.nu + bm_jnt_range = np.ones((bm_nu,2)) * np.array([-1.0, 1.0]) + perception_nu = self.perception.nu + perception_jnt_range = np.ones((perception_nu,2)) * np.array([-1.0, 1.0]) + jnt_range = np.concatenate((bm_jnt_range, perception_jnt_range), axis=0) + action_space = gym.spaces.Box(low=jnt_range[:,0], high=jnt_range[:,1]) + return action_space + + def _initialise_observation_space(self): + """ Initialise observation space. """ + observation = self.get_observation() + obs_dict = dict() + for module in self.perception.perception_modules: + obs_dict[module.modality] = spaces.Box(dtype=np.float32, **module.get_observation_space_params()) + if "stateful_information" in observation: + obs_dict["stateful_information"] = spaces.Box(dtype=np.float32, + **self.task.get_stateful_information_space_params()) + return spaces.Dict(obs_dict) + + def _get_qpos(self, model, data): + qpos = data.qpos[self._independent_dofs].copy() + qpos = self._normalise_qpos(qpos) + return qpos + + def step(self, action): + """ Step simulation forward with given actions. + + Args: + action: Actions sampled from a policy. Limited to range [-1, 1]. + """ + if 'llc' in self.config: #if HRL approach is used + self.task._target_qpos = action # action to pass to LLC + self._steps = 0 # Initialise loop control to 0 + #acc_reward = 0 #To be used when rewards are being accumulated in llc steps + + while self._steps < self._max_steps: # loop for llc controls based on llc_ratio + + llc_action, _states = self.llc_model.predict(self.get_llcobservation(action), deterministic=True) # Get BM action from LLC + # Set control for the bm model + self.bm_model.set_ctrl(self._model, self._data, llc_action) + + # Set control for perception modules (e.g. eye movements) + self.perception.set_ctrl(self._model, self._data, action[self.bm_model.nu:]) + + # Advance the simulation + mujoco.mj_step(self._model, self._data, nstep=int(self._run_parameters["frame_skip"])) # Number of timesteps to skip for LLC + + # Update bm model (e.g. update constraints); updates also effort model + self.bm_model.update(self._model, self._data) + + # Update perception modules + self.perception.update(self._model, self._data) + + dist = np.abs(action - self._get_qpos(self._model, self._data)) + + # Update environment + reward, terminated, truncated, info = self.task.update(self._model, self._data) + + # Add an effort cost to reward + reward -= self.bm_model.get_effort_cost(self._model, self._data) + + #acc_reward += reward #To be used when rewards are being accumulated in llc steps + + # Get observation + obs = self.get_observation() + + # Add frame to stack + if self._render_mode == "rgb_array_list": + self._render_stack.append(self._GUI_rendering()) + elif self._render_mode == "human": + self._GUI_rendering_pygame() + + if truncated or terminated: + break + + # Pointing + if "target_spawned" in info: + if info["target_spawned"] or info["target_hit"]: + break + + # Choice Reaction + elif "new_button_generated" in info: + if info["new_button_generated"] or info["target_hit"]: + break + + self._steps += 1 + if np.all(dist < self._target_radius): + break + + return obs, reward, terminated, truncated, info + + else: + # Set control for the bm model + self.bm_model.set_ctrl(self._model, self._data, action[:self.bm_model.nu]) + + # Set control for perception modules (e.g. eye movements) + self.perception.set_ctrl(self._model, self._data, action[self.bm_model.nu:]) + + # Advance the simulation + mujoco.mj_step(self._model, self._data, nstep=self._run_parameters["frame_skip"]) + + # Update bm model (e.g. update constraints); updates also effort model + self.bm_model.update(self._model, self._data) + + # Update perception modules + self.perception.update(self._model, self._data) + + # Update environment + reward, terminated, truncated, info = self.task.update(self._model, self._data) + + # Add an effort cost to reward + effort_cost = self.bm_model.get_effort_cost(self._model, self._data) + info["EffortCost"] = effort_cost + reward -= effort_cost + + # Get observation + obs = self.get_observation(info) + + # Add frame to stack + if self._render_mode == "rgb_array_list": + self._render_stack.append(self._GUI_rendering()) + elif self._render_mode == "human": + self._GUI_rendering_pygame() + + return obs, reward, terminated, truncated, info + + + def get_observation(self, info=None): + """ Returns an observation from the perception model. + + Returns: + A dict with observations from individual perception modules. May also contain stateful information from a task. + """ + + # Get observation from perception + observation = self.perception.get_observation(self._model, self._data, info) + + # Add any stateful information that is required + stateful_information = self.task.get_stateful_information(self._model, self._data) + if stateful_information.size > 0: #TODO: define stateful_information (and encoder) that can be used as default, if no stateful information is provided (zero-size arrays do not work with sb3 currently...) + observation["stateful_information"] = stateful_information + + return observation + + def get_llcobservation(self,action): + """ Returns an observation from the perception model. + + Returns: + A dict with observations from individual perception modules. May also contain stateful information from a task. + """ + + # Get observation from perception + observation = self.perception.get_observation(self._model, self._data) + + # Remove Vision for LLC + observation.pop("vision") + qpos = self._get_qpos(self._model, self._data) + qpos_diff = action - qpos + + # Stateful Information for LLC policy + stateful_information = qpos_diff + if stateful_information is not None: + observation["stateful_information"] = stateful_information + + return observation + + + def reset(self, seed=None): + """ Reset the simulator and return an observation. """ + + super().reset(seed=seed) + + # Reset sim + mujoco.mj_resetData(self._model, self._data) + + # Reset all models + self.bm_model.reset(self._model, self._data) + self.perception.reset(self._model, self._data) + info = self.task.reset(self._model, self._data) + + # Do a forward so everything will be set + mujoco.mj_forward(self._model, self._data) + + if self._render_mode == "rgb_array_list": + if self._render_stack_clean_at_reset: + self._render_stack = [] + self._render_stack_perception = defaultdict(list) + self._render_stack.append(self._GUI_rendering()) + elif self._render_mode == "human": + self._GUI_rendering_pygame() + + return self.get_observation(), info + + def render(self): + if self._render_mode == "rgb_array_list": + render_stack = self._render_stack + if self._render_stack_pop: + self._render_stack = [] + return render_stack + elif self._render_mode == "rgb_array": + return self._GUI_rendering() + else: + return None + + def get_render_stack_perception(self): + render_stack_perception = self._render_stack_perception + # if self._render_stack_pop: + # self._render_stack_perception = defaultdict(list) + return render_stack_perception + + def _GUI_rendering(self): + # Grab an image from the 'for_testing' camera and grab all GUI-prepared images from included visual perception modules, and display them 'picture-in-picture' + + # Grab images + img, _ = self._GUI_camera.render() + + if self._render_mode_perception == "embed": + # Embed perception camera images into main camera image + + # perception_camera_images = [rgb_or_depth_array for camera in self.perception.cameras + # for rgb_or_depth_array in camera.render() if rgb_or_depth_array is not None] + perception_camera_images = self.perception.get_renders() + + # TODO: add text annotations to perception camera images + if len(perception_camera_images) > 0: + _img_size = img.shape[:2] #(height, width) + + + # Vertical alignment of perception camera images, from bottom right to top right + ## TODO: allow for different inset locations + _desired_subwindow_height = np.round(_img_size[0] / len(perception_camera_images)).astype(int) + _maximum_subwindow_width = np.round(0.2 * _img_size[1]).astype(int) + + perception_camera_images_resampled = [] + for ocular_img in perception_camera_images: + # Convert 2D depth arrays to 3D heatmap arrays + if ocular_img.ndim == 2: + if self._render_show_depths: + ocular_img = matplotlib.pyplot.imshow(ocular_img, cmap=matplotlib.pyplot.cm.jet, interpolation='bicubic').make_image('TkAgg', unsampled=True)[0][ + ..., :3] + matplotlib.pyplot.close() #delete image + else: + continue + + resample_factor = min(_desired_subwindow_height / ocular_img.shape[0], _maximum_subwindow_width / ocular_img.shape[1]) + + resample_height = np.round(ocular_img.shape[0] * resample_factor).astype(int) + resample_width = np.round(ocular_img.shape[1] * resample_factor).astype(int) + resampled_img = np.zeros((resample_height, resample_width, ocular_img.shape[2]), dtype=np.uint8) + for channel in range(ocular_img.shape[2]): + resampled_img[:, :, channel] = scipy.ndimage.zoom(ocular_img[:, :, channel], resample_factor, order=0) + + perception_camera_images_resampled.append(resampled_img) + + ocular_img_bottom = _img_size[0] + for ocular_img_idx, ocular_img in enumerate(perception_camera_images_resampled): + #print(f"Modify ({ocular_img_bottom - ocular_img.shape[0]}, { _img_size[1] - ocular_img.shape[1]})-({ocular_img_bottom}, {img.shape[1]}).") + img[ocular_img_bottom - ocular_img.shape[0]:ocular_img_bottom, _img_size[1] - ocular_img.shape[1]:] = ocular_img + ocular_img_bottom -= ocular_img.shape[0] + # input((len(perception_camera_images_resampled), perception_camera_images_resampled[0].shape, img.shape)) + elif self._render_mode_perception == "separate": + for module, camera_list in self.perception.cameras_dict.items(): + for camera in camera_list: + for rgb_or_depth_array in camera.render(): + if rgb_or_depth_array is not None: + self._render_stack_perception[f"{module.modality}/{type(camera).__name__}"].append(rgb_or_depth_array) + + return img + + def _GUI_rendering_pygame(self): + rgb_array = np.transpose(self._GUI_rendering(), axes=(1, 0, 2)) + + if self._render_screen_size is None: + self._render_screen_size = rgb_array.shape[:2] + + assert self._render_screen_size == rgb_array.shape[ + :2], f"Expected an rgb array of shape {self._render_screen_size} from self._GUI_camera, but received an rgb array of shape {rgb_array.shape[:2]}. " + + if self._render_window is None: + pygame.init() + pygame.display.init() + self._render_window = pygame.display.set_mode(self._render_screen_size) + + if self._render_clock is None: + self._render_clock = pygame.time.Clock() + + surf = pygame.surfarray.make_surface(rgb_array) + self._render_window.blit(surf, (0, 0)) + pygame.event.pump() + self._render_clock.tick(self.fps) + pygame.display.flip() + + def close(self): + """ Close the rendering window (if self._render_mode == 'human').""" + super().close() + if self._render_window is not None: + import pygame + + pygame.display.quit() + pygame.quit() + + @property + def fps(self): + return self._GUI_camera._fps + + def callback(self, callback_name, num_timesteps): + """ Update a callback -- may be useful during training, e.g. for curriculum learning. """ + self.callbacks[callback_name].update(num_timesteps) + + def update_callbacks(self, num_timesteps): + """ Update all callbacks. """ + for callback_name in self.callbacks: + self.callback(callback_name, num_timesteps) + + # def get_logdict_keys(self): + # return list(self.task._info["log_dict"].keys()) + + # def get_logdict_value(self, key): + # return self.task._info["log_dict"].get(key) + + @property + def config(self): + """ Return config. """ + return copy.deepcopy(self._config) + + @property + def run_parameters(self): + """ Return run parameters. """ + # Context cannot be deep copied + exclude = {"rendering_context"} + run_params = {k: copy.deepcopy(self._run_parameters[k]) for k in self._run_parameters.keys() - exclude} + run_params["rendering_context"] = self._run_parameters["rendering_context"] + return run_params + + @property + def simulator_folder(self): + """ Return simulator folder. """ + return self._simulator_folder + + @property + def render_mode(self): + """ Return render mode. """ + return self._render_mode + + def get_state(self): + """ Return a state of the simulator / individual components (biomechanical model, perception model, task). + + This function is used for logging/evaluation purposes, not for RL training. + + Returns: + A dict with one float or numpy vector per keyword. + """ + + # Get time, qpos, qvel, qacc, act_force, act, ctrl of the current simulation + state = {"timestep": self._data.time, + "qpos": self._data.qpos.copy(), + "qvel": self._data.qvel.copy(), + "qacc": self._data.qacc.copy(), + "act_force": self._data.actuator_force.copy(), + "act": self._data.act.copy(), + "ctrl": self._data.ctrl.copy()} + + # Add state from the task + state.update(self.task.get_state(self._model, self._data)) + + # Add state from the biomechanical model + state.update(self.bm_model.get_state(self._model, self._data)) + + # Add state from the perception model + state.update(self.perception.get_state(self._model, self._data)) + + return state + + def close(self, **kwargs): + """ Perform any necessary clean up. + + This function is inherited from gym.Env. It should be automatically called when this object is garbage collected + or the program exists, but that doesn't seem to be the case. This function will be called if this object has been + initialised in the context manager fashion (i.e. using the 'with' statement). """ + self.task.close(**kwargs) + self.perception.close(**kwargs) + self.bm_model.close(**kwargs) diff --git a/src/box/test.simulator.py b/src/box/test.simulator.py new file mode 100644 index 0000000000..06f94e93fd --- /dev/null +++ b/src/box/test.simulator.py @@ -0,0 +1,25 @@ +from uitb.simulator import Simulator + +# 加载仿真器(需替换为实际仿真器路径,如 "uitb/simulators/arm_simulation") +simulator_folder = "uitb/simulators/your_sim_folder" +env = Simulator.get( + simulator_folder=simulator_folder, + render_mode="human", # "human" 弹出Pygame窗口;"rgb_array" 输出图片数组 + render_mode_perception="embed" # 感知图像嵌入主窗口 +) + +# 重置环境 +obs, info = env.reset(seed=42) +print("初始观测值:", {k: v.shape for k in obs}) + +# 运行 1000 步仿真(随机动作示例) +for step in range(1000): + action = env.action_space.sample() + obs, reward, terminated, truncated, info = env.step(action) + if step % 100 == 0: + print(f"第 {step} 步 | 奖励:{reward:.2f} | 终止状态:{terminated}") + if terminated or truncated: + obs, info = env.reset() + +# 关闭仿真(释放资源) +env.close() From 594f0c2afeeb6424f065082fd37fb2c9ee4fd5ec Mon Sep 17 00:00:00 2001 From: Xinyue-cao <648343923@qq.com> Date: Mon, 8 Dec 2025 19:03:55 +0800 Subject: [PATCH 02/14] =?UTF-8?q?=E9=85=8D=E7=BD=AE=E5=8C=96=20HRL=20?= =?UTF-8?q?=E5=8F=82=E6=95=B0,=E6=80=A7=E8=83=BD=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 10 + src/box/1.yaml | 7 + src/box/README.md | 60 + src/box/__pycache__/simulator.cpython-313.pyc | Bin 0 -> 44564 bytes src/box/main.py | 15 + .../__pycache__/base.cpython-313.pyc | Bin 0 -> 5474 bytes src/box/perception/_init_.py | 5 + src/box/perception/base.py | 109 + src/box/perception/joint_state.py | 77 + src/box/{mian.py => perception/vision.py} | 0 src/box/simulator.py | 1754 ++++++++++------- .../__pycache__/functions.cpython-313.pyc | Bin 0 -> 1905 bytes .../__pycache__/rendering.cpython-313.pyc | Bin 0 -> 1563 bytes src/box/utils/functions.py | 25 + src/box/utils/rendering.py | 26 + 15 files changed, 1325 insertions(+), 763 deletions(-) create mode 100644 pyproject.toml create mode 100644 src/box/1.yaml create mode 100644 src/box/README.md create mode 100644 src/box/__pycache__/simulator.cpython-313.pyc create mode 100644 src/box/main.py create mode 100644 src/box/perception/__pycache__/base.cpython-313.pyc create mode 100644 src/box/perception/_init_.py create mode 100644 src/box/perception/base.py create mode 100644 src/box/perception/joint_state.py rename src/box/{mian.py => perception/vision.py} (100%) create mode 100644 src/box/utils/__pycache__/functions.cpython-313.pyc create mode 100644 src/box/utils/__pycache__/rendering.cpython-313.pyc create mode 100644 src/box/utils/functions.py create mode 100644 src/box/utils/rendering.py diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000..ccec03060b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,10 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "your-package-name" +version = "0.1.0" +authors = [{"name" = "Your Name", "email" = "your@email.com"}] +description = "A sample package" +requires-python = ">=3.8" \ No newline at end of file diff --git a/src/box/1.yaml b/src/box/1.yaml new file mode 100644 index 0000000000..431f70d5e4 --- /dev/null +++ b/src/box/1.yaml @@ -0,0 +1,7 @@ +llc: + simulator_name: "llc_simulator" + checkpoint: "best_model.zip" + llc_ratio: 100 + dwell_threshold: 50 + target_radius: 0.05 + joints: ["joint1", "joint2"] \ No newline at end of file diff --git a/src/box/README.md b/src/box/README.md new file mode 100644 index 0000000000..a4f32c0853 --- /dev/null +++ b/src/box/README.md @@ -0,0 +1,60 @@ +user-in-the-box-simulator +基于Gymnasium和MuJoCo的仿真器,集成生物力学模型、感知模块与强化学习任务,支持分层强化学习与可视化渲染 + +一、环境准备 +1.虚拟环境创建与激活 +# 切换到项目根目录 +cd C:\Users\86186\user-in-the-box + +# 创建Python 3.9虚拟环境(兼容性最优) +python -m venv venv --python=3.9 + +# 激活虚拟环境(Windows PowerShell) +\venv\Scripts\Activate.ps1 +2.依赖安装 使用国内镜像源加速安装: +# 核心依赖 +pip install gymnasium==1.2.1 mujoco==2.3.5 stable-baselines3==2.2.1 pygame==2.5.2 opencv-python==4.9.0.80 -i https://pypi.tuna.tsinghua.edu.cn/simple + +# 辅助依赖 +pip install numpy==1.26.4 scipy==1.11.4 matplotlib==3.8.4 ruamel.yaml==0.18.6 certifi -i https://pypi.tuna.tsinghua.edu.cn/simple + +二、核心文件说明 + +1.simulator.py(仿真器核心) +功能:继承 gym.Env,实现仿真环境的初始化、步骤推进(step)、环境重置(reset)和可视化渲染(render),集成生物力学模型、感知模块和任务逻辑。 运行方式:需通过调用脚本(如 test_simulator.py)运行,示例见 “三、运行步骤”。 +2.main.py(辅助脚本) 功能:基于 certifi 查询 CA 证书信息(路径或内容),用于验证网络请求的安全性。 +运行方式: +# 查看证书路径 +python main.py + +# 查看证书内容 +python main.py -c +三、运行步骤 + +仿真器运行(simulator.py) +步骤 1:运行脚本 test_simulator.py + +步骤 2:执行脚本 + +python test_simulator.py +此时会弹出 Pygame 窗口,展示仿真过程(如机械臂运动、感知模块渲染)。 2. 辅助脚本运行(main.py) 在终端执行以下命令,查看证书信息: + +# 查看证书路径 +python main.py + +# 查看证书内容 +python main.py -c + +四、依赖清单 +|库名称 |版本 |用途| +|------|-------|----| +|gymnasium| 1.2.1 |强化学习环境接口| +|mujoco|2.3.5|物理仿真引擎| +|stable-baselines3|2.2.1|强化学习算法库| +|pygame |2.5.2|可视化渲染| +|opencv-python|4.9.0.80|图像感知处理| +|numpy| 1.26.4| 数值计算| +|scipy| 1.11.4|科学计算| +|matplotlib|3.8.4|数据可视化| +|ruamel.yaml|0.18.6 |配置文件解析| +|certifi |2025.10.10 |CA 证书管理| \ No newline at end of file diff --git a/src/box/__pycache__/simulator.cpython-313.pyc b/src/box/__pycache__/simulator.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..233c64a4e9faf67a35d1d74bec72665a4015a211 GIT binary patch literal 44564 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 literal 0 HcmV?d00001 diff --git a/src/box/main.py b/src/box/main.py new file mode 100644 index 0000000000..96f58810d7 --- /dev/null +++ b/src/box/main.py @@ -0,0 +1,15 @@ +import argparse +from certifi import where + +parser = argparse.ArgumentParser() +parser.add_argument("-c", "--contents", action="store_true", help="查看证书文件内容") +args = parser.parse_args() + +if args.contents: + # 读取 certifi 证书文件内容 + cert_path = where() + with open(cert_path, "r", encoding="utf-8") as f: + print(f.read()) +else: + # 打印证书文件路径 + print(where()) \ No newline at end of file diff --git a/src/box/perception/__pycache__/base.cpython-313.pyc b/src/box/perception/__pycache__/base.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a22b27019fd7ab8ee8fb438d3155d8893a024879 GIT binary patch literal 5474 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 ZZ2F$B=WNdji0}7v`#7#PNl-F1{s&J6hNA!g literal 0 HcmV?d00001 diff --git a/src/box/perception/_init_.py b/src/box/perception/_init_.py new file mode 100644 index 0000000000..972cded186 --- /dev/null +++ b/src/box/perception/_init_.py @@ -0,0 +1,5 @@ +from .base import Perception +from .vision import VisionPerception +from .joint_state import JointStatePerception + +__all__ = ["Perception", "VisionPerception", "JointStatePerception"] \ No newline at end of file diff --git a/src/box/perception/base.py b/src/box/perception/base.py new file mode 100644 index 0000000000..3033ccc71b --- /dev/null +++ b/src/box/perception/base.py @@ -0,0 +1,109 @@ +import numpy as np +import mujoco +from collections import defaultdict + +class PerceptionModule: + """感知子模块基类,所有具体感知模块需继承此类""" + def __init__(self, modality, model, data, **kwargs): + self.modality = modality # 感知模态名称(如vision/joint_state) + self.model = model # MuJoCo模型 + self.data = data # MuJoCo数据 + self.kwargs = kwargs # 模块参数 + self.cameras = [] # 关联的相机(视觉类模块用) + + def reset(self, model, data): + """重置感知模块状态""" + self.model = model + self.data = data + + def update(self, model, data): + """每步更新感知数据""" + pass + + def get_observation(self, model, data, info=None): + """获取该模块的观测数据(需子类实现)""" + raise NotImplementedError + + def get_observation_space_params(self): + """获取Gymnasium观测空间参数(需子类实现)""" + raise NotImplementedError + + def get_renders(self): + """获取可视化渲染结果(视觉类模块用)""" + return [] + + def close(self): + """释放资源""" + pass + + +class Perception: + """感知总控制器,管理所有感知子模块,适配仿真器架构""" + def __init__(self, model, data, bm_model, perception_modules, common_kwargs): + self.model = model + self.data = data + self.bm_model = bm_model # 关联生物力学模型 + self.common_kwargs = common_kwargs # 通用参数(如dt、callbacks) + + # 初始化感知子模块 + self.perception_modules = [] + self.cameras_dict = defaultdict(list) # 相机映射(适配渲染) + self.nu = 0 # 感知模块动作维度(无动作则为0,保持与仿真器兼容) + + # 遍历配置的感知模块,初始化实例 + for module_cls, module_kwargs in perception_modules.items(): + module = module_cls( + model=model, + data=data, + **module_kwargs + ) + self.perception_modules.append(module) + # 关联相机(用于渲染) + if hasattr(module, 'cameras') and module.cameras: + self.cameras_dict[module] = module.cameras + + def reset(self, model, data): + """重置所有感知模块""" + self.model = model + self.data = data + for module in self.perception_modules: + module.reset(model, data) + + def update(self, model, data): + """每步更新所有感知模块""" + self.model = model + self.data = data + for module in self.perception_modules: + module.update(model, data) + + def get_observation(self, model, data, info=None): + """收集所有感知模块的观测数据,返回字典(适配仿真器观测空间)""" + observation = {} + for module in self.perception_modules: + obs = module.get_observation(model, data, info) + if obs is not None: + observation[module.modality] = obs + return observation + + def get_state(self, model, data): + """获取感知模块状态(用于仿真器状态记录)""" + state = {} + for module in self.perception_modules: + state[f"perception_{module.modality}"] = module.get_observation(model, data) + return state + + def get_renders(self): + """获取所有视觉类模块的渲染结果(适配仿真器可视化)""" + renders = [] + for module in self.perception_modules: + renders.extend(module.get_renders()) + return renders + + def set_ctrl(self, model, data, ctrl): + """感知模块动作控制(无动作则空实现,保持与仿真器接口兼容)""" + pass + + def close(self, **kwargs): + """释放所有感知模块资源""" + for module in self.perception_modules: + module.close() \ No newline at end of file diff --git a/src/box/perception/joint_state.py b/src/box/perception/joint_state.py new file mode 100644 index 0000000000..c99eea72be --- /dev/null +++ b/src/box/perception/joint_state.py @@ -0,0 +1,77 @@ +import numpy as np +import mujoco +from .base import PerceptionModule + +class JointStatePerception(PerceptionModule): + """关节状态感知模块,采集关节角度、速度、力矩""" + def __init__(self, model, data, joint_names=None, include_velocity=True, + include_torque=True, normalize=True, **kwargs): + super().__init__(modality="joint_state", model=model, data=data,** kwargs) + + # 关节配置 + self.joint_names = joint_names or self._get_all_joints() # 默认所有关节 + self.include_velocity = include_velocity + self.include_torque = include_torque + self.normalize = normalize + + # 获取关节ID和范围(用于归一化) + self.joint_ids = [mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_JOINT, name) + for name in self.joint_names] + self.joint_ranges = model.jnt_range[self.joint_ids] # 关节角度范围(min, max) + + def _get_all_joints(self): + """获取模型中所有关节名称""" + joint_names = [] + for i in range(self.model.njnt): + name = mujoco.mj_id2name(self.model, mujoco.mjtObj.mjOBJ_JOINT, i) + if name: + joint_names.append(name) + return joint_names + + def _normalize_qpos(self, qpos): + """归一化关节角度到[-1, 1]""" + norm_qpos = (qpos - self.joint_ranges[:, 0]) / (self.joint_ranges[:, 1] - self.joint_ranges[:, 0] + 1e-8) + return (norm_qpos - 0.5) * 2 # 映射到[-1,1] + + def get_observation(self, model, data, info=None): + """获取关节状态观测(角度+速度+力矩)""" + # 关节角度 + qpos = data.qpos[[model.jnt_qposadr[jid] for jid in self.joint_ids]] + if self.normalize: + qpos = self._normalize_qpos(qpos) + + # 拼接观测 + obs_list = [qpos.astype(np.float32)] + + # 关节速度 + if self.include_velocity: + qvel = data.qvel[[model.jnt_dofadr[jid] for jid in self.joint_ids]] + # 速度归一化到[-1,1](基于经验范围) + if self.normalize: + qvel = np.clip(qvel / 10.0, -1.0, 1.0) + obs_list.append(qvel.astype(np.float32)) + + # 关节力矩 + if self.include_torque: + torque = data.qfrc_actuator[[model.jnt_dofadr[jid] for jid in self.joint_ids]] + # 力矩归一化到[-1,1] + if self.normalize: + torque = np.clip(torque / 50.0, -1.0, 1.0) + obs_list.append(torque.astype(np.float32)) + + return np.concatenate(obs_list) + + def get_observation_space_params(self): + """定义关节状态观测空间参数""" + # 计算观测维度 + dim = len(self.joint_ids) + if self.include_velocity: + dim += len(self.joint_ids) + if self.include_torque: + dim += len(self.joint_ids) + + return { + "low": -1.0 if self.normalize else -np.inf, + "high": 1.0 if self.normalize else np.inf, + "shape": (dim,) + } \ No newline at end of file diff --git a/src/box/mian.py b/src/box/perception/vision.py similarity index 100% rename from src/box/mian.py rename to src/box/perception/vision.py diff --git a/src/box/simulator.py b/src/box/simulator.py index 954b06b7a5..d647c8fe28 100644 --- a/src/box/simulator.py +++ b/src/box/simulator.py @@ -1,12 +1,16 @@ +import sys +import os +# 获取项目根目录的绝对路径 +project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../")) +# 将根目录加入Python路径 +sys.path.insert(0, project_root) import gymnasium as gym from gymnasium import spaces import pygame import mujoco -import os import numpy as np import scipy import matplotlib -import sys import importlib import shutil import inspect @@ -16,801 +20,1025 @@ from collections import defaultdict import xml.etree.ElementTree as ET -from stable_baselines3 import PPO #required to load a trained LLC policy in HRL approach +from stable_baselines3 import PPO # required to load a trained LLC policy in HRL approach -from .perception.base import Perception -from .utils.rendering import Camera, Context -from .utils.functions import output_path, parent_path, is_suitable_package_name, parse_yaml, write_yaml +# -------------------------- 修复导入路径 -------------------------- +# 统一使用项目根目录的绝对导入 +from src.box.perception.base import Perception +# 若rendering.py中无Camera/Context,需确认文件是否存在或类是否正确定义 +from src.box.utils.rendering import Camera, Context # 确保rendering.py中包含这两个类 +from src.box.utils.functions import output_path, parent_path, is_suitable_package_name, parse_yaml, write_yaml +# ------------------------------------------------------------------ class Simulator(gym.Env): - """ - The Simulator class contains functionality to build a standalone Python package from a config file. The built package - integrates a biomechanical model, a task model, and a perception model into one simulator that implements a gym.Env - interface. - """ - - # May be useful for later, the three digit number suffix is of format X.Y.Z where X is a major version. - version = "1.1.0" - - @classmethod - def get_class(cls, *args): - """ Returns a class from given strings. The last element in args should contain the class name. """ - # TODO check for incorrect module names etc - modules = ".".join(args[:-1]) - if "." in args[-1]: - splitted = args[-1].split(".") - if modules == "": - modules = ".".join(splitted[:-1]) - else: - modules += "." + ".".join(splitted[:-1]) - cls_name = splitted[-1] - else: - cls_name = args[-1] - module = cls.get_module(modules) - return getattr(module, cls_name) - - @classmethod - def get_module(cls, *args): - """ Returns a module from given strings. """ - src = __name__.split(".")[0] - modules = ".".join(args) - return importlib.import_module(src + "." + modules) - - @classmethod - def build(cls, config): - """ Builds a simulator based on a config. The input 'config' may be a dict (parsed from YAML) or path to a YAML file - - Args: - config: - - A dict containing configuration information. See example configs in folder uitb/configs/ - - A path to a config file """ - - # If config is a path to the config file, parse it first - if isinstance(config, str): - if not os.path.isfile(config): - raise FileNotFoundError(f"Given config file {config} does not exist") - config = parse_yaml(config) - - # Make sure required things are defined in config - assert "simulation" in config, "Simulation specs (simulation) must be defined in config" - assert "bm_model" in config["simulation"], "Biomechanical model (bm_model) must be defined in config" - assert "task" in config["simulation"], "task (task) must be defined in config" - - assert "run_parameters" in config["simulation"], "Run parameters (run_parameters) must be defined in config" - run_parameters = config["simulation"]["run_parameters"].copy() - assert "action_sample_freq" in run_parameters, "Action sampling frequency (action_sample_freq) must be defined " \ - "in run parameters" - - # Set simulator version - config["version"] = cls.version - - # Save generated simulators to uitb/simulators - if "simulator_folder" in config: - simulator_folder = os.path.normpath(config["simulator_folder"]) - else: - simulator_folder = os.path.join(output_path(), config["simulator_name"]) - - # If 'package_name' is not defined use 'simulator_name' - if "package_name" not in config: - config["package_name"] = config["simulator_name"] - if not is_suitable_package_name(config["package_name"]): - raise NameError("Package name defined in the config file (either through 'package_name' or 'simulator_name') is " - "not a suitable Python package name. Use only lower-case letters and underscores instead of " - "spaces, and the name cannot start with a number.") - - # The name used in gym has a suffix -v0 - config["gym_name"] = "uitb:" + config["package_name"] + "-v0" - - # Create a simulator in the simulator folder - cls._clone(simulator_folder, config["package_name"]) - - # Load task class - task_cls = cls.get_class("tasks", config["simulation"]["task"]["cls"]) - task_cls.clone(simulator_folder, config["package_name"], app_executable=config["simulation"]["task"].get("kwargs", {}).get("unity_executable", None)) - simulation = task_cls.initialise(config["simulation"]["task"].get("kwargs", {})) - - # Set some compiler options - # TODO: would make more sense to have a separate "environment" class / xml file that defines all these defaults, - # including e.g. cameras, lighting, etc., so that they could be easily changed. Task and biomechanical model would - # be integrated into that object - compiler_defaults = {"inertiafromgeom": "auto", "balanceinertia": "true", "boundmass": "0.001", - "boundinertia": "0.001", "inertiagrouprange": "0 1"} - compiler = simulation.find("compiler") - if compiler is None: - ET.SubElement(simulation, "compiler", compiler_defaults) - else: - compiler.attrib.update(compiler_defaults) - - # Load biomechanical model class - bm_cls = cls.get_class("bm_models", config["simulation"]["bm_model"]["cls"]) - bm_cls.clone(simulator_folder, config["package_name"]) - bm_cls.insert(simulation) - - # Add perception modules - for module_cfg in config["simulation"].get("perception_modules", []): - module_cls = cls.get_class("perception", module_cfg["cls"]) - module_kwargs = module_cfg.get("kwargs", {}) - module_cls.clone(simulator_folder, config["package_name"]) - module_cls.insert(simulation, **module_kwargs) - - # Clone also RL library files so the package will be completely standalone - rl_cls = cls.get_class("rl", config["rl"]["algorithm"]) - rl_cls.clone(simulator_folder, config["package_name"]) - - # TODO read the xml file directly from task.getroot() instead of writing it to a file first; need to input a dict - # of assets to mujoco.MjModel.from_xml_path - simulation_file = os.path.join(simulator_folder, config["package_name"], "simulation") - with open(simulation_file+".xml", 'w') as file: - simulation.write(file, encoding='unicode') - - # Initialise the simulator - model, _, _, _, _, _ = \ - cls._initialise(config, simulator_folder, {**run_parameters, "build": True}) - - # Now that simulator has been initialised, everything should be set. Now we want to save the xml file again, but - # mujoco only is able to save the latest loaded xml file (which is either the task or bm model xml files which are - # are read in their __init__ functions), hence we need to read the file we've generated again before saving the - # modified model - mujoco.MjModel.from_xml_path(simulation_file+".xml") - mujoco.mj_saveLastXML(simulation_file+".xml", model) - - # Save the modified model also as binary for faster loading - mujoco.mj_saveModel(model, simulation_file+".mjcf", None) - - # Input built time into config - config["built"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - - # Save config - write_yaml(config, os.path.join(simulator_folder, "config.yaml")) - - return simulator_folder - - @classmethod - def _clone(cls, simulator_folder, package_name): - """ Create a folder for the simulator being built, and copy or create relevant files. - - Args: - simulator_folder: Location of the simulator. - package_name: Name of the simulator (which is a python package). - """ - - # Create the folder - dst = os.path.join(simulator_folder, package_name) - os.makedirs(dst, exist_ok=True) - - # Copy simulator - src = pathlib.Path(inspect.getfile(cls)) - shutil.copyfile(src, os.path.join(dst, src.name)) - - # Create __init__.py with env registration - with open(os.path.join(dst, "__init__.py"), "w") as file: - file.write("from .simulator import Simulator\n\n") - file.write("from gymnasium.envs.registration import register\n") - file.write("import pathlib\n\n") - file.write("module_folder = pathlib.Path(__file__).parent\n") - file.write("simulator_folder = module_folder.parent\n") - file.write("kwargs = {'simulator_folder': simulator_folder}\n") - file.write("register(id=f'{module_folder.stem}-v0', entry_point=f'{module_folder.stem}.simulator:Simulator', kwargs=kwargs)\n") - - # Copy utils - shutil.copytree(os.path.join(parent_path(src), "utils"), os.path.join(simulator_folder, package_name, "utils"), - dirs_exist_ok=True) - # Copy train - shutil.copytree(os.path.join(parent_path(src), "train"), os.path.join(simulator_folder, package_name, "train"), - dirs_exist_ok=True) - # Copy test - shutil.copytree(os.path.join(parent_path(src), "test"), os.path.join(simulator_folder, package_name, "test"), - dirs_exist_ok=True) - - @classmethod - def _initialise(cls, config, simulator_folder, run_parameters): - """ Initialise a simulator -- i.e., create a MjModel, MjData, and initialise all necessary variables. - - Args: - config: A config dict. - simulator_folder: Location of the simulator. - run_parameters: Important run time variables that may also be used to override parameters. + The Simulator class contains functionality to build a standalone Python package from a config file. + The built package integrates a biomechanical model, a task model, and a perception model into one + simulator that implements a gym.Env interface. + + Key features: + - Support for Hierarchical Reinforcement Learning (HRL) with Low-Level Controller (LLC) + - MuJoCo simulation integration with Gymnasium API + - Configurable rendering (rgb_array, rgb_array_list, human) + - Modular design for perception, biomechanical and task models """ + # Version format: X.Y.Z (major.minor.patch) + version = "1.1.0" + + @classmethod + def get_class(cls, *args): + """ + Returns a class from given module path strings. + The last element in args should contain the class name. + + Args: + *args: Module path components + class name + + Returns: + type: Requested class object + """ + # Handle module path and class name parsing + modules = ".".join(args[:-1]) + if "." in args[-1]: + splitted = args[-1].split(".") + if modules == "": + modules = ".".join(splitted[:-1]) + else: + modules += "." + ".".join(splitted[:-1]) + cls_name = splitted[-1] + else: + cls_name = args[-1] + + module = cls.get_module(modules) + return getattr(module, cls_name) - # Get task class and kwargs - task_cls = cls.get_class("tasks", config["simulation"]["task"]["cls"]) - task_kwargs = config["simulation"]["task"].get("kwargs", {}) - - # Get bm class and kwargs - bm_cls = cls.get_class("bm_models", config["simulation"]["bm_model"]["cls"]) - bm_kwargs = config["simulation"]["bm_model"].get("kwargs", {}) - - # Initialise perception modules - perception_modules = {} - for module_cfg in config["simulation"].get("perception_modules", []): - module_cls = cls.get_class("perception", module_cfg["cls"]) - module_kwargs = module_cfg.get("kwargs", {}) - perception_modules[module_cls] = module_kwargs - - # Get simulation file - simulation_file = os.path.join(simulator_folder, config["package_name"], "simulation") - - # Load the mujoco model; try first with the binary model (faster, contains some parameters that may be lost when - # re-saving xml files like body mass). For some reason the binary model fails to load in some situations (like - # when the simulator has been built on a different computer) - # TODO loading from binary disabled, weird problems (like a body not found from model when loaded from binary, but - # found correctly when model loaded from xml) - # try: - # model = mujoco.MjModel.from_binary_path(simulation_file + ".mjcf") - # except: # TODO what was the exception type - model = mujoco.MjModel.from_xml_path(simulation_file + ".xml") - - # Initialise MjData - data = mujoco.MjData(model) - - # Add frame skip and dt to run parameters - run_parameters["frame_skip"] = int(1 / (model.opt.timestep * run_parameters["action_sample_freq"])) - run_parameters["dt"] = model.opt.timestep*run_parameters["frame_skip"] - - # Initialise a rendering context, required for e.g. some vision modules - run_parameters["rendering_context"] = Context(model, - max_resolution=run_parameters.get("max_resolution", [1280, 960])) - - # Initialise callbacks - callbacks = {} - for cb in run_parameters.get("callbacks", []): - callbacks[cb["name"]] = cls.get_class(cb["cls"])(cb["name"], **cb["kwargs"]) - - # Now initialise the actual classes; model and data are input to the inits so that stuff can be modified if needed - # (e.g. move target to a specific position wrt to a body part) - task = task_cls(model, data, **{**task_kwargs, **callbacks, **run_parameters}) - bm_model = bm_cls(model, data, **{**bm_kwargs, **callbacks, **run_parameters}) - perception = Perception(model, data, bm_model, perception_modules, {**callbacks, **run_parameters}) - - return model, data, task, bm_model, perception, callbacks - - @classmethod - def get(cls, simulator_folder, render_mode="rgb_array", render_mode_perception="embed", render_show_depths=False, run_parameters=None, use_cloned=True): - """ Returns a Simulator that is located in given folder. - - Args: - simulator_folder: Location of the simulator. - render_mode: Whether render() will return a single rgb array (render_mode="rgb_array"), - a list of rgb arrays (render_mode="rgb_array_list"; - adapted from https://github.com/openai/gym/blob/master/gym/wrappers/render_collection.py), - or None while the frames in a separate PyGame window are updated directly when calling - step() or reset() (render_mode="human"; - adapted from https://github.com/openai/gym/blob/master/gym/wrappers/human_rendering.py)). - render_mode_perception: Whether images of visual perception modules should be directly embedded into main camera view ("embed"), stored as separate videos ("separate"), or not used at all [which allows to watch vision in Unity Editor if debug mode is enabled/standalone app is disabled] (None) - render_show_depths: Whether depth images of visual perception modules should be included in rendering. - run_parameters: Can be used to override parameters during run time. - use_cloned: Can be useful for debugging. Set to False to use original files instead of the ones that have been - cloned/copied during building phase. - """ + @classmethod + def get_module(cls, *args): + """ + Returns a module from given path strings. + + Args: + *args: Module path components + + Returns: + module: Imported module object + """ + src = __name__.split(".")[0] + modules = ".".join(args) + return importlib.import_module(src + "." + modules) + + @classmethod + def build(cls, config): + """ + Builds a simulator package from a configuration dict or YAML file path. + + Args: + config: Either a dict with configuration or path to YAML config file + + Returns: + str: Path to the built simulator folder + + Raises: + FileNotFoundError: If config file doesn't exist + AssertionError: If required config fields are missing + NameError: If package name is invalid + """ + # Parse config file if path is provided + if isinstance(config, str): + if not os.path.isfile(config): + raise FileNotFoundError(f"Config file {config} does not exist") + config = parse_yaml(config) + + # Validate required config fields + required_fields = [ + ("simulation", "Simulation specs must be defined in config"), + ("simulation.bm_model", "Biomechanical model must be defined in config"), + ("simulation.task", "Task must be defined in config"), + ("simulation.run_parameters", "Run parameters must be defined in config"), + ("simulation.run_parameters.action_sample_freq", + "Action sampling frequency must be defined in run parameters") + ] + + for field, msg in required_fields: + keys = field.split(".") + current = config + try: + for key in keys: + current = current[key] + except (KeyError, TypeError): + raise AssertionError(msg) + + # Prepare config + run_parameters = config["simulation"]["run_parameters"].copy() + config["version"] = cls.version + + # Determine simulator folder location + if "simulator_folder" in config: + simulator_folder = os.path.normpath(config["simulator_folder"]) + else: + simulator_folder = os.path.join(output_path(), config["simulator_name"]) + + # Validate and set package name + if "package_name" not in config: + config["package_name"] = config["simulator_name"] + + if not is_suitable_package_name(config["package_name"]): + raise NameError( + "Package name (package_name/simulator_name) is invalid. " + "Use lowercase letters and underscores only, cannot start with a number." + ) + + # Set gym registration name + config["gym_name"] = f"uitb:{config['package_name']}-v0" + + # Create simulator package structure + cls._clone(simulator_folder, config["package_name"]) + + # Initialize and integrate task model + task_cls = cls.get_class("tasks", config["simulation"]["task"]["cls"]) + task_kwargs = config["simulation"]["task"].get("kwargs", {}) + unity_exec = task_kwargs.get("unity_executable", None) + task_cls.clone(simulator_folder, config["package_name"], app_executable=unity_exec) + simulation = task_cls.initialise(task_kwargs) + + # Set MuJoCo compiler defaults + compiler_defaults = { + "inertiafromgeom": "auto", + "balanceinertia": "true", + "boundmass": "0.001", + "boundinertia": "0.001", + "inertiagrouprange": "0 1" + } + compiler = simulation.find("compiler") + if compiler is None: + ET.SubElement(simulation, "compiler", compiler_defaults) + else: + compiler.attrib.update(compiler_defaults) + + # Integrate biomechanical model + bm_cls = cls.get_class("bm_models", config["simulation"]["bm_model"]["cls"]) + bm_cls.clone(simulator_folder, config["package_name"]) + bm_cls.insert(simulation) + + # Integrate perception modules + for module_cfg in config["simulation"].get("perception_modules", []): + module_cls = cls.get_class("perception", module_cfg["cls"]) + module_kwargs = module_cfg.get("kwargs", {}) + module_cls.clone(simulator_folder, config["package_name"]) + module_cls.insert(simulation, **module_kwargs) + + # Clone RL library files + if "rl" in config: + rl_cls = cls.get_class("rl", config["rl"]["algorithm"]) + rl_cls.clone(simulator_folder, config["package_name"]) + + # Save initial simulation XML + simulation_file = os.path.join(simulator_folder, config["package_name"], "simulation") + with open(f"{simulation_file}.xml", 'w') as file: + simulation.write(file, encoding='unicode') + + # Initialize simulator to get final model state + model, _, _, _, _, _ = cls._initialise( + config, simulator_folder, {**run_parameters, "build": True} + ) + + # Save updated XML and binary model (faster loading) + mujoco.MjModel.from_xml_path(f"{simulation_file}.xml") + mujoco.mj_saveLastXML(f"{simulation_file}.xml", model) + mujoco.mj_saveModel(model, f"{simulation_file}.mjcf", None) + + # Record build time and save config + config["built"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + write_yaml(config, os.path.join(simulator_folder, "config.yaml")) + + return simulator_folder + + @classmethod + def _clone(cls, simulator_folder, package_name): + """ + Creates simulator package directory structure and copies required files. + + Args: + simulator_folder: Base folder for the simulator + package_name: Name of the Python package + """ + # Create package directory + pkg_dir = os.path.join(simulator_folder, package_name) + os.makedirs(pkg_dir, exist_ok=True) + + # Copy simulator class file + src_file = pathlib.Path(inspect.getfile(cls)) + shutil.copyfile(src_file, os.path.join(pkg_dir, src_file.name)) + + # Create __init__.py with gym registration + init_content = f"""from .simulator import Simulator + +from gymnasium.envs.registration import register +import pathlib - # Read config file - config_file = os.path.join(simulator_folder, "config.yaml") - try: - config = parse_yaml(config_file) - except: - raise FileNotFoundError(f"Could not open file {config_file}") - - # Make sure the simulator has been built - if "built" not in config: - raise RuntimeError("Simulator has not been built") - - # Make sure simulator_folder is in path (used to import gen_cls_cloned) - if simulator_folder not in sys.path: - sys.path.insert(0, simulator_folder) - - # Get Simulator class - gen_cls_cloned = getattr(importlib.import_module(config["package_name"]), "Simulator") - if hasattr(gen_cls_cloned, "version"): - _legacy_mode = False - gen_cls_cloned_version = gen_cls_cloned.version.split("-v")[-1] - else: - _legacy_mode = True - gen_cls_cloned_version = gen_cls_cloned.id.split("-v")[-1] #deprecated - if use_cloned: - gen_cls = gen_cls_cloned - else: - gen_cls = cls - gen_cls_version = gen_cls.version.split("-v")[-1] - - if gen_cls_version.split(".")[0] > gen_cls_cloned_version.split(".")[0]: - raise RuntimeError( - f"""Severe version mismatch. The simulator '{config["simulator_name"]}' has version {gen_cls_cloned_version}, while your uitb package has version {gen_cls_version}.\nTo run with version {gen_cls_cloned_version}, set 'use_cloned=True'.""") - elif gen_cls_version.split(".")[1] > gen_cls_cloned_version.split(".")[1]: - print( - f"""WARNING: Version mismatch. The simulator '{config["simulator_name"]}' has version {gen_cls_cloned_version}, while your uitb package has version {gen_cls_version}.\nTo run with version {gen_cls_version}, set 'use_cloned=True'.""") - - if _legacy_mode: - _simulator = gen_cls(simulator_folder, run_parameters=run_parameters) - else: - try: - _simulator = gen_cls(simulator_folder, render_mode=render_mode, render_mode_perception=render_mode_perception, render_show_depths=render_show_depths, - run_parameters=run_parameters) - except TypeError: - _simulator = gen_cls(simulator_folder, render_mode=render_mode, render_show_depths=render_show_depths, - run_parameters=run_parameters) - - # Return Simulator object - return _simulator - - def __init__(self, simulator_folder, render_mode="rgb_array", render_mode_perception="embed", render_show_depths=False, run_parameters=None): - """ Initialise a new `Simulator`. - - Args: - simulator_folder: Location of a simulator. - render_mode: Whether render() will return a single rgb array (render_mode="rgb_array"), - a list of rgb arrays (render_mode="rgb_array_list"; - adapted from https://github.com/openai/gym/blob/master/gym/wrappers/render_collection.py), - or None while the frames in a separate PyGame window are updated directly when calling - step() or reset() (render_mode="human"; - adapted from https://github.com/openai/gym/blob/master/gym/wrappers/human_rendering.py)). - render_mode_perception: Whether images of visual perception modules should be directly embedded into main camera view ("embed"), stored as separate videos ("separate"), or not used at all [which allows to watch vision in Unity Editor if debug mode is enabled/standalone app is disabled] (None) - render_show_depths: Whether depth images of visual perception modules should be included in rendering. - run_parameters: Can be used to override parameters during run time. - """ +module_folder = pathlib.Path(__file__).parent +simulator_folder = module_folder.parent +kwargs = {{'simulator_folder': simulator_folder}} +register( + id=f'{{module_folder.stem}}-v0', + entry_point=f'{{module_folder.stem}}.simulator:Simulator', + kwargs=kwargs +) +""" + with open(os.path.join(pkg_dir, "__init__.py"), "w") as file: + file.write(init_content) + + # Copy utility modules + utils_src = os.path.join(parent_path(src_file), "utils") + utils_dst = os.path.join(pkg_dir, "utils") + shutil.copytree(utils_src, utils_dst, dirs_exist_ok=True) + + # Copy train/test modules + for subdir in ["train", "test"]: + src = os.path.join(parent_path(src_file), subdir) + dst = os.path.join(pkg_dir, subdir) + shutil.copytree(src, dst, dirs_exist_ok=True) + + @classmethod + def _initialise(cls, config, simulator_folder, run_parameters): + """ + Initializes MuJoCo model/data and all simulator components. + + Args: + config: Simulator configuration dict + simulator_folder: Path to simulator folder + run_parameters: Runtime parameters + + Returns: + tuple: (model, data, task, bm_model, perception, callbacks) + """ + # Load core components + task_cls = cls.get_class("tasks", config["simulation"]["task"]["cls"]) + task_kwargs = config["simulation"]["task"].get("kwargs", {}) + + bm_cls = cls.get_class("bm_models", config["simulation"]["bm_model"]["cls"]) + bm_kwargs = config["simulation"]["bm_model"].get("kwargs", {}) + + # Initialize perception modules + perception_modules = {} + for module_cfg in config["simulation"].get("perception_modules", []): + module_cls = cls.get_class("perception", module_cfg["cls"]) + module_kwargs = module_cfg.get("kwargs", {}) + perception_modules[module_cls] = module_kwargs + + # Load MuJoCo model (try binary first, fall back to XML) + simulation_file = os.path.join(simulator_folder, config["package_name"], "simulation") + try: + model = mujoco.MjModel.from_binary_path(f"{simulation_file}.mjcf") + except (FileNotFoundError, mujoco.MujocoException): + model = mujoco.MjModel.from_xml_path(f"{simulation_file}.xml") + + # Initialize MuJoCo data + data = mujoco.MjData(model) + + # Calculate frame skip and dt + run_parameters["frame_skip"] = int(1 / (model.opt.timestep * run_parameters["action_sample_freq"])) + run_parameters["dt"] = model.opt.timestep * run_parameters["frame_skip"] + + # Initialize rendering context + max_res = run_parameters.get("max_resolution", [1280, 960]) + run_parameters["rendering_context"] = Context(model, max_resolution=max_res) + + # Initialize callbacks + callbacks = {} + for cb in run_parameters.get("callbacks", []): + cb_cls = cls.get_class(cb["cls"]) + callbacks[cb["name"]] = cb_cls(cb["name"], **cb["kwargs"]) + + # Merge parameters for component initialization + common_kwargs = {**run_parameters, **callbacks} + + # Initialize core components + task = task_cls(model, data, **{**task_kwargs, **common_kwargs}) + bm_model = bm_cls(model, data,** {**bm_kwargs, **common_kwargs}) + perception = Perception( + model, data, bm_model, perception_modules, common_kwargs + ) + + return model, data, task, bm_model, perception, callbacks + + @classmethod + def get(cls, simulator_folder, render_mode="rgb_array", + render_mode_perception="embed", render_show_depths=False, + run_parameters=None, use_cloned=True): + """ + Loads a pre-built simulator from folder. + + Args: + simulator_folder: Path to simulator folder + render_mode: Render mode (rgb_array, rgb_array_list, human) + render_mode_perception: How to render perception modules (embed/separate/None) + render_show_depths: Whether to show depth images + run_parameters: Runtime parameters to override + use_cloned: Whether to use cloned files or original source + + Returns: + Simulator: Initialized simulator instance + + Raises: + FileNotFoundError: If config file doesn't exist + RuntimeError: If simulator not built or version mismatch + """ + # Load config + config_path = os.path.join(simulator_folder, "config.yaml") + try: + config = parse_yaml(config_path) + except Exception as e: + raise FileNotFoundError(f"Failed to load config: {e}") + + # Check if simulator is built + if "built" not in config: + raise RuntimeError("Simulator has not been built (missing 'built' timestamp in config)") + + # Add simulator folder to path + if simulator_folder not in sys.path: + sys.path.insert(0, simulator_folder) + + # Load simulator class + try: + gen_cls_cloned = getattr(importlib.import_module(config["package_name"]), "Simulator") + except ImportError as e: + raise RuntimeError(f"Failed to import simulator package: {e}") + + # Handle version checking + _legacy_mode = False + gen_cls_cloned_version = "0.0.0" + + if hasattr(gen_cls_cloned, "version"): + gen_cls_cloned_version = gen_cls_cloned.version + else: + _legacy_mode = True + gen_cls_cloned_version = gen_cls_cloned.id.split("-v")[-1] + + # Select class to use (cloned vs original) + if use_cloned: + gen_cls = gen_cls_cloned + else: + gen_cls = cls + # Version compatibility check + cloned_ver = gen_cls_cloned_version.split(".") + current_ver = gen_cls.version.split(".") + + if cloned_ver[0] < current_ver[0]: + raise RuntimeError( + f"Major version mismatch: Simulator v{gen_cls_cloned_version}, " + f"UITB v{gen_cls.version}. Use use_cloned=True or rebuild simulator." + ) + elif cloned_ver[1] < current_ver[1]: + print(f"Warning: Minor version mismatch - Simulator v{gen_cls_cloned_version}, " + f"UITB v{gen_cls.version}") + + # Initialize simulator + try: + if _legacy_mode: + simulator = gen_cls(simulator_folder, run_parameters=run_parameters) + else: + simulator = gen_cls( + simulator_folder, + render_mode=render_mode, + render_mode_perception=render_mode_perception, + render_show_depths=render_show_depths, + run_parameters=run_parameters + ) + except TypeError: + # Fallback for older versions without perception render mode + simulator = gen_cls( + simulator_folder, + render_mode=render_mode, + render_show_depths=render_show_depths, + run_parameters=run_parameters + ) + + return simulator + + def __init__(self, simulator_folder, render_mode="rgb_array", + render_mode_perception="embed", render_show_depths=False, + run_parameters=None): + """ + Initializes a Simulator instance. + + Args: + simulator_folder: Path to simulator folder + render_mode: Render mode (rgb_array, rgb_array_list, human) + render_mode_perception: How to render perception modules (embed/separate/None) + render_show_depths: Whether to show depth images + run_parameters: Runtime parameters to override + + Raises: + FileNotFoundError: If simulator folder doesn't exist + """ + super().__init__() + + # Validate simulator folder + if not os.path.exists(simulator_folder): + raise FileNotFoundError(f"Simulator folder {simulator_folder} does not exist") + self._simulator_folder = simulator_folder + + # Load config + self._config = parse_yaml(os.path.join(self._simulator_folder, "config.yaml")) + + # Merge run parameters (config defaults + runtime overrides) + self._run_parameters = self._config["simulation"]["run_parameters"].copy() + self._run_parameters.update(run_parameters or {}) + + # Initialize core simulation components + init_results = self._initialise( + self._config, self._simulator_folder, self._run_parameters + ) + self._model, self._data, self.task, self.bm_model, self.perception, self.callbacks = init_results + + # Initialize action/observation spaces + self.action_space = self._initialise_action_space() + self.observation_space = self._initialise_observation_space() + + # Episode statistics + self._episode_statistics = { + "length (seconds)": 0, + "length (steps)": 0, + "reward": 0 + } + + # Rendering setup + self._render_mode = render_mode + self._render_mode_perception = render_mode_perception + self._render_show_depths = render_show_depths + self._render_stack = [] + self._render_stack_perception = defaultdict(list) + self._render_stack_pop = True + self._render_stack_clean_at_reset = True + self._render_screen_size = None + self._render_window = None + self._render_clock = None + + # Initialize GUI camera + self._GUI_camera = Camera( + self._run_parameters["rendering_context"], + self._model, + self._data, + camera_id='for_testing', + dt=self._run_parameters["dt"] + ) + + # HRL (Hierarchical RL) setup + self._setup_hrl() + + def _setup_hrl(self): + """Sets up Hierarchical RL components if configured""" + if 'llc' not in self.config: + return + + # Load LLC simulator + llc_sim_name = self.config["llc"]["simulator_name"] + llc_sim_folder = os.path.join(output_path(), llc_sim_name) + + if llc_sim_folder not in sys.path: + sys.path.insert(0, llc_sim_folder) + + if not os.path.exists(llc_sim_folder): + raise FileNotFoundError(f"LLC simulator folder {llc_sim_folder} does not exist") - # Make sure simulator exists - if not os.path.exists(simulator_folder): - raise FileNotFoundError(f"Simulator folder {simulator_folder} does not exists") - self._simulator_folder = simulator_folder - - # Read config - self._config = parse_yaml(os.path.join(self._simulator_folder, "config.yaml")) - - # Get run parameters: these parameters can be used to override parameters used during training - self._run_parameters = self._config["simulation"]["run_parameters"].copy() - self._run_parameters.update(run_parameters or {}) - - # Initialise simulation - self._model, self._data, self.task, self.bm_model, self.perception, self.callbacks = \ - self._initialise(self._config, self._simulator_folder, self._run_parameters) - - # Set action space TODO for now we assume all actuators have control signals between [-1, 1] - self.action_space = self._initialise_action_space() - - # Set observation space - self.observation_space = self._initialise_observation_space() - - # Collect some episode statistics - self._episode_statistics = {"length (seconds)": 0, "length (steps)": 0, "reward": 0} - - # Initialise viewer - self._GUI_camera = Camera(self._run_parameters["rendering_context"], self._model, self._data, camera_id='for_testing', - dt=self._run_parameters["dt"]) - - self._render_mode = render_mode - self._render_mode_perception = render_mode_perception #whether perception camera views should be directly embedded into camera view of camera_id ("embed"), stored in self._render_stack_perception ("separate"), or not used at all "separate"), or not used at all [which allows to watch vision in Unity Editor if debug mode is enabled/standalone app is disabled] (None) - self._render_stack = [] #only used if render_mode == "rgb_array_list" - self._render_stack_perception = defaultdict(list) #only used if render_mode == "rgb_array_list" and self._render_mode_perception == "separate" - self._render_stack_pop = True #If True, clear the render stack after .render() is called. - self._render_stack_clean_at_reset = True #If True, clear the render stack when .reset() is called. - self._render_show_depths = render_show_depths #If True, depth images of visual perception modules are included in GUI rendering. - self._render_screen_size = None #only used if render_mode == "human" - self._render_window = None #only used if render_mode == "human" - self._render_clock = None #only used if render_mode == "human" - - if 'llc' in self.config: #if HRL approach is used - llc_simulator_folder = os.path.join(output_path(), self.config["llc"]["simulator_name"]) - if llc_simulator_folder not in sys.path: - sys.path.insert(0, llc_simulator_folder) - if not os.path.exists(llc_simulator_folder): - raise FileNotFoundError(f"Simulator folder {llc_simulator_folder} does not exists") - llccheckpoint_dir = os.path.join(llc_simulator_folder, 'checkpoints') - # Load policy TODO should create a load method for uitb.rl.BaseRLModel - print(f'Loading model: {os.path.join(llccheckpoint_dir, self.config["llc"]["checkpoint"])}\n') - self.llc_model = PPO.load(os.path.join(llccheckpoint_dir, self.config["llc"]["checkpoint"])) + # Load LLC policy + llc_checkpoint_dir = os.path.join(llc_sim_folder, 'checkpoints') + llc_checkpoint = self.config["llc"]["checkpoint"] + llc_checkpoint_path = os.path.join(llc_checkpoint_dir, llc_checkpoint) + + try: + print(f"Loading LLC model: {llc_checkpoint_path}") + self.llc_model = PPO.load(llc_checkpoint_path) + except FileNotFoundError: + raise FileNotFoundError(f"LLC checkpoint {llc_checkpoint} not found in {llc_checkpoint_dir}") + except Exception as e: + raise RuntimeError(f"Failed to load LLC model: {str(e)}") + + # Update action space for HRL self.action_space = self._initialise_HRL_action_space() - self._max_steps = self.config["llc"]["llc_ratio"] - self._dwell_threshold = int(0.5*self._max_steps) - self._target_radius = 0.05 - self._independent_dofs = [] - self._independent_joints = [] + + # HRL parameters (from config with defaults) + self._max_steps = self.config["llc"].get("llc_ratio", 100) + self._dwell_threshold = self.config["llc"].get( + "dwell_threshold", int(0.5 * self._max_steps) + ) + self._target_radius = self.config["llc"].get("target_radius", 0.05) + + # Precompute joint information (performance optimization) joints = self.config["llc"]["joints"] + self._independent_jnt_ids = [] + self._independent_dofs = [] + for joint in joints: - joint_id = mujoco.mj_name2id(self._model, mujoco.mjtObj.mjOBJ_JOINT, joint) - if self._model.jnt_type[joint_id] not in [mujoco.mjtJoint.mjJNT_HINGE, mujoco.mjtJoint.mjJNT_SLIDE]: - raise NotImplementedError(f"Only 'hinge' and 'slide' joints are supported, joint " - f"{joint} is of type {mujoco.mjtJoint(self._model.jnt_type[joint_id]).name}") - self._independent_dofs.append(self._model.jnt_qposadr[joint_id]) - self._independent_joints.append(joint_id) - self._jnt_range = self._model.jnt_range[self._independent_joints] - - - #To normalize joint ranges for llc - def _normalise_qpos(self, qpos): - # Normalise to [0, 1] - qpos = (qpos - self._jnt_range[:, 0]) / (self._jnt_range[:, 1] - self._jnt_range[:, 0]) - # Normalise to [-1, 1] - qpos = (qpos - 0.5) * 2 - return qpos - - def _initialise_action_space(self): - """ Initialise action space. """ - num_actuators = self.bm_model.nu + self.perception.nu - actuator_limits = np.ones((num_actuators,2)) * np.array([-1.0, 1.0]) - return spaces.Box(low=np.float32(actuator_limits[:, 0]), high=np.float32(actuator_limits[:, 1])) - - def _initialise_HRL_action_space(self): - bm_nu = self.bm_model.nu - bm_jnt_range = np.ones((bm_nu,2)) * np.array([-1.0, 1.0]) - perception_nu = self.perception.nu - perception_jnt_range = np.ones((perception_nu,2)) * np.array([-1.0, 1.0]) - jnt_range = np.concatenate((bm_jnt_range, perception_jnt_range), axis=0) - action_space = gym.spaces.Box(low=jnt_range[:,0], high=jnt_range[:,1]) - return action_space - - def _initialise_observation_space(self): - """ Initialise observation space. """ - observation = self.get_observation() - obs_dict = dict() - for module in self.perception.perception_modules: - obs_dict[module.modality] = spaces.Box(dtype=np.float32, **module.get_observation_space_params()) - if "stateful_information" in observation: - obs_dict["stateful_information"] = spaces.Box(dtype=np.float32, - **self.task.get_stateful_information_space_params()) - return spaces.Dict(obs_dict) - - def _get_qpos(self, model, data): - qpos = data.qpos[self._independent_dofs].copy() - qpos = self._normalise_qpos(qpos) - return qpos - - def step(self, action): - """ Step simulation forward with given actions. - - Args: - action: Actions sampled from a policy. Limited to range [-1, 1]. - """ - if 'llc' in self.config: #if HRL approach is used - self.task._target_qpos = action # action to pass to LLC - self._steps = 0 # Initialise loop control to 0 - #acc_reward = 0 #To be used when rewards are being accumulated in llc steps + joint_id = mujoco.mj_name2id( + self._model, mujoco.mjtObj.mjOBJ_JOINT, joint + ) + jnt_type = self._model.jnt_type[joint_id] + + if jnt_type not in [mujoco.mjtJoint.mjJNT_HINGE, mujoco.mjtJoint.mjJNT_SLIDE]: + raise NotImplementedError( + f"Only hinge/slide joints are supported. Joint {joint} is " + f"{mujoco.mjtJoint(jnt_type).name}" + ) + + self._independent_jnt_ids.append(joint_id) + self._independent_dofs.append(self._model.jnt_qposadr[joint_id]) + + # Precompute joint ranges (vectorized operations) + self._jnt_range = self._model.jnt_range[self._independent_jnt_ids].astype(np.float32) + self._jnt_range_diff = self._jnt_range[:, 1] - self._jnt_range[:, 0] + # Avoid division by zero + self._jnt_range_diff[self._jnt_range_diff == 0] = 1e-6 + + def _normalise_qpos(self, qpos): + """ + Normalizes joint positions to [-1, 1] range using precomputed joint ranges. + + Args: + qpos: Raw joint positions + + Returns: + np.ndarray: Normalized joint positions + """ + # Normalize to [0, 1] then to [-1, 1] (vectorized for performance) + norm_01 = (qpos - self._jnt_range[:, 0]) / self._jnt_range_diff + return (norm_01 - 0.5) * 2 + + def _initialise_action_space(self): + """ + Initializes default action space (all actuators [-1, 1]). + + Returns: + spaces.Box: Action space definition + """ + num_actuators = self.bm_model.nu + self.perception.nu + low = np.full(num_actuators, -1.0, dtype=np.float32) + high = np.full(num_actuators, 1.0, dtype=np.float32) + return spaces.Box(low=low, high=high) + + def _initialise_HRL_action_space(self): + """ + Initializes action space for HRL (combines BM and perception actions). + + Returns: + spaces.Box: HRL action space definition + """ + # Biomechanical model action space + bm_low = np.full(self.bm_model.nu, -1.0, dtype=np.float32) + bm_high = np.full(self.bm_model.nu, 1.0, dtype=np.float32) + + # Perception module action space + perception_low = np.full(self.perception.nu, -1.0, dtype=np.float32) + perception_high = np.full(self.perception.nu, 1.0, dtype=np.float32) + + # Combine spaces + low = np.concatenate([bm_low, perception_low]) + high = np.concatenate([bm_high, perception_high]) + + return spaces.Box(low=low, high=high, dtype=np.float32) - while self._steps < self._max_steps: # loop for llc controls based on llc_ratio + def _initialise_observation_space(self): + """ + Initializes observation space (perception modules + stateful info). + + Returns: + spaces.Dict: Observation space definition + """ + # Get sample observation to build space + observation = self.get_observation() + obs_dict = {} + + # Add perception modules + for module in self.perception.perception_modules: + obs_dict[module.modality] = spaces.Box( + dtype=np.float32,** module.get_observation_space_params() + ) + + # Add stateful information (ensure non-zero shape) + if "stateful_information" in observation: + state_params = self.task.get_stateful_information_space_params() + # Ensure minimum shape of (1,) to avoid SB3 errors + if state_params["shape"] == (0,): + state_params["shape"] = (1,) + obs_dict["stateful_information"] = spaces.Box( + dtype=np.float32,** state_params + ) + + return spaces.Dict(obs_dict) + + def _get_qpos(self): + """ + Gets normalized joint positions for independent joints. + + Returns: + np.ndarray: Normalized joint positions + """ + qpos = self._data.qpos[self._independent_dofs].copy() + return self._normalise_qpos(qpos) + + def step(self, action): + """ + Advances simulation by one step (supports HRL and normal RL). + + Args: + action: Action vector from policy + + Returns: + tuple: (observation, reward, terminated, truncated, info) + """ + # HRL mode (with LLC) + if 'llc' in self.config: + self.task._target_qpos = action + self._steps = 0 + total_reward = 0 + + # LLC control loop + while self._steps < self._max_steps: + # Get LLC observation and predict action + llc_obs = self.get_llcobservation(action) + llc_action, _ = self.llc_model.predict(llc_obs, deterministic=True) + + # Apply controls + self.bm_model.set_ctrl(self._model, self._data, llc_action) + self.perception.set_ctrl( + self._model, self._data, action[self.bm_model.nu:] + ) + + # Step simulation + mujoco.mj_step( + self._model, self._data, nstep=self._run_parameters["frame_skip"] + ) + + # Update components + self.bm_model.update(self._model, self._data) + self.perception.update(self._model, self._data) + + # Calculate reward and check termination + reward, terminated, truncated, info = self.task.update( + self._model, self._data + ) + reward -= self.bm_model.get_effort_cost(self._model, self._data) + total_reward += reward + + # Get observation + obs = self.get_observation(info) + + # Render if needed + if self._render_mode == "rgb_array_list": + self._render_stack.append(self._GUI_rendering()) + elif self._render_mode == "human": + self._GUI_rendering_pygame() + + # Check termination conditions + if terminated or truncated: + break + + # Task-specific termination checks + if "target_spawned" in info or "new_button_generated" in info: + if info.get("target_hit", False): + break + + # Check joint position error threshold + qpos = self._get_qpos() + dist = np.abs(action - qpos) + if np.all(dist < self._target_radius): + break - llc_action, _states = self.llc_model.predict(self.get_llcobservation(action), deterministic=True) # Get BM action from LLC - # Set control for the bm model - self.bm_model.set_ctrl(self._model, self._data, llc_action) + self._steps += 1 - # Set control for perception modules (e.g. eye movements) - self.perception.set_ctrl(self._model, self._data, action[self.bm_model.nu:]) + # Use total reward from LLC loop + reward = total_reward - # Advance the simulation - mujoco.mj_step(self._model, self._data, nstep=int(self._run_parameters["frame_skip"])) # Number of timesteps to skip for LLC + # Normal RL mode + else: + # Apply controls + self.bm_model.set_ctrl(self._model, self._data, action[:self.bm_model.nu]) + self.perception.set_ctrl( + self._model, self._data, action[self.bm_model.nu:] + ) - # Update bm model (e.g. update constraints); updates also effort model - self.bm_model.update(self._model, self._data) + # Step simulation + mujoco.mj_step( + self._model, self._data, nstep=self._run_parameters["frame_skip"] + ) - # Update perception modules + # Update components + self.bm_model.update(self._model, self._data) self.perception.update(self._model, self._data) - dist = np.abs(action - self._get_qpos(self._model, self._data)) - - # Update environment - reward, terminated, truncated, info = self.task.update(self._model, self._data) - - # Add an effort cost to reward - reward -= self.bm_model.get_effort_cost(self._model, self._data) - - #acc_reward += reward #To be used when rewards are being accumulated in llc steps + # Calculate reward and termination + reward, terminated, truncated, info = self.task.update( + self._model, self._data + ) + + # Add effort cost + effort_cost = self.bm_model.get_effort_cost(self._model, self._data) + info["EffortCost"] = effort_cost + reward -= effort_cost # Get observation - obs = self.get_observation() + obs = self.get_observation(info) - # Add frame to stack + # Render if needed if self._render_mode == "rgb_array_list": - self._render_stack.append(self._GUI_rendering()) + self._render_stack.append(self._GUI_rendering()) elif self._render_mode == "human": - self._GUI_rendering_pygame() - - if truncated or terminated: - break - - # Pointing - if "target_spawned" in info: - if info["target_spawned"] or info["target_hit"]: - break - - # Choice Reaction - elif "new_button_generated" in info: - if info["new_button_generated"] or info["target_hit"]: - break + self._GUI_rendering_pygame() - self._steps += 1 - if np.all(dist < self._target_radius): - break + # Update episode statistics + self._episode_statistics["length (seconds)"] += self._run_parameters["dt"] + self._episode_statistics["length (steps)"] += 1 + self._episode_statistics["reward"] += reward return obs, reward, terminated, truncated, info - else: - # Set control for the bm model - self.bm_model.set_ctrl(self._model, self._data, action[:self.bm_model.nu]) - - # Set control for perception modules (e.g. eye movements) - self.perception.set_ctrl(self._model, self._data, action[self.bm_model.nu:]) - - # Advance the simulation - mujoco.mj_step(self._model, self._data, nstep=self._run_parameters["frame_skip"]) - - # Update bm model (e.g. update constraints); updates also effort model - self.bm_model.update(self._model, self._data) - - # Update perception modules - self.perception.update(self._model, self._data) + def get_observation(self, info=None): + """ + Gets observation from perception modules and task state. + + Args: + info: Additional info from task update + + Returns: + dict: Observation dictionary + """ + # Get perception observations + observation = self.perception.get_observation(self._model, self._data, info) + + # Add stateful information (ensure non-empty) + stateful_info = self.task.get_stateful_information(self._model, self._data) + if stateful_info.size > 0: + observation["stateful_information"] = stateful_info + elif "stateful_information" not in observation: + # Add dummy state to avoid SB3 errors + observation["stateful_information"] = np.array([0.0], dtype=np.float32) + + return observation + + def get_llcobservation(self, action): + """ + Gets observation for LLC (no vision, joint position error). + + Args: + action: Target joint position from HRL + + Returns: + dict: LLC observation dictionary + """ + # Get base observation and remove vision + observation = self.perception.get_observation(self._model, self._data) + observation.pop("vision", None) - # Update environment - reward, terminated, truncated, info = self.task.update(self._model, self._data) + # Calculate joint position error + qpos = self._get_qpos() + qpos_diff = action - qpos - # Add an effort cost to reward - effort_cost = self.bm_model.get_effort_cost(self._model, self._data) - info["EffortCost"] = effort_cost - reward -= effort_cost + # Add stateful information (joint error) + observation["stateful_information"] = qpos_diff.astype(np.float32) - # Get observation - obs = self.get_observation(info) + return observation - # Add frame to stack + def reset(self, seed=None, options=None): + """ + Resets simulator to initial state. + + Args: + seed: Random seed + options: Reset options + + Returns: + tuple: (observation, info) + """ + super().reset(seed=seed) + + # Reset MuJoCo data + mujoco.mj_resetData(self._model, self._data) + + # Reset components + self.bm_model.reset(self._model, self._data) + self.perception.reset(self._model, self._data) + info = self.task.reset(self._model, self._data) + + # Forward pass to update state + mujoco.mj_forward(self._model, self._data) + + # Reset episode statistics + self._episode_statistics = { + "length (seconds)": 0, + "length (steps)": 0, + "reward": 0 + } + + # Render initial frame if self._render_mode == "rgb_array_list": - self._render_stack.append(self._GUI_rendering()) + if self._render_stack_clean_at_reset: + self._render_stack = [] + self._render_stack_perception = defaultdict(list) + self._render_stack.append(self._GUI_rendering()) elif self._render_mode == "human": - self._GUI_rendering_pygame() - - return obs, reward, terminated, truncated, info - - - def get_observation(self, info=None): - """ Returns an observation from the perception model. - - Returns: - A dict with observations from individual perception modules. May also contain stateful information from a task. - """ - - # Get observation from perception - observation = self.perception.get_observation(self._model, self._data, info) + self._GUI_rendering_pygame() - # Add any stateful information that is required - stateful_information = self.task.get_stateful_information(self._model, self._data) - if stateful_information.size > 0: #TODO: define stateful_information (and encoder) that can be used as default, if no stateful information is provided (zero-size arrays do not work with sb3 currently...) - observation["stateful_information"] = stateful_information + return self.get_observation(), info - return observation - - def get_llcobservation(self,action): - """ Returns an observation from the perception model. - - Returns: - A dict with observations from individual perception modules. May also contain stateful information from a task. - """ - - # Get observation from perception - observation = self.perception.get_observation(self._model, self._data) - - # Remove Vision for LLC - observation.pop("vision") - qpos = self._get_qpos(self._model, self._data) - qpos_diff = action - qpos - - # Stateful Information for LLC policy - stateful_information = qpos_diff - if stateful_information is not None: - observation["stateful_information"] = stateful_information - - return observation - - - def reset(self, seed=None): - """ Reset the simulator and return an observation. """ - - super().reset(seed=seed) - - # Reset sim - mujoco.mj_resetData(self._model, self._data) - - # Reset all models - self.bm_model.reset(self._model, self._data) - self.perception.reset(self._model, self._data) - info = self.task.reset(self._model, self._data) - - # Do a forward so everything will be set - mujoco.mj_forward(self._model, self._data) - - if self._render_mode == "rgb_array_list": - if self._render_stack_clean_at_reset: - self._render_stack = [] - self._render_stack_perception = defaultdict(list) - self._render_stack.append(self._GUI_rendering()) - elif self._render_mode == "human": - self._GUI_rendering_pygame() - - return self.get_observation(), info - - def render(self): - if self._render_mode == "rgb_array_list": - render_stack = self._render_stack - if self._render_stack_pop: - self._render_stack = [] - return render_stack - elif self._render_mode == "rgb_array": - return self._GUI_rendering() - else: - return None - - def get_render_stack_perception(self): - render_stack_perception = self._render_stack_perception - # if self._render_stack_pop: - # self._render_stack_perception = defaultdict(list) - return render_stack_perception - - def _GUI_rendering(self): - # Grab an image from the 'for_testing' camera and grab all GUI-prepared images from included visual perception modules, and display them 'picture-in-picture' - - # Grab images - img, _ = self._GUI_camera.render() - - if self._render_mode_perception == "embed": - # Embed perception camera images into main camera image - - # perception_camera_images = [rgb_or_depth_array for camera in self.perception.cameras - # for rgb_or_depth_array in camera.render() if rgb_or_depth_array is not None] - perception_camera_images = self.perception.get_renders() - - # TODO: add text annotations to perception camera images - if len(perception_camera_images) > 0: - _img_size = img.shape[:2] #(height, width) - - - # Vertical alignment of perception camera images, from bottom right to top right - ## TODO: allow for different inset locations - _desired_subwindow_height = np.round(_img_size[0] / len(perception_camera_images)).astype(int) - _maximum_subwindow_width = np.round(0.2 * _img_size[1]).astype(int) - - perception_camera_images_resampled = [] - for ocular_img in perception_camera_images: - # Convert 2D depth arrays to 3D heatmap arrays - if ocular_img.ndim == 2: - if self._render_show_depths: - ocular_img = matplotlib.pyplot.imshow(ocular_img, cmap=matplotlib.pyplot.cm.jet, interpolation='bicubic').make_image('TkAgg', unsampled=True)[0][ - ..., :3] - matplotlib.pyplot.close() #delete image - else: - continue - - resample_factor = min(_desired_subwindow_height / ocular_img.shape[0], _maximum_subwindow_width / ocular_img.shape[1]) - - resample_height = np.round(ocular_img.shape[0] * resample_factor).astype(int) - resample_width = np.round(ocular_img.shape[1] * resample_factor).astype(int) - resampled_img = np.zeros((resample_height, resample_width, ocular_img.shape[2]), dtype=np.uint8) - for channel in range(ocular_img.shape[2]): - resampled_img[:, :, channel] = scipy.ndimage.zoom(ocular_img[:, :, channel], resample_factor, order=0) - - perception_camera_images_resampled.append(resampled_img) - - ocular_img_bottom = _img_size[0] - for ocular_img_idx, ocular_img in enumerate(perception_camera_images_resampled): - #print(f"Modify ({ocular_img_bottom - ocular_img.shape[0]}, { _img_size[1] - ocular_img.shape[1]})-({ocular_img_bottom}, {img.shape[1]}).") - img[ocular_img_bottom - ocular_img.shape[0]:ocular_img_bottom, _img_size[1] - ocular_img.shape[1]:] = ocular_img - ocular_img_bottom -= ocular_img.shape[0] - # input((len(perception_camera_images_resampled), perception_camera_images_resampled[0].shape, img.shape)) - elif self._render_mode_perception == "separate": - for module, camera_list in self.perception.cameras_dict.items(): - for camera in camera_list: - for rgb_or_depth_array in camera.render(): - if rgb_or_depth_array is not None: - self._render_stack_perception[f"{module.modality}/{type(camera).__name__}"].append(rgb_or_depth_array) - - return img - - def _GUI_rendering_pygame(self): - rgb_array = np.transpose(self._GUI_rendering(), axes=(1, 0, 2)) - - if self._render_screen_size is None: - self._render_screen_size = rgb_array.shape[:2] - - assert self._render_screen_size == rgb_array.shape[ - :2], f"Expected an rgb array of shape {self._render_screen_size} from self._GUI_camera, but received an rgb array of shape {rgb_array.shape[:2]}. " - - if self._render_window is None: - pygame.init() - pygame.display.init() - self._render_window = pygame.display.set_mode(self._render_screen_size) - - if self._render_clock is None: - self._render_clock = pygame.time.Clock() - - surf = pygame.surfarray.make_surface(rgb_array) - self._render_window.blit(surf, (0, 0)) - pygame.event.pump() - self._render_clock.tick(self.fps) - pygame.display.flip() - - def close(self): - """ Close the rendering window (if self._render_mode == 'human').""" - super().close() - if self._render_window is not None: - import pygame - - pygame.display.quit() - pygame.quit() - - @property - def fps(self): - return self._GUI_camera._fps - - def callback(self, callback_name, num_timesteps): - """ Update a callback -- may be useful during training, e.g. for curriculum learning. """ - self.callbacks[callback_name].update(num_timesteps) - - def update_callbacks(self, num_timesteps): - """ Update all callbacks. """ - for callback_name in self.callbacks: - self.callback(callback_name, num_timesteps) - - # def get_logdict_keys(self): - # return list(self.task._info["log_dict"].keys()) - - # def get_logdict_value(self, key): - # return self.task._info["log_dict"].get(key) - - @property - def config(self): - """ Return config. """ - return copy.deepcopy(self._config) - - @property - def run_parameters(self): - """ Return run parameters. """ - # Context cannot be deep copied - exclude = {"rendering_context"} - run_params = {k: copy.deepcopy(self._run_parameters[k]) for k in self._run_parameters.keys() - exclude} - run_params["rendering_context"] = self._run_parameters["rendering_context"] - return run_params - - @property - def simulator_folder(self): - """ Return simulator folder. """ - return self._simulator_folder - - @property - def render_mode(self): - """ Return render mode. """ - return self._render_mode - - def get_state(self): - """ Return a state of the simulator / individual components (biomechanical model, perception model, task). - - This function is used for logging/evaluation purposes, not for RL training. - - Returns: - A dict with one float or numpy vector per keyword. - """ - - # Get time, qpos, qvel, qacc, act_force, act, ctrl of the current simulation - state = {"timestep": self._data.time, - "qpos": self._data.qpos.copy(), - "qvel": self._data.qvel.copy(), - "qacc": self._data.qacc.copy(), - "act_force": self._data.actuator_force.copy(), - "act": self._data.act.copy(), - "ctrl": self._data.ctrl.copy()} - - # Add state from the task - state.update(self.task.get_state(self._model, self._data)) - - # Add state from the biomechanical model - state.update(self.bm_model.get_state(self._model, self._data)) - - # Add state from the perception model - state.update(self.perception.get_state(self._model, self._data)) - - return state - - def close(self, **kwargs): - """ Perform any necessary clean up. - - This function is inherited from gym.Env. It should be automatically called when this object is garbage collected - or the program exists, but that doesn't seem to be the case. This function will be called if this object has been - initialised in the context manager fashion (i.e. using the 'with' statement). """ - self.task.close(**kwargs) - self.perception.close(**kwargs) - self.bm_model.close(**kwargs) + def render(self): + """ + Renders simulation frame based on render mode. + + Returns: + np.ndarray/list/None: Rendered frame(s) or None + """ + if self._render_mode == "rgb_array_list": + render_stack = self._render_stack.copy() + if self._render_stack_pop: + self._render_stack = [] + return render_stack + elif self._render_mode == "rgb_array": + return self._GUI_rendering() + return None + + def get_render_stack_perception(self): + """ + Gets perception render stack (for separate perception rendering). + + Returns: + defaultdict: Perception render stack + """ + return copy.deepcopy(self._render_stack_perception) + + def _GUI_rendering(self): + """ + Renders GUI frame with optional perception module overlays. + + Returns: + np.ndarray: RGB image array + """ + # Render main camera + img, _ = self._GUI_camera.render() + + # Embed perception images into main frame + if self._render_mode_perception == "embed": + perception_images = self.perception.get_renders() + + if len(perception_images) > 0: + img_h, img_w = img.shape[:2] + desired_h = np.round(img_h / len(perception_images)).astype(int) + max_w = np.round(0.2 * img_w).astype(int) + + # Resize and embed perception images + resampled_imgs = [] + for perc_img in perception_images: + # Convert depth maps to RGB heatmaps if enabled + if perc_img.ndim == 2: + if self._render_show_depths: + # Create heatmap from depth + cmap = matplotlib.cm.jet + norm = matplotlib.colors.Normalize( + vmin=perc_img.min(), vmax=perc_img.max() + ) + perc_img = cmap(norm(perc_img))[:, :, :3] * 255 + perc_img = perc_img.astype(np.uint8) + else: + continue + + # Calculate resize factor + h, w = perc_img.shape[:2] + scale = min(desired_h / h, max_w / w) + new_h = np.round(h * scale).astype(int) + new_w = np.round(w * scale).astype(int) + + # Resample image (vectorized for performance) + resampled = np.zeros((new_h, new_w, perc_img.shape[2]), dtype=np.uint8) + for c in range(perc_img.shape[2]): + resampled[:, :, c] = scipy.ndimage.zoom(perc_img[:, :, c], scale, order=0) + + resampled_imgs.append(resampled) + + # Overlay resampled images (bottom-right to top-right) + y_pos = img_h + for res_img in resampled_imgs: + h, w = res_img.shape[:2] + y_start = max(0, y_pos - h) + x_start = max(0, img_w - w) + + # Ensure we don't go out of bounds + img[y_start:y_start+h, x_start:x_start+w] = res_img + y_pos = y_start + + # Store separate perception renders + elif self._render_mode_perception == "separate": + for module, cameras in self.perception.cameras_dict.items(): + for camera in cameras: + for img_arr in camera.render(): + if img_arr is not None: + key = f"{module.modality}/{type(camera).__name__}" + self._render_stack_perception[key].append(img_arr) + + return img + + def _GUI_rendering_pygame(self): + """Renders frame to Pygame window (human render mode)""" + # Get rendered image and transpose for Pygame + rgb_array = np.transpose(self._GUI_rendering(), (1, 0, 2)) + + # Initialize Pygame window if needed + if self._render_screen_size is None: + self._render_screen_size = rgb_array.shape[:2] + + if self._render_window is None: + pygame.init() + pygame.display.init() + self._render_window = pygame.display.set_mode(self._render_screen_size) + + if self._render_clock is None: + self._render_clock = pygame.time.Clock() + + # Validate image size + if self._render_screen_size != rgb_array.shape[:2]: + raise ValueError( + f"Render size mismatch: Expected {self._render_screen_size}, " + f"got {rgb_array.shape[:2]}" + ) + + # Update Pygame window + surf = pygame.surfarray.make_surface(rgb_array) + self._render_window.blit(surf, (0, 0)) + pygame.event.pump() + self._render_clock.tick(self.fps) + pygame.display.flip() + + def get_state(self): + """ + Gets full simulator state (for logging/evaluation). + + Returns: + dict: Simulator state including MuJoCo data and component states + """ + # Base MuJoCo state + state = { + "timestep": self._data.time, + "qpos": self._data.qpos.copy(), + "qvel": self._data.qvel.copy(), + "qacc": self._data.qacc.copy(), + "act_force": self._data.actuator_force.copy(), + "act": self._data.act.copy(), + "ctrl": self._data.ctrl.copy() + } + + # Add component-specific state + state.update(self.task.get_state(self._model, self._data)) + state.update(self.bm_model.get_state(self._model, self._data)) + state.update(self.perception.get_state(self._model, self._data)) + + return state + + def close(self, **kwargs): + """Cleans up simulator resources""" + # Clean up components + self.task.close(** kwargs) + self.perception.close(**kwargs) + self.bm_model.close(**kwargs) + + # Clean up Pygame + if self._render_window is not None: + pygame.display.quit() + pygame.quit() + self._render_window = None + self._render_clock = None + + # Property getters (read-only) + @property + def fps(self): + return self._GUI_camera._fps + + @property + def config(self): + return copy.deepcopy(self._config) + + @property + def run_parameters(self): + # Exclude non-serializable objects from copy + exclude = {"rendering_context"} + run_params = { + k: copy.deepcopy(v) + for k, v in self._run_parameters.items() + if k not in exclude + } + run_params["rendering_context"] = self._run_parameters["rendering_context"] + return run_params + + @property + def simulator_folder(self): + return self._simulator_folder + + @property + def render_mode(self): + return self._render_mode + + # Ensure proper cleanup on object deletion + def __del__(self): + try: + self.close() + except Exception: + pass \ No newline at end of file diff --git a/src/box/utils/__pycache__/functions.cpython-313.pyc b/src/box/utils/__pycache__/functions.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1fb8b9766a41546237ae56a23e83d1355092a3c7 GIT binary patch literal 1905 zcmchY?@t^>7{_OJ_ipcQp%=x}S}Gj1nx0lD1Vjks(!}^i?F&|nhD$cf-5#zUdzYEr zQ2L^&iM6z1AW;lh(;x9id)i17lbY0u{t4JiBw2l7QV!@V4fn#NuY8`pz2gKU@0{Ix zcIKIxXJ$V0ow;&jV}w8&EIennh=kn3LNy6~rrZUYIieDkzC>8%OBq##{e%gs2v$@j zu#y@88&HE_gJU!<-^TzaQhpfkBZGx;NO=j&92xPPXojkz)jZ$})D`^fE86Z+y2{ zT6*_XLwkD%ygA3tJ9a$i3OVjddEI`~mGx2XWu&)qrsc|MlUe$>;f6FVV`dFaE08h{ z?Z#f{Pg+)zvs7|4cQWbNW|k*2j+L^_oWDruD!Ahklb{j9^|KI~AecDy92{2_V+~+kC9gBX zvMa=*;6V-WgJAxRSYjb@y}Q_aw5W70OPxCrp)fo|uR&&x43nXHfZ)T_Q~8i%^st}P zn_xcBTBSJOA&jssRr`2SYlF(#^6!5&y|MUCY5Htw_Jg(g#nSATrP-;qZ|4}!B`&xP zCO6ZtLNj9;%#}@^%}p3APF)E_R*zzz$u;hnwmu4?%j>DP^)W-kM(hy?g=*qB@)`2G z5?xUa{Hz=(DlIEY_m%GJ^lI<5#MMOciSA{odnXoTU$2}R6kM_j_eP&65Q~oZ=L!(u z=50#{$uO;|fD5XGPxU0m$RW~3j*u$QG>niDVx*PC;2D#SiLE3<;{oR}2wFq8aqY9U zOS5krf92)1kI$`sf6JxpLBO#y&puzEZpg4wxwL7GorsIBl*=2ID^2R-SvSn}jG<+7 zdK&14y%Xx4UYg-J|CleV2ndTC8g^l=;C~2y9OnnR3O7!f^G#PK?uI*VcK#M^nmK*p z^t@h-#{LpX;ucO?hf~-M=j#YD&?m-- z8pLM{=2-AQUf7zn@jQDk=Pmar#+XAQIU7HMV0#Ekd-Z316;Xi?=?aIv&8W_)|ovbm)o`MS8 n1CGu96e*?mNcdNxY)S%sm2QRv+J&ZPGbGZsazv#4UYNfDz(|l- literal 0 HcmV?d00001 diff --git a/src/box/utils/__pycache__/rendering.cpython-313.pyc b/src/box/utils/__pycache__/rendering.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8dd6ac1b711d838ef4bd52f1db5dbb817040bc8f GIT binary patch literal 1563 zcmbVM&1(}u6rbJfZra$iwYK)t66*&p*y2S65fyq7giu%uIRu7uvkfbo-8#F0dh=ox zO-0d)H}N9PrJ$$c$Dbj@@z~OkliE;v^}U%SrIKD8!Y^;%ym|9}?_(OtWP-r@uztlI ziV?DfAFZV|g~l8RYs4a!a)&VauP}9lOc6^RBUbFPDlJ(|=(G+x9dt(9PAe#8ypD&` zwmd3PX?`>&L0BVM!W4@z)lyi@QkiDO#t73bjTr^qGXrGy1pAx%KMwVW?Gs!IB6sS9+K+NSUFLhy!gFq zya5VQQN=yt#7#VfWntvGzT*e59j7!O*p#wf z@W_WK4WnG-+B6qM%v~r&PDp8mu;Z}HFbsyBApkQUBGDD{L%X`Cs`|7vLyOL(9fO^J zOABR%;xfIUnY!>>KZfW3t3V91#kvsCNvi@fa3sk30D4g9M-Z$AKuVhiJAs-(gdv2J z2;$5LN}~uJvLNokYgfoY3Och-fmLVrhg#d1g(9+C9V4_2fYxgF>0Qtq-KUvLUUMc1 z$=veViOkG-t*M4-p4X=#^m*c88O16PQ%x77{QD-(m@Q{aG#J>Y^^fa86?LURm8HBR{A(DVWfOSv_-Y03c8y#~So(F(NAD%4t$Zj7TDR);# zr^>z6fx&XTIyn4x>gDYx+G@9`h20)bIEONlZ6M1v0?&+^hF@Bg&QwHFkb?;I#3Awg2`Lu#$rJGyx;pjD?1!e@>lV4Xuu;0oDQ4gJ(! zzoJjbR>N&m=SHz~FR%ly`=uhh7OKA_r%<>!+?c%Ux}5wQ3?D_L8=3&WV~V0|k>pP@ au&-&#^dADS-xEU#U_X&iCUyycxcDyuV>1K* literal 0 HcmV?d00001 diff --git a/src/box/utils/functions.py b/src/box/utils/functions.py new file mode 100644 index 0000000000..b30ef1a5e8 --- /dev/null +++ b/src/box/utils/functions.py @@ -0,0 +1,25 @@ +# src/box/utils/functions.py +import os +import yaml + +def output_path(): + """示例:返回输出路径""" + return os.path.abspath(os.path.join(os.path.dirname(__file__), "../../output")) + +def parent_path(path): + """示例:返回父目录路径""" + return os.path.dirname(os.path.abspath(path)) + +def is_suitable_package_name(name): + """示例:验证包名是否合法""" + return name.isidentifier() and name[0].islower() + +def parse_yaml(file_path): + """示例:解析YAML文件""" + with open(file_path, "r", encoding="utf-8") as f: + return yaml.safe_load(f) + +def write_yaml(data, file_path): + """示例:写入YAML文件""" + with open(file_path, "w", encoding="utf-8") as f: + yaml.safe_dump(data, f, default_flow_style=False) \ No newline at end of file diff --git a/src/box/utils/rendering.py b/src/box/utils/rendering.py new file mode 100644 index 0000000000..6c030ddf24 --- /dev/null +++ b/src/box/utils/rendering.py @@ -0,0 +1,26 @@ +# src/box/utils/rendering.py +import mujoco +import numpy as np + +class Context: + """渲染上下文类,用于管理渲染配置""" + def __init__(self, model, max_resolution): + self.model = model + self.max_resolution = max_resolution # 格式:[宽度, 高度] +# 必须确保此文件的路径和内容完全匹配 +class Context: + def __init__(self, model, max_resolution): + self.model = model + self.max_resolution = max_resolution + +class Camera: + def __init__(self, context, model, data, camera_id, dt): + self.context = context + self.model = model + self.data = data + self.camera_id = camera_id + self.dt = dt + self._fps = 1.0 / dt + + def render(self): + return (None, None) \ No newline at end of file From f8c847d4fa57afdf7bbd7865b698783821244db6 Mon Sep 17 00:00:00 2001 From: Xinyue-cao <648343923@qq.com> Date: Mon, 8 Dec 2025 21:59:27 +0800 Subject: [PATCH 03/14] =?UTF-8?q?=E8=A7=A3=E5=86=B3simulator.py=E7=9A=84?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E5=86=B2=E7=AA=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/box/simulator.py | 840 +------------------------------------------ 1 file changed, 17 insertions(+), 823 deletions(-) diff --git a/src/box/simulator.py b/src/box/simulator.py index 307e7bcf14..5fdada1437 100644 --- a/src/box/simulator.py +++ b/src/box/simulator.py @@ -1,27 +1,12 @@ -<<<<<<< HEAD import sys import os -# 获取项目根目录的绝对路径 -project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../")) -# 将根目录加入Python路径 -sys.path.insert(0, project_root) -======= ->>>>>>> origin/main import gymnasium as gym from gymnasium import spaces import pygame import mujoco -<<<<<<< HEAD import numpy as np import scipy import matplotlib -======= -import os -import numpy as np -import scipy -import matplotlib -import sys ->>>>>>> origin/main import importlib import shutil import inspect @@ -31,7 +16,6 @@ from collections import defaultdict import xml.etree.ElementTree as ET -<<<<<<< HEAD from stable_baselines3 import PPO # required to load a trained LLC policy in HRL approach # -------------------------- 修复导入路径 -------------------------- @@ -212,7 +196,7 @@ def build(cls, config): # Initialize simulator to get final model state model, _, _, _, _, _ = cls._initialise( - config, simulator_folder, {**run_parameters, "build": True} + config, simulator_folder, {** run_parameters, "build": True} ) # Save updated XML and binary model (faster loading) @@ -324,11 +308,11 @@ def _initialise(cls, config, simulator_folder, run_parameters): callbacks[cb["name"]] = cb_cls(cb["name"], **cb["kwargs"]) # Merge parameters for component initialization - common_kwargs = {**run_parameters, **callbacks} + common_kwargs = {** run_parameters, **callbacks} # Initialize core components - task = task_cls(model, data, **{**task_kwargs, **common_kwargs}) - bm_model = bm_cls(model, data,** {**bm_kwargs, **common_kwargs}) + task = task_cls(model, data,**{** task_kwargs, **common_kwargs}) + bm_model = bm_cls(model, data, **{** bm_kwargs, **common_kwargs}) perception = Perception( model, data, bm_model, perception_modules, common_kwargs ) @@ -635,7 +619,7 @@ def _initialise_observation_space(self): if state_params["shape"] == (0,): state_params["shape"] = (1,) obs_dict["stateful_information"] = spaces.Box( - dtype=np.float32,** state_params + dtype=np.float32, **state_params ) return spaces.Dict(obs_dict) @@ -1010,7 +994,7 @@ def close(self, **kwargs): # Clean up components self.task.close(** kwargs) self.perception.close(**kwargs) - self.bm_model.close(**kwargs) + self.bm_model.close(** kwargs) # Clean up Pygame if self._render_window is not None: @@ -1019,6 +1003,16 @@ def close(self, **kwargs): self._render_window = None self._render_clock = None + # Callback methods + def callback(self, callback_name, num_timesteps): + """ Update a specific callback """ + self.callbacks[callback_name].update(num_timesteps) + + def update_callbacks(self, num_timesteps): + """ Update all registered callbacks """ + for callback_name in self.callbacks: + self.callback(callback_name, num_timesteps) + # Property getters (read-only) @property def fps(self): @@ -1053,804 +1047,4 @@ def __del__(self): try: self.close() except Exception: - pass -======= -from stable_baselines3 import PPO #required to load a trained LLC policy in HRL approach - -from .perception.base import Perception -from .utils.rendering import Camera, Context -from .utils.functions import output_path, parent_path, is_suitable_package_name, parse_yaml, write_yaml - - -class Simulator(gym.Env): - """ - The Simulator class contains functionality to build a standalone Python package from a config file. The built package - integrates a biomechanical model, a task model, and a perception model into one simulator that implements a gym.Env - interface. - """ - - # May be useful for later, the three digit number suffix is of format X.Y.Z where X is a major version. - version = "1.1.0" - - @classmethod - def get_class(cls, *args): - """ Returns a class from given strings. The last element in args should contain the class name. """ - # TODO check for incorrect module names etc - modules = ".".join(args[:-1]) - if "." in args[-1]: - splitted = args[-1].split(".") - if modules == "": - modules = ".".join(splitted[:-1]) - else: - modules += "." + ".".join(splitted[:-1]) - cls_name = splitted[-1] - else: - cls_name = args[-1] - module = cls.get_module(modules) - return getattr(module, cls_name) - - @classmethod - def get_module(cls, *args): - """ Returns a module from given strings. """ - src = __name__.split(".")[0] - modules = ".".join(args) - return importlib.import_module(src + "." + modules) - - @classmethod - def build(cls, config): - """ Builds a simulator based on a config. The input 'config' may be a dict (parsed from YAML) or path to a YAML file - - Args: - config: - - A dict containing configuration information. See example configs in folder uitb/configs/ - - A path to a config file - """ - - # If config is a path to the config file, parse it first - if isinstance(config, str): - if not os.path.isfile(config): - raise FileNotFoundError(f"Given config file {config} does not exist") - config = parse_yaml(config) - - # Make sure required things are defined in config - assert "simulation" in config, "Simulation specs (simulation) must be defined in config" - assert "bm_model" in config["simulation"], "Biomechanical model (bm_model) must be defined in config" - assert "task" in config["simulation"], "task (task) must be defined in config" - - assert "run_parameters" in config["simulation"], "Run parameters (run_parameters) must be defined in config" - run_parameters = config["simulation"]["run_parameters"].copy() - assert "action_sample_freq" in run_parameters, "Action sampling frequency (action_sample_freq) must be defined " \ - "in run parameters" - - # Set simulator version - config["version"] = cls.version - - # Save generated simulators to uitb/simulators - if "simulator_folder" in config: - simulator_folder = os.path.normpath(config["simulator_folder"]) - else: - simulator_folder = os.path.join(output_path(), config["simulator_name"]) - - # If 'package_name' is not defined use 'simulator_name' - if "package_name" not in config: - config["package_name"] = config["simulator_name"] - if not is_suitable_package_name(config["package_name"]): - raise NameError("Package name defined in the config file (either through 'package_name' or 'simulator_name') is " - "not a suitable Python package name. Use only lower-case letters and underscores instead of " - "spaces, and the name cannot start with a number.") - - # The name used in gym has a suffix -v0 - config["gym_name"] = "uitb:" + config["package_name"] + "-v0" - - # Create a simulator in the simulator folder - cls._clone(simulator_folder, config["package_name"]) - - # Load task class - task_cls = cls.get_class("tasks", config["simulation"]["task"]["cls"]) - task_cls.clone(simulator_folder, config["package_name"], app_executable=config["simulation"]["task"].get("kwargs", {}).get("unity_executable", None)) - simulation = task_cls.initialise(config["simulation"]["task"].get("kwargs", {})) - - # Set some compiler options - # TODO: would make more sense to have a separate "environment" class / xml file that defines all these defaults, - # including e.g. cameras, lighting, etc., so that they could be easily changed. Task and biomechanical model would - # be integrated into that object - compiler_defaults = {"inertiafromgeom": "auto", "balanceinertia": "true", "boundmass": "0.001", - "boundinertia": "0.001", "inertiagrouprange": "0 1"} - compiler = simulation.find("compiler") - if compiler is None: - ET.SubElement(simulation, "compiler", compiler_defaults) - else: - compiler.attrib.update(compiler_defaults) - - # Load biomechanical model class - bm_cls = cls.get_class("bm_models", config["simulation"]["bm_model"]["cls"]) - bm_cls.clone(simulator_folder, config["package_name"]) - bm_cls.insert(simulation) - - # Add perception modules - for module_cfg in config["simulation"].get("perception_modules", []): - module_cls = cls.get_class("perception", module_cfg["cls"]) - module_kwargs = module_cfg.get("kwargs", {}) - module_cls.clone(simulator_folder, config["package_name"]) - module_cls.insert(simulation, **module_kwargs) - - # Clone also RL library files so the package will be completely standalone - rl_cls = cls.get_class("rl", config["rl"]["algorithm"]) - rl_cls.clone(simulator_folder, config["package_name"]) - - # TODO read the xml file directly from task.getroot() instead of writing it to a file first; need to input a dict - # of assets to mujoco.MjModel.from_xml_path - simulation_file = os.path.join(simulator_folder, config["package_name"], "simulation") - with open(simulation_file+".xml", 'w') as file: - simulation.write(file, encoding='unicode') - - # Initialise the simulator - model, _, _, _, _, _ = \ - cls._initialise(config, simulator_folder, {**run_parameters, "build": True}) - - # Now that simulator has been initialised, everything should be set. Now we want to save the xml file again, but - # mujoco only is able to save the latest loaded xml file (which is either the task or bm model xml files which are - # are read in their __init__ functions), hence we need to read the file we've generated again before saving the - # modified model - mujoco.MjModel.from_xml_path(simulation_file+".xml") - mujoco.mj_saveLastXML(simulation_file+".xml", model) - - # Save the modified model also as binary for faster loading - mujoco.mj_saveModel(model, simulation_file+".mjcf", None) - - # Input built time into config - config["built"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - - # Save config - write_yaml(config, os.path.join(simulator_folder, "config.yaml")) - - return simulator_folder - - @classmethod - def _clone(cls, simulator_folder, package_name): - """ Create a folder for the simulator being built, and copy or create relevant files. - - Args: - simulator_folder: Location of the simulator. - package_name: Name of the simulator (which is a python package). - """ - - # Create the folder - dst = os.path.join(simulator_folder, package_name) - os.makedirs(dst, exist_ok=True) - - # Copy simulator - src = pathlib.Path(inspect.getfile(cls)) - shutil.copyfile(src, os.path.join(dst, src.name)) - - # Create __init__.py with env registration - with open(os.path.join(dst, "__init__.py"), "w") as file: - file.write("from .simulator import Simulator\n\n") - file.write("from gymnasium.envs.registration import register\n") - file.write("import pathlib\n\n") - file.write("module_folder = pathlib.Path(__file__).parent\n") - file.write("simulator_folder = module_folder.parent\n") - file.write("kwargs = {'simulator_folder': simulator_folder}\n") - file.write("register(id=f'{module_folder.stem}-v0', entry_point=f'{module_folder.stem}.simulator:Simulator', kwargs=kwargs)\n") - - # Copy utils - shutil.copytree(os.path.join(parent_path(src), "utils"), os.path.join(simulator_folder, package_name, "utils"), - dirs_exist_ok=True) - # Copy train - shutil.copytree(os.path.join(parent_path(src), "train"), os.path.join(simulator_folder, package_name, "train"), - dirs_exist_ok=True) - # Copy test - shutil.copytree(os.path.join(parent_path(src), "test"), os.path.join(simulator_folder, package_name, "test"), - dirs_exist_ok=True) - - @classmethod - def _initialise(cls, config, simulator_folder, run_parameters): - """ Initialise a simulator -- i.e., create a MjModel, MjData, and initialise all necessary variables. - - Args: - config: A config dict. - simulator_folder: Location of the simulator. - run_parameters: Important run time variables that may also be used to override parameters. - """ - - # Get task class and kwargs - task_cls = cls.get_class("tasks", config["simulation"]["task"]["cls"]) - task_kwargs = config["simulation"]["task"].get("kwargs", {}) - - # Get bm class and kwargs - bm_cls = cls.get_class("bm_models", config["simulation"]["bm_model"]["cls"]) - bm_kwargs = config["simulation"]["bm_model"].get("kwargs", {}) - - # Initialise perception modules - perception_modules = {} - for module_cfg in config["simulation"].get("perception_modules", []): - module_cls = cls.get_class("perception", module_cfg["cls"]) - module_kwargs = module_cfg.get("kwargs", {}) - perception_modules[module_cls] = module_kwargs - - # Get simulation file - simulation_file = os.path.join(simulator_folder, config["package_name"], "simulation") - - # Load the mujoco model; try first with the binary model (faster, contains some parameters that may be lost when - # re-saving xml files like body mass). For some reason the binary model fails to load in some situations (like - # when the simulator has been built on a different computer) - # TODO loading from binary disabled, weird problems (like a body not found from model when loaded from binary, but - # found correctly when model loaded from xml) - # try: - # model = mujoco.MjModel.from_binary_path(simulation_file + ".mjcf") - # except: # TODO what was the exception type - model = mujoco.MjModel.from_xml_path(simulation_file + ".xml") - - # Initialise MjData - data = mujoco.MjData(model) - - # Add frame skip and dt to run parameters - run_parameters["frame_skip"] = int(1 / (model.opt.timestep * run_parameters["action_sample_freq"])) - run_parameters["dt"] = model.opt.timestep*run_parameters["frame_skip"] - - # Initialise a rendering context, required for e.g. some vision modules - run_parameters["rendering_context"] = Context(model, - max_resolution=run_parameters.get("max_resolution", [1280, 960])) - - # Initialise callbacks - callbacks = {} - for cb in run_parameters.get("callbacks", []): - callbacks[cb["name"]] = cls.get_class(cb["cls"])(cb["name"], **cb["kwargs"]) - - # Now initialise the actual classes; model and data are input to the inits so that stuff can be modified if needed - # (e.g. move target to a specific position wrt to a body part) - task = task_cls(model, data, **{**task_kwargs, **callbacks, **run_parameters}) - bm_model = bm_cls(model, data, **{**bm_kwargs, **callbacks, **run_parameters}) - perception = Perception(model, data, bm_model, perception_modules, {**callbacks, **run_parameters}) - - return model, data, task, bm_model, perception, callbacks - - @classmethod - def get(cls, simulator_folder, render_mode="rgb_array", render_mode_perception="embed", render_show_depths=False, run_parameters=None, use_cloned=True): - """ Returns a Simulator that is located in given folder. - - Args: - simulator_folder: Location of the simulator. - render_mode: Whether render() will return a single rgb array (render_mode="rgb_array"), - a list of rgb arrays (render_mode="rgb_array_list"; - adapted from https://github.com/openai/gym/blob/master/gym/wrappers/render_collection.py), - or None while the frames in a separate PyGame window are updated directly when calling - step() or reset() (render_mode="human"; - adapted from https://github.com/openai/gym/blob/master/gym/wrappers/human_rendering.py)). - render_mode_perception: Whether images of visual perception modules should be directly embedded into main camera view ("embed"), stored as separate videos ("separate"), or not used at all [which allows to watch vision in Unity Editor if debug mode is enabled/standalone app is disabled] (None) - render_show_depths: Whether depth images of visual perception modules should be included in rendering. - run_parameters: Can be used to override parameters during run time. - use_cloned: Can be useful for debugging. Set to False to use original files instead of the ones that have been - cloned/copied during building phase. - """ - - # Read config file - config_file = os.path.join(simulator_folder, "config.yaml") - try: - config = parse_yaml(config_file) - except: - raise FileNotFoundError(f"Could not open file {config_file}") - - # Make sure the simulator has been built - if "built" not in config: - raise RuntimeError("Simulator has not been built") - - # Make sure simulator_folder is in path (used to import gen_cls_cloned) - if simulator_folder not in sys.path: - sys.path.insert(0, simulator_folder) - - # Get Simulator class - gen_cls_cloned = getattr(importlib.import_module(config["package_name"]), "Simulator") - if hasattr(gen_cls_cloned, "version"): - _legacy_mode = False - gen_cls_cloned_version = gen_cls_cloned.version.split("-v")[-1] - else: - _legacy_mode = True - gen_cls_cloned_version = gen_cls_cloned.id.split("-v")[-1] #deprecated - if use_cloned: - gen_cls = gen_cls_cloned - else: - gen_cls = cls - gen_cls_version = gen_cls.version.split("-v")[-1] - - if gen_cls_version.split(".")[0] > gen_cls_cloned_version.split(".")[0]: - raise RuntimeError( - f"""Severe version mismatch. The simulator '{config["simulator_name"]}' has version {gen_cls_cloned_version}, while your uitb package has version {gen_cls_version}.\nTo run with version {gen_cls_cloned_version}, set 'use_cloned=True'.""") - elif gen_cls_version.split(".")[1] > gen_cls_cloned_version.split(".")[1]: - print( - f"""WARNING: Version mismatch. The simulator '{config["simulator_name"]}' has version {gen_cls_cloned_version}, while your uitb package has version {gen_cls_version}.\nTo run with version {gen_cls_version}, set 'use_cloned=True'.""") - - if _legacy_mode: - _simulator = gen_cls(simulator_folder, run_parameters=run_parameters) - else: - try: - _simulator = gen_cls(simulator_folder, render_mode=render_mode, render_mode_perception=render_mode_perception, render_show_depths=render_show_depths, - run_parameters=run_parameters) - except TypeError: - _simulator = gen_cls(simulator_folder, render_mode=render_mode, render_show_depths=render_show_depths, - run_parameters=run_parameters) - - # Return Simulator object - return _simulator - - def __init__(self, simulator_folder, render_mode="rgb_array", render_mode_perception="embed", render_show_depths=False, run_parameters=None): - """ Initialise a new `Simulator`. - - Args: - simulator_folder: Location of a simulator. - render_mode: Whether render() will return a single rgb array (render_mode="rgb_array"), - a list of rgb arrays (render_mode="rgb_array_list"; - adapted from https://github.com/openai/gym/blob/master/gym/wrappers/render_collection.py), - or None while the frames in a separate PyGame window are updated directly when calling - step() or reset() (render_mode="human"; - adapted from https://github.com/openai/gym/blob/master/gym/wrappers/human_rendering.py)). - render_mode_perception: Whether images of visual perception modules should be directly embedded into main camera view ("embed"), stored as separate videos ("separate"), or not used at all [which allows to watch vision in Unity Editor if debug mode is enabled/standalone app is disabled] (None) - render_show_depths: Whether depth images of visual perception modules should be included in rendering. - run_parameters: Can be used to override parameters during run time. - """ - - # Make sure simulator exists - if not os.path.exists(simulator_folder): - raise FileNotFoundError(f"Simulator folder {simulator_folder} does not exists") - self._simulator_folder = simulator_folder - - # Read config - self._config = parse_yaml(os.path.join(self._simulator_folder, "config.yaml")) - - # Get run parameters: these parameters can be used to override parameters used during training - self._run_parameters = self._config["simulation"]["run_parameters"].copy() - self._run_parameters.update(run_parameters or {}) - - # Initialise simulation - self._model, self._data, self.task, self.bm_model, self.perception, self.callbacks = \ - self._initialise(self._config, self._simulator_folder, self._run_parameters) - - # Set action space TODO for now we assume all actuators have control signals between [-1, 1] - self.action_space = self._initialise_action_space() - - # Set observation space - self.observation_space = self._initialise_observation_space() - - # Collect some episode statistics - self._episode_statistics = {"length (seconds)": 0, "length (steps)": 0, "reward": 0} - - # Initialise viewer - self._GUI_camera = Camera(self._run_parameters["rendering_context"], self._model, self._data, camera_id='for_testing', - dt=self._run_parameters["dt"]) - - self._render_mode = render_mode - self._render_mode_perception = render_mode_perception #whether perception camera views should be directly embedded into camera view of camera_id ("embed"), stored in self._render_stack_perception ("separate"), or not used at all "separate"), or not used at all [which allows to watch vision in Unity Editor if debug mode is enabled/standalone app is disabled] (None) - self._render_stack = [] #only used if render_mode == "rgb_array_list" - self._render_stack_perception = defaultdict(list) #only used if render_mode == "rgb_array_list" and self._render_mode_perception == "separate" - self._render_stack_pop = True #If True, clear the render stack after .render() is called. - self._render_stack_clean_at_reset = True #If True, clear the render stack when .reset() is called. - self._render_show_depths = render_show_depths #If True, depth images of visual perception modules are included in GUI rendering. - self._render_screen_size = None #only used if render_mode == "human" - self._render_window = None #only used if render_mode == "human" - self._render_clock = None #only used if render_mode == "human" - - if 'llc' in self.config: #if HRL approach is used - llc_simulator_folder = os.path.join(output_path(), self.config["llc"]["simulator_name"]) - if llc_simulator_folder not in sys.path: - sys.path.insert(0, llc_simulator_folder) - if not os.path.exists(llc_simulator_folder): - raise FileNotFoundError(f"Simulator folder {llc_simulator_folder} does not exists") - llccheckpoint_dir = os.path.join(llc_simulator_folder, 'checkpoints') - # Load policy TODO should create a load method for uitb.rl.BaseRLModel - print(f'Loading model: {os.path.join(llccheckpoint_dir, self.config["llc"]["checkpoint"])}\n') - self.llc_model = PPO.load(os.path.join(llccheckpoint_dir, self.config["llc"]["checkpoint"])) - self.action_space = self._initialise_HRL_action_space() - self._max_steps = self.config["llc"]["llc_ratio"] - self._dwell_threshold = int(0.5*self._max_steps) - self._target_radius = 0.05 - self._independent_dofs = [] - self._independent_joints = [] - joints = self.config["llc"]["joints"] - for joint in joints: - joint_id = mujoco.mj_name2id(self._model, mujoco.mjtObj.mjOBJ_JOINT, joint) - if self._model.jnt_type[joint_id] not in [mujoco.mjtJoint.mjJNT_HINGE, mujoco.mjtJoint.mjJNT_SLIDE]: - raise NotImplementedError(f"Only 'hinge' and 'slide' joints are supported, joint " - f"{joint} is of type {mujoco.mjtJoint(self._model.jnt_type[joint_id]).name}") - self._independent_dofs.append(self._model.jnt_qposadr[joint_id]) - self._independent_joints.append(joint_id) - self._jnt_range = self._model.jnt_range[self._independent_joints] - - - #To normalize joint ranges for llc - def _normalise_qpos(self, qpos): - # Normalise to [0, 1] - qpos = (qpos - self._jnt_range[:, 0]) / (self._jnt_range[:, 1] - self._jnt_range[:, 0]) - # Normalise to [-1, 1] - qpos = (qpos - 0.5) * 2 - return qpos - - def _initialise_action_space(self): - """ Initialise action space. """ - num_actuators = self.bm_model.nu + self.perception.nu - actuator_limits = np.ones((num_actuators,2)) * np.array([-1.0, 1.0]) - return spaces.Box(low=np.float32(actuator_limits[:, 0]), high=np.float32(actuator_limits[:, 1])) - - def _initialise_HRL_action_space(self): - bm_nu = self.bm_model.nu - bm_jnt_range = np.ones((bm_nu,2)) * np.array([-1.0, 1.0]) - perception_nu = self.perception.nu - perception_jnt_range = np.ones((perception_nu,2)) * np.array([-1.0, 1.0]) - jnt_range = np.concatenate((bm_jnt_range, perception_jnt_range), axis=0) - action_space = gym.spaces.Box(low=jnt_range[:,0], high=jnt_range[:,1]) - return action_space - - def _initialise_observation_space(self): - """ Initialise observation space. """ - observation = self.get_observation() - obs_dict = dict() - for module in self.perception.perception_modules: - obs_dict[module.modality] = spaces.Box(dtype=np.float32, **module.get_observation_space_params()) - if "stateful_information" in observation: - obs_dict["stateful_information"] = spaces.Box(dtype=np.float32, - **self.task.get_stateful_information_space_params()) - return spaces.Dict(obs_dict) - - def _get_qpos(self, model, data): - qpos = data.qpos[self._independent_dofs].copy() - qpos = self._normalise_qpos(qpos) - return qpos - - def step(self, action): - """ Step simulation forward with given actions. - - Args: - action: Actions sampled from a policy. Limited to range [-1, 1]. - """ - if 'llc' in self.config: #if HRL approach is used - self.task._target_qpos = action # action to pass to LLC - self._steps = 0 # Initialise loop control to 0 - #acc_reward = 0 #To be used when rewards are being accumulated in llc steps - - while self._steps < self._max_steps: # loop for llc controls based on llc_ratio - - llc_action, _states = self.llc_model.predict(self.get_llcobservation(action), deterministic=True) # Get BM action from LLC - # Set control for the bm model - self.bm_model.set_ctrl(self._model, self._data, llc_action) - - # Set control for perception modules (e.g. eye movements) - self.perception.set_ctrl(self._model, self._data, action[self.bm_model.nu:]) - - # Advance the simulation - mujoco.mj_step(self._model, self._data, nstep=int(self._run_parameters["frame_skip"])) # Number of timesteps to skip for LLC - - # Update bm model (e.g. update constraints); updates also effort model - self.bm_model.update(self._model, self._data) - - # Update perception modules - self.perception.update(self._model, self._data) - - dist = np.abs(action - self._get_qpos(self._model, self._data)) - - # Update environment - reward, terminated, truncated, info = self.task.update(self._model, self._data) - - # Add an effort cost to reward - reward -= self.bm_model.get_effort_cost(self._model, self._data) - - #acc_reward += reward #To be used when rewards are being accumulated in llc steps - - # Get observation - obs = self.get_observation() - - # Add frame to stack - if self._render_mode == "rgb_array_list": - self._render_stack.append(self._GUI_rendering()) - elif self._render_mode == "human": - self._GUI_rendering_pygame() - - if truncated or terminated: - break - - # Pointing - if "target_spawned" in info: - if info["target_spawned"] or info["target_hit"]: - break - - # Choice Reaction - elif "new_button_generated" in info: - if info["new_button_generated"] or info["target_hit"]: - break - - self._steps += 1 - if np.all(dist < self._target_radius): - break - - return obs, reward, terminated, truncated, info - - else: - # Set control for the bm model - self.bm_model.set_ctrl(self._model, self._data, action[:self.bm_model.nu]) - - # Set control for perception modules (e.g. eye movements) - self.perception.set_ctrl(self._model, self._data, action[self.bm_model.nu:]) - - # Advance the simulation - mujoco.mj_step(self._model, self._data, nstep=self._run_parameters["frame_skip"]) - - # Update bm model (e.g. update constraints); updates also effort model - self.bm_model.update(self._model, self._data) - - # Update perception modules - self.perception.update(self._model, self._data) - - # Update environment - reward, terminated, truncated, info = self.task.update(self._model, self._data) - - # Add an effort cost to reward - effort_cost = self.bm_model.get_effort_cost(self._model, self._data) - info["EffortCost"] = effort_cost - reward -= effort_cost - - # Get observation - obs = self.get_observation(info) - - # Add frame to stack - if self._render_mode == "rgb_array_list": - self._render_stack.append(self._GUI_rendering()) - elif self._render_mode == "human": - self._GUI_rendering_pygame() - - return obs, reward, terminated, truncated, info - - - def get_observation(self, info=None): - """ Returns an observation from the perception model. - - Returns: - A dict with observations from individual perception modules. May also contain stateful information from a task. - """ - - # Get observation from perception - observation = self.perception.get_observation(self._model, self._data, info) - - # Add any stateful information that is required - stateful_information = self.task.get_stateful_information(self._model, self._data) - if stateful_information.size > 0: #TODO: define stateful_information (and encoder) that can be used as default, if no stateful information is provided (zero-size arrays do not work with sb3 currently...) - observation["stateful_information"] = stateful_information - - return observation - - def get_llcobservation(self,action): - """ Returns an observation from the perception model. - - Returns: - A dict with observations from individual perception modules. May also contain stateful information from a task. - """ - - # Get observation from perception - observation = self.perception.get_observation(self._model, self._data) - - # Remove Vision for LLC - observation.pop("vision") - qpos = self._get_qpos(self._model, self._data) - qpos_diff = action - qpos - - # Stateful Information for LLC policy - stateful_information = qpos_diff - if stateful_information is not None: - observation["stateful_information"] = stateful_information - - return observation - - - def reset(self, seed=None): - """ Reset the simulator and return an observation. """ - - super().reset(seed=seed) - - # Reset sim - mujoco.mj_resetData(self._model, self._data) - - # Reset all models - self.bm_model.reset(self._model, self._data) - self.perception.reset(self._model, self._data) - info = self.task.reset(self._model, self._data) - - # Do a forward so everything will be set - mujoco.mj_forward(self._model, self._data) - - if self._render_mode == "rgb_array_list": - if self._render_stack_clean_at_reset: - self._render_stack = [] - self._render_stack_perception = defaultdict(list) - self._render_stack.append(self._GUI_rendering()) - elif self._render_mode == "human": - self._GUI_rendering_pygame() - - return self.get_observation(), info - - def render(self): - if self._render_mode == "rgb_array_list": - render_stack = self._render_stack - if self._render_stack_pop: - self._render_stack = [] - return render_stack - elif self._render_mode == "rgb_array": - return self._GUI_rendering() - else: - return None - - def get_render_stack_perception(self): - render_stack_perception = self._render_stack_perception - # if self._render_stack_pop: - # self._render_stack_perception = defaultdict(list) - return render_stack_perception - - def _GUI_rendering(self): - # Grab an image from the 'for_testing' camera and grab all GUI-prepared images from included visual perception modules, and display them 'picture-in-picture' - - # Grab images - img, _ = self._GUI_camera.render() - - if self._render_mode_perception == "embed": - # Embed perception camera images into main camera image - - # perception_camera_images = [rgb_or_depth_array for camera in self.perception.cameras - # for rgb_or_depth_array in camera.render() if rgb_or_depth_array is not None] - perception_camera_images = self.perception.get_renders() - - # TODO: add text annotations to perception camera images - if len(perception_camera_images) > 0: - _img_size = img.shape[:2] #(height, width) - - - # Vertical alignment of perception camera images, from bottom right to top right - ## TODO: allow for different inset locations - _desired_subwindow_height = np.round(_img_size[0] / len(perception_camera_images)).astype(int) - _maximum_subwindow_width = np.round(0.2 * _img_size[1]).astype(int) - - perception_camera_images_resampled = [] - for ocular_img in perception_camera_images: - # Convert 2D depth arrays to 3D heatmap arrays - if ocular_img.ndim == 2: - if self._render_show_depths: - ocular_img = matplotlib.pyplot.imshow(ocular_img, cmap=matplotlib.pyplot.cm.jet, interpolation='bicubic').make_image('TkAgg', unsampled=True)[0][ - ..., :3] - matplotlib.pyplot.close() #delete image - else: - continue - - resample_factor = min(_desired_subwindow_height / ocular_img.shape[0], _maximum_subwindow_width / ocular_img.shape[1]) - - resample_height = np.round(ocular_img.shape[0] * resample_factor).astype(int) - resample_width = np.round(ocular_img.shape[1] * resample_factor).astype(int) - resampled_img = np.zeros((resample_height, resample_width, ocular_img.shape[2]), dtype=np.uint8) - for channel in range(ocular_img.shape[2]): - resampled_img[:, :, channel] = scipy.ndimage.zoom(ocular_img[:, :, channel], resample_factor, order=0) - - perception_camera_images_resampled.append(resampled_img) - - ocular_img_bottom = _img_size[0] - for ocular_img_idx, ocular_img in enumerate(perception_camera_images_resampled): - #print(f"Modify ({ocular_img_bottom - ocular_img.shape[0]}, { _img_size[1] - ocular_img.shape[1]})-({ocular_img_bottom}, {img.shape[1]}).") - img[ocular_img_bottom - ocular_img.shape[0]:ocular_img_bottom, _img_size[1] - ocular_img.shape[1]:] = ocular_img - ocular_img_bottom -= ocular_img.shape[0] - # input((len(perception_camera_images_resampled), perception_camera_images_resampled[0].shape, img.shape)) - elif self._render_mode_perception == "separate": - for module, camera_list in self.perception.cameras_dict.items(): - for camera in camera_list: - for rgb_or_depth_array in camera.render(): - if rgb_or_depth_array is not None: - self._render_stack_perception[f"{module.modality}/{type(camera).__name__}"].append(rgb_or_depth_array) - - return img - - def _GUI_rendering_pygame(self): - rgb_array = np.transpose(self._GUI_rendering(), axes=(1, 0, 2)) - - if self._render_screen_size is None: - self._render_screen_size = rgb_array.shape[:2] - - assert self._render_screen_size == rgb_array.shape[ - :2], f"Expected an rgb array of shape {self._render_screen_size} from self._GUI_camera, but received an rgb array of shape {rgb_array.shape[:2]}. " - - if self._render_window is None: - pygame.init() - pygame.display.init() - self._render_window = pygame.display.set_mode(self._render_screen_size) - - if self._render_clock is None: - self._render_clock = pygame.time.Clock() - - surf = pygame.surfarray.make_surface(rgb_array) - self._render_window.blit(surf, (0, 0)) - pygame.event.pump() - self._render_clock.tick(self.fps) - pygame.display.flip() - - def close(self): - """ Close the rendering window (if self._render_mode == 'human').""" - super().close() - if self._render_window is not None: - import pygame - - pygame.display.quit() - pygame.quit() - - @property - def fps(self): - return self._GUI_camera._fps - - def callback(self, callback_name, num_timesteps): - """ Update a callback -- may be useful during training, e.g. for curriculum learning. """ - self.callbacks[callback_name].update(num_timesteps) - - def update_callbacks(self, num_timesteps): - """ Update all callbacks. """ - for callback_name in self.callbacks: - self.callback(callback_name, num_timesteps) - - # def get_logdict_keys(self): - # return list(self.task._info["log_dict"].keys()) - - # def get_logdict_value(self, key): - # return self.task._info["log_dict"].get(key) - - @property - def config(self): - """ Return config. """ - return copy.deepcopy(self._config) - - @property - def run_parameters(self): - """ Return run parameters. """ - # Context cannot be deep copied - exclude = {"rendering_context"} - run_params = {k: copy.deepcopy(self._run_parameters[k]) for k in self._run_parameters.keys() - exclude} - run_params["rendering_context"] = self._run_parameters["rendering_context"] - return run_params - - @property - def simulator_folder(self): - """ Return simulator folder. """ - return self._simulator_folder - - @property - def render_mode(self): - """ Return render mode. """ - return self._render_mode - - def get_state(self): - """ Return a state of the simulator / individual components (biomechanical model, perception model, task). - - This function is used for logging/evaluation purposes, not for RL training. - - Returns: - A dict with one float or numpy vector per keyword. - """ - - # Get time, qpos, qvel, qacc, act_force, act, ctrl of the current simulation - state = {"timestep": self._data.time, - "qpos": self._data.qpos.copy(), - "qvel": self._data.qvel.copy(), - "qacc": self._data.qacc.copy(), - "act_force": self._data.actuator_force.copy(), - "act": self._data.act.copy(), - "ctrl": self._data.ctrl.copy()} - - # Add state from the task - state.update(self.task.get_state(self._model, self._data)) - - # Add state from the biomechanical model - state.update(self.bm_model.get_state(self._model, self._data)) - - # Add state from the perception model - state.update(self.perception.get_state(self._model, self._data)) - - return state - - def close(self, **kwargs): - """ Perform any necessary clean up. - - This function is inherited from gym.Env. It should be automatically called when this object is garbage collected - or the program exists, but that doesn't seem to be the case. This function will be called if this object has been - initialised in the context manager fashion (i.e. using the 'with' statement). """ - self.task.close(**kwargs) - self.perception.close(**kwargs) - self.bm_model.close(**kwargs) ->>>>>>> origin/main + pass \ No newline at end of file From 994ce5d97a728b5c95dc82a7181ade7d9090461d Mon Sep 17 00:00:00 2001 From: Xinyue-cao <648343923@qq.com> Date: Tue, 9 Dec 2025 11:25:04 +0800 Subject: [PATCH 04/14] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dbug,=E4=BD=93=E7=8E=B0?= =?UTF-8?q?=E4=BB=BF=E7=9C=9F=E6=95=88=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/box/__pycache__/simulator.cpython-313.pyc | Bin 44564 -> 14082 bytes src/box/{mian.py => _init_.py} | 0 src/box/perception/vision.py | 62 + src/box/simulator.py | 1248 ++++------------- src/box/simulators/2.py | 0 .../simulators/arm_simulation/arm_model.mjcf | 14 + .../arm_simulation/arm_simulation.xml | 14 + src/box/simulators/arm_simulation/config.yaml | 4 + src/box/test.simulator.py | 118 +- 9 files changed, 425 insertions(+), 1035 deletions(-) rename src/box/{mian.py => _init_.py} (100%) create mode 100644 src/box/simulators/2.py create mode 100644 src/box/simulators/arm_simulation/arm_model.mjcf create mode 100644 src/box/simulators/arm_simulation/arm_simulation.xml create mode 100644 src/box/simulators/arm_simulation/config.yaml diff --git a/src/box/__pycache__/simulator.cpython-313.pyc b/src/box/__pycache__/simulator.cpython-313.pyc index 233c64a4e9faf67a35d1d74bec72665a4015a211..22b2ddf71e59673f1a8c18ea54f2034f399c65d5 100644 GIT binary patch literal 14082 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 literal 44564 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 diff --git a/src/box/mian.py b/src/box/_init_.py similarity index 100% rename from src/box/mian.py rename to src/box/_init_.py diff --git a/src/box/perception/vision.py b/src/box/perception/vision.py index e69de29bb2..db72c89b67 100644 --- a/src/box/perception/vision.py +++ b/src/box/perception/vision.py @@ -0,0 +1,62 @@ +import numpy as np +import mujoco +from .base import PerceptionModule +from src.box.utils.rendering import Camera # 复用仿真器的Camera类 + +class VisionPerception(PerceptionModule): + """视觉感知模块,支持RGB/深度图采集""" + def __init__(self, model, data, camera_id="rgb_camera", resolution=(640, 480), + capture_depth=False, normalize=True, **kwargs): + super().__init__(modality="vision", model=model, data=data, **kwargs) + + # 相机配置 + self.camera_id = camera_id + self.resolution = resolution + self.capture_depth = capture_depth + self.normalize = normalize + + # 初始化相机(复用仿真器的Camera类) + self.camera = Camera( + context=self.kwargs.get("rendering_context"), + model=model, + data=data, + camera_id=camera_id, + dt=self.kwargs.get("dt", 0.01) + ) + self.cameras = [self.camera] # 关联到模块相机列表 + + def get_observation(self, model, data, info=None): + """获取视觉观测数据(RGB/深度图)""" + # 渲染相机画面 + rgb_img, depth_img = self.camera.render() + + # 拼接观测(RGB为主,可选深度) + if self.capture_depth: + # 归一化深度图 + depth_img = (depth_img - depth_img.min()) / (depth_img.max() - depth_img.min() + 1e-8) + obs = np.concatenate([rgb_img, depth_img[..., np.newaxis]], axis=-1) + else: + obs = rgb_img + + # 归一化到[0,1](可选) + if self.normalize: + obs = obs.astype(np.float32) / 255.0 + + # 展平为一维向量(适配RL观测空间) + return obs.flatten() + + def get_observation_space_params(self): + """定义视觉观测空间参数(适配Gymnasium)""" + # 计算观测维度:RGB(3通道) + 深度(可选1通道) + channels = 3 + (1 if self.capture_depth else 0) + shape = (self.resolution[0] * self.resolution[1] * channels,) + return { + "low": 0.0, + "high": 1.0, + "shape": shape + } + + def get_renders(self): + """返回原始RGB图像(用于仿真器渲染)""" + rgb_img, _ = self.camera.render() + return [rgb_img] \ No newline at end of file diff --git a/src/box/simulator.py b/src/box/simulator.py index 5fdada1437..866969922e 100644 --- a/src/box/simulator.py +++ b/src/box/simulator.py @@ -1,1050 +1,278 @@ -import sys import os -import gymnasium as gym -from gymnasium import spaces +import numpy as np import pygame import mujoco -import numpy as np -import scipy -import matplotlib -import importlib -import shutil -import inspect -import pathlib -from datetime import datetime -import copy -from collections import defaultdict -import xml.etree.ElementTree as ET - -from stable_baselines3 import PPO # required to load a trained LLC policy in HRL approach - -# -------------------------- 修复导入路径 -------------------------- -# 统一使用项目根目录的绝对导入 -from src.box.perception.base import Perception -# 若rendering.py中无Camera/Context,需确认文件是否存在或类是否正确定义 -from src.box.utils.rendering import Camera, Context # 确保rendering.py中包含这两个类 -from src.box.utils.functions import output_path, parent_path, is_suitable_package_name, parse_yaml, write_yaml -# ------------------------------------------------------------------ - - -class Simulator(gym.Env): - """ - The Simulator class contains functionality to build a standalone Python package from a config file. - The built package integrates a biomechanical model, a task model, and a perception model into one - simulator that implements a gym.Env interface. - - Key features: - - Support for Hierarchical Reinforcement Learning (HRL) with Low-Level Controller (LLC) - - MuJoCo simulation integration with Gymnasium API - - Configurable rendering (rgb_array, rgb_array_list, human) - - Modular design for perception, biomechanical and task models - """ - # Version format: X.Y.Z (major.minor.patch) - version = "1.1.0" - - @classmethod - def get_class(cls, *args): - """ - Returns a class from given module path strings. - The last element in args should contain the class name. - - Args: - *args: Module path components + class name - - Returns: - type: Requested class object - """ - # Handle module path and class name parsing - modules = ".".join(args[:-1]) - if "." in args[-1]: - splitted = args[-1].split(".") - if modules == "": - modules = ".".join(splitted[:-1]) - else: - modules += "." + ".".join(splitted[:-1]) - cls_name = splitted[-1] - else: - cls_name = args[-1] - - module = cls.get_module(modules) - return getattr(module, cls_name) - - @classmethod - def get_module(cls, *args): - """ - Returns a module from given path strings. - - Args: - *args: Module path components - - Returns: - module: Imported module object - """ - src = __name__.split(".")[0] - modules = ".".join(args) - return importlib.import_module(src + "." + modules) - - @classmethod - def build(cls, config): - """ - Builds a simulator package from a configuration dict or YAML file path. - - Args: - config: Either a dict with configuration or path to YAML config file - - Returns: - str: Path to the built simulator folder - - Raises: - FileNotFoundError: If config file doesn't exist - AssertionError: If required config fields are missing - NameError: If package name is invalid - """ - # Parse config file if path is provided - if isinstance(config, str): - if not os.path.isfile(config): - raise FileNotFoundError(f"Config file {config} does not exist") - config = parse_yaml(config) - - # Validate required config fields - required_fields = [ - ("simulation", "Simulation specs must be defined in config"), - ("simulation.bm_model", "Biomechanical model must be defined in config"), - ("simulation.task", "Task must be defined in config"), - ("simulation.run_parameters", "Run parameters must be defined in config"), - ("simulation.run_parameters.action_sample_freq", - "Action sampling frequency must be defined in run parameters") - ] - - for field, msg in required_fields: - keys = field.split(".") - current = config - try: - for key in keys: - current = current[key] - except (KeyError, TypeError): - raise AssertionError(msg) - - # Prepare config - run_parameters = config["simulation"]["run_parameters"].copy() - config["version"] = cls.version - - # Determine simulator folder location - if "simulator_folder" in config: - simulator_folder = os.path.normpath(config["simulator_folder"]) - else: - simulator_folder = os.path.join(output_path(), config["simulator_name"]) - - # Validate and set package name - if "package_name" not in config: - config["package_name"] = config["simulator_name"] - - if not is_suitable_package_name(config["package_name"]): - raise NameError( - "Package name (package_name/simulator_name) is invalid. " - "Use lowercase letters and underscores only, cannot start with a number." - ) - - # Set gym registration name - config["gym_name"] = f"uitb:{config['package_name']}-v0" - - # Create simulator package structure - cls._clone(simulator_folder, config["package_name"]) - - # Initialize and integrate task model - task_cls = cls.get_class("tasks", config["simulation"]["task"]["cls"]) - task_kwargs = config["simulation"]["task"].get("kwargs", {}) - unity_exec = task_kwargs.get("unity_executable", None) - task_cls.clone(simulator_folder, config["package_name"], app_executable=unity_exec) - simulation = task_cls.initialise(task_kwargs) - - # Set MuJoCo compiler defaults - compiler_defaults = { - "inertiafromgeom": "auto", - "balanceinertia": "true", - "boundmass": "0.001", - "boundinertia": "0.001", - "inertiagrouprange": "0 1" - } - compiler = simulation.find("compiler") - if compiler is None: - ET.SubElement(simulation, "compiler", compiler_defaults) - else: - compiler.attrib.update(compiler_defaults) - - # Integrate biomechanical model - bm_cls = cls.get_class("bm_models", config["simulation"]["bm_model"]["cls"]) - bm_cls.clone(simulator_folder, config["package_name"]) - bm_cls.insert(simulation) - - # Integrate perception modules - for module_cfg in config["simulation"].get("perception_modules", []): - module_cls = cls.get_class("perception", module_cfg["cls"]) - module_kwargs = module_cfg.get("kwargs", {}) - module_cls.clone(simulator_folder, config["package_name"]) - module_cls.insert(simulation, **module_kwargs) - - # Clone RL library files - if "rl" in config: - rl_cls = cls.get_class("rl", config["rl"]["algorithm"]) - rl_cls.clone(simulator_folder, config["package_name"]) - - # Save initial simulation XML - simulation_file = os.path.join(simulator_folder, config["package_name"], "simulation") - with open(f"{simulation_file}.xml", 'w') as file: - simulation.write(file, encoding='unicode') - - # Initialize simulator to get final model state - model, _, _, _, _, _ = cls._initialise( - config, simulator_folder, {** run_parameters, "build": True} - ) - - # Save updated XML and binary model (faster loading) - mujoco.MjModel.from_xml_path(f"{simulation_file}.xml") - mujoco.mj_saveLastXML(f"{simulation_file}.xml", model) - mujoco.mj_saveModel(model, f"{simulation_file}.mjcf", None) - - # Record build time and save config - config["built"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - write_yaml(config, os.path.join(simulator_folder, "config.yaml")) - - return simulator_folder - - @classmethod - def _clone(cls, simulator_folder, package_name): - """ - Creates simulator package directory structure and copies required files. - - Args: - simulator_folder: Base folder for the simulator - package_name: Name of the Python package - """ - # Create package directory - pkg_dir = os.path.join(simulator_folder, package_name) - os.makedirs(pkg_dir, exist_ok=True) - - # Copy simulator class file - src_file = pathlib.Path(inspect.getfile(cls)) - shutil.copyfile(src_file, os.path.join(pkg_dir, src_file.name)) - - # Create __init__.py with gym registration - init_content = f"""from .simulator import Simulator - -from gymnasium.envs.registration import register -import pathlib - -module_folder = pathlib.Path(__file__).parent -simulator_folder = module_folder.parent -kwargs = {{'simulator_folder': simulator_folder}} -register( - id=f'{{module_folder.stem}}-v0', - entry_point=f'{{module_folder.stem}}.simulator:Simulator', - kwargs=kwargs -) -""" - with open(os.path.join(pkg_dir, "__init__.py"), "w") as file: - file.write(init_content) - - # Copy utility modules - utils_src = os.path.join(parent_path(src_file), "utils") - utils_dst = os.path.join(pkg_dir, "utils") - shutil.copytree(utils_src, utils_dst, dirs_exist_ok=True) - - # Copy train/test modules - for subdir in ["train", "test"]: - src = os.path.join(parent_path(src_file), subdir) - dst = os.path.join(pkg_dir, subdir) - shutil.copytree(src, dst, dirs_exist_ok=True) +import yaml +from gymnasium import spaces +from typing import Dict, Any, Optional, Tuple +import mujoco.viewer + +class Simulator: + def __init__(self, simulator_folder: str, render_mode: Optional[str] = None): + self.simulator_folder = simulator_folder + self.render_mode = render_mode + self.step_count = 0 + self.terminated = False + self.truncated = False + self.last_reward = 0.0 + self.model = None + self.data = None + self.viewer = None + self.screen = None + + # 1. 先加载配置 + self.config = self._load_config() + # 2. 再加载模型 + self.model, self.data = self._load_model() + # 3. 最后校验配置 + self._validate_config() + + # 初始化动作空间、观测空间、渲染 + self._init_action_space() + self._init_observation_space() + + if self.render_mode: + self._init_render() @classmethod - def _initialise(cls, config, simulator_folder, run_parameters): - """ - Initializes MuJoCo model/data and all simulator components. - - Args: - config: Simulator configuration dict - simulator_folder: Path to simulator folder - run_parameters: Runtime parameters - - Returns: - tuple: (model, data, task, bm_model, perception, callbacks) - """ - # Load core components - task_cls = cls.get_class("tasks", config["simulation"]["task"]["cls"]) - task_kwargs = config["simulation"]["task"].get("kwargs", {}) - - bm_cls = cls.get_class("bm_models", config["simulation"]["bm_model"]["cls"]) - bm_kwargs = config["simulation"]["bm_model"].get("kwargs", {}) - - # Initialize perception modules - perception_modules = {} - for module_cfg in config["simulation"].get("perception_modules", []): - module_cls = cls.get_class("perception", module_cfg["cls"]) - module_kwargs = module_cfg.get("kwargs", {}) - perception_modules[module_cls] = module_kwargs - - # Load MuJoCo model (try binary first, fall back to XML) - simulation_file = os.path.join(simulator_folder, config["package_name"], "simulation") - try: - model = mujoco.MjModel.from_binary_path(f"{simulation_file}.mjcf") - except (FileNotFoundError, mujoco.MujocoException): - model = mujoco.MjModel.from_xml_path(f"{simulation_file}.xml") - - # Initialize MuJoCo data + def get(cls, simulator_folder: str, **kwargs): + return cls(simulator_folder, **kwargs) + + def _load_config(self) -> Dict[str, Any]: + config_path = os.path.join(self.simulator_folder, "config.yaml") + if not os.path.exists(config_path): + default_config = { + "simulation": { + "max_steps": 1000, + "model_path": "arm_model.mjcf", + "control_frequency": 20, + "target_joint_pos": [0.0] + } + } + with open(config_path, "w", encoding="utf-8") as f: + yaml.dump(default_config, f) + with open(config_path, "r", encoding="utf-8") as f: + return yaml.safe_load(f) + + def _load_model(self): + model_path = os.path.join(self.simulator_folder, self.config["simulation"].get("model_path", "arm_model.mjcf")) + if not os.path.exists(model_path): + with open(model_path, "w", encoding="utf-8") as f: + f.write(""" + """) + model = mujoco.MjModel.from_xml_path(model_path) data = mujoco.MjData(model) + return model, data - # Calculate frame skip and dt - run_parameters["frame_skip"] = int(1 / (model.opt.timestep * run_parameters["action_sample_freq"])) - run_parameters["dt"] = model.opt.timestep * run_parameters["frame_skip"] - - # Initialize rendering context - max_res = run_parameters.get("max_resolution", [1280, 960]) - run_parameters["rendering_context"] = Context(model, max_resolution=max_res) - - # Initialize callbacks - callbacks = {} - for cb in run_parameters.get("callbacks", []): - cb_cls = cls.get_class(cb["cls"]) - callbacks[cb["name"]] = cb_cls(cb["name"], **cb["kwargs"]) - - # Merge parameters for component initialization - common_kwargs = {** run_parameters, **callbacks} - - # Initialize core components - task = task_cls(model, data,**{** task_kwargs, **common_kwargs}) - bm_model = bm_cls(model, data, **{** bm_kwargs, **common_kwargs}) - perception = Perception( - model, data, bm_model, perception_modules, common_kwargs - ) - - return model, data, task, bm_model, perception, callbacks - - @classmethod - def get(cls, simulator_folder, render_mode="rgb_array", - render_mode_perception="embed", render_show_depths=False, - run_parameters=None, use_cloned=True): - """ - Loads a pre-built simulator from folder. - - Args: - simulator_folder: Path to simulator folder - render_mode: Render mode (rgb_array, rgb_array_list, human) - render_mode_perception: How to render perception modules (embed/separate/None) - render_show_depths: Whether to show depth images - run_parameters: Runtime parameters to override - use_cloned: Whether to use cloned files or original source - - Returns: - Simulator: Initialized simulator instance - - Raises: - FileNotFoundError: If config file doesn't exist - RuntimeError: If simulator not built or version mismatch - """ - # Load config - config_path = os.path.join(simulator_folder, "config.yaml") - try: - config = parse_yaml(config_path) - except Exception as e: - raise FileNotFoundError(f"Failed to load config: {e}") - - # Check if simulator is built - if "built" not in config: - raise RuntimeError("Simulator has not been built (missing 'built' timestamp in config)") - - # Add simulator folder to path - if simulator_folder not in sys.path: - sys.path.insert(0, simulator_folder) - - # Load simulator class - try: - gen_cls_cloned = getattr(importlib.import_module(config["package_name"]), "Simulator") - except ImportError as e: - raise RuntimeError(f"Failed to import simulator package: {e}") - - # Handle version checking - _legacy_mode = False - gen_cls_cloned_version = "0.0.0" + def _validate_config(self): + if "simulation" not in self.config: + self.config["simulation"] = {} - if hasattr(gen_cls_cloned, "version"): - gen_cls_cloned_version = gen_cls_cloned.version - else: - _legacy_mode = True - gen_cls_cloned_version = gen_cls_cloned.id.split("-v")[-1] - - # Select class to use (cloned vs original) - if use_cloned: - gen_cls = gen_cls_cloned + # 如果有模型,使用模型信息,否则使用默认值 + if self.model is not None: + nq = self.model.nq else: - gen_cls = cls - # Version compatibility check - cloned_ver = gen_cls_cloned_version.split(".") - current_ver = gen_cls.version.split(".") + nq = 1 # 默认值 - if cloned_ver[0] < current_ver[0]: - raise RuntimeError( - f"Major version mismatch: Simulator v{gen_cls_cloned_version}, " - f"UITB v{gen_cls.version}. Use use_cloned=True or rebuild simulator." - ) - elif cloned_ver[1] < current_ver[1]: - print(f"Warning: Minor version mismatch - Simulator v{gen_cls_cloned_version}, " - f"UITB v{gen_cls.version}") - - # Initialize simulator - try: - if _legacy_mode: - simulator = gen_cls(simulator_folder, run_parameters=run_parameters) - else: - simulator = gen_cls( - simulator_folder, - render_mode=render_mode, - render_mode_perception=render_mode_perception, - render_show_depths=render_show_depths, - run_parameters=run_parameters - ) - except TypeError: - # Fallback for older versions without perception render mode - simulator = gen_cls( - simulator_folder, - render_mode=render_mode, - render_show_depths=render_show_depths, - run_parameters=run_parameters - ) - - return simulator - - def __init__(self, simulator_folder, render_mode="rgb_array", - render_mode_perception="embed", render_show_depths=False, - run_parameters=None): - """ - Initializes a Simulator instance. - - Args: - simulator_folder: Path to simulator folder - render_mode: Render mode (rgb_array, rgb_array_list, human) - render_mode_perception: How to render perception modules (embed/separate/None) - render_show_depths: Whether to show depth images - run_parameters: Runtime parameters to override - - Raises: - FileNotFoundError: If simulator folder doesn't exist - """ - super().__init__() - - # Validate simulator folder - if not os.path.exists(simulator_folder): - raise FileNotFoundError(f"Simulator folder {simulator_folder} does not exist") - self._simulator_folder = simulator_folder - - # Load config - self._config = parse_yaml(os.path.join(self._simulator_folder, "config.yaml")) - - # Merge run parameters (config defaults + runtime overrides) - self._run_parameters = self._config["simulation"]["run_parameters"].copy() - self._run_parameters.update(run_parameters or {}) - - # Initialize core simulation components - init_results = self._initialise( - self._config, self._simulator_folder, self._run_parameters + self.config["simulation"].setdefault("target_joint_pos", [0.0] * nq) + self.config["simulation"].setdefault("max_steps", 1000) + self.config["simulation"].setdefault("control_frequency", 20) + + def _init_action_space(self): + """初始化动作空间""" + # 确保模型已加载 + if self.model is None: + raise ValueError("模型未加载,无法初始化动作空间") + + n_actuators = self.model.nu + self.action_space = spaces.Box( + low=-1.0, + high=1.0, + shape=(n_actuators,), + dtype=np.float32 ) - self._model, self._data, self.task, self.bm_model, self.perception, self.callbacks = init_results - - # Initialize action/observation spaces - self.action_space = self._initialise_action_space() - self.observation_space = self._initialise_observation_space() - - # Episode statistics - self._episode_statistics = { - "length (seconds)": 0, - "length (steps)": 0, - "reward": 0 - } - - # Rendering setup - self._render_mode = render_mode - self._render_mode_perception = render_mode_perception - self._render_show_depths = render_show_depths - self._render_stack = [] - self._render_stack_perception = defaultdict(list) - self._render_stack_pop = True - self._render_stack_clean_at_reset = True - self._render_screen_size = None - self._render_window = None - self._render_clock = None - - # Initialize GUI camera - self._GUI_camera = Camera( - self._run_parameters["rendering_context"], - self._model, - self._data, - camera_id='for_testing', - dt=self._run_parameters["dt"] - ) - - # HRL (Hierarchical RL) setup - self._setup_hrl() - - def _setup_hrl(self): - """Sets up Hierarchical RL components if configured""" - if 'llc' not in self.config: - return + print(f"动作空间初始化: 维度={n_actuators}") - # Load LLC simulator - llc_sim_name = self.config["llc"]["simulator_name"] - llc_sim_folder = os.path.join(output_path(), llc_sim_name) + def _init_observation_space(self): + """初始化观测空间""" + # 确保模型已加载 + if self.model is None: + raise ValueError("模型未加载,无法初始化观测空间") - if llc_sim_folder not in sys.path: - sys.path.insert(0, llc_sim_folder) + n_qpos = self.model.nq + n_qvel = self.model.nv - if not os.path.exists(llc_sim_folder): - raise FileNotFoundError(f"LLC simulator folder {llc_sim_folder} does not exist") - - # Load LLC policy - llc_checkpoint_dir = os.path.join(llc_sim_folder, 'checkpoints') - llc_checkpoint = self.config["llc"]["checkpoint"] - llc_checkpoint_path = os.path.join(llc_checkpoint_dir, llc_checkpoint) + obs_dim = n_qpos + n_qvel - try: - print(f"Loading LLC model: {llc_checkpoint_path}") - self.llc_model = PPO.load(llc_checkpoint_path) - except FileNotFoundError: - raise FileNotFoundError(f"LLC checkpoint {llc_checkpoint} not found in {llc_checkpoint_dir}") - except Exception as e: - raise RuntimeError(f"Failed to load LLC model: {str(e)}") - - # Update action space for HRL - self.action_space = self._initialise_HRL_action_space() - - # HRL parameters (from config with defaults) - self._max_steps = self.config["llc"].get("llc_ratio", 100) - self._dwell_threshold = self.config["llc"].get( - "dwell_threshold", int(0.5 * self._max_steps) + self.observation_space = spaces.Box( + low=-np.inf, + high=np.inf, + shape=(obs_dim,), + dtype=np.float32 ) - self._target_radius = self.config["llc"].get("target_radius", 0.05) + print(f"观测空间初始化: 维度={obs_dim} (位置={n_qpos}, 速度={n_qvel})") - # Precompute joint information (performance optimization) - joints = self.config["llc"]["joints"] - self._independent_jnt_ids = [] - self._independent_dofs = [] - - for joint in joints: - joint_id = mujoco.mj_name2id( - self._model, mujoco.mjtObj.mjOBJ_JOINT, joint - ) - jnt_type = self._model.jnt_type[joint_id] - - if jnt_type not in [mujoco.mjtJoint.mjJNT_HINGE, mujoco.mjtJoint.mjJNT_SLIDE]: - raise NotImplementedError( - f"Only hinge/slide joints are supported. Joint {joint} is " - f"{mujoco.mjtJoint(jnt_type).name}" - ) + def _init_render(self): + """初始化渲染""" + if self.render_mode == "human": + try: + pygame.init() + self.screen = pygame.display.set_mode((800, 600)) + pygame.display.set_caption("MuJoCo Arm Simulation") + print("Pygame渲染初始化成功") + except Exception as e: + print(f"渲染初始化失败: {e}") + + def reset(self, seed: Optional[int] = None) -> Tuple[np.ndarray, dict]: + """重置仿真环境""" + if seed is not None: + np.random.seed(seed) - self._independent_jnt_ids.append(joint_id) - self._independent_dofs.append(self._model.jnt_qposadr[joint_id]) - - # Precompute joint ranges (vectorized operations) - self._jnt_range = self._model.jnt_range[self._independent_jnt_ids].astype(np.float32) - self._jnt_range_diff = self._jnt_range[:, 1] - self._jnt_range[:, 0] - # Avoid division by zero - self._jnt_range_diff[self._jnt_range_diff == 0] = 1e-6 - - def _normalise_qpos(self, qpos): - """ - Normalizes joint positions to [-1, 1] range using precomputed joint ranges. - - Args: - qpos: Raw joint positions + # 重置MuJoCo数据 + mujoco.mj_resetData(self.model, self.data) - Returns: - np.ndarray: Normalized joint positions - """ - # Normalize to [0, 1] then to [-1, 1] (vectorized for performance) - norm_01 = (qpos - self._jnt_range[:, 0]) / self._jnt_range_diff - return (norm_01 - 0.5) * 2 - - def _initialise_action_space(self): - """ - Initializes default action space (all actuators [-1, 1]). - - Returns: - spaces.Box: Action space definition - """ - num_actuators = self.bm_model.nu + self.perception.nu - low = np.full(num_actuators, -1.0, dtype=np.float32) - high = np.full(num_actuators, 1.0, dtype=np.float32) - return spaces.Box(low=low, high=high) - - def _initialise_HRL_action_space(self): - """ - Initializes action space for HRL (combines BM and perception actions). + # 重置计数器 + self.step_count = 0 + self.terminated = False + self.truncated = False + self.last_reward = 0.0 - Returns: - spaces.Box: HRL action space definition - """ - # Biomechanical model action space - bm_low = np.full(self.bm_model.nu, -1.0, dtype=np.float32) - bm_high = np.full(self.bm_model.nu, 1.0, dtype=np.float32) - - # Perception module action space - perception_low = np.full(self.perception.nu, -1.0, dtype=np.float32) - perception_high = np.full(self.perception.nu, 1.0, dtype=np.float32) - - # Combine spaces - low = np.concatenate([bm_low, perception_low]) - high = np.concatenate([bm_high, perception_high]) - - return spaces.Box(low=low, high=high, dtype=np.float32) - - def _initialise_observation_space(self): - """ - Initializes observation space (perception modules + stateful info). - - Returns: - spaces.Dict: Observation space definition - """ - # Get sample observation to build space - observation = self.get_observation() - obs_dict = {} - - # Add perception modules - for module in self.perception.perception_modules: - obs_dict[module.modality] = spaces.Box( - dtype=np.float32,** module.get_observation_space_params() - ) - - # Add stateful information (ensure non-zero shape) - if "stateful_information" in observation: - state_params = self.task.get_stateful_information_space_params() - # Ensure minimum shape of (1,) to avoid SB3 errors - if state_params["shape"] == (0,): - state_params["shape"] = (1,) - obs_dict["stateful_information"] = spaces.Box( - dtype=np.float32, **state_params - ) - - return spaces.Dict(obs_dict) - - def _get_qpos(self): - """ - Gets normalized joint positions for independent joints. - - Returns: - np.ndarray: Normalized joint positions - """ - qpos = self._data.qpos[self._independent_dofs].copy() - return self._normalise_qpos(qpos) - - def step(self, action): - """ - Advances simulation by one step (supports HRL and normal RL). + # 获取初始观测 + obs = self._get_obs() - Args: - action: Action vector from policy + # 渲染初始状态 + if self.render_mode == "human": + self.render() - Returns: - tuple: (observation, reward, terminated, truncated, info) - """ - # HRL mode (with LLC) - if 'llc' in self.config: - self.task._target_qpos = action - self._steps = 0 - total_reward = 0 + return obs, {} - # LLC control loop - while self._steps < self._max_steps: - # Get LLC observation and predict action - llc_obs = self.get_llcobservation(action) - llc_action, _ = self.llc_model.predict(llc_obs, deterministic=True) - - # Apply controls - self.bm_model.set_ctrl(self._model, self._data, llc_action) - self.perception.set_ctrl( - self._model, self._data, action[self.bm_model.nu:] - ) - - # Step simulation - mujoco.mj_step( - self._model, self._data, nstep=self._run_parameters["frame_skip"] - ) - - # Update components - self.bm_model.update(self._model, self._data) - self.perception.update(self._model, self._data) - - # Calculate reward and check termination - reward, terminated, truncated, info = self.task.update( - self._model, self._data - ) - reward -= self.bm_model.get_effort_cost(self._model, self._data) - total_reward += reward - - # Get observation - obs = self.get_observation(info) - - # Render if needed - if self._render_mode == "rgb_array_list": - self._render_stack.append(self._GUI_rendering()) - elif self._render_mode == "human": - self._GUI_rendering_pygame() - - # Check termination conditions - if terminated or truncated: - break - - # Task-specific termination checks - if "target_spawned" in info or "new_button_generated" in info: - if info.get("target_hit", False): - break - - # Check joint position error threshold - qpos = self._get_qpos() - dist = np.abs(action - qpos) - if np.all(dist < self._target_radius): - break - - self._steps += 1 - - # Use total reward from LLC loop - reward = total_reward - - # Normal RL mode - else: - # Apply controls - self.bm_model.set_ctrl(self._model, self._data, action[:self.bm_model.nu]) - self.perception.set_ctrl( - self._model, self._data, action[self.bm_model.nu:] - ) - - # Step simulation - mujoco.mj_step( - self._model, self._data, nstep=self._run_parameters["frame_skip"] - ) - - # Update components - self.bm_model.update(self._model, self._data) - self.perception.update(self._model, self._data) - - # Calculate reward and termination - reward, terminated, truncated, info = self.task.update( - self._model, self._data - ) - - # Add effort cost - effort_cost = self.bm_model.get_effort_cost(self._model, self._data) - info["EffortCost"] = effort_cost - reward -= effort_cost - - # Get observation - obs = self.get_observation(info) - - # Render if needed - if self._render_mode == "rgb_array_list": - self._render_stack.append(self._GUI_rendering()) - elif self._render_mode == "human": - self._GUI_rendering_pygame() - - # Update episode statistics - self._episode_statistics["length (seconds)"] += self._run_parameters["dt"] - self._episode_statistics["length (steps)"] += 1 - self._episode_statistics["reward"] += reward - - return obs, reward, terminated, truncated, info - - def get_observation(self, info=None): - """ - Gets observation from perception modules and task state. + def step(self, action: np.ndarray) -> Tuple[np.ndarray, float, bool, bool, dict]: + """执行一步仿真""" + # 将动作应用到执行器 + self.data.ctrl[:] = action * 10.0 - Args: - info: Additional info from task update + # 执行一步仿真 + mujoco.mj_step(self.model, self.data) - Returns: - dict: Observation dictionary - """ - # Get perception observations - observation = self.perception.get_observation(self._model, self._data, info) - - # Add stateful information (ensure non-empty) - stateful_info = self.task.get_stateful_information(self._model, self._data) - if stateful_info.size > 0: - observation["stateful_information"] = stateful_info - elif "stateful_information" not in observation: - # Add dummy state to avoid SB3 errors - observation["stateful_information"] = np.array([0.0], dtype=np.float32) - - return observation - - def get_llcobservation(self, action): - """ - Gets observation for LLC (no vision, joint position error). + # 更新计数器 + self.step_count += 1 - Args: - action: Target joint position from HRL + # 计算奖励 + reward = self._compute_reward() + self.last_reward = reward - Returns: - dict: LLC observation dictionary - """ - # Get base observation and remove vision - observation = self.perception.get_observation(self._model, self._data) - observation.pop("vision", None) - - # Calculate joint position error - qpos = self._get_qpos() - qpos_diff = action - qpos - - # Add stateful information (joint error) - observation["stateful_information"] = qpos_diff.astype(np.float32) - - return observation - - def reset(self, seed=None, options=None): - """ - Resets simulator to initial state. + # 检查是否终止 + self.terminated = self._check_terminated() - Args: - seed: Random seed - options: Reset options + # 检查是否截断(达到最大步数) + max_steps = self.config["simulation"].get("max_steps", 1000) + self.truncated = self.step_count >= max_steps - Returns: - tuple: (observation, info) - """ - super().reset(seed=seed) - - # Reset MuJoCo data - mujoco.mj_resetData(self._model, self._data) - - # Reset components - self.bm_model.reset(self._model, self._data) - self.perception.reset(self._model, self._data) - info = self.task.reset(self._model, self._data) - - # Forward pass to update state - mujoco.mj_forward(self._model, self._data) - - # Reset episode statistics - self._episode_statistics = { - "length (seconds)": 0, - "length (steps)": 0, - "reward": 0 - } - - # Render initial frame - if self._render_mode == "rgb_array_list": - if self._render_stack_clean_at_reset: - self._render_stack = [] - self._render_stack_perception = defaultdict(list) - self._render_stack.append(self._GUI_rendering()) - elif self._render_mode == "human": - self._GUI_rendering_pygame() - - return self.get_observation(), info - - def render(self): - """ - Renders simulation frame based on render mode. + # 获取观测 + obs = self._get_obs() - Returns: - np.ndarray/list/None: Rendered frame(s) or None - """ - if self._render_mode == "rgb_array_list": - render_stack = self._render_stack.copy() - if self._render_stack_pop: - self._render_stack = [] - return render_stack - elif self._render_mode == "rgb_array": - return self._GUI_rendering() - return None - - def get_render_stack_perception(self): - """ - Gets perception render stack (for separate perception rendering). + # 渲染当前状态 + if self.render_mode == "human": + self.render() - Returns: - defaultdict: Perception render stack - """ - return copy.deepcopy(self._render_stack_perception) + return obs, reward, self.terminated, self.truncated, {} - def _GUI_rendering(self): - """ - Renders GUI frame with optional perception module overlays. + def _get_obs(self) -> np.ndarray: + """获取当前观测""" + qpos = self.data.qpos.copy() + qvel = self.data.qvel.copy() - Returns: - np.ndarray: RGB image array - """ - # Render main camera - img, _ = self._GUI_camera.render() - - # Embed perception images into main frame - if self._render_mode_perception == "embed": - perception_images = self.perception.get_renders() - - if len(perception_images) > 0: - img_h, img_w = img.shape[:2] - desired_h = np.round(img_h / len(perception_images)).astype(int) - max_w = np.round(0.2 * img_w).astype(int) - - # Resize and embed perception images - resampled_imgs = [] - for perc_img in perception_images: - # Convert depth maps to RGB heatmaps if enabled - if perc_img.ndim == 2: - if self._render_show_depths: - # Create heatmap from depth - cmap = matplotlib.cm.jet - norm = matplotlib.colors.Normalize( - vmin=perc_img.min(), vmax=perc_img.max() - ) - perc_img = cmap(norm(perc_img))[:, :, :3] * 255 - perc_img = perc_img.astype(np.uint8) - else: - continue - - # Calculate resize factor - h, w = perc_img.shape[:2] - scale = min(desired_h / h, max_w / w) - new_h = np.round(h * scale).astype(int) - new_w = np.round(w * scale).astype(int) - - # Resample image (vectorized for performance) - resampled = np.zeros((new_h, new_w, perc_img.shape[2]), dtype=np.uint8) - for c in range(perc_img.shape[2]): - resampled[:, :, c] = scipy.ndimage.zoom(perc_img[:, :, c], scale, order=0) - - resampled_imgs.append(resampled) + obs = np.concatenate([qpos, qvel]) + return obs.astype(np.float32) - # Overlay resampled images (bottom-right to top-right) - y_pos = img_h - for res_img in resampled_imgs: - h, w = res_img.shape[:2] - y_start = max(0, y_pos - h) - x_start = max(0, img_w - w) - - # Ensure we don't go out of bounds - img[y_start:y_start+h, x_start:x_start+w] = res_img - y_pos = y_start - - # Store separate perception renders - elif self._render_mode_perception == "separate": - for module, cameras in self.perception.cameras_dict.items(): - for camera in cameras: - for img_arr in camera.render(): - if img_arr is not None: - key = f"{module.modality}/{type(camera).__name__}" - self._render_stack_perception[key].append(img_arr) - - return img - - def _GUI_rendering_pygame(self): - """Renders frame to Pygame window (human render mode)""" - # Get rendered image and transpose for Pygame - rgb_array = np.transpose(self._GUI_rendering(), (1, 0, 2)) + def _compute_reward(self) -> float: + """计算奖励函数""" + target_pos = np.array(self.config["simulation"]["target_joint_pos"]) + current_pos = self.data.qpos.copy() - # Initialize Pygame window if needed - if self._render_screen_size is None: - self._render_screen_size = rgb_array.shape[:2] + # 确保数组维度匹配 + if len(target_pos) != len(current_pos): + target_pos = np.zeros_like(current_pos) - if self._render_window is None: - pygame.init() - pygame.display.init() - self._render_window = pygame.display.set_mode(self._render_screen_size) + pos_error = np.sum((current_pos - target_pos) ** 2) + reward = -pos_error - if self._render_clock is None: - self._render_clock = pygame.time.Clock() - - # Validate image size - if self._render_screen_size != rgb_array.shape[:2]: - raise ValueError( - f"Render size mismatch: Expected {self._render_screen_size}, " - f"got {rgb_array.shape[:2]}" - ) - - # Update Pygame window - surf = pygame.surfarray.make_surface(rgb_array) - self._render_window.blit(surf, (0, 0)) - pygame.event.pump() - self._render_clock.tick(self.fps) - pygame.display.flip() + return float(reward) - def get_state(self): - """ - Gets full simulator state (for logging/evaluation). + def _check_terminated(self) -> bool: + """检查是否达到终止条件""" + joint_pos = self.data.qpos.copy() - Returns: - dict: Simulator state including MuJoCo data and component states - """ - # Base MuJoCo state - state = { - "timestep": self._data.time, - "qpos": self._data.qpos.copy(), - "qvel": self._data.qvel.copy(), - "qacc": self._data.qacc.copy(), - "act_force": self._data.actuator_force.copy(), - "act": self._data.act.copy(), - "ctrl": self._data.ctrl.copy() - } - - # Add component-specific state - state.update(self.task.get_state(self._model, self._data)) - state.update(self.bm_model.get_state(self._model, self._data)) - state.update(self.perception.get_state(self._model, self._data)) - - return state - - def close(self, **kwargs): - """Cleans up simulator resources""" - # Clean up components - self.task.close(** kwargs) - self.perception.close(**kwargs) - self.bm_model.close(** kwargs) - - # Clean up Pygame - if self._render_window is not None: - pygame.display.quit() - pygame.quit() - self._render_window = None - self._render_clock = None - - # Callback methods - def callback(self, callback_name, num_timesteps): - """ Update a specific callback """ - self.callbacks[callback_name].update(num_timesteps) - - def update_callbacks(self, num_timesteps): - """ Update all registered callbacks """ - for callback_name in self.callbacks: - self.callback(callback_name, num_timesteps) - - # Property getters (read-only) - @property - def fps(self): - return self._GUI_camera._fps - - @property - def config(self): - return copy.deepcopy(self._config) - - @property - def run_parameters(self): - # Exclude non-serializable objects from copy - exclude = {"rendering_context"} - run_params = { - k: copy.deepcopy(v) - for k, v in self._run_parameters.items() - if k not in exclude - } - run_params["rendering_context"] = self._run_parameters["rendering_context"] - return run_params - - @property - def simulator_folder(self): - return self._simulator_folder - - @property - def render_mode(self): - return self._render_mode + if np.any(np.abs(joint_pos) > np.pi): + return True + + return False - # Ensure proper cleanup on object deletion - def __del__(self): - try: - self.close() - except Exception: - pass \ No newline at end of file + def render(self): + """渲染当前仿真状态""" + if self.render_mode == "human" and self.screen is not None: + # 处理pygame事件 + for event in pygame.event.get(): + if event.type == pygame.QUIT: + self.close() + return + elif event.type == pygame.KEYDOWN: + if event.key == pygame.K_ESCAPE: + self.close() + return + + # 清屏 + self.screen.fill((255, 255, 255)) + + # 绘制简单表示(2D投影) + joint_angle = self.data.qpos[0] if self.model.nq > 0 else 0 + + arm_length = 200 + center_x, center_y = 400, 300 + + end_x = center_x + arm_length * np.cos(joint_angle) + end_y = center_y + arm_length * np.sin(joint_angle) + + # 绘制机械臂 + pygame.draw.line(self.screen, (0, 0, 0), + (center_x, center_y), (end_x, end_y), 5) + + # 绘制关节 + pygame.draw.circle(self.screen, (255, 0, 0), + (int(center_x), int(center_y)), 10) + pygame.draw.circle(self.screen, (0, 0, 255), + (int(end_x), int(end_y)), 8) + + # 显示信息 + font = pygame.font.Font(None, 36) + step_text = font.render(f"Step: {self.step_count}", True, (0, 0, 0)) + reward_text = font.render(f"Reward: {self.last_reward:.2f}", True, (0, 0, 0)) + angle_text = font.render(f"Joint Angle: {joint_angle:.2f} rad", True, (0, 0, 0)) + + self.screen.blit(step_text, (10, 10)) + self.screen.blit(reward_text, (10, 50)) + self.screen.blit(angle_text, (10, 90)) + + # 更新显示 + pygame.display.flip() + + # 控制帧率 + control_freq = self.config["simulation"].get("control_frequency", 20) + pygame.time.delay(int(1000 / control_freq)) + + def close(self): + """关闭环境""" + if self.render_mode == "human": + pygame.quit() \ No newline at end of file diff --git a/src/box/simulators/2.py b/src/box/simulators/2.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/box/simulators/arm_simulation/arm_model.mjcf b/src/box/simulators/arm_simulation/arm_model.mjcf new file mode 100644 index 0000000000..dde587e969 --- /dev/null +++ b/src/box/simulators/arm_simulation/arm_model.mjcf @@ -0,0 +1,14 @@ + + \ No newline at end of file diff --git a/src/box/simulators/arm_simulation/arm_simulation.xml b/src/box/simulators/arm_simulation/arm_simulation.xml new file mode 100644 index 0000000000..dde587e969 --- /dev/null +++ b/src/box/simulators/arm_simulation/arm_simulation.xml @@ -0,0 +1,14 @@ + + \ No newline at end of file diff --git a/src/box/simulators/arm_simulation/config.yaml b/src/box/simulators/arm_simulation/config.yaml new file mode 100644 index 0000000000..49138a68d1 --- /dev/null +++ b/src/box/simulators/arm_simulation/config.yaml @@ -0,0 +1,4 @@ +# 最小化配置示例(具体字段需根据 simulator.py 中的要求调整) +simulation: + model_path: "arm_model.mjcf" # 模型文件名(若有) + render: true \ No newline at end of file diff --git a/src/box/test.simulator.py b/src/box/test.simulator.py index 06f94e93fd..233864623b 100644 --- a/src/box/test.simulator.py +++ b/src/box/test.simulator.py @@ -1,25 +1,93 @@ -from uitb.simulator import Simulator - -# 加载仿真器(需替换为实际仿真器路径,如 "uitb/simulators/arm_simulation") -simulator_folder = "uitb/simulators/your_sim_folder" -env = Simulator.get( - simulator_folder=simulator_folder, - render_mode="human", # "human" 弹出Pygame窗口;"rgb_array" 输出图片数组 - render_mode_perception="embed" # 感知图像嵌入主窗口 -) - -# 重置环境 -obs, info = env.reset(seed=42) -print("初始观测值:", {k: v.shape for k in obs}) - -# 运行 1000 步仿真(随机动作示例) -for step in range(1000): - action = env.action_space.sample() - obs, reward, terminated, truncated, info = env.step(action) - if step % 100 == 0: - print(f"第 {step} 步 | 奖励:{reward:.2f} | 终止状态:{terminated}") - if terminated or truncated: - obs, info = env.reset() - -# 关闭仿真(释放资源) -env.close() +import sys +import os +import importlib.util +import numpy as np + +# 1. 手动指定 simulator.py 的绝对路径 +simulator_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "simulator.py")) + +# 2. 检查文件是否存在 +if not os.path.exists(simulator_path): + raise FileNotFoundError(f"simulator.py 不存在:{simulator_path}") + +# 3. 动态加载 simulator.py 模块 +spec = importlib.util.spec_from_file_location("simulator", simulator_path) +simulator_module = importlib.util.module_from_spec(spec) +sys.modules["simulator"] = simulator_module +spec.loader.exec_module(simulator_module) + +# 4. 从加载的模块中导入 Simulator 类 +Simulator = simulator_module.Simulator + +# 配置仿真器路径(根据你的目录结构) +current_dir = os.path.dirname(os.path.abspath(__file__)) +simulator_folder = os.path.join(current_dir, "simulators", "arm_simulation") + +# 检查仿真器目录是否存在,如果不存在则创建 +if not os.path.exists(simulator_folder): + print(f"创建仿真器目录:{simulator_folder}") + os.makedirs(simulator_folder, exist_ok=True) + +print("=" * 50) +print("机械臂仿真环境") +print("=" * 50) +print(f"仿真器目录: {simulator_folder}") +print(f"当前工作目录: {os.getcwd()}") + +try: + # 创建仿真环境 + env = Simulator.get( + simulator_folder=simulator_folder, + render_mode="human" + ) + + print("仿真环境创建成功!") + print(f"模型关节数(nq): {env.model.nq}") + print(f"模型执行器数(nu): {env.model.nu}") + print(f"模型速度数(nv): {env.model.nv}") + print("=" * 50) + + # 重置环境 + obs, info = env.reset(seed=42) + print(f"初始观测值: {obs}") + print(f"观测值形状: {obs.shape}") + print(f"动作空间形状: {env.action_space.shape}") + print("=" * 50) + + print("\n开始仿真...") + print("按ESC键或关闭窗口可结束仿真") + print("-" * 50) + + # 运行仿真 + for step in range(1000): + # 随机采样动作 + action = env.action_space.sample() + + # 执行一步 + obs, reward, terminated, truncated, info = env.step(action) + + # 每50步打印一次信息 + if step % 50 == 0: + print(f"Step {step:4d} | 奖励: {reward:7.3f} | 终止: {terminated} | 截断: {truncated}") + + # 如果环境终止或截断,则重置 + if terminated or truncated: + print(f"\n环境终止/截断 (Step {step}),重置仿真...") + obs, info = env.reset() + + print("\n仿真完成!") + +except Exception as e: + print(f"\n发生错误: {type(e).__name__}: {e}") + import traceback + traceback.print_exc() + +finally: + # 关闭环境 + print("\n关闭仿真环境...") + try: + if 'env' in locals(): + env.close() + except: + pass + print("仿真结束") \ No newline at end of file From e57a454745ba198a965248e29d442a02f4c48a16 Mon Sep 17 00:00:00 2001 From: Xinyue-cao <648343923@qq.com> Date: Wed, 10 Dec 2025 13:34:28 +0800 Subject: [PATCH 05/14] =?UTF-8?q?feat:=20=E5=88=A0=E9=99=A4GitHub=E4=B8=8A?= =?UTF-8?q?=E7=9A=84Python=E7=BC=93=E5=AD=98=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 10 ---------- src/box/__pycache__/simulator.cpython-313.pyc | Bin 14082 -> 0 bytes .../perception/__pycache__/base.cpython-313.pyc | Bin 5474 -> 0 bytes .../utils/__pycache__/functions.cpython-313.pyc | Bin 1905 -> 0 bytes .../utils/__pycache__/rendering.cpython-313.pyc | Bin 1563 -> 0 bytes 5 files changed, 10 deletions(-) delete mode 100644 pyproject.toml delete mode 100644 src/box/__pycache__/simulator.cpython-313.pyc delete mode 100644 src/box/perception/__pycache__/base.cpython-313.pyc delete mode 100644 src/box/utils/__pycache__/functions.cpython-313.pyc delete mode 100644 src/box/utils/__pycache__/rendering.cpython-313.pyc diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index ccec03060b..0000000000 --- a/pyproject.toml +++ /dev/null @@ -1,10 +0,0 @@ -[build-system] -requires = ["setuptools>=61.0"] -build-backend = "setuptools.build_meta" - -[project] -name = "your-package-name" -version = "0.1.0" -authors = [{"name" = "Your Name", "email" = "your@email.com"}] -description = "A sample package" -requires-python = ">=3.8" \ No newline at end of file diff --git a/src/box/__pycache__/simulator.cpython-313.pyc b/src/box/__pycache__/simulator.cpython-313.pyc deleted file mode 100644 index 22b2ddf71e59673f1a8c18ea54f2034f399c65d5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14082 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 diff --git a/src/box/perception/__pycache__/base.cpython-313.pyc b/src/box/perception/__pycache__/base.cpython-313.pyc deleted file mode 100644 index a22b27019fd7ab8ee8fb438d3155d8893a024879..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5474 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 ZZ2F$B=WNdji0}7v`#7#PNl-F1{s&J6hNA!g diff --git a/src/box/utils/__pycache__/functions.cpython-313.pyc b/src/box/utils/__pycache__/functions.cpython-313.pyc deleted file mode 100644 index 1fb8b9766a41546237ae56a23e83d1355092a3c7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1905 zcmchY?@t^>7{_OJ_ipcQp%=x}S}Gj1nx0lD1Vjks(!}^i?F&|nhD$cf-5#zUdzYEr zQ2L^&iM6z1AW;lh(;x9id)i17lbY0u{t4JiBw2l7QV!@V4fn#NuY8`pz2gKU@0{Ix zcIKIxXJ$V0ow;&jV}w8&EIennh=kn3LNy6~rrZUYIieDkzC>8%OBq##{e%gs2v$@j zu#y@88&HE_gJU!<-^TzaQhpfkBZGx;NO=j&92xPPXojkz)jZ$})D`^fE86Z+y2{ zT6*_XLwkD%ygA3tJ9a$i3OVjddEI`~mGx2XWu&)qrsc|MlUe$>;f6FVV`dFaE08h{ z?Z#f{Pg+)zvs7|4cQWbNW|k*2j+L^_oWDruD!Ahklb{j9^|KI~AecDy92{2_V+~+kC9gBX zvMa=*;6V-WgJAxRSYjb@y}Q_aw5W70OPxCrp)fo|uR&&x43nXHfZ)T_Q~8i%^st}P zn_xcBTBSJOA&jssRr`2SYlF(#^6!5&y|MUCY5Htw_Jg(g#nSATrP-;qZ|4}!B`&xP zCO6ZtLNj9;%#}@^%}p3APF)E_R*zzz$u;hnwmu4?%j>DP^)W-kM(hy?g=*qB@)`2G z5?xUa{Hz=(DlIEY_m%GJ^lI<5#MMOciSA{odnXoTU$2}R6kM_j_eP&65Q~oZ=L!(u z=50#{$uO;|fD5XGPxU0m$RW~3j*u$QG>niDVx*PC;2D#SiLE3<;{oR}2wFq8aqY9U zOS5krf92)1kI$`sf6JxpLBO#y&puzEZpg4wxwL7GorsIBl*=2ID^2R-SvSn}jG<+7 zdK&14y%Xx4UYg-J|CleV2ndTC8g^l=;C~2y9OnnR3O7!f^G#PK?uI*VcK#M^nmK*p z^t@h-#{LpX;ucO?hf~-M=j#YD&?m-- z8pLM{=2-AQUf7zn@jQDk=Pmar#+XAQIU7HMV0#Ekd-Z316;Xi?=?aIv&8W_)|ovbm)o`MS8 n1CGu96e*?mNcdNxY)S%sm2QRv+J&ZPGbGZsazv#4UYNfDz(|l- diff --git a/src/box/utils/__pycache__/rendering.cpython-313.pyc b/src/box/utils/__pycache__/rendering.cpython-313.pyc deleted file mode 100644 index 8dd6ac1b711d838ef4bd52f1db5dbb817040bc8f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1563 zcmbVM&1(}u6rbJfZra$iwYK)t66*&p*y2S65fyq7giu%uIRu7uvkfbo-8#F0dh=ox zO-0d)H}N9PrJ$$c$Dbj@@z~OkliE;v^}U%SrIKD8!Y^;%ym|9}?_(OtWP-r@uztlI ziV?DfAFZV|g~l8RYs4a!a)&VauP}9lOc6^RBUbFPDlJ(|=(G+x9dt(9PAe#8ypD&` zwmd3PX?`>&L0BVM!W4@z)lyi@QkiDO#t73bjTr^qGXrGy1pAx%KMwVW?Gs!IB6sS9+K+NSUFLhy!gFq zya5VQQN=yt#7#VfWntvGzT*e59j7!O*p#wf z@W_WK4WnG-+B6qM%v~r&PDp8mu;Z}HFbsyBApkQUBGDD{L%X`Cs`|7vLyOL(9fO^J zOABR%;xfIUnY!>>KZfW3t3V91#kvsCNvi@fa3sk30D4g9M-Z$AKuVhiJAs-(gdv2J z2;$5LN}~uJvLNokYgfoY3Och-fmLVrhg#d1g(9+C9V4_2fYxgF>0Qtq-KUvLUUMc1 z$=veViOkG-t*M4-p4X=#^m*c88O16PQ%x77{QD-(m@Q{aG#J>Y^^fa86?LURm8HBR{A(DVWfOSv_-Y03c8y#~So(F(NAD%4t$Zj7TDR);# zr^>z6fx&XTIyn4x>gDYx+G@9`h20)bIEONlZ6M1v0?&+^hF@Bg&QwHFkb?;I#3Awg2`Lu#$rJGyx;pjD?1!e@>lV4Xuu;0oDQ4gJ(! zzoJjbR>N&m=SHz~FR%ly`=uhh7OKA_r%<>!+?c%Ux}5wQ3?D_L8=3&WV~V0|k>pP@ au&-&#^dADS-xEU#U_X&iCUyycxcDyuV>1K* From 0b664d1b6c3400985ffa31424b804d001ad657ef Mon Sep 17 00:00:00 2001 From: Xinyue-cao <648343923@qq.com> Date: Wed, 10 Dec 2025 13:45:26 +0800 Subject: [PATCH 06/14] =?UTF-8?q?refactor:=20=E9=87=8D=E5=91=BD=E5=90=8D1.?= =?UTF-8?q?yaml=E4=B8=BAarm=5Fgrab=5Fsimulation=5F1.yaml=EF=BC=88=E6=9C=BA?= =?UTF-8?q?=E6=A2=B0=E8=87=82=E6=8A=93=E5=8F=96=E4=BB=BF=E7=9C=9F=EF=BC=89?= =?UTF-8?q?=EF=BC=8C=E6=B8=85=E7=90=86=E7=BC=93=E5=AD=98=E5=B9=B6=E9=85=8D?= =?UTF-8?q?=E7=BD=AE.gitignore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/box/1.yaml | 7 ------- src/box/arm_grab_simulation_1.yaml | 0 2 files changed, 7 deletions(-) delete mode 100644 src/box/1.yaml create mode 100644 src/box/arm_grab_simulation_1.yaml diff --git a/src/box/1.yaml b/src/box/1.yaml deleted file mode 100644 index 431f70d5e4..0000000000 --- a/src/box/1.yaml +++ /dev/null @@ -1,7 +0,0 @@ -llc: - simulator_name: "llc_simulator" - checkpoint: "best_model.zip" - llc_ratio: 100 - dwell_threshold: 50 - target_radius: 0.05 - joints: ["joint1", "joint2"] \ No newline at end of file diff --git a/src/box/arm_grab_simulation_1.yaml b/src/box/arm_grab_simulation_1.yaml new file mode 100644 index 0000000000..e69de29bb2 From c39a89d32bd2b631a38abec9ac3722f7241f405a Mon Sep 17 00:00:00 2001 From: Xinyue-cao <648343923@qq.com> Date: Wed, 10 Dec 2025 13:52:19 +0800 Subject: [PATCH 07/14] =?UTF-8?q?clean:=20=E5=85=A8=E9=87=8F=E5=88=A0?= =?UTF-8?q?=E9=99=A4=E6=9C=AC=E5=9C=B0+=E8=BF=9C=E7=A8=8B=E7=BC=93?= =?UTF-8?q?=E5=AD=98=E6=96=87=E4=BB=B6=EF=BC=8C=E9=85=8D=E7=BD=AE.gitignor?= =?UTF-8?q?e=E6=B0=B8=E4=B9=85=E5=B1=8F=E8=94=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | Bin 806 -> 1372 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/.gitignore b/.gitignore index 1dc2159b6b88c578fec3c4364425bd0caa923d65..d9575cf1461f8c8a571401e13b94ac019e718faf 100644 GIT binary patch delta 543 zcmZuuJxjw-6g?nF8x#jY)TvT%a7Y&i9R){C)rvG02~GO2^{d5vV4)aL(7{3TAH-Gv zgSh(xlupjwx`@Q{@*+l%OCC4(o^$TG?>&sZX;1Sb(?t}pfi65W(Z&fT!)onX42?*i z4K*x4M~7Vpzj-^8H<(KW6DwH98WxSAknCJMsO0bRv0`;m$1#qmWiva(c*@GoU1~mC za<&GGk<5&iCo&dlG|@%_d+B(Gf_1nIHN<6COYM5J+<`zTQxQaZly=B+xUA|b`=7o3 zc?nZtblcofCUmT#0xRU Date: Wed, 10 Dec 2025 13:57:53 +0800 Subject: [PATCH 08/14] =?UTF-8?q?fix:=20=E5=BC=BA=E5=88=B6=E8=A7=A3?= =?UTF-8?q?=E5=86=B3.gitignore=E5=86=B2=E7=AA=81=EF=BC=8C=E4=BF=9D?= =?UTF-8?q?=E7=95=99=E7=BC=93=E5=AD=98=E5=BF=BD=E7=95=A5=E8=A7=84=E5=88=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | Bin 1372 -> 1676 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/.gitignore b/.gitignore index d9575cf1461f8c8a571401e13b94ac019e718faf..3cce7573d4984bb1a39b0e78fd870a5f2148592a 100644 GIT binary patch delta 39 ocmcb^)x*0XhIO(7v)yD37P-lBEEY_>43q7c9bjA*UIs1(0NHT}_W%F@ delta 7 OcmeC-y~DL3h7|w{Y66h} From 708e72db128e648c54c6f1cd5ecebd0ce820bd0a Mon Sep 17 00:00:00 2001 From: Xinyue-cao <648343923@qq.com> Date: Wed, 10 Dec 2025 14:00:53 +0800 Subject: [PATCH 09/14] =?UTF-8?q?fix:=20=E5=88=A0=E9=99=A4.gitignore?= =?UTF-8?q?=E8=A7=A3=E5=86=B3=E5=86=B2=E7=AA=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | Bin 1676 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .gitignore diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 3cce7573d4984bb1a39b0e78fd870a5f2148592a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1676 zcmd5+v2GJV5S=oyph7~TGfEecGX(`L5*^@RfSZS_xAR3wk1S`PG{fD&c1p3W@hu>E7YVl^EBx`?USYZlStHBk6B z1bz>k1TyWwXERwnTTI~lSUeBaY4G2&G9_`*Q(d}6 z_vkLMQe2az{B&da?Q(8+?Hi5>&f!(5v$;9^7&4>3VY6Ly;gf<%WR&8bviTJ1!jr~R zq4AE5Hr~T^XMKGKwh(WpgS)Clmk-}fhntp9K=qz#&}>IB`uX{|I^*GK(0I7h2BsHx z@N4g>rkTx8?XTc4}IWH^Z4~i|ATt9wUmE*d3y9Y zdf&eCfTOuG{`TQ`f8#X3GKFosX3<^g`ys;aRiLrjth@W>-f%UTulT Date: Wed, 10 Dec 2025 14:09:50 +0800 Subject: [PATCH 10/14] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90YAML=E9=87=8D?= =?UTF-8?q?=E5=91=BD=E5=90=8D+=E7=BC=93=E5=AD=98=E6=B8=85=E7=90=86?= =?UTF-8?q?=EF=BC=8C=E5=88=A0=E9=99=A4.gitignore=E8=A7=84=E9=81=BF?= =?UTF-8?q?=E5=86=B2=E7=AA=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From 6b0e0b8b16070963de730567ec59be184d1743dc Mon Sep 17 00:00:00 2001 From: Xinyue-cao <648343923@qq.com> Date: Wed, 10 Dec 2025 14:09:54 +0800 Subject: [PATCH 11/14] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90YAML=E9=87=8D?= =?UTF-8?q?=E5=91=BD=E5=90=8D+=E7=BC=93=E5=AD=98=E6=B8=85=E7=90=86?= =?UTF-8?q?=EF=BC=8C=E5=88=A0=E9=99=A4.gitignore=E8=A7=84=E9=81=BF?= =?UTF-8?q?=E5=86=B2=E7=AA=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From cfad9241a2cb138077964462344d8cbc04d117cf Mon Sep 17 00:00:00 2001 From: Xinyue-cao <648343923@qq.com> Date: Wed, 10 Dec 2025 14:30:05 +0800 Subject: [PATCH 12/14] =?UTF-8?q?=E5=88=A0=E9=99=A4=E4=B8=8D=E9=9C=80?= =?UTF-8?q?=E8=A6=81=E7=9A=84=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 48 ------------------------------------------------ 1 file changed, 48 deletions(-) delete mode 100644 README.md diff --git a/README.md b/README.md deleted file mode 100644 index c198e51470..0000000000 --- a/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# 神经网络实现代理 - -利用神经网络/ROS 实现 Carla(车辆、行人的感知、规划、控制)、AirSim、Mujoco 中人和载具的代理。 - -## 环境配置 - -* 平台:Windows 10/11,Ubuntu 20.04/22.04 -* 软件:Python 3.7-3.12(需支持3.7)、Pytorch(尽量不使用Tensorflow) -* 相关软件下载 [链接](https://pan.baidu.com/s/1IFhCd8X9lI24oeYQm5-Edw?pwd=hutb) - - -## 贡献指南 - -准备提交代码之前,请阅读 [贡献指南](https://github.com/OpenHUTB/.github/blob/master/CONTRIBUTING.md) 。 -代码的优化包括:注释、[PEP 8 风格调整](https://peps.pythonlang.cn/pep-0008/) 、将神经网络应用到Carla模拟器中、撰写对应 [文档](https://openhutb.github.io/nn/) 、添加 [源代码对应的自动化测试](https://docs.github.com/zh/actions/use-cases-and-examples/building-and-testing/building-and-testing-python) 等(从Carla场景中获取神经网络所需数据或将神经网络的结果输出到场景中)。 - -### 约定 - -* 每个模块位于`src/{模块名}`目录下,`模块名`需要用2-3个单词表示,首字母不需要大写,下划线`_`分隔,不能宽泛,越具体越好 -* 每个模块的入口须为`main.`开头,比如:main.py、main.cpp、main.bat、main.sh等,提供的ROS功能以`main.launch`文件作为启动配置文件 -* 每次pull request都需要保证能够通过main脚本直接运行整个模块,在提交信息中提供运行动图或截图;Pull Request的标题不能随意,需要概括具体的修改内容;README.md文档中提供运行环境和运行步骤的说明 -* 仓库尽量保存文本文件,二进制文件需要慎重,如运行需要示例数据,可以保存少量数据,大量数据可以通过提供网盘链接并说明下载链接和运行说明 - - -### 文档生成 - -测试生成的文档: -1. 安装python 3.11,并使用以下命令安装`mkdocs`和相关依赖: -```shell -pip install mkdocs -i http://mirrors.aliyun.com/pypi/simple --trusted-host mirrors.aliyun.com -pip install -r requirements.txt -``` -(可选)安装完成后使用`mkdocs --version`查看是否安装成功。 - -2. 在命令行中进入`nn`目录下,运行: -```shell -mkdocs build -mkdocs serve -``` -然后使用浏览器打开 [http://127.0.0.1:8000](http://127.0.0.1:8000),查看文档页面能否正常显示。 - -## 参考 - -* [代理模拟器文档](https://openhutb.github.io) -* 已有相关 [无人车](https://openhutb.github.io/doc/used_by/) 、[无人机](https://openhutb.github.io/air_doc/third/used_by/) 、[具身人](https://openhutb.github.io/doc/pedestrian/humanoid/) 的实现 -* [神经网络原理](https://github.com/OpenHUTB/neuro) - - From 3f971bcd3d79e71be774f6ebb312287437f6b6d6 Mon Sep 17 00:00:00 2001 From: Xinyue-cao <648343923@qq.com> Date: Sat, 13 Dec 2025 20:40:19 +0800 Subject: [PATCH 13/14] =?UTF-8?q?feat:=E8=A1=A5=E5=85=85=E6=84=9F=E7=9F=A5?= =?UTF-8?q?=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000000..c198e51470 --- /dev/null +++ b/README.md @@ -0,0 +1,48 @@ +# 神经网络实现代理 + +利用神经网络/ROS 实现 Carla(车辆、行人的感知、规划、控制)、AirSim、Mujoco 中人和载具的代理。 + +## 环境配置 + +* 平台:Windows 10/11,Ubuntu 20.04/22.04 +* 软件:Python 3.7-3.12(需支持3.7)、Pytorch(尽量不使用Tensorflow) +* 相关软件下载 [链接](https://pan.baidu.com/s/1IFhCd8X9lI24oeYQm5-Edw?pwd=hutb) + + +## 贡献指南 + +准备提交代码之前,请阅读 [贡献指南](https://github.com/OpenHUTB/.github/blob/master/CONTRIBUTING.md) 。 +代码的优化包括:注释、[PEP 8 风格调整](https://peps.pythonlang.cn/pep-0008/) 、将神经网络应用到Carla模拟器中、撰写对应 [文档](https://openhutb.github.io/nn/) 、添加 [源代码对应的自动化测试](https://docs.github.com/zh/actions/use-cases-and-examples/building-and-testing/building-and-testing-python) 等(从Carla场景中获取神经网络所需数据或将神经网络的结果输出到场景中)。 + +### 约定 + +* 每个模块位于`src/{模块名}`目录下,`模块名`需要用2-3个单词表示,首字母不需要大写,下划线`_`分隔,不能宽泛,越具体越好 +* 每个模块的入口须为`main.`开头,比如:main.py、main.cpp、main.bat、main.sh等,提供的ROS功能以`main.launch`文件作为启动配置文件 +* 每次pull request都需要保证能够通过main脚本直接运行整个模块,在提交信息中提供运行动图或截图;Pull Request的标题不能随意,需要概括具体的修改内容;README.md文档中提供运行环境和运行步骤的说明 +* 仓库尽量保存文本文件,二进制文件需要慎重,如运行需要示例数据,可以保存少量数据,大量数据可以通过提供网盘链接并说明下载链接和运行说明 + + +### 文档生成 + +测试生成的文档: +1. 安装python 3.11,并使用以下命令安装`mkdocs`和相关依赖: +```shell +pip install mkdocs -i http://mirrors.aliyun.com/pypi/simple --trusted-host mirrors.aliyun.com +pip install -r requirements.txt +``` +(可选)安装完成后使用`mkdocs --version`查看是否安装成功。 + +2. 在命令行中进入`nn`目录下,运行: +```shell +mkdocs build +mkdocs serve +``` +然后使用浏览器打开 [http://127.0.0.1:8000](http://127.0.0.1:8000),查看文档页面能否正常显示。 + +## 参考 + +* [代理模拟器文档](https://openhutb.github.io) +* 已有相关 [无人车](https://openhutb.github.io/doc/used_by/) 、[无人机](https://openhutb.github.io/air_doc/third/used_by/) 、[具身人](https://openhutb.github.io/doc/pedestrian/humanoid/) 的实现 +* [神经网络原理](https://github.com/OpenHUTB/neuro) + + From 26ecceae3660d28b6b9af983903436ed85179a91 Mon Sep 17 00:00:00 2001 From: Xinyue-cao <648343923@qq.com> Date: Mon, 15 Dec 2025 11:40:21 +0800 Subject: [PATCH 14/14] =?UTF-8?q?=E6=B7=BB=E5=8A=A0skeletion=5Fvideo.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/box/skeleton_video.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/box/skeleton_video.py diff --git a/src/box/skeleton_video.py b/src/box/skeleton_video.py new file mode 100644 index 0000000000..e69de29bb2