Пример #1
0
    def __init__(self, agent_definitions, binary_path=None, task_key=None, window_height=512, window_width=512,
                 camera_height=256, camera_width=256, start_world=True, uuid="", gl_version=4, verbose=False,
                 pre_start_steps=2):
        """Constructor for HolodeckEnvironment.
        Positional arguments:
        agent_definitions -- A list of AgentDefinition objects for which agents to expect in the environment
        Keyword arguments:
        binary_path -- The path to the binary to load the world from (default None)
        task_key -- The name of the map within the binary to load (default None)
        height -- The height to load the binary at (default 512)
        width -- The width to load the binary at (default 512)
        start_world -- Whether to load a binary or not (default True)
        uuid -- A unique identifier, used when running multiple instances of holodeck (default "")
        gl_version -- The version of OpenGL to use for Linux (default 4)
        """
        self._window_height = window_height
        self._window_width = window_width
        self._camera_height = camera_height
        self._camera_width = camera_width
        self._uuid = uuid
        self._pre_start_steps = pre_start_steps

        Sensors.set_primary_cam_size(window_height, window_width)
        Sensors.set_pixel_cam_size(camera_height, camera_width)

        if start_world:
            if os.name == "posix":
                self.__linux_start_process__(binary_path, task_key, gl_version, verbose=verbose)
            elif os.name == "nt":
                self.__windows_start_process__(binary_path, task_key, verbose=verbose)
            else:
                raise HolodeckException("Unknown platform: " + os.name)

        # Set up and add the agents
        self._client = HolodeckClient(self._uuid)
        self._sensor_map = dict()
        self._all_agents = list()
        self.agents = dict()
        self._hyperparameters_map = dict()
        self._add_agents(agent_definitions)
        self._agent = self._all_agents[0]

        # Set the default state function
        self.num_agents = len(self._all_agents)
        self._default_state_fn = self._get_single_state if self.num_agents == 1 else self._get_full_state

        # Subscribe settings
        self._reset_ptr = self._client.malloc("RESET", [1], np.bool)
        self._reset_ptr[0] = False
        self._command_bool_ptr = self._client.malloc("command_bool", [1], np.bool)
        megabyte = 1048576  # This is the size of the command buffer that Holodeck expects/will read.
        self._command_buffer_ptr = self._client.malloc("command_buffer", [megabyte], np.byte)

        # self._commands holds commands that are queued up to write to the command buffer on tick.
        self._commands = CommandsGroup()
        self._should_write_to_command_buffer = False

        self._client.acquire()
Пример #2
0
    def __init__(self, agent_definitions=None, binary_path=None, window_size=(720, 1280),
                 start_world=True, uuid="", gl_version=4, verbose=False, pre_start_steps=2,
                 show_viewport=True, ticks_per_sec=30, copy_state=True, scenario=None):

        if agent_definitions is None:
            agent_definitions = []

        # Initialize variables
        self._window_size = window_size
        self._uuid = uuid
        self._pre_start_steps = pre_start_steps
        self._copy_state = copy_state
        self._ticks_per_sec = ticks_per_sec
        self._scenario = scenario
        self._initial_agent_defs = agent_definitions
        self._spawned_agent_defs = []

        # Start world based on OS
        if start_world:
            world_key = self._scenario["world"]
            if os.name == "posix":
                self.__linux_start_process__(binary_path, world_key, gl_version, verbose=verbose,
                                             show_viewport=show_viewport)
            elif os.name == "nt":
                self.__windows_start_process__(binary_path, world_key, verbose=verbose)
            else:
                raise HolodeckException("Unknown platform: " + os.name)

        # Initialize Client
        self._client = HolodeckClient(self._uuid, start_world)
        self._command_center = CommandCenter(self._client)
        self._client.command_center = self._command_center
        self._reset_ptr = self._client.malloc("RESET", [1], np.bool)
        self._reset_ptr[0] = False

        # Set up agents already in the world
        self.agents = dict()
        self._state_dict = dict()
        self._agent = None

        # Spawn agents not yet in the world.
        # TODO implement this section for future build automation update

        # Set the default state function
        self.num_agents = len(self.agents)

        if self.num_agents == 1:
            self._default_state_fn = self._get_single_state
        else:
            self._default_state_fn = self._get_full_state

        self._client.acquire()

        # Flag indicates if the user has called .reset() before .tick() and .step()
        self._initial_reset = False
        self.reset()
Пример #3
0
class HolodeckEnvironment(object):
    """The high level interface for interacting with a Holodeck world.
    Most users will want an environment created for them via `holodeck.make`.

    Args:
        agent_definitions (list of :obj:`AgentDefinition`): Which agents to expect in the environment.
        binary_path (str, optional): The path to the binary to load the world from. Defaults to None.
        task_key (str, optional): The name of the map within the binary to load. Defaults to None.
        window_height (int, optional): The height to load the binary at. Defaults to 512.
        window_width (int, optional): The width to load the binary at. Defaults to 512.
        camera_height (int, optional): The height of all pixel camera sensors. Defaults to 512.
        camera_width (int, optional): The width of all pixel camera sensors. Defaults to 512.
        start_world (bool, optional): Whether to load a binary or not. Defaults to True.
        uuid (str): A unique identifier, used when running multiple instances of holodeck. Defaults to "".
        gl_version (int, optional): The version of OpenGL to use for Linux. Defaults to 4.

    Returns:
        HolodeckEnvironment: A holodeck environment object.
    """

    def __init__(self, agent_definitions, binary_path=None, task_key=None, window_height=512, window_width=512,
                 camera_height=256, camera_width=256, start_world=True, uuid="", gl_version=4, verbose=False,
                 pre_start_steps=2):
        """Constructor for HolodeckEnvironment.
        Positional arguments:
        agent_definitions -- A list of AgentDefinition objects for which agents to expect in the environment
        Keyword arguments:
        binary_path -- The path to the binary to load the world from (default None)
        task_key -- The name of the map within the binary to load (default None)
        height -- The height to load the binary at (default 512)
        width -- The width to load the binary at (default 512)
        start_world -- Whether to load a binary or not (default True)
        uuid -- A unique identifier, used when running multiple instances of holodeck (default "")
        gl_version -- The version of OpenGL to use for Linux (default 4)
        """
        self._window_height = window_height
        self._window_width = window_width
        self._camera_height = camera_height
        self._camera_width = camera_width
        self._uuid = uuid
        self._pre_start_steps = pre_start_steps

        Sensors.set_primary_cam_size(window_height, window_width)
        Sensors.set_pixel_cam_size(camera_height, camera_width)

        if start_world:
            if os.name == "posix":
                self.__linux_start_process__(binary_path, task_key, gl_version, verbose=verbose)
            elif os.name == "nt":
                self.__windows_start_process__(binary_path, task_key, verbose=verbose)
            else:
                raise HolodeckException("Unknown platform: " + os.name)

        # Set up and add the agents
        self._client = HolodeckClient(self._uuid)
        self._sensor_map = dict()
        self._all_agents = list()
        self.agents = dict()
        self._hyperparameters_map = dict()
        self._add_agents(agent_definitions)
        self._agent = self._all_agents[0]

        # Set the default state function
        self.num_agents = len(self._all_agents)
        self._default_state_fn = self._get_single_state if self.num_agents == 1 else self._get_full_state

        # Subscribe settings
        self._reset_ptr = self._client.malloc("RESET", [1], np.bool)
        self._reset_ptr[0] = False
        self._command_bool_ptr = self._client.malloc("command_bool", [1], np.bool)
        megabyte = 1048576  # This is the size of the command buffer that Holodeck expects/will read.
        self._command_buffer_ptr = self._client.malloc("command_buffer", [megabyte], np.byte)

        # self._commands holds commands that are queued up to write to the command buffer on tick.
        self._commands = CommandsGroup()
        self._should_write_to_command_buffer = False

        self._client.acquire()

    @property
    def action_space(self):
        """Gives the action space for the main agent.

        Returns:
            ActionSpace: The action space for the main agent.
        """
        return self._agent.action_space

    def info(self):
        """Returns a string with specific information about the environment.
        This information includes which agents are in the environment and which sensors they have.

        Returns:
            str: The information in a string format.
        """
        result = list()
        result.append("Agents:\n")
        for agent in self._all_agents:
            result.append("\tName: ")
            result.append(agent.name)
            result.append("\n\tType: ")
            result.append(type(agent).__name__)
            result.append("\n\t")
            result.append("Sensors:\n")
            for sensor in self._sensor_map[agent.name].keys():
                result.append("\t\t")
                result.append(Sensors.name(sensor))
                result.append("\n")
        return "".join(result)

    def reset(self):
        """Resets the environment, and returns the state.
        If it is a single agent environment, it returns that state for that agent. Otherwise, it returns a dict from
        agent name to state.

        Returns:
            tuple or dict: For single agent environment, returns the same as `step`.
                For multi-agent environment, returns the same as `tick`.
        """
        self._reset_ptr[0] = True
        self._commands.clear()

        for _ in range(self._pre_start_steps + 1):
            self.tick()

        return self._default_state_fn()

    def step(self, action):
        """Supplies an action to the main agent and tells the environment to tick once.
        Primary mode of interaction for single agent environments.

        Args:
            action (np.ndarray): An action for the main agent to carry out on the next tick.

        Returns:
            tuple: The (state, reward, terminal, info) tuple for the agent. State is a dictionary
            from sensor enum (see :obj:`holodeck.sensors.Sensors`) to np.ndarray.
            Reward is the float reward returned by the environment.
            Terminal is the bool terminal signal returned by the environment.
            Info is any additional info, depending on the world. Defaults to None.
        """
        self._agent.act(action)

        self._handle_command_buffer()

        self._client.release()
        self._client.acquire()

        return self._get_single_state()

    def teleport(self, agent_name, location=None, rotation=None):
        """Teleports the target agent to any given location, and applies a specific rotation.

        Args:
            agent_name (str): The name of the agent to teleport.
            location (np.ndarray or list): XYZ coordinates (in meters) for the agent to be teleported to.
                If no location is given, it isn't teleported, but may still be rotated. Defaults to None.
            rotation (np.ndarray or list): A new rotation target for the agent.
                If no rotation is given, it isn't rotated, but may still be teleported. Defaults to None.
        """
        self.agents[agent_name].teleport(location * 100, rotation)  # * 100 to convert m to cm
        self.tick()

    def act(self, agent_name, action):
        """Supplies an action to a particular agent, but doesn't tick the environment.
        Primary mode of interaction for multi-agent environments. After all agent commands are supplied,
        they can be applied with a call to `tick`.

        Args:
            agent_name (str): The name of the agent to supply an action for.
            action (np.ndarray or list): The action to apply to the agent. This action will be applied every
                time `tick` is called, until a new action is supplied with another call to act.
        """
        self.agents[agent_name].act(action)

    def tick(self):
        """Ticks the environment once. Normally used for multi-agent environments.

        Returns:
            dict: A dictionary from agent name to its full state. The full state is another dictionary
            from :obj:`holodeck.sensors.Sensors` enum to np.ndarray, containing the sensors information
            for each sensor. The sensors always include the reward and terminal sensors.
        """
        self._handle_command_buffer()
        self._client.release()
        self._client.acquire()
        return self._get_full_state()

    def add_state_sensors(self, agent_name, sensors):
        """Adds a sensor to a particular agent. This only works if the world you are running also includes
        that particular sensor on the agent.

        Args:
            agent_name (str): The name of the agent to add the sensor to.
            sensors (:obj:`HolodeckSensor` or list of :obj:`HolodeckSensor`): Sensors to add to the agent.
                Should be objects that inherit from :obj:`HolodeckSensor`.
        """
        if isinstance(sensors, list):
            for sensor in sensors:
                self.add_state_sensors(agent_name, sensor)
        else:
            if agent_name not in self._sensor_map:
                self._sensor_map[agent_name] = dict()

            self._sensor_map[agent_name][sensors] = self._client.malloc(agent_name + "_" + Sensors.name(sensors),
                                                                        Sensors.shape(sensors),
                                                                        Sensors.dtype(sensors))

    def spawn_agent(self, agent_definition, location):
        """Queues a spawn agent command. It will be applied when `tick` or `step` is called next.
        The agent won't be able to be used until the next frame.

        Args:
            agent_definition (:obj:`AgentDefinition`): The definition of the agent to spawn.
            location (np.ndarray or list): The position to spawn the agent in the world, in XYZ coordinates (in meters).
        """
        self._should_write_to_command_buffer = True
        self._add_agents(agent_definition)
        command_to_send = SpawnAgentCommand(location, agent_definition.name, agent_definition.type)
        self._commands.add_command(command_to_send)

    def set_fog_density(self, density):
        """Queue up a change fog density command. It will be applied when `tick` or `step` is called next.
        By the next tick, the exponential height fog in the world will have the new density. If there is no fog in the
        world, it will be automatically created with the given density.

        Args:
            density (float): The new density value, between 0 and 1. The command will not be sent if the given
        density is invalid.
        """
        if density < 0 or density > 1:
            raise HolodeckException("Fog density should be between 0 and 1")

        self._should_write_to_command_buffer = True
        command_to_send = ChangeFogDensityCommand(density)
        self._commands.add_command(command_to_send)

    def set_day_time(self, hour):
        """Queue up a change day time command. It will be applied when `tick` or `step` is called next.
        By the next tick, the lighting and the skysphere will be updated with the new hour. If there is no skysphere
        or directional light in the world, the command will not function properly but will not cause a crash.

        Args:
            hour (int): The hour in military time, between 0 and 23 inclusive.
        """
        self._should_write_to_command_buffer = True
        command_to_send = DayTimeCommand(hour % 24)
        self._commands.add_command(command_to_send)

    def start_day_cycle(self, day_length):
        """Queue up a day cycle command to start the day cycle. It will be applied when `tick` or `step` is called next.
        The sky sphere will now update each tick with an updated sun angle as it moves about the sky. The length of a
        day will be roughly equivalent to the number of minutes given.

        Args:
            day_length (int): The number of minutes each day will be.
        """
        if day_length <= 0:
            raise HolodeckException("The given day length should be between above 0!")

        self._should_write_to_command_buffer = True
        command_to_send = DayCycleCommand(True)
        command_to_send.set_day_length(day_length)
        self._commands.add_command(command_to_send)

    def stop_day_cycle(self):
        """Queue up a day cycle command to stop the day cycle. It will be applied when `tick` or `step` is called next.
        By the next tick, day cycle will stop where it is.
        """
        self._should_write_to_command_buffer = True
        command_to_send = DayCycleCommand(False)
        self._commands.add_command(command_to_send)

    def teleport_camera(self, location, rotation):
        """Queue up a teleport camera command to stop the day cycle.
        By the next tick, the camera's location and rotation will be updated
        """
        self._should_write_to_command_buffer = True
        command_to_send = TeleportCameraCommand(location, rotation)
        self._commands.add_command(command_to_send)

    def set_weather(self, weather_type):
        """Queue up a set weather command. It will be applied when `tick` or `step` is called next.
        By the next tick, the lighting, skysphere, fog, and relevant particle systems will be updated and/or spawned
        to the given weather. If there is no skysphere or directional light in the world, the command may not function
        properly but will not cause a crash.

        NOTE: Because this command can effect the fog density, any changes made by a change_fog_density command before
        a set_weather command called will be undone. It is recommended to call change_fog_density after calling set
        weather.

        Args:
            weather_type (str): The type of weather, which can be 'Rain' or 'Cloudy'. In all downloadable worlds,
            the weather is clear by default. If the given type string is not available, the command will not be sent.

        """
        if not SetWeatherCommand.has_type(weather_type.lower()):
            raise HolodeckException("Invalid weather type " + weather_type)

        self._should_write_to_command_buffer = True
        command_to_send = SetWeatherCommand(weather_type.lower())
        self._commands.add_command(command_to_send)

    def set_control_scheme(self, agent_name, control_scheme):
        """Set the control scheme for a specific agent.

        Args:
            agent_name (str): The name of the agent to set the control scheme for.
            control_scheme (int): A control scheme value (see :obj:`holodeck.agents.ControlSchemes`)
        """
        if agent_name not in self.agents:
            print("No such agent %s" % agent_name)
        else:
            self.agents[agent_name].set_control_scheme(control_scheme)

    def __linux_start_process__(self, binary_path, task_key, gl_version, verbose):
        import posix_ipc
        out_stream = sys.stdout if verbose else open(os.devnull, 'w')
        loading_semaphore = posix_ipc.Semaphore('/HOLODECK_LOADING_SEM' + self._uuid, os.O_CREAT | os.O_EXCL,
                                                initial_value=0)
        self._world_process = subprocess.Popen([binary_path, task_key, '-HolodeckOn', '-opengl' + str(gl_version),
                                                '-LOG=HolodeckLog.txt', '-ResX=' + str(self._window_width),
                                                '-ResY=' + str(self._window_height),'-CamResX=' + str(self._camera_width),
                                                '-CamResY=' + str(self._camera_height), '--HolodeckUUID=' + self._uuid],
                                               stdout=out_stream,
                                               stderr=out_stream)

        atexit.register(self.__on_exit__)

        try:
            loading_semaphore.acquire(100)
        except posix_ipc.BusyError:
            raise HolodeckException("Timed out waiting for binary to load. Ensure that holodeck is not being run with root priveleges.")
        loading_semaphore.unlink()

    def __windows_start_process__(self, binary_path, task_key, verbose):
        import win32event
        out_stream = sys.stdout if verbose else open(os.devnull, 'w')
        loading_semaphore = win32event.CreateSemaphore(None, 0, 1, "Global\\HOLODECK_LOADING_SEM" + self._uuid)
        self._world_process = subprocess.Popen([binary_path, task_key, '-HolodeckOn', '-LOG=HolodeckLog.txt',
                                                '-ResX=' + str(self._window_width), "-ResY=" + str(self._window_height),
                                                '-CamResX=' + str(self._camera_width),
                                                '-CamResY=' + str(self._camera_height), "--HolodeckUUID=" + self._uuid],
                                               stdout=out_stream, stderr=out_stream)
        atexit.register(self.__on_exit__)
        response = win32event.WaitForSingleObject(loading_semaphore, 100000)  # 100 second timeout
        if response == win32event.WAIT_TIMEOUT:
            raise HolodeckException("Timed out waiting for binary to load")

    def __on_exit__(self):
        if hasattr(self, '_world_process'):
            self._world_process.kill()
            self._world_process.wait(5)
        self._client.unlink()

    def _get_single_state(self):
        reward = None
        terminal = None
        for sensor in self._sensor_map[self._agent.name]:
            if sensor == Sensors.REWARD:
                reward = self._sensor_map[self._agent.name][sensor][0]
            elif sensor == Sensors.TERMINAL:
                terminal = self._sensor_map[self._agent.name][sensor][0]

        return copy(self._sensor_map[self._agent.name]), reward, terminal, None

    def _get_full_state(self):
        return copy(self._sensor_map)

    def _handle_command_buffer(self):
        """Checks if we should write to the command buffer, writes all of the queued commands to the buffer, and then
        clears the contents of the self._commands list"""
        if self._should_write_to_command_buffer:
            self._write_to_command_buffer(self._commands.to_json())
            self._should_write_to_command_buffer = False
            self._commands.clear()

    def _prepare_agents(self, agent_definitions):
        if isinstance(agent_definitions, list):
            return [self._prepare_agents(x)[0] for x in agent_definitions]
        return [agent_definitions.type(client=self._client, name=agent_definitions.name)]

    def _add_agents(self, agent_definitions):
        """Add specified agents to the client. Set up their shared memory and sensor linkages.
        Does not spawn an agent in the Holodeck, this is only for documenting and accessing already existing agents.
        This is an internal function.
        Positional Arguments:
        agent_definitions -- The agent(s) to add.
        """
        if not isinstance(agent_definitions, list):
            agent_definitions = [agent_definitions]
        prepared_agents = self._prepare_agents(agent_definitions)
        self._all_agents.extend(prepared_agents)
        for agent in prepared_agents:
            self.agents[agent.name] = agent
        for agent in agent_definitions:
            self.add_state_sensors(agent.name, [Sensors.TERMINAL, Sensors.REWARD])
            self.add_state_sensors(agent.name, agent.sensors)

    def _write_to_command_buffer(self, to_write):
        """Write input to the command buffer.  Reformat input string to the correct format.

        Args:
            to_write (str): The string to write to the command buffer.
        """
        # TODO(mitch): Handle the edge case of writing too much data to the buffer.
        np.copyto(self._command_bool_ptr, True)
        to_write += '0'  # The gason JSON parser in holodeck expects a 0 at the end of the file.
        input_bytes = str.encode(to_write)
        for index, val in enumerate(input_bytes):
            self._command_buffer_ptr[index] = val
Пример #4
0
class HolodeckEnvironment:
    """Proxy for communicating with a Holodeck world

    Instantiate this object using :meth:`holodeck.holodeck.make`.

    Args:
        agent_definitions (:obj:`list` of :class:`AgentDefinition`):
            Which agents are already in the environment

        binary_path (:obj:`str`, optional):
            The path to the binary to load the world from. Defaults to None.

        window_size ((:obj:`int`,:obj:`int`)):
            height, width of the window to open

        start_world (:obj:`bool`, optional):
            Whether to load a binary or not. Defaults to True.

        uuid (:obj:`str`):
            A unique identifier, used when running multiple instances of holodeck. Defaults to "".

        gl_version (:obj:`int`, optional):
            The version of OpenGL to use for Linux. Defaults to 4.

        verbose (:obj:`bool`):
            If engine log output should be printed to stdout

        pre_start_steps (:obj:`int`):
            Number of ticks to call after initializing the world, allows the level to load and settle.

        show_viewport (:obj:`bool`, optional):
            If the viewport should be shown (Linux only) Defaults to True.

        ticks_per_sec (:obj:`int`, optional):
            Number of frame ticks per unreal second. Defaults to 30.

        copy_state (:obj:`bool`, optional):
            If the state should be copied or returned as a reference. Defaults to True.

        scenario (:obj:`dict`):
            The scenario that is to be loaded. See :ref:`scenario-files` for the schema.

    """

    def __init__(self, agent_definitions=None, binary_path=None, window_size=(720, 1280),
                 start_world=True, uuid="", gl_version=4, verbose=False, pre_start_steps=2,
                 show_viewport=True, ticks_per_sec=30, copy_state=True, scenario=None):

        if agent_definitions is None:
            agent_definitions = []

        # Initialize variables
        self._window_size = window_size
        self._uuid = uuid
        self._pre_start_steps = pre_start_steps
        self._copy_state = copy_state
        self._ticks_per_sec = ticks_per_sec
        self._scenario = scenario
        self._initial_agent_defs = agent_definitions
        self._spawned_agent_defs = []

        # Start world based on OS
        if start_world:
            world_key = self._scenario["world"]
            if os.name == "posix":
                self.__linux_start_process__(binary_path, world_key, gl_version, verbose=verbose,
                                             show_viewport=show_viewport)
            elif os.name == "nt":
                self.__windows_start_process__(binary_path, world_key, verbose=verbose)
            else:
                raise HolodeckException("Unknown platform: " + os.name)

        # Initialize Client
        self._client = HolodeckClient(self._uuid, start_world)
        self._command_center = CommandCenter(self._client)
        self._client.command_center = self._command_center
        self._reset_ptr = self._client.malloc("RESET", [1], np.bool)
        self._reset_ptr[0] = False

        # Set up agents already in the world
        self.agents = dict()
        self._state_dict = dict()
        self._agent = None

        # Spawn agents not yet in the world.
        # TODO implement this section for future build automation update

        # Set the default state function
        self.num_agents = len(self.agents)

        if self.num_agents == 1:
            self._default_state_fn = self._get_single_state
        else:
            self._default_state_fn = self._get_full_state

        self._client.acquire()

        # Flag indicates if the user has called .reset() before .tick() and .step()
        self._initial_reset = False
        self.reset()

    @property
    def action_space(self):
        """Gives the action space for the main agent.

        Returns:
            :class:`~holodeck.spaces.ActionSpace`: The action space for the main agent.
        """
        return self._agent.action_space

    def info(self):
        """Returns a string with specific information about the environment.
        This information includes which agents are in the environment and which sensors they have.

        Returns:
            :obj:`str`: Information in a string format.
        """
        result = list()
        result.append("Agents:\n")
        for agent_name in self.agents:
            agent = self.agents[agent_name]
            result.append("\tName: ")
            result.append(agent.name)
            result.append("\n\tType: ")
            result.append(type(agent).__name__)
            result.append("\n\t")
            result.append("Sensors:\n")
            for _, sensor in agent.sensors.items():
                result.append("\t\t")
                result.append(sensor.name)
                result.append("\n")
        return "".join(result)

    def _load_scenario(self):
        """Loads the scenario defined in self._scenario_key.

        Instantiates all agents and sensors.

        If no scenario is defined, does nothing.
        """
        if self._scenario is None:
            return

        for agent in self._scenario['agents']:
            sensors = []
            for sensor in agent['sensors']:
                if 'sensor_type' not in sensor:
                    raise HolodeckException(
                        "Sensor for agent {} is missing required key "
                        "'sensor_type'".format(agent['agent_name']))

                # Default values for a sensor
                sensor_config = {
                    'location': [0, 0, 0],
                    'rotation': [0, 0, 0],
                    'socket': "",
                    'configuration': {},
                    'sensor_name': sensor['sensor_type']
                }
                # Overwrite the default values with what is defined in the scenario config
                sensor_config.update(sensor)

                sensors.append(SensorDefinition(agent['agent_name'],
                                                sensor_config['sensor_name'],
                                                sensor_config['sensor_type'],
                                                socket=sensor_config['socket'],
                                                location=sensor_config['location'],
                                                rotation=sensor_config['rotation'],
                                                config=sensor_config['configuration']))
            # Default values for an agent
            agent_config = {
                'location': [0, 0, 0],
                'rotation': [0, 0, 0],
                'agent_name': agent['agent_type']
            }

            agent_config.update(agent)
            agent_def = AgentDefinition(agent_config['agent_name'], agent_config['agent_type'],
                                        starting_loc=agent_config["location"],
                                        starting_rot=agent_config["rotation"],  sensors=sensors)

            is_main_agent = False
            if "main_agent" in self._scenario:
                is_main_agent = self._scenario["main_agent"] == agent["agent_name"]

            self.add_agent(agent_def, is_main_agent)
            self.agents[agent['agent_name']].set_control_scheme(agent['control_scheme'])
            self._spawned_agent_defs.append(agent_def)

    def reset(self):
        """Resets the environment, and returns the state.
        If it is a single agent environment, it returns that state for that agent. Otherwise, it
        returns a dict from agent name to state.

        Returns (tuple or dict):
            For single agent environment, returns the same as `step`.

            For multi-agent environment, returns the same as `tick`.
        """
        # Reset level
        self._initial_reset = True
        self._reset_ptr[0] = True
        for agent in self.agents.values():
            agent.clear_action()
        self.tick()  # Must tick once to send reset before sending spawning commands
        self.tick()  # Bad fix to potential race condition. See issue BYU-PCCL/holodeck#224
        self.tick()
        # Clear command queue
        if self._command_center.queue_size > 0:
            print("Warning: Reset called before all commands could be sent. Discarding",
                  self._command_center.queue_size, "commands.")
        self._command_center.clear()

        # Load agents
        self._spawned_agent_defs = []
        self.agents = dict()
        self._state_dict = dict()
        for agent_def in self._initial_agent_defs:
            self.add_agent(agent_def)

        self._load_scenario()

        self.num_agents = len(self.agents)

        if self.num_agents == 1:
            self._default_state_fn = self._get_single_state
        else:
            self._default_state_fn = self._get_full_state

        for _ in range(self._pre_start_steps + 1):
            self.tick()

        return self._default_state_fn()

    def step(self, action):
        """Supplies an action to the main agent and tells the environment to tick once.
        Primary mode of interaction for single agent environments.

        Args:
            action (:obj:`np.ndarray`): An action for the main agent to carry out on the next tick.

        Returns:
            (:obj:`dict`, :obj:`float`, :obj:`bool`, info): A 4tuple:
                - State: Dictionary from sensor enum
                    (see :class:`~holodeck.sensors.HolodeckSensor`) to :obj:`np.ndarray`.
                - Reward (:obj:`float`): Reward returned by the environment.
                - Terminal: The bool terminal signal returned by the environment.
                - Info: Any additional info, depending on the world. Defaults to None.
        """
        if not self._initial_reset:
            raise HolodeckException("You must call .reset() before .step()")

        if self._agent is not None:
            self._agent.act(action)

        self._command_center.handle_buffer()
        self._client.release()
        self._client.acquire()

        reward, terminal = self._get_reward_terminal()
        return self._default_state_fn(), reward, terminal, None

    def act(self, agent_name, action):
        """Supplies an action to a particular agent, but doesn't tick the environment.
        Primary mode of interaction for multi-agent environments. After all agent commands are
            supplied, they can be applied with a call to `tick`.

        Args:
            agent_name (:obj:`str`): The name of the agent to supply an action for.
            action (:obj:`np.ndarray` or :obj:`list`): The action to apply to the agent. This
                action will be applied every time `tick` is called, until a new action is supplied
                with another call to act.
        """
        self.agents[agent_name].act(action)

    def tick(self):
        """Ticks the environment once. Normally used for multi-agent environments.

        Returns:
            :obj:`dict`: A dictionary from agent name to its full state. The full state is another
                dictionary from :obj:`holodeck.sensors.Sensors` enum to np.ndarray, containing the
                sensors information for each sensor. The sensors always include the reward and
                terminal sensors.
        """
        if not self._initial_reset:
            raise HolodeckException("You must call .reset() before .tick()")

        self._command_center.handle_buffer()

        self._client.release()
        self._client.acquire()

        return self._default_state_fn()

    def teleport(self, agent_name, location=None, rotation=None):
        """Teleports the target agent to any given location, and applies a specific rotation.

        Args:
            agent_name (:obj:`str`): The name of the agent to teleport.
            location (:obj:`np.ndarray` or :obj:`list`): XYZ coordinates (in meters) for the agent
                to be teleported to.

                If no location is given, it isn't teleported, but may still be rotated. Defaults to
                None.
            rotation (:obj:`np.ndarray` or :obj:`list`): A new rotation target for the agent.
                If no rotation is given, it isn't rotated, but may still be teleported. Defaults to
                None.
        """
        self.agents[agent_name].teleport(location, rotation)

    def set_state(self, agent_name, location, rotation, velocity, angular_velocity):
        """Sets a new state for any agent given a location, rotation and linear and angular
        velocity. Will sweep and be blocked by objects in it's way however

        Args:
            agent_name (:obj:`str`): The name of the agent to teleport.
            location (:obj:`np.ndarray` or :obj:`list`): New ``[x, y, z]`` coordinates for agent
                (see :ref:`coordinate-system`).
            rotation (:obj:`np.ndarray` or :obj:`list`): ``[roll, pitch, yaw`` rotation for the agent
                (see :ref:`rotations`).
            velocity (:obj:`np.ndarray` or :obj:`list`): New velocity ``[x, y, z]`` for the agent.
            angular velocity (:obj:`np.ndarray` or :obj:`list`): A new angular velocity for the
                agent in **degrees**
        """
        self.agents[agent_name].set_state(location, rotation, velocity, angular_velocity)

    def _enqueue_command(self, command_to_send):
        self._command_center.enqueue_command(command_to_send)

    def add_agent(self, agent_def, is_main_agent=False):
        """Add an agent in the world.

        It will be spawn when :meth:`tick` or :meth:`step` is called next.

        The agent won't be able to be used until the next frame.

        Args:
            agent_def (:class:`~holodeck.agents.AgentDefinition`): The definition of the agent to
            spawn.
        """
        if agent_def.name in self.agents:
            raise HolodeckException("Error. Duplicate agent name. ")

        self.agents[agent_def.name] = AgentFactory.build_agent(self._client, agent_def)
        self._state_dict[agent_def.name] = self.agents[agent_def.name].agent_state_dict

        if not agent_def.existing:
            command_to_send = SpawnAgentCommand(agent_def.starting_loc, agent_def.starting_rot, agent_def.name,
                                                agent_def.type.agent_type)
            self._client.command_center.enqueue_command(command_to_send)
        self.agents[agent_def.name].add_sensors(agent_def.sensors)
        if is_main_agent:
            self._agent = self.agents[agent_def.name]

    def set_ticks_per_capture(self, agent_name, ticks_per_capture):
        """Queues a rgb camera rate command. It will be applied when :meth:`tick` or :meth:`step` is
        called next.

        The specified agent's rgb camera will capture images every specified number of ticks.

        The sensor's image will remain unchanged between captures.

        This method must be called after every call to env.reset.

        Args:
            agent_name (:obj:`str`): The name of the agent whose rgb camera should be modified.
            ticks_per_capture (:obj:`int`): The amount of ticks to wait between camera captures.
        """
        if not isinstance(ticks_per_capture, int) or ticks_per_capture < 1:
            print("Ticks per capture value " + str(ticks_per_capture) + " invalid")
        elif agent_name not in self.agents:
            print("No such agent %s" % agent_name)
        else:
            self.agents[agent_name].set_ticks_per_capture(ticks_per_capture)
            command_to_send = RGBCameraRateCommand(agent_name, ticks_per_capture)
            self._enqueue_command(command_to_send)

    def draw_line(self, start, end, color=None, thickness=10.0):
        """Draws a debug line in the world

        Args:
            start (:obj:`list` of :obj:`float`): The start ``[x, y, z]`` location of the line.
                (see :ref:`coordinate-system`)
            end (:obj:`list` of :obj:`float`): The end ``[x, y, z]`` location of the line
            color (:obj:`list``): ``[r, g, b]`` color value
            thickness (:obj:`float`): thickness of the line
        """
        color = [255, 0, 0] if color is None else color
        command_to_send = DebugDrawCommand(0, start, end, color, thickness)
        self._enqueue_command(command_to_send)

    def draw_arrow(self, start, end, color=None, thickness=10.0):
        """Draws a debug arrow in the world

        Args:
            start (:obj:`list` of :obj:`float`): The start ``[x, y, z]`` location of the line.
                (see :ref:`coordinate-system`)
            end (:obj:`list` of :obj:`float`): The end ``[x, y, z]`` location of the arrow
            color (:obj:`list`): ``[r, g, b]`` color value
            thickness (:obj:`float`): thickness of the arrow
        """
        color = [255, 0, 0] if color is None else color
        command_to_send = DebugDrawCommand(1, start, end, color, thickness)
        self._enqueue_command(command_to_send)

    def draw_box(self, center, extent, color=None, thickness=10.0):
        """Draws a debug box in the world

        Args:
            center (:obj:`list` of :obj:`float`): The start ``[x, y, z]`` location of the box.
                (see :ref:`coordinate-system`)
            extent (:obj:`list` of :obj:`float`): The ``[x, y, z]`` extent of the box
            color (:obj:`list`): ``[r, g, b]`` color value
            thickness (:obj:`float`): thickness of the lines
        """
        color = [255, 0, 0] if color is None else color
        command_to_send = DebugDrawCommand(2, center, extent, color, thickness)
        self._enqueue_command(command_to_send)

    def draw_point(self, loc, color=None, thickness=10.0):
        """Draws a debug point in the world

        Args:
            loc (:obj:`list` of :obj:`float`): The ``[x, y, z]`` start of the box. 
                (see :ref:`coordinate-system`)
            color (:obj:`list` of :obj:`float`): ``[r, g, b]`` color value
            thickness (:obj:`float`): thickness of the point
        """
        color = [255, 0, 0] if color is None else color
        command_to_send = DebugDrawCommand(3, loc, [0, 0, 0], color, thickness)
        self._enqueue_command(command_to_send)

    def set_fog_density(self, density):
        """Change the fog density.

        The change will occur when :meth:`tick` or :meth:`step` is called next.

        By the next tick, the exponential height fog in the world will have the new density. If
        there is no fog in the world, it will be created with the given density.

        Args:
            density (:obj:`float`): The new density value, between 0 and 1. The command will not be
                sent if the given density is invalid.
        """
        if density < 0 or density > 1:
            raise HolodeckException("Fog density should be between 0 and 1")

        self.send_world_command("SetFogDensity", num_params=[density])

    def set_day_time(self, hour):
        """Change the time of day.

        Daytime will change when :meth:`tick` or :meth:`step` is called next.

        By the next tick, the lighting and the skysphere will be updated with the new hour.

        If there is no skysphere, skylight, or directional source light in the world, this command
        will fail silently.

        Args:
            hour (:obj:`int`): The hour in 24-hour format, between 0 and 23 inclusive.
        """
        self.send_world_command("SetHour", num_params=[hour % 24])

    def start_day_cycle(self, day_length):
        """Start the day cycle.

        The cycle will start when :meth:`tick` or :meth:`step` is called next.

        The sky sphere will then update each tick with an updated sun angle as it moves about the]
        sky. The length of a day will be roughly equivalent to the number of minutes given.

        If there is no skysphere, skylight, or directional source light in the world, this command
        will fail silently.

        Args:
            day_length (:obj:`int`): The number of minutes each day will be.
        """
        if day_length <= 0:
            raise HolodeckException("The given day length should be between above 0!")

        self.send_world_command("SetDayCycle", num_params=[1, day_length])

    def stop_day_cycle(self):
        """Stop the day cycle.

        The cycle will stop when :meth:`tick` or :meth:`step` is called next.

        By the next tick, day cycle will stop where it is.

        If there is no skysphere, skylight, or directional source light in the world, this command
        will fail silently.
        """
        self.send_world_command("SetDayCycle", num_params=[0, -1])

    def set_weather(self, weather_type):
        """Set the world's weather.

        The new weather will be applied when :meth:`tick` or :meth:`step` is called next.

        By the next tick, the lighting, skysphere, fog, and relevant particle systems will be
        updated and/or spawned
        to the given weather.

        If there is no skysphere, skylight, or directional source light in the world, this command
        will fail silently.

        ..note::
            Because this command can affect the fog density, any changes made by a
            ``change_fog_density`` command before a set_weather command called will be undone. It is
            recommended to call ``change_fog_density`` after calling set weather if you wish to
            apply your specific changes.

        In all downloadable worlds, the weather is clear by default.

        If the given type string is not available, the command will not be sent.

        Args:
            weather_type (:obj:`str`): The type of weather, which can be ``rain`` or ``cloudy``.

        """
        if not weather_type.lower() in ["rain", "cloudy"]:
            raise HolodeckException("Invalid weather type " + weather_type)

        self.send_world_command("SetWeather", string_params=[weather_type])

    def teleport_camera(self, location, rotation):
        """Teleport the camera to the given location

        By the next tick, the camera's location and rotation will be updated

        Args:
            location (:obj:`list` of :obj:`float`): The ``[x, y, z]`` location to give the camera
                (see :ref:`coordinate-system`)
            rotation (:obj:`list` of :obj:`float`): The ``[roll, pitch, yaw]`` rotation to give the camera
                (see :ref:`rotations`)

        """
        self._enqueue_command(TeleportCameraCommand(location, rotation))

    def should_render_viewport(self, render_viewport):
        """Controls whether the viewport is rendered or not

        Args:
            render_viewport (:obj:`boolean`): If the viewport should be rendered
        """
        self._enqueue_command(RenderViewportCommand(render_viewport))

    def set_render_quality(self, render_quality):
        """Adjusts the rendering quality of Holodeck.
        Args:
            render_quality (:obj:`int`): An integer between 0 = Low Quality and 3 = Epic quality.
        """
        self._enqueue_command(RenderQualityCommand(render_quality))

    def set_control_scheme(self, agent_name, control_scheme):
        """Set the control scheme for a specific agent.

        Args:
            agent_name (:obj:`str`): The name of the agent to set the control scheme for.
            control_scheme (:obj:`int`): A control scheme value
                (see :class:`~holodeck.agents.ControlSchemes`)
        """
        if agent_name not in self.agents:
            print("No such agent %s" % agent_name)
        else:
            self.agents[agent_name].set_control_scheme(control_scheme)

    def set_sensor_enabled(self, agent_name, sensor_name, enabled):
        """Enable or disable an agent's sensor.

        Args:
            agent_name (:obj:`str`): The name of the agent whose sensor will be switched
            sensor_name (:obj:`str`): The name of the sensor to be switched
            enabled (:obj:`bool`): Boolean representing whether to enable or disable the sensor
        """
        if agent_name not in self._sensor_map:
            print("No such agent %s" % agent_name)
        else:
            command_to_send = SetSensorEnabledCommand(agent_name, sensor_name, enabled)
            self._enqueue_command(command_to_send)

    def send_world_command(self, name, num_params=None, string_params=None):
        """Send a custom command.

        A custom command sends an abitrary command that may only exist in a specific world or
        package. It is given a name and any amount of string and number parameters that allow it to
        alter the state of the world.

        Args:
            name (:obj:`str`): The name of the command, ex "OpenDoor"
            num_params (obj:`list` of :obj:`int`): List of arbitrary number parameters
            string_params (obj:`list` of :obj:`int`): List of arbitrary string parameters
        """
        num_params = [] if num_params is None else num_params
        string_params = [] if string_params is None else string_params

        command_to_send = CustomCommand(name, num_params, string_params)
        self._enqueue_command(command_to_send)

    def __linux_start_process__(self, binary_path, task_key, gl_version, verbose,
                                show_viewport=True):
        import posix_ipc
        out_stream = sys.stdout if verbose else open(os.devnull, 'w')
        loading_semaphore = \
            posix_ipc.Semaphore('/HOLODECK_LOADING_SEM' + self._uuid, os.O_CREAT | os.O_EXCL,
                                initial_value=0)
        # Copy the environment variables and re,pve the DISPLAY variable to hide viewport
        # https://answers.unrealengine.com/questions/815764/in-the-release-notes-it-says-the-engine-can-now-cr.html?sort=oldest
        environment = dict(os.environ.copy())
        if not show_viewport:
            del environment['DISPLAY']
        self._world_process = \
            subprocess.Popen([binary_path, task_key, '-HolodeckOn', '-opengl' + str(gl_version),
                              '-LOG=HolodeckLog.txt', '-ResX=' + str(self._window_size[1]),
                              '-ResY=' + str(self._window_size[0]), '--HolodeckUUID=' + self._uuid,
                              '-TicksPerSec=' + str(self._ticks_per_sec)],
                             stdout=out_stream,
                             stderr=out_stream,
                             env=environment)

        atexit.register(self.__on_exit__)

        try:
            loading_semaphore.acquire(10)
        except posix_ipc.BusyError:
            raise HolodeckException("Timed out waiting for binary to load. Ensure that holodeck is "
                                    "not being run with root priveleges.")
        loading_semaphore.unlink()

    def __windows_start_process__(self, binary_path, task_key, verbose):
        import win32event
        out_stream = sys.stdout if verbose else open(os.devnull, 'w')
        loading_semaphore = win32event.CreateSemaphore(None, 0, 1,
                                                       'Global\\HOLODECK_LOADING_SEM' + self._uuid)
        self._world_process = \
            subprocess.Popen([binary_path, task_key, '-HolodeckOn', '-LOG=HolodeckLog.txt',
                              '-ResX=' + str(self._window_size[1]), '-ResY=' +
                              str(self._window_size[0]), '-TicksPerSec=' + str(self._ticks_per_sec),
                              '--HolodeckUUID=' + self._uuid],
                             stdout=out_stream, stderr=out_stream)

        atexit.register(self.__on_exit__)
        response = win32event.WaitForSingleObject(loading_semaphore, 100000)  # 100 second timeout
        if response == win32event.WAIT_TIMEOUT:
            raise HolodeckException("Timed out waiting for binary to load")

    def __on_exit__(self):
        if hasattr(self, '_exited'):
            return

        self._client.unlink()
        if hasattr(self, '_world_process'):
            self._world_process.kill()
            self._world_process.wait(5)

        self._exited = True

    # Context manager APIs, allows `with` statement to be used
    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        # TODO: Surpress exceptions?
        self.__on_exit__()

    def _get_single_state(self):

        if self._agent is not None:
            return self._create_copy(self._state_dict[self._agent.name]) if self._copy_state \
                else self._state_dict[self._agent.name]

        return self._get_full_state()

    def _get_full_state(self):
        return self._create_copy(self._state_dict) if self._copy_state else self._state_dict

    def _get_reward_terminal(self):
        reward = None
        terminal = None
        if self._agent is not None:
            for sensor in self._state_dict[self._agent.name]:
                if "Task" in sensor:
                    reward = self._state_dict[self._agent.name][sensor][0]
                    terminal = self._state_dict[self._agent.name][sensor][1] == 1
        return reward, terminal

    def _create_copy(self, obj):
        if isinstance(obj, dict):  # Deep copy dictionary
            copy = dict()
            for k, v in obj.items():
                if isinstance(v, dict):
                    copy[k] = self._create_copy(v)
                else:
                    copy[k] = np.copy(v)
            return copy
        return None  # Not implemented for other types
Пример #5
0
    def __init__(self,
                 agent_definitions=None,
                 binary_path=None,
                 window_size=None,
                 start_world=True,
                 uuid="",
                 gl_version=4,
                 verbose=False,
                 pre_start_steps=2,
                 show_viewport=True,
                 ticks_per_sec=30,
                 copy_state=True,
                 scenario=None):

        if agent_definitions is None:
            agent_definitions = []

        # Initialize variables

        if window_size is None:
            # Check if it has been configured in the scenario
            if scenario is not None and "window_height" in scenario:
                self._window_size = scenario["window_height"], scenario[
                    "window_width"]
            else:
                # Default resolution
                self._window_size = 720, 1280
        else:
            self._window_size = window_size

        self._uuid = uuid
        self._pre_start_steps = pre_start_steps
        self._copy_state = copy_state
        self._ticks_per_sec = ticks_per_sec
        self._scenario = scenario
        self._initial_agent_defs = agent_definitions
        self._spawned_agent_defs = []

        # Start world based on OS
        if start_world:
            world_key = self._scenario["world"]
            if os.name == "posix":
                self.__linux_start_process__(binary_path,
                                             world_key,
                                             gl_version,
                                             verbose=verbose,
                                             show_viewport=show_viewport)
            elif os.name == "nt":
                self.__windows_start_process__(binary_path,
                                               world_key,
                                               verbose=verbose)
            else:
                raise HolodeckException("Unknown platform: " + os.name)

        # Initialize Client
        self._client = HolodeckClient(self._uuid, start_world)
        self._command_center = CommandCenter(self._client)
        self._client.command_center = self._command_center
        self._reset_ptr = self._client.malloc("RESET", [1], np.bool)
        self._reset_ptr[0] = False

        # Initialize environment controller
        self.weather = WeatherController(self.send_world_command)

        # Set up agents already in the world
        self.agents = dict()
        self._state_dict = dict()
        self._agent = None

        # Set the default state function
        self.num_agents = len(self.agents)

        if self.num_agents == 1:
            self._default_state_fn = self._get_single_state
        else:
            self._default_state_fn = self._get_full_state

        self._client.acquire()

        if os.name == "posix" and show_viewport == False:
            self.should_render_viewport(False)

        # Flag indicates if the user has called .reset() before .tick() and .step()
        self._initial_reset = False
        self.reset()
Пример #6
0
class HolodeckEnvironment:
    """Proxy for communicating with a Holodeck world

    Instantiate this object using :meth:`holodeck.holodeck.make`.

    Args:
        agent_definitions (:obj:`list` of :class:`AgentDefinition`):
            Which agents are already in the environment

        binary_path (:obj:`str`, optional):
            The path to the binary to load the world from. Defaults to None.

        window_size ((:obj:`int`,:obj:`int`)):
            height, width of the window to open

        start_world (:obj:`bool`, optional):
            Whether to load a binary or not. Defaults to True.

        uuid (:obj:`str`):
            A unique identifier, used when running multiple instances of holodeck. Defaults to "".

        gl_version (:obj:`int`, optional):
            The version of OpenGL to use for Linux. Defaults to 4.

        verbose (:obj:`bool`):
            If engine log output should be printed to stdout

        pre_start_steps (:obj:`int`):
            Number of ticks to call after initializing the world, allows the level to load and settle.

        show_viewport (:obj:`bool`, optional):
            If the viewport should be shown (Linux only) Defaults to True.

        ticks_per_sec (:obj:`int`, optional):
            Number of frame ticks per unreal second. Defaults to 30.

        copy_state (:obj:`bool`, optional):
            If the state should be copied or returned as a reference. Defaults to True.

        scenario (:obj:`dict`):
            The scenario that is to be loaded. See :ref:`scenario-files` for the schema.

    """
    def __init__(self,
                 agent_definitions=None,
                 binary_path=None,
                 window_size=None,
                 start_world=True,
                 uuid="",
                 gl_version=4,
                 verbose=False,
                 pre_start_steps=2,
                 show_viewport=True,
                 ticks_per_sec=30,
                 copy_state=True,
                 scenario=None):

        if agent_definitions is None:
            agent_definitions = []

        # Initialize variables

        if window_size is None:
            # Check if it has been configured in the scenario
            if scenario is not None and "window_height" in scenario:
                self._window_size = scenario["window_height"], scenario[
                    "window_width"]
            else:
                # Default resolution
                self._window_size = 720, 1280
        else:
            self._window_size = window_size

        self._uuid = uuid
        self._pre_start_steps = pre_start_steps
        self._copy_state = copy_state
        self._ticks_per_sec = ticks_per_sec
        self._scenario = scenario
        self._initial_agent_defs = agent_definitions
        self._spawned_agent_defs = []

        # Start world based on OS
        if start_world:
            world_key = self._scenario["world"]
            if os.name == "posix":
                self.__linux_start_process__(binary_path,
                                             world_key,
                                             gl_version,
                                             verbose=verbose,
                                             show_viewport=show_viewport)
            elif os.name == "nt":
                self.__windows_start_process__(binary_path,
                                               world_key,
                                               verbose=verbose)
            else:
                raise HolodeckException("Unknown platform: " + os.name)

        # Initialize Client
        self._client = HolodeckClient(self._uuid, start_world)
        self._command_center = CommandCenter(self._client)
        self._client.command_center = self._command_center
        self._reset_ptr = self._client.malloc("RESET", [1], np.bool)
        self._reset_ptr[0] = False

        # Initialize environment controller
        self.weather = WeatherController(self.send_world_command)

        # Set up agents already in the world
        self.agents = dict()
        self._state_dict = dict()
        self._agent = None

        # Set the default state function
        self.num_agents = len(self.agents)

        if self.num_agents == 1:
            self._default_state_fn = self._get_single_state
        else:
            self._default_state_fn = self._get_full_state

        self._client.acquire()

        if os.name == "posix" and show_viewport == False:
            self.should_render_viewport(False)

        # Flag indicates if the user has called .reset() before .tick() and .step()
        self._initial_reset = False
        self.reset()

    @property
    def action_space(self):
        """Gives the action space for the main agent.

        Returns:
            :class:`~holodeck.spaces.ActionSpace`: The action space for the main agent.
        """
        return self._agent.action_space

    def info(self):
        """Returns a string with specific information about the environment.
        This information includes which agents are in the environment and which sensors they have.

        Returns:
            :obj:`str`: Information in a string format.
        """
        result = list()
        result.append("Agents:\n")
        for agent_name in self.agents:
            agent = self.agents[agent_name]
            result.append("\tName: ")
            result.append(agent.name)
            result.append("\n\tType: ")
            result.append(type(agent).__name__)
            result.append("\n\t")
            result.append("Sensors:\n")
            for _, sensor in agent.sensors.items():
                result.append("\t\t")
                result.append(sensor.name)
                result.append("\n")
        return "".join(result)

    def _load_scenario(self):
        """Loads the scenario defined in self._scenario_key.

        Instantiates agents, sensors, and weather.

        If no scenario is defined, does nothing.
        """
        if self._scenario is None:
            return

        for agent in self._scenario['agents']:
            sensors = []
            for sensor in agent['sensors']:
                if 'sensor_type' not in sensor:
                    raise HolodeckException(
                        "Sensor for agent {} is missing required key "
                        "'sensor_type'".format(agent['agent_name']))

                # Default values for a sensor
                sensor_config = {
                    'location': [0, 0, 0],
                    'rotation': [0, 0, 0],
                    'socket': "",
                    'configuration': None,
                    'sensor_name': sensor['sensor_type'],
                    'existing': False
                }
                # Overwrite the default values with what is defined in the scenario config
                sensor_config.update(sensor)

                sensors.append(
                    SensorDefinition(agent['agent_name'],
                                     agent['agent_type'],
                                     sensor_config['sensor_name'],
                                     sensor_config['sensor_type'],
                                     socket=sensor_config['socket'],
                                     location=sensor_config['location'],
                                     rotation=sensor_config['rotation'],
                                     config=sensor_config['configuration']))
            # Default values for an agent
            agent_config = {
                'location': [0, 0, 0],
                'rotation': [0, 0, 0],
                'agent_name': agent['agent_type'],
                'existing': False,
                "location_randomization": [0, 0, 0],
                "rotation_randomization": [0, 0, 0]
            }

            agent_config.update(agent)
            is_main_agent = False

            if "main_agent" in self._scenario:
                is_main_agent = self._scenario["main_agent"] == agent[
                    "agent_name"]

            agent_location = agent_config["location"]
            agent_rotation = agent_config["rotation"]

            # Randomize the agent start location
            dx = agent_config["location_randomization"][0]
            dy = agent_config["location_randomization"][1]
            dz = agent_config["location_randomization"][2]

            agent_location[0] += random.uniform(-dx, dx)
            agent_location[1] += random.uniform(-dy, dy)
            agent_location[2] += random.uniform(-dz, dz)

            # Randomize the agent rotation
            d_pitch = agent_config["rotation_randomization"][0]
            d_roll = agent_config["rotation_randomization"][1]
            d_yaw = agent_config["rotation_randomization"][1]

            agent_rotation[0] += random.uniform(-d_pitch, d_pitch)
            agent_rotation[1] += random.uniform(-d_roll, d_roll)
            agent_rotation[2] += random.uniform(-d_yaw, d_yaw)

            agent_def = AgentDefinition(agent_config['agent_name'],
                                        agent_config['agent_type'],
                                        starting_loc=agent_location,
                                        starting_rot=agent_rotation,
                                        sensors=sensors,
                                        existing=agent_config["existing"],
                                        is_main_agent=is_main_agent)

            self.add_agent(agent_def, is_main_agent)
            self.agents[agent['agent_name']].set_control_scheme(
                agent['control_scheme'])
            self._spawned_agent_defs.append(agent_def)

        if "weather" in self._scenario:
            weather = self._scenario["weather"]
            if "hour" in weather:
                self.weather.set_day_time(weather["hour"])
            if "type" in weather:
                self.weather.set_weather(weather["type"])
            if "fog_density" in weather:
                self.weather.set_fog_density(weather["fog_density"])
            if "day_cycle_length" in weather:
                day_cycle_length = weather["day_cycle_length"]
                self.weather.start_day_cycle(day_cycle_length)

    def reset(self):
        """Resets the environment, and returns the state.
        If it is a single agent environment, it returns that state for that agent. Otherwise, it
        returns a dict from agent name to state.

        Returns (tuple or dict):
            For single agent environment, returns the same as `step`.

            For multi-agent environment, returns the same as `tick`.
        """
        # Reset level
        self._initial_reset = True
        self._reset_ptr[0] = True
        for agent in self.agents.values():
            agent.clear_action()
        self.tick(
        )  # Must tick once to send reset before sending spawning commands
        self.tick(
        )  # Bad fix to potential race condition. See issue BYU-PCCL/holodeck#224
        self.tick()
        # Clear command queue
        if self._command_center.queue_size > 0:
            print(
                "Warning: Reset called before all commands could be sent. Discarding",
                self._command_center.queue_size, "commands.")
        self._command_center.clear()

        # Load agents
        self._spawned_agent_defs = []
        self.agents = dict()
        self._state_dict = dict()
        for agent_def in self._initial_agent_defs:
            self.add_agent(agent_def, agent_def.is_main_agent)

        self._load_scenario()

        self.num_agents = len(self.agents)

        if self.num_agents == 1:
            self._default_state_fn = self._get_single_state
        else:
            self._default_state_fn = self._get_full_state

        for _ in range(self._pre_start_steps + 1):
            self.tick()

        return self._default_state_fn()

    def step(self, action, ticks=1):
        """Supplies an action to the main agent and tells the environment to tick once.
        Primary mode of interaction for single agent environments.

        Args:
            action (:obj:`np.ndarray`): An action for the main agent to carry out on the next tick.
            ticks (:obj:`int`): Number of times to step the environment wiht this action.
                If ticks > 1, this function returns the last state generated.

        Returns:
            (:obj:`dict`, :obj:`float`, :obj:`bool`, info): A 4tuple:
                - State: Dictionary from sensor enum
                    (see :class:`~holodeck.sensors.HolodeckSensor`) to :obj:`np.ndarray`.
                - Reward (:obj:`float`): Reward returned by the environment.
                - Terminal: The bool terminal signal returned by the environment.
                - Info: Any additional info, depending on the world. Defaults to None.
        """
        if not self._initial_reset:
            raise HolodeckException("You must call .reset() before .step()")

        for _ in range(ticks):
            if self._agent is not None:
                self._agent.act(action)

            self._command_center.handle_buffer()
            self._client.release()
            self._client.acquire()

            reward, terminal = self._get_reward_terminal()
            last_state = self._default_state_fn(), reward, terminal, None

        return last_state

    def act(self, agent_name, action):
        """Supplies an action to a particular agent, but doesn't tick the environment.
           Primary mode of interaction for multi-agent environments. After all agent commands are
           supplied, they can be applied with a call to `tick`.

        Args:
            agent_name (:obj:`str`): The name of the agent to supply an action for.
            action (:obj:`np.ndarray` or :obj:`list`): The action to apply to the agent. This
                action will be applied every time `tick` is called, until a new action is supplied
                with another call to act.
        """
        self.agents[agent_name].act(action)

    def get_joint_constraints(self, agent_name, joint_name):
        """Returns the corresponding swing1, swing2 and twist limit values for the
                specified agent and joint. Will return None if the joint does not exist for the agent.

        Returns:
            (:obj )
        """
        return self.agents[agent_name].get_joint_constraints(joint_name)

    def tick(self, num_ticks=1):
        """Ticks the environment once. Normally used for multi-agent environments.
        Args:
            num_ticks (:obj:`int`): Number of ticks to perform. Defaults to 1. 
        Returns:
            :obj:`dict`: A dictionary from agent name to its full state. The full state is another
                dictionary from :obj:`holodeck.sensors.Sensors` enum to np.ndarray, containing the
                sensors information for each sensor. The sensors always include the reward and
                terminal sensors.

                Will return the state from the last tick executed.
        """
        if not self._initial_reset:
            raise HolodeckException("You must call .reset() before .tick()")

        for _ in range(num_ticks):
            self._command_center.handle_buffer()

            self._client.release()
            self._client.acquire()
            state = self._default_state_fn()

        return state

    def _enqueue_command(self, command_to_send):
        self._command_center.enqueue_command(command_to_send)

    def add_agent(self, agent_def, is_main_agent=False):
        """Add an agent in the world.

        It will be spawn when :meth:`tick` or :meth:`step` is called next.

        The agent won't be able to be used until the next frame.

        Args:
            agent_def (:class:`~holodeck.agents.AgentDefinition`): The definition of the agent to
            spawn.
        """
        if agent_def.name in self.agents:
            raise HolodeckException("Error. Duplicate agent name. ")

        self.agents[agent_def.name] = AgentFactory.build_agent(
            self._client, agent_def)
        self._state_dict[agent_def.name] = self.agents[
            agent_def.name].agent_state_dict

        if not agent_def.existing:
            command_to_send = SpawnAgentCommand(
                location=agent_def.starting_loc,
                rotation=agent_def.starting_rot,
                name=agent_def.name,
                agent_type=agent_def.type.agent_type,
                is_main_agent=agent_def.is_main_agent)

            self._client.command_center.enqueue_command(command_to_send)
        self.agents[agent_def.name].add_sensors(agent_def.sensors)
        if is_main_agent:
            self._agent = self.agents[agent_def.name]

    def spawn_prop(self,
                   prop_type,
                   location=None,
                   rotation=None,
                   scale=1,
                   sim_physics=False,
                   material="",
                   tag=""):
        """Spawns a basic prop object in the world like a box or sphere. 
        
        Prop will not persist after environment reset.

        Args:
            prop_type (:obj:`string`):
                The type of prop to spawn. Can be ``box``, ``sphere``, ``cylinder``, or ``cone``.

            location (:obj:`list` of :obj:`float`):
                The ``[x, y, z]`` location of the prop.

            rotation (:obj:`list` of :obj:`float`):
                The ``[roll, pitch, yaw]`` rotation of the prop.

            scale (:obj:`list` of :obj:`float`) or (:obj:`float`):
                The ``[x, y, z]`` scalars to the prop size, where the default size is 1 meter.
                If given a single float value, then every dimension will be scaled to that value.

            sim_physics (:obj:`boolean`):
                Whether the object is mobile and is affected by gravity.

            material (:obj:`string`):
                The type of material (texture) to apply to the prop. Can be ``white``, ``gold``,
                ``cobblestone``, ``brick``, ``wood``, ``grass``, ``steel``, or ``black``. If left
                empty, the prop will have the a simple checkered gray material.

            tag (:obj:`string`):
                The tag to apply to the prop. Useful for task references, like the
                :ref:`location-task`.
        """
        location = [0, 0, 0] if location is None else location
        rotation = [0, 0, 0] if rotation is None else rotation
        # if the given scale is an single value, then scale every dimension to that value
        if not isinstance(scale, list):
            scale = [scale, scale, scale]
        sim_physics = 1 if sim_physics else 0

        prop_type = prop_type.lower()
        material = material.lower()

        available_props = ["box", "sphere", "cylinder", "cone"]
        available_materials = [
            "white", "gold", "cobblestone", "brick", "wood", "grass", "steel",
            "black"
        ]

        if prop_type not in available_props:
            raise HolodeckException(
                "{} not an available prop. Available prop types: {}".format(
                    prop_type, available_props))
        if material not in available_materials and material is not "":
            raise HolodeckException(
                "{} not an available material. Available material types: {}".
                format(material, available_materials))

        self.send_world_command(
            "SpawnProp",
            num_params=[location, rotation, scale, sim_physics],
            string_params=[prop_type, material, tag])

    def draw_line(self, start, end, color=None, thickness=10.0):
        """Draws a debug line in the world

        Args:
            start (:obj:`list` of :obj:`float`): The start ``[x, y, z]`` location of the line.
                (see :ref:`coordinate-system`)
            end (:obj:`list` of :obj:`float`): The end ``[x, y, z]`` location of the line
            color (:obj:`list``): ``[r, g, b]`` color value
            thickness (:obj:`float`): thickness of the line
        """
        color = [255, 0, 0] if color is None else color
        command_to_send = DebugDrawCommand(0, start, end, color, thickness)
        self._enqueue_command(command_to_send)

    def draw_arrow(self, start, end, color=None, thickness=10.0):
        """Draws a debug arrow in the world

        Args:
            start (:obj:`list` of :obj:`float`): The start ``[x, y, z]`` location of the line.
                (see :ref:`coordinate-system`)
            end (:obj:`list` of :obj:`float`): The end ``[x, y, z]`` location of the arrow
            color (:obj:`list`): ``[r, g, b]`` color value
            thickness (:obj:`float`): thickness of the arrow
        """
        color = [255, 0, 0] if color is None else color
        command_to_send = DebugDrawCommand(1, start, end, color, thickness)
        self._enqueue_command(command_to_send)

    def draw_box(self, center, extent, color=None, thickness=10.0):
        """Draws a debug box in the world

        Args:
            center (:obj:`list` of :obj:`float`): The start ``[x, y, z]`` location of the box.
                (see :ref:`coordinate-system`)
            extent (:obj:`list` of :obj:`float`): The ``[x, y, z]`` extent of the box
            color (:obj:`list`): ``[r, g, b]`` color value
            thickness (:obj:`float`): thickness of the lines
        """
        color = [255, 0, 0] if color is None else color
        command_to_send = DebugDrawCommand(2, center, extent, color, thickness)
        self._enqueue_command(command_to_send)

    def draw_point(self, loc, color=None, thickness=10.0):
        """Draws a debug point in the world

        Args:
            loc (:obj:`list` of :obj:`float`): The ``[x, y, z]`` start of the box. 
                (see :ref:`coordinate-system`)
            color (:obj:`list` of :obj:`float`): ``[r, g, b]`` color value
            thickness (:obj:`float`): thickness of the point
        """
        color = [255, 0, 0] if color is None else color
        command_to_send = DebugDrawCommand(3, loc, [0, 0, 0], color, thickness)
        self._enqueue_command(command_to_send)

    def move_viewport(self, location, rotation):
        """Teleport the camera to the given location

        By the next tick, the camera's location and rotation will be updated

        Args:
            location (:obj:`list` of :obj:`float`): The ``[x, y, z]`` location to give the camera
                (see :ref:`coordinate-system`)
            rotation (:obj:`list` of :obj:`float`): The ``[roll, pitch, yaw]`` rotation to give the camera
                (see :ref:`rotations`)

        """
        # test_viewport_capture_after_teleport
        self._enqueue_command(TeleportCameraCommand(location, rotation))

    def should_render_viewport(self, render_viewport):
        """Controls whether the viewport is rendered or not

        Args:
            render_viewport (:obj:`boolean`): If the viewport should be rendered
        """
        self._enqueue_command(RenderViewportCommand(render_viewport))

    def set_render_quality(self, render_quality):
        """Adjusts the rendering quality of Holodeck.
        
        Args:
            render_quality (:obj:`int`): An integer between 0 = Low Quality and 3 = Epic quality.
        """
        self._enqueue_command(RenderQualityCommand(render_quality))

    def set_control_scheme(self, agent_name, control_scheme):
        """Set the control scheme for a specific agent.

        Args:
            agent_name (:obj:`str`): The name of the agent to set the control scheme for.
            control_scheme (:obj:`int`): A control scheme value
                (see :class:`~holodeck.agents.ControlSchemes`)
        """
        if agent_name not in self.agents:
            print("No such agent %s" % agent_name)
        else:
            self.agents[agent_name].set_control_scheme(control_scheme)

    def send_world_command(self, name, num_params=None, string_params=None):
        """Send a world command.

        A world command sends an abitrary command that may only exist in a specific world or
        package. It is given a name and any amount of string and number parameters that allow it to
        alter the state of the world.
        
        If a command is sent that does not exist in the world, the environment will exit.

        Args:
            name (:obj:`str`): The name of the command, ex "OpenDoor"
            num_params (obj:`list` of :obj:`int`): List of arbitrary number parameters
            string_params (obj:`list` of :obj:`string`): List of arbitrary string parameters
        """
        num_params = [] if num_params is None else num_params
        string_params = [] if string_params is None else string_params

        command_to_send = CustomCommand(name, num_params, string_params)
        self._enqueue_command(command_to_send)

    def __linux_start_process__(self,
                                binary_path,
                                task_key,
                                gl_version,
                                verbose,
                                show_viewport=True):
        import posix_ipc
        out_stream = sys.stdout if verbose else open(os.devnull, 'w')
        loading_semaphore = \
            posix_ipc.Semaphore('/HOLODECK_LOADING_SEM' + self._uuid, os.O_CREAT | os.O_EXCL,
                                initial_value=0)
        # Copy the environment variables and remove the DISPLAY variable to hide viewport
        # https://answers.unrealengine.com/questions/815764/in-the-release-notes-it-says-the-engine-can-now-cr.html?sort=oldest
        environment = dict(os.environ.copy())
        if not show_viewport and 'DISPLAY' in environment:
            del environment['DISPLAY']
        self._world_process = \
            subprocess.Popen([binary_path, task_key, '-HolodeckOn', '-opengl' + str(gl_version),
                              '-LOG=HolodeckLog.txt', '-ForceRes', '-ResX=' + str(self._window_size[1]),
                              '-ResY=' + str(self._window_size[0]), '--HolodeckUUID=' + self._uuid,
                              '-TicksPerSec=' + str(self._ticks_per_sec)],
                             stdout=out_stream,
                             stderr=out_stream,
                             env=environment)

        atexit.register(self.__on_exit__)

        try:
            loading_semaphore.acquire(10)
        except posix_ipc.BusyError:
            raise HolodeckException(
                "Timed out waiting for binary to load. Ensure that holodeck is "
                "not being run with root priveleges.")
        loading_semaphore.unlink()

    def __windows_start_process__(self, binary_path, task_key, verbose):
        import win32event
        out_stream = sys.stdout if verbose else open(os.devnull, 'w')
        loading_semaphore = win32event.CreateSemaphore(
            None, 0, 1, 'Global\\HOLODECK_LOADING_SEM' + self._uuid)
        self._world_process = \
            subprocess.Popen([binary_path, task_key, '-HolodeckOn', '-LOG=HolodeckLog.txt',
                              '-ForceRes', '-ResX=' + str(self._window_size[1]), '-ResY=' +
                              str(self._window_size[0]), '-TicksPerSec=' + str(self._ticks_per_sec),
                              '--HolodeckUUID=' + self._uuid],
                             stdout=out_stream, stderr=out_stream)

        atexit.register(self.__on_exit__)
        response = win32event.WaitForSingleObject(loading_semaphore,
                                                  100000)  # 100 second timeout
        if response == win32event.WAIT_TIMEOUT:
            raise HolodeckException("Timed out waiting for binary to load")

    def __on_exit__(self):
        if hasattr(self, '_exited'):
            return

        self._client.unlink()
        if hasattr(self, '_world_process'):
            self._world_process.kill()
            self._world_process.wait(5)

        self._exited = True

    # Context manager APIs, allows `with` statement to be used
    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        # TODO: Surpress exceptions?
        self.__on_exit__()

    def _get_single_state(self):

        if self._agent is not None:
            return self._create_copy(self._state_dict[self._agent.name]) if self._copy_state \
                else self._state_dict[self._agent.name]

        return self._get_full_state()

    def _get_full_state(self):
        return self._create_copy(
            self._state_dict) if self._copy_state else self._state_dict

    def _get_reward_terminal(self):
        reward = None
        terminal = None
        if self._agent is not None:
            for sensor in self._state_dict[self._agent.name]:
                if "Task" in sensor:
                    reward = self._state_dict[self._agent.name][sensor][0]
                    terminal = self._state_dict[
                        self._agent.name][sensor][1] == 1
        return reward, terminal

    def _create_copy(self, obj):
        if isinstance(obj, dict):  # Deep copy dictionary
            copy = dict()
            for k, v in obj.items():
                if isinstance(v, dict):
                    copy[k] = self._create_copy(v)
                else:
                    copy[k] = np.copy(v)
            return copy
        return None  # Not implemented for other types