Rework of Observations and Entity Differentiation, lazy obs build by notification

This commit is contained in:
Steffen Illium
2021-12-22 10:48:36 +01:00
parent 7f7a3d9a3b
commit b43f595207
14 changed files with 961 additions and 487 deletions

View File

@ -1,7 +1,9 @@
import abc
import enum
import time
from collections import defaultdict
from enum import Enum
from itertools import chain
from pathlib import Path
from typing import List, Union, Iterable, Dict
import numpy as np
@ -14,7 +16,8 @@ from environments.factory.base.shadow_casting import Map
from environments.helpers import Constants as c, Constants
from environments import helpers as h
from environments.factory.base.objects import Agent, Tile, Action
from environments.factory.base.registers import Actions, Entities, Agents, Doors, FloorTiles, WallTiles, PlaceHolders
from environments.factory.base.registers import Actions, Entities, Agents, Doors, FloorTiles, WallTiles, PlaceHolders, \
GlobalPositions
from environments.utility_classes import MovementProperties, ObservationProperties, MarlFrameStack
from environments.utility_classes import AgentRenderOptions as a_obs
@ -30,17 +33,31 @@ class BaseFactory(gym.Env):
def action_space(self):
return spaces.Discrete(len(self._actions))
@property
def named_action_space(self):
return {x.identifier.value: idx for idx, x in enumerate(self._actions.values())}
@property
def observation_space(self):
if r := self._pomdp_r:
z = self._obs_cube.shape[0]
xy = r*2 + 1
level_shape = (z, xy, xy)
obs, _ = self._build_observations()
if self.n_agents > 1:
shape = obs[0].shape
else:
level_shape = self._obs_cube.shape
space = spaces.Box(low=0, high=1, shape=level_shape, dtype=np.float32)
shape = obs.shape
space = spaces.Box(low=0, high=1, shape=shape, dtype=np.float32)
return space
@property
def named_observation_space(self):
# Build it
_, named_obs = self._build_observations()
if self.n_agents > 1:
# Only return the first named obs space, as their structure at the moment is same.
return [{key.name: val for key, val in named_ob.items()} for named_ob in named_obs.values()][0]
else:
return {key.name: val for key, val in named_obs.items()}
@property
def pomdp_diameter(self):
return self._pomdp_r * 2 + 1
@ -86,11 +103,14 @@ class BaseFactory(gym.Env):
self.obs_prop = obs_prop
self.level_name = level_name
self._level_shape = None
self._obs_shape = None
self.verbose = verbose
self._renderer = None # expensive - don't use it when not required !
self._entities = Entities()
self.n_agents = n_agents
level_filepath = Path(__file__).parent.parent / h.LEVELS_DIR / f'{self.level_name}.txt'
self._parsed_level = h.parse_level(level_filepath)
self.max_steps = max_steps
self._pomdp_r = self.obs_prop.pomdp_r
@ -114,10 +134,12 @@ class BaseFactory(gym.Env):
# Objects
self._entities = Entities()
# Level
level_filepath = Path(__file__).parent.parent / h.LEVELS_DIR / f'{self.level_name}.txt'
parsed_level = h.parse_level(level_filepath)
level_array = h.one_hot_level(parsed_level)
level_array = h.one_hot_level(self._parsed_level)
level_array = np.pad(level_array, self.obs_prop.pomdp_r, 'constant', constant_values=1)
self._level_shape = level_array.shape
self._obs_shape = self._level_shape if not self.obs_prop.pomdp_r else (self.pomdp_diameter, ) * 2
# Walls
walls = WallTiles.from_argwhere_coordinates(
@ -134,13 +156,14 @@ class BaseFactory(gym.Env):
self._entities.register_additional_items({c.FLOOR: floor})
# NOPOS
self._NO_POS_TILE = Tile(c.NO_POS.value)
self._NO_POS_TILE = Tile(c.NO_POS.value, None)
# Doors
if self.parse_doors:
parsed_doors = h.one_hot_level(parsed_level, c.DOOR)
parsed_doors = h.one_hot_level(self._parsed_level, c.DOOR)
parsed_doors = np.pad(parsed_doors, self.obs_prop.pomdp_r, 'constant', constant_values=0)
if np.any(parsed_doors):
door_tiles = [floor.by_pos(pos) for pos in np.argwhere(parsed_doors == c.OCCUPIED_CELL.value)]
door_tiles = [floor.by_pos(tuple(pos)) for pos in np.argwhere(parsed_doors == c.OCCUPIED_CELL.value)]
doors = Doors.from_tiles(door_tiles, self._level_shape,
entity_kwargs=dict(context=floor)
)
@ -153,12 +176,11 @@ class BaseFactory(gym.Env):
# Agents
agents_to_spawn = self.n_agents-len(self._injected_agents)
agents_kwargs = dict(level_shape=self._level_shape,
individual_slices=self.obs_prop.render_agents == a_obs.SEPERATE,
hide_from_obs_builder=self.obs_prop.render_agents == a_obs.LEVEL,
is_observable=self.obs_prop.render_agents != a_obs.NOT)
agents_kwargs = dict(individual_slices=self.obs_prop.render_agents == a_obs.SEPERATE,
hide_from_obs_builder=self.obs_prop.render_agents in [a_obs.NOT, a_obs.LEVEL],
)
if agents_to_spawn:
agents = Agents.from_tiles(floor.empty_tiles[:agents_to_spawn], **agents_kwargs)
agents = Agents.from_tiles(floor.empty_tiles[:agents_to_spawn], self._level_shape, **agents_kwargs)
else:
agents = Agents(**agents_kwargs)
if self._injected_agents:
@ -173,10 +195,10 @@ class BaseFactory(gym.Env):
# TODO: Make this accept Lists for multiple placeholders
# Empty Observations with either [0, 1, N(0, 1)]
placeholder = PlaceHolders.from_tiles([self._NO_POS_TILE], self._level_shape,
entity_kwargs=dict(
fill_value=self.obs_prop.additional_agent_placeholder)
)
placeholder = PlaceHolders.from_values(self.obs_prop.additional_agent_placeholder, self._level_shape,
entity_kwargs=dict(
fill_value=self.obs_prop.additional_agent_placeholder)
)
self._entities.register_additional_items({c.AGENT_PLACEHOLDER: placeholder})
@ -184,24 +206,22 @@ class BaseFactory(gym.Env):
if additional_entities := self.additional_entities:
self._entities.register_additional_items(additional_entities)
if self.obs_prop.show_global_position_info:
global_positions = GlobalPositions(self._level_shape)
obs_shape_2d = self._level_shape if not self._pomdp_r else ((self.pomdp_diameter,) * 2)
global_positions.spawn_GlobalPositionObjects(obs_shape_2d, self[c.AGENT])
self._entities.register_additional_items({c.GLOBAL_POSITION: global_positions})
# Return
return self._entities
def _init_obs_cube(self):
arrays = self._entities.obs_arrays
obs_cube_z = sum([a.shape[0] if not self[key].is_per_agent else 1 for key, a in arrays.items()])
obs_cube_z += 1 if self.obs_prop.show_global_position_info else 0
self._obs_cube = np.zeros((obs_cube_z, *self._level_shape), dtype=np.float32)
def reset(self) -> (np.ndarray, int, bool, dict):
def reset(self) -> (np.typing.ArrayLike, int, bool, dict):
_ = self._base_init_env()
self._init_obs_cube()
self.do_additional_reset()
self._steps = 0
obs = self._get_observations()
obs, _ = self._build_observations()
return obs
def step(self, actions):
@ -264,7 +284,7 @@ class BaseFactory(gym.Env):
# Post step Hook for later use
info.update(self.hook_post_step())
obs = self._get_observations()
obs, _ = self._build_observations()
return obs, reward, done, info
@ -284,141 +304,120 @@ class BaseFactory(gym.Env):
else:
return c.NOT_VALID
def _get_observations(self) -> np.ndarray:
state_array_dict = self._entities.obs_arrays
if self.n_agents == 1:
obs = self._build_per_agent_obs(self[c.AGENT][0], state_array_dict)
elif self.n_agents >= 2:
obs = np.stack([self._build_per_agent_obs(agent, state_array_dict) for agent in self[c.AGENT]])
else:
raise ValueError('n_agents cannot be smaller than 1!!')
return obs
def _build_observations(self) -> np.typing.ArrayLike:
# Observation dict:
per_agent_expl_idx = dict()
per_agent_obsn = dict()
# Generel Observations
lvl_obs = self[c.WALLS].as_array()
door_obs = self[c.DOORS].as_array()
agent_obs = self[c.AGENT].as_array() if self.obs_prop.render_agents != a_obs.NOT else None
placeholder_obs = self[c.AGENT_PLACEHOLDER].as_array() if self[c.AGENT_PLACEHOLDER] else None
add_obs_dict = self._additional_observations()
def _build_per_agent_obs(self, agent: Agent, state_array_dict) -> np.ndarray:
agent_pos_is_omitted = False
agent_omit_idx = None
if self.obs_prop.omit_agent_self and self.n_agents == 1:
pass
elif self.obs_prop.omit_agent_self and self.obs_prop.render_agents in [a_obs.COMBINED, ] and self.n_agents > 1:
state_array_dict[c.AGENT][0, agent.x, agent.y] -= agent.encoding
agent_pos_is_omitted = True
elif self.obs_prop.omit_agent_self and self.obs_prop.render_agents == a_obs.SEPERATE and self.n_agents > 1:
agent_omit_idx = next((i for i, a in enumerate(self[c.AGENT]) if a == agent))
running_idx, shadowing_idxs, can_be_shadowed_idxs = 0, [], []
self._obs_cube[:] = 0
# FIXME: Refactor this! Make a globally build observation, then add individual per-agent-obs
for key, array in state_array_dict.items():
# Flush state array object representation to obs cube
if not self[key].hide_from_obs_builder:
if self[key].is_per_agent:
per_agent_idx = self[key].idx_by_entity(agent)
z = 1
self._obs_cube[running_idx: running_idx+z] = array[per_agent_idx]
else:
if key == c.AGENT and agent_omit_idx is not None:
z = array.shape[0] - 1
for array_idx in range(array.shape[0]):
self._obs_cube[running_idx: running_idx+z] = array[[x for x in range(array.shape[0])
if x != agent_omit_idx]]
# Agent OBS are combined
elif key == c.AGENT and self.obs_prop.omit_agent_self \
and self.obs_prop.render_agents == a_obs.COMBINED:
z = 1
self._obs_cube[running_idx: running_idx + z] = array
# Each Agent is rendered on a seperate array slice
for agent_idx, agent in enumerate(self[c.AGENT]):
obs_dict = dict()
# Build Agent Observations
if self.obs_prop.render_agents != a_obs.NOT:
if self.obs_prop.omit_agent_self:
if self.obs_prop.render_agents == a_obs.SEPERATE:
agent_obs = np.take(agent_obs, [x for x in range(self.n_agents) if x != agent_idx], axis=0)
else:
z = array.shape[0]
self._obs_cube[running_idx: running_idx + z] = array
# Define which OBS SLices cast a Shadow
if self[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[key].can_be_shadowed:
for i in range(z):
can_be_shadowed_idxs.append(running_idx + i)
running_idx += z
agent_obs = agent_obs.copy()
agent_obs[(0, *agent.pos)] -= agent.encoding
if agent_pos_is_omitted:
state_array_dict[c.AGENT][0, agent.x, agent.y] += agent.encoding
if self._pomdp_r:
obs = self._do_pomdp_obs_cutout(agent, self._obs_cube)
else:
obs = self._obs_cube
obs = obs.copy()
if self.obs_prop.cast_shadows:
obs_block_light = [obs[idx] != c.OCCUPIED_CELL.value for idx in shadowing_idxs]
door_shadowing = False
if self.parse_doors:
if doors := self[c.DOORS]:
if door := doors.by_pos(agent.pos):
if door.is_closed:
for group in door.connectivity_subgroups:
if agent.last_pos not in group:
door_shadowing = True
if self._pomdp_r:
blocking = [tuple(np.subtract(x, agent.pos) + (self._pomdp_r, self._pomdp_r))
for x in group]
xs, ys = zip(*blocking)
else:
xs, ys = zip(*group)
# 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:
light_block_map = light_block_map.do_fov(self._pomdp_r, self._pomdp_r, max(self._level_shape))
else:
light_block_map = light_block_map.do_fov(*agent.pos, max(self._level_shape))
if door_shadowing:
# noinspection PyUnboundLocalVariable
light_block_map[xs, ys] = 0
agent.temp_light_map = light_block_map
for obs_idx in can_be_shadowed_idxs:
obs[obs_idx] = ((obs[obs_idx] * light_block_map) + 0.) - (1 - light_block_map) # * obs[0])
else:
pass
# Agents observe other agents as wall
if self.obs_prop.render_agents == a_obs.LEVEL and self.n_agents > 1:
other_agent_obs = self[c.AGENT].as_array()
if self.obs_prop.omit_agent_self:
other_agent_obs[:, agent.x, agent.y] -= agent.encoding
# Build Level Observations
if self.obs_prop.render_agents == a_obs.LEVEL:
lvl_obs = lvl_obs.copy()
lvl_obs += agent_obs
obs_dict[c.WALLS] = lvl_obs
if self.obs_prop.render_agents in [a_obs.SEPERATE, a_obs.COMBINED]:
obs_dict[c.AGENT] = agent_obs
if self[c.AGENT_PLACEHOLDER]:
obs_dict[c.AGENT_PLACEHOLDER] = placeholder_obs
obs_dict[c.DOORS] = door_obs
obs_dict.update(add_obs_dict)
observations = np.vstack(list(obs_dict.values()))
if self.obs_prop.pomdp_r:
oobs = self._do_pomdp_obs_cutout(agent, other_agent_obs)[0]
# noinspection PyUnresolvedReferences
mask = (oobs != c.SHADOWED_CELL.value).astype(int)
obs[0] += oobs * mask
observations = self._do_pomdp_cutout(agent, observations)
raw_obs = self._additional_raw_observations(agent)
observations = np.vstack((observations, *list(raw_obs.values())))
keys = list(chain(obs_dict.keys(), raw_obs.keys()))
idxs = np.cumsum([x.shape[0] for x in chain(obs_dict.values(), raw_obs.values())]) - 1
per_agent_expl_idx[agent.name] = {key: list(range(a, b)) for key, a, b in
zip(keys, idxs, list(idxs[1:]) + [idxs[-1]+1, ])}
# Shadow Casting
try:
light_block_obs = [obs_idx for key, obs_idx in per_agent_expl_idx[agent.name].items()
if self[key].is_blocking_light]
# Flatten
light_block_obs = [x for y in light_block_obs for x in y]
shadowed_obs = [obs_idx for key, obs_idx in per_agent_expl_idx[agent.name].items()
if self[key].can_be_shadowed]
# Flatten
shadowed_obs = [x for y in shadowed_obs for x in y]
except AttributeError as e:
print('Check your Keys! Only use Constants as Keys!')
print(e)
raise e
if self.obs_prop.cast_shadows:
obs_block_light = observations[light_block_obs] != c.OCCUPIED_CELL.value
door_shadowing = False
if self.parse_doors:
if doors := self[c.DOORS]:
if door := doors.by_pos(agent.pos):
if door.is_closed:
for group in door.connectivity_subgroups:
if agent.last_pos not in group:
door_shadowing = True
if self._pomdp_r:
blocking = [
tuple(np.subtract(x, agent.pos) + (self._pomdp_r, self._pomdp_r))
for x in group]
xs, ys = zip(*blocking)
else:
xs, ys = zip(*group)
# noinspection PyUnresolvedReferences
obs_block_light[:, xs, ys] = False
light_block_map = Map((np.prod(obs_block_light, axis=0) != True).astype(int).squeeze())
if self._pomdp_r:
light_block_map = light_block_map.do_fov(self._pomdp_r, self._pomdp_r, max(self._level_shape))
else:
light_block_map = light_block_map.do_fov(*agent.pos, max(self._level_shape))
if door_shadowing:
# noinspection PyUnboundLocalVariable
light_block_map[xs, ys] = 0
agent.temp_light_map = light_block_map.copy()
observations[shadowed_obs] = ((observations[shadowed_obs] * light_block_map) + 0.) - (1 - light_block_map)
else:
obs[0] += other_agent_obs
pass
# Additional Observation:
for additional_obs in self.additional_obs_build():
obs[running_idx:running_idx+additional_obs.shape[0]] = additional_obs
running_idx += additional_obs.shape[0]
for additional_per_agent_obs in self.additional_per_agent_obs_build(agent):
obs[running_idx:running_idx + additional_per_agent_obs.shape[0]] = additional_per_agent_obs
running_idx += additional_per_agent_obs.shape[0]
per_agent_obsn[agent.name] = observations
return obs
if self.n_agents == 1:
agent_name = self[c.AGENT][0].name
obs, explained_idx = per_agent_obsn[agent_name], per_agent_expl_idx[agent_name]
elif self.n_agents >= 2:
obs, explained_idx = np.stack(list(per_agent_obsn.values())), per_agent_expl_idx
else:
raise ValueError
def _do_pomdp_obs_cutout(self, agent, obs_to_be_padded):
return obs, explained_idx
def _do_pomdp_cutout(self, agent, obs_to_be_padded):
assert obs_to_be_padded.ndim == 3
r, d = self._pomdp_r, self.pomdp_diameter
x0, x1 = max(0, agent.x - r), min(agent.x + r + 1, self._level_shape[0])
y0, y1 = max(0, agent.y - r), min(agent.y + r + 1, self._level_shape[1])
# Other Agent Obs = oobs
oobs = obs_to_be_padded[:, x0:x1, y0:y1]
if oobs.shape[0:] != (d, d):
if oobs.shape[1:] != (d, d):
if xd := oobs.shape[1] % d:
if agent.x > r:
x0_pad = 0
@ -478,7 +477,7 @@ class BaseFactory(gym.Env):
if doors := self[c.DOORS]:
if self.doors_have_area:
if door := doors.by_pos(new_tile.pos):
if door.can_collide:
if door.is_open:
return agent.tile, c.NOT_VALID
else: # door.is_closed:
pass
@ -569,7 +568,7 @@ class BaseFactory(gym.Env):
if not self._renderer: # lazy init
from environments.factory.base.renderer import Renderer, RenderEntity
global Renderer, RenderEntity
height, width = self._obs_cube.shape[1:]
height, width = self._level_shape
self._renderer = Renderer(width, height, view_radius=self._pomdp_r, fps=5)
# noinspection PyUnboundLocalVariable
@ -636,20 +635,6 @@ class BaseFactory(gym.Env):
# Functions which provide additions to functions of the base class
# Always call super!!!!!!
@abc.abstractmethod
def additional_obs_build(self) -> List[np.ndarray]:
return []
def additional_per_agent_obs_build(self, agent) -> List[np.ndarray]:
additional_per_agent_obs = []
if self.obs_prop.show_global_position_info:
pos_array = np.zeros(self.observation_space.shape[1:])
for xy in range(1):
pos_array[0, xy] = agent.pos[xy] / self._level_shape[xy]
additional_per_agent_obs.append(pos_array)
return additional_per_agent_obs
@abc.abstractmethod
def do_additional_reset(self) -> None:
pass
@ -666,6 +651,17 @@ class BaseFactory(gym.Env):
def check_additional_done(self) -> bool:
return False
@abc.abstractmethod
def _additional_observations(self) -> Dict[Constants, np.typing.ArrayLike]:
return {}
@abc.abstractmethod
def _additional_raw_observations(self, agent) -> Dict[Constants, np.typing.ArrayLike]:
additional_raw_observations = {}
if self.obs_prop.show_global_position_info:
additional_raw_observations.update({c.GLOBAL_POSITION: self[c.GLOBAL_POSITION].by_entity(agent).as_array()})
return additional_raw_observations
@abc.abstractmethod
def calculate_additional_reward(self, agent: Agent) -> (int, dict):
return 0, {}

View File

@ -3,22 +3,26 @@ from enum import Enum
from typing import Union
import networkx as nx
import numpy as np
from environments import helpers as h
from environments.helpers import Constants as c
import itertools
##########################################################################
# ##################### Base Object Definition ######################### #
##########################################################################
class Object:
"""Generell Objects for Organisation and Maintanance such as Actions etc..."""
_u_idx = defaultdict(lambda: 0)
def __bool__(self):
return True
@property
def is_blocking_light(self):
return self._is_blocking_light
@property
def name(self):
return self._name
@ -43,7 +47,7 @@ class Object:
elif self._str_ident is not None and self._enum_ident is None:
self._name = f'{self.__class__.__name__}[{self._str_ident}]'
elif self._str_ident is None and self._enum_ident is None:
self._name = f'{self.__class__.__name__}#{self._u_idx[self.__class__.__name__]}'
self._name = f'{self.__class__.__name__}#{Object._u_idx[self.__class__.__name__]}'
Object._u_idx[self.__class__.__name__] += 1
else:
raise ValueError('Please use either of the idents.')
@ -68,16 +72,56 @@ class Object:
return other.name == self.name
class Entity(Object):
class EnvObject(Object):
@property
def can_collide(self):
return True
"""Objects that hold Information that are observable, but have no position on the env grid. Inventories etc..."""
_u_idx = defaultdict(lambda: 0)
@property
def encoding(self):
return c.OCCUPIED_CELL.value
def __init__(self, register, **kwargs):
super(EnvObject, self).__init__(**kwargs)
self._register = register
class BoundingMixin:
@property
def bound_entity(self):
return self._bound_entity
def __init__(self, entity_to_be_bound, *args, **kwargs):
super().__init__(*args, **kwargs)
assert entity_to_be_bound is not None
self._bound_entity = entity_to_be_bound
def __repr__(self):
s = super(BoundingMixin, self).__repr__()
i = s[:s.find('(')]
return f'{s[:i]}[{self.bound_entity.name}]{s[i:]}'
@property
def name(self):
return f'{super(BoundingMixin, self).name}({self._bound_entity.name})'
def belongs_to_entity(self, entity):
return entity == self.bound_entity
class Entity(EnvObject):
"""Full Env Entity that lives on the env Grid. Doors, Items, Dirt etc..."""
@property
def is_blocking_light(self):
return self._is_blocking_light
@property
def can_collide(self):
return True
@property
def x(self):
return self.pos[0]
@ -94,9 +138,10 @@ class Entity(Object):
def tile(self):
return self._tile
def __init__(self, tile, **kwargs):
super().__init__(**kwargs)
def __init__(self, tile, *args, is_blocking_light=True, **kwargs):
super().__init__(*args, **kwargs)
self._tile = tile
self._is_blocking_light = is_blocking_light
tile.enter(self)
def summarize_state(self, **_) -> dict:
@ -104,7 +149,7 @@ class Entity(Object):
tile=str(self.tile.name), can_collide=bool(self.can_collide))
def __repr__(self):
return f'{self.name}(@{self.pos})'
return super(Entity, self).__repr__() + f'(@{self.pos})'
class MoveableEntity(Entity):
@ -118,7 +163,7 @@ class MoveableEntity(Entity):
if self._last_tile:
return self._last_tile.pos
else:
return c.NO_POS
return c.NO_POS.value
@property
def direction_of_view(self):
@ -137,45 +182,66 @@ class MoveableEntity(Entity):
curr_tile.leave(self)
self._tile = next_tile
self._last_tile = curr_tile
self._register.notify_change_to_value(self)
return True
else:
return False
##########################################################################
# ####################### Objects and Entitys ########################## #
##########################################################################
class Action(Object):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class PlaceHolder(MoveableEntity):
class PlaceHolder(Object):
def __init__(self, *args, fill_value=0, **kwargs):
super().__init__(*args, **kwargs)
self._fill_value = fill_value
@property
def last_tile(self):
return self.tile
@property
def direction_of_view(self):
return self.pos
@property
def can_collide(self):
return False
@property
def encoding(self):
return c.NO_POS.value[0]
return self._fill_value
@property
def name(self):
return "PlaceHolder"
class Tile(Object):
class GlobalPosition(EnvObject):
def belongs_to_entity(self, entity):
return self._agent == entity
def __init__(self, level_shape, obs_shape, agent, normalized: bool = True):
super(GlobalPosition, self).__init__(self)
self._obs_shape = (1, *obs_shape) if len(obs_shape) == 2 else obs_shape
self._agent = agent
self._level_shape = level_shape
self._normalized = normalized
def as_array(self):
pos_array = np.zeros(self._obs_shape)
for xy in range(1):
pos_array[0, 0, xy] = self._agent.pos[xy] / self._level_shape[xy]
return pos_array
class Tile(EnvObject):
@property
def encoding(self):
return c.FREE_CELL.value
@property
def guests_that_can_collide(self):
@ -197,8 +263,8 @@ class Tile(Object):
def pos(self):
return self._pos
def __init__(self, pos, **kwargs):
super(Tile, self).__init__(**kwargs)
def __init__(self, pos, *args, **kwargs):
super(Tile, self).__init__(*args, **kwargs)
self._guests = dict()
self._pos = tuple(pos)
@ -233,6 +299,11 @@ class Tile(Object):
class Wall(Tile):
@property
def encoding(self):
return c.OCCUPIED_CELL.value
pass
@ -247,7 +318,8 @@ class Door(Entity):
@property
def encoding(self):
return 1 if self.is_closed else 2
# This is important as it shadow is checked by occupation value
return c.OCCUPIED_CELL.value if self.is_closed else 2
@property
def str_state(self):
@ -307,11 +379,13 @@ class Door(Entity):
def _open(self):
self.connectivity.add_edges_from([(self.pos, x) for x in range(len(self.connectivity_subgroups))])
self._state = c.OPEN_DOOR
self._register.notify_change_to_value(self)
self.time_to_close = self.auto_close_interval
def _close(self):
self.connectivity.remove_node(self.pos)
self._state = c.CLOSED_DOOR
self._register.notify_change_to_value(self)
def is_linked(self, old_pos, new_pos):
try:

View File

@ -1,18 +1,23 @@
import numbers
import random
from abc import ABC
from typing import List, Union, Dict
from typing import List, Union, Dict, Tuple
import numpy as np
from environments.factory.base.objects import Entity, Tile, Agent, Door, Action, Wall, Object, PlaceHolder
from environments.factory.base.objects import Entity, Tile, Agent, Door, Action, Wall, PlaceHolder, GlobalPosition, \
Object, EnvObject
from environments.utility_classes import MovementProperties
from environments import helpers as h
from environments.helpers import Constants as c
##########################################################################
# ##################### Base Register Definition ####################### #
##########################################################################
class Register:
_accepted_objects = Entity
class ObjectRegister:
_accepted_objects = Object
@property
def name(self):
@ -48,6 +53,12 @@ class Register:
def items(self):
return self._register.items()
def _get_index(self, item):
try:
return next(i for i, v in enumerate(self._register.values()) if v == item)
except (StopIteration, AssertionError):
return None
def __getitem__(self, item):
if isinstance(item, (int, np.int64, np.int32)):
if item < 0:
@ -65,39 +76,66 @@ class Register:
return f'{self.__class__.__name__}({self._register})'
class ObjectRegister(Register):
class EnvObjectRegister(ObjectRegister):
hide_from_obs_builder = False
_accepted_objects = EnvObject
def __init__(self, level_shape: (int, int), *args, individual_slices=False, is_per_agent=False, **kwargs):
super(ObjectRegister, self).__init__(*args, **kwargs)
self.is_per_agent = is_per_agent
self.individual_slices = individual_slices
self._level_shape = level_shape
def __init__(self, obs_shape: (int, int), *args, **kwargs):
super(EnvObjectRegister, self).__init__(*args, **kwargs)
self._shape = obs_shape
self._array = None
self.hide_from_obs_builder = False
self._lazy_eval_transforms = []
def register_item(self, other):
super(ObjectRegister, self).register_item(other)
def register_item(self, other: EnvObject):
super(EnvObjectRegister, self).register_item(other)
if self._array is None:
self._array = np.zeros((1, *self._level_shape))
else:
if self.individual_slices:
self._array = np.concatenate((self._array, np.zeros((1, *self._array.shape[1:]))))
self._array = np.zeros((1, *self._shape))
self.notify_change_to_value(other)
def as_array(self):
if self._lazy_eval_transforms:
idxs, values = zip(*self._lazy_eval_transforms)
# nuumpy put repects the ordering so that
np.put(self._array, idxs, values)
self._lazy_eval_transforms = []
return self._array
def summarize_states(self, n_steps=None):
return [val.summarize_state(n_steps=n_steps) for val in self.values()]
def notify_change_to_free(self, env_object: EnvObject):
self._array_change_notifyer(env_object, value=c.FREE_CELL.value)
class EntityObjectRegister(ObjectRegister, ABC):
def notify_change_to_value(self, env_object: EnvObject):
self._array_change_notifyer(env_object)
def as_array(self):
raise NotImplementedError
def _array_change_notifyer(self, env_object: EnvObject, value=None):
pos = self._get_index(env_object)
value = value if value is not None else env_object.encoding
self._lazy_eval_transforms.append((pos, value))
def __delitem__(self, name):
self.notify_change_to_free(self._register[name])
del self._register[name]
def delete_env_object(self, env_object: EnvObject):
del self[env_object.name]
def delete_env_object_by_name(self, name):
del self[name]
class EntityRegister(EnvObjectRegister, ABC):
_accepted_objects = Entity
@classmethod
def from_tiles(cls, tiles, *args, entity_kwargs=None, **kwargs):
# objects_name = cls._accepted_objects.__name__
register_obj = cls(*args, **kwargs)
entities = [cls._accepted_objects(tile, str_ident=i, **entity_kwargs if entity_kwargs is not None else {})
entities = [cls._accepted_objects(tile, register_obj, str_ident=i,
**entity_kwargs if entity_kwargs is not None else {})
for i, tile in enumerate(tiles)]
register_obj.register_additional_items(entities)
return register_obj
@ -115,86 +153,172 @@ class EntityObjectRegister(ObjectRegister, ABC):
def tiles(self):
return [entity.tile for entity in self]
def __init__(self, *args, is_blocking_light=False, is_observable=True, can_be_shadowed=True, **kwargs):
super(EntityObjectRegister, self).__init__(*args, **kwargs)
self.can_be_shadowed = can_be_shadowed
self.is_blocking_light = is_blocking_light
self.is_observable = is_observable
@property
def encodings(self):
return [x.encoding for x in self]
def by_pos(self, pos):
if isinstance(pos, np.ndarray):
pos = tuple(pos)
def __init__(self, level_shape, *args,
is_blocking_light: bool = False,
can_be_shadowed: bool = True,
individual_slices: bool = False, **kwargs):
super(EntityRegister, self).__init__(level_shape, *args, **kwargs)
self._lazy_eval_transforms = []
self.can_be_shadowed = can_be_shadowed
self.individual_slices = individual_slices
self.is_blocking_light = is_blocking_light
def __delitem__(self, name):
idx, obj = next((i, obj) for i, obj in enumerate(self) if obj.name == name)
obj.tile.leave(obj)
super(EntityRegister, self).__delitem__(name)
if self.individual_slices:
self._array = np.delete(self._array, idx, axis=0)
def as_array(self):
if self._lazy_eval_transforms:
idxs, values = zip(*self._lazy_eval_transforms)
# numpy put repects the ordering so that
# Todo: Export the index building in a seperate function
np.put(self._array, [np.ravel_multi_index(idx, self._array.shape) for idx in idxs], values)
self._lazy_eval_transforms = []
return self._array
def _array_change_notifyer(self, entity, pos=None, value=None):
# Todo: Export the contruction in a seperate function
pos = pos if pos is not None else entity.pos
value = value if value is not None else entity.encoding
x, y = pos
if self.individual_slices:
idx = (self._get_index(entity), x, y)
else:
idx = (0, x, y)
self._lazy_eval_transforms.append((idx, value))
def by_pos(self, pos: Tuple[int, int]):
try:
return next(item for item in self.values() if item.pos == pos)
return next(item for item in self if item.pos == tuple(pos))
except StopIteration:
return None
class MovingEntityObjectRegister(EntityObjectRegister, ABC):
class BoundRegisterMixin(EnvObjectRegister, ABC):
@classmethod
def from_entities_to_bind(self, entitites):
def from_values(cls, values: Union[str, numbers.Number, List[Union[str, numbers.Number]]],
*args, object_kwargs=None, **kwargs):
# objects_name = cls._accepted_objects.__name__
if isinstance(values, (str, numbers.Number)):
values = [values]
register_obj = cls(*args, **kwargs)
objects = [cls._accepted_objects(register_obj, str_ident=i, fill_value=value,
**object_kwargs if object_kwargs is not None else {})
for i, value in enumerate(values)]
register_obj.register_additional_items(objects)
return register_obj
class MovingEntityObjectRegister(EntityRegister, ABC):
def __init__(self, *args, **kwargs):
super(MovingEntityObjectRegister, self).__init__(*args, **kwargs)
def by_pos(self, pos):
if isinstance(pos, np.ndarray):
pos = tuple(pos)
def notify_change_to_value(self, entity):
super(MovingEntityObjectRegister, self).notify_change_to_value(entity)
if entity.last_pos != c.NO_POS.value:
try:
self._array_change_notifyer(entity, entity.last_pos, value=c.FREE_CELL.value)
except AttributeError:
pass
##########################################################################
# ################# Objects and Entity Registers ####################### #
##########################################################################
class GlobalPositions(EnvObjectRegister):
_accepted_objects = GlobalPosition
is_blocking_light = False
can_be_shadowed = False
hide_from_obs_builder = True
def __init__(self, *args, **kwargs):
super(GlobalPositions, self).__init__(*args, is_per_agent=True, individual_slices=True, **kwargs)
def as_array(self):
# Todo make this lazy?
return np.stack([gp.as_array() for inv_idx, gp in enumerate(self)])
def spawn_GlobalPositionObjects(self, obs_shape, agents):
global_positions = [self._accepted_objects(self._shape, obs_shape, agent)
for _, agent in enumerate(agents)]
# noinspection PyTypeChecker
self.register_additional_items(global_positions)
def summarize_states(self, n_steps=None):
return {}
def idx_by_entity(self, entity):
try:
return next(x for x in self if x.pos == pos)
return next((idx for idx, inv in enumerate(self) if inv.belongs_to_entity(entity)))
except StopIteration:
return None
def __delitem__(self, name):
idx = next(i for i, entity in enumerate(self) if entity.name == name)
del self._register[name]
if self.individual_slices:
self._array = np.delete(self._array, idx, axis=0)
def delete_entity(self, item):
self.delete_entity_by_name(item.name)
def delete_entity_by_name(self, name):
del self[name]
def by_entity(self, entity):
try:
return next((inv for inv in self if inv.belongs_to_entity(entity)))
except StopIteration:
return None
class PlaceHolders(MovingEntityObjectRegister):
class PlaceHolders(EnvObjectRegister):
_accepted_objects = PlaceHolder
def __init__(self, *args, fill_value: Union[str, int] = 0, **kwargs):
def __init__(self, *args, **kwargs):
assert not 'individual_slices' in kwargs, 'Keyword - "individual_slices": "True" and must not be altered'
kwargs.update(individual_slices=False)
super().__init__(*args, **kwargs)
self.fill_value = fill_value
@classmethod
def from_values(cls, values: Union[str, numbers.Number, List[Union[str, numbers.Number]]],
*args, object_kwargs=None, **kwargs):
# objects_name = cls._accepted_objects.__name__
if isinstance(values, (str, numbers.Number)):
values = [values]
register_obj = cls(*args, **kwargs)
objects = [cls._accepted_objects(register_obj, str_ident=i, fill_value=value,
**object_kwargs if object_kwargs is not None else {})
for i, value in enumerate(values)]
register_obj.register_additional_items(objects)
return register_obj
# noinspection DuplicatedCode
def as_array(self):
if isinstance(self.fill_value, numbers.Number):
self._array[:] = self.fill_value
elif isinstance(self.fill_value, str):
if self.fill_value.lower() in ['normal', 'n']:
self._array = np.random.normal(size=self._array.shape)
for idx, placeholder in enumerate(self):
if isinstance(placeholder.encoding, numbers.Number):
self._array[idx][:] = placeholder.fill_value
elif isinstance(placeholder.fill_value, str):
if placeholder.fill_value.lower() in ['normal', 'n']:
self._array[:] = np.random.normal(size=self._array.shape)
else:
raise ValueError('Choose one of: ["normal", "N"]')
else:
raise ValueError('Choose one of: ["normal", "N"]')
else:
raise TypeError('Objects of type "str" or "number" is required here.')
raise TypeError('Objects of type "str" or "number" is required here.')
if self.individual_slices:
return self._array
else:
return self._array[None, 0]
return self._array
class Entities(Register):
_accepted_objects = EntityObjectRegister
class Entities(ObjectRegister):
_accepted_objects = EntityRegister
@property
def observable_arrays(self):
# FIXME: Find a better name
return {key: val.as_array() for key, val in self.items() if val.is_observable}
def arrays(self):
return {key: val.as_array() for key, val in self.items()}
@property
def obs_arrays(self):
# FIXME: Find a better name
return {key: val.as_array() for key, val in self.items() if val.is_observable and not val.hide_from_obs_builder}
return {key: val.as_array() for key, val in self.items() if not val.hide_from_obs_builder}
@property
def names(self):
@ -220,34 +344,34 @@ class Entities(Register):
return found_entities
class WallTiles(EntityObjectRegister):
class WallTiles(EntityRegister):
_accepted_objects = Wall
_light_blocking = True
hide_from_obs_builder = True
def as_array(self):
if not np.any(self._array):
# Which is Faster?
# indices = [x.pos for x in self]
# np.put(self._array, [np.ravel_multi_index((0, *x), self._array.shape) for x in indices], self.encodings)
x, y = zip(*[x.pos for x in self])
self._array[0, x, y] = self.encoding
return self._array
def __init__(self, *args, **kwargs):
super(WallTiles, self).__init__(*args, individual_slices=False,
is_blocking_light=self._light_blocking, **kwargs)
super(WallTiles, self).__init__(*args, is_blocking_light=self._light_blocking, individual_slices=False,
**kwargs)
@property
def encoding(self):
return c.OCCUPIED_CELL.value
@property
def array(self):
return self._array
@classmethod
def from_argwhere_coordinates(cls, argwhere_coordinates, *args, **kwargs):
tiles = cls(*args, **kwargs)
# noinspection PyTypeChecker
tiles.register_additional_items(
[cls._accepted_objects(pos, is_blocking_light=cls._light_blocking)
[cls._accepted_objects(pos, tiles, is_blocking_light=cls._light_blocking)
for pos in argwhere_coordinates]
)
return tiles
@ -264,12 +388,11 @@ class WallTiles(EntityObjectRegister):
class FloorTiles(WallTiles):
_accepted_objects = Tile
_light_blocking = False
def __init__(self, *args, **kwargs):
super(FloorTiles, self).__init__(*args, is_observable=False, **kwargs)
super(FloorTiles, self).__init__(*args, **kwargs)
@property
def encoding(self):
@ -297,22 +420,21 @@ class FloorTiles(WallTiles):
class Agents(MovingEntityObjectRegister):
_accepted_objects = Agent
def __init__(self, *args, hide_from_obs_builder=False, **kwargs):
super().__init__(*args, **kwargs)
self.hide_from_obs_builder = hide_from_obs_builder
# noinspection DuplicatedCode
def as_array(self):
self._array[:] = c.FREE_CELL.value
# noinspection PyTupleAssignmentBalance
for z, x, y, v in zip(range(len(self)), *zip(*[x.pos for x in self]), [x.encoding for x in self]):
if self.individual_slices:
self._array[z, x, y] += v
else:
self._array[0, x, y] += v
@DeprecationWarning
def Xas_array(self):
# Super Safe Version
# self._array[:] = c.FREE_CELL.value
indices = list(zip(range(len(self)), *zip(*[x.last_pos for x in self])))
np.put(self._array, [np.ravel_multi_index(x, self._array.shape) for x in indices], c.FREE_CELL.value)
indices = list(zip(range(len(self)), *zip(*[x.pos for x in self])))
np.put(self._array, [np.ravel_multi_index(x, self._array.shape) for x in indices], self.encodings)
if self.individual_slices:
return self._array
else:
@ -329,17 +451,11 @@ class Agents(MovingEntityObjectRegister):
self._register[agent.name] = agent
class Doors(EntityObjectRegister):
class Doors(EntityRegister):
def __init__(self, *args, **kwargs):
super(Doors, self).__init__(*args, is_blocking_light=True, **kwargs)
def as_array(self):
self._array[:] = 0
for door in self:
self._array[0, door.x, door.y] = door.encoding
return self._array
_accepted_objects = Door
def get_near_position(self, position: (int, int)) -> Union[None, Door]:
@ -353,8 +469,7 @@ class Doors(EntityObjectRegister):
door.tick()
class Actions(Register):
class Actions(ObjectRegister):
_accepted_objects = Action
@property
@ -385,7 +500,7 @@ class Actions(Register):
return action in self.movement_actions.values()
class Zones(Register):
class Zones(ObjectRegister):
@property
def accounting_zones(self):

View File

@ -13,7 +13,7 @@ mult_array = np.asarray([
class Map(object):
# Multipliers for transforming coordinates to other octants:
def __init__(self, map_array: np.ndarray, diamond_slope: float = 0.9):
def __init__(self, map_array: np.typing.ArrayLike, diamond_slope: float = 0.9):
self.data = map_array
self.width, self.height = map_array.shape
self.light = np.full_like(self.data, c.FREE_CELL.value)