From 5476f617c6750cd3d6b9dba84fd0e9274d89eb03 Mon Sep 17 00:00:00 2001 From: Chanumask Date: Fri, 6 Sep 2024 11:01:42 +0200 Subject: [PATCH] added changes from code submission branch and coin entity --- .gitlab-ci.yml | 2 +- README.md | 2 +- README_submission.md | 77 ++++ docs/source/conf.py | 2 +- marl_factory_grid/__init__.py | 2 +- marl_factory_grid/algorithms/marl/__init__.py | 1 - marl_factory_grid/algorithms/rl/__init__.py | 1 + marl_factory_grid/algorithms/rl/a2c_coin.py | 297 +++++++++++++++ marl_factory_grid/algorithms/rl/base_a2c.py | 112 ++++++ .../algorithms/{marl => rl}/base_ac.py | 2 +- .../dirt_quadrant_config.yaml | 4 +- .../two_rooms_one_door_modified_config.yaml | 4 +- .../configs/dirt_quadrant_config.yaml | 4 +- .../{marl => rl}/configs/environment_changes | 0 .../two_rooms_one_door_modified_config.yaml | 4 +- marl_factory_grid/algorithms/rl/constants.py | 37 ++ .../algorithms/{marl => rl}/iac.py | 4 +- .../algorithms/{marl => rl}/mappo.py | 6 +- .../algorithms/{marl => rl}/memory.py | 0 .../algorithms/{marl => rl}/networks.py | 0 .../algorithms/{marl => rl}/seac.py | 6 +- .../algorithms/{marl => rl}/snac.py | 4 +- marl_factory_grid/algorithms/rl/utils.py | 337 ++++++++++++++++++ .../algorithms/static/TSP_coin_agent.py | 40 +++ marl_factory_grid/configs/test_config.yaml | 46 ++- marl_factory_grid/environment/factory.py | 28 +- marl_factory_grid/environment/rules.py | 34 +- marl_factory_grid/modules/clean_up/groups.py | 17 +- marl_factory_grid/modules/coins/__init__.py | 4 + marl_factory_grid/modules/coins/actions.py | 36 ++ marl_factory_grid/modules/coins/coinpiles.png | Bin 0 -> 103379 bytes marl_factory_grid/modules/coins/constants.py | 11 + marl_factory_grid/modules/coins/entitites.py | 46 +++ marl_factory_grid/modules/coins/groups.py | 108 ++++++ marl_factory_grid/modules/coins/rules.py | 59 +++ .../utils/plotting/plot_single_runs.py | 125 +++++++ marl_factory_grid/utils/renderer.py | 1 - marl_factory_grid/utils/states.py | 10 +- studies/marl_adapted.py | 15 +- studies/normalization_study.py | 2 +- studies/viz_policy.py | 2 +- test_run.py | 5 +- 42 files changed, 1429 insertions(+), 68 deletions(-) create mode 100644 README_submission.md delete mode 100644 marl_factory_grid/algorithms/marl/__init__.py create mode 100644 marl_factory_grid/algorithms/rl/__init__.py create mode 100644 marl_factory_grid/algorithms/rl/a2c_coin.py create mode 100644 marl_factory_grid/algorithms/rl/base_a2c.py rename marl_factory_grid/algorithms/{marl => rl}/base_ac.py (99%) rename marl_factory_grid/algorithms/{marl => rl}/configs/MultiAgentConfigs/dirt_quadrant_config.yaml (90%) rename marl_factory_grid/algorithms/{marl => rl}/configs/MultiAgentConfigs/two_rooms_one_door_modified_config.yaml (91%) rename marl_factory_grid/algorithms/{marl => rl}/configs/dirt_quadrant_config.yaml (90%) rename marl_factory_grid/algorithms/{marl => rl}/configs/environment_changes (100%) rename marl_factory_grid/algorithms/{marl => rl}/configs/two_rooms_one_door_modified_config.yaml (90%) create mode 100644 marl_factory_grid/algorithms/rl/constants.py rename marl_factory_grid/algorithms/{marl => rl}/iac.py (92%) rename marl_factory_grid/algorithms/{marl => rl}/mappo.py (93%) rename marl_factory_grid/algorithms/{marl => rl}/memory.py (100%) rename marl_factory_grid/algorithms/{marl => rl}/networks.py (100%) rename marl_factory_grid/algorithms/{marl => rl}/seac.py (91%) rename marl_factory_grid/algorithms/{marl => rl}/snac.py (90%) create mode 100644 marl_factory_grid/algorithms/rl/utils.py create mode 100644 marl_factory_grid/algorithms/static/TSP_coin_agent.py create mode 100644 marl_factory_grid/modules/coins/__init__.py create mode 100644 marl_factory_grid/modules/coins/actions.py create mode 100644 marl_factory_grid/modules/coins/coinpiles.png create mode 100644 marl_factory_grid/modules/coins/constants.py create mode 100644 marl_factory_grid/modules/coins/entitites.py create mode 100644 marl_factory_grid/modules/coins/groups.py create mode 100644 marl_factory_grid/modules/coins/rules.py diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 47b5268..7d13a27 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -10,7 +10,7 @@ build-job: # This job runs in the build stage, which runs first. variables: TWINE_USERNAME: $USER_NAME TWINE_PASSWORD: $API_KEY - TWINE_REPOSITORY: marl-factory-grid + TWINE_REPOSITORY: rl-factory-grid image: python:slim script: diff --git a/README.md b/README.md index b93f9eb..45eea00 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ Existing modules include a variety of functionalities within the environment: - [Agents](marl_factory_grid/algorithms) implement either static strategies or learning algorithms based on the specific configuration. -- Their action set includes opening [door entities](marl_factory_grid/modules/doors/entitites.py), cleaning +- Their action set includes opening [door entities](marl_factory_grid/modules/doors/entitites.py), collecting [coins](marl_factory_grid/modules/coins/coin cleaning [dirt](marl_factory_grid/modules/clean_up/entitites.py), picking up [items](marl_factory_grid/modules/items/entitites.py) and delivering them to designated drop-off locations. diff --git a/README_submission.md b/README_submission.md new file mode 100644 index 0000000..12e6513 --- /dev/null +++ b/README_submission.md @@ -0,0 +1,77 @@ +# About EDYS + +## Tackling emergent dysfunctions (EDYs) in cooperation with Fraunhofer-IKS. + +Collaborating with Fraunhofer-IKS, this project is dedicated to investigating Emergent Dysfunctions (EDYs) within +multi-agent environments. In multi-agent reinforcement learning (MARL), a population of agents learns by interacting +with each other in a shared environment and adapt their behavior based on the feedback they receive from the environment +and the actions of other agents. + +In this context, emergent behavior describes spontaneous behaviors resulting from interactions among agents and +environmental stimuli, rather than explicit programming. This promotes natural, adaptable behavior, increases system +unpredictability for dynamic learning , enables diverse strategies, and encourages collective intelligence for complex +problem-solving. However, the complex dynamics of the environment also give rise to emerging dysfunctions—unexpected +issues from agent interactions. This research aims to enhance our understanding of EDYs and their impact on multi-agent +systems. + +### Project Objectives: + +- Create an environment that provokes emerging dysfunctions. + + - This is achieved by creating a high level of background noise in the domain, where various entities perform + diverse tasks, resulting in a deliberately chaotic dynamic. + - The goal is to observe and analyze naturally occurring emergent dysfunctions within the complexity generated in + this dynamic environment. + + +- Observational Framework: + + - The project introduces an environment that is designed to capture dysfunctions as they naturally occur. + - The environment allows for continuous monitoring of agent behaviors, actions, and interactions. + - Tracking emergent dysfunctions in real-time provides valuable data for analysis and understanding. + + +- Compatibility + - The Framework allows learning entities from different manufacturers and projects with varying representations + of actions and observations to interact seamlessly within the environment. + + +## Setup + +Install this environment using `pip install marl-factory-grid`. For more information refer +to ['installation'](docs/source/installation.rst). + +## Usage + +The environment is configured to automatically load necessary objects, including entities, rules, and assets, based on your requirements. +You can utilize existing configurations to replicate the experiments from [this paper](PAPER). + +- Preconfigured Studies: + The studies folder contains predefined studies that can be used to replicate the experiments. + These studies provide a structured way to validate and analyze the outcomes observed in different scenarios. + - Creating your own scenarios: + If you want to use the environment with custom entities, rules or levels refer to the [complete repository](). + + + +Existing modules include a variety of functionalities within the environment: + +- [Agents](marl_factory_grid/algorithms) implement either static strategies or learning algorithms based on the specific + configuration. +- Their action set includes opening [door entities](marl_factory_grid/modules/doors/entitites.py), collecting [coins](marl_factory_grid/modules/coins/coin cleaning + [dirt](marl_factory_grid/modules/clean_up/entitites.py), picking + up [items](marl_factory_grid/modules/items/entitites.py) and + delivering them to designated drop-off locations. +- Agents are equipped with a [battery](marl_factory_grid/modules/batteries/entitites.py) that gradually depletes over + time if not charged at a chargepod. +- The [maintainer](marl_factory_grid/modules/maintenance/entities.py) aims to + repair [machines](marl_factory_grid/modules/machines/entitites.py) that lose health over time. + + +## Limitations + +The provided code and documentation are tailored for replicating and validating experiments as described in the paper. +Modifications to the environment, such as adding new entities, creating additional rules, or customizing behavior beyond the provided scope are not supported in this release. +If you are interested in accessing the complete project, including features not covered in this release, refer to the [full repository](LINK FULL REPO). + +For further details on running the experiments, please consult the relevant documentation provided in the studies' folder. diff --git a/docs/source/conf.py b/docs/source/conf.py index 8d0a105..b7fd9ef 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -6,7 +6,7 @@ # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information -project = 'marl-factory-grid' +project = 'rl-factory-grid' copyright = '2023, Steffen Illium, Robert Mueller, Joel Friedrich' author = 'Steffen Illium, Robert Mueller, Joel Friedrich' release = '2.5.0' diff --git a/marl_factory_grid/__init__.py b/marl_factory_grid/__init__.py index d8f4799..cc4ebeb 100644 --- a/marl_factory_grid/__init__.py +++ b/marl_factory_grid/__init__.py @@ -1,7 +1,7 @@ from .quickstart import init from marl_factory_grid.environment.factory import Factory """ -Main module of the 'marl-factory-grid'-environment. +Main module of the 'rl-factory-grid'-environment. Configure the :class:.Factory with any 'conf.yaml' file. Examples can be found in :module:.levels . """ diff --git a/marl_factory_grid/algorithms/marl/__init__.py b/marl_factory_grid/algorithms/marl/__init__.py deleted file mode 100644 index a4c30ef..0000000 --- a/marl_factory_grid/algorithms/marl/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from marl_factory_grid.algorithms.marl.memory import MARLActorCriticMemory diff --git a/marl_factory_grid/algorithms/rl/__init__.py b/marl_factory_grid/algorithms/rl/__init__.py new file mode 100644 index 0000000..ecc0a81 --- /dev/null +++ b/marl_factory_grid/algorithms/rl/__init__.py @@ -0,0 +1 @@ +from marl_factory_grid.algorithms.rl.memory import MARLActorCriticMemory diff --git a/marl_factory_grid/algorithms/rl/a2c_coin.py b/marl_factory_grid/algorithms/rl/a2c_coin.py new file mode 100644 index 0000000..f2aa31b --- /dev/null +++ b/marl_factory_grid/algorithms/rl/a2c_coin.py @@ -0,0 +1,297 @@ +import os +import torch +from typing import Union, List +import numpy as np +from tqdm import tqdm + +from marl_factory_grid.algorithms.rl.base_a2c import PolicyGradient +from marl_factory_grid.algorithms.rl.constants import Names +from marl_factory_grid.algorithms.rl.utils import transform_observations, _as_torch, is_door_close, \ + get_coin_piles_positions, update_target_pile, update_ordered_coin_piles, get_all_collected_coin_piles, \ + distribute_indices, set_agents_spawnpoints, get_ordered_coin_piles, handle_finished_episode, save_configs, \ + save_agent_models, get_all_observations, get_agents_positions +from marl_factory_grid.algorithms.utils import add_env_props +from marl_factory_grid.utils.plotting.plot_single_runs import plot_action_maps, plot_reward_development, \ + create_info_maps + +nms = Names +ListOrTensor = Union[List, torch.Tensor] + + +class A2C: + def __init__(self, train_cfg, eval_cfg): + self.results_path = None + self.agents = None + self.act_dim = None + self.obs_dim = None + self.factory = add_env_props(train_cfg) + self.eval_factory = add_env_props(eval_cfg) + self.__training = True + self.train_cfg = train_cfg + self.eval_cfg = eval_cfg + self.cfg = train_cfg + self.n_agents = train_cfg[nms.ENV][nms.N_AGENTS] + self.setup() + self.reward_development = [] + self.action_probabilities = {agent_idx: [] for agent_idx in range(self.n_agents)} + + def setup(self): + """ Initialize agents and create entry for run results according to configuration """ + self.obs_dim = 2 + 2 * len(get_coin_piles_positions(self.factory)) if self.cfg[nms.ALGORITHM][ + nms.PILE_OBSERVABILITY] == nms.ALL else 4 + self.act_dim = 4 # The 4 movement directions + self.agents = [PolicyGradient(self.factory, agent_id=i, obs_dim=self.obs_dim, act_dim=self.act_dim) for i in + range(self.n_agents)] + + if self.cfg[nms.ENV][nms.SAVE_AND_LOG]: + # Define study_out_path and check if it exists + base_dir = os.path.dirname(os.path.abspath(__file__)) # Directory of the script + study_out_path = os.path.join(base_dir, '../../../study_out') + study_out_path = os.path.abspath(study_out_path) + + if not os.path.exists(study_out_path): + raise FileNotFoundError(f"The directory {study_out_path} does not exist.") + + # Create results folder + runs = os.listdir(study_out_path) + run_numbers = [int(run[3:]) for run in runs if run[:3] == "run"] + next_run_number = max(run_numbers) + 1 if run_numbers else 0 + self.results_path = os.path.join(study_out_path, f"run{next_run_number}") + os.mkdir(self.results_path) + + # Save settings in results folder + save_configs(self.results_path, self.cfg, self.factory.conf, self.eval_factory.conf) + + def set_cfg(self, eval=False): + if eval: + self.cfg = self.eval_cfg + else: + self.cfg = self.train_cfg + + def load_agents(self, runs_list): + """ Initialize networks with parameters of already trained agents """ + for idx, run in enumerate(runs_list): + run_path = f"./study_out/{run}" + self.agents[idx].pi.load_model_parameters(f"{run_path}/PolicyNet_model_parameters.pth") + self.agents[idx].vf.load_model_parameters(f"{run_path}/ValueNet_model_parameters.pth") + + @torch.no_grad() + def train_loop(self): + """ Function for training agents """ + env = self.factory + n_steps, max_steps = [self.cfg[nms.ALGORITHM][k] for k in [nms.N_STEPS, nms.MAX_STEPS]] + global_steps, episode = 0, 0 + indices = distribute_indices(env, self.cfg, self.n_agents) + coin_piles_positions = get_coin_piles_positions(env) + target_pile = [partition[0] for partition in + indices] # list of pointers that point to the current target pile for each agent + collected_coin_piles = [{pos: False for pos in coin_piles_positions} for _ in range(self.n_agents)] + + pbar = tqdm(total=max_steps) + while global_steps < max_steps: + _ = env.reset() + if self.cfg[nms.ENV][nms.TRAIN_RENDER]: + env.render() + set_agents_spawnpoints(env, self.n_agents) + ordered_coin_piles = get_ordered_coin_piles(env, collected_coin_piles, self.cfg, self.n_agents) + # Reset current target pile at episode begin if all piles have to be collected in one episode + if self.cfg[nms.ALGORITHM][nms.PILE_ALL_DONE] == nms.ALL: + target_pile = [partition[0] for partition in indices] + collected_coin_piles = [{pos: False for pos in coin_piles_positions} for _ in range(self.n_agents)] + + # Supply each agent with its local observation + obs = transform_observations(env, ordered_coin_piles, target_pile, self.cfg, self.n_agents) + done, rew_log = [False] * self.n_agents, 0 + + while not all(done): + action = self.use_door_or_move(env, obs, collected_coin_piles) \ + if nms.DOORS in env.state.entities.keys() else self.get_actions(obs) + _, next_obs, reward, done, info = env.step(action) + next_obs = transform_observations(env, ordered_coin_piles, target_pile, self.cfg, self.n_agents) + + # Handle case where agent is on field with coin + reward, done = self.handle_coin(env, collected_coin_piles, ordered_coin_piles, target_pile, indices, + reward, done) + + if n_steps != 0 and (global_steps + 1) % n_steps == 0: done = True + + done = [done] * self.n_agents if isinstance(done, bool) else done + for ag_i, agent in enumerate(self.agents): + if action[ag_i] in range(self.act_dim): + # Add agent results into respective rollout buffers + agent._episode[-1] = (next_obs[ag_i], action[ag_i], reward[ag_i], agent._episode[-1][-1]) + + # Visualize state update + if self.cfg[nms.ENV][nms.TRAIN_RENDER]: env.render() + + obs = next_obs + + if all(done): handle_finished_episode(obs, self.agents, self.cfg) + + global_steps += 1 + rew_log += sum(reward) + + if global_steps >= max_steps: break + + self.reward_development.append(rew_log) + episode += 1 + pbar.update(global_steps - pbar.n) + + pbar.close() + if self.cfg[nms.ENV][nms.SAVE_AND_LOG]: + plot_reward_development(self.reward_development, self.results_path) + create_info_maps(env, get_all_observations(env, self.cfg, self.n_agents), + get_coin_piles_positions(env), self.results_path, self.agents, self.act_dim, self) + save_agent_models(self.results_path, self.agents) + plot_action_maps(env, [self], self.results_path) + + @torch.inference_mode(True) + def eval_loop(self, n_episodes): + """ Function for performing inference """ + env = self.eval_factory + self.set_cfg(eval=True) + episode, results = 0, [] + coin_piles_positions = get_coin_piles_positions(env) + indices = distribute_indices(env, self.cfg, self.n_agents) + target_pile = [partition[0] for partition in + indices] # list of pointers that point to the current target pile for each agent + if self.cfg[nms.ALGORITHM][nms.PILE_ALL_DONE] == nms.DISTRIBUTED: + collected_coin_piles = [{coin_piles_positions[idx]: False for idx in indices[i]} for i in + range(self.n_agents)] + else: collected_coin_piles = [{pos: False for pos in coin_piles_positions} for _ in range(self.n_agents)] + + while episode < n_episodes: + _ = env.reset() + set_agents_spawnpoints(env, self.n_agents) + if self.cfg[nms.ENV][nms.EVAL_RENDER]: + # Don't render auxiliary piles + if self.cfg[nms.ALGORITHM][nms.AUXILIARY_PILES]: + auxiliary_piles = [pile for idx, pile in enumerate(env.state.entities[nms.COIN_PILES]) if + idx % 2 == 0] + for pile in auxiliary_piles: + pile.set_new_amount(0) + env.render() + env._renderer.fps = 5 # Slow down agent movement + + # Reset current target pile at episode begin if all piles have to be collected in one episode + if self.cfg[nms.ALGORITHM][nms.PILE_ALL_DONE] in [nms.ALL, nms.DISTRIBUTED, nms.SHARED]: + target_pile = [partition[0] for partition in indices] + if self.cfg[nms.ALGORITHM][nms.PILE_ALL_DONE] == nms.DISTRIBUTED: + collected_coin_piles = [{coin_piles_positions[idx]: False for idx in indices[i]} for i in + range(self.n_agents)] + else: collected_coin_piles = [{pos: False for pos in coin_piles_positions} for _ in range(self.n_agents)] + + ordered_coin_piles = get_ordered_coin_piles(env, collected_coin_piles, self.cfg, self.n_agents) + + # Supply each agent with its local observation + obs = transform_observations(env, ordered_coin_piles, target_pile, self.cfg, self.n_agents) + done, rew_log, eps_rew = [False] * self.n_agents, 0, torch.zeros(self.n_agents) + + while not all(done): + action = self.use_door_or_move(env, obs, collected_coin_piles, det=True) \ + if nms.DOORS in env.state.entities.keys() else self.execute_policy(obs, env, + collected_coin_piles) # zero exploration + _, next_obs, reward, done, info = env.step(action) + + # Handle case where agent is on field with coin + reward, done = self.handle_coin(env, collected_coin_piles, ordered_coin_piles, target_pile, indices, + reward, done) + + # Get transformed next_obs that might have been updated because of handle_coin + next_obs = transform_observations(env, ordered_coin_piles, target_pile, self.cfg, self.n_agents) + + done = [done] * self.n_agents if isinstance(done, bool) else done + + if self.cfg[nms.ENV][nms.EVAL_RENDER]: env.render() + + obs = next_obs + + episode += 1 + + # -------------------------------------- HELPER FUNCTIONS ------------------------------------------------- # + + def get_actions(self, observations) -> ListOrTensor: + """ Given local observations, get actions for both agents """ + actions = [agent.step(_as_torch(observations[ag_i]).view(-1).to(torch.float32)) for ag_i, agent in + enumerate(self.agents)] + return actions + + def execute_policy(self, observations, env, collected_coin_piles) -> ListOrTensor: + """ Execute agent policies deterministically for inference """ + actions = [agent.policy(_as_torch(observations[ag_i]).view(-1).to(torch.float32)) for ag_i, agent in + enumerate(self.agents)] + for agent_idx in range(self.n_agents): + if all(collected_coin_piles[agent_idx].values()): + actions[agent_idx] = np.array(next( + action_i for action_i, a in enumerate(env.state[nms.AGENT][agent_idx].actions) if + a.name == nms.NOOP)) + return actions + + def use_door_or_move(self, env, obs, collected_coin_piles, det=False): + """ Function that handles automatic actions like door opening and forced Noop""" + action = [] + for agent_idx, agent in enumerate(self.agents): + agent_obs = _as_torch((obs)[agent_idx]).view(-1).to(torch.float32) + # Use Noop operation if agent already reached its target. (Only relevant for two-rooms setting) + if all(collected_coin_piles[agent_idx].values()): + action.append(next(action_i for action_i, a in enumerate(env.state[nms.AGENT][agent_idx].actions) if + a.name == nms.NOOP)) + if not det: + # Include agent experience entry manually + agent._episode.append((None, None, None, agent.vf(agent_obs))) + else: + if door := is_door_close(env, agent_idx): + if door.is_closed: + action.append(next( + action_i for action_i, a in enumerate(env.state[nms.AGENT][agent_idx].actions) if + a.name == nms.USE_DOOR)) + # Don't include action in agent experience + else: + if det: action.append(int(agent.pi(agent_obs, det=True)[0])) + else: action.append(int(agent.step(agent_obs))) + else: + if det: action.append(int(agent.pi(agent_obs, det=True)[0])) + else: action.append(int(agent.step(agent_obs))) + return action + + def handle_coin(self, env, collected_coin_piles, ordered_coin_piles, target_pile, indices, reward, done): + """ Check if agent moved on field with coin. If that is the case collect coin automatically """ + agents_positions = get_agents_positions(env, self.n_agents) + coin_piles_positions = get_coin_piles_positions(env) + if any([True for pos in agents_positions if pos in coin_piles_positions]): + # Only simulate collecting the coin + for idx, pos in enumerate(agents_positions): + if pos in collected_coin_piles[idx].keys() and not collected_coin_piles[idx][pos]: + + # If coin piles should be collected in a specific order + if ordered_coin_piles[idx]: + if pos == ordered_coin_piles[idx][target_pile[idx]]: + reward[idx] += 50 + collected_coin_piles[idx][pos] = True + # Set pointer to next coin pile + update_target_pile(env, idx, target_pile, indices, self.cfg) + update_ordered_coin_piles(idx, collected_coin_piles, ordered_coin_piles, env, + self.cfg, self.n_agents) + if self.cfg[nms.ALGORITHM][nms.PILE_ALL_DONE] == nms.SINGLE: + done = True + if all(collected_coin_piles[idx].values()): + # Reset collected_coin_piles indicator + for pos in coin_piles_positions: + collected_coin_piles[idx][pos] = False + else: + reward[idx] += 50 + collected_coin_piles[idx][pos] = True + + # Indicate that renderer can hide coin pile + coin_at_position = env.state[nms.COIN_PILES].by_pos(pos) + coin_at_position[0].set_new_amount(0) + + if self.cfg[nms.ALGORITHM][nms.PILE_ALL_DONE] in [nms.ALL, nms.DISTRIBUTED]: + if all([all(collected_coin_piles[i].values()) for i in range(self.n_agents)]): + done = True + elif self.cfg[nms.ALGORITHM][nms.PILE_ALL_DONE] == nms.SHARED: + # End episode if both agents together have collected all coin piles + if all(get_all_collected_coin_piles(coin_piles_positions, collected_coin_piles, self.n_agents).values()): + done = True + + return reward, done diff --git a/marl_factory_grid/algorithms/rl/base_a2c.py b/marl_factory_grid/algorithms/rl/base_a2c.py new file mode 100644 index 0000000..1406d5f --- /dev/null +++ b/marl_factory_grid/algorithms/rl/base_a2c.py @@ -0,0 +1,112 @@ +import numpy as np +import torch as th +import scipy as sp +from collections import deque +from torch import nn + +cumulate_discount = lambda x, gamma: sp.signal.lfilter([1], [1, - gamma], x[::-1], axis=0)[::-1] + + +class Net(th.nn.Module): + def __init__(self, shape, activation, lr): + super().__init__() + self.net = th.nn.Sequential(*[layer + for io, a in zip(zip(shape[:-1], shape[1:]), + [activation] * (len(shape) - 2) + [th.nn.Identity]) + for layer in [th.nn.Linear(*io), a()]]) + self.optimizer = th.optim.Adam(self.net.parameters(), lr=lr) + + # Initialize weights uniformly, so that for the policy net all actions have approximately the same + # probability in the beginning + for module in self.modules(): + if isinstance(module, nn.Linear): + nn.init.uniform_(module.weight, a=-0.1, b=0.1) + if module.bias is not None: + nn.init.uniform_(module.bias, a=-0.1, b=0.1) + + def save_model(self, path): + th.save(self.net, f"{path}/{self.__class__.__name__}_model.pth") + + def save_model_parameters(self, path): + th.save(self.net.state_dict(), f"{path}/{self.__class__.__name__}_model_parameters.pth") + + def load_model_parameters(self, path): + self.net.load_state_dict(th.load(path)) + self.net.eval() + + +class ValueNet(Net): + def __init__(self, obs_dim, hidden_sizes=[64, 64], activation=th.nn.ReLU, lr=1e-3): + super().__init__([obs_dim] + hidden_sizes + [1], activation, lr) + + def forward(self, obs): return self.net(obs) + + def loss(self, states, returns): return ((returns - self(states)) ** 2).mean() + + +class PolicyNet(Net): + def __init__(self, obs_dim, act_dim, hidden_sizes=[64, 64], activation=th.nn.Tanh, lr=3e-4): + super().__init__([obs_dim] + hidden_sizes + [act_dim], activation, lr) + self.distribution = lambda obs: th.distributions.Categorical(logits=self.net(obs)) + + def forward(self, obs, act=None, det=False): + """Given an observation: Returns policy distribution and probablilty for a given action + or Returns a sampled action and its corresponding probablilty""" + pi = self.distribution(obs) + if act is not None: return pi, pi.log_prob(act) + act = self.net(obs).argmax() if det else pi.sample() # sample from the learned distribution + return act, pi.log_prob(act) + + def loss(self, states, actions, advantages): + _, logp = self.forward(states, actions) + loss = -(logp * advantages).mean() + return loss + + +class PolicyGradient: + """ Autonomous agent using vanilla policy gradient. """ + + def __init__(self, env, seed=42, gamma=0.99, agent_id=0, act_dim=None, obs_dim=None): + self.env = env + self.gamma = gamma # Setup env and discount + th.manual_seed(seed) + np.random.seed(seed) # Seed Torch, numpy and gym + # Keep track of previous rewards and performed steps to calcule the mean Return metric + self._episode, self.ep_returns, self.num_steps = [], deque(maxlen=100), 0 + # Get observation and action shapes + if not obs_dim: + obs_size = env.observation_space.shape if len(env.state.entities.by_name("Agents")) == 1 \ + else env.observation_space[agent_id].shape # Single agent case vs. multi-agent case + obs_dim = np.prod(obs_size) + if not act_dim: + act_dim = env.action_space[agent_id].n + self.vf = ValueNet(obs_dim) # Setup Value Network (Critic) + self.pi = PolicyNet(obs_dim, act_dim) # Setup Policy Network (Actor) + + def step(self, obs): + """ Given an observation, get action and probs from policy and values from critic""" + with th.no_grad(): + (a, _), v = self.pi(obs), self.vf(obs) + self._episode.append((None, None, None, v)) + return a.numpy() + + def policy(self, obs, det=True): + return self.pi(obs, det=det)[0].numpy() + + def finish_episode(self): + """Process self._episode & reset self.env, Returns (s,a,G,V)-Tuple and new inital state""" + s, a, r, v = (np.array(e) for e in zip(*self._episode)) # Get trajectories from rollout + self.ep_returns.append(sum(r)) + self._episode = [] # Add episode return to buffer & reset + return s, a, r, v # state, action, Return, Value Tensors + + def train(self, states, actions, returns, advantages): # Update policy weights + self.pi.optimizer.zero_grad() + self.vf.optimizer.zero_grad() # Reset optimizer + states = states.flatten(1, -1) # Reduce dimensionality to rollout_dim x input_dim + policy_loss = self.pi.loss(states, actions, advantages) # Calculate Policy loss + policy_loss.backward() + self.pi.optimizer.step() # Apply Policy loss + value_loss = self.vf.loss(states, returns) # Calculate Value loss + value_loss.backward() + self.vf.optimizer.step() # Apply Value loss diff --git a/marl_factory_grid/algorithms/marl/base_ac.py b/marl_factory_grid/algorithms/rl/base_ac.py similarity index 99% rename from marl_factory_grid/algorithms/marl/base_ac.py rename to marl_factory_grid/algorithms/rl/base_ac.py index 0c15250..f1ef3d1 100644 --- a/marl_factory_grid/algorithms/marl/base_ac.py +++ b/marl_factory_grid/algorithms/rl/base_ac.py @@ -2,7 +2,7 @@ import torch from typing import Union, List, Dict import numpy as np from torch.distributions import Categorical -from marl_factory_grid.algorithms.marl.memory import MARLActorCriticMemory +from marl_factory_grid.algorithms.rl.memory import MARLActorCriticMemory from marl_factory_grid.algorithms.utils import add_env_props, instantiate_class from pathlib import Path import pandas as pd diff --git a/marl_factory_grid/algorithms/marl/configs/MultiAgentConfigs/dirt_quadrant_config.yaml b/marl_factory_grid/algorithms/rl/configs/MultiAgentConfigs/dirt_quadrant_config.yaml similarity index 90% rename from marl_factory_grid/algorithms/marl/configs/MultiAgentConfigs/dirt_quadrant_config.yaml rename to marl_factory_grid/algorithms/rl/configs/MultiAgentConfigs/dirt_quadrant_config.yaml index 599b7f4..99b7ea4 100644 --- a/marl_factory_grid/algorithms/marl/configs/MultiAgentConfigs/dirt_quadrant_config.yaml +++ b/marl_factory_grid/algorithms/rl/configs/MultiAgentConfigs/dirt_quadrant_config.yaml @@ -1,5 +1,5 @@ agent: - classname: marl_factory_grid.algorithms.marl.networks.RecurrentAC + classname: marl_factory_grid.algorithms.rl.networks.RecurrentAC n_agents: 2 obs_emb_size: 96 action_emb_size: 16 @@ -18,7 +18,7 @@ env: eval_render: True save_and_log: True record: False -method: marl_factory_grid.algorithms.marl.LoopSEAC +method: marl_factory_grid.algorithms.rl.LoopSEAC algorithm: gamma: 0.99 entropy_coef: 0.01 diff --git a/marl_factory_grid/algorithms/marl/configs/MultiAgentConfigs/two_rooms_one_door_modified_config.yaml b/marl_factory_grid/algorithms/rl/configs/MultiAgentConfigs/two_rooms_one_door_modified_config.yaml similarity index 91% rename from marl_factory_grid/algorithms/marl/configs/MultiAgentConfigs/two_rooms_one_door_modified_config.yaml rename to marl_factory_grid/algorithms/rl/configs/MultiAgentConfigs/two_rooms_one_door_modified_config.yaml index 8b8bf13..421a8d1 100644 --- a/marl_factory_grid/algorithms/marl/configs/MultiAgentConfigs/two_rooms_one_door_modified_config.yaml +++ b/marl_factory_grid/algorithms/rl/configs/MultiAgentConfigs/two_rooms_one_door_modified_config.yaml @@ -1,5 +1,5 @@ agent: - classname: marl_factory_grid.algorithms.marl.networks.RecurrentAC + classname: marl_factory_grid.algorithms.rl.networks.RecurrentAC n_agents: 2 obs_emb_size: 96 action_emb_size: 16 @@ -18,7 +18,7 @@ env: eval_render: True save_and_log: True record: False -method: marl_factory_grid.algorithms.marl.LoopSEAC +method: marl_factory_grid.algorithms.rl.LoopSEAC algorithm: gamma: 0.99 entropy_coef: 0.01 diff --git a/marl_factory_grid/algorithms/marl/configs/dirt_quadrant_config.yaml b/marl_factory_grid/algorithms/rl/configs/dirt_quadrant_config.yaml similarity index 90% rename from marl_factory_grid/algorithms/marl/configs/dirt_quadrant_config.yaml rename to marl_factory_grid/algorithms/rl/configs/dirt_quadrant_config.yaml index d254f5e..6c11d02 100644 --- a/marl_factory_grid/algorithms/marl/configs/dirt_quadrant_config.yaml +++ b/marl_factory_grid/algorithms/rl/configs/dirt_quadrant_config.yaml @@ -1,5 +1,5 @@ agent: - classname: marl_factory_grid.algorithms.marl.networks.RecurrentAC + classname: marl_factory_grid.algorithms.rl.networks.RecurrentAC n_agents: 1 obs_emb_size: 96 action_emb_size: 16 @@ -18,7 +18,7 @@ env: eval_render: True save_and_log: True record: False -method: marl_factory_grid.algorithms.marl.LoopSEAC +method: marl_factory_grid.algorithms.rl.LoopSEAC algorithm: gamma: 0.99 entropy_coef: 0.01 diff --git a/marl_factory_grid/algorithms/marl/configs/environment_changes b/marl_factory_grid/algorithms/rl/configs/environment_changes similarity index 100% rename from marl_factory_grid/algorithms/marl/configs/environment_changes rename to marl_factory_grid/algorithms/rl/configs/environment_changes diff --git a/marl_factory_grid/algorithms/marl/configs/two_rooms_one_door_modified_config.yaml b/marl_factory_grid/algorithms/rl/configs/two_rooms_one_door_modified_config.yaml similarity index 90% rename from marl_factory_grid/algorithms/marl/configs/two_rooms_one_door_modified_config.yaml rename to marl_factory_grid/algorithms/rl/configs/two_rooms_one_door_modified_config.yaml index 95ddf07..d28d86d 100644 --- a/marl_factory_grid/algorithms/marl/configs/two_rooms_one_door_modified_config.yaml +++ b/marl_factory_grid/algorithms/rl/configs/two_rooms_one_door_modified_config.yaml @@ -1,5 +1,5 @@ agent: - classname: marl_factory_grid.algorithms.marl.networks.RecurrentAC + classname: marl_factory_grid.algorithms.rl.networks.RecurrentAC n_agents: 1 obs_emb_size: 96 action_emb_size: 16 @@ -18,7 +18,7 @@ env: eval_render: True save_and_log: False record: False -method: marl_factory_grid.algorithms.marl.LoopSEAC +method: marl_factory_grid.algorithms.rl.LoopSEAC algorithm: gamma: 0.99 entropy_coef: 0.01 diff --git a/marl_factory_grid/algorithms/rl/constants.py b/marl_factory_grid/algorithms/rl/constants.py new file mode 100644 index 0000000..fadf2fe --- /dev/null +++ b/marl_factory_grid/algorithms/rl/constants.py @@ -0,0 +1,37 @@ +class Names: + ENV = 'env' + ENV_NAME = 'env_name' + N_AGENTS = 'n_agents' + ALGORITHM = 'algorithm' + MAX_STEPS = 'max_steps' + N_STEPS = 'n_steps' + TRAIN_RENDER = 'train_render' + EVAL_RENDER = 'eval_render' + AGENT = 'Agent' + PILE_OBSERVABILITY = 'pile-observability' + PILE_ORDER = 'pile-order' + ALL = 'all' + FIXED = 'fixed' + AGENTS = 'agents' + DYNAMIC = 'dynamic' + SMART = 'smart' + DIRT_PILES = 'DirtPiles' + COIN_PILES = 'CoinPiles' + AUXILIARY_PILES = "auxiliary_piles" + DOORS = 'Doors' + DOOR = 'Door' + GAMMA = 'gamma' + ADVANTAGE = 'advantage' + REINFORCE = 'reinforce' + ADVANTAGE_AC = "Advantage-AC" + TD_ADVANTAGE_AC = "TD-Advantage-AC" + CHUNK_EPISODE = 'chunk-episode' + POS_POINTER = 'pos_pointer' + POSITIONS = 'positions' + SAVE_AND_LOG = 'save_and_log' + NOOP = 'Noop' + USE_DOOR = 'use_door' + PILE_ALL_DONE = 'pile_all_done' + SINGLE = 'single' + DISTRIBUTED = 'distributed' + SHARED = 'shared' diff --git a/marl_factory_grid/algorithms/marl/iac.py b/marl_factory_grid/algorithms/rl/iac.py similarity index 92% rename from marl_factory_grid/algorithms/marl/iac.py rename to marl_factory_grid/algorithms/rl/iac.py index d2730c8..ea8d4ee 100644 --- a/marl_factory_grid/algorithms/marl/iac.py +++ b/marl_factory_grid/algorithms/rl/iac.py @@ -1,9 +1,9 @@ import torch -from marl_factory_grid.algorithms.marl.base_ac import BaseActorCritic, nms +from marl_factory_grid.algorithms.rl.base_ac import BaseActorCritic, nms from marl_factory_grid.algorithms.utils import instantiate_class from pathlib import Path from natsort import natsorted -from marl_factory_grid.algorithms.marl.memory import MARLActorCriticMemory +from marl_factory_grid.algorithms.rl.memory import MARLActorCriticMemory class LoopIAC(BaseActorCritic): diff --git a/marl_factory_grid/algorithms/marl/mappo.py b/marl_factory_grid/algorithms/rl/mappo.py similarity index 93% rename from marl_factory_grid/algorithms/marl/mappo.py rename to marl_factory_grid/algorithms/rl/mappo.py index e86a394..d40eaf2 100644 --- a/marl_factory_grid/algorithms/marl/mappo.py +++ b/marl_factory_grid/algorithms/rl/mappo.py @@ -1,6 +1,6 @@ -from marl_factory_grid.algorithms.marl.base_ac import Names as nms -from marl_factory_grid.algorithms.marl.snac import LoopSNAC -from marl_factory_grid.algorithms.marl.memory import MARLActorCriticMemory +from marl_factory_grid.algorithms.rl.base_ac import Names as nms +from marl_factory_grid.algorithms.rl.snac import LoopSNAC +from marl_factory_grid.algorithms.rl.memory import MARLActorCriticMemory import torch from torch.distributions import Categorical from marl_factory_grid.algorithms.utils import instantiate_class diff --git a/marl_factory_grid/algorithms/marl/memory.py b/marl_factory_grid/algorithms/rl/memory.py similarity index 100% rename from marl_factory_grid/algorithms/marl/memory.py rename to marl_factory_grid/algorithms/rl/memory.py diff --git a/marl_factory_grid/algorithms/marl/networks.py b/marl_factory_grid/algorithms/rl/networks.py similarity index 100% rename from marl_factory_grid/algorithms/marl/networks.py rename to marl_factory_grid/algorithms/rl/networks.py diff --git a/marl_factory_grid/algorithms/marl/seac.py b/marl_factory_grid/algorithms/rl/seac.py similarity index 91% rename from marl_factory_grid/algorithms/marl/seac.py rename to marl_factory_grid/algorithms/rl/seac.py index 07e8267..d1384e3 100644 --- a/marl_factory_grid/algorithms/marl/seac.py +++ b/marl_factory_grid/algorithms/rl/seac.py @@ -1,8 +1,8 @@ import torch from torch.distributions import Categorical -from marl_factory_grid.algorithms.marl.iac import LoopIAC -from marl_factory_grid.algorithms.marl.base_ac import nms -from marl_factory_grid.algorithms.marl.memory import MARLActorCriticMemory +from marl_factory_grid.algorithms.rl.iac import LoopIAC +from marl_factory_grid.algorithms.rl.base_ac import nms +from marl_factory_grid.algorithms.rl.memory import MARLActorCriticMemory class LoopSEAC(LoopIAC): diff --git a/marl_factory_grid/algorithms/marl/snac.py b/marl_factory_grid/algorithms/rl/snac.py similarity index 90% rename from marl_factory_grid/algorithms/marl/snac.py rename to marl_factory_grid/algorithms/rl/snac.py index 11be902..02a9d89 100644 --- a/marl_factory_grid/algorithms/marl/snac.py +++ b/marl_factory_grid/algorithms/rl/snac.py @@ -1,5 +1,5 @@ -from marl_factory_grid.algorithms.marl.base_ac import BaseActorCritic -from marl_factory_grid.algorithms.marl.base_ac import nms +from marl_factory_grid.algorithms.rl.base_ac import BaseActorCritic +from marl_factory_grid.algorithms.rl.base_ac import nms import torch from torch.distributions import Categorical from pathlib import Path diff --git a/marl_factory_grid/algorithms/rl/utils.py b/marl_factory_grid/algorithms/rl/utils.py new file mode 100644 index 0000000..9204f4c --- /dev/null +++ b/marl_factory_grid/algorithms/rl/utils.py @@ -0,0 +1,337 @@ +import copy +from typing import List +import numpy as np +import torch + +from marl_factory_grid.algorithms.rl.constants import Names as nms + +from marl_factory_grid.algorithms.rl.base_a2c import cumulate_discount + + +def _as_torch(x): + """ Helper function to convert different list types to a torch tensor """ + if isinstance(x, np.ndarray): + return torch.from_numpy(x) + elif isinstance(x, List): + return torch.tensor(x) + elif isinstance(x, (int, float)): + return torch.tensor([x]) + return x + + +def transform_observations(env, ordered_coins, target_coin, cfg, n_agents): + """ Function that extracts local observations from global state + Requires that agents have observations -CoinPiles and -Self (cf. environment configs) """ + agents_positions = get_agents_positions(env, n_agents) + coin_observability_is_all = cfg[nms.ALGORITHM][nms.PILE_OBSERVABILITY] == nms.ALL + if coin_observability_is_all: + trans_obs = [torch.zeros(2 + 2 * len(ordered_coins[0])) for _ in range(len(agents_positions))] + else: + # Only show current target pile + trans_obs = [torch.zeros(4) for _ in range(len(agents_positions))] + for i, pos in enumerate(agents_positions): + agent_x, agent_y = pos[0], pos[1] + trans_obs[i][0] = agent_x + trans_obs[i][1] = agent_y + idx = 2 + if coin_observability_is_all: + for coin_pos in ordered_coins[i]: + trans_obs[i][idx] = coin_pos[0] + trans_obs[i][idx + 1] = coin_pos[1] + idx += 2 + else: + trans_obs[i][2] = ordered_coins[i][target_coin[i]][0] + trans_obs[i][3] = ordered_coins[i][target_coin[i]][1] + return trans_obs + + +def get_all_observations(env, cfg, n_agents): + """ Helper function that returns all possible agent observations """ + coins_positions = [env.state.entities[nms.COIN_PILES][pile_idx].pos for pile_idx in + range(len(env.state.entities[nms.COIN_PILES]))] + if cfg[nms.ALGORITHM][nms.PILE_OBSERVABILITY] == nms.ALL: + obs = [torch.zeros(2 + 2 * len(coins_positions))] + observations = [[]] + # Fill in pile positions + idx = 2 + for pile_pos in coins_positions: + obs[0][idx] = pile_pos[0] + obs[0][idx + 1] = pile_pos[1] + idx += 2 + else: + # Have multiple observation layers of the map for each coin pile one + obs = [torch.zeros(4) for _ in range(n_agents) for _ in coins_positions] + observations = [[] for _ in coins_positions] + for idx, pile_pos in enumerate(coins_positions): + obs[idx][2] = pile_pos[0] + obs[idx][3] = pile_pos[1] + valid_agent_positions = env.state.entities.floorlist + + for idx, pos in enumerate(valid_agent_positions): + for obs_layer in range(len(obs)): + observation = copy.deepcopy(obs[obs_layer]) + observation[0] = pos[0] + observation[1] = pos[1] + observations[obs_layer].append(observation) + + return observations + + +def get_coin_piles_positions(env): + """ Get positions of coin piles on the map """ + return [env.state.entities[nms.COIN_PILES][pile_idx].pos for pile_idx in + range(len(env.state.entities[nms.COIN_PILES]))] + + +def get_agents_positions(env, n_agents): + """ Get positions of agents on the map """ + return [env.state.moving_entites[agent_idx].pos for agent_idx in range(n_agents)] + + +def get_ordered_coin_piles(env, collected_coins, cfg, n_agents): + """ This function determines in which order the agents should collect the coin piles + Each agent can have its individual pile order """ + ordered_coin_piles = [[] for _ in range(n_agents)] + coin_piles_positions = get_coin_piles_positions(env) + agents_positions = get_agents_positions(env, n_agents) + for agent_idx in range(n_agents): + if cfg[nms.ALGORITHM][nms.PILE_ORDER] in [nms.FIXED, nms.AGENTS]: + ordered_coin_piles[agent_idx] = coin_piles_positions + elif cfg[nms.ALGORITHM][nms.PILE_ORDER] in [nms.SMART, nms.DYNAMIC]: + # Calculate distances for remaining unvisited coin piles + remaining_target_piles = [pos for pos, value in collected_coins[agent_idx].items() if not value] + pile_distances = {pos: 0 for pos in remaining_target_piles} + agent_pos = agents_positions[agent_idx] + for pos in remaining_target_piles: + pile_distances[pos] = np.abs(agent_pos[0] - pos[0]) + np.abs(agent_pos[1] - pos[1]) + + if cfg[nms.ALGORITHM][nms.PILE_ORDER] == nms.SMART: + # Check if there is an agent on the direct path to any of the remaining coin piles + for pile_pos in remaining_target_piles: + for other_pos in agents_positions: + if other_pos != agent_pos: + if agent_pos[0] == other_pos[0] == pile_pos[0] or agent_pos[1] == other_pos[1] == pile_pos[ + 1]: + # Get the line between the agent and the target + path = bresenham(agent_pos[0], agent_pos[1], pile_pos[0], pile_pos[1]) + + # Check if the entity lies on the path between the agent and the target + if other_pos in path: + pile_distances[pile_pos] += np.abs(agent_pos[0] - other_pos[0]) + np.abs( + agent_pos[1] - other_pos[1]) + + sorted_pile_distances = dict(sorted(pile_distances.items(), key=lambda item: item[1])) + # Insert already visited coin piles + ordered_coin_piles[agent_idx] = [pos for pos in coin_piles_positions if pos not in remaining_target_piles] + # Fill up with sorted positions + for pos in sorted_pile_distances.keys(): + ordered_coin_piles[agent_idx].append(pos) + + else: + print("Not a valid pile order option.") + exit() + + return ordered_coin_piles + + +def bresenham(x0, y0, x1, y1): + """Bresenham's line algorithm to get the coordinates of a line between two points.""" + dx = np.abs(x1 - x0) + dy = np.abs(y1 - y0) + sx = 1 if x0 < x1 else -1 + sy = 1 if y0 < y1 else -1 + err = dx - dy + + coordinates = [] + while True: + coordinates.append((x0, y0)) + if x0 == x1 and y0 == y1: + break + e2 = 2 * err + if e2 > -dy: + err -= dy + x0 += sx + if e2 < dx: + err += dx + y0 += sy + return coordinates + + +def update_ordered_coin_piles(agent_idx, collected_coin_piles, ordered_coin_piles, env, cfg, n_agents): + """ Update the order of the remaining coin piles """ + # Only update ordered_coin_pile for agent that reached its target pile + updated_ordered_coin_piles = get_ordered_coin_piles(env, collected_coin_piles, cfg, n_agents) + for i in range(len(ordered_coin_piles[agent_idx])): + ordered_coin_piles[agent_idx][i] = updated_ordered_coin_piles[agent_idx][i] + + +def distribute_indices(env, cfg, n_agents): + """ Distribute coin piles evenly among the agents """ + indices = [] + n_coin_piles = len(get_coin_piles_positions(env)) + agents_positions = get_agents_positions(env, n_agents) + if n_coin_piles == 1 or cfg[nms.ALGORITHM][nms.PILE_ORDER] in [nms.FIXED, nms.DYNAMIC, nms.SMART]: + indices = [[0] for _ in range(n_agents)] + else: + base_count = n_coin_piles // n_agents + remainder = n_coin_piles % n_agents + + start_index = 0 + for i in range(n_agents): + # Add an extra index to the first 'remainder' objects + end_index = start_index + base_count + (1 if i < remainder else 0) + indices.append(list(range(start_index, end_index))) + start_index = end_index + + # Static form: auxiliary pile, primary pile, auxiliary pile, ... + # -> Starting with index 0 even piles are auxiliary piles, odd piles are primary piles + if cfg[nms.ALGORITHM][nms.AUXILIARY_PILES] and nms.DOORS in env.state.entities.keys(): + door_positions = [door.pos for door in env.state.entities[nms.DOORS]] + distances = {door_pos: [] for door_pos in door_positions} + + # Calculate distance of every agent to every door + for door_pos in door_positions: + for agent_pos in agents_positions: + distances[door_pos].append(np.abs(door_pos[0] - agent_pos[0]) + np.abs(door_pos[1] - agent_pos[1])) + + def duplicate_indices(lst, item): + return [i for i, x in enumerate(lst) if x == item] + + # Get agent indices of agents with same distance to door + affected_agents = {door_pos: {} for door_pos in door_positions} + for door_pos in distances.keys(): + dist = distances[door_pos] + dist_set = set(dist) + for d in dist_set: + affected_agents[door_pos][str(d)] = duplicate_indices(dist, d) + + updated_indices = [] + + for door_pos, agent_distances in affected_agents.items(): + if len(agent_distances) == 0: + # Remove auxiliary piles for all agents + # (In config, we defined every pile with an even numbered index to be an auxiliary pile) + updated_indices = [[ele for ele in lst if ele % 2 != 0] for lst in indices] + else: + for distance, agent_indices in agent_distances.items(): + # For each distance group, pick one random agent to keep the auxiliary pile + # selected_agent = np.random.choice(agent_indices) + selected_agent = 0 + for agent_idx in agent_indices: + if agent_idx == selected_agent: + updated_indices.append(indices[agent_idx]) + else: + updated_indices.append([ele for ele in indices[agent_idx] if ele % 2 != 0]) + + indices = updated_indices + + return indices + + +def update_target_pile(env, agent_idx, target_pile, indices, cfg): + """ Get the next target pile for a given agent """ + if cfg[nms.ALGORITHM][nms.PILE_ORDER] in [nms.FIXED, nms.DYNAMIC, nms.SMART]: + if target_pile[agent_idx] + 1 < len(get_coin_piles_positions(env)): + target_pile[agent_idx] += 1 + else: + target_pile[agent_idx] = 0 + else: + if target_pile[agent_idx] + 1 in indices[agent_idx]: + target_pile[agent_idx] += 1 + + +def is_door_close(env, agent_idx): + """ Checks whether the agent is close to a door """ + neighbourhood = [y for x in env.state.entities.neighboring_positions(env.state[nms.AGENT][agent_idx].pos) + for y in env.state.entities.pos_dict[x] if nms.DOOR in y.name] + if neighbourhood: + return neighbourhood[0] + + +def get_all_collected_coin_piles(coin_piles_positions, collected_coin_piles, n_agents): + """ Returns all coin piles collected by any agent """ + meta_collected_coin_piles = {pos: False for pos in coin_piles_positions} + for agent_idx in range(n_agents): + for (pos, collected) in collected_coin_piles[agent_idx].items(): + if collected: + meta_collected_coin_piles[pos] = True + return meta_collected_coin_piles + + +def handle_finished_episode(obs, agents, cfg): + """ Finish up episode, calculate advantages and perform policy net and value net updates""" + with torch.inference_mode(False): + for ag_i, agent in enumerate(agents): + # Get states, actions, rewards and values from rollout buffer + data = agent.finish_episode() + # Chunk episode data, such that there will be no memory failure for very long episodes + chunks = split_into_chunks(data, cfg) + for (s, a, R, V) in chunks: + # Calculate discounted return and advantage + G = cumulate_discount(R, cfg[nms.ALGORITHM][nms.GAMMA]) + if cfg[nms.ALGORITHM][nms.ADVANTAGE] == nms.REINFORCE: + A = G + elif cfg[nms.ALGORITHM][nms.ADVANTAGE] == nms.ADVANTAGE_AC: + A = G - V # Actor-Critic Advantages + elif cfg[nms.ALGORITHM][nms.ADVANTAGE] == nms.TD_ADVANTAGE_AC: + with torch.no_grad(): + A = R + cfg[nms.ALGORITHM][nms.GAMMA] * np.append(V[1:], agent.vf( + _as_torch(obs[ag_i]).view(-1).to( + torch.float32)).numpy()) - V # TD Actor-Critic Advantages + else: + print("Not a valid advantage option.") + exit() + + rollout = (torch.tensor(x.copy()).to(torch.float32) for x in (s, a, G, A)) + # Update policy and value net of agent with experience from rollout buffer + agent.train(*rollout) + + +def split_into_chunks(data_tuple, cfg): + """ Chunks episode data into approximately equal sized chunks to prevent system memory failure from overload """ + result = [data_tuple] + chunk_size = cfg[nms.ALGORITHM][nms.CHUNK_EPISODE] + if chunk_size > 0: + # Get the maximum length of the lists in the tuple to handle different lengths + max_length = max(len(lst) for lst in data_tuple) + + # Prepare a list to store the result + result = [] + + # Split each list into chunks and add them to the result + for i in range(0, max_length, chunk_size): + # Create a sublist containing the ith chunk from each list + sublist = [lst[i:i + chunk_size] for lst in data_tuple if i < len(lst)] + result.append(sublist) + + return result + + +def set_agents_spawnpoints(env, n_agents): + """ Tell environment where the agents should spawn in the next episode """ + for agent_idx in range(n_agents): + agent_name = list(env.state.agents_conf.keys())[agent_idx] + current_pos_pointer = env.state.agents_conf[agent_name][nms.POS_POINTER] + # Making the reset dependent on the number of spawnpoints and not the number of coinpiles allows + # for having multiple subsequent spawnpoints with the same target pile + if current_pos_pointer == len(env.state.agents_conf[agent_name][nms.POSITIONS]) - 1: + env.state.agents_conf[agent_name][nms.POS_POINTER] = 0 + else: + env.state.agents_conf[agent_name][nms.POS_POINTER] += 1 + + +def save_configs(results_path, cfg, factory_conf, eval_factory_conf): + """ Save configurations for logging purposes """ + with open(f"{results_path}/MARL_config.txt", "w") as txt_file: + txt_file.write(str(cfg)) + with open(f"{results_path}/train_env_config.txt", "w") as txt_file: + txt_file.write(str(factory_conf)) + with open(f"{results_path}/eval_env_config.txt", "w") as txt_file: + txt_file.write(str(eval_factory_conf)) + + +def save_agent_models(results_path, agents): + """ Save model parameters after training """ + for idx, agent in enumerate(agents): + agent.pi.save_model_parameters(results_path) + agent.vf.save_model_parameters(results_path) diff --git a/marl_factory_grid/algorithms/static/TSP_coin_agent.py b/marl_factory_grid/algorithms/static/TSP_coin_agent.py new file mode 100644 index 0000000..fe80a5b --- /dev/null +++ b/marl_factory_grid/algorithms/static/TSP_coin_agent.py @@ -0,0 +1,40 @@ +from marl_factory_grid.algorithms.static.TSP_base_agent import TSPBaseAgent + +from marl_factory_grid.modules.coins import constants as c +from marl_factory_grid.environment import constants as e + +future_planning = 7 + + +class TSPCoinAgent(TSPBaseAgent): + + def __init__(self, *args, **kwargs): + """ + Initializes a TSPCoinAgent that aims to collect coins in the environment. + """ + super(TSPCoinAgent, self).__init__(*args, **kwargs) + self.fallback_action = e.NOOP + + def predict(self, *_, **__): + """ + Predicts the next action based on the presence of coins in the environment. + + :return: Predicted action. + :rtype: int + """ + coin_at_position = self._env.state[c.COIN].by_pos(self.state.pos) + if coin_at_position: + # Translate the action_object to an integer to have the same output as any other model + action = c.COLLECT + elif door := self._door_is_close(self._env.state): + action = self._use_door_or_move(door, c.COIN) + else: + action = self._predict_move(c.COIN) + self.action_list.append(action) + # Translate the action_object to an integer to have the same output as any other model + try: + action_obj = next(action_i for action_i, a in enumerate(self.state.actions) if a.name == action) + except (StopIteration, UnboundLocalError): + print('Will not happen') + raise EnvironmentError + return action_obj diff --git a/marl_factory_grid/configs/test_config.yaml b/marl_factory_grid/configs/test_config.yaml index 606a817..8eea6ef 100644 --- a/marl_factory_grid/configs/test_config.yaml +++ b/marl_factory_grid/configs/test_config.yaml @@ -40,10 +40,27 @@ Agents: # - DropOffLocations # - Maintainers # Clones: 0 - Target test agent: +# Target test agent: +# Actions: +# - Noop +# - Charge +# - DoorUse +# - Move8 +# Observations: +# - Combined: +# - Other +# - Walls +# - GlobalPosition +# - Battery +# - Destinations +# - Doors +# - Maintainers +# Clones: 1 + Coin test agent: Actions: - Noop - Charge + - Collect - DoorUse - Move8 Observations: @@ -52,6 +69,8 @@ Agents: - Walls - GlobalPosition - Battery + - ChargePods + - CoinPiles - Destinations - Doors - Maintainers @@ -67,11 +86,18 @@ Entities: Destinations: coords_or_quantity: 1 spawn_mode: GROUPED - DirtPiles: +# DirtPiles: +# coords_or_quantity: 10 +# initial_amount: 2 +# clean_amount: 1 +# dirt_spawn_r_var: 0.1 +# max_global_amount: 20 +# max_local_amount: 5 + CoinPiles: coords_or_quantity: 10 initial_amount: 2 - clean_amount: 1 - dirt_spawn_r_var: 0.1 + collect_amount: 1 + coin_spawn_r_var: 0.1 max_global_amount: 20 max_local_amount: 5 Doors: @@ -90,24 +116,26 @@ Entities: General: env_seed: 69 individual_rewards: true - level_name: quadrant + level_name: two_rooms pomdp_r: 3 verbose: false tests: false Rules: # Environment Dynamics - EntitiesSmearDirtOnMove: - smear_ratio: 0.2 + # EntitiesSmearDirtOnMove: + # smear_ratio: 0.2 DoorAutoClose: close_frequency: 10 MoveMaintainers: # Respawn Stuff - RespawnDirt: - respawn_freq: 15 + # RespawnDirt: + # respawn_freq: 15 RespawnItems: respawn_freq: 15 + RespawnCoins: + respawn_freq: 15 # Utilities WatchCollisions: diff --git a/marl_factory_grid/environment/factory.py b/marl_factory_grid/environment/factory.py index aa6a268..0b239b4 100644 --- a/marl_factory_grid/environment/factory.py +++ b/marl_factory_grid/environment/factory.py @@ -81,7 +81,7 @@ class Factory(gym.Env): def __init__(self, config_file: Union[str, PathLike], custom_modules_path: Union[None, PathLike] = None, custom_level_path: Union[None, PathLike] = None): """ - Initializes the marl-factory-grid as Gym environment. + Initializes the rl-factory-grid as Gym environment. :param config_file: Path to the configuration file. :type config_file: Union[str, PathLike] @@ -271,15 +271,37 @@ class Factory(gym.Env): if not self._renderer: # lazy init from marl_factory_grid.utils.renderer import Renderer global Renderer - self._renderer = Renderer(self.map.level_shape, view_radius=self.conf.pomdp_r, fps=10) + self._renderer = Renderer(self.map.level_shape, view_radius=self.conf.pomdp_r, fps=10) render_entities = self.state.entities.render() + + # Hide entities where certain conditions are met (e.g., amount <= 0 for DirtPiles) + render_entities = self.filter_entities(render_entities) + + # Mask entities based on dynamic conditions instead of hardcoding level-specific logic + if self.conf['General']['level_name'] == 'two_rooms': + render_entities = self.mask_entities(render_entities) + if self.conf.pomdp_r: for render_entity in render_entities: if render_entity.name == c.AGENT: render_entity.aux = self.obs_builder.curr_lightmaps[render_entity.real_name] return self._renderer.render(render_entities, self._recorder) + def filter_entities(self, entities): + """ Generalized method to filter out entities that shouldn't be rendered. """ + if 'DirtPiles' in self.state.entities.keys(): + entities = [entity for entity in entities if not (entity.name == 'DirtPiles' and entity.amount <= 0)] + return entities + + def mask_entities(self, entities): + """ Generalized method to mask entities based on dynamic conditions. """ + for entity in entities: + if entity.name == 'CoinPiles': + entity.mask = 'Destinations' + entity.mask_value = 1 + return entities + def set_recorder(self, recorder): self._recorder = recorder @@ -298,7 +320,7 @@ class Factory(gym.Env): summary.update({entity_group.name.lower(): entity_group.summarize_states()}) # TODO Section End ######## for key in list(summary.keys()): - if key not in ['step', 'walls', 'doors', 'agents', 'items', 'dirtPiles', 'batteries']: + if key not in ['step', 'walls', 'doors', 'agents', 'items', 'dirtPiles', 'batteries', 'coinPiles']: del summary[key] return summary diff --git a/marl_factory_grid/environment/rules.py b/marl_factory_grid/environment/rules.py index 763f16c..91a21f4 100644 --- a/marl_factory_grid/environment/rules.py +++ b/marl_factory_grid/environment/rules.py @@ -168,14 +168,25 @@ class SpawnEntity(Rule): return results +def _get_position(spawn_rule, positions, empty_positions, positions_pointer): + """ + Internal usage, selects positions based on rule. + """ + if spawn_rule and spawn_rule == "random": + position = random.choice(([x for x in positions if x in empty_positions])) + elif spawn_rule and spawn_rule == "order": + position = ([x for x in positions if x in empty_positions])[positions_pointer] + else: + position = h.get_first([x for x in positions if x in empty_positions]) + return position + + class SpawnAgents(Rule): def __init__(self): """ - TODO - - - :return: + Finds suitable spawn positions according to the given spawn rule, creates agents with these positions and adds + them to state.agents. """ super().__init__() pass @@ -183,8 +194,9 @@ class SpawnAgents(Rule): def on_reset(self, state): spawn_rule = None for rule in state.rules.rules: - if isinstance(rule, marl_factory_grid.environment.rules.AgentSpawnRule): + if isinstance(rule, AgentSpawnRule): spawn_rule = rule.spawn_rule + break if not hasattr(state, 'agent_spawn_positions'): state.agent_spawn_positions = [] @@ -200,7 +212,7 @@ class SpawnAgents(Rule): other = agent_conf['other'].copy() positions_pointer = agent_conf['pos_pointer'] - if position := self._get_position(spawn_rule, positions, empty_positions, positions_pointer): + if position := _get_position(spawn_rule, positions, empty_positions, positions_pointer): assert state.check_pos_validity(position), 'smth went wrong....' agents.add_item(Agent(actions, observations, position, str_ident=agent_name, **other)) state.agent_spawn_positions.append(position) @@ -213,21 +225,13 @@ class SpawnAgents(Rule): state.agent_spawn_positions.append(chosen_position) return [] - def _get_position(self, spawn_rule, positions, empty_positions, positions_pointer): - if spawn_rule and spawn_rule == "random": - position = random.choice(([x for x in positions if x in empty_positions])) - elif spawn_rule and spawn_rule == "order": - position = ([x for x in positions if x in empty_positions])[positions_pointer] - else: - position = h.get_first([x for x in positions if x in empty_positions]) - - return position class AgentSpawnRule(Rule): def __init__(self, spawn_rule): self.spawn_rule = spawn_rule super().__init__() + class DoneAtMaxStepsReached(Rule): def __init__(self, max_steps: int = 500): diff --git a/marl_factory_grid/modules/clean_up/groups.py b/marl_factory_grid/modules/clean_up/groups.py index 8a99439..f199dcb 100644 --- a/marl_factory_grid/modules/clean_up/groups.py +++ b/marl_factory_grid/modules/clean_up/groups.py @@ -1,4 +1,5 @@ import ast +import random from marl_factory_grid.environment import constants as c from marl_factory_grid.environment.groups.collection import Collection from marl_factory_grid.modules.clean_up.entitites import DirtPile @@ -33,7 +34,7 @@ class DirtPiles(Collection): return sum([dirt.amount for dirt in self]) def __init__(self, *args, max_local_amount=5, clean_amount=1, max_global_amount: int = 20, coords_or_quantity=10, - initial_amount=2, amount_var=0.2, n_var=0.2, **kwargs): + initial_amount=2, amount_var=0.2, n_var=0.2, randomize=False, randomization_seed=0, **kwargs): """ A Collection of dirt piles that triggers their spawn. @@ -67,6 +68,8 @@ class DirtPiles(Collection): self.max_local_amount = max_local_amount self.coords_or_quantity = coords_or_quantity self.initial_amount = initial_amount + self.randomize = randomize + self.randomized_selection = None def trigger_spawn(self, state, coords_or_quantity=0, amount=0, ignore_blocking=False) -> [Result]: if ignore_blocking: @@ -85,7 +88,17 @@ class DirtPiles(Collection): else: n_new = [pos for pos in coords_or_quantity] - amounts = [amount if amount else (self.initial_amount ) # removed rng amount + if self.randomize: + if not self.randomized_selection: + n_new_prime = [] + for n in n_new: + if random.random() < 0.5: + n_new_prime.append(n) + n_new = n_new_prime + self.randomized_selection = n_new + else: + n_new = self.randomized_selection + amounts = [amount if amount else self.initial_amount # removed rng amount for _ in range(len(n_new))] spawn_counter = 0 diff --git a/marl_factory_grid/modules/coins/__init__.py b/marl_factory_grid/modules/coins/__init__.py new file mode 100644 index 0000000..2b47c1c --- /dev/null +++ b/marl_factory_grid/modules/coins/__init__.py @@ -0,0 +1,4 @@ +from .actions import Collect +from .entitites import CoinPile +from .groups import CoinPiles +from .rules import DoneOnAllCoinsCollected diff --git a/marl_factory_grid/modules/coins/actions.py b/marl_factory_grid/modules/coins/actions.py new file mode 100644 index 0000000..42e3eab --- /dev/null +++ b/marl_factory_grid/modules/coins/actions.py @@ -0,0 +1,36 @@ +from typing import Union + +from marl_factory_grid.environment.actions import Action +from marl_factory_grid.utils.results import ActionResult + +from marl_factory_grid.modules.coins import constants as d + +from marl_factory_grid.environment import constants as c + + +class Collect(Action): + + def __init__(self): + """ + Attempts to reduce coin amount on entity's position. Fails if no coin is found at the at agents' position. + """ + super().__init__(d.COLLECT, d.REWARD_COLLECT_VALID, d.REWARD_COLLECT_FAIL) + + def do(self, entity, state) -> Union[None, ActionResult]: + if coin_pile := next((x for x in state.entities.pos_dict[entity.pos] if "coin" in x.name.lower()), None): + new_coin_pile_amount = coin_pile.amount - state[d.COIN].collect_amount + + if new_coin_pile_amount <= 0: + state[d.COIN].delete_env_object(coin_pile) + else: + coin_pile.set_new_amount(max(new_coin_pile_amount, c.VALUE_FREE_CELL)) + valid = c.VALID + print_str = f'{entity.name} did just collect some coins at {entity.pos}.' + state.print(print_str) + + else: + valid = c.NOT_VALID + print_str = f'{entity.name} just tried to collect some coins at {entity.pos}, but failed.' + state.print(print_str) + + return self.get_result(valid, entity) diff --git a/marl_factory_grid/modules/coins/coinpiles.png b/marl_factory_grid/modules/coins/coinpiles.png new file mode 100644 index 0000000000000000000000000000000000000000..38b084e6373815003485a40286132a095f61d11c GIT binary patch literal 103379 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 z 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 z zT@=3ztIA_c(4~x%G&QP9#CVma#yCW84oXm!L@MZU_O~qOax)0Vrm11TF_VjICTRQE zv~(K0u81KZWfpFBHo!ON>#{H!?%^W;K6L%Im%Av7C?AC96I%)kGqKoq;MsNSz8CNO z7yLAm@PzMip6@2mVEQ3>bdY%~WO8zwrKM|dM-8QJjgT%%tYE0wj@Ji57`ujBX%Lf>qO6m2itFsM9vy= z^g0EzprLLNfE)7>G-qjGi`SFY9^XEE{F4BGW17iP%2GiSf9VI^CAKM&*w2Dmf=Awp zo-PcvpeWt!+(LW)8e(%S3b>n#`I>?ugtp}TlN5%B&$6&Vt?2tk(rjlxPF_P{1e;-k zpwcdpyH4i9tm1)ZCi&9j{LInRqpj_u+2S!cNUoy72&ZnGv)q~49P}%h37(UXz4{me zF;EUa-RHBM>h_T$xGS%CbX`KZP+V8Ux=Zn!swtF**w;H24=p;_GA6Uh?~HM`-v%?W`OO+WmoJkISC?b3r5FED<3U%ZX9)%_ z_+a;-cbV4fmKWj$+&2%GgK1>1)1_|Z1RKi0lIh>bw7=!!-GS|q?9<03qal>8V^L=l ztSM>@7O`?xh1woI+?IctKhp>t=-UBE7?_a=?^_k+8}93edIiHpU1a0;=>NGlqSn)* zPvUxLOpfoLiNOX0{F7UcgEt$S@_8YuIn18e^2>KH)LjN;j4UKj)gN6&&&X0<&?i&E z3)_VI1%4{|>(4Pj?NIW}rcIdLT;_Hxi!$fLJ)gU8Cqe@zc+V&f);4SCW2rJFX;!U zZ~NhA-RQZ_i$x6z4dQMV&5m$uG6brF+v`o?x&ry28`H^PRQuPOP}{rBk%sj;#$;)$ zRb*b$n30DDCn~B-b=z9)i?&BS(IdIH1rhvOsumGFxut_?g{Y`L{)$jA30Ht~I=0ZEGfns`KlLlqb5sr^U2YG`!BZ zIBTHZwtSG#d6tUwIBe+n>v7t>^W?AZOqxa=db#^{sOlS#>5uDwB#VuYbkaDF3nN8;`$u zt#X-dxqO5YM`2Bk8-E1Oqq7@xa4lBu7ZF)e!5^=oH_s0l*_n*;Z$K-j#Gn-{`k})a z^#sxox_q90Gxm3*ybV|0IQCkP-rHk7zap3ZQf+YEWi3qjPSwqWNV*d!HbOaci5zFQ zr+wS4?2i5Ew9u5&05X<9h`lcwX6u1%$|e@hFVMQA3YlBR5{i2!9^%c9ncSX(FWPtF z8s!zfJvkn6=eaK+Oc_yIk}$CJq^pQY?Zsn{Gx{zlk=b|H+J_5JYfZ2$ZB7K9re~!pLbsVVaxflS#VVAVt_mWz zPy;wUq9&jHVWZq>*3gBnslC;&?FVy;19F3NqbN69rq zpwcSed6Bc{D3CPgoFzcNF?hgh*@r&-e#zX|-8pP#_gT@THN6iAbq6U!%{c4*y8p(4# zG=%2r6in=`=_!qn*10$aF=Zb(fiw|m*= z00SE$8x(fdoOJNezy*&<)I{xEIV5$!A>WPI3Iv>DOgqWF7J62dTcGV!@NpXTDen!< zf^*gV5m0%DPe$*w2MgoQ1Fs4>1BpY$`KC7QRESYUW~K)L(m8qg9^BE>HsW4<=79A! z0GOtZRzIEilCIVnk~>+qN6|eL@O(8nY}2;zu*~RWPHyNXNkT1=VjoJ7wkleG>w%s! zu5Ik-k1g1{Ha~~S{7b-|==JVdfWkx;rpKx$SQ?@JTn>4 zSR<@kq=y&NugLy`Gb=^#@1bAHc4jWUtU|e`pE`k*vT>#=q|H4>hH%~W_0ymsKjQrJ zl^oYJcl^#nHKCsJT3KV(@F1N@2+OBDCU$kW;+?mXq{d4U(T0X#rlK%86}|mIsrK}} z!gA-^fFWg&v>?2-@s#P;;~vXvMnGbFwP?;R;HC}|7RqMZM|rhOjR%q}4Qy+p&J=?J zs42-d1z8ju08?_59C0z>`FP?vp$zeAuv9MkC8^^EUWF-~DTB(tzw<5~|F$@|+F1Se zk~ivvEb2ME3(q=qO)KR$(@eZd)0^m2yKGwxl(c2jF*U!{oXeH2>$@(M#YM$0(J8A# zXiYX_;pls?>5<@*Nk4URSM&~0AFYS-y^R8#`8y7QtfC%@6qCE#iD(9jSMT+PcC;&b zcpiV-dlbcs+#~19KFCKHX??>9Y^#k1E>RSl5x>u)Nl@6@NAx))Z2lR)u^A)1y=-z8 z2+e(GKp1-yS4oJ7mpz{A=#M#uFC6@KOPcpG{zHWJ zCOs_Csxh-5CGiz7*e$AZD|ji_j}9@GQ3^L*ZX6V4t_Sye!&I7%J2E9mQMfScIFL1~ z297Q>p$t@3grbfSmgkM2?TS;FEb}G@vs@XX@D14&TAoksT3p1(7z%3=WiVtx9I@L| zsSjDFU7Sm6qacq(bQenVSB{>tu_Y-j^gn&wYU_RgJ>wjpbjs8V(MV~`dY7a-Pvdc# zu&;dNk8y3u+u-eE0Ii@8yb=FMw> z0pJPup~8h}ZpuDGwJC-Wv|Jmc;dXI~$ii4D6qwNQrDRg`Dv9#iEAMw>n$3~QiH!Vb zuypK>I!(HIR+ihhA%1r~M4T-kWNWpUf8(q^HqWgX6-v^#oYFNQ7WOGT_JmgFk^)Q_ zNOH`_p-9G?owx*|X z5-bCyxX=5wn_sm2QY@W(@#pCqznLcvw=hGMQ+TIzd^nOADZ9Y5xzMoLgI^zyIvy5n zN2>L+m;Y(fg^qOwlW7*!v!eRwn*cH>2zp0}dP>1O%0t19up9Vig{ztrQQbczNyg3^ zpA1^-&JT3zP}~G(<#vXrtuyYNJklQ@s{7Hl3K4|UA{5xw$B@qolyEP*+FOi00d(G} z2T7#TSSg!j@;Ta(KIyoa2t4R&5}Sk;|6lv&R?R*wMUg6GA>=jR2Z#-kCJLmY zQJPSu)#~k5Z8{!pg@jn8P+{C9C)P06%`m~9k4@O!C$DG&1(%4s)lqk`*?HxGU5bdf z2W*2TR=wjc(z5>7yYXfT_&L=7P!e`-w754|EEmT%!|u~WANBRpHjJ}08FPPBJd7xh z=?dK~P!D7`0iA!A9fNuJmer^0~BBCHtDy4LX#AuZ6 zZWt(C8{MS>A}u*!gmg*AU?ZfZM&}qkdUWSAf6t$=y*~GS&bhAZexGXsle@@DYA^2@ z4!a-P5Z;x5CXDFS6}5A^shT7CcmJ|V{&!zD!>0Py;|VL2595=O284V6+dA_p;6q#R zqiCXy7A~1U6$W--Rk~tT$((^qi$LQiPZa2N?KM=d;uI%C-8^$;{zNeFW2`2s`PAI! z*Fv3lo~J;B!wNp-{8!!Rpufib43Z1%gVyh@MmU*V4%(`fv#Mgk1uU>3^A&|xj+L?DF~o@%A+mQnD+4?t@M9UZ9K`DsB`7ySoc8^p^Cqo^NKr7 z_|1aAMUD|g;2_%dtKzg4cP!8UM)JuAMn)_@=HRgu#CdfK zaY5ard~FKuD=z>noCCIIT!dZhq2&q(4l-*=quAzGM+UyDDaX{|!<=Q8#)5qxjaw3# z(Rim>>DbND*(y&dJcw8Q+3a}0TV>P`S43_-7lQ}2{G;${v5zygtT)VwaIRyF9rqmk zV~)<7%NQuwOau5QrIms6#v(cIfdAWf@vmkTZC5TQeVo=;dg_B&12`*P`P0ozE5q}4qu$2t2Je%v5+tB*d$Ys^+0u-k4pv7_c0WBC~p1;+B zjv>Rm{9{&w%wFBSL$I(&!)hwT@6-)UQlIQfnl~LpV7;xvQJJPn;gF}f6XyF3)QBRcL?oxX1r765F305uKcB$oX>qW+;B&yYic5^m zY5eq!$}G0YdwcS8kIemoGgQdE5R#-MyFRDi$9wpRWublj6R#6kj35`U_lzk#(&3La zBluH130cg#=Lf;*&DOiH(Sb-z3p)gl{AK;k^nYJYendf^@krml{Vn-Fy`_nqj#Z&H zpIQ_+dN?`Y*drlwm7$Kdt+whjL$PZ~U-`~;FFf8@Evx%uJ6l!Omy>bkfaZNwLy9-& zuIgdg{&3uo2LTlOCI@zT1KE5%BEx1BUYW@D`B4OGX;G@>h;Y+&g7N-@FpCXryWmPu#(s$|q|31k%_NUZTl#J9*n|I5i z&XLP-DEG!FP2=V{K4cwbZ(?VoK0E8t-q47bZ5*kNPz#P3oe~gKgco<+i+rFD2tB{| z&B;WwGQBbAuhh$|KQ7MEnGU)15Vf~Ad68*5aD(+5RrlSOEr3wm?+|KAG>t}Y^f2uB zudQ7hX$$g+v|^I!+>@lRXB-RsEcWbYdb-TLIMHD{-d|alAtCq4PV@$F!#0Pu|C#$R z?`LiDxHI$eKEc-f%3~>GMW8Ew;IZS|qkk0!@OYb=3{R%E-*7WxtwhwoM6kNE-A2OK zC~*3*G3WVgn6>!gcZpzd;idtEVi@wPAGosXzzWNnKzg{P3vkSwTjP7MqLk z{fP1@-*P1-FP%=SwH&jZoO8+RxjF|2vy%FV=$DsM{wSw$+2m`_w64|$#}e6*YR@4k z@T70gItK<>QIBnF^1YNZIORz-e|h?C{v%h=jj!B%*e8-V$2=r7@b9VT8WJ_HDZs2! zHs7d#tQ=O-!2qdN`uhLX$*GVh;&DvK+co4M+TW*(vlPljjRD)N+tpQxmY3&ujWKf5 zp-EN3_(nCv?C)j<`e!}HkjBhg@$X^WIozOUv|n>e-Ueh=z3|1$%K*H92NoLSP{cCK zM2kh*D?s@)E8Biep(j@bgAPSs3DJU%{a^d6b^h3t#0I)rR?F zcS34OOkLE`CR|#v@!?NQ+y_DPVYF15Z?KD5B#0x|GE}~pbN`I0&mXyFTJ@gTX8&|? zq5sFk=m!fl+OzRiG^gw}UMwdYAtyngEH}eIH7w6IvL=CRKf1ouz70f^xc)@}-(u5s zJV|Sr5M#YeXtxd5n;LEICo>g1H1N^uRUsw6+{#f+<0uZ~jHn@MTCWg^s&XWyWOV;y z;7bi?Z4^+F~ybd1Sdl4-F-AGu!Z0EqUMAFSkBt8-A7bottS~t#jChpd6f{9u3vii z07+e;JrwV`!=R96`!76)=4iZzP8p$wVlImEG}>NTKGejlGG_(b@2=D{vV8^K~S6lxYsAF0vJJ`Wk9XJ$!T^J9(yuNK~Dud1n*fwbve>0KH?m%}h zh&MSGO6lp8VSN*zuiJW0Wg+X@6R#zcV=(WonD3;kcc==d5?FBV>nb5pxgj7*HIs6; z|9@)39Obp(WZ39{(~seGu9!lw!?qoS36+*r2+buv&dy3bZ0ObmB-!GA4Z-`-p5 zZ{X|TxI@KNB|P$YcQXtVrmMk`p$yc-bg^s&3P&)}`rmzJZ(0~!Zh3KuSh&e%vR{eC zGK>-F_Fp)fQZ;bdnNW^lT8b&;8r(zuDoMWvjmIPN%jwpl$N zkxNF8+aKfK#piYGtX(^=H#GM4)s(%yPXDXwrnD-84rx%Zr~8B;V1_s~_NdUkW6ulB z4u7ZE5s=OHz~WRT0^HZW)9dw|$D-Hu-l2<9%vc`?%KMPmLin$Vs!X4c73bZ&@K^7AerYrW7zT|Atc%o zzFl|xgNx>LIrKQJPz|@!Sn#%jqT4e3V&@O1wLJVU3cH|tr}d!tpZw57o`;EeV12}1 z&OtQCLfeSlOwda49Dh6}*B|zm$_vhEN(&|$>FFJTtsGs%0VD|+e{{V&sy?0-4p}&x z%=+|1!%|zvqmi#fVovq|Z{fylffh?OMftU!ByX|S-)%D4LsYHYt zyw`+|Iy}q2s^8hkmhXBFFgQx&m&$X?%C>6@N`PJB-xRcboFt|2nwckZkb~&HGt&_B z^W99+HG>=dHh3M<+Hqithqo&r|4%$7dN5vJMUa|E;}ju~+#x2|ky>Skzz111uE(5i z*sJGdm^q`2*YjCMmJhFoXRla$I45OeQOJ{fUARD~<3}nzWmy76WA*?(eNHY!c}u}j zqveZy{mj06t>GAxe8Cot!`YVN*rlTH7%vWIPjCg!1Ir9(h(0vCE!ZP9oRj~farS(~ zFQ}n-dxIpwi2w z1|j%}VacRUeqvp|!<$^+{4f_cQq}w$gN7G>laFf+t|w$u1R>7DpUBE@=U6HNm%GRM z`UoOLxxhS<)(`VQNJ z6So%Zs}0d6ZE|T!=HDEI97th_J>@!=geFy%embm>K50Wh3}%u;CO9F53Z(tJi_+9_ z#Lb1#Tpg$%y}mAoy&4m`2|a5$yR~0mY#|ZZ9vv(3vrg=htm4So#;dt02UW$*H_BLM zP6uud7w6H(iJ(`8SJ&brWPT{4ydxzyR(8hR3nzJFGHNKqcq zMN~DKp7V{pacA6cToYP*P-91W#~@mAp|H#CAHINNlu2G;Z@7j69pA?U2AYyS@^slu z!XbROT_Lo2G?bCVpM6zlBoW}ZB6)UeZ@MT8wPAHfiP*mH2d@)&uqXK4i>9Jyo7%pw z-+U_iqr0mR5<}KlluM~-2Lm(F{rZ6sn!VWP$2(da`giTq+&jm2RsmZ)(Z$k}e4i0P z2$!Jp7Nqc!NKV&J)HXlo$b;=eC4?2X)txpZB+ zeUG+Xxu$V(Dm&UW+!N`a+o{U*n`<^Mhe_2m>c<6WJ6!d+sqT&$o3klS`E~holI}KQ zmUk0ad}IBRs`1a4XZ_>$vq3&}miH*NFs~=m`?E&%1r-w*uueEvNPEG1bMo`vQOkZx43rdId|-qN%_D>k8U$rnsUd$S)^4-%*x1q&Fu z1m7SIR&$^#+;6f)f$G|(aJ4e?i`g#WHzG-2(NiYI)7_jkGaf)FRHkI>ZEPPw(kxr> z?K9wWXleexJJjZT5!NJgn6bzYR07s9Ef~n|yp# zKdnb!MK$n2R1qqC3@#^mDef3VB=fVIlL9utMzvX%59VG=vRjv1iGrC@rtX`WoJ^ya4qtY{)%Hw>XC}0sE!hdKD7Ofs! z2Y6U5RX>Y`gZUPO3k4nD68*0}Jh5G#rlFKlFWZAVyV-$s4dMKhh|Nh^;?tPYyvpn( z*d>62y`T)H_;+%0B}xJ#c<=kemku4#!;moh4#-O>HhuEZN%nljr?2kgDW}s9d?&Jd zSEe!@<7T%h+RvdFD6Co_d9~Z>6FoRL^vp(S!*=tCT;VD4AMMjnCIY;_E8iJX(+!anYW0r$`bmMW%W?<+;61K^@K*p=1%B0&`Tc z)cnaDHI=Dbq`7+C8{?A!BB5~le+{kXBYWbGpOc_y5@D`bLdPSroo^}yq6HiRJ9*@) zvOj#k&0wUtcU(k$q&7+gWhb9L8q#2SH+9l3K$fud)NbpzK1Zg+#fF9~Pwjg7k#9_r zhF_5`!c0&TrGu?8_Bk8cV!z^6IG`V~`Oe4azK3`|2mJi&d)V+rbSe&!Ad$|<)9N*IBK8k&0v}b ze8l|OXT=Q0E(wD{9Uiu^m0--5`l{V!H4)}Bmp8_a34=Q~&m0_|F z{q0!(uiU!1&0oZ>%vSF>2I1rdJ`~g!QcMi@|4R?#Q2?1j&(w_!O8)NDCe$lW4V1$V z7b7A|LX{f_r4MO&xtu&G@Q3YH|K+o!|C{A8k{9W-Yqj4Sx2_fp&o@c=sG8P|0WWPG zeiuyx#*_OkRx9onAk-TeP-Mh0pA>Xx#|r7{vVVi$PhHYaHCd}QO@4Acgue|9Jc-k9 zYiKttJXa8SIB3r{F7uG6oJp~O-+I?Mvmuwxli;G*W60@?5o-UQo=W5^6ie-1@nK41 z0S%wx;>wKB&B*ED37=tsrzq~fXv10K71Sk7cW6(E+t7=T+R3`b6km>v6RqRd46<-e zvqNa}?Yw5y+i6n~OZ-rVzbp$bGm|7pjIm(u$hfnj^}POF6aVEqD=NNxVbD|k!85dL z{rarE9T^_GYle)ZB);?z+M4(uQed$yOf%2v<`~aB4;o(|Eyzi(2|I~{4ptzns@7lH z@vrpO_g906*^Gq0+pGdgx?=4KV)6ZLBV9ryYD=7+btEtl{8CI{i!psoVhDYTd?=3A zyd)%o?ArGFy0O~z>EtrF(>e8LwO9R?jV2X$?BXvc%ZA8$S|oZ(X0GKf_`axh`_P*q z%(BNVdS1P5l6#24cG-x#)$GiWX$d8YekUWOb zo-vtno`sglLQkKU(RM_JmZIqG5Pd^9zT`7El1_R@Uf(yuZ(0=%K2y_9q= zRLYx`RbRg{fCPx&-?g?>gVvigkojGGqP zJ^)r3i(^}T1W1+0hGfnRoTUIC?~96AeAE~r(KUd_47VFkCGx=xOuVdol;iEQpO*P6 zq*=v3T2b&#Vg6r<|MaWjIKdAWb>T?m@?;gP^?=8?kgt>P+6E3jq2}}!5ihJiVujeO zy9bR*;yF%S1U$et;!`UzJER)WF7UgZ^|22%4oO_$L-!Ipqm}c*8KWRsQadlLI{$yZOuC9|#JpW!dghd9;U6bnN;g z=WJ6qilBI9b-+gzCo|@5oOEOd$P~-gqD-y2H(@ojBvi^}iw)lrTREij0ICm2jj94f zn^sGd^0=gIu6qUF{^|6h%Guj2DjTK;QxTY?@qx_VYjkoPmAI>7CyOE>O9K!4H= z64On0#0&z|l|z3#x_KoU3nhSzU$cWH9#a2Cn94skv_OXu^@+?c^r2fL$9C3l|K4Hp^Utn7EqcD zYFrkk{n4r~2R2ksyG7=1ms}I{F#V_V!jI?Q5&YVtWNcL^ef8I=&Sn(Ze>gli-ko$Z z&}HYivUahN-lQ5muo<}ic|p8_uR^$tdh#pJue*=ww)3_dNZoR-%OnN2xJFJ+>p*C& zm)&kkrj-a;Z*JHmo`$GkFgw(aT3}Ot+5M2a!UVVeZm&5E?*S3+H7EDAdJs;?-DN)H zzX%UL+_2SK*q#0NhK2V*pg7^>1wdO=M~)oc*lM*>(qr84Xk(pILg& z%Jq!d3(Sx9Kq1uO{Agm^akBo{=X=lVM1GZ0AZok~#3Ic`yHSQ04v~Vi9j}lN;x2 z`8zl$A(ocZPauKpeXo3)I{U@{(UBS8AK)9gA%I0*vp}<(>=u(rK zTy%M1oLan0xsR!u`?cB*I^X^A4l|$n+tU<5RA_j^HzRbnA3QlBvG|qY_^fGN`ha-V%bg}#?N#`2>w$LsvON_XjK;dYg7QKd9Ba#z{6?aCeqK%s2p&=2 z*O!$&N_Jp4B*rOBarSyr`R(%I41TJ4fvQPSss|7LWjn;CKkdIzrR>kh+!*%EuYotB zP_4_!kq;=9hvEmYyAqhg5uZ3rCsTX&3@l0=B?3N$eW)n}HV2jZzqNwuE>!ca(ZK6V z3G!P{XD=3`{$gp-NAn}6+>tIMb-nW4D{FXL$}q0BBZ*Z!0wRX&VA$Pe>|hQ5mDiZ% zWlh($tsxip_ErFsuHZM2So){#V59ilQTPP1G2E3r1mJs<6rc_F?3vEH$PPH9zZ;Fj7t| z%v8H+U-+3{3TC)3H0$!F=Sp6($oM#i3!>cEIND!dB+zk}OqJ{WWNviT?_ElR z9HXYI54)S>tfwfgpT=bR!t=8VnT5AMzhu(Jn!SlV+>4%Fo3iN-ejm^O{lMPL%W`6)?hf`gGGDpYB5k19M&9XG*x__C5l1&HF_F&EZL;e5R{Kxgr|-VN-9;ViHHSP!9oNoEGt@czCJq5Q)zanGVz8 zdQ*k|=~oRy6)o&)6%g*)f>VXe;^SK?Vng1&hUX{x;^PJWjwrFp00v}!=l^E|H3=oL2R=n;Z}>_#YR_5qGEr(a zUb{5kvnHBA80T1s8b+MT-lm3b_l)h;scI{gK?C1iA090F}eEG7M1mkOD&fg2xp?P+;< zrXEDz4Q5?Ta|~@~%~H$Xt?gH)ac?&&yQeK& zEBm;sJAVGSTXxHs?qqZFkT;q7zWh&lntW8pli_5w15u0Od(IAunJtl76;ra0Th100Y1r+<3~-cIiA7_Y zHaF5|7pjaZKRc$Mf;W5^KGi{l>VDGH#yXL*+ZlyE!gNTp$C`itn8aPzSo{NSu+uoR z1K&*=MQ*z46T~b(H2cXf+?tywBbX$%>#cMAN)QZI4&)~k-ldl8@7e{%^@ zZf$s&tIBuVIXKh8Pypppl=TPE^;Wrur-qVcM`{$S{JdmZEG3}*lC#nkITv?$=--ds z5pxkcqE>btk0#k)T~zyI1RFI|(9WVoJA;}lsUuPvmL%_gAB+V zH+JiXY3om3CcF91M zjB^I6SJ~RU{~Ef<;4!5z0jKLaN6N4shtddL@*$Df3yfL z#GYHxx9rWYx?wVc`WP9Vm%)cG%{}+Sb+Ai4MQA~ou@EpIX{}}+^+EUJdTM0S_ma#~ z^BS=ZWtAI}c}?Trp8Julzn$~EvfXsM)*i=6B*tCu=Xw2X45G=#wL)({iU0CAU=Xea zv;b@B2RGEzo|{bVB62$@WTLJ{-6vX@EWCE-P&l(gF~h0&(NS}53-bsp>5Qm0=0Tz> zv53Q5kI!nK>kzu>;G2TK8TL@C)y^T`RM8+C2pT89Doi|7xQKFK=2o4`8*+@!Z8h5k z-;J7GOCp~h9l!W88pQG+USQWPq~&MV%vnD0QW6ERDDIg9z7RfZhFkV9Cvpga9 zFc=pCvjca#+3{ja?W()w!1+gFldpWK)PpC4Qa)J4bBPu;dM?G_8q=AP=lc2t6pZ0G zmG+ZmjP?AmDjMa{+8kg^`^0O)XS(`*8$Y<#VdIw_+OqQ{q7REL7RRx<0Ex=2D$gaK zOGU`Mm`t_c=@Ft&$jaMJR&}?+z0AmSILx}?yPHVD^VPe;dfPsso{n)uw=sLk9h}Lj z-iX4i!$5{Gdj&X&PiKR>uL|^ zVL|&5Al|2+tEKa>znG-<=(?fWjh|zP#B$lfs81+k{)2p6}tl`iAwLuwYMmp7{Onq;S?!M z4}2xXFw4mcg>ss@NLKvvJGyfB^j`N)=6D#}7GgnJG2Rax=sl4XNy*i|>lczF#x}p% z_YV%ZueW6UewJyR&iA3L`{3wbf8o|$>l%HT6y;4tZQMAAsrOn~t3D|Aj~K8!%0uTU zFqSbNC*dZUeGvA*XMwz0mVMTPj(lhPrCxr;VU zvLJmc>%{JR@O+##&|2YM+FP{wY4t!_fm#-H6@Au;z6iSW2=Zv*i6weXqa+oy5N2|A z*0O^MZsxvRc2G6;w;I6)EqTv%3R7-OWnS+J3LZ}`ITTNnwtORw)HOv8ejJS_KM8(* z?~oL0GMbJ3J!t^n5957MdaKv=Gb$j(bM;D#v8=RvLKT#kY1x?BA0YFut~h+D6YEiZ zy5&$>U*arZL%F-ZSmLlHO>BzkUO57LohgHaM!f1ezT2+b*Bm!xeUSIM`c?TGk&)Ak zMY(^IAT$-$F!4acH!az+>Ae6>O&h09&3VjgR~$Y>$EeQ9j@9?Vo?%A@11FM41j#1ZwIO*Z@YUQHSJvsuT5aj%>9Y+~ z<*g&hCmfyBpCPJidf&prrOHys)dH=`=+0ozUI7oL)IV0n2A~^ea$L2LK-L9NNkWQ$ z1etr^Kq*%=T5|?j({xkWzZ^7XnrSwM4I?jg!en3+ETKdJquN=uhTZz=ec}H7hB_dn*nM_ua4L96_ul9Fep9Ap6Mem%>8JVl=wxa9k z*jh#tSB=Z<-{{xXZRb@nmuQmS{z^LHb++D=`)|aFQR~}1;9flciA#2|4SxShliotr znyC+O>t+8TK=NJa3%qz7p5FWS7rM_2t+V&iz9Ih1vf6T6ACb-ydInmK6+vXzATsHp zwX>c1Gcl-{3pzlW1UAh!@3hzn>iet>Du-;IFQa6!9f-E)g+`ly4|n8yC#t!Mn^!X7 z)$pZqvjDxV(*X}%>{?_hJKOJ;@N?Ff&T+<2Op-+H2`)>*ICwf{bnLH7qxf^Dn30hn;)lVECjh(5bk$zH=$8qy*^_X_$Ub2;iH`|i^ zgbA%b3=T1f&XYykNqFMs@rDuao=ImBMJUS*&HNO#8cmY}5Xgn=qjh2F=+V7)Mvf_m z>jJLZqY;vqk6(OJllVvW>v`UTjZOQSAZS~sx?%i^mCxXda;_Apw|$VPYA8^9P|50y zkM*5ns7Tw>^rO=$v(m0(7tmw-dnf894o`;^^qsXF#|2~L2ka}*W!n&jpK~uqXd7kg z_WHE2xglS4+CSvbYd@&GPmymgkpGIawAL8c zNI<)6Z*n5hlMI`o$J7yNvqyq@CD1BTDoHb?w7Svh2FfW3hqW4QjE^P~os(>4Wh7yu zr*5KUt+hi>>Zh;BB4j^W{$oyA)b1)kk?*qif9hg@4v_#&c=DY?>nWhbTkPVm_vr0P z3A=>fk?rJ!Z5943yF-JnOP9|;V65ZIHz8_PXEjIw5izJ#i0sVP9|>}b9Phc*$l;vO z$s0ffu`qznAClw?Bsh|cJUx# zv(Q#^;1FG}%p6b%{)Li1*Im3?^pZ@A#g+t~ehJgsi zQ0wIa<5cKSd4n-P(h2)nI~lAu{+n6<>P<0P9jL|3MV6|}3;8yb;xn5Btj{VOY^B8H z(q61A__$MdRmSdQchl$g-DIBm+X^ckWzA$T-0j%vp~cbxz}&tW^U5v(!Iktu6K5SG zDCP-1_(gXG)jNZ+biOHfdO-3xqUMUQ<_g);Cdg{_)>F8m`0TLuJ`c2SAaR=w#|BTt zt0#M#26u=_Y#lvRPzi(y|9h+BO|_InmhPnWMW4`@4%R4`M7&r=$qnz?fp6P?XPuC; zn$O!lJx!qNtcuo-T=s~ebNA%A$9Rf0CdF_mGrbh59~d~zr^qC3qy5V{&V@9|E%=$7 z!Bk_n*~KwXAPVATvfl>#C-z{&Mh`ad{#iO=Y?g#HTR4{X%H2&lBm!!79LgPuSTIe# zK*YQA_r34s@U2nIi|B0-FszS2zI-paGQ#Efu%%#M)3C>vwDdijIB+Lk2$C$}soTht zoqHDIbY(Mt66Jg?uRHJZt@<7$@W?o(RsY_MqESR2^UG( ziO5mU?5!)FO;uS%T2>XYng}nr5fJ+}HWgnH^i*2L@4+{b+VxdBq*obZJlheIYTN~? zM6l$Ione90*?MXSPFs&`M7Z2&?|!;F(z3NKNadzr0Vw6ydHH_1^~C2nNNIG7V_#PE ztF_HUsrBs*rJXNGvL!5nd+77CiOy+T_l`~$E+WmYU8~WgvW7Re@ZzlLGzKmIcLo*N zSNJYJbN0qf*?%*Cpnagt_D=6^3@LsrP5YYm8S30Bx2iu6$TzPIOZlKNcx<-n(x(8& zMwiq3gVLJHdsZJERa?DNZS^!{;}oZvDd0)re>Us$;g7{*2a(~j0GKG-H~s_M_IJbcqG_O1JDe2uL-sSUuz&J~#<3DBj< z^qMLU>=T&Cd~V2Pt&Ca`Q!__Nh%)uO0#jrI%o}7gJavUz;NteTiXyLr?f}=(H|+ea z0gJJuG{VmV{y?rVX1LtPskZ3YRv1NO*Sv{V`kpy$p6cKDpr$E(ua>8+BTD{$U9{~- zAAVI+<`3(Q@C6 zHQV7DC+TN~7vU!~+h0RFf-dCEYn`vH7+8?vI>fRK)M)9;FYyeJ@8X)XTDn?-wjX!K zIhz^bgT)&X(Gq13gO9Fij@l*!h4bux;zHzwPqmlitl7d1OZviSj=w#Wx=-=|pJeKc z;fn;JPc6yq1p%(tx49Q(-UTYJ7!Jw{W_5rPbmk}3iuYC2YF!n~p{o&8NL3hg) z3L`$?P*1rnGZ=nT>pz6fhXm?kQ3T*d9Z^{4qMp+-^sur}M=d0GL2F~~_W+Tc;<2_* z=5;QmI!)kVX=}&(v%lueP8;_gh@FsKIR(8_-M(W~pBtK=Gvk7A@^ujCQuTu9;19p? zt+R91EDm0?O@Q8d#7WjK>{07*+-Xi9v4b%PzLy}~i;cAPV0(l~ULdl299BPHa6}#Y z;S93O#K+`vwH`IBSU~krY72i`DZdyn!Fly1me5o$bU`0uE%j`I288zNfJ&zf3uH=4 zyey9?-2Z-v8|y1QH*#w$bmd#rjUdT;kiDe(kA#cNx(HcO4G1nZRphz2&j{lHZy~i* zP1xSIE;1RLZNy)Lp5ncipAH-F$uw1I9iIx@K*W>mm|HL0z?AwGyl3#N`z43N6WfeejD>i_ zW&E1kNeUmcHEkx(o_jg$kpZCSm%X?(;ELkMpRXUA@WP)P?$O5p4FTnG9Y$1M@0^~X z<5y_+@PT)vk7ogt?0=?Jo83L6D?)0|Or}Vso@8<*s*t&k`u~1xE#jWW3 z6Y$ugotNF?`yTlN!_2>%R+bt+PqRx2U@2yvc%uUp%JAuvx-oK@#=qB!Ft=SS7j;HL zi4yE|cs_$Tu~5i}g_Z+`)|!8b&tB)?PK{x6(e}D+_whQ~+Ji#@5=&{P-`UrZWO%9G zP%FE@8ctuvXD{71>P%``wT77M3kM}r_(dgeGWU!&ihCZ|oD1%8w_G+4B_17|+rXI6 z3*0t?d%M(Rs+8{z%=fdX{duyLXk%W*8@*G_C}&|Xs68UbPU6)G{#gjIV|s&WVr0;Z z>Mrlj0ajy}P)=^5>#f78$GqL~m9I5= z=OfG6oPI1$6l|t4v?XqjfgR4MUv3Q*2lUEr*vmh0IN@s@`jy+}ORCbSfhzPD&}4%s zZ2Uzx5@-Vu9y4g~O+Ndab^9J8SbAx6!CS}7%%qU}TGA|dbyK>tI6=RdPy(7k%Jn#X_U7o$1y*R_bbf(lM0nj#zKc#tuK6L zxX&Vh*WF?Q2h>aA_YhBD84#)`KFJ)2TXwlIAbn>zFI-3Pm{uD1!Q6b0Ys8Tz-QMe^lqqE5$CjP9V?sd&H&k)*{GU( z2bal&`Ms>5RbCfEK^Fu9Ri1`+ukgQ3&{~b18)^H>{Bew+dAs*> zl3$gVCC6*9ACy~SGZUL1EU`>I*z54407^V6Mwa09qUTdf_vo8*Y#XC|+n$>C6At_sRjZU}&3pTfUr4M4dE z&&Yrw4vtv^`n6f>UPXQVi*+N{Ft!!l=?Ms%vJru-Wf&{XIKLE1N4cO7>n6q~ZV<^k zO?@h$8#c$a#t`#n{Gv$bw8x zmdv}i^=wkqba#0*=(W|ZeC!&!%ql?vLZ-#Rm1|ipAotm`ZeJvuFXZ>uO$u?Hga5WrlX5J#v)_65FNqFFAtqI zL_-HouwJ+;G|QQS%fAY`^J5@CUpY(QW5(p4*k%%%b&2bm|18eSpzs_mb{l0!N*F@G z#RUjw=N7j?6s0l_T$p6$JJxDb+x^ne@JDr4!*Y;mvP-6!tAl5W#m@ne! zRp4fDUJz@aNgC$zKdpdY8NJ%>s5M#YF!=ylkiemn^j0uX_sRyBUGWM%srm z+GYCO`9Jdgt=)2fOpq3GN6zHqK8Gv99gPpYhEfz)b4jCphVg=;@GZD?d3~GC%es5? zfQ%O4w>yeP|8^#JdU|o@F!&#U|L9se=X#Y`gLSB84tX&My}Mf;37rLA&h0f|me}ie z2-%&3&6I;oBE~iaEMJMc>AJNTC+9-E4%e=Z%F6Ce(0AB2Oi$84n}y=WmuQpQn$}Y* zKMIp8DWJ}t0Y^3Wo9JcY6~3z4`62+5G(<3j@bjf%_IAF2%tP%TWIMe*fy7oSzdUiCe^l2J+|g7X2-lMsh}VI>z%BX*Hu$uL?_ViKpWh07SFy0 zTTSkRoB&ELED#P(810a^BJhbh)#CA?tyRCq&&mQEXyArsA6*AU+3TUGf^Ca(o9W!<&D#so> zw2iXr;SE?h9D5P=s2{L1wuC^@FSHP2yPf@v7g2mvr9ZYTcj^n*=Y;IgnDrnOHTp8j zAz8lMByqq1Rtu}Y%?vUAZxXCK@V4l1JAwbEio@%!!s*{OAyJt>U zYm%suLgn z_Vj6GyL`Ik;d)c~hSf&??(0$21yM|c|5WP<6%TIB7TtPy?MZQbQe#dv2x!8#tV$++ zG`-nZ{pv|a`{Cy+uuyl|^>S1BXYS8-+b2?KlG}?u)<4oSpLWmv{F#s5&a`_b5$bxr zBCVT3y|kq>_252@`u;s{!2WPAh4}Rv4mifKf744#zw)e@H#>R=uT3mJtkUpDYXzdK zZ5=55v++LITMqO=BA^|F8lB`G_G->|;84^~QL)yw^zZI*nLXR#-VX(T8wl^PML5wD z?-O%5PW7Z@sxP2YWay4?*^xdLh_dS8T__v?;{`_h3`a%@U8yK_@CHq5WrYzbD zoO>`GEoWg2RfEAf<+U+ApJg7~e56!c{xTew)SHhz{cZ&>8Si1Z5mU@ypv>aSTz}>! zHW^Szw?rbco3{R;1%Hp`h~~w&RoPMkknsFXCfAu z6qiiumSw^-ZiIbfq2H*%3Hsx$VG9NquZ#CHT7MP!dh?3Jkj=Yg{az=3gLcD8yYDBL zkE{2~9_#<5q2`^nL*uyq6$As**1awhzQ^VFA^2Vq(A#$2%XQR4pK*;?qi%EIa-l4P`R2*HxdMvi}E+aj0NIU?`-_!rshPn&TTQM9tZ1g>6X z;H(EWqAJ8?pUqsf12=mG0YFmBz1(^^YczIT3YFhZFFIgf2yHHs^IH+R1mtQSi;v-$ z6fLJH58=gEEuN34RvL){I~q9qTY0izLB<~Y4hyH9UHlI61xudcdGZo$#FVs?`v=}R z$7x*3LosE-Wn?g7c=(b(OYm_?ANSV>=CNsw3}|sL=c32i{|Bc)SihL*I$NoFGl_KL zk=_A(EY_Y#lczh2#-ioxq5GWfymacOTNm7TdfvHQjYpJ8i&qF{$&Lnu4^9HI4F_PF zQsJwSidv-bfPlvj5ctrrwn)nc-%1Sdgr2$-dlyVpO-1DCLSpxUtIC`Des8(O(M=IM znmXi`xSBaU`tBy<^KSdx(++gMzM~|MG6@6>cJ*Aa*-JYAzE}ONtV!7|SMq0U^q4$rO;8C_?UFSkfzP@-?!wIW54c9L(o$%VIx$ah_q?Mh!A%OAsI z@=4E{zI8KQ&3H5I0&I9DAd^aq8M5*_sF4TujTlWbmyTL@_0nhDjax67WxH}QW3105 zw3+ZYhH=u)l`0pEWtHW$>6*!U&}kg{@GTzy3Vr+@ z_-jSnbg!q#7SgWE<$-nKF($`Mb}tF5X+aR-BedrvQ~T_AL4|8}loqe+HRLY>AC1o0 ze(j?P8^%nP47vgpqWsch;3f3F+btmAv4TE=;hQ=Z?7hCsLeaL7HZH=#wRPd@`JQXa zLpRs4DE8&67WpAWC;CpYHL)!L`UyAM+I8`GqVH|w;zWY9h_bPzwe-v(oTE@ksMon^ z{%1<)I?xWN(O`%!k*1E}sw{7%l}j_f@uFMIZz{isYASqBMZ{16;4u{&TV>VVc2gAT zi#y5khvI4Wzy8U~2hTcg^aS4HqmIX6umTkuWA zs8pc_gGnk_n?pG1;~0V(j(zxp{J=OK6LDDcI^BNKrP3H85qrznxjtVLE{xOz0vb(X>S}$LZR>mY=y7YLFvRhe%SKE}z2GKW zPe?-_@s)}oAxV8>%yl6;RDNCe@p6H9?K3?ykVc025~Yoljk3(;-}^%YGCdnZl$*BH4zf1uaA*iiSC!aDK9E*$OYD3k58j45gx)m1Hwyu~kF^iekBCKyBxikq zH7b0Zxj3a8{s|*@x&qN2Fi1lQ1Oz;O;5$T9M)TymVV;dAbS9>DBJ$-@LiOyH*T}oM zK2OM;Zx#Y%GU>#PrWf3F=V_OWb|oN>bWP#dN2$Qj$Qfd@xcv9YcgjRvKZ~A^{_IfK zksFVvX4hgorbjP!uZYZVUBBd(ivw3Ik0g*(E@d{a)bWVc!h|3_8>MbhP0ONeKeaP$ zz9%1T{m7sA_Un(WZT2?`#Jhmk!QcOZ&lP|D|L}*Nn~r9Ou8H4ORP~c-+DQ?n+ZyWf z9~0w6W*$}_+`8+Z|77{w{~&xFjohuzijUi0Yms;*Pay5nOxT)rD4wx3k#L%gU6PJm zDiOIubFY~jfb>s32opzFo|++!dKO6Nr zP$hJ~tC8pj(FbdBi6rR+gAYj3W>;BSIzlybc66UL>Pa$n`Doisww`wBcvk{LF8|*0 z6@Cy5Ot4T|EXNAN#`4ubd4YqJ0`F&VWV5v!uU}390dEY{HIG%^80!Bj%=(&+oy{c~ z;LpwQQvVDCaC|m+iy8^c#oX1>N^K5HaVQV0HLv|QBtIwtNjz~|lZ$RkM9w$VVgCYx zQ<$g+Y#5bTo1t%5ZH5uLJ`BHy1K$p=7u@^yVRNuj=;MIE)%G>~?t2;P)a~r<==Uvm z`O2rTO-C-P99GE=&8^Xk}St^RDSWF&(%6ppLVj`o`~g{Pr_{ zt5~g>0rvom&y?An?6z7>$yNh%lDCEiDy7R4rsSymf$xq^uSrRrjAJ zYqVq5pF(u(wGK(XY22;8-}zeayyBl@jca4wfGnB0>Z)o_>6@!fKyzRmOrBsI~5R9MrDw);@X!r)~n-J;7Z7ag{BCDr;mVC}# zh4nKD%ncL3BtZKOmDC#Od#c4|MLyQIa?b5+U3Am&MWtb)@X*BlWHnE?=OlnY=-asjlIqA^z5WSz>*zTVHFh!dtxI}I)b@=Sykp;X zeb2SpmL|=*c|D#hpbu|Pqwi^>$@|~)9oHBO40waMU$x+o!Y4lX4cptx@*9dW`P+5t zzV6}^`IPbLI3DXm%~da@ft%442uVc@5g8dl==wMZ4c&+h+X%ORoc9rE>@k{h<;qf| zestZH3pRS0E4l2GPNRk+?fAE+4E8|}En8oeXwoVgyd;x`zUA_K$Hv&PI2R>j$pz#& zc1&s|D6j0?nSXTkRV~Sy+aaoefVJYJp3(bz46EP%)aswUS7cl2q3<1|5_>bts@0+? zeIu&aWixY&<$+tyFzj?FvM+SM7wA>t%}%+lIEKEqrInw1(v31{v}!vdCt~Cqp*#AR z&}ln})C;!{28MJ<2#?7RN=)arr&A@nbXPwADYtiU$yE(A_z~JETPZTiLsKy{r@W~w zYPZ!LE$V}D-u?1q6912l+xmq-=5HPkO6_>)@Vh_tXGd9d_(zI%`QI&F{Y@8k^T{~g z(#I>}>L#w?P+xNUG-#gKJyb@4 z&;!;A#6(Y;t35!R`y+tL=q*YltfN8em-VVuc8t5ywe{SU#mp^_n87_0!R@QMvT}qv ztr|?XvI3_EZ!|qG!S;g7$GbLGZu^z@>$PKjCpE?#bOup+LLa^Z1$4%QZl9--`Os#O z7KY8FSMO%-= z-9I_(p8V9G`>tmTVhDKiIGft>2!Z|7pFj8VA1s>gTbD)lpKtHvPwb3FkvD$|q-mNK zPN!Vod@vz~J%07UGM?`5-+$1I)k*`)M{G+&H7uN2*I>ukKHI4tx&_kq{FZALOz2^R zkS=;_0u>4kPXDo!VI3J~4oLPuC9 zY56E|X`_iDR3%=v~i{KQ>d*uYNvHmx1Zh3o9|7s z(Z724EBh~mHRkKV+0>3l43PS>yD$Epu1){etaN{AYchG;)+COwSuFA0KE#CGkl5q} z;xs~MBa=o_KV5kzWOhG6gT+i+eQx%V;>Z;2h$GG#usCg?+Qfct5(tzO_uhmO) zm#{+$S$FGHZUfH);KXS$L zz|}gkZ87^L=mR!?_E&Y))njSuTW-9yE3I7hdAu#T6k#5L6AWf(Rq0D}2WH zVXE+OZ8%y={OK9MVy4SM-(DhOP7}2lDwk_lUv`HF*WHYsiqfw!rww6Uy)|?Ky=@Fm zEW2oy#PzRF^Y|avP4?4aS=#z>b~WS?1U&OQKl7(9Ce8A{IGSgFJ)fpuap7E)#LdEG zSbJ)=%dnd#;o5o-*UOgJRhAiV$b9vTREUm^-QN0#2yv=4s*I@ zX$%N>Gf*AEPJ${Iwiggc&CrYLIp&hFoXGa2WifX}c_4S)Rn^>Vu*V|%RQKUuR&20( z!z?F?xF`A3Zj?(noou;eBodEOn_e|wDMX-`hIg)t6bYCccq(lv!dGD9@QVn^X7YYlXD;b zns2+j2xPu~oM8=lbn)B=zu|mUHs3f~xbGD6{O_KAA|I#v^5b@@+Kz@U`Cl43UGHbv z?E1HOrTHS`cn#?sXu#}gN)W0#J$-B{YqXuRd3bgFV=M!ZiiM21D$+rayyt-{aiAHcN*E29OX=;lZfi)`zD?fR!+d;-ZkYVGIow`*0@eg(z$fvo zKQSwszuk1%H#~X%+<3&~FOeg&xEq`4t_Gxo&>=GWtOZ`l@DXzjH*~wL`{PE)Kx-fl zX$tfjK*9!Hw+VZ`dysCk#x!mkG)TRYrcsFKF&(?GuE@pW&=pJ$s%R8@GG~ksZN~>{ zSS5Az6n+yvjUE$)FgMGrU>Zcs)Q-jzm*f}ap0Xux32Xd~(A6-?kH@`=`?vdgBcH%q zc{bo~SS`VOdw+Ms8+k7$e(60zGmoWpS~t5$+v@US-_^@Qo9MM_u zugkXo#h-fR=H)P6zFFMcn+`rDR=LY; zqDUjsvq@jP4wlAr6FFv*GBIr?hn)pAC9S0XyMFv{9;S)g$If?#*3f@%rn%#k+7^<< z7yi_!nIANH{}rPobFHaLSE>OlW>;Lb+;g=WN9?=n+g2!g+O+l)?Z)f!5VEd{plQFX z+0oHD>wAwpLDzelopTukKiU$>Z>u35>mKAzSt6KluCQoO^c;`(A?iS@)2E+vu1jz* zBObSvsd?++!w_ij`=y4*SBC^1WA!}X^*}kVZu4oUy5?6n@Df(;$Qd3JPZA%N=h*#ddz^QMQD(`aD z#VwbQcHLx0f@U^#vFc?P)3uI($`?+(;!*0NKf|Hjn|5x$`HR1ke4b#qcDH-41N(OM zT%TXc!HyQE?pZ+(b>63A5Ky>o^HQw$OKDeH8Gd7||M1|FJ3PGM<|6WiH1o1z4v;N*UvJZP`%^fzh^ zYkSi*g(Ua|6NqM#<}t8QiP(JuNkeG_u1EcT@;*-%k~Kb7631D!EBXUjv++xsPFyzH z7P)U*w|tUriQFXwMI-am*(UI6u3IKH5ug3^P#Sy`gimVa*BZE!a_y5~7)*aU*3lZF z4}ta9hx@D70`3WvvvptVcwCGkmEruD=yZW$DjRw>aL3;@a@IZL3L|QIk}~V*fyBH~ z7w(IRRR>M@lpxwRm}XtNW_jS6k~#`O%@sr;=if7v;Yb@{+B^?^vuT1bDUE!5!8CF< zP%$lA@sMsNZ~9%<`m~<~)OHw*(3NjfpDiN-38id?!o&|(^;_3%_)q0%TQIAFS+IXvJv@wS-7fctp|ct0)E-6o|Cjw>qQ-O$(2#ozJDu@|M7Ny;kUzh`TfHg z*1E?B`aYs(-~Y$IsO{RnGG9dhr|~5J@(Ww^6*DF+t=qOogu?jvFj&!lfFke+t-PJ+ zI$h#u>JRpJ!FQXH2UpuTq#tA3cjfX(S`h2l*RZ@ zkToKvjpu4`N2VP??r80KzU89CU-ufC8djV=P0IRKWoT^GS|8iXHeX;ka{J`m`x)Hr z*ZIAAE#TgOfJa>iL-~24@$c%STy2$ugx=wu@K)_y&R_Em0ya*9#I17C%}pHA#xZQd zBsbMbR%+0t9yLd%p*N7YzPmxbMBv0mIpH&`LvPS%e9n!>yKX!=uiqkaX02OKTi=7F zG*U->uLJIkV1%vsO&XyLXTcp@vRKXWj5-<6EIKX~DMIv%;HOWLU#k3?i8s!A*8lDR&?O3#ed*qnE+enM|b zA6|0Zjt?@dLMe&X4M)2nji*tgSyyQ5dp3y3_7O=1M*cF zu@MYgO48L%b4^z}vy1bQG^K;fE%5HCQLb>)DmKtADMgwewLBtW3CTa8r_=awX z{{#bZ42iv4qK9j@KO1m&@J_upd{SPNe_wu9-u^md*;?KAztISN-RO@zC;yx^-BNW6 zBQLzO+Q!CfCb==@TVkGdX2jmv>WpFOnU9W|K;2+QI>TZ~HWHCfZHNc!Zn1>#$V|1; z3y>)u>J&W)K8=Ln)8o^5CK%#BCUh{Au`j2go?EYz`Qtk@(Zr?72XhJz_iwnvLx}v) z&E~gUr!un{`i%r;`D|hdLaAD+ByzJdpXIIlmF-dYLwS_`J`rIhq-p+TTsM~gJlX((M3Vl|EJ~kSbsTz-M`{?2b4Skh-9z>{0F>}9_ zR-;A%laI~}azLC*5&J@t{DF1H*X2x&?Ga6bJqd>KO%FB5{h75#{*OiBn;y;~Y(+^U zrSgi^z%s*_tz4}k9-+ZcO><|6!;e|AuAK%=5B6)@El{9v{21=g@Nw{DenRL&I{M@B zwc$7Z`tSl?1I$a%Ta@E{`r_oh`114REpXJ$K?4TVjN!h2u75|=3TC4>BJR@n5Mh>; z-lDc%=v%53gSY60s~JPr(>R7-5c;g=C#U}Gn9K8t>JLL?s&ghpB=YTz9$}Qmr-<6< z^WyKH<@`gyKHHZdGET1EXpz4y>CbvTBxd} zG$m>6N7vM7%;b7&Tk=aso=spP-)b#sFgf-ur`6iypVrFQA3M_EV@}s5yNT=*Yn-LI z=UZlxj#3^R(AK;$}cy)L_X07s2 zzL2t=*-d}0Um_2@P>Uo1>JN@@LXNtkJ4`BtblRp8f!}iV^1ziMbkoEk@qw_!JVwFH zId(7Wi z;cju0R;7?~RlZFSp)9S3O5df@-nq(#A&#V(o1F;}da-1wUTZ=#`L8kOYC=!$mQv1J zS3W;~Yy8;O~Gn{3ud#oXDh+h1_fBcTBuKxE$k$h_xP5!`z3)^|B z=7yzWZKFm3E=fx@CUgH0He+4j;)4!r<$6%Xd^5P(SOe3PT4_?4l@+-!7yG_DUYb11 z;aDPvPsZEriG~JA4G--m*W~)3j=^t}1@w>TOsle`K_YMwIbEobdzR}wow#wfE1$il zr54NV&g-{YEImsY{%8cC^Z=U$OblEeNIu{V!^+pn+`q(FM@pP}VkHcVa2yDomun=1 z;K#ct^_@W4(iEvf7&u{Kn|{Y4ald2PN&P_p;C)&61gLQ9ZeqhTXyS~GQ#YcR@o_YA zzk0&==rkfU=7tE6BL6ITzLpG*-^X?4%+aU8K9V6l2pmcsWh#XsF%4Y;wuGPAY|kAX z-tzNZ%DJn{LV3imbCt5TltK|_*8a5RGTI--$zF{oprVKfD!|q@6t;gj)C5 zf^X$>Kl6>dadY(N=VkhTUb^<{o;Wv}Oi~edlc}Ln6QPeT%^?$kor@m$S4L}z1~q)! zY5+`Qk+UPeeFKAL#j+u0zjtb!A@&N3T*aO=fkRi-boJ4QqFpgQs%I6fY2gZDXrON| zlHsRsUU4c%Jzx#9v@MQ|ttae$m7aG=GI99`qGxFw6BHB44m=~GH%*^&CghPX;w3e9 z#yhm*Nvs_0V!-Qw_d;1i>R#LSROroz>Rh+&w+h*ly=sE)RU&N6j8b2kdamCyIcBJf zoK*MhE73E&8{UK$2s&)$3 z@3N*4R$i}v>ptzc)n8{1r2)0tpcLtpa;_K)*Y7-2iAo@1lHk$oz|D?s*<`^|WZpKc z5bdx zYBW1(?TmNQU^9uXDD=B26eRxCy5LkMH2C__JE3oBhlcfbbTLE>DG&sWm436;ZIqd@ znm9p=&)gXM+tT3qjh%Upkh)EJwh;Qf*ip{%)L*0Zr;8xq9^u~>Gt#Lyepp5#7iA~` zs%vT+j=@#Z7+FGP!!?!aL|drTF*izYPGj|9cq;@-HL+!hXxsXhF#3|QS<09m8mtkH zT$*l)z!56wm0=i0PS21M!!dhv1%XaAfUxza_hGO+_)~oJXJ-wigSx4-T5pBYk5-Oj zojvmk8WH)?{+@kfwXQAZ5{#7BhH(fJgeV(n=W*H{HqHL1>waS=Prg6y;{WjOYxAo? zD}S)?pwzs_9_-lmnJ2ElGcL>jVz!9?N}43^Jij|iMeb_EJo%Vn`w+lSP8y)J+Yg$& zzNcaMtvoeqNNF@|e3J%K8aak&QZ+h^sWS#zsj(@`eKk6_#Oaka?u}XaP;>2BU9gbt z=!s$Y4SG#O5HDNw{KgSC7kSEex6?(hYpi#dwvMGbT&!~@O4*Eh(u>`0y7FzVfPc=M z5jv5r#vkx%hk99c zpS%YX$i`+iILSoflk*bPwsnp+o*O-5_=wu-iA&#@9B=aE9mEA`8^%llN4=q`Npo5R}j2NDlj4ScMj#wdE>C;!;_Ch7i@MbUg~-Nk=&cV}yB zG!k{hBQ<1b8v{WOpegbCgf(K;MJzx5=9$1OWs#QSMv)elhPx42>bG52)1k(ub-BEF z0brm-pBLq8U|r55GzTU+E!=``!Gam{H1KM0kF`7e@f=HWh!61FpTXBQe#mdNM#R&L z7(SX6Vz;hV99jxTHEtbdXGH|Lw09$Vmdc6bnFI(SpxId@$2l3mdm&s)O@D1bpSV-F z-OuJRq+*Eq&flD!q>J+QSWUn8<*#_ZL){$iSzSL&w&<64KDRYrR`Y8*{6y z4X0PeZ<+$B;$LAY5M(%eUvge}goq*Kq1@?lVHqP!&Ag5ED4D4IahXENb@bnfO$t-} zvEh1!5!?IbI{72*J)tvxtnY6a@0Vx13;ws&8#~7l*e0MnTF}I;^zp+dLLlEmT0Dk3 z{v9B`NI-5y+>1STG`nTX_o8L#ikY-?3CKfFAe|L#CR4SOwk)H=Nm~7OmZv}7CF#F* zuk63{p7-^WNdg`?4DUSP0fH@UbMybGXq&%2ue)y=Pm{Nun~v2KP1WG>G3(=%cIdkq zv8`T8dl3C#Hdc<^+%Up%cJO2geB;93!3n-q(oavGV7N43_fk+je$iU)6P#BKZxk;oxjTRaANL`B*%y&Bo*TZh}!@LbA-kES?ZvRTYfcz zCj;KCk%-8cqX}6e?oQ6$owPy-yR46F5P+*gsb(vu#plr*q7x@M}{vN zU2j}U9`&^4Smivgle#R%FHW-fKjdlm1Jf@5_}6{s%|jKN&v?KCiThI<1Ku#`g8$tM zm%lV=>%VkZ#Q#;2G+%ap8jbVxNqyMoZ9JFtwq_cZW~JAPIY%Bp(Ig%X*n}qK$;MZ? zz_LD(17q%DcGnaO+s2X2Ve0lsbWqy(E2UwUulO$(;Q4{rAPSnqlNL%%zLnw7un$Kt z=<-0#s&3E(8cE1O-2M*BpF||m5oo2Wf(1%5}`cJoGM4U zaz)m*0>Dh|B4+Awl{L2gn;MO~@Y=S{u|UT7ZO5c5y(1SRq|saBOI@ME?rw4NE_d`NpDplVy`NZc`0&N`7^%7drb7D=p8q0(v@v`kVer* zM``;nl4Sfdm!I7Kd@vI8aNyys;g11mgP#AnZ#dUg)t^`{%Kx(ImftuUT|Boty%1%I z8ZCK1Q}K=ZVmhp@%N2S!d;ekvR8t-XKt^+}oOSx^+Fl`|)lO4_)R8$J#}U(KiLjP4~ObjW;ucum6Yp3Oy>&-G0d2vdD{`p=GV*aBG8 zX%#ie3wi4PeUiujvTG;5`R;4i=3(O7!-JDr5b(f)4PL%v+!PEHxXARR$0^!x$3nhWBDGj%!3j`gBzhd4a(H zet1FJamuF&>(p|fYm~KV>`Zt=VA6Z5O1GRzGoK&%b>+*sV_)~G#-6>Nv5!RMrt+zp ztSOpgKZ&AWpN#8&o^EX`7wR)Yx11+oJ_t&x!$MP^XzVWVF=61UFn=k1@1XcKl)8fa2E z+QCk}Ur(%_a84eF$o0ej*FFCn^CP^M{)YYWAz-SO+4m5??~2xSImq^Q+?C`7AIk~w zp%8tU8fkmt_5@)RY}YvVIq=U6AQ@hu(mjrfz~Wdb2ZQd}`RY-Q@{F+yAg$U$r^iA> z%>*`EQYP&iO&t6D%t=V1i5^B&Q|=&Xo{?)vzXCqCIEO&N-TpUtxXB#oePBizGPiGA zZ%*I2l_kIu}R<0`5Hq@%$M&hl#Q8qtaG;yK#@ae70{X~)_ zKUTL}pMLk>yf#zOJ?sVcSmLC{2Rsx&TmLr zCTNuyCt-3>JPgZg}XT7bXGd-o}O_{p3iQ^ksvbE}Rj2YTTsY3jCZv8*~= zGlyi~Ee@}{25sE5^v2coAe>P$xyo)Ntvs&Vw!D-i^@k^^`xohW{7Zl2zd5*dTagAl zWVroX40s5Dw*Hx~`(sax;^m)Q6wAL_vq3l307XjyZR{S+#ZeqZ~A>4g^>iu-iv-iyf}Vam$(Hi66%fj*!upIy~T4k z9c_!ObloPoiNJM^r8bPcV6xZGPDcnJ6zVex=;Ipx4#G^2J9|F((P1{Vqd378VaSW~2^upzBP ziWb22MYbb$EqhHpIKmN*eemo41X;gfe}P}kutRp(vchYxprvVxwqeU_k`^e41PS6c zLtq9o)7{lom6>}^W*4S$u?UI-5;Mp%lU-SvS+}gL@8p-4FURa^8Y4U7AVn^5%5TGp z1=mi?wL;m~%WU$ZFZ5qcszY!6`u{vTjaN$8$=H1*{{h0QfApDru(x>3+UB=6N9}*l z^Tj9j4j;-XakVjl$Z3Ej8exH~gr>ezaf8qS3Z^K+S$y$KB$dEK498!Xio_H#ED}2W z+>&V7H#o--2U9N>o4q^ zI{oZ;;{QkO#g~Wu#RG$)lo>R2k~y0k4G4Z`Yw5W68^lZMz+Xo!6yju^8S7aMAqD{- zH8rM-M=<_I826DjG^+xma=+nnGkTgjSX>kN~Ee?S&;dLG*#NN!}jBj(ny@&-@NkxPk!{5f zHT+be;MnrOv$N9GjWiGa*Yh_3KXD?G94>D(O3?i7th|gu=FjSkYfS3zC6PM zd4rhQG_Fxmq=p@|@Gxa;)?`ktn}gMG$~e%Cz`cZbge_Z$*h&&AF7Y12bW1+m?D@k% zeBKZ}duIWxV6gVlz59?b__xwTa2gi>b|&WUJ3cAFx=eg4t!OzFZG08(TCn!UE+SiV zWIM|o%0Cn4D4|Vh>&9FGiT}OM>~~a={muTp*DpNv^)N~_VHaYn#3bx^P_caN)QQ1h z>c7x5_BS`{>}z@94lgcfQ)CdxE=Q^o6rux>aoUyvxlo9Nk*^Sh6!Km3wM0#y^VvBP zIY^Y7nmj*h;GSkgHD72sru{a7j(|YuNEpn=h6Ko*AS_AfF^+IDW4_%0HTW=!JS0F~ zkniZDCQ8RJk}e!cgQOmsKWC2Trj7SVKnM%O9j^;U2hmf*Z^AZI0N}XxJu3nhj^}1G zK1aOsn0O9&c_TPx{@Ho`FyL$M{sVCT9qmPb=Y@~^T^P1L41hyLhW*$!npN zPsXdFg5#n^G}uE!G0l|a2zeGNM?#Eb=BGXz7ebvWRr|*(GvDf~;ko0<{`#)R&E+d$ zSAbULe!ORGzmWAWPO8ywSyw+dEVBcH-Vjo`FN9uz*vP>VUi6_kQcG-gD;i}*L!lDd zgCRy7G%gD*jKmJjz;giEeJBrZEyO?h4sVUU+fU&At`hfSs`<2cx|{j1+v6#mNz`FV z9$e7gS@H6>=YJsdTfrdQLZCkW2cnHzdZU#(ct8{!d=>&h&J2lUOxI!FKaUuG`_=!W z^dAIwc&2SR??(fYdFb+iKazUz1^=zBEF^SR+GwiS>qyG6e_RN|%8SF^v=vio<&$Mm zRhz<2IUS7__`GPKm9t`{vWx>bZSCuItNL%`#(gI@=C5jFUi+orUtPy*y;G(p>@w_} zuO#6EgErxXm_DLi^WUo4{kBt+&-I6@Ule_afIY&9p~!3`j242#QbWI?-B2J!;lfr~ zk^?odgpUVdP+%D4-F>orM{}(SkvYgFh=Gugd$ZIC!#n0&d%nkTNB{^uywk1sN|Xr)x1JrAefgxb z&DTbC{VP)H$A=3G{a!YJo(Ay|Mn+nj7fv9YgWFIB=1Dve`iH|@M?nDhLI|=o_-EN0y16)j6W^A1 z8Hhc@H-;||32e)e>z74at%zp2D*SXJnvoR^iU3Egv!A@iYnxbEIaj$lo(p61&;%Iz z%prX&RCD(2IS_j6AEoe5Mdn_Egc_T^BRW6(?Qg?8Q{MnvUApZSIfK4uJ^)@u(abD& zx^#)mKRjlQ?QsW)dkCFn?ObDu;CL2Vy`#BcE?{HrSWG9^MP03nda@2e-vpu4q9NWr z$egWcEz(zBH?4E$3oTzVTD>T}IW1Pk@16VuVNoQ~OMF1FRq7HR2tZ!TGpA1MFRXp8 zZQbvTynR-g{NTWp*rch`0N!VgBC5Em9d{sLTjhEO1Sr!C-e??BHKIhuL}L+ zG5wv6xuNyX;nuWwx_-jlA$Cn6k7wD*oDBWq+%hcuz~LKXT7Fm7ZHP&=4ELUB>J8CU zm6(oEp>FVAZ0PtBQ?F33&THrW#X`4#uZ;Q=rOSW3w6uC5O+iZ_(pUR{g8l26oE#Q; z{Y+Jh-xyEL7Z-~D!;5{H<*K1Lg0SJ*>bYo~4#c4$cqr#PVSgvGNVu1{7oTf-huaRS z-W;fg?}U`i?<7tejYQ7bB{3aH-c3YBZF~x?ytyu>AoJ09ihWWzTTW1^ZiJXhQ#I1R zGm!rKWv2g3y5gVqJhXP{$rnU}%cPf>5ReG{0SBAn-~Q4se8Oh_%WbV+m|FErp7}#% z4|;p9Bp2i2vXpF+SI`u?BGYMkJKd!0P6ByiyU}q=!ZreV$_g>-z2Q4y>sBE4*utfi z0!i0mGK$UWO~cgyH$`K|Vl)AnLo2r+a_39uo4nG>pVwykcexSY(wTbYOYe?WQXhH3 zosbAUA>h?NdLb_kE+5O=_SsRZezTVO7qnIn_4_KzwGdp6mDAY5*sPgmzLQPK?nK;b zvwS}f<>%tsT8t-SQCC&4E+|_orqd0vIk_e#(}`f6xs}@0 zwpVLw-t3vl-(KO47&^eiG8xoNyCpaqd`~9r3bywrcd(T2X$vFxjDA zCQYGrC@|DID6HtZ=J$kq4dnMdA@o!!+inEoLde#jp@Yo1vLD^F8V6Isfsau$o=irf z8jZwgjBuK4mAQrfzM-@|Q)KPSM*F`M)|^h$&l2tyNg5~I2J-Ze{_I&it3Q zQNLf|*gEq_W=)eJI?bEJx zUme%Q3D4Y;;MQxXyHZ~QI-P`c_Xe~TNaz+Ya9UQ8m}g;UHI`rssFi` ziC1vFm)hFBeR5@GoHX);p9T;1*PL+IpgGwy2PaFVn?Bt*|JBOMuLvVQ)6e8W4wlY^ z7MU+NQFdl=F3L8p-%Jy;v{DvlQ&BiZlBUSi2&Wk3)HsrQAaowccqC<=pcA@0FTDu~ z+l8)dMM7ukWZ6R*#7^92Jh+uzOo(IBu*5FPs0L9-^Smt2h=(@bavl9?8|}oj)y=+n;ByD2$gH0?@Z0XD%)w3%DF5jgpE6Y8uGP z8vD~A2u(Xgcm^^D>7z8#%?nXY0UEA63r|SccCgS%u@)=u`8q{Gy z>%I*O={Uxz65CAFl@-loiel@$8F)xL9iH8razY!^CNnUL^-Kw^LGh?bPR=-&3xjYYDp4b>)zHjQAYC&NjwV?rjgV#_^5G$ohgn>H3m;-STe<3SL{WrkT!LrZ*EVUR6K>g4HFFf=0cdOK_o^Y>_2t8q2 z0Irqd)Cv8=tm&0a_Aq4Q=j%3m&Z_L^RAK&;Qq8cRNk!u{x-Fn>m!xyy^(KUBT+ca7 z%Odv&GKa8)cI3Gz3`A*^fk~lB*gkL#_b|XskSpusB=f^7}%y-)pS5`4!i?r$OOIb)M}rc|Yf{U+)^Bd@D2*bDFOSgOMm`siAX= zU7H9!;X|Oaa7d8URai~15)MS}-2kPo2SWFvbsM6wBhC>I;d_=I6~lc;L?fJ7Te&1I zzWv6u8n0c;jXtxm(0@70@|UgjXWc|BZyan!&p-dYG{`OCV?iSHgpUlI`u5J@@uF_) zp8+#}t^xQ92lQ^UuS|XKxYqrnM(4v!J7Z)e706wOfxi^MP;q8z4*S>>5`J3TO6aU8 zT4>-VlcM!wQPmTWIlFk4L|z;eLulj0LLt_#UG(QKzuB&?uWn?{zqu#RUhbFW-zg#g zb=0`?$N%e}Pm(5{a6cdsdcsEzYVAMz;wxD`@cXorKjXaqqIJDra4!2%QRwpGu2qaTCxJuHN`O%f}hIrw{Xw)zv;XcI8UqeE|#|Ucf z7muGV+krWpW$h!alb^R)P?ni(0ZN>1M(GTioZQ=%@^GX^TUsqA+ z(#Nzn_Ia|vTz9Ha>|BH=avFKkJy$n-fkcix3VFp`h?O(xkpD3=Lb_;+Ao_!wg~yx< z2gZEE`4!0<8n@%*2S;lL_)}6CwTbviF#KX^3&BAPce$uqZJjr*DfWnk;XYX|F8RvW z8{^e$Ya1J_ZtBsw+_bOvO7*u|>obkb-mUjtzW&rxFLLf$Lc)CyekTbD_Z>RTeQ9;I zpJkiJw6LF5-v7MyNWh;GL$?Tt9z!|ozea~GH#l`>q4=VBN&Z;9i;@4{Mx z5}}>**#z(P-Lh9+)+WCyv!$g;+uL7_M(b_8akj|pOL?Zx_@el+w#D_M&n!-*{N~mR zO-R@w*!o@)67ExUdw(7fY2id%*uz2^I{ z!ul3u%}9iYDP}&JVdWGw5Pa*rv$h;bRjg%2uPTc^v}?OOtw(2?X|vKeQ%X6=jS**! z_TP6|`R-_N@Y<74elzulC+v9aPKlDRtHGxEb0<&bxfm`u;~$n*9)ni)RO`&=8=pNU zl|NvlSb$btLPTfANTsO>x)zjTQRqn^(bQ}MVu%)Hqb$GiL3E@ugAC-}f?TM%Q}Q$Z z_AQ-4QZ@%~p^GEt!-?I-As}fz5l(@q1A#}94wDhXwj*@BjHKn5}G&CHdcz-3TK^$LT*Q8k&mSDmqF}5z-cd8kuN%z<>2^rq&|yt zy;3NBp=yVXZHnsgU;AHao_NBp!mZLJAz?=$D8nzlpw2(y^JY9<>=o`2=j5lo^PdvF zc)WG_<6e}H3Z<5`YI;V4!sQfnQiG%$ka;&{xFZ{qP&8%}2W&;+5fX}?8Y7I96$vR2 zJJJWsb_@$d70D?!#S=Wf1Jlg!19Q#Iy8|F}l6lD2_^g4zNzC-menT7{J65KcW)>O< zoZEO@jE*wGH7s`tgQG4!VQD#KsZs8q$7n>L$`s1A|p^!3=|ao_zk8+2{t1meTWY6jGe{p zU_xgf^{wcpfOUvTXbdRkBN1m7AP@2AA-515O#LEpOwWe%(5wOCMjk5%(ym?Wrp~$A zwzhH3mcrWwr%ZvGRj*dE9676&Gu6B%MDzdf0$;~*Z^_22uCL3@r@o#BvL!rtxcO^L zNZ4f{xl@Dx(ZYtwdPT2PZpljjDR1GVdO}%oRQUX`Q~5z>vwdKjfzZA%+A9T}FbuR~ z6FpAD5%i(SoquE^{Bx9b_Z6`kzyVn{q#>AwWKVLRC5jPbxtXWi?o7;+d^gKL(FVU$ zF%FLnZ5d#mF)tYw^DhiIL>P~=$l(g=^|Zl%Q>|*{R93R)Y23zEZ;Ywyov_TtZB&tZ(dWYH+h@S(|TUS?O zZ{0fb>55U}td{Z&wEI_)^rH6d<+jN-DyQ0|#dZH4PO*4xpS^VCQgi(H={AvfLc+~R zgr4xg17w5m;2ZL_7hgArj^*X%bUXlq?-9x!gT8$PtZ`TM!7y;GMV-e>>zu4X;uGiF>&i{u(b6t^t1sm`e_c!Qrjq``q^+(kXghj{(-cmf zbm9-b8HOp$E|ZXu@IFX{o{;bpK(c@BwHLC%O5Ha?Eof~QTj35WsUAYyVJY-sA z=Txg#y{w6o)>xngg9X1wD%TjdT7o^1wd)3*_O?D zTVzp=1&{XU;=ntcfvk#u$#rKtgqDWxlMddkIAE(;Jf^{Q*@%FnpiUz#F6ThF;p8%y z)l-OdEmB+De}ik+43o|`xF7gkCB2>C-a*QC15s?YLc zHJqnHodX2!Dq+P~ST)wFnof&;T});DkXWw7$&*N$n#&UsK58UFPe}NP5eQ#=1EhaS zofE>8b*{~zr#2g@vsUT`6#!j$ZM-uL6mw;TreX|=){VAM>S?6~G|T3CYn4>sdY;#{ z;BkW(Z&5*rWa`_ng^q8vl&x?v+`9&~1l6{XGIZ^5 z;#SLE`{c^96DtpS@xo_jM4d=GAt50lAz@p<%0cJ4{Sc9mkdTm&kdTm&kdTm&kdTm& zkdTm&kdTm&kdTm&kdTm&kdTm&kdTm&kdTm&kdTm&@G(J%{}0P`3Udxf1oi*`002ov JPDHLkV1ja*(gFYg literal 0 HcmV?d00001 diff --git a/marl_factory_grid/modules/coins/constants.py b/marl_factory_grid/modules/coins/constants.py new file mode 100644 index 0000000..a1f22a5 --- /dev/null +++ b/marl_factory_grid/modules/coins/constants.py @@ -0,0 +1,11 @@ +COIN = 'CoinPiles' + +COLLECT = 'do_collect_action' + +COLLECT_VALID = 'collect_valid' +COLLECT_FAIL = 'collect_fail' +COLLECT_ALL = 'all_collected' + +REWARD_COLLECT_VALID: float = 0.5 +REWARD_COLLECT_FAIL: float = -0.1 +REWARD_COLLECT_ALL: float = 4.5 diff --git a/marl_factory_grid/modules/coins/entitites.py b/marl_factory_grid/modules/coins/entitites.py new file mode 100644 index 0000000..76d242f --- /dev/null +++ b/marl_factory_grid/modules/coins/entitites.py @@ -0,0 +1,46 @@ +from marl_factory_grid.environment.entity.entity import Entity +from marl_factory_grid.utils.utility_classes import RenderEntity +from marl_factory_grid.modules.coins import constants as d + + +class CoinPile(Entity): + + @property + def amount(self): + """ + Internal Usage + """ + return self._amount + + @property + def encoding(self): + return self._amount + + def __init__(self, *args, amount=2, max_local_amount=5, **kwargs): + """ + Represents a pile of coins at a specific position in the environment that agents can interact with. Agents can + clean the dirt pile or, depending on activated rules, interact with it in different ways. + + :param amount: The amount of coins in the pile. + :type amount: float + + :param max_local_amount: The maximum amount of dirt allowed in a single pile at one position. + :type max_local_amount: float + """ + super(CoinPile, self).__init__(*args, **kwargs) + self._amount = amount + self.max_local_amount = max_local_amount + + def set_new_amount(self, amount): + """ + Internal Usage + """ + self._amount = min(amount, self.max_local_amount) + + def summarize_state(self): + state_dict = super().summarize_state() + state_dict.update(amount=float(self.amount)) + return state_dict + + def render(self): + return RenderEntity(d.COIN, self.pos, min(0.15 + self.amount, 1.5), 'scale') diff --git a/marl_factory_grid/modules/coins/groups.py b/marl_factory_grid/modules/coins/groups.py new file mode 100644 index 0000000..38d2f36 --- /dev/null +++ b/marl_factory_grid/modules/coins/groups.py @@ -0,0 +1,108 @@ +import ast +from marl_factory_grid.environment import constants as c +from marl_factory_grid.environment.groups.collection import Collection +from marl_factory_grid.modules.coins.entitites import CoinPile +from marl_factory_grid.utils.results import Result +from marl_factory_grid.utils import helpers as h + + +class CoinPiles(Collection): + _entity = CoinPile + + @property + def var_is_blocking_light(self): + return False + + @property + def var_can_collide(self): + return False + + @property + def var_can_move(self): + return False + + @property + def var_has_position(self): + return True + + @property + def global_amount(self) -> float: + """ + Internal Usage + """ + return sum([dirt.amount for dirt in self]) + + def __init__(self, *args, max_local_amount=5, collect_amount=1, max_global_amount: int = 20, coords_or_quantity=10, + initial_amount=2, amount_var=0.2, n_var=0.2, **kwargs): + """ + A Collection of dirt piles that triggers their spawn. + + :param max_local_amount: The maximum amount of coins allowed in a single pile at one position. + :type max_local_amount: int + + :param clean_amount: The amount of coins removed by a single collecting action. + :type clean_amount: int + + :param max_global_amount: The maximum total amount of coins allowed in the environment. + :type max_global_amount: int + + :param coords_or_quantity: Determines whether to use coordinates or quantity when triggering coin pile spawn. + :type coords_or_quantity: Union[Tuple[int, int], int] + + :param initial_amount: The initial amount of coin in each newly spawned pile. + :type initial_amount: int + + :param amount_var: The variability in the initial amount of coin in each pile. + :type amount_var: float + + :param n_var: The variability in the number of new coin piles spawned. + :type n_var: float + + """ + super(CoinPiles, self).__init__(*args, **kwargs) + self.amount_var = amount_var + self.n_var = n_var + self.collect_amount = collect_amount + self.max_global_amount = max_global_amount + self.max_local_amount = max_local_amount + self.coords_or_quantity = coords_or_quantity + self.initial_amount = initial_amount + + def trigger_spawn(self, state, coords_or_quantity=0, amount=0, ignore_blocking=False) -> [Result]: + if ignore_blocking: + print("##########################################") + print("Blocking should not be ignored for this Entity") + print("Exiting....") + exit() + coords_or_quantity = coords_or_quantity if coords_or_quantity else self.coords_or_quantity + if isinstance(coords_or_quantity, int): + n_new = int(abs(coords_or_quantity + (state.rng.uniform(-self.n_var, self.n_var)))) + n_new = state.get_n_random_free_positions(n_new) + else: + coords_or_quantity = ast.literal_eval(coords_or_quantity) + if isinstance(coords_or_quantity[0], int): + n_new = [coords_or_quantity] + else: + n_new = [pos for pos in coords_or_quantity] + + amounts = [amount if amount else (self.initial_amount ) # removed rng amount + for _ in range(len(n_new))] + + spawn_counter = 0 + for idx, (pos, a) in enumerate(zip(n_new, amounts)): + if not self.global_amount > self.max_global_amount: + if coin := self.by_pos(pos): + coin = h.get_first(coin) + new_value = coin.amount + a + coin.set_new_amount(new_value) + else: + super().spawn([pos], amount=a) + spawn_counter += 1 + else: + return Result(identifier=f'{self.name}_spawn', validity=c.NOT_VALID, value=spawn_counter) + + return Result(identifier=f'{self.name}_spawn', validity=c.VALID, value=spawn_counter) + + def __repr__(self): + s = super(CoinPiles, self).__repr__() + return f'{s[:-1]}, {self.global_amount}]' diff --git a/marl_factory_grid/modules/coins/rules.py b/marl_factory_grid/modules/coins/rules.py new file mode 100644 index 0000000..7122b28 --- /dev/null +++ b/marl_factory_grid/modules/coins/rules.py @@ -0,0 +1,59 @@ +from marl_factory_grid.modules.coins import constants as d +from marl_factory_grid.environment import constants as c + +from marl_factory_grid.environment.rules import Rule +from marl_factory_grid.utils.helpers import is_move +from marl_factory_grid.utils.results import TickResult +from marl_factory_grid.utils.results import DoneResult + + +class DoneOnAllCoinsCollected(Rule): + + def __init__(self, reward: float = d.REWARD_COLLECT_ALL): + """ + Defines a 'Done'-condition which triggers, when there is no more 'Dirt' in the environment. + + :type reward: float + :parameter reward: Given reward when condition triggers. + """ + super().__init__() + self.reward = reward + + def on_check_done(self, state) -> [DoneResult]: + if len(state[d.COIN]) == 0 and state.curr_step: + return [DoneResult(validity=c.VALID, identifier=self.name, reward=self.reward)] + return [] + + +class RespawnCoins(Rule): + + def __init__(self, respawn_freq: int = 15, respawn_n: int = 5, respawn_amount: float = 1.0): + """ + Defines the spawn pattern of initial and additional 'Dirt'-entities. + First chooses positions, then tries to spawn dirt until 'respawn_n' or the maximal global amount is reached. + If there is already some, it is topped up to min(max_local_amount, amount). + + :type respawn_freq: int + :parameter respawn_freq: In which frequency should this Rule try to spawn new 'Dirt'? + :type respawn_n: int + :parameter respawn_n: How many respawn positions are considered. + :type respawn_amount: float + :parameter respawn_amount: Defines how much dirt 'amount' is placed every 'spawn_freq' ticks. + """ + super().__init__() + self.respawn_n = respawn_n + self.respawn_amount = respawn_amount + self.respawn_freq = respawn_freq + self._next_coin_spawn = respawn_freq + + def tick_step(self, state): + collection = state[d.COIN] + if self._next_coin_spawn < 0: + result = [] # No CoinPile Spawn + elif not self._next_coin_spawn: + result = [collection.trigger_spawn(state, coords_or_quantity=self.respawn_n, amount=self.respawn_amount)] + self._next_coin_spawn = self.respawn_freq + else: + self._next_coin_spawn -= 1 + result = [] + return result diff --git a/marl_factory_grid/utils/plotting/plot_single_runs.py b/marl_factory_grid/utils/plotting/plot_single_runs.py index 5fbb024..a4dd040 100644 --- a/marl_factory_grid/utils/plotting/plot_single_runs.py +++ b/marl_factory_grid/utils/plotting/plot_single_runs.py @@ -7,7 +7,10 @@ from typing import Union import numpy as np import pandas as pd +import torch +from matplotlib import pyplot as plt +from marl_factory_grid.algorithms.rl.utils import _as_torch from marl_factory_grid.utils.helpers import IGNORED_DF_COLUMNS from marl_factory_grid.utils.plotting.plotting_utils import prepare_plot @@ -253,3 +256,125 @@ direction_mapping = { 'south_east': (1, 1), 'south_west': (-1, 1) } + + +def plot_reward_development(reward_development, results_path): + smoothed_data = np.convolve(reward_development, np.ones(10) / 10, mode='valid') + plt.plot(smoothed_data) + plt.ylim([-10, max(smoothed_data) + 20]) + plt.title('Smoothed Reward Development') + plt.xlabel('Episode') + plt.ylabel('Reward') + plt.savefig(f"{results_path}/smoothed_reward_development.png") + plt.show() + + +def plot_collected_coins_per_step(): + # Observed behaviour for multi-agent setting consisting of run0 and run0 + cleaned_dirt_per_step_emergent = [0, 0, 0, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 4, 4, 4, 4, 5] + cleaned_dirt_per_step = [0, 0, 0, 1, 1, 2, 2, 3, 3, 3, 4, 5] # RL and TSP + + plt.step(range(1, len(cleaned_dirt_per_step) + 1), cleaned_dirt_per_step, color='green', linewidth=3, label='Prevented (RL)') + plt.step(range(1, len(cleaned_dirt_per_step_emergent) + 1), cleaned_dirt_per_step_emergent, linestyle='--', color='darkred', linewidth=3, label='Emergent') + plt.step(range(1, len(cleaned_dirt_per_step) + 1), cleaned_dirt_per_step, linestyle='dotted', color='darkorange', linewidth=3, label='Prevented (TSP)') + plt.xlabel("Environment step", fontsize=20) + plt.ylabel("Collected Coins", fontsize=20) + yint = range(min(cleaned_dirt_per_step), max(cleaned_dirt_per_step) + 1) + plt.yticks(yint, fontsize=17) + plt.xticks(range(1, len(cleaned_dirt_per_step_emergent) + 1), fontsize=17) + frame1 = plt.gca() + # Only display every 5th tick label + for idx, xlabel_i in enumerate(frame1.axes.get_xticklabels()): + if (idx + 1) % 5 != 0: + xlabel_i.set_visible(False) + xlabel_i.set_fontsize(0.0) + # Change order of labels in legend + handles, labels = frame1.get_legend_handles_labels() + order = [0, 2, 1] + plt.legend([handles[idx] for idx in order], [labels[idx] for idx in order], prop={'size': 20}) + fig = plt.gcf() + fig.set_size_inches(8, 7) + plt.savefig("../study_out/number_of_collected_coins.pdf") + plt.show() + + +def plot_reached_flags_per_step(): + # Observed behaviour for multi-agent setting consisting of runs 1 + 2 + reached_flags_per_step_emergent = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + reached_flags_per_step_RL = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2] + reached_flags_per_step_TSP = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2] + + plt.step(range(1, len(reached_flags_per_step_RL) + 1), reached_flags_per_step_RL, color='green', linewidth=3, label='Prevented (RL)') + plt.step(range(1, len(reached_flags_per_step_emergent) + 1), reached_flags_per_step_emergent, linestyle='--', color='darkred', linewidth=3, label='Emergent') + plt.step(range(1, len(reached_flags_per_step_TSP) + 1), reached_flags_per_step_TSP, linestyle='dotted', color='darkorange', linewidth=3, label='Prevented (TSP)') + plt.xlabel("Environment step", fontsize=20) + plt.ylabel("Reached Flags", fontsize=20) + yint = range(min(reached_flags_per_step_RL), max(reached_flags_per_step_RL) + 1) + plt.yticks(yint, fontsize=17) + plt.xticks(range(1, len(reached_flags_per_step_emergent) + 1), fontsize=17) + frame1 = plt.gca() + # Only display every 5th tick label + for idx, xlabel_i in enumerate(frame1.axes.get_xticklabels()): + if (idx + 1) % 5 != 0: + xlabel_i.set_visible(False) + xlabel_i.set_fontsize(0.0) + # Change order of labels in legend + handles, labels = frame1.get_legend_handles_labels() + order = [0, 2, 1] + plt.legend([handles[idx] for idx in order], [labels[idx] for idx in order], prop={'size': 20}) + fig = plt.gcf() + fig.set_size_inches(8, 7) + plt.savefig("../study_out/number_of_reached_flags.pdf") + plt.show() + + +def create_info_maps(env, all_valid_observations, dirt_piles_positions, results_path, agents, act_dim, + a2c_instance): + # Create value map + with open(f"{results_path}/info_maps.txt", "w") as txt_file: + for obs_layer, pos in enumerate(dirt_piles_positions): + observations_shape = ( + max(t[0] for t in env.state.entities.floorlist) + 2, + max(t[1] for t in env.state.entities.floorlist) + 2) + value_maps = [np.zeros(observations_shape) for _ in agents] + likeliest_action = [np.full(observations_shape, np.NaN) for _ in agents] + action_probabilities = [np.zeros((observations_shape[0], observations_shape[1], act_dim)) for + _ in agents] + for obs in all_valid_observations[obs_layer]: + for idx, agent in enumerate(agents): + x, y = int(obs[0]), int(obs[1]) + try: + value_maps[idx][x][y] = agent.vf(obs) + probs = agent.pi.distribution(obs).probs + likeliest_action[idx][x][y] = torch.argmax( + probs) # get the likeliest action at the current agent position + action_probabilities[idx][x][y] = probs + except: + pass + + txt_file.write("=======Value Maps=======\n") + for agent_idx, vmap in enumerate(value_maps): + txt_file.write(f"Value map of agent {agent_idx} for target pile {pos}:\n") + vmap = _as_torch(vmap).round(decimals=4) + max_digits = max(len(str(vmap.max().item())), len(str(vmap.min().item()))) + for idx, row in enumerate(vmap): + txt_file.write(' '.join(f" {elem:>{max_digits + 1}}" for elem in row.tolist())) + txt_file.write("\n") + txt_file.write("\n") + txt_file.write("=======Likeliest Action=======\n") + for agent_idx, amap in enumerate(likeliest_action): + txt_file.write(f"Likeliest action map of agent {agent_idx} for target pile {pos}:\n") + txt_file.write(np.array2string(amap)) + txt_file.write("\n") + txt_file.write("=======Action Probabilities=======\n") + for agent_idx, pmap in enumerate(action_probabilities): + a2c_instance.action_probabilities[agent_idx].append(pmap) + txt_file.write(f"Action probability map of agent {agent_idx} for target pile {pos}:\n") + for d in range(pmap.shape[0]): + row = '[' + for r in range(pmap.shape[1]): + row += "[" + ', '.join(f"{x:7.4f}" for x in pmap[d, r]) + "]" + txt_file.write(row + "]") + txt_file.write("\n") + + return action_probabilities diff --git a/marl_factory_grid/utils/renderer.py b/marl_factory_grid/utils/renderer.py index f5b9ea1..3982dc4 100644 --- a/marl_factory_grid/utils/renderer.py +++ b/marl_factory_grid/utils/renderer.py @@ -348,7 +348,6 @@ class Renderer: self.save_counter += 1 full_path = os.path.join(out_dir, unique_filename) pygame.image.save(self.screen, full_path) - print(f"Image saved as {unique_filename}") if __name__ == '__main__': diff --git a/marl_factory_grid/utils/states.py b/marl_factory_grid/utils/states.py index 0c9e965..a0a9030 100644 --- a/marl_factory_grid/utils/states.py +++ b/marl_factory_grid/utils/states.py @@ -118,9 +118,8 @@ class Gamestate(object): self._floortile_graph = None self.tests = StepTests(*tests) - # Pointer that defines current spawn points of agents - for agent in self.agents_conf: - self.agents_conf[agent]["pos_pointer"] = 0 + # Initialize position pointers for agents + self._initialize_position_pointers() def reset(self): self.curr_step = 0 @@ -138,6 +137,11 @@ class Gamestate(object): def __repr__(self): return f'{self.__class__.__name__}({len(self.entities)} Entitites @ Step {self.curr_step})' + def _initialize_position_pointers(self): + """ Initialize the position pointers for each agent in the configuration.""" + for agent in self.agents_conf: + self.agents_conf[agent]["pos_pointer"] = 0 + @property def random_free_position(self) -> (int, int): """ diff --git a/studies/marl_adapted.py b/studies/marl_adapted.py index 0f03a38..d5b0026 100644 --- a/studies/marl_adapted.py +++ b/studies/marl_adapted.py @@ -1,10 +1,11 @@ import copy from pathlib import Path -from marl_factory_grid.algorithms.marl.a2c_dirt import A2C +from marl_factory_grid.algorithms.rl.a2c_coin import A2C from marl_factory_grid.algorithms.utils import load_yaml_file + def single_agent_training(config_name): - cfg_path = Path(f'../marl_factory_grid/algorithms/marl/configs/{config_name}_config.yaml') + cfg_path = Path(f'../marl_factory_grid/algorithms/rl/configs/{config_name}_config.yaml') train_cfg = load_yaml_file(cfg_path) # Use environment config with fixed spawnpoints for eval @@ -21,7 +22,7 @@ def single_agent_training(config_name): def single_agent_eval(config_name, run): - cfg_path = Path(f'../marl_factory_grid/algorithms/marl/configs/{config_name}_config.yaml') + cfg_path = Path(f'../marl_factory_grid/algorithms/rl/configs/{config_name}_config.yaml') train_cfg = load_yaml_file(cfg_path) # Use environment config with fixed spawnpoints for eval @@ -34,7 +35,7 @@ def single_agent_eval(config_name, run): def multi_agent_eval(config_name, runs, emergent_phenomenon=False): - cfg_path = Path(f'../marl_factory_grid/algorithms/marl/configs/MultiAgentConfigs/{config_name}_config.yaml') + cfg_path = Path(f'../marl_factory_grid/algorithms/rl/configs/MultiAgentConfigs/{config_name}_config.yaml') train_cfg = load_yaml_file(cfg_path) # Use environment config with fixed spawnpoints for eval @@ -85,12 +86,14 @@ def two_rooms_one_door_modified_single_agent_eval(agent_name): def dirt_quadrant_5_multi_agent_eval(emergent_phenomenon): multi_agent_eval("dirt_quadrant", ["run4", "run5"], emergent_phenomenon) -def dirt_quadrant_5_multi_agent_ctde_eval(emergent_phenomenon): # run7 == run4 + +def dirt_quadrant_5_multi_agent_ctde_eval(emergent_phenomenon): # run7 == run4 multi_agent_eval("dirt_quadrant", ["run4", "run7"], emergent_phenomenon) + def two_rooms_one_door_modified_multi_agent_eval(emergent_phenomenon): multi_agent_eval("two_rooms_one_door_modified", ["run2", "run3"], emergent_phenomenon) if __name__ == '__main__': - dirt_quadrant_5_multi_agent_ctde_eval(True) \ No newline at end of file + dirt_quadrant_5_multi_agent_ctde_eval(True) diff --git a/studies/normalization_study.py b/studies/normalization_study.py index dd48c60..7d2c9da 100644 --- a/studies/normalization_study.py +++ b/studies/normalization_study.py @@ -2,7 +2,7 @@ from marl_factory_grid.algorithms.utils import Checkpointer from pathlib import Path from marl_factory_grid.algorithms.utils import load_yaml_file, add_env_props, instantiate_class, load_class -# from algorithms.marl import LoopSNAC, LoopIAC, LoopSEAC +# from algorithms.rl import LoopSNAC, LoopIAC, LoopSEAC for i in range(0, 5): diff --git a/studies/viz_policy.py b/studies/viz_policy.py index a3efd3e..b128872 100644 --- a/studies/viz_policy.py +++ b/studies/viz_policy.py @@ -5,7 +5,7 @@ from algorithms.utils import load_yaml_file from tqdm import trange study = 'example_config#0' #study_root = Path(__file__).parent / study -study_root = Path('/Users/romue/PycharmProjects/EDYS/algorithms/marl/') +study_root = Path('/Users/romue/PycharmProjects/EDYS/algorithms/rl/') #['L2NoAh_gru', 'L2NoCh_gru', 'nomix_gru']: render = True diff --git a/test_run.py b/test_run.py index ee2b25b..a7ef76f 100644 --- a/test_run.py +++ b/test_run.py @@ -3,6 +3,7 @@ from pprint import pprint from tqdm import trange +from marl_factory_grid.algorithms.static.TSP_coin_agent import TSPCoinAgent from marl_factory_grid.algorithms.static.TSP_dirt_agent import TSPDirtAgent from marl_factory_grid.algorithms.static.TSP_item_agent import TSPItemAgent from marl_factory_grid.algorithms.static.TSP_target_agent import TSPTargetAgent @@ -30,7 +31,7 @@ if __name__ == '__main__': factory.render() action_spaces = factory.action_space # agents = [TSPDirtAgent(factory, 0), TSPItemAgent(factory, 1), TSPTargetAgent(factory, 2)] - agents = [TSPTargetAgent(factory, 0), TSPTargetAgent(factory, 1)] + agents = [TSPCoinAgent(factory, 0)] while not done: a = [x.predict() for x in agents] obs_type, _, _, done, info = factory.step(a) @@ -39,5 +40,3 @@ if __name__ == '__main__': if done: print(f'Episode {episode} done...') break - - plot_routes(factory, agents)