Evolife
Evolife has been developed to study Genetic algorithms, Natural evolution and behavioural ecology.
Curves.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2""" @brief Stores data that can be used to plot curves and stored into a file.
3"""
4
5#============================================================================#
6# EVOLIFE http://evolife.telecom-paris.fr Jean-Louis Dessalles #
7# Telecom Paris 2022-04-11 www.dessalles.fr #
8# -------------------------------------------------------------------------- #
9# License: Creative Commons BY-NC-SA #
10#============================================================================#
11# Documentation: https://evolife.telecom-paris.fr/Classes #
12#============================================================================#
13
14
15
18
19
20
21
22import sys
23if __name__ == '__main__': sys.path.append('../..') # for tests
24
25from functools import reduce
26from Evolife.Tools.Tools import transpose, error
27
28
29
33EvolifeColours = ['#808080', '#000000', '#FFFFFF', '#0000FF', '#FF0000', '#FFFF00', '#A06000', '#0080A0', '#FF80A0', '#94DCDC',
34 '#008000', '#009500', '#00AA00', '#00BF00', '#00D400', '#00E900', '#00FE00', '#64FF64', '#78FF78', '#8CFF8C', '#A0FFA0', '#B4FFB4',
35 '#800000', '#950000', '#AA0000', '#BF0000', '#D40000', '#E90000', '#FE0000', '#FF6464', '#FF7878', '#FF8C8C', '#FFA0A0', '#FFB4B4',
36 '#000080', '#101095', '#2020AA', '#3030BF', '#3838D4', '#4747E9', '#5555FE', '#6464FF', '#7878FF', '#8C8CFF', '#A0A0FF', '#B4B4FF',
37 ]
38EvolifeColourNames = ['grey', 'black', 'white', 'blue', 'red', 'yellow', 'brown', 'blue02', 'pink', 'lightblue',
39 'green', 'green1', 'green2', 'green3', 'green4', 'green5', 'green6', 'green7', 'green8', 'green9', 'green10', 'green11', # 21
40 'red0', 'red1', 'red2', 'red3', 'red4', 'red5', 'red6', 'red7', 'red8', 'red9', 'red10', 'red11', # 33
41 'blue0', 'blue1', 'blue2', 'blue3', 'blue4', 'blue5', 'blue6', 'blue7', 'blue8', 'blue9', 'blue10', 'blue11',
42 ]
43
44Shades = {'green':(10, 21), 'red':(22,33), 'blue':(34, 45)}
45
46def Shade(x, BaseColour='green', Min=0, Max=1, darkToLight=True, invisible='white'):
47 """ compute a shade for a given base colour
48 Green colours between 10 and 21
49 Red colours between 22 and 33
50 Blue colours between 34 and 45
51 """
52
53 Frame = lambda x, Range, Min, Max: int(((x - Min) * Range) / (Max - Min))
54
55 if Min != Max and x >= Min and x <= Max:
56 if BaseColour == 'grey':
57 if darkToLight:
58 return '#' + ('%02X' % Frame(x, 255, Min, Max)) * 3 # grey shades
59 else:
60 return '#' + ('%02X' % Frame(Max-x, 255, Min, Max)) * 3 # grey shades
61 shades = Shades[BaseColour]
62 if darkToLight:
63 return EvolifeColourNames[shades[0] + Frame(x, (shades[1] - shades[0]), Min, Max)]
64 else:
65 # return Shade(Max - x, BaseColour, Min, Max, True, invisible)
66 # return EvolifeColourNames[shades[1] - int(((Max - x) * (shades[1] - shades[0])) / (Max - Min))]
67 return EvolifeColourNames[shades[0] + Frame(Max - x, (shades[1] - shades[0]), Min, Max)]
68 return invisible
69
70def EvolifeColourID(Colour_designation, default=(4,'red')):
71 """ Recognizes Colour_designation as a number, a name, a (R,V,B) tuple or a #RRVVBB pattern.
72 Returns the recognized colour as a couple (Number, Name)
73 """
74 ID = None
75 try:
76 if str(Colour_designation).isdigit() and int(Colour_designation) in range(len(EvolifeColours)): # colour given by number
77 ID = int(Colour_designation)
78 return (ID, EvolifeColours[ID])
79 elif Colour_designation in EvolifeColourNames: # colour given by name
80 ID = EvolifeColourNames.index(Colour_designation)
81 return (ID, EvolifeColours[ID])
82 ColourCode = Colour_designation # colour probably given by code #RRGGBB
83 if isinstance(Colour_designation, tuple):
84 ColourCode = '#%02X%02X%02X' % Colour_designation
85 if ColourCode in EvolifeColours: # known colour
86 ID = EvolifeColours.index(ColourCode)
87 return (ID, EvolifeColours[ID])
88 return (0, ColourCode) # unknown colour
89 except (AttributeError, TypeError):
90 print('colour error', Colour_designation)
91 pass
92 return default
93
94
95
98class Stroke:
99 """ stores coordinates as: (x, y, colour, size)
100 Missing values are completed with default values
101 A fractional size value means a fraction of the reference size (typically window width)
102 A negative size value means that size is provided in logical coordinates
103 (and that the object should be resized when zoomed). Otherwise size means pixels.
104 """
105
106 def __init__(self, Coordinates, RefSize=None):
107 """ A Stroke is a point or shape.
108 It is represented by a tuple (x, y, colour, size).
109 Missing values are completed by default values.
110 A fractional size value means a fraction of the reference size (typically window width)
111 A negative size value means that size is provided in logical coordinates
112 (and that the object should be resized when zoomed). Otherwise size means pixels.
113 """
114 DefCoord = 10 # default value
115 DefColour = 4 # default value
116 DefSize = 3 # default value
117 DefaultStroke = (DefCoord, DefCoord, DefColour, DefSize)
118 self.PixelSize = True # by default, size is provided in pixels
119 if Coordinates:
120 self.Coord = Coordinates[:4] + DefaultStroke[min(len(Coordinates), 4):4] # completing with default values
121 (self.x, self.y, self.colour, self.size) = self.Coord
122 if RefSize and abs(self.size) < 1:
123 # Fractional size means a fraction of reference size (typically window width)
124 self.size = RefSize * self.size
125 if self.size < 0:
126 self.PixelSize = False # negative size means that size is provided in logical coordinates (and that the object might be resized when zoomed)
127 self.size = abs(self.size)
128 # self.size = max(1, self.size)
129 self.Coord = (self.x, self.y, self.colour, self.size)
130 else:
131 self.Coord = None
132 (self.x, self.y, self.colour, self.size) = 0,0,0,0
133
134 def point(self):
135 """ returns (x,y)
136 """
137 return (self.x, self.y)
138
139 def endpoint(self):
140 """ coordinates + size
141 """
142 return (self.x + self.size, self.y + self.size)
143
144 def scroll(self):
145 self.y -= 1
146 C1 = list(self.Coord)
147 C1[1] -=1
148 self.Coord = tuple(C1)
149
150 def __add__(self, Other): # allows to add with None
151 if Other.Coord: return self.Coord + Other.Coord
152 else: return self.Coord
153
154 def __str__(self): return '%s, %s, %s, %s' % (str(self.x), str(self.y), str(self.colour), str(self.size))
155
156
159class Curve:
160 """ Holds a complete (continuous) curve in memory
161 """
162 def __init__(self, colour, ID, ColName=None, Legend=None):
163 """ creation of a curve.
164 A curve is a list of successive connected positions + a list of dicontinuities
165 """
166 self.ID = ID # typically: number of the curve
167 self.colour = colour # Evolife colour
168 self.Name = str(ID) # default, but will receive a string
169 try: self.ColName = EvolifeColourNames[ID] # default
170 except IndexError: self.ColName = colour
171 if ColName is not None:
172 self.ColName = ColName
173 self.Name = ColName # second default
174 self.Legend = Legend if Legend is not None else self.Name
175 self.thick = 3 # thickness
176 self.erase()
177
178 def erase(self):
179 """ reset curve
180 """
181 self.start((0,0))
182
183 def start(self,StartPos):
184 """ A curve is a list of successive connected positions + a list of dicontinuities
185 """
186 self.CurrentPosition = 0 # Current position for reading
187 self.positions = [StartPos] # Stores successive points
189 self.currentDiscontinuity = 0 # to accelerate reading
190
191 def name(self, N = ""):
192 """ sets the curve's name
193 """
194 if N != "": self.Name = N
195 return self.Name
196
197 def legend(self, L=""):
198 """ sets the curve's caption
199 """
200 if L: self.Legend = L
201 return self.Legend
202
203 def last(self):
204 """ returns the last position in the curve
205 """
206 return self.positions[-1]
207
208 def add(self, Pos, Draw=True):
209 """ Adds a new position to the curve.
210 Notes a discontinuity if 'Draw' is False.
211 """
212 # print('adding %s to %s (draw=%s)' % (str(Pos), self.ColName, Draw))
213 if not Draw:
214 self.discontinuities.append(self.length())
215 self.positions.append(Pos)
216
217 def length(self):
218 return len(self.positions)
219
220 def X_coord(self):
221 """ list of x-coordinates
222 """
223 return tuple(map(lambda P: P[0], self.positions))
224
225 def Y_coord(self):
226 """ list of y-coordinates
227 """
228 return tuple(map(lambda P: round(P[1],3), self.positions))
229
230 def Avg(self, start=0):
231 """ compute average value of Y_coord
232 """
233 #ValidValues = [Y for Y in self.Y_coord()[start:] if Y >= 0]
234 try: ValidValues = [P[1] for P in self.positions if P[0] >= start and P[1] >= 0]
235 except TypeError:
236 print(start, self.positions)
237 if len(ValidValues):
238 return int(round(float(sum(ValidValues)) / len(ValidValues)))
239 else:
240 return 0
241
242 def __iter__(self):
243 # defines the class as an iterator
244 return self
245
246 def __next__(self):
247 """2.6-3.x version"""
248 return self.next()
249
250 def next(self):
251 """ Iteratively returns segments of the curve
252 """
253 # if self.CurrentPosition+1 in self.discontinuities:
254 if len(self.discontinuities) > self.currentDiscontinuity \
256 self.currentDiscontinuity += 1
257 # one segment must be skipped
258 self.CurrentPosition += 1
259 if self.length() < 2 or self.CurrentPosition >= self.length()-1:
260 self.CurrentPosition = 0 # ready for later use
261 self.currentDiscontinuity = 0
262 raise StopIteration
263 self.CurrentPosition += 1
264 return (self.positions[self.CurrentPosition-1], self.positions[self.CurrentPosition])
265
266 def __str__(self): return self.Name
267
268 def __repr__(self): return self.Name
269
270
273class Curves:
274 """ Stores a list of 'Curves'
275 """
276
277 def __init__(self):
278 """ Creates a list of curves matching all available Evolife colours
279 """
280 self.Colours = EvolifeColours
281 self.Curves = [Curve(Colour, Number, EvolifeColourNames[Number]) for (Number,Colour) in enumerate(EvolifeColours)]
282 self.UsedCurves = [] # curves actually used, ordered by first use
283
284
285 def start_Curve(self, Curve_id, location):
286 """ defines where a curve should start
287 """
288 try:
289 self.Curves[Curve_id].start(location)
290 except IndexError:
291 error("Curves: unknown Curve ID")
292
293 def CurveAddPoint(self, Curve_id, Point, Draw=True):
294 """ Adds a point to a Curve. Stores the Curve as "used"
295 """
296 if Curve_id not in self.UsedCurves:
297 self.UsedCurves.append(Curve_id)
298 # print(self.Curves[Curve_id])
299 self.Curves[Curve_id].add(Point, Draw=Draw)
300
301 def Curvenames(self, Names):
302 """ records names for Curves.
303 Names = list of (Colour, Name, Legend) tuples (Name and Legend replaced by '' if missing)
304 """
305 Str = '\nDisplay: \n\t'
306 self.UsedCurves = [] # reconstructing UsedCurves
307 try:
308 for Curve_description in Names:
309 (Curve_designation, Name, Legend) = Curve_description + (0, '', '')[len(Curve_description):]
310 CurveId = EvolifeColourID(Curve_designation, default=None)[0]
311 for P in self.Curves:
312 if P.ID == CurveId:
313 P.name(Name)
314 P.legend(Legend if Legend else Name)
315 Str += '\n\t%s:\t%s' % (P.ColName, P.legend())
316 break
317 self.UsedCurves.append(CurveId)
318 Str += '\n'
319 except IndexError:
320 error("Curves: unknown Curve ID")
321 return Str
322
323 def ActiveCurves(self):
324 """ returns actually used curves
325 """
326 # return [P for P in self.Curves if len(P.positions) > 1]
327 return [self.Curves[Cid] for Cid in self.UsedCurves]
328
329 def Legend(self):
330 """ returns tuples (ID, colour, colourname, curvename, legend) representing active curves
331 """
332 return [(P.ID, P.colour, P.ColName, P.Name, P.Legend) for P in self.ActiveCurves()]
333
334 def dump(self, ResultFileName=None, ResultHeader='', DumpStart=0):
335 """ Saves Curves to a file.
336 Average values are stored in a file with '_res' appended to ResultFileName
337 """
338 active_Curves = []
339 if ResultFileName == None: return {}
340 # DumpStart = points below this x-value are removed from the computation of average values
341
342 # dump: classification of Curves sharing x-coordinates
343 X_coordinates = list(set([P.X_coord() for P in self.Curves]))
344 X_coordinates.sort(key=lambda x: len(x), reverse=True)
345 if len(X_coordinates) <= 2:
346 # only one Curve or several Curves sharing x-coordinates
347 # active_Curves = [P for P in self.Curves if P.X_coord() == X_coordinates[0]]
348 active_Curves = [P for P in self.ActiveCurves() if P.X_coord() == X_coordinates[0]]
349 # print('saving Curves %s to %s' % (active_Curves, ResultFileName))
350 Coords = [('Year',) + tuple([P.name() for P in active_Curves])]
351 Coords += transpose([X_coordinates[0]] \
352 + [P.Y_coord() for P in active_Curves])
353 else:
354 active_Curves = self.ActiveCurves()
355 Coords = reduce(lambda x,y: x+y, [P.positions for P in self.Curves
356 if len(P.positions) > 1])
357
358 File_dump = open(ResultFileName + '.csv', 'w')
359 for C in Coords:
360 File_dump.write(';'.join([str(x) for x in C]))
361 File_dump.write('\n')
362 File_dump.close()
363
364 # editing the header
365 if ResultHeader:
366 HeaderLines = ResultHeader.split('\n')
367 HeaderLines[0] += 'LastStep;'
368 # Writing Curve names sorted by colours at the end of the first line
369 HeaderLines[0] += ';'.join([P.name() for P in active_Curves])
370 Header = '\n'.join(HeaderLines)
371 else: Header = ''
372
373 # storing average values
374 AvgStr = Header
375 try: AvgStr += '%d;' % active_Curves[0].X_coord()[-1] # storing actual max time value
376 except IndexError: pass
377 AvgStr += ';'.join([str(P.Avg(DumpStart)) for P in active_Curves])
378 AvgStr += '\n'
379
380 Averages = open(ResultFileName + '_res.csv', 'w').write(AvgStr)
381
382 # returning average values
383 ResultDict = dict()
384 try: ResultDict['LastStep'] = str(active_Curves[0].X_coord()[-1])
385 except IndexError: ResultDict['LastStep'] = 0
386 for P in active_Curves: ResultDict[P.name()] = str(P.Avg(DumpStart))
387 return ResultDict
388
389
390
391if __name__ == "__main__":
392
393 print(__doc__)
394
395__author__ = 'Dessalles'
Holds a complete (continuous) curve in memory.
Definition: Curves.py:159
def __init__(self, colour, ID, ColName=None, Legend=None)
creation of a curve.
Definition: Curves.py:162
def Y_coord(self)
list of y-coordinates
Definition: Curves.py:225
def add(self, Pos, Draw=True)
Adds a new position to the curve.
Definition: Curves.py:208
def erase(self)
reset curve
Definition: Curves.py:178
def __next__(self)
2.6-3.x version
Definition: Curves.py:246
def Avg(self, start=0)
compute average value of Y_coord
Definition: Curves.py:230
def name(self, N="")
sets the curve's name
Definition: Curves.py:191
def legend(self, L="")
sets the curve's caption
Definition: Curves.py:197
def next(self)
Iteratively returns segments of the curve.
Definition: Curves.py:250
def X_coord(self)
list of x-coordinates
Definition: Curves.py:220
def last(self)
returns the last position in the curve
Definition: Curves.py:203
def start(self, StartPos)
A curve is a list of successive connected positions + a list of dicontinuities.
Definition: Curves.py:183
Stores a list of 'Curves'.
Definition: Curves.py:273
def dump(self, ResultFileName=None, ResultHeader='', DumpStart=0)
Saves Curves to a file.
Definition: Curves.py:334
def ActiveCurves(self)
returns actually used curves
Definition: Curves.py:323
def start_Curve(self, Curve_id, location)
defines where a curve should start
Definition: Curves.py:285
def __init__(self)
Creates a list of curves matching all available Evolife colours.
Definition: Curves.py:277
def Legend(self)
returns tuples (ID, colour, colourname, curvename, legend) representing active curves
Definition: Curves.py:329
def CurveAddPoint(self, Curve_id, Point, Draw=True)
Adds a point to a Curve.
Definition: Curves.py:293
def Curvenames(self, Names)
records names for Curves.
Definition: Curves.py:301
Stroke: drawing element (point or segment) #.
Definition: Curves.py:98
def endpoint(self)
coordinates + size
Definition: Curves.py:139
def point(self)
returns (x,y)
Definition: Curves.py:134
def __add__(self, Other)
Definition: Curves.py:150
def EvolifeColourID(Colour_designation, default=(4, 'red'))
Recognizes Colour_designation as a number, a name, a (R,V,B) tuple or a #RRVVBB pattern.
Definition: Curves.py:70
def Shade(x, BaseColour='green', Min=0, Max=1, darkToLight=True, invisible='white')
compute a shade for a given base colour Green colours between 10 and 21 Red colours between 22 and 33...
Definition: Curves.py:46
Various functions.
Definition: Tools.py:1
def transpose(Matrix)
groups ith items in each list of Matrix
Definition: Tools.py:105
def error(ErrMsg, Explanation='')
Definition: Tools.py:182