Everything is an object now

This commit is contained in:
Steffen Illium
2021-08-26 17:47:15 +02:00
parent bd0a8090ab
commit 0fc4db193f
7 changed files with 613 additions and 447 deletions
+121 -142
View File
@@ -1,7 +1,8 @@
import abc
import time
from enum import Enum
from pathlib import Path
from typing import List, Union, Iterable
from typing import List, Union, Iterable, Dict
import gym
import numpy as np
@@ -14,8 +15,8 @@ from environments.factory.base.shadow_casting import Map
from environments.factory.renderer import Renderer, RenderEntity
from environments.helpers import Constants as c, Constants
from environments import helpers as h
from environments.factory.base.objects import Slice, Agent, Tile, Action
from environments.factory.base.registers import StateSlices, Actions, Entities, Agents, Doors, FloorTiles
from environments.factory.base.objects import Agent, Tile, Action
from environments.factory.base.registers import Actions, Entities, Agents, Doors, FloorTiles, WallTiles
from environments.utility_classes import MovementProperties
REC_TAC = 'rec'
@@ -30,9 +31,13 @@ class BaseFactory(gym.Env):
@property
def observation_space(self):
slices = self._slices.n_observable_slices
level_shape = (self.pomdp_r * 2 + 1, self.pomdp_r * 2 + 1) if self.pomdp_r else self._level_shape
space = spaces.Box(low=0, high=1, shape=(slices, *level_shape), dtype=np.float32)
if r := self.pomdp_r:
z = self._obs_cube.shape[0]
xy = r*2 + 1
level_shape = (z, xy, xy)
else:
level_shape = self._obs_cube.shape
space = spaces.Box(low=0, high=1, shape=level_shape, dtype=np.float32)
return space
@property
@@ -51,8 +56,8 @@ class BaseFactory(gym.Env):
def __init__(self, level_name='simple', n_agents=1, max_steps=int(5e2), pomdp_r: Union[None, int] = 0,
movement_properties: MovementProperties = MovementProperties(), parse_doors=False,
combin_agent_slices_in_obs: bool = False, frames_to_stack=0, record_episodes=False,
omit_agent_slice_in_obs=False, done_at_collision=False, cast_shadows=True,
combin_agent_obs: bool = False, frames_to_stack=0, record_episodes=False,
omit_agent_in_obs=False, done_at_collision=False, cast_shadows=True,
verbose=False, doors_have_area=True, env_seed=time.time_ns(), **kwargs):
assert frames_to_stack != 1 and frames_to_stack >= 0, "'frames_to_stack' cannot be negative or 1."
@@ -69,8 +74,8 @@ class BaseFactory(gym.Env):
self.max_steps = max_steps
self.pomdp_r = pomdp_r
self.combin_agent_slices_in_obs = combin_agent_slices_in_obs
self.omit_agent_slice_in_obs = omit_agent_slice_in_obs
self.combin_agent_obs = combin_agent_obs
self.omit_agent_in_obs = omit_agent_in_obs
self.cast_shadows = cast_shadows
self.frames_to_stack = frames_to_stack
@@ -87,86 +92,74 @@ class BaseFactory(gym.Env):
# Reset
self.reset()
def _init_state_slices(self) -> StateSlices:
state_slices = StateSlices()
def _base_init_env(self):
# Objects
entities = {}
# Level
level_filepath = Path(__file__).parent.parent / h.LEVELS_DIR / f'{self.level_name}.txt'
parsed_level = h.parse_level(level_filepath)
level = [Slice(c.LEVEL, h.one_hot_level(parsed_level), is_blocking_light=True)]
self._level_shape = level[0].shape
level_array = h.one_hot_level(parsed_level)
self._level_shape = level_array.shape
# Walls
walls = WallTiles.from_argwhere_coordinates(
np.argwhere(level_array == c.OCCUPIED_CELL.value),
self._level_shape
)
entities.update({c.WALLS: walls})
# Floor
floor = FloorTiles.from_argwhere_coordinates(
np.argwhere(level_array == c.FREE_CELL.value),
self._level_shape
)
entities.update({c.FLOOR: floor})
# NOPOS
self.NO_POS_TILE = Tile(c.NO_POS, c.NO_POS.value)
# Doors
parsed_doors = h.one_hot_level(parsed_level, c.DOOR)
if parsed_doors.any():
doors = [Slice(c.DOORS, parsed_doors, is_blocking_light=True)]
else:
doors = []
if np.any(parsed_doors):
door_tiles = [floor.by_pos(pos) for pos in np.argwhere(parsed_doors == c.OCCUPIED_CELL.value)]
doors = Doors.from_tiles(door_tiles, self._level_shape, context=floor, is_blocking_light=True)
entities.update({c.DOORS: doors})
# Agents
agents = []
agent_names = [f'{c.AGENT.value}#{i}' for i in range(self.n_agents)]
agents = Agents.from_tiles(floor.empty_tiles[:self.n_agents], self._level_shape)
entities.update({c.AGENT: agents})
if self.combin_agent_slices_in_obs and self.omit_agent_slice_in_obs:
if self.n_agents == 1:
observables = [False]
else:
observables = [True] + ([False] * (self.n_agents - 1))
elif self.combin_agent_slices_in_obs and not self.omit_agent_slice_in_obs:
observables = [True] + ([False] * (self.n_agents - 1))
elif not self.combin_agent_slices_in_obs and self.omit_agent_slice_in_obs:
observables = [False] + ([True] * (self.n_agents - 1))
elif not self.combin_agent_slices_in_obs and not self.omit_agent_slice_in_obs:
observables = [True] * self.n_agents
else:
raise RuntimeError('This should not happen!')
for observable, agent_name in zip(observables, agent_names):
agents.append(Slice(agent_name, np.zeros_like(level[0].slice, dtype=np.float32), is_observable=observable))
state_slices.register_additional_items(level+doors+agents+self.additional_slices)
return state_slices
def _init_obs_cube(self) -> np.ndarray:
x, y = self._slices.by_enum(c.LEVEL).shape
state = np.zeros((len(self._slices), x, y), dtype=np.float32)
state[0] = self._slices.by_enum(c.LEVEL).slice
if r := self.pomdp_r:
self._padded_obs_cube = np.full((len(self._slices), x + r*2, y + r*2), c.FREE_CELL.value, dtype=np.float32)
self._padded_obs_cube[0] = c.OCCUPIED_CELL.value
self._padded_obs_cube[:, r:r+x, r:r+y] = state
if self.combin_agent_slices_in_obs and self.n_agents > 1:
self._combined_obs_cube = np.zeros(self.observation_space.shape, dtype=np.float32)
return state
def _init_entities(self):
# Tile Init
self._tiles = FloorTiles.from_argwhere_coordinates(self._slices.by_enum(c.LEVEL).free_tiles)
# Door Init
if self.parse_doors:
tiles = [self._tiles.by_pos(x) for x in self._slices.by_enum(c.DOORS).occupied_tiles]
self._doors = Doors.from_tiles(tiles, context=self._tiles, has_area=self.doors_have_area)
# Agent Init on random positions
self._agents = Agents.from_tiles(self._base_rng.choice(self._tiles, self.n_agents))
entities = Entities()
entities.register_additional_items([self._agents])
if self.parse_doors:
entities.register_additional_items([self._doors])
# All entities
self._entities = Entities()
self._entities.register_additional_items(entities)
# Additional Entitites from SubEnvs
if additional_entities := self.additional_entities:
entities.register_additional_items(additional_entities)
self._entities.register_additional_items(additional_entities)
return entities
# Return
return self._entities
def _init_obs_cube(self):
arrays = self._entities.arrays
if self.omit_agent_in_obs and self.n_agents == 1:
del arrays[c.AGENT]
obs_cube_z = sum([a.shape[0] if not self._entities[key].is_per_agent else 1 for key, a in arrays.items()])
self._obs_cube = np.zeros((obs_cube_z, *self._level_shape), dtype=np.float32)
# Optionally Pad this obs cube for pomdp cases
if r := self.pomdp_r:
x, y = self._level_shape
self._padded_obs_cube = np.full((obs_cube_z, x + r*2, y + r*2), c.SHADOWED_CELL.value, dtype=np.float32)
# self._padded_obs_cube[0] = c.OCCUPIED_CELL.value
self._padded_obs_cube[:, r:r+x, r:r+y] = self._obs_cube
def reset(self) -> (np.ndarray, int, bool, dict):
self._slices = self._init_state_slices()
self._obs_cube = self._init_obs_cube()
self._entitites = self._init_entities()
_ = self._base_init_env()
self._init_obs_cube()
self.do_additional_reset()
self._flush_state()
self._steps = 0
obs = self._get_observations()
@@ -182,7 +175,7 @@ class BaseFactory(gym.Env):
self.hook_pre_step()
# Move this in a seperate function?
for action, agent in zip(actions, self._agents):
for action, agent in zip(actions, self._entities[c.AGENT]):
agent.clear_temp_sate()
action_obj = self._actions[action]
if self._actions.is_moving_action(action_obj):
@@ -200,9 +193,6 @@ class BaseFactory(gym.Env):
# In-between step Hook for later use
info = self.do_additional_step()
# Write to observation cube
self._flush_state()
tiles_with_collisions = self.get_all_tiles_with_collisions()
for tile in tiles_with_collisions:
guests = tile.guests_that_can_collide
@@ -216,7 +206,7 @@ class BaseFactory(gym.Env):
# Step the door close intervall
if self.parse_doors:
self._doors.tick_doors()
self._entities[c.DOORS].tick_doors()
# Finalize
reward, reward_info = self.calculate_reward()
@@ -237,9 +227,9 @@ class BaseFactory(gym.Env):
def _handle_door_interaction(self, agent):
# Check if agent really is standing on a door:
if self.doors_have_area:
door = self._doors.get_near_position(agent.pos)
door = self._entities[c.DOORS].get_near_position(agent.pos)
else:
door = self._doors.by_pos(agent.pos)
door = self._entities[c.DOORS].by_pos(agent.pos)
if door is not None:
door.use()
return c.VALID.value
@@ -247,36 +237,44 @@ class BaseFactory(gym.Env):
else:
return c.NOT_VALID.value
def _flush_state(self):
self._obs_cube[np.arange(len(self._slices)) != self._slices.get_idx(c.LEVEL)] = c.FREE_CELL.value
if self.parse_doors:
for door in self._doors:
if door.is_open and self._obs_cube[self._slices.get_idx(c.DOORS)][door.pos] != c.OPEN_DOOR.value:
self._obs_cube[self._slices.get_idx(c.DOORS)][door.pos] = c.OPEN_DOOR.value
elif door.is_closed and self._obs_cube[self._slices.get_idx(c.DOORS)][door.pos] != c.CLOSED_DOOR.value:
self._obs_cube[self._slices.get_idx(c.DOORS)][door.pos] = c.CLOSED_DOOR.value
for agent in self._agents:
self._obs_cube[self._slices.get_idx_by_name(agent.name)][agent.pos] = c.OCCUPIED_CELL.value
if agent.last_pos != c.NO_POS:
self._obs_cube[self._slices.get_idx_by_name(agent.name)][agent.last_pos] = c.FREE_CELL.value
def _get_observations(self) -> np.ndarray:
if self.n_agents == 1:
obs = self._build_per_agent_obs(self._agents[0])
obs = self._build_per_agent_obs(self._entities[c.AGENT][0])
elif self.n_agents >= 2:
obs = np.stack([self._build_per_agent_obs(agent) for agent in self._agents])
obs = np.stack([self._build_per_agent_obs(agent) for agent in self._entities[c.AGENT]])
else:
raise ValueError('n_agents cannot be smaller than 1!!')
return obs
def _build_per_agent_obs(self, agent: Agent) -> np.ndarray:
first_agent_slice = self._slices.AGENTSTARTIDX
plain_arrays = self._entities.arrays
if self.omit_agent_in_obs and self.n_agents == 1:
del plain_arrays[c.AGENT]
running_idx, shadowing_idxs, can_be_shadowed_idxs = 0, [], []
for key, array in plain_arrays.items():
if self._entities[key].is_per_agent:
per_agent_idx = self._entities[key].get_idx_by_name(agent.name)
z = 1
self._obs_cube[running_idx: z] = array[per_agent_idx]
else:
z = array.shape[0]
self._obs_cube[running_idx: z] = array
# Define which OBS SLices cast a Shadow
if self._entities[key].is_blocking_light:
for i in range(z):
shadowing_idxs.append(running_idx + i)
# Define which OBS SLices are effected by shadows
if self._entities[key].can_be_shadowed:
for i in range(z):
can_be_shadowed_idxs.append(running_idx + i)
running_idx += z
if r := self.pomdp_r:
x, y = self._level_shape
self._padded_obs_cube[:, r:r + x, r:r + y] = self._obs_cube
global_x, global_y = agent.pos
global_x += r
global_y += r
global_x, global_y = map(sum, zip(agent.pos, (r, r)))
x0, x1 = max(0, global_x - self.pomdp_r), global_x + self.pomdp_r + 1
y0, y1 = max(0, global_y - self.pomdp_r), global_y + self.pomdp_r + 1
obs = self._padded_obs_cube[:, x0:x1, y0:y1]
@@ -284,10 +282,9 @@ class BaseFactory(gym.Env):
obs = self._obs_cube
if self.cast_shadows:
obs_block_light = [obs[idx] != c.OCCUPIED_CELL.value for idx, obs_slice
in enumerate(self._slices) if obs_slice.is_blocking_light]
obs_block_light = [obs[idx] != c.OCCUPIED_CELL.value for idx in shadowing_idxs]
door_shadowing = False
if door := self._doors.by_pos(agent.pos):
if door := self._entities[c.DOORS].by_pos(agent.pos):
if door.is_closed:
for group in door.connectivity_subgroups:
if agent.last_pos not in group:
@@ -298,8 +295,9 @@ class BaseFactory(gym.Env):
xs, ys = zip(*blocking)
else:
xs, ys = zip(*group)
# noinspection PyTypeChecker
obs_block_light[self._slices.get_idx(c.LEVEL)][xs, ys] = False
# noinspection PyUnresolvedReferences
obs_block_light[0][xs, ys] = False
light_block_map = Map((np.prod(obs_block_light, axis=0) != True).astype(int))
if self.pomdp_r:
@@ -310,28 +308,18 @@ class BaseFactory(gym.Env):
# noinspection PyUnboundLocalVariable
light_block_map[xs, ys] = 0
agent.temp_light_map = light_block_map
for obs_idx in range(obs.shape[0]):
if self._slices[obs_idx].can_be_shadowed:
obs[obs_idx] = (obs[obs_idx] * light_block_map) - (
(1 - light_block_map) * obs[self._slices.get_idx(c.LEVEL)]
)
for obs_idx in can_be_shadowed_idxs:
obs[obs_idx] = (obs[obs_idx] * light_block_map) - (
(1 - light_block_map) * obs[0]
)
if self.combin_agent_slices_in_obs and self.n_agents > 1:
agent_obs = np.sum(obs[[key for key, l_slice in self._slices.items() if c.AGENT.name in l_slice.name and
(not self.omit_agent_slice_in_obs and l_slice.name != agent.name)]],
axis=0, keepdims=True)
obs = np.concatenate((obs[:first_agent_slice], agent_obs, obs[first_agent_slice+self.n_agents:]))
return obs
else:
if self.omit_agent_slice_in_obs:
obs_new = obs[[key for key, val in self._slices.items() if val.name != agent.name]]
return obs_new
else:
return obs
return obs
def get_all_tiles_with_collisions(self) -> List[Tile]:
tiles_with_collisions = list()
for tile in self._tiles:
for tile in self._entities[c.FLOOR]:
if tile.is_occupied():
guests = [guest for guest in tile.guests if guest.can_collide]
if len(guests) >= 2:
@@ -353,7 +341,7 @@ class BaseFactory(gym.Env):
x_new = agent.x + x_diff
y_new = agent.y + y_diff
new_tile = self._tiles.by_pos((x_new, y_new))
new_tile = self._entities[c.FLOOR].by_pos((x_new, y_new))
if new_tile:
valid = c.VALID
else:
@@ -362,13 +350,13 @@ class BaseFactory(gym.Env):
return tile, valid
if self.parse_doors and agent.last_pos != c.NO_POS:
if door := self._doors.by_pos(new_tile.pos):
if door := self._entities[c.DOORS].by_pos(new_tile.pos):
if door.can_collide:
return agent.tile, c.NOT_VALID
else: # door.is_closed:
pass
if door := self._doors.by_pos(agent.pos):
if door := self._entities[c.DOORS].by_pos(agent.pos):
if door.is_open:
pass
else: # door.is_closed:
@@ -388,7 +376,7 @@ class BaseFactory(gym.Env):
info_dict = dict()
reward = 0
for agent in self._agents:
for agent in self._entities[c.AGENT]:
if self._actions.is_moving_action(agent.temp_action):
if agent.temp_valid:
# info_dict.update(movement=1)
@@ -427,16 +415,15 @@ class BaseFactory(gym.Env):
height, width = self._obs_cube.shape[1:]
self._renderer = Renderer(width, height, view_radius=self.pomdp_r, fps=5)
walls = [RenderEntity('wall', pos)
for pos in np.argwhere(self._slices.by_enum(c.LEVEL).slice == c.OCCUPIED_CELL.value)]
walls = [RenderEntity('wall', wall.pos) for wall in self._entities[c.WALLS]]
agents = []
for i, agent in enumerate(self._agents):
for i, agent in enumerate(self._entities[c.AGENT]):
name, state = h.asset_str(agent)
agents.append(RenderEntity(name, agent.pos, 1, 'none', state, i + 1, agent.temp_light_map))
doors = []
if self.parse_doors:
for i, door in enumerate(self._doors):
for i, door in enumerate(self._entities[c.DOORS]):
name, state = 'door_open' if door.is_open else 'door_closed', 'blank'
doors.append(RenderEntity(name, door.pos, 1, 'none', state, i + 1))
additional_assets = self.render_additional_assets()
@@ -454,7 +441,9 @@ class BaseFactory(gym.Env):
def _summarize_state(self):
summary = {f'{REC_TAC}_step': self._steps}
for entity in self._entitites:
self._entities[c.WALLS].summarize_state()
for entity in self._entities:
if hasattr(entity, 'summarize_state'):
summary.update({f'{REC_TAC}_{entity.name}': entity.summarize_state()})
return summary
@@ -475,24 +464,14 @@ class BaseFactory(gym.Env):
return []
@property
def additional_entities(self) -> Union[Entities, List[Entities]]:
def additional_entities(self) -> Dict[(Enum, Entities)]:
"""
When heriting from this Base Class, you musst implement this methode!!!
:return: A single Entites collection or a list of such.
:rtype: Union[Entities, List[Entities]]
"""
return []
@property
def additional_slices(self) -> Union[Slice, List[Slice]]:
"""
When heriting from this Base Class, you musst implement this methode!!!
:return: A list of Slice-objects.
:rtype: List[Slice]
"""
return []
return {}
# Functions which provide additions to functions of the base class
# Always call super!!!!!!