From 01da9ac5d2a1f9f77db9cd6b34a1de3d38b6c6db Mon Sep 17 00:00:00 2001 From: Nikkel Mollenhauer <57323886+NikkelM@users.noreply.github.com> Date: Wed, 6 Apr 2022 16:52:57 +0200 Subject: [PATCH] Refactored agents-key in configuration files (#396) * changed jsons to new format * changed test-data * Adapted to new agent format in json * Adapted to new agent-structure in json * Fixed tests * Adapted to new agents-config format in json * Adapted to new agents-format in json * Adapted to new agents-format, upload still not working * Fixed typo * Fixed merge for agents * Fixed tests * Small refactoring * Updated docstring * Added comment * Added migration * Recommerce :) * Updated comment * Changed interval for updating API indicator Co-authored-by: Judith <39854388+felix-20@users.noreply.github.com> --- recommerce/configuration/config_validation.py | 26 ++- .../configuration/environment_config.py | 221 ++++++------------ .../environment_config_agent_monitoring.json | 10 +- .../environment_config_exampleprinter.json | 7 +- .../environment_config_training.json | 7 +- recommerce/monitoring/exampleprinter.py | 9 +- recommerce/rl/training_scenario.py | 5 +- .../environment_config_agent_monitoring.json | 10 +- .../environment_config_exampleprinter.json | 7 +- .../environment_config_training.json | 7 +- tests/test_environment_config.py | 129 +++++----- webserver/alpha_business_app/buttons.py | 13 +- webserver/alpha_business_app/config_merger.py | 53 +++-- webserver/alpha_business_app/config_parser.py | 43 ++-- ...011_alter_agentconfig_argument_and_more.py | 23 ++ webserver/alpha_business_app/models/config.py | 28 +-- .../alpha_business_app/static/js/custom.js | 4 +- .../tests/constant_tests.py | 19 +- .../tests/test_api_interaction.py | 2 +- .../tests/test_config_model.py | 31 +-- .../tests/test_configuration_parser.py | 21 +- .../test_data/test_environment_config.json | 10 +- .../alpha_business_app/tests/test_prefill.py | 144 ++++++------ webserver/templates/configurator.html | 2 +- webserver/templates/index.html | 2 +- 25 files changed, 396 insertions(+), 437 deletions(-) create mode 100644 webserver/alpha_business_app/migrations/0011_alter_agentconfig_argument_and_more.py diff --git a/recommerce/configuration/config_validation.py b/recommerce/configuration/config_validation.py index 1beb2944..e350f431 100644 --- a/recommerce/configuration/config_validation.py +++ b/recommerce/configuration/config_validation.py @@ -1,3 +1,5 @@ +# This file contains logic used by the webserver to validate configuration files + from recommerce.configuration.environment_config import EnvironmentConfig from recommerce.configuration.hyperparameter_config import HyperparameterConfig @@ -11,11 +13,10 @@ def validate_config(config: dict, config_is_final: bool) -> tuple: config_is_final (bool): Whether or not the config must contain all required keys. Returns: - triple: success: A status (True) and the split hyperparameter_config and environment_config dictionaries as a tuple. + tuple: success: A status (True) and the split hyperparameter_config and environment_config dictionaries as a tuple. failure: A status (False) and the errormessage as a string. """ try: - # validate the config using `recommerce` validation logic # first check if the environment and hyperparameter parts are already split up if 'environment' in config and 'hyperparameter' in config: assert len(config) == 2, 'Your config should not contain keys other than "environment" and "hyperparameter"' @@ -54,11 +55,11 @@ def validate_sub_keys(config_class: HyperparameterConfig or EnvironmentConfig, c AssertionError: If the given config contains a key that is invalid. """ for key, _ in config.items(): - # TODO: Remove this workaround with the agent-rework in the config files + # we need to separately check agents, since it is a list of dictionaries if key == 'agents': - for agent_key in config['agents'].keys(): - assert all(this_key in {'agent_class', 'argument'} for this_key in config['agents'][agent_key]), \ - f'an invalid key for agents was provided: {config["agents"][agent_key].keys()}' + for agent in config['agents']: + assert all(agent_key in {'name', 'agent_class', 'argument'} for agent_key in agent.keys()), \ + f'an invalid key for agents was provided: {agent.keys()}' # the key is key of a dictionary in the config elif top_level_keys[key]: assert isinstance(config[key], dict), f'The value of this key must be of type dict: {key}, but was {type(config[key])}' @@ -128,7 +129,8 @@ def check_config_types(hyperparameter_config: dict, environment_config: dict, mu # check types for environment_config task = environment_config['task'] if must_contain else 'None' - EnvironmentConfig.check_types(environment_config, task, must_contain) + single_agent = task in ['training', 'exampleprinter'] + EnvironmentConfig.check_types(environment_config, task, single_agent, must_contain) if __name__ == '__main__': # pragma: no cover @@ -152,16 +154,18 @@ def check_config_types(hyperparameter_config: dict, environment_config: dict, mu 'storage_cost_per_product': 0.1 }, 'episodes': 5, - 'agents': { - 'CE Rebuy Agent (QLearning)': { + 'agents': [ + { + 'name': 'CE Rebuy Agent (QLearning)', 'agent_class': 'recommerce.rl.q_learning.q_learning_agent.QLearningCERebuyAgent', 'argument': '' }, - 'CE Rebuy Agent (QLearaning)': { + { + 'name': 'CE Rebuy Agent (QLearaning)', 'agent_class': 'recommerce.rl.q_learning.q_learning_agent.QLearningCERebuyAgent', 'argument': '' } - } + ] } hyper, env = split_combined_config(test_config) check_config_types(hyper, env) diff --git a/recommerce/configuration/environment_config.py b/recommerce/configuration/environment_config.py index 0774df19..f7c420a9 100644 --- a/recommerce/configuration/environment_config.py +++ b/recommerce/configuration/environment_config.py @@ -64,7 +64,7 @@ def get_required_fields(cls, dict_key) -> dict: 'episodes': False, 'plot_interval': False, 'marketplace': False, - 'agents': True + 'agents': False } elif dict_key == 'agents': return { @@ -74,10 +74,8 @@ def get_required_fields(cls, dict_key) -> dict: else: raise AssertionError(f'The given level does not exist in an environment-config: {dict_key}') - # This function should always contain ALL keys that are possible, so the webserver-config is independent of the given "task" - # since the user does not need to specify a "task". The subclasses should overwrite this method. @classmethod - def check_types(cls, config: dict, task: str = 'None', must_contain: bool = True) -> None: + def check_types(cls, config: dict, task: str = 'None', single_agent: bool = False, must_contain: bool = True) -> None: """ Check if all given variables have the correct types. If must_contain is True, all keys must exist, else non-existing keys will be skipped. @@ -85,12 +83,12 @@ def check_types(cls, config: dict, task: str = 'None', must_contain: bool = True Args: config (dict): The config to check. task (str): The task for which the variables should be checked. + single_agent (bool): Whether or not only one agent is permitted. must_contain (bool, optional): Whether or not all variables must be present in the config. Defaults to True. Raises: - AssertionError: If an unknown key was passed. + AssertionError: If a key's value has an incorrect type or the marketplace or an agent could not be parsed to a valid class. KeyError: If the dictionary is missing a key but should contain all keys. - ValueError: If one of the passed strings (marketplace, agents) could not be parsed to a valid class. """ if task in {'None', 'agent_monitoring'}: types_dict = { @@ -99,49 +97,46 @@ def check_types(cls, config: dict, task: str = 'None', must_contain: bool = True 'episodes': int, 'plot_interval': int, 'marketplace': str, - 'agents': dict + 'agents': list } - types_dict_agents = { - 'agent_class': str, - # str for modelfiles, list for FixedPrice-Agent price-list - 'argument': (str, list) - } - elif task in {'training', 'exampleprinter'}: types_dict = { 'task': str, 'marketplace': str, - 'agents': dict - } - types_dict_agents = { - 'agent_class': str, - # str for modelfiles, list for FixedPrice-Agent price-list - 'argument': (str, list) + 'agents': list } else: raise AssertionError(f'This task is unknown: {task}') + types_dict_agents = { + 'name': str, + 'agent_class': str, + # str for modelfiles, list for FixedPrice-Agent price-lists + 'argument': (str, list) + } + for key, value in types_dict.items(): try: assert isinstance(config[key], value), f'{key} must be a {value} but was {type(config[key])}' - # make sure the class can be parsed/is valid - if key == 'marketplace': + # make sure the agent-classes can be parsed and that each entry in the dictionary has the correct type + if key == 'agents': + if single_agent: + assert len(config['agents']) == 1, f'Only one agent is permitted for this task, but {len(config["agents"])} were given.' + for agent in config['agents']: + # check types of the entries in the current agent dictionary + for checked_key, checked_value in types_dict_agents.items(): + assert isinstance(agent[checked_key], checked_value), \ + f'{checked_key} must be a {checked_value} but was {type(agent[checked_key])}' + try: + get_class(agent['agent_class']) + except Exception as error: + raise AssertionError(f'This agent could not be parsed to a valid class: "{config["agents"][agent]["agent_class"]}"') from error + # make sure the marketplace class can be parsed/is valid + elif key == 'marketplace': try: get_class(config['marketplace']) - except ValueError as error: + except Exception as error: raise AssertionError(f'The marketplace could not be parsed to a valid class: "{config["marketplace"]}"') from error - # TODO: Refactor this when the agent structure was changed in the json files - if key == 'agents': - for agent in config['agents']: - for agent_key, agent_value in types_dict_agents.items(): - if agent_key == 'agent_class': - try: - get_class(config['agents'][agent]['agent_class']) - except ValueError as error: - raise AssertionError(f'This agent could not be parsed to a valid class: \ -"{config["agents"][agent]["agent_class"]}"') from error - assert isinstance(config['agents'][agent][agent_key], agent_value), \ - f'{agent_key} must be a {agent_value} but was {type(config["agents"][agent][agent_key])}' except KeyError as error: if must_contain: raise KeyError(f'Your config is missing the following required key: {key}') from error @@ -157,102 +152,52 @@ def __str__(self) -> str: """ return f'{self.__class__.__name__}: {self.__dict__}' - def _check_top_level_structure(self, config: dict) -> None: + def _check_config_structure(self, config: dict, single_agent: bool) -> None: """ - Utility function that checks if all required top-level fields exist and have the right types. + Utility function that checks if all required fields exist and have the right types. Args: config (dict): The config to be checked. - """ - assert 'task' in config, f'The config must have a "task" field: {config}' - assert 'marketplace' in config, f'The config must have a "marketplace" field: {config}' - assert 'agents' in config, f'The config must have an "agents" field: {config}' - - self.check_types(config, config['task']) - - def _check_and_adjust_agents_structure(self, agents_config: dict, single_agent: bool) -> tuple: - """ - Utility function that checks if the agents field has the correct structure and shortens it if necessary. - - Args: - agents_config (dict): The dict to be checked. single_agent (bool): Whether or not only one agent should be used. - Note that if single_agent is True and the agent dictionary is too long, it will be shortened globally. - - Returns: - dict: The agents_config, shortened if single_agent is True. - list: The list of agent_dictionaries extracted from agents_config. """ - assert all(isinstance(agents_config[agent], dict) for agent in agents_config), \ - f'All agents in the "agents" field must be dictionaries: {[agents_config[agent] for agent in agents_config]}, \ -{[type(agents_config[agent]) for agent in agents_config]}' - - # Shorten the agent dictionary if only one is necessary - if single_agent and len(agents_config) > 1: - used_agent = list(agents_config.items())[0] - agents_config = {used_agent[0]: used_agent[1]} - print(f'Multiple agents were provided but only the first one will be used:\n{agents_config}\n') - - # Save the agents in agents_config in a list for easier access - agent_dictionaries = [agents_config[agent] for agent in agents_config] - - # CHECK: Agents::agent_class - assert all('agent_class' in agent for agent in agent_dictionaries), f'Each agent must have an "agent_class" field: {agent_dictionaries}' - assert all(isinstance(agent['agent_class'], str) for agent in agent_dictionaries), \ - f'The "agent_class" fields must be strings: {agent_dictionaries} ({[type(agent["agent_class"]) for agent in agent_dictionaries]})' - - # CHECK: Agents::argument - assert all('argument' in agent for agent in agent_dictionaries), f'Each agent must have an "argument" field: {agent_dictionaries}' + assert 'task' in config, f'The config must have a "task" field: {config}' - return agents_config, agent_dictionaries + self.check_types(config, config['task'], single_agent) - def _parse_agent_arguments(self, agent_dictionaries: dict, needs_modelfile: bool) -> tuple: + def _parse_and_set_agents(self, agent_list: list, needs_modelfile: bool) -> None: """ - Utility function that parses the provided agent arguments, making sure they are the correct type for the agent. + Utility function that gets the class of the agents and parses the provided arguments, + making sure they are the correct type for each agent. Args: - agent_dictionaries (dict): The agents for which to parse the arguments. + agent_list (list): The agents in the config for which to parse the arguments. needs_modelfile (bool): Whether or not RL-agents need modelfiles in this config. - - Returns: - list: A list of agent classes. - list: A list of parsed arguments. """ - agent_classes = [get_class(agent['agent_class']) for agent in agent_dictionaries] - - # If a modelfile is needed, the self.agents will be a list of tuples (as required by agent_monitoring), else just a list of classes - arguments_list = [] - for current_agent in range(len(agent_classes)): - current_config_argument = agent_dictionaries[current_agent]['argument'] + for agent in agent_list: + # parse the provided string into the class + agent['agent_class'] = get_class(agent['agent_class']) # This if-else contains the parsing logic for the different types of arguments agents can have, e.g. modelfiles or fixed-price-lists - if needs_modelfile and issubclass(agent_classes[current_agent], (QLearningAgent, ActorCriticAgent)): - assert isinstance(current_config_argument, str), \ - f'The "argument" field of this agent must be a string: {agent_classes[current_agent]} ({type(current_config_argument)})' - assert current_config_argument.endswith('.dat'), \ - f'The "argument" field must be a modelfile and therefore end in ".dat": {current_config_argument}' + if needs_modelfile and issubclass(agent['agent_class'], (QLearningAgent, ActorCriticAgent)): + assert isinstance(agent['argument'], str), \ + f'The "argument" field of this agent ({agent["name"]}) must be a string but was ({type(agent["argument"])})' + assert agent['argument'].endswith('.dat'), \ + f'The "argument" field must contain a modelfile and therefore end in ".dat": {agent["argument"]}' # Check that the modelfile exists. Taken from am_configuration::_get_modelfile_path() - full_path = os.path.abspath(os.path.join(PathManager.data_path, current_config_argument)) + full_path = os.path.abspath(os.path.join(PathManager.data_path, agent['argument'])) assert os.path.exists(full_path), f'the specified modelfile does not exist: {full_path}' - arguments_list.append(current_config_argument) - - elif issubclass(agent_classes[current_agent], FixedPriceAgent): - assert isinstance(current_config_argument, list), \ - f'The "argument" field of this agent must be a list: {agent_classes[current_agent]} ({type(current_config_argument)})' + elif issubclass(agent['agent_class'], FixedPriceAgent): + assert isinstance(agent['argument'], list), \ + f'The "argument" field of this agent ({agent["name"]}) must be a list but was ({type(agent["argument"])})' # Subclasses of FixedPriceAgent solely accept tuples - arguments_list.append(tuple(current_config_argument)) + agent['argument'] = tuple(agent['argument']) - # if this agent doesn't have modelfiles or *fixed_price-lists*, append None - # we need to append *something* since the subsequent call creates a list of tuples using the `arguments_list` - # if we were to only append items for agents with modelfiles or *fixed_price-lists*, the lists would have different lengths and the - # process of matching the correct ones would get a lot more difficult + # check if some argument was provided even though an empty string should have been passed else: - if current_config_argument != '' and current_config_argument is not None: - print(f'Your passed argument {current_config_argument} in the "argument" field will be discarded!') - arguments_list.append(None) + assert agent['argument'] == '', f'For agent "{agent["name"]}" no argument should have been passed, but got "{agent["argument"]}"!' - return agent_classes, arguments_list + self.agent = agent_list def _set_marketplace(self, marketplace_string: str) -> None: """ @@ -265,24 +210,12 @@ def _set_marketplace(self, marketplace_string: str) -> None: assert issubclass(self.marketplace, SimMarket), \ f'The type of the passed marketplace must be a subclass of SimMarket: {self.marketplace}' - def _set_agents(self, agent_classes: list, arguments_list: list) -> None: - """ - Utility function that creates a list of tuples from the agent classes and their arguments - and sets the resulting list as an instance variable. - - Args: - agent_classes (list): A list of the different agent classes. - arguments_list (list): A list of arguments for the different agents. - """ - # Create a list of tuples (agent_class, argument) - self.agent = list(zip(agent_classes, arguments_list)) - def _assert_agent_marketplace_fit(self) -> None: """ Utility function that makes sure the agent(s) and marketplace are of the same type. """ - assert all(issubclass(agent[0], CircularAgent) == issubclass(self.marketplace, CircularEconomy) for agent in self.agent), \ + assert all(issubclass(agent['agent_class'], CircularAgent) == issubclass(self.marketplace, CircularEconomy) for agent in self.agent), \ f'The agents and marketplace must be of the same economy type (Linear/Circular): {self.agent} and {self.marketplace}' def _validate_config(self, config: dict, single_agent: bool, needs_modelfile: bool) -> None: @@ -292,23 +225,18 @@ def _validate_config(self, config: dict, single_agent: bool, needs_modelfile: bo Args: config (dict): The config dictionary to be validated. single_agent (bool): Whether or not only one agent should be used. - Note that if single_agent is True and the agent dictionary is too long, it will be shortened globally. needs_modelfile (bool): Whether or not the config must include modelfiles. Raises: AssertionError: In case the provided configuration is invalid. """ - self._check_top_level_structure(config) - - config['agents'], agent_dictionaries = self._check_and_adjust_agents_structure(config['agents'], single_agent) - - agent_classes, arguments_list = self._parse_agent_arguments(agent_dictionaries, needs_modelfile) - - self._set_agents(agent_classes, arguments_list) + self._check_config_structure(config, single_agent) self._set_marketplace(config['marketplace']) + self._parse_and_set_agents(config['agents'], needs_modelfile) + self._assert_agent_marketplace_fit() @abstractmethod @@ -334,10 +262,11 @@ class TrainingEnvironmentConfig(EnvironmentConfig): def _validate_config(self, config: dict) -> None: super(TrainingEnvironmentConfig, self)._validate_config(config, single_agent=True, needs_modelfile=False) - # Since we only have one agent without any arguments, we extract it from the provided list - self.agent = self.agent[0][0] - assert issubclass(self.agent, (QLearningAgent, ActorCriticAgent)), \ - f'The first component must be a subclass of either QLearningAgent or ActorCriticAgent: {self.agent}' + # Since we only have one agent we extract it from the provided list + # TODO: In #370 we can have more than one agent, since the rest are competitors + self.agent = self.agent[0] + assert issubclass(self.agent['agent_class'], (QLearningAgent, ActorCriticAgent)), \ + f'The agent must be a subclass of either QLearningAgent or ActorCriticAgent: {self.agent}' def _get_task(self) -> str: return 'training' @@ -357,19 +286,6 @@ class AgentMonitoringEnvironmentConfig(EnvironmentConfig): Each entry in the list is a tuple with the first item being the agent class, the second being a list. If the agent needs a modelfile, this will be the first entry in the list, the other entry is always an informal name for the agent. """ - def _check_top_level_structure(self, config: dict) -> None: - super(AgentMonitoringEnvironmentConfig, self)._check_top_level_structure(config) - assert 'enable_live_draw' in config, f'The config must have an "enable_live_draw" field: {config}' - assert 'episodes' in config, f'The config must have an "episodes" field: {config}' - assert 'plot_interval' in config, f'The config must have a "plot_interval" field: {config}' - - assert isinstance(config['enable_live_draw'], bool), \ - f'The "enable_live_draw" field must be a bool: {config["enable_live_draw"]} ({type(config["enable_live_draw"])})' - assert isinstance(config['episodes'], int), \ - f'The "episodes" field must be a int: {config["episodes"]} ({type(config["episodes"])})' - assert isinstance(config['plot_interval'], int), \ - f'The "plot_interval" field must be a int: {config["plot_interval"]} ({type(config["plot_interval"])})' - def _validate_config(self, config: dict) -> None: # TODO: subfolder_name variable @@ -379,15 +295,16 @@ def _validate_config(self, config: dict) -> None: self.episodes = config['episodes'] self.plot_interval = config['plot_interval'] - # Since RuleBasedAgents do not have modelfiles, we need to adjust the passed lists to remove the "None" entry + # Since the agent_monitoring does not accept the dictionary but instead wants a list of tuples, we need to adapt the dictionary passed_agents = self.agent self.agent = [] - for current_agent in range(len(passed_agents)): - # No modelfile - if passed_agents[current_agent][1] is None: - self.agent.append((passed_agents[current_agent][0], [list(config['agents'].keys())[current_agent]])) + for current_agent in passed_agents: + # with modelfile + if issubclass(current_agent['agent_class'], (QLearningAgent, ActorCriticAgent)): + self.agent.append((current_agent['agent_class'], [current_agent['argument'], current_agent['name']])) + # without modelfile else: - self.agent.append((passed_agents[current_agent][0], [passed_agents[current_agent][1], list(config['agents'].keys())[current_agent]])) + self.agent.append((current_agent['agent_class'], [current_agent['name']])) def _get_task(self) -> str: return 'agent_monitoring' diff --git a/recommerce/default_data/configuration_files/environment_config_agent_monitoring.json b/recommerce/default_data/configuration_files/environment_config_agent_monitoring.json index 31614a96..e8610285 100644 --- a/recommerce/default_data/configuration_files/environment_config_agent_monitoring.json +++ b/recommerce/default_data/configuration_files/environment_config_agent_monitoring.json @@ -4,14 +4,16 @@ "episodes": 500, "plot_interval": 100, "marketplace": "recommerce.market.circular.circular_sim_market.CircularEconomyRebuyPriceMonopolyScenario", - "agents": { - "Rule_Based Agent": { + "agents": [ + { + "name": "Rule_Based Agent", "agent_class": "recommerce.market.circular.circular_vendors.RuleBasedCERebuyAgent", "argument": "" }, - "CE Rebuy Agent (QLearning)": { + { + "name": "CE Rebuy Agent (QLearning)", "agent_class": "recommerce.rl.q_learning.q_learning_agent.QLearningCERebuyAgent", "argument": "CircularEconomyRebuyPriceMonopolyScenario_QLearningCERebuyAgent.dat" } - } + ] } diff --git a/recommerce/default_data/configuration_files/environment_config_exampleprinter.json b/recommerce/default_data/configuration_files/environment_config_exampleprinter.json index 4316e738..5737563e 100644 --- a/recommerce/default_data/configuration_files/environment_config_exampleprinter.json +++ b/recommerce/default_data/configuration_files/environment_config_exampleprinter.json @@ -1,10 +1,11 @@ { "task": "exampleprinter", "marketplace": "recommerce.market.circular.circular_sim_market.CircularEconomyRebuyPriceOneCompetitor", - "agents": { - "CE Rebuy Agent (QLearning)": { + "agents": [ + { + "name": "CE Rebuy Agent (QLearning)", "agent_class": "recommerce.rl.q_learning.q_learning_agent.QLearningCERebuyAgent", "argument": "CircularEconomyRebuyPriceOneCompetitor_QLearningCERebuyAgent.dat" } - } + ] } diff --git a/recommerce/default_data/configuration_files/environment_config_training.json b/recommerce/default_data/configuration_files/environment_config_training.json index 21632323..62334a98 100644 --- a/recommerce/default_data/configuration_files/environment_config_training.json +++ b/recommerce/default_data/configuration_files/environment_config_training.json @@ -1,10 +1,11 @@ { "task": "training", "marketplace": "recommerce.market.circular.circular_sim_market.CircularEconomyRebuyPriceMonopolyScenario", - "agents": { - "CE Rebuy Agent (QLearning)": { + "agents": [ + { + "name": "CE Rebuy Agent (QLearning)", "agent_class": "recommerce.rl.q_learning.q_learning_agent.QLearningCERebuyAgent", "argument": "" } - } + ] } diff --git a/recommerce/monitoring/exampleprinter.py b/recommerce/monitoring/exampleprinter.py index f2c2d832..1b82515e 100644 --- a/recommerce/monitoring/exampleprinter.py +++ b/recommerce/monitoring/exampleprinter.py @@ -101,17 +101,18 @@ def main(): # pragma: no cover printer = ExamplePrinter() config: ExampleprinterEnvironmentConfig = EnvironmentConfigLoader.load('environment_config_exampleprinter') + # TODO: Theoretically, the name of the agent is saved in config['name'], but we don't use it yet. marketplace = config.marketplace() # QLearningAgents need more initialization - if issubclass(config.agent[0], QLearningAgent): + if issubclass(config.agent['agent_class'], QLearningAgent): printer.setup_exampleprinter(marketplace=marketplace, - agent=config.agent[0]( + agent=config.agent['agent_class']( n_observations=marketplace.observation_space.shape[0], n_actions=marketplace.get_n_actions(), - load_path=os.path.abspath(os.path.join(PathManager.data_path, config.agent[1])))) + load_path=os.path.abspath(os.path.join(PathManager.data_path, config.agent['argument'])))) else: - printer.setup_exampleprinter(marketplace=marketplace, agent=config.agent[0]()) + printer.setup_exampleprinter(marketplace=marketplace, agent=config.agent['agent_class']()) print(f'The final profit was: {printer.run_example()}') diff --git a/recommerce/rl/training_scenario.py b/recommerce/rl/training_scenario.py index 1a0288c3..4c819e13 100644 --- a/recommerce/rl/training_scenario.py +++ b/recommerce/rl/training_scenario.py @@ -57,9 +57,8 @@ def train_from_config(): Use the `environment_config_training.json` file to decide on the training parameters. """ config: TrainingEnvironmentConfig = EnvironmentConfigLoader.load('environment_config_training') - # Since we store a tuple (class, None) in config.agent, we just want the first component - # Since we store an one-element list [class] in config.marketplace, we just want the stored element - run_training_session(config.marketplace, config.agent) + # TODO: Theoretically, the name of the agent is saved in config['name'], but we don't use it yet. + run_training_session(config.marketplace, config.agent['agent_class']) def main(): diff --git a/tests/test_data/environment_config_agent_monitoring.json b/tests/test_data/environment_config_agent_monitoring.json index 1c030d66..b96f6bf5 100644 --- a/tests/test_data/environment_config_agent_monitoring.json +++ b/tests/test_data/environment_config_agent_monitoring.json @@ -4,14 +4,16 @@ "episodes": 50, "plot_interval": 25, "marketplace": "recommerce.market.circular.circular_sim_market.CircularEconomyRebuyPriceMonopolyScenario", - "agents": { - "Rule_Based Agent": { + "agents": [ + { + "name": "Rule_Based Agent", "agent_class": "recommerce.market.circular.circular_vendors.RuleBasedCERebuyAgent", "argument": "" }, - "CE Rebuy Agent (QLearning)": { + { + "name": "CE Rebuy Agent (QLearning)", "agent_class": "recommerce.rl.q_learning.q_learning_agent.QLearningCERebuyAgent", "argument": "CircularEconomyRebuyPriceMonopolyScenario_QLearningCERebuyAgent.dat" } - } + ] } diff --git a/tests/test_data/environment_config_exampleprinter.json b/tests/test_data/environment_config_exampleprinter.json index 4316e738..5737563e 100644 --- a/tests/test_data/environment_config_exampleprinter.json +++ b/tests/test_data/environment_config_exampleprinter.json @@ -1,10 +1,11 @@ { "task": "exampleprinter", "marketplace": "recommerce.market.circular.circular_sim_market.CircularEconomyRebuyPriceOneCompetitor", - "agents": { - "CE Rebuy Agent (QLearning)": { + "agents": [ + { + "name": "CE Rebuy Agent (QLearning)", "agent_class": "recommerce.rl.q_learning.q_learning_agent.QLearningCERebuyAgent", "argument": "CircularEconomyRebuyPriceOneCompetitor_QLearningCERebuyAgent.dat" } - } + ] } diff --git a/tests/test_data/environment_config_training.json b/tests/test_data/environment_config_training.json index 21632323..62334a98 100644 --- a/tests/test_data/environment_config_training.json +++ b/tests/test_data/environment_config_training.json @@ -1,10 +1,11 @@ { "task": "training", "marketplace": "recommerce.market.circular.circular_sim_market.CircularEconomyRebuyPriceMonopolyScenario", - "agents": { - "CE Rebuy Agent (QLearning)": { + "agents": [ + { + "name": "CE Rebuy Agent (QLearning)", "agent_class": "recommerce.rl.q_learning.q_learning_agent.QLearningCERebuyAgent", "argument": "" } - } + ] } diff --git a/tests/test_environment_config.py b/tests/test_environment_config.py index 8719b2ad..7df1c71a 100644 --- a/tests/test_environment_config.py +++ b/tests/test_environment_config.py @@ -9,68 +9,6 @@ from recommerce.market.circular.circular_vendors import RuleBasedCERebuyAgent from recommerce.rl.q_learning.q_learning_agent import QLearningCERebuyAgent -valid_training_dict = { - 'task': 'training', - 'marketplace': 'recommerce.market.circular.circular_sim_market.CircularEconomyRebuyPriceMonopolyScenario', - 'agents': { - 'CE Rebuy Agent (QLearning)': { - 'agent_class': 'recommerce.rl.q_learning.q_learning_agent.QLearningCERebuyAgent', - 'argument': '' - } - } -} - -valid_agent_monitoring_dict = { - 'task': 'agent_monitoring', - 'enable_live_draw': False, - 'episodes': 10, - 'plot_interval': 5, - 'marketplace': 'recommerce.market.circular.circular_sim_market.CircularEconomyRebuyPriceMonopolyScenario', - 'agents': { - 'Rule_Based Agent': { - 'agent_class': 'recommerce.market.circular.circular_vendors.RuleBasedCERebuyAgent', - 'argument': '' - }, - 'CE Rebuy Agent (QLearning)': { - 'agent_class': 'recommerce.rl.q_learning.q_learning_agent.QLearningCERebuyAgent', - 'argument': 'CircularEconomyRebuyPriceMonopolyScenario_QLearningCERebuyAgent.dat' - } - } -} - -valid_exampleprinter_dict = { - 'task': 'exampleprinter', - 'marketplace': 'recommerce.market.circular.circular_sim_market.CircularEconomyRebuyPriceMonopolyScenario', - 'agents': { - 'CE Rebuy Agent (QLearning)': { - 'agent_class': 'recommerce.rl.q_learning.q_learning_agent.QLearningCERebuyAgent', - 'argument': 'CircularEconomyRebuyPriceMonopolyScenario_QLearningCERebuyAgent.dat' - } - } -} - -invalid_agent_dict = { - 'task': 'exampleprinter', - 'marketplace': 'recommerce.market.circular.circular_sim_market.CircularEconomyRebuyPriceMonopolyScenario', - 'agents': { - 'Agent_name': { - 'agent_class': 'recommerce.rl.q_learning.q_learning_agent.QLearningCERebuyAgent', - 'argument': '' - } - } -} - -invalid_task_dict = { - 'task': 'not_existing_test_task', - 'marketplace': 'recommerce.market.circular.circular_sim_market.CircularEconomyRebuyPriceMonopolyScenario', - 'agents': { - 'Agent_name': { - 'agent_class': 'recommerce.rl.q_learning.q_learning_agent.QLearningCERebuyAgent', - 'argument': 'CircularEconomyRebuyPriceMonopolyScenario_QLearningCERebuyAgent.dat' - } - } -} - def test_abstract_parent_class(): with pytest.raises(TypeError) as error_message: @@ -79,10 +17,22 @@ def test_abstract_parent_class(): def test_str_representation(): - config = env_config.TrainingEnvironmentConfig(valid_training_dict) + test_dict = { + 'task': 'training', + 'marketplace': 'recommerce.market.circular.circular_sim_market.CircularEconomyRebuyPriceMonopolyScenario', + 'agents': [ + { + 'name': 'CE Rebuy Agent (QLearning)', + 'agent_class': 'recommerce.rl.q_learning.q_learning_agent.QLearningCERebuyAgent', + 'argument': '' + } + ] + } + config = env_config.TrainingEnvironmentConfig(test_dict) assert str(config) == ("TrainingEnvironmentConfig: {'task': 'training', " - "'agent': , " - "'marketplace': }") + "'marketplace': , " + "'agent': {'name': 'CE Rebuy Agent (QLearning)', " + "'agent_class': , 'argument': ''}}") get_class_testcases = [ @@ -138,9 +88,47 @@ def test_get_task(tested_class: env_config.EnvironmentConfig, expected_task): valid_ConfigLoader_validate_testcases = [ - valid_training_dict, - valid_agent_monitoring_dict, - valid_exampleprinter_dict + { + 'task': 'training', + 'marketplace': 'recommerce.market.circular.circular_sim_market.CircularEconomyRebuyPriceMonopolyScenario', + 'agents': [ + { + 'name': 'CE Rebuy Agent (QLearning)', + 'agent_class': 'recommerce.rl.q_learning.q_learning_agent.QLearningCERebuyAgent', + 'argument': '' + } + ] + }, + { + 'task': 'agent_monitoring', + 'enable_live_draw': False, + 'episodes': 10, + 'plot_interval': 5, + 'marketplace': 'recommerce.market.circular.circular_sim_market.CircularEconomyRebuyPriceMonopolyScenario', + 'agents': [ + { + 'name': 'Rule_Based Agent', + 'agent_class': 'recommerce.market.circular.circular_vendors.RuleBasedCERebuyAgent', + 'argument': '' + }, + { + 'name': 'CE Rebuy Agent (QLearning)', + 'agent_class': 'recommerce.rl.q_learning.q_learning_agent.QLearningCERebuyAgent', + 'argument': 'CircularEconomyRebuyPriceMonopolyScenario_QLearningCERebuyAgent.dat' + } + ] + }, + { + 'task': 'exampleprinter', + 'marketplace': 'recommerce.market.circular.circular_sim_market.CircularEconomyRebuyPriceMonopolyScenario', + 'agents': [ + { + 'name': 'CE Rebuy Agent (QLearning)', + 'agent_class': 'recommerce.rl.q_learning.q_learning_agent.QLearningCERebuyAgent', + 'argument': 'CircularEconomyRebuyPriceMonopolyScenario_QLearningCERebuyAgent.dat' + } + ] + } ] @@ -152,9 +140,10 @@ def test_valid_ConfigLoader_validate(config): valid_ConfigLoader_load_training_testcases = [ # TODO: Currently no testcases for ActorCriticAgents ('training', 'recommerce.market.circular.circular_sim_market.CircularEconomyRebuyPriceMonopolyScenario', - {'CE Rebuy Agent (QLearning)': {'agent_class': 'recommerce.rl.q_learning.q_learning_agent.QLearningCERebuyAgent', 'argument': ''}}), + [{'name': 'CE Rebuy Agent (QLearning)', 'agent_class': 'recommerce.rl.q_learning.q_learning_agent.QLearningCERebuyAgent', + 'argument': ''}]), ('training', 'recommerce.market.circular.circular_sim_market.CircularEconomyRebuyPriceOneCompetitor', - {'CE Rebuy Agent (QLearning)': {'agent_class': 'recommerce.rl.q_learning.q_learning_agent.QLearningCEAgent', 'argument': ''}}) + [{'name': 'CE Rebuy Agent (QLearning)', 'agent_class': 'recommerce.rl.q_learning.q_learning_agent.QLearningCEAgent', 'argument': ''}]) ] diff --git a/webserver/alpha_business_app/buttons.py b/webserver/alpha_business_app/buttons.py index 763daccf..792c0629 100644 --- a/webserver/alpha_business_app/buttons.py +++ b/webserver/alpha_business_app/buttons.py @@ -292,13 +292,9 @@ def _start(self) -> HttpResponse: # convert post request to normal dict post_request = dict(self.request.POST.lists()) - try: - config_dict = ConfigFlatDictParser().flat_dict_to_hierarchical_config_dict(post_request) - except AssertionError: - self.message = ['error', 'Could not create config: Please eliminate identical Agent names'] - return self._decide_rendering() + config_dict = ConfigFlatDictParser().flat_dict_to_hierarchical_config_dict(post_request) - validate_status, validate_data = validate_config(config_dict, True) + validate_status, validate_data = validate_config(config=config_dict, config_is_final=True) if not validate_status: self.message = ['error', validate_data] return self._decide_rendering() @@ -311,8 +307,7 @@ def _start(self) -> HttpResponse: if Container.objects.filter(id=response['id']).exists(): # we will kindly ask the user to try it again and stop the container # TODO insert better handling here - print('the new container has the same id, as another container') - self.message = ['error', 'please try again'] + self.message = ['error', 'The new container has the same id as an already existing container, please try again.'] return self._remove() # get all necessary parameters for container object container_name = self.request.POST['experiment_name'] @@ -320,7 +315,7 @@ def _start(self) -> HttpResponse: config_object = ConfigModelParser().parse_config(copy.deepcopy(config_dict)) command = config_object.environment.task Container.objects.create(id=response['id'], config=config_object, name=container_name, command=command) - config_object.name = f'used for {container_name}' + config_object.name = f'Config for {container_name}' config_object.save() return redirect('/observe', {'success': 'You successfully launched an experiment'}) else: diff --git a/webserver/alpha_business_app/config_merger.py b/webserver/alpha_business_app/config_merger.py index 89bef090..e8b3db78 100644 --- a/webserver/alpha_business_app/config_merger.py +++ b/webserver/alpha_business_app/config_merger.py @@ -13,7 +13,7 @@ def merge_config_objects(self, config_object_ids: list) -> tuple: config_object_ids (list): The id's of the config objects that should be merged. Returns: - tuple (dict, dict): the final merged dict and the error dict whith the latest error + tuple (dict, dict): the final merged dict and the error dict with the latest error """ configuration_objects = [Config.objects.get(id=config_id) for config_id in config_object_ids] configuration_dicts = [config.as_dict() for config in configuration_objects] @@ -35,17 +35,23 @@ def _merge_config_into_base_config(self, base_config: dict, merging_config: dict Returns: dict: a final merged config """ - # get contained dicts for recursion - contained_dicts_merge = [(key, value) for key, value in merging_config.items() if type(value) == dict] + # get contained dicts and lists for recursion + contained_dicts_merge = [(key, value) for key, value in merging_config.items() if type(value) in [dict, list]] # get contained values for updating - contained_values_merge = [(key, value) for key, value in merging_config.items() if type(value) != dict and value is not None] + contained_values_merge = [(key, value) for key, value in merging_config.items() if type(value) != dict and type(value) != list + and value is not None] for key, sub_dict in contained_dicts_merge: if key == 'agents': - base_config[key] = self._merge_agents_into_base_agents(base_config[key], sub_dict) + # simply concatenate the agents + # watch out: the order is not necessarily safe, so if we assume an agent to be in the first position + # e.g. for training, the pre-fill might re-order the agents so it doesn't work anymore! + base_config['agents'] += sub_dict + # currently not used since we simply concatenate the agents + # base_config[key] = self._merge_agents_into_base_agents(base_config[key], sub_dict) continue new_config_path = f'{current_config_path}-{key}' if current_config_path else key - base_config[key] = self._merge_config_into_base_config(base_config[key], sub_dict, new_config_path) + base_config[key] = self._merge_config_into_base_config(base_config[key], sub_dict, new_config_path) # update values for key, value in contained_values_merge: @@ -57,21 +63,26 @@ def _merge_config_into_base_config(self, base_config: dict, merging_config: dict return base_config - def _merge_agents_into_base_agents(self, base_agent_config: dict, merge_agent_config: dict) -> dict: - """ - Merges an agents config part into a base agents config part. It will be checked if two of the merged agents have the same name. - - Args: - base_agent_config (dict): the config that will be merged into - merge_agent_config (dict): the config that should be merged - - Returns: - dict: a final merged agents config - """ - for agent_name, _ in merge_agent_config.items(): - if agent_name in base_agent_config: - self._update_error_dict(['environment', 'agents'], f'multiple {agent_name}') - return {**base_agent_config, **merge_agent_config} + # working version of comparing the agent lists for agents with the same names. + # Currently not used since we simply concatenate the agent lists + # def _merge_agents_into_base_agents(self, base_agent_config: list, merge_agent_config: list) -> list: + # """ + # Merges an agents config part into a base agents config part. It will be checked if two of the merged agents have the same name. + + # Args: + # base_agent_config (list): the config that will be merged into + # merge_agent_config (list): the config that should be merged + + # Returns: + # list: a final merged agents config + # """ + # base_names = [agent['name'] for agent in base_agent_config] + # for agent in merge_agent_config: + # if agent['name'] in base_names: + # self._update_error_dict(['environment', 'agents'], f'multiple agents named {agent["name"]}') + # else: + # base_agent_config.append(agent) + # return base_agent_config def _update_error_dict(self, key_words: list, update_message: str) -> None: """ diff --git a/webserver/alpha_business_app/config_parser.py b/webserver/alpha_business_app/config_parser.py index 592a4a38..5c31d30b 100644 --- a/webserver/alpha_business_app/config_parser.py +++ b/webserver/alpha_business_app/config_parser.py @@ -18,8 +18,6 @@ def flat_dict_to_hierarchical_config_dict(self, flat_dict: dict) -> dict: Returns: dict: hierarchical config dict """ - # first check for duplicated agent names - assert len(flat_dict['environment-agents-name']) == len(set(flat_dict['environment-agents-name'])) # prepare flat_dict, convert all numbers to int or float for key, value_list in flat_dict.items(): converted_values = [self._converted_to_int_or_float_if_possible(value) for value in value_list] @@ -53,7 +51,7 @@ def _flat_environment_to_hierarchical(self, flat_dict: dict) -> dict: environment_dict = self._first_list_element_without_empty(environment_dict) # add parsed agents environment_dict['agents'] = self._flat_agents_to_hierarchical(raw_agents_dict) - # add enable_live_draw if exists# + # add enable_live_draw if exists environment_dict['enable_live_draw'] = 'enable_live_draw' in flat_dict return environment_dict @@ -67,15 +65,15 @@ def _flat_agents_to_hierarchical(self, flat_dict: dict) -> dict: Returns: dict: a hierarchical agents dictionary. """ - final_dict = {} + final_list = [] for agent_index in range(len(flat_dict['name'])): - argument = flat_dict['argument'][agent_index] agent_dict = { + 'name': flat_dict['name'][agent_index], 'agent_class': flat_dict['agent_class'][agent_index], - 'argument': argument + 'argument': flat_dict['argument'][agent_index] } - final_dict[flat_dict['name'][agent_index]] = remove_none_values_from_dict(agent_dict) - return final_dict + final_list.append(remove_none_values_from_dict(agent_dict)) + return final_list def _flat_hyperparameter_to_hierarchical(self, flat_dict: dict) -> dict: """ @@ -183,23 +181,19 @@ def parse_config_dict_to_datastructure(self, name: str, config_dict: dict): config_dict (dict): part of the config dict belonging to the keyword name. Returns: - an model instance of the config dict. + A model instance of the config dict. """ - if config_dict == {}: + if not config_dict: return if name == 'agents': - # since django does only support many-to-one relationships (not one-to-many), - # we need to parse the agents slightly different, to be able to reference many agents with the agents keyword + # since django does not support a list-datatype we need to parse the agents slightly different return self._parse_agents_to_datastructure(config_dict) # get all key value pairs, that contain another dict - containing_dict = [(name, value) for name, value in config_dict.items() if type(value) == dict] + containing_dict = [(name, value) for name, value in config_dict.items() if type(value) in [dict, list]] # loop through of these pairs, in order to parse these dictionaries and add # the parsed sub-element to the current element - sub_elements = [] - for keyword, config in containing_dict: - sub_elements += [(keyword, self.parse_config_dict_to_datastructure(keyword, config))] - + sub_elements = [(keyword, self.parse_config_dict_to_datastructure(keyword, config)) for keyword, config in containing_dict] # get all elements that do not contain another dictionary not_containing_dict = dict([(name, value) for name, value in config_dict.items() if type(value) != dict]) @@ -211,21 +205,20 @@ def parse_config_dict_to_datastructure(self, name: str, config_dict: dict): config_class = to_config_class_name(name) return self._create_object_from(config_class, not_containing_dict) - def _parse_agents_to_datastructure(self, agent_dict: dict) -> AgentsConfig: + def _parse_agents_to_datastructure(self, agent_list: list) -> AgentsConfig: """ - Parses the part of the config for the keywort `agents`. + Parses the part of the config for the keyword `agents`. Args: - agent_dict (dict): the dictionary of agents. + agent_list (list): the list of agents. Returns: - AgentsConfig: an instance of AgentsConfig multiple agents are referring to. + AgentsConfig: an instance of AgentsConfig. """ agents = AgentsConfig.objects.create() - for agent_name, agent_parameters in agent_dict.items(): - agent_parameters['agents_config'] = agents - agent_parameters['name'] = agent_name - AgentConfig.objects.create(**agent_parameters) + for agent in agent_list: + agent['agents_config'] = agents + AgentConfig.objects.create(**agent) return agents def _create_object_from(self, class_name: str, parameters: dict): diff --git a/webserver/alpha_business_app/migrations/0011_alter_agentconfig_argument_and_more.py b/webserver/alpha_business_app/migrations/0011_alter_agentconfig_argument_and_more.py new file mode 100644 index 00000000..258d75d8 --- /dev/null +++ b/webserver/alpha_business_app/migrations/0011_alter_agentconfig_argument_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.0.1 on 2022-04-06 08:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('alpha_business_app', '0010_alter_environmentconfig_marketplace'), + ] + + operations = [ + migrations.AlterField( + model_name='agentconfig', + name='argument', + field=models.CharField(default='', max_length=200), + ), + migrations.AlterField( + model_name='environmentconfig', + name='task', + field=models.CharField(choices=[(1, 'training'), (2, 'agent_monitoring'), (3, 'exampleprinter')], max_length=14, null=True), + ), + ] diff --git a/webserver/alpha_business_app/models/config.py b/webserver/alpha_business_app/models/config.py index 78b90fb6..729431de 100644 --- a/webserver/alpha_business_app/models/config.py +++ b/webserver/alpha_business_app/models/config.py @@ -29,17 +29,17 @@ class EnvironmentConfig(models.Model): episodes = models.IntegerField(null=True) plot_interval = models.IntegerField(null=True) marketplace = models.CharField(max_length=150, null=True) - task = models.CharField(max_length=14, choices=((1, 'training'), (2, 'monitoring'), (3, 'exampleprinter')), null=True) + task = models.CharField(max_length=14, choices=((1, 'training'), (2, 'agent_monitoring'), (3, 'exampleprinter')), null=True) def as_dict(self) -> dict: - agents_dict = self.agents.as_dict() if self.agents is not None else None + agents_list = self.agents.as_list() if self.agents is not None else None return remove_none_values_from_dict({ 'enable_live_draw': self.enable_live_draw, 'episodes': self.episodes, 'plot_interval': self.plot_interval, 'marketplace': self.marketplace, 'task': self.task, - 'agents': agents_dict + 'agents': agents_list }) @staticmethod @@ -50,36 +50,32 @@ def get_empty_structure_dict(): 'plot_interval': None, 'marketplace': None, 'task': None, - 'agents': AgentsConfig.get_empty_structure_dict() + 'agents': AgentsConfig.get_empty_structure_list() } class AgentsConfig(models.Model): - def as_dict(self) -> dict: + def as_list(self) -> dict: referencing_agents = self.agentconfig_set.all() - final_dict = {} - for agent in referencing_agents: - final_dict = {**final_dict, **agent.as_dict()} - return final_dict + return [agent.as_dict() for agent in referencing_agents] @staticmethod - def get_empty_structure_dict(): - return {} + def get_empty_structure_list(): + return [] class AgentConfig(models.Model): agents_config = models.ForeignKey('AgentsConfig', on_delete=models.CASCADE, null=True) name = models.CharField(max_length=100, default='') agent_class = models.CharField(max_length=100, null=True) - argument = models.CharField(max_length=200, null=True) + argument = models.CharField(max_length=200, default='') def as_dict(self) -> dict: - return { - self.name: remove_none_values_from_dict({ + return remove_none_values_from_dict({ + 'name': self.name, 'agent_class': self.agent_class, 'argument': self.argument }) - } class HyperparameterConfig(models.Model): @@ -191,4 +187,4 @@ def to_config_class_name(name: str) -> str: def remove_none_values_from_dict(dict_with_none_values: dict) -> dict: - return {k: v for k, v in dict_with_none_values.items() if v is not None} + return {key: value for key, value in dict_with_none_values.items() if value is not None} diff --git a/webserver/alpha_business_app/static/js/custom.js b/webserver/alpha_business_app/static/js/custom.js index ab3b41bd..ad589656 100644 --- a/webserver/alpha_business_app/static/js/custom.js +++ b/webserver/alpha_business_app/static/js/custom.js @@ -20,9 +20,9 @@ $(document).ready(function() { }); }; - // check for API status all 5 seconds + // check for API status when loading the page and every 30 seconds afterwards updateAPIHealth(); - window.setInterval(function() {updateAPIHealth()}, 5000); + window.setInterval(function() {updateAPIHealth()}, 30000); $("select.task-selection").change(function () { // displays the monitoring options when 'agent_monitoring' is selected diff --git a/webserver/alpha_business_app/tests/constant_tests.py b/webserver/alpha_business_app/tests/constant_tests.py index 597b3cbb..cdc77ce7 100644 --- a/webserver/alpha_business_app/tests/constant_tests.py +++ b/webserver/alpha_business_app/tests/constant_tests.py @@ -32,12 +32,13 @@ 'task': 'training', 'marketplace': 'recommerce.market.circular.circular_sim_market.CircularEconomyRebuyPriceMonopolyScenario', 'enable_live_draw': False, - 'agents': { - 'Rule_Based Agent': { + 'agents': [ + { + 'name': 'Rule_Based Agent', 'agent_class': 'recommerce.market.circular.circular_vendors.RuleBasedCERebuyAgent', 'argument': '' } - } + ] }, 'hyperparameter': { 'rl': { @@ -68,16 +69,18 @@ 'task': 'monitoring', 'enable_live_draw': True, 'marketplace': 'recommerce.market.circular.circular_sim_market.CircularEconomyRebuyPriceMonopolyScenario', - 'agents': { - 'Rule_Based Agent': { + 'agents': [ + { + 'name': 'Rule_Based Agent', 'agent_class': 'recommerce.market.circular.circular_vendors.RuleBasedCERebuyAgent', 'argument': '' }, - 'CE Rebuy Agent (QLearning)': { + { + 'name': 'CE Rebuy Agent (QLearning)', 'agent_class': 'recommerce.rl.q_learning.q_learning_agent.QLearningCERebuyAgent', 'argument': 'CircularEconomyRebuyPriceMonopolyScenario_QLearningCERebuyAgent.dat' } - } + ] }, 'hyperparameter': { 'rl': { @@ -110,7 +113,7 @@ 'plot_interval': None, 'marketplace': None, 'task': None, - 'agents': {} + 'agents': [] }, 'hyperparameter': { 'rl': { diff --git a/webserver/alpha_business_app/tests/test_api_interaction.py b/webserver/alpha_business_app/tests/test_api_interaction.py index 98071418..e422716a 100644 --- a/webserver/alpha_business_app/tests/test_api_interaction.py +++ b/webserver/alpha_business_app/tests/test_api_interaction.py @@ -242,7 +242,7 @@ def test_start_button(self): redirect_mock.assert_called_once_with('/observe', {'success': 'You successfully launched an experiment'}) config_object = Config.objects.all()[1] - assert 'used for test_experiment' == config_object.name + assert 'Config for test_experiment' == config_object.name def _setup_button_handler(self, view: str, request: RequestFactory) -> ButtonHandler: return ButtonHandler(request, view=view, diff --git a/webserver/alpha_business_app/tests/test_config_model.py b/webserver/alpha_business_app/tests/test_config_model.py index dc91903c..f6f5de1e 100644 --- a/webserver/alpha_business_app/tests/test_config_model.py +++ b/webserver/alpha_business_app/tests/test_config_model.py @@ -135,35 +135,40 @@ def test_config_to_dict(self): 'environment': { 'task': 'training', 'marketplace': 'recommerce.market.circular.circular_sim_market.CircularEconomyRebuyPriceMonopolyScenario', - 'agents': { - 'Rule_Based Agent': { + 'agents': [ + { + 'name': 'Rule_Based Agent', 'agent_class': 'recommerce.market.circular.circular_vendors.RuleBasedCERebuyAgent', 'argument': '' } - } + ] } } assert expected_dict == final_config.as_dict() def test_dict_representation_of_agent(self): test_agent = AgentConfig.objects.create(name='test_agent', agent_class='test_class', argument='1234') - expected_dict = {'test_agent': {'agent_class': 'test_class', 'argument': '1234'}} - assert expected_dict == test_agent.as_dict() + expected_dict = {'name': 'test_agent', 'agent_class': 'test_class', 'argument': '1234'} + assert expected_dict == test_agent.as_dict(), (expected_dict, test_agent.as_dict()) - def test_dict_representation_of_agents(self): + def test_list_representation_of_agents(self): test_agents = AgentsConfig.objects.create() AgentConfig.objects.create(name='test_agent1', agent_class='test_class', agents_config=test_agents) AgentConfig.objects.create(name='test_agent2', agent_class='test_class', agents_config=test_agents) - expected_dict = { - 'test_agent1': { - 'agent_class': 'test_class' + expected_list = [ + { + 'name': 'test_agent1', + 'agent_class': 'test_class', + 'argument': '' }, - 'test_agent2': { - 'agent_class': 'test_class' + { + 'name': 'test_agent2', + 'agent_class': 'test_class', + 'argument': '' } - } - assert expected_dict == test_agents.as_dict() + ] + assert expected_list == test_agents.as_list() def test_dict_representation_of_empty_config(self): test_config = Config.objects.create() diff --git a/webserver/alpha_business_app/tests/test_configuration_parser.py b/webserver/alpha_business_app/tests/test_configuration_parser.py index f5c3bd6b..7f79641f 100644 --- a/webserver/alpha_business_app/tests/test_configuration_parser.py +++ b/webserver/alpha_business_app/tests/test_configuration_parser.py @@ -33,12 +33,13 @@ class ConfigParserTest(TestCase): 'task': 'training', 'enable_live_draw': False, 'marketplace': 'recommerce.market.circular.circular_sim_market.CircularEconomyRebuyPriceMonopolyScenario', - 'agents': { - 'Rule_Based Agent': { + 'agents': [ + { + 'name': 'Rule_Based Agent', 'agent_class': 'recommerce.market.circular.circular_vendors.RuleBasedCERebuyAgent', 'argument': '' } - } + ] } } @@ -209,20 +210,24 @@ def test_parsing_config_dict(self): assert '' == all_agents[0].argument def test_parsing_agents(self): - test_dict = { - 'test_agent1': { - 'agent_class': 'test_class' + test_dict = [ + { + 'name': 'test_agent1', + 'agent_class': 'test_class', + 'argument': '' }, - 'test_agent2': { + { + 'name': 'test_agent2', 'agent_class': 'test_class', 'argument': '1234' } - } + ] agents = self.parser._parse_agents_to_datastructure(test_dict) all_agents = agents.agentconfig_set.all() assert 'test_agent1' == all_agents[0].name assert 'test_class' == all_agents[0].agent_class + assert '' == all_agents[0].argument assert 'test_agent2' == all_agents[1].name assert 'test_class' == all_agents[1].agent_class diff --git a/webserver/alpha_business_app/tests/test_data/test_environment_config.json b/webserver/alpha_business_app/tests/test_data/test_environment_config.json index 1c030d66..b96f6bf5 100644 --- a/webserver/alpha_business_app/tests/test_data/test_environment_config.json +++ b/webserver/alpha_business_app/tests/test_data/test_environment_config.json @@ -4,14 +4,16 @@ "episodes": 50, "plot_interval": 25, "marketplace": "recommerce.market.circular.circular_sim_market.CircularEconomyRebuyPriceMonopolyScenario", - "agents": { - "Rule_Based Agent": { + "agents": [ + { + "name": "Rule_Based Agent", "agent_class": "recommerce.market.circular.circular_vendors.RuleBasedCERebuyAgent", "argument": "" }, - "CE Rebuy Agent (QLearning)": { + { + "name": "CE Rebuy Agent (QLearning)", "agent_class": "recommerce.rl.q_learning.q_learning_agent.QLearningCERebuyAgent", "argument": "CircularEconomyRebuyPriceMonopolyScenario_QLearningCERebuyAgent.dat" } - } + ] } diff --git a/webserver/alpha_business_app/tests/test_prefill.py b/webserver/alpha_business_app/tests/test_prefill.py index 1f591ef1..fee29446 100644 --- a/webserver/alpha_business_app/tests/test_prefill.py +++ b/webserver/alpha_business_app/tests/test_prefill.py @@ -92,16 +92,23 @@ def test_merge_two_configs_with_conflicts(self): 'plot_interval': None, 'marketplace': 'recommerce.market.circular.circular_sim_market.CircularEconomyRebuyPriceMonopolyScenario', 'task': 'monitoring', - 'agents': { - 'Rule_Based Agent': { + 'agents': [ + { + 'name': 'Rule_Based Agent', 'agent_class': 'recommerce.market.circular.circular_vendors.RuleBasedCERebuyAgent', 'argument': '' }, - 'CE Rebuy Agent (QLearning)': { + { + 'name': 'Rule_Based Agent', + 'agent_class': 'recommerce.market.circular.circular_vendors.RuleBasedCERebuyAgent', + 'argument': '' + }, + { + 'name': 'CE Rebuy Agent (QLearning)', 'agent_class': 'recommerce.rl.q_learning.q_learning_agent.QLearningCERebuyAgent', 'argument': 'CircularEconomyRebuyPriceMonopolyScenario_QLearningCERebuyAgent.dat' } - } + ] }, 'hyperparameter': { 'rl': { 'gamma': 0.8, @@ -130,7 +137,7 @@ def test_merge_two_configs_with_conflicts(self): 'plot_interval': None, 'marketplace': None, 'task': 'changed environment task from training to monitoring', - 'agents': 'multiple Rule_Based Agent' + 'agents': [] }, 'hyperparameter': { 'rl': { @@ -159,69 +166,70 @@ def test_merge_two_configs_with_conflicts(self): assert expected_final_config == final_config assert expected_error_dict == error_dict, f'{expected_error_dict}\n\n{error_dict}' - def test_merge_one_agent(self): - test_agent_dict = { - 'Rule_Based Agent': { - 'agent_class': 'recommerce.market.circular.circular_vendors.RuleBasedCERebuyAgent', - 'argument': '' - } - } - merger = ConfigMerger() - actual = merger._merge_agents_into_base_agents({}, test_agent_dict) - assert test_agent_dict == actual - - def test_merge_two_same_agents(self): - test_agent_dict1 = { - 'Rule_Based Agent': { - 'agent_class': 'recommerce.market.circular.circular_vendors.RuleBasedCERebuyAgent', - 'argument': '' - } - } - test_agent_dict2 = { - 'Rule_Based Agent': { - 'agent_class': 'recommerce.market.circular.circular_vendors.RuleBasedCERebuyAgent', - 'argument': '' - } - } - merger = ConfigMerger() - actual = merger._merge_agents_into_base_agents(test_agent_dict1, test_agent_dict2) - - expected_dict = { - 'Rule_Based Agent': { - 'agent_class': 'recommerce.market.circular.circular_vendors.RuleBasedCERebuyAgent', - 'argument': '' - } - } - expected_error = copy.deepcopy(EMPTY_STRUCTURE_CONFIG) - expected_error['environment']['agents'] = 'multiple Rule_Based Agent' - - assert expected_dict == actual - assert expected_error == merger.error_dict - - def test_merge_two_agents(self): - test_agent_dict = { - 'test agent': { - 'agent_class': 'recommerce.market.circular.circular_vendors.RuleBasedCERebuyAgent', - 'argument': '' - }, - 'Rule_Based Agent': { - 'agent_class': 'recommerce.market.circular.circular_vendors.RuleBasedCERebuyAgent', - 'argument': '' - } - } - merger = ConfigMerger() - actual = merger._merge_agents_into_base_agents({}, test_agent_dict) - expected_dict = { - 'test agent': { - 'agent_class': 'recommerce.market.circular.circular_vendors.RuleBasedCERebuyAgent', - 'argument': '' - }, - 'Rule_Based Agent': { - 'agent_class': 'recommerce.market.circular.circular_vendors.RuleBasedCERebuyAgent', - 'argument': '' - } - } - assert expected_dict == actual + # following tests are commented since we currently simply append multiple agents and do not merge + # def test_merge_one_agent(self): + # test_agent_dict = { + # 'Rule_Based Agent': { + # 'agent_class': 'recommerce.market.circular.circular_vendors.RuleBasedCERebuyAgent', + # 'argument': '' + # } + # } + # merger = ConfigMerger() + # actual = merger._merge_agents_into_base_agents({}, test_agent_dict) + # assert test_agent_dict == actual + + # def test_merge_two_same_agents(self): + # test_agent_dict1 = { + # 'Rule_Based Agent': { + # 'agent_class': 'recommerce.market.circular.circular_vendors.RuleBasedCERebuyAgent', + # 'argument': '' + # } + # } + # test_agent_dict2 = { + # 'Rule_Based Agent': { + # 'agent_class': 'recommerce.market.circular.circular_vendors.RuleBasedCERebuyAgent', + # 'argument': '' + # } + # } + # merger = ConfigMerger() + # actual = merger._merge_agents_into_base_agents(test_agent_dict1, test_agent_dict2) + + # expected_dict = { + # 'Rule_Based Agent': { + # 'agent_class': 'recommerce.market.circular.circular_vendors.RuleBasedCERebuyAgent', + # 'argument': '' + # } + # } + # expected_error = copy.deepcopy(EMPTY_STRUCTURE_CONFIG) + # expected_error['environment']['agents'] = 'multiple Rule_Based Agent' + + # assert expected_dict == actual + # assert expected_error == merger.error_dict + + # def test_merge_two_agents(self): + # test_agent_dict = { + # 'test agent': { + # 'agent_class': 'recommerce.market.circular.circular_vendors.RuleBasedCERebuyAgent', + # 'argument': '' + # }, + # 'Rule_Based Agent': { + # 'agent_class': 'recommerce.market.circular.circular_vendors.RuleBasedCERebuyAgent', + # 'argument': '' + # } + # } + # merger = ConfigMerger() + # actual = merger._merge_agents_into_base_agents({}, test_agent_dict) + # expected_dict = { + # 'test agent': { + # 'agent_class': 'recommerce.market.circular.circular_vendors.RuleBasedCERebuyAgent', + # 'argument': '' + # }, + # 'Rule_Based Agent': { + # 'agent_class': 'recommerce.market.circular.circular_vendors.RuleBasedCERebuyAgent', + # 'argument': '' + # } + # } + # assert expected_dict == actual def test_update_error_dict(self): expected_error_dict = copy.deepcopy(EMPTY_STRUCTURE_CONFIG) diff --git a/webserver/templates/configurator.html b/webserver/templates/configurator.html index 95acd08f..e4e486bf 100644 --- a/webserver/templates/configurator.html +++ b/webserver/templates/configurator.html @@ -2,7 +2,7 @@ {% block title %}Configurator{% endblock title %} {% block content %} {% load static %} -

You can configurate your experiments here

+

You can configure your experiments here

diff --git a/webserver/templates/index.html b/webserver/templates/index.html index 33702163..fa8d828a 100644 --- a/webserver/templates/index.html +++ b/webserver/templates/index.html @@ -1,7 +1,7 @@ {% extends "base.html" %} {% block title %}Welcome{% endblock title %} {% block content %} -

Welcome to the Alpha Business simulation

+

Welcome to the Recommerce simulation