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

1

2 from scicom.knowledgespread.utils import epistemicRange, ageFunction

3 import networkx as nx

4 import mesa

5

6

7 class ScientistAgent(mesa.Agent):

8 """A scientist with an idea.

9

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

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

12 describes the year at which the agent becomes active.

13 """

14

15 def __init__(

16 self,

17 unique_id,

18 model,

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

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

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

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

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

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

25 ):

26 super().__init__(unique_id, model)

27 self.pos = pos

28 self.a = productivity[0]

29 self.b = productivity[1]

30 self.c = productivity[2]

31 self.topicledger = topicledger

32 self.geopos = geopos

33 self.birthtime = birthtime

34 self.age = 0

35 self.opposition = opposition

36

37 def _currentActivationWeight(self):

38 """Returns an age dependent activation weight.

39

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

41 Can be drawn from random distribution in model initialization.

42 """

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

44

45 def _changeEpistemicPosition(self, neighbors):

46 """Calculate the change in epistemic space.

47

48 From all neighbors select one random choice.

49 To update the agents position, determine the heading

50 towards the selected neighbor. If the agent is an

51 oposing one, inverte the direction. Then select a

52 random amount to move into the selected direction.

53 The new position is noted down in the topic ledger

54 an the the agent is moved.

55 """

56 # Select random elemt from potential neighbors

57 neighborID = self.random.choice(neighbors)

58 if isinstance(neighborID, float) or isinstance(neighborID, int):

59 neighbors = [

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

61 ]

62 if neighbors:

63 neighbor = neighbors[0]

64 else:

65 return

66 else:

67 neighbor = neighborID

68 # Get heading

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

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

71 if self.opposition is True:

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

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

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

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

76 # New mental position

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

78 try:

79 # Move agent

80 self.topicledger.append(topic)

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

82 except:

83 # Out of bounds of epi space

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

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

86 self.topicledger.append(topic)

87

88 def updateSocialNetwork(self):

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

90 pass

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 pass

152 else:

153 self.age += 1

154 currentActivation = self.model.random.choices(

155 population=[0, 1],

156 weights=[

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

158 ],

159 k=1

160 )

161 if currentActivation[0] == 1:

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

163 res = self.moveSocSpace()

164 if res is False:

165 self.moveEpiSpace()