Coverage for src/scicom/knowledgespread/model.py: 0%

1 import networkx as nx

2 import mesa

3 from scicom.knowledgespread.agents import ScientistAgent

4 from scicom.knowledgespread.utils import GenerateInitalPopulation, GenerateSocNet

5

6

7 def 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

14

15

16 def getNetworkStructure(model):

17 return [len(x) for x in nx.community.louvain_communities(model.socialNetwork)]

18

19

20 class KnowledgeSpread(mesa.Model):

21 """A model for knowledge spread.

22

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.

27

28 Agents activation probability is age-dependent. After reaching a personal productivity end,

29 agents are removed from the scheduler.

30

31 """

32

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 ):

47

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

58

59 # Random Schedule

60 self.schedule = mesa.time.RandomActivation(self)

61

62 # Epistemic layer space

63 self.space = mesa.space.ContinuousSpace(

64 epiDim, epiDim, False, -epiDim, -epiDim

65 )

66

67 # Create initial setup of agents

68 # TODO: The topic vector could be a higher dimensional vector derived from text embeddings.

69 self._setupAgents()

70

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)

88

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)

99

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)

111

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 )

119

120 # Start in running mode

121 self.running = True

122

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}.")

147

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

157

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)

165

166 def run(self, n):

167 """Run model for n steps."""

168 for _ in range(n):

169 self.step()