Evolife
Evolife has been developed to study Genetic algorithms, Natural evolution and behavioural ecology.
SocialSimulation.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2""" @brief A basic framework to run social simulations.
3"""
4
5
6
7#============================================================================#
8# EVOLIFE http://evolife.telecom-paris.fr Jean-Louis Dessalles #
9# Telecom Paris 2022-04-11 www.dessalles.fr #
10# -------------------------------------------------------------------------- #
11# License: Creative Commons BY-NC-SA #
12#============================================================================#
13# Documentation: https://evolife.telecom-paris.fr/Classes #
14#============================================================================#
15
16
17from time import sleep
18from random import sample, randint, shuffle
19
20import sys
21import os.path
22sys.path.append('../../..') # to include path to Evolife
23
24from Evolife.Scenarii import Parameters
25from Evolife.Tools import Tools
26from Evolife.Ecology import Observer
27from Evolife.Social import Alliances
28from Evolife.Ecology import Learner
29from Evolife.Graphics import Evolife_Window, Evolife_Batch
30
31
32class Global(Parameters.Parameters):
33 """ Global elements, mainly parameters
34 """
35 def __init__(self, ConfigFile='_Params.evo'):
36 # Parameter values
37 Parameters.Parameters.__init__(self, ConfigFile)
38 self.Parameters = self # compatibility
39 self.ScenarioName = self['ScenarioName']
40 # Definition of interactions
41 self.Interactions = None # to be overloaded
42
43 def Dump_(self, PopDump, ResultFileName, DumpFeatures, ExpeID, Verbose=False):
44 """ Saves parameter values, then agents' investment in signalling, then agents' distance to best friend
45 """
46 if Verbose: print("Saving data to %s.*" % ResultFileName)
47 SNResultFile = open(ResultFileName + '_dmp.csv', 'w')
48 SNResultFile.write('%s\n' % ExpeID)
49 for Feature in DumpFeatures:
50 SNResultFile.write(";".join(PopDump(Feature)))
51 SNResultFile.write("\n")
52 SNResultFile.close()
53
54 # def Param(self, ParameterName): return self.Parameters.Parameter(ParameterName)
55
56
57class Social_Observer(Observer.Experiment_Observer):
58 """ Stores some global observation and display variables
59 """
60
61 def __init__(self, Parameters=None):
62 """ Experiment_Observer constructor
63 + declaration of an average social distance curve
64 + initialization of social links
65 """
66 Observer.Experiment_Observer.__init__(self, Parameters)
67 #additional parameters
68 self.Alliances = [] # social links, for display
69 if self.Parameter('AvgFriendDistance'):
70 self.curve('FriendDistance', Color='yellow', Legend='Avg distance to best friend')
71 # self.curve('SignalLevel', 50, Color='yellow', Legend='Avg Signal Level')
72
73 def get_data(self, Slot, Consumption=True):
74 """ Experiment_Observer's get_data + slot 'Network' that contains social links
75 """
76 if Slot == 'Network': return self.Alliances # returns stored links
77 return Observer.Experiment_Observer.get_data(self, Slot, Consumption=Consumption)
78
79 def hot_phase(self):
80 """ The hot phase is a limited amount of time, controlled by the 'LearnHorizon' parameter,
81 during which learning is faster.
82 """
83 return self.StepId < self.TimeLimit * self.Parameter('LearnHorizon') / 100.0
84
85class Social_Individual(Alliances.Follower, Learner.Learner):
86 """ A social individual has friends and can learn
87 """
88
89 def __init__(self, IdNb, features={}, maxQuality=100, parameters=None, SocialSymmetry=True):
90 """ Initalizes social links and learning parameters
91 """
92 if parameters: self.Param = parameters.Param
93 else: self.Param = None # but this will provoke an error
94 self.ID = "A%d" % IdNb # Identity number
95 if SocialSymmetry:
96 Alliances.Follower.__init__(self, self.Param('MaxFriends'), self.Param('MaxFriends'))
97 else:
98 Alliances.Follower.__init__(self, self.Param('MaxFriends'), self.Param('MaxFollowers'))
99 self.Quality = (100.0 * IdNb) / maxQuality # quality may be displayed
100 # Learnable features
101 Learner.Learner.__init__(self, features, MemorySpan=self.Param('MemorySpan'), AgeMax=self.Param('AgeMax'),
102 Infancy=self.Param('Infancy'), Imitation=self.Param('ImitationStrength'),
103 Speed=self.Param('LearningSpeed'), JumpProbability=self.Param('JumpProbability', 0),
104 Conservatism=self.Param('LearningConservatism'),
105 LearningSimilarity=self.Param('LearningSimilarity'),
106 toric=self.Param('Toric'), Start=self.Param('LearningStart', default=-1))
107 self.Points = 0 # stores current performance
108 self.update()
109
110 def Reset(self, Newborn=True):
111 """ called by Learner at initialization and when born again
112 Resets social links and learning experience
113 """
114 self.forgetAllforgetAll() # erase friendships
115 Learner.Learner.Reset(self, Newborn=Newborn)
116
117 def reinit(self):
118 """ called at the beginning of each year
119 sets Points to zero and possibly erases social links (based on parameter 'EraseNetwork')
120 """
121 # self.lessening_friendship() # eroding past gurus performances
122 if self.Param('EraseNetwork'): self.forgetAllforgetAll()
123 self.Points = 0
124
125 def update(self, infancy=True):
126 """ updates values for display
127 """
128 Colour = 'green%d' % int(1 + 10 * (1 - float(self.Age)/(1+self.Param('AgeMax'))))
129 if infancy and not self.adult(): Colour = 'red'
130 if self.Features: y = self.Features[self.Features.keys()[0]]
131 else: y = 17
132 self.location = (self.Quality, y, Colour, 2)
133
134
135 def Interact(self, Partner):
136 """ to be overloaded
137 """
138 pass
139 return True
140
141 def assessment(self):
142 """ Social benefit from having friends - called by Learning
143 (to be overloaded)
144 """
145 pass
146
147 def __str__(self):
148 return "%s[%s]" % (self.ID, str(self.Features))
149
151 """ defines a population of interacting agents
152 """
153 def __init__(self, parameters, NbAgents, Observer, IndividualClass=None, features={}):
154 """ creates a population of social individuals
155 """
156 if IndividualClass is None: IndividualClass = Social_Individual
157 self.Features = features
158 self.Pop = [IndividualClass(IdNb, maxQuality=NbAgents, features=features.keys(),
159 parameters=parameters) for IdNb in range(NbAgents)]
160 self.PopSize = NbAgents
161 self.Obs = Observer
162 self.Param = parameters.Param
163 self.NbGroup = parameters.get('NbGroup', 1) # number of groups
164
165 def positions(self):
166 """ returns the list of agents' locations
167 """
168 return [(A.ID, A.location) for A in self.Pop]
169
170 def neighbours(self, Agent):
171 """ Returns agents of neighbouring qualitied
172 """
173 AgentQualityRank = self.Pop.index(Agent)
174 return [self.Pop[NBhood] for NBhood in [AgentQualityRank - 1, AgentQualityRank + 1]
175 if NBhood >= 0 and NBhood < self.PopSize]
176
177 def SignalAvg(self):
178 """ average signal in the population
179 """
180 Avg = 0
181 for I in self.Pop: Avg += I.SignalLevel
182 if self.Pop: Avg /= len(self.Pop)
183 return Avg
184
185 def FeatureAvg(self, Feature):
186 """ average value of Feature's value
187 """
188 Avg = 0
189 for I in self.Pop: Avg += I.feature(Feature)
190 if self.Pop: Avg /= len(self.Pop)
191 return Avg
192
193 def FriendDistance(self):
194 """ average distance between friends
195 """
196 FD = []
197 for I in self.Pop:
198 BF = I.best_friend()
199 if BF: FD.append(abs(I.Quality - BF.Quality))
200 if FD: return sum(FD) / len(FD)
201 return 0
202
203 def display(self):
204 """ Updates agents positions and social links for display.
205 Updates average feature values for curves
206 """
207 if self.Obs.Visible(): # Statistics for display
208 for agent in self.Pop:
209 agent.update(infancy=self.Obs.hot_phase()) # update location for display
210 # self.Obs.Positions[agent.ID] = agent.location # Observer stores agent location
211 # ------ Observer stores social links
212 self.Obs.Alliances = [(agent.ID, [T.ID for T in agent.social_signature()]) for agent in self.Pop]
213 self.Obs.record(self.positions(), Window='Field')
214 # self.Obs.curve('SignalLevel', self.SignalAvg())
215 if self.Param('AvgFriendDistance'): self.Obs.curve('FriendDistance', self.FriendDistance())
216 Colours = ['brown', 'blue', 'red', 'green', 'white']
217 for F in sorted(list(self.Features.keys())):
218 self.Obs.curve(F, self.FeatureAvg(F), Color=self.Features[F][0],
219 Legend='Avg of %s' % F)
220
222 """ tells agents to reinitialize each year
223 """
224 for agent in self.Pop: agent.reinit()
225
226 def interactions(self, group=None, NbInteractions=None):
227 """ interactions occur within a group
228 """
229 if NbInteractions is None: NbInteractions = self.Param('NbInteractions')
230 if group is None: group = self.Pop
231 for encounter in range(NbInteractions):
232 Player, Partner = sample(group, 2)
233 Player.Interact(Partner)
234
235 def learning(self):
236 """ called at each 'run', several times per year
237 """
238 Learners = sample(self.Pop, Tools.chances(self.Param('LearningProbability')/100.0, len(self.Pop)))
239 # ------ some agents learn
240 for agent in self.Pop:
241 agent.assessment() # storing current scores (with possible cross-benefits)
242 if agent in Learners:
243 agent.Learns(self.neighbours(agent), hot=self.Obs.hot_phase())
244 agent.update() # update location for display
245 for agent in self.Pop: # now cross-benefits are completed
246 agent.wins(agent.Points) # Stores points for learning
247
248 def One_Run(self):
249 """ This procedure is repeatedly called by the simulation thread.
250 It increments the year through season().
251 Then for each run (there are NbRunPerYear runs each year),
252 interactions take place within groups
253 """
254 # ====================
255 # Display
256 # ====================
257 self.Obs.season() # increments year
258 # ====================
259 # Interactions
260 # ====================
261 for Run in range(self.Param('NbRunPerYear')):
263
264 # ------ interactions witing groups
265 GroupLength = len(self.Pop) // self.NbGroup
266 if self.NbGroup > 1:
267 Pop = self.Pop[:]
268 shuffle(Pop)
269 else: Pop = self.Pop
270 for groupID in range(self.NbGroup): # interaction only within groups
271 group = Pop[groupID * GroupLength: (groupID + 1) * GroupLength]
272 self.interactions(group)
273
274 # ------ learning
275 self.learning()
276
277 self.display()
278 return True # This value is forwarded to "ReturnFromThread"
279
280 def __len__(self): return len(self.Pop)
281
282
283def Start(Params=None, PopClass=Social_Population, ObsClass=Social_Observer, DumpFeatures=None, Windows='FNC'):
284 """ Launches the simulation
285 """
286 if Params is None: Params = Global()
287 Observer_ = ObsClass(Params) # Observer contains statistics
288 Observer_.setOutputDir('___Results')
289 Views = []
290 if 'F' in Windows: Views.append(('Field', 770, 70, 520, 370))
291 if 'N' in Windows: Views.append(('Network', 530, 200))
292 # if 'T' in Windows: Views.append(('Trajectories', 500, 350))
293 Observer_.recordInfo('DefaultViews', Views) # Evolife should start with that window open
294 # Observer.record((100, 100, 0, 0), Window='Field') # to resize the field
295 Pop = PopClass(Params, Params['NbAgents'], Observer_) # population of agents
296 if DumpFeatures is None: DumpFeatures = list(Pop.Features.keys()) + ['DistanceToBestFriend']
297 Observer_.recordInfo('DumpFeatures', DumpFeatures)
298 BatchMode = Params['BatchMode']
299
300 if BatchMode:
301
308 Evolife_Batch.Start(Pop.One_Run, Observer_)
309 else:
310
313 """ launching window
314 """
315 try:
316 Evolife_Window.Start(Pop.One_Run, Observer_, Capabilities=Windows+'P',
317 Options={'Background':Params.get('Background', 'lightblue')})
318 except Exception as Msg:
319 from sys import excepthook, exc_info
320 excepthook(exc_info()[0],exc_info()[1],exc_info()[2])
321 input('[Entree]')
322
323 Params.Dump_(Pop.Dump, Observer_.get_info('ResultFile'), Observer_.get_info('DumpFeatures'),
324 Observer_.get_info('ExperienceID'), Verbose = not BatchMode)
325 if not BatchMode: print("Bye.......")
326 # sleep(2.1)
327 return
328
329
330
331if __name__ == "__main__":
332 Gbl = Global()
333 if Gbl['RandomSeed'] > 0: random.seed(Gbl['RandomSeed'])
334 Start(Gbl)
335
336
337
338__author__ = 'Dessalles'
Augmented version of Friends for asymmetrical links - replaces 'Alliances'.
Definition: Alliances.py:320
def forgetAll(self)
calls 'detach' for self's followers and then for self.
Definition: Alliances.py:404
def forgetAll(self)
The individual quits its friends
Definition: Alliances.py:297
Global elements, mainly parameters.
def __init__(self, ConfigFile='_Params.evo')
def Dump_(self, PopDump, ResultFileName, DumpFeatures, ExpeID, Verbose=False)
Saves parameter values, then agents' investment in signalling, then agents' distance to best friend.
A social individual has friends and can learn.
def Interact(self, Partner)
to be overloaded
def __init__(self, IdNb, features={}, maxQuality=100, parameters=None, SocialSymmetry=True)
Initalizes social links and learning parameters.
def assessment(self)
Social benefit from having friends - called by Learning (to be overloaded)
def reinit(self)
called at the beginning of each year sets Points to zero and possibly erases social links (based on p...
def update(self, infancy=True)
updates values for display
def Reset(self, Newborn=True)
called by Learner at initialization and when born again Resets social links and learning experience
Stores some global observation and display variables.
def __init__(self, Parameters=None)
Experiment_Observer constructor.
def get_data(self, Slot, Consumption=True)
Experiment_Observer's get_data + slot 'Network' that contains social links.
def hot_phase(self)
The hot phase is a limited amount of time, controlled by the 'LearnHorizon' parameter,...
defines a population of interacting agents
def FeatureAvg(self, Feature)
average value of Feature's value
def learning(self)
called at each 'run', several times per year
def __init__(self, parameters, NbAgents, Observer, IndividualClass=None, features={})
creates a population of social individuals
def interactions(self, group=None, NbInteractions=None)
interactions occur within a group
def season_initialization(self)
tells agents to reinitialize each year
def FriendDistance(self)
average distance between friends
def neighbours(self, Agent)
Returns agents of neighbouring qualitied.
def positions(self)
returns the list of agents' locations
def One_Run(self)
This procedure is repeatedly called by the simulation thread.
def SignalAvg(self)
average signal in the population
def display(self)
Updates agents positions and social links for display.
def Start(Params=None, PopClass=Social_Population, ObsClass=Social_Observer, DumpFeatures=None, Windows='FNC')
Launches the simulation.