diff --git a/examples/preferencial_attachment/README.md b/examples/preferencial_attachment/README.md new file mode 100644 index 00000000..a46717e3 --- /dev/null +++ b/examples/preferencial_attachment/README.md @@ -0,0 +1,47 @@ +# Preferential Attachment Network + +This model simulates the generation of **scale-free networks** using **preferential attachment**, inspired by the NetLogo [Preferential Attachment model](http://ccl.northwestern.edu/netlogo/models/PreferentialAttachment). It demonstrates how "hubs" with many connections emerge naturally when new nodes prefer to connect to already well-connected nodes. + +## Summary + +In this simulation, new nodes are added to a growing network one by one. Each new node connects to an existing node, where the probability of connection is **proportional to the degree** (number of connections) of the existing nodes. This leads to the formation of **Barabási-Albert scale-free networks**, where a few nodes (hubs) accumulate many connections, while most nodes have very few. + +Such networks are common in real-world systems such as: +- The World Wide Web (webpages linking to other pages), +- Social networks (users connecting with popular accounts), +- Citation networks (new papers citing widely cited publications). + +## Installation + +Ensure that you have installed the latest version of **Mesa**. + + +## Usage + +To run the simulation: + +```bash +solara run app.py +``` + +## Model Details + +### Agents + +- **NodeAgent**: Represents a node in the network. Each agent keeps track of its degree and updates its connections as the network grows. New nodes prefer connecting to higher-degree nodes, simulating the preferential attachment process. + +### Environment + +- **Network**: The model uses Mesa's `Network` to maintain the graph structure, initialized with the fixed no. of agents. Each step connects a node to the existing network, and the visualization updates to reflect the current state of the network. + + +### Agent Behaviors + +- **Preferential Connection**: When a new node is added, it connects to one existing node, with a probability weighted by the existing node’s degree. +- **Growth Step**: Each step corresponds to one node being added to the network. +- **Degree Monitoring**: A line plot is used to track the nodes with degree one. + +## References + +- Wilensky, U. (2005). *NetLogo Preferential Attachment model*. Center for Connected Learning and Computer-Based Modeling, Northwestern University, Evanston, IL. Available at: [NetLogo Preferential Attachment](http://ccl.northwestern.edu/netlogo/models/PreferentialAttachment) +- Barabási, A.-L., & Albert, R. (1999). *Emergence of scaling in random networks*. Science, 286(5439), 509-512. \ No newline at end of file diff --git a/examples/preferencial_attachment/app.py b/examples/preferencial_attachment/app.py new file mode 100644 index 00000000..548766c9 --- /dev/null +++ b/examples/preferencial_attachment/app.py @@ -0,0 +1,46 @@ +from mesa.visualization import ( + SolaraViz, + make_plot_component, + make_space_component, +) +from preferencial_attachment.model import AgentNetwork + + +def node_portrayal(agent): + return {"color": "blue", "size": 30} + + +model_params = { + "seed": { + "type": "InputText", + "value": 42, + "label": "Random Seed", + }, + "num": { + "type": "SliderInt", + "value": 30, + "label": "No. of agents", + "min": 10, + "max": 100, + "step": 1, + }, +} + + +def post_process_lineplot(ax): + ax.set_ylim(ymin=0) + ax.set_xlim(xmin=0) + ax.set_ylabel("Nodes with Degree 1") + ax.set_xlabel("Steps") + + +SpacePlot = make_space_component(node_portrayal) +StatePlot = make_plot_component(measure="Degree", post_process=post_process_lineplot) +model = AgentNetwork() + +page = SolaraViz( + model, + components=[SpacePlot, StatePlot], + model_params=model_params, + name="Agent Network", +) diff --git a/examples/preferencial_attachment/preferencial_attachment/__init__.py b/examples/preferencial_attachment/preferencial_attachment/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/preferencial_attachment/preferencial_attachment/agents.py b/examples/preferencial_attachment/preferencial_attachment/agents.py new file mode 100644 index 00000000..de33de2b --- /dev/null +++ b/examples/preferencial_attachment/preferencial_attachment/agents.py @@ -0,0 +1,7 @@ +from mesa.experimental.cell_space import FixedAgent + + +class NodeAgent(FixedAgent): + def __init__(self, model, cell): + super().__init__(model) + self.cell = cell diff --git a/examples/preferencial_attachment/preferencial_attachment/model.py b/examples/preferencial_attachment/preferencial_attachment/model.py new file mode 100644 index 00000000..17d43870 --- /dev/null +++ b/examples/preferencial_attachment/preferencial_attachment/model.py @@ -0,0 +1,60 @@ +import networkx as nx +import numpy as np +from mesa import DataCollector, Model +from mesa.experimental.cell_space import Network + +from .agents import NodeAgent + + +def calculate_nodes_with_degree_1(model): + _, degree = zip(*model.graph.degree()) + return sum(1 for deg in degree if deg == 1) + + +class AgentNetwork(Model): + def __init__(self, num=30, seed=42): + """Initialize the model. + + Args: + num: Number of Node Agents, + seed : Random seed for reproducibility. + """ + super().__init__(seed=seed) + self.num = num + self.random = seed + self.curr_node = 1 + self.graph = nx.Graph() + + # Adding nodes to the graph + for i in range(self.num): + self.graph.add_node(i) + + self.graph.add_edge(0, 1) + + self.datacollector = DataCollector( + { + "Degree": calculate_nodes_with_degree_1, + } + ) + + self.grid = Network(self.graph, capacity=1, random=self.random) + NodeAgent.create_agents(model=self, n=self.num, cell=list(self.grid.all_cells)) + + def step(self): + self.curr_node += 1 + + nodes, degree = zip(*self.graph.degree()) + total_degree = sum(degree) + + # probabilities of connecting to an existing node + probabilities = [d / total_degree for d in degree] + + # choose an existing node based on the computed probabilities + chosen_node = np.random.choice(nodes, p=probabilities) + + # add the new node and connect it to the choosen node + self.graph.add_edge(chosen_node, self.curr_node) + + self.datacollector.collect(self) + if self.steps >= self.num - 2: + self.running = False