Coverage for src/scicom/knowledgespread/model.py: 0%
71 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-15 13:26 +0200
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-15 13:26 +0200
1import networkx as nx
2import mesa
3from scicom.knowledgespread.agents import ScientistAgent
4from scicom.knowledgespread.utils import GenerateInitalPopulation, GenerateSocNet
7def getActiveAgents(model):
8 """Get all agents active at time t."""
9 active = 0
10 for x in model.schedule.agents:
11 if x.birthtime <= model.schedule.time:
12 active += 1
13 return active
16def getNetworkStructure(model):
17 return [len(x) for x in nx.community.louvain_communities(model.socialNetwork)]
20class KnowledgeSpread(mesa.Model):
21 """A model for knowledge spread.
23 Agents have an initial topic vector and are positioned in epistemic space.
24 The number of agents can grow linearly, as a s-curve, or exponentially.
25 Agents initial positions on epistemic space can be diverse (checker board-like),
26 around a central position or in opossing camps.
28 Agents activation probability is age-dependent. After reaching a personal productivity end,
29 agents are removed from the scheduler.
31 """
33 def __init__(
34 self,
35 num_scientists: int = 100,
36 num_timesteps: int = 20,
37 epiDim: float = 1.0001, # This allows the boundary to be equal +/- one, will raise an exception otherwise
38 epiRange: float = 0.01, # Range of visibility in epistemic space.
39 oppositionPercent: float = 0.05, # Weight for random draw of opossing agents
40 loadInitialConditions: bool = False,
41 epiInit: str = "complex",
42 timeInit: str = "saturate",
43 beta: int = 8,
44 slope: int = 5,
45 base: int = 2,
46 ):
48 self.numScientists = num_scientists
49 self.numTimesteps = num_timesteps
50 self.epiRange = epiRange
51 self.opposPercent = oppositionPercent
52 self.loadInitialConditions = loadInitialConditions
53 self.epiInit = epiInit
54 self.timeInit = timeInit
55 self.beta = beta
56 self.slope = slope
57 self.base = base
59 # Random Schedule
60 self.schedule = mesa.time.RandomActivation(self)
62 # Epistemic layer space
63 self.space = mesa.space.ContinuousSpace(
64 epiDim, epiDim, False, -epiDim, -epiDim
65 )
67 # Create initial setup of agents
68 # TODO: The topic vector could be a higher dimensional vector derived from text embeddings.
69 self._setupAgents()
71 # Create agents from initial conditions.
72 agents = []
73 for ix, cond in self.initpop.iterrows():
74 opose = self.random.choices([False, True], weights=[1 - self.opposPercent, self.opposPercent], k=1)
75 # TODO: The distribution of productivity length could be empirically motivated.
76 prodlen = self.random.choices(list(range(15, 55, 1)), k=1)
77 agent = ScientistAgent(
78 unique_id=cond["id"],
79 model=self,
80 pos=(cond["x"], cond["y"]),
81 topicledger=[(cond["x"], cond["y"], cond["z"])],
82 geopos=(45, 45),
83 birthtime=cond["t"],
84 opposition=opose[0],
85 productivity=(7, 7, prodlen[0])
86 )
87 agents.append(agent)
89 # Setup social layer connections and space
90 edges = self._setupSocialSpace()
91 self.socialNetwork = nx.from_pandas_edgelist(
92 edges,
93 source='from_id',
94 target='to_id',
95 edge_attr=["time", "dist"]
96 )
97 # TODO: What is the effect of the GRID dimensions on social dynamics?
98 self.grid = mesa.space.MultiGrid(1000, 1000, torus=False)
100 # Add agents to epistemic space and schedule.
101 for agent in agents:
102 self.space.place_agent(
103 agent, pos=agent.pos
104 )
105 x = self.random.randrange(self.grid.width)
106 y = self.random.randrange(self.grid.height)
107 self.grid.place_agent(
108 agent, (x, y)
109 )
110 self.schedule.add(agent)
112 # Setup data collection
113 self.datacollector = mesa.DataCollector(
114 model_reporters={
115 "Active Agents": lambda m: getActiveAgents(m),
116 "Graph structure": lambda m: getNetworkStructure(m),
117 },
118 )
120 # Start in running mode
121 self.running = True
123 def _setupAgents(self):
124 """Create initial setup of agents."""
125 if self.loadInitialConditions is False:
126 epiInit = self.epiInit
127 timeInit = self.timeInit
128 beta = self.beta
129 slope = self.slope
130 base = self.base
131 else:
132 raise NotImplementedError("The reading-in of external initial conditions is not implemented yet. Come back later!")
133 epiOpt = ["complex", "central", "polarized"]
134 timeOpt = ["saturate", "linear", "exponential"]
135 if epiInit in epiOpt and timeInit in timeOpt:
136 generate = GenerateInitalPopulation(self.numScientists, self.numTimesteps)
137 initalPop = generate.sample(
138 fcE=epiInit,
139 fcT=timeInit,
140 beta=beta,
141 slope=slope,
142 base=base
143 )
144 self.initpop = initalPop
145 else:
146 raise KeyError(f"Choose epiInit from {epiOpt} and timeInit from {timeOpt}.")
148 def _setupSocialSpace(self, nEdges=4, density=0.2, densityGrowth=0):
149 """Setup initial social connections."""
150 genSoc = GenerateSocNet(self.initpop)
151 socNet = genSoc.run(
152 nEdges=nEdges,
153 density=density,
154 densityGrowth=densityGrowth
155 )
156 return socNet
158 def step(self):
159 """Run one simulation step."""
160 if not len(self.schedule.agents) > 1:
161 self.running = False
162 else:
163 self.schedule.step()
164 self.datacollector.collect(self)
166 def run(self, n):
167 """Run model for n steps."""
168 for _ in range(n):
169 self.step()