Skip to content

Commit 2235755

Browse files
Franco CastagnaFranco Castagna
authored andcommitted
refactor: remove reactor to simplify algorithm
1 parent 8d9fa4a commit 2235755

4 files changed

Lines changed: 286 additions & 476 deletions

File tree

core/src/main/kotlin/org/evomaster/core/search/algorithms/CroAlgorithm.kt

Lines changed: 180 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,13 @@ import kotlin.math.abs
1212
*/
1313
class CroAlgorithm<T> : AbstractGeneticAlgorithm<T>() where T : Individual {
1414

15+
companion object {
16+
private const val ENERGY_TOLERANCE = 1e-9
17+
}
18+
19+
private data class EnergyContext(var container: Double)
20+
1521
private val molecules: MutableList<Molecule<T>> = mutableListOf()
16-
private lateinit var reactor: CroReactor<T>
1722

1823
// container is the global energy reservoir.
1924
// It collects kinetic energy lost in reactions and can be borrowed to enable otherwise infeasible decompositions, keeping total energy conserved.
@@ -34,17 +39,6 @@ class CroAlgorithm<T> : AbstractGeneticAlgorithm<T>() where T : Individual {
3439
// Initialize the underlying GA population to reuse sampling utilities
3540
super.setupBeforeSearch()
3641

37-
// Initialize reactor with dependencies once (allow overriding via useReactor in tests)
38-
if (!this::reactor.isInitialized) {
39-
reactor = CroReactor(
40-
config,
41-
randomness,
42-
this::mutate,
43-
this::potential,
44-
this::xover
45-
)
46-
}
47-
4842
// Convert GA population to CRO molecules with initial KE
4943
getViewOfPopulation().forEach { evaluatedSuite ->
5044
molecules.add(Molecule(evaluatedSuite.copy(), config.croInitialKineticEnergy, 0))
@@ -54,12 +48,6 @@ class CroAlgorithm<T> : AbstractGeneticAlgorithm<T>() where T : Individual {
5448
initialEnergy = getCurrentEnergy()
5549
}
5650

57-
/**
58-
* Allows tests or callers to override the reactor instance.
59-
* Must be called before [setupBeforeSearch].
60-
*/
61-
fun useReactor(reactor: CroReactor<T>) { this.reactor = reactor }
62-
6351
/**
6452
* Read-only snapshot of molecules for assertions in tests.
6553
*/
@@ -75,8 +63,8 @@ class CroAlgorithm<T> : AbstractGeneticAlgorithm<T>() where T : Individual {
7563
val selectedMolecule = molecules[moleculeIndex]
7664

7765
if (decompositionCheck(selectedMolecule)) {
78-
val energyCtx = CroReactor.EnergyContext(container)
79-
val decomposedOffspring = reactor.decomposition(
66+
val energyCtx = EnergyContext(container)
67+
val decomposedOffspring = decomposition(
8068
parent = selectedMolecule,
8169
energy = energyCtx,
8270
)
@@ -86,8 +74,8 @@ class CroAlgorithm<T> : AbstractGeneticAlgorithm<T>() where T : Individual {
8674
molecules.addAll(decomposedOffspring)
8775
}
8876
} else {
89-
val energyCtx = CroReactor.EnergyContext(container)
90-
val collidedMolecule = reactor.onWallIneffectiveCollision(
77+
val energyCtx = EnergyContext(container)
78+
val collidedMolecule = onWallIneffectiveCollision(
9179
molecule = selectedMolecule,
9280
energy = energyCtx,
9381
)
@@ -110,7 +98,7 @@ class CroAlgorithm<T> : AbstractGeneticAlgorithm<T>() where T : Individual {
11098

11199
val shouldSynthesize = synthesisCheck(firstMolecule) && synthesisCheck(secondMolecule)
112100
if (shouldSynthesize) {
113-
val fusedOffspring = reactor.synthesis(
101+
val fusedOffspring = synthesis(
114102
first = firstMolecule,
115103
second = secondMolecule,
116104
)
@@ -121,7 +109,7 @@ class CroAlgorithm<T> : AbstractGeneticAlgorithm<T>() where T : Individual {
121109
molecules.removeAt(highIndex)
122110
}
123111
} else {
124-
val updatedPair = reactor.intermolecularIneffectiveCollision(
112+
val updatedPair = intermolecularIneffectiveCollision(
125113
first = firstMolecule,
126114
second = secondMolecule,
127115
)
@@ -135,7 +123,7 @@ class CroAlgorithm<T> : AbstractGeneticAlgorithm<T>() where T : Individual {
135123

136124
// Adjust container if external factors changed fitness values, to conserve energy
137125
val current = getCurrentEnergy()
138-
if (abs(current - initialEnergy) > 1e-9) {
126+
if (abs(current - initialEnergy) > ENERGY_TOLERANCE) {
139127
val delta = current - initialEnergy
140128
container -= delta
141129
}
@@ -149,15 +137,23 @@ class CroAlgorithm<T> : AbstractGeneticAlgorithm<T>() where T : Individual {
149137
}
150138
}
151139

152-
private fun potential(evaluatedSuite: WtsEvalIndividual<T>): Double = -evaluatedSuite.calculateCombinedFitness()
140+
protected open fun computePotential(evaluatedSuite: WtsEvalIndividual<T>): Double = -evaluatedSuite.calculateCombinedFitness()
141+
142+
protected open fun applyMutation(wts: WtsEvalIndividual<T>) {
143+
mutate(wts)
144+
}
145+
146+
protected open fun applyCrossover(first: WtsEvalIndividual<T>, second: WtsEvalIndividual<T>) {
147+
xover(first, second)
148+
}
153149

154150
private fun decompositionCheck(molecule: Molecule<T>): Boolean = molecule.numCollisions > config.croDecompositionThreshold
155151

156152
private fun synthesisCheck(molecule: Molecule<T>): Boolean = molecule.kineticEnergy <= config.croSynthesisThreshold
157153

158154
private fun getCurrentEnergy(): Double {
159155
var energy = container
160-
molecules.forEach { molecule -> energy += potential(molecule.suite) + molecule.kineticEnergy }
156+
molecules.forEach { molecule -> energy += computePotential(molecule.suite) + molecule.kineticEnergy }
161157
return energy
162158
}
163159

@@ -168,7 +164,163 @@ class CroAlgorithm<T> : AbstractGeneticAlgorithm<T>() where T : Individual {
168164
* @return true if energy has been conserved in the system, false otherwise
169165
*/
170166
private fun hasEnergyBeenConserved(energy: Double): Boolean {
171-
return abs(this.initialEnergy - energy) < 0.000000001
167+
return abs(this.initialEnergy - energy) < ENERGY_TOLERANCE
168+
}
169+
170+
private fun computeEnergySurplus(totalBeforePotential: Double, totalBeforeKinetic: Double, totalAfterPotential: Double): Double =
171+
(totalBeforePotential + totalBeforeKinetic) - totalAfterPotential
172+
173+
private fun tryBorrowFromContainerToCoverDeficit(deficit: Double, energy: EnergyContext): Double? {
174+
val fractionA = randomness.nextDouble()
175+
val fractionB = randomness.nextDouble()
176+
val borrowableAmount = fractionA * fractionB * energy.container
177+
return if (deficit + borrowableAmount >= 0) {
178+
energy.container *= (1.0 - fractionA * fractionB)
179+
deficit + borrowableAmount
180+
} else {
181+
null
182+
}
183+
}
184+
185+
/**
186+
* Applies the uni-molecular on-wall ineffective collision:
187+
* mutate the molecule, keep it if energy surplus is non-negative,
188+
* split surplus between kinetic energy and the global container, and
189+
* increment collisions.
190+
*/
191+
private fun onWallIneffectiveCollision(
192+
molecule: Molecule<T>,
193+
energy: EnergyContext,
194+
): Molecule<T>? {
195+
val oldPotential = computePotential(molecule.suite)
196+
val oldKinetic = molecule.kineticEnergy
197+
val updated = molecule.copy(suite = molecule.suite.copy(), numCollisions = molecule.numCollisions + 1)
198+
199+
applyMutation(updated.suite)
200+
201+
val newPotential = computePotential(updated.suite)
202+
val netOnWallEnergy = computeEnergySurplus(oldPotential, oldKinetic, newPotential)
203+
if (netOnWallEnergy < 0) return null
204+
205+
val retainedFraction = randomness.nextDouble(config.croKineticEnergyLossRate, 1.0)
206+
updated.kineticEnergy = netOnWallEnergy * retainedFraction
207+
energy.container += netOnWallEnergy * (1.0 - retainedFraction)
208+
return updated
209+
}
210+
211+
/**
212+
* Performs decomposition of a molecule into two offspring.
213+
* Mutates two copies, accepts if total surplus (after optional borrowing
214+
* from the container) is non-negative, and distributes kinetic energy
215+
* and resets collisions.
216+
*/
217+
private fun decomposition(
218+
parent: Molecule<T>,
219+
energy: EnergyContext,
220+
): List<Molecule<T>>? {
221+
val parentPotential = computePotential(parent.suite)
222+
val parentKinetic = parent.kineticEnergy
223+
224+
val first = Molecule(parent.suite.copy(), kineticEnergy = 0.0, numCollisions = 0)
225+
val second = Molecule(parent.suite.copy(), kineticEnergy = 0.0, numCollisions = 0)
226+
227+
applyMutation(first.suite)
228+
applyMutation(second.suite)
229+
230+
val firstPotential = computePotential(first.suite)
231+
val secondPotential = computePotential(second.suite)
232+
233+
var netEnergyToDistribute = computeEnergySurplus(parentPotential, parentKinetic, firstPotential + secondPotential)
234+
if (netEnergyToDistribute < 0) {
235+
val covered = tryBorrowFromContainerToCoverDeficit(netEnergyToDistribute, energy)
236+
if (covered == null) {
237+
parent.numCollisions += 1
238+
return null
239+
}
240+
netEnergyToDistribute = covered
241+
}
242+
243+
val energySplitFraction = randomness.nextDouble()
244+
first.kineticEnergy = netEnergyToDistribute * energySplitFraction
245+
second.kineticEnergy = netEnergyToDistribute * (1.0 - energySplitFraction)
246+
first.numCollisions = 0
247+
second.numCollisions = 0
248+
return listOf(first, second)
249+
}
250+
251+
/**
252+
* Handles the inter-molecular ineffective collision, mutating both molecules
253+
* and accepting the new pair when energy is conserved or improved, while
254+
* splitting surplus kinetic energy between the offspring.
255+
*/
256+
private fun intermolecularIneffectiveCollision(
257+
first: Molecule<T>,
258+
second: Molecule<T>,
259+
): Pair<Molecule<T>, Molecule<T>>? {
260+
val firstPotential = computePotential(first.suite)
261+
val firstKinetic = first.kineticEnergy
262+
val secondPotential = computePotential(second.suite)
263+
val secondKinetic = second.kineticEnergy
264+
265+
val updatedFirst = first.copy(suite = first.suite.copy(), numCollisions = first.numCollisions + 1)
266+
val updatedSecond = second.copy(suite = second.suite.copy(), numCollisions = second.numCollisions + 1)
267+
applyMutation(updatedFirst.suite)
268+
applyMutation(updatedSecond.suite)
269+
270+
val updatedFirstPotential = computePotential(updatedFirst.suite)
271+
val updatedSecondPotential = computePotential(updatedSecond.suite)
272+
val netInterEnergy = computeEnergySurplus(
273+
firstPotential + secondPotential,
274+
firstKinetic + secondKinetic,
275+
updatedFirstPotential + updatedSecondPotential
276+
)
277+
278+
if (netInterEnergy >= 0) {
279+
val energySplitFraction = randomness.nextDouble()
280+
updatedFirst.kineticEnergy = netInterEnergy * energySplitFraction
281+
updatedSecond.kineticEnergy = netInterEnergy * (1.0 - energySplitFraction)
282+
return Pair(updatedFirst, updatedSecond)
283+
}
284+
return null
285+
}
286+
287+
/**
288+
* Executes synthesis between two molecules: crossover their suites, keep
289+
* the fitter fused offspring if energy allows, and reset the resulting
290+
* molecule’s collisions; otherwise increment collisions on the parents.
291+
*/
292+
private fun synthesis(
293+
first: Molecule<T>,
294+
second: Molecule<T>,
295+
): Molecule<T>? {
296+
val firstPotential = computePotential(first.suite)
297+
val firstKinetic = first.kineticEnergy
298+
val secondPotential = computePotential(second.suite)
299+
val secondKinetic = second.kineticEnergy
300+
301+
val firstOffspring = Molecule(first.suite.copy(), 0.0, 0)
302+
val secondOffspring = Molecule(second.suite.copy(), 0.0, 0)
303+
304+
applyCrossover(firstOffspring.suite, secondOffspring.suite)
305+
306+
val fused = if (firstOffspring.suite.calculateCombinedFitness() >= secondOffspring.suite.calculateCombinedFitness()) firstOffspring else secondOffspring
307+
val fusedPotential = computePotential(fused.suite)
308+
309+
val netSynthesisEnergy = computeEnergySurplus(
310+
firstPotential + secondPotential,
311+
firstKinetic + secondKinetic,
312+
fusedPotential
313+
)
314+
315+
if (netSynthesisEnergy >= 0) {
316+
fused.kineticEnergy = netSynthesisEnergy
317+
fused.numCollisions = 0
318+
return fused
319+
}
320+
321+
first.numCollisions += 1
322+
second.numCollisions += 1
323+
return null
172324
}
173325
}
174326

0 commit comments

Comments
 (0)