def process_side_channel_message(self, data: bytes) -> None: """ Separates the data received from Python into individual messages for each registered side channel and calls on_message_received on them. :param data: The packed message sent by Unity """ offset = 0 while offset < len(data): try: channel_id = uuid.UUID(bytes_le=bytes(data[offset:offset + 16])) offset += 16 message_len, = struct.unpack_from("<i", data, offset) offset = offset + 4 message_data = data[offset:offset + message_len] offset = offset + message_len except (struct.error, ValueError, IndexError): raise UnityEnvironmentException( "There was a problem reading a message in a SideChannel. " "Please make sure the version of MLAgents in Unity is " "compatible with the Python version.") if len(message_data) != message_len: raise UnityEnvironmentException( "The message received by the side channel {0} was " "unexpectedly short. Make sure your Unity Environment " "sending side channel data properly.".format(channel_id)) if channel_id in self._side_channels_dict: incoming_message = IncomingMessage(message_data) self._side_channels_dict[channel_id].on_message_received( incoming_message) else: get_logger(__name__).warning( f"Unknown side channel data received. Channel type: {channel_id}." )
def launch_executable(file_name: str, args: List[str]) -> subprocess.Popen: """ Launches a Unity executable and returns the process handle for it. :param file_name: the name of the executable :param args: List of string that will be passed as command line arguments when launching the executable. """ launch_string = validate_environment_path(file_name) if launch_string is None: raise UnityEnvironmentException( f"Couldn't launch the {file_name} environment. Provided filename does not match any environments." ) else: logger.debug(f"The launch string is {launch_string}") logger.debug(f"Running with args {args}") # Launch Unity environment subprocess_args = [launch_string] + args try: return subprocess.Popen( subprocess_args, # start_new_session=True means that signals to the parent python process # (e.g. SIGINT from keyboard interrupt) will not be sent to the new process on POSIX platforms. # This is generally good since we want the environment to have a chance to shutdown, # but may be undesirable in come cases; if so, we'll add a command-line toggle. # Note that on Windows, the CTRL_C signal will still be sent. start_new_session=True, ) except PermissionError as perm: # This is likely due to missing read or execute permissions on file. raise UnityEnvironmentException( f"Error when trying to launch environment - make sure " f"permissions are set correctly. For example " f'"chmod -R 755 {launch_string}"') from perm
def executable_launcher(self, file_name, no_graphics, args): launch_string = self.validate_environment_path(file_name) if launch_string is None: self._close(0) raise UnityEnvironmentException( f"Couldn't launch the {file_name} environment. Provided filename does not match any environments." ) else: logger.debug("This is the launch string {}".format(launch_string)) # Launch Unity environment subprocess_args = [launch_string] if no_graphics: subprocess_args += ["-nographics", "-batchmode"] subprocess_args += [ UnityEnvironment.PORT_COMMAND_LINE_ARG, str(self.port) ] subprocess_args += args try: self.proc1 = subprocess.Popen( subprocess_args, # start_new_session=True means that signals to the parent python process # (e.g. SIGINT from keyboard interrupt) will not be sent to the new process on POSIX platforms. # This is generally good since we want the environment to have a chance to shutdown, # but may be undesirable in come cases; if so, we'll add a command-line toggle. # Note that on Windows, the CTRL_C signal will still be sent. start_new_session=True, ) except PermissionError as perm: # This is likely due to missing read or execute permissions on file. raise UnityEnvironmentException( f"Error when trying to launch environment - make sure " f"permissions are set correctly. For example " f'"chmod -R 755 {launch_string}"') from perm
def _parse_side_channel_message(side_channels: Dict[uuid.UUID, SideChannel], data: bytes) -> None: offset = 0 while offset < len(data): try: channel_id = uuid.UUID(bytes_le=bytes(data[offset:offset + 16])) offset += 16 message_len, = struct.unpack_from("<i", data, offset) offset = offset + 4 message_data = data[offset:offset + message_len] offset = offset + message_len except Exception: raise UnityEnvironmentException( "There was a problem reading a message in a SideChannel. " "Please make sure the version of MLAgents in Unity is " "compatible with the Python version.") if len(message_data) != message_len: raise UnityEnvironmentException( "The message received by the side channel {0} was " "unexpectedly short. Make sure your Unity Environment " "sending side channel data properly.".format(channel_id)) if channel_id in side_channels: incoming_message = IncomingMessage(message_data) side_channels[channel_id].on_message_received(incoming_message) else: logger.warning( "Unknown side channel data received. Channel type " ": {0}.".format(channel_id))
def create_environment_factory( env_path: Optional[str], no_graphics: bool, seed: int, start_port: int, env_args: Optional[List[str]], ) -> Callable[[int, List[SideChannel]], BaseEnv]: if env_path is not None: launch_string = UnityEnvironment.validate_environment_path(env_path) if launch_string is None: raise UnityEnvironmentException( f"Couldn't launch the {env_path} environment. Provided filename does not match any environments." ) def create_unity_environment( worker_id: int, side_channels: List[SideChannel] ) -> UnityEnvironment: # Make sure that each environment gets a different seed env_seed = seed + worker_id return UnityEnvironment( file_name=env_path, worker_id=worker_id, seed=env_seed, no_graphics=no_graphics, base_port=start_port, args=env_args, side_channels=side_channels, ) return create_unity_environment
def _raise_version_exception(unity_com_ver: str) -> None: raise UnityEnvironmentException( f"The communication API version is not compatible between Unity and python. " f"Python API: {UnityEnvironment.API_VERSION}, Unity API: {unity_com_ver}.\n " f"Please find the versions that work best together from our release page.\n" "https://github.com/Unity-Technologies/ml-agents/releases" )
def close(self): """ Sends a shutdown signal to the unity environment, and closes the socket connection. """ if self._loaded: self._close() else: raise UnityEnvironmentException("No Unity environment is loaded.")
def failing_env_factory(worker_id, config): import time # Sleep momentarily to allow time for the EnvManager to be waiting for the # subprocess response. We won't be able to capture failures from the subprocess # that cause it to close the pipe before we can send the first message. time.sleep(0.1) raise UnityEnvironmentException()
def _create_output_path(output_path): try: if not os.path.exists(output_path): os.makedirs(output_path) except Exception: raise UnityEnvironmentException( f"The folder {output_path} containing the " "generated model could not be " "accessed. Please make sure the " "permissions are set correctly.")
def _poll_process(self) -> None: """ Check the status of the subprocess. If it has exited, raise a UnityEnvironmentException :return: None """ if not self._process: return poll_res = self._process.poll() if poll_res is not None: exc_msg = self._returncode_to_env_message(self._process.returncode) raise UnityEnvironmentException(exc_msg)
def reset(self) -> None: if self._loaded: outputs = self._communicator.exchange(self._generate_reset_input()) if outputs is None: raise UnityCommunicatorStoppedException("Communicator has exited.") self._update_behavior_specs(outputs) rl_output = outputs.rl_output self._update_state(rl_output) self._is_first_message = False self._env_actions.clear() else: raise UnityEnvironmentException("No Unity environment is loaded.")
def _make_unity_env( env_path: Optional[str] = None, port: int = UnityEnvironment.BASE_ENVIRONMENT_PORT, seed: int = -1, env_args: Optional[List[str]] = None, engine_config: Optional[EngineConfig] = None, side_channels: Optional[List[SideChannel]] = None) -> UnityEnvironment: """ Create a UnityEnvironment. """ # Use Unity Editor if env file is not provided. if env_path is None: port = UnityEnvironment.DEFAULT_EDITOR_PORT else: launch_string = UnityEnvironment.validate_environment_path(env_path) if launch_string is None: raise UnityEnvironmentException( f"Couldn't launch the {env_path} environment. Provided filename does not match any environments." ) logger.info(f"Starting environment from {env_path}.") # Configure Unity Engine. if engine_config is None: engine_config = EngineConfig.default_config() engine_configuration_channel = EngineConfigurationChannel() engine_configuration_channel.set_configuration(engine_config) if side_channels is None: side_channels = [engine_configuration_channel] else: side_channels.append(engine_configuration_channel) # Find an available port to connect to Unity environment. while True: try: env = UnityEnvironment( file_name=env_path, seed=seed, base_port=port, args=env_args, side_channels=side_channels, ) except UnityWorkerInUseException: logger.debug(f"port {port} in use.") port += 1 else: logger.info(f"Connected to environment using port {port}.") break return env
def _get_side_channels_dict( side_channels: Optional[List[SideChannel]] ) -> Dict[uuid.UUID, SideChannel]: """ Converts a list of side channels into a dictionary of channel_id to SideChannel :param side_channels: The list of side channels. """ side_channels_dict: Dict[uuid.UUID, SideChannel] = {} if side_channels is not None: for _sc in side_channels: if _sc.channel_id in side_channels_dict: raise UnityEnvironmentException( f"There cannot be two side channels with " f"the same channel id {_sc.channel_id}.") side_channels_dict[_sc.channel_id] = _sc return side_channels_dict
def create_environment_factory_aai( env_path: Optional[str], # docker_target_name: Optional[str], seed: Optional[int], start_port: int, n_arenas_per_env: int, arenas_configurations: ArenaConfig, resolution: Optional[int], ) -> Callable[[int, List[SideChannel]], BaseEnv]: if env_path is not None: launch_string = AnimalAIEnvironment.validate_environment_path(env_path) if launch_string is None: raise UnityEnvironmentException( f"Couldn't launch the {env_path} environment. Provided filename does not match any environments." ) # docker_training = docker_target_name is not None # if docker_training and env_path is not None: # # Comments for future maintenance: # # Some OS/VM instances (e.g. COS GCP Image) mount filesystems # # with COS flag which prevents execution of the Unity scene, # # to get around this, we will copy the executable into the # # container. # # Navigate in docker path and find env_path and copy it. # env_path = prepare_for_docker_run(docker_target_name, env_path) seed_count = 10000 seed_pool = [np.random.randint(0, seed_count) for _ in range(seed_count)] def create_unity_environment( worker_id: int, side_channels: List[SideChannel]) -> AnimalAIEnvironment: env_seed = seed if not env_seed: env_seed = seed_pool[worker_id % len(seed_pool)] return AnimalAIEnvironment( file_name=env_path, worker_id=worker_id, base_port=start_port, seed=env_seed, # docker_training=docker_training, n_arenas=n_arenas_per_env, arenas_configurations=arenas_configurations, resolution=resolution, side_channels=side_channels, ) return create_unity_environment
def step_result_to_brain_info( step_result: BatchedStepResult, group_spec: AgentGroupSpec, agent_id_prefix: int = None, ) -> BrainInfo: n_agents = step_result.n_agents() vis_obs_indices = [] vec_obs_indices = [] for index, observation in enumerate(step_result.obs): if len(observation.shape) == 2: vec_obs_indices.append(index) elif len(observation.shape) == 4: vis_obs_indices.append(index) else: raise UnityEnvironmentException( "Invalid input received from the environment, the observation should " "either be a vector of float or a PNG image") if len(vec_obs_indices) == 0: vec_obs = np.zeros((n_agents, 0), dtype=np.float32) else: vec_obs = np.concatenate([step_result.obs[i] for i in vec_obs_indices], axis=1) vis_obs = [step_result.obs[i] for i in vis_obs_indices] mask = np.ones((n_agents, np.sum(group_spec.action_size)), dtype=np.float32) if group_spec.is_action_discrete(): mask = np.ones((n_agents, np.sum(group_spec.discrete_action_branches)), dtype=np.float32) if step_result.action_mask is not None: mask = 1 - np.concatenate(step_result.action_mask, axis=1) if agent_id_prefix is None: agent_ids = [str(ag_id) for ag_id in list(step_result.agent_id)] else: agent_ids = [ f"${agent_id_prefix}-{ag_id}" for ag_id in step_result.agent_id ] return BrainInfo( vis_obs, vec_obs, list(step_result.reward), agent_ids, list(step_result.done), list(step_result.max_step), mask, )
def create_environment_factory( env_path: Optional[str], docker_target_name: Optional[str], no_graphics: bool, seed: Optional[int], start_port: int, env_args: Optional[List[str]], ) -> Callable[[int, List[SideChannel]], BaseEnv]: if env_path is not None: launch_string = UnityEnvironment.validate_environment_path(env_path) if launch_string is None: raise UnityEnvironmentException( f"Couldn't launch the {env_path} environment. Provided filename does not match any environments." ) docker_training = docker_target_name is not None if docker_training and env_path is not None: # Comments for future maintenance: # Some OS/VM instances (e.g. COS GCP Image) mount filesystems # with COS flag which prevents execution of the Unity scene, # to get around this, we will copy the executable into the # container. # Navigate in docker path and find env_path and copy it. env_path = prepare_for_docker_run(docker_target_name, env_path) seed_count = 10000 seed_pool = [np.random.randint(0, seed_count) for _ in range(seed_count)] def create_unity_environment( worker_id: int, side_channels: List[SideChannel]) -> UnityEnvironment: env_seed = seed if not env_seed: env_seed = seed_pool[worker_id % len(seed_pool)] return UnityEnvironment( file_name=env_path, worker_id=worker_id, seed=env_seed, docker_training=docker_training, no_graphics=no_graphics, base_port=start_port, args=env_args, side_channels=side_channels, ) return create_unity_environment
def step(self) -> None: if self._is_first_message: return self.reset() if not self._loaded: raise UnityEnvironmentException("No Unity environment is loaded.") # fill the blanks for missing actions for group_name in self._env_specs: if group_name not in self._env_actions: n_agents = 0 if group_name in self._env_state: n_agents = len(self._env_state[group_name][0]) self._env_actions[group_name] = self._env_specs[ group_name].create_empty_action(n_agents) step_input = self._generate_step_input(self._env_actions) with hierarchical_timer("communicator.exchange"): outputs = self.communicator.exchange(step_input) if outputs is None: raise UnityCommunicationException("Communicator has stopped.") self._update_behavior_specs(outputs) rl_output = outputs.rl_output self._update_state(rl_output) self._env_actions.clear()
def __init__( self, file_name: Optional[str] = None, worker_id: int = 0, base_port: Optional[int] = None, seed: int = 0, no_graphics: bool = False, timeout_wait: int = 60, additional_args: Optional[List[str]] = None, side_channels: Optional[List[SideChannel]] = None, log_folder: Optional[str] = None, ): """ Starts a new unity environment and establishes a connection with the environment. Notice: Currently communication between Unity and Python takes place over an open socket without authentication. Ensure that the network where training takes place is secure. :string file_name: Name of Unity environment binary. :int base_port: Baseline port number to connect to Unity environment over. worker_id increments over this. If no environment is specified (i.e. file_name is None), the DEFAULT_EDITOR_PORT will be used. :int worker_id: Offset from base_port. Used for training multiple environments simultaneously. :bool no_graphics: Whether to run the Unity simulator in no-graphics mode :int timeout_wait: Time (in seconds) to wait for connection from environment. :list args: Addition Unity command line arguments :list side_channels: Additional side channel for no-rl communication with Unity :str log_folder: Optional folder to write the Unity Player log file into. Requires absolute path. """ atexit.register(self._close) self._additional_args = additional_args or [] self._no_graphics = no_graphics # If base port is not specified, use BASE_ENVIRONMENT_PORT if we have # an environment, otherwise DEFAULT_EDITOR_PORT if base_port is None: base_port = ( self.BASE_ENVIRONMENT_PORT if file_name else self.DEFAULT_EDITOR_PORT ) self._port = base_port + worker_id self._buffer_size = 12000 # If true, this means the environment was successfully loaded self._loaded = False # The process that is started. If None, no process was started self._proc1 = None self._timeout_wait: int = timeout_wait self._communicator = self._get_communicator(worker_id, base_port, timeout_wait) self._worker_id = worker_id self._side_channel_manager = SideChannelManager(side_channels) self._log_folder = log_folder # If the environment name is None, a new environment will not be launched # and the communicator will directly try to connect to an existing unity environment. # If the worker-id is not 0 and the environment name is None, an error is thrown if file_name is None and worker_id != 0: raise UnityEnvironmentException( "If the environment name is None, " "the worker-id must be 0 in order to connect with the Editor." ) if file_name is not None: try: self._proc1 = env_utils.launch_executable( file_name, self._executable_args() ) except UnityEnvironmentException: self._close(0) raise else: logger.info( f"Listening on port {self._port}. " f"Start training by pressing the Play button in the Unity Editor." ) self._loaded = True rl_init_parameters_in = UnityRLInitializationInputProto( seed=seed, communication_version=self.API_VERSION, package_version=mlagents_envs.__version__, capabilities=UnityEnvironment._get_capabilities_proto(), ) try: aca_output = self._send_academy_parameters(rl_init_parameters_in) aca_params = aca_output.rl_initialization_output except UnityTimeOutException: self._close(0) raise if not UnityEnvironment._check_communication_compatibility( aca_params.communication_version, UnityEnvironment.API_VERSION, aca_params.package_version, ): self._close(0) UnityEnvironment._raise_version_exception(aca_params.communication_version) UnityEnvironment._warn_csharp_base_capabilities( aca_params.capabilities, aca_params.package_version, UnityEnvironment.API_VERSION, ) self._env_state: Dict[str, Tuple[DecisionSteps, TerminalSteps]] = {} self._env_specs: Dict[str, BehaviorSpec] = {} self._env_actions: Dict[str, np.ndarray] = {} self._is_first_message = True self._update_behavior_specs(aca_output)
def _raise_version_exception(unity_com_ver: str) -> None: raise UnityEnvironmentException( f"The communication API version is not compatible between Unity and python. " f"Python API: {UnityEnvironment.API_VERSION}, Unity API: {unity_com_ver}.\n " f"Please go to https://github.com/Unity-Technologies/ml-agents/releases/tag/latest_release " f"to download the latest version of ML-Agents.")
def executable_launcher(self, file_name, docker_training, no_graphics, args): launch_string = self.validate_environment_path(file_name) if launch_string is None: self._close(0) raise UnityEnvironmentException( f"Couldn't launch the {file_name} environment. Provided filename does not match any environments." ) else: logger.debug("This is the launch string {}".format(launch_string)) # Launch Unity environment if not docker_training: subprocess_args = [launch_string] if no_graphics: subprocess_args += ["-nographics", "-batchmode"] subprocess_args += [ UnityEnvironment.PORT_COMMAND_LINE_ARG, str(self.port), ] subprocess_args += args try: self.proc1 = subprocess.Popen( subprocess_args, # start_new_session=True means that signals to the parent python process # (e.g. SIGINT from keyboard interrupt) will not be sent to the new process on POSIX platforms. # This is generally good since we want the environment to have a chance to shutdown, # but may be undesirable in come cases; if so, we'll add a command-line toggle. # Note that on Windows, the CTRL_C signal will still be sent. start_new_session=True, ) except PermissionError as perm: # This is likely due to missing read or execute permissions on file. raise UnityEnvironmentException( f"Error when trying to launch environment - make sure " f"permissions are set correctly. For example " f'"chmod -R 755 {launch_string}"') from perm else: # Comments for future maintenance: # xvfb-run is a wrapper around Xvfb, a virtual xserver where all # rendering is done to virtual memory. It automatically creates a # new virtual server automatically picking a server number `auto-servernum`. # The server is passed the arguments using `server-args`, we are telling # Xvfb to create Screen number 0 with width 640, height 480 and depth 24 bits. # Note that 640 X 480 are the default width and height. The main reason for # us to add this is because we'd like to change the depth from the default # of 8 bits to 24. # Unfortunately, this means that we will need to pass the arguments through # a shell which is why we set `shell=True`. Now, this adds its own # complications. E.g SIGINT can bounce off the shell and not get propagated # to the child processes. This is why we add `exec`, so that the shell gets # launched, the arguments are passed to `xvfb-run`. `exec` replaces the shell # we created with `xvfb`. # docker_ls = ( f"exec xvfb-run --auto-servernum --server-args='-screen 0 640x480x24'" f" {launch_string} {UnityEnvironment.PORT_COMMAND_LINE_ARG} {self.port}" ) self.proc1 = subprocess.Popen( docker_ls, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, )
def __init__( self, file_name: Optional[str] = None, worker_id: int = 0, base_port: Optional[int] = None, seed: int = 0, no_graphics: bool = False, timeout_wait: int = 60, args: Optional[List[str]] = None, side_channels: Optional[List[SideChannel]] = None, ): """ Starts a new unity environment and establishes a connection with the environment. Notice: Currently communication between Unity and Python takes place over an open socket without authentication. Ensure that the network where training takes place is secure. :string file_name: Name of Unity environment binary. :int base_port: Baseline port number to connect to Unity environment over. worker_id increments over this. If no environment is specified (i.e. file_name is None), the DEFAULT_EDITOR_PORT will be used. :int worker_id: Offset from base_port. Used for training multiple environments simultaneously. :bool no_graphics: Whether to run the Unity simulator in no-graphics mode :int timeout_wait: Time (in seconds) to wait for connection from environment. :list args: Addition Unity command line arguments :list side_channels: Additional side channel for no-rl communication with Unity """ args = args or [] atexit.register(self._close) # If base port is not specified, use BASE_ENVIRONMENT_PORT if we have # an environment, otherwise DEFAULT_EDITOR_PORT if base_port is None: base_port = (self.BASE_ENVIRONMENT_PORT if file_name else self.DEFAULT_EDITOR_PORT) self.port = base_port + worker_id self._buffer_size = 12000 # If true, this means the environment was successfully loaded self._loaded = False # The process that is started. If None, no process was started self.proc1 = None self.timeout_wait: int = timeout_wait self.communicator = self.get_communicator(worker_id, base_port, timeout_wait) self.worker_id = worker_id self.side_channels: Dict[uuid.UUID, SideChannel] = {} if side_channels is not None: for _sc in side_channels: if _sc.channel_id in self.side_channels: raise UnityEnvironmentException( "There cannot be two side channels with the same channel id {0}." .format(_sc.channel_id)) self.side_channels[_sc.channel_id] = _sc # If the environment name is None, a new environment will not be launched # and the communicator will directly try to connect to an existing unity environment. # If the worker-id is not 0 and the environment name is None, an error is thrown if file_name is None and worker_id != 0: raise UnityEnvironmentException( "If the environment name is None, " "the worker-id must be 0 in order to connect with the Editor.") if file_name is not None: self.executable_launcher(file_name, no_graphics, args) else: logger.info( f"Listening on port {self.port}. " f"Start training by pressing the Play button in the Unity Editor." ) self._loaded = True rl_init_parameters_in = UnityRLInitializationInputProto( seed=seed, communication_version=self.API_VERSION, package_version=mlagents_envs.__version__, ) try: aca_output = self.send_academy_parameters(rl_init_parameters_in) aca_params = aca_output.rl_initialization_output except UnityTimeOutException: self._close(0) raise unity_communicator_version = aca_params.communication_version if unity_communicator_version != UnityEnvironment.API_VERSION: self._close(0) raise UnityEnvironmentException( f"The communication API version is not compatible between Unity and python. " f"Python API: {UnityEnvironment.API_VERSION}, Unity API: {unity_communicator_version}.\n " f"Please go to https://github.com/Unity-Technologies/ml-agents/releases/tag/latest_release " f"to download the latest version of ML-Agents.") else: logger.info( f"Connected to Unity environment with package version {aca_params.package_version} " f"and communication version {aca_params.communication_version}" ) self._env_state: Dict[str, BatchedStepResult] = {} self._env_specs: Dict[str, AgentGroupSpec] = {} self._env_actions: Dict[str, np.ndarray] = {} self._is_first_message = True self._update_group_specs(aca_output)
def __init__( self, file_name: Optional[str] = None, worker_id: int = 0, base_port: int = 5005, seed: int = 0, docker_training: bool = False, no_graphics: bool = False, timeout_wait: int = 60, args: Optional[List[str]] = None, side_channels: Optional[List[SideChannel]] = None, ): """ Starts a new unity environment and establishes a connection with the environment. Notice: Currently communication between Unity and Python takes place over an open socket without authentication. Ensure that the network where training takes place is secure. :string file_name: Name of Unity environment binary. :int base_port: Baseline port number to connect to Unity environment over. worker_id increments over this. :int worker_id: Number to add to communication port (5005) [0]. Used for asynchronous agent scenarios. :bool docker_training: Informs this class whether the process is being run within a container. :bool no_graphics: Whether to run the Unity simulator in no-graphics mode :int timeout_wait: Time (in seconds) to wait for connection from environment. :bool train_mode: Whether to run in training mode, speeding up the simulation, by default. :list args: Addition Unity command line arguments :list side_channels: Additional side channel for no-rl communication with Unity """ args = args or [] atexit.register(self._close) self.port = base_port + worker_id self._buffer_size = 12000 self._version_ = UnityEnvironment.API_VERSION # If true, this means the environment was successfully loaded self._loaded = False # The process that is started. If None, no process was started self.proc1 = None self.timeout_wait: int = timeout_wait self.communicator = self.get_communicator(worker_id, base_port, timeout_wait) self.worker_id = worker_id self.side_channels: Dict[int, SideChannel] = {} if side_channels is not None: for _sc in side_channels: if _sc.channel_type in self.side_channels: raise UnityEnvironmentException( "There cannot be two side channels with the same channel type {0}." .format(_sc.channel_type)) self.side_channels[_sc.channel_type] = _sc # If the environment name is None, a new environment will not be launched # and the communicator will directly try to connect to an existing unity environment. # If the worker-id is not 0 and the environment name is None, an error is thrown if file_name is None and worker_id != 0: raise UnityEnvironmentException( "If the environment name is None, " "the worker-id must be 0 in order to connect with the Editor.") if file_name is not None: self.executable_launcher(file_name, docker_training, no_graphics, args) else: logger.info( f"Listening on port {self.port}. " f"Start training by pressing the Play button in the Unity Editor." ) self._loaded = True rl_init_parameters_in = UnityRLInitializationInputProto(seed=seed) try: aca_output = self.send_academy_parameters(rl_init_parameters_in) aca_params = aca_output.rl_initialization_output except UnityTimeOutException: self._close() raise # TODO : think of a better way to expose the academyParameters self._unity_version = aca_params.version if self._unity_version != self._version_: self._close() raise UnityEnvironmentException( f"The API number is not compatible between Unity and python. " f"Python API: {self._version_}, Unity API: {self._unity_version}.\n" f"Please go to https://github.com/Unity-Technologies/ml-agents/releases/tag/latest_release" f"to download the latest version of ML-Agents.") self._env_state: Dict[str, BatchedStepResult] = {} self._env_specs: Dict[str, AgentGroupSpec] = {} self._env_actions: Dict[str, np.ndarray] = {} self._is_first_message = True self._update_group_specs(aca_output)
def executable_launcher(self, file_name, docker_training, no_graphics, args): cwd = os.getcwd() file_name = (file_name.strip().replace(".app", "").replace( ".exe", "").replace(".x86_64", "").replace(".x86", "")) true_filename = os.path.basename(os.path.normpath(file_name)) logger.debug("The true file name is {}".format(true_filename)) launch_string = None if platform == "linux" or platform == "linux2": candidates = glob.glob(os.path.join(cwd, file_name) + ".x86_64") if len(candidates) == 0: candidates = glob.glob(os.path.join(cwd, file_name) + ".x86") if len(candidates) == 0: candidates = glob.glob(file_name + ".x86_64") if len(candidates) == 0: candidates = glob.glob(file_name + ".x86") if len(candidates) > 0: launch_string = candidates[0] elif platform == "darwin": candidates = glob.glob( os.path.join(cwd, file_name + ".app", "Contents", "MacOS", true_filename)) if len(candidates) == 0: candidates = glob.glob( os.path.join(file_name + ".app", "Contents", "MacOS", true_filename)) if len(candidates) == 0: candidates = glob.glob( os.path.join(cwd, file_name + ".app", "Contents", "MacOS", "*")) if len(candidates) == 0: candidates = glob.glob( os.path.join(file_name + ".app", "Contents", "MacOS", "*")) if len(candidates) > 0: launch_string = candidates[0] elif platform == "win32": candidates = glob.glob(os.path.join(cwd, file_name + ".exe")) if len(candidates) == 0: candidates = glob.glob(file_name + ".exe") if len(candidates) > 0: launch_string = candidates[0] if launch_string is None: self._close() raise UnityEnvironmentException( "Couldn't launch the {0} environment. " "Provided filename does not match any environments.".format( true_filename)) else: logger.debug("This is the launch string {}".format(launch_string)) # Launch Unity environment if not docker_training: subprocess_args = [launch_string] if no_graphics: subprocess_args += ["-nographics", "-batchmode"] subprocess_args += ["--port", str(self.port)] subprocess_args += args try: self.proc1 = subprocess.Popen( subprocess_args, # start_new_session=True means that signals to the parent python process # (e.g. SIGINT from keyboard interrupt) will not be sent to the new process on POSIX platforms. # This is generally good since we want the environment to have a chance to shutdown, # but may be undesirable in come cases; if so, we'll add a command-line toggle. # Note that on Windows, the CTRL_C signal will still be sent. start_new_session=True, ) except PermissionError as perm: # This is likely due to missing read or execute permissions on file. raise UnityEnvironmentException( f"Error when trying to launch environment - make sure " f"permissions are set correctly. For example " f'"chmod -R 755 {launch_string}"') from perm else: # Comments for future maintenance: # xvfb-run is a wrapper around Xvfb, a virtual xserver where all # rendering is done to virtual memory. It automatically creates a # new virtual server automatically picking a server number `auto-servernum`. # The server is passed the arguments using `server-args`, we are telling # Xvfb to create Screen number 0 with width 640, height 480 and depth 24 bits. # Note that 640 X 480 are the default width and height. The main reason for # us to add this is because we'd like to change the depth from the default # of 8 bits to 24. # Unfortunately, this means that we will need to pass the arguments through # a shell which is why we set `shell=True`. Now, this adds its own # complications. E.g SIGINT can bounce off the shell and not get propagated # to the child processes. This is why we add `exec`, so that the shell gets # launched, the arguments are passed to `xvfb-run`. `exec` replaces the shell # we created with `xvfb`. # docker_ls = ("exec xvfb-run --auto-servernum" " --server-args='-screen 0 640x480x24'" " {0} --port {1}").format(launch_string, str(self.port)) self.proc1 = subprocess.Popen( docker_ls, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, )
def __init__( self, file_name: Optional[str] = None, worker_id: int = 0, base_port: Optional[int] = None, seed: int = 0, no_graphics: bool = False, timeout_wait: int = 60, additional_args: Optional[List[str]] = None, side_channels: Optional[List[SideChannel]] = None, log_folder: Optional[str] = None, ): """ Starts a new unity environment and establishes a connection with the environment. Notice: Currently communication between Unity and Python takes place over an open socket without authentication. Ensure that the network where training takes place is secure. Args: file_name: The file path to the environment. worker_id: The index of the worker in the environment. base_port: The base port to use for running the environment. seed: The seed to use for running the environment. no_graphics: Whether to run the environment in no-graphics mode. timeout_wait: Timeout for connecting to the environment. additional_args: Additional command line arguments to pass to the Unity executable. side_channels: List of side channel to register. log_folder: The folder to save the Unity executable to. Raises: UnityEnvironmentException: If file_name does not correspond to a valid environment. """ if side_channels is None: side_channels = [ EnvironmentParametersChannel(), EngineConfigurationChannel(), ] for sc in side_channels: if isinstance(sc, EnvironmentParametersChannel): self._env_parameters_channel = sc elif isinstance(sc, EngineConfigurationChannel): self._engine_configuration_channel = sc super().__init__( file_name, worker_id, base_port, seed, no_graphics, timeout_wait, additional_args, side_channels, log_folder, ) if not self.behavior_specs: super().reset() self.name = list(self.behavior_specs.keys())[0] self.group_spec = self.behavior_specs[self.name] super().reset() decision_steps, _ = self.get_steps(self.name) self._previous_decision_step = decision_steps # Set action spaces if self.group_spec.action_spec.is_discrete(): self.action_size = self.group_spec.action_spec.discrete_size branches = self.group_spec.action_spec.discrete_branches self._action_spec = branches[0] elif self.group_spec.action_spec.is_continuous(): self.action_size = self.group_spec.action_spec.continuous_size else: raise UnityEnvironmentException( "The environment does not contain a valid action space.") # Set observations spaces observation_spec = [] shapes = self._get_vis_obs_shape() observation_spec.append(shapes[0]) # (64, 64, 3) if self._get_vec_obs_size() > 0: observation_spec.append((self._get_vec_obs_size(), )) # (2,) self._observation_spec = observation_spec