@@ -3,11 +3,8 @@ package org.evomaster.core.search.algorithms
33import org.evomaster.core.EMConfig
44import org.evomaster.core.search.Individual
55import org.evomaster.core.search.algorithms.wts.WtsEvalIndividual
6- import org.slf4j.LoggerFactory
76import 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 */
1815class 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