"""
Backend for AGE-ABM Visual Interface
@Author Max Hall
@Co-Authors Meghan Ireland and Matthew Fleischman
"""
# Imports
import os
from jsonpickle import dumps, decode
import backend.backendutils as utils
from backend.backend_classes import ImportDetails, Attribute, Component, Agent, System, Generator
from backend.backend_classes import AgentOfInterest, ModelDetails
from utils.helper_methods import adding_name_checker, getting_name_checker, get_data_types
[docs]
class ModelEnv:
""" This is a base class which will store all the data relating to an instance of creating a
new model using the GUI this includes storing dictionaries containing imports, components,
agents, systems and data collection systems classes as well as a model details class """
def __init__(self, name : str = "Custom", exist_proj : str = None):
if exist_proj is not None: # pragma: no cover
with open(exist_proj, 'r', encoding='UTF-8') as file:
all_data = decode(file.read())
# Stores the imports classes
self.imports = all_data["imports"]
# Stores components classes
self.components = all_data["components"]
# Stores agents classes
self.agents = all_data["agents"]
# Stores systems classes
self.systems = all_data["systems"]
# Stores the model class
self.model_details = all_data["model_details"]
# Stores the agents_of_interest classes
self.agents_of_interest = all_data["agents_of_interest"]
else:
formatted_name = adding_name_checker(name, [])
if formatted_name in [1,2]:
formatted_name = "Default_Name"
# Stores import details classes
self.imports = {"ECAgent.Core" : ImportDetails("Core", "as")
, "ECAgent.Tags" : ImportDetails("Tags", "as")
, "ECAgent.Collectors" : ImportDetails("Collectors", "as")
, "numpy" : ImportDetails("np", "as")}
# Stores components classes
self.components = {"Counter" : Component("This keeps count of number of agents", True
, {"counter" : Attribute("int", "comp_lvl", 0)})}
# Stores agents classes
self.agents = {}
# Stores systems classes
self.systems = {}
# Stores the model class
self.model_details = ModelDetails(formatted_name, f"The {formatted_name} Model")
# Stores data collection system classes
self.agents_of_interest = {}
[docs]
def get_list_imports(self):
"""Gets a list of all imports added to the model.
Returns
-------
Attribute : list[str]
| Returns a list of imports names.
"""
if self.imports:
return list(self.imports)
return []
[docs]
def get_list_components(self):
"""Gets a list of all components added to the model.
Returns
-------
Attribute : list[str]
| Returns a list of components names.
"""
if self.components:
arr1 = list(self.components)
arr1.remove("Counter")
return arr1
return []
[docs]
def get_list_attributes(self, relevent_component : str):
"""Gets a list of attributes which a relevent_component has.
Parameters
----------
relevent_component : str
The relevent_component having the attributes names.
Returns
-------
Attribute : list[str]
| Returns a list of that components attributes names.
"""
# Tries to format the name to correct format
formatted_name = getting_name_checker(relevent_component, list(self.components))
# Checks for error messages
if formatted_name == 1:
return [] # Name already in use
return list(self.components[formatted_name].get_attributes())
[docs]
def get_list_agents(self):
"""Gets a list of all agents added to the model.
Returns
-------
Attribute : list[str]
| Returns a list of agents names
"""
if self.agents:
return list(self.agents)
return []
[docs]
def get_list_systems(self):
"""Gets a list of all systems added to the model.
Returns
-------
Attribute : list[str]
| Returns a list of systems names
"""
if self.systems:
return list(self.systems)
return []
[docs]
def get_list_gen_attributes(self, relevent_system : str, relevent_generator : str):
"""Gets a list of attributes which a relevent_generator has.
Parameters
----------
relevent_system : str
The relevent_system having the generators names.
relevent_generator : str
The relevent_generator having the attributes names.
Returns
-------
Attributes : list[str]
| Returns a list of that generators attributes names
"""
# Getting the formatted name
formatted_system_name = getting_name_checker(relevent_system, self.get_list_systems())
# Checks for error messages
if formatted_system_name == 1:
return [] # Name not already in use
# Getting the formatted name
formatted_gen_name = getting_name_checker(relevent_generator
, list(self.get_list_generators(formatted_system_name)))
# Checks for error messages
if formatted_gen_name == 1:
return [] # Name not already in use
if self.get_generator(formatted_system_name, formatted_gen_name).get_attributes():
return list(self.get_generator(formatted_system_name
, formatted_gen_name).get_attributes())
return []
[docs]
def get_list_generators(self, relevent_system : str):
"""Gets a list of generators which a relevent_system has.
Parameters
----------
relevent_system : str
The relevent_system having the generators names.
Returns
-------
Generators : list[str]
| Returns a list of that components attributes
"""
# Getting the formatted name
formatted_system_name = getting_name_checker(relevent_system, self.get_list_systems())
# Checks for error messages
if formatted_system_name == 1:
return [] # Name already in use
if self.get_system(formatted_system_name).get_generators():
return list(self.get_system(formatted_system_name).get_generators())
return []
[docs]
def get_list_agents_of_interest(self):
"""Gets a list of all agents_of_interest added to the model.
Returns
-------
Attribute : list[str]
| Returns a list of agents_of_interest names
"""
if self.agents_of_interest:
return list(self.agents_of_interest)
return []
[docs]
def add_import(self, library : str, nickname : str, import_type : str = "*"
, list_of_methods : list[str] = None):
"""Stores the newly created import into the models imports dictionary.
Parameters
----------
library : str
The library
library: str
The nickname for the library being imported
import_type : str
"as" implies it has an alias,
"from" implies it is a list of things being imported from the library,
"*" imports everything from the library and is the default
list_of_methods : list[str], Optional
If import_type is "from", then this is the list of things to be imported from the library
Returns
-------
error term : int
| Returns 0 on success
| Returns 1 if library import already exist
| Returns 2 if invalid import_type used
| Returns 3 if type is from and no methods selected
| Returns 4 if nickname already in use
"""
if library in self.imports:
return 1
if import_type not in ["as", "from", "*"]:
return 2
if import_type == "from" and (list_of_methods is None or list_of_methods == []):
return 3
for import_lib in self.get_list_imports():
if nickname == self.get_import(import_lib).get_nickname():
return 4
self.imports[library] = ImportDetails(nickname, import_type, list_of_methods)
return 0
[docs]
def edit_import(self, library : str, nickname : str = None, import_type : str = None
, list_of_methods : list[str] = None):
"""Edits the provided named import into the models imports dictionary.
Must specify what import you would like to edit.
Parameters
----------
library : str
The library
nickname: str, Optional
The nickname for the library being imported
import_type : str, Optional
"as" implies it has an alias, "from" implies it is a list of things being imported from the library,
"*" imports everything from the library and is the default
list_of_methods : list[str], Optional
If import_type is "from", then this is the list of things to be imported from the library
Returns
-------
error term : int
| Returns 0 on success
| Returns 1 if library import doesn't already exist
| Returns 2 if nickname already in use
| Returns 3 if invalid import_type used
| Returns 4 if type is from and no methods selected
"""
if library not in self.imports:
return 1
if nickname is not None:
for import_lib in self.get_list_imports():
if nickname == self.get_import(import_lib).get_nickname():
return 2
self.get_import(library).edit_nickname(nickname)
if import_type is not None:
if import_type not in ["as", "from", "*"]:
return 3
if import_type != "from":
self.get_import(library).edit_list_of_methods(None)
if import_type == "from" and (list_of_methods is None or len(list_of_methods) == 0):
return 4
if import_type == "from":
self.get_import(library).edit_list_of_methods(list_of_methods)
self.get_import(library).edit_import_type(import_type)
if list_of_methods is not None:
if import_type is None and self.get_import(library).get_import_type() != "from":
return 3
if len(list_of_methods) == 0:
return 4
self.get_import(library).edit_list_of_methods(list_of_methods)
return 0
[docs]
def get_import(self, library : str):
"""Gets the requested import from the models imports dictionary.
Parameters
----------
library : str
The import's library.
Returns
-------
library : ImportDetails
| Returns an ImportDetails object containing the details from the requested import.
| Returns None if library import doesn't already exist.
"""
if library in self.imports:
return self.imports[library]
return None
[docs]
def remove_import(self, library : str):
"""Removes a set import from the models imports dictionary.
Parameters
----------
library : str
The library of the import to be removed.
Returns
-------
error term : int
| Returns 0 on success
| Returns 1 if library import doesn't already exist
"""
if library in self.imports:
if library == "ECAgent.Environments":
self.edit_model_details(gridworld=False)
else:
del self.imports[library]
return 0
return 1
[docs]
def add_attribute(self, relevent_component : str, name : str, type_attr : str
, define_level : str, default_value : int = None):
"""Stores the newly created attribute into the models attributes dictionary.
Parameters
----------
relevent_component : str
The relevent_component having the attributes name.
name : str
The attribute's name.
type_attr: str ["int", "float", "bool"]
The primitive type expected.
default_value : int, Optional
If there is a default value for the attribute, else it defaults to 'None'
define_level : str, Optional ['comp_lvl', 'agent_lvl', 'model_lvl']
If there is a user defined level, else it defaults to 'None'
Returns
-------
error term : int
| Returns 0 on success
| Returns 1 if attribute name already exists
| Returns 2 if invalid characters used in attribute name
| Returns 3 if type attribute not valid
| Returns 4 if no default value provided
| Returns 5 if invalid define level used
"""
# Tries to format the name to correct format
formatted_comp_name = getting_name_checker(relevent_component, self.get_list_components())
# Checks for error messages
if formatted_comp_name in [1,2, "Counter"]:
return 1 # Name not already in use or invalid chars
# Tries to format the name to correct format
formatted_name = adding_name_checker(name
, list(self.components[formatted_comp_name].get_attributes()), True)
# Checks for error messages
if formatted_name in [1,2]:
return formatted_name # Name already in use or invalid chars
if type_attr not in ["int", "float", "bool"]:
return 3
# Handles different creation based on define_level
if define_level == "comp_lvl":
if default_value is None:
return 4
self.components[formatted_comp_name].get_attributes()[formatted_name] = Attribute(type_attr, "comp_lvl", default_value)
elif define_level == "agent_lvl":
self.components[formatted_comp_name].get_attributes()[formatted_name] = Attribute(type_attr, "agent_lvl", {})
elif define_level == "model_lvl":
self.components[formatted_comp_name].get_attributes()[formatted_name] = Attribute(type_attr, "model_lvl")
if self.get_component(formatted_comp_name).get_is_class_component() is False: # Only add it at the agent_lvl
self.model_details.add_initial_parameter(f"{formatted_comp_name.upper()}_{formatted_name.upper()}", type_attr)
else:
return 5 # If the define level isn't a valid one
return 0
[docs]
def add_agent_lvl_default_value(self, agent_name : str, component_name : str
, attribute_name : str, default_value : str):
"""Edits the provided default value for the named attribute into the models attribute's
default value dictionary.
Parameters
----------
agent_name : str
The agent_name having the attribute_name.
component_name : str
The component_name having the attribute_name.
attribute_name : str
The attribute_name having the default_value.
default_value : int
The default value code for the attribute.
Returns
-------
error term : int
| Returns 0 on success
| Returns 1 if name doesn't already exists
| Returns 2 if invalid characters used in name
| Returns 3 if agent level define level not used
"""
# Getting the formatted name
formatted_agent_name = getting_name_checker(agent_name, self.get_list_agents())
# Checks for error messages
if formatted_agent_name in [1,2]:
return formatted_agent_name # Name not already in use or invalid chars
formatted_comp_name = getting_name_checker(component_name
, self.get_agent(formatted_agent_name).get_given_components())
# Checks for error messages
if formatted_comp_name in [1,2]:
return formatted_comp_name # Name not already in use or invalid chars
formatted_attr_name = getting_name_checker(attribute_name
, self.get_list_attributes(formatted_comp_name), True)
# Checks for error messages
if formatted_attr_name in [1,2]:
return formatted_attr_name # Name already in use or invalid chars
attribute = self.get_attribute(formatted_comp_name, formatted_attr_name)
if attribute.get_define_level() != "agent_lvl":
return 3 # If isn't an agent level attribute
attribute.get_default_value()[formatted_agent_name] = default_value
return 0
[docs]
def edit_attribute(self, relevent_component : str, name : str, type_attr : str = None
, define_level : str = None, default_value : int = None):
"""Edits the provided named attribute into the models attributes dictionary.
Must specify what attribute you would like to edit.
Parameters
----------
relevent_component : str
The relevent_component having the attributes name.
name : str
The attribute's name.
type_attr: str, Optional
The primitive type expected.
default_value : int, Optional
If there is a default value for the attribute.
define_level : str, Optional ['comp_lvl', 'agent_lvl', 'model_lvl']
If there is a user defined level, else it defaults to 'None'.
Returns
-------
error term : int
| Returns 0 on success
| Returns 1 if attribute name doesn't already exists
| Returns 2 if invalid characters used in attribute name
| Returns 3 if invalid attribute type
| Returns 4 if invalid define level
| Returns 5 if define level is component, but no default value given
"""
# Getting the formatted name
formatted_comp_name = getting_name_checker(relevent_component, self.get_list_components())
# Checks for error messages
if formatted_comp_name in [1,2, "Counter"]:
return 1 # Name not already in use or invalid chars
# Getting the formatted name
formatted_name = getting_name_checker(name
, list(self.components[formatted_comp_name].get_attributes()), True)
# Checks for error messages
if formatted_name in [1,2]:
return formatted_name # Name not already in use or invalid chars
chosen_attribute = self.components[formatted_comp_name].get_attributes()[formatted_name]
if type_attr is not None:
if type_attr not in ["bool", "int", "float"]:
return 3
chosen_attribute.edit_type_attr(type_attr)
if define_level is not None:
if define_level not in ["comp_lvl", "agent_lvl", "model_lvl"]:
return 4
# If is now "model_lvl"
old_lvl = self.get_attribute(formatted_comp_name, formatted_name).get_define_level()
if define_level == "model_lvl" and define_level != old_lvl:
if self.get_component(formatted_comp_name).get_is_class_component() is False:
self.model_details.add_initial_parameter(f"{formatted_comp_name.upper()}_{formatted_name.upper()}", chosen_attribute.get_type_attr())
self.get_attribute(formatted_comp_name, formatted_name).edit_default_value(None)
# If was previously "model_lvl"
elif old_lvl == "model_lvl" and define_level != old_lvl:
self.model_details.remove_initial_parameter(f"{formatted_comp_name.upper()}_{formatted_name.upper()}")
# If was comp_lvl and now isn't
if define_level != "comp_lvl":
chosen_attribute.edit_default_value(None)
if define_level == "comp_lvl" and default_value is None:
return 5
if define_level == "agent_lvl":
chosen_attribute.edit_default_value({})
chosen_attribute.edit_define_level(define_level)
if default_value is not None:
if define_level == "agent_lvl" and str(type(default_value)) != "<class 'dict'>":
chosen_attribute.edit_default_value({})
chosen_attribute.edit_default_value(default_value)
return 0
[docs]
def get_attribute(self, relevent_component : str, name : str):
"""Gets the requested attribute from the models attributes dictionary.
Parameters
----------
relevent_component : str
The relevent_component having the attributes name.
name : str
The attribute's name.
Returns
-------
Attribute : dict
| Returns a dictionary containing the details from the requested attribute.
| Returns None if attribute doesn't already exist.
"""
# Getting the formatted name
formatted_comp_name = getting_name_checker(relevent_component, list(self.components))
# Checks for error messages
if formatted_comp_name in [1,2]:
return None # Name not already in use or invalid chars
# Getting the formatted name
formatted_name = getting_name_checker(name
, list(self.components[formatted_comp_name].get_attributes()), True)
# Checks for error messages
if formatted_name in [1,2]:
return None # Name already in use or invalid chars
return self.get_component(formatted_comp_name).get_attributes()[formatted_name]
[docs]
def remove_attribute(self, relevent_component : str, name : str):
"""Removes a set attribute from the models attributes dictionary.
Parameters
----------
relevent_component : str
The relevent_component having the attributes name.
name : str
The name of the attribute to be removed.
Returns
-------
error term : int
| Returns 0 on success
| Returns 1 if attribute name doesn't already exists
| Returns 2 if invalid characters used in attribute name
"""
# Getting the formatted name
formatted_comp_name = getting_name_checker(relevent_component, self.get_list_components())
# Checks for error messages
if formatted_comp_name in [1,2, "Counter"]:
return 1 # Name not already in use or invalid chars
# Getting the formatted name
formatted_name = getting_name_checker(name
, list(self.components[formatted_comp_name].get_attributes()), True)
# Checks for error messages
if formatted_name in [1,2]:
return formatted_name # Name already in use or invalid chars
if self.get_attribute(relevent_component, formatted_name).get_define_level() == "model_lvl":
self.model_details.remove_initial_parameter(f"{relevent_component.upper()}_{formatted_name.upper()}")
del self.components[relevent_component].get_attributes()[formatted_name]
return 0
[docs]
def add_component(self, name : str, description : str, is_class_component : bool = False
, attributes : dict = None):
"""Stores the newly created component into the models components dictionary.
Parameters
----------
name : str
The component's name.
description : str
A description of the component.
is_class_component : bool, Optional
User can specify if this is a class component or not.
attributes : dict
User provided attributes of the components.
Returns
-------
error term : int
| Returns 0 on success
| Returns 1 if component name already exists
| Returns 2 if invalid characters used in component name
"""
# Tries to format the name to correct format
formatted_name = adding_name_checker(name, list(self.components))
# Checks for error messages
if formatted_name in [1, "Counter"]:
return 1 # Name already in use
if formatted_name == 2:
return 2 # Name uses invalid chars
if attributes is None:
attributes = {}
self.components[formatted_name] = Component(description, is_class_component, attributes)
return 0
[docs]
def edit_component(self, name : str, description : str = None
, is_class_component : bool = None):
"""Edits the provided named component into the models components dictionary.
Parameters
----------
name : str
The component's name.
description : str, Optional
A description of the component.
attributes : dict, Optional
User provided attributes for the component.
is_class_component : bool, Optional
Tells the viewer if the component is a class component.
Returns
-------
error term : int
| Returns 0 on success
| Returns 1 if component name doesn't already exists
| Returns 2 if invalid characters used in component name
"""
# Tries to format the name to correct format
formatted_name = getting_name_checker(name, list(self.components))
# Checks for error messages
if formatted_name in [1, "Counter"]:
return 1 # Name not already in use
if description is not None:
self.get_component(formatted_name).edit_description(description)
if is_class_component is not None:
self.get_component(formatted_name).edit_is_class_component(is_class_component)
return 0
[docs]
def get_component(self, name : str):
"""Gets the requested component from the models components dictionary.
Parameters
----------
name : str
The component's name.
Returns
-------
component : Component
| Returns the component object correlating to the given name.
| Returns None if name doesn't correspond to existing component
"""
# Tries to format the name to correct format
formatted_name = getting_name_checker(name, list(self.components))
# Checks for error messages
if formatted_name == 1:
return None # Name not already in use
return self.components[formatted_name]
[docs]
def remove_component(self, name : str):
"""Removes a set component from the models component dictionary.
Parameters
----------
name : str
The name of the component to be removed.
Returns
-------
error term : int
| Returns 0 on success
| Returns 1 if component name doesn't already exists
| Returns 2 if invalid characters used in component name
"""
# Tries to format the name to correct format
formatted_name = getting_name_checker(name, list(self.components))
# Checks for error messages
if formatted_name in [1, "Counter"]:
return 1 # Name not already in use
given_attributes = self.get_list_attributes(formatted_name)
for attr in given_attributes:
if self.get_attribute(formatted_name, attr).get_define_level() == "model_lvl":
self.model_details.remove_initial_parameter(f"{formatted_name.upper()}_{attr.upper()}")
del self.components[formatted_name]
return 0
[docs]
def add_agent(self, name : str, description : str, given_components : list[str]
, init_coordinates : list[str] = None):
"""Stores the newly created agent into the models agents dictionary.
Gets parameters from components
Parameters
----------
name : str
The agent's name.
description : str
A description of the agent.
given_components : str[]
User provided attributes of the agents.
Returns
-------
error term : int
| Returns 0 on success
| Returns 1 if agent name already exists
| Returns 2 if invalid characters used in agent name
| Returns 3 if invalid component name supplied
| Returns 4 if gridworld and no initial co-ordinates supplied
"""
given_components = given_components.copy()
formatted_comp_names = [getting_name_checker(comp, list(self.get_list_components())) for comp in given_components]
if 1 in formatted_comp_names:
return 3 # A name is not currently a component
# Making sure the agent has a counter component in order to keep its ID's unique
if "Counter" not in formatted_comp_names:
formatted_comp_names.append("Counter")
# Tries to format the name to correct format
formatted_name = adding_name_checker(name, list(self.agents))
# Checks for error messages
if formatted_name == 1:
return 1 # Name already in use
if formatted_name == 2:
return 2 # Name uses invalid chars
# Adding the agent
if self.model_details.get_gridworld():
if init_coordinates is None:
return 4
self.agents[formatted_name] = Agent(description, formatted_comp_names, init_coordinates)
else:
self.agents[formatted_name] = Agent(description, formatted_comp_names)
self.model_details.add_num_agent(formatted_name.upper())
for comp in formatted_comp_names:
if self.get_component(comp).get_is_class_component() and comp != "Counter":
for attr in self.get_list_attributes(comp):
attribute = self.get_attribute(comp, attr)
self.model_details.add_initial_parameter(f"{formatted_name.upper()}_{comp.upper()}_{attr.upper()}",attribute.get_type_attr())
return 0
[docs]
def edit_agent(self, name : str, description : str = None, given_components : list[str] = None
, init_coordinates : list[str] = None):
"""Edits the provided named agent into the models agents dictionary.
Parameters
----------
name : str
The agent's name.
description : str, Optional
A description of the agent.
given_components : list[str], Optional
User provided components for the agent.
Returns
-------
error term : int
| Returns 0 on success
| Returns 1 if agent name doesn't already exists
| Returns 2 if invalid characters used in agent name
| Returns 3 if invalid component name given
| Returns 4 if gridworld and no initial co-ordinates supplied
"""
# Tries to format the name to correct format
formatted_name = getting_name_checker(name, list(self.agents))
# Checks for error messages
if formatted_name == 1:
return 1 # Name not already in use
if description is not None:
self.get_agent(formatted_name).edit_description(description)
if given_components is not None:
given_components = given_components.copy()
formatted_comp_names = [getting_name_checker(comp_name, list(self.get_list_components())) for comp_name in given_components]
if 1 in formatted_comp_names:
return 3 # A name is not currently a component
# Making sure the agent has a counter component in order to keep its ID's unique
if "Counter" not in formatted_comp_names:
formatted_comp_names.append("Counter")
# Getting the non-overlapping components
uncommon_components = [comp for comp in self.get_agent(formatted_name).get_given_components() if comp not in formatted_comp_names]
new_components = [comp for comp in formatted_comp_names if comp not in self.get_agent(formatted_name).get_given_components()]
# Removes initial value requirement for discarded class components
for comp in uncommon_components:
if self.get_component(comp).get_is_class_component():
for attr in self.get_list_attributes(comp):
self.model_details.remove_initial_parameter(f"{formatted_name.upper()}_{comp.upper()}_{attr.upper()}")
# Adds initial value requirement for new class components
for comp in new_components:
if self.get_component(comp).get_is_class_component() and comp != "Counter":
for attr in self.get_list_attributes(comp):
attribute = self.get_attribute(comp, attr)
self.model_details.add_initial_parameter(f"{formatted_name.upper()}_{comp.upper()}_{attr.upper()}", attribute.get_type_attr())
self.get_agent(formatted_name).edit_given_components(formatted_comp_names)
if self.model_details.get_gridworld():
if init_coordinates is None and self.get_agent(formatted_name).get_init_coordinates() is None:
return 4
if init_coordinates is not None:
init_coordinates = init_coordinates.copy()
self.get_agent(formatted_name).edit_init_coordinates(init_coordinates)
return 0
[docs]
def get_agent(self, name : str):
"""Gets the requested agent from the models agents dictionary.
Parameters
----------
name : str
The agent's name.
Returns
-------
agent : Agent
| Returns an Agent object corresponding to the given name.
| Returns None if no match.
"""
# Tries to format the name to correct format
formatted_name = getting_name_checker(name, list(self.agents))
# Checks for error messages
if formatted_name == 1:
return None # Name already in use
return self.agents[formatted_name]
[docs]
def remove_agent(self, name : str):
"""Removes a set agent from the models agents dictionary.
Parameters
----------
name : str
The name of the agent to be removed.
Returns
-------
error term : int
| Returns 0 on success
| Returns 1 if agent name doesn't already exists
| Returns 2 if invalid characters used in agent name
"""
# Tries to format the name to correct format
formatted_name = getting_name_checker(name, list(self.agents))
# Checks for error messages
if formatted_name == 1:
return 1 # Name already in use
# Removes initial value requirement for discarded agents' class components
given_components = self.get_agent(formatted_name).get_given_components()
for comp in given_components:
if self.get_component(comp).get_is_class_component() and comp != "Counter":
for attr in self.get_list_attributes(comp):
self.model_details.remove_initial_parameter(f"{formatted_name.upper()}_{comp.upper()}_{attr.upper()}")
del self.agents[formatted_name]
self.model_details.remove_num_agent(formatted_name.upper())
return 0
[docs]
def add_system(self, name : str, description : str, start : int = None, end : int = None
, frequency : int = None, generators : dict = None):
"""Stores the newly created system into the models systems dictionary.
Parameters
----------
name : str
The system's name.
description : str
A description of the system.
start : int, Optional
The iteration that the system must start on
end : int, Optional
The iteration that the end must start on
frequency : int, Optional
Per how many iterations must the system execute
generators : dict[Generator], Optional
Rather use add, edit, get, remove generator
Returns
-------
error term : int
| Returns 0 on success
| Returns 1 if system name already exists
| Returns 2 if invalid characters used in system name
"""
# Tries to format the name to correct format
formatted_name = adding_name_checker(name, list(self.systems))
# Checks for error messages
if formatted_name == 1:
return 1 # Name already in use
if formatted_name == 2:
return 2 # Name uses invalid chars
# Adds system and the required extra parameters if needed
self.systems[formatted_name] = System(description)
if start is not None:
self.edit_system(formatted_name, start=start)
if end is not None:
self.edit_system(formatted_name, end=end)
if frequency is not None:
self.edit_system(formatted_name, frequency=frequency)
if generators is not None:
self.edit_system(formatted_name, generators=generators)
return 0
[docs]
def edit_system(self, name : str, description : str = None, execute_function_body : str = None
, start : int = None, end : int = None, frequency : int = None
, generators : dict = None):
"""Edits the provided named system into the models system dictionary.
Parameters
----------
name : str
The system's name.
description : str, Optional
A description of the system.
execute_function_body : str, Optional
User provided execute function for the system.
start : int, Optional
The iteration that the system must start on.
end : int, Optional
The iteration that the end must start on.
frequency : int, Optional
Per how many iterations must the system execute.
Returns
-------
error term : int
| Returns 0 on success
| Returns 1 if system name doesn't already exists
| Returns 2 if invalid characters used in system name
"""
# Tries to format the name to correct format
formatted_name = getting_name_checker(name, list(self.systems))
# Checks for error messages
if formatted_name == 1:
return 1 # Name already in use
if description is not None:
self.get_system(formatted_name).edit_description(description)
if execute_function_body is not None:
self.get_system(formatted_name).edit_execute_function_body(execute_function_body)
if start is not None:
self.get_system(formatted_name).edit_start(start)
if end is not None:
self.get_system(formatted_name).edit_end(end)
if frequency is not None:
self.get_system(formatted_name).edit_frequency(frequency)
if generators is not None:
self.get_system(formatted_name).edit_generators(generators)
return 0
[docs]
def get_system(self, name : str):
"""Gets the requested system from the models systems dictionary.
Parameters
----------
name : str
The system's name.
Returns
-------
system : System
| Returns a System object which matches the provided name.
| Returns None if no match.
"""
# Tries to format the name to correct format
formatted_name = getting_name_checker(name, list(self.systems))
# Checks for error messages
if formatted_name == 1:
return None # Name already in use
return self.systems[formatted_name]
[docs]
def remove_system(self, name : str):
"""Removes a set system from the models system dictionary.
Parameters
----------
name : str
The name of the system to be removed.
Returns
-------
error term : int
| Returns 0 on success
| Returns 1 if system name doesn't already exists
| Returns 2 if invalid characters used in system name
"""
# Tries to format the name to correct format
formatted_name = getting_name_checker(name, list(self.systems))
# Checks for error messages
if formatted_name == 1:
return 1 # Name not already in use
del self.systems[formatted_name]
return 0
[docs]
def add_gen_attribute(self, relevent_system : str, relevent_generator : str, name : str
, type_attr : str):
"""Stores the newly created attribute into the system generators attributes dictionary.
Parameters
----------
relevent_system : str
The relevent_system having the relevent_generator.
relevent_component : str
The relevent_generator having the attribute.
name : str
The attribute's name.
type_attr : str ["int", "bool", "float"]
The type of the attribute
Returns
-------
error term : int
| Returns 0 on success
| Returns 1 if names already exists
| Returns 2 if invalid characters used in the names
| Returns 3 if invalid type_attr
"""
# Getting the formatted name
formatted_system_name = getting_name_checker(relevent_system, self.get_list_systems())
# Checks for error messages
if formatted_system_name in [1,2]:
return formatted_system_name # Name not already in use or invalid chars
formatted_gen_name = getting_name_checker(relevent_generator
, self.get_list_generators(formatted_system_name))
# Checks for error messages
if formatted_gen_name in [1,2]:
return formatted_gen_name # Name not already in use or invalid chars
formatted_attr_name = adding_name_checker(name
, list(self.get_list_gen_attributes(formatted_system_name
, formatted_gen_name)), True)
# Checks for error messages
if formatted_attr_name in [1,2]:
return formatted_attr_name # Name already in use or invalid chars
# Invalid type of attribute
if type_attr not in ["int", "float", "bool"]:
return 3
# Adds system generator attribute
self.get_generator(formatted_system_name, formatted_gen_name).get_attributes()[formatted_attr_name] = Attribute(type_attr, "model_lvl")
self.model_details.add_initial_parameter(f"{formatted_system_name.upper()}_{formatted_attr_name.upper()}", type_attr)
return 0
[docs]
def get_gen_attribute(self, relevent_system : str, relevent_generator : str, name : str):
"""Gets the requested attribute from the system generators attributes dictionary.
Parameters
----------
relevent_system : str
The relevent_system having the relevent_generator.
relevent_component : str
The relevent_generator having the attribute.
name : str
The attribute's name.
Returns
-------
gen_attribute : Attribute
| Returns the Attribute object corresponding the the provided names.
| Returns None if no match.
"""
# Getting the formatted name
formatted_system_name = getting_name_checker(relevent_system, self.get_list_systems())
# Checks for error messages
if formatted_system_name in [1,2]:
return None # Name not already in use or invalid chars
# Getting the formatted name
formatted_gen_name = getting_name_checker(relevent_generator
, self.get_list_generators(formatted_system_name))
# Checks for error messages
if formatted_gen_name in [1,2]:
return None # Name not already in use or invalid chars
# Getting the formatted name
formatted_attr_name = getting_name_checker(name
, list(self.get_list_gen_attributes(formatted_system_name
, formatted_gen_name))
, True)
# Checks for error messages
if formatted_attr_name in [1,2]:
return None # Name already in use or invalid chars
return self.get_generator(formatted_system_name, formatted_gen_name).get_attributes()[formatted_attr_name]
[docs]
def remove_gen_attribute(self, relevent_system : str, relevent_generator : str, name : str):
"""Removes a set attribute from the generators attribute dictionary of a specific system.
Parameters
----------
name : str
The name of the system to be removed.
Returns
-------
error term : int
| Returns 0 on success
| Returns 1 if names don't already exists
| Returns 2 if invalid characters used in names
"""
# Getting the formatted name
formatted_system_name = getting_name_checker(relevent_system, self.get_list_systems())
# Checks for error messages
if formatted_system_name in [1,2]:
return formatted_system_name # Name not already in use or invalid chars
# Getting the formatted name
formatted_gen_name = getting_name_checker(relevent_generator, self.get_list_generators(formatted_system_name))
# Checks for error messages
if formatted_gen_name in [1,2]:
return formatted_gen_name # Name not already in use or invalid chars
# Getting the formatted name
formatted_attr_name = getting_name_checker(name
, list(self.get_list_gen_attributes(formatted_system_name
, formatted_gen_name))
, True)
# Checks for error messages
if formatted_attr_name in [1,2]:
return formatted_attr_name # Name already in use or invalid chars
self.model_details.remove_initial_parameter(f"{formatted_system_name.upper()}_{formatted_attr_name.upper()}")
del self.get_generator(formatted_system_name, formatted_gen_name).get_attributes()[formatted_attr_name]
return 0
[docs]
def add_generator(self, relevent_system : str, name : str, description : str
, nickname : str = None, return_value : str = None
, attributes : dict = None):
"""Stores the newly created generator into the systems generators dictionary.
Parameters
----------
relevent_system : str
The relevent_system having the generator name.
name : str
The generator's name.
description: str
A description of the generator.
nickname : str, Optional
The nickname for cells using this type.
return_value : str, Optional
The code used to generate the return value.
attributes : dict, Optional
User provided attributes of the generator.
Returns
-------
error term : int
| Returns 0 on success
| Returns 1 if names already exists
| Returns 2 if invalid characters used in the names
| Returns 3 if nickname already in use in this system
"""
# Getting the formatted name
formatted_system_name = getting_name_checker(relevent_system, self.get_list_systems())
# Checks for error messages
if formatted_system_name in [1,2]:
return formatted_system_name # Name not already in use or invalid chars
# Getting the formatted name
formatted_gen_name = adding_name_checker(name
, list(self.get_list_generators(formatted_system_name)))
# Checks for error messages
if formatted_gen_name in [1,2]:
return formatted_gen_name # Name not already in use or invalid chars
if attributes is None:
attributes = {}
# Set nickname if it isn't provided
if nickname is None:
nickname = formatted_gen_name.lower()
formatted_nickname = adding_name_checker(nickname, [], True)
if formatted_nickname in [self.get_generator(formatted_system_name, gen_name).get_nickname() for gen_name in self.get_list_generators(formatted_system_name)]:
return 3
# Add generator
self.get_system(formatted_system_name).get_generators()[formatted_gen_name] = Generator(description, formatted_nickname, return_value, attributes)
return 0
[docs]
def edit_generator(self, relevent_system : str, name : str, description : str = None
, nickname :str = None, return_value : str = None):
"""Stores the newly created generator into the systems generators dictionary.
Parameters
----------
relevent_system : str
The relevent_system having the generator name.
name : str
The generator's name.
description: str, Optional
A description of the generator.
nickname : str, Optional
The nickname for cells using this type.
return_value : str, Optional
The code used to generate the return value.
attributes : dict, Optional
User provided attributes of the generator.
Returns
-------
error term : int
| Returns 0 on success
| Returns 1 if names don't already exists
| Returns 2 if invalid characters used in the names
| Returns 3 if nickname already in use in this system
"""
# Getting the formatted name
formatted_system_name = getting_name_checker(relevent_system, self.get_list_systems())
# Checks for error messages
if formatted_system_name in [1,2]:
return formatted_system_name # Name not already in use or invalid chars
# Getting the formatted name
formatted_gen_name = getting_name_checker(name
, list(self.get_list_generators(formatted_system_name)))
# Checks for error messages
if formatted_gen_name in [1,2]:
return formatted_gen_name # Name not already in use or invalid chars
if description is not None:
self.get_system(formatted_system_name).get_generators()[formatted_gen_name].edit_description(description)
if nickname is not None:
formatted_nickname = adding_name_checker(nickname, [], True)
if formatted_nickname in [self.get_generator(formatted_system_name, gen_name).get_nickname() for gen_name in self.get_list_generators(formatted_system_name)]:
return 3
self.get_generator(formatted_system_name, formatted_gen_name).edit_nickname(nickname)
if return_value is not None:
self.get_system(formatted_system_name).get_generators()[formatted_gen_name].edit_return_value(return_value)
return 0
[docs]
def get_generator(self, relevent_system : str, name : str):
"""Gets the requested generator from the systems generators dictionary.
Parameters
----------
relevent_system : str
Name of system the generator belongs to.
name : str
Name of generator.
Returns
-------
generator : Generator
| Returns the Generator object matching the provided names
| Returns None if no Match
"""
# Getting the formatted name
formatted_system_name = getting_name_checker(relevent_system, self.get_list_systems())
# Checks for error messages
if formatted_system_name in [1,2]:
return None # Name not already in use or invalid chars
# Getting the formatted name
formatted_gen_name = getting_name_checker(name
, list(self.get_list_generators(formatted_system_name)))
# Checks for error messages
if formatted_gen_name in [1,2]:
return None # Name not already in use or invalid chars
return self.get_system(formatted_system_name).get_generators()[formatted_gen_name]
[docs]
def remove_generator(self, relevent_system : str, name : str):
"""Removes a set generator from the systems generators dictionary.
Parameters
----------
relevent_system : str
Name of system the generator belongs to.
name : str
Name of generator.
Returns
-------
error term : int
| Returns 0 on success
| Returns 1 if names don't already exists
| Returns 2 if invalid characters used in the names
"""
# Getting the formatted name
formatted_system_name = getting_name_checker(relevent_system, self.get_list_systems())
# Checks for error messages
if formatted_system_name in [1,2]:
return formatted_system_name # Name not already in use or invalid chars
# Getting the formatted name
formatted_gen_name = getting_name_checker(name
, list(self.get_list_generators(formatted_system_name)))
# Checks for error messages
if formatted_gen_name in [1,2]:
return formatted_gen_name # Name not already in use or invalid chars
del self.get_system(formatted_system_name).get_generators()[formatted_gen_name]
return 0
[docs]
def edit_model_details(self, name : str = None, description : str = None
, gridworld : bool = None, sim_length : int = None
, sim_seed : int = None):
"""Stores the provided data about the model.
Parameters
----------
name : str, Optional
Changes the model's name.
description : str, Optional
Changes the description of the model.
initial_parameters : dict, Optional
Store the initial parameters the user wants for the model.
sim_length : int, Optional
Changes the number of iterations of the model will be executed.
sim_seed : int, Optional
Changes the seed for the random generator for the model.
Returns
-------
error term : int
| Returns 0 on success
| Returns 1 if same name as before
| Returns 2 on invalid name
"""
if name is not None:
formatted_name = adding_name_checker(name, [self.get_model_details().get_name()])
if formatted_name in [1,2]:
return formatted_name
self.model_details.edit_name(formatted_name)
if description is not None:
self.model_details.edit_description(description)
if gridworld is True and not self.model_details.get_gridworld(): # Ensures isn't already True
self.model_details.edit_gridworld(gridworld)
self.model_details.add_initial_parameter("WIDTH", "int")
self.model_details.add_initial_parameter("HEIGHT", "int")
self.add_import("ECAgent.Environments", "Gridworld"
, "from", ["GridWorld", "PositionComponent", "discrete_grid_pos_to_id"])
if gridworld is False and self.model_details.get_gridworld(): # Ensures isn't already False
self.model_details.edit_gridworld(gridworld)
self.model_details.remove_initial_parameter("WIDTH")
self.model_details.remove_initial_parameter("HEIGHT")
del self.imports["ECAgent.Environments"]
if sim_length is not None:
self.model_details.edit_sim_length(sim_length)
if sim_seed is not None:
self.model_details.edit_sim_seed(sim_seed)
return 0
[docs]
def get_model_details(self):
"""Gets the requested system from the models systems dictionary.
Returns
-------
model_details : ModelDetails
| Returns an Model Details Class.
"""
return self.model_details
[docs]
def add_execute(self, parent_system : str, execute_function_body : str):
"""Stores an execute function into the respective parent system models system dictionary.
Parameters
----------
parent_system : str
The parent system's name.
execute_function_body : str
User provided execute function for the system.
Returns
-------
error term : int
| Returns 0 on success
| Returns 1 if name doesn't already exists
"""
# Getting the formatted name
formatted_system_name = getting_name_checker(parent_system, self.get_list_systems())
# Checks for error messages
if formatted_system_name in [1,2]:
return formatted_system_name # Name not already in use or invalid chars
self.edit_system(formatted_system_name, execute_function_body = execute_function_body)
return 0
[docs]
def get_execute(self, parent_system : str):
"""Gets the execute function from the respective parent system models system dictionary.
Parameters
----------
parent_system : str
The parent system's name.
execute_function_body : str
User provided execute function for the system.
Returns
-------
execute : str
| Returns execute function body.
| Returns None if system name is invalid or doesn't match.
"""
# Getting the formatted name
formatted_system_name = getting_name_checker(parent_system, self.get_list_systems())
# Checks for error messages
if formatted_system_name in [1,2]:
return None # Name not already in use or invalid chars
return self.get_system(parent_system).get_execute_function_body()
[docs]
def add_agent_of_interest(self, name : str, comp_of_interest : list[str] = None
, count : bool = False, position : bool = False
, individual : bool = False, mean_val : bool = False
, median_val : bool = False, total_val : bool = False
, max_val : bool = False, min_val : bool = False
, variance : bool = False, std_dev : bool = False):
"""Adds the agent of interest to the agents of interest dictionary.
Parameters
----------
name : str
The name of the agent of interest to be added.
comp_of_interest : list[str], Optional
The list of desired components for the agent of interest.
count : bool, Optional
Specifies if the number of agents must be collected.
position : bool, Optional
Specifies is the user wants to keep the postions of the agent ids
individual : bool, Optional
Specifies if component values must be collected per individual agent id.
mean_val : bool, Optional
Specifies if the mean of the component values for the agent type must be collected.
median_val : bool, Optional
Specifies if the median of the component values for the agent type must be collected.
total_val : bool, Optional
Specifies if the total of the component values for the agent type must be collected.
max_val : bool, Optional
Specifies if the maximum of the component values for the agent type must be collected.
min_val : bool, Optional
Specifies if the minimum of the component values for the agent type must be collected.
variance : bool, Optional
Specifies if the variance of the component values for the agent type must be collected.
std_dev : bool, Optional
Specifies if the standard deviation of the component values for the agent type must be collected.
Returns
-------
error term : int
| Returns 0 on success
| Returns 1 if name error
| Returns 2 if invalid characters used in the name
| Returns 3 if no components of interest provided
"""
# Tries to format the name to correct format
formatted_name = adding_name_checker(name, list(self.agents_of_interest))
# Checks for error messages
if formatted_name == 1:
return 1 # Name already in use
if formatted_name == 2:
return 2 # Name uses invalid chars
# Checks that agent name exists
if formatted_name not in self.agents:
return 1 # Name not already in use
if comp_of_interest is None:
return 3
formatted_comp_names = [getting_name_checker(comp_name, self.get_agent(formatted_name).get_given_components()) for comp_name in comp_of_interest]
if 1 in formatted_comp_names:
return 1 # Name not already in use
self.agents_of_interest[formatted_name] = AgentOfInterest(formatted_comp_names, count
, position, individual, mean_val, median_val
, total_val, max_val, min_val, variance
, std_dev)
return 0
[docs]
def get_list_valid_agents(self, agent_of_focus : str, comp_of_focus : str = None
, count : bool = False):
"""Gets a list of all valid compatible agents_of_interest in the model, needed for visualisation.
Parameters
----------
agent_of_focus : str
The main agent which is being visualised
comp_of_focus : str, Optional
The main component which is being visualised, Defaults to None for if count True
count : bool, Optional
Defaults to False, relates to if just checking for count as no comp_of_focus needed
Returns
-------
Agents_of_Interest : list[str]
| Returns a list of compatible agents of interest
| Returns an empty list if invalid names provided or no valid agents of interest
"""
# Getting the formatted name
formatted_agent_of_focus = getting_name_checker(agent_of_focus, list(self.agents_of_interest))
# Checks for error messages
if formatted_agent_of_focus in [1,2]:
return [] # Name not already in use or invalid chars
if count:
valid_agents_of_interest = []
for agent_name in self.get_list_agents_of_interest():
if self.get_agent_of_interest(agent_name).get_count() and agent_name != formatted_agent_of_focus:
valid_agents_of_interest.append(agent_name)
return valid_agents_of_interest
# Getting the formatted name
formatted_comp_of_focus = getting_name_checker(comp_of_focus
, list(self.get_agent_of_interest(formatted_agent_of_focus).get_comp_of_interest()))
# Checks for error messages
if formatted_comp_of_focus in [1,2]:
return [] # Name not already in use or invalid chars
valid_agents_of_interest = []
for agent_name in self.get_list_agents_of_interest():
agent_given_components = self.get_agent_of_interest(agent_name).get_comp_of_interest()
if formatted_comp_of_focus in agent_given_components and agent_name != formatted_agent_of_focus:
valid_agents_of_interest.append(agent_name)
return valid_agents_of_interest
[docs]
def get_list_valid_plot_data_types(self, agents_of_focus : list[str]):
"""Gets a list of all valid plot_data_types needed for visualisation.
Parameters
----------
agents_of_focus : str
The agents being evaluated
Returns
-------
plot_data_types : list[str]
| Returns a list of compatible plot_data_types.
| Returns an empty list if invalid agent names or no possible plot_data_types.
"""
formatted_agent_names = [getting_name_checker(agent, list(self.agents_of_interest)) for agent in agents_of_focus]
if 1 in formatted_agent_names:
return [] # A name is not currently an agent
if len(formatted_agent_names) == 0:
return [] # No agents parsed
possible_data_types = ["count", "individual", "mean_val", "median_val", "total_val"
, "min_val", "max_val", "variance_val", "std_dev_val"]
available_data_types = possible_data_types.copy()
for agent_of_interest in formatted_agent_names:
temp_data_types = get_data_types(self.get_agent_of_interest(agent_of_interest))
for data_type in possible_data_types:
if data_type not in temp_data_types and data_type in available_data_types:
available_data_types.remove(data_type)
# If len() = 0, implies it's an invalid combo
# or the attribute is in common, but the wrong data was collected
return available_data_types
[docs]
def get_agent_of_interest(self, name : str):
"""Gets the requested agent of interest object from the agents of interest dictionary.
Parameters
----------
name : str
The agent of interest's name.
Returns
-------
agent_of_interest[name] : AgentOfInterest
| Returns the agent of interest object to return.
"""
# Tries to format the name to correct format
formatted_name = getting_name_checker(name, list(self.agents_of_interest))
# Checks for error messages
if formatted_name == 1:
return None # Name not already in use
return self.agents_of_interest[formatted_name]
[docs]
def remove_agent_of_interest(self, name : str):
"""Removes the provided data collection system from the data collection dictionary.
Parameters
----------
name : str
The name of the agent of interest to be removed.
Returns
-------
error term : int
| Returns 0 on success
| Returns 1 if name doesn't already exists
| Returns 2 if invalid characters used in the name
"""
# Tries to format the name to correct format
formatted_name = getting_name_checker(name, list(self.agents_of_interest))
# Checks for error messages
if formatted_name in [1,2]:
return formatted_name # Name not already in use
del self.agents_of_interest[formatted_name]
return 0
[docs]
def create_model(self, file_name : str, visualise_model = False):
"""Generates a python file(s) necessary for the model to run.
Parameters
----------
file_name : str
The file_name of the model to be written to.
visualise_model : bool
The specifies if the main file will be used for visualisation or not.
Returns
-------
error term : int
| Returns 0 on success
| Returns 1 if file generation fails
"""
if ".py" in file_name: # Makes sure there is no .py at the end of the filename
file_name = file_name.replace(".py", "")
try:
file_store = utils.WriteModelFile(file_name)
file_store.create_imports_section(self)
file_store.create_components_section(self)
file_store.create_agents_section(self)
file_store.create_systems_section(self)
file_store.create_data_collection_section(self)
file_store.create_model_section(self)
file_store.create_model_file()
file_store.create_main_file_section(self, visualise_model)
file_store.create_main_file()
return 0
except Exception as exception: # pragma: no cover
print(exception)
return 1
[docs]
def store_model(self):
""" Stores the current model as a json file which allows us to reload in an existing
project.
Returns
-------
error term : int
| Returns 0 on success
| Returns 1 if model storing fails
"""
try:
if not os.path.isdir("ModelAsJSONFiles"):
os.makedirs("ModelAsJSONFiles")
file_name = "ModelAsJSONFiles/" + self.get_model_details().get_name() + ".json"
with open(file_name, 'w', encoding='UTF-8') as file:
all_data = {"imports" : self.imports, "components" : self.components
, "agents" : self.agents, "systems" : self.systems
, "model_details" : self.model_details
, "agents_of_interest" : self.agents_of_interest}
encoded_data = dumps(all_data)
file.write(encoded_data)
return 0
except EOFError as exception: # pragma: no cover
print(exception)
return 1