1"""
2Plot Evolution Live
3===================
4
5This program is spawned by a bash script to run parallel to our main program when performing an evolution.
6Plots of the active evolution are created and updated in this script using matplotlib.
7"""
8
9import matplotlib.pyplot as plt
10import matplotlib.animation as animation
11from matplotlib import style
12from matplotlib import colormaps # type: ignore
13import configparser
14import re
15import math
16import numpy as np
17import sys
18from PlotConfig import PlotConfig
19from utilities import determine_color
20from os.path import exists
21from os import mkdir
22import argparse
23
24"""
25Static parameters can be found and changed in the config.ini file in the root project folder
26DO NOT CHANGE THEM HERE
27"""
28
29MAX_VIOLIN_PLOTS = 11
30HEATMAP_BINS = 40
31
32config = PlotConfig('./workspace/plot_config.ini')
33FRAME_INTERVAL = None # Filled in by argparse later
34
35
[docs]
36def run():
37 """Temporary function to run all of Plot Evolution Live."""
38
39 # ArgParse was put in the run() function to allow sphinx to document this code (which is probably of little value). This may or may not be temporary.
40 arg_parser = argparse.ArgumentParser()
41 arg_parser.add_argument("-f", "--frame-interval", required=False, default=10000)
42 args = arg_parser.parse_args()
43 FRAME_INTERVAL = int(args.frame_interval)
44
45
46 def animate_generation(i):
47 graph_data = open('workspace/alllivedata.log','r').read()
48 lines = graph_data.split('\n')
49 xs = []
50 ys = []
51
52 is_transparent = False
53 for line in lines:
54 if len(line) > 1:
55 x, y, z = line.split(',')
56 all_ys = y.split(';')
57 if len(all_ys) > 1:
58 is_transparent = True
59 for y in all_ys:
60 xs.append(int(x))
61 ys.append(float(y))
62 avg = 0.0
63 if len(ys) > 0:
64 avg = sum(ys)/len(ys)
65
66 ax1.clear()
67 ax1.set_xlim([0, config.get_population_size()+1]) # type: ignore
68 # ax1.set_xticks(range(1, config.get_population_size(), 1))
69 ax1.hlines(y=avg, xmin=0, xmax=config.get_population_size()+1, color="violet", linestyles="dotted")
70 ax1.scatter(xs, ys, color=((accent_color2 + "dd") if is_transparent else (accent_color2 + "ff") ))
71 if config.is_pulse_count():
72 title = 'Circuit Pulses this Generation'
73 ylabel = 'Pulses'
74 # Add a line for desired frequency
75 if config.is_pulse_count():
76 ax1.hlines(y=config.get_desired_frequency(), xmin=1, xmax=config.get_population_size(), color="red", linestyles="dotted")
77 ax1.set_ylim([0, None]) # type: ignore
78 else:
79 title = 'Circuit Fitness this Generation'
80 ylabel = 'Fitness'
81 ax1.set(xlabel='Circuit Number', ylabel=ylabel, title=title)
82
83 if formal:
84 ax1.legend(['Average Fitness', 'Individual Fitness', 'Target Fitness'],\
85 bbox_to_anchor=(1.05, 0.5), loc="center left", borderaxespad=0)
86
87 def animate_epoch(i):
88 graph_data = open('workspace/bestlivedata.log','r').read()
89 lines = graph_data.split('\n')
90 xs = []
91 ys = []
92 zs = []
93 ws = []
94 ts = []
95 ds = []
96 for line in lines:
97 if len(line) > 1:
98 x, y, z, w, t, d = line.split(',')
99 xs.append(int(x))
100 ys.append(float(y))
101 zs.append(float(z))
102 ws.append(float(w))
103 ts.append(float(t))
104 ds.append(float(d))
105 ax2.clear()
106 # ax2.set_yscale('symlog')
107 if config.using_transfer_interval():
108 for i in range(0,len(lines),config.get_transfer_interval()):
109 ax2.axvline(x=i, color=accent_color, linestyle="dashed")
110
111 plots = []
112 labels = []
113 # Plot the overall best before the gen best so the gen best line appears on top
114 plots += ax2.plot(xs, ts, color="#00b87d") # Ovr best Fitness
115 labels.append("Overall Best")
116 plots += ax2.plot(xs, ys, color="green") # Generation/Epoch Best Fitness
117 plots += ax2.plot(xs, zs, color="red") # Generation Worst Fitness
118 plots += ax2.plot(xs, ws, color=yellow) # Generation Average Fitness
119 labels.append("Best")
120 labels.append("Worst")
121 labels.append("Average")
122 ax2.tick_params(axis='y', labelcolor=accent_color)
123
124 if config.get_diversity_measure() != "NONE":
125 ax3.clear()
126 plots += ax3.plot(xs, ds, color="#5a70ed") # Generation diversity measure
127 ax3.tick_params(axis='y', labelcolor='#5a70ed')
128 ax3.set_ylabel('Diversity', color='#5a70ed')
129 ax3.set_ylim(bottom=0)
130 ax3.yaxis.set_label_position("right")
131
132 ax2.set(xlabel='Generation', ylabel='Fitness', title='Circuit Fitness per Generation')
133
134 if formal:
135 ax2.legend(plots, labels, bbox_to_anchor=(1.15, 0.5), loc="center left", borderaxespad=0)
136
137 if(config.get_save_plots()):
138 fig.savefig(str(plots_dir.joinpath("1_main.png")), bbox_inches="tight")
139
140 def animate_epoch_pulses(i):
141 graph_data = open('workspace/pulselivedata.log','r').read()
142 lines = graph_data.split('\n')
143 xs = [] # closest to desired frequency
144 ys = [] # avg # of pulses
145 zs = [] # min # of pulses
146 ws = [] # max # of pulses
147 ts = []
148 for line in lines:
149 if len(line) > 1:
150 t, d = line.split(':')
151 d = list(map(lambda x: int(x), d.split(',')))
152 xs.append(d[0])
153 ys.append(np.average(d))
154 zs.append(min(d))
155 ws.append(max(d))
156 ts.append(int(t))
157 ax9.clear()
158
159 plots = []
160 labels = ['Minimum', 'Maximum', 'Average', 'Best', 'Desired Frequency']
161 plots += ax9.plot(ts, zs, color="cornflowerblue", linewidth=0.75)
162 plots += ax9.plot(ts, ws, color="coral", linewidth=0.75)
163 plots += ax9.plot(ts, ys, color=yellow, linewidth=0.75)
164 plots += ax9.plot(ts, xs, color="lime")
165 ax9.tick_params(axis='y', labelcolor=accent_color)
166
167 if config.is_pulse_count():
168 ax9.hlines(y=config.get_desired_frequency(), xmin=1, xmax=len(lines), color="violet", linestyles="dotted")
169 # labels.append("Desired Frequency")
170 ax9.set(xlabel='Generation', ylabel='Pulses', title='Circuit Pulse Count per Generation')
171
172 if config.using_transfer_interval():
173 for i in range(0,len(lines),config.get_transfer_interval()):
174 ax9.axvline(x=i, color=accent_color, linestyle="dashed")
175
176 if formal:
177 ax9.legend(plots, labels, bbox_to_anchor=(1.15, 0.5), loc="center left", borderaxespad=0)
178
179 if(config.get_save_plots()):
180 fig4.savefig(str(plots_dir.joinpath("2_pulses.png")), bbox_inches="tight")
181
182
183 def animate_waveform(i):
184 graph_data = open('workspace/waveformlivedata.log','r').read()
185 lines = graph_data.split('\n')
186 pulse_trigger = [341*3.3/715]*500
187 xs = []
188 ys = []
189 for line in lines:
190 if len(line) > 1:
191 x, y = line.split(',')
192 xs.append(int(x))
193 ys.append(float(y) * 3.3/715)
194 ax4.clear()
195 if config.is_tone_discriminator():
196 ax4.set_xlim([0, 1000]) # type: ignore
197 else:
198 ax4.set_xlim([0, 500]) # type: ignore
199 #ax4.set_ylim([0, 750])
200 ax4.set_ylim([-0.2, 3.5]) # type: ignore
201 ax4.plot(pulse_trigger, "r--")
202 ax4.plot(xs, ys, color="blue")
203
204 if formal:
205 ax4.legend(['Trigger Voltage', 'Circuit Voltage'], bbox_to_anchor=(1.15, 0.5), loc="lower center", borderaxespad=0)
206
207 ax4.set(xlabel='Time (μs)', ylabel='Voltage (V)', title='Current Hardware Waveform')
208
209 def animate_state(i):
210 graph_data = open('workspace/statelivedata.log','r').read()
211 lines = graph_data.split('\n')
212 pulse_trigger = [341*3.3/715]*500
213 xs = []
214 ys = []
215 for line in lines:
216 if len(line) > 1:
217 x, y = line.split(',')
218 xs.append(int(x))
219 ys.append(float(y))
220 ax5.clear()
221 ax5.set_xlim([0, 1000]) # type: ignore
222 #ax4.set_ylim([0, 750])
223 ax5.set_ylim([-0.1, 1.1]) # type: ignore
224 ax5.plot(pulse_trigger, "r--")
225 ax5.plot(xs, ys, color="blue")
226
227 if formal:
228 ax5.legend(['Trigger Voltage', 'Circuit Voltage'], bbox_to_anchor=(1.15, 0.5), loc="lower center", borderaxespad=0)
229
230 ax5.set(xlabel='Time (μs)', ylabel='Voltage (V)', title='Current State')
231
232 # def animate_map(i):
233 # graph_data = open('workspace/maplivedata.log','r').read()
234 # lines = graph_data.split('\n')
235 # xs = []
236 # ys = []
237 # fits = []
238 # if len(lines) > 0 and len(lines[0]) > 0:
239 # scale_factor = int(lines[0])
240 # lines.pop(0) # Remove scale factor from the lines set
241
242 # for line in lines:
243 # vals = line.split(' ')
244 # if (len(vals) > 2 and len(vals[2]) > 0):
245 # row = int(vals[0])
246 # col = int(vals[1])
247 # fit = float(vals[2])
248 # fits.append(fit)
249 # xs.append((col + 0.5) * scale_factor)
250 # ys.append((row + 0.5) * scale_factor)
251
252 # ax5.clear()
253
254 # # Add a line to the middle that separates possible from impossible cells
255 # ax5.plot([0, 750], [0, 750], color='#444444', linewidth=0.5)
256
257 # scatterplot = ax5.scatter(xs, ys, c=fits, s=50, cmap='winter')
258 # plt.colorbar(scatterplot)
259
260 # ax5.set_xlim(0, 750)
261 # ax5.set_ylim(0, 750)
262 # ax5.set_aspect('equal')
263 # #ax5.set_xticks(np.arange(0, 1024, 50), minor=True)
264 # #ax5.set_yticks(np.arange(0, 1024, 50), minor=True)
265 # #ax5.grid(color = '#363636', which = 'minor')
266 # ax5.set(xlabel='Max Voltage (norm)', ylabel='Min Voltage (norm)', title='Elite Map')
267
268 # if(config.get_save_plots()):
269 # fig_map.savefig(str(plots_dir.joinpath("5_map.png")), bbox_inches="tight")
270
271 def animate_pops(i):
272 graph_data = open('workspace/poplivedata.log','r').read()
273 lines = graph_data.split('\n')
274 xs = []
275 ys = []
276 ylabels = []
277 ylabel_i = 1
278
279 x = 1
280 for line in lines:
281 if len(line) > 1:
282 xs.append(x)
283 x = x + 1
284
285 args = line.split(' ')
286 parsed = []
287 for i in range(len(args)):
288 if len(args[i]) > 0:
289 parsed.append(int(args[i]))
290 if len(ys) <= 0:
291 # Haven't initialized the y's list yet, lets throw in an empty list for each out our source populations
292 for i in range(len(parsed)):
293 ys.append([])
294
295 # Now we need to add to ys based on the index in parsed
296 ylabels.append("Population " + str(ylabel_i))
297 ylabel_i = ylabel_i + 1
298 for i in range(len(parsed)):
299 ys[i].append(parsed[i])
300
301 if len(ys) > 0:
302 ax6.clear()
303 ax6.stackplot(xs, ys, labels=ylabels)
304 ax6.legend( bbox_to_anchor=(1.15, 0.5), loc="center left", borderaxespad=0)
305 ax6.set(xlabel='Generation', ylabel='Number from Population', title='Circuits from Each Source Population')
306
307 def anim_violin_plots(i):
308 data = open('workspace/violinlivedata.log','r').read()
309 collections = []
310 gens = []
311 widths = []
312 lines = data.split('\n')
313 # Decide which generations to include based on the number to have and the number available
314 interval = len(lines) / (MAX_VIOLIN_PLOTS - 1)
315 if len(lines) < MAX_VIOLIN_PLOTS:
316 interval = 1
317 # Makes sure the first generation displayed will always be generation 2 (the first where we have interesting data)
318 index = 1 - interval
319 while int(index + interval) < len(lines):
320 index = index + interval
321 int_index = int(index)
322 line = lines[int_index]
323 if len(line) > 1:
324 vals = line.split(':')
325 gen = int(vals[0])
326 gens.append(gen)
327 pts = vals[1].split(',')
328 collections.append(list(map(lambda x: float(x), pts)))
329
330 # Make sure that we always include the final generation
331 # File always ends with a blank line, so go 2 lines back
332 line = lines[len(lines)-2]
333 if len(line) > 1:
334 vals = line.split(':')
335 gen = int(vals[0])
336 gens.append(gen)
337 pts = vals[1].split(',')
338 collections.append(list(map(lambda x: float(x), pts)))
339
340 for i in range(0, len(collections)):
341 widths.append(interval * 0.5)
342
343 if len(collections) > 0:
344 ax7.clear()
345 if config.using_transfer_interval():
346 for i in range(0,len(lines),config.get_transfer_interval()):
347 ax7.axvline(x=i, color=accent_color, linestyle="dashed")
348 ax7.violinplot(collections, positions=gens, widths=widths)
349 ax7.set(xlabel='Generation', ylabel='Fitness', title='Fitness Violin Plots')
350
351 if(config.get_save_plots()):
352 fig2.savefig(str(plots_dir.joinpath("3_violin_plots.png")))
353
354 def anim_violin_plots_pulse(i):
355 data = open('workspace/pulselivedata.log','r').read()
356 collections = []
357 gens = []
358 widths = []
359 lines = data.split('\n')
360 # Decide which generations to include based on the number to have and the number available
361 interval = len(lines) / MAX_VIOLIN_PLOTS
362 if len(lines) < MAX_VIOLIN_PLOTS:
363 interval = 1
364 # Makes sure the first generation displayed will always be generation 2 (the first where we have interesting data)
365 index = 1 - interval
366 while int(index + interval) < len(lines):
367 index = index + interval
368 int_index = int(index)
369 line = lines[int_index]
370 if len(line) > 1:
371 vals = line.split(':')
372 gen = int(vals[0])
373 gens.append(gen)
374 pts = vals[1].split(',')
375 collections.append(list(map(lambda x: float(x), pts)))
376
377 for i in range(0, len(collections)):
378 widths.append(interval * 0.5)
379
380 if len(collections) > 0:
381 ax10.clear()
382 ax10.violinplot(collections, positions=gens, widths=widths)
383 if config.using_transfer_interval():
384 for i in range(0,len(lines),config.get_transfer_interval()):
385 ax10.axvline(x=i, color=accent_color, linestyle="dashed")
386 if config.is_pulse_count():
387 ax10.hlines(y=config.get_desired_frequency(), xmin=1, xmax=len(lines), color="violet", linestyles="dotted")
388 ax10.set(xlabel='Generation', ylabel='Pulses', title='Pulse Violin Plots')
389 ax10.set(xlabel='Generation', ylabel='Pulses')
390
391 def anim_heatmap(i):
392 global max_pulses
393 if config.is_pulse_count():
394 data = open('workspace/pulselivedata.log','r').read()
395 else:
396 data = open('workspace/heatmaplivedata.log','r').read()
397
398
399 lines = data.split('\n')
400 collections = []
401 gens = []
402
403 for line in lines:
404 if len(line) > 1:
405 vals = line.split(':')
406 pts = vals[1].split(',')
407 for pt in pts:
408 gens.append(int(vals[0]))
409 if config.is_pulse_count():
410 collections.append(float(pt))
411 else:
412 collections.append(float(pt)*3.3/715)
413
414 ax8.clear()
415 ax8.hist2d(gens,collections,bins=HEATMAP_BINS)
416
417 if config.is_pulse_count():
418 ax8.set(xlabel='Generation', ylabel='Pulses', title='Pulse Count Histogram')
419 else:
420 ax8.set(xlabel='Generation', ylabel='Voltage (V)', title='Voltage Heatmap')
421
422 if config.using_transfer_interval():
423 for i in range(0,len(lines),config.get_transfer_interval()):
424 ax8.axvline(x=i, color=accent_color, linestyle="dashed")
425
426 if(config.get_save_plots()):
427 fig3.savefig(str(plots_dir.joinpath("4_heatmap.png")))
428
429 def animate_pulse_map(i):
430 graph_data = open('workspace/maplivedata.log','r').read()
431 lines = graph_data.split('\n')
432 xs = []
433 fits = []
434 if len(lines) > 0 and len(lines[0]) > 0:
435 scale_factor = int(lines[0])
436 lines.pop(0) # Remove scale factor from the lines set
437
438 for line in lines:
439 # Two values; the pulse count (frequency) and the fitness
440 for line in lines:
441 vals = line.split(' ')
442
443 col = int(vals[0])
444 fit = float(vals[1])
445 fits.append(fit)
446 xs.append((col-0.5) * scale_factor)
447
448 ax5.clear()
449
450 ax5.bar(xs, fits, width=scale_factor)
451
452 ax5.set_xlim(1000, 150_000)
453 ax5.set_ylim(0, 1000)
454 ax5.set(xlabel='Frequency (Hz)', ylabel='Fitness', title='Elite Map')
455
456 # def plot(fig, function):
457 # if formal:
458 # return function(0)
459 # else:
460 # return animation.FuncAnimation(fig, function, interval=FRAME_INTERVAL, cache_frame_data=False)
461
462
463 plots_dir = config.get_plots_directory()
464
465 formal = False
466 if len(sys.argv) > 1 and sys.argv[1] == 'formal':
467 formal = True
468 plots_dir = plots_dir.joinpath("Formal")
469 accent_color = "black"
470 accent_color2 = "#65187A"
471 heatmap_color = 'Blues'
472 yellow = "goldenrod"
473 plot = lambda fig, function : function(0)
474 else:
475 style.use('dark_background')
476 accent_color = "white"
477 accent_color2 = "#f0f8ff"
478 heatmap_color = 'viridis'
479 yellow = "yellow"
480 plot = lambda fig, function : animation.FuncAnimation(fig, function, interval=FRAME_INTERVAL, cache_frame_data=False)
481
482
483 if not exists(plots_dir):
484 mkdir(plots_dir)
485
486 fig = plt.figure(figsize=(9,7))
487 rows = 2
488 cols = 1
489 has_wf_plot = False
490 has_st_plot = False
491 if not config.is_pulse_count():
492 rows = rows + 1
493 has_wf_plot = True
494
495 if config.is_tone_discriminator():
496 rows = rows + 1
497 has_st_plot = True
498
499 has_pop_plot = False
500 if config.uses_init_existing_population():
501 rows = rows + 1
502 has_pop_plot = True
503
504 ax1 = fig.add_subplot(rows, cols, 2)
505 ani = plot(fig, animate_generation)
506 ax2 = fig.add_subplot(rows, cols, 1)
507 ax3 = ax2.twinx()
508 ani2 = plot(fig, animate_epoch)
509
510 if has_wf_plot:
511 ax4 = fig.add_subplot(rows, cols, 3)
512 ani3 = plot(fig, animate_waveform)
513
514 if has_st_plot:
515 ax5 = fig.add_subplot(rows, cols, 4)
516 ani4 = plot(fig, animate_state)
517
518 if has_pop_plot:
519 ax6 = fig.add_subplot(rows, cols, rows * cols)
520 ani6 = plot(fig, animate_pops)
521
522 fig2 = plt.figure()
523 ax7 = fig2.add_subplot(1, 1, 1)
524 ani7 = plot(fig2, anim_violin_plots)
525
526 fig3 = plt.figure()
527 ax8 = fig3.add_subplot(1,1,1)
528 ani8 = plot(fig3, anim_heatmap)
529
530 if config.is_pulse_count():
531 fig4 = plt.figure()
532 ax9 = fig4.add_subplot(2,1,1)
533 ani9 = plot(fig4, animate_epoch_pulses)
534 ax10 = fig4.add_subplot(2,1,2)
535 ani10 = plot(fig4, anim_violin_plots_pulse)
536
537 # if config.get_selection_type() == 'MAP_ELITES':
538 # fig_map = plt.figure()
539 # ax5 = fig_map.add_subplot(1, 1, 1)
540 # if config.get_fitness_func() == "PULSE_CONSISTENCY":
541 # ani4 = plot(fig_map, animate_pulse_map)
542 # else:
543 # ani4 = plot(fig_map, animate_map)
544
545 plt.subplots_adjust(hspace=0.50)
546 fig.tight_layout(pad=5.0)
547 plt.show(block=(not formal))
548 #plt.show(block=True)
549
550# only run if this is the main method.
551if (__name__ == "__main__"):
552 run()