|
1 | | -from gym_unity.envs.unity_env import UnityEnv, UnityGymException |
| 1 | +import logging |
| 2 | +import itertools |
| 3 | +import gym |
| 4 | +import numpy as np |
| 5 | +from mlagents.envs.environment import UnityEnvironment |
| 6 | +from gym import error, spaces |
| 7 | + |
| 8 | + |
| 9 | +class UnityGymException(error.Error): |
| 10 | + """ |
| 11 | + Any error related to the gym wrapper of ml-agents. |
| 12 | + """ |
| 13 | + |
| 14 | + pass |
| 15 | + |
| 16 | + |
| 17 | +logging.basicConfig(level=logging.INFO) |
| 18 | +logger = logging.getLogger("gym_unity") |
| 19 | + |
| 20 | + |
| 21 | +class UnityEnv(gym.Env): |
| 22 | + """ |
| 23 | + Provides Gym wrapper for Unity Learning Environments. |
| 24 | + Multi-agent environments use lists for object types, as done here: |
| 25 | + https://github.com/openai/multiagent-particle-envs |
| 26 | + """ |
| 27 | + |
| 28 | + def __init__( |
| 29 | + self, |
| 30 | + environment_filename: str, |
| 31 | + worker_id: int = 0, |
| 32 | + use_visual: bool = False, |
| 33 | + uint8_visual: bool = False, |
| 34 | + multiagent: bool = False, |
| 35 | + flatten_branched: bool = False, |
| 36 | + no_graphics: bool = False, |
| 37 | + allow_multiple_visual_obs: bool = False, |
| 38 | + ): |
| 39 | + """ |
| 40 | + Environment initialization |
| 41 | + :param environment_filename: The UnityEnvironment path or file to be wrapped in the gym. |
| 42 | + :param worker_id: Worker number for environment. |
| 43 | + :param use_visual: Whether to use visual observation or vector observation. |
| 44 | + :param uint8_visual: Return visual observations as uint8 (0-255) matrices instead of float (0.0-1.0). |
| 45 | + :param multiagent: Whether to run in multi-agent mode (lists of obs, reward, done). |
| 46 | + :param flatten_branched: If True, turn branched discrete action spaces into a Discrete space rather than |
| 47 | + MultiDiscrete. |
| 48 | + :param no_graphics: Whether to run the Unity simulator in no-graphics mode |
| 49 | + :param allow_multiple_visual_obs: If True, return a list of visual observations instead of only one. |
| 50 | + """ |
| 51 | + self._env = UnityEnvironment( |
| 52 | + environment_filename, worker_id, no_graphics=no_graphics |
| 53 | + ) |
| 54 | + self.name = self._env.academy_name |
| 55 | + self.visual_obs = None |
| 56 | + self._current_state = None |
| 57 | + self._n_agents = None |
| 58 | + self._multiagent = multiagent |
| 59 | + self._flattener = None |
| 60 | + self.game_over = ( |
| 61 | + False |
| 62 | + ) # Hidden flag used by Atari environments to determine if the game is over |
| 63 | + self._allow_multiple_visual_obs = allow_multiple_visual_obs |
| 64 | + |
| 65 | + # Check brain configuration |
| 66 | + if len(self._env.brains) != 1: |
| 67 | + raise UnityGymException( |
| 68 | + "There can only be one brain in a UnityEnvironment " |
| 69 | + "if it is wrapped in a gym." |
| 70 | + ) |
| 71 | + if len(self._env.external_brain_names) <= 0: |
| 72 | + raise UnityGymException( |
| 73 | + "There are not any external brain in the UnityEnvironment" |
| 74 | + ) |
| 75 | + |
| 76 | + self.brain_name = self._env.external_brain_names[0] |
| 77 | + brain = self._env.brains[self.brain_name] |
| 78 | + |
| 79 | + if use_visual and brain.number_visual_observations == 0: |
| 80 | + raise UnityGymException( |
| 81 | + "`use_visual` was set to True, however there are no" |
| 82 | + " visual observations as part of this environment." |
| 83 | + ) |
| 84 | + self.use_visual = brain.number_visual_observations >= 1 and use_visual |
| 85 | + |
| 86 | + if not use_visual and uint8_visual: |
| 87 | + logger.warning( |
| 88 | + "`uint8_visual was set to true, but visual observations are not in use. " |
| 89 | + "This setting will not have any effect." |
| 90 | + ) |
| 91 | + else: |
| 92 | + self.uint8_visual = uint8_visual |
| 93 | + |
| 94 | + if brain.number_visual_observations > 1 and not self._allow_multiple_visual_obs: |
| 95 | + logger.warning( |
| 96 | + "The environment contains more than one visual observation. " |
| 97 | + "You must define allow_multiple_visual_obs=True to received them all. " |
| 98 | + "Otherwise, please note that only the first will be provided in the observation." |
| 99 | + ) |
| 100 | + |
| 101 | + if brain.num_stacked_vector_observations != 1: |
| 102 | + raise UnityGymException( |
| 103 | + "There can only be one stacked vector observation in a UnityEnvironment " |
| 104 | + "if it is wrapped in a gym." |
| 105 | + ) |
| 106 | + |
| 107 | + # Check for number of agents in scene. |
| 108 | + initial_info = self._env.reset()[self.brain_name] |
| 109 | + self._check_agents(len(initial_info.agents)) |
| 110 | + |
| 111 | + # Set observation and action spaces |
| 112 | + if brain.vector_action_space_type == "discrete": |
| 113 | + if len(brain.vector_action_space_size) == 1: |
| 114 | + self._action_space = spaces.Discrete(brain.vector_action_space_size[0]) |
| 115 | + else: |
| 116 | + if flatten_branched: |
| 117 | + self._flattener = ActionFlattener(brain.vector_action_space_size) |
| 118 | + self._action_space = self._flattener.action_space |
| 119 | + else: |
| 120 | + self._action_space = spaces.MultiDiscrete( |
| 121 | + brain.vector_action_space_size |
| 122 | + ) |
| 123 | + |
| 124 | + else: |
| 125 | + if flatten_branched: |
| 126 | + logger.warning( |
| 127 | + "The environment has a non-discrete action space. It will " |
| 128 | + "not be flattened." |
| 129 | + ) |
| 130 | + high = np.array([1] * brain.vector_action_space_size[0]) |
| 131 | + self._action_space = spaces.Box(-high, high, dtype=np.float32) |
| 132 | + high = np.array([np.inf] * brain.vector_observation_space_size) |
| 133 | + self.action_meanings = brain.vector_action_descriptions |
| 134 | + if self.use_visual: |
| 135 | + if brain.camera_resolutions[0]["blackAndWhite"]: |
| 136 | + depth = 1 |
| 137 | + else: |
| 138 | + depth = 3 |
| 139 | + self._observation_space = spaces.Box( |
| 140 | + 0, |
| 141 | + 1, |
| 142 | + dtype=np.float32, |
| 143 | + shape=( |
| 144 | + brain.camera_resolutions[0]["height"], |
| 145 | + brain.camera_resolutions[0]["width"], |
| 146 | + depth, |
| 147 | + ), |
| 148 | + ) |
| 149 | + else: |
| 150 | + self._observation_space = spaces.Box(-high, high, dtype=np.float32) |
| 151 | + |
| 152 | + def reset(self): |
| 153 | + """Resets the state of the environment and returns an initial observation. |
| 154 | + In the case of multi-agent environments, this is a list. |
| 155 | + Returns: observation (object/list): the initial observation of the |
| 156 | + space. |
| 157 | + """ |
| 158 | + info = self._env.reset()[self.brain_name] |
| 159 | + n_agents = len(info.agents) |
| 160 | + self._check_agents(n_agents) |
| 161 | + self.game_over = False |
| 162 | + |
| 163 | + if not self._multiagent: |
| 164 | + obs, reward, done, info = self._single_step(info) |
| 165 | + else: |
| 166 | + obs, reward, done, info = self._multi_step(info) |
| 167 | + return obs |
| 168 | + |
| 169 | + def step(self, action): |
| 170 | + """Run one timestep of the environment's dynamics. When end of |
| 171 | + episode is reached, you are responsible for calling `reset()` |
| 172 | + to reset this environment's state. |
| 173 | + Accepts an action and returns a tuple (observation, reward, done, info). |
| 174 | + In the case of multi-agent environments, these are lists. |
| 175 | + Args: |
| 176 | + action (object/list): an action provided by the environment |
| 177 | + Returns: |
| 178 | + observation (object/list): agent's observation of the current environment |
| 179 | + reward (float/list) : amount of reward returned after previous action |
| 180 | + done (boolean/list): whether the episode has ended. |
| 181 | + info (dict): contains auxiliary diagnostic information, including BrainInfo. |
| 182 | + """ |
| 183 | + |
| 184 | + # Use random actions for all other agents in environment. |
| 185 | + if self._multiagent: |
| 186 | + if not isinstance(action, list): |
| 187 | + raise UnityGymException( |
| 188 | + "The environment was expecting `action` to be a list." |
| 189 | + ) |
| 190 | + if len(action) != self._n_agents: |
| 191 | + raise UnityGymException( |
| 192 | + "The environment was expecting a list of {} actions.".format( |
| 193 | + self._n_agents |
| 194 | + ) |
| 195 | + ) |
| 196 | + else: |
| 197 | + if self._flattener is not None: |
| 198 | + # Action space is discrete and flattened - we expect a list of scalars |
| 199 | + action = [self._flattener.lookup_action(_act) for _act in action] |
| 200 | + action = np.array(action) |
| 201 | + else: |
| 202 | + if self._flattener is not None: |
| 203 | + # Translate action into list |
| 204 | + action = self._flattener.lookup_action(action) |
| 205 | + |
| 206 | + info = self._env.step(action)[self.brain_name] |
| 207 | + n_agents = len(info.agents) |
| 208 | + self._check_agents(n_agents) |
| 209 | + self._current_state = info |
| 210 | + |
| 211 | + if not self._multiagent: |
| 212 | + obs, reward, done, info = self._single_step(info) |
| 213 | + self.game_over = done |
| 214 | + else: |
| 215 | + obs, reward, done, info = self._multi_step(info) |
| 216 | + self.game_over = all(done) |
| 217 | + return obs, reward, done, info |
| 218 | + |
| 219 | + def _single_step(self, info): |
| 220 | + if self.use_visual: |
| 221 | + visual_obs = info.visual_observations |
| 222 | + |
| 223 | + if self._allow_multiple_visual_obs: |
| 224 | + visual_obs_list = [] |
| 225 | + for obs in visual_obs: |
| 226 | + visual_obs_list.append(self._preprocess_single(obs[0])) |
| 227 | + self.visual_obs = visual_obs_list |
| 228 | + else: |
| 229 | + self.visual_obs = self._preprocess_single(visual_obs[0][0]) |
| 230 | + |
| 231 | + default_observation = self.visual_obs |
| 232 | + else: |
| 233 | + default_observation = info.vector_observations[0, :] |
| 234 | + |
| 235 | + return ( |
| 236 | + default_observation, |
| 237 | + info.rewards[0], |
| 238 | + info.local_done[0], |
| 239 | + {"text_observation": info.text_observations[0], "brain_info": info}, |
| 240 | + ) |
| 241 | + |
| 242 | + def _preprocess_single(self, single_visual_obs): |
| 243 | + if self.uint8_visual: |
| 244 | + return (255.0 * single_visual_obs).astype(np.uint8) |
| 245 | + else: |
| 246 | + return single_visual_obs |
| 247 | + |
| 248 | + def _multi_step(self, info): |
| 249 | + if self.use_visual: |
| 250 | + self.visual_obs = self._preprocess_multi(info.visual_observations) |
| 251 | + default_observation = self.visual_obs |
| 252 | + else: |
| 253 | + default_observation = info.vector_observations |
| 254 | + return ( |
| 255 | + list(default_observation), |
| 256 | + info.rewards, |
| 257 | + info.local_done, |
| 258 | + {"text_observation": info.text_observations, "brain_info": info}, |
| 259 | + ) |
| 260 | + |
| 261 | + def _preprocess_multi(self, multiple_visual_obs): |
| 262 | + if self.uint8_visual: |
| 263 | + return [ |
| 264 | + (255.0 * _visual_obs).astype(np.uint8) |
| 265 | + for _visual_obs in multiple_visual_obs |
| 266 | + ] |
| 267 | + else: |
| 268 | + return multiple_visual_obs |
| 269 | + |
| 270 | + def render(self, mode="rgb_array"): |
| 271 | + return self.visual_obs |
| 272 | + |
| 273 | + def close(self): |
| 274 | + """Override _close in your subclass to perform any necessary cleanup. |
| 275 | + Environments will automatically close() themselves when |
| 276 | + garbage collected or when the program exits. |
| 277 | + """ |
| 278 | + self._env.close() |
| 279 | + |
| 280 | + def get_action_meanings(self): |
| 281 | + return self.action_meanings |
| 282 | + |
| 283 | + def seed(self, seed=None): |
| 284 | + """Sets the seed for this env's random number generator(s). |
| 285 | + Currently not implemented. |
| 286 | + """ |
| 287 | + logger.warn("Could not seed environment %s", self.name) |
| 288 | + return |
| 289 | + |
| 290 | + def _check_agents(self, n_agents): |
| 291 | + if not self._multiagent and n_agents > 1: |
| 292 | + raise UnityGymException( |
| 293 | + "The environment was launched as a single-agent environment, however" |
| 294 | + "there is more than one agent in the scene." |
| 295 | + ) |
| 296 | + elif self._multiagent and n_agents <= 1: |
| 297 | + raise UnityGymException( |
| 298 | + "The environment was launched as a mutli-agent environment, however" |
| 299 | + "there is only one agent in the scene." |
| 300 | + ) |
| 301 | + if self._n_agents is None: |
| 302 | + self._n_agents = n_agents |
| 303 | + logger.info("{} agents within environment.".format(n_agents)) |
| 304 | + elif self._n_agents != n_agents: |
| 305 | + raise UnityGymException( |
| 306 | + "The number of agents in the environment has changed since " |
| 307 | + "initialization. This is not supported." |
| 308 | + ) |
| 309 | + |
| 310 | + @property |
| 311 | + def metadata(self): |
| 312 | + return {"render.modes": ["rgb_array"]} |
| 313 | + |
| 314 | + @property |
| 315 | + def reward_range(self): |
| 316 | + return -float("inf"), float("inf") |
| 317 | + |
| 318 | + @property |
| 319 | + def spec(self): |
| 320 | + return None |
| 321 | + |
| 322 | + @property |
| 323 | + def action_space(self): |
| 324 | + return self._action_space |
| 325 | + |
| 326 | + @property |
| 327 | + def observation_space(self): |
| 328 | + return self._observation_space |
| 329 | + |
| 330 | + @property |
| 331 | + def number_agents(self): |
| 332 | + return self._n_agents |
| 333 | + |
| 334 | + |
| 335 | +class ActionFlattener: |
| 336 | + """ |
| 337 | + Flattens branched discrete action spaces into single-branch discrete action spaces. |
| 338 | + """ |
| 339 | + |
| 340 | + def __init__(self, branched_action_space): |
| 341 | + """ |
| 342 | + Initialize the flattener. |
| 343 | + :param branched_action_space: A List containing the sizes of each branch of the action |
| 344 | + space, e.g. [2,3,3] for three branches with size 2, 3, and 3 respectively. |
| 345 | + """ |
| 346 | + self._action_shape = branched_action_space |
| 347 | + self.action_lookup = self._create_lookup(self._action_shape) |
| 348 | + self.action_space = spaces.Discrete(len(self.action_lookup)) |
| 349 | + |
| 350 | + @classmethod |
| 351 | + def _create_lookup(self, branched_action_space): |
| 352 | + """ |
| 353 | + Creates a Dict that maps discrete actions (scalars) to branched actions (lists). |
| 354 | + Each key in the Dict maps to one unique set of branched actions, and each value |
| 355 | + contains the List of branched actions. |
| 356 | + """ |
| 357 | + possible_vals = [range(_num) for _num in branched_action_space] |
| 358 | + all_actions = [list(_action) for _action in itertools.product(*possible_vals)] |
| 359 | + # Dict should be faster than List for large action spaces |
| 360 | + action_lookup = { |
| 361 | + _scalar: _action for (_scalar, _action) in enumerate(all_actions) |
| 362 | + } |
| 363 | + return action_lookup |
| 364 | + |
| 365 | + def lookup_action(self, action): |
| 366 | + """ |
| 367 | + Convert a scalar discrete action into a unique set of branched actions. |
| 368 | + :param: action: A scalar value representing one of the discrete actions. |
| 369 | + :return: The List containing the branched actions. |
| 370 | + """ |
| 371 | + return self.action_lookup[action] |
0 commit comments