Skip to content

Commit 50afae0

Browse files
committed
refactor: created CroMolecule.kt, CroReactor.kt, testing: added CroReactorTest.kt
1 parent 6c744dd commit 50afae0

4 files changed

Lines changed: 450 additions & 268 deletions

File tree

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

Lines changed: 61 additions & 268 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,8 @@ package org.evomaster.core.search.algorithms
33
import org.evomaster.core.EMConfig
44
import org.evomaster.core.search.Individual
55
import org.evomaster.core.search.algorithms.wts.WtsEvalIndividual
6-
import org.slf4j.LoggerFactory
76
import kotlin.math.abs
87

9-
ollision rate cr, Decomposition threshold dt, Synthesis threshold st, Initial kinetic energy ke, Kinetic energy loss rate kr,
10-
118
/**
129
* Chemical Reaction Optimization (CRO)
1310
*
@@ -17,17 +14,8 @@ ollision rate cr, Decomposition threshold dt, Synthesis threshold st, Initial ki
1714
*/
1815
class CroAlgorithm<T> : AbstractGeneticAlgorithm<T>() where T : Individual {
1916

20-
companion object {
21-
private val log = LoggerFactory.getLogger(CroAlgorithm::class.java)
22-
}
23-
24-
private data class Molecule<T : Individual>(
25-
var suite: WtsEvalIndividual<T>,
26-
var kineticEnergy: Double,
27-
var numCollisions: Int
28-
)
29-
3017
private val molecules: MutableList<Molecule<T>> = mutableListOf()
18+
private lateinit var reactor: CroReactor<T>
3119

3220
// container is the global energy reservoir.
3321
// It collects kinetic energy lost in reactions and can be borrowed to enable otherwise infeasible decompositions, keeping total energy conserved.
@@ -48,68 +36,86 @@ class CroAlgorithm<T> : AbstractGeneticAlgorithm<T>() where T : Individual {
4836
// Initialize the underlying GA population to reuse sampling utilities
4937
super.setupBeforeSearch()
5038

39+
// Initialize reactor with dependencies once
40+
reactor = CroReactor(
41+
config = config,
42+
randomness = randomness,
43+
mutate = this::mutate,
44+
potential = this::potential,
45+
xover = this::xover
46+
)
47+
5148
// Convert GA population to CRO molecules with initial KE
52-
getViewOfPopulation().forEach { w ->
53-
molecules.add(Molecule(w.copy(), config.croInitialKineticEnergy, 0))
49+
getViewOfPopulation().forEach { evaluatedSuite ->
50+
molecules.add(Molecule(evaluatedSuite.copy(), config.croInitialKineticEnergy, 0))
5451
}
5552

56-
5753
// initialEnergy is the system’s starting total energy, used to enforce conservation.
5854
initialEnergy = getCurrentEnergy()
5955
}
6056

6157
override fun searchOnce() {
6258

6359
if (randomness.nextDouble() > config.croMolecularCollisionRate || molecules.size == 1) {
64-
log.debug("an uni-molecular collision has occurred")
6560
// Uni-molecular collision
66-
val idx = randomness.nextInt(molecules.size)
67-
val m = molecules[idx]
68-
69-
if (decompositionCheck(m)) {
70-
log.debug("a decomposition has occurred")
71-
val offsprings = decomposition(m)
72-
if (offsprings != null) {
73-
molecules.removeAt(idx)
74-
molecules.addAll(offsprings)
61+
val moleculeIndex = randomness.nextInt(molecules.size)
62+
val selectedMolecule = molecules[moleculeIndex]
63+
64+
if (decompositionCheck(selectedMolecule)) {
65+
val energyCtx = CroReactor.EnergyContext(container)
66+
val decomposedOffspring = reactor.decomposition(
67+
parent = selectedMolecule,
68+
energy = energyCtx,
69+
)
70+
container = energyCtx.container
71+
if (decomposedOffspring != null) {
72+
molecules.removeAt(moleculeIndex)
73+
molecules.addAll(decomposedOffspring)
7574
}
7675
} else {
77-
log.debug("an on-wall ineffective collision has occurred")
78-
val newMolecule = onWallIneffectiveCollision(m)
79-
if (newMolecule != null) {
80-
molecules[idx] = newMolecule
76+
val energyCtx = CroReactor.EnergyContext(container)
77+
val collidedMolecule = reactor.onWallIneffectiveCollision(
78+
molecule = selectedMolecule,
79+
energy = energyCtx,
80+
)
81+
container = energyCtx.container
82+
if (collidedMolecule != null) {
83+
molecules[moleculeIndex] = collidedMolecule
8184
}
8285
}
8386
} else {
84-
log.debug("an inter-molecular collision has occurred")
8587
// Inter-molecular collision
86-
val i1 = randomness.nextInt(molecules.size)
87-
var i2 = randomness.nextInt(molecules.size)
88-
while (i2 == i1) {
88+
val firstIndex = randomness.nextInt(molecules.size)
89+
var secondIndex = randomness.nextInt(molecules.size)
90+
while (secondIndex == firstIndex) {
8991
// find a different molecule as an inter-molecular collision involves at least two molecules
90-
i2 = randomness.nextInt(molecules.size)
92+
secondIndex = randomness.nextInt(molecules.size)
9193
}
9294

93-
val m1 = molecules[i1]
94-
val m2 = molecules[i2]
95+
val firstMolecule = molecules[firstIndex]
96+
val secondMolecule = molecules[secondIndex]
9597

96-
val shouldSynthesize = synthesisCheck(m1) && synthesisCheck(m2)
98+
val shouldSynthesize = synthesisCheck(firstMolecule) && synthesisCheck(secondMolecule)
9799
if (shouldSynthesize) {
98-
log.debug("a synthesis has occurred")
99-
val offspring = synthesis(m1, m2)
100-
if (offspring != null) {
101-
val low = minOf(i1, i2)
102-
val high = maxOf(i1, i2)
103-
molecules[low] = offspring
104-
molecules.removeAt(high)
100+
val fusedOffspring = reactor.synthesis(
101+
first = firstMolecule,
102+
second = secondMolecule,
103+
)
104+
if (fusedOffspring != null) {
105+
val lowIndex = minOf(firstIndex, secondIndex)
106+
val highIndex = maxOf(firstIndex, secondIndex)
107+
molecules[lowIndex] = fusedOffspring
108+
molecules.removeAt(highIndex)
105109
}
106110
} else {
107-
log.debug("an inter-molecular ineffective collision has occurred")
108-
val pair = intermolecularIneffectiveCollision(m1, m2)
109-
if (pair != null) {
110-
val (n1, n2) = pair
111-
molecules[i1] = n1
112-
molecules[i2] = n2
111+
val updatedPair = reactor.intermolecularIneffectiveCollision(
112+
first = firstMolecule,
113+
second = secondMolecule,
114+
)
115+
if (updatedPair != null) {
116+
val (updatedFirst, updatedSecond) = updatedPair
117+
molecules[firstIndex] = updatedFirst
118+
molecules[secondIndex] = updatedSecond
113119
}
114120
}
115121
}
@@ -130,228 +136,15 @@ class CroAlgorithm<T> : AbstractGeneticAlgorithm<T>() where T : Individual {
130136
}
131137
}
132138

133-
private fun potential(w: WtsEvalIndividual<T>): Double = -w.calculateCombinedFitness()
134-
135-
private fun decompositionCheck(m: Molecule<T>): Boolean = m.numCollisions > config.croDecompositionThreshold
136-
137-
private fun synthesisCheck(m: Molecule<T>): Boolean = m.kineticEnergy <= config.croSynthesisThreshold
138-
139-
/**
140-
* An on-wall ineffective collision represents the situation when a molecule collides with a wall
141-
* of the container and then bounces away remaining in one single unit.
142-
* @param m the input molecule
143-
* @return a new molecule if the on-wall mutation is energetically affordable; null otherwise
144-
*/
145-
private fun onWallIneffectiveCollision(m: Molecule<T>): Molecule<T>? {
146-
val pe = potential(m.suite)
147-
val ke = m.kineticEnergy
148-
val newM = m.copy(suite = m.suite.copy(), numCollisions = m.numCollisions + 1)
149-
150-
// mutate and evaluate in-place
151-
mutate(newM.suite)
152-
153-
val peNew = potential(newM.suite)
154-
val netOnWallEnergy = calculateNetOnWallEnergy(pe, ke, peNew)
155-
if (netOnWallEnergy < 0) {
156-
return null
157-
}
158-
applyOnWallEnergy(newM, netOnWallEnergy)
159-
log.debug("(" + pe + "," + ke + ") vs (" + peNew + "," + newM.kineticEnergy + ")\n" + "Container: " + container)
160-
return newM
161-
}
162-
163-
private fun calculateNetOnWallEnergy(pe: Double, ke: Double, peNew: Double): Double {
164-
// Available energy minus new potential energy; negative => not affordable
165-
return (pe + ke) - peNew
166-
}
139+
private fun potential(evaluatedSuite: WtsEvalIndividual<T>): Double = -evaluatedSuite.calculateCombinedFitness()
167140

168-
private fun applyOnWallEnergy(newM: Molecule<T>, netEnergy: Double) {
169-
val retainedFraction = randomness.nextDouble(config.croKineticEnergyLossRate, 1.0)
170-
newM.kineticEnergy = netEnergy * retainedFraction
171-
container += netEnergy * (1.0 - retainedFraction)
172-
}
173-
174-
/**
175-
* Decomposition: unary global operator. A molecule hits a wall and breaks into two molecules.
176-
*
177-
* @param m the parent molecule to decompose
178-
* @return a list with two offspring if the split is energetically affordable; null otherwise
179-
*/
180-
private fun decomposition(m: Molecule<T>): List<Molecule<T>>? {
181-
182-
// The idea of decomposition is to allow the system to explore other regions of the solution
183-
// space after enough local search by the ineffective collisions, similar to what mutation does
184-
// in evolutionary algorithms.
185-
186-
val pe = potential(m.suite)
187-
val ke = m.kineticEnergy
188-
189-
// Clone the molecules for the decomposition
190-
val o1 = Molecule(m.suite.copy(), ke = 0.0, numCollisions = 0)
191-
val o2 = Molecule(m.suite.copy(), ke = 0.0, numCollisions = 0)
192-
193-
// Mutate them
194-
mutate(o1.suite)
195-
mutate(o2.suite)
196-
197-
val pe1 = potential(o1.suite)
198-
val pe2 = potential(o2.suite)
199-
200-
// Compute the net energy balance for decomposition; if it's negative (deficit),
201-
// try to borrow from the container. Abort decomposition if borrowing cannot cover it.
202-
var netEnergyToDistribute = calculateNetEnergyToDistribute(pe, ke, pe1, pe2)
203-
if (netEnergyToDistribute < 0) {
204-
val covered = tryBorrowFromContainerToCoverDeficit(netEnergyToDistribute)
205-
if (covered == null) {
206-
m.numCollisions += 1
207-
return null
208-
}
209-
netEnergyToDistribute = covered
210-
}
211-
212-
// distribute kinetic energy after decomposition
213-
updateMoleculesAfterDecomposition(o1, o2, netEnergyToDistribute)
214-
log.debug("(" + pe + "," + ke + ") vs (" + pe1 + "," + o1.kineticEnergy + ") --- (" + pe2 + "," + o2.kineticEnergy + ")\n" + "Container: " + container + " of " + initialEnergy)
215-
return listOf(o1, o2)
216-
}
217-
218-
private fun calculateNetEnergyToDistribute(pe: Double, ke: Double, pe1: Double, pe2: Double): Double {
219-
// Balance after paying offspring potentials; positive => surplus, negative => deficit
220-
return (pe + ke) - (pe1 + pe2)
221-
}
141+
private fun decompositionCheck(molecule: Molecule<T>): Boolean = molecule.numCollisions > config.croDecompositionThreshold
222142

223-
private fun tryBorrowFromContainerToCoverDeficit(deficit: Double): Double? {
224-
// Draw a random fraction (d1*d2) of the container to cover the deficit if possible
225-
val d1 = randomness.nextDouble()
226-
val d2 = randomness.nextDouble()
227-
val canBorrow = d1 * d2 * container
228-
if (deficit + canBorrow >= 0) {
229-
container *= (1.0 - d1 * d2)
230-
return deficit + canBorrow
231-
}
232-
return null
233-
}
234-
235-
private fun updateMoleculesAfterDecomposition(m1: Molecule<T>, m2: Molecule<T>, netEnergyToDistribute: Double) {
236-
// distribute energy
237-
val energySplitFraction = randomness.nextDouble()
238-
m1.kineticEnergy = netEnergyToDistribute * energySplitFraction
239-
m2.kineticEnergy = netEnergyToDistribute * (1.0 - energySplitFraction)
240-
// reset number of collisions
241-
m1.numCollisions = 0
242-
m2.numCollisions = 0
243-
}
244-
245-
/**
246-
* Inter-molecular ineffective collision takes place when two molecules collide and then bounce away
247-
* as two separate molecules.
248-
*
249-
* @param m1 the first input molecule
250-
* @param m2 the second input molecule
251-
* @return a Pair of updated molecules if the combined energy can pay the new potentials (accepted);
252-
* null otherwise (collision rejected)
253-
*/
254-
private fun intermolecularIneffectiveCollision(m1: Molecule<T>, m2: Molecule<T>): Pair<Molecule<T>, Molecule<T>>? {
255-
// Snapshot current energies
256-
val pe1 = potential(m1.suite)
257-
val ke1 = m1.kineticEnergy
258-
val pe2 = potential(m2.suite)
259-
val ke2 = m2.kineticEnergy
260-
261-
// Clone, mark collision, and mutate both
262-
val n1 = m1.copy(suite = m1.suite.copy(), numCollisions = m1.numCollisions + 1)
263-
val n2 = m2.copy(suite = m2.suite.copy(), numCollisions = m2.numCollisions + 1)
264-
mutate(n1.suite)
265-
mutate(n2.suite)
266-
267-
// Compute new potentials and the net energy available to distribute as KE
268-
val pe1n = potential(n1.suite)
269-
val pe2n = potential(n2.suite)
270-
val netInterCollisionEnergy = calculateNetInterCollisionEnergy(pe1, ke1, pe2, ke2, pe1n, pe2n)
271-
272-
if (netInterCollisionEnergy >= 0) {
273-
distributeInterCollisionEnergy(n1, n2, netInterCollisionEnergy)
274-
log.debug("(" + pe1 + "," + ke1 + ") vs (" + pe1n + "," + n1.kineticEnergy + ")\n(" + pe2 + "," + ke2 + ") vs (" + pe2n + "," + n2.kineticEnergy + ")\n" + "Container: " + container)
275-
return Pair(n1, n2)
276-
}
277-
return null
278-
}
279-
280-
private fun calculateNetInterCollisionEnergy(
281-
pe1: Double, ke1: Double,
282-
pe2: Double, ke2: Double,
283-
pe1n: Double, pe2n: Double
284-
): Double {
285-
// Balance before vs after the collision; positive -> surplus to split as KE
286-
return (pe1 + pe2 + ke1 + ke2) - (pe1n + pe2n)
287-
}
288-
289-
private fun distributeInterCollisionEnergy(n1: Molecule<T>, n2: Molecule<T>, netEnergy: Double) {
290-
val energySplitFraction = randomness.nextDouble()
291-
n1.kineticEnergy = netEnergy * energySplitFraction
292-
n2.kineticEnergy = netEnergy * (1.0 - energySplitFraction)
293-
}
294-
295-
/**
296-
* Synthesis: does the opposite of decomposition. A synthesis happens when multiple (assume two)
297-
* molecules hit against each other and fuse together.
298-
* @param m1 the first input molecule
299-
* @param m2 the second input molecule
300-
* @return the fused offspring if energetically affordable; null otherwise
301-
*/
302-
private fun synthesis(m1: Molecule<T>, m2: Molecule<T>): Molecule<T>? {
303-
val pe1 = potential(m1.suite)
304-
val ke1 = m1.kineticEnergy
305-
val pe2 = potential(m2.suite)
306-
val ke2 = m2.kineticEnergy
307-
308-
val o1 = Molecule(m1.suite.copy(), 0.0, 0)
309-
val o2 = Molecule(m2.suite.copy(), 0.0, 0)
310-
311-
// crossover suites
312-
xover(o1.suite, o2.suite)
313-
314-
// choose the better offspring (higher combined fitness => lower potential energy)
315-
val fused = selectBetterOffspring(o1, o2)
316-
val peNew = potential(fused.suite)
317-
318-
// Compute net energy available to assign as KE to the fused molecule
319-
val netSynthesisEnergy = calculateNetSynthesisEnergy(pe1, ke1, pe2, ke2, peNew)
320-
321-
if (netSynthesisEnergy >= 0) {
322-
applySynthesisEnergy(fused, netSynthesisEnergy)
323-
log.debug("(" + pe1 + "," + ke1 + ") --- (" + pe2 + "," + ke2 + ") vs (" + peNew + "," + fused.kineticEnergy + ")\n" + "Container: " + container)
324-
return fused
325-
}
326-
327-
// Not affordable: reject and increase collision counters
328-
m1.numCollisions += 1
329-
m2.numCollisions += 1
330-
return null
331-
}
332-
333-
private fun selectBetterOffspring(o1: Molecule<T>, o2: Molecule<T>): Molecule<T> {
334-
return if (o1.suite.calculateCombinedFitness() >= o2.suite.calculateCombinedFitness()) o1 else o2
335-
}
336-
337-
private fun calculateNetSynthesisEnergy(
338-
pe1: Double, ke1: Double,
339-
pe2: Double, ke2: Double,
340-
peNew: Double
341-
): Double {
342-
// Total energy before fusion minus new potential energy
343-
val totalBefore = pe1 + pe2 + ke1 + ke2
344-
return totalBefore - peNew
345-
}
346-
347-
private fun applySynthesisEnergy(fused: Molecule<T>, netEnergy: Double) {
348-
fused.kineticEnergy = netEnergy
349-
fused.numCollisions = 0
350-
}
143+
private fun synthesisCheck(molecule: Molecule<T>): Boolean = molecule.kineticEnergy <= config.croSynthesisThreshold
351144

352145
private fun getCurrentEnergy(): Double {
353146
var energy = container
354-
molecules.forEach { energy += potential(it.suite) + it.kineticEnergy }
147+
molecules.forEach { molecule -> energy += potential(molecule.suite) + molecule.kineticEnergy }
355148
return energy
356149
}
357150

0 commit comments

Comments
 (0)