From bb3b98b1bd2a25eb056747809f2f58f4163e200e Mon Sep 17 00:00:00 2001 From: Weihang Zheng Date: Sun, 21 Jan 2024 00:18:55 -0500 Subject: [PATCH 1/3] initial commit for csv writing, multiple rounds, and test case selection --- src/nepiada/baseline.py | 13 ++- src/nepiada/tester/test.py | 79 +++++++++++++---- src/nepiada/tester/test_cases/default.py | 104 +++++++++++++++++++++++ 3 files changed, 178 insertions(+), 18 deletions(-) create mode 100644 src/nepiada/tester/test_cases/default.py diff --git a/src/nepiada/baseline.py b/src/nepiada/baseline.py index 2e6d242..f566974 100644 --- a/src/nepiada/baseline.py +++ b/src/nepiada/baseline.py @@ -1,7 +1,6 @@ # Local imports import numpy as np import env.nepiada as nepiada -from utils.config import BaselineConfig import pygame @@ -193,10 +192,20 @@ def step(agent_name, agent_instance, observations, infos, env, config): return min_action -def main(included_data=None): +import json + + +def main(included_data=None, config_file=None): if included_data is None: included_data = ["observations", "rewards", "terminations", "truncations", "infos"] + if config_file: + # Dynamically import BaselineConfig from the given module path + from importlib import import_module + BaselineConfig = import_module(config_file).BaselineConfig + else: + # Import BaselineConfig from the default utils.config + from utils.config import BaselineConfig env_config = BaselineConfig() env = nepiada.parallel_env(config=env_config) observations, infos = env.reset() diff --git a/src/nepiada/tester/test.py b/src/nepiada/tester/test.py index 69edad3..b6e624f 100644 --- a/src/nepiada/tester/test.py +++ b/src/nepiada/tester/test.py @@ -1,36 +1,49 @@ import numpy as np import sys -sys.path.append("..") # Adds higher directory to python modules path. +import csv +import os -# Import the main function from the simulation script +sys.path.append("..") # Adds higher directory to python modules path. from baseline import main as baseline_run import env.nepiada as nepiada class SimulationTester: - def __init__(self, included_data=None): + def __init__(self, included_data=None, num_runs=1, config_file=None): """ - Initialize the SimulationTester with specified data components to include. + Initialize the SimulationTester with specified data components to include, number of simulation runs, + and an optional configuration file. :param included_data: List of strings indicating which data components to include in results. + :param num_runs: Number of times to run the simulation. + :param config_file: Optional configuration file for the simulation. """ + self.num_runs = num_runs + self.config_file = config_file if included_data is None: self.included_data = ["observations", "rewards", "terminations", "truncations", "infos"] else: self.included_data = included_data - def run_simulation(self): + def run_simulation(self, config_file=None): """ Run the simulation and store the results. + :param config_file: Optional configuration file for the simulation. """ - self.results, self.agents, self.config, self.env = baseline_run(included_data=self.included_data) + if config_file is None: + config_file = self.config_file + + if config_file: + # If a config file is provided, pass it to baseline_run + self.results, self.agents, self.config, self.env = baseline_run(included_data=self.included_data, config_file=config_file) + else: + # If no config file is provided, call baseline_run without the config_file parameter + self.results, self.agents, self.config, self.env = baseline_run(included_data=self.included_data) + def calculate_convergence_score(self): """ - This function calculates the convergence score based on the average reward acrooss all agent + This function calculates the convergence score based on the average reward across all agents in the last iteration of the algorithm. - The reward of the agents in turn are calculated using two metrics: global arrangement - and local arrangement costs, which are described in Pavel and Dian's paper. - IMPORTANT: An ideal / globally optimal NE will have a score of zero. Lower the score the closer it is to globally optimal NE. """ @@ -52,21 +65,55 @@ def calculate_convergence_score(self): return convergence_score + def run_multiple_simulations(self): + """ + Run the simulation multiple times and calculate the average convergence score. + """ + scores = [] + for _ in range(self.num_runs): + self.run_simulation(self.config_file) + score = self.calculate_convergence_score() + if score != -1: + scores.append(score) + + return np.mean(scores) if scores else -1 + def print_results(self): """ - Print the stored simulation results. + Print the stored simulation results and write them to a CSV file in a specified directory. """ if not hasattr(self, 'results'): print("No results to display. Run the simulation first.") return - for step_result in self.results: - print(step_result) + # Ensure the simulation directory exists + if not os.path.exists(self.config.simulation_dir): + os.makedirs(self.config.simulation_dir) + + csv_filename = os.path.join(self.config.simulation_dir, "simulation_results.csv") + + with open(csv_filename, mode='w', newline='') as file: + writer = csv.writer(file) + + # Writing header + headers = ['Step'] + list(self.results[0].keys()) + writer.writerow(headers) + + # Writing data and printing to console + for step, step_result in enumerate(self.results): + print(step_result) # Printing to console + row = [step] + list(step_result.values()) + writer.writerow(row) + + convergence_score = self.calculate_convergence_score() + print("\nCalculating score, ideal NE is (0): ", convergence_score) + writer.writerow(["Convergence Score", convergence_score]) - print("\nCalculating score, ideal NE is (0): ", self.calculate_convergence_score()) + print(f"Results written to {csv_filename}") # Example usage if __name__ == "__main__": - tester = SimulationTester() - tester.run_simulation() + tester = SimulationTester(num_runs=1, config_file="tester.test_cases.default") + average_score = tester.run_multiple_simulations() + print("Average Convergence Score over", tester.num_runs, "runs:", average_score) tester.print_results() diff --git a/src/nepiada/tester/test_cases/default.py b/src/nepiada/tester/test_cases/default.py new file mode 100644 index 0000000..d666aac --- /dev/null +++ b/src/nepiada/tester/test_cases/default.py @@ -0,0 +1,104 @@ +# TODO: Make an enum +# Default: 0 - stay, 1 - up, 2 - down, 3 - left, 4 - right + +from utils.noise import GaussianNoise +# Importing WIDTH and HEIGHT from anim_consts which is two levels above +from utils.anim_consts import WIDTH, HEIGHT + + + +class Config: + """ + We allow our environment to be customizable based on the user's requirements. + The a brief description of all parameters is given below + + dim: Dimension of the environment + size: The size of each dimension of the environment + iterations: Max number of iterations allowed + + agent_grid_width: Width of the final agent formation, note the goal is to have a rectangular formation + agent_grid_height: Height of the final agent formation, note the goal is to have a rectangular formation + num_good_agents: Number of truthful agents + num_adversarial_agents: Number of rogue or un-truthful agents + + dynamic_obs: Set to true if you wish to update observation graph based on agent's current position + obs_radius: The only supported form of dynamic observation is including proximal agents that fall within the observation radius + + dynamic_comms: If set to true, the communication graph is updated based on the agent's current position and the dynamic comms radius. If set to false, the communication graph is static and all agents can communicate with all other agents. + dynamic_comms_radius: If dynamic_comms is set to true, the communication graph is updated based on the radius set here + dynamic_comms_enforce_minimum: If dynamic_comms is set to true, the communication graph ensures that each agent has at least this many neighbours + + possible_moves: The valid actions for each agent + """ + + # Initialization parameters + dim: int = 2 + size: int = 50 + iterations: int = 1 + simulation_dir: str = "plots" + + # Agent related parameterss + agent_grid_width: int = 3 + agent_grid_height: int = 3 + num_good_agents: int = 7 + num_adversarial_agents: int = 2 + + # Graph update parameters + dynamic_obs: bool = True + obs_radius: int = 10 + dynamic_comms: bool = True + dynamic_comms_radius: int = 15 + dynamic_comms_enforce_minimum: int = 1 + noise = GaussianNoise() + + # Agent update parameters + # Possible moves for each drone. Key is the action, value is the (dx, dy) tuple + possible_moves: {int: int} = { + 0: (0, 0), + 1: (0, 1), + 2: (0, -1), + 3: (-1, 0), + 4: (1, 0), + } + empty_cell: int = -1 + global_reward_weight: int = 1 + local_reward_weight: int = 1 + # screen_height: int = 400 + # screen_width: int = 400 + + def __init__(self): + self._process_screen_size() + + def _process_screen_size(self): + height = getattr(self, "screen_height", None) + width = getattr(self, "screen_width", None) + + if width and height: + return + + if not height: + height = HEIGHT * (max(self.size // 30, 0) + 1) + self.screen_height = height + + if not width: + width = WIDTH * (max(self.size // 30, 0) + 1) + self.screen_width = width + + return + + +# Baseline specific configuration parameters +class BaselineConfig(Config): + D: int = 1 + + def __init__(self): + super().__init__() + + +# Baseline specific configuration parameters +class EpsilonBaselineConfig(Config): + D: int = 1 + epsilon: int = 0.2 + + def __init__(self): + super().__init__() From 29cebd53623882d6c4d5c9b8d02fd307eaedee00 Mon Sep 17 00:00:00 2001 From: Weihang Zheng Date: Sun, 28 Jan 2024 12:10:57 -0500 Subject: [PATCH 2/3] addressed comments by moving test cases into individual json files --- src/nepiada/baseline.py | 11 +-- src/nepiada/tester/test.py | 2 +- src/nepiada/tester/test_cases/default.json | 3 + src/nepiada/tester/test_cases/default.py | 104 --------------------- src/nepiada/utils/config.py | 24 +++-- 5 files changed, 25 insertions(+), 119 deletions(-) create mode 100644 src/nepiada/tester/test_cases/default.json delete mode 100644 src/nepiada/tester/test_cases/default.py diff --git a/src/nepiada/baseline.py b/src/nepiada/baseline.py index f566974..0b45bfa 100644 --- a/src/nepiada/baseline.py +++ b/src/nepiada/baseline.py @@ -193,20 +193,17 @@ def step(agent_name, agent_instance, observations, infos, env, config): return min_action import json - +from utils.config import BaselineConfig def main(included_data=None, config_file=None): if included_data is None: included_data = ["observations", "rewards", "terminations", "truncations", "infos"] if config_file: - # Dynamically import BaselineConfig from the given module path - from importlib import import_module - BaselineConfig = import_module(config_file).BaselineConfig + env_config = BaselineConfig(json_path=config_file) else: - # Import BaselineConfig from the default utils.config - from utils.config import BaselineConfig - env_config = BaselineConfig() + env_config = BaselineConfig() + env = nepiada.parallel_env(config=env_config) observations, infos = env.reset() diff --git a/src/nepiada/tester/test.py b/src/nepiada/tester/test.py index b6e624f..e43f618 100644 --- a/src/nepiada/tester/test.py +++ b/src/nepiada/tester/test.py @@ -113,7 +113,7 @@ def print_results(self): # Example usage if __name__ == "__main__": - tester = SimulationTester(num_runs=1, config_file="tester.test_cases.default") + tester = SimulationTester(num_runs=1, config_file="test_cases/default.json") average_score = tester.run_multiple_simulations() print("Average Convergence Score over", tester.num_runs, "runs:", average_score) tester.print_results() diff --git a/src/nepiada/tester/test_cases/default.json b/src/nepiada/tester/test_cases/default.json new file mode 100644 index 0000000..dc0bbb0 --- /dev/null +++ b/src/nepiada/tester/test_cases/default.json @@ -0,0 +1,3 @@ +{ + "iterations": 5 +} diff --git a/src/nepiada/tester/test_cases/default.py b/src/nepiada/tester/test_cases/default.py deleted file mode 100644 index d666aac..0000000 --- a/src/nepiada/tester/test_cases/default.py +++ /dev/null @@ -1,104 +0,0 @@ -# TODO: Make an enum -# Default: 0 - stay, 1 - up, 2 - down, 3 - left, 4 - right - -from utils.noise import GaussianNoise -# Importing WIDTH and HEIGHT from anim_consts which is two levels above -from utils.anim_consts import WIDTH, HEIGHT - - - -class Config: - """ - We allow our environment to be customizable based on the user's requirements. - The a brief description of all parameters is given below - - dim: Dimension of the environment - size: The size of each dimension of the environment - iterations: Max number of iterations allowed - - agent_grid_width: Width of the final agent formation, note the goal is to have a rectangular formation - agent_grid_height: Height of the final agent formation, note the goal is to have a rectangular formation - num_good_agents: Number of truthful agents - num_adversarial_agents: Number of rogue or un-truthful agents - - dynamic_obs: Set to true if you wish to update observation graph based on agent's current position - obs_radius: The only supported form of dynamic observation is including proximal agents that fall within the observation radius - - dynamic_comms: If set to true, the communication graph is updated based on the agent's current position and the dynamic comms radius. If set to false, the communication graph is static and all agents can communicate with all other agents. - dynamic_comms_radius: If dynamic_comms is set to true, the communication graph is updated based on the radius set here - dynamic_comms_enforce_minimum: If dynamic_comms is set to true, the communication graph ensures that each agent has at least this many neighbours - - possible_moves: The valid actions for each agent - """ - - # Initialization parameters - dim: int = 2 - size: int = 50 - iterations: int = 1 - simulation_dir: str = "plots" - - # Agent related parameterss - agent_grid_width: int = 3 - agent_grid_height: int = 3 - num_good_agents: int = 7 - num_adversarial_agents: int = 2 - - # Graph update parameters - dynamic_obs: bool = True - obs_radius: int = 10 - dynamic_comms: bool = True - dynamic_comms_radius: int = 15 - dynamic_comms_enforce_minimum: int = 1 - noise = GaussianNoise() - - # Agent update parameters - # Possible moves for each drone. Key is the action, value is the (dx, dy) tuple - possible_moves: {int: int} = { - 0: (0, 0), - 1: (0, 1), - 2: (0, -1), - 3: (-1, 0), - 4: (1, 0), - } - empty_cell: int = -1 - global_reward_weight: int = 1 - local_reward_weight: int = 1 - # screen_height: int = 400 - # screen_width: int = 400 - - def __init__(self): - self._process_screen_size() - - def _process_screen_size(self): - height = getattr(self, "screen_height", None) - width = getattr(self, "screen_width", None) - - if width and height: - return - - if not height: - height = HEIGHT * (max(self.size // 30, 0) + 1) - self.screen_height = height - - if not width: - width = WIDTH * (max(self.size // 30, 0) + 1) - self.screen_width = width - - return - - -# Baseline specific configuration parameters -class BaselineConfig(Config): - D: int = 1 - - def __init__(self): - super().__init__() - - -# Baseline specific configuration parameters -class EpsilonBaselineConfig(Config): - D: int = 1 - epsilon: int = 0.2 - - def __init__(self): - super().__init__() diff --git a/src/nepiada/utils/config.py b/src/nepiada/utils/config.py index 94a7940..4beadb2 100644 --- a/src/nepiada/utils/config.py +++ b/src/nepiada/utils/config.py @@ -4,6 +4,8 @@ from utils.noise import GaussianNoise from .anim_consts import WIDTH, HEIGHT +import json + class Config: """ @@ -32,7 +34,7 @@ class Config: # Initialization parameters dim: int = 2 size: int = 50 - iterations: int = 50 + iterations: int = 1 simulation_dir: str = "plots" # Agent related parameterss @@ -64,7 +66,15 @@ class Config: # screen_height: int = 400 # screen_width: int = 400 - def __init__(self): + def __init__(self, json_path=None, **kwargs): + + # If a JSON file path is provided, load and update attributes + if json_path: + with open(json_path, 'r') as file: + params = json.load(file) + for key, value in params.items(): + setattr(self, key, value) + self._process_screen_size() def _process_screen_size(self): @@ -89,14 +99,14 @@ def _process_screen_size(self): class BaselineConfig(Config): D: int = 1 - def __init__(self): - super().__init__() + def __init__(self, json_path=None, **kwargs): + super().__init__(json_path=json_path, **kwargs) # Baseline specific configuration parameters class EpsilonBaselineConfig(Config): D: int = 1 - epsilon: int = 0.2 + epsilon: float = 0.2 - def __init__(self): - super().__init__() + def __init__(self, json_path=None, **kwargs): + super().__init__(json_path=json_path, **kwargs) From 43e16a725d62c4abab3fdd4c510f36de84e39daa Mon Sep 17 00:00:00 2001 From: Weihang Zheng Date: Fri, 2 Feb 2024 02:28:34 -0500 Subject: [PATCH 3/3] added new test cases from previous report --- src/nepiada/tester/test.py | 2 +- src/nepiada/tester/test_cases/everyone_is_your_enemy.json | 5 +++++ src/nepiada/tester/test_cases/large_map.json | 5 +++++ src/nepiada/utils/config.py | 4 ++-- 4 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 src/nepiada/tester/test_cases/everyone_is_your_enemy.json create mode 100644 src/nepiada/tester/test_cases/large_map.json diff --git a/src/nepiada/tester/test.py b/src/nepiada/tester/test.py index e43f618..0ef79be 100644 --- a/src/nepiada/tester/test.py +++ b/src/nepiada/tester/test.py @@ -113,7 +113,7 @@ def print_results(self): # Example usage if __name__ == "__main__": - tester = SimulationTester(num_runs=1, config_file="test_cases/default.json") + tester = SimulationTester(num_runs=1, config_file="test_cases/everyone_is_your_enemy.json") average_score = tester.run_multiple_simulations() print("Average Convergence Score over", tester.num_runs, "runs:", average_score) tester.print_results() diff --git a/src/nepiada/tester/test_cases/everyone_is_your_enemy.json b/src/nepiada/tester/test_cases/everyone_is_your_enemy.json new file mode 100644 index 0000000..ebe9465 --- /dev/null +++ b/src/nepiada/tester/test_cases/everyone_is_your_enemy.json @@ -0,0 +1,5 @@ +{ + "iterations": 5, + "num_good_agents": 3, + "num_adversarial_agents": 6 +} diff --git a/src/nepiada/tester/test_cases/large_map.json b/src/nepiada/tester/test_cases/large_map.json new file mode 100644 index 0000000..a5a684a --- /dev/null +++ b/src/nepiada/tester/test_cases/large_map.json @@ -0,0 +1,5 @@ +{ + "iterations": 5, + "screen_height": 250, + "screen_width": 250 +} diff --git a/src/nepiada/utils/config.py b/src/nepiada/utils/config.py index 4beadb2..65e5cbd 100644 --- a/src/nepiada/utils/config.py +++ b/src/nepiada/utils/config.py @@ -63,8 +63,8 @@ class Config: empty_cell: int = -1 global_reward_weight: int = 1 local_reward_weight: int = 1 - # screen_height: int = 400 - # screen_width: int = 400 + screen_height: int = 400 + screen_width: int = 400 def __init__(self, json_path=None, **kwargs):