Source code for CircuitPopulation

   1""" 
   2Circuit Population
   3------------------
   4
   5This class was reviewed, and should be fully documented at a basic level.
   6
   7"""
   8import os
   9import numpy as np
  10from typing import NamedTuple
  11from shutil import copyfile
  12from sortedcontainers import SortedKeyList
  13from math import ceil
  14from numpy.random import default_rng
  15from pathlib import Path
  16from itertools import zip_longest
  17from collections import namedtuple
  18from time import time
  19from subprocess import run
  20import random
  21import math
  22from mmap import mmap
  23from Circuit.FileBasedCircuit import FileBasedCircuit
  24from Circuit.FullySimCircuit import FullySimCircuit
  25from Circuit.IntrinsicCircuit import IntrinsicCircuit
  26from Circuit.PulseCountFitnessFunction import PulseCountFitnessFunction
  27from Circuit.SimHardwareCircuit import SimHardwareCircuit
  28from Circuit.ToneDiscriminatorFitnessFunction import ToneDiscriminatorFitnessFunction
  29from Circuit.VarMaxFitnessFunction import VarMaxFitnessFunction
  30from Config import Config
  31from ascTemplateBuilder import ascTemplateBuilder
  32from utilities import wipe_folder
  33from datetime import datetime
  34
  35RANDOMIZE_UNTIL_NOT_SET_ERR_MSG = '''\
  36RANDOMIZE_UNTIL not set in config.ini, continuing without randomization'''
  37
  38INVALID_VARIANCE_ERR_MSG = '''\
  39VARIANCE_THRESHOLD <= 0 as set in config.ini, continuing without randomization'''
  40
  41# SEED_HARDWARE is the hardware file used as an initial template for the Circuits
  42# NOTE The Seed file is provided as a way to kickstart the evolutionary process
  43# without having to perform a time-consuming random search for a seedable circuit.
  44# Contact repository authors if you're interested in a new seed file.
  45SEED_HARDWARE_FILEPATH = Path("data/seed-hardware.asc")
  46
  47# The basename (filename without path or extensions) of the Circuit
  48# hardware, bitstream, and data files.
  49CIRCUIT_FILE_BASENAME = "hardware"
  50
  51ELITE_MAP_SCALE_FACTOR = 50
  52
  53# Create a named tuple for easy and clear storage of information about
  54# a Circuit (currently its name and fitness)
  55CircuitInfo = namedtuple("CircuitInfo", ["name", "fitness"])
  56
  57# Named tuple for circuit's path and fitness; currently only used for combining populations
  58CircuitPathInfo = namedtuple("CircuitPathInfo", ["path", "fitness"])
  59
  60# Bin sizes for elite maps
  61ELITE_MAP_SCALE_FACTOR = 50
  62PULSE_ELITE_MAP_SCALE_FACTOR = 5000
  63
[docs] 64def is_pulse_func(config): 65 """ 66 Used in multiple places, will be removed soon. 67 68 .. todo:: 69 unite the is_pulse_func() functions for ease of change. 70 71 Parameters 72 ---------- 73 config : Config 74 Configuration Class to interact with config 75 76 Returns 77 ------- 78 bool 79 True if it is any type of oscilator (uses count pulses), False otherwise. 80 """ 81 return (config.get_fitness_func() == 'PULSE_COUNT' or config.get_fitness_func() == 'TOLERANT_PULSE_COUNT' 82 or config.get_fitness_func() == 'SENSITIVE_PULSE_COUNT' or config.get_fitness_func() == 'PULSE_CONSISTENCY')
83
[docs] 84class CircuitPopulation: 85 """Manages the initializing the population of circuits, 86 updating and recording information about the population throughout evolution, 87 and deciding when to stop evolution""" 88 # SECTION Initialization functions
[docs] 89 def __init__(self, mcu, config: Config, logger): 90 """ 91 Generates the initial population of circuits with the following arguments 92 93 Parameters 94 ---------- 95 mcu : Microcontroller 96 Object containing an instance of Microcontroller class 97 config : Config 98 Object containing an instance of Config class 99 logger : Logger 100 Object containing an instance of Logger class 101 """ 102 self.__config = config 103 self.__microcontroller = mcu 104 105 # A list of Circuits that's sorted by fitness decreasing order 106 # (to get it to sort in decreasing order I had to multiply the 107 # sort key by negative one to reverse the natural sorting order 108 # since sortedcontainers don't have a way to be in reverse order). 109 self.__circuits = SortedKeyList(key=lambda ckt: -1 * ckt.get_fitness()) 110 self.__logger = logger 111 self.__overall_best_circuit_info = CircuitInfo("", 0) 112 self.__rand = default_rng() 113 self.__current_epoch = 0 114 self.__best_epoch = 0 115 num_rows = 3 116 if(config.get_routing_type == "NEWSE"): 117 num_rows = 2 118 num_cols = len(config.get_accessed_columns()) 119 self.__population_bistream_sum = np.zeros(16*6*num_rows*num_cols) 120 121 # Set the selection type here since the selection type should 122 # not change during a run. This way we don't have to branch each 123 # time we run selection. 124 if config.get_selection_type() == "SINGLE_ELITE": 125 self.__run_selection = self.__run_single_elite_tournament 126 elif config.get_selection_type() == "FRAC_ELITE": 127 self.__run_selection = self.__run_fractional_elite_tournament 128 elif config.get_selection_type() == "CLASSIC_TOURN": 129 self.__run_selection = self.__run_classic_tournament 130 elif config.get_selection_type() == "FIT_PROP_SEL": 131 self.__run_selection = self.__run_fitness_proportional_selection 132 elif config.get_selection_type() == "RANK_PROP_SEL": 133 self.__run_selection = self.__run_rank_proportional_selection 134 elif config.get_selection_type() == "MAP_ELITES": 135 self.__run_selection = self.__run_map_elites_selection 136 else: 137 self.__log_error( 138 1, "Invalid Selection method in config.ini. Exiting...") 139 exit() 140 141 elitism_fraction = config.get_elitism_fraction() 142 population_size = config.get_population_size() 143 self.__n_elites = int(ceil(elitism_fraction * population_size))
144
[docs] 145 def run_fitness_sensitity(self): 146 """ 147 Gets the same circuit, runs it repeatedly and reports each fitness. 148 Internally has a while loop to determine how many times to run. 149 """ 150 #create circuit object 151 self.__log_info(1, "Creating circuit object for fitness sensitivity experiment") 152 ckt = self.__construct_circuit( 153 1, 154 "hardware1", 155 self.__config.get_test_circuit(), 156 self.__generate_sine_funcs() 157 ) 158 159 using_time = self.__config.using_sensitivity_time() 160 start_time = time() 161 stop_time = self.__config.get_sensitivity_time() 162 163 using_trials = self.__config.using_sensitivity_trials() 164 cur_trial = 0 165 num_trials = self.__config.get_sensitivity_trials() 166 167 #loop through trials and log fitness 168 should_continue = True 169 while should_continue: 170 self.__eval_circuit_once(ckt) 171 fitness = ckt.get_fitness() 172 173 with open("workspace/fitnesssensitivity.log", "a") as live_file: 174 if self.__config.is_pulse_func(): 175 data2 = ckt.get_extra_data('pulses') 176 else: 177 data2 = ckt.get_extra_data('mean_voltage') 178 179 #get temp and humidity reading 180 t = 0 181 h = 0 182 if(self.__config.reading_temp_humidity()): 183 t = self.__microcontroller.measure_temp() 184 h = self.__microcontroller.measure_humidity() 185 self.__log_event(4, "Recorded temperature: " + str(t) + ". Recorded humidity: " + str(h)) 186 187 188 now = datetime.now() 189 timestamp = now.strftime("%H.%M.%S") 190 191 live_file.write(("{}:{},{},{},{},{}\n").format(str(cur_trial), fitness, data2, t, h, timestamp)) 192 self.__log_event(2, "Trial " + str(cur_trial) + " done. Fitness recorded and logged to file: " + str(fitness)) 193 194 cur_trial += 1 195 should_continue = ((not using_time) or (time() - start_time < stop_time)) and \ 196 ((not using_trials) or (cur_trial < num_trials)) 197 198 self.__log_event(1, "Fitness sensitivity trails done.")
199 200 def __generate_sine_funcs(self): 201 """ 202 Builds a list of randomly generated sine functions used in the simulation mode. 203 204 Returns 205 ------- 206 list[functions] 207 List of randomly generated sine functions 208 """ 209 sine_funcs = [] 210 self.__sine_strs = [] 211 for i in range(100): 212 # Don't let amplitude and y-offset get too out of hand 213 a = random.uniform(0, 100) 214 b = random.uniform(0.02, 2) 215 c = (random.randint(0, 7) / 8) * (2 * math.pi / b) 216 d = random.uniform(100, 900) 217 # We provide many parameters with default values here, because Python closures 218 # work like JS using the "var" keyword, and do not "properly" create environments the way we'd expect 219 # For this reason, we add default parameters, providing our current var values to them 220 # This works because the variable values are then *evaluated* as the lambda (closure) is constructed 221 # Before this fix, we had a bug where every single sine function would be exactly the same; 222 # all holding a/b/c/d values from the very last function to be generated 223 sine_funcs.append((lambda x,a=a,b=b,c=c,d=d: a * math.sin(b * (x + c)) + d)) 224 sine_str = "Sine function: " + str(i) + " | y = " + str(a) + " * sin(" + str(b) + " * (x + " + str(c) + ")) + " + str(d) 225 self.__sine_strs.append(sine_str) 226 return sine_funcs 227 228 def __construct_circuit(self, index, file_name, seed_arg, sine_funcs): 229 if self.__config.get_simulation_mode() == 'FULLY_SIM': 230 return FullySimCircuit(index, file_name, self.__config, sine_funcs, self.__rand) 231 elif self.__config.get_simulation_mode() == 'SIM_HARDWARE': 232 return SimHardwareCircuit(index, file_name, self.__config, seed_arg, self.__logger, self.__rand) 233 else: 234 fit_func = None 235 if self.__config.get_fitness_func() == 'VARIANCE': 236 fit_func = VarMaxFitnessFunction(500) 237 elif self.__config.get_fitness_func() in ['PULSE_COUNT', 'SENSITIVE_PULSE_COUNT', 'TOLERANT_PULSE_COUNT']: 238 fit_func = PulseCountFitnessFunction() 239 elif self.__config.get_fitness_func() == 'TONE_DISCRIMINATOR': 240 fit_func = ToneDiscriminatorFitnessFunction() 241 242 return IntrinsicCircuit(index, file_name, self.__config, seed_arg, self.__rand, self.__logger, self.__microcontroller, fit_func) 243
[docs] 244 def populate(self): 245 """ 246 Creates initial population based on the config. 247 1. Clears the files used to keep track of circuit 248 2. Uses appropriate initialization method specified by config. 249 3. Handles randomization until condition in config is met. 250 """ 251 # Always creates a circuit with the seed file, but if we have certain randomization 252 # modes then perform necessary operations 253 sine_funcs = self.__generate_sine_funcs() 254 255 # Wipe the current folder, so if we go from 100 circuits in one experiment to 50 in the next, 256 # we don't still have 100 (with 50 that we use and 50 residual ones) 257 wipe_folder(self.__config.get_asc_directory()) 258 wipe_folder(self.__config.get_bin_directory()) 259 wipe_folder(self.__config.get_data_directory()) 260 wipe_folder(self.__config.get_generations_directory()) 261 262 self.__multiple_populations = False 263 if self.__config.get_init_mode() == "EXISTING_POPULATION": 264 # Need to assign where each circuit gets its source from 265 # Get number of subpopulations, then grab random circuits from each 266 subdirectories = next(os.walk(self.__config.get_src_pops_dir()))[1] 267 subdirectory_files = list(map(lambda dir: next(os.walk(self.__config.get_src_pops_dir().joinpath(dir)))[2], subdirectories)) 268 self.__num_subpops = len(subdirectories) 269 self.__multiple_populations = True 270 # Existing population setting, load in all circuits from each population and get the ones with the highest fitness 271 # If any are missing the fitness measure, then we will randomly select them. 272 # We could manually measure their fitnesses, but as of now we've decided that is too slow 273 all_subdir_circuits = [] 274 for i in range(len(subdirectories)): 275 # Load every circuit 276 subdir_circuits = SortedKeyList( 277 key=lambda ckt: -ckt.fitness 278 ) 279 for file in subdirectory_files[i]: 280 path = self.__config.get_src_pops_dir().joinpath(subdirectories[i]).joinpath(file) 281 hw_file = open(path, "r+") 282 mmapped_file = mmap(hw_file.fileno(), 0) 283 hw_file.close() 284 fitness = float(FileBasedCircuit.get_file_attribute_st(mmapped_file, "fitness")) 285 if fitness == None: 286 fitness = 0 287 subdir_circuits.add(CircuitPathInfo(path, fitness)) 288 289 all_subdir_circuits.append(subdir_circuits) 290 subdirectory_index = 0 291 292 # if we're using custom i/o pin configurations 293 # need to configure to io tiles of the seed circuit 294 template = SEED_HARDWARE_FILEPATH 295 if self.__config.get_using_configurable_io(): 296 template = "workspace/template/seed.asc" 297 template_builder = ascTemplateBuilder(self.__config, self.__logger) 298 template_builder.configure_seed_io(SEED_HARDWARE_FILEPATH, template) 299 300 for index in range(1, self.__config.get_population_size() + 1): 301 file_name = "hardware" + str(index) 302 if self.__config.get_init_mode() == "EXISTING_POPULATION": 303 # Grab the top circuit from the current population, unless it is empty, then we'll jump to the next one 304 while len(all_subdir_circuits[subdirectory_index]) <= 0: 305 subdirectory_index = (subdirectory_index + 1) % len(all_subdir_circuits) 306 seedArg = all_subdir_circuits[subdirectory_index].pop(0).path 307 subdirectory_index = (subdirectory_index + 1) % len(all_subdir_circuits) 308 else: 309 seedArg = template 310 311 ckt = self.__construct_circuit(index, file_name, seedArg, sine_funcs) 312 if self.__config.get_init_mode() == "RANDOM": 313 ckt.randomize_bitstream() 314 elif self.__config.get_init_mode() == "CLONE_SEED_MUTATE": 315 # Call mutate once on this circuit 316 ckt.mutate() 317 elif self.__config.get_init_mode() == "EXISTING_POPULATION": 318 # Make sure the circuit puts a line at the top of its .asc file denoting the source population 319 ckt.set_file_attribute('src_population', str(subdirectory_index)) 320 321 self.__circuits.add(ckt) 322 self.__log_event(3, "Created circuit: {0}".format(ckt)) 323 324 # If map-elites selection method selected, then randomly generate until we fill up 25% of the map 325 '''if self.__config.get_selection_type() == 'MAP_ELITES': 326 self.__log_event(1, 'Randomizing until map is 25% full...') 327 elites = list(filter(lambda x: x != 0, [j for sub in self.__generate_map() for j in sub])) 328 elite_count = len(elites) 329 while elite_count < 0.1 * (21 * 21 / 2): 330 self.__log_event(3, "Got %s%% (%s)" % (elite_count / (21*21/2) * 100, elite_count)) 331 # Need to mutate non-elites 332 for ckt in self.__circuits: 333 if not ckt in elites: 334 ckt.copy_sim(random.choice(elites)) 335 ckt.mutate() 336 #ckt.randomize_bitstream() 337 ckt.evaluate_sim(False) 338 339 elite_map = self.__generate_map() 340 elites = list(filter(lambda x: x != 0, [j for sub in elite_map for j in sub])) 341 elite_count = len(elites) 342 self.__output_map_file(elite_map)''' 343 344 # Randomize initial circuits until waveform variance or 345 # pulses are found 346 if self.__config.get_simulation_mode() != "FULLY_INTRINSIC": 347 pass # No randomization implemented for simulation mode 348 elif self.__config.get_randomization_type() == "PULSE": 349 self.__log_info(1, "PULSE randomization mode selected.") 350 self.__randomize_until_pulses() 351 elif self.__config.get_randomization_type() == "VARIANCE": 352 self.__log_info(1, "VARIANCE randomization mode selected.") 353 if self.__config.get_randomize_threshold() <= 0: 354 self.__log_error(INVALID_VARIANCE_ERR_MSG) 355 else: 356 self.__randomize_until_variance() 357 elif self.__config.get_randomization_type() == "VOLTAGE": 358 self.__randomize_until_voltage() 359 elif self.__config.get_randomization_type() == "NO": 360 self.__log_info(1, "NO randomization mode selected.") 361 else: 362 self.__log_error(1, RANDOMIZE_UNTIL_NOT_SET_ERR_MSG) 363 364 # Output the first data point to live data files 365 self.__write_to_livedata()
366 367 def __randomize_until_pulses(self): 368 """ 369 Randomizes population until minimum number of pulses is found. 370 Called by populate(self) 371 Should only be used with pulse count fitness functions 372 """ 373 no_pulses_generated = True 374 while no_pulses_generated: 375 # NOTE Randomize until pulses will continue mutating and 376 # not revert to the original seed-hardware until restarting 377 self.__log_event(3, "Randomizing to generate pulses") 378 for circuit in self.__circuits: 379 if self.__config.get_randomize_mode() == 'RANDOM': 380 circuit.randomize_bitstream() 381 else: 382 circuit.mutate() 383 384 circuit.evaluate_once() 385 pulses = circuit.get_extra_data('pulses') 386 th = self.__config.get_randomize_threshold() 387 if (pulses > th): 388 no_pulses_generated = False 389 self.__log_info(1, "Pulse generated! Exiting randomization. Pulses recorded:", pulses) 390 break 391 392 def __randomize_until_voltage(self): 393 """ 394 Randomizes population until a mean voltage is found near the desired value 395 called by populate(self) 396 Should only be used with variance maximization fitness function 397 """ 398 while True: 399 self.__log_event(3, "Randomizing to get voltage") 400 for circuit in self.__circuits: 401 if self.__config.get_randomize_mode() == 'RANDOM': 402 circuit.randomize_bitstream() 403 else: 404 circuit.mutate() 405 406 circuit.evaluate_once() 407 mean_voltage = circuit.get_extra_data('mean_voltage') 408 if (abs(mean_voltage - 341) < 10): 409 self.__log_info(1, "Voltage Achieved! Exiting randomization. Voltage:", mean_voltage) 410 break 411 412 # NOTE This is whole function going to be upgraded to handle a from-scratch circuit seeding process. 413 # https://github.com/evolvablehardware/BitstreamEvolution/issues/3 414 def __randomize_until_variance(self): 415 """ 416 Randomizes population until minimum variance fitness is found. 417 called by populate(self) 418 Should only be used with variance maximization fitness function 419 """ 420 # Variance threshold is the desired variance 421 bestVariance = 0 422 variance = 0 423 while bestVariance < self.__config.get_randomize_threshold(): 424 self.__log_event(3, "Randomizing to generate variance") 425 for circuit in self.__circuits: 426 circuit.randomize_bitstream() 427 circuit.evaluate_once() 428 variance = circuit.get_fitness() 429 self.__log_info(3, "Variance generated:", variance) 430 431 with open("workspace/randomizationdata.log", "a") as liveFile: 432 liveFile.write(str(variance) + "\n") 433 434 if variance > bestVariance: 435 self.__log_info(3, "New best variance: ", variance) 436 bestVariance = variance 437 self.__overall_best_circuit_info = CircuitInfo(str(circuit), variance) 438 copyfile(circuit.get_hardware_file_path(), self.__config.get_best_file()) 439 break 440 441 self.__log_info(3, "Variance generated! Exiting randomization. Fitness:", variance) 442 443 def __next_epoch(self): 444 """ 445 Moves to the next generation/epoch 446 Currently, only needs to increase the generation by 1 447 All other generation-specific behavior will be derived from this value 448 """ 449 self.__current_epoch += 1 450 451 def __should_continue_evo(self): 452 """ 453 Checks with config whether we have reached any of the end conditions for the simulation run. 454 455 Returns 456 ------- 457 bool 458 True if evolution should continue, False otherwise. 459 """ 460 should_continue = True 461 if self.__config.using_n_generations(): 462 if self.get_current_epoch() >= self.__config.get_n_generations(): 463 should_continue = False 464 if self.__config.using_target_fitness(): 465 if self.__overall_best_circuit_info.fitness >= self.__config.get_target_fitness(): 466 should_continue = False 467 return should_continue 468 469 def __eval_circuit_once(self, circuit): 470 circuit.clear_data() 471 if isinstance(circuit, FileBasedCircuit): 472 circuit.upload() 473 for i in range(self.__config.get_num_samples()): 474 circuit.collect_data_once() 475 476 circuit.calculate_fitness() 477
[docs] 478 def evolve(self): 479 """ 480 Runs an evolutionary loop and records the circuit with the highest fitness throughout the loop, 481 while also storing statistics in a file for the plot to access. 482 """ 483 if len(self.__circuits) == 0: 484 self.__log_error( 485 1, "Attempting to evolve with empty population. Exiting...") 486 exit() 487 488 # Set initial values for 'best' data 489 self.__overall_best_circuit_info = CircuitInfo( 490 str(self.__circuits[0]), 491 self.__circuits[0].get_fitness() 492 ) 493 self.__best_epoch = 0 494 self.__next_epoch() 495 496 while(self.__should_continue_evo()): #self.get_current_epoch() < self.__config.get_n_generations()): 497 498 #self.__log_event(3, "Starting evo cycle", self.get_current_epoch( 499 #), "<", self.__config.get_n_generations(), "?") 500 501 # Since sortedcontainers don't update when the value by 502 # which an item is sorted gets updated, we have to add the 503 # Circuits to a new list after we evaluate them and then 504 # make the new list the working Circuit list. 505 reevaulated_circuits = SortedKeyList( 506 key=lambda ckt: -ckt.get_fitness() 507 ) 508 509 # Evaluate all the Circuits in this CircuitPopulation. 510 start = time() 511 512 for circuit in self.__circuits: 513 circuit.clear_data() 514 515 for i in range(self.__config.get_num_passes()): 516 # Shuffle the circuits each time 517 circuits = np.random.permutation(self.__circuits) 518 for circuit in circuits: 519 if isinstance(circuit, FileBasedCircuit): 520 circuit.upload() 521 for i in range(self.__config.get_num_samples()): 522 circuit.collect_data_once() 523 524 for circuit in self.__circuits: 525 circuit.calculate_fitness() 526 527 self.__population_bistream_sum = np.zeros(self.__population_bistream_sum.size) 528 for circuit in self.__circuits: 529 # If evaluate returns true, then a circuit has surpassed 530 # the threshold and we are done. 531 532 # fitness = circuit.get_fitness() 533 fitness = circuit.get_fitness() 534 535 # Save off various circuit metrics 536 if self.__config.get_simulation_mode() != 'FULLY_SIM': 537 circuit.set_file_attribute("fitness", str(fitness)) 538 if self.__config.is_pulse_count(): 539 circuit.set_file_attribute("pulse_count", str(circuit.get_extra_data('pulses'))) 540 541 # Commented out for now while we test 542 # Pretty sure this was originally for pulse count only, leaving it commented out since things are working right now 543 '''if fitness > self.__config.get_randomize_threshold(): 544 self.__log_event(1, "{} fitness: {}".format(circuit, fitness)) 545 return''' 546 reevaulated_circuits.add(circuit) 547 548 #add the circuit's bistream to our population sum - for diversity calculation and visualization 549 if self.__config.get_simulation_mode() != 'FULLY_SIM': 550 self.__population_bistream_sum += circuit.get_bitstream() 551 552 epoch_time = time() - start 553 self.__circuits = reevaulated_circuits 554 555 # If one of the new Circuits has a higher fitness than our 556 # recorded best, make it the recorded best. 557 best_circuit_info = self.get_overall_best_circuit_info() 558 self.__log_event(2, "Best circuit info", best_circuit_info.fitness) 559 self.__log_event(2, "Circuit 0 info", 560 self.__circuits[0].get_fitness()) 561 if self.__circuits[0].get_fitness() > best_circuit_info.fitness: 562 self.__overall_best_circuit_info = CircuitInfo( 563 str(self.__circuits[0]), 564 self.__circuits[0].get_fitness() 565 ) 566 self.__best_epoch = self.get_current_epoch() 567 # Copy this circuit to the best file 568 if isinstance(self.__circuits[0], FileBasedCircuit): 569 copyfile(self.__circuits[0].get_hardware_file_path(), self.__config.get_best_file()) 570 571 # For tone discriminator experiments, update the best waveform and best state data 572 # Each file will contain all sampled data points from the new best circuit 573 if (self.__config.get_fitness_func() == "TONE_DISCRIMINATOR"): 574 with open("workspace/bestwaveformlivedata.log", "w+") as waveLive: 575 waveLive.write("NEW BEST BELOW: " + str(self.__circuits[0]) + " in gen " + str(self.get_current_epoch()) + "\n") 576 i = 1 577 for points in self.__circuits[0].get_waveform_td(): 578 waveLive.write(str(i) + ", " + str(points) + "\n") 579 i += 1 580 with open("workspace/beststatelivedata.log", "w+") as stateLive: 581 stateLive.write("NEW BEST BELOW: " + str(self.__circuits[0]) + " in gen " + str(self.get_current_epoch()) + "\n") 582 i = 1 583 for points in self.__circuits[0].get_state_td(): 584 stateLive.write(str(i) + ", " + str(points) + "\n") 585 i += 1 586 self.__log_event(2, "New best found") 587 588 self.__logger.log_generation(self, epoch_time) 589 # The circuits that are protected from randomization 590 self.__protected_elites = [] 591 self.__run_selection() 592 593 # Remove bottom X% of population to replace with random circuits 594 # (just randomize bitstream of the bottom X%) 595 if self.__config.get_random_injection() > 0: 596 amt = int(self.__config.get_random_injection() * self.__config.get_population_size()) 597 circuits_to_randomize = self.__circuits[-amt:] 598 for ckt in circuits_to_randomize: 599 if ckt not in self.__protected_elites: 600 ckt.randomize_bitstream() 601 602 self.__write_to_livedata() 603 self.__next_epoch() 604 605 if self.__config.using_transfer_interval(): 606 if self.__current_epoch % self.__config.get_transfer_interval() == 0: 607 self.__microcontroller.switch_fpga() 608 609 # We have finished evolution! Lets quickly re-evaluate the top circuit, since it 610 # will then output its waveform 611 if not is_pulse_func(self.__config): 612 self.__eval_circuit_once(self.__circuits[0]) 613 # Also, log the name of the top circuit 614 self.__log_event(1, "Top Circuit in Final Generation:", self.__circuits[0])
615 616 def __write_to_livedata(self): 617 """ 618 Runs each generation to write data to files used to store data needed for Live plots (PlotEvolutionLive.py) 619 """ 620 fitness_sum = 0 621 for c in self.__circuits: 622 fitness_sum = fitness_sum + c.get_fitness() 623 # Calculate the diversity measure 624 diversity = 0 625 if self.__config.get_diversity_measure() == "HAMMING_DIST": 626 diversity = self.avg_hamming_dist() 627 elif self.__config.get_diversity_measure() == "UNIQUE": 628 diversity = self.count_unique() 629 elif self.__config.get_diversity_measure() == "DIFFERING_BITS": 630 diversity = self.count_differing_bits() 631 elif self.__config.get_diversity_measure() == "NONE": 632 diversity = 0 633 # Providing any invalid measure of diversity will make it constantly 0 634 # Write the generation data (avg/best/worst fitness, etc) to file 635 if self.get_current_epoch() > 0: 636 with open("workspace/bestlivedata.log", "a") as liveFile: 637 avg = fitness_sum / self.__config.get_population_size() 638 # Format: Epoch, Best Fitness, Worst Fitness, Average Fitness, Ovr Best Fitness, Diversity Measure 639 liveFile.write("{}, {}, {}, {}, {}, {}\n".format( 640 str(self.get_current_epoch()), 641 str(self.__circuits[0].get_fitness()), 642 str(self.__circuits[-1].get_fitness()), 643 str(avg), 644 str(self.get_overall_best_circuit_info().fitness), 645 diversity 646 )) 647 648 if self.__multiple_populations: 649 # Write the population counts to file (i.e. count of circuits from each source population) 650 with open("workspace/poplivedata.log", "a") as live_file: 651 counts = [0] * self.__num_subpops 652 for ckt in self.__circuits: 653 population = int(ckt.get_file_attribute('src_population')) 654 counts[population] = counts[population] + 1 655 live_file.write(("{} " * self.__num_subpops + "\n").format(*counts)) 656 657 if (self.__current_epoch > 0): 658 with open("workspace/violinlivedata.log", "a") as live_file: 659 fits = [] 660 for ckt in self.__circuits: 661 fits.append(str(ckt.get_fitness())) 662 live_file.write(("{}:{}\n").format(self.__current_epoch, ",".join(fits))) 663 664 if self.__config.get_simulation_mode() == "FULLY_INTRINSIC": 665 if not self.__config.is_pulse_func(): 666 with open("workspace/heatmaplivedata.log", "a") as live_file2: 667 best = self.__circuits[0] 668 if (self.__config.get_fitness_func() == "TONE_DISCRIMINATOR"): 669 # Need a slightly different function for tone discriminator waveform 670 data = best.get_waveform_td() 671 else: 672 data = best.get_waveform() 673 live_file2.write(("{}:{}\n").format(self.__current_epoch, ",".join(data))) 674 else: 675 with open("workspace/pulselivedata.log", "a") as live_file3: 676 data = [] 677 for ckt in self.__circuits: 678 data.append(str(ckt.get_extra_data('pulses'))) 679 live_file3.write(("{}:{}\n").format(self.__current_epoch, ",".join(data))) 680 681 if self.__config.saving_population_bistream(): 682 if(self.__current_epoch % 683 self.__config.get_population_bistream_save_interval() == 0): 684 with open("workspace/bitstream_avg.log", "a") as live_file4: 685 data = self.get_differing_bits_str() 686 live_file4.write(("{}:{}\n").format(self.__current_epoch, data)) 687 688 # TODO: Re-enable this. Temporarily disabled in case files get too large 689 #self.__save_generation() 690 691 def __save_generation(self): 692 """ 693 Saves the current generation to the generations directory 694 Each generation gets its own file 695 696 Saves all modifiable parts of a generation so it can be reconstructed. 697 698 called by __write_to_livedata(self) 699 """ 700 gen_lines = [] 701 # At the top, add the necessary config params such as routing and accessed columns 702 gen_lines.append(self.__config.get_routing_type()) 703 gen_lines.append(','.join(self.__config.get_accessed_columns())) 704 # Now, add the bitstream for each circuit on its own line 705 # We want the circuits in number order though 706 sorted_by_index = SortedKeyList( 707 key=lambda ckt: ckt.get_index() 708 ) 709 for ckt in self.__circuits: 710 sorted_by_index.add(ckt) 711 # Now add each circuit 712 for ckt in sorted_by_index: 713 bitstream = ckt.get_intrinsic_modifiable_bitstream() 714 bitstring = ''.join(bitstream) 715 gen_lines.add(bitstring) 716 # Now actually write the file 717 path = self.__config.get_generations_directory().joinpath('gen' + str(self.__current_epoch) + '.log') 718 with open(path, 'w') as f: 719 f.writelines(gen_lines) 720 721 if (self.__current_epoch > 0): 722 with open("workspace/heatmaplivedata.log", "a") as live_file: 723 best = self.__circuits[0] 724 if (self.__config.get_fitness_func() == "TONE_DISCRIMINATOR"): 725 # Need a slightly different function for tone discriminator waveform 726 live_file.write(("{}:{}\n").format(self.__current_epoch, ",".join(best.get_waveform_td()))) 727 else: 728 live_file.write(("{}:{}\n").format(self.__current_epoch, ",".join(best.get_waveform()))) 729 730 # SECTION Selection algorithms. 731 def __run_classic_tournament(self): 732 """ 733 Selection Algorithm that randomly pairs together circuits, compares their fitness, and preforms crossover on and mutates the "loser" 734 """ 735 population = self.__rand.permutation(self.__circuits) 736 737 self.__log_event(3, "Tournament Number:", self.get_current_epoch()) 738 739 # For all Circuits in the CircuitPopulation, take two random 740 # circuits at a time from the population and compare them. Copy 741 # some genes from the fittest of the two to the least fittest of 742 # the two and mutate the latter. 743 for ckt1, ckt2 in CircuitPopulation.__group(population, 2): 744 winner = ckt1 745 loser = ckt2 746 if ckt2.get_fitness() > ckt1.get_fitness(): 747 winner = ckt2 748 loser = ckt1 749 750 self.__log_event(3, 751 "Fitness {}: {} < Fitness {}: {}".format( 752 loser, 753 loser.get_fitness(), 754 winner, 755 winner.get_fitness() 756 )) 757 758 if self.__rand.uniform(0, 1) <= self.__config.get_crossover_probability(): 759 self.__single_point_crossover(winner, loser) 760 else: 761 self.__log_event(3, "Cloning:", winner, " ---> ", loser) 762 loser.copy_from(winner) 763 764 loser.mutate() 765 766 def __run_single_elite_tournament(self): 767 """ 768 Selection Algorithm that mutates the hardware of every circuit that is not the current best circuit 769 """ 770 self.__log_event(3, "Tournament Number: {}".format( 771 str(self.get_current_epoch()))) 772 773 best = self.__circuits[0] 774 self.__protected_elites.append(best) 775 for ckt in self.__circuits: 776 # Mutate the hardware of every circuit that is not the best 777 if ckt != best: 778 if ckt.get_fitness() <= best.get_fitness(): 779 ckt.mutate() 780 else: 781 self.__log_info(2, ckt, "is current BEST") 782 783 def __run_fitness_proportional_selection(self): 784 """ 785 Selection algorithm that compares every circuit in the population to a random elite (chosen proportionally based on each elite's fitness). 786 If circuit has a lower fitness, crossover or mutate the circuit 787 """ 788 self.__log_event(2, "Number of Elites:", self.__n_elites) 789 self.__log_event(2, "Ranked Fitness:", self.__circuits) 790 791 # Generate a group of elites from the best n = <self.__n_elites> 792 # Circuits. Based on their fitness values, map each Circuit with 793 # a probabilty value (used later for crossover/copying/mutation). 794 elites = {} 795 elite_sum = 0 796 797 for i in range(self.__n_elites): 798 elites[self.__circuits[i]] = 0 799 elite_sum += self.__circuits[i].get_fitness() 800 if elite_sum > 0: 801 for elite in elites.keys(): 802 elites[elite] = elite.get_fitness() / elite_sum 803 elif elite_sum == 0: 804 for elite in elites.keys(): 805 elites[elite] = 1 / self.__n_elites 806 else: 807 # elite_sum is negative. This should not be possible. 808 self.__log_error(1, "Elite_sum is negative. Exiting...") 809 exit() 810 811 self.__log_event(2, "Elite Group:", elites.keys()) 812 self.__log_event(2, "Elite Probabilites:", elites.values()) 813 self.__protected_elites = elites.keys() 814 815 # For all Circuits in this CircuitPopulation, choose a random 816 # elite (based on the associated probabilities calculated above) 817 # and compare it to the Circuit. If the Circuit has lower 818 # fitness than the elite, perform crossover (with the elite) and 819 # mutation on it (or copy the elite's hardware if crossover is 820 # disabled). 821 elite_prob_sum = sum(elites.values()) 822 for ckt in self.__circuits: 823 if self.__n_elites != 0: 824 if elite_prob_sum > 0: 825 rand_elite = self.__rand.choice( 826 list(elites.keys()), 827 self.__n_elites, 828 p=list(elites.values()) 829 )[0] 830 else: # If fitness isn't negative, this should never happen 831 rand_elite = self.__rand.choice(list(elites.keys()))[0] 832 else: 833 rand_elite = self.__rand.choice(self.__circuits) 834 835 self.__log_event(4, "Elite", rand_elite) 836 837 if ckt.get_fitness() <= rand_elite.get_fitness() and ckt != rand_elite and ckt not in elites: 838 # if self.__config.get_crossover_probability() == 0: 839 # self.__log_event(3, "Cloning:", rand_elite, " ---> ", ckt) 840 # ckt.copy_from(rand_elite) 841 # else: 842 # self.__single_point_crossover(rand_elite, ckt) 843 if self.__rand.uniform(0, 1) <= self.__config.get_crossover_probability(): 844 self.__single_point_crossover(rand_elite, ckt) 845 else: 846 self.__log_event(4, "Cloning:", rand_elite, " ---> ", ckt) 847 ckt.copy_from(rand_elite) 848 ckt.mutate() 849 850 def __run_rank_proportional_selection(self): 851 ''' 852 Selection algorithm that compares every circuit in the population to a random elite (chosen proportionally based on each elite's rank). 853 If circuit has a lower fitness, crossover or mutate the circuit 854 ''' 855 self.__log_event(2, "Number of Elites:", self.__n_elites) 856 self.__log_event(2, "Ranked Fitness:", self.__circuits) 857 858 # Generate a group of elites from the best n = <self.__n_elites> 859 # Circuits. Based on their fitness values, map each Circuit with 860 # a probabilty value (used later for crossover/copying/mutation). 861 elites = {} 862 # can use summation formula since sum of ranks is the sum of natural numbers 863 elite_sum = (self.__n_elites) * (self.__n_elites + 1) / 2 864 if (elite_sum > 0): 865 for i in range(self.__n_elites): 866 # Using (self.__n_elites - i) since highest ranked indiviual is at self.__circuits[0] 867 elites[self.__circuits[i]] = (self.__n_elites - i) / elite_sum 868 else: 869 # elite_sum is negative. This should not be possible. 870 self.__log_error(1, "Elite_sum is zero or negative. Exiting...") 871 exit() 872 873 self.__log_event(3, "Elite Group:", elites.keys()) 874 self.__log_event(3, "Elite Probabilites:", elites.values()) 875 self.__protected_elites = elites.keys() 876 #self.__log_event(3, "Elite", rand_elite) 877 878 # For all Circuits in this CircuitPopulation, choose a random 879 # elite (based on the associated probabilities calculated above) 880 # and compare it to the Circuit. If the Circuit has lower 881 # fitness than the elite, perform crossover (with the elite) and 882 # mutation on it (or copy the elite's hardware if crossover is 883 # disabled). 884 elite_prob_sum = sum(elites.values()) 885 for ckt in self.__circuits: 886 if self.__n_elites != 0: 887 if elite_prob_sum > 0: 888 rand_elite = self.__rand.choice( 889 list(elites.keys()), 890 self.__n_elites, 891 p=list(elites.values()) 892 )[0] 893 else: # If fitness isn't negative, this should never happen 894 rand_elite = self.__rand.choice(list(elites.keys()))[0] 895 else: 896 rand_elite = self.__rand.choice(self.__circuits) 897 898 if ckt.get_fitness() <= rand_elite.get_fitness() and ckt != rand_elite and ckt not in elites: 899 # if self.__config.get_crossover_probability() == 0: 900 # self.__log_event(3, "Cloning:", rand_elite, " ---> ", ckt) 901 # ckt.copy_from(rand_elite) 902 # else: 903 # self.__single_point_crossover(rand_elite, ckt) 904 905 if self.__rand.uniform(0, 1) <= self.__config.get_crossover_probability(): 906 self.__single_point_crossover(rand_elite, ckt) 907 else: 908 self.__log_event(3, "Cloning:", rand_elite, " ---> ", ckt) 909 ckt.copy_from(rand_elite) 910 ckt.mutate() 911 912 def __run_fractional_elite_tournament(self): 913 """ 914 Selection algorithm that compares every circuit in the population to a random elite. If circuit has a lower fitness, crossover or mutate the circuit 915 """ 916 self.__log_info(2, "Number of Elites: ", str(self.__n_elites)) 917 self.__log_info(2, "Ranked Fitness: ", self.__circuits) 918 919 # Generate a group of elite Circuits from the 920 # n = <self.__n_elites> best performing Circuits. 921 elite_group = [] 922 for i in range(0, self.__n_elites): 923 elite_group.append(self.__circuits[i]) 924 self.__log_info(3, "Elite Group:", elite_group) 925 926 # For all the Circuits in the CircuitPopulation compare the 927 # Circuit against a random elite Circuit from the group 928 # generated above. If the Circuit's fitness is less than than 929 # the elite's perform crossover (or clone if crossover is 930 # disabled) and then mutate the Circuit. 931 self.__protected_elites = elite_group 932 for ckt in self.__circuits: 933 rand_elite = self.__rand.choice(elite_group) 934 if ckt.get_fitness() <= rand_elite.get_fitness() and ckt != rand_elite and ckt not in elite_group: 935 # if self.__config.crossover_probability == 0: 936 # self.__log_event(3, "Cloning:", rand_elite, " ---> ", ckt) 937 # ckt.replace_hardware_file(rand_elite.get_hardware_filepath) 938 # else: 939 # self.__single_point_crossover(rand_elite, ckt) 940 941 if self.__rand.uniform(0, 1) <= self.__config.get_crossover_probability(): 942 self.__single_point_crossover(rand_elite, ckt) 943 else: 944 self.__log_event(3, "Cloning:", rand_elite, " ---> ", ckt) 945 ckt.copy_from(rand_elite) 946 ckt.mutate() 947 948 def __run_map_elites_selection(self): 949 """ 950 Selection Algorithm that is an alternate version of the map elites algorithm from another paper. 951 This version of map elites will protect the highest-fitness individual in each "square" 952 We're going to have slightly granular squares to make sure that circuits have room to spread out early 953 to hopefully promote diversity 954 Group size length of 50 means we'll have 21x21 groups 955 """ 956 957 if self.__config.get_map_elites_dimension() == 1: 958 elite_map = self.__generate_pulse_map() 959 elites = list(filter(lambda x: x != 0, [j for j in elite_map])) 960 else: 961 elite_map = self.__generate_map() 962 elites = list(filter(lambda x: x != 0, [j for sub in elite_map for j in sub])) 963 964 self.__protected_elites = elites 965 966 for ckt in self.__circuits: 967 # If not an elite, then we will clone and mutate 968 if ckt not in elites: 969 rand_elite = self.__rand.choice(elites) 970 ckt.copy_from(rand_elite) 971 ckt.mutate() 972 973 self.__output_map_file(elite_map) 974 975 def __output_map_file(self, elite_map): 976 """ 977 Writes the map to a file (workspace/maplivedata.log) 978 979 Parameters 980 ---------- 981 elite_map : Circuit[][] 982 2D array of circuits that fell into these groupings depending on their characteristics. 983 """ 984 with open("workspace/maplivedata.log", "w+") as liveFile: 985 # First line describes granularity/scale factor 986 liveFile.write("{}\n".format(str(ELITE_MAP_SCALE_FACTOR))) 987 # If square is empty, write a "blank" to that line 988 if self.__config.get_map_elites_dimension() == 1: 989 for c in range(len(elite_map)): 990 ckt = elite_map[c] 991 if ckt != 0: 992 liveFile.write("{} {}\n".format(c, ckt.get_fitness())) 993 else: 994 for r in range(len(elite_map)): 995 sl = elite_map[r] 996 for c in range(len(sl)): 997 ckt = sl[c] 998 to_write = "" 999 if ckt != 0: 1000 to_write = str(ckt.get_fitness()) 1001 liveFile.write("{} {} {}\n".format(r, c, to_write)) 1002 1003 def __generate_map(self): 1004 """ 1005 Generates the elite map for this generation based on variance. 1006 1007 Returns 1008 ------- 1009 list(list(Circuit)) 1010 A 2D array of circuits catagorized based off of shared characteristics 1011 """ 1012 # If the value is not a circuit (i.e. it is 0) then we know the spot is open to be filled in 1013 # Go up to 21 since upper bound is 1024 1014 # Can't do [[0]*21]*21 because this will make all the sub-arrays point to same memory location 1015 elite_map = [] 1016 for i in range(22): 1017 elite_map.append([0]*21) 1018 # Evaluate each circuit's fitness and where it falls on the elite map 1019 # Populate elite map first 1020 for ckt in self.__circuits: 1021 row = math.floor(ckt.get_low_value() / ELITE_MAP_SCALE_FACTOR) 1022 col = math.floor(ckt.get_high_value() / ELITE_MAP_SCALE_FACTOR) 1023 if elite_map[row][col] == 0 or ckt.get_fitness() > elite_map[row][col].get_fitness(): 1024 elite_map[row][col] = ckt 1025 return elite_map 1026 1027 def __generate_pulse_map(self): 1028 """ 1029 Generates the elite map for this generation based on pulse count. 1030 1031 Returns 1032 ------- 1033 Circuit[][] 1034 A 2D array of circuits catagorized based off of shared characteristics 1035 """ 1036 1037 elite_map = [] 1038 for i in range((150_000 - 1_000) / PULSE_ELITE_MAP_SCALE_FACTOR): 1039 elite_map.append(0) 1040 for ckt in self.__circuits: 1041 col = math.floor(ckt.get_mean_frequency() / PULSE_ELITE_MAP_SCALE_FACTOR) 1042 if elite_map[col] == 0 or ckt.get_fitness() > elite_map[col].get_fitness(): 1043 elite_map[col] = ckt 1044 return elite_map 1045 1046 # SECTION Getters.
[docs] 1047 def get_current_best_circuit(self): 1048 """ 1049 Gets the circuit in the current generation with the highest fitness 1050 1051 Returns 1052 ------- 1053 Circuit 1054 Returns the best circuit in population. 1055 """ 1056 return self.__circuits[0]
1057
[docs] 1058 def get_overall_best_circuit_info(self): 1059 """ 1060 Returns the information of the circuit with the highest fitness throughout the run 1061 1062 Returns 1063 ------- 1064 CircuitInfo 1065 Returns the info object for the overall best circuit throughout the run. 1066 """ 1067 return self.__overall_best_circuit_info
1068
[docs] 1069 def get_current_epoch(self): 1070 """ 1071 Returns the generation number 1072 1073 Returns 1074 ------- 1075 int 1076 Returns the generation number of the current evolution. 1077 """ 1078 return self.__current_epoch
1079
[docs] 1080 def get_best_epoch(self): 1081 """ 1082 Returns the generation number that contained the circuit with the highest fitness 1083 1084 Returns 1085 ------- 1086 int 1087 Generation number that hat the circuit with the highest fitness 1088 """ 1089 return self.__best_epoch
1090 1091 # SECTION Miscellaneous helper functions. 1092 def __single_point_crossover(self, source, dest): 1093 """ 1094 Copy some series of chiasmas (points of genetic exchange) from fitter circuit into children 1095 1096 Parameters 1097 ---------- 1098 source : Circuit 1099 The circuit you are copying data from. 1100 dest : Circuit 1101 The circuit you are overwriting data from source to. 1102 """ 1103 crossover_point = 0 1104 1105 # Replace magic values with more generalized solutions 1106 if self.__config.get_simulation_mode() == "FULLY_SIM": 1107 crossover_point = self.__rand.integers( 1108 1, len(self.__circuits[0].get_bitstream()) - 1) 1109 elif self.__config.get_routing_type() == "MOORE": 1110 crossover_point = self.__rand.integers(1, 3) 1111 elif self.__config.get_routing_type() == "NWSE": 1112 crossover_point = self.__rand.integers(13, 15) 1113 else: 1114 self.__log_error( 1115 1, "Invalid routing type specified in config.ini. Exiting...") 1116 exit() 1117 dest.crossover(source, crossover_point) 1118
[docs] 1119 def avg_hamming_dist(self): 1120 """ 1121 Calculates and returns the average Hamming distance for the population 1122 1123 Returns 1124 ------- 1125 float 1126 Returns Hamming distance in the population. 1127 """ 1128 running_total = 0 1129 n = len(self.__circuits) 1130 num_pairs = n * (n-1) / 2 1131 1132 self.__log_event(4, "Starting Hamming Distance Calculation") 1133 bitstreams = list(map(lambda c: c.get_bitstream(), self.__circuits)) 1134 1135 # We now have all the bitstreams, we can do the faster hamming calculation by comparing each bit of them 1136 # Then we multiply the count of 1s for that bit by the count of 0s for that bit and add it to the running_total 1137 # Divide that by # of pairs at the end (calculation shown below) 1138 running_total = 0 1139 n = len(self.__circuits) 1140 num_pairs = n * (n-1) / 2 1141 self.__log_event(4, "HDIST - Entering loop") 1142 for i in range(len(bitstreams[0])): 1143 ones_count = 0 1144 zero_count = 0 1145 for j in range(n): 1146 if bitstreams[j][i] == 0: 1147 zero_count = zero_count + 1 1148 else: 1149 ones_count = ones_count + 1 1150 running_total = running_total + ones_count * zero_count 1151 1152 running_total = running_total / num_pairs 1153 self.__log_event(4, "HDIST - Final value", running_total) 1154 return running_total
1155
[docs] 1156 def count_unique(self): 1157 """ 1158 Returns the number of unique files in the population 1159 1160 Returns 1161 ------- 1162 int 1163 Number of unique circuits in the population 1164 1165 """ 1166 if self.__config.get_simulation_mode() == "FULLY_SIM": 1167 bitstreams = [] 1168 for ckt in self.__circuits: 1169 bitstreams.append(ckt.get_sim_bitstream()) 1170 bitstreams = self.__unique(bitstreams) 1171 self.__log_event( 1172 2, "Number of Unique Individuals:", len(bitstreams)) 1173 return len(bitstreams) 1174 1175 # If not FULLY_SIM, then run this 1176 # TODO: Optimize 1177 bin_dir = self.__config.get_bin_directory() 1178 dir_list = os.listdir(bin_dir) 1179 files = [f for f in dir_list if os.path.isfile( 1180 str(bin_dir)+'/'+f)] # Filter out non-files 1181 unique_file_paths = [] 1182 for file in files: 1183 full_path = str(bin_dir) + '/' + file 1184 not_unique = False 1185 for u in unique_file_paths: 1186 if self.__files_eq(full_path, u): 1187 not_unique = True 1188 break 1189 if not not_unique: 1190 unique_file_paths.append(full_path) 1191 self.__log_event(2, "Number of Unique Individuals:", 1192 len(unique_file_paths)) 1193 return len(unique_file_paths)
1194 1195 def __unique(self, arrays): 1196 """ 1197 Returns an array of unique arrays from the input 1198 1199 Parameters 1200 ---------- 1201 arrays : list[list[T]] 1202 An array containing arrays 1203 1204 Returns 1205 ------- 1206 list[T] 1207 Returns a list of all of the unique lists contained in the arrays variable 1208 """ 1209 soln = [] 1210 for a in arrays: 1211 # Check if its in soln; if not, then add it 1212 shouldAdd = True 1213 for b in soln: 1214 if self.__arr_eq(a, b): 1215 shouldAdd = False 1216 break 1217 if shouldAdd: 1218 soln.append(a) 1219 return soln 1220
[docs] 1221 def count_differing_bits(self): 1222 """ 1223 Returns the number of bits in the bistream where 2 circuits have different values 1224 1225 Returns 1226 ------- 1227 int 1228 Number of bits in the bistream where 2 circuits have different values 1229 1230 """ 1231 if self.__config.get_simulation_mode() == "FULLY_SIM": 1232 bitstream_sums = np.zeros[len(self.__circuits[0].get_sim_bitstream())] 1233 for ckt in self.__circuits: 1234 bitstream_sums += np.array(ckt.get_sim_bitstream()) 1235 else: 1236 bitstream_sums = self.__population_bistream_sum 1237 1238 count = 0 1239 for bit_sum in bitstream_sums: 1240 if bit_sum != 0 and bit_sum != len(self.__circuits): 1241 count += 1 1242 self.__log_event( 1243 2, "Number of differing bits:", count) 1244 return count
1245
[docs] 1246 def get_differing_bits_str(self): 1247 """ 1248 Returns an ASCII string that represents the number of circuits with a 1 at each bit in the bitstream 1249 Returns 1250 ------- 1251 str 1252 The number of circuits with a 1 at each bit in the bitstream 1253 1254 """ 1255 s = "" 1256 for bit in self.__population_bistream_sum: 1257 s += chr(int(bit)+32) 1258 return s
1259 1260 def __arr_eq(self, ar1, ar2): 1261 """ 1262 Returns True if the arrays or equal or False otherwise 1263 Compares each element of ar1 and ar2 1264 1265 Parameters 1266 ---------- 1267 ar1 : list 1268 an array 1269 ar2 : list 1270 an array 1271 1272 Returns 1273 ------- 1274 bool 1275 True if the arrays are equivalent in content and order, False otherwise. 1276 """ 1277 if len(ar1) != len(ar2): 1278 return False 1279 for i in range(0, len(ar1)): 1280 if ar1[i] != ar2[i]: 1281 return False 1282 return True 1283 1284 def __files_eq(self, fp1, fp2): 1285 """ 1286 Returns true if the files are equal (have the same content) 1287 1288 Parameters 1289 ---------- 1290 fp1 : str 1291 Path to file 1 1292 fp2 : str 1293 Path to file 2 1294 1295 Returns 1296 ------- 1297 bool 1298 True if both files contain the same content, False otherwise. 1299 """ 1300 content1 = [] 1301 content2 = [] 1302 with open(fp1, 'rb') as content: 1303 content1 = content.read() 1304 with open(fp2, 'rb') as content: 1305 content2 = content.read() 1306 return list(content1) == list(content2) 1307 1308 # TODO Take a closer look at this function 1309 @staticmethod 1310 def __group(iterable, n, fillvalue=None): 1311 """ 1312 .. todo:: 1313 Take a closer look at this function. Not sure why, but a comment here told me to. 1314 Also, further document what this function is I couldn't tell. 1315 1316 Collect data into fixed-length chunks or blocks 1317 #grouper('ABCDEFG', 3, 'x') --> ABC DEF Gxx 1318 Taken from python recipes. 1319 """ 1320 1321 args = [iter(iterable)] * n 1322 return zip_longest(fillvalue=fillvalue, *args) 1323 1324 def __log_event(self, level, *event): 1325 """ 1326 Emit an event-level log. This function is fulfilled through 1327 the logger. 1328 1329 Parameters 1330 ---------- 1331 level : int 1332 The level of importance of the logged information (lower level = higher importance) 1333 event : tuple[string] 1334 The message being logged 1335 """ 1336 self.__logger.log_event(level, *event) 1337 1338 def __log_info(self, level, *info): 1339 """ 1340 Emit an info-level log. This function is fulfilled through 1341 the logger. 1342 1343 Parameters 1344 ---------- 1345 level : int 1346 The level of importance of the logged information (lower level = higher importance) 1347 info : tuple[string] 1348 The message being logged 1349 """ 1350 self.__logger.log_info(level, *info) 1351 1352 def __log_error(self, level, *error): 1353 """ 1354 Emit an error-level log. This function is fulfilled through 1355 the logger. 1356 1357 Parameters 1358 ---------- 1359 level : int 1360 The level of importance of the logged information (lower level = higher importance) 1361 error : tuple[string] 1362 The message being logged 1363 """ 1364 self.__logger.log_error(level, *error) 1365 1366 def __log_warning(self, level, *warning): 1367 """ 1368 Emit a warning-level log. This function is fulfilled through 1369 the logger. 1370 1371 Parameters 1372 ---------- 1373 level : int 1374 The level of importance of the logged information (lower level = higher importance) 1375 warning : tuple[string] 1376 The message being logged 1377 """ 1378 self.__logger.log_warning(level, *warning)