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()
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()
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
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
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()
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