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