Source code for PlotEvolutionLive

  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()