Coverage for .tox/p311/lib/python3.10/site-packages/scicom/knowledgespread/agents.py: 0%

71 statements  

« prev     ^ index     » next       coverage.py v7.5.0, created at 2024-04-26 11:45 +0200

1 

2import mesa 

3import networkx as nx 

4 

5from scicom.knowledgespread.utils import ageFunction, epistemicRange 

6 

7 

8class ScientistAgent(mesa.Agent): 

9 """A scientist with an idea. 

10  

11 Each scientist has a geographic position, is related to other 

12 agents by a social network, and is intialized with a starttime, that 

13 describes the year at which the agent becomes active.  

14 """ 

15 

16 def __init__( 

17 self, 

18 unique_id, 

19 model, 

20 pos: tuple, # The projected position in 2D epistemic space. For actual movements and neighborhood calculations take into account z coordinate as well. 

21 topicledger: list, # Representing the mental model of the agent: A list of all visited topics represented as triples [(x,y,z), (x,y,z)] 

22 geopos: tuple, # Representing scientific affiliation, a tuple of latitude/longitude of current affiliation. Could also keep track of previous affiliations 

23 birthtime: int, # A step number that represents the timestep at which the scientist becomes active 

24 productivity: tuple, # Parameters determining the shape of the activation weight function 

25 opposition: bool = False, # Whether or not an agent is always oposing new epistemic positions. 

26 ): 

27 super().__init__(unique_id, model) 

28 self.pos = pos 

29 self.a = productivity[0] 

30 self.b = productivity[1] 

31 self.c = productivity[2] 

32 self.topicledger = topicledger 

33 self.geopos = geopos 

34 self.birthtime = birthtime 

35 self.age = 0 

36 self.opposition = opposition 

37 

38 def _currentActivationWeight(self) -> float: 

39 """Return an age dependent activation weight. 

40 

41 A bell-shaped function with a ramp, plateuax and decend. 

42 Can be drawn from random distribution in model initialization. 

43 """ 

44 return ageFunction(self, self.a, self.b, self.c, radius=1) 

45 

46 def _changeEpistemicPosition(self, neighbors): 

47 """Calculate the change in epistemic space. 

48 

49 From all neighbors select one random choice. 

50 To update the agents position, determine the heading  

51 towards the selected neighbor. If the agent is an 

52 oposing one, inverte the direction. Then select a  

53 random amount to move into the selected direction. 

54 The new position is noted down in the topic ledger 

55 an the the agent is moved. 

56 """ 

57 # Select random elemt from potential neighbors 

58 neighborID = self.random.choice(neighbors) 

59 if isinstance(neighborID, (float, int)): 

60 neighbors = [ 

61 x for x in self.model.schedule.agents if x.unique_id == neighborID 

62 ] 

63 if neighbors: 

64 neighbor = neighbors[0] 

65 else: 

66 return 

67 else: 

68 neighbor = neighborID 

69 # Get heading 

70 direction = self.model.space.get_heading(self.pos, neighbor.pos) 

71 # Some agents always opose the epistemic position and therefore move in the oposite direction 

72 if self.opposition is True: 

73 direction = (- direction[0], - direction[1]) 

74 # Select new postion with random amount into direction of neighbor 

75 amount = self.model.random.random() 

76 new_pos = (self.pos[0] + amount * direction[0], self.pos[1] + amount * direction[1]) 

77 # New mental position 

78 topic = (new_pos[0], new_pos[1], self.model.random.random()) 

79 try: 

80 # Move agent 

81 self.topicledger.append(topic) 

82 self.model.space.move_agent(self, new_pos) 

83 except: 

84 # Out of bounds of epi space 

85 # TODO: What is a sensible exception of movement in this case. 

86 # Current solution: Append topic to ledger but do not move 

87 self.topicledger.append(topic) 

88 

89 def updateSocialNetwork(self): 

90 """Create new links in agents social network.""" 

91 

92 def moveGeoSpace(self): 

93 pass 

94 

95 def moveSocSpace(self, maxDist=1): 

96 """Change epistemic position based on social network.""" 

97 neighbors = [] 

98 currentTime = self.model.schedule.time 

99 G = self.model.socialNetwork 

100 H = nx.Graph(((u, v, e) for u, v, e in G.edges(data=True) if e["time"] <= currentTime)) 

101 for dist in range(0, maxDist + 1, 1): 

102 neighb = nx.descendants_at_distance( 

103 H, 

104 self.unique_id, 

105 dist, 

106 ) 

107 neighbors.extend(list(neighb)) 

108 if neighbors: 

109 self._changeEpistemicPosition(neighbors) 

110 return True 

111 else: 

112 return False 

113 

114 def moveEpiSpace(self): 

115 """Change epistemic position based on distance in epistemic space.""" 

116 neighbors = self.model.space.get_neighbors( 

117 self.pos, 

118 radius=epistemicRange( 

119 self.model.epiRange, 

120 self.model.schedule.time - self.birthtime, 

121 ), 

122 ) 

123 if neighbors: 

124 self._changeEpistemicPosition(neighbors) 

125 else: 

126 # Random search for new epistemic position 

127 direction = (self.model.random.random(), self.model.random.random()) 

128 new_pos = self.model.random.random() * direction 

129 self.model.space.move_agent(self, new_pos) 

130 

131 def attendConference(self): 

132 pass 

133 

134 def step(self): 

135 """Agents activity starts after having reached the birthtime. 

136  

137 After initial start, at each step the agents age is increased by one. 

138 Each agent has a randomly generated age-dependent activation  

139 probability. Moveing happens first due to social connections. If  

140 no move due to social connections was possible, a move due to  

141 epistemic space search is attempted. 

142 

143 Once the possible activation weight drops below a threshold,  

144 the agent is removed from the schedule. 

145 """ 

146 if self.model.schedule.time < self.birthtime: 

147 pass 

148 elif self._currentActivationWeight() <= 0.00001 and self.age > 1: 

149 self.model.schedule.remove(self) 

150 # self.age += 1 

151 else: 

152 self.age += 1 

153 currentActivation = self.model.random.choices( 

154 population=[0, 1], 

155 weights=[ 

156 1 - self._currentActivationWeight(), self._currentActivationWeight(), 

157 ], 

158 k=1, 

159 ) 

160 if currentActivation[0] == 1: 

161 # TODO: Should the choice of movement be another random process? 

162 res = self.moveSocSpace() 

163 if res is False: 

164 self.moveEpiSpace()