diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..7906f53 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +global-include *.txt *.png *.jpg \ No newline at end of file diff --git a/README.md b/README.md index 6b16c79..9b29f31 100644 --- a/README.md +++ b/README.md @@ -3,14 +3,11 @@ Tackling emergent dysfunctions (EDYs) in cooperation with Fraunhofer-IKS ## Setup -1. Make sure to install `virtualenv` using `pip install virtualenv` -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` -## +Just install this environment by `pip install marl-factory-grid`. ## First Steps + ### Quickstart Most of the env. objects (entites, rules and assets) can be loaded automatically. Just define what your environment needs in a *yaml*-configfile like: diff --git a/algorithms/static/TSP_base_agent.py b/algorithms/static/TSP_base_agent.py index 0bdd8c4..eabd99d 100644 --- a/algorithms/static/TSP_base_agent.py +++ b/algorithms/static/TSP_base_agent.py @@ -1,10 +1,11 @@ +import itertools from random import choice import numpy as np +import networkx as nx 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 environment import constants as c @@ -15,6 +16,41 @@ from abc import abstractmethod, ABC 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): def __init__(self, state, agent_i, static_problem: bool = True): diff --git a/environment/assets/agent/__init__.py b/environment/assets/agent/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/environment/factory.py b/environment/factory.py index cc56600..a7251f6 100644 --- a/environment/factory.py +++ b/environment/factory.py @@ -57,7 +57,7 @@ class BaseFactory(gym.Env): self.conf = FactoryConfigParser(self._config_file) # Attribute Assignment 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() 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 from environment.utils.renderer import 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() if self.conf.pomdp_r: diff --git a/environment/utils/config_parser.py b/environment/utils/config_parser.py index fe5db31..ffb27d5 100644 --- a/environment/utils/config_parser.py +++ b/environment/utils/config_parser.py @@ -1,4 +1,6 @@ +from os import PathLike from pathlib import Path +from typing import Union import yaml @@ -20,9 +22,10 @@ class FactoryConfigParser(object): default_actions = [c.MOVE8, c.NOOP] 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 = 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 def __getattr__(self, item): @@ -67,8 +70,13 @@ class FactoryConfigParser(object): entities.extend(x for x in self.entities if x != c.DEFAULTS) for entity in entities: - folder_path = MODULE_PATH if entity not in self.default_entites else DEFAULT_PATH - entity_class = locate_and_import_class(entity, folder_path) + try: + 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_kwargs = self.entities.get(entity, {}) entity_symbol = entity_class.symbol if hasattr(entity_class, 'symbol') else None entity_classes.update({entity: {'class': entity_class, 'kwargs': entity_kwargs, 'symbol': entity_symbol}}) @@ -86,11 +94,15 @@ class FactoryConfigParser(object): parsed_actions = list() for action in actions: folder_path = MODULE_PATH if action not in base_env_actions else DEFAULT_PATH - class_or_classes = locate_and_import_class(action, folder_path) + try: + 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: parsed_actions.extend(class_or_classes) except TypeError: parsed_actions.append(class_or_classes) + parsed_actions = [x() for x in parsed_actions] # Observation @@ -114,7 +126,10 @@ class FactoryConfigParser(object): for rule in rules: folder_path = MODULE_PATH if rule not in self.default_rules else DEFAULT_PATH - rule_class = locate_and_import_class(rule, folder_path) + try: + 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, {}) rules_classes.update({rule: {'class': rule_class, 'kwargs': rule_kwargs}}) return rules_classes diff --git a/environment/utils/helpers.py b/environment/utils/helpers.py index bdcca7c..2650729 100644 --- a/environment/utils/helpers.py +++ b/environment/utils/helpers.py @@ -1,10 +1,9 @@ import importlib -import itertools + from collections import defaultdict from pathlib import PurePath, Path from typing import Union, Dict, List -import networkx as nx import numpy as np from numpy.typing import ArrayLike @@ -210,58 +209,25 @@ def asset_str(agent): 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] = ''): """Locate an object by name or dotted path, importing as necessary.""" import sys sys.path.append("..") - folder_path = Path(folder_path) - module_paths = [x for x in folder_path.rglob('*.py') if x.is_file() and '__init__' not in x.name] + folder_path = Path(folder_path).resolve() + 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' # package = str(possible_package_path) if possible_package_path.exists() else None 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: - 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()) and x not in ['Entity', 'NamedTuple', 'List', 'Rule', 'Union', 'random', 'Floor' 'TickResult', 'ActionResult', 'Action', 'Agent', 'deque', 'BoundEntityMixin', 'RenderEntity', 'TemplateRule', 'defaultdict', 'is_move', 'Objects', 'PositionMixin', 'IsBoundMixin', 'EnvObject', - 'EnvObjects',]]) + 'EnvObjects']]) try: model_class = mod.__getattribute__(class_name) return model_class @@ -269,4 +235,4 @@ def locate_and_import_class(class_name, folder_path: Union[str, PurePath] = ''): continue raise AttributeError(f'Class "{class_name}" was not found!!!"\n' f'Check the {folder_path.name} name.\n' - f'Possible Options are:\n{set(all_found_modules)}') \ No newline at end of file + f'Possible Options are:\n{set(all_found_modules)}') diff --git a/environment/utils/observation_builder.py b/environment/utils/observation_builder.py index 4421aff..153e11d 100644 --- a/environment/utils/observation_builder.py +++ b/environment/utils/observation_builder.py @@ -191,6 +191,9 @@ class RayCaster: self.ray_targets = self.build_ray_targets() 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): 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]] @@ -202,12 +205,9 @@ class RayCaster: rot_M = np.unique(np.round(rot_M @ north), axis=0) return rot_M.astype(int) - @staticmethod - def ray_block_cache(cache_dict, key, callback, ents): + def ray_block_cache(self, cache_dict, key, callback, ents): if key not in cache_dict: 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] def visible_entities(self, entities): @@ -222,14 +222,23 @@ class RayCaster: entities_hit = entities.pos_dict[(x, y)] hits = self.ray_block_cache(cache_blocking, (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) - 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( cache_blocking, 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) for key in ((x, y-cy), (x-cx, y)) ]) if (cx != 0 and cy != 0) else False @@ -238,13 +247,6 @@ class RayCaster: if hits or diag_hits: break 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 def get_rays(self): diff --git a/environment/utils/renderer.py b/environment/utils/renderer.py index 2275c71..ea2c9b7 100644 --- a/environment/utils/renderer.py +++ b/environment/utils/renderer.py @@ -3,12 +3,12 @@ import sys from pathlib import Path from collections import deque from itertools import product + +import numpy as np import pygame from typing import Tuple, Union import time -import torch - from environment.utils.render import RenderEntity AGENT: str = 'agent' @@ -133,7 +133,8 @@ class Renderer: pygame.display.flip() self.clock.tick(self.fps) 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__': diff --git a/quickstart/all_test_config.yaml b/quickstart/all_test_config.yaml index ae7f6c5..9e491ef 100644 --- a/quickstart/all_test_config.yaml +++ b/quickstart/all_test_config.yaml @@ -29,8 +29,9 @@ Entities: Agents: Wolfgang: Actions: - - Move8 - - DoorUse + - Noop + - Noop + - Noop - CleanUp Observations: - Self diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 40c6cd6..0000000 --- a/requirements.txt +++ /dev/null @@ -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 \ No newline at end of file diff --git a/setup.py b/setup.py index f76b435..f7c2889 100644 --- a/setup.py +++ b/setup.py @@ -4,29 +4,34 @@ this_directory = Path(__file__).parent long_description = (this_directory / "README.md").read_text() -setup(name='Marl-Neon-Grid', - version='0.1.4.4', - description='A collection of MARL gridworlds to study coordination and cooperation.', - author='Robert Müller', +setup(name='Marl-Factory-Grid', + version='0.0.5', + description='A framework to research MARL agents in various setings.', + author='Steffen Illium', 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', keywords=[ 'artificial intelligence', 'pytorch', 'multiagent reinforcement learning', - 'simulation' + 'simulation', + 'emergence', + 'gymnasium', + 'environment', + ], classifiers=[ 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', 'Topic :: Scientific/Engineering :: Artificial Intelligence', 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.11', ], long_description=long_description, long_description_content_type='text/markdown', packages=find_packages(exclude=['examples']), include_package_data=True, - install_requires=['numpy', 'pygame>=2.0', 'numba>=0.56', 'gymnasium>=0.26', 'seaborn', 'pandas'] - ) \ No newline at end of file + install_requires=['numpy', 'pygame>=2.0', 'numba>=0.56', 'gymnasium>=0.26', 'seaborn', 'pandas', + 'pyyaml', 'networkx', 'torch', 'tqdm'] + )