From c0059de6ad8768e20c9b847b29f88804560b9d7b Mon Sep 17 00:00:00 2001 From: Alexis DUBURCQ Date: Thu, 19 Nov 2020 19:06:11 +0100 Subject: [PATCH] [gym] Make sure environments are fully configured at init. (#229) * [core] Catch some seldom exceptions. * [core] Enable to call 'initialize' multiple times for 'Engine*'. * [core] 'isSimulationRunning' is set to True at the end of 'start' instead of the beginning. * [core|python] Several critical bug fixes. * [gym] Make sure the environment is completely initialized at '__init__', which is expected by OpenAI Gym API. * [gym] Fix some internal buffersnot properly reset. * [gym] Fix base env '_setup' method not called at reset. * [gym] Send zero motor efforts at reset since the initial action is undefined. Co-authored-by: Alexis Duburcq --- CMakeLists.txt | 2 +- core/src/control/AbstractController.cc | 5 +- core/src/engine/Engine.cc | 13 + core/src/engine/EngineMultiRobot.cc | 70 ++++- .../common/gym_jiminy/common/block_bases.py | 10 +- .../common/gym_jiminy/common/control_impl.py | 3 - .../common/gym_jiminy/common/env_bases.py | 257 +++++++---------- .../gym_jiminy/common/env_locomotion.py | 46 ++-- .../common/gym_jiminy/common/generic_bases.py | 12 +- .../gym_jiminy/common/pipeline_bases.py | 260 ++++++++++-------- .../common/gym_jiminy/common/utils.py | 24 +- .../common/gym_jiminy/common/wrappers.py | 104 +++---- .../gym_jiminy/envs/gym_jiminy/envs/anymal.py | 8 +- .../unit_py/atlas_standing_meshcat.png | Bin 29087 -> 29075 bytes .../unit_py/test_pipeline_control.py | 19 +- python/jiminy_py/src/jiminy_py/controller.py | 14 +- python/jiminy_py/src/jiminy_py/dynamics.py | 2 +- 17 files changed, 452 insertions(+), 397 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 91fbe9530..b1d5b9449 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.10) # Set the build version -set(BUILD_VERSION 1.4.11) +set(BUILD_VERSION 1.4.12) # Add definition of Jiminy version for C++ headers add_definitions("-DJIMINY_VERSION=\"${BUILD_VERSION}\"") diff --git a/core/src/control/AbstractController.cc b/core/src/control/AbstractController.cc index 3fc41fefb..49f14434d 100644 --- a/core/src/control/AbstractController.cc +++ b/core/src/control/AbstractController.cc @@ -33,8 +33,11 @@ namespace jiminy return hresult_t::ERROR_INIT_FAILED; } + // Backup robot robot_ = robot; - sensorsData_ = robot_->getSensorsData(); + + // Reset the controller completely + reset(true); try { diff --git a/core/src/engine/Engine.cc b/core/src/engine/Engine.cc index 0ea03d285..e315331fa 100644 --- a/core/src/engine/Engine.cc +++ b/core/src/engine/Engine.cc @@ -23,6 +23,19 @@ namespace jiminy { hresult_t returnCode = hresult_t::SUCCESS; + // Make sure the simulation is properly stopped + if (isSimulationRunning_) + { + stop(); + } + + // Remove the existing system if already initialized + if(isInitialized_) + { + removeSystem(""); // It cannot fail at this point + isInitialized_ = false; + } + /* Add the system without associated name, since it is irrelevant for a single robot engine. */ returnCode = addSystem("", std::move(robot), diff --git a/core/src/engine/EngineMultiRobot.cc b/core/src/engine/EngineMultiRobot.cc index ee4f1a9b1..cb62d32b0 100644 --- a/core/src/engine/EngineMultiRobot.cc +++ b/core/src/engine/EngineMultiRobot.cc @@ -67,6 +67,13 @@ namespace jiminy std::shared_ptr controller, callbackFunctor_t callbackFct) { + // Make sure that no simulation is running + if (isSimulationRunning_) + { + PRINT_ERROR("A simulation is already running. Stop it before adding a new system.") + return hresult_t::ERROR_GENERIC; + } + if (!robot->getIsInitialized()) { PRINT_ERROR("Robot not initialized.") @@ -119,8 +126,18 @@ namespace jiminy { hresult_t returnCode = hresult_t::SUCCESS; - // Remove every coupling forces involving the system - returnCode = removeCouplingForces(systemName); + // Make sure that no simulation is running + if (isSimulationRunning_) + { + PRINT_ERROR("A simulation is already running. Stop it before removing a system.") + returnCode = hresult_t::ERROR_GENERIC; + } + + if (returnCode == hresult_t::SUCCESS) + { + // Remove every coupling forces involving the system + returnCode = removeCouplingForces(systemName); + } if (returnCode == hresult_t::SUCCESS) { @@ -195,8 +212,18 @@ namespace jiminy { hresult_t returnCode = hresult_t::SUCCESS; + // Make sure that no simulation is running + if (isSimulationRunning_) + { + PRINT_ERROR("A simulation is already running. Stop it before adding coupling forces.") + returnCode = hresult_t::ERROR_GENERIC; + } + int32_t systemIdx1; - returnCode = getSystemIdx(systemName1, systemIdx1); + if (returnCode == hresult_t::SUCCESS) + { + returnCode = getSystemIdx(systemName1, systemIdx1); + } int32_t systemIdx2; if (returnCode == hresult_t::SUCCESS) @@ -239,8 +266,18 @@ namespace jiminy { hresult_t returnCode = hresult_t::SUCCESS; + // Make sure that no simulation is running + if (isSimulationRunning_) + { + PRINT_ERROR("A simulation is already running. Stop it before removing coupling forces.") + returnCode = hresult_t::ERROR_GENERIC; + } + systemHolder_t * system1; - returnCode = getSystem(systemName1, system1); + if (returnCode == hresult_t::SUCCESS) + { + returnCode = getSystem(systemName1, system1); + } if (returnCode == hresult_t::SUCCESS) { @@ -268,8 +305,18 @@ namespace jiminy { hresult_t returnCode = hresult_t::SUCCESS; + // Make sure that no simulation is running + if (isSimulationRunning_) + { + PRINT_ERROR("A simulation is already running. Stop it before removing coupling forces.") + returnCode = hresult_t::ERROR_GENERIC; + } + systemHolder_t * system; - returnCode = getSystem(systemName, system); + if (returnCode == hresult_t::SUCCESS) + { + returnCode = getSystem(systemName, system); + } if (returnCode == hresult_t::SUCCESS) { @@ -580,9 +627,6 @@ namespace jiminy // Reset the robot, controller, engine, and registered impulse forces if requested reset(resetRandomNumbers, resetDynamicForceRegister); - // At this point, consider that the simulation is running - isSimulationRunning_ = true; - auto systemIt = systems_.begin(); auto systemDataIt = systemsDataHolder_.begin(); for ( ; systemIt != systems_.end(); ++systemIt, ++systemDataIt) @@ -807,6 +851,14 @@ namespace jiminy } } + // At this point, consider that the simulation is running + isSimulationRunning_ = true; + + if (returnCode != hresult_t::SUCCESS) + { + stop(); + } + return returnCode; } @@ -2129,7 +2181,7 @@ namespace jiminy // Convert contact force from the global frame to the local frame to store it in contactForces_ pinocchio::SE3 const & transformContactInJoint = system.robot->pncModel_.frames[frameIdx].placement; - system.robot->contactForces_[i] = transformContactInJoint.act(fextLocal); + system.robot->contactForces_[i] = transformContactInJoint.actInv(fextLocal); } // Compute the force at collision bodies diff --git a/python/gym_jiminy/common/gym_jiminy/common/block_bases.py b/python/gym_jiminy/common/gym_jiminy/common/block_bases.py index 9f036b0ee..f0ee0feb5 100644 --- a/python/gym_jiminy/common/gym_jiminy/common/block_bases.py +++ b/python/gym_jiminy/common/gym_jiminy/common/block_bases.py @@ -208,7 +208,9 @@ def _refresh_observation_space(self) -> None: it, then using `BaseObserverBlock` or `BlockInterface` directly is probably the way to go. """ + # Assertion(s) for type checker assert self.env is not None + self.observation_space = self.env.action_space def reset(self, env: Union[gym.Wrapper, BaseJiminyEnv]) -> None: @@ -219,10 +221,11 @@ def reset(self, env: Union[gym.Wrapper, BaseJiminyEnv]) -> None: :param env: Environment to control, eventually already wrapped. """ + # Call base implementation super().reset(env) # Assertion(s) for type checker - assert self.env is not None and self.env.control_dt is not None + assert self.env is not None self.control_dt = self.env.control_dt * self.update_ratio @@ -334,7 +337,9 @@ def _refresh_action_space(self) -> None: it, then using `BaseControllerBlock` or `BlockInterface` directly is probably the way to go. """ + # Assertion(s) for type checker assert self.env is not None + self.action_space = self.env.observation_space def reset(self, env: Union[gym.Wrapper, BaseJiminyEnv]) -> None: @@ -345,10 +350,11 @@ def reset(self, env: Union[gym.Wrapper, BaseJiminyEnv]) -> None: :param env: Environment to observe, eventually already wrapped. """ + # Call base implementation super().reset(env) # Assertion(s) for type checker - assert self.env is not None and self.env.observe_dt is not None + assert self.env is not None self.observe_dt = self.env.observe_dt * self.update_ratio diff --git a/python/gym_jiminy/common/gym_jiminy/common/control_impl.py b/python/gym_jiminy/common/gym_jiminy/common/control_impl.py index 44796d204..6207235a5 100644 --- a/python/gym_jiminy/common/gym_jiminy/common/control_impl.py +++ b/python/gym_jiminy/common/gym_jiminy/common/control_impl.py @@ -77,9 +77,6 @@ def _setup(self) -> None: It updates the mapping from motors to encoders indices. """ - # Assertion(s) for type checker - assert self.robot is not None and self.system_state is not None - # Refresh the mapping between the motors and encoders encoder_joints = [] for name in self.robot.sensors_names[encoder.type]: diff --git a/python/gym_jiminy/common/gym_jiminy/common/env_bases.py b/python/gym_jiminy/common/gym_jiminy/common/env_bases.py index f1ee7a599..390df9e89 100644 --- a/python/gym_jiminy/common/gym_jiminy/common/env_bases.py +++ b/python/gym_jiminy/common/gym_jiminy/common/env_bases.py @@ -20,11 +20,11 @@ from jiminy_py.dynamics import compute_freeflyer_state_from_fixed_body from jiminy_py.simulator import Simulator from jiminy_py.viewer import sleep, play_logfiles -from jiminy_py.controller import BaseJiminyController +from jiminy_py.controller import BaseJiminyController, ControllerHandleType from pinocchio import neutral -from .utils import _clamp, zeros, SpaceDictRecursive +from .utils import _clamp, zeros, fill, SpaceDictRecursive from .generic_bases import ObserveAndControlInterface from .play import loop_interactive @@ -62,19 +62,15 @@ class BaseJiminyEnv(gym.Env, ObserveAndControlInterface): 'render.modes': ['human', 'rgb_array'], } - simulator: Optional[Simulator] - def __init__(self, - simulator: Optional[Simulator], + simulator: Simulator, step_dt: float, enforce_bounded: Optional[bool] = False, debug: bool = False, **kwargs: Any) -> None: r""" :param simulator: Jiminy Python simulator used for physics - computations. Can be `None` if `_setup` has been - overwritten such that 'self.simulator' is a valid and - completely initialized engine. + computations. :param step_dt: Simulation timestep for learning. Note that it is independent from the controller and observation update periods. The latter are configured via @@ -94,7 +90,7 @@ def __init__(self, # pylint: disable=unused-argument # Initialize the interfaces through multiple inheritance - super().__init__() + super().__init__(**kwargs) # Backup some user arguments self.simulator = simulator @@ -108,40 +104,43 @@ def __init__(self, self._log_data: Optional[Dict[str, np.ndarray]] = None self.log_path: Optional[str] = None - # Current observation and action of the robot - self._state: Optional[Tuple[np.ndarray, np.ndarray]] = None - self._sensors_data: Optional[Dict[str, np.ndarray]] = None + # Initialize sensors data shared memory + self._sensors_data = OrderedDict(self.robot.sensors_data) # Information about the learning process self._info: Dict[str, Any] = {} # Number of simulation steps performed self.num_steps = -1 - self.max_steps: Optional[int] = None + self.max_steps = int( + self.simulator.simulation_duration_max // self.step_dt) self._num_steps_beyond_done: Optional[int] = None - # Set the seed of the simulation and reset the environment. - # Note that `reset` is called to make sure the environment is - # completely configured at initialization, which is required by many - # external libraries since it is an implicit required on the OpenAI - # Gym standard interface. + # Initialize the seed of the environment self.seed() - self.reset() + + # Refresh the observation and action spaces + self._refresh_observation_space() + self._refresh_action_space() + + # Assertion(s) for type checker + assert (self.observation_space is not None and + self.action_space is not None) + + # Initialize some internal buffers + self._action = zeros(self.action_space) + self._state = (np.zeros(self.robot.nq), np.zeros(self.robot.nv)) + self._observation = zeros(self.observation_space) @property def robot(self) -> jiminy.Robot: - """ TODO: Write documentation. + """ Get robot. """ - if self.simulator is None: - raise RuntimeError("Backend simulator undefined.") return self.simulator.robot def _get_time_space(self) -> gym.Space: """Get time space. """ - # Assertion(s) for type checker - assert self.simulator is not None - return gym.spaces.Box( low=0.0, high=self.simulator.simulation_duration_max, shape=(1,), dtype=np.float64) @@ -162,9 +161,6 @@ def _get_state_space(self, value 'simulator.use_theoretical_model'. Optional: `None` by default. """ - # Assertion(s) for type checker - assert self.simulator is not None and self.robot is not None - # Handling of default argument if use_theoretical_model is None: use_theoretical_model = self.simulator.use_theoretical_model @@ -379,99 +375,65 @@ def _refresh_action_space(self) -> None: high=effort_limit[motors_velocity_idx], dtype=np.float64) - def set_state(self, qpos: np.ndarray, qvel: np.ndarray) -> None: - """Reset the simulation and specify the initial state of the robot. + def reset(self, + controller_hook: Optional[ + Callable[[], Optional[ControllerHandleType]]] = None + ) -> SpaceDictRecursive: + """Reset the environment. + In practice, it resets the backend simulator and set the initial state + of the robot. The initial state is obtained by calling '_sample_state'. This method is also in charge of setting the initial action (at the beginning) and observation (at the end). .. warning:: It starts the simulation immediately. As a result, it is not - possible to changed the robot (included options), nor to register - log variable. Yet, it is possible to do it passing a custom - 'controller_hook' to `reset` method. - - :param qpos: Configuration of the robot. - :param qvel: Velocity vector of the robot. - """ - # Assertion(s) for type checker - assert self.simulator is not None - - # Reset the simulator - self.simulator.reset() - - # Set default action. It will be used for the initial step. - self._action = zeros(self.action_space) - - # Start the engine, in order to initialize the sensors data - hresult = self.simulator.start( - qpos, qvel, self.simulator.use_theoretical_model) - if hresult != jiminy.hresult_t.SUCCESS: - raise RuntimeError( - "Failed to start the simulation. Probably because the " - "initial state is invalid.") - - # Initialize some internal buffers - self.num_steps = 0 - self.max_steps = int( - self.simulator.simulation_duration_max // self.step_dt) - self._num_steps_beyond_done = None - self._log_data = None - - # Create a new log file - if self.debug: - fd, self.log_path = tempfile.mkstemp(prefix="log_", suffix=".data") - os.close(fd) - - # - self._state = (qpos, qvel) - - # Update the observation - self._observation = self.compute_observation() - - def reset(self, - controller_hook: Optional[Callable[[], None]] = None - ) -> SpaceDictRecursive: - """Reset the environment. - - The initial state is obtained by calling '_sample_state'. + possible to change the robot (included options), nor to register + log variable. The only way to do so is via 'controller_hook'. :param controller_hook: Custom controller hook. It will be executed right after initialization of the environment, and just before actually starting the - simulation. It is useful to override partially + simulation. It is a callable that optionally + returns a controller handle. If so, it will be + used to initialize the low-level jiminy + controller. It is useful to override partially the configuration of the low-level engine, set a custom low-level controller handle, or to - register custom variables to the telemetry. - `None` to disable. + register custom variables to the telemetry. Set + to `None` if unused. Optional: Disable by default. :returns: Initial observation of the episode. """ # pylint: disable=arguments-differ - # Define default controller hook - def register() -> None: - nonlocal self - - # Assertion(s) for type checker - assert self.simulator is not None - - self.simulator.controller.set_controller_handle( - self._send_command) + # Assertion(s) for type checker + assert self.observation_space is not None - # Stop simulator if still running - if self.simulator is not None: - self.simulator.stop() + # Reset the simulator + self.simulator.reset() # Make sure the environment is properly setup self._setup() - # Initialize sensors data shared memory + # Re-initialize sensors data shared memory. + # It must be done because the robot may have changed. self._sensors_data = OrderedDict(self.robot.sensors_data) - # Set the seed of the simulator - self.simulator.seed(self._seed) + # Set default action. + # It will be used for the initial step. + fill(self._action, 0.0) + + # Reset some internal buffers + self.num_steps = 0 + self._num_steps_beyond_done = None + self._log_data = None + + # Create a new log file + if self.debug: + fd, self.log_path = tempfile.mkstemp(prefix="log_", suffix=".data") + os.close(fd) # Extract the controller and observer update period. # There is no actual observer by default, apart from the robot's state @@ -481,35 +443,30 @@ def register() -> None: engine_options = self.simulator.engine.get_options() self.control_dt = \ float(engine_options['stepper']['controllerUpdatePeriod']) - self.observer_dt = \ + self.observe_dt = \ float(engine_options['stepper']['sensorsUpdatePeriod']) - # Refresh the observation and action spaces - self._refresh_observation_space() - self._refresh_action_space() - - # Assertion(s) for type checker - assert self.observation_space is not None - - # Initialize some internal buffers - self._state = (np.zeros(self.robot.nq), np.zeros(self.robot.nv)) - self._observation = zeros(self.observation_space) - self._action = zeros(self.action_space) - # Enforce the low-level controller. - # Note that `BaseJiminyController` is used by default instead of - # `jiminy.ControllerFunctor`. Although it is less efficient because - # it adds an extra layer of indirection, it makes it possible to update - # the controller handle without instantiating a new controller. + # The backend robot may have changed, for example if it is randomly + # generated based on different URDF files. As a result, it is necessary + # to instantiate a new low-level controller. + # Note that `BaseJiminyController` is used by default in place of + # `jiminy.ControllerFunctor`. Although it is less efficient because it + # adds an extra layer of indirection, it makes it possible to update + # the controller handle without instantiating a new controller, which + # is necessary to allow registering telemetry variables before knowing + # the controller handle in advance. controller = BaseJiminyController() controller.initialize(self.robot) self.simulator.set_controller(controller) - # Run controller hook - if controller_hook is None: - register() - else: - controller_hook() + # Run controller hook and set the controller handle + controller_handle = None + if controller_hook is not None: + controller_handle = controller_hook() + if controller_handle is None: + controller_handle = self._send_command + self.simulator.controller.set_controller_handle(controller_handle) # Sample the initial state and reset the low-level engine qpos, qvel = self._sample_state() @@ -519,7 +476,17 @@ def register() -> None: "The initial state provided by `_sample_state` is " "inconsistent with the dimension or types of joints of the " "model.") - self.set_state(qpos, qvel) + + # Start the engine, in order to initialize the sensors data + hresult = self.simulator.start( + qpos, qvel, self.simulator.use_theoretical_model) + if hresult != jiminy.hresult_t.SUCCESS: + raise RuntimeError( + "Failed to start the simulation. Probably because the " + "initial state is invalid.") + + # Update the observation + self._observation = self.compute_observation() # Make sure the state is valid, otherwise there `compute_observation` # and s`_refresh_observation_space` are probably inconsistent. @@ -562,14 +529,16 @@ def seed(self, seed: Optional[int] = None) -> Sequence[np.uint32]: self._seed = np.uint32( seeding._int_list_from_bigint(seeding.hash_seed(self._seed))[0]) + # Reset the seed of Jiminy Engine + self.simulator.seed(self._seed) + return [self._seed] def close(self) -> None: """Terminate the Python Jiminy engine. Mostly defined for compatibility with Gym OpenAI. """ - if self.simulator is not None: - self.simulator.close() + self.simulator.close() def step(self, action: Optional[np.ndarray] = None @@ -581,11 +550,6 @@ def step(self, :returns: Next observation, reward, status of the episode (done or not), and a dictionary of extra information """ - # Assertion(s) for type checker - assert (self.simulator is not None and - self.max_steps is not None and - isinstance(self._action, np.ndarray)) - # Make sure a simulation is already running if not self.simulator.is_simulation_running: raise RuntimeError( @@ -656,9 +620,10 @@ def step(self, def get_log(self) -> Tuple[Dict[str, np.ndarray], Dict[str, str]]: """Get log of recorded variable since the beginning of the episode. """ - # Assertion(s) for type checker - assert self.simulator is not None - + if not self.simulator.is_simulation_running: + raise RuntimeError( + "No simulation running. Please call `reset` at least one " + "before getting log.") return self.simulator.get_log() def render(self, @@ -678,9 +643,6 @@ def render(self, :returns: RGB array if 'mode' is 'rgb_array', None otherwise. """ - # Assertion(s) for type checker - assert self.simulator is not None - if mode == 'human': return_rgb_array = False elif mode == 'rgb_array': @@ -696,9 +658,6 @@ def replay(self, **kwargs: Any) -> None: :param kwargs: Extra keyword arguments for `play_logfiles` delegation. """ - # Assertion(s) for type checker - assert self.simulator is not None and self.robot is not None - if self._log_data is not None: log_data = self._log_data else: @@ -743,16 +702,8 @@ def _setup(self) -> None: By default, it enforces some options of the engine. .. note:: - This method is called internally by `reset` method at the very - beginning. This method can be overwritten to postpone the engine - and robot creation at `reset`. One have to delegate the creation - and initialization of the engine to this method, so that it - alleviates the requirement to specify a valid the engine during the - instantiation of the environment. + This method is called internally by `reset` methods. """ - # Assertion(s) for type checker - assert self.simulator is not None and self.robot is not None - # Extract some proxies robot_options = self.robot.get_options() engine_options = self.simulator.engine.get_options() @@ -760,7 +711,7 @@ def _setup(self) -> None: # Disable part of the telemetry in non debug mode, to speed up the # simulation. Only the required data for log replay are enabled. It is # up to the user to overload this method if logging more data is - # necessary for terminal reward computation. + # necessary for computating the terminal reward. for field in robot_options["telemetry"].keys(): robot_options["telemetry"][field] = self.debug for field in engine_options["telemetry"].keys(): @@ -820,9 +771,6 @@ def _neutral(self) -> np.ndarray: initial state. It can be overloaded to ensure static stability of the configuration. """ - # Assertion(s) for type checker - assert self.simulator is not None and self.robot is not None - # Get the neutral configuration of the actual model qpos = neutral(self.robot.pinocchio_model) @@ -852,9 +800,6 @@ def _sample_state(self) -> Tuple[np.ndarray, np.ndarray]: This method is called internally by `reset` to generate the initial state. It can be overloaded to act as a random state generator. """ - # Assertion(s) for type checker - assert self.simulator is not None and self.robot is not None - # Get the neutral configuration qpos = self._neutral() @@ -876,9 +821,6 @@ def compute_observation(self # type: ignore[override] """ # pylint: disable=arguments-differ - # Assertion(s) for type checker - assert self.simulator is not None - # Update some internal buffers if self.simulator.is_simulation_running: self._state = self.simulator.state @@ -972,8 +914,12 @@ def __init__(self, simulator: Optional[Simulator], step_dt: float, debug: bool = False) -> None: + # Initialize base class super().__init__(simulator, step_dt, debug) + # Define some internal buffers + self._desired_goal: Optional[np.ndarray] = None + def _refresh_observation_space(self) -> None: # Assertion(s) for type checker assert isinstance(self._desired_goal, np.ndarray), ( @@ -994,6 +940,9 @@ def _refresh_observation_space(self) -> None: def compute_observation(self # type: ignore[override] ) -> SpaceDictRecursive: + # Assertion(s) for type checker + assert self._desired_goal is not None + return OrderedDict( observation=super().compute_observation(), achieved_goal=self._get_achieved_goal(), diff --git a/python/gym_jiminy/common/gym_jiminy/common/env_locomotion.py b/python/gym_jiminy/common/gym_jiminy/common/env_locomotion.py index 4831daf8f..73ff43813 100644 --- a/python/gym_jiminy/common/gym_jiminy/common/env_locomotion.py +++ b/python/gym_jiminy/common/gym_jiminy/common/env_locomotion.py @@ -70,8 +70,6 @@ class WalkerJiminyEnv(BaseJiminyEnv): 'render.modes': ['human'], } - simulator: Optional[Simulator] - def __init__(self, urdf_path: str, hardware_path: Optional[str] = None, @@ -151,40 +149,39 @@ def __init__(self, self._forces_profile: Sequence[ForceProfileType] = [] self._f_xy_profile_spline: Optional[ nb.core.dispatcher.Dispatcher] = None - self._power_consumption_max: Optional[float] = None - self._height_neutral: Optional[float] = None + self._power_consumption_max = 0.0 + self._height_neutral = 0.0 # Configure and initialize the learning environment - super().__init__(None, step_dt, enforce_bounded, debug, **kwargs) + simulator = Simulator.build( + self.urdf_path, self.hardware_path, self.mesh_path, + has_freeflyer=True, use_theoretical_model=False, + config_path=self.config_path, + avoid_instable_collisions=self.avoid_instable_collisions, + debug=debug) + super().__init__(simulator, step_dt, enforce_bounded, debug, **kwargs) def _setup(self) -> None: """Configure the environment. It is doing the following steps, successively: - - creates a low-level engine is necessary, - updates some proxies that will be used for computing the reward and termination condition, - enforce some options of the low-level robot and engine, - randomize the environment according to 'std_ratio'. - .. note:: + .. note:: TODO WRONG This method is called internally by `reset` method at the very - beginning. This method can be overwritten to implement new - contributions to the environment stochasticity, or to create - custom low-level robot if the model must be different for each - learning eposide for some reason. + beginning. One must overide it to implement new contributions to + the environment stochasticity, or to create custom low-level robot + if the model must be different for each learning episode. """ + # Call the base implementation + super()._setup() + # Check that a valid engine is available, and if not, create one - if self.simulator is None: - self.simulator = Simulator.build( - self.urdf_path, self.hardware_path, self.mesh_path, - has_freeflyer=True, use_theoretical_model=False, - config_path=self.config_path, - avoid_instable_collisions=self.avoid_instable_collisions, - debug=self.debug) - else: - self.simulator.remove_forces() + self.simulator.remove_forces() if not self.robot.has_freeflyer: raise RuntimeError( @@ -381,11 +378,6 @@ def is_done(self) -> bool: # type: ignore[override] """ # pylint: disable=arguments-differ - # Assertion(s) for type checker - assert (self.simulator is not None and - self._state is not None and - self._height_neutral is not None) - if self._state[0][2] < self._height_neutral * 0.75: return True if self.simulator.stepper_state.t >= self.simu_duration_max: @@ -407,10 +399,6 @@ def compute_reward(self, # type: ignore[override] """ # pylint: disable=arguments-differ - # Assertion(s) for type checker - assert (self.simulator is not None and - self._power_consumption_max is not None) - reward_dict = info.setdefault('reward', {}) # Define some proxies diff --git a/python/gym_jiminy/common/gym_jiminy/common/generic_bases.py b/python/gym_jiminy/common/gym_jiminy/common/generic_bases.py index 774ae1298..b581f59df 100644 --- a/python/gym_jiminy/common/gym_jiminy/common/generic_bases.py +++ b/python/gym_jiminy/common/gym_jiminy/common/generic_bases.py @@ -13,7 +13,7 @@ class ControlInterface: """Controller interface for both controllers and environments. """ - control_dt: Optional[float] + control_dt: float action_space: Optional[gym.Space] _action: Optional[SpaceDictRecursive] @@ -27,7 +27,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: :param kwargs: Extra keyword arguments. See 'args'. """ # Define some attributes - self.control_dt = None + self.control_dt = 0.0 self.action_space = None self._action = None @@ -104,7 +104,7 @@ def compute_reward_terminal(self, info: Dict[str, Any]) -> float: class ObserveInterface: """Observer interface for both observers and environments. """ - observe_dt: Optional[float] + observe_dt: float observation_space: Optional[gym.Space] _observation: Optional[SpaceDictRecursive] @@ -118,14 +118,14 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: :param kwargs: Extra keyword arguments. See 'args'. """ # Define some attributes - self.observe_dt = None + self.observe_dt = 0.0 self.observation_space = None self._observation = None # Call super to allow mixing interfaces through multiple inheritance super().__init__(*args, **kwargs) # type: ignore[call-arg] - def fetch_observation(self) -> None: + def refresh_observation(self) -> None: """Refresh the observation. .. warning:: @@ -218,7 +218,7 @@ def _send_command(self, # breakpoint. A dedicated `BaseObserverBlock` must be used if one wants # to observe a low-frequency features instead of overloading # `compute_observation` and `_refresh_observation_space` directly. - self.fetch_observation() + self.refresh_observation() # Compute the command to send to the motors np.copyto(u_command, self.compute_command( diff --git a/python/gym_jiminy/common/gym_jiminy/common/pipeline_bases.py b/python/gym_jiminy/common/gym_jiminy/common/pipeline_bases.py index 473cdd07d..ae0ae18ab 100644 --- a/python/gym_jiminy/common/gym_jiminy/common/pipeline_bases.py +++ b/python/gym_jiminy/common/gym_jiminy/common/pipeline_bases.py @@ -18,8 +18,10 @@ import numpy as np import gym +from jiminy_py.controller import ControllerHandleType + from .utils import ( - _is_breakpoint, _clamp, zeros, set_value, register_variables, + _is_breakpoint, _clamp, zeros, fill, set_value, register_variables, SpaceDictRecursive) from .generic_bases import ObserveAndControlInterface from .env_bases import BaseJiminyEnv @@ -54,14 +56,15 @@ def __init__(self, the wrapped block. Optional: disable by default. """ - # Initialize base wrapper and interfaces through multiple inheritance - super().__init__(env) - # Backup some user arguments self.augment_observation = augment_observation + # Initialize base wrapper and interfaces through multiple inheritance + super().__init__(env) + # Define some internal buffers self._dt_eps: Optional[float] = None + self._command = zeros(self.env.unwrapped.action_space) def __dir__(self) -> Sequence[str]: """Attribute lookup. @@ -101,28 +104,35 @@ def get_observation(self, bypass: bool = False) -> SpaceDictRecursive: return _clamp(self.observation_space, self._observation) def reset(self, - controller_hook: Optional[Callable[[], None]] = None, + controller_hook: Optional[ + Callable[[], Optional[ControllerHandleType]]] = None, **kwargs: Any) -> SpaceDictRecursive: """Reset the unified environment. In practice, it resets the environment and initializes the generic pipeline internal buffers through the use of 'controller_hook'. - :param controller_hook: Custom controller hook to use in place of the - one provided by the controller itself. Used - for chaining multiple `ControlledJiminyEnv`. - It is not meant to be defined manually. + :param controller_hook: Used internally for chaining multiple + `BasePipelineWrapper`. It is not meant to be + defined manually. Optional: None by default. :param kwargs: Extra keyword arguments to comply with OpenAI Gym API. """ # pylint: disable=unused-argument # Define chained controller hook - def register() -> None: + def register() -> ControllerHandleType: + """Register the block to the higher-level block. + + This method is used internally to make sure that `_setup` method + of connected blocks are called in the right order, namely from the + lowest-level block to the highest-level one, right after reset of + the low-level simulator and just before performing the first step. + """ nonlocal self, controller_hook # Assertion(s) for type checker - assert self.env is not None and self.env.simulator is not None + assert self.env is not None # Get the temporal resolution of simulator steps engine_options = self.simulator.engine.get_options() @@ -131,16 +141,17 @@ def register() -> None: # Initialize the pipeline wrapper self._setup() - # Register the controller handle or use the custom hook is defined - if controller_hook is None: - self.env.simulator.controller.set_controller_handle( - self._send_command) - else: - controller_hook() + # Forward the controller handle provided by the controller hook, + # if any, or use the one of the controller otherwise. + controller_handle = None + if controller_hook is not None: + controller_handle = controller_hook() + if controller_handle is None: + controller_handle = self._send_command + return controller_handle # Reset base pipeline - self.env.reset( # type: ignore[call-arg] - controller_hook=register, **kwargs) + self.env.reset(register, **kwargs) # type: ignore[call-arg] return self.get_observation() @@ -169,12 +180,41 @@ def step(self, def _setup(self) -> None: """Configure the wrapper. - This method does nothing by default. One is expected to overload it. + By default, it only resets some internal buffers. .. note:: This method must be called once, after the environment has been reset. This is done automatically when calling `reset` method. """ + fill(self._action, 0.0) + fill(self._command, 0.0) + fill(self._observation, 0.0) + + def compute_observation(self # type: ignore[override] + ) -> SpaceDictRecursive: + """Compute the unified observation. + + By default, it forwards the observation computed by the environment. + + :param measure: Observation of the environment. + """ + # pylint: disable=arguments-differ + + self.env.refresh_observation() + return self.get_observation(bypass=True).copy() # No deepcopy ! + + def compute_command(self, + measure: SpaceDictRecursive, + action: SpaceDictRecursive + ) -> SpaceDictRecursive: + """Compute the motors efforts to apply on the robot. + + By default, it forwards the command computed by the environment. + + :param measure: Observation of the environment. + :param action: Target to achieve. + """ + return self.env.compute_command(measure, action) class ControlledJiminyEnv(BasePipelineWrapper): @@ -230,7 +270,8 @@ class ControlledJiminyEnv(BasePipelineWrapper): def __init__(self, env: Union[gym.Wrapper, BaseJiminyEnv], controller: BaseControllerBlock, - augment_observation: bool = False): + augment_observation: bool = False, + **kwargs: Any): """ .. note:: As a reminder, `env.step_dt` refers to the learning step period, @@ -256,6 +297,8 @@ def __init__(self, if the observation space is of type `gym.spaces.Dict`. Optional: disable by default. + :param kwargs: Extra keyword arguments to allow automatic pipeline + wrapper generation. """ # Initialize base wrapper super().__init__(env, augment_observation) @@ -263,27 +306,14 @@ def __init__(self, # Backup user arguments self.controller = controller - # Define some internal buffers - self._target: Optional[SpaceDictRecursive] = None - self._command: Optional[np.ndarray] = None - self.controller_name: Optional[str] = None - - def _setup(self) -> None: - # Assertion(s) for type checker - assert self.simulator is not None - # Assertion(s) for type checker - assert (self.env.control_dt is not None and - self.env.action_space is not None and + assert (self.env.action_space is not None and self.env.observation_space is not None) # Reset the controller self.controller.reset(self.env) self.control_dt = self.controller.control_dt - # Assertion(s) for type checker - assert self.control_dt is not None - # Make sure the controller period is lower than environment timestep assert self.control_dt <= self.env.unwrapped.step_dt, ( "The control update period must be lower than or equal to the " @@ -298,16 +328,6 @@ def _setup(self) -> None: # Assertion(s) for type checker assert self.action_space is not None - # Initialize the controller's input action and output target - self._action = zeros(self.action_space) - self._target = zeros(self.env.action_space) - - # Initialize the unified observation with zero target - self._observation = self.compute_observation() - - # Initialize the command to apply on the robot - self._command = zeros(self.env.unwrapped.action_space) - # Check that 'augment_observation' can be enabled assert not self.augment_observation or isinstance( self.env.observation_space, gym.spaces.Dict), ( @@ -321,10 +341,25 @@ def _setup(self) -> None: 'targets', gym.spaces.Dict()).spaces[self.controller_name] = \ self.controller.action_space + # Initialize some internal buffers + self._action = zeros(self.action_space) + self._target = zeros(self.env.action_space) + self._observation = zeros(self.observation_space) + + def _setup(self) -> None: + """Configure the wrapper. + + In addition to the base implementation, it resgisters the controller's + target to the telemetry. + """ + # Call base implementation + super()._setup() + + # Reset some additional internal buffers + fill(self._target, 0.0) + # Register the controller target to the telemetry. - # It may be useful later for computing the terminal reward or debug. - # Note that it is not necessary for the controller to be fully - # initialized before registering variables. + # It may be useful for computing the terminal reward or debugging. register_variables( self.simulator.controller, self.controller.get_fieldnames(), self._action, self.controller_name) @@ -344,14 +379,12 @@ def compute_command(self, :param measure: Observation of the environment. :param action: High-level target to achieve. """ - # Assertion(s) for type checker - assert self.simulator is not None and self._command is not None - # Update the target to send to the subsequent block if necessary. # Note that `_observation` buffer has already been updated right before # calling this method by `_send_command`, so it can be used as measure # argument without issue. - if _is_breakpoint(measure['t'], self.control_dt, self._dt_eps): + t = self.simulator.stepper_state.t + if _is_breakpoint(t, self.control_dt, self._dt_eps): target = self.controller.compute_command(self._observation, action) set_value(self._target, target) @@ -360,8 +393,11 @@ def compute_command(self, # update the command of the right period. Ultimately, this is done # automatically by the engine, which is calling `_send_command` at the # right period. - np.copyto(self._command, self.env.compute_command( - self._observation, self._target)) + if self.env.simulator.is_simulation_running: + # Do not update command during the first iteration because the + # action is undefined at this point + np.copyto(self._command, self.env.compute_command( + self._observation, self._target)) return self._command @@ -382,13 +418,14 @@ def compute_observation(self # type: ignore[override] :returns: Original environment observation, eventually including controllers targets if requested. """ - # pylint: disable=arguments-differ + # Get environment observation + obs = super().compute_observation() - self.env.fetch_observation() - obs = self.get_observation(bypass=True).copy() # No deepcopy ! + # Add target to observation if requested if self.augment_observation: obs.setdefault('targets', OrderedDict())[ self.controller_name] = self._action + return obs def step(self, @@ -439,7 +476,8 @@ class ObservedJiminyEnv(BasePipelineWrapper): def __init__(self, env: Union[gym.Wrapper, BaseJiminyEnv], observer: BaseObserverBlock, - augment_observation: bool = False): + augment_observation: bool = False, + **kwargs: Any): """ :param env: Environment to control. It can be an already controlled environment wrapped in `ObservedJiminyEnv` if one desires @@ -451,6 +489,8 @@ def __init__(self, option is only available if the observation space is of type `gym.spaces.Dict`. Optional: disable by default. + :param kwargs: Extra keyword arguments to allow automatic pipeline + wrapper generation. """ # Initialize base wrapper super().__init__(env, augment_observation) @@ -458,63 +498,6 @@ def __init__(self, # Backup user arguments self.observer = observer - # Reset the unified environment - self.reset() - - def compute_observation(self # type: ignore[override] - ) -> SpaceDictRecursive: - """Compute high-level features based on the current wrapped - environment's observation. - - It gathers the original observation from the environment with the - features computed by the observer, if requested, otherwise it forwards - the features directly without any further processing. - - .. warning:: - Beware it updates and returns '_observation' buffer to deal with - multiple observers with different update periods. Even so, it is - safe to call this method multiple times successively. - - :returns: Updated part of the observation only for efficiency. - """ - # pylint: disable=arguments-differ - - # Refresh environment observation - self.env.fetch_observation() - if self.augment_observation: - obs = self.get_observation(bypass=True).copy() # No deepcopy ! - - # Get the current time - t = self.simulator.stepper_state.t - - # Update observed features if necessary - if _is_breakpoint(t, self.observe_dt, self._dt_eps): - features = self.observer.compute_observation(obs) - if self.augment_observation: - obs.setdefault( - 'features', OrderedDict())[self.observer_name] = features - else: - obs = features - else: - if not self.augment_observation: - obs = OrderedDict() # Nothing new to observe. - - return obs - - def compute_command(self, - measure: SpaceDictRecursive, - action: SpaceDictRecursive - ) -> SpaceDictRecursive: - """Compute the motors efforts to apply on the robot. - - In practice, it forwards the command computed by the environment. - - :param measure: Observation of the environment. - :param action: Target to achieve. - """ - return self.env.compute_command(measure, action) - - def _setup(self) -> None: # Assertion(s) for type checker assert (self.env.action_space is not None and self.env.observation_space is not None) @@ -525,13 +508,6 @@ def _setup(self) -> None: # Update the action space self.action_space = self.env.action_space - # Initialize the unified observation with zero target - self._observation = self.compute_observation() - - # Initialize the environment's action and command - self._action = zeros(self.action_space) - self._command = zeros(self.env.unwrapped.action_space) - # Reset the observer self.observer.reset(self.env) @@ -557,6 +533,46 @@ def _setup(self) -> None: else: self.observation_space = self.observer.observation_space + # Initialize some internal buffers + self._action = zeros(self.action_space) + self._observation = zeros(self.observation_space) + + def compute_observation(self # type: ignore[override] + ) -> SpaceDictRecursive: + """Compute high-level features based on the current wrapped + environment's observation. + + It gathers the original observation from the environment with the + features computed by the observer, if requested, otherwise it forwards + the features directly without any further processing. + + .. warning:: + Beware it updates and returns '_observation' buffer to deal with + multiple observers with different update periods. Even so, it is + safe to call this method multiple times successively. + + :returns: Updated part of the observation only for efficiency. + """ + # pylint: disable=arguments-differ + + # Get environment observation + obs = super().compute_observation() + + # Update observed features if necessary + t = self.simulator.stepper_state.t + if _is_breakpoint(t, self.observe_dt, self._dt_eps): + features = self.observer.compute_observation(obs) + if self.augment_observation: + obs.setdefault( + 'features', OrderedDict())[self.observer_name] = features + else: + obs = features + else: + if not self.augment_observation: + obs = OrderedDict() # Nothing new to observe. + + return obs + def build_pipeline(env_config: Tuple[ Type[BaseJiminyEnv], diff --git a/python/gym_jiminy/common/gym_jiminy/common/utils.py b/python/gym_jiminy/common/gym_jiminy/common/utils.py index 49d5e4cb4..61a4535af 100644 --- a/python/gym_jiminy/common/gym_jiminy/common/utils.py +++ b/python/gym_jiminy/common/gym_jiminy/common/utils.py @@ -33,23 +33,37 @@ def zeros(space: gym.Space) -> SpaceDictRecursive: f"Space of type {type(space)} is not supported by this method.") +def fill(data: SpaceDictRecursive, + fill_value: float) -> None: + """Set every element of 'data' from `Gym.Space` to scalar 'fill_value'. + """ + if isinstance(data, dict): + for sub_data in data.values(): + fill(sub_data, fill_value) + elif isinstance(data, np.ndarray): + data.fill(fill_value) + else: + raise NotImplementedError( + f"Data of type {type(data)} is not supported by this method.") + + def set_value(data: SpaceDictRecursive, - fill_value: SpaceDictRecursive) -> None: - """Partially set 'data' from `Gym.Space` to 'fill_value'. + value: SpaceDictRecursive) -> None: + """Partially set 'data' from `Gym.Space` to 'value'. It avoids memory allocation, so that memory pointers of 'data' remains unchanged. A direct consequences, it is necessary to preallocate memory beforehand, and to work with fixed size buffers. .. note:: - If 'data' is a dictionary, 'fill_value' must be a subtree of 'data', + If 'data' is a dictionary, 'value' must be a subtree of 'data', whose leaf values must be broadcastable with the ones of 'data'. """ if isinstance(data, dict): - for field, sub_val in fill_value.items(): + for field, sub_val in value.items(): set_value(data[field], sub_val) elif isinstance(data, np.ndarray): - np.copyto(data, fill_value) + np.copyto(data, value) else: raise NotImplementedError( f"Data of type {type(data)} is not supported by this method.") diff --git a/python/gym_jiminy/common/gym_jiminy/common/wrappers.py b/python/gym_jiminy/common/gym_jiminy/common/wrappers.py index bf0f5b88d..fce8ca436 100644 --- a/python/gym_jiminy/common/gym_jiminy/common/wrappers.py +++ b/python/gym_jiminy/common/gym_jiminy/common/wrappers.py @@ -1,13 +1,13 @@ from copy import deepcopy from functools import reduce from collections import deque -from typing import Tuple, Type, Dict, Sequence, List, Optional, Any, Iterator +from typing import Tuple, Type, Dict, Sequence, List, Any, Iterator import numpy as np import gym -from .utils import SpaceDictRecursive +from .utils import zeros, SpaceDictRecursive class PartialFrameStack(gym.Wrapper): @@ -30,46 +30,7 @@ def __init__(self, will be stacked. :param num_stack: Number of observation frames to partially stack. """ - # Backup user arguments - self.nested_fields_list: List[List[str]] = list( - map(list, nested_fields_list)) # type: ignore[arg-type] - self.leaf_fields_list: List[List[str]] = [] - self.num_stack = num_stack - - # Define some internal buffers - self._observation: Optional[SpaceDictRecursive] = None - - # Initialize base wrapper - super().__init__(env) - - # Create internal buffers - self._frames: List[deque] = [] - - def get_observation(self) -> SpaceDictRecursive: - assert (self._observation is not None and - all(len(frames) == self.num_stack for frames in self._frames)) - - # Replace nested fields of original observation by the stacked ones - for fields, frames in zip(self.leaf_fields_list, self._frames): - root_obs = reduce(lambda d, key: d[key], fields[:-1], - self._observation) - root_obs[fields[-1]] = np.stack(frames) - - return self._observation - - def step(self, - action: SpaceDictRecursive - ) -> Tuple[SpaceDictRecursive, float, bool, Dict[str, Any]]: - self._observation, reward, done, info = self.env.step(action) - - # Backup the nested observation fields to stack - for fields, frames in zip(self.leaf_fields_list, self._frames): - leaf_obs = reduce(lambda d, key: d[key], fields, self._observation) - frames.append(leaf_obs) - - return self.get_observation(), reward, done, info - - def reset(self, **kwargs: Any) -> SpaceDictRecursive: + # Define helper that will be used to determine the leaf fields to stack def _get_branches(root: Any) -> Iterator[List[str]]: if isinstance(root, dict): for field, node in root.items(): @@ -79,10 +40,20 @@ def _get_branches(root: Any) -> Iterator[List[str]]: else: yield [field] - self._observation = self.env.reset(**kwargs) + # Backup user arguments + self.nested_fields_list: List[List[str]] = list( + map(list, nested_fields_list)) # type: ignore[arg-type] + self.num_stack = num_stack + + # Initialize base wrapper + super().__init__(env) + + # Define some internal buffers + self._observation: SpaceDictRecursive = zeros( + self.env.observation_space) - # Determine leaf fields to stack - self.leaf_fields_list = [] + # Get the leaf fields to stack + self.leaf_fields_list: List[List[str]] = [] for fields in self.nested_fields_list: root_field = reduce( lambda d, key: d[key], fields, self._observation) @@ -109,9 +80,34 @@ def _get_branches(root: Any) -> Iterator[List[str]]: root_space.spaces[fields[-1]] = gym.spaces.Box( low=low, high=high, dtype=space.dtype) - # Allocate the frames buffers - self._frames = [deque(maxlen=self.num_stack) - for _ in range(len(self.leaf_fields_list))] + # Allocate internal frames buffers + self._frames: List[deque] = [ + deque(maxlen=self.num_stack) + for _ in range(len(self.leaf_fields_list))] + + def get_observation(self) -> SpaceDictRecursive: + # Replace nested fields of original observation by the stacked ones + for fields, frames in zip(self.leaf_fields_list, self._frames): + root_obs = reduce(lambda d, key: d[key], fields[:-1], + self._observation) + root_obs[fields[-1]] = np.stack(frames) + return self._observation + + def step(self, + action: SpaceDictRecursive + ) -> Tuple[SpaceDictRecursive, float, bool, Dict[str, Any]]: + # Perform a single step + self._observation, reward, done, info = self.env.step(action) + + # Backup the nested observation fields to stack + for fields, frames in zip(self.leaf_fields_list, self._frames): + leaf_obs = reduce(lambda d, key: d[key], fields, self._observation) + frames.append(leaf_obs) + + return self.get_observation(), reward, done, info + + def reset(self, **kwargs: Any) -> SpaceDictRecursive: + self._observation = self.env.reset(**kwargs) # Initialize the frames by duplicating the original one for fields, frames in zip(self.leaf_fields_list, self._frames): @@ -161,11 +157,21 @@ def build_wrapper(env_config: Tuple[ (wrapper_class,), {}) - def __init__(self: wrapped_env_class) -> None: # type: ignore[valid-type] + def __init__(self: gym.Wrapper) -> None: env = env_class(**env_kwargs) super(wrapped_env_class, self).__init__( # type: ignore[arg-type] env, **wrapper_kwargs) + def __dir__(self: gym.Wrapper) -> Sequence[str]: + """Attribute lookup. + + It is mainly used by autocomplete feature of Ipython. It is overloaded + to get consistent autocompletion wrt `getattr`. + """ + return super( # type: ignore[arg-type] + wrapped_env_class, self).__dir__() + self.env.__dir__() + wrapped_env_class.__init__ = __init__ # type: ignore[misc] + wrapped_env_class.__dir__ = __dir__ # type: ignore[assignment] return wrapped_env_class diff --git a/python/gym_jiminy/envs/gym_jiminy/envs/anymal.py b/python/gym_jiminy/envs/gym_jiminy/envs/anymal.py index ac7c30f6d..5937b7c0b 100644 --- a/python/gym_jiminy/envs/gym_jiminy/envs/anymal.py +++ b/python/gym_jiminy/envs/gym_jiminy/envs/anymal.py @@ -63,11 +63,11 @@ def __init__(self, debug: bool = False, **kwargs): debug=debug, **kwargs) - def _refresh_observation_space(self) -> None: - self.observation_space = self._get_state_space() + # def _refresh_observation_space(self) -> None: + # self.observation_space = self._get_state_space() - def fetch_obs(self) -> None: - return np.concatenate(self._state) + # def compute_observation(self) -> None: + # return np.concatenate(self._state) ANYmalPDControlJiminyEnv = build_pipeline(**{ diff --git a/python/gym_jiminy/unit_py/atlas_standing_meshcat.png b/python/gym_jiminy/unit_py/atlas_standing_meshcat.png index fca496b085fe80a9f6fd910cef6b7fb04eb07001..28f252a79764c67048b8c6841b8bd6b18098594d 100644 GIT binary patch literal 29075 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 literal 29087 zcmYIP1zc3y*S+*8H7L@pA|fR%ogyV6EuGRK-O>UA0)n)(3X;+-F@!WYgmgDZ_jktk z-v6856%cNmdt&dk)?VkW@^cydn-n)82*Q_rCZ!5N7<3SXt_8yeze#otZ~)&hUrWfU z!NA8GW*P?mf5YLK_G<_tG(!DD`z)Go2|;v_tdzK#d+O$tW~8a5>w}|fH$C?Z#xfgC zmn7a{uHj9ZWcnMoUBuA3A;4DjylP> z?u(;iKz#(zCM*pgBGjir0Ph*2SQzTFYD$JD`p-`}4mU{t{yB{dL*We^_2b|TBkZ8R ze|;W_miO-!L1Z=o(o(3;gJAiRf39ugfap;V$-3Q20Z0A7An@3xRsNr|LjHFanS%d6 z_mzFzEK>jXug((Hm=XUx0uSTAOE>>}K#26}KW7>9hsFGRmi52wXe;@zZFpRC|DL6V zA@R>MJ7(#e|7piV-T&PT4ffyr=?4D$20`TiJrjGM_Me-m{{N#A4gX#+_;!v?;-9-c z{`v14yq&xE?*sn-S4Q~%J)m;#KI(kX9rv~VZ5r8s7e2l>`R{H2zjy!t)$#uKcKGc7 zwpNl8{ol_0@xQzMf2aNL-SIfEz{_(H*B{MMhP4{dBv-KS7=sah6o~47CY7;Mm}ZuX zRzk1>BL+P|R^-7B!sEF+x(ab-v8JH~g*!1@V?FKzhOF1Eh8N;eQl5SohxvYQa7o#k zI~U=YXr2u;0dyb9O@DIMq;1rGR%3=c`ET^X;pl{c!HJHiQc})QWcfa5;Ct<~ljxh7 zORC(S#P|Ix^tnMh=erS#A3rt?S?L>QrB^|ps-o5k)a1}NI-K@-_OP_NIxH=X8dIF(bsc*xo_v=nr-L4;Qie!7((a@C z*qka0Lzd>}U$SUSw zYhhsUln(xu59teqJ&RLq9Js2{B@jLBQrb;vI!&pju`$xrzx=Vn$dI-F7B%IIKgqW+ z>msEa^byO;%cU4^9ahj7ZhVyIi(6qlW=-q?o%1@H6cbJ78;OP$PxFEsoM^Bhga<5x z;!o{!cllMb^oth;vt>fD@g!ixlNrq~&tbF|?X;~o{BiHU;UjhCe-lKDHona2KAI8( zaWTY()u(VY0Ye#7_uTl~Jq$%}S?d{j_oR5|YS}2>VTZKNWwziz z^;Z{!dk?PH4UFm89m+FJ9uroh?G}>K!|prf`B$97Cw^HlnR81makkZ6xBoP`L2sDM zD3C_*gQJbrny_{5b$m9>cCx*r2#jd(k(l5XE*(QwoXXm745-jui9T2An?@^m_snw` z{jP=I0lUigd4%)}VZv7oS4~AX>NTQpNGhMwXJuTqyhmn*4{AEmcdHOW4tFRGPVDc3 zer%0F1u>r`b-@hdV(O|1TW*TZB`3t!)6b$OO?1#Ra-`?yn$-`H&D+l;paB9 zZi${W_(jp7M_#tqI+84Ya+@Fh8s**5R-PVIWI6B&Eyf?R8JcbBDyG>6bL&)KNCcLSCg=2#F^_@TKZPu$ovddyUY}9;1??c^An&lnM zgZ33#-xGTOY7$Ag8wkga%7*>K84L~@XDQCBC!GN>7`<{A#cnG+q)759x@t&FG661q zk=^CXO})!d6NH9*qfozm_#pd;m4D9;7(vZ9^rs$5QU`L?#6|Q5#nD)lR;M4jO2Xu^b_oM3x#nYfT_ryO|p+i$jbU#50Lk6lG6?C^N2#-7kKLQr401)N|-X zHkzJ}IrS!<^H7FGbd# zR{GTgH^iu!18+5iKoOaF15I+cgu~lkVy9)KCMSM?8y^=J_f>_?$r8il?k&)@sQhzq zlMD~%)w0uPiZq8HmEjcXkDV{g4DhUH4ZWlzMR;Q>Q3 z3I)Yb`1w1}+{mhT3}ng8hf_NXd)<6-718^ByX_8 zj84RVWA)q`2*y@Kev%iSM!O~Pim5`hWvG0|__*R41|puulmuBw*g`tX*HPxPOA!(| z0@uLJ8;x8bJP&CFJ(oHNA8?@t@yl;Z7aP;z^#MJ0AR4z~E8qDH;uyKz_uJm-hDP=E zZ3{7pYKcuH>BJH|YU3t;y;tZGH=mJLt=Wp4A{}N<+0mek3H5^tPH1*(2y$r+!+;OB z*u^|a;sc1|Io3amthT6?1210geq^Bli6ZHe6M2#XcG1Q>j^Dw#kh7DuZ11Gal?`T9a9A7%jNF|aNCyClF+6n^_Qr&x*s z!TfXJdc&AOQn#Kde0bC84BfI2n}_b%z?3s`iPty8 z9xY9goY$`R-MI*lk>Tv?!UJJcoRquK%CTHIGs=U;8hc(r_3_N=qc+Yhv=-m3P=9~_ zATi8mHy&a!*f#NEa;`;ULAcS^JUYDwifQ*i)Z8$MejB!0@YdG-Z5Zr>-$C8j<$|AM zNxYSW00zT-_|rX379wg>MQ(b6qYt;7L~*mWkRP1wuU|OZU0Oi?xl^=gxO~rd+3zuA zl?R|Og+7`QnK3_ApU>2*#yg~$lG{}s)QP5|&?{NeLjDrv;0=j3$s6>GEg}@LboU7K zj|?L2`EC?ny}mfDx>jOgWiwisMsVO^q;I3gqMFp${dacVgtfxcS9*OKF0*Hp5)697 zM4>kMV(uGK^Pg@fgg8T42`}4al*i!@=M};#&TlZEG|@w*FEi*S&&cGz5z^UTVCTd; zP~ns{V*%)8;PQ7Yg2yZIp-z!2EH6+6pS-%58wRuJ@(-=_w3+KV1H(Qxf zWaw9g5(k%?p^RuX8CWXhg!B&ZFleB{J&A;Y4ik>$fBTv9K5}<)*)RIjfYkMbJz^!n zF{Hyq@5HzHBoKhKsK067pD7$bEjGQ{pe7xAbm-?C$xk~{X!q3qTBOMKn!GR*pB?kJ z@(+Cf-YH(ZN1?_wcovsz-ZP=}kYdD!z{qbgP4r6+0)d~)FS|Tk@Hu#zN3gn)Gp)$! zYaI7KQ#*b2BK`a3uB!l<7L)vq-eGjG*;+Q=e0SP$8xp%NX~BH~o5`nN)PBrVPRqm) zb{uu-bu*;&8XFm*g%}x{m|zrbpob+IeffH;v}v)s*&Y4v*mWsdBm>5~Ik>_31QLQ? zzVr+CY>1LRr*I-=n{5fiVL#jfsIvSGK;MIp=;FGX9jU%g+MYB<+f}6~NFcfK32B5t zA~HE&bb2yk#RL9USX|s%#`1k~>mGE<5YkFtq*sTnhb%2CL#M~&<>fWhCIPR!b1phO zBMUNSMvF5aNXrHlI?@HBKZ0>dODs25I~hWr;_pyIFEr5<6) z^YkJXDWOMc)LWfu<11aXkW}rk>JpT9qq${2T}p-oqTihkAne}6MSJqL1qEC9vbE?(hOG{=jq*UKz zU_43p64vjZrW{lRr6NPhTvlYS4r`Ef{+F9?1C77H5Iqp|RO5!-Inhm0d)mS^)WER( z9T-fsnPY&q(z|>7$wj?*#-s$94TZhTw$m$?u#1ufpifD-d#B4ZsIH;wD}>QI2vb5O zG5BTfi|jvzHKu=K3`l}2)T1Nj*sJ^JJZR)jfB*f&OUT)c!18;6B~|Nr+eJQcNB1{o~Zj;HJIx8EmS0jm?FtU+^~zbhMZ+=49w! z-7TU?VubJ_*MA~Gj@`UbR3Y<6-@emBF3(&n5-U_re*TJv2&Y)c3n|!3&kd!^Q|Q>O@6KDq|u6qQNJl zRuv^xRiO%ufySk(6p1c?#Zn_VZ-`R-1hF z5HfyR29-eRg23C$e!3SV)}SvVk|C2nYy>E;GeIeee5Rg;96{fXXpWAKAO#CnwdPE8 zB5Bq^Ih=bTMv-PTLuyYKN(w{boopWdTC7~NmTgKeQd02?T=D4IVLs2S9OY*13da=p zx8aR2E)7F&QA5pl{r24LX^4o@YS-+kz(|y@pTT-{!wTzgAr8gcVC;>^ez;71$yHd> z3GYHMyMt9I4yS_!e0i?9A3i*c)^2*)S!igkDxj6@LNIeYxPv<8&V5ypS?IKM+slXh zif;?luU=9R4SE>Q#`HY^NUdYj&(u0OBse(u%l#K?*98IF9!ZhfN$(Dv^1GUiIU^ny zOpfMw^i&Or%hgs&v7XA&_8%|s)_?7gM!EzUeKb`#a!dE10gd%K>3<67t}+}G#k(tX zK#RcuVofpkJWlFW9@36s3STy7QRCozB>8?jfQQ^PXL9{HNnQaW4xxr5#zcee$rp_{ zde6wfF+r}6T8Lzw0a0#07?lp5a8bOArF5Zde8PukV)NuhZNs(^mk5CuYeR2g%B4p? zt&D(sk{dpAU3Nf@Yu!6tHa?%JpT2OSv`5&YRd2)k2;cHU5?r)_B4MnqcRq|OW8e%+uOYP2Klplo^k`pQTlj4K zWV4$67VNZ<5a|NeLqa(tGd^iFUvz^Ym{1HPnYW43xK!oFD&HBGe#G0|aIFK3v5QwC z%rv&TJLu=5ljlt$#daoTd6H>&FKczSc|10q;jxX4T{>`=-*hk#AB6fT1!c%W$OuBA z`AsFpIB}5-zrm_#0K$`6Fu@?5#$44`+EN0-s>Q;1d(f+arhADKYTmCmwZrC^_T&=| z;&^ftIT4=CNnak2)WxE3#=o|ZpI=(g@^{LOUuG)T`Y#L zw@Kg@|;9O@~H>0cRKa|TT)GH8mbbKB{d+_HcN(D_>?cU=K7?G
  • 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 BDhU7p diff --git a/python/gym_jiminy/unit_py/test_pipeline_control.py b/python/gym_jiminy/unit_py/test_pipeline_control.py index f787b9f4b..c5dc80a23 100644 --- a/python/gym_jiminy/unit_py/test_pipeline_control.py +++ b/python/gym_jiminy/unit_py/test_pipeline_control.py @@ -16,13 +16,22 @@ def setUp(self): """ TODO: Write documentation """ warnings.filterwarnings("ignore", category=DeprecationWarning) - self.env = AtlasPDControlJiminyEnv() + self.env = AtlasPDControlJiminyEnv(debug=False) def test_pid_standing(self): """ TODO: Write documentation """ + # Check that it is not possible to get simulation log at this point + self.assertRaises(RuntimeError, self.env.get_log) + # Reset the environment - obs_init = self.env.reset() + def configure_telemetry() -> None: + nonlocal self + engine_options = self.env.simulator.engine.get_options() + engine_options['telemetry']['enableVelocity'] = True + self.env.simulator.engine.set_options(engine_options) + + obs_init = self.env.reset(controller_hook=configure_telemetry) # The initial target corresponds to the initial joints state, so that # the robot stand-still. @@ -59,8 +68,8 @@ def test_pid_standing(self): data = log_data[log_name] self.assertTrue(np.all(np.abs(data) < 1e-9)) - # Check that the velocity is close to zero at the end + # Check that the whole-body robot velocity is close to zero at the end velocity_mes = np.stack([ - log_data['.'.join((encoder.type, name, encoder.fieldnames[1]))] - for name in self.env.robot.sensors_names[encoder.type]], axis=-1) + log_data['.'.join(('HighLevelController', name))] + for name in self.env.robot.logfile_velocity_headers], axis=-1) self.assertTrue(np.all(np.abs(velocity_mes[-1000:]) < 1e-3)) diff --git a/python/jiminy_py/src/jiminy_py/controller.py b/python/jiminy_py/src/jiminy_py/controller.py index 75d2b3447..c26e9bfb9 100644 --- a/python/jiminy_py/src/jiminy_py/controller.py +++ b/python/jiminy_py/src/jiminy_py/controller.py @@ -12,6 +12,11 @@ from tqdm import tqdm +ControllerHandleType = Callable[[ + float, np.ndarray, np.ndarray, jiminy.sensorsData, np.ndarray +], None] + + class BaseJiminyController(jiminy.ControllerFunctor): """Base class to instantiate a Jiminy controller based on a callable function that can be changed on-the-fly. @@ -63,12 +68,9 @@ def reset(self) -> None: super().reset() self.close_progress_bar() - def set_controller_handle( - self, - controller_handle: Callable[[ - float, np.ndarray, np.ndarray, jiminy.sensorsData, np.ndarray - ], None] - ) -> None: + def set_controller_handle(self, + controller_handle: ControllerHandleType + ) -> None: r"""Set the controller callback function to use. :param compute_command: diff --git a/python/jiminy_py/src/jiminy_py/dynamics.py b/python/jiminy_py/src/jiminy_py/dynamics.py index 87ea317ed..484e31e2d 100644 --- a/python/jiminy_py/src/jiminy_py/dynamics.py +++ b/python/jiminy_py/src/jiminy_py/dynamics.py @@ -496,7 +496,7 @@ def compute_freeflyer_state_from_fixed_body( w_M_ff = w_M_ground.act(ff_M_fixed_body.inverse()) position[:7] = pin.SE3ToXYZQUAT(w_M_ff) - if fixed_body_name is None: + if fixed_body_name is not None: if velocity is not None: ff_v_fixed_body = get_body_world_velocity( robot, fixed_body_name, use_theoretical_model=False)