SocialDynamics_Lab / tests /test_simulation.py
ImAnouar1's picture
Upload 16 files
1025a87 verified
Raw History Blame Contribute Delete
5.88 kB
from dataclasses import replace
import networkx as nx
import numpy as np
from socialdynamics.experiments import aggregate, beta_sweep, run_ensemble, summarise_sweep
from socialdynamics.presets import PRESETS
from socialdynamics.simulation import OpinionSimulation, SimulationConfig
def _preset_config(name: str, **overrides) -> SimulationConfig:
p = dict(PRESETS[name])
p["misinformation_direction"] = 1.0 if p["misinformation_direction"].startswith("Positive") else -1.0
p.update(overrides)
return SimulationConfig(**p)
def _reference_step_v11(sim: OpinionSimulation, old: np.ndarray) -> np.ndarray:
"""The v1.1 per-agent loop, kept verbatim as a regression oracle."""
cfg = sim.config
new = old.copy()
beta = 6.0 * cfg.confirmation_bias
for agent in range(cfg.n_agents):
if sim.stubborn_mask[agent]:
continue
neighbors = list(sim.graph.neighbors(agent))
if neighbors:
w = np.exp(-beta * np.abs(old[agent] - old[neighbors]))
total = float(np.sum(w))
target = float(np.sum(w * old[neighbors]) / total) if total > 1e-12 else float(old[agent])
else:
target = float(old[agent])
delta = cfg.social_influence * (target - old[agent])
if sim.misinformation_mask[agent] and cfg.misinformation_strength > 0:
delta += 0.22 * cfg.misinformation_strength * (cfg.misinformation_direction - old[agent])
if cfg.noise > 0:
delta += float(sim._ref_rng.normal(0.0, cfg.noise))
new[agent] = float(np.clip(old[agent] + delta, -1.0, 1.0))
return new
def test_vectorised_update_matches_v11_loop():
for kw in ({}, {"network": "Scale Free", "initial_opinion": "Polarized"}, {"stubborn_fraction": 0.2}):
cfg = SimulationConfig(steps=40, seed=17, **kw)
fast = OpinionSimulation(cfg)
ref = OpinionSimulation(cfg)
ref._ref_rng = ref.rng
x = ref.opinions.copy()
for _ in range(cfg.steps):
fast.step()
x = _reference_step_v11(ref, x)
assert np.max(np.abs(fast.opinions - x)) < 1e-12
def test_simulation_is_seed_deterministic():
for rate in (0.0, 0.2):
cfg = SimulationConfig(n_agents=100, steps=20, seed=123, rewiring_rate=rate)
a, b = OpinionSimulation(cfg), OpinionSimulation(cfg)
a.run()
b.run()
assert np.array_equal(a.history_array, b.history_array)
assert set(map(frozenset, a.graph.edges())) == set(map(frozenset, b.graph.edges()))
def test_opinions_stay_bounded():
cfg = SimulationConfig(n_agents=100, steps=50, seed=3, misinformation_exposure=1.0,
misinformation_strength=1.0, social_influence=1.0, noise=0.2)
sim = OpinionSimulation(cfg)
sim.run()
assert np.all(sim.history_array >= -1.0)
assert np.all(sim.history_array <= 1.0)
def test_stubborn_agents_never_move():
cfg = SimulationConfig(n_agents=100, steps=30, seed=4, stubborn_fraction=0.20, stubborn_mode="Split extremes",
misinformation_exposure=0.50, misinformation_strength=0.8, rewiring_rate=0.3)
sim = OpinionSimulation(cfg)
initial = sim.opinions.copy()
mask = sim.stubborn_mask.copy()
sim.run()
assert np.array_equal(sim.opinions[mask], initial[mask])
def test_misinformation_can_shift_mean_positive():
base = dict(n_agents=100, network="Random", initial_opinion="Neutral", stubborn_fraction=0.0,
confirmation_bias=0.2, social_influence=0.4, noise=0.0, steps=30, seed=11)
control = OpinionSimulation(SimulationConfig(**base, misinformation_exposure=0.0, misinformation_strength=0.0))
treatment = OpinionSimulation(SimulationConfig(**base, misinformation_exposure=1.0, misinformation_strength=0.8))
control.run()
treatment.run()
assert treatment.opinions.mean() > control.opinions.mean() + 0.1
def test_rewiring_conserves_edges_and_creates_no_isolates():
sim = OpinionSimulation(_preset_config("Echo Chamber"))
edges0 = sim.graph.number_of_edges()
degree0 = dict(sim.graph.degree())
sim.run()
assert sim.rewired_ties > 0
assert sim.graph.number_of_edges() == edges0
assert not list(nx.isolates(sim.graph))
assert nx.number_of_selfloops(sim.graph) == 0
assert sum(dict(sim.graph.degree()).values()) == sum(degree0.values())
def test_rewiring_does_not_perturb_the_noise_stream():
cfg = SimulationConfig(steps=1, seed=8)
a = OpinionSimulation(cfg)
b = OpinionSimulation(replace(cfg, rewiring_rate=0.5))
a.step()
b.step()
assert np.array_equal(a.opinions, b.opinions) # first update precedes any rewiring
def test_filter_alone_does_not_segregate_but_rewiring_does():
seeds = [42, 43, 44]
filtered = run_ensemble(_preset_config("Filtered, Not Segregated"), seeds, n_permutations=100)
chamber = run_ensemble(_preset_config("Echo Chamber"), seeds, n_permutations=100)
for run in filtered:
assert run["selective_exposure"] > 0.9 # strong filter...
assert abs(run["assortativity"]) < 0.2 # ...but no clustering
assert run["assortativity_z"] < 3.0
for run in chamber:
assert run["assortativity"] > 0.9
assert run["assortativity_z"] > 10.0
# and the old index would have ranked this *below* the unsegregated case
assert run["selective_exposure"] < min(r["selective_exposure"] for r in filtered)
def test_sweep_and_aggregate_shapes():
base = SimulationConfig(steps=20)
rows = beta_sweep(base, (0.0, 1.0), (0.0, 0.1), seeds=[1, 2], n_permutations=20)
assert len(rows) == 8
summary = summarise_sweep(rows, ("assortativity", "selective_exposure"))
assert len(summary) == 4
assert all(r["n"] == 2 for r in summary)
agg = aggregate(rows, ("assortativity",))
assert agg["assortativity_ci95"] >= 0.0