Source code for TrivialImplementation

  1import random
  2from BitstreamEvolutionProtocols import Circuit, Individual,FPGA_Compilation_Data, Population, CircuitFactory, Measurement, EvaluatePopulationFitness, GenData, GenDataFactory, GenerateInitialPopulation, GenerateMeasurements, Hardware, Reproducer, DataRequest
  3from pathlib import Path
  4from returns.result import Result, Success, Failure
  5import functools as ft
  6from typing import Any
  7from collections.abc import Iterable
  8import asyncio
  9
 10"""
 11As discussed in the main meeting (3/28/2025), we are first putting together a trivial implementation of all of the components.
 12
 13In this implementation:
 14    - Fitness is an integer
 15    - Individuals & Circuits only hold an integer
 16    - Circuits are also Individuals, but we still use the correct type hints as if they may not be
 17    - The circuit's fitness equals the integer it holds
 18    - The simulation is run for 500 generations.
 19    - The larger the circuit's integer fitness, the more fit
 20    - Selection is performed as desired (Just top half, somewhat random, etc.)
 21    - Mutations, occouring in reproduction, simply increment or decrement the Individual's Integer for their child.
 22
 23This should be fairly simple.
 24While writing this code, please write tests in the test_TrivialImplementation.py file as you are writing your code.
 25Make sure all functions that are tests begin with "test_" to make sure pytest can find them.
 26Run tests with "pytest" on the command line.
 27See "test_BitstreamEvolutionProtocols.py" for examples with writing tests.
 28"""
 29
 30## ---------------------------- Circuit & Individual & CircuitFactory Code (Isaac) ---------------------------------
 31
[docs] 32class TrivialCircuit: 33 "This is a very simple circuit that is also the Individual evolution is performed on" 34 def __init__(self,inherent_fitness:int): 35 self.inherent_fitness = inherent_fitness 36 37 def compile(self, fpga: FPGA_Compilation_Data) -> Result[None,Exception]: 38 return Success(None)
39 40 41def TrivialCircuitFactory(population: Population) -> dict[Circuit,list[tuple[Population,Individual]]]: 42 # they are the same thing for this implementation 43 output: dict[Circuit,list[tuple[Population,Individual]]] = dict() 44 for individual,fitness in population: 45 output[individual] = [(population,individual)] 46 return output 47 48 49 50 51## --------------------------------------------- Generate & Reproduce Populations --------------------------------------------------
[docs] 52def TrivialReproduceWithMutation (population: Population,random: random.Random) -> Population: 53 """Return a population where the top half are kept and each gets a mutated child. 54 55 Each child is the parent incremented or decremented randomly. This primarily 56 ensures the population remains the same size. If an odd-length population is 57 passed, the next individual is kept but does not reproduce or mutate. The 58 output population has all of its fitnesses unevaluated (None). 59 """ 60 population.sort(lambda x: x, True) 61 individuals = list(iter(population)) 62 63 population_size = len(individuals) 64 #keep_extra = population_size % 2 != 0 65 66 kept_individuals = individuals[0:((population_size+1) // 2)] 67 new_pop = [i[0] for i in kept_individuals] #get only the trivial circuits 68 for (individual, fitness) in kept_individuals[0:(population_size//2)]: 69 # mutate & add child 70 if random.random() < 0.5: 71 new_pop.append(TrivialCircuit(fitness + 1)) 72 else: 73 new_pop.append(TrivialCircuit(fitness - 1)) 74 return Population(new_pop, None)
75 76
[docs] 77def TrivialGenerateInitialPopulation(population_size:int, 78 random: random.Random, min_fitness:int, max_fitness:int) -> Population: 79 """ 80 Generates a a list of two populations which have fitnesses numbered from 81 zero and population_size sorted in descending order 82 83 Here min & max fitness refers to the minimum and maximum inherent fitnesses that can be generated for an individual. 84 """ 85 86 new_pop = [] 87 for _ in range(population_size): 88 new_pop.append(TrivialCircuit(random.randint(min_fitness,max_fitness))) 89 90 #create new population with fitnesses unspecified b/c "unknown" 91 return Population(new_pop, None)
92 93 94## ------------------------------------ Generate Measurements -------------------------------------------- 95#This is currently basically the same thing as the abstract measurement class
[docs] 96class Trivial_Meas(Measurement[TrivialCircuit,int]): 97 def record_measurement_result(self, result:int): 98 return super().record_measurement_result(result)
99 # I don't remember why I made Measurement an Abstract Base Class. 100 # Maybe to fix types??? 101 102def TrivialGenerateMeasurements(factory: TrivialCircuitFactory, population: Population 103 ) -> dict[Measurement,list[tuple[Population,Individual]]]: 104 measurement_map:dict[Measurement,list[tuple[Population,Individual]]] = {} 105 106 circuits:dict[Circuit,list[tuple[Population,Individual]]] = factory(population) 107 108 for circuit in circuits.keys(): 109 meas = Trivial_Meas("FPGA_REQUEST_FAKE", 110 data_request=DataRequest.NONE, 111 circuit_to_measure=circuit, 112 num_samples=1) 113 measurement_map[meas] = circuits[circuit] #dependancies are the same 114 115 return measurement_map 116
[docs] 117def FakeHardwareTrivialEvaluateMeasurements(measurements: Iterable[Measurement])->None: 118 "perform the measurements and edit them in place. This would normally be done by the H" 119 for meas in measurements: 120 meas.record_FPGA_used("USED_FPGA") 121 meas.record_measurement_result(meas.circuit.inherent_fitness)
122
[docs] 123def TrivialEvaluatePopulationFitness(population:Population,measurement_dependants:dict[Measurement,list[tuple[Population,Individual]]]): 124 "Turns measurements into fitness values and applies them to the provided population, completely evaluating the population, and only editing that population." 125 for meas in measurement_dependants.keys(): 126 for pop, indiv in measurement_dependants[meas]: 127 128 individual_fitness = 0 129 match meas.result: 130 case Success(fitness): 131 individual_fitness = fitness 132 case Failure(exception): 133 individual_fitness = 0 134 case _: 135 individual_fitness = 0 136 137 #Only change population if it was passed in as argument. 138 if pop in [population]: 139 pop.set_fitness(indiv,individual_fitness) 140 141 # default fitness for all values with no known fitness value discoverd in the above process. 142 population.set_fitness_of_unevaluated_individuals(0)
143 144 145
[docs] 146class TrivialHardware(Hardware): 147 def __init__(self,FPGAs:list[str] = ["FAKE_FPGA1", "FAKE FPGA2"]): 148 self.FPGAs = FPGAs 149 150 async def request_measurement(self, measurement: Trivial_Meas)->Trivial_Meas: 151 measurement.record_FPGA_used(random.choice(self.FPGAs)) 152 measurement.record_measurement_result(measurement.circuit.inherent_fitness) 153 return measurement 154 155 def get_available_FPGAs(self)->list[str]: return self.FPGAs
156 157 158def TrivialEvaluateMeasurements(measurements: Iterable[Measurement], HW: TrivialHardware)->None: 159 async def _run_all(): 160 tasks = [HW.request_measurement(meas) for meas in measurements] 161 await asyncio.gather(*tasks) 162 asyncio.run(_run_all()) 163 164 165## ------------------------------------ Trivial Evolution Object ----------------------------------------- 166
[docs] 167def FakeMeasuringFitnessTrivialImplemention(unevaluated_population: Population)->Population: 168 """This is a function that prevents me from having to use async & hardware while testing out TrivialEvolution. 169 This returns the same population, it just evaluates it.""" 170 171 for individual, fitness in unevaluated_population: 172 unevaluated_population.set_fitness(individual,individual.inherent_fitness) 173 174 return unevaluated_population
175 176
[docs] 177class TrivialEvolution: 178 """Utilizes the protocols defined to run experiments. 179 180 This is an example that should be generalized for a more general solution. 181 There should be different versions of Evolution for structurally different 182 experiments (e.g. multiple populations of individuals evolved simultaneously, 183 bacterial populations where only some individuals are evaluated and reproduce 184 each loop). 185 186 Any other evolution implementations should try to maintain as similar of 187 function signatures as possible, with arguments communicated with protocols 188 that are as general as possible. 189 190 This implementation generates, evaluates, and reproduces entire populations 191 at once, and does so in discrete timesteps. 192 """ 193
[docs] 194 def __init__(self, 195 generation_data_factory:GenDataFactory, 196 #circuit_factory:CircuitFactory, 197 reproducer:Reproducer, 198 generate_intial_population: GenerateInitialPopulation, 199 #evaluate_population_fitness: EvaluatePopulationFitness, 200 #generate_measurements: GenerateMeasurements, 201 #hardware:Hardware 202 ): 203 """ 204 Initializes the Evolution with all of the objects and functions needed for it 205 to carry out it the evolution. 206 This does not execute any functions passed in. 207 """ 208 self._generation_data_factory:GenDataFactory = generation_data_factory 209 #self._circuit_factory:CircuitFactory = circuit_factory 210 self._reproduce:Reproducer = reproducer 211 self._generate_intial_population:GenerateInitialPopulation = generate_intial_population
212 #self._evaluate_population_fitness:EvaluatePopulationFitness = evaluate_population_fitness 213 #self._generate_measurements:GenerateMeasurements = generate_measurements 214 #self._hardware:Hardware = hardware 215
[docs] 216 def run(self): 217 """ 218 This Function Runs the evolution run specified by the protocols provided, using them 219 according to how the architecture of this evolution object is configured. 220 221 This implementations generates, evaluates, and reproduces entire populations at once, 222 and does so in discrete timesteps. 223 """ 224 prev_population:Population|None = None 225 current_population:Population = self._generate_intial_population() 226 current_gendata:GenData|None = None 227 228 while (current_gendata:= 229 self._generation_data_factory(gen_data=current_gendata) 230 ) is not None: 231 232 # measurements:list[Measurement] = self._generate_measurements(self._circuit_factory,[current_population]) 233 # tasks = [self.__hardware.request_measurement(m) for m in measurements] 234 # self._hardware.request_measurement(measurement for measurement in measurements) 235 # results = asyncio.run( asyncio.gather(*tasks) ) # I added asyncio.run to make sure that the async experiements were run at this point 236 # # Maybe figure out task groups. See https://docs.python.org/3/library/asyncio-task.html#coroutines-and-tasks 237 # 238 # prev_population = self._evaluate_population_fitness(current_population,measurements) 239 240 #NOTE: If we want to add multiple populations, create different evolution object, 241 # and specify how you create initial populations for each set of individuals, 242 # then specify in circuit_factory how to turn one individual from each population into a circuit, 243 # then, generate measurements decides which circuits it wants to make from those individuals 244 # and it should be provided with a list that is the same number of populations as the number of arguments in circuit_factory. 245 246 247 248 #move current_population to prev. & reproduce to current_pop 249 prev_population = FakeMeasuringFitnessTrivialImplemention(current_population) 250 current_population = self._reproduce(prev_population) 251 252 print("Trivial Evolution Complete!")
253 254 255