|
| 1 | +import networkx as nx |
| 2 | +import numpy as np |
| 3 | + |
| 4 | +import mesa |
| 5 | +from mesa import Agent |
| 6 | +from mesa.examples.advanced.alliance_formation.agents import AllianceAgent |
| 7 | +from mesa.experimental.meta_agents.meta_agent import ( |
| 8 | + create_meta_agent, |
| 9 | + find_combinations, |
| 10 | +) |
| 11 | + |
| 12 | + |
| 13 | +class MultiLevelAllianceModel(mesa.Model): |
| 14 | + """ |
| 15 | + Model for simulating multi-level alliances among agents. |
| 16 | + """ |
| 17 | + |
| 18 | + def __init__(self, n=50, mean=0.5, std_dev=0.1, seed=42): |
| 19 | + """ |
| 20 | + Initialize the model. |
| 21 | +
|
| 22 | + Args: |
| 23 | + n (int): Number of agents. |
| 24 | + mean (float): Mean value for normal distribution. |
| 25 | + std_dev (float): Standard deviation for normal distribution. |
| 26 | + seed (int): Random seed. |
| 27 | + """ |
| 28 | + super().__init__(seed=seed) |
| 29 | + self.population = n |
| 30 | + self.network = nx.Graph() # Initialize the network |
| 31 | + self.datacollector = mesa.DataCollector(model_reporters={"Network": "network"}) |
| 32 | + |
| 33 | + # Create Agents |
| 34 | + power = self.rng.normal(mean, std_dev, n) |
| 35 | + power = np.clip(power, 0, 1) |
| 36 | + position = self.rng.normal(mean, std_dev, n) |
| 37 | + position = np.clip(position, 0, 1) |
| 38 | + AllianceAgent.create_agents(self, n, power, position) |
| 39 | + agent_ids = [ |
| 40 | + (agent.unique_id, {"size": 300, "level": 0}) for agent in self.agents |
| 41 | + ] |
| 42 | + self.network.add_nodes_from(agent_ids) |
| 43 | + |
| 44 | + def add_link(self, meta_agent, agents): |
| 45 | + """ |
| 46 | + Add links between a meta agent and its constituent agents in the network. |
| 47 | +
|
| 48 | + Args: |
| 49 | + meta_agent (MetaAgent): The meta agent. |
| 50 | + agents (list): List of agents. |
| 51 | + """ |
| 52 | + for agent in agents: |
| 53 | + self.network.add_edge(meta_agent.unique_id, agent.unique_id) |
| 54 | + |
| 55 | + def calculate_shapley_value(self, agents): |
| 56 | + """ |
| 57 | + Calculate the Shapley value of the two agents. |
| 58 | +
|
| 59 | + Args: |
| 60 | + agents (list): List of agents. |
| 61 | +
|
| 62 | + Returns: |
| 63 | + tuple: Potential utility, new position, and level. |
| 64 | + """ |
| 65 | + positions = agents.get("position") |
| 66 | + new_position = 1 - (max(positions) - min(positions)) |
| 67 | + potential_utility = agents.agg("power", sum) * 1.2 * new_position |
| 68 | + |
| 69 | + value_0 = 0.5 * agents[0].power + 0.5 * (potential_utility - agents[1].power) |
| 70 | + value_1 = 0.5 * agents[1].power + 0.5 * (potential_utility - agents[0].power) |
| 71 | + |
| 72 | + if value_0 > agents[0].power and value_1 > agents[1].power: |
| 73 | + if agents[0].level > agents[1].level: |
| 74 | + level = agents[0].level |
| 75 | + elif agents[0].level == agents[1].level: |
| 76 | + level = agents[0].level + 1 |
| 77 | + else: |
| 78 | + level = agents[1].level |
| 79 | + |
| 80 | + return potential_utility, new_position, level |
| 81 | + |
| 82 | + def only_best_combination(self, combinations): |
| 83 | + """ |
| 84 | + Filter to keep only the best combination for each agent. |
| 85 | +
|
| 86 | + Args: |
| 87 | + combinations (list): List of combinations. |
| 88 | +
|
| 89 | + Returns: |
| 90 | + dict: Unique combinations. |
| 91 | + """ |
| 92 | + best = {} |
| 93 | + # Determine best option for EACH agent |
| 94 | + for group, value in combinations: |
| 95 | + agent_ids = sorted(group.get("unique_id")) # by default is bilateral |
| 96 | + # Deal with all possibilities |
| 97 | + if ( |
| 98 | + agent_ids[0] not in best and agent_ids[1] not in best |
| 99 | + ): # if neither in add both |
| 100 | + best[agent_ids[0]] = [group, value, agent_ids] |
| 101 | + best[agent_ids[1]] = [group, value, agent_ids] |
| 102 | + elif ( |
| 103 | + agent_ids[0] in best and agent_ids[1] in best |
| 104 | + ): # if both in, see if both would be trading up |
| 105 | + if ( |
| 106 | + value[0] > best[agent_ids[0]][1][0] |
| 107 | + and value[0] > best[agent_ids[1]][1][0] |
| 108 | + ): |
| 109 | + # Remove the old alliances |
| 110 | + del best[best[agent_ids[0]][2][1]] |
| 111 | + del best[best[agent_ids[1]][2][0]] |
| 112 | + # Add the new alliance |
| 113 | + best[agent_ids[0]] = [group, value, agent_ids] |
| 114 | + best[agent_ids[1]] = [group, value, agent_ids] |
| 115 | + elif ( |
| 116 | + agent_ids[0] in best |
| 117 | + ): # if only agent_ids[0] in, see if it would be trading up |
| 118 | + if value[0] > best[agent_ids[0]][1][0]: |
| 119 | + # Remove the old alliance for agent_ids[0] |
| 120 | + del best[best[agent_ids[0]][2][1]] |
| 121 | + # Add the new alliance |
| 122 | + best[agent_ids[0]] = [group, value, agent_ids] |
| 123 | + best[agent_ids[1]] = [group, value, agent_ids] |
| 124 | + elif ( |
| 125 | + agent_ids[1] in best |
| 126 | + ): # if only agent_ids[1] in, see if it would be trading up |
| 127 | + if value[0] > best[agent_ids[1]][1][0]: |
| 128 | + # Remove the old alliance for agent_ids[1] |
| 129 | + del best[best[agent_ids[1]][2][0]] |
| 130 | + # Add the new alliance |
| 131 | + best[agent_ids[0]] = [group, value, agent_ids] |
| 132 | + best[agent_ids[1]] = [group, value, agent_ids] |
| 133 | + |
| 134 | + # Create a unique dictionary of the best combinations |
| 135 | + unique_combinations = {} |
| 136 | + for group, value, agents_nums in best.values(): |
| 137 | + unique_combinations[tuple(agents_nums)] = [group, value] |
| 138 | + |
| 139 | + return unique_combinations.values() |
| 140 | + |
| 141 | + def step(self): |
| 142 | + """ |
| 143 | + Execute one step of the model. |
| 144 | + """ |
| 145 | + # Get all other agents of the same type |
| 146 | + agent_types = list(self.agents_by_type.keys()) |
| 147 | + |
| 148 | + for agent_type in agent_types: |
| 149 | + similar_agents = self.agents_by_type[agent_type] |
| 150 | + |
| 151 | + # Find the best combinations using find_combinations |
| 152 | + if ( |
| 153 | + len(similar_agents) > 1 |
| 154 | + ): # only form alliances if there are more than 1 agent |
| 155 | + combinations = find_combinations( |
| 156 | + self, |
| 157 | + similar_agents, |
| 158 | + size=2, |
| 159 | + evaluation_func=self.calculate_shapley_value, |
| 160 | + filter_func=self.only_best_combination, |
| 161 | + ) |
| 162 | + |
| 163 | + for alliance, attributes in combinations: |
| 164 | + class_name = f"MetaAgentLevel{attributes[2]}" |
| 165 | + meta = create_meta_agent( |
| 166 | + self, |
| 167 | + class_name, |
| 168 | + alliance, |
| 169 | + Agent, |
| 170 | + meta_attributes={ |
| 171 | + "level": attributes[2], |
| 172 | + "power": attributes[0], |
| 173 | + "position": attributes[1], |
| 174 | + }, |
| 175 | + ) |
| 176 | + |
| 177 | + # Update the network if a new meta agent instance created |
| 178 | + if meta: |
| 179 | + self.network.add_node( |
| 180 | + meta.unique_id, |
| 181 | + size=(meta.level + 1) * 300, |
| 182 | + level=meta.level, |
| 183 | + ) |
| 184 | + self.add_link(meta, meta.agents) |
0 commit comments