MaxNoichl commited on
Commit
b3bfda0
·
verified ·
1 Parent(s): b5e14e0

Minor fixes

Browse files
Files changed (1) hide show
  1. app.py +395 -591
app.py CHANGED
@@ -1,8 +1,6 @@
1
  # -*- coding: utf-8 -*-
2
  """revolutions_exploration.ipynb
3
-
4
  Automatically generated by Colaboratory.
5
-
6
  Original file is located at
7
  https://colab.research.google.com/drive/1omNn2hrbDL_s1qwCOr7ViaIjrRW61YDt
8
  """
@@ -12,14 +10,12 @@ Original file is located at
12
  # !pip install gradio
13
  # # !pip install gradio==3.50.2
14
 
15
-
16
-
17
  # Commented out IPython magic to ensure Python compatibility.
18
  # %%capture
19
- #
20
  # !pip install cmocean
21
  # !pip install mesa
22
- #
23
  # !pip install opinionated
24
 
25
  import random
@@ -52,177 +48,113 @@ import opinionated
52
  import matplotlib.pyplot as plt
53
 
54
  plt.style.use("opinionated_rc")
55
- #from opinionated.core import download_googlefont
56
- #download_googlefont('Quicksand', add_to_cache=True)
57
- #plt.rc('font', family='Quicksand')
58
 
59
  experiences = {
60
- 'dissident_experiences': [1,0,0],
61
- 'supporter_experiences': [1,1,1],
62
- }
63
 
64
  def apply_half_life_decay(data_list, half_life, decay_factors=None):
65
  steps = len(data_list)
66
-
67
- # Check if decay_factors are provided and are of the correct length
68
  if decay_factors is None or len(decay_factors) < steps:
69
  decay_factors = [0.5 ** (i / half_life) for i in range(steps)]
70
  decayed_list = [data_list[i] * decay_factors[steps - 1 - i] for i in range(steps)]
71
-
72
-
73
  return decayed_list
74
 
75
-
76
-
77
- half_life=20
78
  decay_factors = [0.5 ** (i / half_life) for i in range(200)]
79
 
80
- def get_beta_mean_from_experience_dict(experiences, half_life=20,decay_factors=None): #note: precomputed decay supersedes halflife!
81
- eta = 1e-10
82
- return beta.mean(sum(apply_half_life_decay(experiences['dissident_experiences'], half_life,decay_factors))+eta,
83
- sum(apply_half_life_decay(experiences['supporter_experiences'], half_life,decay_factors))+eta)
84
-
85
-
86
- def get_beta_sample_from_experience_dict(experiences, half_life=20,decay_factors=None):
87
- eta = 1e-10
88
-
89
- # print(sum(apply_half_life_decay(experiences['dissident_experiences'], half_life)))
90
- # print(sum(apply_half_life_decay(experiences['supporter_experiences'], half_life)))
91
- return beta.rvs(sum(apply_half_life_decay(experiences['dissident_experiences'], half_life,decay_factors))+eta,
92
- sum(apply_half_life_decay(experiences['supporter_experiences'], half_life,decay_factors))+eta, size=1)[0]
93
 
 
 
 
 
 
 
 
94
 
95
- print(get_beta_mean_from_experience_dict(experiences,half_life,decay_factors))
96
- print(get_beta_sample_from_experience_dict(experiences,half_life))
97
 
98
  #@title Load network functionality
99
 
100
  def generate_community_points(num_communities, total_nodes, powerlaw_exponent=2.0, sigma=0.05, plot=False):
101
  """
102
- This function generates points in 2D space, where points are grouped into communities.
103
- Each community is represented by a Gaussian distribution.
104
-
105
- Args:
106
- num_communities (int): Number of communities (gaussian distributions).
107
- total_nodes (int): Total number of points to be generated.
108
- powerlaw_exponent (float): The power law exponent for the powerlaw sequence.
109
- sigma (float): The standard deviation for the gaussian distributions.
110
- plot (bool): If True, the function plots the generated points.
111
-
112
- Returns:
113
- numpy.ndarray: An array of generated points.
114
  """
115
-
116
- # Sample from a powerlaw distribution
117
  sequence = nx.utils.powerlaw_sequence(num_communities, powerlaw_exponent)
118
-
119
- # Normalize sequence to represent probabilities
120
  probabilities = sequence / np.sum(sequence)
121
 
122
- # Assign nodes to communities based on probabilities
123
  community_assignments = np.random.choice(num_communities, size=total_nodes, p=probabilities)
124
-
125
- # Calculate community_sizes from community_assignments
126
  community_sizes = np.bincount(community_assignments)
127
- # Ensure community_sizes has length equal to num_communities
128
  if len(community_sizes) < num_communities:
129
  community_sizes = np.pad(community_sizes, (0, num_communities - len(community_sizes)), 'constant')
130
 
131
  points = []
132
  community_centers = []
133
 
134
- # For each community
135
  for i in range(num_communities):
136
- # Create a random center for this community
137
  center = np.random.rand(2)
138
  community_centers.append(center)
139
-
140
- # Sample from Gaussian distributions with the center and sigma
141
  community_points = np.random.normal(center, sigma, (community_sizes[i], 2))
142
-
143
  points.append(community_points)
144
 
145
  points = np.concatenate(points)
146
 
147
- # Optional plotting
148
  if plot:
149
- plt.figure(figsize=(8,8))
150
  plt.scatter(points[:, 0], points[:, 1], alpha=0.5)
151
- # for center in community_centers:
152
  sns.kdeplot(x=points[:, 0], y=points[:, 1], levels=5, color="k", linewidths=1)
153
- # plt.xlim(0, 1)
154
- # plt.ylim(0, 1)
155
  plt.show()
156
 
157
  return points
158
 
159
-
160
  def graph_from_coordinates(coords, radius):
161
  """
162
- This function creates a random geometric graph from an array of coordinates.
163
-
164
- Args:
165
- coords (numpy.ndarray): An array of coordinates.
166
- radius (float): A radius of circles or spheres.
167
-
168
- Returns:
169
- networkx.Graph: The created graph.
170
  """
171
-
172
- # Create a KDTree for efficient query
173
  kdtree = sp.spatial.cKDTree(coords)
174
  edge_indexes = kdtree.query_pairs(radius)
175
  g = nx.Graph()
176
  g.add_nodes_from(list(range(len(coords))))
177
  g.add_edges_from(edge_indexes)
178
-
179
  return g
180
 
181
-
182
  def plot_graph(graph, positions):
183
- """
184
- This function plots a graph with the given positions.
185
-
186
- Args:
187
- graph (networkx.Graph): The graph to be plotted.
188
- positions (dict): A dictionary of positions for the nodes.
189
- """
190
-
191
- plt.figure(figsize=(8,8))
192
  pos_dict = {i: positions[i] for i in range(len(positions))}
193
  nx.draw_networkx_nodes(graph, pos_dict, node_size=30, node_color="#1a2340", alpha=0.7)
194
  nx.draw_networkx_edges(graph, pos_dict, edge_color="grey", width=1, alpha=1)
195
  plt.show()
196
 
197
-
198
-
199
  def ensure_neighbors(graph):
200
  """
201
- Ensure that all nodes in a NetworkX graph have at least one neighbor.
202
-
203
- Parameters:
204
- graph (networkx.Graph): The NetworkX graph to check.
205
-
206
- Returns:
207
- networkx.Graph: The updated NetworkX graph where all nodes have at least one neighbor.
208
  """
209
  nodes = list(graph.nodes())
210
  for node in nodes:
211
  if len(list(graph.neighbors(node))) == 0:
212
- # The node has no neighbors, so select another node to connect it with
213
  other_node = random.choice(nodes)
214
- while other_node == node: # Make sure we don't connect the node to itself
215
  other_node = random.choice(nodes)
216
  graph.add_edge(node, other_node)
217
  return graph
218
 
219
-
220
- def compute_homophily(G,attr_name='attr'):
221
  same_attribute_edges = sum(G.nodes[n1][attr_name] == G.nodes[n2][attr_name] for n1, n2 in G.edges())
222
  total_edges = G.number_of_edges()
223
  return same_attribute_edges / total_edges if total_edges > 0 else 0
224
 
225
- def assign_initial_attributes(G, ratio,attr_name='attr'):
226
  nodes = list(G.nodes)
227
  random.shuffle(nodes)
228
  attr_boundary = int(ratio * len(nodes))
@@ -230,13 +162,12 @@ def assign_initial_attributes(G, ratio,attr_name='attr'):
230
  G.nodes[node][attr_name] = 0 if i < attr_boundary else 1
231
  return G
232
 
233
- def distribute_attributes(G, target_homophily, seed=None, max_iter=10000, cooling_factor=0.9995,attr_name='attr'):
234
  random.seed(seed)
235
- current_homophily = compute_homophily(G,attr_name)
236
  temp = 1.0
237
 
238
  for i in range(max_iter):
239
- # pick two random nodes with different attributes and swap their attributes
240
  nodes = list(G.nodes)
241
  random.shuffle(nodes)
242
  for node1, node2 in zip(nodes[::2], nodes[1::2]):
@@ -244,35 +175,25 @@ def distribute_attributes(G, target_homophily, seed=None, max_iter=10000, coolin
244
  G.nodes[node1][attr_name], G.nodes[node2][attr_name] = G.nodes[node2][attr_name], G.nodes[node1][attr_name]
245
  break
246
 
247
- new_homophily = compute_homophily(G,attr_name)
248
  delta_homophily = new_homophily - current_homophily
249
  dir_factor = np.sign(target_homophily - current_homophily)
250
 
251
- # if the new homophily is closer to the target, or if the simulated annealing condition is met, accept the swap
252
  if abs(new_homophily - target_homophily) < abs(current_homophily - target_homophily) or \
253
  (delta_homophily / temp < 700 and random.random() < np.exp(dir_factor * delta_homophily / temp)):
254
  current_homophily = new_homophily
255
- else: # else, undo the swap
256
  G.nodes[node1][attr_name], G.nodes[node2][attr_name] = G.nodes[node2][attr_name], G.nodes[node1][attr_name]
257
 
258
- temp *= cooling_factor # cool down
259
 
260
  return G
261
 
262
-
263
  def reindex_graph_to_match_attributes(G1, G2, attr_name):
264
- # Get a sorted list of nodes in G1 based on the attribute
265
  G1_sorted_nodes = sorted(G1.nodes(data=True), key=lambda x: x[1][attr_name])
266
-
267
- # Get a sorted list of nodes in G2 based on the attribute
268
  G2_sorted_nodes = sorted(G2.nodes(data=True), key=lambda x: x[1][attr_name])
269
-
270
- # Create a mapping from the G2 node IDs to the G1 node IDs
271
  mapping = {G2_node[0]: G1_node[0] for G2_node, G1_node in zip(G2_sorted_nodes, G1_sorted_nodes)}
272
-
273
- # Generate the new graph with the updated nodes
274
  G2_updated = nx.relabel_nodes(G2, mapping)
275
-
276
  return G2_updated
277
 
278
  ##########################
@@ -289,16 +210,11 @@ def compute_std(model):
289
  agent_estimations = [agent.estimation for agent in model.schedule.agents]
290
  return np.std(agent_estimations)
291
 
292
-
293
-
294
-
295
  class PoliticalAgent(Agent):
296
  """An agent in the political model.
297
-
298
  Attributes:
299
- estimation (float): Agent's current expectation of political change.
300
- dissident (bool): True if the agent supports a regime change, False otherwise.
301
- networks_estimations (dict): A dictionary storing the estimations of the agent for each network.
302
  """
303
 
304
  def __init__(self, unique_id, model, dissident):
@@ -307,104 +223,94 @@ class PoliticalAgent(Agent):
307
  'dissident_experiences': [1],
308
  'supporter_experiences': [1],
309
  }
310
- # self.estimation = estimation
311
  self.estimations = []
312
- self.estimation = .5 #hardcoded_mean, will change in first step if agent interacts.
313
-
314
  self.experiments = []
315
-
316
-
317
  self.dissident = dissident
318
- # self.historical_estimations = []
319
 
320
  def update_estimation(self, network_id):
321
  """Update the agent's estimation for a given network."""
322
- # Get the neighbors from the network
323
- potential_partners = [self.model.schedule.agents[n] for n in self.model.networks[network_id]['network'].neighbors(self.unique_id)]
324
-
325
-
326
-
327
 
328
- current_estimate =get_beta_mean_from_experience_dict(self.experiences,half_life=self.model.half_life,decay_factors=self.model.decay_factors)
329
  self.estimations.append(current_estimate)
330
- self.estimation =current_estimate
331
- current_experiment = get_beta_sample_from_experience_dict(self.experiences,half_life=self.model.half_life, decay_factors=self.model.decay_factors)
332
  self.experiments.append(current_experiment)
333
 
334
  if potential_partners:
335
  partner = random.choice(potential_partners)
336
  if self.model.networks[network_id]['type'] == 'physical':
337
- if current_experiment >= self.model.threshold:
 
 
 
 
 
 
 
 
 
 
 
 
338
 
339
- if partner.dissident: # removed division by 100?
340
- self.experiences['dissident_experiences'].append(1)
 
341
  self.experiences['supporter_experiences'].append(0)
342
- else:
343
  self.experiences['dissident_experiences'].append(0)
344
- self.experiences['supporter_experiences'].append(1)
345
-
346
- partner.experiences['dissident_experiences'].append(1 * self.model.social_learning_factor)
347
- partner.experiences['supporter_experiences'].append(0)
348
-
349
- else:
350
- partner.experiences['dissident_experiences'].append(0)
351
- partner.experiences['supporter_experiences'].append(1 * self.model.social_learning_factor)
352
-
353
-
354
- # else:
355
- # pass
356
- # Only one network for the moment!
357
- elif self.model.networks[network_id]['type'] == 'social_media':
358
- if partner.dissident: # removed division by 100?
359
- self.experiences['dissident_experiences'].append(1 * self.model.social_media_factor)
360
- self.experiences['supporter_experiences'].append(0)
361
- else:
362
- self.experiences['dissident_experiences'].append(0)
363
- self.experiences['supporter_experiences'].append(1 * self.model.social_media_factor)
364
-
365
- # self.networks_estimations[network_id] = self.estimation
366
 
367
  def combine_estimations(self):
368
- # """Combine the estimations from all networks using a bounded confidence model."""
 
 
369
  values = [list(d.values())[0] for d in self.current_estimations]
370
-
371
  if len(values) > 0:
372
- # Filter the network estimations based on the bounded confidence range
373
  within_range = [value for value in values if abs(self.estimation - value) <= self.model.bounded_confidence_range]
374
-
375
- # If there are any estimations within the range, update the estimation
376
  if len(within_range) > 0:
377
  self.estimation = np.mean(within_range)
378
 
379
-
380
-
381
-
382
  def step(self):
383
- """Agent step function which updates the estimation for each network and then combines the estimations."""
384
- if not hasattr(self, 'current_estimations'): # agents might already have this attribute because they were partnered up in the past.
385
  self.current_estimations = []
386
-
387
  for network_id in self.model.networks.keys():
388
  self.update_estimation(network_id)
389
-
390
  self.combine_estimations()
391
- # self.historical_estimations.append(self.current_estimations)
392
  del self.current_estimations
393
 
394
-
395
  class PoliticalModel(Model):
396
- """A model of a political system with multiple interacting agents.
397
-
398
- Attributes:
399
- networks (dict): A dictionary of networks with network IDs as keys and NetworkX Graph objects as values.
400
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
401
 
402
- def __init__(self, n_agents, networks, share_regime_supporters,
403
- # initial_expectation_of_change,
404
- threshold,
405
- social_learning_factor=1,social_media_factor=1, # one for equal learning, lower gets discounted
406
- half_life=20, print_agents=False, print_frequency=30,
407
- early_stopping_steps=20, early_stopping_range=0.01, agent_reporters=True,intervention_list=[],randomID=False):
408
  self.num_agents = n_agents
409
  self.threshold = threshold
410
  self.social_learning_factor = social_learning_factor
@@ -412,43 +318,58 @@ class PoliticalModel(Model):
412
  self.print_agents_state = print_agents
413
  self.half_life = half_life
414
  self.intervention_list = intervention_list
415
- self.model_id = randomID
416
 
417
  self.print_frequency = print_frequency
418
  self.early_stopping_steps = early_stopping_steps
419
  self.early_stopping_range = early_stopping_range
420
 
421
-
422
  self.mean_estimations = []
423
- self.decay_factors = [0.5 ** (i / self.half_life) for i in range(500)] # Nte this should be larger than
424
 
425
- # we could use this for early stopping!
426
  self.running = True
427
  self.share_regime_supporters = share_regime_supporters
 
428
  self.schedule = RandomActivation(self)
429
  self.networks = networks
430
 
431
- # Assign dissident as argument to networks, compute homophilies, and match up the networks so that the same id leads to the same atrribute
432
  for i, this_network in enumerate(self.networks):
433
- self.networks[this_network]["network"] = assign_initial_attributes(self.networks[this_network]["network"],self.share_regime_supporters,attr_name='dissident')
434
- if 'homophily' in self.networks[this_network]:
435
- self.networks[this_network]["network"] = distribute_attributes(self.networks[this_network]["network"],
436
- self.networks[this_network]['homophily'], max_iter=5000, cooling_factor=0.995,attr_name='dissident')
437
- self.networks[this_network]['network_data_to_keep']['actual_homophily'] = compute_homophily(self.networks[this_network]["network"],attr_name='dissident')
438
- if i>0:
439
- self.networks[this_network]["network"] = reindex_graph_to_match_attributes(self.networks[next(iter(self.networks))]["network"], self.networks[this_network]["network"], 'dissident')
440
-
441
- # print(self.networks)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
442
 
 
 
 
443
  for i in range(self.num_agents):
444
- # estimation = random.normalvariate(initial_expectation_of_change, 0.2) We set a flat prior now
445
-
446
- agent = PoliticalAgent(i, self, self.networks[next(iter(self.networks))]["network"].nodes(data=True)[i]['dissident'])
447
  self.schedule.add(agent)
448
- # Should we update to the real share here?!
449
- ####################
450
 
451
- # Keep the attributes in the model and define model reporters
452
  model_reporters = {
453
  "Mean": compute_mean,
454
  "Median": compute_median,
@@ -461,496 +382,379 @@ class PoliticalModel(Model):
461
  attr_name = this_network + '_' + key
462
  setattr(self, attr_name, value)
463
 
464
- # Define a reporter function for this attribute
465
  def reporter(model, attr_name=attr_name):
466
  return getattr(model, attr_name)
467
 
468
- # Add the reporter function to the dictionary
469
  model_reporters[attr_name] = reporter
470
 
471
- # Initialize DataCollector with the dynamic model reporters
472
  if agent_reporters:
473
  self.datacollector = DataCollector(
474
  model_reporters=model_reporters,
475
- agent_reporters={"Estimation": "estimation", "Dissident": "dissident"}#, "Historical Estimations": "historical_estimations"}
476
  )
477
  else:
478
- self.datacollector = DataCollector(
479
- model_reporters=model_reporters
480
- )
481
 
 
 
482
 
 
 
 
483
 
 
 
484
 
 
 
485
 
486
- def step(self):
487
- """Model step function which activates the step function of each agent."""
 
 
488
 
489
- self.datacollector.collect(self) # Collect data
 
490
 
491
- # do interventions, if present:
492
- for this_intervention in self.intervention_list:
493
- # print(this_intervention)
494
- if this_intervention['time'] == len(self.mean_estimations):
495
-
496
- if this_intervention['type'] == 'threshold_adjustment':
497
- self.threshold = max(0, min(1, self.threshold + this_intervention['strength']))
498
-
499
- if this_intervention['type'] == 'share_adjustment':
500
- target_supporter_share = max(0, min(1, self.share_regime_supporters + this_intervention['strength']))
501
- agents = [self.schedule._agents[i] for i in self.schedule._agents]
502
- current_supporters = sum(not agent.dissident for agent in agents)
503
- total_agents = len(agents)
504
- current_share = current_supporters / total_agents
505
-
506
- # Calculate the number of agents to change
507
- required_supporters = int(target_supporter_share * total_agents)
508
- agents_to_change = abs(required_supporters - current_supporters)
509
-
510
- if current_share < target_supporter_share:
511
- # Not enough supporters, need to increase
512
- dissidents = [agent for agent in agents if agent.dissident]
513
- for agent in random.sample(dissidents, agents_to_change):
514
- agent.dissident = False
515
- elif current_share > target_supporter_share:
516
- # Too many supporters, need to reduce
517
- supporters = [agent for agent in agents if not agent.dissident]
518
- for agent in random.sample(supporters, agents_to_change):
519
- agent.dissident = True
520
- # print(self.threshold)
521
- if this_intervention['type'] == 'social_media_adjustment':
522
- self.social_media_factor = max(0, min(1, self.social_media_factor + this_intervention['strength']))
523
 
 
 
524
 
525
  self.schedule.step()
526
  current_mean_estimation = compute_mean(self)
527
  self.mean_estimations.append(current_mean_estimation)
528
 
529
- # Implement the early stopping criteria
530
  if len(self.mean_estimations) >= self.early_stopping_steps:
531
  recent_means = self.mean_estimations[-self.early_stopping_steps:]
532
  if max(recent_means) - min(recent_means) < self.early_stopping_range:
533
- # if self.print_agents_state:
534
- # print('Early stopping at: ', self.schedule.steps)
535
- # self.print_agents()
536
- self.running = False
537
-
538
- # if self.print_agents_state and (self.schedule.steps % self.print_frequency == 0 or self.schedule.steps == 1):
539
- # print(self.schedule.steps)
540
- # self.print_agents()
541
-
542
-
543
-
544
-
545
-
546
-
547
- # def run_simulation(n_agents=300, share_regime_supporters=0.4, threshold=0.5, social_learning_factor=1, simulation_steps=400, half_life=20):
548
- # # Helper functions like graph_from_coordinates, ensure_neighbors should be defined outside this function
549
-
550
- # # Complete graph
551
- # G = nx.complete_graph(n_agents)
552
-
553
- # # Networks dictionary
554
- # networks = {
555
- # "physical": {"network": G, "type": "physical", "positions": nx.circular_layout(G)}#kamada_kawai
556
- # }
557
-
558
- # # Intervention list
559
- # intervention_list = [ ]
560
-
561
- # # Initialize the model
562
- # model = PoliticalModel(n_agents, networks, share_regime_supporters, threshold,
563
- # social_learning_factor, half_life=half_life, print_agents=False, print_frequency=50, agent_reporters=True, intervention_list=intervention_list)
564
-
565
- # # Run the model
566
- # for _ in tqdm.tqdm_notebook(range(simulation_steps)): # Run for specified number of steps
567
- # model.step()
568
- # return model
569
-
570
- # # Example usage
571
-
572
- # radius=.09
573
- # physical_graph_points = np.random.rand(100, 2)
574
- # physical_graph = graph_from_coordinates(physical_graph_points, radius)
575
- # physical_graph = nx.convert_node_labels_to_integers(ensure_neighbors(physical_graph))
576
-
577
- # # unconnected nodes: link or drop?
578
- # networks = {
579
- # "physical": {"network": physical_graph, "type": "physical", "positions": physical_graph_points, 'network_data_to_keep':{'radius':radius},'homophily':0. }}
580
-
581
-
582
- # model = PoliticalModel(100, networks, .5, .5,.5, half_life=20, print_agents=False, print_frequency=50, agent_reporters=True, intervention_list=[])
583
-
584
-
585
- # for _ in tqdm.tqdm_notebook(range(40)): # Run for specified number of steps
586
- # model.step()
587
-
588
- # import matplotlib.pyplot as plt
589
- # import pandas as pd
590
-
591
- # # Assuming 'model' is defined and has a datacollector with the necessary data
592
- # agent_df = model.datacollector.get_agent_vars_dataframe().reset_index()
593
-
594
- # # Pivot the dataframe for Estimation
595
- # agent_df_pivot = agent_df.pivot(index='Step', columns='AgentID', values='Estimation')
596
-
597
- # # Create the result plot
598
- # run_plot, ax = plt.subplots(figsize=(12, 8))
599
-
600
- # # Define colors for Dissident and Supporter
601
- # colors = {1: '#d6a44b', 0: '#1b4968'} # 1 for Dissident, 0 for Supporter
602
- # labels = {1: 'Dissident', 0: 'Supporter'}
603
- # legend_handles = []
604
-
605
- # # Plot each agent's data
606
- # for agent_id in agent_df_pivot.columns:
607
- # # Get the agent type (Dissident or Supporter)
608
- # agent_type = agent_df[agent_df['AgentID'] == agent_id]['Dissident'].iloc[0]
609
-
610
- # # Plot
611
- # line, = plt.plot(agent_df_pivot.index, agent_df_pivot[agent_id], color=colors[agent_type], alpha=0.1)
612
-
613
-
614
- # # Compute and plot the mean estimation for each group
615
- # for agent_type, color in colors.items():
616
- # mean_estimation = agent_df_pivot.loc[:, agent_df[agent_df['Dissident'] == agent_type]['AgentID']].mean(axis=1)
617
- # plt.plot(mean_estimation.index, mean_estimation, color=color, linewidth=2, label=f'{labels[agent_type]}')
618
-
619
- # # Set the plot title and labels
620
- # plt.title('Agent Estimation Over Time', loc='right')
621
- # plt.xlabel('Time step')
622
- # plt.ylabel('Estimation')
623
-
624
- # # Add legend
625
- # plt.legend(loc='lower right')
626
-
627
-
628
- # plt.show()
629
 
630
  import PIL
631
 
632
- def run_and_plot_simulation(separate_agent_types=False,n_agents=300, share_regime_supporters=0.4, threshold=0.5, social_learning_factor=1, simulation_steps=40, half_life=20,
633
- phys_network_radius=.06, powerlaw_exponent=3,physical_network_type='physical_network_type_fully_connected',
634
- introduce_physical_homophily_true_false=False,physical_homophily=.5,
635
- introduce_social_media_homophily_true_false=False,social_media_homophily=5,social_media_network_type_random_geometric_radius=.07,social_media_network_type_powerlaw_exponent=3,
636
- social_media_network_type='Powerlaw',use_social_media_network=False):
637
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
638
  print(physical_network_type)
639
 
640
  networks = {}
641
 
642
- # Set up physical network:
643
  if physical_network_type == 'Fully Connected':
644
  G = nx.complete_graph(n_agents)
645
- networks['physical'] = {"network": G, "type": "physical", "positions": nx.circular_layout(G)}
646
 
647
  elif physical_network_type == "Powerlaw":
648
- s = nx.utils.powerlaw_sequence(n_agents, powerlaw_exponent) #100 nodes, power-law exponent 2.5
649
  G = nx.expected_degree_graph(s, selfloops=False)
650
  G = nx.convert_node_labels_to_integers(ensure_neighbors(G))
651
- networks['physical'] = {"network": G, "type": "physical", "positions": nx.kamada_kawai_layout(G)}
652
 
653
  elif physical_network_type == "Random Geometric":
654
  physical_graph_points = np.random.rand(n_agents, 2)
655
  G = graph_from_coordinates(physical_graph_points, phys_network_radius)
656
  G = nx.convert_node_labels_to_integers(ensure_neighbors(G))
657
- networks['physical'] = {"network": G, "type": "physical", "positions": physical_graph_points}
658
 
659
  if introduce_physical_homophily_true_false:
660
- networks['physical']['homophily'] = physical_homophily
661
  networks['physical']['network_data_to_keep'] = {}
662
 
663
-
664
- # Set up social media network:
665
-
666
  if use_social_media_network:
667
- if social_media_network_type == 'Fully Connected':
668
- G = nx.complete_graph(n_agents)
669
- networks['social_media'] = {"network": G, "type": "social_media", "positions": nx.circular_layout(G)}
670
-
671
- elif social_media_network_type == "Powerlaw":
672
- s = nx.utils.powerlaw_sequence(n_agents, social_media_network_type_powerlaw_exponent) # 100 nodes, power-law exponent adjusted for social media
673
- G = nx.expected_degree_graph(s, selfloops=False)
674
- G = nx.convert_node_labels_to_integers(ensure_neighbors(G))
675
- networks['social_media'] = {"network": G, "type": "social_media", "positions": nx.kamada_kawai_layout(G)}
676
-
677
- elif social_media_network_type == "Random Geometric":
678
- social_media_graph_points = np.random.rand(n_agents, 2)
679
- G = graph_from_coordinates(social_media_graph_points, social_media_network_type_random_geometric_radius)
680
- G = nx.convert_node_labels_to_integers(ensure_neighbors(G))
681
- networks['social_media'] = {"network": G, "type": "social_media", "positions": social_media_graph_points}
682
-
683
- if introduce_social_media_homophily_true_false:
684
- networks['social_media']['homophily'] = social_media_homophily
685
- networks['social_media']['network_data_to_keep'] = {}
686
-
687
-
688
-
689
- intervention_list = [ ]
690
-
691
- # Initialize the model
692
- model = PoliticalModel(n_agents, networks, share_regime_supporters, threshold,
693
- social_learning_factor, half_life=half_life, print_agents=False, print_frequency=50, agent_reporters=True, intervention_list=intervention_list)
 
 
 
 
 
 
 
 
 
694
 
695
- # Run the model
696
- for _ in tqdm.tqdm_notebook(range(simulation_steps)): # Run for specified number of steps
697
  model.step()
698
 
699
-
700
-
701
  agent_df = model.datacollector.get_agent_vars_dataframe().reset_index()
702
-
703
- # Pivot the dataframe
704
  agent_df_pivot = agent_df.pivot(index='Step', columns='AgentID', values='Estimation')
705
 
706
-
707
- # Create the esult-plot
708
  run_plot, ax = plt.subplots(figsize=(12, 8))
709
  if not separate_agent_types:
710
  for column in agent_df_pivot.columns:
711
  plt.plot(agent_df_pivot.index, agent_df_pivot[column], color='gray', alpha=0.1)
712
-
713
- # Compute and plot the mean estimation
714
- mean_estimation = agent_df_pivot.mean(axis=1)
715
- plt.plot(mean_estimation.index, mean_estimation, color='black', linewidth=2)
716
-
717
-
718
-
719
  else:
720
- # Define colors for Dissident and Supporter
721
- colors = {1: '#d6a44b', 0: '#1b4968'} # 1 for Dissident, 0 for Supporter
722
  labels = {1: 'Dissident', 0: 'Supporter'}
723
- legend_handles = []
724
 
725
- # Plot each agent's data
726
  for agent_id in agent_df_pivot.columns:
727
- # Get the agent type (Dissident or Supporter)
728
  agent_type = agent_df[agent_df['AgentID'] == agent_id]['Dissident'].iloc[0]
 
729
 
730
- # Plot
731
- line, = plt.plot(agent_df_pivot.index, agent_df_pivot[agent_id], color=colors[agent_type], alpha=0.1)
732
-
733
-
734
- # Compute and plot the mean estimation for each group
735
  for agent_type, color in colors.items():
736
  mean_estimation = agent_df_pivot.loc[:, agent_df[agent_df['Dissident'] == agent_type]['AgentID']].mean(axis=1)
737
  plt.plot(mean_estimation.index, mean_estimation, color=color, linewidth=2, label=f'{labels[agent_type]}')
738
  plt.legend(loc='lower right')
739
 
740
-
741
-
742
- # Set the plot title and labels
743
  plt.title('Agent Estimation Over Time', loc='right')
744
  plt.xlabel('Time step')
745
  plt.ylabel('Estimation')
746
 
747
-
748
- plt.savefig('run_plot.png' ,bbox_inches='tight',
749
- dpi =400, transparent=True)
750
  run_plot = PIL.Image.open('run_plot.png').convert('RGBA')
751
 
752
- # Create the network-plot
753
  n_networks = len(networks)
754
- network_plot, axs = plt.subplots(1, n_networks, figsize=( 9.5 * n_networks,8))
755
-
756
  if n_networks == 1:
757
  axs = [axs]
 
758
  estimations = {}
759
  for agent in model.schedule.agents:
760
  estimations[agent.unique_id] = agent.estimation
 
761
  for idx, (network_id, network_dict) in enumerate(networks.items()):
762
  network = network_dict['network']
763
- # Collect estimations and set the node attributes
764
-
765
-
766
  nx.set_node_attributes(network, estimations, 'estimation')
767
 
768
- # Use the positions provided in the network dict if available
769
  if 'positions' in network_dict:
770
  pos = network_dict['positions']
771
  else:
772
  pos = nx.kamada_kawai_layout(network)
773
 
774
- # Draw the network with nodes colored by their estimation values
775
  node_colors = [estimations[node] for node in network.nodes]
776
  axs[idx].set_title(f'Network: {network_id}', loc='right')
777
- # nx.draw(network, pos, node_size=50, node_color=node_colors,
778
- # cmap=cmocean.tools.crop_by_percent(cmocean.cm.curl, 20, which='both', N=None),
779
- # with_labels=False,vmin=0, vmax=1, ax=axs[idx])
780
- # Drawing nodes
781
- nx.draw_networkx_nodes(network, pos, node_size=50, node_color=node_colors,
782
- cmap=cmocean.tools.crop_by_percent(cmocean.cm.curl, 20, which='both', N=None),
783
- vmin=0, vmax=1, ax=axs[idx])
784
 
785
- # Drawing edges with semi-transparency
786
- nx.draw_networkx_edges(network, pos, alpha=0.3, ax=axs[idx]) # alpha value for semi-transparency
787
-
788
-
789
- # Create a dummy ScalarMappable with the same colormap
790
- sm = mpl.cm.ScalarMappable(cmap=cmocean.tools.crop_by_percent(cmocean.cm.curl, 20, which='both', N=None),
791
- norm=plt.Normalize(vmin=0, vmax=1))
 
 
 
 
792
  sm.set_array([])
793
  network_plot.colorbar(sm, ax=axs[idx])
794
- plt.savefig('network_plot.png' ,bbox_inches='tight',
795
- dpi =400, transparent=True)
796
 
 
797
  network_plot = PIL.Image.open('network_plot.png').convert('RGBA')
798
 
799
  return run_plot, network_plot
800
 
801
-
802
- # run_and_plot_simulation(n_agents=300, share_regime_supporters=0.4, threshold=0.5, social_learning_factor=1, simulation_steps=40, half_life=20)
803
-
804
  import gradio as gr
805
  import matplotlib.pyplot as plt
806
 
807
-
808
- # Gradio interface
809
  with gr.Blocks(theme=gr.themes.Monochrome()) as demo:
810
- with gr.Column():
811
- gr.Markdown("""# Simulate the emergence of social movements
812
- Vary the parameters below, and click 'Run Simulation' to run.
813
- """)
814
- with gr.Row():
815
- with gr.Column():
816
-
817
- with gr.Group():
818
- separate_agent_types = gr.Checkbox(value=False, label="Separate agent types in plot")
819
-
820
- # Sliders for each parameter
821
- n_agents_slider = gr.Slider(minimum=100, maximum=500, step=10, label="Number of Agents", value=150)
822
- share_regime_slider = gr.Slider(minimum=0.0, maximum=1.0, step=0.01, label="Share of Regime Supporters", value=0.4)
823
- threshold_slider = gr.Slider(minimum=0.0, maximum=1.0, step=0.01, label="Threshold", value=0.5)
824
- social_learning_slider = gr.Slider(minimum=0.0, maximum=2.0, step=0.1, label="Social Learning Factor", value=1.0)
825
- steps_slider = gr.Slider(minimum=10, maximum=100, step=5, label="Simulation Steps", value=40)
826
- half_life_slider = gr.Slider(minimum=5, maximum=50, step=5, label="Half-Life", value=20)
827
-
828
-
829
- # physical network settings
830
- with gr.Group():
831
- # with gr.Group():
832
- gr.Markdown("""**Physical Network Settings:**""")
833
- # Define the checkbox
834
- introduce_physical_homophily_true_false = gr.Checkbox(value=False, label="Stipulate Homophily")
835
-
836
- # Define a group to hold the slider
837
- with gr.Group(visible=False) as homophily_group:
838
- physical_homophily = gr.Slider(0, 1, label="Homophily", info='How much homophily to stipulate.')
839
-
840
- # Function to update the visibility of the group based on the checkbox
841
- def update_homophily_group_visibility(checkbox_state):
842
- return {
843
- homophily_group: gr.Group(visible=checkbox_state) # The group visibility depends on the checkbox
844
- }
845
-
846
- # Bind the function to the checkbox
847
- introduce_physical_homophily_true_false.change(
848
- update_homophily_group_visibility,
849
- inputs=introduce_physical_homophily_true_false,
850
- outputs=homophily_group
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
851
  )
852
 
853
-
854
- physical_network_type = gr.Dropdown(label="Physical Network Type", value="Fully Connected",choices=["Fully Connected", "Random Geometric","Powerlaw"])#value ="Fully Connected"
855
-
856
-
857
- with gr.Group(visible=True) as physical_network_type_fully_connected_group:
858
- gr.Markdown("""""")
859
-
860
- with gr.Group(visible=False) as physical_network_type_random_geometric_group:
861
- physical_network_type_random_geometric_radius = gr.Slider(minimum=.0, maximum=.5,label="Radius")
862
-
863
- with gr.Group(visible=False) as physical_network_type_powerlaw_group:
864
- physical_network_type_random_geometric_powerlaw_exponent = gr.Slider(minimum=.0, maximum=5.2,label="Powerlaw Exponent")
865
-
866
- def update_sliders(option):
867
- return {
868
- physical_network_type_fully_connected_group: gr.Group(visible=option == "Fully Connected"),
869
- physical_network_type_random_geometric_group: gr.Group(visible=option == "Random Geometric"),
870
- physical_network_type_powerlaw_group: gr.Group(visible=option == "Powerlaw") }
871
-
872
-
873
- physical_network_type.change(update_sliders, inputs=physical_network_type, outputs=[physical_network_type_fully_connected_group,
874
- physical_network_type_random_geometric_group,
875
- physical_network_type_powerlaw_group])
876
-
877
- # social media settings:
878
- use_social_media_network = gr.Checkbox(value=False, label="Use social media network")
879
- with gr.Group(visible=False) as social_media_group:
880
- gr.Markdown("""**Social Media Network Settings:**""")
881
-
882
- # Define the checkbox for social media network
883
- social_media_factor = gr.Slider(0, 2, label="Social Media Factor", info='How strongly to weigh the social media network against learning in the real world.')
884
- introduce_social_media_homophily_true_false = gr.Checkbox(value=False, label="Stipulate Homophily")
885
-
886
- # Define a group to hold the slider for social media network
887
- with gr.Group(visible=False) as social_media_homophily_group:
888
- social_media_homophily = gr.Slider(0, 1, label="Homophily", info='How much homophily to stipulate in social media network.')
889
-
890
- # Function to update the visibility of the group based on the checkbox for social media network
891
- def update_social_media_homophily_group_visibility(checkbox_state):
892
- return {
893
- social_media_homophily_group: gr.Group(visible=checkbox_state) # The group visibility depends on the checkbox for social media network
894
- }
895
-
896
- # Bind the function to the checkbox for social media network
897
- introduce_social_media_homophily_true_false.change(
898
- update_social_media_homophily_group_visibility,
899
- inputs=introduce_social_media_homophily_true_false,
900
- outputs=social_media_homophily_group
901
- )
902
-
903
- social_media_network_type = gr.Dropdown(label="Social Media Network Type", value="Fully Connected", choices=["Fully Connected", "Random Geometric", "Powerlaw"])
904
-
905
- with gr.Group(visible=True) as social_media_network_type_fully_connected_group:
906
- gr.Markdown("""""")
907
-
908
- with gr.Group(visible=False) as social_media_network_type_random_geometric_group:
909
- social_media_network_type_random_geometric_radius = gr.Slider(minimum=0.0, maximum=0.5, label="Radius")
910
-
911
- with gr.Group(visible=False) as social_media_network_type_powerlaw_group:
912
- social_media_network_type_powerlaw_exponent = gr.Slider(minimum=0.0, maximum=5.2, label="Powerlaw Exponent")
913
-
914
- def update_social_media_network_sliders(option):
915
- return {
916
- social_media_network_type_fully_connected_group: gr.Group(visible=option == "Fully Connected"),
917
- social_media_network_type_random_geometric_group: gr.Group(visible=option == "Random Geometric"),
918
- social_media_network_type_powerlaw_group: gr.Group(visible=option == "Powerlaw")
919
- }
920
-
921
- social_media_network_type.change(update_social_media_network_sliders, inputs=social_media_network_type, outputs=[social_media_network_type_fully_connected_group,
922
- social_media_network_type_random_geometric_group,
923
- social_media_network_type_powerlaw_group])
924
- def update_social_media_group_visibility(checkbox_state):
925
- return {social_media_group: gr.Group(visible=checkbox_state) }
926
- use_social_media_network.change(update_social_media_group_visibility,inputs=use_social_media_network,outputs=social_media_group)
927
-
928
-
929
- with gr.Column():
930
- # Button to trigger the simulation
931
- button = gr.Button("Run Simulation")
932
- plot_output = gr.Image(label="Simulation Result")
933
- network_output = gr.Image(label="Networks")
934
- # gr.Button(value="Download Results",link="/file=network_plot.png")
935
-
936
-
937
-
938
- # Function to call when button is clicked
939
- def run_simulation_and_plot(*args):
940
- fig = run_and_plot_simulation(*args)
941
- return fig
942
-
943
- # Setting up the button click event
944
- button.click(
945
- run_simulation_and_plot,
946
- inputs=[separate_agent_types,n_agents_slider, share_regime_slider, threshold_slider, social_learning_slider,
947
- steps_slider, half_life_slider, physical_network_type_random_geometric_radius,physical_network_type_random_geometric_powerlaw_exponent,physical_network_type,
948
- introduce_physical_homophily_true_false,physical_homophily,
949
- introduce_social_media_homophily_true_false,social_media_homophily,social_media_network_type_random_geometric_radius,social_media_network_type_powerlaw_exponent,social_media_network_type,use_social_media_network],
950
- outputs=[plot_output,network_output]
951
- )
952
 
953
  # Launch the interface
954
  if __name__ == "__main__":
955
- demo.launch(debug=True)
956
-
 
1
  # -*- coding: utf-8 -*-
2
  """revolutions_exploration.ipynb
 
3
  Automatically generated by Colaboratory.
 
4
  Original file is located at
5
  https://colab.research.google.com/drive/1omNn2hrbDL_s1qwCOr7ViaIjrRW61YDt
6
  """
 
10
  # !pip install gradio
11
  # # !pip install gradio==3.50.2
12
 
 
 
13
  # Commented out IPython magic to ensure Python compatibility.
14
  # %%capture
15
+ #
16
  # !pip install cmocean
17
  # !pip install mesa
18
+ #
19
  # !pip install opinionated
20
 
21
  import random
 
48
  import matplotlib.pyplot as plt
49
 
50
  plt.style.use("opinionated_rc")
51
+ # from opinionated.core import download_googlefont
52
+ # download_googlefont('Quicksand', add_to_cache=True)
53
+ # plt.rc('font', family='Quicksand')
54
 
55
  experiences = {
56
+ 'dissident_experiences': [1, 0, 0],
57
+ 'supporter_experiences': [1, 1, 1],
58
+ }
59
 
60
  def apply_half_life_decay(data_list, half_life, decay_factors=None):
61
  steps = len(data_list)
 
 
62
  if decay_factors is None or len(decay_factors) < steps:
63
  decay_factors = [0.5 ** (i / half_life) for i in range(steps)]
64
  decayed_list = [data_list[i] * decay_factors[steps - 1 - i] for i in range(steps)]
 
 
65
  return decayed_list
66
 
67
+ half_life = 20
 
 
68
  decay_factors = [0.5 ** (i / half_life) for i in range(200)]
69
 
70
+ def get_beta_mean_from_experience_dict(experiences, half_life=20, decay_factors=None):
71
+ eta = 1e-10
72
+ return beta.mean(
73
+ sum(apply_half_life_decay(experiences['dissident_experiences'], half_life, decay_factors)) + eta,
74
+ sum(apply_half_life_decay(experiences['supporter_experiences'], half_life, decay_factors)) + eta
75
+ )
 
 
 
 
 
 
 
76
 
77
+ def get_beta_sample_from_experience_dict(experiences, half_life=20, decay_factors=None):
78
+ eta = 1e-10
79
+ return beta.rvs(
80
+ sum(apply_half_life_decay(experiences['dissident_experiences'], half_life, decay_factors)) + eta,
81
+ sum(apply_half_life_decay(experiences['supporter_experiences'], half_life, decay_factors)) + eta,
82
+ size=1
83
+ )[0]
84
 
85
+ # print(get_beta_mean_from_experience_dict(experiences, half_life, decay_factors))
86
+ # print(get_beta_sample_from_experience_dict(experiences, half_life))
87
 
88
  #@title Load network functionality
89
 
90
  def generate_community_points(num_communities, total_nodes, powerlaw_exponent=2.0, sigma=0.05, plot=False):
91
  """
92
+ Generate 2D points grouped into communities (Gaussian around random centers).
 
 
 
 
 
 
 
 
 
 
 
93
  """
 
 
94
  sequence = nx.utils.powerlaw_sequence(num_communities, powerlaw_exponent)
 
 
95
  probabilities = sequence / np.sum(sequence)
96
 
 
97
  community_assignments = np.random.choice(num_communities, size=total_nodes, p=probabilities)
 
 
98
  community_sizes = np.bincount(community_assignments)
 
99
  if len(community_sizes) < num_communities:
100
  community_sizes = np.pad(community_sizes, (0, num_communities - len(community_sizes)), 'constant')
101
 
102
  points = []
103
  community_centers = []
104
 
 
105
  for i in range(num_communities):
 
106
  center = np.random.rand(2)
107
  community_centers.append(center)
 
 
108
  community_points = np.random.normal(center, sigma, (community_sizes[i], 2))
 
109
  points.append(community_points)
110
 
111
  points = np.concatenate(points)
112
 
 
113
  if plot:
114
+ plt.figure(figsize=(8, 8))
115
  plt.scatter(points[:, 0], points[:, 1], alpha=0.5)
 
116
  sns.kdeplot(x=points[:, 0], y=points[:, 1], levels=5, color="k", linewidths=1)
 
 
117
  plt.show()
118
 
119
  return points
120
 
 
121
  def graph_from_coordinates(coords, radius):
122
  """
123
+ Create a random geometric graph from an array of coordinates.
 
 
 
 
 
 
 
124
  """
 
 
125
  kdtree = sp.spatial.cKDTree(coords)
126
  edge_indexes = kdtree.query_pairs(radius)
127
  g = nx.Graph()
128
  g.add_nodes_from(list(range(len(coords))))
129
  g.add_edges_from(edge_indexes)
 
130
  return g
131
 
 
132
  def plot_graph(graph, positions):
133
+ plt.figure(figsize=(8, 8))
 
 
 
 
 
 
 
 
134
  pos_dict = {i: positions[i] for i in range(len(positions))}
135
  nx.draw_networkx_nodes(graph, pos_dict, node_size=30, node_color="#1a2340", alpha=0.7)
136
  nx.draw_networkx_edges(graph, pos_dict, edge_color="grey", width=1, alpha=1)
137
  plt.show()
138
 
 
 
139
  def ensure_neighbors(graph):
140
  """
141
+ Ensure that all nodes have at least one neighbor.
 
 
 
 
 
 
142
  """
143
  nodes = list(graph.nodes())
144
  for node in nodes:
145
  if len(list(graph.neighbors(node))) == 0:
 
146
  other_node = random.choice(nodes)
147
+ while other_node == node:
148
  other_node = random.choice(nodes)
149
  graph.add_edge(node, other_node)
150
  return graph
151
 
152
+ def compute_homophily(G, attr_name='attr'):
 
153
  same_attribute_edges = sum(G.nodes[n1][attr_name] == G.nodes[n2][attr_name] for n1, n2 in G.edges())
154
  total_edges = G.number_of_edges()
155
  return same_attribute_edges / total_edges if total_edges > 0 else 0
156
 
157
+ def assign_initial_attributes(G, ratio, attr_name='attr'):
158
  nodes = list(G.nodes)
159
  random.shuffle(nodes)
160
  attr_boundary = int(ratio * len(nodes))
 
162
  G.nodes[node][attr_name] = 0 if i < attr_boundary else 1
163
  return G
164
 
165
+ def distribute_attributes(G, target_homophily, seed=None, max_iter=10000, cooling_factor=0.9995, attr_name='attr'):
166
  random.seed(seed)
167
+ current_homophily = compute_homophily(G, attr_name)
168
  temp = 1.0
169
 
170
  for i in range(max_iter):
 
171
  nodes = list(G.nodes)
172
  random.shuffle(nodes)
173
  for node1, node2 in zip(nodes[::2], nodes[1::2]):
 
175
  G.nodes[node1][attr_name], G.nodes[node2][attr_name] = G.nodes[node2][attr_name], G.nodes[node1][attr_name]
176
  break
177
 
178
+ new_homophily = compute_homophily(G, attr_name)
179
  delta_homophily = new_homophily - current_homophily
180
  dir_factor = np.sign(target_homophily - current_homophily)
181
 
 
182
  if abs(new_homophily - target_homophily) < abs(current_homophily - target_homophily) or \
183
  (delta_homophily / temp < 700 and random.random() < np.exp(dir_factor * delta_homophily / temp)):
184
  current_homophily = new_homophily
185
+ else:
186
  G.nodes[node1][attr_name], G.nodes[node2][attr_name] = G.nodes[node2][attr_name], G.nodes[node1][attr_name]
187
 
188
+ temp *= cooling_factor
189
 
190
  return G
191
 
 
192
  def reindex_graph_to_match_attributes(G1, G2, attr_name):
 
193
  G1_sorted_nodes = sorted(G1.nodes(data=True), key=lambda x: x[1][attr_name])
 
 
194
  G2_sorted_nodes = sorted(G2.nodes(data=True), key=lambda x: x[1][attr_name])
 
 
195
  mapping = {G2_node[0]: G1_node[0] for G2_node, G1_node in zip(G2_sorted_nodes, G1_sorted_nodes)}
 
 
196
  G2_updated = nx.relabel_nodes(G2, mapping)
 
197
  return G2_updated
198
 
199
  ##########################
 
210
  agent_estimations = [agent.estimation for agent in model.schedule.agents]
211
  return np.std(agent_estimations)
212
 
 
 
 
213
  class PoliticalAgent(Agent):
214
  """An agent in the political model.
 
215
  Attributes:
216
+ estimation (float): current expectation of political change
217
+ dissident (bool): True if supports regime change
 
218
  """
219
 
220
  def __init__(self, unique_id, model, dissident):
 
223
  'dissident_experiences': [1],
224
  'supporter_experiences': [1],
225
  }
 
226
  self.estimations = []
227
+ self.estimation = 0.5
 
228
  self.experiments = []
 
 
229
  self.dissident = dissident
 
230
 
231
  def update_estimation(self, network_id):
232
  """Update the agent's estimation for a given network."""
233
+ # neighbors are node ids, map to agent objects via model.id2agent
234
+ potential_partners = [self.model.id2agent[n] for n in self.model.networks[network_id]['network'].neighbors(self.unique_id)]
 
 
 
235
 
236
+ current_estimate = get_beta_mean_from_experience_dict(self.experiences, half_life=self.model.half_life, decay_factors=self.model.decay_factors)
237
  self.estimations.append(current_estimate)
238
+ self.estimation = current_estimate
239
+ current_experiment = get_beta_sample_from_experience_dict(self.experiences, half_life=self.model.half_life, decay_factors=self.model.decay_factors)
240
  self.experiments.append(current_experiment)
241
 
242
  if potential_partners:
243
  partner = random.choice(potential_partners)
244
  if self.model.networks[network_id]['type'] == 'physical':
245
+ if current_experiment >= self.model.threshold:
246
+ if partner.dissident:
247
+ self.experiences['dissident_experiences'].append(1)
248
+ self.experiences['supporter_experiences'].append(0)
249
+ else:
250
+ self.experiences['dissident_experiences'].append(0)
251
+ self.experiences['supporter_experiences'].append(1)
252
+
253
+ partner.experiences['dissident_experiences'].append(1 * self.model.social_learning_factor)
254
+ partner.experiences['supporter_experiences'].append(0)
255
+ else:
256
+ partner.experiences['dissident_experiences'].append(0)
257
+ partner.experiences['supporter_experiences'].append(1 * self.model.social_learning_factor)
258
 
259
+ elif self.model.networks[network_id]['type'] == 'social_media':
260
+ if partner.dissident:
261
+ self.experiences['dissident_experiences'].append(1 * self.model.social_media_factor)
262
  self.experiences['supporter_experiences'].append(0)
263
+ else:
264
  self.experiences['dissident_experiences'].append(0)
265
+ self.experiences['supporter_experiences'].append(1 * self.model.social_media_factor)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266
 
267
  def combine_estimations(self):
268
+ # Placeholder for bounded confidence, not used currently
269
+ if not hasattr(self, "current_estimations"):
270
+ return
271
  values = [list(d.values())[0] for d in self.current_estimations]
 
272
  if len(values) > 0:
 
273
  within_range = [value for value in values if abs(self.estimation - value) <= self.model.bounded_confidence_range]
 
 
274
  if len(within_range) > 0:
275
  self.estimation = np.mean(within_range)
276
 
 
 
 
277
  def step(self):
278
+ if not hasattr(self, 'current_estimations'):
 
279
  self.current_estimations = []
 
280
  for network_id in self.model.networks.keys():
281
  self.update_estimation(network_id)
 
282
  self.combine_estimations()
 
283
  del self.current_estimations
284
 
 
285
  class PoliticalModel(Model):
286
+ """A model of a political system with multiple interacting agents."""
287
+
288
+ def __init__(
289
+ self,
290
+ n_agents,
291
+ networks,
292
+ share_regime_supporters,
293
+ threshold,
294
+ social_learning_factor=1,
295
+ social_media_factor=1,
296
+ half_life=20,
297
+ print_agents=False,
298
+ print_frequency=30,
299
+ early_stopping_steps=20,
300
+ early_stopping_range=0.01,
301
+ agent_reporters=True,
302
+ intervention_list=None,
303
+ rng_seed=None,
304
+ ):
305
+ # Important: initialize parent so self.random exists
306
+ try:
307
+ super().__init__(rng_seed=rng_seed) # Mesa >= 3.0
308
+ except TypeError:
309
+ super().__init__(seed=rng_seed) # Mesa < 3.0
310
+
311
+ if intervention_list is None:
312
+ intervention_list = []
313
 
 
 
 
 
 
 
314
  self.num_agents = n_agents
315
  self.threshold = threshold
316
  self.social_learning_factor = social_learning_factor
 
318
  self.print_agents_state = print_agents
319
  self.half_life = half_life
320
  self.intervention_list = intervention_list
 
321
 
322
  self.print_frequency = print_frequency
323
  self.early_stopping_steps = early_stopping_steps
324
  self.early_stopping_range = early_stopping_range
325
 
 
326
  self.mean_estimations = []
327
+ self.decay_factors = [0.5 ** (i / self.half_life) for i in range(500)]
328
 
 
329
  self.running = True
330
  self.share_regime_supporters = share_regime_supporters
331
+
332
  self.schedule = RandomActivation(self)
333
  self.networks = networks
334
 
335
+ # Align attributes across networks and compute homophilies
336
  for i, this_network in enumerate(self.networks):
337
+ self.networks[this_network]["network"] = assign_initial_attributes(
338
+ self.networks[this_network]["network"],
339
+ self.share_regime_supporters,
340
+ attr_name='dissident'
341
+ )
342
+ if 'homophily' in self.networks[this_network]:
343
+ self.networks[this_network]["network"] = distribute_attributes(
344
+ self.networks[this_network]["network"],
345
+ self.networks[this_network]['homophily'],
346
+ max_iter=5000,
347
+ cooling_factor=0.995,
348
+ attr_name='dissident'
349
+ )
350
+ self.networks[this_network]['network_data_to_keep']['actual_homophily'] = compute_homophily(
351
+ self.networks[this_network]["network"],
352
+ attr_name='dissident'
353
+ )
354
+ if i > 0:
355
+ # Reindex so node ids match across networks
356
+ first_key = next(iter(self.networks))
357
+ self.networks[this_network]["network"] = reindex_graph_to_match_attributes(
358
+ self.networks[first_key]["network"],
359
+ self.networks[this_network]["network"],
360
+ 'dissident'
361
+ )
362
 
363
+ # Create agents and an id -> agent map for stable lookups
364
+ self.id2agent = {}
365
+ first_key = next(iter(self.networks))
366
  for i in range(self.num_agents):
367
+ dissident_flag = self.networks[first_key]["network"].nodes[i]['dissident']
368
+ agent = PoliticalAgent(i, self, dissident_flag)
 
369
  self.schedule.add(agent)
370
+ self.id2agent[i] = agent
 
371
 
372
+ # Model reporters
373
  model_reporters = {
374
  "Mean": compute_mean,
375
  "Median": compute_median,
 
382
  attr_name = this_network + '_' + key
383
  setattr(self, attr_name, value)
384
 
 
385
  def reporter(model, attr_name=attr_name):
386
  return getattr(model, attr_name)
387
 
 
388
  model_reporters[attr_name] = reporter
389
 
 
390
  if agent_reporters:
391
  self.datacollector = DataCollector(
392
  model_reporters=model_reporters,
393
+ agent_reporters={"Estimation": "estimation", "Dissident": "dissident"}
394
  )
395
  else:
396
+ self.datacollector = DataCollector(model_reporters=model_reporters)
 
 
397
 
398
+ def step(self):
399
+ self.datacollector.collect(self)
400
 
401
+ # Interventions
402
+ for this_intervention in self.intervention_list:
403
+ if this_intervention['time'] == len(self.mean_estimations):
404
 
405
+ if this_intervention['type'] == 'threshold_adjustment':
406
+ self.threshold = max(0, min(1, self.threshold + this_intervention['strength']))
407
 
408
+ if this_intervention['type'] == 'share_adjustment':
409
+ target_supporter_share = max(0, min(1, self.share_regime_supporters + this_intervention['strength']))
410
 
411
+ agents = list(self.schedule.agents) # stable across Mesa versions
412
+ current_supporters = sum(not agent.dissident for agent in agents)
413
+ total_agents = len(agents)
414
+ current_share = current_supporters / total_agents
415
 
416
+ required_supporters = int(target_supporter_share * total_agents)
417
+ agents_to_change = abs(required_supporters - current_supporters)
418
 
419
+ if current_share < target_supporter_share:
420
+ dissidents = [agent for agent in agents if agent.dissident]
421
+ for agent in random.sample(dissidents, min(agents_to_change, len(dissidents))):
422
+ agent.dissident = False
423
+ elif current_share > target_supporter_share:
424
+ supporters = [agent for agent in agents if not agent.dissident]
425
+ for agent in random.sample(supporters, min(agents_to_change, len(supporters))):
426
+ agent.dissident = True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
427
 
428
+ if this_intervention['type'] == 'social_media_adjustment':
429
+ self.social_media_factor = max(0, min(1, self.social_media_factor + self_intervention['strength']))
430
 
431
  self.schedule.step()
432
  current_mean_estimation = compute_mean(self)
433
  self.mean_estimations.append(current_mean_estimation)
434
 
 
435
  if len(self.mean_estimations) >= self.early_stopping_steps:
436
  recent_means = self.mean_estimations[-self.early_stopping_steps:]
437
  if max(recent_means) - min(recent_means) < self.early_stopping_range:
438
+ self.running = False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
439
 
440
  import PIL
441
 
442
+ def run_and_plot_simulation(
443
+ separate_agent_types=False,
444
+ n_agents=300,
445
+ share_regime_supporters=0.4,
446
+ threshold=0.5,
447
+ social_learning_factor=1,
448
+ simulation_steps=40,
449
+ half_life=20,
450
+ phys_network_radius=.06,
451
+ powerlaw_exponent=3,
452
+ physical_network_type='Fully Connected',
453
+ introduce_physical_homophily_true_false=False,
454
+ physical_homophily=.5,
455
+ introduce_social_media_homophily_true_false=False,
456
+ social_media_homophily=.5,
457
+ social_media_network_type_random_geometric_radius=.07,
458
+ social_media_network_type_powerlaw_exponent=3,
459
+ social_media_network_type='Powerlaw',
460
+ use_social_media_network=False,
461
+ social_media_factor=1.0, # NEW: wired from UI
462
+ rng_seed=None
463
+ ):
464
  print(physical_network_type)
465
 
466
  networks = {}
467
 
468
+ # Physical network
469
  if physical_network_type == 'Fully Connected':
470
  G = nx.complete_graph(n_agents)
471
+ networks['physical'] = {"network": G, "type": "physical", "positions": nx.circular_layout(G)}
472
 
473
  elif physical_network_type == "Powerlaw":
474
+ s = nx.utils.powerlaw_sequence(n_agents, powerlaw_exponent)
475
  G = nx.expected_degree_graph(s, selfloops=False)
476
  G = nx.convert_node_labels_to_integers(ensure_neighbors(G))
477
+ networks['physical'] = {"network": G, "type": "physical", "positions": nx.kamada_kawai_layout(G)}
478
 
479
  elif physical_network_type == "Random Geometric":
480
  physical_graph_points = np.random.rand(n_agents, 2)
481
  G = graph_from_coordinates(physical_graph_points, phys_network_radius)
482
  G = nx.convert_node_labels_to_integers(ensure_neighbors(G))
483
+ networks['physical'] = {"network": G, "type": "physical", "positions": physical_graph_points}
484
 
485
  if introduce_physical_homophily_true_false:
486
+ networks['physical']['homophily'] = physical_homophily
487
  networks['physical']['network_data_to_keep'] = {}
488
 
489
+ # Social media network
 
 
490
  if use_social_media_network:
491
+ if social_media_network_type == 'Fully Connected':
492
+ G = nx.complete_graph(n_agents)
493
+ networks['social_media'] = {"network": G, "type": "social_media", "positions": nx.circular_layout(G)}
494
+
495
+ elif social_media_network_type == "Powerlaw":
496
+ s = nx.utils.powerlaw_sequence(n_agents, social_media_network_type_powerlaw_exponent)
497
+ G = nx.expected_degree_graph(s, selfloops=False)
498
+ G = nx.convert_node_labels_to_integers(ensure_neighbors(G))
499
+ networks['social_media'] = {"network": G, "type": "social_media", "positions": nx.kamada_kawai_layout(G)}
500
+
501
+ elif social_media_network_type == "Random Geometric":
502
+ social_media_graph_points = np.random.rand(n_agents, 2)
503
+ G = graph_from_coordinates(social_media_graph_points, social_media_network_type_random_geometric_radius)
504
+ G = nx.convert_node_labels_to_integers(ensure_neighbors(G))
505
+ networks['social_media'] = {"network": G, "type": "social_media", "positions": social_media_graph_points}
506
+
507
+ if introduce_social_media_homophily_true_false:
508
+ networks['social_media']['homophily'] = social_media_homophily
509
+ networks['social_media']['network_data_to_keep'] = {}
510
+
511
+ intervention_list = []
512
+
513
+ model = PoliticalModel(
514
+ n_agents,
515
+ networks,
516
+ share_regime_supporters,
517
+ threshold,
518
+ social_learning_factor=social_learning_factor,
519
+ social_media_factor=social_media_factor, # NEW
520
+ half_life=half_life,
521
+ print_agents=False,
522
+ print_frequency=50,
523
+ agent_reporters=True,
524
+ intervention_list=intervention_list,
525
+ rng_seed=rng_seed
526
+ )
527
 
528
+ for _ in tqdm.tqdm(range(simulation_steps)):
 
529
  model.step()
530
 
 
 
531
  agent_df = model.datacollector.get_agent_vars_dataframe().reset_index()
 
 
532
  agent_df_pivot = agent_df.pivot(index='Step', columns='AgentID', values='Estimation')
533
 
 
 
534
  run_plot, ax = plt.subplots(figsize=(12, 8))
535
  if not separate_agent_types:
536
  for column in agent_df_pivot.columns:
537
  plt.plot(agent_df_pivot.index, agent_df_pivot[column], color='gray', alpha=0.1)
538
+ mean_estimation = agent_df_pivot.mean(axis=1)
539
+ plt.plot(mean_estimation.index, mean_estimation, color='black', linewidth=2)
 
 
 
 
 
540
  else:
541
+ colors = {1: '#d6a44b', 0: '#1b4968'}
 
542
  labels = {1: 'Dissident', 0: 'Supporter'}
 
543
 
 
544
  for agent_id in agent_df_pivot.columns:
 
545
  agent_type = agent_df[agent_df['AgentID'] == agent_id]['Dissident'].iloc[0]
546
+ plt.plot(agent_df_pivot.index, agent_df_pivot[agent_id], color=colors[agent_type], alpha=0.1)
547
 
 
 
 
 
 
548
  for agent_type, color in colors.items():
549
  mean_estimation = agent_df_pivot.loc[:, agent_df[agent_df['Dissident'] == agent_type]['AgentID']].mean(axis=1)
550
  plt.plot(mean_estimation.index, mean_estimation, color=color, linewidth=2, label=f'{labels[agent_type]}')
551
  plt.legend(loc='lower right')
552
 
 
 
 
553
  plt.title('Agent Estimation Over Time', loc='right')
554
  plt.xlabel('Time step')
555
  plt.ylabel('Estimation')
556
 
557
+ plt.savefig('run_plot.png', bbox_inches='tight', dpi=400, transparent=True)
 
 
558
  run_plot = PIL.Image.open('run_plot.png').convert('RGBA')
559
 
560
+ # Network plot
561
  n_networks = len(networks)
562
+ network_plot, axs = plt.subplots(1, n_networks, figsize=(9.5 * n_networks, 8))
 
563
  if n_networks == 1:
564
  axs = [axs]
565
+
566
  estimations = {}
567
  for agent in model.schedule.agents:
568
  estimations[agent.unique_id] = agent.estimation
569
+
570
  for idx, (network_id, network_dict) in enumerate(networks.items()):
571
  network = network_dict['network']
 
 
 
572
  nx.set_node_attributes(network, estimations, 'estimation')
573
 
 
574
  if 'positions' in network_dict:
575
  pos = network_dict['positions']
576
  else:
577
  pos = nx.kamada_kawai_layout(network)
578
 
 
579
  node_colors = [estimations[node] for node in network.nodes]
580
  axs[idx].set_title(f'Network: {network_id}', loc='right')
 
 
 
 
 
 
 
581
 
582
+ nx.draw_networkx_nodes(
583
+ network, pos, node_size=50, node_color=node_colors,
584
+ cmap=cmocean.tools.crop_by_percent(cmocean.cm.curl, 20, which='both', N=None),
585
+ vmin=0, vmax=1, ax=axs[idx]
586
+ )
587
+ nx.draw_networkx_edges(network, pos, alpha=0.3, ax=axs[idx])
588
+
589
+ sm = mpl.cm.ScalarMappable(
590
+ cmap=cmocean.tools.crop_by_percent(cmocean.cm.curl, 20, which='both', N=None),
591
+ norm=plt.Normalize(vmin=0, vmax=1)
592
+ )
593
  sm.set_array([])
594
  network_plot.colorbar(sm, ax=axs[idx])
 
 
595
 
596
+ plt.savefig('network_plot.png', bbox_inches='tight', dpi=400, transparent=True)
597
  network_plot = PIL.Image.open('network_plot.png').convert('RGBA')
598
 
599
  return run_plot, network_plot
600
 
 
 
 
601
  import gradio as gr
602
  import matplotlib.pyplot as plt
603
 
 
 
604
  with gr.Blocks(theme=gr.themes.Monochrome()) as demo:
605
+ with gr.Column():
606
+ gr.Markdown("""# Simulate the emergence of social movements
607
+ Vary the parameters below, and click 'Run Simulation' to run.
608
+ """)
609
+ with gr.Row():
610
+ with gr.Column():
611
+ with gr.Group():
612
+ separate_agent_types = gr.Checkbox(value=False, label="Separate agent types in plot")
613
+
614
+ n_agents_slider = gr.Slider(minimum=100, maximum=500, step=10, label="Number of Agents", value=150)
615
+ share_regime_slider = gr.Slider(minimum=0.0, maximum=1.0, step=0.01, label="Share of Regime Supporters", value=0.4)
616
+ threshold_slider = gr.Slider(minimum=0.0, maximum=1.0, step=0.01, label="Threshold", value=0.5)
617
+ social_learning_slider = gr.Slider(minimum=0.0, maximum=2.0, step=0.1, label="Social Learning Factor", value=1.0)
618
+ steps_slider = gr.Slider(minimum=10, maximum=100, step=5, label="Simulation Steps", value=40)
619
+ half_life_slider = gr.Slider(minimum=5, maximum=50, step=5, label="Half-Life", value=20)
620
+
621
+ # Physical network settings
622
+ with gr.Group():
623
+ gr.Markdown("""**Physical Network Settings:**""")
624
+ introduce_physical_homophily_true_false = gr.Checkbox(value=False, label="Stipulate Homophily")
625
+
626
+ with gr.Group(visible=False) as homophily_group:
627
+ physical_homophily = gr.Slider(0, 1, label="Homophily", info='How much homophily to stipulate.')
628
+
629
+ def update_homophily_group_visibility(checkbox_state):
630
+ return {homophily_group: gr.Group(visible=checkbox_state)}
631
+
632
+ introduce_physical_homophily_true_false.change(
633
+ update_homophily_group_visibility,
634
+ inputs=introduce_physical_homophily_true_false,
635
+ outputs=homophily_group
636
+ )
637
+
638
+ physical_network_type = gr.Dropdown(label="Physical Network Type", value="Fully Connected",
639
+ choices=["Fully Connected", "Random Geometric", "Powerlaw"])
640
+
641
+ with gr.Group(visible=True) as physical_network_type_fully_connected_group:
642
+ gr.Markdown("""""")
643
+
644
+ with gr.Group(visible=False) as physical_network_type_random_geometric_group:
645
+ physical_network_type_random_geometric_radius = gr.Slider(minimum=.0, maximum=.5, label="Radius")
646
+
647
+ with gr.Group(visible=False) as physical_network_type_powerlaw_group:
648
+ physical_network_type_random_geometric_powerlaw_exponent = gr.Slider(minimum=.0, maximum=5.2, label="Powerlaw Exponent")
649
+
650
+ def update_sliders(option):
651
+ return {
652
+ physical_network_type_fully_connected_group: gr.Group(visible=option == "Fully Connected"),
653
+ physical_network_type_random_geometric_group: gr.Group(visible=option == "Random Geometric"),
654
+ physical_network_type_powerlaw_group: gr.Group(visible=option == "Powerlaw")
655
+ }
656
+
657
+ physical_network_type.change(
658
+ update_sliders,
659
+ inputs=physical_network_type,
660
+ outputs=[physical_network_type_fully_connected_group,
661
+ physical_network_type_random_geometric_group,
662
+ physical_network_type_powerlaw_group]
663
+ )
664
+
665
+ # Social media settings
666
+ use_social_media_network = gr.Checkbox(value=False, label="Use social media network")
667
+ with gr.Group(visible=False) as social_media_group:
668
+ gr.Markdown("""**Social Media Network Settings:**""")
669
+
670
+ social_media_factor = gr.Slider(0, 2, label="Social Media Factor",
671
+ info='Weight of social media vs learning in the real world.',
672
+ value=1.0)
673
+ introduce_social_media_homophily_true_false = gr.Checkbox(value=False, label="Stipulate Homophily")
674
+
675
+ with gr.Group(visible=False) as social_media_homophily_group:
676
+ social_media_homophily = gr.Slider(0, 1, label="Homophily", info='How much homophily to stipulate in social media network.')
677
+
678
+ def update_social_media_homophily_group_visibility(checkbox_state):
679
+ return {social_media_homophily_group: gr.Group(visible=checkbox_state)}
680
+
681
+ introduce_social_media_homophily_true_false.change(
682
+ update_social_media_homophily_group_visibility,
683
+ inputs=introduce_social_media_homophily_true_false,
684
+ outputs=social_media_homophily_group
685
+ )
686
+
687
+ social_media_network_type = gr.Dropdown(label="Social Media Network Type", value="Fully Connected",
688
+ choices=["Fully Connected", "Random Geometric", "Powerlaw"])
689
+
690
+ with gr.Group(visible=True) as social_media_network_type_fully_connected_group:
691
+ gr.Markdown("""""")
692
+
693
+ with gr.Group(visible=False) as social_media_network_type_random_geometric_group:
694
+ social_media_network_type_random_geometric_radius = gr.Slider(minimum=0.0, maximum=0.5, label="Radius")
695
+
696
+ with gr.Group(visible=False) as social_media_network_type_powerlaw_group:
697
+ social_media_network_type_powerlaw_exponent = gr.Slider(minimum=0.0, maximum=5.2, label="Powerlaw Exponent")
698
+
699
+ def update_social_media_network_sliders(option):
700
+ return {
701
+ social_media_network_type_fully_connected_group: gr.Group(visible=option == "Fully Connected"),
702
+ social_media_network_type_random_geometric_group: gr.Group(visible=option == "Random Geometric"),
703
+ social_media_network_type_powerlaw_group: gr.Group(visible=option == "Powerlaw")
704
+ }
705
+
706
+ social_media_network_type.change(
707
+ update_social_media_network_sliders,
708
+ inputs=social_media_network_type,
709
+ outputs=[social_media_network_type_fully_connected_group,
710
+ social_media_network_type_random_geometric_group,
711
+ social_media_network_type_powerlaw_group]
712
+ )
713
+
714
+ def update_social_media_group_visibility(checkbox_state):
715
+ return {social_media_group: gr.Group(visible=checkbox_state)}
716
+
717
+ use_social_media_network.change(
718
+ update_social_media_group_visibility,
719
+ inputs=use_social_media_network,
720
+ outputs=social_media_group
721
  )
722
 
723
+ with gr.Column():
724
+ button = gr.Button("Run Simulation")
725
+ plot_output = gr.Image(label="Simulation Result")
726
+ network_output = gr.Image(label="Networks")
727
+
728
+ def run_simulation_and_plot(*args):
729
+ fig = run_and_plot_simulation(*args)
730
+ return fig
731
+
732
+ button.click(
733
+ run_simulation_and_plot,
734
+ inputs=[
735
+ separate_agent_types,
736
+ n_agents_slider,
737
+ share_regime_slider,
738
+ threshold_slider,
739
+ social_learning_slider,
740
+ steps_slider,
741
+ half_life_slider,
742
+ physical_network_type_random_geometric_radius,
743
+ physical_network_type_random_geometric_powerlaw_exponent,
744
+ physical_network_type,
745
+ introduce_physical_homophily_true_false,
746
+ physical_homophily,
747
+ introduce_social_media_homophily_true_false,
748
+ social_media_homophily,
749
+ social_media_network_type_random_geometric_radius,
750
+ social_media_network_type_powerlaw_exponent,
751
+ social_media_network_type,
752
+ use_social_media_network,
753
+ social_media_factor, # NEW: now wired through
754
+ ],
755
+ outputs=[plot_output, network_output]
756
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
757
 
758
  # Launch the interface
759
  if __name__ == "__main__":
760
+ demo.launch(debug=True)