From cf153f66ad14ac83d1ae997e9dc12acf2d3c618a Mon Sep 17 00:00:00 2001 From: Kallinteris Andreas <30759571+Kallinteris-Andreas@users.noreply.github.com> Date: Tue, 2 May 2023 17:55:43 +0300 Subject: [PATCH 01/29] Add Hopper and Walker2D models for v5 --- gymnasium/envs/mujoco/assets/hopper_v5.xml | 53 +++++++++++++++ gymnasium/envs/mujoco/assets/walker2d_v5.xml | 68 ++++++++++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 gymnasium/envs/mujoco/assets/hopper_v5.xml create mode 100644 gymnasium/envs/mujoco/assets/walker2d_v5.xml diff --git a/gymnasium/envs/mujoco/assets/hopper_v5.xml b/gymnasium/envs/mujoco/assets/hopper_v5.xml new file mode 100644 index 000000000..e9ec942d7 --- /dev/null +++ b/gymnasium/envs/mujoco/assets/hopper_v5.xml @@ -0,0 +1,53 @@ + + + + + + + + + diff --git a/gymnasium/envs/mujoco/assets/walker2d_v5.xml b/gymnasium/envs/mujoco/assets/walker2d_v5.xml new file mode 100644 index 000000000..cf8042a7c --- /dev/null +++ b/gymnasium/envs/mujoco/assets/walker2d_v5.xml @@ -0,0 +1,68 @@ + + + + + + + + From 0cbdd72943166cc174242a558fde30843fd6b21b Mon Sep 17 00:00:00 2001 From: Kallinteris Andreas <30759571+Kallinteris-Andreas@users.noreply.github.com> Date: Tue, 9 May 2023 12:13:49 +0300 Subject: [PATCH 02/29] Delete hopper_v5.xml --- gymnasium/envs/mujoco/assets/hopper_v5.xml | 53 ---------------------- 1 file changed, 53 deletions(-) delete mode 100644 gymnasium/envs/mujoco/assets/hopper_v5.xml diff --git a/gymnasium/envs/mujoco/assets/hopper_v5.xml b/gymnasium/envs/mujoco/assets/hopper_v5.xml deleted file mode 100644 index e9ec942d7..000000000 --- a/gymnasium/envs/mujoco/assets/hopper_v5.xml +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - - - From db3734ea0c23698724e05b1f72042b7dca9b8b7b Mon Sep 17 00:00:00 2001 From: Kallinteris Andreas <30759571+Kallinteris-Andreas@users.noreply.github.com> Date: Tue, 9 May 2023 12:14:07 +0300 Subject: [PATCH 03/29] Delete walker2d_v5.xml --- gymnasium/envs/mujoco/assets/walker2d_v5.xml | 68 -------------------- 1 file changed, 68 deletions(-) delete mode 100644 gymnasium/envs/mujoco/assets/walker2d_v5.xml diff --git a/gymnasium/envs/mujoco/assets/walker2d_v5.xml b/gymnasium/envs/mujoco/assets/walker2d_v5.xml deleted file mode 100644 index cf8042a7c..000000000 --- a/gymnasium/envs/mujoco/assets/walker2d_v5.xml +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - - - From a2d2e64a4595a046ab2b826bb2de3d8f82f9b4bf Mon Sep 17 00:00:00 2001 From: Kallinteris Andreas Date: Tue, 9 May 2023 16:19:18 +0300 Subject: [PATCH 04/29] General MuJoCo Env Documention Cleanup --- gymnasium/envs/mujoco/ant_v4.py | 1 - gymnasium/envs/mujoco/half_cheetah_v4.py | 17 +- gymnasium/envs/mujoco/hopper_v4.py | 18 +- gymnasium/envs/mujoco/humanoid_v4.py | 21 +- gymnasium/envs/mujoco/humanoidstandup_v4.py | 93 +++++-- gymnasium/envs/mujoco/pusher_v4.py | 2 +- gymnasium/envs/mujoco/reacher_v4.py | 7 +- gymnasium/envs/mujoco/swimmer_v4.py | 7 +- gymnasium/envs/mujoco/walker2d_v4.py | 257 ++++++++++---------- 9 files changed, 237 insertions(+), 186 deletions(-) diff --git a/gymnasium/envs/mujoco/ant_v4.py b/gymnasium/envs/mujoco/ant_v4.py index b95b3aa51..9775aad59 100644 --- a/gymnasium/envs/mujoco/ant_v4.py +++ b/gymnasium/envs/mujoco/ant_v4.py @@ -38,7 +38,6 @@ class AntEnv(MujocoEnv, utils.EzPickle): | 7 | Torque applied on the rotor between the back left two links | -1 | 1 | angle_3 (back_leg) | hinge | torque (N m) | ## Observation Space - Observations consist of positional values of different body parts of the ant, followed by the velocities of those individual parts (their derivatives) with all the positions ordered before all the velocities. diff --git a/gymnasium/envs/mujoco/half_cheetah_v4.py b/gymnasium/envs/mujoco/half_cheetah_v4.py index b0286ec0d..13ba74669 100644 --- a/gymnasium/envs/mujoco/half_cheetah_v4.py +++ b/gymnasium/envs/mujoco/half_cheetah_v4.py @@ -18,7 +18,7 @@ class HalfCheetahEnv(MujocoEnv, utils.EzPickle): This environment is based on the work by P. Wawrzyński in ["A Cat-Like Robot Real-Time Learning to Run"](http://staff.elka.pw.edu.pl/~pwawrzyn/pub-s/0812_LSCLRR.pdf). - The HalfCheetah is a 2-dimensional robot consisting of 9 links and 8 + The HalfCheetah is a 2-dimensional robot consisting of 9 body parts and 8 joints connecting them (including two paws). The goal is to apply a torque on the joints to make the cheetah run forward (right) as fast as possible, with a positive reward allocated based on the distance moved forward and a @@ -28,7 +28,7 @@ class HalfCheetahEnv(MujocoEnv, utils.EzPickle): (connecting to the thighs) and feet (connecting to the shins). ## Action Space - The action space is a `Box(-1, 1, (6,), float32)`. An action represents the torques applied between *links*. + The action space is a `Box(-1, 1, (6,), float32)`. An action represents the torques applied at the hinge joints. | Num | Action | Control Min | Control Max | Name (in corresponding XML file) | Joint | Unit | | --- | --------------------------------------- | ----------- | ----------- | -------------------------------- | ----- | ------------ | @@ -41,22 +41,21 @@ class HalfCheetahEnv(MujocoEnv, utils.EzPickle): ## Observation Space - Observations consist of positional values of different body parts of the cheetah, followed by the velocities of those individual parts (their derivatives) with all the positions ordered before all the velocities. - By default, observations do not include the x-coordinate of the cheetah's center of mass. It may + By default, observations do not include the cheetah's `rootx`. It may be included by passing `exclude_current_positions_from_observation=False` during construction. - In that case, the observation space will have 18 dimensions where the first dimension - represents the x-coordinate of the cheetah's center of mass. - Regardless of whether `exclude_current_positions_from_observation` was set to true or false, the x-coordinate + In that case, the observation will be a `Box(-Inf, Inf, (18,), float64)` where the first element + represents the `rootx`/ + Regardless of whether `exclude_current_positions_from_observation` was set to true or false, the will be returned in `info` with key `"x_position"`. - However, by default, the observation is a `ndarray` with shape `(17,)` where the elements correspond to the following: - + However, by default, the observation is a `Box(-Inf, Inf, (17,), float64)` where the elements correspond to the following: | Num | Observation | Min | Max | Name (in corresponding XML file) | Joint | Unit | | --- | ------------------------------------ | ---- | --- | -------------------------------- | ----- | ------------------------ | + | excluded | x-coordinate of the front tip | -Inf | Inf | rootx | slide | position (m) | | 0 | z-coordinate of the front tip | -Inf | Inf | rootz | slide | position (m) | | 1 | angle of the front tip | -Inf | Inf | rooty | hinge | angle (rad) | | 2 | angle of the second rotor | -Inf | Inf | bthigh | hinge | angle (rad) | diff --git a/gymnasium/envs/mujoco/hopper_v4.py b/gymnasium/envs/mujoco/hopper_v4.py index e4191beea..e399aa0c1 100644 --- a/gymnasium/envs/mujoco/hopper_v4.py +++ b/gymnasium/envs/mujoco/hopper_v4.py @@ -28,7 +28,7 @@ class HopperEnv(MujocoEnv, utils.EzPickle): connecting the four body parts. ## Action Space - The action space is a `Box(-1, 1, (3,), float32)`. An action represents the torques applied between *links* + The action space is a `Box(-1, 1, (3,), float32)`. An action represents the torques applied at the hinge joints. | Num | Action | Control Min | Control Max | Name (in corresponding XML file) | Joint | Unit | |-----|------------------------------------|-------------|-------------|----------------------------------|-------|--------------| @@ -37,31 +37,31 @@ class HopperEnv(MujocoEnv, utils.EzPickle): | 2 | Torque applied on the foot rotor | -1 | 1 | foot_joint | hinge | torque (N m) | ## Observation Space - Observations consist of positional values of different body parts of the hopper, followed by the velocities of those individual parts (their derivatives) with all the positions ordered before all the velocities. By default, observations do not include the x-coordinate of the hopper. It may be included by passing `exclude_current_positions_from_observation=False` during construction. - In that case, the observation space will have 12 dimensions where the first dimension + In that case, the observation space will be `Box(-Inf, Inf, (12,), float64)` where the first observation represents the x-coordinate of the hopper. Regardless of whether `exclude_current_positions_from_observation` was set to true or false, the x-coordinate will be returned in `info` with key `"x_position"`. - However, by default, the observation is a `ndarray` with shape `(11,)` where the elements + However, by default, the observation is a `Box(-Inf, Inf, (11,), float64)` where the elements correspond to the following: | Num | Observation | Min | Max | Name (in corresponding XML file) | Joint | Unit | | --- | ------------------------------------------------ | ---- | --- | -------------------------------- | ----- | ------------------------ | - | 0 | z-coordinate of the top (height of hopper) | -Inf | Inf | rootz | slide | position (m) | - | 1 | angle of the top | -Inf | Inf | rooty | hinge | angle (rad) | + | excluded | x-coordinate of the torso | -Inf | Inf | rootx | slide | position (m) | + | 0 | z-coordinate of the torso (height of hopper) | -Inf | Inf | rootz | slide | position (m) | + | 1 | angle of the torso | -Inf | Inf | rooty | hinge | angle (rad) | | 2 | angle of the thigh joint | -Inf | Inf | thigh_joint | hinge | angle (rad) | | 3 | angle of the leg joint | -Inf | Inf | leg_joint | hinge | angle (rad) | | 4 | angle of the foot joint | -Inf | Inf | foot_joint | hinge | angle (rad) | - | 5 | velocity of the x-coordinate of the top | -Inf | Inf | rootx | slide | velocity (m/s) | - | 6 | velocity of the z-coordinate (height) of the top | -Inf | Inf | rootz | slide | velocity (m/s) | - | 7 | angular velocity of the angle of the top | -Inf | Inf | rooty | hinge | angular velocity (rad/s) | + | 5 | velocity of the x-coordinate of the torso | -Inf | Inf | rootx | slide | velocity (m/s) | + | 6 | velocity of the z-coordinate (height) of the torso | -Inf | Inf | rootz | slide | velocity (m/s) | + | 7 | angular velocity of the angle of the torso | -Inf | Inf | rooty | hinge | angular velocity (rad/s) | | 8 | angular velocity of the thigh hinge | -Inf | Inf | thigh_joint | hinge | angular velocity (rad/s) | | 9 | angular velocity of the leg hinge | -Inf | Inf | leg_joint | hinge | angular velocity (rad/s) | | 10 | angular velocity of the foot hinge | -Inf | Inf | foot_joint | hinge | angular velocity (rad/s) | diff --git a/gymnasium/envs/mujoco/humanoid_v4.py b/gymnasium/envs/mujoco/humanoid_v4.py index bc6ecddd7..341ac6c28 100644 --- a/gymnasium/envs/mujoco/humanoid_v4.py +++ b/gymnasium/envs/mujoco/humanoid_v4.py @@ -44,31 +44,32 @@ class HumanoidEnv(MujocoEnv, utils.EzPickle): | 7 | Torque applied on the rotor between torso/abdomen and the left hip (x-coordinate) | -0.4 | 0.4 | left_hip_x (left_thigh) | hinge | torque (N m) | | 8 | Torque applied on the rotor between torso/abdomen and the left hip (z-coordinate) | -0.4 | 0.4 | left_hip_z (left_thigh) | hinge | torque (N m) | | 9 | Torque applied on the rotor between torso/abdomen and the left hip (y-coordinate) | -0.4 | 0.4 | left_hip_y (left_thigh) | hinge | torque (N m) | - | 10 | Torque applied on the rotor between the left hip/thigh and the left shin | -0.4 | 0.4 | left_knee | hinge | torque (N m) | - | 11 | Torque applied on the rotor between the torso and right upper arm (coordinate -1) | -0.4 | 0.4 | right_shoulder1 | hinge | torque (N m) | - | 12 | Torque applied on the rotor between the torso and right upper arm (coordinate -2) | -0.4 | 0.4 | right_shoulder2 | hinge | torque (N m) | - | 13 | Torque applied on the rotor between the right upper arm and right lower arm | -0.4 | 0.4 | right_elbow | hinge | torque (N m) | - | 14 | Torque applied on the rotor between the torso and left upper arm (coordinate -1) | -0.4 | 0.4 | left_shoulder1 | hinge | torque (N m) | - | 15 | Torque applied on the rotor between the torso and left upper arm (coordinate -2) | -0.4 | 0.4 | left_shoulder2 | hinge | torque (N m) | - | 16 | Torque applied on the rotor between the left upper arm and left lower arm | -0.4 | 0.4 | left_elbow | hinge | torque (N m) | + | 10 | Torque applied on the rotor between the left hip/thigh and the left shin | -0.4 | 0.4 | left_knee | hinge | torque (N m) | + | 11 | Torque applied on the rotor between the torso and right upper arm (coordinate -1) | -0.4 | 0.4 | right_shoulder1 | hinge | torque (N m) | + | 12 | Torque applied on the rotor between the torso and right upper arm (coordinate -2) | -0.4 | 0.4 | right_shoulder2 | hinge | torque (N m) | + | 13 | Torque applied on the rotor between the right upper arm and right lower arm | -0.4 | 0.4 | right_elbow | hinge | torque (N m) | + | 14 | Torque applied on the rotor between the torso and left upper arm (coordinate -1) | -0.4 | 0.4 | left_shoulder1 | hinge | torque (N m) | + | 15 | Torque applied on the rotor between the torso and left upper arm (coordinate -2) | -0.4 | 0.4 | left_shoulder2 | hinge | torque (N m) | + | 16 | Torque applied on the rotor between the left upper arm and left lower arm | -0.4 | 0.4 | left_elbow | hinge | torque (N m) | ## Observation Space - Observations consist of positional values of different body parts of the Humanoid, followed by the velocities of those individual parts (their derivatives) with all the positions ordered before all the velocities. By default, observations do not include the x- and y-coordinates of the torso. These may be included by passing `exclude_current_positions_from_observation=False` during construction. - In that case, the observation space will be a `Box(-1, 1, (378,), float64)` where the first two observations + In that case, the observation space will be a `Box(-Inf, Inf, (378,), float64)` where the first two observations represent the x- and y-coordinates of the torso. Regardless of whether `exclude_current_positions_from_observation` was set to true or false, the x- and y-coordinates will be returned in `info` with keys `"x_position"` and `"y_position"`, respectively. - However, by default, the observation is a `Box(-1, 1, (376,), float64)`. The elements correspond to the following: + However, by default, the observation is a `Box(-Inf, Inf, (376,), float64)`. The elements correspond to the following: | Num | Observation | Min | Max | Name (in corresponding XML file) | Joint | Unit | | --- | --------------------------------------------------------------------------------------------------------------- | ---- | --- | -------------------------------- | ----- | -------------------------- | + | excluded | x-coordinate of the torso (centre) | -Inf | Inf | root | free | position (m) | + | excluded | y-coordinate of the torso (centre) | -Inf | Inf | root | free | position (m) | | 0 | z-coordinate of the torso (centre) | -Inf | Inf | root | free | position (m) | | 1 | x-orientation of the torso (centre) | -Inf | Inf | root | free | angle (rad) | | 2 | y-orientation of the torso (centre) | -Inf | Inf | root | free | angle (rad) | diff --git a/gymnasium/envs/mujoco/humanoidstandup_v4.py b/gymnasium/envs/mujoco/humanoidstandup_v4.py index 2cee9058d..797c37ecd 100644 --- a/gymnasium/envs/mujoco/humanoidstandup_v4.py +++ b/gymnasium/envs/mujoco/humanoidstandup_v4.py @@ -52,16 +52,23 @@ class HumanoidStandupEnv(MujocoEnv, utils.EzPickle): | 16 | Torque applied on the rotor between the left upper arm and left lower arm | -0.4 | 0.4 | left_elbow | hinge | torque (N m) | ## Observation Space + Observations consist of positional values of different body parts of the Humanoid, + followed by the velocities of those individual parts (their derivatives) with all the + positions ordered before all the velocities. - The state space consists of positional values of different body parts of the Humanoid, - followed by the velocities of those individual parts (their derivatives) with all the positions ordered before all the velocities. + By default, observations do not include the x- and y-coordinates of the torso. These may + be included by passing `exclude_current_positions_from_observation=False` during construction. + In that case, the observation space will be a `Box(-Inf, Inf, (378,), float64)` where the first two observations + represent the x- and y-coordinates of the torso. + Regardless of whether `exclude_current_positions_from_observation` was set to true or false, the x- and y-coordinates + will be returned in `info` with keys `"x_position"` and `"y_position"`, respectively. - **Note:** The x- and y-coordinates of the torso are being omitted to produce position-agnostic behavior in policies - - The observation is a `ndarray` with shape `(376,)` where the elements correspond to the following: + However, by default, the observation is a `Box(-Inf, Inf, (376,), float64)`. The elements correspond to the following: | Num | Observation | Min | Max | Name (in corresponding XML file) | Joint | Unit | | --- | --------------------------------------------------------------------------------------------------------------- | ---- | --- | -------------------------------- | ----- | -------------------------- | + | excluded | x-coordinate of the torso (centre) | -Inf | Inf | root | free | position (m) | + | excluded | y-coordinate of the torso (centre) | -Inf | Inf | root | free | position (m) | | 0 | z-coordinate of the torso (centre) | -Inf | Inf | root | free | position (m) | | 1 | x-orientation of the torso (centre) | -Inf | Inf | root | free | angle (rad) | | 2 | y-orientation of the torso (centre) | -Inf | Inf | root | free | angle (rad) | @@ -96,21 +103,20 @@ class HumanoidStandupEnv(MujocoEnv, utils.EzPickle): | 31 | x-coordinate of the angular velocity of the angle between pelvis and right hip (in right_thigh) | -Inf | Inf | right_hip_x | hinge | anglular velocity (rad/s) | | 32 | z-coordinate of the angular velocity of the angle between pelvis and right hip (in right_thigh) | -Inf | Inf | right_hip_z | hinge | anglular velocity (rad/s) | | 33 | y-coordinate of the angular velocity of the angle between pelvis and right hip (in right_thigh) | -Inf | Inf | right_hip_y | hinge | anglular velocity (rad/s) | - | 35 | angular velocity of the angle between right hip and the right shin (in right_knee) | -Inf | Inf | right_knee | hinge | anglular velocity (rad/s) | - | 36 | x-coordinate of the angular velocity of the angle between pelvis and left hip (in left_thigh) | -Inf | Inf | left_hip_x | hinge | anglular velocity (rad/s) | - | 37 | z-coordinate of the angular velocity of the angle between pelvis and left hip (in left_thigh) | -Inf | Inf | left_hip_z | hinge | anglular velocity (rad/s) | - | 38 | y-coordinate of the angular velocity of the angle between pelvis and left hip (in left_thigh) | -Inf | Inf | left_hip_y | hinge | anglular velocity (rad/s) | - | 39 | angular velocity of the angle between left hip and the left shin (in left_knee) | -Inf | Inf | left_knee | hinge | anglular velocity (rad/s) | - | 40 | coordinate-1 (multi-axis) of the angular velocity of the angle between torso and right arm (in right_upper_arm) | -Inf | Inf | right_shoulder1 | hinge | anglular velocity (rad/s) | - | 41 | coordinate-2 (multi-axis) of the angular velocity of the angle between torso and right arm (in right_upper_arm) | -Inf | Inf | right_shoulder2 | hinge | anglular velocity (rad/s) | - | 42 | angular velocity of the angle between right upper arm and right_lower_arm | -Inf | Inf | right_elbow | hinge | anglular velocity (rad/s) | - | 43 | coordinate-1 (multi-axis) of the angular velocity of the angle between torso and left arm (in left_upper_arm) | -Inf | Inf | left_shoulder1 | hinge | anglular velocity (rad/s) | - | 44 | coordinate-2 (multi-axis) of the angular velocity of the angle between torso and left arm (in left_upper_arm) | -Inf | Inf | left_shoulder2 | hinge | anglular velocity (rad/s) | - | 45 | angular velocity of the angle between left upper arm and left_lower_arm | -Inf | Inf | left_elbow | hinge | anglular velocity (rad/s) | - + | 34 | angular velocity of the angle between right hip and the right shin (in right_knee) | -Inf | Inf | right_knee | hinge | anglular velocity (rad/s) | + | 35 | x-coordinate of the angular velocity of the angle between pelvis and left hip (in left_thigh) | -Inf | Inf | left_hip_x | hinge | anglular velocity (rad/s) | + | 36 | z-coordinate of the angular velocity of the angle between pelvis and left hip (in left_thigh) | -Inf | Inf | left_hip_z | hinge | anglular velocity (rad/s) | + | 37 | y-coordinate of the angular velocity of the angle between pelvis and left hip (in left_thigh) | -Inf | Inf | left_hip_y | hinge | anglular velocity (rad/s) | + | 38 | angular velocity of the angle between left hip and the left shin (in left_knee) | -Inf | Inf | left_knee | hinge | anglular velocity (rad/s) | + | 39 | coordinate-1 (multi-axis) of the angular velocity of the angle between torso and right arm (in right_upper_arm) | -Inf | Inf | right_shoulder1 | hinge | anglular velocity (rad/s) | + | 40 | coordinate-2 (multi-axis) of the angular velocity of the angle between torso and right arm (in right_upper_arm) | -Inf | Inf | right_shoulder2 | hinge | anglular velocity (rad/s) | + | 41 | angular velocity of the angle between right upper arm and right_lower_arm | -Inf | Inf | right_elbow | hinge | anglular velocity (rad/s) | + | 42 | coordinate-1 (multi-axis) of the angular velocity of the angle between torso and left arm (in left_upper_arm) | -Inf | Inf | left_shoulder1 | hinge | anglular velocity (rad/s) | + | 43 | coordinate-2 (multi-axis) of the angular velocity of the angle between torso and left arm (in left_upper_arm) | -Inf | Inf | left_shoulder2 | hinge | anglular velocity (rad/s) | + | 44 | angular velocity of the angle between left upper arm and left_lower_arm | -Inf | Inf | left_elbow | hinge | anglular velocity (rad/s) | Additionally, after all the positional and velocity based values in the table, - the state_space consists of (in order): + the observation contains (in order): - *cinert:* Mass and inertia of a single rigid body relative to the center of mass (this is an intermediate result of transition). It has shape 14*10 (*nbody * 10*) and hence adds to another 140 elements in the state space. @@ -120,8 +126,55 @@ class HumanoidStandupEnv(MujocoEnv, utils.EzPickle): `(23,)` *(nv * 1)* and hence adds another 23 elements to the state space. - *cfrc_ext:* This is the center of mass based external force on the body. It has shape 14 * 6 (*nbody * 6*) and hence adds to another 84 elements in the state space. - where *nbody* stands for the number of bodies in the robot and *nv* stands for the number - of degrees of freedom (*= dim(qvel)*) + where *nbody* stands for the number of bodies in the robot and *nv* stands for the + number of degrees of freedom (*= dim(qvel)*) + + The body parts are: + + | id (for `v2`,`v3`,`v4`) | body part | + | --- | ------------ | + | 0 | worldBody (note: all values are constant 0) | + | 1 | torso | + | 2 | lwaist | + | 3 | pelvis | + | 4 | right_thigh | + | 5 | right_sin | + | 6 | right_foot | + | 7 | left_thigh | + | 8 | left_sin | + | 9 | left_foot | + | 10 | right_upper_arm | + | 11 | right_lower_arm | + | 12 | left_upper_arm | + | 13 | left_lower_arm | + + The joints are: + + | id (for `v2`,`v3`,`v4`) | joint | + | --- | ------------ | + | 0 | root | + | 1 | root | + | 2 | root | + | 3 | root | + | 4 | root | + | 5 | root | + | 6 | abdomen_z | + | 7 | abdomen_y | + | 8 | abdomen_x | + | 9 | right_hip_x | + | 10 | right_hip_z | + | 11 | right_hip_y | + | 12 | right_knee | + | 13 | left_hip_x | + | 14 | left_hiz_z | + | 15 | left_hip_y | + | 16 | left_knee | + | 17 | right_shoulder1 | + | 18 | right_shoulder2 | + | 19 | right_elbow| + | 20 | left_shoulder1 | + | 21 | left_shoulder2 | + | 22 | left_elfbow | The (x,y,z) coordinates are translational DOFs while the orientations are rotational DOFs expressed as quaternions. One can read more about free joints on the diff --git a/gymnasium/envs/mujoco/pusher_v4.py b/gymnasium/envs/mujoco/pusher_v4.py index a2929cd1e..55ba517e0 100644 --- a/gymnasium/envs/mujoco/pusher_v4.py +++ b/gymnasium/envs/mujoco/pusher_v4.py @@ -41,7 +41,7 @@ class PusherEnv(MujocoEnv, utils.EzPickle): - The coordinates of the object to be moved - The coordinates of the goal position - The observation is a `ndarray` with shape `(23,)` where the elements correspond to the table below. + The observation is a `Box(-Inf, Inf, (23,), float64)` where the elements correspond to the table below. An analogy can be drawn to a human arm in order to help understand the state space, with the words flex and roll meaning the same as human joints. diff --git a/gymnasium/envs/mujoco/reacher_v4.py b/gymnasium/envs/mujoco/reacher_v4.py index b701cff3f..49c6d4892 100644 --- a/gymnasium/envs/mujoco/reacher_v4.py +++ b/gymnasium/envs/mujoco/reacher_v4.py @@ -23,7 +23,6 @@ class ReacherEnv(MujocoEnv, utils.EzPickle): | 1 | Torque applied at the second hinge (connecting the two links) | -1 | 1 | joint1 | hinge | torque (N m) | ## Observation Space - Observations consist of - The cosine of the angles of the two arms @@ -32,7 +31,7 @@ class ReacherEnv(MujocoEnv, utils.EzPickle): - The angular velocities of the arms - The vector between the target and the reacher's fingertip (3 dimensional with the last element being 0) - The observation is a `ndarray` with shape `(11,)` where the elements correspond to the following: + The observation is a `Box(-Inf, Inf, (11,), float64)` where the elements correspond to the following: | Num | Observation | Min | Max | Name (in corresponding XML file) | Joint | Unit | | --- | ---------------------------------------------------------------------------------------------- | ---- | --- | -------------------------------- | ----- | ------------------------ | @@ -40,8 +39,8 @@ class ReacherEnv(MujocoEnv, utils.EzPickle): | 1 | cosine of the angle of the second arm | -Inf | Inf | cos(joint1) | hinge | unitless | | 2 | sine of the angle of the first arm | -Inf | Inf | sin(joint0) | hinge | unitless | | 3 | sine of the angle of the second arm | -Inf | Inf | sin(joint1) | hinge | unitless | - | 4 | x-coordinate of the target | -Inf | Inf | target_x | slide | position (m) | - | 5 | y-coordinate of the target | -Inf | Inf | target_y | slide | position (m) | + | 4 | x-coordinate of the target | -Inf | Inf | target_x | slide | position (m) | + | 5 | y-coordinate of the target | -Inf | Inf | target_y | slide | position (m) | | 6 | angular velocity of the first arm | -Inf | Inf | joint0 | hinge | angular velocity (rad/s) | | 7 | angular velocity of the second arm | -Inf | Inf | joint1 | hinge | angular velocity (rad/s) | | 8 | x-value of position_fingertip - position_target | -Inf | Inf | NA | slide | position (m) | diff --git a/gymnasium/envs/mujoco/swimmer_v4.py b/gymnasium/envs/mujoco/swimmer_v4.py index e2fe8b2c8..a936ca71a 100644 --- a/gymnasium/envs/mujoco/swimmer_v4.py +++ b/gymnasium/envs/mujoco/swimmer_v4.py @@ -44,19 +44,18 @@ class SwimmerEnv(MujocoEnv, utils.EzPickle): | 1 | Torque applied on the second rotor | -1 | 1 | motor2_rot | hinge | torque (N m) | ## Observation Space - By default, observations consists of: * θi: angle of part *i* with respect to the *x* axis * θi': its derivative with respect to time (angular velocity) In the default case, observations do not include the x- and y-coordinates of the front tip. These may be included by passing `exclude_current_positions_from_observation=False` during construction. - Then, the observation space will have 10 dimensions where the first two dimensions + Then, the observation space will be `Box(-Inf, Inf, (10,), float64)` where the first two observations represent the x- and y-coordinates of the front tip. Regardless of whether `exclude_current_positions_from_observation` was set to true or false, the x- and y-coordinates will be returned in `info` with keys `"x_position"` and `"y_position"`, respectively. - By default, the observation is a `ndarray` with shape `(8,)` where the elements correspond to the following: + By default, the observation is a `Box(-Inf, Inf, (8,), float64)` where the elements correspond to the following: | Num | Observation | Min | Max | Name (in corresponding XML file) | Joint | Unit | | --- | ------------------------------------ | ---- | --- | -------------------------------- | ----- | ------------------------ | @@ -67,7 +66,7 @@ class SwimmerEnv(MujocoEnv, utils.EzPickle): | 4 | velocity of the tip along the y-axis | -Inf | Inf | slider2 | slide | velocity (m/s) | | 5 | angular velocity of front tip | -Inf | Inf | free_body_rot | hinge | angular velocity (rad/s) | | 6 | angular velocity of first rotor | -Inf | Inf | motor1_rot | hinge | angular velocity (rad/s) | - | 7 | angular velocity of second rotor | -Inf | Inf | motor2_rot | hinge | angular velocity (rad/s) | + | 7 | angular velocity of second rotor | -Inf | Inf | motor2_rot | hinge | angular velocity (rad/s) | ## Rewards The reward consists of two parts: diff --git a/gymnasium/envs/mujoco/walker2d_v4.py b/gymnasium/envs/mujoco/walker2d_v4.py index 83756b190..fd2b14f28 100644 --- a/gymnasium/envs/mujoco/walker2d_v4.py +++ b/gymnasium/envs/mujoco/walker2d_v4.py @@ -15,134 +15,135 @@ class Walker2dEnv(MujocoEnv, utils.EzPickle): """ - ## Description - - This environment builds on the [hopper](https://gymnasium.farama.org/environments/mujoco/hopper/) environment - by adding another set of legs making it possible for the robot to walk forward instead of - hop. Like other Mujoco environments, this environment aims to increase the number of independent state - and control variables as compared to the classic control environments. The walker is a - two-dimensional two-legged figure that consist of seven main body parts - a single torso at the top - (with the two legs splitting after the torso), two thighs in the middle below the torso, two legs - in the bottom below the thighs, and two feet attached to the legs on which the entire body rests. - The goal is to walk in the in the forward (right) - direction by applying torques on the six hinges connecting the seven body parts. - - ## Action Space - The action space is a `Box(-1, 1, (6,), float32)`. An action represents the torques applied at the hinge joints. - - | Num | Action | Control Min | Control Max | Name (in corresponding XML file) | Joint | Unit | - |-----|----------------------------------------|-------------|-------------|----------------------------------|-------|--------------| - | 0 | Torque applied on the thigh rotor | -1 | 1 | thigh_joint | hinge | torque (N m) | - | 1 | Torque applied on the leg rotor | -1 | 1 | leg_joint | hinge | torque (N m) | - | 2 | Torque applied on the foot rotor | -1 | 1 | foot_joint | hinge | torque (N m) | - | 3 | Torque applied on the left thigh rotor | -1 | 1 | thigh_left_joint | hinge | torque (N m) | - | 4 | Torque applied on the left leg rotor | -1 | 1 | leg_left_joint | hinge | torque (N m) | - | 5 | Torque applied on the left foot rotor | -1 | 1 | foot_left_joint | hinge | torque (N m) | - - ## Observation Space - Observations consist of positional values of different body parts of the walker, - followed by the velocities of those individual parts (their derivatives) with all the positions ordered before all the velocities. - - By default, observations do not include the x-coordinate of the top. It may - be included by passing `exclude_current_positions_from_observation=False` during construction. - In that case, the observation space will have 18 dimensions where the first dimension - represent the x-coordinates of the top of the walker. - Regardless of whether `exclude_current_positions_from_observation` was set to true or false, the x-coordinate - of the top will be returned in `info` with key `"x_position"`. - - By default, observation is a `ndarray` with shape `(17,)` where the elements correspond to the following: - - | Num | Observation | Min | Max | Name (in corresponding XML file) | Joint | Unit | - | --- | -------------------------------------------------- | ---- | --- | -------------------------------- | ----- | ------------------------ | - | 0 | z-coordinate of the torso (height of hopper) | -Inf | Inf | rootz | slide | position (m) | - | 1 | angle of the torso | -Inf | Inf | rooty | hinge | angle (rad) | - | 2 | angle of the thigh joint | -Inf | Inf | thigh_joint | hinge | angle (rad) | - | 3 | angle of the leg joint | -Inf | Inf | leg_joint | hinge | angle (rad) | - | 4 | angle of the foot joint | -Inf | Inf | foot_joint | hinge | angle (rad) | - | 5 | angle of the left thigh joint | -Inf | Inf | thigh_left_joint | hinge | angle (rad) | - | 6 | angle of the left leg joint | -Inf | Inf | leg_left_joint | hinge | angle (rad) | - | 7 | angle of the left foot joint | -Inf | Inf | foot_left_joint | hinge | angle (rad) | - | 8 | velocity of the x-coordinate of the torso | -Inf | Inf | rootx | slide | velocity (m/s) | - | 9 | velocity of the z-coordinate (height) of the rorso | -Inf | Inf | rootz | slide | velocity (m/s) | - | 10 | angular velocity of the angle of the top | -Inf | Inf | rooty | hinge | angular velocity (rad/s) | - | 11 | angular velocity of the thigh hinge | -Inf | Inf | thigh_joint | hinge | angular velocity (rad/s) | - | 12 | angular velocity of the leg hinge | -Inf | Inf | leg_joint | hinge | angular velocity (rad/s) | - | 13 | angular velocity of the foot hinge | -Inf | Inf | foot_joint | hinge | angular velocity (rad/s) | - | 14 | angular velocity of the thigh hinge | -Inf | Inf | thigh_left_joint | hinge | angular velocity (rad/s) | - | 15 | angular velocity of the leg hinge | -Inf | Inf | leg_left_joint | hinge | angular velocity (rad/s) | - | 16 | angular velocity of the foot hinge | -Inf | Inf | foot_left_joint | hinge | angular velocity (rad/s) | - - ## Rewards - The reward consists of three parts: - - *healthy_reward*: Every timestep that the walker is alive, it receives a fixed reward of value `healthy_reward`, - - *forward_reward*: A reward of walking forward which is measured as - *`forward_reward_weight` * (x-coordinate before action - x-coordinate after action)/dt*. - *dt* is the time between actions and is dependeent on the frame_skip parameter - (default is 4), where the frametime is 0.002 - making the default - *dt = 4 * 0.002 = 0.008*. This reward would be positive if the walker walks forward (positive x direction). - - *ctrl_cost*: A cost for penalising the walker if it - takes actions that are too large. It is measured as - *`ctrl_cost_weight` * sum(action2)* where *`ctrl_cost_weight`* is - a parameter set for the control and has a default value of 0.001 - - The total reward returned is ***reward*** *=* *healthy_reward bonus + forward_reward - ctrl_cost* and `info` will also contain the individual reward terms - - ## Starting State - All observations start in state - (0.0, 1.25, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0) - with a uniform noise in the range of [-`reset_noise_scale`, `reset_noise_scale`] added to the values for stochasticity. - - ## Episode End - The walker is said to be unhealthy if any of the following happens: - - 1. Any of the state space values is no longer finite - 2. The height of the walker is ***not*** in the closed interval specified by `healthy_z_range` - 3. The absolute value of the angle (`observation[1]` if `exclude_current_positions_from_observation=False`, else `observation[2]`) is ***not*** in the closed interval specified by `healthy_angle_range` - - If `terminate_when_unhealthy=True` is passed during construction (which is the default), - the episode ends when any of the following happens: - - 1. Truncation: The episode duration reaches a 1000 timesteps - 2. Termination: The walker is unhealthy - - If `terminate_when_unhealthy=False` is passed, the episode is ended only when 1000 timesteps are exceeded. - - ## Arguments - - No additional arguments are currently supported in v2 and lower. - - ```python - import gymnasium as gym - env = gym.make('Walker2d-v4') - ``` - - v3 and beyond take `gymnasium.make` kwargs such as `xml_file`, `ctrl_cost_weight`, `reset_noise_scale`, etc. - - ```python - import gymnasium as gym - env = gym.make('Walker2d-v4', ctrl_cost_weight=0.1, ....) - ``` - - | Parameter | Type | Default | Description | - | -------------------------------------------- | --------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | - | `xml_file` | **str** | `"walker2d.xml"` | Path to a MuJoCo model | - | `forward_reward_weight` | **float** | `1.0` | Weight for _forward_reward_ term (see section on reward) | - | `ctrl_cost_weight` | **float** | `1e-3` | Weight for _ctr_cost_ term (see section on reward) | - | `healthy_reward` | **float** | `1.0` | Constant reward given if the ant is "healthy" after timestep | - | `terminate_when_unhealthy` | **bool** | `True` | If true, issue a done signal if the z-coordinate of the walker is no longer healthy | - | `healthy_z_range` | **tuple** | `(0.8, 2)` | The z-coordinate of the top of the walker must be in this range to be considered healthy | - | `healthy_angle_range` | **tuple** | `(-1, 1)` | The angle must be in this range to be considered healthy | - | `reset_noise_scale` | **float** | `5e-3` | Scale of random perturbations of initial position and velocity (see section on Starting State) | - | `exclude_current_positions_from_observation` | **bool** | `True` | Whether or not to omit the x-coordinate from observations. Excluding the position can serve as an inductive bias to induce position-agnostic behavior in policies | - - - ## Version History - - * v4: All MuJoCo environments now use the MuJoCo bindings in mujoco >= 2.1.3 - * v3: Support for `gymnasium.make` kwargs such as `xml_file`, `ctrl_cost_weight`, `reset_noise_scale`, etc. rgb rendering comes from tracking camera (so agent does not run away from screen) - * v2: All continuous control environments now use mujoco-py >= 1.50 - * v1: max_time_steps raised to 1000 for robot based tasks. Added reward_threshold to environments. - * v0: Initial versions release (1.0.0) + ## Description + + This environment builds on the [hopper](https://gymnasium.farama.org/environments/mujoco/hopper/) environment + by adding another set of legs making it possible for the robot to walk forward instead of + hop. Like other Mujoco environments, this environment aims to increase the number of independent state + and control variables as compared to the classic control environments. The walker is a + two-dimensional two-legged figure that consist of seven main body parts - a single torso at the top + (with the two legs splitting after the torso), two thighs in the middle below the torso, two legs + in the bottom below the thighs, and two feet attached to the legs on which the entire body rests. + The goal is to walk in the in the forward (right) + direction by applying torques on the six hinges connecting the seven body parts. + + ## Action Space + The action space is a `Box(-1, 1, (6,), float32)`. An action represents the torques applied at the hinge joints. + + | Num | Action | Control Min | Control Max | Name (in corresponding XML file) | Joint | Unit | + |-----|----------------------------------------|-------------|-------------|----------------------------------|-------|--------------| + | 0 | Torque applied on the thigh rotor | -1 | 1 | thigh_joint | hinge | torque (N m) | + | 1 | Torque applied on the leg rotor | -1 | 1 | leg_joint | hinge | torque (N m) | + | 2 | Torque applied on the foot rotor | -1 | 1 | foot_joint | hinge | torque (N m) | + | 3 | Torque applied on the left thigh rotor | -1 | 1 | thigh_left_joint | hinge | torque (N m) | + | 4 | Torque applied on the left leg rotor | -1 | 1 | leg_left_joint | hinge | torque (N m) | + | 5 | Torque applied on the left foot rotor | -1 | 1 | foot_left_joint | hinge | torque (N m) | + + ## Observation Space + Observations consist of positional values of different body parts of the walker, + followed by the velocities of those individual parts (their derivatives) with all the positions ordered before all the velocities. + + By default, observations do not include the x-coordinate of the torso. It may + be included by passing `exclude_current_positions_from_observation=False` during construction. + In that case, the observation space will be `Box(-Inf, Inf, (18,), float64)` where the first observation + represent the x-coordinates of the torso of the walker. + Regardless of whether `exclude_current_positions_from_observation` was set to true or false, the x-coordinate + of the torso will be returned in `info` with key `"x_position"`. + dimension + By default, observation is a `Box(-Inf, Inf, (17,), float64)` where the elements correspond to the following: + + | Num | Observation | Min | Max | Name (in corresponding XML file) | Joint | Unit | + | --- | -------------------------------------------------- | ---- | --- | -------------------------------- | ----- | ------------------------ | + | excluded | x-coordinate of the torso | -Inf | Inf | rootx | slide | position (m) | + | 0 | z-coordinate of the torso (height of Walker2d) | -Inf | Inf | rootz | slide | position (m) | + | 1 | angle of the torso | -Inf | Inf | rooty | hinge | angle (rad) | + | 2 | angle of the thigh joint | -Inf | Inf | thigh_joint | hinge | angle (rad) | + | 3 | angle of the leg joint | -Inf | Inf | leg_joint | hinge | angle (rad) | + | 4 | angle of the foot joint | -Inf | Inf | foot_joint | hinge | angle (rad) | + | 5 | angle of the left thigh joint | -Inf | Inf | thigh_left_joint | hinge | angle (rad) | + | 6 | angle of the left leg joint | -Inf | Inf | leg_left_joint | hinge | angle (rad) | + | 7 | angle of the left foot joint | -Inf | Inf | foot_left_joint | hinge | angle (rad) | + | 8 | velocity of the x-coordinate of the torso | -Inf | Inf | rootx | slide | velocity (m/s) | + | 9 | velocity of the z-coordinate (height) of the torso | -Inf | Inf | rootz | slide | velocity (m/s) | + | 10 | angular velocity of the angle of the torso | -Inf | Inf | rooty | hinge | angular velocity (rad/s) | + | 11 | angular velocity of the thigh hinge | -Inf | Inf | thigh_joint | hinge | angular velocity (rad/s) | + | 12 | angular velocity of the leg hinge | -Inf | Inf | leg_joint | hinge | angular velocity (rad/s) | + | 13 | angular velocity of the foot hinge | -Inf | Inf | foot_joint | hinge | angular velocity (rad/s) | + | 14 | angular velocity of the thigh hinge | -Inf | Inf | thigh_left_joint | hinge | angular velocity (rad/s) | + | 15 | angular velocity of the leg hinge | -Inf | Inf | leg_left_joint | hinge | angular velocity (rad/s) | + | 16 | angular velocity of the foot hinge | -Inf | Inf | foot_left_joint | hinge | angular velocity (rad/s) | + + ## Rewards + The reward consists of three parts: + - *healthy_reward*: Every timestep that the walker is alive, it receives a fixed reward of value `healthy_reward`, + - *forward_reward*: A reward of walking forward which is measured as + *`forward_reward_weight` * (x-coordinate before action - x-coordinate after action)/dt*. + *dt* is the time between actions and is dependeent on the frame_skip parameter + (default is 4), where the frametime is 0.002 - making the default + *dt = 4 * 0.002 = 0.008*. This reward would be positive if the walker walks forward (positive x direction). + - *ctrl_cost*: A cost for penalising the walker if it + takes actions that are too large. It is measured as + *`ctrl_cost_weight` * sum(action2)* where *`ctrl_cost_weight`* is + a parameter set for the control and has a default value of 0.001 + + The total reward returned is ***reward*** *=* *healthy_reward bonus + forward_reward - ctrl_cost* and `info` will also contain the individual reward terms + + ## Starting State + All observations start in state + (0.0, 1.25, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0) + with a uniform noise in the range of [-`reset_noise_scale`, `reset_noise_scale`] added to the values for stochasticity. + + ## Episode End + The walker is said to be unhealthy if any of the following happens: + + 1. Any of the state space values is no longer finite + 2. The height of the walker is ***not*** in the closed interval specified by `healthy_z_range` + 3. The absolute value of the angle (`observation[1]` if `exclude_current_positions_from_observation=False`, else `observation[2]`) is ***not*** in the closed interval specified by `healthy_angle_range` + + If `terminate_when_unhealthy=True` is passed during construction (which is the default), + the episode ends when any of the following happens: + + 1. Truncation: The episode duration reaches a 1000 timesteps + 2. Termination: The walker is unhealthy + + If `terminate_when_unhealthy=False` is passed, the episode is ended only when 1000 timesteps are exceeded. + + ## Arguments + + No additional arguments are currently supported in v2 and lower. + + ```python + import gymnasium as gym + env = gym.make('Walker2d-v4') + ``` + + v3 and beyond take `gymnasium.make` kwargs such as `xml_file`, `ctrl_cost_weight`, `reset_noise_scale`, etc. + + ```python + import gymnasium as gym + env = gym.make('Walker2d-v4', ctrl_cost_weight=0.1, ....) + ``` + + | Parameter | Type | Default | Description | + | -------------------------------------------- | --------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | + | `xml_file` | **str** | `"walker2d.xml"` | Path to a MuJoCo model | + | `forward_reward_weight` | **float** | `1.0` | Weight for _forward_reward_ term (see section on reward) | + | `ctrl_cost_weight` | **float** | `1e-3` | Weight for _ctr_cost_ term (see section on reward) | + | `healthy_reward` | **float** | `1.0` | Constant reward given if the ant is "healthy" after timestep | + | `terminate_when_unhealthy` | **bool** | `True` | If true, issue a done signal if the z-coordinate of the walker is no longer healthy | + | `healthy_z_range` | **tuple** | `(0.8, 2)` | The z-coordinate of the torso of the walker must be in this range to be considered healthy | + | `healthy_angle_range` | **tuple** | `(-1, 1)` | The angle must be in this range to be considered healthy | + | `reset_noise_scale` | **float** | `5e-3` | Scale of random perturbations of initial position and velocity (see section on Starting State) | + | `exclude_current_positions_from_observation` | **bool** | `True` | Whether or not to omit the x-coordinate from observations. Excluding the position can serve as an inductive bias to induce position-agnostic behavior in policies | + + + ## Version History + + * v4: All MuJoCo environments now use the MuJoCo bindings in mujoco >= 2.1.3 + * v3: Support for `gymnasium.make` kwargs such as `xml_file`, `ctrl_cost_weight`, `reset_noise_scale`, etc. rgb rendering comes from tracking camera (so agent does not run away from screen) + * v2: All continuous control environments now use mujoco-py >= 1.50 + * v1: max_time_steps raised to 1000 for robot based tasks. Added reward_threshold to environments. + * v0: Initial versions release (1.0.0) """ metadata = { From f58bb5e9520d4e95b2c09a3b033dc4c3f6797d5f Mon Sep 17 00:00:00 2001 From: Kallinteris Andreas Date: Tue, 9 May 2023 16:21:00 +0300 Subject: [PATCH 05/29] typofix --- gymnasium/envs/mujoco/half_cheetah_v4.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gymnasium/envs/mujoco/half_cheetah_v4.py b/gymnasium/envs/mujoco/half_cheetah_v4.py index 13ba74669..940426f76 100644 --- a/gymnasium/envs/mujoco/half_cheetah_v4.py +++ b/gymnasium/envs/mujoco/half_cheetah_v4.py @@ -46,7 +46,7 @@ class HalfCheetahEnv(MujocoEnv, utils.EzPickle): By default, observations do not include the cheetah's `rootx`. It may be included by passing `exclude_current_positions_from_observation=False` during construction. - In that case, the observation will be a `Box(-Inf, Inf, (18,), float64)` where the first element + In that case, the observation space will be a `Box(-Inf, Inf, (18,), float64)` where the first element represents the `rootx`/ Regardless of whether `exclude_current_positions_from_observation` was set to true or false, the will be returned in `info` with key `"x_position"`. From 7a4bc32008e1865eaa286bbdfd1c44a929195163 Mon Sep 17 00:00:00 2001 From: Kallinteris Andreas Date: Tue, 9 May 2023 16:28:31 +0300 Subject: [PATCH 06/29] typo fix --- gymnasium/envs/mujoco/half_cheetah_v4.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gymnasium/envs/mujoco/half_cheetah_v4.py b/gymnasium/envs/mujoco/half_cheetah_v4.py index 940426f76..b6c59f72e 100644 --- a/gymnasium/envs/mujoco/half_cheetah_v4.py +++ b/gymnasium/envs/mujoco/half_cheetah_v4.py @@ -47,7 +47,7 @@ class HalfCheetahEnv(MujocoEnv, utils.EzPickle): By default, observations do not include the cheetah's `rootx`. It may be included by passing `exclude_current_positions_from_observation=False` during construction. In that case, the observation space will be a `Box(-Inf, Inf, (18,), float64)` where the first element - represents the `rootx`/ + represents the `rootx`. Regardless of whether `exclude_current_positions_from_observation` was set to true or false, the will be returned in `info` with key `"x_position"`. From 24186314c12362dcdd37778f2ca418705dce68de Mon Sep 17 00:00:00 2001 From: Kallinteris Andreas Date: Tue, 9 May 2023 20:14:25 +0300 Subject: [PATCH 07/29] update following @pseudo-rnd-thoughts reviews --- gymnasium/envs/mujoco/ant_v4.py | 24 +- gymnasium/envs/mujoco/half_cheetah_v4.py | 2 +- gymnasium/envs/mujoco/hopper_v4.py | 22 +- gymnasium/envs/mujoco/humanoid_v4.py | 86 +++---- gymnasium/envs/mujoco/humanoidstandup_v4.py | 86 +++---- gymnasium/envs/mujoco/swimmer_v4.py | 2 + gymnasium/envs/mujoco/walker2d_v4.py | 258 ++++++++++---------- 7 files changed, 241 insertions(+), 239 deletions(-) diff --git a/gymnasium/envs/mujoco/ant_v4.py b/gymnasium/envs/mujoco/ant_v4.py index 9775aad59..4bec72528 100644 --- a/gymnasium/envs/mujoco/ant_v4.py +++ b/gymnasium/envs/mujoco/ant_v4.py @@ -53,8 +53,6 @@ class AntEnv(MujocoEnv, utils.EzPickle): | Num | Observation | Min | Max | Name (in corresponding XML file) | Joint | Unit | |-----|--------------------------------------------------------------|--------|--------|----------------------------------------|-------|--------------------------| - | excluded | x-coordinate of the torso (centre) | -Inf | Inf | torso | free | position (m) | - | excluded | y-coordinate of the torso (centre) | -Inf | Inf | torso | free | position (m) | | 0 | z-coordinate of the torso (centre) | -Inf | Inf | torso | free | position (m) | | 1 | x-orientation of the torso (centre) | -Inf | Inf | torso | free | angle (rad) | | 2 | y-orientation of the torso (centre) | -Inf | Inf | torso | free | angle (rad) | @@ -82,6 +80,8 @@ class AntEnv(MujocoEnv, utils.EzPickle): | 24 | angular velocity of the angle between back left links | -Inf | Inf | ankle_3 (back_leg) | hinge | angle (rad) | | 25 | angular velocity of angle between torso and back right link | -Inf | Inf | hip_4 (right_back_leg) | hinge | angle (rad) | | 26 | angular velocity of the angle between back right links | -Inf | Inf | ankle_4 (right_back_leg) | hinge | angle (rad) | + | excluded | x-coordinate of the torso (centre) | -Inf | Inf | torso | free | position (m) | + | excluded | y-coordinate of the torso (centre) | -Inf | Inf | torso | free | position (m) | If version < `v4` or `use_contact_forces` is `True` then the observation space is extended by 14*6 = 84 elements, which are contact forces @@ -90,16 +90,16 @@ class AntEnv(MujocoEnv, utils.EzPickle): | id (for `v2`, `v3`, `v4)` | body parts | | --- | ------------ | - | 0 | worldBody (note: forces are always full of zeros) | - | 1 | torso | - | 2 | front_left_leg | - | 3 | aux_1 (front left leg) | - | 4 | ankle_1 (front left leg) | - | 5 | front_right_leg | - | 6 | aux_2 (front right leg) | - | 7 | ankle_2 (front right leg) | - | 8 | back_leg (back left leg) | - | 9 | aux_3 (back left leg) | + | 0 | worldbody (note: forces are always full of zeros) | + | 1 | torso | + | 2 | front_left_leg | + | 3 | aux_1 (front left leg) | + | 4 | ankle_1 (front left leg) | + | 5 | front_right_leg | + | 6 | aux_2 (front right leg) | + | 7 | ankle_2 (front right leg) | + | 8 | back_leg (back left leg) | + | 9 | aux_3 (back left leg) | | 10 | ankle_3 (back left leg) | | 11 | right_back_leg | | 12 | aux_4 (back right leg) | diff --git a/gymnasium/envs/mujoco/half_cheetah_v4.py b/gymnasium/envs/mujoco/half_cheetah_v4.py index b6c59f72e..f90d59065 100644 --- a/gymnasium/envs/mujoco/half_cheetah_v4.py +++ b/gymnasium/envs/mujoco/half_cheetah_v4.py @@ -55,7 +55,6 @@ class HalfCheetahEnv(MujocoEnv, utils.EzPickle): | Num | Observation | Min | Max | Name (in corresponding XML file) | Joint | Unit | | --- | ------------------------------------ | ---- | --- | -------------------------------- | ----- | ------------------------ | - | excluded | x-coordinate of the front tip | -Inf | Inf | rootx | slide | position (m) | | 0 | z-coordinate of the front tip | -Inf | Inf | rootz | slide | position (m) | | 1 | angle of the front tip | -Inf | Inf | rooty | hinge | angle (rad) | | 2 | angle of the second rotor | -Inf | Inf | bthigh | hinge | angle (rad) | @@ -73,6 +72,7 @@ class HalfCheetahEnv(MujocoEnv, utils.EzPickle): | 14 | velocity of the tip along the y-axis | -Inf | Inf | fthigh | hinge | angular velocity (rad/s) | | 15 | angular velocity of front tip | -Inf | Inf | fshin | hinge | angular velocity (rad/s) | | 16 | angular velocity of second rotor | -Inf | Inf | ffoot | hinge | angular velocity (rad/s) | + | excluded | x-coordinate of the front tip | -Inf | Inf | rootx | slide | position (m) | ## Rewards The reward consists of two parts: diff --git a/gymnasium/envs/mujoco/hopper_v4.py b/gymnasium/envs/mujoco/hopper_v4.py index e399aa0c1..ef6cc0741 100644 --- a/gymnasium/envs/mujoco/hopper_v4.py +++ b/gymnasium/envs/mujoco/hopper_v4.py @@ -51,20 +51,20 @@ class HopperEnv(MujocoEnv, utils.EzPickle): However, by default, the observation is a `Box(-Inf, Inf, (11,), float64)` where the elements correspond to the following: - | Num | Observation | Min | Max | Name (in corresponding XML file) | Joint | Unit | - | --- | ------------------------------------------------ | ---- | --- | -------------------------------- | ----- | ------------------------ | - | excluded | x-coordinate of the torso | -Inf | Inf | rootx | slide | position (m) | - | 0 | z-coordinate of the torso (height of hopper) | -Inf | Inf | rootz | slide | position (m) | - | 1 | angle of the torso | -Inf | Inf | rooty | hinge | angle (rad) | - | 2 | angle of the thigh joint | -Inf | Inf | thigh_joint | hinge | angle (rad) | - | 3 | angle of the leg joint | -Inf | Inf | leg_joint | hinge | angle (rad) | - | 4 | angle of the foot joint | -Inf | Inf | foot_joint | hinge | angle (rad) | + | Num | Observation | Min | Max | Name (in corresponding XML file) | Joint | Unit | + | --- | -------------------------------------------------- | ---- | --- | -------------------------------- | ----- | ------------------------ | + | 0 | z-coordinate of the torso (height of hopper) | -Inf | Inf | rootz | slide | position (m) | + | 1 | angle of the torso | -Inf | Inf | rooty | hinge | angle (rad) | + | 2 | angle of the thigh joint | -Inf | Inf | thigh_joint | hinge | angle (rad) | + | 3 | angle of the leg joint | -Inf | Inf | leg_joint | hinge | angle (rad) | + | 4 | angle of the foot joint | -Inf | Inf | foot_joint | hinge | angle (rad) | | 5 | velocity of the x-coordinate of the torso | -Inf | Inf | rootx | slide | velocity (m/s) | | 6 | velocity of the z-coordinate (height) of the torso | -Inf | Inf | rootz | slide | velocity (m/s) | | 7 | angular velocity of the angle of the torso | -Inf | Inf | rooty | hinge | angular velocity (rad/s) | - | 8 | angular velocity of the thigh hinge | -Inf | Inf | thigh_joint | hinge | angular velocity (rad/s) | - | 9 | angular velocity of the leg hinge | -Inf | Inf | leg_joint | hinge | angular velocity (rad/s) | - | 10 | angular velocity of the foot hinge | -Inf | Inf | foot_joint | hinge | angular velocity (rad/s) | + | 8 | angular velocity of the thigh hinge | -Inf | Inf | thigh_joint | hinge | angular velocity (rad/s) | + | 9 | angular velocity of the leg hinge | -Inf | Inf | leg_joint | hinge | angular velocity (rad/s) | + | 10 | angular velocity of the foot hinge | -Inf | Inf | foot_joint | hinge | angular velocity (rad/s) | + | excluded | x-coordinate of the torso | -Inf | Inf | rootx | slide | position (m) | ## Rewards diff --git a/gymnasium/envs/mujoco/humanoid_v4.py b/gymnasium/envs/mujoco/humanoid_v4.py index 341ac6c28..5b3d9df1d 100644 --- a/gymnasium/envs/mujoco/humanoid_v4.py +++ b/gymnasium/envs/mujoco/humanoid_v4.py @@ -54,8 +54,8 @@ class HumanoidEnv(MujocoEnv, utils.EzPickle): ## Observation Space Observations consist of positional values of different body parts of the Humanoid, - followed by the velocities of those individual parts (their derivatives) with all the - positions ordered before all the velocities. + followed by the velocities of those individual parts (their derivatives) with all the + positions ordered before all the velocities. By default, observations do not include the x- and y-coordinates of the torso. These may be included by passing `exclude_current_positions_from_observation=False` during construction. @@ -68,8 +68,6 @@ class HumanoidEnv(MujocoEnv, utils.EzPickle): | Num | Observation | Min | Max | Name (in corresponding XML file) | Joint | Unit | | --- | --------------------------------------------------------------------------------------------------------------- | ---- | --- | -------------------------------- | ----- | -------------------------- | - | excluded | x-coordinate of the torso (centre) | -Inf | Inf | root | free | position (m) | - | excluded | y-coordinate of the torso (centre) | -Inf | Inf | root | free | position (m) | | 0 | z-coordinate of the torso (centre) | -Inf | Inf | root | free | position (m) | | 1 | x-orientation of the torso (centre) | -Inf | Inf | root | free | angle (rad) | | 2 | y-orientation of the torso (centre) | -Inf | Inf | root | free | angle (rad) | @@ -115,6 +113,8 @@ class HumanoidEnv(MujocoEnv, utils.EzPickle): | 42 | coordinate-1 (multi-axis) of the angular velocity of the angle between torso and left arm (in left_upper_arm) | -Inf | Inf | left_shoulder1 | hinge | anglular velocity (rad/s) | | 43 | coordinate-2 (multi-axis) of the angular velocity of the angle between torso and left arm (in left_upper_arm) | -Inf | Inf | left_shoulder2 | hinge | anglular velocity (rad/s) | | 44 | angular velocity of the angle between left upper arm and left_lower_arm | -Inf | Inf | left_elbow | hinge | anglular velocity (rad/s) | + | excluded | x-coordinate of the torso (centre) | -Inf | Inf | root | free | position (m) | + | excluded | y-coordinate of the torso (centre) | -Inf | Inf | root | free | position (m) | Additionally, after all the positional and velocity based values in the table, the observation contains (in order): @@ -133,49 +133,49 @@ class HumanoidEnv(MujocoEnv, utils.EzPickle): The body parts are: | id (for `v2`,`v3`,`v4`) | body part | - | --- | ------------ | - | 0 | worldBody (note: all values are constant 0) | - | 1 | torso | - | 2 | lwaist | - | 3 | pelvis | - | 4 | right_thigh | - | 5 | right_sin | - | 6 | right_foot | - | 7 | left_thigh | - | 8 | left_sin | - | 9 | left_foot | - | 10 | right_upper_arm | - | 11 | right_lower_arm | - | 12 | left_upper_arm | - | 13 | left_lower_arm | + | --- | ------------ | + | 0 | worldBody (note: all values are constant 0) | + | 1 | torso | + | 2 | lwaist | + | 3 | pelvis | + | 4 | right_thigh | + | 5 | right_sin | + | 6 | right_foot | + | 7 | left_thigh | + | 8 | left_sin | + | 9 | left_foot | + | 10 | right_upper_arm | + | 11 | right_lower_arm | + | 12 | left_upper_arm | + | 13 | left_lower_arm | The joints are: | id (for `v2`,`v3`,`v4`) | joint | - | --- | ------------ | - | 0 | root | - | 1 | root | - | 2 | root | - | 3 | root | - | 4 | root | - | 5 | root | - | 6 | abdomen_z | - | 7 | abdomen_y | - | 8 | abdomen_x | - | 9 | right_hip_x | - | 10 | right_hip_z | - | 11 | right_hip_y | - | 12 | right_knee | - | 13 | left_hip_x | - | 14 | left_hiz_z | - | 15 | left_hip_y | - | 16 | left_knee | - | 17 | right_shoulder1 | - | 18 | right_shoulder2 | - | 19 | right_elbow| - | 20 | left_shoulder1 | - | 21 | left_shoulder2 | - | 22 | left_elfbow | + | --- | ------------ | + | 0 | root | + | 1 | root | + | 2 | root | + | 3 | root | + | 4 | root | + | 5 | root | + | 6 | abdomen_z | + | 7 | abdomen_y | + | 8 | abdomen_x | + | 9 | right_hip_x | + | 10 | right_hip_z | + | 11 | right_hip_y | + | 12 | right_knee | + | 13 | left_hip_x | + | 14 | left_hiz_z | + | 15 | left_hip_y | + | 16 | left_knee | + | 17 | right_shoulder1 | + | 18 | right_shoulder2 | + | 19 | right_elbow| + | 20 | left_shoulder1 | + | 21 | left_shoulder2 | + | 22 | left_elfbow | The (x,y,z) coordinates are translational DOFs while the orientations are rotational DOFs expressed as quaternions. One can read more about free joints on the diff --git a/gymnasium/envs/mujoco/humanoidstandup_v4.py b/gymnasium/envs/mujoco/humanoidstandup_v4.py index 797c37ecd..40e791c9b 100644 --- a/gymnasium/envs/mujoco/humanoidstandup_v4.py +++ b/gymnasium/envs/mujoco/humanoidstandup_v4.py @@ -53,8 +53,8 @@ class HumanoidStandupEnv(MujocoEnv, utils.EzPickle): ## Observation Space Observations consist of positional values of different body parts of the Humanoid, - followed by the velocities of those individual parts (their derivatives) with all the - positions ordered before all the velocities. + followed by the velocities of those individual parts (their derivatives) with all the + positions ordered before all the velocities. By default, observations do not include the x- and y-coordinates of the torso. These may be included by passing `exclude_current_positions_from_observation=False` during construction. @@ -67,8 +67,6 @@ class HumanoidStandupEnv(MujocoEnv, utils.EzPickle): | Num | Observation | Min | Max | Name (in corresponding XML file) | Joint | Unit | | --- | --------------------------------------------------------------------------------------------------------------- | ---- | --- | -------------------------------- | ----- | -------------------------- | - | excluded | x-coordinate of the torso (centre) | -Inf | Inf | root | free | position (m) | - | excluded | y-coordinate of the torso (centre) | -Inf | Inf | root | free | position (m) | | 0 | z-coordinate of the torso (centre) | -Inf | Inf | root | free | position (m) | | 1 | x-orientation of the torso (centre) | -Inf | Inf | root | free | angle (rad) | | 2 | y-orientation of the torso (centre) | -Inf | Inf | root | free | angle (rad) | @@ -114,6 +112,8 @@ class HumanoidStandupEnv(MujocoEnv, utils.EzPickle): | 42 | coordinate-1 (multi-axis) of the angular velocity of the angle between torso and left arm (in left_upper_arm) | -Inf | Inf | left_shoulder1 | hinge | anglular velocity (rad/s) | | 43 | coordinate-2 (multi-axis) of the angular velocity of the angle between torso and left arm (in left_upper_arm) | -Inf | Inf | left_shoulder2 | hinge | anglular velocity (rad/s) | | 44 | angular velocity of the angle between left upper arm and left_lower_arm | -Inf | Inf | left_elbow | hinge | anglular velocity (rad/s) | + | excluded | x-coordinate of the torso (centre) | -Inf | Inf | root | free | position (m) | + | excluded | y-coordinate of the torso (centre) | -Inf | Inf | root | free | position (m) | Additionally, after all the positional and velocity based values in the table, the observation contains (in order): @@ -132,49 +132,49 @@ class HumanoidStandupEnv(MujocoEnv, utils.EzPickle): The body parts are: | id (for `v2`,`v3`,`v4`) | body part | - | --- | ------------ | - | 0 | worldBody (note: all values are constant 0) | - | 1 | torso | - | 2 | lwaist | - | 3 | pelvis | - | 4 | right_thigh | - | 5 | right_sin | - | 6 | right_foot | - | 7 | left_thigh | - | 8 | left_sin | - | 9 | left_foot | - | 10 | right_upper_arm | - | 11 | right_lower_arm | - | 12 | left_upper_arm | - | 13 | left_lower_arm | + | --- | ------------ | + | 0 | worldBody (note: all values are constant 0) | + | 1 | torso | + | 2 | lwaist | + | 3 | pelvis | + | 4 | right_thigh | + | 5 | right_sin | + | 6 | right_foot | + | 7 | left_thigh | + | 8 | left_sin | + | 9 | left_foot | + | 10 | right_upper_arm | + | 11 | right_lower_arm | + | 12 | left_upper_arm | + | 13 | left_lower_arm | The joints are: | id (for `v2`,`v3`,`v4`) | joint | - | --- | ------------ | - | 0 | root | - | 1 | root | - | 2 | root | - | 3 | root | - | 4 | root | - | 5 | root | - | 6 | abdomen_z | - | 7 | abdomen_y | - | 8 | abdomen_x | - | 9 | right_hip_x | - | 10 | right_hip_z | - | 11 | right_hip_y | - | 12 | right_knee | - | 13 | left_hip_x | - | 14 | left_hiz_z | - | 15 | left_hip_y | - | 16 | left_knee | - | 17 | right_shoulder1 | - | 18 | right_shoulder2 | - | 19 | right_elbow| - | 20 | left_shoulder1 | - | 21 | left_shoulder2 | - | 22 | left_elfbow | + | --- | ------------ | + | 0 | root | + | 1 | root | + | 2 | root | + | 3 | root | + | 4 | root | + | 5 | root | + | 6 | abdomen_z | + | 7 | abdomen_y | + | 8 | abdomen_x | + | 9 | right_hip_x | + | 10 | right_hip_z | + | 11 | right_hip_y | + | 12 | right_knee | + | 13 | left_hip_x | + | 14 | left_hiz_z | + | 15 | left_hip_y | + | 16 | left_knee | + | 17 | right_shoulder1 | + | 18 | right_shoulder2 | + | 19 | right_elbow| + | 20 | left_shoulder1 | + | 21 | left_shoulder2 | + | 22 | left_elfbow | The (x,y,z) coordinates are translational DOFs while the orientations are rotational DOFs expressed as quaternions. One can read more about free joints on the diff --git a/gymnasium/envs/mujoco/swimmer_v4.py b/gymnasium/envs/mujoco/swimmer_v4.py index a936ca71a..6db5f4f08 100644 --- a/gymnasium/envs/mujoco/swimmer_v4.py +++ b/gymnasium/envs/mujoco/swimmer_v4.py @@ -67,6 +67,8 @@ class SwimmerEnv(MujocoEnv, utils.EzPickle): | 5 | angular velocity of front tip | -Inf | Inf | free_body_rot | hinge | angular velocity (rad/s) | | 6 | angular velocity of first rotor | -Inf | Inf | motor1_rot | hinge | angular velocity (rad/s) | | 7 | angular velocity of second rotor | -Inf | Inf | motor2_rot | hinge | angular velocity (rad/s) | + | excluded | position of the tip along the x-axis | -Inf | Inf | slider1 | slide | position (m) | + | excluded | position of the tip along the y-axis | -Inf | Inf | slider2 | slide | position (m) | ## Rewards The reward consists of two parts: diff --git a/gymnasium/envs/mujoco/walker2d_v4.py b/gymnasium/envs/mujoco/walker2d_v4.py index fd2b14f28..204f17644 100644 --- a/gymnasium/envs/mujoco/walker2d_v4.py +++ b/gymnasium/envs/mujoco/walker2d_v4.py @@ -15,135 +15,135 @@ class Walker2dEnv(MujocoEnv, utils.EzPickle): """ - ## Description - - This environment builds on the [hopper](https://gymnasium.farama.org/environments/mujoco/hopper/) environment - by adding another set of legs making it possible for the robot to walk forward instead of - hop. Like other Mujoco environments, this environment aims to increase the number of independent state - and control variables as compared to the classic control environments. The walker is a - two-dimensional two-legged figure that consist of seven main body parts - a single torso at the top - (with the two legs splitting after the torso), two thighs in the middle below the torso, two legs - in the bottom below the thighs, and two feet attached to the legs on which the entire body rests. - The goal is to walk in the in the forward (right) - direction by applying torques on the six hinges connecting the seven body parts. - - ## Action Space - The action space is a `Box(-1, 1, (6,), float32)`. An action represents the torques applied at the hinge joints. - - | Num | Action | Control Min | Control Max | Name (in corresponding XML file) | Joint | Unit | - |-----|----------------------------------------|-------------|-------------|----------------------------------|-------|--------------| - | 0 | Torque applied on the thigh rotor | -1 | 1 | thigh_joint | hinge | torque (N m) | - | 1 | Torque applied on the leg rotor | -1 | 1 | leg_joint | hinge | torque (N m) | - | 2 | Torque applied on the foot rotor | -1 | 1 | foot_joint | hinge | torque (N m) | - | 3 | Torque applied on the left thigh rotor | -1 | 1 | thigh_left_joint | hinge | torque (N m) | - | 4 | Torque applied on the left leg rotor | -1 | 1 | leg_left_joint | hinge | torque (N m) | - | 5 | Torque applied on the left foot rotor | -1 | 1 | foot_left_joint | hinge | torque (N m) | - - ## Observation Space - Observations consist of positional values of different body parts of the walker, - followed by the velocities of those individual parts (their derivatives) with all the positions ordered before all the velocities. - - By default, observations do not include the x-coordinate of the torso. It may - be included by passing `exclude_current_positions_from_observation=False` during construction. - In that case, the observation space will be `Box(-Inf, Inf, (18,), float64)` where the first observation - represent the x-coordinates of the torso of the walker. - Regardless of whether `exclude_current_positions_from_observation` was set to true or false, the x-coordinate - of the torso will be returned in `info` with key `"x_position"`. - dimension - By default, observation is a `Box(-Inf, Inf, (17,), float64)` where the elements correspond to the following: - - | Num | Observation | Min | Max | Name (in corresponding XML file) | Joint | Unit | - | --- | -------------------------------------------------- | ---- | --- | -------------------------------- | ----- | ------------------------ | - | excluded | x-coordinate of the torso | -Inf | Inf | rootx | slide | position (m) | - | 0 | z-coordinate of the torso (height of Walker2d) | -Inf | Inf | rootz | slide | position (m) | - | 1 | angle of the torso | -Inf | Inf | rooty | hinge | angle (rad) | - | 2 | angle of the thigh joint | -Inf | Inf | thigh_joint | hinge | angle (rad) | - | 3 | angle of the leg joint | -Inf | Inf | leg_joint | hinge | angle (rad) | - | 4 | angle of the foot joint | -Inf | Inf | foot_joint | hinge | angle (rad) | - | 5 | angle of the left thigh joint | -Inf | Inf | thigh_left_joint | hinge | angle (rad) | - | 6 | angle of the left leg joint | -Inf | Inf | leg_left_joint | hinge | angle (rad) | - | 7 | angle of the left foot joint | -Inf | Inf | foot_left_joint | hinge | angle (rad) | - | 8 | velocity of the x-coordinate of the torso | -Inf | Inf | rootx | slide | velocity (m/s) | - | 9 | velocity of the z-coordinate (height) of the torso | -Inf | Inf | rootz | slide | velocity (m/s) | - | 10 | angular velocity of the angle of the torso | -Inf | Inf | rooty | hinge | angular velocity (rad/s) | - | 11 | angular velocity of the thigh hinge | -Inf | Inf | thigh_joint | hinge | angular velocity (rad/s) | - | 12 | angular velocity of the leg hinge | -Inf | Inf | leg_joint | hinge | angular velocity (rad/s) | - | 13 | angular velocity of the foot hinge | -Inf | Inf | foot_joint | hinge | angular velocity (rad/s) | - | 14 | angular velocity of the thigh hinge | -Inf | Inf | thigh_left_joint | hinge | angular velocity (rad/s) | - | 15 | angular velocity of the leg hinge | -Inf | Inf | leg_left_joint | hinge | angular velocity (rad/s) | - | 16 | angular velocity of the foot hinge | -Inf | Inf | foot_left_joint | hinge | angular velocity (rad/s) | - - ## Rewards - The reward consists of three parts: - - *healthy_reward*: Every timestep that the walker is alive, it receives a fixed reward of value `healthy_reward`, - - *forward_reward*: A reward of walking forward which is measured as - *`forward_reward_weight` * (x-coordinate before action - x-coordinate after action)/dt*. - *dt* is the time between actions and is dependeent on the frame_skip parameter - (default is 4), where the frametime is 0.002 - making the default - *dt = 4 * 0.002 = 0.008*. This reward would be positive if the walker walks forward (positive x direction). - - *ctrl_cost*: A cost for penalising the walker if it - takes actions that are too large. It is measured as - *`ctrl_cost_weight` * sum(action2)* where *`ctrl_cost_weight`* is - a parameter set for the control and has a default value of 0.001 - - The total reward returned is ***reward*** *=* *healthy_reward bonus + forward_reward - ctrl_cost* and `info` will also contain the individual reward terms - - ## Starting State - All observations start in state - (0.0, 1.25, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0) - with a uniform noise in the range of [-`reset_noise_scale`, `reset_noise_scale`] added to the values for stochasticity. - - ## Episode End - The walker is said to be unhealthy if any of the following happens: - - 1. Any of the state space values is no longer finite - 2. The height of the walker is ***not*** in the closed interval specified by `healthy_z_range` - 3. The absolute value of the angle (`observation[1]` if `exclude_current_positions_from_observation=False`, else `observation[2]`) is ***not*** in the closed interval specified by `healthy_angle_range` - - If `terminate_when_unhealthy=True` is passed during construction (which is the default), - the episode ends when any of the following happens: - - 1. Truncation: The episode duration reaches a 1000 timesteps - 2. Termination: The walker is unhealthy - - If `terminate_when_unhealthy=False` is passed, the episode is ended only when 1000 timesteps are exceeded. - - ## Arguments - - No additional arguments are currently supported in v2 and lower. - - ```python - import gymnasium as gym - env = gym.make('Walker2d-v4') - ``` - - v3 and beyond take `gymnasium.make` kwargs such as `xml_file`, `ctrl_cost_weight`, `reset_noise_scale`, etc. - - ```python - import gymnasium as gym - env = gym.make('Walker2d-v4', ctrl_cost_weight=0.1, ....) - ``` - - | Parameter | Type | Default | Description | - | -------------------------------------------- | --------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | - | `xml_file` | **str** | `"walker2d.xml"` | Path to a MuJoCo model | - | `forward_reward_weight` | **float** | `1.0` | Weight for _forward_reward_ term (see section on reward) | - | `ctrl_cost_weight` | **float** | `1e-3` | Weight for _ctr_cost_ term (see section on reward) | - | `healthy_reward` | **float** | `1.0` | Constant reward given if the ant is "healthy" after timestep | - | `terminate_when_unhealthy` | **bool** | `True` | If true, issue a done signal if the z-coordinate of the walker is no longer healthy | - | `healthy_z_range` | **tuple** | `(0.8, 2)` | The z-coordinate of the torso of the walker must be in this range to be considered healthy | - | `healthy_angle_range` | **tuple** | `(-1, 1)` | The angle must be in this range to be considered healthy | - | `reset_noise_scale` | **float** | `5e-3` | Scale of random perturbations of initial position and velocity (see section on Starting State) | - | `exclude_current_positions_from_observation` | **bool** | `True` | Whether or not to omit the x-coordinate from observations. Excluding the position can serve as an inductive bias to induce position-agnostic behavior in policies | - - - ## Version History - - * v4: All MuJoCo environments now use the MuJoCo bindings in mujoco >= 2.1.3 - * v3: Support for `gymnasium.make` kwargs such as `xml_file`, `ctrl_cost_weight`, `reset_noise_scale`, etc. rgb rendering comes from tracking camera (so agent does not run away from screen) - * v2: All continuous control environments now use mujoco-py >= 1.50 - * v1: max_time_steps raised to 1000 for robot based tasks. Added reward_threshold to environments. - * v0: Initial versions release (1.0.0) + ## Description + + This environment builds on the [hopper](https://gymnasium.farama.org/environments/mujoco/hopper/) environment + by adding another set of legs making it possible for the robot to walk forward instead of + hop. Like other Mujoco environments, this environment aims to increase the number of independent state + and control variables as compared to the classic control environments. The walker is a + two-dimensional two-legged figure that consist of seven main body parts - a single torso at the top + (with the two legs splitting after the torso), two thighs in the middle below the torso, two legs + in the bottom below the thighs, and two feet attached to the legs on which the entire body rests. + The goal is to walk in the in the forward (right) + direction by applying torques on the six hinges connecting the seven body parts. + + ## Action Space + The action space is a `Box(-1, 1, (6,), float32)`. An action represents the torques applied at the hinge joints. + + | Num | Action | Control Min | Control Max | Name (in corresponding XML file) | Joint | Unit | + |-----|----------------------------------------|-------------|-------------|----------------------------------|-------|--------------| + | 0 | Torque applied on the thigh rotor | -1 | 1 | thigh_joint | hinge | torque (N m) | + | 1 | Torque applied on the leg rotor | -1 | 1 | leg_joint | hinge | torque (N m) | + | 2 | Torque applied on the foot rotor | -1 | 1 | foot_joint | hinge | torque (N m) | + | 3 | Torque applied on the left thigh rotor | -1 | 1 | thigh_left_joint | hinge | torque (N m) | + | 4 | Torque applied on the left leg rotor | -1 | 1 | leg_left_joint | hinge | torque (N m) | + | 5 | Torque applied on the left foot rotor | -1 | 1 | foot_left_joint | hinge | torque (N m) | + + ## Observation Space + Observations consist of positional values of different body parts of the walker, + followed by the velocities of those individual parts (their derivatives) with all the positions ordered before all the velocities. + + By default, observations do not include the x-coordinate of the torso. It may + be included by passing `exclude_current_positions_from_observation=False` during construction. + In that case, the observation space will be `Box(-Inf, Inf, (18,), float64)` where the first observation + represent the x-coordinates of the torso of the walker. + Regardless of whether `exclude_current_positions_from_observation` was set to true or false, the x-coordinate + of the torso will be returned in `info` with key `"x_position"`. + + By default, observation is a `Box(-Inf, Inf, (17,), float64)` where the elements correspond to the following: + + | Num | Observation | Min | Max | Name (in corresponding XML file) | Joint | Unit | + | --- | -------------------------------------------------- | ---- | --- | -------------------------------- | ----- | ------------------------ | + | excluded | x-coordinate of the torso | -Inf | Inf | rootx | slide | position (m) | + | 0 | z-coordinate of the torso (height of Walker2d) | -Inf | Inf | rootz | slide | position (m) | + | 1 | angle of the torso | -Inf | Inf | rooty | hinge | angle (rad) | + | 2 | angle of the thigh joint | -Inf | Inf | thigh_joint | hinge | angle (rad) | + | 3 | angle of the leg joint | -Inf | Inf | leg_joint | hinge | angle (rad) | + | 4 | angle of the foot joint | -Inf | Inf | foot_joint | hinge | angle (rad) | + | 5 | angle of the left thigh joint | -Inf | Inf | thigh_left_joint | hinge | angle (rad) | + | 6 | angle of the left leg joint | -Inf | Inf | leg_left_joint | hinge | angle (rad) | + | 7 | angle of the left foot joint | -Inf | Inf | foot_left_joint | hinge | angle (rad) | + | 8 | velocity of the x-coordinate of the torso | -Inf | Inf | rootx | slide | velocity (m/s) | + | 9 | velocity of the z-coordinate (height) of the torso | -Inf | Inf | rootz | slide | velocity (m/s) | + | 10 | angular velocity of the angle of the torso | -Inf | Inf | rooty | hinge | angular velocity (rad/s) | + | 11 | angular velocity of the thigh hinge | -Inf | Inf | thigh_joint | hinge | angular velocity (rad/s) | + | 12 | angular velocity of the leg hinge | -Inf | Inf | leg_joint | hinge | angular velocity (rad/s) | + | 13 | angular velocity of the foot hinge | -Inf | Inf | foot_joint | hinge | angular velocity (rad/s) | + | 14 | angular velocity of the thigh hinge | -Inf | Inf | thigh_left_joint | hinge | angular velocity (rad/s) | + | 15 | angular velocity of the leg hinge | -Inf | Inf | leg_left_joint | hinge | angular velocity (rad/s) | + | 16 | angular velocity of the foot hinge | -Inf | Inf | foot_left_joint | hinge | angular velocity (rad/s) | + + ## Rewards + The reward consists of three parts: + - *healthy_reward*: Every timestep that the walker is alive, it receives a fixed reward of value `healthy_reward`, + - *forward_reward*: A reward of walking forward which is measured as + *`forward_reward_weight` * (x-coordinate before action - x-coordinate after action)/dt*. + *dt* is the time between actions and is dependeent on the frame_skip parameter + (default is 4), where the frametime is 0.002 - making the default + *dt = 4 * 0.002 = 0.008*. This reward would be positive if the walker walks forward (positive x direction). + - *ctrl_cost*: A cost for penalising the walker if it + takes actions that are too large. It is measured as + *`ctrl_cost_weight` * sum(action2)* where *`ctrl_cost_weight`* is + a parameter set for the control and has a default value of 0.001 + + The total reward returned is ***reward*** *=* *healthy_reward bonus + forward_reward - ctrl_cost* and `info` will also contain the individual reward terms + + ## Starting State + All observations start in state + (0.0, 1.25, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0) + with a uniform noise in the range of [-`reset_noise_scale`, `reset_noise_scale`] added to the values for stochasticity. + + ## Episode End + The walker is said to be unhealthy if any of the following happens: + + 1. Any of the state space values is no longer finite + 2. The height of the walker is ***not*** in the closed interval specified by `healthy_z_range` + 3. The absolute value of the angle (`observation[1]` if `exclude_current_positions_from_observation=False`, else `observation[2]`) is ***not*** in the closed interval specified by `healthy_angle_range` + + If `terminate_when_unhealthy=True` is passed during construction (which is the default), + the episode ends when any of the following happens: + + 1. Truncation: The episode duration reaches a 1000 timesteps + 2. Termination: The walker is unhealthy + + If `terminate_when_unhealthy=False` is passed, the episode is ended only when 1000 timesteps are exceeded. + + ## Arguments + + No additional arguments are currently supported in v2 and lower. + + ```python + import gymnasium as gym + env = gym.make('Walker2d-v4') + ``` + + v3 and beyond take `gymnasium.make` kwargs such as `xml_file`, `ctrl_cost_weight`, `reset_noise_scale`, etc. + + ```python + import gymnasium as gym + env = gym.make('Walker2d-v4', ctrl_cost_weight=0.1, ....) + ``` + + | Parameter | Type | Default | Description | + | -------------------------------------------- | --------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | + | `xml_file` | **str** | `"walker2d.xml"` | Path to a MuJoCo model | + | `forward_reward_weight` | **float** | `1.0` | Weight for _forward_reward_ term (see section on reward) | + | `ctrl_cost_weight` | **float** | `1e-3` | Weight for _ctr_cost_ term (see section on reward) | + | `healthy_reward` | **float** | `1.0` | Constant reward given if the ant is "healthy" after timestep | + | `terminate_when_unhealthy` | **bool** | `True` | If true, issue a done signal if the z-coordinate of the walker is no longer healthy | + | `healthy_z_range` | **tuple** | `(0.8, 2)` | The z-coordinate of the torso of the walker must be in this range to be considered healthy | + | `healthy_angle_range` | **tuple** | `(-1, 1)` | The angle must be in this range to be considered healthy | + | `reset_noise_scale` | **float** | `5e-3` | Scale of random perturbations of initial position and velocity (see section on Starting State) | + | `exclude_current_positions_from_observation` | **bool** | `True` | Whether or not to omit the x-coordinate from observations. Excluding the position can serve as an inductive bias to induce position-agnostic behavior in policies | + + + ## Version History + + * v4: All MuJoCo environments now use the MuJoCo bindings in mujoco >= 2.1.3 + * v3: Support for `gymnasium.make` kwargs such as `xml_file`, `ctrl_cost_weight`, `reset_noise_scale`, etc. rgb rendering comes from tracking camera (so agent does not run away from screen) + * v2: All continuous control environments now use mujoco-py >= 1.50 + * v1: max_time_steps raised to 1000 for robot based tasks. Added reward_threshold to environments. + * v0: Initial versions release (1.0.0) """ metadata = { From 7639d18ced28a06d641135cfc91df4b5fa6487d3 Mon Sep 17 00:00:00 2001 From: Kallinteris Andreas <30759571+Kallinteris-Andreas@users.noreply.github.com> Date: Fri, 16 Jun 2023 14:03:06 +0000 Subject: [PATCH 08/29] refactor `tests/env/test_mojoco.py` -> `tests/env/mujoco/test_mojoco_v3.py` --- tests/envs/{test_mujoco.py => mujoco/test_mujoco_v3.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/envs/{test_mujoco.py => mujoco/test_mujoco_v3.py} (100%) diff --git a/tests/envs/test_mujoco.py b/tests/envs/mujoco/test_mujoco_v3.py similarity index 100% rename from tests/envs/test_mujoco.py rename to tests/envs/mujoco/test_mujoco_v3.py From 61d0848b741764d07dcb97fca4300d51b647c844 Mon Sep 17 00:00:00 2001 From: Kallinteris Andreas <30759571+Kallinteris-Andreas@users.noreply.github.com> Date: Mon, 23 Oct 2023 18:29:19 +0300 Subject: [PATCH 09/29] Update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 4b1809efe..43ac766d5 100644 --- a/setup.py +++ b/setup.py @@ -31,7 +31,7 @@ def get_description(): long_description += line else: break - return long_description + return long_descriptiona setup(name="gymnasium", version=get_version(), long_description=get_description()) From 5831a19a8aa8adbf3b32da26eb862a4b874c1c10 Mon Sep 17 00:00:00 2001 From: Kallinteris Andreas <30759571+Kallinteris-Andreas@users.noreply.github.com> Date: Mon, 23 Oct 2023 18:29:32 +0300 Subject: [PATCH 10/29] do nothing --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 43ac766d5..4b1809efe 100644 --- a/setup.py +++ b/setup.py @@ -31,7 +31,7 @@ def get_description(): long_description += line else: break - return long_descriptiona + return long_description setup(name="gymnasium", version=get_version(), long_description=get_description()) From d99cc5d285fa38a104cc6d5efa29aeb83bf05539 Mon Sep 17 00:00:00 2001 From: Kallinteris Andreas <30759571+Kallinteris-Andreas@users.noreply.github.com> Date: Fri, 3 Nov 2023 14:12:21 +0000 Subject: [PATCH 11/29] [MuJoCo] add action space figures --- .../mujoco/action_space_figures/ant.png | Bin 0 -> 30776 bytes .../mujoco/action_space_figures/credits | 2 ++ .../action_space_figures/half_cheetah.png | Bin 0 -> 28167 bytes .../mujoco/action_space_figures/hopper.png | Bin 0 -> 20167 bytes .../mujoco/action_space_figures/humanoid.png | Bin 0 -> 42808 bytes .../mujoco/action_space_figures/pusher.png | Bin 0 -> 18746 bytes .../mujoco/action_space_figures/reacher.png | Bin 0 -> 16528 bytes .../mujoco/action_space_figures/swimmer.png | Bin 0 -> 17823 bytes .../mujoco/action_space_figures/walker2d.png | Bin 0 -> 26438 bytes gymnasium/envs/mujoco/ant_v5.py | 4 ++++ gymnasium/envs/mujoco/half_cheetah_v5.py | 4 ++++ gymnasium/envs/mujoco/hopper_v5.py | 4 ++++ gymnasium/envs/mujoco/humanoid_v5.py | 4 ++++ gymnasium/envs/mujoco/humanoidstandup_v5.py | 4 ++++ gymnasium/envs/mujoco/pusher_v5.py | 4 ++++ gymnasium/envs/mujoco/reacher_v5.py | 4 ++++ gymnasium/envs/mujoco/swimmer_v5.py | 4 ++++ gymnasium/envs/mujoco/walker2d_v5.py | 4 ++++ 18 files changed, 38 insertions(+) create mode 100644 docs/environments/mujoco/action_space_figures/ant.png create mode 100644 docs/environments/mujoco/action_space_figures/credits create mode 100644 docs/environments/mujoco/action_space_figures/half_cheetah.png create mode 100644 docs/environments/mujoco/action_space_figures/hopper.png create mode 100644 docs/environments/mujoco/action_space_figures/humanoid.png create mode 100644 docs/environments/mujoco/action_space_figures/pusher.png create mode 100644 docs/environments/mujoco/action_space_figures/reacher.png create mode 100644 docs/environments/mujoco/action_space_figures/swimmer.png create mode 100644 docs/environments/mujoco/action_space_figures/walker2d.png diff --git a/docs/environments/mujoco/action_space_figures/ant.png b/docs/environments/mujoco/action_space_figures/ant.png new file mode 100644 index 0000000000000000000000000000000000000000..a39470c1b29f2f9329376965352bdf6c4e573655 GIT binary patch literal 30776 zcmX6_1z1$u7Cs;%4bt5$(k0#9Ee+Bj-Q67`4N_9lErK)x(j{He9nvB2w)Y+V0EfZh z>{$D+HBl-`(x^y;NDu^}%F0NnK@bcR___`O7JT%8GcN+4$luARDMFApEd&LILeL#} zC~y~o+*l!K-xz}U(jf@X@l%Vc0Qd&Hsl2oV^z`pfURy~Lc;tnvtfJ(L9T*h2XOyy* zmDb=PjCUqV?hy2>6M|rg!7G-4$7Tq1Syds({jaQqsD|glk%74}wlqM1y~bF9~|2xm-W^Gqn-RBBWx#dift6jgF2U zAJuc5kKPNhzy_0_c75~w^SPnLO~hlr)Dh6p*7jf&@Zk3N*Xw4NXVsS~MK0@JNmx?9 z-PdbtFy-{BFOG;s|67giY!&!{P9Ju5_L(Y!#ijRll2gxYsAGxWIM3CXxVyV=ZEaz| zk-Tv()UGzffHSwW{NCK0F|ys)GQIU>)zAj zb;ox5h%$m$e$AKvu9g8`7{wRLHi@o(p5b93{Np>lfaF5IH${XJ4Pi)!}?^Z!e2sD;Td$=VmA{gTwOf{{Cd4@nCx-i*qaH7Orw?@AhA(R~qfkThZm^<@(G|;Hmf5C+H`!F)=Rg?#D~bE-nF9gfHL= zn)BrJD95c`i=`^u4`wQtTig!j>vxJ3vwjOEkO|u_em(jTg$2G^S>joaP6*QW?jB}8 z3R>(=7ULp_2*wbxWNkX9RXpl-ZtCR{c^A^f>a_mjZM}ut$%;YsYyz0T$G^7*-`$@> zo<2TLf6vFJiWO~-l}BEPl5>!@zHZGeHddXG!lKsB3SL5Rxm)mbbRyGn@%Hv^vR_`` z*m#K`;pus^{M{oYBm@R}e0+3n7GPmX5FzKhUT*q4peo+o#uNG^kQVN((t0S1&j}0V z&19Xu;y%_pV!Qw8>Dw#lZNGosG(((V$X@T^b;$MX)aFR8O;BD+2{~!EL3M zCn0ZQwbb@`nET3Xfi>dh>PpOIgAM&2&fq92DiZ#C`V2DOn=As0z{bL%$r&g-goG-h0qqN4hEV^GBiuoBnx99(|Zqsov zIsvnLeK^nIw*Sj~DAlh*diLhGEGmb4$_0vr)9##ZL^9zu!z1YI|g4gp9f~-zoXXQPFo4K?7Vv3^0tO26U2f*JUS-uX zfQN--JT*?f9>yrsyxm%8s`j`!U87U_WHo^T2OWN=Kyjs*H7ADx6-2HRjGx?;b%tlT z)^Vy0+vG~7#Kpyh>EOS9HX(hkIQTjFXP0=JPCNy+2`)HFrp;Z(~Uh5emdi+0XM0;WS5HNS)%V8H!NK9%S%U9|Rn%al7%V03wBs8l!XPOs zYDTJ<;K!eecFbQIXgT*$?frOBK8i^wuNY3Qq-p$|Ixm8UI`d=}ZmJY$Z{@vI;(}$9 zXh{UUv7;o^N`FmGPBy#j{40tyG|z=vEXQ)Z&bLJ|Bnl?vgW6W>4b&JLb^W46n1xa8@73m4aUU3VmTyqL5KR+0I!h^=`r%wMGygDq9LQo-f> z=tmhAIJv9fz|>c48b^zfROPt#k@mWdY`*uaz8P^xk>?IqkdC$8ah4SUuixFpM1l0Y zox7%HiUj4vo-3#d@`{R}#E?QFOt_#pV}ywb`v28oOtEQPI6ZYNQnP;JH=NFPdwZMB z?=fT3h#3_+wzD`lcevEd!OcCRM$5wD&=Zczm#tJii;mCQX~!LKz3g$C|7W9L9K&_} z2V#ao@wAQ4<$lo=*S5e@oJd`%RorL=PrC^=jVj?Eoy{JU-k+$?{j|0N8rx4TqW`|* zzS!sqiAgF~XyNg*AGZqZr))ipQm!v_daDMBd?0$SL#O1_=%oJ&XBDkns}dH{Z*~sr zS?lyUUTKRF%a11&M1tlTtdl0JK5@S%Y_$lKpd=IY4i%e@U1m0FkL@xxH8nL(qvFo; zJKL0nI$1jYnlfx`ZCy?&3wJHvUmZPzl9Q7m$bPlGQmwSRrw2?JGFh9`MxXRC;siZ4 z_W%FBmm% zUS{ke_pFyO%$ttLH;o&maV0sKPWV1hxH(PmWsJ>%=(!uEWioxgJu@kWQucVITi~58 zHXhzHD3$50X&Y9Q#Nd`Q)Y%h(4g*n1#nupao7SSCp@BmP3M?yQY%@&gH3d89ZMp72 zRBl>Yn)zZ^7Yt;~iVs0xCqIK+_a=MqTB@q58foJckvJh^Z%*5lc6MBxq2ie*7>q&G zj3PNYw02wao}nO5{28V{$2yL?Cr3xzfIf<+MuW1uyG@6_V^?E zWPS44U0W$5S`H0{Nx{JnkLL=3z2rXdY!Gy%pr6chWFL(mBPG#f&PM;Ov;&V0`^)P7 z+pN6&1Eh9A9rwFLi7Nkd*Yf$b?k=SS3nOI=My^W~hAo>7;jyx8I|Sb0p<0nfg|Xu4 z#}6-SztEE{M2>!f%`sU@{Y)gAd&~ML7}#r?C{vg(gb^j7r>6&r=;~%(GGIo*tvI^7 z!$Z7p-b@SPZCmy@y1K$ao!Mp29nt?zoKD`&&gc3XS0NL;Q0hGce;>WH{P0t2o<**Ow9|?afH}@g-p^!HXnL!<$A2NM(fc25{D-G;y!9s5PUDP z8oRWy9Sz9DpH5_JM?ODG+K*LB>AG;JH3Ff*R?IfXgho;GlwT4EMD=B z)Y0%Uy6dDLEspuiGuqj~>FMpAIkFw{0$ukCOa^=i%!^fpPrNUo(|)o5o%rIC5^Zhm zp2e?Uzj_L^S5(A~?dTybwYbH|P@@sBZ^otN@VPNUzgGjE8V}hBzeenme!Kj#S^IeI5~OUys75Y29_Y$CV8h=I^HgfvC~d42>rExKY<_id-eBMRrG8 zj)o1!09}@^=K72u2-Ks0IX6a$_eglYJh;@m6FA+;4batNf`I@E9!h<^*kG-GveMRZ zyG=wG$L+i+6C^TTB#&3BK}bOGe~_ju{D4hHw))-Uq&Ei7(#-5vrl3*=yE*tFQBl$B zlhy0%>i}>VfwIj>sMz8Eurfyt&LCmoj*5zk+IRh=!v6ZD8UUUVkdg*)w!XQdD zvv(D3`vSq{=gprek#ZhnGpPbZ=dWGe+w^TO30sSeMX>csBG`wLx5So1kC$5Hg+SR% zB5>Zp912|smTR0b?#uh|(EX)nVG3>arSOPmv-7D0v0N2*a?C{mA1kf9rN6~C^PES+ zu|HuPynJp2IJSDDaV+Wy!o)HZXraJ_1Z7=aLV@b?va;yd*aj39K>>lX^70fp8huP? z91PE5lfyHJsYE%Q6^6Tt6rh>OLRllf%V_}4nPw%)F~ES=1Zj|Cz`b~J?kNBV3j>Kz zCW4btfb^u`HoiS0lm_qHHev)&O>@KNFHy?+ zXIf50-o%7o*Z35zs$0j+ZiK~zFSuVhz&u{My-cc_a|}8$mp*l)oRYZ|AOH*F4ko}A zPY)LDIV8iVB9jvC?%!}D@K&{GbTJWQPqESWSF-NZ6cYpPGj1jKyqC5Wr=~@JP7NlO zHF}MZ_1|2iS}rvE-MdRkNi9DBic%n*5Z)9fwwC%@udc2xU11wQdhhFFgK|fE`{Otw z9)PxSv9TqJ)ZF;8FM|!`!NaPuysX4tjRl@InXmu3-{arsA z4^NY52>jbCVqVAJ07QUS5q`XR2R>O?SiDa7RNSK{IkTg==l{2SDM~T&ET3ZkUW7<- z8`)_ZjY;1>g>Nz|^FztEZ$H|OYi#Bm!j?la6r7LVCW*ao#WiYkYe`C5j*!~rLM~*S z3ZwgotX$}>P?3w(oBAjEJ-ng7;cw}PfSl+pH&oLF6*jzeIDe(W$&!G5PDXhXx&-dE z>=(jgDb@{r%k|kcR-ro90j)yGm&ZVgTBYxk>hya=8B8&q(Asg_stE0t$yFEG`p(#p@*~uW?TGQUe ztBr-1xGH$pa^Zs&IDGZXqhIF<buiTU1R z)ECSR*oA3U@x9Sg(OfXh)MV0iBE=mk^xdBm3%<{#YT3rzWD32v4K6JAqUlgN`L;dQ z&#j-9DVH-RT3TYy$b@w^1Y0LP=R!*|%>Ak_IlWuN{uRa?-AR zrEBP1#+@CFaHZ7xGLzAaLJSLIXEpbN027Two+9tvRFQ-LV|UCT)`5Bq_K~0Ua){0- zH=GadO1AukJ9#R?MZ^h`_;MtNPyI>BtLPg(?Zq^+zIUpaVWc7OhSh9UUzr?t(+#_R zF~e+{wjXz6nXnlV9m`_=jH9Qf*1~_Qfs&@m0VDK1AS~&7K>se=4A)N$DMpx=n_@RR z&BLp=Tb-I!+%^}*%&(^+FN5-?_CPh{=FVMAzVS;f6!`W{c&9mir1;*NvU0_4?1yAT zojq+7sioX=HNP|{Q_Z#|jS(QtD;LV1XQSPRKZ1unDbaBD#ADFREBOpGnZ_M*4m*=} z%YJbA8!8ct4exO6IGMf+xEZ{2I65vsC;;sEToe?)$t)^EkvfH>X&7&-Zo@H>L zkAj&#jDZ%?ki)2sXq8{_oR_F^{pe55LAM5_&5%aFBYO9xXORB;A9)xS9Ld`G$IGc< zQWP65TQFK>7+aEW_0nQT#}onsoUP^%$QEYgePPkw6mxJiehGKGQt)*S>KkVt;kfKJ z4s~X1NXs4Y@956`THrZ&(mUl-TXJdoii=CaJJE5hZ*;nAx@o2vv#~2aN|Z8aU@$gJ zj55|p!|oC<@Y-WkgtF_CKlkdzj($vadp46UYv)XOxW8(}_`YJVuVU8Ip}z+U+0tnb z2GS8uBE2$mtwv!rHW9q^|6f6A~_WVbZ#j`!rAK~38CFPiB(=ya8@1PJV z3E^Q4_X{e#vtNBr-%`>Zhj|LsE3q_n-<67xt;xzUPkuz?3P>UlnH#eu>A)e|rb*zak2rA5CJ;ZVfh zwR*>{5IgA=(-I$rHtSp5ERv05eRb zK5(M;8lNykeJm^yp^_998keDO{2Cr=g}v^Aw21CZqoFbD>E`K(_$H<02khnNYF-35 zS31eU$xPaSrpALn*;ID+#y|FT<56J*t6@dsnK_&E%SX1U?==YD_2>>)S~*$y8m);~ zel8-igXH286(yfRD71kMghmuF-DZ!z+o9r=)?O(#L!Lx&Vp6v%v$-1?BgXx+@g#mx zJ7mZJ5I-f`6F{>;oVH)~q36ck2b+BE@P|ta30=a?rDNI@vgC5t4vNdwVPp{HS zf%7r58WpwDKZ!F&&bBO-z?b9?JQmn9HnBmO)3D5%e7daaJLY=hV6QzD~JsZ)>)iELTa@Xr{hS5fO=pu-M^ zGxrL_&Z*rIxwu;nDwAswq`1&QrgGI6;*>9!gVQY#-dMOylty5te6>KU;7G5grSEfi z_2yiLmvyi&(O{!VWmp@lCi3_-#di4w;InZ+fNY*955vLAB00&;t%1>qM5%wICm`&l z{cSWU%Yav)f)5)-sZCVfhrDyyv^SgT&7xSlRcEYWsW&v}=AW`}9~~MhU72e|wtjUx z>^m5GSM8C861_QYL}HD+6}}1U#bzYmSMY^8S;+yHA(j(!VJvF%M4^t9JQC+FE9!Hb z%O{fgk*6(t%H-74$vs!blm*Ay&z~u&G56Nj&)gf^aealJzMr)7d!CXf&J8>0Cc!!J z*Ra0oUoi5gWm;!w^riwbm$~U_VXNHb^v<-Abp2gp5$$IAZ0^n_iiOU7JoP5y!tJ;Z z97h^yr3{1jtL02w7&>luEB-s-F}OyHAv;D|zGSY-CO=m_tuOqIA;RxWmA|WIzkg;h zP|ZH>_)50bwBliBuro9?^uvb_&yjIM!^5i#zR%ckdmJy_o^(8glYapUUIZFRi{n~% zECI)`MSVfRI|+%OO{JIxUs=qYPJCMKlIj2iO@72?QO|zk8=)$*;Dl!t+#xs)zI8=PBqXMaa zZ*ZC{V@N_$kS~3zkT=d#WT?C2u}y2|02Iv_ zvCy%)cu>*Tj};*|Z1sfMnY5GQ#6jG$nTj=_d5pgym^g$iTXk)V6IE_CTNW9jbEg;w zJTLO-(c&(muO82dz0zMiKZe03Ea*Z22Y<`+y07}{we(+7dHKRzh`u@%T_n^rXVEgQ zTep(Pq~#zj+)lP0$wu3Q!3&2|LY0X zQUym*bniNl#7CeI< zn^8bOpB@`%T%RN;NrZgAIpr#=Uxx1F%24{^yQ5T;knF{U#dv0Zvm5x}{*^st&*EdG zWLNC2RfrUYH8m#_=Mq(V2L99j2*C+^?J`xDf33?`IKcE=P`5?{P41>%VC zwiN39<!{tKj=Qh6tUlPR<^dbmX0TW z86^P&0|T_i`o_kh39F93XG5oJJ(V$~8jRA?(#`tqP8(8fhUkz^i>vimj^NXE#}kkk zz+E9Tr)Fmh8g~W?KmFwp5Fic{Yx7XR${o`#HI$16wv1N!>#qdVn78Y_$Y>E*czDtU z6Q=LpO<1|w*;VK;0S)&3;@5|}gKFD0M6!5V3KQo2Nd!MNUN;3ypsx+|gqVCaaGV`T9-_@BU~1Jwo?01y;Q#Dlg{1CJZi=5=oI-Tesg zun?&N7zn7E=T}$EjEoCS4y!=GCVY6yHGtW>-tp?COmbtO+(RG`!CY7r?+)m{lWTY1 zxwNLcK8FuG#OkZFT#uza??HLns6a+C7(7kP*xTY$`g!(GO<^Hids?pl-GCKm4!_5- z_%=#)57?Z_xk4mZSmNZrSb9`rf*k8#3z`(^c)XH@gN#+j175p&5k0l1Mgqp{b}-Z2 z+#E+H+hAnq2!V8~}P_9Ug043aY&mMYOQc-nD zt9o8@UnM{dd2Q6*3bg)*`+p>(6&(;mZ36?pqlL!6z`%CD`;}&w7f=nD%XkugA-}r@ zhN?MgYJtb>#b&yXuf`;4IUK)ssYhDzA;yVOlEayCW)>p7a}Nt25>ejoIy_Xa)fy~H zR5^X+!y`$&C*C-78$Q8`4wU%Q`;(6T-d?WWGh2QArB<(VAW1*8sM@!oG;s3sCA+am z5|a#y6=0-L8gzVhSNyY?MFLjVNG6w{_r;f9k!F2TJiN5@bb8zvTSWw62K>0biwjp_ zVc|AIpkg@`sl8QIUHkoe9LN&xM+;zBxOyeC%y?}?qeOC5@))rQ2g@$uu_S&_H2+(< zL|cmM>^2@BDZ`u=;y*sFDX{-RO&aj-FZws(>}EfY7Ms>C%Hv{y!U0Z7lb-M{hBFA7 zn3%Y_x=MvbWgRC36(`p+n5<)c=2iS(g5POI{@|S9upE{6+XVZLJ)=s$850ZZSpWgh zOSH;qk_MP?hZYz485zq6G_wqOh$~}W>wVq5ywg3qj|(e~({@}lHsE!zHMCS6?@)Bs zac=mWBs>zY`Q(W{b9+qIb^LNz*Sq0t*Q3hu&R!^2*z@!^m?>Jif7dgp#p48R=fD~} zU4fR@mg$o$Is_zYCh1qb;YJDC?&ubo6pn|6nh6-RcHyPZ<5vDUh+SS-T>&7_(9i(x zu+er_7ypY|sr$(aP_^l?k&wdn7n`b^ns|~1G#FDT6Tjr;{r;IqMUQQ>((2{zo_jn_ zE+Ej_($WI_<*5At%f?39FjUm#f!x2>f)sEW^9>6pwG(FTxGZMsYy?GRjx5m{=95`@ z#@euu2yw|gzu|#_93~L5O&@Y)E1^dj|JDU3z$IO>b zolXS!gmE)RnmU#Z-_we}d`wT*I6N{_{lHJdERSS6Hf{5_1F?H0KY3tTc#$;{+{)GN zM9=JXkxu-8*+R1m9cI)oE6$*xAeZf7CwKSok|`agRHYJNzyoI!F-{8Tdn`;$6)+(# zSZjZ8&mB0p`;1wE1Al*j0z(#GOcLKtMlCnB-Z2!#>zdAkYAqRmZ;c6wT$97M28loc zbi~AQ6OdZ;uMXmJlyhp!U|q|qjzfHVFuPE9Dj8{(ch9hApD|IXS|T9H#f;y8x2Q zJ^vG741gYixV=pMbnxD_5y%8F-_%oXx3N zDss3bVs{|gXu0k$XxO}cDe#MM*vA1OP6}gBy&fMSOZ>Hf?u?0W&9y`SeK<;|j?H%kk&Vo3qF#%#_cnKaI7|&h@>jRsMlYb)PyDL3UgM zlHX!A)H1#w@^$>eUtB9ef&OcU9O$>q#I0^0a-Ji=Rbp88w0(gf=4Z15 zxqmi{ww{RII}8xfsNr)AHON)Uma^r>B&o@!$HO%I64MOiK(xhx4zegXjAzCKWjp!;1ryysFkkI@g@iGtH<} zCHj@q;t?lN0!Ik+)GF2rMbj9bgy+8aej4_2vc6i87B*Pn~A8TV=Oxzvzw z)(kb|Srd~`g@%)^h9;uBR4-s&HuN+}TGJ3pS zlgp;&0LO@FZw;6S|q5s-w&`SZyl`G3-Qy-rWe z?_43qSFf@%qjUMd0t}(FW4HKc*ThXavY8M8z(`B##)?7udO=24?0zBwWkO)rQ{+TtO~l{YdI!%hxH zkh2+e%i?{(m7p0D@V6b@!t;>a-Lx+DJr6(p*@~@ z6Ou?!s#a?0QObk-P(pvtnj`S<>#lwu9Qd592tp8?C_v&P%Vv(C$b}>S1B#;^M~8<3 zfJ4#lBkB!H0Q{7(kmTo?kDMJzk7e`h6G+e;{_3~*Dd4-n(sksm{OCS($-&hk=s!vw zq_5na{}LfoMeAKxLDB?!N^KDGfQFPUP5PwG>!er!U16J7#xmfb5AtC?TFOrrw`)h1 z?2GQij;{U6jsuXTbBY9=`d$FZ2noI@5x%IcD?DS$AV_yGqzs~0mra-q2%-1%_0b9n zX3G`nVvan6!qLf?v5^1{7XWFrf{7ba?b{pRz^(wh&7tFA#&xPYbplH_u%F!Lxw?EA zsud?Dr1B}v=H&;EKS)*dgX|fx^w@;|YQ#5`Pb$S8#v7qXAtM3v);~Qpdg{K|WJkqT zXLK`g(&FN%zn*p>;PoU;zd4mtF=M%Dd5{`8n7cP0bmK(oDmImJ&Ev_oyaG!c1U~Va0 zxHf`1N3wW?Ut*HJasE-~_q#I)pg{Q^P>1Z7TN;hKf&oLq!NCDo{H>gBwOQvk$2C}_ zu)6K~u-uWDgieuhOj(&OD$OT1b1Tk1Gw%$+J~K`TVx1$j>;p)wi6#Q$~i2ZH3YG1#hVck(#z2)ye2Er$L{4RSZvuons z1sTB(W%jHfWmT#54Ekl`2E49i?Pf1F0M{gAa0%k1Jlk@l3fzF$ayVcA>;>8qpiF=) zRC-wjNfiJh9VS%l5zA*Bm0{%`B~{ zaqJ9)0j3@=h#_fdrRp=kfatOR`F?=>SLinb_`$3P(p%=_R#DWO1O1D$XQ#Z z4evM%yK^z*zo1{dCOBmCHJ${(QHF`V^KUvv$rywn4cUvPkD)z$jw!c(BWg4h)YJ6&0ESX}Kh);>yo9#0KlVV!sC>YB_~y{I)vPpAE0U3c{cWLq9L5`@RC zE`SNE^oe(Ef4@wH9u79BzW)4Isk+DE97T#8C?39>KVQyC$BtO=H1f}Vq>=w1L(SCl z?1U{tVYb=`z`vk~2x%Fa$b^K1@85lrzUKGl(_Ds*IZq>_rK0O-s;Cr6Q(}O7$|xF) zZ|gtgO{+IOc`*NO--9w-swh!`-se*uyjOU4hxWoDBspr||0PwE&b_qNVmh|_~# z01l%F_PtFaFeRuWWxEUPsgWk=C01FvVPSy0P%^JZ=Cc$m`bkQ`NuB4;;z#65Gj^}8 z;OC%FJ%_;U=$B91gP1U&-vCd+L%XgmU~hrASxIp*cZK&1J^ia!pyXPE+}1yM>=2|Z zy}O4cioA`}2|K)*LP8$h|<0ddHc)_Ka4`DplY zSQ*|rs%KFUZh|5ltV9GmiO$V=!t(@nYF?DjwnTlutnT;x4*9?4zf+04FI~sFE+59g zpBR|=eEyzY+(W%LQHT@saism11|?r-*k4w0oB0rtKvGc!J-(~qXa3D9i=zob1qnT0 zM!zPmXf%RY4a`#)vaFAh?hX|0hRu!{G`@>M#m}C3yr!Y0Wnpek!tZVea;v9SQ+<8g z<<;$k+;($cR11EymndaZCdxd6N2%d9;AZ&r=@US-@3^zlnBF??P15Mhx8qccBjd+s z4v!Bn7oi|=|JuOmwQXKVH>hI9vT5Y2SF?qKSbyU4y&N$yZ99ydG2_U{&yuAihM(t1 zzrDh1``fS(7J`26yY<;LjG{o}EyW~&A_%a+ZAcAw+rZR+kLdQCy-scmsnKZ>h0bh) zz^WpmKvPpQ1RgaL?3>P@XZII-8#_B5APxuztE!`O^Dha=-UuJk)zi~6H8s`S3wYxj z8k((*zqYI(0eW?^%90{yU|;~4Be)&o=f^W^U$}H3fN@R3obIYH11#%0&iVP(tWdZ4 z2TV~2C-{>u`-qulR1OsJ$h2RyB!;YfG1Y%zfiOs~sv&>LZ^8FAIF{(SL&49}(=~CR zgJxL@JwLtu8}N8b$Hk=yTALu4xtL~TNB9}iVB*PTnaG=-+Zhw!31Hj z;NW0DL5}C^U;H8ES&H5kK~Bf&b~HATYA8vIYOo|dL%z-A@#ic1ad&{&EuAsR{C-ze z=%hykf)Eo37lPh#ikFZfIKP_S<}uJL<`x=FHjKs^AHe75`#M2zBlSh23=xZVIFqY9 zB?y8_HTXcP3S`XC(G$SKoyVwqdwU=jgaa7^>RPT{UF9|imdgsS>DbT^5vSGoLA+8X zCj&S4m76pc7S_SBoossnIy(p=a551lrn3ZmWN_J7C(+@BClIH|dx-#vid1nAXoSnv zR_Wx4Qewc2nst1Y*o;v8Sz{$}O)eFtla@>ukowxCo`!e+_pTXAN?`b^OHm)aX2y-0 zL9@y$^~_e1sDkf~BN(p7wVfzIQlC)UQR%S<>n+EG_GyGoZfZhV5iFbUO=nkN~7bB~6xbn?r1Ypg;6<^(5Z=1}Jqf~oUv+#cdq z=Z`lM^D-%YlJWWEFb^SGa`=Un=E>(fKfo+al$vtP&qL@uO+ENf$!;m z8-4SG0yoA>z|h{lDlH8U`VGkS)#c^L@Nffm`Wu&GNycN41V$$l3iXje2IAYtj~^$2 z6`{L-os*LTbkdNfE$3UX>Mpz=KpTQ__97_1%`Gi}Ra8_k`S{!eFy`s$8B4+s%rH7y zTEe)#A3uK7)UX~P>7gJZd-CxSm}jKde!<7ReAi*)%FdUpfcV^@NOh<^)yX=Zyzdt{gG4@LX7ZU2 zCco>C2gh8MVcSAoCMG_oo<wj`n z2e$(9i3)P^&gbUpwJgzGeqyC$hACWs|NdQ}kNK=Z4`+E=yAB;cazM7*=m_{=LVcim^c&KwmFpU0jUH>TH^Rf4h>n3f>U z{U`xzCM*PU>uQFE%xObF#5?i3zly_Wqok!R*2)0Off55@ZXEOFOBngW?yjj+Q7Sl5 z@X`Quh*i1_4Gy-0yricOuo-}}ST7VULeA&54+ou|pWAw(e(wEHBawIQZY9ft1rzbH zJWI3@CQb_ISHSVPzq#SO=CD$-TWP%oMi2o3fd(V!8UkwEm}RXFu)qj;9rN7SV1rcB zC}gNVD$wdle{uy;%VQdliK^&@hk$S1$TaETkY~>(jvY4htM0W<+Q6I!7D|{{{!GQ&XV5qIBf?L1w!j1h`x10Ltoq+q zmX{wS2tVqa7_TfY`aC^8IHu2qqY_o9Z%rekz^hkhD#dtJa`Dyo;(V8cB1Ek(Mt&nq zM23t3Lt|-4|3DD*@6;6P>g(%M4Hj{!=-J!bOG@_o{33Yy^3Oqa$MN!ayf85n)^sq% zC}4g81Z#|@Nc;(=qDrSWSg9m|_zfnvsbY>mv&YHG+S(cl{5t@Drb?8nd0Z9&r|dNa zF*9n+D6mw3+@jp0xIa^wB~?&bPBCsOVDUL^C+@()l3N^pYDh-7s1JpQG(_r)E6bBu zTUW)y$@J{*h<8Cz%>)Vz6i5Jz)QCt9!9pPu6*C#DwG<98aG%7-I^^7dO3MoXMo{6%uA)8CzXlopS~H+z zqY%ESrE7W~CzC=*l`pAix{3$@gq*A_s5k*2t4W7};Oye^mT3kkmEf+xKfNz@6SK00 zEb9GyeL))m&-;Z7z+u4wqN%0Djvoui*NiS>pskCu^BvfC=)}Ar4d(0T2LPV6rRDWe z(<(e1oZUi0)X)|h3Eu@U0)eaoCSnmJ_dq2eM=)i@2VG04$##ly!NIVEaqeI>NmbPM zw$exti&@@C(3l>CtswZpQ&)WXa#|PMTSbl=V{%fNo}Lc#4BpJra>MZ(Xl;RkTHKGy z)JlP?0b1(7Nvp1-^JVq`n1_V8xFC}c+QqnSe$fl%I{4w#>Qtze0+X^GlwfcgfF`}t z>3z^60*QbEE-DIwyyO_53sCs=>-S)PWpTe(V@v^UViM#Sz`nk<0_{nlk%Wqy8yEMZ zEmK2!Q)sO8+o1^myYc-IGyNFlzEH7z;!2g-LnYdzfAwu`jSVp9C<#~yEW0;eXN4gl zJ$2?oU~@qb=+2utxCDOq8;}i28fMS-a9gy2IXWI@XTs4e2xOX!^{vx*HAl zxgkAm*xtRSwHR;%Th0i=d#&3`m`a?R<-tChv8dY-DFGP zB}%1M{6tefAXlCOD`5QgNf+AvzDpIntuvEd7WeY<0x9~3lvmm-I!|COL9N<3I{IgE zH@Ox&unFcj2`W|C$uc5}HFvGnmWBojzf~#KK!Jbu;u5r~4cRz4vcidiyBYfLvITbp zh|WQdEeM;lxpfam+_%iG2@?Ma6dD5& zmN;Tjpz;N*tH<|B)@;-lPCw-J@DVTH>z|StD#!GI-~lePKCkPp3LYLPS}+j4pf_hs zFA@TRPMfzI00>~uLnUR^VZg|31C66#S%7{n5?)8+f8|?81A-iz^$~(WB>Lfs?cELK1V6lLPS7FEzY3ECmy zA<&14`88NVT3YYtGY1#mPyduCl<@ui{ifCefK{ z=-{J8EP}&|NN?H{_h1AAzlJeDM;fh`Mu#7%j1!0Qvi;5I$npn^z__>(kPrZ|T|hjD zNlE?Je?-k9e&`027xaFn%23bDf6amg6AdsNUp892KrqhWgw?;gV{*bVS49Mz`69Ih z5`J#`CEhl}G1FS9c;YuUzrFw^;UAy{7Bg2L4^?sEE%xo z0iXeA^fL&wvjLX+39k{R0lxNn?kQBF#_czCS>sW;YUYO zjkAyIYso+C4~6RHNU*3AZ0)DnV`0N)f<6tl8X)1vL^Eub-bZjbW2nn_Wpdd973u<* z-{57zDDez;CebO!6Y-4p^c0kWz=h|Z_2)=o!RLkoo~NCwt7Km}UhC8tKRsstb$8T% zPRtVCYueM@{aaFr2^aZ$j{EL7fE`91{(fMefZ2 zmr+rn>_<-6!LITXhC^1J%~62@LtN-Fac9j>3_YV{ISKVBD?GIPHWqqJt9AAwB9>D; z^!g&qEXPwO+!%Q|Indhi95(15=8&LGLi8_6mZKr2R-(pSP0x5h7PYeaGH(Ykr2?(X z0_dPARtQR=#bwelHmCryh77gy?b+5V?W&5hGC2i>P-Kum_&1Z_rodEK)O!Id0&G)K zFc@GzB+G39sqWzJkEVvkudI|>TwP4iZYLpaban}s&!09s*$cvJBdg|`|FTLTy>xrE z=s>5C?g;3^&!6Cld%u4hABBR>Ft88*_Mi*ft+pFfYQ2Dmw_p192ei1;WA6j7_qd(q zu<3jY(Bn_Dv$@)LfEwO_Y72V#0DCGcTd3*=5hpM@z93Hj%cQ3ALb^y(NM zZaea(hMmDX0*zm**%sLXBvt^pj*X24qT%M|CNN5HA+U(z0g&uT48^r-TnG&XT{tfe zsuZecg##X2L9t3Du=_7l|JKu!m(=L~nIGue2fKcnq+mueh9Z9eMRDHt0ioyw{5;m>EB~wBx=9EeycB0Gx#cP`;2ZAbXkt%uXa|l9ijgda*YZ zaI+S{lWNCC{r5g+S1Yvi?*r3%yq3)e2q7F7`4E7+3KooPEgd{Eb_pc_p zL^2N4(Q_IK-)8mZU)K+6WE7R<9~n_pYFNiA;=SV9=4Pg5e%4=Xq9PNjVZABqI={u( zIJ4B1yKQqHT`(LcVX}&Gc4J?G-u%FzUJOuvtXMt-tPL&!^v&~IvHTCfnrqev90+V1 zGxk580vQV7jcOo>f;0`QblIo+^l;Xjn}}T@dk&nUBZD=G#(sNcqy{I zt@K*OxQs0?*s|ZSL?AI%?k!P0YLoD4tP}M`I1W%8@y-x4<5-=PM1?Ff1XYD` z66eF&5DVE;!Xxa2sSN0MVG-2r$}^^FE2XDCbB~s(Vn?&?SG87bR13q^vd-Q9xo|%L znuAj)m;a4B>sP^pt#M8v?JBNxz89?&2J;&VpjXrCM85v8wo0y&qX>sV{<&_{pm8Du z8gAkQ_wG(Jjf4I8@H4x+|K#&Op7y*K; zjsb6Yzy!`4Pz=z}s+p2Nti&9I@c>=&2R-XRW(R@pk`hZXv0(6K7+}kN1r{6Npv-A8 zy{6#62k(4fi|VI0Az{Tv4F>^*urlpA&=tO-e3B@wP9>mfwN|bx0^K$-rBZ4J12wg2 zrE%K1h$cQ3%_<%|ngT8LuZi)hCo+`#wBB0QRwEY-_-@)yr`zkt;7FJAq_vB1mC`;OHjVy;{&z=5fSK(1YsW?CS1&@LKS-Oeh(mW zqEm07_$dG@`#L)x+M<3Bg0z5?bV+x2w}1#pOE(D8 z9ZHKxmvo0T(kLLHbf}twB?519zcIcy-XHG_$GFOs!*TDm*P8QJGeteW&d1`i zU3;R#IqR`Fh)=q@>6@)bZA%&3=9liQH=s*cVcFR zV;-rhiuwMg&m9B1C@wa3SQ@5bJ)z-v;w*;wpQoy!-+q0PeVB|p(k*S`+tnIcari0C zm@e}bhHI(#X!7wLONH}sb*YucYp&9a`aAiRhKmRcw$GwUUNN8Nts>mmv;n_FLd5Y| zgU*0eeJb#vA>JzxfsmA;d}-Aducwk<*P`b5=HAa!wvdO9DaS6q+TpV!UV!pcR7@;a z#2xM}%I(|Gpa2=%r9NCErcg8I{I!J#z#w4%4srkkaDs;#dTDA<{DB`G4$rLFQ#gKz zNqL3q1c{?BFHgUj-ur!YGVag_ogX_JTXbwJNL&}wSo*IO8^rOu@%mpI5H0W+Zyr2K z8fbA|8m=i}KR+E-S4{_1fSXK;p)K92rW&sDfPQ`jerVCGB9A zSVu1o5Aq>>n+dHN|0~|@ou3C2uRoEr>+Wuz@ctf|$^o0_BN{$8yO@8DfX~AVk{RHk zv$U{)Hd>w@fAzK;W730sjg`AB>_*W&tF`)$&`rTFtR!wJsc&Z7W!kK)DWY(czj;HV zziAEli;KKC;zi#Y$dTEgZ-Vn_HsH5}hOTZCsFp!uNyMf*J#+pv{5D5Qn1MB!t>CCd z!H2Us=is{%68}sddWFimkO~KD?>nfj@Yae);V{x>hb0P>#Z=9tAL?#-`r=cj1>Z)X z7x{4_M>b`s5pR_IG7xZ@oeU)THIzdT4vH5Scw3y^s^7gI*r5A08$m}%>*x^0^??T$ z9~}iwK&`jh?8H56VSv>@7k?AR(jAVq+6`U6P+I3d7yV3${coMuo!6JR%j<5fsl3!T z3S6BEUFhAaTz~3Isy1ljvo;7Ae)QcMu3QJCuC=go3xdlDn`HE;ulCNu%FyAzm|<#n(7_WgdaD`kQ{l z&`-hJ$WxP(tgNh1+&LY-(tWE9)$d!miD?72Vi3;P zTNhm_(Ek`btzQtRw!Z5K+Ypv+9@=5$*b6>#-CW z4B)Qv0-MZ@!Kac!!D@5qo3o7m)^nX3S8GjCGvM!y9^zX^AV(@m(m=!0IZg_kNX0_9 z-FVd0&e>07D`s?cbfD!QU0i%pI#p%-zR85PX^b5DoH2KuYcWmxzHXvJ0ewQyt6jR5 zOsB?%Rzcd9=wkKQ%e3?FKvHeA^5TjWs618ae|#o7OJ7rixAlu08-RNrWdw?rM3y^w zl>C_}{yu%QABv{UvH6uti@0dR?l8mg%ZelVzsa%+V`j72RukJ(I?J)2q^^jnGwrCi zuS6D6VZ(D?Ix3v7+xejcNcZE4nf&~Gh^7Jz@7dDpU2Z`BG?}LX>RB$qy&fL72+PUY zC{N9<{6#wW+ndo+qhGN4M$aBhn~mq{r8Z^qd9Fc^t@7dsSlWved)S7PLS&Js(Twu2%#VdH#8|qM>B0vF z+bntIK<}6NpqonOwNkp45GP>1;1c!=n82AtAA=`_pgr z4p2a7gtM;3lIAs_xv(WPKj1n{&$s>)#*OSBUW-69Cn>ZqzWbFfOwM}pETbif zTB1{nGMA||LBj}D-I+TL%QKiWOlsr(0uQ(GV!Siskm%V2$G{$z4;em{J+};IOu}oW zCr|8>$qGjiIzxXfiZz+1Fn%gVP4?4(Ey0eEcQW&&uX6^u+dg5rs0YhP$Yh1O)6{zG zlR1xa`XF;Md5g!daj&dTw{Ay=Y?1ZhA9y67AX8vX>3A4X4{1v9htJ=>b++!W6&Bo%AMcj-XWZLtFK2cp6My5ezH(pO z-qm0d)S~_@-@EQ=<50@=7XHgVV6UKAm=Bzf+*g%x7!WJ}mM=O* z5e3DLzCjIKnRwA5v=(=BPFkKJp_nQux+VNI4Dr#RM+-(MCW+8Kk}NzOl4cb{wORB~ zO6Y!LP@?1T@YdVaX}JfrXyHsV7)5{YQab(LAQK-8-1l6Ok+S*Yep+c684x|;3ne9B zEzKhBZEd1VOr@>P-!0c3Gvx~)BOwwbr>A9$OC^wEWFbf7wH5m* zs~zD|8zMSn@7MLDD(9D&mOpmkUFZb?_AWI~`O~Lfr6fpdBVI@loQ5B0FeVx7l;}WW z2$_{L%bjvRH()fPTqr`;Na7-N&{lFVf<{7PYssmS^_*a;_4z&pz#=E@mM8VOP>+ zne7^>&o>XGFi|8o@Hz7x!-YD#F)~B3FqrQnbh6DNOLqK%7u%zKeWj}1ZP;NGA}@3~ zroO2Y!@A2xwqvI64h}zuw!G~cjV=L_?ksj5ACUamK0FbRdUmAU=bh-kI?0n+B7DHm zCa|{qjU<*{HzRBQY}v^G9s_>(EFk1un?lR0% ztD8F{a4;9fgEn{Pu>0z+WALyB%cSSY+B>>ZwRGOwh}!4ZK~eJW9OvH|+P1pY+CKNo ztwnv#BwOW!UaXZKA8&RZ)w61L_jvT`N9P{KrEx#J)5Y6Fc&BKA{*bXCBbCC5BCWR! zktmTp4<0=#)p~2hdFoUWj*S`$C&TH(0LKsH7~scPYI1tQ#l;ocK4~`!;_D+K%B&5Cv455S>~ScB zq4qmj^8VH(Wz9PeWDkJ4Z4NW#0JNN)ouQ64*4L8*{|uziH@9DZ=U0}L(PcD%83r){ z@SCgjjzF%oJ-@z!4LDcAUl1Dl>1pkjAN?`m+H~NM?arUGJc)lB!w}$Po~cA zZ^R_~+jUNc{#zCt%wkxq^ti8%C4ghvUoQ;_23u?P_;;S6A0aAOSV`KD%>5dO9f{Uak9vy71>@ zeReNzZ!T6=!zRacV0nKM`26VD)J!dZxKAoBO&B&1A!ZBtE*+Y#7jU9O^rL&nx2uI9?*eKVvm1POEb*5klX;V;C z#3AEHzq02|2fRThUjwk%V`xr6k#Y|L|8#W+t$3?+vBX^9qXXm*_vU#pHp;$CH)Qv) z625|?1UQ3kHd&DTLJepFY=M?mBEx=G7kbP$-LJEue-*cU&~L&1H7)xYA_{D&9+c6m z1r;;)XH$IFB_Q+{GtlPXd%e?Jl?GxMVAHDdUiYkKjNhStod&b4+N?xbCw7C$EP%4YWdo*e^ZMgYH&}QJfVgrw5ju3ePdGR`T!Z=_ z0FJ+hebf*p3F>ML1W;1Fz?p=EhF+g{VDawZTe+8LI_+ zzHf)Du2by;GRBOh|pZ~SF*#f@4wk~_AOXF(3f?sbo zwSt>eD$x+YAM4=+4}GjFYxESZDE_+x&)KdpKYkl5qHK0B?iUlYkXm_+nMFw3oDq_o zRo5QG%k_L!O;SmY5F9P~A$#M8{2KA>npk8}9f-rAa<*uysU(p;+#-Uc}0`c?} z++BuIL7t$ zj%wSv)-+2#jl}AAPl?vMW-p9?V-ZvEN@wLU?Fe{noGi4pJLaHxV_!6GYXG@Tp<+yV z^NrQ=$piE?xkzDn(qYR2hLC!P7Z35V*z)T9v*n17KjNeFy|c9@!jVPO44vvrYr^Ji zp)D<=M*Zq$&i#G0S#nz8*GoB3J4Az(gbsaP7UH9c0aBlu7~oCtwiW-GGoQMaSm_hC zCzl@9_ITg1n6}Q-diq_}0|-s++^ZM}gcMC~+$Q9r?X>n;gaoz!(V*FamlfI>YLivl z*SNbFM@L82yaxLEUv@AF4?Wq`53L*?zhRmQ4@ETWo>$DGR)A3jN+L@SeY~nlK$9WWg4~4~ryjsg&xooI2S1O7_k; zknx{+8Qi0su%{xZT|(yTds;Zi`x}4U6wzc>yxzT(ldGn9`sfoagn9O0TL-1+R`a}H zv{<}IgxC`ADME6t->4DwZ>GsE?f`4?pnG5`T_QA7ey(BbXD@Pd_?r;2DGS#I6(HY2 zI=N!|aHi%k_Q`cqpMsb)w?=}DZEI8YZxFieovxstZ_8-2lq*W; z%pK}_QzOE^WhQ=T2pK+eQte|dn(?9gSvrzthmC_kL@R#b;km6N%0ynNX!|Z6FABxs z(w1mG-LMG9t?B`;5#6}QQ@N&Mb7CrXC7Sra9uiyJN@bsexFjWMP4&UkoiX9IbI*gq zZx4wsJsGc_}6IRMg;=veU?CuPAKV*9}7|RV*mJ#u}R>-*w;JcPgE7^_e`L663sT47wV;kB$PwffO|p$+vKk zLLvXHI!s)##A3c8yot{tT<0^&Ikm4)T?dD$_ASDbpsMwabislzVDUaXJcDJZ*Fno64O-IqeNkheRGrzj8p2cwtg1lW!74 z`oM=UGS)Yz=C}JNKIJku^B7qaq{wo7^301SL&1)#zfZ|xQsd)YpbG%uDu#+%v?RSxSsL47lHLT$$f`_I!Zxq1(;;41sSctFS zD6e)DE@;Rg-4lPIGeY!R)^yqT$t%X+RC#VGV20N8*!~=fmbov!>rzeewf4jVA~WmO z=Lm>cIX+!-A(4mova$^e`PXp5LNh{|rmZ>O zO3&_(;!DMD(OQ$b&zhuyU=-_gx4$-B?6`JayX{ko5rTjyc-#eH@-I=?MfKWywVSdC5 zUE8KnQ^q-xUCEmGe{ruFR8zBrT`iyV} zfG%!ZZHjCJ^Z;3IjTIa+wl5%6iSSnWpH2~z$+f3xagK(qU?s`Z9YmmX9m!R7vBz&M z`Vt@^9vx{L*^F#T7w~)iemei??K}?<15hE~(C(n-1gad^pt!!pg#{qUzz0Saqsw+w zXHwG%JrNim*ae{RSD?p7LKGAhLU8ukGKcRY{Bn+R)ywI`^1U)otsnY3Ew zRM`SfY5t~1hGb!;#38eM$;MA(c{rFP_Rf`rU#pl$_ti1wQE){jZy3oouHc{egxXzJhxI%fFz zFfG2hhacXnOq++?-Il#@(b?G)w6jg=wlUcydA}onavt%b65l%P9;rzy%RMqjI|2Gi zLi@!w*X8<@eH^e}0<8{A3e7b+a#~6X1h_zF4tyRv@l#6Ub6UGR!B(T3w6wVwX}>S> zfydunXdm89N=;48%zOgbxyi{8*#ZdE&H<;N06A|&6$xe0Elx@%$j7gubcbw@O*U)!HGzl zr)_w2BtDZwq2o_oPM&79I}#uv^JO57zm)pIICJ_}e7V}a9A!?4q&7E~t5HJC!<4%3 z?b@)?$T$B^zVM#{dxK-IiCu=bY;7 z>gs~+79@L- zJvZ5*g+`DP81~s>solSM5480-Bk6n_o`1grlM2B&#-LFmkO+p=wR(644Ed933i7-A zin2+h3s;YhcL{wr1U>W?^&FW_r(omXlff1y&S9Eh5$Mh7YJgY`{h@UngC`$L3IZvj zC{U4(${pmM6^mBL`&uMC9nx|~AU>6_37UM5Y)SmuB7dtor%NG4{(2?c$0IFG^rgLm zN{9AbS8mWx`^Hf-s>$VutMaraBc~6!j92B9fsPv)<{`6YSG(d;?efumbcLYUP{56gJu2+qa#y$Rr@4n;brZFVNSD(eM+dJ+ zng0qCKM!wlKu9;!pc02d!%&*)A`t!(`=~^uHmZ>XU|&eC(u=LX_(WPZlc`#994?+c z@#ps%Jn~RqqAARuz1P@*i2IMRz?*^15P`Vy+HI?#EY#zkCrB368K*;Ci#zrCJf++s zShUpWoLMg5r@`?Xb?m1AF#zoMipOiJFr78V)lSUMT`DS#L^9wEa+wea$km262?DkO z%>c3Lfu=Am(3-pR^`)hyOrQ{(15AZ#c^*&zLzN=f#)^YS7_NBI_bN|`$*<*_*q_QQ-#!8j_31rMzZXE7%B^_`7> znzOS>gL!#elGPAl645y3z$!}%*vMRtp3<4dUGz`DPQ26fLUD9TeDCVw*G~FjJCj3~ z>=`5G^M9{)HqN#mr>GzBY>9d(gv2X*%r z>7T8P3r_I_stz`XeI?4N7}V2MhjtbbQ%S$a`G&Z|c*^}Lt*%k4CJnMVqOBSeU2!HA zs9gRQZowfc^so0ED)^)o#P3{oCNJsp7t~A5C$4NIDS%X#qa)dKNeO`fh>> z;2j|<2lmnFd^xl)SuGATtom6uh8);3!wZ9N-MY11wE?ZVP5n7YN)RtVJLm+SN5FDs zc}YQgy*r+djQH5s7umNaa2k59@gX(LTDD!|nN{M1x}&3E)L+F!HRrIwipE-Ce)tC< z!09wkuU8dzG4*#}62c|_X7B9CA}tBG{K0P(yN5vVi$2y0E>J00%o6z*C6Ub0~}Emd^Z^ptuDZ^;if zOy7*$$T|7bCPL7zzn#sh)kQ)rTo2@iB;wYubmI2T&bZDb*b!h(RW*49fYo$6UhY`) zNl(#)ok}sNrxnx=kNT+|XND2c^>k!*w>UB&MTa1)xMdAPNh4+8EQ>zRs18=sr>TVa z6Zb;QdFELB2Lk@cQ97VDeR&g!5Gv=U{=tTu2w-XU!a5@o)(eY%8) z<6vt!B8>&Ip_!hA>am+D ziBUMc@unQ0hRa69#zsWufpw!yD9{=12DDhf#1F56&fxBSMg9N|lmdGJx(vda5Bj*g zDLZ4oAxzG)&5h#|B=vO2oAyt=8K$X_{n^WaK#VgNiKXG?-j96nMysr!^Ko^K8TCmC zHk^T#qh{5Wx}FCNHB>{z_)^E-hc*5Mc(|Aly&sPhj4(#t+Mvd$C6P~&ckcUGe$1GS z#Cp%c9A6fXdHpG`O0psWe>Iz>v~bmD&lmeKP)zoKz3KDk@DZER;^MCMS%|cNj#jVL zD7I%6aNb&wnzb=CJ)Hx6ZO`1?%`_3%l^~AsM%CnZ?0`H+moxwh9t<2D;1aytznzK| zt}n5E-i zN4}kD`-X(@`=nOfddE(4co&Mqutx0Og2p>eOr%}M=sEJdq5u)3X!g5yaDw%SxvJSP zzcr=tNX3y>#4UjSuTk9!W7&w$eerMAGnI|w%+ zzd7RA&K+zBwdqy>S|3c#!yV+XM_+Lwj@B_W{0lfK5(0$dE!IQCh>x%tgAwawZ>d7J z;!(D!j_RA~5MV?h-i!sTpS--kS9>BUa7qD0b4d(`7)bqwHjYIY;&J0Q%RFUi7jh6i z%)PFb&Kq|!BICt1dQGqyV}nlPxb|4W5N}LD5ddm4fPz3u2T79q z^Yu(DEEu{%xsri?0NPg(#5K|kX9_{LU|CD!1l2}pm$HhAL*56-!E<%p`cik_G4b5( zATf-QZ`#)P>Ws?k4sL3W&CuklWORD|@xGcWE^Z{@WMyZ}1p?>ofQZz(4C zMsO_9o+U5x@F1ywy5J(jLpgR*C0`6*{o~ag29W~cN_H^J2NQ(-hd$us1Jn#~r}=pi zCZ@;%tB!{fxnvOO0PM($yfOT5@Tu1qvA(_MC9&5&|iQJj13bMUdq#qUKPZmr@8voLOFKVEG*zeNuwPRgS-y(T+C zA12RTxH&h?D+s#5O9)9sX^Q|ULfFEJOADODO&W0KzmzZVR9O>}WhNB#PRc~%dI0$p zYR=|H_-rv1ee>sHs~6#dV}6H9s8rF|o=oeyWynTk__>iZ%C_7+kH4RycmD-L&WHAP zP~52Jh>Ljr(nov*xiYwRKn4s8VfpJ=9?`?tG)4_n4DeSw1j>=wLOQJu*|u@W^=qDb zT(M&1X9?S7_fqIQ=LD0zw=daYKGavxJ%b{~7aloSRDjg}wMr%dy9XBc zd4ME>Mo{oZAOqt3fq{X2y%{)B8w{P`7%Yt54RR>EEBtg#ZMbHfU!x(c#? z^P9BYfF&HWvHEIs99w1}{MG6%sq>RV+pq*m(xmr%oDz47h*>=%$RaI6F<1%8I3@2^ zli7~tw=+E<<}&#=qi5Q+2m<+#zFfnq6F~cOlePC2&HkMo)?yt)F>KEdwwnPED^w*l zcc4Mi1K1z{I=rlAf`+Vamd*;4C6HeM>kXEf`NIobG^0$Dnwrzi=5Ik*Y*W+GkSps` z7!d|(8$sp`kiAM4D1j;a=$%y)zglPQqq+9_2ZuUhy zMFqCvRIo2+OeXgl-0~X*keK%gSRD5*g3!eM{=Dp4gMgKND0kojB@I|n;B0{S3f4Q~ z#fuk+vrM;(6DOR=q|8i+`rV0moF(QBLr=`PNuGM&0E_|>5|mVBof0>M+um1^9SOJ} zgTuqqJI5?6EbEGi15n1@nN9_qEG?j9mjK z(l`j$SLNU;%h4qOR~1ZCCWt|cuEMS{wxqYPo0fG|@%y5>pS(rXdB1bUFSNDw`loKd zB>J5@yAZPZ!8k1p4@E&q>3)K;zrX((6HesL()&~RsVlzsqd$uV{SyXLNgNqAD#|T$ zT5~fqGY^kHVvQS~^kKV-FbxS9Izr6baeeNsMku%H%zIEbrJf0kibAI#P9RERKd|6>XEd=K&&<{EqciIVb-%9^)CWbTUTWJQX3-F7t6`XVOZ6)8paAy zltP&!E@qr(Tv-;d;^7WLcj2_quzGZ!iV^|P*aCC-XN#F_@6p(doSfL&TiUjF)e9$J zvKhQUpN7Run;|K1kAf3PX$r$ z3L!9jHp8`&m@?xq5(VDx#tR0P@WKfnNLL}m1pHaLC*jx#HA2Du!ECE8|9wYCN3*Ru zKTS=|LkMXZ&5^i(O93XaTL=y2pD}Z=mNS&-3+gRW_4t^k5lI?qTpy~pD==IqeE(MMd z3{wIRh+#KNt^O|v;n1xyp(G>Q-oJTCS$uj4CyRcriKb>9aQ=$U^$OxR>r7uyA(u`# z#0VMx2o&d+_`O-fw3+wDY+p;O+G?O4V*9$7rC$rz?IwB*r+jne*(#dVb=C5 zKa_}|k_gh(a-^P1B38f?;qs;*vJ^Z-jp13jnd|_jcMgMhMav3lP%_;(*x0B6 zO$Cpt3-yI4?EM+5uI@1K=kPoa)FU_kygv@mx!w#5^Oo3g!sdfL?|^pb1Rg&g5^Sot zzO_SPif2$;;i23iX<=o}-f!JPFX#(6aSlBM^e= zhD~sm-3tC_QHL2`kP=p9#}Cn&j7j!9J3^S?QS4fEQ7iE_xx@M&WZ1lX^kaYk+|upx z<_77tjx|r3!FI$z55pM2iKJI$c>5mMLfJ2$#aA4AzwNh zZor3AxC4<0(1-!A?Zlg&n3P10^I{lGmjeSgLw?Xq8XB1#71%OJt0c_X3@3)|7|hu^ zikmTHbF+S3;Qrz; z1V=Ii1$>#GZ?6j)Vns)hQDY=HnS6%0dGiioFceND+Kk)r-*}}!aH92=pO3FzcK{Xy z9E|`?JlP)}9bK-oMEiAu0kOcpU}J+08vO3McR;QEyk7Bv`|}jTiC1y2{$pD-jg9Zf zXt=t9nH>5qNGw%j1O!u7SHc{0(3;H2g@yNFFU-cX8GZvXTd-Nf7x1IRJvQ-BWY}~o zrSml)aS2X>7)4vy%}>L`sm9z|=kno#d`=4oT{xVAf|+*^Z^V7yfgJ?if556y_de8O zHyQyb*+FRtX-r_bBvm)Yl+gg$s)4~g7*5Rw)`671X&lSSfLB>@5&j~vbPH}6UBZ`I zHFY+OCx_OwOiX==PN%RIUH)(*@=}S8t8^~4{}@|j6~I9hFtNxl$1_RZ-NUUq|pHRp@Zj>r=RbkIXmw|$+-u# z0+dg{W^|HSIf~SI$CCtNR@KDhAurl|s7PWMp-TL1t6 literal 0 HcmV?d00001 diff --git a/docs/environments/mujoco/action_space_figures/credits b/docs/environments/mujoco/action_space_figures/credits new file mode 100644 index 000000000..6529e36dd --- /dev/null +++ b/docs/environments/mujoco/action_space_figures/credits @@ -0,0 +1,2 @@ +The figures were first made my Christian Schroeder de Witt (under no license), and he provided permission to use them on this repo +Then modified by @Kallinteris-Andreas diff --git a/docs/environments/mujoco/action_space_figures/half_cheetah.png b/docs/environments/mujoco/action_space_figures/half_cheetah.png new file mode 100644 index 0000000000000000000000000000000000000000..6d9150ac6e29571fff058d90757fe2df1fadeb5b GIT binary patch literal 28167 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

lZ5%v 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 literal 0 HcmV?d00001 diff --git a/docs/environments/mujoco/action_space_figures/pusher.png b/docs/environments/mujoco/action_space_figures/pusher.png new file mode 100644 index 0000000000000000000000000000000000000000..fb78bbc1eb628afd2dfe29f965df7e97477d82bd GIT binary patch literal 18746 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 H=>Pu!@$;sn literal 0 HcmV?d00001 diff --git a/docs/environments/mujoco/action_space_figures/reacher.png b/docs/environments/mujoco/action_space_figures/reacher.png new file mode 100644 index 0000000000000000000000000000000000000000..ebf2e3eeab80dc8191bb8aff8fc19d948f60f5d4 GIT binary patch literal 16528 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 UBoXt+PKjuO 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 literal 0 HcmV?d00001 diff --git a/gymnasium/envs/mujoco/ant_v5.py b/gymnasium/envs/mujoco/ant_v5.py index d4f3435d5..5dd48d62c 100644 --- a/gymnasium/envs/mujoco/ant_v5.py +++ b/gymnasium/envs/mujoco/ant_v5.py @@ -38,6 +38,10 @@ class AntEnv(MujocoEnv, utils.EzPickle): For more information see section "Version History". ## Action Space + ```{figure} action_space_figures/ant.png + :name: ant + ``` + The action space is a `Box(-1, 1, (8,), float32)`. An action represents the torques applied at the hinge joints. | Num | Action | Control Min | Control Max | Name (in corresponding XML file) | Joint | Type (Unit) | diff --git a/gymnasium/envs/mujoco/half_cheetah_v5.py b/gymnasium/envs/mujoco/half_cheetah_v5.py index a58fc8583..a5989f773 100644 --- a/gymnasium/envs/mujoco/half_cheetah_v5.py +++ b/gymnasium/envs/mujoco/half_cheetah_v5.py @@ -41,6 +41,10 @@ class HalfCheetahEnv(MujocoEnv, utils.EzPickle): ## Action Space + ```{figure} action_space_figures/half_cheetah.png + :name: half_cheetah + ``` + The action space is a `Box(-1, 1, (6,), float32)`. An action represents the torques applied at the hinge joints. | Num | Action | Control Min | Control Max | Name (in corresponding XML file) | Joint | Type (Unit) | diff --git a/gymnasium/envs/mujoco/hopper_v5.py b/gymnasium/envs/mujoco/hopper_v5.py index c058c3de4..093199262 100644 --- a/gymnasium/envs/mujoco/hopper_v5.py +++ b/gymnasium/envs/mujoco/hopper_v5.py @@ -42,6 +42,10 @@ class HopperEnv(MujocoEnv, utils.EzPickle): For more information see section "Version History". ## Action Space + ```{figure} action_space_figures/hopper.png + :name: hopper + ``` + The action space is a `Box(-1, 1, (3,), float32)`. An action represents the torques applied at the hinge joints. | Num | Action | Control Min | Control Max | Name (in corresponding XML file) | Joint | Type (Unit) | diff --git a/gymnasium/envs/mujoco/humanoid_v5.py b/gymnasium/envs/mujoco/humanoid_v5.py index 778ac23e0..625ea26e5 100644 --- a/gymnasium/envs/mujoco/humanoid_v5.py +++ b/gymnasium/envs/mujoco/humanoid_v5.py @@ -46,6 +46,10 @@ class HumanoidEnv(MujocoEnv, utils.EzPickle): ## Action Space + ```{figure} action_space_figures/humanoid.png + :name: humanoid + ``` + The action space is a `Box(-1, 1, (17,), float32)`. An action represents the torques applied at the hinge joints. | Num | Action | Control Min | Control Max | Name (in corresponding XML file) | Joint | Type (Unit) | diff --git a/gymnasium/envs/mujoco/humanoidstandup_v5.py b/gymnasium/envs/mujoco/humanoidstandup_v5.py index fe6330560..2aa1d918a 100644 --- a/gymnasium/envs/mujoco/humanoidstandup_v5.py +++ b/gymnasium/envs/mujoco/humanoidstandup_v5.py @@ -40,6 +40,10 @@ class HumanoidStandupEnv(MujocoEnv, utils.EzPickle): ## Action Space + ```{figure} action_space_figures/humanoid.png + :name: humanoid + ``` + The action space is a `Box(-1, 1, (17,), float32)`. An action represents the torques applied at the hinge joints. | Num | Action | Control Min | Control Max | Name (in corresponding XML file) | Joint | Type (Unit) | diff --git a/gymnasium/envs/mujoco/pusher_v5.py b/gymnasium/envs/mujoco/pusher_v5.py index c1be462ce..d0e7e8b32 100644 --- a/gymnasium/envs/mujoco/pusher_v5.py +++ b/gymnasium/envs/mujoco/pusher_v5.py @@ -34,6 +34,10 @@ class PusherEnv(MujocoEnv, utils.EzPickle): ## Action Space + ```{figure} action_space_figures/pusher.png + :name: pusher + ``` + The action space is a `Box(-2, 2, (7,), float32)`. An action `(a, b)` represents the torques applied at the hinge joints. | Num | Action | Control Min | Control Max | Name (in corresponding XML file) | Joint | Type (Unit) | diff --git a/gymnasium/envs/mujoco/reacher_v5.py b/gymnasium/envs/mujoco/reacher_v5.py index 6b0bdd630..3005480a5 100644 --- a/gymnasium/envs/mujoco/reacher_v5.py +++ b/gymnasium/envs/mujoco/reacher_v5.py @@ -30,6 +30,10 @@ class ReacherEnv(MujocoEnv, utils.EzPickle): ## Action Space + ```{figure} action_space_figures/reacher.png + :name: reacher + ``` + The action space is a `Box(-1, 1, (2,), float32)`. An action `(a, b)` represents the torques applied at the hinge joints. | Num | Action | Control Min | Control Max |Name (in corresponding XML file)| Joint | Type (Unit) | diff --git a/gymnasium/envs/mujoco/swimmer_v5.py b/gymnasium/envs/mujoco/swimmer_v5.py index df727261e..8d8166e1c 100644 --- a/gymnasium/envs/mujoco/swimmer_v5.py +++ b/gymnasium/envs/mujoco/swimmer_v5.py @@ -47,6 +47,10 @@ class SwimmerEnv(MujocoEnv, utils.EzPickle): ## Action Space + ```{figure} action_space_figures/swimmer.png + :name: swimmer + ``` + The action space is a `Box(-1, 1, (2,), float32)`. An action represents the torques applied between *links* | Num | Action | Control Min | Control Max | Name (in corresponding XML file) | Joint | Type (Unit) | diff --git a/gymnasium/envs/mujoco/walker2d_v5.py b/gymnasium/envs/mujoco/walker2d_v5.py index aa43a9875..ee623850a 100644 --- a/gymnasium/envs/mujoco/walker2d_v5.py +++ b/gymnasium/envs/mujoco/walker2d_v5.py @@ -43,6 +43,10 @@ class Walker2dEnv(MujocoEnv, utils.EzPickle): ## Action Space + ```{figure} action_space_figures/walker2d.png + :name: walker2d + ``` + The action space is a `Box(-1, 1, (6,), float32)`. An action represents the torques applied at the hinge joints. | Num | Action | Control Min | Control Max | Name (in corresponding XML file) | Joint | Type (Unit) | From 14fb4d818a3586e6b8f97c28df7845cbd57fd76c Mon Sep 17 00:00:00 2001 From: Kallinteris Andreas Date: Tue, 5 Dec 2023 19:56:07 +0200 Subject: [PATCH 12/29] replace `flat.copy()` with `flatten()` --- gymnasium/envs/mujoco/ant_v5.py | 6 +++--- gymnasium/envs/mujoco/half_cheetah_v5.py | 4 ++-- gymnasium/envs/mujoco/hopper_v5.py | 4 ++-- gymnasium/envs/mujoco/humanoid_v5.py | 12 ++++++------ gymnasium/envs/mujoco/humanoidstandup_v5.py | 12 ++++++------ gymnasium/envs/mujoco/pusher_v5.py | 4 ++-- gymnasium/envs/mujoco/reacher_v5.py | 6 +++--- gymnasium/envs/mujoco/swimmer_v5.py | 4 ++-- gymnasium/envs/mujoco/walker2d_v5.py | 4 ++-- 9 files changed, 28 insertions(+), 28 deletions(-) diff --git a/gymnasium/envs/mujoco/ant_v5.py b/gymnasium/envs/mujoco/ant_v5.py index 4efd7ced5..5f15ad2cd 100644 --- a/gymnasium/envs/mujoco/ant_v5.py +++ b/gymnasium/envs/mujoco/ant_v5.py @@ -404,14 +404,14 @@ def step(self, action): return observation, reward, terminated, False, info def _get_obs(self): - position = self.data.qpos.flat.copy() - velocity = self.data.qvel.flat.copy() + position = self.data.qpos.flatten() + velocity = self.data.qvel.flatten() if self._exclude_current_positions_from_observation: position = position[2:] if self._include_cfrc_ext_in_observation: - contact_force = self.contact_forces[1:].flat.copy() + contact_force = self.contact_forces[1:].flatten() return np.concatenate((position, velocity, contact_force)) else: return np.concatenate((position, velocity)) diff --git a/gymnasium/envs/mujoco/half_cheetah_v5.py b/gymnasium/envs/mujoco/half_cheetah_v5.py index a5989f773..6499ca7fb 100644 --- a/gymnasium/envs/mujoco/half_cheetah_v5.py +++ b/gymnasium/envs/mujoco/half_cheetah_v5.py @@ -263,8 +263,8 @@ def step(self, action): return observation, reward, False, False, info def _get_obs(self): - position = self.data.qpos.flat.copy() - velocity = self.data.qvel.flat.copy() + position = self.data.qpos.flatten() + velocity = self.data.qvel.flatten() if self._exclude_current_positions_from_observation: position = position[1:] diff --git a/gymnasium/envs/mujoco/hopper_v5.py b/gymnasium/envs/mujoco/hopper_v5.py index 093199262..a1f1086fc 100644 --- a/gymnasium/envs/mujoco/hopper_v5.py +++ b/gymnasium/envs/mujoco/hopper_v5.py @@ -301,8 +301,8 @@ def terminated(self): return terminated def _get_obs(self): - position = self.data.qpos.flat.copy() - velocity = np.clip(self.data.qvel.flat.copy(), -10, 10) + position = self.data.qpos.flatten() + velocity = np.clip(self.data.qvel.flatten(), -10, 10) if self._exclude_current_positions_from_observation: position = position[1:] diff --git a/gymnasium/envs/mujoco/humanoid_v5.py b/gymnasium/envs/mujoco/humanoid_v5.py index c3afc977a..b0f078ec0 100644 --- a/gymnasium/envs/mujoco/humanoid_v5.py +++ b/gymnasium/envs/mujoco/humanoid_v5.py @@ -454,24 +454,24 @@ def terminated(self): return terminated def _get_obs(self): - position = self.data.qpos.flat.copy() - velocity = self.data.qvel.flat.copy() + position = self.data.qpos.flatten() + velocity = self.data.qvel.flatten() if self._include_cinert_in_observation is True: - com_inertia = self.data.cinert[1:].flat.copy() + com_inertia = self.data.cinert[1:].flatten() else: com_inertia = np.array([]) if self._include_cvel_in_observation is True: - com_velocity = self.data.cvel[1:].flat.copy() + com_velocity = self.data.cvel[1:].flatten() else: com_velocity = np.array([]) if self._include_qfrc_actuator_in_observation is True: - actuator_forces = self.data.qfrc_actuator[6:].flat.copy() + actuator_forces = self.data.qfrc_actuator[6:].flatten() else: actuator_forces = np.array([]) if self._include_cfrc_ext_in_observation is True: - external_contact_forces = self.data.cfrc_ext[1:].flat.copy() + external_contact_forces = self.data.cfrc_ext[1:].flatten() else: external_contact_forces = np.array([]) diff --git a/gymnasium/envs/mujoco/humanoidstandup_v5.py b/gymnasium/envs/mujoco/humanoidstandup_v5.py index ce6ab01c0..46e591ac8 100644 --- a/gymnasium/envs/mujoco/humanoidstandup_v5.py +++ b/gymnasium/envs/mujoco/humanoidstandup_v5.py @@ -405,24 +405,24 @@ def __init__( } def _get_obs(self): - position = self.data.qpos.flat.copy() - velocity = self.data.qvel.flat.copy() + position = self.data.qpos.flatten() + velocity = self.data.qvel.flatten() if self._include_cinert_in_observation is True: - com_inertia = self.data.cinert[1:].flat.copy() + com_inertia = self.data.cinert[1:].flatten() else: com_inertia = np.array([]) if self._include_cvel_in_observation is True: - com_velocity = self.data.cvel[1:].flat.copy() + com_velocity = self.data.cvel[1:].flatten() else: com_velocity = np.array([]) if self._include_qfrc_actuator_in_observation is True: - actuator_forces = self.data.qfrc_actuator[6:].flat.copy() + actuator_forces = self.data.qfrc_actuator[6:].flatten() else: actuator_forces = np.array([]) if self._include_cfrc_ext_in_observation is True: - external_contact_forces = self.data.cfrc_ext[1:].flat.copy() + external_contact_forces = self.data.cfrc_ext[1:].flatten() else: external_contact_forces = np.array([]) diff --git a/gymnasium/envs/mujoco/pusher_v5.py b/gymnasium/envs/mujoco/pusher_v5.py index d0e7e8b32..99d4eabf0 100644 --- a/gymnasium/envs/mujoco/pusher_v5.py +++ b/gymnasium/envs/mujoco/pusher_v5.py @@ -266,8 +266,8 @@ def reset_model(self): def _get_obs(self): return np.concatenate( [ - self.data.qpos.flat[:7], - self.data.qvel.flat[:7], + self.data.qpos.flatten()[:7], + self.data.qvel.flatten()[:7], self.get_body_com("tips_arm"), self.get_body_com("object"), self.get_body_com("goal"), diff --git a/gymnasium/envs/mujoco/reacher_v5.py b/gymnasium/envs/mujoco/reacher_v5.py index 3005480a5..e8ba867d1 100644 --- a/gymnasium/envs/mujoco/reacher_v5.py +++ b/gymnasium/envs/mujoco/reacher_v5.py @@ -231,13 +231,13 @@ def reset_model(self): return self._get_obs() def _get_obs(self): - theta = self.data.qpos.flat[:2] + theta = self.data.qpos.flatten()[:2] return np.concatenate( [ np.cos(theta), np.sin(theta), - self.data.qpos.flat[2:], - self.data.qvel.flat[:2], + self.data.qpos.flatten()[2:], + self.data.qvel.flatten()[:2], (self.get_body_com("fingertip") - self.get_body_com("target"))[:2], ] ) diff --git a/gymnasium/envs/mujoco/swimmer_v5.py b/gymnasium/envs/mujoco/swimmer_v5.py index 8d8166e1c..c49267bce 100644 --- a/gymnasium/envs/mujoco/swimmer_v5.py +++ b/gymnasium/envs/mujoco/swimmer_v5.py @@ -256,8 +256,8 @@ def step(self, action): return observation, reward, False, False, info def _get_obs(self): - position = self.data.qpos.flat.copy() - velocity = self.data.qvel.flat.copy() + position = self.data.qpos.flatten() + velocity = self.data.qvel.flatten() if self._exclude_current_positions_from_observation: position = position[2:] diff --git a/gymnasium/envs/mujoco/walker2d_v5.py b/gymnasium/envs/mujoco/walker2d_v5.py index ee623850a..dde3e9b03 100644 --- a/gymnasium/envs/mujoco/walker2d_v5.py +++ b/gymnasium/envs/mujoco/walker2d_v5.py @@ -295,8 +295,8 @@ def terminated(self): return terminated def _get_obs(self): - position = self.data.qpos.flat.copy() - velocity = np.clip(self.data.qvel.flat.copy(), -10, 10) + position = self.data.qpos.flatten() + velocity = np.clip(self.data.qvel.flatten(), -10, 10) if self._exclude_current_positions_from_observation: position = position[1:] From 47a705928888a54b2bb5a1cfba8d53aa3b6f5b92 Mon Sep 17 00:00:00 2001 From: Kallinteris Andreas Date: Wed, 6 Dec 2023 10:32:01 +0200 Subject: [PATCH 13/29] add `MuJoCo.test_model_sensors` --- tests/envs/mujoco/test_mujoco_v5.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/envs/mujoco/test_mujoco_v5.py b/tests/envs/mujoco/test_mujoco_v5.py index b68d8b5ff..8e5beed3c 100644 --- a/tests/envs/mujoco/test_mujoco_v5.py +++ b/tests/envs/mujoco/test_mujoco_v5.py @@ -627,6 +627,26 @@ def test_model_object_count(version: str): assert env.model.ntendon == 0 +# note: fails with `mujoco-mjx==3.0.1` +@pytest.mark.parametrize("version", ["v5", "v4", "v3", "v2"]) +def test_model_sensors(version: str): + """Verify that all the sensors of the model are loaded.""" + env = gym.make(f"Ant-{version}").unwrapped + assert env.data.cfrc_ext.shape == (14, 6) + + env = gym.make(f"Humanoid-{version}").unwrapped + assert env.data.cinert.shape == (14, 10) + assert env.data.cvel.shape == (14, 6) + assert env.data.qfrc_actuator.shape == (23,) + assert env.data.cfrc_ext.shape == (14, 6) + + env = gym.make(f"HumanoidStandup-{version}").unwrapped + assert env.data.cinert.shape == (14, 10) + assert env.data.cvel.shape == (14, 6) + assert env.data.qfrc_actuator.shape == (23,) + assert env.data.cfrc_ext.shape == (14, 6) + + def test_dt(): """Assert that env.dt gets assigned correctly.""" env_a = gym.make("Ant-v5", include_cfrc_ext_in_observation=False).unwrapped From 9dc31e26b99efa752026b3f0641dd8a881c3957c Mon Sep 17 00:00:00 2001 From: Kallinteris Andreas Date: Wed, 6 Dec 2023 10:46:26 +0200 Subject: [PATCH 14/29] `test_model_sensors` remove check for standup `v3` --- tests/envs/mujoco/test_mujoco_v5.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/envs/mujoco/test_mujoco_v5.py b/tests/envs/mujoco/test_mujoco_v5.py index 8e5beed3c..2a8df3c2b 100644 --- a/tests/envs/mujoco/test_mujoco_v5.py +++ b/tests/envs/mujoco/test_mujoco_v5.py @@ -640,11 +640,12 @@ def test_model_sensors(version: str): assert env.data.qfrc_actuator.shape == (23,) assert env.data.cfrc_ext.shape == (14, 6) - env = gym.make(f"HumanoidStandup-{version}").unwrapped - assert env.data.cinert.shape == (14, 10) - assert env.data.cvel.shape == (14, 6) - assert env.data.qfrc_actuator.shape == (23,) - assert env.data.cfrc_ext.shape == (14, 6) + if version != "v3": # HumanoidStandup v3 does not exist + env = gym.make(f"HumanoidStandup-{version}").unwrapped + assert env.data.cinert.shape == (14, 10) + assert env.data.cvel.shape == (14, 6) + assert env.data.qfrc_actuator.shape == (23,) + assert env.data.cfrc_ext.shape == (14, 6) def test_dt(): From bededa31233fd6a3384215f706c0fa58aae143fe Mon Sep 17 00:00:00 2001 From: Kallinteris Andreas Date: Wed, 6 Dec 2023 14:01:21 +0200 Subject: [PATCH 15/29] factorize `_get_rew()` out of `step` --- gymnasium/envs/mujoco/ant_v5.py | 31 ++++++++++------ gymnasium/envs/mujoco/half_cheetah_v5.py | 22 +++++++---- gymnasium/envs/mujoco/hopper_v5.py | 29 +++++++++------ gymnasium/envs/mujoco/humanoid_v5.py | 37 +++++++++++-------- gymnasium/envs/mujoco/humanoidstandup_v5.py | 26 ++++++++----- .../mujoco/inverted_double_pendulum_v5.py | 18 +++++---- gymnasium/envs/mujoco/pusher_v5.py | 21 +++++++---- gymnasium/envs/mujoco/reacher_v5.py | 21 +++++++---- gymnasium/envs/mujoco/swimmer_v5.py | 22 +++++++---- gymnasium/envs/mujoco/walker2d_v5.py | 30 +++++++++------ 10 files changed, 161 insertions(+), 96 deletions(-) diff --git a/gymnasium/envs/mujoco/ant_v5.py b/gymnasium/envs/mujoco/ant_v5.py index 5f15ad2cd..ecda862be 100644 --- a/gymnasium/envs/mujoco/ant_v5.py +++ b/gymnasium/envs/mujoco/ant_v5.py @@ -376,6 +376,22 @@ def step(self, action): xy_velocity = (xy_position_after - xy_position_before) / self.dt x_velocity, y_velocity = xy_velocity + observation = self._get_obs() + reward, reward_info = self._get_rew(x_velocity, action) + terminated = self.terminated + info = { + "x_position": self.data.qpos[0], + "y_position": self.data.qpos[1], + "distance_from_origin": np.linalg.norm(self.data.qpos[0:2], ord=2), + "x_velocity": x_velocity, + "y_velocity": y_velocity, + } | reward_info + + if self.render_mode == "human": + self.render() + return observation, reward, terminated, False, info + + def _get_rew(self, x_velocity: float, action): forward_reward = x_velocity * self._forward_reward_weight healthy_reward = self.healthy_reward rewards = forward_reward + healthy_reward @@ -383,25 +399,16 @@ def step(self, action): ctrl_cost = self.control_cost(action) contact_cost = self.contact_cost costs = ctrl_cost + contact_cost - - observation = self._get_obs() reward = rewards - costs - terminated = self.terminated - info = { + + reward_info = { "reward_forward": forward_reward, "reward_ctrl": -ctrl_cost, "reward_contact": -contact_cost, "reward_survive": healthy_reward, - "x_position": self.data.qpos[0], - "y_position": self.data.qpos[1], - "distance_from_origin": np.linalg.norm(self.data.qpos[0:2], ord=2), - "x_velocity": x_velocity, - "y_velocity": y_velocity, } - if self.render_mode == "human": - self.render() - return observation, reward, terminated, False, info + return reward, reward_info def _get_obs(self): position = self.data.qpos.flatten() diff --git a/gymnasium/envs/mujoco/half_cheetah_v5.py b/gymnasium/envs/mujoco/half_cheetah_v5.py index 6499ca7fb..0432c4e99 100644 --- a/gymnasium/envs/mujoco/half_cheetah_v5.py +++ b/gymnasium/envs/mujoco/half_cheetah_v5.py @@ -245,23 +245,29 @@ def step(self, action): x_position_after = self.data.qpos[0] x_velocity = (x_position_after - x_position_before) / self.dt - ctrl_cost = self.control_cost(action) - - forward_reward = self._forward_reward_weight * x_velocity - observation = self._get_obs() - reward = forward_reward - ctrl_cost + reward, reward_info = self._get_rew(x_velocity, action) info = { "x_position": x_position_after, "x_velocity": x_velocity, - "reward_forward": forward_reward, - "reward_ctrl": -ctrl_cost, - } + } | reward_info if self.render_mode == "human": self.render() return observation, reward, False, False, info + def _get_rew(self, x_velocity: float, action): + forward_reward = self._forward_reward_weight * x_velocity + ctrl_cost = self.control_cost(action) + + reward = forward_reward - ctrl_cost + + reward_info = { + "reward_forward": forward_reward, + "reward_ctrl": -ctrl_cost, + } + return reward, reward_info + def _get_obs(self): position = self.data.qpos.flatten() velocity = self.data.qvel.flatten() diff --git a/gymnasium/envs/mujoco/hopper_v5.py b/gymnasium/envs/mujoco/hopper_v5.py index a1f1086fc..a207f09f5 100644 --- a/gymnasium/envs/mujoco/hopper_v5.py +++ b/gymnasium/envs/mujoco/hopper_v5.py @@ -316,29 +316,36 @@ def step(self, action): x_position_after = self.data.qpos[0] x_velocity = (x_position_after - x_position_before) / self.dt - ctrl_cost = self.control_cost(action) + observation = self._get_obs() + reward, reward_info = self._get_rew(x_velocity, action) + terminated = self.terminated + info = { + "x_position": x_position_after, + "z_distance_from_origin": self.data.qpos[1] - self.init_qpos[1], + "x_velocity": x_velocity, + } | reward_info + + if self.render_mode == "human": + self.render() + return observation, reward, terminated, False, info + def _get_rew(self, x_velocity: float, action): forward_reward = self._forward_reward_weight * x_velocity healthy_reward = self.healthy_reward + ctrl_cost = self.control_cost(action) + rewards = forward_reward + healthy_reward costs = ctrl_cost - - observation = self._get_obs() reward = rewards - costs - terminated = self.terminated - info = { + + reward_info = { "reward_forward": forward_reward, "reward_ctrl": -ctrl_cost, "reward_survive": healthy_reward, - "x_position": x_position_after, - "z_distance_from_origin": self.data.qpos[1] - self.init_qpos[1], - "x_velocity": x_velocity, } - if self.render_mode == "human": - self.render() - return observation, reward, terminated, False, info + return reward, reward_info def reset_model(self): noise_low = -self._reset_noise_scale diff --git a/gymnasium/envs/mujoco/humanoid_v5.py b/gymnasium/envs/mujoco/humanoid_v5.py index b0f078ec0..d7697cca8 100644 --- a/gymnasium/envs/mujoco/humanoid_v5.py +++ b/gymnasium/envs/mujoco/humanoid_v5.py @@ -497,23 +497,10 @@ def step(self, action): xy_velocity = (xy_position_after - xy_position_before) / self.dt x_velocity, y_velocity = xy_velocity - ctrl_cost = self.control_cost(action) - contact_cost = self.contact_cost - costs = ctrl_cost + contact_cost - - forward_reward = self._forward_reward_weight * x_velocity - healthy_reward = self.healthy_reward - - rewards = forward_reward + healthy_reward - observation = self._get_obs() - reward = rewards - costs + reward, reward_info = self._get_rew(x_velocity, action) terminated = self.terminated info = { - "reward_survive": healthy_reward, - "reward_forward": forward_reward, - "reward_ctrl": -ctrl_cost, - "reward_contact": -contact_cost, "x_position": self.data.qpos[0], "y_position": self.data.qpos[1], "tendon_lenght": self.data.ten_length, @@ -521,12 +508,32 @@ def step(self, action): "distance_from_origin": np.linalg.norm(self.data.qpos[0:2], ord=2), "x_velocity": x_velocity, "y_velocity": y_velocity, - } + } | reward_info if self.render_mode == "human": self.render() return observation, reward, terminated, False, info + def _get_rew(self, x_velocity: float, action): + forward_reward = self._forward_reward_weight * x_velocity + healthy_reward = self.healthy_reward + + ctrl_cost = self.control_cost(action) + contact_cost = self.contact_cost + + costs = ctrl_cost + contact_cost + rewards = forward_reward + healthy_reward + reward = rewards - costs + + reward_info = { + "reward_survive": healthy_reward, + "reward_forward": forward_reward, + "reward_ctrl": -ctrl_cost, + "reward_contact": -contact_cost, + } + + return reward, reward_info + def reset_model(self): noise_low = -self._reset_noise_scale noise_high = self._reset_noise_scale diff --git a/gymnasium/envs/mujoco/humanoidstandup_v5.py b/gymnasium/envs/mujoco/humanoidstandup_v5.py index 46e591ac8..dda5cd639 100644 --- a/gymnasium/envs/mujoco/humanoidstandup_v5.py +++ b/gymnasium/envs/mujoco/humanoidstandup_v5.py @@ -444,6 +444,20 @@ def step(self, action): self.do_simulation(action, self.frame_skip) pos_after = self.data.qpos[2] + reward, reward_info = self._get_rew(pos_after, action) + info = { + "x_position": self.data.qpos[0], + "y_position": self.data.qpos[1], + "z_distance_from_origin": self.data.qpos[2] - self.init_qpos[2], + "tendon_lenght": self.data.ten_length, + "tendon_velocity": self.data.ten_velocity, + } | reward_info + + if self.render_mode == "human": + self.render() + return self._get_obs(), reward, False, False, info + + def _get_rew(self, pos_after: float, action): uph_cost = (pos_after - 0) / self.model.opt.timestep quad_ctrl_cost = self._ctrl_cost_weight * np.square(self.data.ctrl).sum() @@ -455,20 +469,14 @@ def step(self, action): quad_impact_cost = np.clip(quad_impact_cost, min_impact_cost, max_impact_cost) reward = uph_cost - quad_ctrl_cost - quad_impact_cost + 1 - info = { + + reward_info = { "reward_linup": uph_cost, "reward_quadctrl": -quad_ctrl_cost, "reward_impact": -quad_impact_cost, - "x_position": self.data.qpos[0], - "y_position": self.data.qpos[1], - "z_distance_from_origin": self.data.qpos[2] - self.init_qpos[2], - "tendon_lenght": self.data.ten_length, - "tendon_velocity": self.data.ten_velocity, } - if self.render_mode == "human": - self.render() - return self._get_obs(), reward, False, False, info + return reward, reward_info def reset_model(self): noise_low = -self._reset_noise_scale diff --git a/gymnasium/envs/mujoco/inverted_double_pendulum_v5.py b/gymnasium/envs/mujoco/inverted_double_pendulum_v5.py index dfa190ca2..0e09f4806 100644 --- a/gymnasium/envs/mujoco/inverted_double_pendulum_v5.py +++ b/gymnasium/envs/mujoco/inverted_double_pendulum_v5.py @@ -194,27 +194,31 @@ def __init__( def step(self, action): self.do_simulation(action, self.frame_skip) + x, _, y = self.data.site_xpos[0] observation = self._get_obs() + terminated = bool(y <= 1) + reward, reward_info = self._get_rew(x, y, terminated) - x, _, y = self.data.site_xpos[0] - v1, v2 = self.data.qvel[1:3] + info = {} | reward_info - terminated = bool(y <= 1) + if self.render_mode == "human": + self.render() + return observation, reward, terminated, False, info + def _get_rew(self, x, y, terminated): + v1, v2 = self.data.qvel[1:3] dist_penalty = 0.01 * x**2 + (y - 2) ** 2 vel_penalty = 1e-3 * v1**2 + 5e-3 * v2**2 alive_bonus = self._healthy_reward * int(not terminated) reward = alive_bonus - dist_penalty - vel_penalty - info = { + reward_info = { "reward_survive": alive_bonus, "distance_penalty": -dist_penalty, "velocity_penalty": -vel_penalty, } - if self.render_mode == "human": - self.render() - return observation, reward, terminated, False, info + return reward, reward_info def _get_obs(self): return np.concatenate( diff --git a/gymnasium/envs/mujoco/pusher_v5.py b/gymnasium/envs/mujoco/pusher_v5.py index 99d4eabf0..c44dec7b6 100644 --- a/gymnasium/envs/mujoco/pusher_v5.py +++ b/gymnasium/envs/mujoco/pusher_v5.py @@ -220,6 +220,16 @@ def __init__( } def step(self, action): + reward, reward_info = self._get_rew(action) + self.do_simulation(action, self.frame_skip) + + observation = self._get_obs() + info = {} | reward_info + if self.render_mode == "human": + self.render() + return observation, reward, False, False, info + + def _get_rew(self, action): vec_1 = self.get_body_com("object") - self.get_body_com("tips_arm") vec_2 = self.get_body_com("object") - self.get_body_com("goal") @@ -227,18 +237,15 @@ def step(self, action): reward_dist = -np.linalg.norm(vec_2) * self._reward_dist_weight reward_ctrl = -np.square(action).sum() * self._reward_control_weight - self.do_simulation(action, self.frame_skip) - - observation = self._get_obs() reward = reward_dist + reward_ctrl + reward_near - info = { + + reward_info = { "reward_dist": reward_dist, "reward_ctrl": reward_ctrl, "reward_near": reward_near, } - if self.render_mode == "human": - self.render() - return observation, reward, False, False, info + + return reward, reward_info def reset_model(self): qpos = self.init_qpos diff --git a/gymnasium/envs/mujoco/reacher_v5.py b/gymnasium/envs/mujoco/reacher_v5.py index e8ba867d1..317fead3b 100644 --- a/gymnasium/envs/mujoco/reacher_v5.py +++ b/gymnasium/envs/mujoco/reacher_v5.py @@ -197,21 +197,28 @@ def __init__( } def step(self, action): + reward, reward_info = self._get_rew(action) + self.do_simulation(action, self.frame_skip) + + observation = self._get_obs() + info = {} | reward_info + if self.render_mode == "human": + self.render() + return observation, reward, False, False, info + + def _get_rew(self, action): vec = self.get_body_com("fingertip") - self.get_body_com("target") reward_dist = -np.linalg.norm(vec) * self._reward_dist_weight reward_ctrl = -np.square(action).sum() * self._reward_control_weight - self.do_simulation(action, self.frame_skip) - - observation = self._get_obs() reward = reward_dist + reward_ctrl - info = { + + reward_info = { "reward_dist": reward_dist, "reward_ctrl": reward_ctrl, } - if self.render_mode == "human": - self.render() - return observation, reward, False, False, info + + return reward, reward_info def reset_model(self): qpos = ( diff --git a/gymnasium/envs/mujoco/swimmer_v5.py b/gymnasium/envs/mujoco/swimmer_v5.py index c49267bce..78cf4a5fe 100644 --- a/gymnasium/envs/mujoco/swimmer_v5.py +++ b/gymnasium/envs/mujoco/swimmer_v5.py @@ -234,27 +234,33 @@ def step(self, action): xy_velocity = (xy_position_after - xy_position_before) / self.dt x_velocity, y_velocity = xy_velocity - forward_reward = self._forward_reward_weight * x_velocity - - ctrl_cost = self.control_cost(action) - observation = self._get_obs() - reward = forward_reward - ctrl_cost + reward, reward_info = self._get_rew(x_velocity, action) info = { - "reward_forward": forward_reward, - "reward_ctrl": -ctrl_cost, "x_position": xy_position_after[0], "y_position": xy_position_after[1], "distance_from_origin": np.linalg.norm(xy_position_after, ord=2), "x_velocity": x_velocity, "y_velocity": y_velocity, - } + } | reward_info if self.render_mode == "human": self.render() return observation, reward, False, False, info + def _get_rew(self, x_velocity: float, action): + forward_reward = self._forward_reward_weight * x_velocity + ctrl_cost = self.control_cost(action) + + reward = forward_reward - ctrl_cost + reward_info = { + "reward_forward": forward_reward, + "reward_ctrl": -ctrl_cost, + } + + return reward, reward_info + def _get_obs(self): position = self.data.qpos.flatten() velocity = self.data.qvel.flatten() diff --git a/gymnasium/envs/mujoco/walker2d_v5.py b/gymnasium/envs/mujoco/walker2d_v5.py index dde3e9b03..4eaf6d99a 100644 --- a/gymnasium/envs/mujoco/walker2d_v5.py +++ b/gymnasium/envs/mujoco/walker2d_v5.py @@ -310,30 +310,36 @@ def step(self, action): x_position_after = self.data.qpos[0] x_velocity = (x_position_after - x_position_before) / self.dt - ctrl_cost = self.control_cost(action) + observation = self._get_obs() + reward, reward_info = self._get_rew(x_velocity, action) + terminated = self.terminated + info = { + "x_position": x_position_after, + "z_distance_from_origin": self.data.qpos[1] - self.init_qpos[1], + "x_velocity": x_velocity, + } | reward_info + if self.render_mode == "human": + self.render() + + return observation, reward, terminated, False, info + + def _get_rew(self, x_velocity: float, action): forward_reward = self._forward_reward_weight * x_velocity healthy_reward = self.healthy_reward + ctrl_cost = self.control_cost(action) rewards = forward_reward + healthy_reward costs = ctrl_cost - - observation = self._get_obs() reward = rewards - costs - terminated = self.terminated - info = { + + reward_info = { "reward_forward": forward_reward, "reward_ctrl": -ctrl_cost, "reward_survive": healthy_reward, - "x_position": x_position_after, - "z_distance_from_origin": self.data.qpos[1] - self.init_qpos[1], - "x_velocity": x_velocity, } - if self.render_mode == "human": - self.render() - - return observation, reward, terminated, False, info + return reward, reward_info def reset_model(self): noise_low = -self._reset_noise_scale From 999d8880b36864b52ae28e447f4545836736fe4d Mon Sep 17 00:00:00 2001 From: Kallinteris Andreas Date: Wed, 6 Dec 2023 15:23:20 +0200 Subject: [PATCH 16/29] some cleanup --- gymnasium/envs/mujoco/ant_v5.py | 1 + gymnasium/envs/mujoco/hopper_v5.py | 4 ++-- gymnasium/envs/mujoco/humanoid_v5.py | 4 ++-- gymnasium/envs/mujoco/inverted_double_pendulum_v5.py | 1 + gymnasium/envs/mujoco/swimmer_v5.py | 1 + gymnasium/envs/mujoco/walker2d_v5.py | 4 ++-- 6 files changed, 9 insertions(+), 6 deletions(-) diff --git a/gymnasium/envs/mujoco/ant_v5.py b/gymnasium/envs/mujoco/ant_v5.py index ecda862be..a15c893c8 100644 --- a/gymnasium/envs/mujoco/ant_v5.py +++ b/gymnasium/envs/mujoco/ant_v5.py @@ -399,6 +399,7 @@ def _get_rew(self, x_velocity: float, action): ctrl_cost = self.control_cost(action) contact_cost = self.contact_cost costs = ctrl_cost + contact_cost + reward = rewards - costs reward_info = { diff --git a/gymnasium/envs/mujoco/hopper_v5.py b/gymnasium/envs/mujoco/hopper_v5.py index a207f09f5..03ef34ab4 100644 --- a/gymnasium/envs/mujoco/hopper_v5.py +++ b/gymnasium/envs/mujoco/hopper_v5.py @@ -332,11 +332,11 @@ def step(self, action): def _get_rew(self, x_velocity: float, action): forward_reward = self._forward_reward_weight * x_velocity healthy_reward = self.healthy_reward + rewards = forward_reward + healthy_reward ctrl_cost = self.control_cost(action) - - rewards = forward_reward + healthy_reward costs = ctrl_cost + reward = rewards - costs reward_info = { diff --git a/gymnasium/envs/mujoco/humanoid_v5.py b/gymnasium/envs/mujoco/humanoid_v5.py index d7697cca8..797804a95 100644 --- a/gymnasium/envs/mujoco/humanoid_v5.py +++ b/gymnasium/envs/mujoco/humanoid_v5.py @@ -517,12 +517,12 @@ def step(self, action): def _get_rew(self, x_velocity: float, action): forward_reward = self._forward_reward_weight * x_velocity healthy_reward = self.healthy_reward + rewards = forward_reward + healthy_reward ctrl_cost = self.control_cost(action) contact_cost = self.contact_cost - costs = ctrl_cost + contact_cost - rewards = forward_reward + healthy_reward + reward = rewards - costs reward_info = { diff --git a/gymnasium/envs/mujoco/inverted_double_pendulum_v5.py b/gymnasium/envs/mujoco/inverted_double_pendulum_v5.py index 0e09f4806..5fff0dba0 100644 --- a/gymnasium/envs/mujoco/inverted_double_pendulum_v5.py +++ b/gymnasium/envs/mujoco/inverted_double_pendulum_v5.py @@ -210,6 +210,7 @@ def _get_rew(self, x, y, terminated): dist_penalty = 0.01 * x**2 + (y - 2) ** 2 vel_penalty = 1e-3 * v1**2 + 5e-3 * v2**2 alive_bonus = self._healthy_reward * int(not terminated) + reward = alive_bonus - dist_penalty - vel_penalty reward_info = { diff --git a/gymnasium/envs/mujoco/swimmer_v5.py b/gymnasium/envs/mujoco/swimmer_v5.py index 78cf4a5fe..154eb5d0e 100644 --- a/gymnasium/envs/mujoco/swimmer_v5.py +++ b/gymnasium/envs/mujoco/swimmer_v5.py @@ -254,6 +254,7 @@ def _get_rew(self, x_velocity: float, action): ctrl_cost = self.control_cost(action) reward = forward_reward - ctrl_cost + reward_info = { "reward_forward": forward_reward, "reward_ctrl": -ctrl_cost, diff --git a/gymnasium/envs/mujoco/walker2d_v5.py b/gymnasium/envs/mujoco/walker2d_v5.py index 4eaf6d99a..0dfeee2e0 100644 --- a/gymnasium/envs/mujoco/walker2d_v5.py +++ b/gymnasium/envs/mujoco/walker2d_v5.py @@ -327,9 +327,9 @@ def step(self, action): def _get_rew(self, x_velocity: float, action): forward_reward = self._forward_reward_weight * x_velocity healthy_reward = self.healthy_reward - ctrl_cost = self.control_cost(action) - rewards = forward_reward + healthy_reward + + ctrl_cost = self.control_cost(action) costs = ctrl_cost reward = rewards - costs From 0f59baa80d90a8795abd4e97127baaacf12e3d71 Mon Sep 17 00:00:00 2001 From: Kallinteris Andreas Date: Wed, 6 Dec 2023 16:19:06 +0200 Subject: [PATCH 17/29] support `python==3.8` --- gymnasium/envs/mujoco/ant_v5.py | 2 +- gymnasium/envs/mujoco/half_cheetah_v5.py | 2 +- gymnasium/envs/mujoco/hopper_v5.py | 2 +- gymnasium/envs/mujoco/humanoid_v5.py | 2 +- gymnasium/envs/mujoco/humanoidstandup_v5.py | 2 +- gymnasium/envs/mujoco/inverted_double_pendulum_v5.py | 2 +- gymnasium/envs/mujoco/pusher_v5.py | 2 +- gymnasium/envs/mujoco/reacher_v5.py | 2 +- gymnasium/envs/mujoco/swimmer_v5.py | 2 +- gymnasium/envs/mujoco/walker2d_v5.py | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/gymnasium/envs/mujoco/ant_v5.py b/gymnasium/envs/mujoco/ant_v5.py index a15c893c8..a0a1db8a0 100644 --- a/gymnasium/envs/mujoco/ant_v5.py +++ b/gymnasium/envs/mujoco/ant_v5.py @@ -385,7 +385,7 @@ def step(self, action): "distance_from_origin": np.linalg.norm(self.data.qpos[0:2], ord=2), "x_velocity": x_velocity, "y_velocity": y_velocity, - } | reward_info + }.update(reward_info) if self.render_mode == "human": self.render() diff --git a/gymnasium/envs/mujoco/half_cheetah_v5.py b/gymnasium/envs/mujoco/half_cheetah_v5.py index 0432c4e99..efc9602cd 100644 --- a/gymnasium/envs/mujoco/half_cheetah_v5.py +++ b/gymnasium/envs/mujoco/half_cheetah_v5.py @@ -250,7 +250,7 @@ def step(self, action): info = { "x_position": x_position_after, "x_velocity": x_velocity, - } | reward_info + }.update(reward_info) if self.render_mode == "human": self.render() diff --git a/gymnasium/envs/mujoco/hopper_v5.py b/gymnasium/envs/mujoco/hopper_v5.py index 03ef34ab4..68668e5ec 100644 --- a/gymnasium/envs/mujoco/hopper_v5.py +++ b/gymnasium/envs/mujoco/hopper_v5.py @@ -323,7 +323,7 @@ def step(self, action): "x_position": x_position_after, "z_distance_from_origin": self.data.qpos[1] - self.init_qpos[1], "x_velocity": x_velocity, - } | reward_info + }.update(reward_info) if self.render_mode == "human": self.render() diff --git a/gymnasium/envs/mujoco/humanoid_v5.py b/gymnasium/envs/mujoco/humanoid_v5.py index 797804a95..953f3fa5a 100644 --- a/gymnasium/envs/mujoco/humanoid_v5.py +++ b/gymnasium/envs/mujoco/humanoid_v5.py @@ -508,7 +508,7 @@ def step(self, action): "distance_from_origin": np.linalg.norm(self.data.qpos[0:2], ord=2), "x_velocity": x_velocity, "y_velocity": y_velocity, - } | reward_info + }.update(reward_info) if self.render_mode == "human": self.render() diff --git a/gymnasium/envs/mujoco/humanoidstandup_v5.py b/gymnasium/envs/mujoco/humanoidstandup_v5.py index dda5cd639..b1e97f7ad 100644 --- a/gymnasium/envs/mujoco/humanoidstandup_v5.py +++ b/gymnasium/envs/mujoco/humanoidstandup_v5.py @@ -451,7 +451,7 @@ def step(self, action): "z_distance_from_origin": self.data.qpos[2] - self.init_qpos[2], "tendon_lenght": self.data.ten_length, "tendon_velocity": self.data.ten_velocity, - } | reward_info + }.update(reward_info) if self.render_mode == "human": self.render() diff --git a/gymnasium/envs/mujoco/inverted_double_pendulum_v5.py b/gymnasium/envs/mujoco/inverted_double_pendulum_v5.py index 5fff0dba0..bf2835577 100644 --- a/gymnasium/envs/mujoco/inverted_double_pendulum_v5.py +++ b/gymnasium/envs/mujoco/inverted_double_pendulum_v5.py @@ -199,7 +199,7 @@ def step(self, action): terminated = bool(y <= 1) reward, reward_info = self._get_rew(x, y, terminated) - info = {} | reward_info + info = reward_info if self.render_mode == "human": self.render() diff --git a/gymnasium/envs/mujoco/pusher_v5.py b/gymnasium/envs/mujoco/pusher_v5.py index c44dec7b6..490c4b016 100644 --- a/gymnasium/envs/mujoco/pusher_v5.py +++ b/gymnasium/envs/mujoco/pusher_v5.py @@ -224,7 +224,7 @@ def step(self, action): self.do_simulation(action, self.frame_skip) observation = self._get_obs() - info = {} | reward_info + info = reward_info if self.render_mode == "human": self.render() return observation, reward, False, False, info diff --git a/gymnasium/envs/mujoco/reacher_v5.py b/gymnasium/envs/mujoco/reacher_v5.py index 317fead3b..db23e1961 100644 --- a/gymnasium/envs/mujoco/reacher_v5.py +++ b/gymnasium/envs/mujoco/reacher_v5.py @@ -201,7 +201,7 @@ def step(self, action): self.do_simulation(action, self.frame_skip) observation = self._get_obs() - info = {} | reward_info + info = reward_info if self.render_mode == "human": self.render() return observation, reward, False, False, info diff --git a/gymnasium/envs/mujoco/swimmer_v5.py b/gymnasium/envs/mujoco/swimmer_v5.py index 154eb5d0e..2a3368574 100644 --- a/gymnasium/envs/mujoco/swimmer_v5.py +++ b/gymnasium/envs/mujoco/swimmer_v5.py @@ -242,7 +242,7 @@ def step(self, action): "distance_from_origin": np.linalg.norm(xy_position_after, ord=2), "x_velocity": x_velocity, "y_velocity": y_velocity, - } | reward_info + }.update(reward_info) if self.render_mode == "human": self.render() diff --git a/gymnasium/envs/mujoco/walker2d_v5.py b/gymnasium/envs/mujoco/walker2d_v5.py index 0dfeee2e0..33d2e5fa7 100644 --- a/gymnasium/envs/mujoco/walker2d_v5.py +++ b/gymnasium/envs/mujoco/walker2d_v5.py @@ -317,7 +317,7 @@ def step(self, action): "x_position": x_position_after, "z_distance_from_origin": self.data.qpos[1] - self.init_qpos[1], "x_velocity": x_velocity, - } | reward_info + }.update(reward_info) if self.render_mode == "human": self.render() From 76f5e17a4fadfc6bbd4a2571bc2df810e1a47260 Mon Sep 17 00:00:00 2001 From: Kallinteris Andreas Date: Wed, 6 Dec 2023 16:55:58 +0200 Subject: [PATCH 18/29] fix for real this time --- gymnasium/envs/mujoco/ant_v5.py | 3 ++- gymnasium/envs/mujoco/half_cheetah_v5.py | 3 ++- gymnasium/envs/mujoco/hopper_v5.py | 3 ++- gymnasium/envs/mujoco/humanoid_v5.py | 3 ++- gymnasium/envs/mujoco/humanoidstandup_v5.py | 3 ++- gymnasium/envs/mujoco/swimmer_v5.py | 3 ++- gymnasium/envs/mujoco/walker2d_v5.py | 3 ++- 7 files changed, 14 insertions(+), 7 deletions(-) diff --git a/gymnasium/envs/mujoco/ant_v5.py b/gymnasium/envs/mujoco/ant_v5.py index a0a1db8a0..586e80cc3 100644 --- a/gymnasium/envs/mujoco/ant_v5.py +++ b/gymnasium/envs/mujoco/ant_v5.py @@ -385,7 +385,8 @@ def step(self, action): "distance_from_origin": np.linalg.norm(self.data.qpos[0:2], ord=2), "x_velocity": x_velocity, "y_velocity": y_velocity, - }.update(reward_info) + **reward_info + } if self.render_mode == "human": self.render() diff --git a/gymnasium/envs/mujoco/half_cheetah_v5.py b/gymnasium/envs/mujoco/half_cheetah_v5.py index efc9602cd..99acfeb51 100644 --- a/gymnasium/envs/mujoco/half_cheetah_v5.py +++ b/gymnasium/envs/mujoco/half_cheetah_v5.py @@ -250,7 +250,8 @@ def step(self, action): info = { "x_position": x_position_after, "x_velocity": x_velocity, - }.update(reward_info) + **reward_info + } if self.render_mode == "human": self.render() diff --git a/gymnasium/envs/mujoco/hopper_v5.py b/gymnasium/envs/mujoco/hopper_v5.py index 68668e5ec..826acba80 100644 --- a/gymnasium/envs/mujoco/hopper_v5.py +++ b/gymnasium/envs/mujoco/hopper_v5.py @@ -323,7 +323,8 @@ def step(self, action): "x_position": x_position_after, "z_distance_from_origin": self.data.qpos[1] - self.init_qpos[1], "x_velocity": x_velocity, - }.update(reward_info) + **reward_info + } if self.render_mode == "human": self.render() diff --git a/gymnasium/envs/mujoco/humanoid_v5.py b/gymnasium/envs/mujoco/humanoid_v5.py index 953f3fa5a..10a2c989b 100644 --- a/gymnasium/envs/mujoco/humanoid_v5.py +++ b/gymnasium/envs/mujoco/humanoid_v5.py @@ -508,7 +508,8 @@ def step(self, action): "distance_from_origin": np.linalg.norm(self.data.qpos[0:2], ord=2), "x_velocity": x_velocity, "y_velocity": y_velocity, - }.update(reward_info) + **reward_info + } if self.render_mode == "human": self.render() diff --git a/gymnasium/envs/mujoco/humanoidstandup_v5.py b/gymnasium/envs/mujoco/humanoidstandup_v5.py index b1e97f7ad..828a79845 100644 --- a/gymnasium/envs/mujoco/humanoidstandup_v5.py +++ b/gymnasium/envs/mujoco/humanoidstandup_v5.py @@ -451,7 +451,8 @@ def step(self, action): "z_distance_from_origin": self.data.qpos[2] - self.init_qpos[2], "tendon_lenght": self.data.ten_length, "tendon_velocity": self.data.ten_velocity, - }.update(reward_info) + **reward_info + } if self.render_mode == "human": self.render() diff --git a/gymnasium/envs/mujoco/swimmer_v5.py b/gymnasium/envs/mujoco/swimmer_v5.py index 2a3368574..34700dc25 100644 --- a/gymnasium/envs/mujoco/swimmer_v5.py +++ b/gymnasium/envs/mujoco/swimmer_v5.py @@ -242,7 +242,8 @@ def step(self, action): "distance_from_origin": np.linalg.norm(xy_position_after, ord=2), "x_velocity": x_velocity, "y_velocity": y_velocity, - }.update(reward_info) + **reward_info + } if self.render_mode == "human": self.render() diff --git a/gymnasium/envs/mujoco/walker2d_v5.py b/gymnasium/envs/mujoco/walker2d_v5.py index 33d2e5fa7..7e2df6d8c 100644 --- a/gymnasium/envs/mujoco/walker2d_v5.py +++ b/gymnasium/envs/mujoco/walker2d_v5.py @@ -317,7 +317,8 @@ def step(self, action): "x_position": x_position_after, "z_distance_from_origin": self.data.qpos[1] - self.init_qpos[1], "x_velocity": x_velocity, - }.update(reward_info) + **reward_info + } if self.render_mode == "human": self.render() From 724e47fbe74c5ea1fff7b0da146509959387ee31 Mon Sep 17 00:00:00 2001 From: Kallinteris Andreas Date: Wed, 6 Dec 2023 17:02:25 +0200 Subject: [PATCH 19/29] `black` --- gymnasium/envs/mujoco/ant_v5.py | 2 +- gymnasium/envs/mujoco/half_cheetah_v5.py | 6 +----- gymnasium/envs/mujoco/hopper_v5.py | 2 +- gymnasium/envs/mujoco/humanoid_v5.py | 2 +- gymnasium/envs/mujoco/humanoidstandup_v5.py | 2 +- gymnasium/envs/mujoco/swimmer_v5.py | 2 +- gymnasium/envs/mujoco/walker2d_v5.py | 2 +- 7 files changed, 7 insertions(+), 11 deletions(-) diff --git a/gymnasium/envs/mujoco/ant_v5.py b/gymnasium/envs/mujoco/ant_v5.py index 586e80cc3..8b3ab177e 100644 --- a/gymnasium/envs/mujoco/ant_v5.py +++ b/gymnasium/envs/mujoco/ant_v5.py @@ -385,7 +385,7 @@ def step(self, action): "distance_from_origin": np.linalg.norm(self.data.qpos[0:2], ord=2), "x_velocity": x_velocity, "y_velocity": y_velocity, - **reward_info + **reward_info, } if self.render_mode == "human": diff --git a/gymnasium/envs/mujoco/half_cheetah_v5.py b/gymnasium/envs/mujoco/half_cheetah_v5.py index 99acfeb51..0649f4503 100644 --- a/gymnasium/envs/mujoco/half_cheetah_v5.py +++ b/gymnasium/envs/mujoco/half_cheetah_v5.py @@ -247,11 +247,7 @@ def step(self, action): observation = self._get_obs() reward, reward_info = self._get_rew(x_velocity, action) - info = { - "x_position": x_position_after, - "x_velocity": x_velocity, - **reward_info - } + info = {"x_position": x_position_after, "x_velocity": x_velocity, **reward_info} if self.render_mode == "human": self.render() diff --git a/gymnasium/envs/mujoco/hopper_v5.py b/gymnasium/envs/mujoco/hopper_v5.py index 826acba80..68813d190 100644 --- a/gymnasium/envs/mujoco/hopper_v5.py +++ b/gymnasium/envs/mujoco/hopper_v5.py @@ -323,7 +323,7 @@ def step(self, action): "x_position": x_position_after, "z_distance_from_origin": self.data.qpos[1] - self.init_qpos[1], "x_velocity": x_velocity, - **reward_info + **reward_info, } if self.render_mode == "human": diff --git a/gymnasium/envs/mujoco/humanoid_v5.py b/gymnasium/envs/mujoco/humanoid_v5.py index 10a2c989b..1834d6d48 100644 --- a/gymnasium/envs/mujoco/humanoid_v5.py +++ b/gymnasium/envs/mujoco/humanoid_v5.py @@ -508,7 +508,7 @@ def step(self, action): "distance_from_origin": np.linalg.norm(self.data.qpos[0:2], ord=2), "x_velocity": x_velocity, "y_velocity": y_velocity, - **reward_info + **reward_info, } if self.render_mode == "human": diff --git a/gymnasium/envs/mujoco/humanoidstandup_v5.py b/gymnasium/envs/mujoco/humanoidstandup_v5.py index 828a79845..99b35cc50 100644 --- a/gymnasium/envs/mujoco/humanoidstandup_v5.py +++ b/gymnasium/envs/mujoco/humanoidstandup_v5.py @@ -451,7 +451,7 @@ def step(self, action): "z_distance_from_origin": self.data.qpos[2] - self.init_qpos[2], "tendon_lenght": self.data.ten_length, "tendon_velocity": self.data.ten_velocity, - **reward_info + **reward_info, } if self.render_mode == "human": diff --git a/gymnasium/envs/mujoco/swimmer_v5.py b/gymnasium/envs/mujoco/swimmer_v5.py index 34700dc25..a231cc627 100644 --- a/gymnasium/envs/mujoco/swimmer_v5.py +++ b/gymnasium/envs/mujoco/swimmer_v5.py @@ -242,7 +242,7 @@ def step(self, action): "distance_from_origin": np.linalg.norm(xy_position_after, ord=2), "x_velocity": x_velocity, "y_velocity": y_velocity, - **reward_info + **reward_info, } if self.render_mode == "human": diff --git a/gymnasium/envs/mujoco/walker2d_v5.py b/gymnasium/envs/mujoco/walker2d_v5.py index 7e2df6d8c..555ca4944 100644 --- a/gymnasium/envs/mujoco/walker2d_v5.py +++ b/gymnasium/envs/mujoco/walker2d_v5.py @@ -317,7 +317,7 @@ def step(self, action): "x_position": x_position_after, "z_distance_from_origin": self.data.qpos[1] - self.init_qpos[1], "x_velocity": x_velocity, - **reward_info + **reward_info, } if self.render_mode == "human": From 32c1cb84e83d51a2fade88167cd886f1ead48260 Mon Sep 17 00:00:00 2001 From: Kallinteris Andreas Date: Sun, 10 Dec 2023 15:11:34 +0200 Subject: [PATCH 20/29] add prototype --- gymnasium/envs/mujoco/f.py | 315 +++++++++++++++++++++++++++++++++++++ 1 file changed, 315 insertions(+) create mode 100644 gymnasium/envs/mujoco/f.py diff --git a/gymnasium/envs/mujoco/f.py b/gymnasium/envs/mujoco/f.py new file mode 100644 index 000000000..18f55433a --- /dev/null +++ b/gymnasium/envs/mujoco/f.py @@ -0,0 +1,315 @@ +from os import path + +import numpy as np + +import gymnasium +from gymnasium.envs.mujoco import MujocoRenderer + + +try: + import jax + import mujoco + from jax import numpy as jnp + from mujoco import mjx +except ImportError as e: + MJX_IMPORT_ERROR = e +else: + MJX_IMPORT_ERROR = None + +DEFAULT_CAMERA_CONFIG = { # TODO reuse the one from v5 + "distance": 4.0, +} + + +class MJXEnv( + gymnasium.functional.FuncEnv[ + mjx._src.types.Data, jnp.ndarray, jnp.ndarray, jnp.ndarray, bool, MujocoRenderer + ] +): + """The Base for MJX Environments""" + + def __init__(self, model_path, frame_skip): + if MJX_IMPORT_ERROR is not None: + raise gymnasium.error.DependencyNotInstalled( + f"{MJX_IMPORT_ERROR}. " + "(HINT: you need to install mujoco, run `pip install gymnasium[mjx]`.)" # TODO actually create gymnasium[mjx] + ) + + # NOTE can not be JITted because of `Box` not support jax.numpy + if model_path.startswith(".") or model_path.startswith("/"): # TODO cleanup + self.fullpath = model_path + elif model_path.startswith("~"): + self.fullpath = path.expanduser(model_path) + else: + self.fullpath = path.join(path.dirname(__file__), "assets", model_path) + if not path.exists(self.fullpath): + raise OSError(f"File {self.fullpath} does not exist") + + self.frame_skip = frame_skip + + self.model = mujoco.MjModel.from_xml_path( + self.fullpath + ) # TODO? do not store and replace with mjx.get_model with mjx==3.1 + # NOTE too much state? + # alternatives state implementions + # 1. functional_state = (mjx_data, mjx_model), least internal state in MJXenv, most state in functional_state + # 2. functional_state = [qpos,qvel], most internal state in MJXenv, least state in functional_state + self.mjx_model = mjx.device_put(self.model) + + # set action space + low_action_bound, high_action_bound = self.mjx_model.actuator_ctrlrange.T + # TODO change bounds and types when and if `Box` supports JAX nativly + self.action_space = gymnasium.spaces.Box( + low=np.array(low_action_bound), + high=np.array(high_action_bound), + dtype=np.float32, + ) + # self.action_space = gymnasium.spaces.Box(low=low_action_bound, high=high_action_bound, dtype=low_action_bound.dtype) + # observation_space: gymnasium.spaces.Box # set by the sub-class + + def initial(self, rng: jax.random.PRNGKey) -> mjx._src.types.Data: + mjx_data = mjx.make_data( + self.model + ) # TODO? find a more performant alternative that does not allocate? + qpos, qvel = self._gen_init_state(rng) + mjx_data = mjx_data.replace(qpos=qpos, qvel=qvel) + mjx_data = mjx.forward(self.mjx_model, mjx_data) + + return mjx_data + + def transition( + self, state: mjx._src.types.Data, action: jnp.ndarray, rng=None + ) -> mjx._src.types.Data: + """Step through the simulator using `action` for `self.dt`.""" + mjx_data = state + mjx_data = mjx_data.replace(ctrl=action) + mjx_data = jax.lax.fori_loop( + 0, self.frame_skip, lambda _, x: mjx.step(self.mjx_model, x), mjx_data + ) + + return mjx_data + # TODO fix sensors with MJX>=3.1 + + def reward( + self, + state: mjx._src.types.Data, + action: jnp.ndarray, + next_state: mjx._src.types.Data, + ) -> jnp.ndarray: + return self._get_reward(state, action, next_state)[0] + + def transition_info( + self, + state: mjx._src.types.Data, + action: jnp.ndarray, + next_state: mjx._src.types.Data, + ) -> dict: + return self._get_reward(state, action, next_state)[1] + + def render_image( + self, state: mjx._src.types.Data, render_state: MujocoRenderer + ) -> tuple[MujocoRenderer, np.ndarray | None]: + mjx_data = state + mujoco_renderer = render_state + + data = mujoco.MjData(self.model) + mjx.device_get_into(data, mjx_data) # TODO use get_data instead once mjx==3.1 + mujoco.mj_forward(self.model, data) + + mujoco_renderer.data = data + + frame = mujoco_renderer.render( + self.render_mode, self.camera_id, self.camera_name + ) + + return mujoco_renderer, frame + + def render_init( + self, + default_camera_config: dict[str, float] = {}, + camera_id: int | None = None, + camera_name: str | None = None, + max_geom=1000, + width=480, + height=480, + render_mode="rgb_array", + ) -> MujocoRenderer: + # TODO storing to much state? it should probably be moved internal to MujocoRenderer + self.render_mode = render_mode + self.camera_id = camera_id + self.camera_name = camera_name + + return MujocoRenderer( + self.model, + None, + default_camera_config, + width, + height, + max_geom, + ) + + def render_close(self, render_state: MujocoRenderer) -> None: + mujoco_renderer = render_state + if mujoco_renderer is not None: + mujoco_renderer.close() + + @property + def dt(self) -> float: + return self.mjx_model.opt.timestep * self.frame_skip + + def _gen_init_state(self, rng) -> tuple[jnp.ndarray, jnp.ndarray]: + """ + Returns: `(qpos, qvel)` + """ + # NOTE alternatives + # 1. return the state in a single vector + # 2. return it a dictionary keyied by "qpos" & "qvel" + raise NotImplementedError + + def _get_reward( + self, + state: mjx._src.types.Data, + action: jnp.ndarray, + next_state: mjx._src.types.Data, + ) -> tuple[jnp.ndarray, dict]: + """ + Generates `reward` and `transition_info`, we rely on the JIT's SEE to optimize it. + Returns: `(reward, transition_info)` + """ + raise NotImplementedError + + def observation(self, state: mjx._src.types.Data) -> jnp.ndarray: + raise NotImplementedError + + def terminal(self, state: mjx._src.types.Data) -> bool: + raise NotImplementedError + + def state_info(self, state: mjx._src.types.Data) -> dict: + raise NotImplementedError + + +# TODO in which file to place this class? in `half_cheetah_v5.py`? +class HalfCheetahMJXEnv(MJXEnv, gymnasium.utils.EzPickle): + def __init__( + self, + xml_file: str = "half_cheetah.xml", + frame_skip: int = 5, + forward_reward_weight: float = 1.0, + ctrl_cost_weight: float = 0.1, + reset_noise_scale: float = 0.1, + exclude_current_positions_from_observation: bool = True, + **kwargs, + ): + gymnasium.utils.EzPickle.__init__( + self, + xml_file, + frame_skip, + forward_reward_weight, + ctrl_cost_weight, + reset_noise_scale, + exclude_current_positions_from_observation, + **kwargs, + ) + + self._forward_reward_weight = forward_reward_weight + self._ctrl_cost_weight = ctrl_cost_weight + + self._reset_noise_scale = reset_noise_scale + + self._exclude_current_positions_from_observation = ( + exclude_current_positions_from_observation + ) + + MJXEnv.__init__( + self, + model_path=xml_file, + frame_skip=frame_skip, + **kwargs, + ) + + obs_size = ( + self.mjx_model.nq + + self.mjx_model.nv + - exclude_current_positions_from_observation + ) + + self.observation_space = gymnasium.spaces.Box( # TODO use jnp when and if `Box` supports jax natively + low=-np.inf, high=np.inf, shape=(obs_size,), dtype=np.float32 + ) + + self.observation_structure = { + "skipped_qpos": 1 * exclude_current_positions_from_observation, + "qpos": self.mjx_model.nq - 1 * exclude_current_positions_from_observation, + "qvel": self.mjx_model.nv, + } + + def _gen_init_state(self, rng) -> tuple[jnp.ndarray, jnp.ndarray]: + noise_low = -self._reset_noise_scale + noise_high = self._reset_noise_scale + + qpos = self.mjx_model.qpos0 + jax.random.uniform( + key=rng, minval=noise_low, maxval=noise_high, shape=(self.mjx_model.nq,) + ) + qvel = self._reset_noise_scale * jax.random.normal( + key=rng, shape=(self.mjx_model.nv,) + ) + + return qpos, qvel + + def observation(self, state: mjx._src.types.Data) -> jnp.ndarray: + mjx_data = state + position = mjx_data.qpos.flatten() + velocity = mjx_data.qvel.flatten() + + if self._exclude_current_positions_from_observation: + position = position[1:] + + observation = jnp.concatenate((position, velocity)) + return observation + + def _get_reward( + self, + state: mjx._src.types.Data, + action: jnp.ndarray, + next_state: mjx._src.types.Data, + ) -> tuple[jnp.ndarray, dict]: + mjx_data_old = state + mjx_data_new = next_state + x_position_before = mjx_data_old.qpos[0] + x_position_after = mjx_data_new.qpos[0] + x_velocity = (x_position_after - x_position_before) / self.dt + + forward_reward = self._forward_reward_weight * x_velocity + ctrl_cost = self._ctrl_cost_weight * jnp.sum(jnp.square(action)) + + reward = forward_reward - ctrl_cost + reward_info = { + "reward_forward": forward_reward, + "reward_ctrl": -ctrl_cost, + "x_velocity": x_velocity, + } + + return reward, reward_info + + def terminal(self, state: mjx._src.types.Data) -> bool: + return False + # NOTE or: return jnp.array(False) + + def state_info(self, state: mjx._src.types.Data) -> dict: + mjx_data = state + x_position_after = mjx_data.qpos[0] + info = { + "x_position": x_position_after, + } + return info + + def render_init( + self, default_camera_config: dict[str, float] = DEFAULT_CAMERA_CONFIG, **kwargs + ) -> MujocoRenderer: + return super().render_init( + default_camera_config=default_camera_config, **kwargs + ) + + +# TODO add vector environment +# TODO consider requirement of `metaworld` & `gymansium_robotics.RobotEnv` From 30cc2318ea5be4314eb0d7f23f142c5160708866 Mon Sep 17 00:00:00 2001 From: Kallinteris Andreas Date: Fri, 15 Dec 2023 20:45:28 +0200 Subject: [PATCH 21/29] cleanup --- gymnasium/envs/mujoco/f.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gymnasium/envs/mujoco/f.py b/gymnasium/envs/mujoco/f.py index 18f55433a..c3a6f1922 100644 --- a/gymnasium/envs/mujoco/f.py +++ b/gymnasium/envs/mujoco/f.py @@ -68,9 +68,8 @@ def __init__(self, model_path, frame_skip): # observation_space: gymnasium.spaces.Box # set by the sub-class def initial(self, rng: jax.random.PRNGKey) -> mjx._src.types.Data: - mjx_data = mjx.make_data( - self.model - ) # TODO? find a more performant alternative that does not allocate? + # TODO? find a more performant alternative that does not allocate? + mjx_data = mjx.make_data(self.model) qpos, qvel = self._gen_init_state(rng) mjx_data = mjx_data.replace(qpos=qpos, qvel=qvel) mjx_data = mjx.forward(self.mjx_model, mjx_data) @@ -109,6 +108,7 @@ def transition_info( def render_image( self, state: mjx._src.types.Data, render_state: MujocoRenderer ) -> tuple[MujocoRenderer, np.ndarray | None]: + # NOTE function can not be jitted mjx_data = state mujoco_renderer = render_state From 925fcdcd0de25306e857ee5d22fcb98dd94435dd Mon Sep 17 00:00:00 2001 From: Kallinteris Andreas Date: Thu, 1 Feb 2024 11:44:44 +0200 Subject: [PATCH 22/29] update mjx envs --- gymnasium/envs/mjx/__init__.py | 1 + gymnasium/envs/mjx/assets | 1 + gymnasium/envs/mjx/locomotion_2d.py | 306 ++++++++++++++++++++++++++++ gymnasium/envs/mjx/mjx_env.py | 220 ++++++++++++++++++++ 4 files changed, 528 insertions(+) create mode 100644 gymnasium/envs/mjx/__init__.py create mode 120000 gymnasium/envs/mjx/assets create mode 100644 gymnasium/envs/mjx/locomotion_2d.py create mode 100644 gymnasium/envs/mjx/mjx_env.py diff --git a/gymnasium/envs/mjx/__init__.py b/gymnasium/envs/mjx/__init__.py new file mode 100644 index 000000000..ae16426ef --- /dev/null +++ b/gymnasium/envs/mjx/__init__.py @@ -0,0 +1 @@ +from gymnasium.envs.mjx.mjx_env import MJXEnv diff --git a/gymnasium/envs/mjx/assets b/gymnasium/envs/mjx/assets new file mode 120000 index 000000000..ca8ce1e5b --- /dev/null +++ b/gymnasium/envs/mjx/assets @@ -0,0 +1 @@ +../mujoco/assets \ No newline at end of file diff --git a/gymnasium/envs/mjx/locomotion_2d.py b/gymnasium/envs/mjx/locomotion_2d.py new file mode 100644 index 000000000..c9060eed1 --- /dev/null +++ b/gymnasium/envs/mjx/locomotion_2d.py @@ -0,0 +1,306 @@ +import gymnasium + +try: + import jax + from jax import numpy as jnp + from mujoco import mjx +except ImportError as e: + MJX_IMPORT_ERROR = e +else: + MJX_IMPORT_ERROR = None + +import numpy as np +from typing import Tuple + +from gymnasium.envs.mujoco import MujocoRenderer +from gymnasium.envs.mjx.mjx_env import MJXEnv +from gymnasium.envs.mujoco.half_cheetah_v5 import DEFAULT_CAMERA_CONFIG + +from typing import Dict + + +#class Locomotion_2d_Env(MJXEnv, gymnasium.utils.EzPickle): +class Locomotion_2d_Env(MJXEnv): + """Base environment class for 2d locomotion environments such as HalfCheetah, Hopper & Walker2d.""" + def __init__( + self, + xml_file: str, + frame_skip: int, + params: Dict[str, any], # NOTE not API compliant + #forward_reward_weight: float, + #ctrl_cost_weight: float, + #healthy_reward: float, + #terminate_when_unhealthy: bool, + #healthy_state_range: Tuple[float, float], + #healthy_z_range: Tuple[float, float], + #healthy_angle_range: Tuple[float, float], + #reset_noise_scale: float, + #exclude_current_positions_from_observation: bool, + #**kwargs, + ): + """ + gymnasium.utils.EzPickle.__init__( + self, + xml_file, + frame_skip, + forward_reward_weight, + ctrl_cost_weight, + healthy_reward, + terminate_when_unhealthy, + healthy_state_range, + healthy_z_range, + healthy_angle_range, + reset_noise_scale, + exclude_current_positions_from_observation, + **kwargs, + ) + + self._forward_reward_weight = forward_reward_weight + self._ctrl_cost_weight = ctrl_cost_weight + self._healthy_reward = healthy_reward + + self._terminate_when_unhealthy = terminate_when_unhealthy + self._healthy_state_range = healthy_state_range + self._healthy_z_range = healthy_z_range + self._healthy_angle_range = healthy_angle_range + + self._reset_noise_scale = reset_noise_scale + + self._exclude_current_positions_from_observation = ( + exclude_current_positions_from_observation + ) + """ + + MJXEnv.__init__( + self, + model_path=xml_file, + frame_skip=frame_skip, + #**kwargs, + ) + + obs_size = ( + self.mjx_model.nq + + self.mjx_model.nv + - params["exclude_current_positions_from_observation"] + ) + + self.observation_space = gymnasium.spaces.Box( # TODO use jnp when and if `Box` supports jax natively + low=-np.inf, high=np.inf, shape=(obs_size,), dtype=np.float32 + ) + + self.observation_structure = { + "skipped_qpos": 1 * params["exclude_current_positions_from_observation"], + "qpos": self.mjx_model.nq - 1 * params["exclude_current_positions_from_observation"], + "qvel": self.mjx_model.nv, + } + + def _gen_init_physics_state(self, rng, params: Dict[str, any]) -> "tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]": + noise_low = -params["reset_noise_scale"] + noise_high = params["reset_noise_scale"] + + qpos = self.mjx_model.qpos0 + jax.random.uniform( + key=rng, minval=noise_low, maxval=noise_high, shape=(self.mjx_model.nq,) + ) + qvel = params["reset_noise_scale"] * jax.random.normal( + key=rng, shape=(self.mjx_model.nv,) + ) + act = jnp.empty(self.mjx_model.na) + + return qpos, qvel, act + + def observation(self, state: mjx.Data, rng: jax.random.PRNGKey, params: Dict[str, any]) -> jnp.ndarray: + mjx_data = state + + position = mjx_data.qpos.flatten() + velocity = mjx_data.qvel.flatten() + + if params["exclude_current_positions_from_observation"]: + position = position[1:] + + observation = jnp.concatenate((position, velocity)) + return observation + + def _get_reward( + self, + state: mjx.Data, + action: jnp.ndarray, + next_state: mjx.Data, + params: Dict[str, any], + ) -> "tuple[jnp.ndarray, dict]": + mjx_data_old = state + mjx_data_new = next_state + + x_position_before = mjx_data_old.qpos[0] + x_position_after = mjx_data_new.qpos[0] + x_velocity = (x_position_after - x_position_before) / self.dt + + forward_reward = params["forward_reward_weight"] * x_velocity + healthy_reward = params["healthy_reward"] * float(self._gen_is_healty(mjx_data_new, params)) + rewards = forward_reward + healthy_reward + + costs = ctrl_cost = params["ctrl_cost_weight"] * jnp.sum(jnp.square(action)) + + reward = rewards - costs + reward_info = { + "reward_survive": healthy_reward, # TODO? make optional + "reward_forward": forward_reward, + "reward_ctrl": -ctrl_cost, + "x_velocity": x_velocity, + } + + return reward, reward_info + + def terminal(self, state: mjx.Data, rng: jax.random.PRNGKey, params: Dict[str, any]) -> bool: + return (not self._gen_is_healty(state, params)) and params["terminate_when_unhealthy"] + + def state_info(self, state: mjx.Data, params: Dict[str, any]) -> dict[str, float]: + mjx_data = state + x_position_after = mjx_data.qpos[0] + info = { + "x_position": x_position_after, + } + return info + + def render_init( + self, default_camera_config: "dict[str, float]" = DEFAULT_CAMERA_CONFIG, **kwargs + ) -> MujocoRenderer: + return super().render_init( + default_camera_config=default_camera_config, **kwargs + ) + + def _gen_is_healty(self, state: mjx.Data, params: Dict[str, any]): + mjx_data = state + + z, angle = mjx_data.qpos[1:3] + physics_state = jnp.concatenate((mjx_data.qpos[2:], mjx_data.qvel, mjx_data.act)) + + min_state, max_state = params["healthy_state_range"] + min_z, max_z = params["healthy_z_range"] + min_angle, max_angle = params["healthy_angle_range"] + + healthy_state = jnp.all(jnp.logical_and(min_state < physics_state, physics_state < max_state)) + healthy_z = jnp.logical_and(min_z < z, z < max_z) + healthy_angle = jnp.logical_and(min_angle < angle, angle < max_angle) + + return False + #is_healthy = all((healthy_state, healthy_z, healthy_angle)) + #is_healthy = jnp.all(jnp.concatenate((healthy_state, healthy_z, healthy_angle))) + #is_healthy = bool(healthy_state) and bool(healthy_z) and bool(healthy_angle) + is_healthy = healthy_state + return is_healthy + + +# The following could be implemented as **kwargs in register() +# TODO fix camera configs +class HalfCheetahMJXEnv(Locomotion_2d_Env): + def __init__( + self, + params: Dict[str, any], + xml_file: str = "half_cheetah.xml", + frame_skip: int = 5, + #forward_reward_weight: float = 1.0, + #ctrl_cost_weight: float = 0.1, + #healthy_reward: float = 0, + #terminate_when_unhealthy: bool = True, + #healthy_state_range: Tuple[float, float] = (-jnp.inf, jnp.inf), + #healthy_z_range: Tuple[float, float] = (-jnp.inf, jnp.inf), + #healthy_angle_range: Tuple[float, float] = (-jnp.inf, jnp.inf), + #reset_noise_scale: float = 0.1, + #exclude_current_positions_from_observation: bool = True, + #**kwargs, + ): + super().__init__( + params=params, + xml_file=xml_file, + frame_skip=frame_skip, + #forward_reward_weight=forward_reward_weight, + #ctrl_cost_weight=ctrl_cost_weight, + #healthy_reward=healthy_reward, + #terminate_when_unhealthy=terminate_when_unhealthy, + #healthy_state_range=healthy_state_range, + #healthy_z_range=healthy_z_range, + #healthy_angle_range=healthy_angle_range, + #reset_noise_scale=reset_noise_scale, + #exclude_current_positions_from_observation=exclude_current_positions_from_observation, + #**kwargs, + ) + + def get_default_params(**kwargs) -> dict[str, any]: + default = { + "xml_file": "half_cheetah.xml", + "frame_skip": 5, + "forward_reward_weight": 1.0, + "ctrl_cost_weight": 0.1, + "healthy_reward": 0, + "terminate_when_unhealthy": True, + "healthy_state_range": (-jnp.inf, jnp.inf), + "healthy_z_range": (-jnp.inf, jnp.inf), + "healthy_angle_range": (-jnp.inf, jnp.inf), + "reset_noise_scale" : 0.1, + "exclude_current_positions_from_observation": True, + } + return {**default, **kwargs} + + +class HopperMJXEnv(Locomotion_2d_Env): + def __init__( + self, + xml_file: str = "hopper.xml", + frame_skip: int = 4, + #forward_reward_weight: float = 1.0, + #ctrl_cost_weight: float = 1e-3, + #healthy_reward: float = 1.0, + #terminate_when_unhealthy: bool = True, + #healthy_state_range: Tuple[float, float] = (-100.0, 100.0), + #healthy_z_range: Tuple[float, float] = (0.7, jnp.inf), + #healthy_angle_range: Tuple[float, float] = (-0.2, 0.2), + #reset_noise_scale: float = 5e-3, + #exclude_current_positions_from_observation: bool = True, + #**kwargs, + ): + super().__init__( + xml_file=xml_file, + frame_skip=frame_skip, + #forward_reward_weight=forward_reward_weight, + #ctrl_cost_weight=ctrl_cost_weight, + #healthy_reward=healthy_reward, + #terminate_when_unhealthy=terminate_when_unhealthy, + #healthy_state_range=healthy_state_range, + #healthy_z_range=healthy_z_range, + #healthy_angle_range=healthy_angle_range, + #reset_noise_scale=reset_noise_scale, + #exclude_current_positions_from_observation=exclude_current_positions_from_observation, + #**kwargs, + ) + + +class Walker2dMJXEnv(Locomotion_2d_Env): + def __init__( + self, + xml_file: str = "walker2d_v5.xml", + frame_skip: int = 4, + #forward_reward_weight: float = 1.0, + #ctrl_cost_weight: float = 1e-3, + #healthy_reward: float = 1.0, + #terminate_when_unhealthy: bool = True, + #healthy_state_range: Tuple[float, float] = (-jnp.inf, jnp.inf), + #healthy_z_range: Tuple[float, float] = (0.8, 2.0), + #healthy_angle_range: Tuple[float, float] = (-1.0, 1.0), + #reset_noise_scale: float = 5e-3, + #exclude_current_positions_from_observation: bool = True, + #**kwargs, + ): + super().__init__( + xml_file=xml_file, + frame_skip=frame_skip, + #forward_reward_weight=forward_reward_weight, + #ctrl_cost_weight=ctrl_cost_weight, + #healthy_reward=healthy_reward, + #terminate_when_unhealthy=terminate_when_unhealthy, + #healthy_state_range=healthy_state_range, + #healthy_z_range=healthy_z_range, + #healthy_angle_range=healthy_angle_range, + #reset_noise_scale=reset_noise_scale, + #exclude_current_positions_from_observation=exclude_current_positions_from_observation, + #**kwargs, + ) diff --git a/gymnasium/envs/mjx/mjx_env.py b/gymnasium/envs/mjx/mjx_env.py new file mode 100644 index 000000000..1d9a4ce30 --- /dev/null +++ b/gymnasium/envs/mjx/mjx_env.py @@ -0,0 +1,220 @@ +import numpy as np +from typing import Union + +import gymnasium +from gymnasium.envs.mujoco import MujocoRenderer +from gymnasium.envs.mujoco.mujoco_env import expand_model_path +from typing import Dict + + +try: + import jax + import mujoco + from jax import numpy as jnp + from mujoco import mjx +except ImportError as e: + MJX_IMPORT_ERROR = e +else: + MJX_IMPORT_ERROR = None + + +# state = np.empty(mujoco.mj_stateSize(env.unwrapped.model, mujoco.mjtState.mjSTATE_PHYSICS)) +# mujoco.mj_getState(env.unwrapped.model, env.unwrapped.data, state, spec=mujoco.mjtState.mjSTATE_PHYSICS) + +# mujoco.mj_setState(env.unwrapped.model, env.unwrapped.data, state, spec=mujoco.mjtState.mjSTATE_PHYSICS) + + +""" +# TODO unit test these +def mjx_get_physics_state(mjx_data: mjx.Data) -> jnp.ndarray: + ""Get physics state of `mjx_data` similar to mujoco.get_state."" + return jnp.concatenate([mjx_data.qpos, mjx_data.qvel, mjx_data.act]) + + +def mjx_set_physics_state(mjx_data: mjx.Data, mjx_physics_state) -> mjx.Data: + ""Sets the physics state in `mjx_data`."" + qpos_end_index = mjx_data.qpos.size + qvel_end_index = qpos_end_index + mjx_data.qvel.size + + qpos = mjx_physics_state[:qpos_end_index] + qvel = mjx_physics_state[qpos_end_index: qvel_end_index] + act = mjx_physics_state[qvel_end_index:] + assert qpos.size == mjx_data.qpos.size + assert qvel.size == mjx_data.qvel.size + assert act.size == mjx_data.act.size + + return mjx_data.replace(qpos=qpos, qvel=qvel, act=act) +""" + + +class MJXEnv( + gymnasium.functional.FuncEnv[ + #mjx.Data, jnp.ndarray, jnp.ndarray, jnp.ndarray, bool, MujocoRenderer, Dict[str, any] + mjx.Data, jnp.ndarray, jnp.ndarray, jnp.ndarray, bool, MujocoRenderer, + ] +): + """The Base class for MJX Environments in Gymnasium.""" + + """ + def mjx_get_physics_state_put_version(self, mjx_data: mjx.Data) -> np.ndarray: + ""version based on @btaba suggestion"" + # data = mujoco.MjData(self.model) + # mjx.device_get_into(data, mjx_data) + data = mjx.get_data(self.model, mjx_data) + state = np.empty(mujoco.mj_stateSize(self.model, mujoco.mjtState.mjSTATE_PHYSICS)) + mujoco.mj_getState(self.model, data, state, spec=mujoco.mjtState.mjSTATE_PHYSICS) + + return state + """ + + def __init__(self, model_path, frame_skip): + # NOTE can not be JITted because mjx_model.actuator_ctrlrange does not support jax.numpy + if MJX_IMPORT_ERROR is not None: + raise gymnasium.error.DependencyNotInstalled( + f"{MJX_IMPORT_ERROR}. " + "(HINT: you need to install mujoco, run `pip install gymnasium[mjx]`.)" # TODO actually create gymnasium[mjx] + ) + + fullpath = expand_model_path(model_path) + + self.frame_skip = frame_skip + + self.model = mujoco.MjModel.from_xml_path(fullpath) + self.mjx_model = mjx.put_model(self.model) + + self.action_space = gymnasium.spaces.Box( + low=self.model.actuator_ctrlrange.T[0], + high=self.model.actuator_ctrlrange.T[1], + dtype=np.float32 + ) + # TODO change bounds and types when and if `Box` supports JAX nativly + # self.action_space = gymnasium.spaces.Box(low=self.mjx_model.actuator_ctrlrange.T[0], high=self.mjx_model.actuator_ctrlrange.T[1], dtype=np.float32) + # observation_space: gymnasium.spaces.Box # set by subclass + + def initial(self, rng: jax.random.PRNGKey, params: Dict[str, any]) -> mjx.Data: + # TODO? find a more performant alternative that does not allocate? + mjx_data = mjx.make_data(self.model) + qpos, qvel, act = self._gen_init_physics_state(rng, params) + mjx_data = mjx_data.replace(qpos=qpos, qvel=qvel, act=act) + mjx_data = mjx.forward(self.mjx_model, mjx_data) + + return mjx_data + + def transition( + self, state: mjx.Data, action: jnp.ndarray, rng: jax.random.PRNGKey, params: Dict[str, any] + ) -> mjx.Data: + """Step through the simulator using `action` for `self.dt` (note `rng` argument is ignored).""" + mjx_data = state + + mjx_data = mjx_data.replace(ctrl=action) + mjx_data = jax.lax.fori_loop( + 0, self.frame_skip, lambda _, x: mjx.step(self.mjx_model, x), mjx_data + ) + + # TODO fix sensors with MJX>=3.2 + return mjx_data + + def reward( + self, + state: mjx.Data, + action: jnp.ndarray, + next_state: mjx.Data, + rng: jax.random.PRNGKey, + params: Dict[str, any] + ) -> jnp.ndarray: + return self._get_reward(state, action, next_state, params)[0] + + def transition_info( + self, + state: mjx.Data, + action: jnp.ndarray, + next_state: mjx.Data, + params: Dict[str, any], + ) -> dict: + return self._get_reward(state, action, next_state, params)[1] + + #TODO update RENDER + def render_init( + self, + params: Dict[str, any], + default_camera_config: "dict[str, float]" = {}, + camera_id: Union[int, None] = None, + camera_name: Union[str, None] = None, + max_geom=1000, + width=480, + height=480, + render_mode="rgb_array", + ) -> MujocoRenderer: + # TODO storing to much state? it should probably be moved internal to MujocoRenderer + self.render_mode = render_mode + + return MujocoRenderer( + self.model, + None, # no DATA + default_camera_config, + width, + height, + max_geom, + camera_id, + camera_name, + ) + + def render_image( + self, state: mjx.Data, render_state: MujocoRenderer, params: Dict[str, any], + ) -> "tuple[MujocoRenderer, Union[np.ndarray, None]]": + # NOTE this function can not be jitted + mjx_data = state + mujoco_renderer = render_state + + data = mjx.get_data(self.model, mjx_data) + mujoco.mj_forward(self.model, data) + + mujoco_renderer.data = data + + frame = mujoco_renderer.render( + self.render_mode, self.camera_id, self.camera_name + ) + + return mujoco_renderer, frame + + def render_close(self, render_state: MujocoRenderer, params: Dict[str, any]) -> None: + mujoco_renderer = render_state + if mujoco_renderer is not None: + mujoco_renderer.close() + + @property + def dt(self) -> float: + return self.mjx_model.opt.timestep * self.frame_skip + + def _gen_init_physics_state(self, rng, params: Dict[str, any]) -> "tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]": + """Generates the initial physics state. + Returns: `(qpos, qvel, act)` + """ + raise NotImplementedError + + def _get_reward( + self, + state: mjx.Data, + action: jnp.ndarray, + next_state: mjx.Data, + params: Dict[str, any], + ) -> "tuple[jnp.ndarray, dict[str, float]]": + """ + Generates `reward` and `transition_info`, we rely on the JIT's SEE to optimize it. + Returns: `(reward, transition_info)` + """ + raise NotImplementedError + + def observation(self, state: mjx.Data, rng: jax.random.PRNGKey, params: Dict[str, any]) -> jnp.ndarray: + raise NotImplementedError + + def terminal(self, state: mjx.Data, rng: jax.random.PRNGKey, params: Dict[str, any]) -> bool: + raise NotImplementedError + + def state_info(self, state: mjx.Data, params: Dict[str, any]) -> dict: + raise NotImplementedError + + +# TODO add vector environment +# TODO consider requirement of `metaworld` & `gymansium_robotics.RobotEnv` & `mo-gymnasium` +# TODO unit testing From 08299e708aa62102e44df46d6000d000097da6a2 Mon Sep 17 00:00:00 2001 From: Kallinteris Andreas Date: Mon, 5 Feb 2024 14:24:54 +0200 Subject: [PATCH 23/29] huge update --- gymnasium/envs/mjx/__init__.py | 1 + gymnasium/envs/mjx/ant.py | 189 +++++++++++++++++ gymnasium/envs/mjx/humanoid.py | 293 ++++++++++++++++++++++++++ gymnasium/envs/mjx/locomotion_2d.py | 292 ++++++++++---------------- gymnasium/envs/mjx/manipulation.py | 241 +++++++++++++++++++++ gymnasium/envs/mjx/mjx_env.py | 171 +++++++++------ gymnasium/envs/mjx/pendulum.py | 211 +++++++++++++++++++ gymnasium/envs/mjx/swimmer.py | 128 +++++++++++ gymnasium/envs/mujoco/f.py | 315 ---------------------------- 9 files changed, 1277 insertions(+), 564 deletions(-) create mode 100644 gymnasium/envs/mjx/ant.py create mode 100644 gymnasium/envs/mjx/humanoid.py create mode 100644 gymnasium/envs/mjx/manipulation.py create mode 100644 gymnasium/envs/mjx/pendulum.py create mode 100644 gymnasium/envs/mjx/swimmer.py delete mode 100644 gymnasium/envs/mujoco/f.py diff --git a/gymnasium/envs/mjx/__init__.py b/gymnasium/envs/mjx/__init__.py index ae16426ef..97a9e6c29 100644 --- a/gymnasium/envs/mjx/__init__.py +++ b/gymnasium/envs/mjx/__init__.py @@ -1 +1,2 @@ +"""Contains the base class and environments for MJX.""" from gymnasium.envs.mjx.mjx_env import MJXEnv diff --git a/gymnasium/envs/mjx/ant.py b/gymnasium/envs/mjx/ant.py new file mode 100644 index 000000000..f3d2534a6 --- /dev/null +++ b/gymnasium/envs/mjx/ant.py @@ -0,0 +1,189 @@ +"""Contains the class for the `Ant` environment.""" +import gymnasium + + +try: + import jax + from jax import numpy as jnp + from mujoco import mjx +except ImportError as e: + MJX_IMPORT_ERROR = e +else: + MJX_IMPORT_ERROR = None + +from typing import Dict, Tuple + +import numpy as np +from gymnasium.envs.mjx.mjx_env import MJXEnv + +from gymnasium.envs.mujoco.ant_v5 import DEFAULT_CAMERA_CONFIG + + +class Ant_MJXEnv(MJXEnv): + # NOTE: MJX does not yet support cfrc_ext and therefore this class can not be instantiated + """Class for Ant.""" + + def __init__( + self, + params: Dict[str, any], + ): + """Sets the `obveration_space`.""" + MJXEnv.__init__(self, params=params) + + obs_size = ( + self.mjx_model.nq + + self.mjx_model.nv + - 2 * params["exclude_current_positions_from_observation"] + + (self.mjx_model.nbody - 1) * 6 * params["include_cfrc_ext_in_observation"] + ) + self.observation_space = gymnasium.spaces.Box( + low=-np.inf, high=np.inf, shape=(obs_size,), dtype=np.float64 + ) + + self.observation_structure = { + "skipped_qpos": 2 * params["exclude_current_positions_from_observation"], + "qpos": self.mjx_model.nq + - 2 * params["exclude_current_positions_from_observation"], + "qvel": self.mjx_model.nv, + "cfrc_ext": (self.mjx_model.nbody - 1) + * 6 + * params["include_cfrc_ext_in_observation"], + } + + def _gen_init_physics_state( + self, rng, params: Dict[str, any] + ) -> Tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]: + """Sets `qpos` (positional elements) from a CUD and `qvel` (velocity elements) from a gaussian.""" + noise_low = -params["reset_noise_scale"] + noise_high = params["reset_noise_scale"] + + qpos = self.mjx_model.qpos0 + jax.random.uniform( + key=rng, minval=noise_low, maxval=noise_high, shape=(self.mjx_model.nq,) + ) + qvel = params["reset_noise_scale"] * jax.random.normal( + key=rng, shape=(self.mjx_model.nv,) + ) + act = jnp.empty(self.mjx_model.na) + + return qpos, qvel, act + + def observation( + self, state: mjx.Data, rng: jax.random.PRNGKey, params: Dict[str, any] + ) -> jnp.ndarray: + """Observes the `qpos` (posional elements) and `qvel` (velocity elements) and `cfrc_ext` (external contact forces) of the robot.""" + mjx_data = state + + position = mjx_data.qpos.flatten() + velocity = mjx_data.qvel.flatten() + + if params["exclude_current_positions_from_observation"]: + position = position[2:] + + if self._include_cfrc_ext_in_observation: + contact_force = self._get_contact_forces(mjx_data, params) + observation = jnp.concatenate((position, velocity, contact_force)) + else: + observation = jnp.concatenate((position, velocity)) + + return observation + + def _get_contact_forces(self, mjx_data: mjx.Data, params: Dict[str, any]): + """Get External Contact Forces (`cfrc_ext`) clipped by `contact_force_range`.""" + raw_contact_forces = mjx_data.cfrc_ext + min_value, max_value = params["contact_force_range"] + contact_forces = jnp.clip(raw_contact_forces, min_value, max_value) + return contact_forces + + def _get_reward( + self, + state: mjx.Data, + action: jnp.ndarray, + next_state: mjx.Data, + params: Dict[str, any], + ) -> Tuple[jnp.ndarray, Dict]: + """Reward = forward_reward + healthy_reward - ctrl_cost - contact cost.""" + mjx_data_old = state + mjx_data_new = next_state + + xy_position_before = mjx_data_old.xpos[params["main_body"], :2] + xy_position_after = mjx_data_new.xpos[params["main_body"], :2] + + xy_velocity = (xy_position_after - xy_position_before) / self.dt(params) + x_velocity, y_velocity = xy_velocity + + forward_reward = x_velocity * params["forward_reward_weight"] + healthy_reward = ( + self._gen_is_healthy(mjx_data_new, params) * params["healthy_reward"] + ) + rewards = forward_reward + healthy_reward + + ctrl_cost = params["ctrl_cost_weight"] * jnp.sum(jnp.square(action)) + contact_cost = params["contact_cost_weight"] * jnp.sum( + jnp.square(self._get_contact_forces(mjx_data_new, params)) + ) + costs = ctrl_cost + contact_cost + + reward = rewards - costs + + reward_info = { + "reward_forward": forward_reward, + "reward_ctrl": -ctrl_cost, + "reward_contact": -contact_cost, + "reward_survive": healthy_reward, + } + + return reward, reward_info + + def _gen_is_healty(self, state: mjx.Data, params: Dict[str, any]) -> jnp.ndarray: + """Checks if the robot is in a healthy potision.""" + mjx_data = state + + z = mjx_data.qpos[2] + min_z, max_z = params["healthy_z_range"] + is_healthy = ( + jnp.isfinite( + jnp.concatenate(mjx_data.qpos, mjx_data.qvel.mjx_data.act) + ).all() + and min_z <= z <= max_z + ) + return is_healthy + + def state_info(self, state: mjx.Data, params: Dict[str, any]) -> Dict[str, float]: + """Includes state information exclueded from `observation()`.""" + mjx_data = state + + info = { + "x_position": mjx_data.qpos[0], + "y_position": mjx_data.qpos[1], + "distance_from_origin": jnp.linalg.norm(mjx_data.qpos[0:2], ord=2), + } + return info + + def terminal( + self, state: mjx.Data, rng: jax.random.PRNGKey, params: Dict[str, any] + ) -> bool: + """Terminates if unhealthy.""" + return jnp.logical_and( + jnp.logical_not(self._gen_is_healty(state, params)), + params["terminate_when_unhealthy"], + ) + + def get_default_params(**kwargs) -> Dict[str, any]: + """Get the default parameter for the Ant environment.""" + default = { + "xml_file": "ant.xml", + "frame_skip": 5, + "default_camera_config": DEFAULT_CAMERA_CONFIG, + "forward_reward_weight": 1, + "ctrl_cost_weight": 0.5, + "contact_cost_weight": 5e-4, + "healthy_reward": 1.0, + "main_body": 1, + "terminate_when_unhealthy": True, + "healthy_z_range": (0.2, 1.0), + "contact_force_range": (-1.0, 1.0), + "reset_noise_scale": 0.1, + "exclude_current_positions_from_observation": True, + "include_cfrc_ext_in_observation": True, + } + return {**MJXEnv.get_default_params(), **default, **kwargs} diff --git a/gymnasium/envs/mjx/humanoid.py b/gymnasium/envs/mjx/humanoid.py new file mode 100644 index 000000000..86e0333de --- /dev/null +++ b/gymnasium/envs/mjx/humanoid.py @@ -0,0 +1,293 @@ +"""Contains the classes for the humaanoid environments environments, `Humanoid` and `HumanoidStandup`.""" +import gymnasium + + +try: + import jax + from jax import numpy as jnp + from mujoco import mjx +except ImportError as e: + MJX_IMPORT_ERROR = e +else: + MJX_IMPORT_ERROR = None + +from typing import Dict, Tuple + +import numpy as np + +from gymnasium.envs.mjx.mjx_env import MJXEnv +from gymnasium.envs.mujoco.humanoid_v5 import ( + DEFAULT_CAMERA_CONFIG as HUMANOID_DEFAULT_CAMERA_CONFIG, +) +from gymnasium.envs.mujoco.humanoidstandup_v5 import ( + DEFAULT_CAMERA_CONFIG as HUMANOIDSTANDUP_DEFAULT_CAMERA_CONFIG, +) + + +class BaseHumanoid_MJXEnv(MJXEnv): + # NOTE: MJX does not yet support many features therefore this class can not be instantiated + """Base environment class for humanoid environments such as Humanoid, & HumanoidStandup""" + + def __init__( + self, + params: Dict[str, any], + ): + """Sets the `obveration_space`.""" + MJXEnv.__init__(self, params=params) + + obs_size = ( + self.mjx_model.nq + + self.mjx_model.nv + - 2 * params["exclude_current_positions_from_observation"] + + (self.mjx_model.nbody - 1) * 10 * params["include_cinert_in_observation"] + + (self.mjx_model.nbody - 1) * 6 * params["include_cvel_in_observation"] + + (self.mjx_model.nv - 6) * params["include_qfrc_actuator_in_observation"] + + (self.mjx_model.nbody - 1) * 6 * params["include_cfrc_ext_in_observation"] + ) + + self.observation_space = gymnasium.spaces.Box( + low=-np.inf, high=np.inf, shape=(obs_size,), dtype=np.float64 + ) + + self.observation_structure = { + "skipped_qpos": 2 * params["exclude_current_positions_from_observation"], + "qpos": self.mjx_model.nq + - 2 * params["exclude_current_positions_from_observation"], + "qvel": self.mjx_model.nv, + "cinert": (self.mjx_model.nbody - 1) + * 10 + * params["include_cinert_in_observation"], + "cvel": (self.mjx_model.nbody - 1) + * 6 + * params["include_cvel_in_observation"], + "qfrc_actuator": (self.mjx_model.nv - 6) + * params["include_qfrc_actuator_in_observation"], + "cfrc_ext": (self.mjx_model.nbody - 1) + * 6 + * params["include_cfrc_ext_in_observation"], + "ten_lenght": 0, + "ten_velocity": 0, + } + + def observation( + self, state: mjx.Data, rng: jax.random.PRNGKey, params: Dict[str, any] + ) -> jnp.ndarray: + """Observes the `qpos` (posional elements) and `qvel` (velocity elements) and `cfrc_ext` (external contact forces) of the robot.""" + mjx_data = state + + position = mjx_data.qpos.flatten() + velocity = mjx_data.qvel.flatten() + + if params["exclude_current_positions_from_observation"]: + position = position[2:] + + if params["include_cinert_in_observation"] is True: + com_inertia = mjx_data.cinert[1:].flatten() + else: + com_inertia = jnp.array([]) + if params["include_cvel_in_observation"] is True: + com_velocity = mjx_data.cvel[1:].flatten() + else: + com_velocity = jnp.array([]) + + if params["include_qfrc_actuator_in_observation"] is True: + actuator_forces = mjx_data.qfrc_actuator[6:].flatten() + else: + actuator_forces = jnp.array([]) + if params["include_cfrc_ext_in_observation"] is True: + external_contact_forces = mjx_data.cfrc_ext[1:].flatten() + else: + external_contact_forces = jnp.array([]) + + observation = jnp.concatenate( + ( + position, + velocity, + com_inertia, + com_velocity, + actuator_forces, + external_contact_forces, + ) + ) + return observation + + def state_info(self, state: mjx.Data, params: Dict[str, any]) -> Dict[str, float]: + """Includes state information exclueded from `observation()`.""" + mjx_data = state + + info = { + "x_position": mjx_data.qpos[0], + "y_position": mjx_data.qpos[1], + "tendon_lenght": mjx_data.ten_length, + "tendon_velocity": mjx_data.ten_velocity, + "distance_from_origin": jnp.linalg.norm(mjx_data.qpos[0:2], ord=2), + } + return info + + def _gen_init_physics_state( + self, rng, params: Dict[str, any] + ) -> Tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]: + """Sets `qpos` (positional elements) and `qvel` (velocity elements) form a CUD.""" + noise_low = -params["reset_noise_scale"] + noise_high = params["reset_noise_scale"] + + qpos = self.mjx_model.qpos0 + jax.random.uniform( + key=rng, minval=noise_low, maxval=noise_high, shape=(self.mjx_model.nq,) + ) + qvel = jax.random.uniform( + key=rng, minval=noise_low, maxval=noise_high, shape=(self.mjx_model.nq,) + ) + act = jnp.empty(self.mjx_model.na) + + return qpos, qvel, act + + +class HumanoidMJXEnv(BaseHumanoid_MJXEnv): + """Class for Humanoid.""" + + def mass_center(self, mjx_data): + mass = np.expand_dims(self.mjx_model.body_mass, axis=1) + xpos = mjx_data.xipos + return (jnp.sum(mass * xpos, axis=0) / jnp.sum(mass))[0:2] + + def _get_reward( + self, + state: mjx.Data, + action: jnp.ndarray, + next_state: mjx.Data, + params: Dict[str, any], + ) -> Tuple[jnp.ndarray, Dict]: + """Reward = forward_reward + healthy_reward - ctrl_cost - contact_cost.""" + mjx_data_old = state + mjx_data_new = next_state + + xy_position_before = self.mass_center(mjx_data_old) + xy_position_after = self.mass_center(mjx_data_new) + + xy_velocity = (xy_position_after - xy_position_before) / self.dt + x_velocity, y_velocity = xy_velocity + + forward_reward = params["forward_reward_weight"] * x_velocity + healthy_reward = params["healthy_reward"] * self._gen_is_healty( + mjx_data_new, params + ) + rewards = forward_reward + healthy_reward + + ctrl_cost = params["ctrl_cost_weight"] * jnp.sum(jnp.square(action)) + contact_cost = self._get_conctact_cost(mjx_data_new, params) + costs = ctrl_cost + contact_cost + + reward = rewards - costs + + reward_info = { + "reward_survive": healthy_reward, + "reward_forward": forward_reward, + "reward_ctrl": -ctrl_cost, + "reward_contact": -contact_cost, + } + + return reward, reward_info + + def _get_conctact_cost(self, mjx_data: mjx.Data, params: Dict[str, any]): + contact_forces = mjx_data.cfrc_ext + contact_cost = params["contact_cost_weight"] * jnp.sum( + jnp.square(contact_forces) + ) + min_cost, max_cost = params["contact_cost_range"] + contact_cost = jnp.clip(contact_cost, min_cost, max_cost) + return contact_cost + + def _gen_is_healty(self, state: mjx.Data, params: Dict[str, any]): + """Checks if the robot is in a healthy potision.""" + mjx_data = state + + min_z, max_z = params["healthy_z_range"] + is_healthy = min_z < mjx_data.qpos[2] < max_z + + return is_healthy + + def terminal( + self, state: mjx.Data, rng: jax.random.PRNGKey, params: Dict[str, any] + ) -> bool: + """Terminates if unhealthy.""" + return jnp.logical_and( + jnp.logical_not(self._gen_is_healty(state, params)), + params["terminate_when_unhealthy"], + ) + + def get_default_params(**kwargs) -> Dict[str, any]: + """Get the default parameter for the Humanoid environment.""" + default = { + "xml_file": "humanoid.xml", + "frame_skip": 5, + "default_camera_config": HUMANOID_DEFAULT_CAMERA_CONFIG, + "forward_reward_weight": 1.25, + "ctrl_cost_weight": 0.1, + "contact_cost_weight": 5e-7, + "contact_cost_range": (-np.inf, 10.0), + "healthy_reward": 5.0, + "terminate_when_unhealthy": True, + "healthy_z_range": (1.0, 2.0), + "reset_noise_scale": 1e-2, + "exclude_current_positions_from_observation": True, + "include_cinert_in_observation": True, + "include_cvel_in_observation": True, + "include_qfrc_actuator_in_observation": True, + "include_cfrc_ext_in_observation": True, + } + return {**MJXEnv.get_default_params(), **default, **kwargs} + + +class HumanoidStandupMJXEnv(BaseHumanoid_MJXEnv): + """Class for HumanoidStandup.""" + + def _get_reward( + self, + state: mjx.Data, + action: jnp.ndarray, + next_state: mjx.Data, + params: Dict[str, any], + ) -> Tuple[jnp.ndarray, Dict]: + """Reward = uph_cost - quad_ctrl_cost - quad_impact_cost + 1.""" + mjx_data_new = next_state + + pos_after = mjx_data_new.qpos[2] + + uph_cost = (pos_after - 0) / self.mjx_model.opt.timestep + + quad_ctrl_cost = params["ctrl_cost_weight"] * jnp.square(action).sum() + + quad_impact_cost = ( + params["impact_cost_weight"] * jnp.square(mjx_data_new.cfrc_ext).sum() + ) + min_impact_cost, max_impact_cost = params["impact_cost_range"] + quad_impact_cost = np.clip(quad_impact_cost, min_impact_cost, max_impact_cost) + + reward = uph_cost - quad_ctrl_cost - quad_impact_cost + 1 + + reward_info = { + "reward_linup": uph_cost, + "reward_quadctrl": -quad_ctrl_cost, + "reward_impact": -quad_impact_cost, + } + + return reward, reward_info + + def get_default_params(**kwargs) -> Dict[str, any]: + """Get the default parameter for the Humanoid environment.""" + default = { + "xml_file": "humanoidstandup.xml", + "frame_skip": 5, + "default_camera_config": HUMANOIDSTANDUP_DEFAULT_CAMERA_CONFIG, + "uph_cost_weight": 1, + "ctrl_cost_weight": 0.1, + "impact_cost_weight": 0.5e-6, + "impact_cost_range": (-np.inf, 10.0), + "reset_noise_scale": 1e-2, + "exclude_current_positions_from_observation": True, + "include_cinert_in_observation": True, + "include_cvel_in_observation": True, + "include_qfrc_actuator_in_observation": True, + "include_cfrc_ext_in_observation": True, + } + return {**MJXEnv.get_default_params(), **default, **kwargs} diff --git a/gymnasium/envs/mjx/locomotion_2d.py b/gymnasium/envs/mjx/locomotion_2d.py index c9060eed1..e7a4968dd 100644 --- a/gymnasium/envs/mjx/locomotion_2d.py +++ b/gymnasium/envs/mjx/locomotion_2d.py @@ -1,5 +1,7 @@ +"""Contains the classes for the 2d locomotion environments, `HalfCheetah`, `Hopper` and `Walker2D`.""" import gymnasium + try: import jax from jax import numpy as jnp @@ -9,74 +11,31 @@ else: MJX_IMPORT_ERROR = None +from typing import Dict, Tuple + import numpy as np -from typing import Tuple -from gymnasium.envs.mujoco import MujocoRenderer from gymnasium.envs.mjx.mjx_env import MJXEnv -from gymnasium.envs.mujoco.half_cheetah_v5 import DEFAULT_CAMERA_CONFIG - -from typing import Dict - - -#class Locomotion_2d_Env(MJXEnv, gymnasium.utils.EzPickle): -class Locomotion_2d_Env(MJXEnv): +from gymnasium.envs.mujoco.half_cheetah_v5 import ( + DEFAULT_CAMERA_CONFIG as HALFCHEETAH_DEFAULT_CAMERA_CONFIG, +) +from gymnasium.envs.mujoco.hopper_v5 import ( + DEFAULT_CAMERA_CONFIG as HOPPER_DEFAULT_CAMERA_CONFIG, +) +from gymnasium.envs.mujoco.walker2d_v5 import ( + DEFAULT_CAMERA_CONFIG as WALKER2D_DEFAULT_CAMERA_CONFIG, +) + + +class Locomotion_2d_MJXEnv(MJXEnv): """Base environment class for 2d locomotion environments such as HalfCheetah, Hopper & Walker2d.""" + def __init__( self, - xml_file: str, - frame_skip: int, - params: Dict[str, any], # NOTE not API compliant - #forward_reward_weight: float, - #ctrl_cost_weight: float, - #healthy_reward: float, - #terminate_when_unhealthy: bool, - #healthy_state_range: Tuple[float, float], - #healthy_z_range: Tuple[float, float], - #healthy_angle_range: Tuple[float, float], - #reset_noise_scale: float, - #exclude_current_positions_from_observation: bool, - #**kwargs, + params: Dict[str, any], # NOTE not API compliant (yet?) ): - """ - gymnasium.utils.EzPickle.__init__( - self, - xml_file, - frame_skip, - forward_reward_weight, - ctrl_cost_weight, - healthy_reward, - terminate_when_unhealthy, - healthy_state_range, - healthy_z_range, - healthy_angle_range, - reset_noise_scale, - exclude_current_positions_from_observation, - **kwargs, - ) - - self._forward_reward_weight = forward_reward_weight - self._ctrl_cost_weight = ctrl_cost_weight - self._healthy_reward = healthy_reward - - self._terminate_when_unhealthy = terminate_when_unhealthy - self._healthy_state_range = healthy_state_range - self._healthy_z_range = healthy_z_range - self._healthy_angle_range = healthy_angle_range - - self._reset_noise_scale = reset_noise_scale - - self._exclude_current_positions_from_observation = ( - exclude_current_positions_from_observation - ) - """ - - MJXEnv.__init__( - self, - model_path=xml_file, - frame_skip=frame_skip, - #**kwargs, - ) + """Sets the `obveration.shape`.""" + MJXEnv.__init__(self, params=params) obs_size = ( self.mjx_model.nq @@ -90,11 +49,15 @@ def __init__( self.observation_structure = { "skipped_qpos": 1 * params["exclude_current_positions_from_observation"], - "qpos": self.mjx_model.nq - 1 * params["exclude_current_positions_from_observation"], + "qpos": self.mjx_model.nq + - 1 * params["exclude_current_positions_from_observation"], "qvel": self.mjx_model.nv, } - def _gen_init_physics_state(self, rng, params: Dict[str, any]) -> "tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]": + def _gen_init_physics_state( + self, rng, params: Dict[str, any] + ) -> Tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]: + """Sets `qpos` (positional elements) from a CUD and `qvel` (velocity elements) from a gaussian.""" noise_low = -params["reset_noise_scale"] noise_high = params["reset_noise_scale"] @@ -108,7 +71,10 @@ def _gen_init_physics_state(self, rng, params: Dict[str, any]) -> "tuple[jnp.nda return qpos, qvel, act - def observation(self, state: mjx.Data, rng: jax.random.PRNGKey, params: Dict[str, any]) -> jnp.ndarray: + def observation( + self, state: mjx.Data, rng: jax.random.PRNGKey, params: Dict[str, any] + ) -> jnp.ndarray: + """Observes the `qpos` (posional elements) and `qvel` (velocity elements) of the robot.""" mjx_data = state position = mjx_data.qpos.flatten() @@ -126,16 +92,19 @@ def _get_reward( action: jnp.ndarray, next_state: mjx.Data, params: Dict[str, any], - ) -> "tuple[jnp.ndarray, dict]": + ) -> Tuple[jnp.ndarray, Dict]: + """Reward = foward_reward + healty_reward - control_cost.""" mjx_data_old = state mjx_data_new = next_state x_position_before = mjx_data_old.qpos[0] x_position_after = mjx_data_new.qpos[0] - x_velocity = (x_position_after - x_position_before) / self.dt + x_velocity = (x_position_after - x_position_before) / self.dt(params) forward_reward = params["forward_reward_weight"] * x_velocity - healthy_reward = params["healthy_reward"] * float(self._gen_is_healty(mjx_data_new, params)) + healthy_reward = params["healthy_reward"] * self._gen_is_healty( + mjx_data_new, params + ) rewards = forward_reward + healthy_reward costs = ctrl_cost = params["ctrl_cost_weight"] * jnp.sum(jnp.square(action)) @@ -150,85 +119,61 @@ def _get_reward( return reward, reward_info - def terminal(self, state: mjx.Data, rng: jax.random.PRNGKey, params: Dict[str, any]) -> bool: - return (not self._gen_is_healty(state, params)) and params["terminate_when_unhealthy"] + def terminal( + self, state: mjx.Data, rng: jax.random.PRNGKey, params: Dict[str, any] + ) -> bool: + """Terminates if unhealthy.""" + return jnp.logical_and( + jnp.logical_not(self._gen_is_healty(state, params)), + params["terminate_when_unhealthy"], + ) - def state_info(self, state: mjx.Data, params: Dict[str, any]) -> dict[str, float]: + def state_info(self, state: mjx.Data, params: Dict[str, any]) -> Dict[str, float]: + """Includes state information exclueded from `observation()`.""" mjx_data = state - x_position_after = mjx_data.qpos[0] + info = { - "x_position": x_position_after, + "x_position": mjx_data.qpos[0], } return info - def render_init( - self, default_camera_config: "dict[str, float]" = DEFAULT_CAMERA_CONFIG, **kwargs - ) -> MujocoRenderer: - return super().render_init( - default_camera_config=default_camera_config, **kwargs - ) - def _gen_is_healty(self, state: mjx.Data, params: Dict[str, any]): + """Checks if the robot is a healthy potision.""" mjx_data = state z, angle = mjx_data.qpos[1:3] - physics_state = jnp.concatenate((mjx_data.qpos[2:], mjx_data.qvel, mjx_data.act)) + physics_state = jnp.concatenate( + (mjx_data.qpos[2:], mjx_data.qvel, mjx_data.act) + ) min_state, max_state = params["healthy_state_range"] min_z, max_z = params["healthy_z_range"] min_angle, max_angle = params["healthy_angle_range"] - healthy_state = jnp.all(jnp.logical_and(min_state < physics_state, physics_state < max_state)) + healthy_state = jnp.all( + jnp.logical_and(min_state < physics_state, physics_state < max_state) + ) healthy_z = jnp.logical_and(min_z < z, z < max_z) healthy_angle = jnp.logical_and(min_angle < angle, angle < max_angle) - return False - #is_healthy = all((healthy_state, healthy_z, healthy_angle)) - #is_healthy = jnp.all(jnp.concatenate((healthy_state, healthy_z, healthy_angle))) - #is_healthy = bool(healthy_state) and bool(healthy_z) and bool(healthy_angle) - is_healthy = healthy_state + # NOTE there is probably a clearer way to write this + is_healthy = jnp.logical_and( + jnp.logical_and(healthy_state, healthy_z), healthy_angle + ) + return is_healthy -# The following could be implemented as **kwargs in register() -# TODO fix camera configs -class HalfCheetahMJXEnv(Locomotion_2d_Env): - def __init__( - self, - params: Dict[str, any], - xml_file: str = "half_cheetah.xml", - frame_skip: int = 5, - #forward_reward_weight: float = 1.0, - #ctrl_cost_weight: float = 0.1, - #healthy_reward: float = 0, - #terminate_when_unhealthy: bool = True, - #healthy_state_range: Tuple[float, float] = (-jnp.inf, jnp.inf), - #healthy_z_range: Tuple[float, float] = (-jnp.inf, jnp.inf), - #healthy_angle_range: Tuple[float, float] = (-jnp.inf, jnp.inf), - #reset_noise_scale: float = 0.1, - #exclude_current_positions_from_observation: bool = True, - #**kwargs, - ): - super().__init__( - params=params, - xml_file=xml_file, - frame_skip=frame_skip, - #forward_reward_weight=forward_reward_weight, - #ctrl_cost_weight=ctrl_cost_weight, - #healthy_reward=healthy_reward, - #terminate_when_unhealthy=terminate_when_unhealthy, - #healthy_state_range=healthy_state_range, - #healthy_z_range=healthy_z_range, - #healthy_angle_range=healthy_angle_range, - #reset_noise_scale=reset_noise_scale, - #exclude_current_positions_from_observation=exclude_current_positions_from_observation, - #**kwargs, - ) +# The following could maybe be implemented as **kwargs in register() +class HalfCheetahMJXEnv(Locomotion_2d_MJXEnv): + """Class for HalfCheetah.""" - def get_default_params(**kwargs) -> dict[str, any]: + def get_default_params(**kwargs) -> Dict[str, any]: + """Get the default parameter for the HalfCheetah environment.""" default = { "xml_file": "half_cheetah.xml", "frame_skip": 5, + "default_camera_config": HALFCHEETAH_DEFAULT_CAMERA_CONFIG, "forward_reward_weight": 1.0, "ctrl_cost_weight": 0.1, "healthy_reward": 0, @@ -236,71 +181,52 @@ def get_default_params(**kwargs) -> dict[str, any]: "healthy_state_range": (-jnp.inf, jnp.inf), "healthy_z_range": (-jnp.inf, jnp.inf), "healthy_angle_range": (-jnp.inf, jnp.inf), - "reset_noise_scale" : 0.1, + "reset_noise_scale": 0.1, "exclude_current_positions_from_observation": True, } - return {**default, **kwargs} + return {**Locomotion_2d_MJXEnv.get_default_params(), **default, **kwargs} -class HopperMJXEnv(Locomotion_2d_Env): - def __init__( - self, - xml_file: str = "hopper.xml", - frame_skip: int = 4, - #forward_reward_weight: float = 1.0, - #ctrl_cost_weight: float = 1e-3, - #healthy_reward: float = 1.0, - #terminate_when_unhealthy: bool = True, - #healthy_state_range: Tuple[float, float] = (-100.0, 100.0), - #healthy_z_range: Tuple[float, float] = (0.7, jnp.inf), - #healthy_angle_range: Tuple[float, float] = (-0.2, 0.2), - #reset_noise_scale: float = 5e-3, - #exclude_current_positions_from_observation: bool = True, - #**kwargs, - ): - super().__init__( - xml_file=xml_file, - frame_skip=frame_skip, - #forward_reward_weight=forward_reward_weight, - #ctrl_cost_weight=ctrl_cost_weight, - #healthy_reward=healthy_reward, - #terminate_when_unhealthy=terminate_when_unhealthy, - #healthy_state_range=healthy_state_range, - #healthy_z_range=healthy_z_range, - #healthy_angle_range=healthy_angle_range, - #reset_noise_scale=reset_noise_scale, - #exclude_current_positions_from_observation=exclude_current_positions_from_observation, - #**kwargs, - ) +class HopperMJXEnv(Locomotion_2d_MJXEnv): + # NOTE: MJX does not yet support condim=1 and therefore this class can not be instantiated + """Class for Hopper.""" + + def get_default_params(**kwargs) -> Dict[str, any]: + """Get the default parameter for the Hopper environment.""" + default = { + "xml_file": "hopper.xml", + "frame_skip": 4, + "default_camera_config": HOPPER_DEFAULT_CAMERA_CONFIG, + "forward_reward_weight": 1.0, + "ctrl_cost_weight": 1e-3, + "healthy_reward": 1.0, + "terminate_when_unhealthy": True, + "healthy_state_range": (-100.0, 100.0), + "healthy_z_range": (0.7, jnp.inf), + "healthy_angle_range": (-0.2, 0.2), + "reset_noise_scale": 5e-3, + "exclude_current_positions_from_observation": True, + } + return {**Locomotion_2d_MJXEnv.get_default_params(), **default, **kwargs} -class Walker2dMJXEnv(Locomotion_2d_Env): - def __init__( - self, - xml_file: str = "walker2d_v5.xml", - frame_skip: int = 4, - #forward_reward_weight: float = 1.0, - #ctrl_cost_weight: float = 1e-3, - #healthy_reward: float = 1.0, - #terminate_when_unhealthy: bool = True, - #healthy_state_range: Tuple[float, float] = (-jnp.inf, jnp.inf), - #healthy_z_range: Tuple[float, float] = (0.8, 2.0), - #healthy_angle_range: Tuple[float, float] = (-1.0, 1.0), - #reset_noise_scale: float = 5e-3, - #exclude_current_positions_from_observation: bool = True, - #**kwargs, - ): - super().__init__( - xml_file=xml_file, - frame_skip=frame_skip, - #forward_reward_weight=forward_reward_weight, - #ctrl_cost_weight=ctrl_cost_weight, - #healthy_reward=healthy_reward, - #terminate_when_unhealthy=terminate_when_unhealthy, - #healthy_state_range=healthy_state_range, - #healthy_z_range=healthy_z_range, - #healthy_angle_range=healthy_angle_range, - #reset_noise_scale=reset_noise_scale, - #exclude_current_positions_from_observation=exclude_current_positions_from_observation, - #**kwargs, - ) +class Walker2dMJXEnv(Locomotion_2d_MJXEnv): + """Class for Walker2d.""" + + def get_default_params(**kwargs) -> Dict[str, any]: + """Get the default parameter for the Walker2d environment.""" + default = { + "xml_file": "walker2d_v5.xml", + "frame_skip": 4, + "default_camera_config": WALKER2D_DEFAULT_CAMERA_CONFIG, + "forward_reward_weight": 1.0, + "ctrl_cost_weight": 1e-3, + "healthy_reward": 1.0, + "terminate_when_unhealthy": True, + "healthy_state_range": (-jnp.inf, jnp.inf), + "healthy_z_range": (0.8, 2.0), + "healthy_angle_range": (-1.0, 1.0), + "reset_noise_scale": 5e-3, + "exclude_current_positions_from_observation": True, + } + return {**Locomotion_2d_MJXEnv.get_default_params(), **default, **kwargs} diff --git a/gymnasium/envs/mjx/manipulation.py b/gymnasium/envs/mjx/manipulation.py new file mode 100644 index 000000000..8d46e416f --- /dev/null +++ b/gymnasium/envs/mjx/manipulation.py @@ -0,0 +1,241 @@ +"""Contains the classes for the manipulation environments, `Pusher`, `Reacher`.""" +import gymnasium + + +try: + import jax + from jax import numpy as jnp + from mujoco import mjx +except ImportError as e: + MJX_IMPORT_ERROR = e +else: + MJX_IMPORT_ERROR = None + +from typing import Dict, Tuple + +import numpy as np +from gymnasium.envs.mjx.mjx_env import MJXEnv + +from gymnasium.envs.mujoco.pusher_v5 import ( + DEFAULT_CAMERA_CONFIG as PUSHER_DEFAULT_CAMERA_CONFIG, +) +from gymnasium.envs.mujoco.reacher_v5 import ( + DEFAULT_CAMERA_CONFIG as REACHER_HOPPER_DEFAULT_CAMERA_CONFIG, +) + + +class Reacher_MJXEnv(MJXEnv): + """Class for Reacher.""" + + def __init__( + self, + params: Dict[str, any], + ): + """Sets the `obveration_space`.""" + MJXEnv.__init__(self, params=params) + + self.observation_space = gymnasium.spaces.Box( # TODO use jnp when and if `Box` supports jax natively + low=-np.inf, high=np.inf, shape=(10,), dtype=np.float32 + ) + + def _gen_init_physics_state( + self, rng, params: Dict[str, any] + ) -> Tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]: + """Sets `arm.qpos` (positional elements) and `arm.qvel` (velocity elements) from a CUD and the `goal.qpos` from a cicrular uniform distribution.""" + qpos = self.mjx_model.qpos0 + jax.random.uniform( + key=rng, minval=-0.1, maxval=0.1, shape=(self.mjx_model.nq,) + ) + + while True: + goal = jax.random.uniform(key=rng, minval=-0.2, maxval=0.2, shape=(2,)) + if jnp.less(jnp.linalg.norm(goal), jnp.array(0.2)): + break + qpos.at[-2:].set(goal) + + qvel = jax.random.uniform( + key=rng, minval=-0.005, maxval=0.005, shape=(self.mjx_model.nv,) + ) + qvel.at[-2:].set(jnp.zeros(2)) + + act = jnp.empty(self.mjx_model.na) + + return qpos, qvel, act + + def _get_goal(self, mjx_data: mjx.Data) -> jnp.ndarray: + return mjx_data.qpos[-2:] + + def _set_goal(self, mjx_data: mjx.Data, goal: jnp.ndarray) -> mjx.Data: + """Add the coordinate of `goal` to `mjx_data`.""" + mjx_data = mjx_data.replace(qpos=mjx_data.qpos.at[-2:].set(goal)) + return mjx_data + + def observation( + self, state: mjx.Data, rng: jax.random.PRNGKey, params: Dict[str, any] + ) -> jnp.ndarray: + """Observes the `sin(theta)` & `cos(theta)` & `qpos` & `qvel` & 'fingertip - target' distance""" + mjx_data = state + + position = mjx_data.qpos.flatten() + velocity = mjx_data.qvel.flatten() + theta = position[:2] + + fingertip_position = mjx_data.xpos[3] # TODO make this dynamic + target_position = mjx_data.xpos[4] # TODO make this dynamic + observation = jnp.concatenate( + ( + jnp.cos(theta), + jnp.sin(theta), + position[2:], + velocity[:2], + (fingertip_position - target_position)[:2], + ) + ) + + return observation + + def _get_reward( + self, + state: mjx.Data, + action: jnp.ndarray, + next_state: mjx.Data, + params: Dict[str, any], + ) -> Tuple[jnp.ndarray, Dict]: + """Reward = reward_dist + reward_ctrl.""" + mjx_data = next_state + + fingertip_position = mjx_data.xpos[3] # TODO make this dynamic + target_position = mjx_data.xpos[4] # TODO make this dynamic + + vec = fingertip_position - target_position + reward_dist = -jnp.linalg.norm(vec) * params["reward_dist_weight"] + reward_ctrl = -jnp.square(action).sum() * params["reward_control_weight"] + + reward = reward_dist + reward_ctrl + + reward_info = { + "reward_dist": reward_dist, + "reward_ctrl": reward_ctrl, + } + + return reward, reward_info + + def get_default_params(**kwargs) -> Dict[str, any]: + """Get the default parameter for the Reacher environment.""" + default = { + "xml_file": "reacher.xml", + "frame_skip": 2, + "default_camera_config": REACHER_HOPPER_DEFAULT_CAMERA_CONFIG, + "reward_dist_weight": 1, + "reward_control_weight": 1, + } + return {**MJXEnv.get_default_params(), **default, **kwargs} + + +class Pusher_MJXEnv(MJXEnv): + # NOTE: MJX does not yet support condim=1 and therefore this class can not be instantiated + """Class for Pusher.""" + + def __init__( + self, + params: Dict[str, any], + ): + """Sets the `obveration_space`.""" + MJXEnv.__init__(self, params=params) + + self.observation_space = gymnasium.spaces.Box( # TODO use jnp when and if `Box` supports jax natively + low=-np.inf, high=np.inf, shape=(23,), dtype=np.float32 + ) + + def _gen_init_physics_state( + self, rng, params: Dict[str, any] + ) -> Tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]: + """Sets `arm.qpos` (positional elements) and `arm.qvel` (velocity elements) from a CUD and the `goal.qpos` from a cicrular uniform distribution.""" + qpos = self.mjx_model.qpos0 + + goal_pos = jnp.zeroes(2) + while True: + cylinder_pos = np.concatenate( + [ + jax.random.uniform(key=rng, minval=-0.3, maxval=0.3, shape=1), + jax.random.uniform(key=rng, minval=-0.2, maxval=0.2, shape=1), + ] + ) + if jnp.linalg.norm(cylinder_pos - goal_pos) > 0.17: + break + + qpos.at[-4:-2].set(cylinder_pos) + qpos.at[-2:].set(goal_pos) + qvel = jax.random.uniform( + key=rng, minval=-0.005, maxval=0.005, shape=(self.mjx_model.nv,) + ) + qvel.at[-4:].set(0) + + act = jnp.empty(self.mjx_model.na) + + return qpos, qvel, act + + def observation( + self, state: mjx.Data, rng: jax.random.PRNGKey, params: Dict[str, any] + ) -> jnp.ndarray: + """Observes the & `qpos` & `qvel` & `tips_arm` & `object` `goal`.""" + mjx_data = state + + position = mjx_data.qpos.flatten() + velocity = mjx_data.qvel.flatten() + tips_arm_position = mjx_data.xpos[10] # TODO make this dynamic + object_position = mjx_data.xpos[11] # TODO make this dynamic + goal_position = mjx_data.xpos[12] # TODO make this dynamic + + observation = jnp.concatenate( + ( + position[:7], + velocity[:7], + tips_arm_position, + object_position, + goal_position, + ) + ) + + return observation + + def _get_reward( + self, + state: mjx.Data, + action: jnp.ndarray, + next_state: mjx.Data, + params: Dict[str, any], + ) -> Tuple[jnp.ndarray, Dict]: + """Reward = reward_dist + reward_ctrl + reward_near.""" + mjx_data = next_state + tips_arm_position = mjx_data.xpos[10] # TODO make this dynamic + object_position = mjx_data.xpos[11] # TODO make this dynamic + goal_position = mjx_data.xpos[12] # TODO make this dynamic + + vec_1 = object_position - tips_arm_position + vec_2 = object_position - goal_position + + reward_near = -jnp.linalg.norm(vec_1) * params["reward_near_weight"] + reward_dist = -jnp.linalg.norm(vec_2) * params["reward_dist_weight"] + reward_ctrl = -jnp.square(action).sum() * params["reward_control_weight"] + + reward = reward_dist + reward_ctrl + reward_near + + reward_info = { + "reward_dist": reward_dist, + "reward_ctrl": reward_ctrl, + "reward_near": reward_near, + } + + return reward, reward_info + + def get_default_params(**kwargs) -> Dict[str, any]: + """Get the default parameter for the Reacher environment.""" + default = { + "xml_file": "pusher.xml", + "frame_skip": 5, + "default_camera_config": PUSHER_DEFAULT_CAMERA_CONFIG, + "reward_near_weight": 0.5, + "reward_dist_weight": 1, + "reward_control_weight": 0.1, + } + return {**MJXEnv.get_default_params(), **default, **kwargs} diff --git a/gymnasium/envs/mjx/mjx_env.py b/gymnasium/envs/mjx/mjx_env.py index 1d9a4ce30..8208ff03e 100644 --- a/gymnasium/envs/mjx/mjx_env.py +++ b/gymnasium/envs/mjx/mjx_env.py @@ -1,10 +1,14 @@ +"""Contains the base class for MJX based robot environments. + +Note: This is expted to be used my `gymnasium`, `gymnasium-robotics`, `metaworld` and 3rd party libraries. +""" +from typing import Dict, Tuple, Union + import numpy as np -from typing import Union import gymnasium from gymnasium.envs.mujoco import MujocoRenderer from gymnasium.envs.mujoco.mujoco_env import expand_model_path -from typing import Dict try: @@ -47,51 +51,53 @@ def mjx_set_physics_state(mjx_data: mjx.Data, mjx_physics_state) -> mjx.Data: """ +# TODO add type hint to `params` +# TODO add render `metadata` +# TODO add init_qvel class MJXEnv( gymnasium.functional.FuncEnv[ - #mjx.Data, jnp.ndarray, jnp.ndarray, jnp.ndarray, bool, MujocoRenderer, Dict[str, any] - mjx.Data, jnp.ndarray, jnp.ndarray, jnp.ndarray, bool, MujocoRenderer, + mjx.Data, + jnp.ndarray, + jnp.ndarray, + jnp.ndarray, + bool, + MujocoRenderer, + Dict[str, any], ] ): - """The Base class for MJX Environments in Gymnasium.""" + """The Base class for MJX Environments in Gymnasium. + `observation`, `terminal`, and `state_info` should be defined in sub-classes. """ - def mjx_get_physics_state_put_version(self, mjx_data: mjx.Data) -> np.ndarray: - ""version based on @btaba suggestion"" - # data = mujoco.MjData(self.model) - # mjx.device_get_into(data, mjx_data) - data = mjx.get_data(self.model, mjx_data) - state = np.empty(mujoco.mj_stateSize(self.model, mujoco.mjtState.mjSTATE_PHYSICS)) - mujoco.mj_getState(self.model, data, state, spec=mujoco.mjtState.mjSTATE_PHYSICS) - return state - """ + def __init__(self, params: Dict[str, any]): + """Create the `mjx.Model` of the robot defined in `params["xml_file"]`. - def __init__(self, model_path, frame_skip): - # NOTE can not be JITted because mjx_model.actuator_ctrlrange does not support jax.numpy + Keep `mujoco.MjModel` of model for rendering purposes. + The Sub-class environments are expected to define `self.observation_space` + """ if MJX_IMPORT_ERROR is not None: raise gymnasium.error.DependencyNotInstalled( f"{MJX_IMPORT_ERROR}. " - "(HINT: you need to install mujoco, run `pip install gymnasium[mjx]`.)" # TODO actually create gymnasium[mjx] + "(HINT: you need to install mujoco-mjx, run `pip install gymnasium[mjx]`.)" # TODO actually create gymnasium[mjx] ) - fullpath = expand_model_path(model_path) - - self.frame_skip = frame_skip + fullpath = expand_model_path(params["xml_file"]) self.model = mujoco.MjModel.from_xml_path(fullpath) self.mjx_model = mjx.put_model(self.model) + # observation_space: gymnasium.spaces.Box # set by subclass self.action_space = gymnasium.spaces.Box( low=self.model.actuator_ctrlrange.T[0], high=self.model.actuator_ctrlrange.T[1], - dtype=np.float32 + dtype=np.float32, ) # TODO change bounds and types when and if `Box` supports JAX nativly # self.action_space = gymnasium.spaces.Box(low=self.mjx_model.actuator_ctrlrange.T[0], high=self.mjx_model.actuator_ctrlrange.T[1], dtype=np.float32) - # observation_space: gymnasium.spaces.Box # set by subclass def initial(self, rng: jax.random.PRNGKey, params: Dict[str, any]) -> mjx.Data: + """Initializes and returns the `mjx.Data`.""" # TODO? find a more performant alternative that does not allocate? mjx_data = mjx.make_data(self.model) qpos, qvel, act = self._gen_init_physics_state(rng, params) @@ -101,14 +107,18 @@ def initial(self, rng: jax.random.PRNGKey, params: Dict[str, any]) -> mjx.Data: return mjx_data def transition( - self, state: mjx.Data, action: jnp.ndarray, rng: jax.random.PRNGKey, params: Dict[str, any] + self, + state: mjx.Data, + action: jnp.ndarray, + rng: jax.random.PRNGKey, + params: Dict[str, any], ) -> mjx.Data: - """Step through the simulator using `action` for `self.dt` (note `rng` argument is ignored).""" + """Step through the simulator using `action` for `self.dt` (note: `rng` argument is ignored).""" mjx_data = state mjx_data = mjx_data.replace(ctrl=action) mjx_data = jax.lax.fori_loop( - 0, self.frame_skip, lambda _, x: mjx.step(self.mjx_model, x), mjx_data + 0, params["frame_skip"], lambda _, x: mjx.step(self.mjx_model, x), mjx_data ) # TODO fix sensors with MJX>=3.2 @@ -120,8 +130,9 @@ def reward( action: jnp.ndarray, next_state: mjx.Data, rng: jax.random.PRNGKey, - params: Dict[str, any] + params: Dict[str, any], ) -> jnp.ndarray: + """Returns the reward.""" return self._get_reward(state, action, next_state, params)[0] def transition_info( @@ -130,39 +141,36 @@ def transition_info( action: jnp.ndarray, next_state: mjx.Data, params: Dict[str, any], - ) -> dict: + ) -> Dict: + """Includes just reward info.""" return self._get_reward(state, action, next_state, params)[1] - #TODO update RENDER def render_init( self, params: Dict[str, any], - default_camera_config: "dict[str, float]" = {}, - camera_id: Union[int, None] = None, - camera_name: Union[str, None] = None, - max_geom=1000, - width=480, - height=480, - render_mode="rgb_array", ) -> MujocoRenderer: - # TODO storing to much state? it should probably be moved internal to MujocoRenderer - self.render_mode = render_mode - + """Returns a `MujocoRenderer` object.""" return MujocoRenderer( self.model, - None, # no DATA - default_camera_config, - width, - height, - max_geom, - camera_id, - camera_name, + None, # no MuJoCo DATA + params["default_camera_config"], + params["width"], + params["height"], + params["max_geom"], + params["camera_id"], + params["camera_name"], ) def render_image( - self, state: mjx.Data, render_state: MujocoRenderer, params: Dict[str, any], - ) -> "tuple[MujocoRenderer, Union[np.ndarray, None]]": - # NOTE this function can not be jitted + self, + state: mjx.Data, + render_state: MujocoRenderer, + params: Dict[str, any], + ) -> Tuple[MujocoRenderer, Union[np.ndarray, None]]: + """Renders the `mujoco` frame of the environment by converting `mjx.Data` to `mujoco.MjData`. + + NOTE: this function can not be jitted. + """ mjx_data = state mujoco_renderer = render_state @@ -171,23 +179,29 @@ def render_image( mujoco_renderer.data = data - frame = mujoco_renderer.render( - self.render_mode, self.camera_id, self.camera_name - ) + frame = mujoco_renderer.render(params["render_mode"]) return mujoco_renderer, frame - def render_close(self, render_state: MujocoRenderer, params: Dict[str, any]) -> None: + def render_close( + self, render_state: MujocoRenderer, params: Dict[str, any] + ) -> None: + """Closes the `MujocoRender` object.""" mujoco_renderer = render_state if mujoco_renderer is not None: mujoco_renderer.close() - @property - def dt(self) -> float: - return self.mjx_model.opt.timestep * self.frame_skip + def dt(self, params: Dict[str, any]) -> float: + """Returns the duration between timesteps (`dt`).""" + return self.mjx_model.opt.timestep * params["frame_skip"] - def _gen_init_physics_state(self, rng, params: Dict[str, any]) -> "tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]": + def _gen_init_physics_state( + self, rng, params: Dict[str, any] + ) -> Tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]: """Generates the initial physics state. + + `MJXEnv` Equivalent of `MujocoEnv.model.` + Returns: `(qpos, qvel, act)` """ raise NotImplementedError @@ -198,21 +212,46 @@ def _get_reward( action: jnp.ndarray, next_state: mjx.Data, params: Dict[str, any], - ) -> "tuple[jnp.ndarray, dict[str, float]]": - """ - Generates `reward` and `transition_info`, we rely on the JIT's SEE to optimize it. - Returns: `(reward, transition_info)` + ) -> Tuple[jnp.ndarray, Dict[str, float]]: + """Generates `reward` and `transition_info`, we rely on the JIT's SEE to optimize it. + + Returns: `(reward, reward_info)` """ raise NotImplementedError - def observation(self, state: mjx.Data, rng: jax.random.PRNGKey, params: Dict[str, any]) -> jnp.ndarray: - raise NotImplementedError + def terminal( + self, + state: mjx.Data, + rng: jax.random.PRNGKey, + params: Dict[str, any] | None = None, + ) -> jnp.ndarray: + """Should be overwritten if the sub-class environment terminates.""" + return jnp.array(False) + + def get_default_params(**kwargs) -> Dict[str, any]: + """Generate the default parameters for rendering.""" + default = { + "default_camera_config": {}, + "camera_id": None, + "camera_name": None, + "max_geom": 1000, + "width": 480, + "height": 480, + "render_mode": None, + } + return default - def terminal(self, state: mjx.Data, rng: jax.random.PRNGKey, params: Dict[str, any]) -> bool: - raise NotImplementedError + """ + def mjx_get_physics_state_put_version(self, mjx_data: mjx.Data) -> np.ndarray: + ""version based on @btaba suggestion"" + # data = mujoco.MjData(self.model) + # mjx.device_get_into(data, mjx_data) + data = mjx.get_data(self.model, mjx_data) + state = np.empty(mujoco.mj_stateSize(self.model, mujoco.mjtState.mjSTATE_PHYSICS)) + mujoco.mj_getState(self.model, data, state, spec=mujoco.mjtState.mjSTATE_PHYSICS) - def state_info(self, state: mjx.Data, params: Dict[str, any]) -> dict: - raise NotImplementedError + return state + """ # TODO add vector environment diff --git a/gymnasium/envs/mjx/pendulum.py b/gymnasium/envs/mjx/pendulum.py new file mode 100644 index 000000000..65dfea98e --- /dev/null +++ b/gymnasium/envs/mjx/pendulum.py @@ -0,0 +1,211 @@ +"""Contains the classes for the Inverted Pendulum environments, `InvertedPendulum`, `InvertedDoublePendulum`.""" +import gymnasium + + +try: + import jax + from jax import numpy as jnp + from mujoco import mjx +except ImportError as e: + MJX_IMPORT_ERROR = e +else: + MJX_IMPORT_ERROR = None + +from typing import Dict, Tuple + +import numpy as np +from gymnasium.envs.mjx.mjx_env import MJXEnv + +from gymnasium.envs.mujoco.inverted_double_pendulum_v5 import ( + DEFAULT_CAMERA_CONFIG as INVERTED_DOUBLE_PENDULUM_DEFAULT_CAMERA_CONFIG, +) +from gymnasium.envs.mujoco.inverted_pendulum_v5 import ( + DEFAULT_CAMERA_CONFIG as INVERTED_PENDULUM_DEFAULT_CAMERA_CONFIG, +) + + +class InvertedDoublePendulumMJXEnv(MJXEnv): + def __init__( + self, + params: Dict[str, any], # NOTE not API compliant (yet?) + ): + """Sets the `obveration_space.shape`.""" + MJXEnv.__init__(self, params=params) + + # TODO use jnp when and if `Box` supports jax natively + self.observation_space = gymnasium.spaces.Box( + low=-np.inf, high=np.inf, shape=(9,), dtype=np.float32 + ) + + def _gen_init_physics_state( + self, rng, params: Dict[str, any] + ) -> Tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]: + """Sets `qpos` (positional elements) from a CUD and `qvel` (velocity elements) from a gaussian.""" + noise_low = -params["reset_noise_scale"] + noise_high = params["reset_noise_scale"] + + qpos = self.mjx_model.qpos0 + jax.random.uniform( + key=rng, minval=noise_low, maxval=noise_high, shape=(self.mjx_model.nq,) + ) + qvel = params["reset_noise_scale"] * jax.random.normal( + key=rng, shape=(self.mjx_model.nv,) + ) + act = jnp.empty(self.mjx_model.na) + + return qpos, qvel, act + + def observation( + self, state: mjx.Data, rng: jax.random.PRNGKey, params: Dict[str, any] + ) -> jnp.ndarray: + """Observes the `qpos` (posional elements) and `qvel` (velocity elements) of the robot.""" + mjx_data = state + + velocity = mjx_data.qvel.flatten() + + observation = jnp.concatenate( + ( + mjx_data.qpos.flatten()[:1], # `cart` x-position + jnp.sin(mjx_data.qpos[1:]), + jnp.cos(mjx_data.qpos[1:]), + jnp.clip(velocity, -10, 10), + jnp.clip(mjx_data.qfrc_constraint, -10, 10)[:1], + ) + ) + return observation + + def _get_reward( + self, + state: mjx.Data, + action: jnp.ndarray, + next_state: mjx.Data, + params: Dict[str, any], + ) -> Tuple[jnp.ndarray, Dict]: + """Reward = alive_bonus - dist_penalty - vel_penalty.""" + + mjx_data_new = next_state + + v = mjx_data_new.qvel[1:3] + x, _, y = mjx_data_new.site_xpos[0] + + dist_penalty = 0.01 * x**2 + (y - 2) ** 2 + vel_penalty = jnp.array([1e-3, 5e-3]).T * jnp.square(v) + alive_bonus = params["healthy_reward"] * self._gen_is_healty(mjx_data_new) + + reward = alive_bonus - dist_penalty - vel_penalty + + reward_info = { + "reward_survive": alive_bonus, + "distance_penalty": -dist_penalty, + "velocity_penalty": -vel_penalty, + } + + return reward, reward_info + + def _gen_is_healty(self, state: mjx.Data): + """Checks if the pendulum is upright.""" + mjx_data = state + + y = mjx_data.site_xpos[0][2] + + return jnp.array(y > 1) + + def terminal( + self, state: mjx.Data, rng: jax.random.PRNGKey, params: Dict[str, any] + ) -> bool: + """Terminates if unhealty.""" + return jnp.logical_not(self._gen_is_healty(state)) + + def get_default_params(**kwargs) -> Dict[str, any]: + """Get the parameters for the Walker2d environment""" + default = { + "xml_file": "inverted_double_pendulum.xml", + "frame_skip": 5, + "default_camera_config": INVERTED_DOUBLE_PENDULUM_DEFAULT_CAMERA_CONFIG, + "healthy_reward": 10.0, + "reset_noise_scale": 0.1, + } + return {**MJXEnv.get_default_params(), **default, **kwargs} + + +class InvertedPendulumMJXEnv(MJXEnv): + def __init__( + self, + params: Dict[str, any], # NOTE not API compliant (yet?) + ): + """Sets the `obveration_space.shape`.""" + MJXEnv.__init__(self, params=params) + + # TODO use jnp when and if `Box` supports jax natively + self.observation_space = gymnasium.spaces.Box( + low=-np.inf, high=np.inf, shape=(4,), dtype=np.float32 + ) + + self.observation_structure = { + "qpos": self.mjx_model.nq, + "qvel": self.mjx_model.nv, + } + + def _gen_init_physics_state( + self, rng, params: Dict[str, any] + ) -> Tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]: + """Sets `qpos` (positional elements) and `qvel` (velocity elements) form a CUD.""" + noise_low = -params["reset_noise_scale"] + noise_high = params["reset_noise_scale"] + + qpos = self.mjx_model.qpos0 + jax.random.uniform( + key=rng, minval=noise_low, maxval=noise_high, shape=(self.mjx_model.nq,) + ) + qvel = jax.random.uniform( + key=rng, minval=noise_low, maxval=noise_high, shape=(self.mjx_model.nq,) + ) + act = jnp.empty(self.mjx_model.na) + + return qpos, qvel, act + + def observation( + self, state: mjx.Data, rng: jax.random.PRNGKey, params: Dict[str, any] + ) -> jnp.ndarray: + """Observes the `qpos` (posional elements) and `qvel` (velocity elements) of the robot.""" + mjx_data = state + + position = mjx_data.qpos.flatten() + velocity = mjx_data.qvel.flatten() + + observation = jnp.concatenate((position, velocity)) + return observation + + def _get_reward( + self, + state: mjx.Data, + action: jnp.ndarray, + next_state: mjx.Data, + params: Dict[str, any], + ) -> Tuple[jnp.ndarray, Dict]: + reward = jnp.array(self._gen_is_healty(next_state), dtype=jnp.float32) + reward_info = {"reward_survive": reward} + return reward, reward_info + + def _gen_is_healty(self, state: mjx.Data): + """Checks if the pendulum is upright.""" + mjx_data = state + + angle = mjx_data.qpos[1] + + return jnp.abs(angle) <= 0.2 + + def terminal( + self, state: mjx.Data, rng: jax.random.PRNGKey, params: Dict[str, any] + ) -> bool: + """Terminates if unhealty.""" + return jnp.logical_not(self._gen_is_healty(state)) + + def get_default_params(**kwargs) -> Dict[str, any]: + """Get the parameters for the Walker2d environment""" + default = { + "xml_file": "inverted_pendulum.xml", + "frame_skip": 2, + "default_camera_config": INVERTED_PENDULUM_DEFAULT_CAMERA_CONFIG, + "reset_noise_scale": 0.01, + } + + return {**MJXEnv.get_default_params(), **default, **kwargs} diff --git a/gymnasium/envs/mjx/swimmer.py b/gymnasium/envs/mjx/swimmer.py new file mode 100644 index 000000000..0110aa482 --- /dev/null +++ b/gymnasium/envs/mjx/swimmer.py @@ -0,0 +1,128 @@ +"""Contains the class for the `Swimmer` environment.""" +import gymnasium + + +try: + import jax + from jax import numpy as jnp + from mujoco import mjx +except ImportError as e: + MJX_IMPORT_ERROR = e +else: + MJX_IMPORT_ERROR = None + +from typing import Dict, Tuple + +import numpy as np +from gymnasium.envs.mjx.mjx_env import MJXEnv + + +class Swimmer_MJXEnv(MJXEnv): + # NOTE: MJX does not yet support condim=1 and therefore this class can not be instantiated + """Class for Swimmer.""" + + def __init__( + self, + params: Dict[str, any], + ): + """Sets the `obveration_space`.""" + MJXEnv.__init__(self, params=params) + + obs_size = ( + self.mjx_model.nq + + self.mjx_model.nv + - 2 * params["exclude_current_positions_from_observation"] + ) + self.observation_space = gymnasium.spaces.Box( + low=-np.inf, high=np.inf, shape=(obs_size,), dtype=np.float64 + ) + + self.observation_structure = { + "skipped_qpos": 2 * params["exclude_current_positions_from_observation"], + "qpos": self.mjx_model.nq + - 2 * params["exclude_current_positions_from_observation"], + "qvel": self.mjx_model.nv, + } + + def _gen_init_physics_state( + self, rng, params: Dict[str, any] + ) -> Tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]: + """Sets `qpos` (positional elements) and `qvel` (velocity elements) form a CUD.""" + noise_low = -params["reset_noise_scale"] + noise_high = params["reset_noise_scale"] + + qpos = self.mjx_model.qpos0 + jax.random.uniform( + key=rng, minval=noise_low, maxval=noise_high, shape=(self.mjx_model.nq,) + ) + qvel = jax.random.uniform( + key=rng, minval=noise_low, maxval=noise_high, shape=(self.mjx_model.nq,) + ) + act = jnp.empty(self.mjx_model.na) + + return qpos, qvel, act + + def observation( + self, state: mjx.Data, rng: jax.random.PRNGKey, params: Dict[str, any] + ) -> jnp.ndarray: + """Observes the `qpos` (posional elements) and `qvel` (velocity elements) of the robot.""" + mjx_data = state + + position = mjx_data.qpos.flatten() + velocity = mjx_data.qvel.flatten() + + if params["exclude_current_positions_from_observation"]: + position = position[2:] + + observation = jnp.concatenate((position, velocity)) + return observation + + def _get_reward( + self, + state: mjx.Data, + action: jnp.ndarray, + next_state: mjx.Data, + params: Dict[str, any], + ) -> Tuple[jnp.ndarray, Dict]: + """Reward = reward_dist + reward_ctrl.""" + mjx_data_old = state + mjx_data_new = next_state + + x_position_before = mjx_data_old.qpos[0] + x_position_after = mjx_data_new.qpos[0] + x_velocity = (x_position_after - x_position_before) / self.dt(params) + + forward_reward = params["forward_reward_weight"] * x_velocity + ctrl_cost = params["ctrl_cost_weight"] * jnp.sum(jnp.square(action)) + + reward = forward_reward - ctrl_cost + + reward_info = { + "reward_forward": forward_reward, + "reward_ctrl": -ctrl_cost, + } + + return reward, reward_info + + def state_info(self, state: mjx.Data, params: Dict[str, any]) -> Dict[str, float]: + """Includes state information exclueded from `observation()`.""" + mjx_data = state + + info = { + "x_position": mjx_data.qpos[0], + "y_position": mjx_data.qpos[1], + "distance_from_origin": jnp.linalg.norm(mjx_data.qpos[0:2], ord=2), + } + return info + + def get_default_params(**kwargs) -> Dict[str, any]: + """Get the default parameter for the Swimmer environment.""" + default = { + "xml_file": "swimmer.xml", + "frame_skip": 4, + "default_camera_config": {}, + "forward_reward_weight": 1.0, + "ctrl_cost_weight": 1e-4, + "reset_noise_scale": 0.1, + "exclude_current_positions_from_observation": True, + } + return {**MJXEnv.get_default_params(), **default, **kwargs} diff --git a/gymnasium/envs/mujoco/f.py b/gymnasium/envs/mujoco/f.py deleted file mode 100644 index c3a6f1922..000000000 --- a/gymnasium/envs/mujoco/f.py +++ /dev/null @@ -1,315 +0,0 @@ -from os import path - -import numpy as np - -import gymnasium -from gymnasium.envs.mujoco import MujocoRenderer - - -try: - import jax - import mujoco - from jax import numpy as jnp - from mujoco import mjx -except ImportError as e: - MJX_IMPORT_ERROR = e -else: - MJX_IMPORT_ERROR = None - -DEFAULT_CAMERA_CONFIG = { # TODO reuse the one from v5 - "distance": 4.0, -} - - -class MJXEnv( - gymnasium.functional.FuncEnv[ - mjx._src.types.Data, jnp.ndarray, jnp.ndarray, jnp.ndarray, bool, MujocoRenderer - ] -): - """The Base for MJX Environments""" - - def __init__(self, model_path, frame_skip): - if MJX_IMPORT_ERROR is not None: - raise gymnasium.error.DependencyNotInstalled( - f"{MJX_IMPORT_ERROR}. " - "(HINT: you need to install mujoco, run `pip install gymnasium[mjx]`.)" # TODO actually create gymnasium[mjx] - ) - - # NOTE can not be JITted because of `Box` not support jax.numpy - if model_path.startswith(".") or model_path.startswith("/"): # TODO cleanup - self.fullpath = model_path - elif model_path.startswith("~"): - self.fullpath = path.expanduser(model_path) - else: - self.fullpath = path.join(path.dirname(__file__), "assets", model_path) - if not path.exists(self.fullpath): - raise OSError(f"File {self.fullpath} does not exist") - - self.frame_skip = frame_skip - - self.model = mujoco.MjModel.from_xml_path( - self.fullpath - ) # TODO? do not store and replace with mjx.get_model with mjx==3.1 - # NOTE too much state? - # alternatives state implementions - # 1. functional_state = (mjx_data, mjx_model), least internal state in MJXenv, most state in functional_state - # 2. functional_state = [qpos,qvel], most internal state in MJXenv, least state in functional_state - self.mjx_model = mjx.device_put(self.model) - - # set action space - low_action_bound, high_action_bound = self.mjx_model.actuator_ctrlrange.T - # TODO change bounds and types when and if `Box` supports JAX nativly - self.action_space = gymnasium.spaces.Box( - low=np.array(low_action_bound), - high=np.array(high_action_bound), - dtype=np.float32, - ) - # self.action_space = gymnasium.spaces.Box(low=low_action_bound, high=high_action_bound, dtype=low_action_bound.dtype) - # observation_space: gymnasium.spaces.Box # set by the sub-class - - def initial(self, rng: jax.random.PRNGKey) -> mjx._src.types.Data: - # TODO? find a more performant alternative that does not allocate? - mjx_data = mjx.make_data(self.model) - qpos, qvel = self._gen_init_state(rng) - mjx_data = mjx_data.replace(qpos=qpos, qvel=qvel) - mjx_data = mjx.forward(self.mjx_model, mjx_data) - - return mjx_data - - def transition( - self, state: mjx._src.types.Data, action: jnp.ndarray, rng=None - ) -> mjx._src.types.Data: - """Step through the simulator using `action` for `self.dt`.""" - mjx_data = state - mjx_data = mjx_data.replace(ctrl=action) - mjx_data = jax.lax.fori_loop( - 0, self.frame_skip, lambda _, x: mjx.step(self.mjx_model, x), mjx_data - ) - - return mjx_data - # TODO fix sensors with MJX>=3.1 - - def reward( - self, - state: mjx._src.types.Data, - action: jnp.ndarray, - next_state: mjx._src.types.Data, - ) -> jnp.ndarray: - return self._get_reward(state, action, next_state)[0] - - def transition_info( - self, - state: mjx._src.types.Data, - action: jnp.ndarray, - next_state: mjx._src.types.Data, - ) -> dict: - return self._get_reward(state, action, next_state)[1] - - def render_image( - self, state: mjx._src.types.Data, render_state: MujocoRenderer - ) -> tuple[MujocoRenderer, np.ndarray | None]: - # NOTE function can not be jitted - mjx_data = state - mujoco_renderer = render_state - - data = mujoco.MjData(self.model) - mjx.device_get_into(data, mjx_data) # TODO use get_data instead once mjx==3.1 - mujoco.mj_forward(self.model, data) - - mujoco_renderer.data = data - - frame = mujoco_renderer.render( - self.render_mode, self.camera_id, self.camera_name - ) - - return mujoco_renderer, frame - - def render_init( - self, - default_camera_config: dict[str, float] = {}, - camera_id: int | None = None, - camera_name: str | None = None, - max_geom=1000, - width=480, - height=480, - render_mode="rgb_array", - ) -> MujocoRenderer: - # TODO storing to much state? it should probably be moved internal to MujocoRenderer - self.render_mode = render_mode - self.camera_id = camera_id - self.camera_name = camera_name - - return MujocoRenderer( - self.model, - None, - default_camera_config, - width, - height, - max_geom, - ) - - def render_close(self, render_state: MujocoRenderer) -> None: - mujoco_renderer = render_state - if mujoco_renderer is not None: - mujoco_renderer.close() - - @property - def dt(self) -> float: - return self.mjx_model.opt.timestep * self.frame_skip - - def _gen_init_state(self, rng) -> tuple[jnp.ndarray, jnp.ndarray]: - """ - Returns: `(qpos, qvel)` - """ - # NOTE alternatives - # 1. return the state in a single vector - # 2. return it a dictionary keyied by "qpos" & "qvel" - raise NotImplementedError - - def _get_reward( - self, - state: mjx._src.types.Data, - action: jnp.ndarray, - next_state: mjx._src.types.Data, - ) -> tuple[jnp.ndarray, dict]: - """ - Generates `reward` and `transition_info`, we rely on the JIT's SEE to optimize it. - Returns: `(reward, transition_info)` - """ - raise NotImplementedError - - def observation(self, state: mjx._src.types.Data) -> jnp.ndarray: - raise NotImplementedError - - def terminal(self, state: mjx._src.types.Data) -> bool: - raise NotImplementedError - - def state_info(self, state: mjx._src.types.Data) -> dict: - raise NotImplementedError - - -# TODO in which file to place this class? in `half_cheetah_v5.py`? -class HalfCheetahMJXEnv(MJXEnv, gymnasium.utils.EzPickle): - def __init__( - self, - xml_file: str = "half_cheetah.xml", - frame_skip: int = 5, - forward_reward_weight: float = 1.0, - ctrl_cost_weight: float = 0.1, - reset_noise_scale: float = 0.1, - exclude_current_positions_from_observation: bool = True, - **kwargs, - ): - gymnasium.utils.EzPickle.__init__( - self, - xml_file, - frame_skip, - forward_reward_weight, - ctrl_cost_weight, - reset_noise_scale, - exclude_current_positions_from_observation, - **kwargs, - ) - - self._forward_reward_weight = forward_reward_weight - self._ctrl_cost_weight = ctrl_cost_weight - - self._reset_noise_scale = reset_noise_scale - - self._exclude_current_positions_from_observation = ( - exclude_current_positions_from_observation - ) - - MJXEnv.__init__( - self, - model_path=xml_file, - frame_skip=frame_skip, - **kwargs, - ) - - obs_size = ( - self.mjx_model.nq - + self.mjx_model.nv - - exclude_current_positions_from_observation - ) - - self.observation_space = gymnasium.spaces.Box( # TODO use jnp when and if `Box` supports jax natively - low=-np.inf, high=np.inf, shape=(obs_size,), dtype=np.float32 - ) - - self.observation_structure = { - "skipped_qpos": 1 * exclude_current_positions_from_observation, - "qpos": self.mjx_model.nq - 1 * exclude_current_positions_from_observation, - "qvel": self.mjx_model.nv, - } - - def _gen_init_state(self, rng) -> tuple[jnp.ndarray, jnp.ndarray]: - noise_low = -self._reset_noise_scale - noise_high = self._reset_noise_scale - - qpos = self.mjx_model.qpos0 + jax.random.uniform( - key=rng, minval=noise_low, maxval=noise_high, shape=(self.mjx_model.nq,) - ) - qvel = self._reset_noise_scale * jax.random.normal( - key=rng, shape=(self.mjx_model.nv,) - ) - - return qpos, qvel - - def observation(self, state: mjx._src.types.Data) -> jnp.ndarray: - mjx_data = state - position = mjx_data.qpos.flatten() - velocity = mjx_data.qvel.flatten() - - if self._exclude_current_positions_from_observation: - position = position[1:] - - observation = jnp.concatenate((position, velocity)) - return observation - - def _get_reward( - self, - state: mjx._src.types.Data, - action: jnp.ndarray, - next_state: mjx._src.types.Data, - ) -> tuple[jnp.ndarray, dict]: - mjx_data_old = state - mjx_data_new = next_state - x_position_before = mjx_data_old.qpos[0] - x_position_after = mjx_data_new.qpos[0] - x_velocity = (x_position_after - x_position_before) / self.dt - - forward_reward = self._forward_reward_weight * x_velocity - ctrl_cost = self._ctrl_cost_weight * jnp.sum(jnp.square(action)) - - reward = forward_reward - ctrl_cost - reward_info = { - "reward_forward": forward_reward, - "reward_ctrl": -ctrl_cost, - "x_velocity": x_velocity, - } - - return reward, reward_info - - def terminal(self, state: mjx._src.types.Data) -> bool: - return False - # NOTE or: return jnp.array(False) - - def state_info(self, state: mjx._src.types.Data) -> dict: - mjx_data = state - x_position_after = mjx_data.qpos[0] - info = { - "x_position": x_position_after, - } - return info - - def render_init( - self, default_camera_config: dict[str, float] = DEFAULT_CAMERA_CONFIG, **kwargs - ) -> MujocoRenderer: - return super().render_init( - default_camera_config=default_camera_config, **kwargs - ) - - -# TODO add vector environment -# TODO consider requirement of `metaworld` & `gymansium_robotics.RobotEnv` From 04ed837bc6c12781842fd269b7c7ba3307173e07 Mon Sep 17 00:00:00 2001 From: Kallinteris Andreas Date: Mon, 5 Feb 2024 14:39:05 +0200 Subject: [PATCH 24/29] `pre-commit` --- gymnasium/envs/mjx/ant.py | 2 +- gymnasium/envs/mjx/humanoid.py | 3 ++- gymnasium/envs/mjx/manipulation.py | 4 ++-- gymnasium/envs/mjx/mjx_env.py | 3 ++- gymnasium/envs/mjx/pendulum.py | 11 +++++++---- gymnasium/envs/mjx/swimmer.py | 1 + 6 files changed, 15 insertions(+), 9 deletions(-) diff --git a/gymnasium/envs/mjx/ant.py b/gymnasium/envs/mjx/ant.py index f3d2534a6..ef1990427 100644 --- a/gymnasium/envs/mjx/ant.py +++ b/gymnasium/envs/mjx/ant.py @@ -14,8 +14,8 @@ from typing import Dict, Tuple import numpy as np -from gymnasium.envs.mjx.mjx_env import MJXEnv +from gymnasium.envs.mjx.mjx_env import MJXEnv from gymnasium.envs.mujoco.ant_v5 import DEFAULT_CAMERA_CONFIG diff --git a/gymnasium/envs/mjx/humanoid.py b/gymnasium/envs/mjx/humanoid.py index 86e0333de..7921cc3e0 100644 --- a/gymnasium/envs/mjx/humanoid.py +++ b/gymnasium/envs/mjx/humanoid.py @@ -26,7 +26,7 @@ class BaseHumanoid_MJXEnv(MJXEnv): # NOTE: MJX does not yet support many features therefore this class can not be instantiated - """Base environment class for humanoid environments such as Humanoid, & HumanoidStandup""" + """Base environment class for humanoid environments such as Humanoid, & HumanoidStandup.""" def __init__( self, @@ -146,6 +146,7 @@ class HumanoidMJXEnv(BaseHumanoid_MJXEnv): """Class for Humanoid.""" def mass_center(self, mjx_data): + """Calculates the xpos based center of mass.""" mass = np.expand_dims(self.mjx_model.body_mass, axis=1) xpos = mjx_data.xipos return (jnp.sum(mass * xpos, axis=0) / jnp.sum(mass))[0:2] diff --git a/gymnasium/envs/mjx/manipulation.py b/gymnasium/envs/mjx/manipulation.py index 8d46e416f..ec9856a13 100644 --- a/gymnasium/envs/mjx/manipulation.py +++ b/gymnasium/envs/mjx/manipulation.py @@ -14,8 +14,8 @@ from typing import Dict, Tuple import numpy as np -from gymnasium.envs.mjx.mjx_env import MJXEnv +from gymnasium.envs.mjx.mjx_env import MJXEnv from gymnasium.envs.mujoco.pusher_v5 import ( DEFAULT_CAMERA_CONFIG as PUSHER_DEFAULT_CAMERA_CONFIG, ) @@ -72,7 +72,7 @@ def _set_goal(self, mjx_data: mjx.Data, goal: jnp.ndarray) -> mjx.Data: def observation( self, state: mjx.Data, rng: jax.random.PRNGKey, params: Dict[str, any] ) -> jnp.ndarray: - """Observes the `sin(theta)` & `cos(theta)` & `qpos` & `qvel` & 'fingertip - target' distance""" + """Observes the `sin(theta)` & `cos(theta)` & `qpos` & `qvel` & 'fingertip - target' distance.""" mjx_data = state position = mjx_data.qpos.flatten() diff --git a/gymnasium/envs/mjx/mjx_env.py b/gymnasium/envs/mjx/mjx_env.py index 8208ff03e..7dbaa0c4b 100644 --- a/gymnasium/envs/mjx/mjx_env.py +++ b/gymnasium/envs/mjx/mjx_env.py @@ -54,6 +54,7 @@ def mjx_set_physics_state(mjx_data: mjx.Data, mjx_physics_state) -> mjx.Data: # TODO add type hint to `params` # TODO add render `metadata` # TODO add init_qvel +# TODO create pip install gymnasium[mjx] class MJXEnv( gymnasium.functional.FuncEnv[ mjx.Data, @@ -79,7 +80,7 @@ def __init__(self, params: Dict[str, any]): if MJX_IMPORT_ERROR is not None: raise gymnasium.error.DependencyNotInstalled( f"{MJX_IMPORT_ERROR}. " - "(HINT: you need to install mujoco-mjx, run `pip install gymnasium[mjx]`.)" # TODO actually create gymnasium[mjx] + "(HINT: you need to install mujoco-mjx, run `pip install gymnasium[mjx]`.)" ) fullpath = expand_model_path(params["xml_file"]) diff --git a/gymnasium/envs/mjx/pendulum.py b/gymnasium/envs/mjx/pendulum.py index 65dfea98e..1daa7e634 100644 --- a/gymnasium/envs/mjx/pendulum.py +++ b/gymnasium/envs/mjx/pendulum.py @@ -14,8 +14,8 @@ from typing import Dict, Tuple import numpy as np -from gymnasium.envs.mjx.mjx_env import MJXEnv +from gymnasium.envs.mjx.mjx_env import MJXEnv from gymnasium.envs.mujoco.inverted_double_pendulum_v5 import ( DEFAULT_CAMERA_CONFIG as INVERTED_DOUBLE_PENDULUM_DEFAULT_CAMERA_CONFIG, ) @@ -25,6 +25,8 @@ class InvertedDoublePendulumMJXEnv(MJXEnv): + """Class for InvertedDoublePendulum.""" + def __init__( self, params: Dict[str, any], # NOTE not API compliant (yet?) @@ -81,7 +83,6 @@ def _get_reward( params: Dict[str, any], ) -> Tuple[jnp.ndarray, Dict]: """Reward = alive_bonus - dist_penalty - vel_penalty.""" - mjx_data_new = next_state v = mjx_data_new.qvel[1:3] @@ -116,7 +117,7 @@ def terminal( return jnp.logical_not(self._gen_is_healty(state)) def get_default_params(**kwargs) -> Dict[str, any]: - """Get the parameters for the Walker2d environment""" + """Get the parameters for the InvertedDoublePendulum environment.""" default = { "xml_file": "inverted_double_pendulum.xml", "frame_skip": 5, @@ -128,6 +129,8 @@ def get_default_params(**kwargs) -> Dict[str, any]: class InvertedPendulumMJXEnv(MJXEnv): + """Class for InvertedPendulum.""" + def __init__( self, params: Dict[str, any], # NOTE not API compliant (yet?) @@ -200,7 +203,7 @@ def terminal( return jnp.logical_not(self._gen_is_healty(state)) def get_default_params(**kwargs) -> Dict[str, any]: - """Get the parameters for the Walker2d environment""" + """Get the parameters for the InvertedPendulum environment.""" default = { "xml_file": "inverted_pendulum.xml", "frame_skip": 2, diff --git a/gymnasium/envs/mjx/swimmer.py b/gymnasium/envs/mjx/swimmer.py index 0110aa482..e33dd5d07 100644 --- a/gymnasium/envs/mjx/swimmer.py +++ b/gymnasium/envs/mjx/swimmer.py @@ -14,6 +14,7 @@ from typing import Dict, Tuple import numpy as np + from gymnasium.envs.mjx.mjx_env import MJXEnv From 696b0a0382bda1d55bda7f1c00d52b06d6b646c2 Mon Sep 17 00:00:00 2001 From: Kallinteris Andreas Date: Thu, 15 Feb 2024 05:44:24 +0200 Subject: [PATCH 25/29] update --- gymnasium/envs/mjx/ant.py | 27 +++++----- gymnasium/envs/mjx/humanoid.py | 27 +++++----- gymnasium/envs/mjx/locomotion_2d.py | 80 ++++++++++++++++++++--------- gymnasium/envs/mjx/manipulation.py | 10 +++- gymnasium/envs/mjx/mjx_env.py | 10 ++++ gymnasium/envs/mjx/pendulum.py | 15 +++--- gymnasium/envs/mjx/swimmer.py | 18 +++---- 7 files changed, 117 insertions(+), 70 deletions(-) diff --git a/gymnasium/envs/mjx/ant.py b/gymnasium/envs/mjx/ant.py index ef1990427..e6d6536e2 100644 --- a/gymnasium/envs/mjx/ant.py +++ b/gymnasium/envs/mjx/ant.py @@ -30,16 +30,6 @@ def __init__( """Sets the `obveration_space`.""" MJXEnv.__init__(self, params=params) - obs_size = ( - self.mjx_model.nq - + self.mjx_model.nv - - 2 * params["exclude_current_positions_from_observation"] - + (self.mjx_model.nbody - 1) * 6 * params["include_cfrc_ext_in_observation"] - ) - self.observation_space = gymnasium.spaces.Box( - low=-np.inf, high=np.inf, shape=(obs_size,), dtype=np.float64 - ) - self.observation_structure = { "skipped_qpos": 2 * params["exclude_current_positions_from_observation"], "qpos": self.mjx_model.nq @@ -50,6 +40,14 @@ def __init__( * params["include_cfrc_ext_in_observation"], } + obs_size = self.observation_structure["qpos"] + obs_size += self.observation_structure["qvel"] + obs_size += self.observation_space["cfrc_ext"] + + self.observation_space = gymnasium.spaces.Box( + low=-np.inf, high=np.inf, shape=(obs_size,), dtype=np.float64 + ) + def _gen_init_physics_state( self, rng, params: Dict[str, any] ) -> Tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]: @@ -79,11 +77,12 @@ def observation( if params["exclude_current_positions_from_observation"]: position = position[2:] - if self._include_cfrc_ext_in_observation: - contact_force = self._get_contact_forces(mjx_data, params) - observation = jnp.concatenate((position, velocity, contact_force)) + if params["include_cfrc_ext_in_observation"] is True: + external_contact_forces = self._get_contact_forces(mjx_data, params) else: - observation = jnp.concatenate((position, velocity)) + external_contact_forces = jnp.array([]) + + observation = jnp.concatenate((position, velocity, external_contact_forces)) return observation diff --git a/gymnasium/envs/mjx/humanoid.py b/gymnasium/envs/mjx/humanoid.py index 7921cc3e0..68a90d9f5 100644 --- a/gymnasium/envs/mjx/humanoid.py +++ b/gymnasium/envs/mjx/humanoid.py @@ -35,20 +35,6 @@ def __init__( """Sets the `obveration_space`.""" MJXEnv.__init__(self, params=params) - obs_size = ( - self.mjx_model.nq - + self.mjx_model.nv - - 2 * params["exclude_current_positions_from_observation"] - + (self.mjx_model.nbody - 1) * 10 * params["include_cinert_in_observation"] - + (self.mjx_model.nbody - 1) * 6 * params["include_cvel_in_observation"] - + (self.mjx_model.nv - 6) * params["include_qfrc_actuator_in_observation"] - + (self.mjx_model.nbody - 1) * 6 * params["include_cfrc_ext_in_observation"] - ) - - self.observation_space = gymnasium.spaces.Box( - low=-np.inf, high=np.inf, shape=(obs_size,), dtype=np.float64 - ) - self.observation_structure = { "skipped_qpos": 2 * params["exclude_current_positions_from_observation"], "qpos": self.mjx_model.nq @@ -69,6 +55,17 @@ def __init__( "ten_velocity": 0, } + obs_size = self.observation_structure["qpos"] + obs_size += self.observation_structure["qvel"] + obs_size += self.observation_structure["cinert"] + obs_size += self.observation_structure["cvel"] + obs_size += self.observation_structure["qfrc_actuator"] + obs_size += self.observation_space["cfrc_ext"] + + self.observation_space = gymnasium.spaces.Box( + low=-np.inf, high=np.inf, shape=(obs_size,), dtype=np.float64 + ) + def observation( self, state: mjx.Data, rng: jax.random.PRNGKey, params: Dict[str, any] ) -> jnp.ndarray: @@ -135,7 +132,7 @@ def _gen_init_physics_state( key=rng, minval=noise_low, maxval=noise_high, shape=(self.mjx_model.nq,) ) qvel = jax.random.uniform( - key=rng, minval=noise_low, maxval=noise_high, shape=(self.mjx_model.nq,) + key=rng, minval=noise_low, maxval=noise_high, shape=(self.mjx_model.nv,) ) act = jnp.empty(self.mjx_model.na) diff --git a/gymnasium/envs/mjx/locomotion_2d.py b/gymnasium/envs/mjx/locomotion_2d.py index e7a4968dd..e1d7f4279 100644 --- a/gymnasium/envs/mjx/locomotion_2d.py +++ b/gymnasium/envs/mjx/locomotion_2d.py @@ -37,16 +37,6 @@ def __init__( """Sets the `obveration.shape`.""" MJXEnv.__init__(self, params=params) - obs_size = ( - self.mjx_model.nq - + self.mjx_model.nv - - params["exclude_current_positions_from_observation"] - ) - - self.observation_space = gymnasium.spaces.Box( # TODO use jnp when and if `Box` supports jax natively - low=-np.inf, high=np.inf, shape=(obs_size,), dtype=np.float32 - ) - self.observation_structure = { "skipped_qpos": 1 * params["exclude_current_positions_from_observation"], "qpos": self.mjx_model.nq @@ -54,22 +44,12 @@ def __init__( "qvel": self.mjx_model.nv, } - def _gen_init_physics_state( - self, rng, params: Dict[str, any] - ) -> Tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]: - """Sets `qpos` (positional elements) from a CUD and `qvel` (velocity elements) from a gaussian.""" - noise_low = -params["reset_noise_scale"] - noise_high = params["reset_noise_scale"] + obs_size = self.observation_structure["qpos"] + obs_size += self.observation_structure["qvel"] - qpos = self.mjx_model.qpos0 + jax.random.uniform( - key=rng, minval=noise_low, maxval=noise_high, shape=(self.mjx_model.nq,) - ) - qvel = params["reset_noise_scale"] * jax.random.normal( - key=rng, shape=(self.mjx_model.nv,) + self.observation_space = gymnasium.spaces.Box( # TODO use jnp when and if `Box` supports jax natively + low=-np.inf, high=np.inf, shape=(obs_size,), dtype=np.float32 ) - act = jnp.empty(self.mjx_model.na) - - return qpos, qvel, act def observation( self, state: mjx.Data, rng: jax.random.PRNGKey, params: Dict[str, any] @@ -134,6 +114,7 @@ def state_info(self, state: mjx.Data, params: Dict[str, any]) -> Dict[str, float info = { "x_position": mjx_data.qpos[0], + "z_distance_from_origin": mjx_data.qpos[1] - self.mjx_model.qpos0[1], } return info @@ -168,6 +149,23 @@ def _gen_is_healty(self, state: mjx.Data, params: Dict[str, any]): class HalfCheetahMJXEnv(Locomotion_2d_MJXEnv): """Class for HalfCheetah.""" + def _gen_init_physics_state( + self, rng, params: Dict[str, any] + ) -> Tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]: + """Sets `qpos` (positional elements) from a CUD and `qvel` (velocity elements) from a gaussian.""" + noise_low = -params["reset_noise_scale"] + noise_high = params["reset_noise_scale"] + + qpos = self.mjx_model.qpos0 + jax.random.uniform( + key=rng, minval=noise_low, maxval=noise_high, shape=(self.mjx_model.nq,) + ) + qvel = params["reset_noise_scale"] * jax.random.normal( + key=rng, shape=(self.mjx_model.nv,) + ) + act = jnp.empty(self.mjx_model.na) + + return qpos, qvel, act + def get_default_params(**kwargs) -> Dict[str, any]: """Get the default parameter for the HalfCheetah environment.""" default = { @@ -191,6 +189,23 @@ class HopperMJXEnv(Locomotion_2d_MJXEnv): # NOTE: MJX does not yet support condim=1 and therefore this class can not be instantiated """Class for Hopper.""" + def _gen_init_physics_state( + self, rng, params: Dict[str, any] + ) -> Tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]: + """Sets `qpos` (positional elements) and `qvel` (velocity elements) form a CUD.""" + noise_low = -params["reset_noise_scale"] + noise_high = params["reset_noise_scale"] + + qpos = self.mjx_model.qpos0 + jax.random.uniform( + key=rng, minval=noise_low, maxval=noise_high, shape=(self.mjx_model.nq,) + ) + qvel = jax.random.uniform( + key=rng, minval=noise_low, maxval=noise_high, shape=(self.mjx_model.nv,) + ) + act = jnp.empty(self.mjx_model.na) + + return qpos, qvel, act + def get_default_params(**kwargs) -> Dict[str, any]: """Get the default parameter for the Hopper environment.""" default = { @@ -213,6 +228,23 @@ def get_default_params(**kwargs) -> Dict[str, any]: class Walker2dMJXEnv(Locomotion_2d_MJXEnv): """Class for Walker2d.""" + def _gen_init_physics_state( + self, rng, params: Dict[str, any] + ) -> Tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]: + """Sets `qpos` (positional elements) and `qvel` (velocity elements) form a CUD.""" + noise_low = -params["reset_noise_scale"] + noise_high = params["reset_noise_scale"] + + qpos = self.mjx_model.qpos0 + jax.random.uniform( + key=rng, minval=noise_low, maxval=noise_high, shape=(self.mjx_model.nq,) + ) + qvel = jax.random.uniform( + key=rng, minval=noise_low, maxval=noise_high, shape=(self.mjx_model.nv,) + ) + act = jnp.empty(self.mjx_model.na) + + return qpos, qvel, act + def get_default_params(**kwargs) -> Dict[str, any]: """Get the default parameter for the Walker2d environment.""" default = { diff --git a/gymnasium/envs/mjx/manipulation.py b/gymnasium/envs/mjx/manipulation.py index ec9856a13..b84210ce6 100644 --- a/gymnasium/envs/mjx/manipulation.py +++ b/gymnasium/envs/mjx/manipulation.py @@ -48,7 +48,15 @@ def _gen_init_physics_state( while True: goal = jax.random.uniform(key=rng, minval=-0.2, maxval=0.2, shape=(2,)) - if jnp.less(jnp.linalg.norm(goal), jnp.array(0.2)): + c_bool = jnp.less(jnp.linalg.norm(goal), jnp.array(0.2)) + c_bool = jnp.less(jnp.linalg.norm(jnp.array([-0.15, 0.1])), 0.2) + #breakpoint() + #if c_bool: + #break + # if jnp.less(jnp.linalg.norm(goal), jnp.array(0.2)): + # break + # TODO FIX THIS + if True: break qpos.at[-2:].set(goal) diff --git a/gymnasium/envs/mjx/mjx_env.py b/gymnasium/envs/mjx/mjx_env.py index 7dbaa0c4b..a32a93315 100644 --- a/gymnasium/envs/mjx/mjx_env.py +++ b/gymnasium/envs/mjx/mjx_env.py @@ -118,9 +118,19 @@ def transition( mjx_data = state mjx_data = mjx_data.replace(ctrl=action) + """ mjx_data = jax.lax.fori_loop( 0, params["frame_skip"], lambda _, x: mjx.step(self.mjx_model, x), mjx_data ) + """ + # """ + d, _ = jax.lax.scan( + lambda x, _: (mjx.step(self.mjx_model, x), None), + init=mjx_data, + xs=None, + length=params["frame_skip"], + ) + # """ # TODO fix sensors with MJX>=3.2 return mjx_data diff --git a/gymnasium/envs/mjx/pendulum.py b/gymnasium/envs/mjx/pendulum.py index 1daa7e634..8a0ccad04 100644 --- a/gymnasium/envs/mjx/pendulum.py +++ b/gymnasium/envs/mjx/pendulum.py @@ -138,16 +138,19 @@ def __init__( """Sets the `obveration_space.shape`.""" MJXEnv.__init__(self, params=params) - # TODO use jnp when and if `Box` supports jax natively - self.observation_space = gymnasium.spaces.Box( - low=-np.inf, high=np.inf, shape=(4,), dtype=np.float32 - ) - self.observation_structure = { "qpos": self.mjx_model.nq, "qvel": self.mjx_model.nv, } + obs_size = self.observation_structure["qpos"] + obs_size += self.observation_structure["qvel"] + + # TODO use jnp when and if `Box` supports jax natively + self.observation_space = gymnasium.spaces.Box( + low=-np.inf, high=np.inf, shape=(obs_size,), dtype=np.float32 + ) + def _gen_init_physics_state( self, rng, params: Dict[str, any] ) -> Tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]: @@ -159,7 +162,7 @@ def _gen_init_physics_state( key=rng, minval=noise_low, maxval=noise_high, shape=(self.mjx_model.nq,) ) qvel = jax.random.uniform( - key=rng, minval=noise_low, maxval=noise_high, shape=(self.mjx_model.nq,) + key=rng, minval=noise_low, maxval=noise_high, shape=(self.mjx_model.nv,) ) act = jnp.empty(self.mjx_model.na) diff --git a/gymnasium/envs/mjx/swimmer.py b/gymnasium/envs/mjx/swimmer.py index e33dd5d07..4b70573b6 100644 --- a/gymnasium/envs/mjx/swimmer.py +++ b/gymnasium/envs/mjx/swimmer.py @@ -29,15 +29,6 @@ def __init__( """Sets the `obveration_space`.""" MJXEnv.__init__(self, params=params) - obs_size = ( - self.mjx_model.nq - + self.mjx_model.nv - - 2 * params["exclude_current_positions_from_observation"] - ) - self.observation_space = gymnasium.spaces.Box( - low=-np.inf, high=np.inf, shape=(obs_size,), dtype=np.float64 - ) - self.observation_structure = { "skipped_qpos": 2 * params["exclude_current_positions_from_observation"], "qpos": self.mjx_model.nq @@ -45,6 +36,13 @@ def __init__( "qvel": self.mjx_model.nv, } + obs_size = self.observation_structure["qpos"] + obs_size += self.observation_structure["qvel"] + + self.observation_space = gymnasium.spaces.Box( + low=-np.inf, high=np.inf, shape=(obs_size,), dtype=np.float64 + ) + def _gen_init_physics_state( self, rng, params: Dict[str, any] ) -> Tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]: @@ -56,7 +54,7 @@ def _gen_init_physics_state( key=rng, minval=noise_low, maxval=noise_high, shape=(self.mjx_model.nq,) ) qvel = jax.random.uniform( - key=rng, minval=noise_low, maxval=noise_high, shape=(self.mjx_model.nq,) + key=rng, minval=noise_low, maxval=noise_high, shape=(self.mjx_model.nv,) ) act = jnp.empty(self.mjx_model.na) From a7c614a9682771d2ee6e27562806a2440f26dc15 Mon Sep 17 00:00:00 2001 From: Kallinteris Andreas Date: Thu, 15 Feb 2024 05:50:19 +0200 Subject: [PATCH 26/29] `pre-commit` --- gymnasium/envs/mjx/manipulation.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/gymnasium/envs/mjx/manipulation.py b/gymnasium/envs/mjx/manipulation.py index b84210ce6..ea78da098 100644 --- a/gymnasium/envs/mjx/manipulation.py +++ b/gymnasium/envs/mjx/manipulation.py @@ -48,13 +48,13 @@ def _gen_init_physics_state( while True: goal = jax.random.uniform(key=rng, minval=-0.2, maxval=0.2, shape=(2,)) - c_bool = jnp.less(jnp.linalg.norm(goal), jnp.array(0.2)) - c_bool = jnp.less(jnp.linalg.norm(jnp.array([-0.15, 0.1])), 0.2) - #breakpoint() - #if c_bool: - #break + # c_bool = jnp.less(jnp.linalg.norm(goal), jnp.array(0.2)) + # c_bool = jnp.less(jnp.linalg.norm(jnp.array([-0.15, 0.1])), 0.2) + # breakpoint() + # if c_bool: + # break # if jnp.less(jnp.linalg.norm(goal), jnp.array(0.2)): - # break + # break # TODO FIX THIS if True: break From 3e56f409cfd14e368847a27a1cbd06ce87e4f803 Mon Sep 17 00:00:00 2001 From: Kallinteris Andreas Date: Thu, 22 Feb 2024 10:09:55 +0200 Subject: [PATCH 27/29] fix reacher --- gymnasium/envs/mjx/manipulation.py | 25 +++++++++++++------------ gymnasium/envs/mjx/mjx_env.py | 6 ------ 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/gymnasium/envs/mjx/manipulation.py b/gymnasium/envs/mjx/manipulation.py index ea78da098..af9576019 100644 --- a/gymnasium/envs/mjx/manipulation.py +++ b/gymnasium/envs/mjx/manipulation.py @@ -38,6 +38,14 @@ def __init__( low=-np.inf, high=np.inf, shape=(10,), dtype=np.float32 ) + def _body_goal(self, goal, rng): + goal = jax.random.uniform(key=rng, minval=-0.2, maxval=0.2, shape=(2,)) + return goal + + def _validate_goal(self, goal): + """Check if the `goal` is within a circle of radius 0.2 meters.""" + return jnp.less(jnp.linalg.norm(goal), jnp.array(0.2)) + def _gen_init_physics_state( self, rng, params: Dict[str, any] ) -> Tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]: @@ -46,18 +54,11 @@ def _gen_init_physics_state( key=rng, minval=-0.1, maxval=0.1, shape=(self.mjx_model.nq,) ) - while True: - goal = jax.random.uniform(key=rng, minval=-0.2, maxval=0.2, shape=(2,)) - # c_bool = jnp.less(jnp.linalg.norm(goal), jnp.array(0.2)) - # c_bool = jnp.less(jnp.linalg.norm(jnp.array([-0.15, 0.1])), 0.2) - # breakpoint() - # if c_bool: - # break - # if jnp.less(jnp.linalg.norm(goal), jnp.array(0.2)): - # break - # TODO FIX THIS - if True: - break + goal = jax.lax.while_loop( + self._validate_goal, + lambda goal: self._body_goal(goal, rng), + init_val=jnp.array((10.0, 0.0)), + ) qpos.at[-2:].set(goal) qvel = jax.random.uniform( diff --git a/gymnasium/envs/mjx/mjx_env.py b/gymnasium/envs/mjx/mjx_env.py index a32a93315..7827ffc6d 100644 --- a/gymnasium/envs/mjx/mjx_env.py +++ b/gymnasium/envs/mjx/mjx_env.py @@ -22,12 +22,6 @@ MJX_IMPORT_ERROR = None -# state = np.empty(mujoco.mj_stateSize(env.unwrapped.model, mujoco.mjtState.mjSTATE_PHYSICS)) -# mujoco.mj_getState(env.unwrapped.model, env.unwrapped.data, state, spec=mujoco.mjtState.mjSTATE_PHYSICS) - -# mujoco.mj_setState(env.unwrapped.model, env.unwrapped.data, state, spec=mujoco.mjtState.mjSTATE_PHYSICS) - - """ # TODO unit test these def mjx_get_physics_state(mjx_data: mjx.Data) -> jnp.ndarray: From a5b9bba0014b79276263f2b7812810d1cc1f4693 Mon Sep 17 00:00:00 2001 From: Kallinteris Andreas Date: Sat, 12 Oct 2024 08:51:14 +0300 Subject: [PATCH 28/29] `pre-commit` --- gymnasium/envs/mjx/__init__.py | 1 + gymnasium/envs/mjx/ant.py | 1 + gymnasium/envs/mjx/humanoid.py | 1 + gymnasium/envs/mjx/locomotion_2d.py | 1 + gymnasium/envs/mjx/manipulation.py | 1 + gymnasium/envs/mjx/mjx_env.py | 1 + gymnasium/envs/mjx/pendulum.py | 1 + gymnasium/envs/mjx/swimmer.py | 1 + 8 files changed, 8 insertions(+) diff --git a/gymnasium/envs/mjx/__init__.py b/gymnasium/envs/mjx/__init__.py index 97a9e6c29..ad6ec9f00 100644 --- a/gymnasium/envs/mjx/__init__.py +++ b/gymnasium/envs/mjx/__init__.py @@ -1,2 +1,3 @@ """Contains the base class and environments for MJX.""" + from gymnasium.envs.mjx.mjx_env import MJXEnv diff --git a/gymnasium/envs/mjx/ant.py b/gymnasium/envs/mjx/ant.py index e6d6536e2..80d0f12e4 100644 --- a/gymnasium/envs/mjx/ant.py +++ b/gymnasium/envs/mjx/ant.py @@ -1,4 +1,5 @@ """Contains the class for the `Ant` environment.""" + import gymnasium diff --git a/gymnasium/envs/mjx/humanoid.py b/gymnasium/envs/mjx/humanoid.py index 68a90d9f5..1c46ec689 100644 --- a/gymnasium/envs/mjx/humanoid.py +++ b/gymnasium/envs/mjx/humanoid.py @@ -1,4 +1,5 @@ """Contains the classes for the humaanoid environments environments, `Humanoid` and `HumanoidStandup`.""" + import gymnasium diff --git a/gymnasium/envs/mjx/locomotion_2d.py b/gymnasium/envs/mjx/locomotion_2d.py index e1d7f4279..ad52f0695 100644 --- a/gymnasium/envs/mjx/locomotion_2d.py +++ b/gymnasium/envs/mjx/locomotion_2d.py @@ -1,4 +1,5 @@ """Contains the classes for the 2d locomotion environments, `HalfCheetah`, `Hopper` and `Walker2D`.""" + import gymnasium diff --git a/gymnasium/envs/mjx/manipulation.py b/gymnasium/envs/mjx/manipulation.py index af9576019..fbf5f0b85 100644 --- a/gymnasium/envs/mjx/manipulation.py +++ b/gymnasium/envs/mjx/manipulation.py @@ -1,4 +1,5 @@ """Contains the classes for the manipulation environments, `Pusher`, `Reacher`.""" + import gymnasium diff --git a/gymnasium/envs/mjx/mjx_env.py b/gymnasium/envs/mjx/mjx_env.py index 7827ffc6d..b8cd6056b 100644 --- a/gymnasium/envs/mjx/mjx_env.py +++ b/gymnasium/envs/mjx/mjx_env.py @@ -2,6 +2,7 @@ Note: This is expted to be used my `gymnasium`, `gymnasium-robotics`, `metaworld` and 3rd party libraries. """ + from typing import Dict, Tuple, Union import numpy as np diff --git a/gymnasium/envs/mjx/pendulum.py b/gymnasium/envs/mjx/pendulum.py index 8a0ccad04..b62eede13 100644 --- a/gymnasium/envs/mjx/pendulum.py +++ b/gymnasium/envs/mjx/pendulum.py @@ -1,4 +1,5 @@ """Contains the classes for the Inverted Pendulum environments, `InvertedPendulum`, `InvertedDoublePendulum`.""" + import gymnasium diff --git a/gymnasium/envs/mjx/swimmer.py b/gymnasium/envs/mjx/swimmer.py index 4b70573b6..b3f321f44 100644 --- a/gymnasium/envs/mjx/swimmer.py +++ b/gymnasium/envs/mjx/swimmer.py @@ -1,4 +1,5 @@ """Contains the class for the `Swimmer` environment.""" + import gymnasium From 4e28d8a1ae0f7612b38054b91abbc901ba79320f Mon Sep 17 00:00:00 2001 From: Kallinteris Andreas Date: Sat, 12 Oct 2024 09:01:12 +0300 Subject: [PATCH 29/29] update func_env --- gymnasium/envs/mjx/mjx_env.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gymnasium/envs/mjx/mjx_env.py b/gymnasium/envs/mjx/mjx_env.py index b8cd6056b..3098b55de 100644 --- a/gymnasium/envs/mjx/mjx_env.py +++ b/gymnasium/envs/mjx/mjx_env.py @@ -10,6 +10,7 @@ import gymnasium from gymnasium.envs.mujoco import MujocoRenderer from gymnasium.envs.mujoco.mujoco_env import expand_model_path +from gymnasium.experimental.functional import FuncEnv try: @@ -51,7 +52,7 @@ def mjx_set_physics_state(mjx_data: mjx.Data, mjx_physics_state) -> mjx.Data: # TODO add init_qvel # TODO create pip install gymnasium[mjx] class MJXEnv( - gymnasium.functional.FuncEnv[ + FuncEnv[ mjx.Data, jnp.ndarray, jnp.ndarray,