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)