Doors now Working,

Pypi install adjustments.
This commit is contained in:
Steffen Illium
2023-06-16 09:58:28 +02:00
parent 45cb83746c
commit b4326b514c
12 changed files with 108 additions and 96 deletions

1
MANIFEST.in Normal file
View File

@ -0,0 +1 @@
global-include *.txt *.png *.jpg

View File

@ -3,14 +3,11 @@
Tackling emergent dysfunctions (EDYs) in cooperation with Fraunhofer-IKS Tackling emergent dysfunctions (EDYs) in cooperation with Fraunhofer-IKS
## Setup ## Setup
1. Make sure to install `virtualenv` using `pip install virtualenv` Just install this environment by `pip install marl-factory-grid`.
2. Create a new virtual environment `virtualenv venv`
3. Activate the virtual environment `source venv/bin/activate`
4. Install the required dependencies `pip install -r requirements.txt`
##
## First Steps ## First Steps
### Quickstart ### Quickstart
Most of the env. objects (entites, rules and assets) can be loaded automatically. Most of the env. objects (entites, rules and assets) can be loaded automatically.
Just define what your environment needs in a *yaml*-configfile like: Just define what your environment needs in a *yaml*-configfile like:

View File

@ -1,10 +1,11 @@
import itertools
from random import choice from random import choice
import numpy as np import numpy as np
import networkx as nx
from networkx.algorithms.approximation import traveling_salesman as tsp from networkx.algorithms.approximation import traveling_salesman as tsp
from environment.utils.helpers import points_to_graph
from modules.doors import constants as do from modules.doors import constants as do
from environment import constants as c from environment import constants as c
@ -15,6 +16,41 @@ from abc import abstractmethod, ABC
future_planning = 7 future_planning = 7
def points_to_graph(coordiniates_or_tiles, allow_euclidean_connections=True, allow_manhattan_connections=True):
"""
Given a set of coordinates, this function contructs a non-directed graph, by conncting adjected points.
There are three combinations of settings:
Allow all neigbors: Distance(a, b) <= sqrt(2)
Allow only manhattan: Distance(a, b) == 1
Allow only euclidean: Distance(a, b) == sqrt(2)
:param coordiniates_or_tiles: A set of coordinates.
:type coordiniates_or_tiles: Tiles
:param allow_euclidean_connections: Whether to regard diagonal adjected cells as neighbors
:type: bool
:param allow_manhattan_connections: Whether to regard directly adjected cells as neighbors
:type: bool
:return: A graph with nodes that are conneceted as specified by the parameters.
:rtype: nx.Graph
"""
assert allow_euclidean_connections or allow_manhattan_connections
if hasattr(coordiniates_or_tiles, 'positions'):
coordiniates_or_tiles = coordiniates_or_tiles.positions
possible_connections = itertools.combinations(coordiniates_or_tiles, 2)
graph = nx.Graph()
for a, b in possible_connections:
diff = np.linalg.norm(np.asarray(a)-np.asarray(b))
if allow_manhattan_connections and allow_euclidean_connections and diff <= np.sqrt(2):
graph.add_edge(a, b)
elif not allow_manhattan_connections and allow_euclidean_connections and diff == np.sqrt(2):
graph.add_edge(a, b)
elif allow_manhattan_connections and not allow_euclidean_connections and diff == 1:
graph.add_edge(a, b)
return graph
class TSPBaseAgent(ABC): class TSPBaseAgent(ABC):
def __init__(self, state, agent_i, static_problem: bool = True): def __init__(self, state, agent_i, static_problem: bool = True):

View File

View File

@ -57,7 +57,7 @@ class BaseFactory(gym.Env):
self.conf = FactoryConfigParser(self._config_file) self.conf = FactoryConfigParser(self._config_file)
# Attribute Assignment # Attribute Assignment
self.level_filepath = Path(__file__).parent.parent / h.LEVELS_DIR / f'{self.conf.level_name}.txt' self.level_filepath = Path(__file__).parent.parent / h.LEVELS_DIR / f'{self.conf.level_name}.txt'
self._renderer = None # expensive - don't use it when not required ! self._renderer = None # expensive - don't use; unless required !
parsed_entities = self.conf.load_entities() parsed_entities = self.conf.load_entities()
self.map = LevelParser(self.level_filepath, parsed_entities, self.conf.pomdp_r) self.map = LevelParser(self.level_filepath, parsed_entities, self.conf.pomdp_r)
@ -173,7 +173,7 @@ class BaseFactory(gym.Env):
if not self._renderer: # lazy init if not self._renderer: # lazy init
from environment.utils.renderer import Renderer from environment.utils.renderer import Renderer
global Renderer global Renderer
self._renderer = Renderer(self.map.level_shape, view_radius=self.conf.pomdp_r, fps=20) self._renderer = Renderer(self.map.level_shape, view_radius=self.conf.pomdp_r, fps=10)
render_entities = self.state.entities.render() render_entities = self.state.entities.render()
if self.conf.pomdp_r: if self.conf.pomdp_r:

View File

@ -1,4 +1,6 @@
from os import PathLike
from pathlib import Path from pathlib import Path
from typing import Union
import yaml import yaml
@ -20,9 +22,10 @@ class FactoryConfigParser(object):
default_actions = [c.MOVE8, c.NOOP] default_actions = [c.MOVE8, c.NOOP]
default_observations = [c.WALLS, c.AGENTS] default_observations = [c.WALLS, c.AGENTS]
def __init__(self, config_path): def __init__(self, config_path, custom_modules_path: Union[None, PathLike] = None):
self.config_path = Path(config_path) self.config_path = Path(config_path)
self.config = yaml.safe_load(config_path.open()) self.custom_modules_path = Path(config_path) if custom_modules_path is not None else custom_modules_path
self.config = yaml.safe_load(self.config_path.open())
self.do_record = False self.do_record = False
def __getattr__(self, item): def __getattr__(self, item):
@ -67,7 +70,12 @@ class FactoryConfigParser(object):
entities.extend(x for x in self.entities if x != c.DEFAULTS) entities.extend(x for x in self.entities if x != c.DEFAULTS)
for entity in entities: for entity in entities:
try:
folder_path = MODULE_PATH if entity not in self.default_entites else DEFAULT_PATH folder_path = MODULE_PATH if entity not in self.default_entites else DEFAULT_PATH
folder_path = (Path(__file__) / '..' / '..' / '..' / folder_path)
entity_class = locate_and_import_class(entity, folder_path)
except AttributeError:
folder_path = self.custom_modules_path
entity_class = locate_and_import_class(entity, folder_path) entity_class = locate_and_import_class(entity, folder_path)
entity_kwargs = self.entities.get(entity, {}) entity_kwargs = self.entities.get(entity, {})
entity_symbol = entity_class.symbol if hasattr(entity_class, 'symbol') else None entity_symbol = entity_class.symbol if hasattr(entity_class, 'symbol') else None
@ -86,11 +94,15 @@ class FactoryConfigParser(object):
parsed_actions = list() parsed_actions = list()
for action in actions: for action in actions:
folder_path = MODULE_PATH if action not in base_env_actions else DEFAULT_PATH folder_path = MODULE_PATH if action not in base_env_actions else DEFAULT_PATH
try:
class_or_classes = locate_and_import_class(action, folder_path) class_or_classes = locate_and_import_class(action, folder_path)
except AttributeError:
class_or_classes = locate_and_import_class(action, self.custom_modules_path)
try: try:
parsed_actions.extend(class_or_classes) parsed_actions.extend(class_or_classes)
except TypeError: except TypeError:
parsed_actions.append(class_or_classes) parsed_actions.append(class_or_classes)
parsed_actions = [x() for x in parsed_actions] parsed_actions = [x() for x in parsed_actions]
# Observation # Observation
@ -114,7 +126,10 @@ class FactoryConfigParser(object):
for rule in rules: for rule in rules:
folder_path = MODULE_PATH if rule not in self.default_rules else DEFAULT_PATH folder_path = MODULE_PATH if rule not in self.default_rules else DEFAULT_PATH
try:
rule_class = locate_and_import_class(rule, folder_path) rule_class = locate_and_import_class(rule, folder_path)
except AttributeError:
rule_class = locate_and_import_class(rule, self.custom_modules_path)
rule_kwargs = self.rules.get(rule, {}) rule_kwargs = self.rules.get(rule, {})
rules_classes.update({rule: {'class': rule_class, 'kwargs': rule_kwargs}}) rules_classes.update({rule: {'class': rule_class, 'kwargs': rule_kwargs}})
return rules_classes return rules_classes

View File

@ -1,10 +1,9 @@
import importlib import importlib
import itertools
from collections import defaultdict from collections import defaultdict
from pathlib import PurePath, Path from pathlib import PurePath, Path
from typing import Union, Dict, List from typing import Union, Dict, List
import networkx as nx
import numpy as np import numpy as np
from numpy.typing import ArrayLike from numpy.typing import ArrayLike
@ -210,58 +209,25 @@ def asset_str(agent):
return c.AGENT, 'idle' return c.AGENT, 'idle'
def points_to_graph(coordiniates_or_tiles, allow_euclidean_connections=True, allow_manhattan_connections=True):
"""
Given a set of coordinates, this function contructs a non-directed graph, by conncting adjected points.
There are three combinations of settings:
Allow all neigbors: Distance(a, b) <= sqrt(2)
Allow only manhattan: Distance(a, b) == 1
Allow only euclidean: Distance(a, b) == sqrt(2)
:param coordiniates_or_tiles: A set of coordinates.
:type coordiniates_or_tiles: Tiles
:param allow_euclidean_connections: Whether to regard diagonal adjected cells as neighbors
:type: bool
:param allow_manhattan_connections: Whether to regard directly adjected cells as neighbors
:type: bool
:return: A graph with nodes that are conneceted as specified by the parameters.
:rtype: nx.Graph
"""
assert allow_euclidean_connections or allow_manhattan_connections
if hasattr(coordiniates_or_tiles, 'positions'):
coordiniates_or_tiles = coordiniates_or_tiles.positions
possible_connections = itertools.combinations(coordiniates_or_tiles, 2)
graph = nx.Graph()
for a, b in possible_connections:
diff = np.linalg.norm(np.asarray(a)-np.asarray(b))
if allow_manhattan_connections and allow_euclidean_connections and diff <= np.sqrt(2):
graph.add_edge(a, b)
elif not allow_manhattan_connections and allow_euclidean_connections and diff == np.sqrt(2):
graph.add_edge(a, b)
elif allow_manhattan_connections and not allow_euclidean_connections and diff == 1:
graph.add_edge(a, b)
return graph
def locate_and_import_class(class_name, folder_path: Union[str, PurePath] = ''): def locate_and_import_class(class_name, folder_path: Union[str, PurePath] = ''):
"""Locate an object by name or dotted path, importing as necessary.""" """Locate an object by name or dotted path, importing as necessary."""
import sys import sys
sys.path.append("..") sys.path.append("..")
folder_path = Path(folder_path) folder_path = Path(folder_path).resolve()
module_paths = [x for x in folder_path.rglob('*.py') if x.is_file() and '__init__' not in x.name] module_paths = [x.resolve() for x in folder_path.rglob('*.py') if x.is_file() and '__init__' not in x.name]
# possible_package_path = folder_path / '__init__.py' # possible_package_path = folder_path / '__init__.py'
# package = str(possible_package_path) if possible_package_path.exists() else None # package = str(possible_package_path) if possible_package_path.exists() else None
all_found_modules = list() all_found_modules = list()
package_pos = next(idx for idx, x in enumerate(Path(__file__).resolve().parts) if x == 'environment')
for module_path in module_paths: for module_path in module_paths:
mod = importlib.import_module('.'.join([x.replace('.py', '') for x in module_path.parts])) module_parts = [x.replace('.py', '') for idx, x in enumerate(module_path.parts) if idx >= package_pos]
mod = importlib.import_module('.'.join(module_parts))
all_found_modules.extend([x for x in dir(mod) if not(x.startswith('__') or len(x) < 2 or x.isupper()) all_found_modules.extend([x for x in dir(mod) if not(x.startswith('__') or len(x) < 2 or x.isupper())
and x not in ['Entity', 'NamedTuple', 'List', 'Rule', 'Union', 'random', 'Floor' and x not in ['Entity', 'NamedTuple', 'List', 'Rule', 'Union', 'random', 'Floor'
'TickResult', 'ActionResult', 'Action', 'Agent', 'deque', 'TickResult', 'ActionResult', 'Action', 'Agent', 'deque',
'BoundEntityMixin', 'RenderEntity', 'TemplateRule', 'defaultdict', 'BoundEntityMixin', 'RenderEntity', 'TemplateRule', 'defaultdict',
'is_move', 'Objects', 'PositionMixin', 'IsBoundMixin', 'EnvObject', 'is_move', 'Objects', 'PositionMixin', 'IsBoundMixin', 'EnvObject',
'EnvObjects',]]) 'EnvObjects']])
try: try:
model_class = mod.__getattribute__(class_name) model_class = mod.__getattribute__(class_name)
return model_class return model_class

View File

@ -191,6 +191,9 @@ class RayCaster:
self.ray_targets = self.build_ray_targets() self.ray_targets = self.build_ray_targets()
self.obs_shape_cube = np.array([self.pomdp_r, self.pomdp_r]) self.obs_shape_cube = np.array([self.pomdp_r, self.pomdp_r])
def __repr__(self):
return f'{self.__class__.__name__}({self.agent.name})'
def build_ray_targets(self): def build_ray_targets(self):
north = np.array([0, -1])*self.pomdp_r north = np.array([0, -1])*self.pomdp_r
thetas = [np.deg2rad(deg) for deg in np.linspace(-self.degs // 2, self.degs // 2, self.n_rays)[::-1]] thetas = [np.deg2rad(deg) for deg in np.linspace(-self.degs // 2, self.degs // 2, self.n_rays)[::-1]]
@ -202,12 +205,9 @@ class RayCaster:
rot_M = np.unique(np.round(rot_M @ north), axis=0) rot_M = np.unique(np.round(rot_M @ north), axis=0)
return rot_M.astype(int) return rot_M.astype(int)
@staticmethod def ray_block_cache(self, cache_dict, key, callback, ents):
def ray_block_cache(cache_dict, key, callback, ents):
if key not in cache_dict: if key not in cache_dict:
cache_dict[key] = callback() cache_dict[key] = callback()
if any(True for e in ents.pos_dict[key] if e.is_blocking_light) and not cache_dict[key]:
print()
return cache_dict[key] return cache_dict[key]
def visible_entities(self, entities): def visible_entities(self, entities):
@ -222,14 +222,23 @@ class RayCaster:
entities_hit = entities.pos_dict[(x, y)] entities_hit = entities.pos_dict[(x, y)]
hits = self.ray_block_cache(cache_blocking, hits = self.ray_block_cache(cache_blocking,
(x, y), (x, y),
lambda: any(True for e in entities_hit if e.is_blocking_light), lambda: any(e.is_blocking_light for e in entities_hit),
entities) entities)
diag_hits = all([ try:
d = next(x for x in entities_hit if 'Door' in x.name)
if d.pos in entities.pos_dict.keys():
if d.is_closed and not entities.pos_dict[d.pos]:
print()
except StopIteration:
pass
diag_hits = any([
self.ray_block_cache( self.ray_block_cache(
cache_blocking, cache_blocking,
key, key,
lambda: all(False for e in entities.pos_dict[key] if not e.is_blocking_light), # lambda: all(False for e in entities.pos_dict[key] if not e.is_blocking_light),
lambda: any(e.is_blocking_light for e in entities.pos_dict[key]),
entities) entities)
for key in ((x, y-cy), (x-cx, y)) for key in ((x, y-cy), (x-cx, y))
]) if (cx != 0 and cy != 0) else False ]) if (cx != 0 and cy != 0) else False
@ -238,13 +247,6 @@ class RayCaster:
if hits or diag_hits: if hits or diag_hits:
break break
rx, ry = x, y rx, ry = x, y
try:
d = next(x for x in visible if 'Door' in x.name)
v = [x for x in visible if tuple(np.subtract(x.pos, d.pos)) in [(1, 0), (0, 1), (-1, 0), (0, -1)] and x.name.startswith('Floor')]
if len(v) > 2:
pass
except StopIteration:
pass
return visible return visible
def get_rays(self): def get_rays(self):

View File

@ -3,12 +3,12 @@ import sys
from pathlib import Path from pathlib import Path
from collections import deque from collections import deque
from itertools import product from itertools import product
import numpy as np
import pygame import pygame
from typing import Tuple, Union from typing import Tuple, Union
import time import time
import torch
from environment.utils.render import RenderEntity from environment.utils.render import RenderEntity
AGENT: str = 'agent' AGENT: str = 'agent'
@ -133,7 +133,8 @@ class Renderer:
pygame.display.flip() pygame.display.flip()
self.clock.tick(self.fps) self.clock.tick(self.fps)
rgb_obs = pygame.surfarray.array3d(self.screen) rgb_obs = pygame.surfarray.array3d(self.screen)
return torch.from_numpy(rgb_obs).permute(2, 0, 1) return np.transpose(rgb_obs, (2, 0, 1))
# return torch.from_numpy(rgb_obs).permute(2, 0, 1)
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -29,8 +29,9 @@ Entities:
Agents: Agents:
Wolfgang: Wolfgang:
Actions: Actions:
- Move8 - Noop
- DoorUse - Noop
- Noop
- CleanUp - CleanUp
Observations: Observations:
- Self - Self

View File

@ -1,12 +0,0 @@
numpy~=1.24.3
tqdm~=4.60.0
pandas~=1.2.3
seaborn~=0.12.2
matplotlib>=3.3.4
pygame~=2.0.1
networkx>=2.6.3
simplejson~=3.17.2
PyYAML~=5.3.1
natsort~=7.1.1
torch~=1.10.0
gymnasium~=0.28.1

View File

@ -4,29 +4,34 @@ this_directory = Path(__file__).parent
long_description = (this_directory / "README.md").read_text() long_description = (this_directory / "README.md").read_text()
setup(name='Marl-Neon-Grid', setup(name='Marl-Factory-Grid',
version='0.1.4.4', version='0.0.5',
description='A collection of MARL gridworlds to study coordination and cooperation.', description='A framework to research MARL agents in various setings.',
author='Robert Müller', author='Steffen Illium',
author_email='steffen.illium@ifi.lmu.de', author_email='steffen.illium@ifi.lmu.de',
url='https://github.com/romue404/marl-neon-grid', url='https://github.com/illiumst/marl-factory-grid/import',
license='MIT', license='MIT',
keywords=[ keywords=[
'artificial intelligence', 'artificial intelligence',
'pytorch', 'pytorch',
'multiagent reinforcement learning', 'multiagent reinforcement learning',
'simulation' 'simulation',
'emergence',
'gymnasium',
'environment',
], ],
classifiers=[ classifiers=[
'Development Status :: 4 - Beta', 'Development Status :: 4 - Beta',
'Intended Audience :: Developers', 'Intended Audience :: Developers',
'Topic :: Scientific/Engineering :: Artificial Intelligence', 'Topic :: Scientific/Engineering :: Artificial Intelligence',
'License :: OSI Approved :: MIT License', 'License :: OSI Approved :: MIT License',
'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.11',
], ],
long_description=long_description, long_description=long_description,
long_description_content_type='text/markdown', long_description_content_type='text/markdown',
packages=find_packages(exclude=['examples']), packages=find_packages(exclude=['examples']),
include_package_data=True, include_package_data=True,
install_requires=['numpy', 'pygame>=2.0', 'numba>=0.56', 'gymnasium>=0.26', 'seaborn', 'pandas'] install_requires=['numpy', 'pygame>=2.0', 'numba>=0.56', 'gymnasium>=0.26', 'seaborn', 'pandas',
'pyyaml', 'networkx', 'torch', 'tqdm']
) )