text: import os
  from datetime import datetime
  import numpy as np
  from gym import spaces
  from ray.rllib.env.multi_agent_env import MultiAgentEnv, ENV_STATE
  
  from gym_pybullet_drones.envs.BaseAviary import DroneModel, Physics, BaseAviary
  from gym_pybullet_drones.envs.single_agent_rl.BaseSingleAgentAviary import ActionType, ObservationType
  from gym_pybullet_drones.utils.utils import nnlsRPM
  from gym_pybullet_drones.control.DSLPIDControl import DSLPIDControl
  from gym_pybullet_drones.control.SimplePIDControl import SimplePIDControl
  
  ######################################################################################################################################################
  #### Multi-drone environment class for multi-agent reinforcement learning applications ###############################################################
  ######################################################################################################################################################
  class BaseMultiagentAviary(BaseAviary, MultiAgentEnv):
  
      ####################################################################################################
      #### Initialize the environment ####################################################################
      ####################################################################################################
      #### Arguments #####################################################################################
      #### - drone_model (DroneModel)         desired drone type (associated to an .urdf file) ###########
      #### - num_drones (int)                 desired number of drones in the aviary #####################
      #### - neighbourhood_radius (float)     used to compute the drones' adjacency matrix, in meters ####
      #### - initial_xyzs ((3,1) array)       initial XYZ position of the drones #########################
      #### - initial_rpys ((3,1) array)       initial orientations of the drones (radians) ###############
      #### - physics (Physics)                desired implementation of physics/dynamics #################
      #### - freq (int)                       the frequency (Hz) at which the physics engine advances ####
      #### - aggregate_phy_steps (int)        number of physics updates within one call of .step() #######
      #### - gui (bool)                       whether to use PyBullet's GUI ##############################
      #### - record (bool)                    whether to save a video of the simulation ##################
      #### - obstacles (bool)                 whether to add obstacles to the simulation #################
      #### - user_debug_gui (bool)            whether to draw the drones' axes and the GUI sliders #######
      ####################################################################################################
      def __init__(self,
                   drone_model: DroneModel=DroneModel.CF2X,
                   num_drones: int=2,
                   neighbourhood_radius: float=np.inf,
                   initial_xyzs=None,
                   initial_rpys=None,
                   physics: Physics=Physics.PYB,
                   freq: int=240,
                   aggregate_phy_steps: int=1,
                   gui=False,
                   record=False, 
                   obs: ObservationType=ObservationType.KIN,
                   act: ActionType=ActionType.RPM
                   ):
          if num_drones < 2:
              print("[ERROR] in BaseMultiagentAviary.__init__(), num_drones should be >= 2" )
              exit()
          vision_attributes = True if obs == ObservationType.RGB else False
          dynamics_attributes = True if act in [ActionType.DYN, ActionType.ONE_D_DYN] else False
          self.OBS_TYPE = obs
          self.ACT_TYPE = act
          self.EPISODE_LEN_SEC = 5
          #### Create integrated controllers #########################
          if act in [ActionType.PID, ActionType.ONE_D_PID]:
              os.environ['KMP_DUPLICATE_LIB_OK']='True'
              if self.DRONE_MODEL in [DroneModel.CF2X, DroneModel.CF2P]:
                  self.ctrl = [DSLPIDControl(BaseAviary(drone_model=DroneModel.CF2X)) for i in range(self.NUM_DRONES)]
              elif self.DRONE_MODEL == DroneModel.HB:
                  self.ctrl = [SimplePIDControl(BaseAviary(drone_model=DroneModel.HB)) for i in range(self.NUM_DRONES)]
          super().__init__(drone_model=drone_model,
                           num_drones=num_drones,
                           neighbourhood_radius=neighbourhood_radius,
                           initial_xyzs=initial_xyzs,
                           initial_rpys=initial_rpys,
                           physics=physics,
                           freq=freq,
                           aggregate_phy_steps=aggregate_phy_steps,
                           gui=gui,
                           record=record, 
                           obstacles=True, # Add obstacles for RGB observations and/or FlyThruGate
                           user_debug_gui=False, # Remove of RPM sliders from all single agent learning aviaries
                           vision_attributes=vision_attributes,
                           dynamics_attributes=dynamics_attributes
                           )
  
      ####################################################################################################
      #### Add obstacles to the environment from .urdf files #############################################
      ####################################################################################################
      def _addObstacles(self):
          if self.OBS_TYPE == ObservationType.RGB:
              p.loadURDF("block.urdf",
                         [1, 0, .1],
                         p.getQuaternionFromEuler([0, 0, 0]),
                         physicsClientId=self.CLIENT
                         )
              p.loadURDF("cube_small.urdf",
                         [0, 1, .1],
                         p.getQuaternionFromEuler([0, 0, 0]),
                         physicsClientId=self.CLIENT
                         )
              p.loadURDF("duck_vhacd.urdf",
                         [-1, 0, .1],
                         p.getQuaternionFromEuler([0, 0, 0]),
                         physicsClientId=self.CLIENT
                         )
              p.loadURDF("teddy_vhacd.urdf",
                         [0, -1, .1],
                         p.getQuaternionFromEuler([0, 0, 0]),
                         physicsClientId=self.CLIENT
                         )
          else:
              pass
  
      ####################################################################################################
      #### Return the action space of the environment, a Dict of Box(4,) with NUM_DRONES entries #########
      ####################################################################################################
      def _actionSpace(self):
          if self.ACT_TYPE==ActionType.RPM or self.ACT_TYPE==ActionType.DYN:
              size = 4
          elif self.ACT_TYPE==ActionType.PID:
              size = 3
          elif self.ACT_TYPE in [ActionType.ONE_D_RPM, ActionType.ONE_D_DYN, ActionType.ONE_D_PID]:
              size = 1
          else:
              print("[ERROR] in BaseMultiagentAviary._actionSpace()")
              exit()
          return spaces.Dict({i: spaces.Box(low=-1*np.ones(size),
                                            high=np.ones(size),
                                            dtype=np.float32
                                            ) for i in range(self.NUM_DRONES)})
  
      ####################################################################################################
      #### Preprocess the action passed to step() ########################################################
      ####################################################################################################
      #### Arguments #####################################################################################
      #### - action (dict of (4,1) array)     unclipped RPMs commanded to the 4 motors of each drone #####
      ####################################################################################################
      #### Returns #######################################################################################
      #### - clip_action ((N_DRONES,4,1) arr) clipped RPMs commanded to the 4 motors of each drone #######
      ####################################################################################################
      def _preprocessAction(self,
                            action
                            ):
          rpm = np.zeros((self.NUM_DRONES,4))
          for k, v in action.items():
              if self.ACT_TYPE == ActionType.RPM: 
                  rpm[int(k),:] = np.array(self.HOVER_RPM * (1+0.05*v))
              elif self.ACT_TYPE == ActionType.DYN: 
                  rpm[int(k),:] = nnlsRPM(thrust=(self.GRAVITY*(v[0]+1)),
                                          x_torque=(0.05*self.MAX_XY_TORQUE*v[1]),
                                          y_torque=(0.05*self.MAX_XY_TORQUE*v[2]),
                                          z_torque=(0.05*self.MAX_Z_TORQUE*v[3]),
                                          counter=self.step_counter,
                                          max_thrust=self.MAX_THRUST,
                                          max_xy_torque=self.MAX_XY_TORQUE,
                                          max_z_torque=self.MAX_Z_TORQUE,
                                          a=self.A,
                                          inv_a=self.INV_A,
                                          b_coeff=self.B_COEFF,
                                          gui=self.GUI
                                          )
              elif self.ACT_TYPE == ActionType.PID: 
                  state = self._getDroneStateVector(int(k))
                  rpm, _, _ = self.ctrl[k].computeControl(control_timestep=self.AGGR_PHY_STEPS*self.TIMESTEP, 
                                                          cur_pos=state[0:3],
                                                          cur_quat=state[3:7],
                                                          cur_vel=state[10:13],
                                                          cur_ang_vel=state[13:16],
                                                          target_pos=state[0:3]+0.1*v
                                                          )
                  rpm[int(k),:] = rpm
              elif self.ACT_TYPE == ActionType.ONE_D_RPM: 
                  rpm[int(k),:] = np.repeat(self.HOVER_RPM * (1+0.05*v), 4)
              elif self.ACT_TYPE == ActionType.ONE_D_DYN: 
                  rpm[int(k),:] = nnlsRPM(thrust=(self.GRAVITY*(1+0.05*v[0])),
                                          x_torque=0,
                                          y_torque=0,
                                          z_torque=0,
                                          counter=self.step_counter,
                                          max_thrust=self.MAX_THRUST,
                                          max_xy_torque=self.MAX_XY_TORQUE,
                                          max_z_torque=self.MAX_Z_TORQUE,
                                          a=self.A,
                                          inv_a=self.INV_A,
                                          b_coeff=self.B_COEFF,
                                          gui=self.GUI
                                          )
              elif self.ACT_TYPE == ActionType.ONE_D_PID:
                  state = self._getDroneStateVector(int(k))
                  rpm, _, _ = self.ctrl[k].computeControl(control_timestep=self.AGGR_PHY_STEPS*self.TIMESTEP, 
                                                          cur_pos=state[0:3],
                                                          cur_quat=state[3:7],
                                                          cur_vel=state[10:13],
                                                          cur_ang_vel=state[13:16],
                                                          target_pos=state[0:3]+0.1*np.array([0,0,v[0]])
                                                          )
                  rpm[int(k),:] = rpm
              else:
                  print("[ERROR] in BaseMultiagentAviary._preprocessAction()")
                  exit()
          return rpm
  
      ####################################################################################################
      #### Return the observation space of the environment, a Dict with NUM_DRONES entries of Dict of ####
      #### { Box(4,), MultiBinary(NUM_DRONES) } ##########################################################
      ####################################################################################################
      def _observationSpace(self):
          if self.OBS_TYPE == ObservationType.RGB:
              return spaces.Dict({i: spaces.Box(low=0,
                                                high=255,
                                                shape=(self.IMG_RES[1], self.IMG_RES[0], 4), dtype=np.uint8
                                                ) for i in range(self.NUM_DRONES)})
          elif self.OBS_TYPE == ObservationType.KIN:
              ############################################################
              #### OBS OF SIZE 20 (WITH QUATERNION AND RPMS)
              #### Observation vector ### X        Y        Z       Q1   Q2   Q3   Q4   R       P       Y       VX       VY       VZ       WR       WP       WY       P0            P1            P2            P3
              # obs_lower_bound = np.array([-1,      -1,      0,      -1,  -1,  -1,  -1,  -1,     -1,     -1,     -1,      -1,      -1,      -1,      -1,      -1,      -1,           -1,           -1,           -1])
              # obs_upper_bound = np.array([1,       1,       1,      1,   1,   1,   1,   1,      1,      1,      1,       1,       1,       1,       1,       1,       1,            1,            1,            1])          
              # return spaces.Box( low=obs_lower_bound, high=obs_upper_bound, dtype=np.float32 )
              ############################################################
              #### OBS SPACE OF SIZE 12
              return spaces.Dict({i: spaces.Box(low=np.array([-1,-1,0, -1,-1,-1, -1,-1,-1, -1,-1,-1]),
                                                high=np.array([1,1,1, 1,1,1, 1,1,1, 1,1,1]),
                                                dtype=np.float32
                                                ) for i in range(self.NUM_DRONES)})
              ############################################################
          else:
              print("[ERROR] in BaseMultiagentAviary._observationSpace()")
  
      ####################################################################################################
      #### Return the current observation of the environment #############################################
      ####################################################################################################
      #### Returns #######################################################################################
      #### - obs (dict)                       {"0":{"state": np.arr(20,),"neighbors": np.arr(NUM_DRONES)},
      ####                                    .. "NUM_DRONES-1": {..} } ##################################
      ####                                    for the "state"'s content see _observationSpace() ##########
      ####                                    "neighbors" is the drone's row of the adjacency matrix #####
      ####################################################################################################
      def _computeObs(self):
          if self.OBS_TYPE == ObservationType.RGB:
              if self.step_counter%self.IMG_CAPTURE_FREQ == 0: 
                  for i in range(self.NUM_DRONES):
                      self.rgb[i], self.dep[i], self.seg[i] = self._getDroneImages(i,
                                                                                   segmentation=False
                                                                                   )
                      #### Printing observation to PNG frames example ############
                      if self.RECORD:
                          self._exportImage(img_type=ImageType.RGB,
                                            img_input=self.rgb[i],
                                            path=self.ONBOARD_IMG_PATH+"drone_"+str(i),
                                            frame_num=int(self.step_counter/self.IMG_CAPTURE_FREQ)
                                            )
              return {i: self.rgb[i] for i in range(self.NUM_DRONES)}
          elif self.OBS_TYPE == ObservationType.KIN: 
              ############################################################
              #### OBS OF SIZE 20 (WITH QUATERNION AND RPMS)
              # return {   i   : self._clipAndNormalizeState(self._getDroneStateVector(i)) for i in range(self.NUM_DRONES) }
              ############################################################
              #### OBS SPACE OF SIZE 12
              obs_12 = np.zeros((self.NUM_DRONES,12))
              for i in range(self.NUM_DRONES):
                  obs = self._clipAndNormalizeState(self._getDroneStateVector(i))
                  obs_12[i, :] = np.hstack([obs[0:3], obs[7:10], obs[10:13], obs[13:16]]).reshape(12,)
              return {i: obs_12[i, :] for i in range(self.NUM_DRONES)}
              ############################################################
          else:
              print("[ERROR] in BaseMultiagentAviary._computeObs()")
  
      ####################################################################################################
      #### Normalize the 20 values in the simulation state to the [-1,1] range ###########################
      ####################################################################################################
      #### Arguments #####################################################################################
      #### - state ((20,1) array)             raw simulation state #######################################
      ####################################################################################################
      #### Returns #######################################################################################
      #### - ...
      ####################################################################################################
      def _clipAndNormalizeState(self,
                                 state
                                 ):
          raise NotImplementedError
  