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 __init__(self, name, shape, dtype=np.float32, uuid=""): self.shape = shape self.dtype = dtype size = reduce(lambda x, y: x * y, shape) size_bytes = np.dtype(dtype).itemsize * size self._mem_path = None self._mem_pointer = None if os.name == "nt": self._mem_path = "/HOLODECK_MEM" + uuid + "_" + name self._mem_pointer = mmap.mmap(0, size_bytes, self._mem_path) elif os.name == "posix": self._mem_path = "/dev/shm/HOLODECK_MEM" + uuid + "_" + name f = os.open(self._mem_path, os.O_CREAT | os.O_TRUNC | os.O_RDWR) self._mem_file = f os.ftruncate(f, size_bytes) os.fsync(f) # TODO - I think we are leaking a file object here. Unfortunately, we # can't just .close() it since numpy acquires a reference to it # below and I can't find a way to release it in __linux_unlink__() self._mem_pointer = mmap.mmap(f, size_bytes) else: raise HolodeckException("Currently unsupported os: " + os.name) self.np_array = np.ndarray(shape, dtype=dtype) self.np_array.data = (Shmem._numpy_to_ctype[dtype] * size).from_buffer(self._mem_pointer)
def world_info(world_name, world_config=None, base_indent=0): """Gets and prints the information of a world. Args: world_name (:obj:`str`): the name of the world to retrieve information for world_config (:obj:`dict`, optional): A dictionary containing the world's configuration. Will find the config if None. Defaults to None. base_indent (:obj:`int`, optional): How much to indent output """ if world_config is None: for config, _ in _iter_packages(): for world in config["worlds"]: if world["name"] == world_name: world_config = world if world_config is None: raise HolodeckException("Couldn't find world " + world_name) print(base_indent * ' ', world_config["name"]) base_indent += 4 if "agents" in world_config: _print_agent_info(world_config["agents"], base_indent) print(base_indent * ' ', "Scenarios:") for scenario, _ in _iter_scenarios(world_name): scenario_info(scenario=scenario, base_indent=base_indent + 2)
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 world_info(world_name, world_config=None, initial_indent="", next_indent=" "): """Gets and prints the information of a world. Args: world_name (str): the name of the world to retrieve information for world_config (dict optional): A dictionary containing the world's configuration. Will find the config if None. Defaults to None. initial_indent (str optional): This indent will apply to each output line. Defaults to "". next_indent (str optional): This indent will be applied within each nested line. Defaults to " ". """ if world_config is None: for config, _ in _iter_packages(): for world in config["maps"]: if world["name"] == world_name: world_config = world if world_config is None: raise HolodeckException("Couldn't find world " + world_name) second_indent = initial_indent + next_indent agent_indent = second_indent + next_indent sensor_indent = agent_indent + next_indent print(initial_indent, world_config["name"]) print(second_indent, "Resolution:", world_config["window_width"], "x", world_config["window_height"]) print(second_indent, "Agents:") for agent in world_config["agents"]: print(agent_indent, "Name:", agent["agent_name"]) print(agent_indent, "Type:", agent["agent_type"]) print(agent_indent, "Sensors:") for sensor in agent["sensors"]: print(sensor_indent, sensor)
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 __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 unlink(self): """unlinks the shared memory""" if os.name == "posix": self.__linux_unlink__() elif os.name == "nt": self.__windows_unlink__() else: raise HolodeckException("Currently unsupported os: " + os.name)
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()
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 set_location(self, location): """Set where agent will be spawned. Args: location (:obj:`list` of :obj:`float`): ``[x, y, z]`` location to spawn agent (see :ref:`coordinate-system`) """ if len(location) != 3: raise HolodeckException( "Invalid location given to spawn agent command") self.add_number_parameters(location)
def remove(package_name): """Removes a holodeck package. Args: package_name (str): the name of the package to remove """ if package_name not in packages: raise HolodeckException("Unknown package name " + package_name) for config, path in _iter_packages(): if config["name"] == package_name: shutil.rmtree(path)
def set_rotation(self, rotation): """Set where agent will be spawned. Args: rotation (:obj:`list` of :obj:`float`): ``[roll, pitch, yaw]`` rotation for agent. (see :ref:`rotations`) """ if len(rotation) != 3: raise HolodeckException( "Invalid rotation given to spawn agent command") self.add_number_parameters(rotation)
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 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 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_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 _write_to_command_buffer(self, to_write): """Write input to the command buffer. Reformat input string to the correct format. Args: to_write (:class:`str`): The string to write to the command 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) if len(input_bytes) > self.max_buffer: raise HolodeckException( "Error: Command length exceeds buffer size") for index, val in enumerate(input_bytes): self._command_buffer_ptr[index] = val
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 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 install(package_name): """Installs a holodeck package. Args: package_name (str): The name of the package to install """ holodeck_path = util.get_holodeck_path() binary_website = "https://s3.amazonaws.com/holodeckworlds/" if package_name not in packages: raise HolodeckException("Unknown package name " + package_name) package_url = packages[package_name] print("Installing " + package_name + " at " + holodeck_path) install_path = os.path.join(holodeck_path, "worlds") binary_url = binary_website + util.get_os_key() + "_" + package_url _download_binary(binary_url, install_path) if os.name == "posix": _make_binary_excecutable(package_name, install_path)
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 get_package_config_for_scenario(scenario): """For the given scenario, returns the package config associated with it (config.json) Args: scenario (:obj:`dict`): scenario dict to look up the package for Returns: :obj:`dict`: package configuration dictionary """ world_name = scenario["world"] for config, path in _iter_packages(): for world in config["worlds"]: if world["name"] == world_name: return config raise HolodeckException( "Could not find a package that contains world {}".format(world_name))
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 __init__(self, uuid=""): self._uuid = uuid # Important functions self._get_semaphore_fn = None self._release_semaphore_fn = None self._semaphore1 = None self._semaphore2 = None self.unlink = None self._memory = dict() self._sensors = dict() self._agents = dict() self._settings = dict() if os.name == "nt": self.__windows_init__() elif os.name == "posix": self.__posix_init__() else: raise HolodeckException("Currently unsupported os: " + os.name)
def __init__(self, name, shape, dtype=np.float32, uuid=""): self.shape = shape self.dtype = dtype size = reduce(lambda x, y: x * y, shape) size_bytes = np.dtype(dtype).itemsize * size self._mem_path = None self._mem_pointer = None if os.name == "nt": self._mem_path = "/HOLODECK_MEM" + uuid + "_" + name self._mem_pointer = mmap.mmap(0, size_bytes, self._mem_path) elif os.name == "posix": self._mem_path = "/dev/shm/HOLODECK_MEM" + uuid + "_" + name f = os.open(self._mem_path, os.O_CREAT | os.O_TRUNC | os.O_RDWR) os.ftruncate(f, size_bytes) self._mem_pointer = mmap.mmap(f, size_bytes) else: raise HolodeckException("Currently unsupported os: " + os.name) self.np_array = np.ndarray(shape, dtype=dtype) self.np_array.data = (Shmem._numpy_to_ctype[dtype] * size).from_buffer( self._mem_pointer)
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 make(world_name, gl_version=GL_VERSION.OPENGL4, window_res=None, cam_res=None, verbose=False): """Creates a holodeck environment using the supplied world name. Args: world_name (str): The name of the world to load as an environment. Must match the name of a world in an installed package. gl_version (int, optional): The OpenGL version to use (Linux only). Defaults to GL_VERSION.OPENGL4. window_res ((int, int), optional): The resolution to load the game window at. Defaults to (512, 512). cam_res ((int, int), optional): The resolution to load the pixel camera sensors at. Defaults to (256, 256). verbose (bool): Whether to run in verbose mode. Defaults to False. Returns: HolodeckEnvironment: A holodeck environment instantiated with all the settings necessary for the specified world, and other supplied arguments. """ holodeck_worlds = _get_worlds_map() if world_name not in holodeck_worlds: raise HolodeckException("Invalid World Name") param_dict = copy(holodeck_worlds[world_name]) param_dict["start_world"] = True param_dict["uuid"] = str(uuid.uuid4()) param_dict["gl_version"] = gl_version param_dict["verbose"] = verbose if window_res is not None: param_dict["window_width"] = window_res[0] param_dict["window_height"] = window_res[1] if cam_res is not None: param_dict["camera_width"] = cam_res[0] param_dict["camera_height"] = cam_res[1] return HolodeckEnvironment(**param_dict)
def install(package_name, url=None): """Installs a holodeck package. Args: package_name (:obj:`str`): The name of the package to install """ if package_name is None and url is None: raise HolodeckException( "You must specify the URL or a valid package name") _check_for_old_versions() holodeck_path = util.get_holodeck_path() if url is None: # If the URL is none, we need to derive it packages = available_packages() if package_name not in packages: print("Package not found. Available packages are:", file=sys.stderr) pprint.pprint(packages, width=10, indent=4, stream=sys.stderr) return # example: %backend%/packages/0.1.0/DefaultWorlds/Linux.zip url = "{backend_url}packages/{holodeck_version}/{package_name}/{platform}.zip".format( backend_url=BACKEND_URL, holodeck_version=util.get_holodeck_version(), package_name=package_name, platform=util.get_os_key()) install_path = os.path.join(holodeck_path, "worlds", package_name) print("Installing {} from {} to {}".format(package_name, url, install_path)) _download_binary(url, install_path)