# Alliance Formation Model (Meta-Agent Example) ## Summary This model demonstrates Mesa's meta agent capability. **Overview of meta agent:** Complex systems often have multiple levels of components. A city is not a single entity, but it is made of districts,neighborhoods, buildings, and people. A forest comprises an ecosystem of trees, plants, animals, and microorganisms. An organization is not one entity, but is made of departments, sub-departments, and people. A person is not a single entity, but it is made of micro biomes, organs and cells. This reality is the motivation for meta-agents. It allows users to represent these multiple levels, where each level can have agents with sub-agents. This model demonstrates Mesa's ability to dynamically create new classes of agents that are composed of existing agents. These meta-agents inherits functions and attributes from their sub-agents and users can specify new functionality or attributes they want the meta agent to have. For example, if a user is doing a factory simulation with autonomous systems, each major component of that system can be a sub-agent of the overall robot agent. Or, if someone is doing a simulation of an organization, individuals can be part of different organizational units that are working for some purpose. To provide a simple demonstration of this capability is an alliance formation model. In this simulation n agents are created, who have two attributes (1) power and (2) preference. Each attribute is a number between 0 and 1 over a gaussian distribution. Agents then randomly select other agents and use the [bilateral shapley value](https://en.wikipedia.org/wiki/Shapley_value) to determine if they should form an alliance. If the expected utility support an alliances, the agent creates a meta-agent. Subsequent steps may add agents to the meta-agent, create new instances of similar hierarchy, or create a new hierarchy level where meta-agents form an alliance of meta-agents. In this visualization of this model a new meta-agent hierarchy will be a larger node and a new color. In MetaAgents current configuration, agents being part of multiple meta-agents is not supported. If you would like to see an example of explicit meta-agent formation see the [warehouse model in the Mesa example's repository](https://github.com/projectmesa/mesa-examples/tree/main/examples/warehouse) ## Installation This model requires Mesa's recommended install and scipy ``` $ pip install mesa[rec] ``` ## How to Run To run the model interactively, in this directory, run the following command ``` $ solara run app.py ``` ## Files - `model.py`: Contains creation of agents, the network and management of agent execution. - `agents.py`: Contains logic for forming alliances and creation of new agents - `app.py`: Contains the code for the interactive Solara visualization. ## Further Reading The full tutorial describing how the model is built can be found at: https://mesa.readthedocs.io/en/latest/tutorials/intro_tutorial.html An example of the bilateral shapley value in another model: [Techno-Social Energy Infrastructure Siting: Sustainable Energy Modeling Programming (SEMPro)](https://www.jasss.org/16/3/6.html) ## Agents ```python import mesa class AllianceAgent(mesa.Agent): """ Agent has three attributes power (float), position (float) and level (int) """ def __init__(self, model, power, position, level=0): super().__init__(model) self.power = power self.position = position self.level = level """ For this demo model agent only need attributes. More complex models could have functions that define agent behavior. """ ``` ## Model ```python import networkx as nx import numpy as np import mesa from mesa import Agent from mesa.examples.advanced.alliance_formation.agents import AllianceAgent from mesa.experimental.meta_agents.meta_agent import ( create_meta_agent, find_combinations, ) class MultiLevelAllianceModel(mesa.Model): """ Model for simulating multi-level alliances among agents. """ def __init__(self, n=50, mean=0.5, std_dev=0.1, seed=42): """ Initialize the model. Args: n (int): Number of agents. mean (float): Mean value for normal distribution. std_dev (float): Standard deviation for normal distribution. seed (int): Random seed. """ super().__init__(seed=seed) self.population = n self.network = nx.Graph() # Initialize the network self.datacollector = mesa.DataCollector(model_reporters={"Network": "network"}) # Create Agents power = self.rng.normal(mean, std_dev, n) power = np.clip(power, 0, 1) position = self.rng.normal(mean, std_dev, n) position = np.clip(position, 0, 1) AllianceAgent.create_agents(self, n, power, position) agent_ids = [ (agent.unique_id, {"size": 300, "level": 0}) for agent in self.agents ] self.network.add_nodes_from(agent_ids) def add_link(self, meta_agent, agents): """ Add links between a meta agent and its constituent agents in the network. Args: meta_agent (MetaAgent): The meta agent. agents (list): List of agents. """ for agent in agents: self.network.add_edge(meta_agent.unique_id, agent.unique_id) def calculate_shapley_value(self, agents): """ Calculate the Shapley value of the two agents. Args: agents (list): List of agents. Returns: tuple: Potential utility, new position, and level. """ positions = agents.get("position") new_position = 1 - (max(positions) - min(positions)) potential_utility = agents.agg("power", sum) * 1.2 * new_position value_0 = 0.5 * agents[0].power + 0.5 * (potential_utility - agents[1].power) value_1 = 0.5 * agents[1].power + 0.5 * (potential_utility - agents[0].power) if value_0 > agents[0].power and value_1 > agents[1].power: if agents[0].level > agents[1].level: level = agents[0].level elif agents[0].level == agents[1].level: level = agents[0].level + 1 else: level = agents[1].level return potential_utility, new_position, level def only_best_combination(self, combinations): """ Filter to keep only the best combination for each agent. Args: combinations (list): List of combinations. Returns: dict: Unique combinations. """ best = {} # Determine best option for EACH agent for group, value in combinations: agent_ids = sorted(group.get("unique_id")) # by default is bilateral # Deal with all possibilities if ( agent_ids[0] not in best and agent_ids[1] not in best ): # if neither in add both best[agent_ids[0]] = [group, value, agent_ids] best[agent_ids[1]] = [group, value, agent_ids] elif ( agent_ids[0] in best and agent_ids[1] in best ): # if both in, see if both would be trading up if ( value[0] > best[agent_ids[0]][1][0] and value[0] > best[agent_ids[1]][1][0] ): # Remove the old alliances del best[best[agent_ids[0]][2][1]] del best[best[agent_ids[1]][2][0]] # Add the new alliance best[agent_ids[0]] = [group, value, agent_ids] best[agent_ids[1]] = [group, value, agent_ids] elif ( agent_ids[0] in best ): # if only agent_ids[0] in, see if it would be trading up if value[0] > best[agent_ids[0]][1][0]: # Remove the old alliance for agent_ids[0] del best[best[agent_ids[0]][2][1]] # Add the new alliance best[agent_ids[0]] = [group, value, agent_ids] best[agent_ids[1]] = [group, value, agent_ids] elif ( agent_ids[1] in best ): # if only agent_ids[1] in, see if it would be trading up if value[0] > best[agent_ids[1]][1][0]: # Remove the old alliance for agent_ids[1] del best[best[agent_ids[1]][2][0]] # Add the new alliance best[agent_ids[0]] = [group, value, agent_ids] best[agent_ids[1]] = [group, value, agent_ids] # Create a unique dictionary of the best combinations unique_combinations = {} for group, value, agents_nums in best.values(): unique_combinations[tuple(agents_nums)] = [group, value] return unique_combinations.values() def step(self): """ Execute one step of the model. """ # Get all other agents of the same type agent_types = list(self.agents_by_type.keys()) for agent_type in agent_types: similar_agents = self.agents_by_type[agent_type] # Find the best combinations using find_combinations if ( len(similar_agents) > 1 ): # only form alliances if there are more than 1 agent combinations = find_combinations( self, similar_agents, size=2, evaluation_func=self.calculate_shapley_value, filter_func=self.only_best_combination, ) for alliance, attributes in combinations: class_name = f"MetaAgentLevel{attributes[2]}" meta = create_meta_agent( self, class_name, alliance, Agent, meta_attributes={ "level": attributes[2], "power": attributes[0], "position": attributes[1], }, ) # Update the network if a new meta agent instance created if meta: self.network.add_node( meta.unique_id, size=(meta.level + 1) * 300, level=meta.level, ) self.add_link(meta, meta.agents) ``` ## App ```python import matplotlib.pyplot as plt import networkx as nx import solara from matplotlib.figure import Figure from mesa.examples.advanced.alliance_formation.model import MultiLevelAllianceModel from mesa.visualization import SolaraViz from mesa.visualization.utils import update_counter model_params = { "seed": { "type": "InputText", "value": 42, "label": "Random Seed", }, "n": { "type": "SliderInt", "value": 50, "label": "Number of agents:", "min": 10, "max": 100, "step": 1, }, } # Create visualization elements. The visualization elements are solara components # that receive the model instance as a "prop" and display it in a certain way. # Under the hood these are just classes that receive the model instance. # You can also author your own visualization elements, which can also be functions # that receive the model instance and return a valid solara component. @solara.component def plot_network(model): update_counter.get() g = model.network pos = nx.fruchterman_reingold_layout(g) fig = Figure() ax = fig.subplots() labels = {agent.unique_id: agent.unique_id for agent in model.agents} node_sizes = [g.nodes[node]["size"] for node in g.nodes] node_colors = [g.nodes[node]["size"] for node in g.nodes()] nx.draw( g, pos, node_size=node_sizes, node_color=node_colors, cmap=plt.cm.coolwarm, labels=labels, ax=ax, ) solara.FigureMatplotlib(fig) # Create initial model instance model = MultiLevelAllianceModel(50) # Create the SolaraViz page. This will automatically create a server and display the # visualization elements in a web browser. # Display it using the following command in the example directory: # solara run app.py # It will automatically update and display any changes made to this file page = SolaraViz( model, components=[plot_network], model_params=model_params, name="Alliance Formation Model", ) page # noqa ```