Skip to content

Commit f8fe979

Browse files
authored
Merge pull request #1380 from WebFuzzing/feature/chemical-reaction-optimization
CRO Algorithm
2 parents ef7ed45 + c99c207 commit f8fe979

5 files changed

Lines changed: 788 additions & 2 deletions

File tree

core/src/main/kotlin/org/evomaster/core/EMConfig.kt

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1162,7 +1162,7 @@ class EMConfig {
11621162

11631163
enum class Algorithm {
11641164
DEFAULT, SMARTS, MIO, RANDOM, WTS, MOSA, RW,
1165-
StandardGA, MonotonicGA, SteadyStateGA, BreederGA, CellularGA, OnePlusLambdaLambdaGA, MuLambdaEA, MuPlusLambdaEA, LIPS // GA variants still work-in-progress.
1165+
StandardGA, MonotonicGA, SteadyStateGA, BreederGA, CellularGA, OnePlusLambdaLambdaGA, MuLambdaEA, MuPlusLambdaEA, LIPS, CRO // GA variants still work-in-progress.
11661166
}
11671167

11681168
@Cfg("The algorithm used to generate test cases. The default depends on whether black-box or white-box testing is done.")
@@ -1575,6 +1575,27 @@ class EMConfig {
15751575
@Min(1.0)
15761576
var tournamentSize = 10
15771577

1578+
// --- Chemical Reaction Optimization (CRO) parameters ---
1579+
@Cfg("CRO: Molecular collision rate c_r (probability of binary reactions)")
1580+
@Probability
1581+
var croMolecularCollisionRate: Double = 0.2
1582+
1583+
@Cfg("CRO: Kinetic energy loss rate k_r (lower bound of retained fraction after on-wall)")
1584+
@Probability
1585+
var croKineticEnergyLossRate: Double = 0.2
1586+
1587+
@Cfg("CRO: Initial kinetic energy assigned to each molecule")
1588+
@Min(0.0)
1589+
var croInitialKineticEnergy: Double = 1000.0
1590+
1591+
@Cfg("CRO: Decomposition threshold d_t (min number of collisions before decomposition)")
1592+
@Min(0.0)
1593+
var croDecompositionThreshold: Int = 500
1594+
1595+
@Cfg("CRO: Synthesis KE threshold s_t (molecule can synthesize if KE ≤ s_t)")
1596+
@Min(0.0)
1597+
var croSynthesisThreshold: Double = 10.0
1598+
15781599
@Cfg("When sampling new test cases to evaluate, probability of using some smart strategy instead of plain random")
15791600
@Probability
15801601
var probOfSmartSampling = 0.95

core/src/main/kotlin/org/evomaster/core/Main.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -791,6 +791,9 @@ class Main {
791791
EMConfig.Algorithm.OnePlusLambdaLambdaGA ->
792792
Key.get(object : TypeLiteral<OnePlusLambdaLambdaGeneticAlgorithm<RestIndividual>>() {})
793793

794+
EMConfig.Algorithm.CRO ->
795+
Key.get(object : TypeLiteral<CroAlgorithm<RestIndividual>>() {})
796+
794797
else -> throw IllegalStateException("Unrecognized algorithm ${config.algorithm}")
795798
}
796799
}
Lines changed: 341 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,341 @@
1+
package org.evomaster.core.search.algorithms
2+
3+
import org.evomaster.core.EMConfig
4+
import org.evomaster.core.search.Individual
5+
import org.evomaster.core.search.algorithms.wts.WtsEvalIndividual
6+
import kotlin.math.abs
7+
8+
/**
9+
* Chemical Reaction Optimization (CRO)
10+
*
11+
* Each molecule corresponds to a [WtsEvalIndividual] (a test suite).
12+
*/
13+
open class CroAlgorithm<T> : AbstractGeneticAlgorithm<T>() where T : Individual {
14+
15+
companion object {
16+
private const val ENERGY_TOLERANCE = 1e-9
17+
}
18+
19+
private data class EnergyContext(var container: Double)
20+
21+
data class Molecule<T : Individual>(
22+
var suite: WtsEvalIndividual<T>,
23+
var kineticEnergy: Double,
24+
var numCollisions: Int
25+
)
26+
27+
private val molecules: MutableList<Molecule<T>> = mutableListOf()
28+
29+
// container is the global energy reservoir.
30+
// It collects kinetic energy lost in reactions and can be borrowed to enable otherwise infeasible decompositions, keeping total energy conserved.
31+
private var container: Double = 0.0
32+
33+
34+
// initialEnergy is the system’s starting total energy, used to enforce conservation.
35+
// It’s computed right after building the initial molecules as: buffer + Σ(PE + KE) over all molecules.
36+
private var initialEnergy: Double = 0.0
37+
38+
override fun getType(): EMConfig.Algorithm = EMConfig.Algorithm.CRO
39+
40+
override fun setupBeforeSearch() {
41+
// Reuse GA population initialization to sample and evaluate initial suites
42+
molecules.clear()
43+
container = 0.0
44+
45+
// Initialize the underlying GA population to reuse sampling utilities
46+
super.setupBeforeSearch()
47+
48+
// Convert GA population to CRO molecules with initial KE
49+
getViewOfPopulation().forEach { evaluatedSuite ->
50+
molecules.add(Molecule(evaluatedSuite.copy(), config.croInitialKineticEnergy, 0))
51+
}
52+
53+
// initialEnergy is the system’s starting total energy, used to enforce conservation.
54+
initialEnergy = getCurrentEnergy()
55+
}
56+
57+
/**
58+
* Read-only snapshot of molecules for assertions in tests.
59+
*/
60+
fun getMoleculesSnapshot(): List<Molecule<T>> = molecules.map { m ->
61+
Molecule(m.suite, m.kineticEnergy, m.numCollisions)
62+
}
63+
64+
override fun searchOnce() {
65+
66+
if (randomness.nextDouble() > config.croMolecularCollisionRate || molecules.size == 1) {
67+
performUniMolecularCollision()
68+
} else {
69+
performInterMolecularCollision()
70+
}
71+
72+
// Adjust container if external factors changed fitness values, to conserve energy
73+
val current = getCurrentEnergy()
74+
if (abs(current - initialEnergy) > ENERGY_TOLERANCE) {
75+
val delta = current - initialEnergy
76+
container -= delta
77+
}
78+
79+
// Sanity check: conservation of energy must hold
80+
val energyAfter = getCurrentEnergy()
81+
if (!hasEnergyBeenConserved(energyAfter)) {
82+
throw RuntimeException("Current amount of energy (" + energyAfter
83+
+ ") in the system is not equal to its initial amount of energy (" + this.initialEnergy
84+
+ "). Conservation of energy has failed!")
85+
}
86+
}
87+
88+
private fun performUniMolecularCollision() {
89+
// Uni-molecular collision
90+
val moleculeIndex = randomness.nextInt(molecules.size)
91+
val selectedMolecule = molecules[moleculeIndex]
92+
93+
if (decompositionCheck(selectedMolecule)) {
94+
val energyCtx = EnergyContext(container)
95+
val decomposedOffspring = decomposition(
96+
parent = selectedMolecule,
97+
energy = energyCtx,
98+
)
99+
container = energyCtx.container
100+
if (decomposedOffspring != null) {
101+
molecules.removeAt(moleculeIndex)
102+
molecules.addAll(decomposedOffspring)
103+
}
104+
} else {
105+
val energyCtx = EnergyContext(container)
106+
val collidedMolecule = onWallIneffectiveCollision(
107+
molecule = selectedMolecule,
108+
energy = energyCtx,
109+
)
110+
container = energyCtx.container
111+
if (collidedMolecule != null) {
112+
molecules[moleculeIndex] = collidedMolecule
113+
}
114+
}
115+
}
116+
117+
private fun performInterMolecularCollision() {
118+
// Inter-molecular collision
119+
val firstIndex = randomness.nextInt(molecules.size)
120+
var secondIndex = randomness.nextInt(molecules.size)
121+
while (secondIndex == firstIndex) {
122+
// find a different molecule as an inter-molecular collision involves at least two molecules
123+
secondIndex = randomness.nextInt(molecules.size)
124+
}
125+
126+
val firstMolecule = molecules[firstIndex]
127+
val secondMolecule = molecules[secondIndex]
128+
129+
val shouldSynthesize = synthesisCheck(firstMolecule) && synthesisCheck(secondMolecule)
130+
if (shouldSynthesize) {
131+
val fusedOffspring = synthesis(
132+
first = firstMolecule,
133+
second = secondMolecule,
134+
)
135+
if (fusedOffspring != null) {
136+
val lowIndex = minOf(firstIndex, secondIndex)
137+
val highIndex = maxOf(firstIndex, secondIndex)
138+
molecules[lowIndex] = fusedOffspring
139+
molecules.removeAt(highIndex)
140+
}
141+
} else {
142+
val updatedPair = intermolecularIneffectiveCollision(
143+
first = firstMolecule,
144+
second = secondMolecule,
145+
)
146+
if (updatedPair != null) {
147+
val (updatedFirst, updatedSecond) = updatedPair
148+
molecules[firstIndex] = updatedFirst
149+
molecules[secondIndex] = updatedSecond
150+
}
151+
}
152+
}
153+
154+
protected open fun computePotential(evaluatedSuite: WtsEvalIndividual<T>): Double = -evaluatedSuite.calculateCombinedFitness()
155+
156+
protected open fun applyMutation(wts: WtsEvalIndividual<T>) {
157+
mutate(wts)
158+
}
159+
160+
protected open fun applyCrossover(first: WtsEvalIndividual<T>, second: WtsEvalIndividual<T>) {
161+
xover(first, second)
162+
}
163+
164+
private fun decompositionCheck(molecule: Molecule<T>): Boolean = molecule.numCollisions > config.croDecompositionThreshold
165+
166+
private fun synthesisCheck(molecule: Molecule<T>): Boolean = molecule.kineticEnergy <= config.croSynthesisThreshold
167+
168+
private fun getCurrentEnergy(): Double {
169+
var energy = container
170+
molecules.forEach { molecule -> energy += computePotential(molecule.suite) + molecule.kineticEnergy }
171+
return energy
172+
}
173+
174+
/**
175+
* Given a certain amount of energy, it checks whether energy has been conserved in the system.
176+
*
177+
* @param energy current measured total energy (container + sum of potentials and kinetic energies)
178+
* @return true if energy has been conserved in the system, false otherwise
179+
*/
180+
private fun hasEnergyBeenConserved(energy: Double): Boolean {
181+
return abs(this.initialEnergy - energy) < ENERGY_TOLERANCE
182+
}
183+
184+
private fun computeEnergySurplus(totalBeforePotential: Double, totalBeforeKinetic: Double, totalAfterPotential: Double): Double =
185+
(totalBeforePotential + totalBeforeKinetic) - totalAfterPotential
186+
187+
private fun tryBorrowFromContainerToCoverDeficit(deficit: Double, energy: EnergyContext): Double? {
188+
val fractionA = randomness.nextDouble()
189+
val fractionB = randomness.nextDouble()
190+
val borrowableAmount = fractionA * fractionB * energy.container
191+
return if (deficit + borrowableAmount >= 0) {
192+
energy.container *= (1.0 - fractionA * fractionB)
193+
deficit + borrowableAmount
194+
} else {
195+
null
196+
}
197+
}
198+
199+
/**
200+
* Applies the uni-molecular on-wall ineffective collision:
201+
* mutate the molecule, keep it if energy surplus is non-negative,
202+
* split surplus between kinetic energy and the global container, and
203+
* increment collisions.
204+
*/
205+
private fun onWallIneffectiveCollision(
206+
molecule: Molecule<T>,
207+
energy: EnergyContext,
208+
): Molecule<T>? {
209+
val oldPotential = computePotential(molecule.suite)
210+
val oldKinetic = molecule.kineticEnergy
211+
val updated = molecule.copy(suite = molecule.suite.copy(), numCollisions = molecule.numCollisions + 1)
212+
213+
applyMutation(updated.suite)
214+
215+
val newPotential = computePotential(updated.suite)
216+
val netOnWallEnergy = computeEnergySurplus(oldPotential, oldKinetic, newPotential)
217+
if (netOnWallEnergy < 0) return null
218+
219+
val retainedFraction = randomness.nextDouble(config.croKineticEnergyLossRate, 1.0)
220+
updated.kineticEnergy = netOnWallEnergy * retainedFraction
221+
energy.container += netOnWallEnergy * (1.0 - retainedFraction)
222+
return updated
223+
}
224+
225+
/**
226+
* Performs decomposition of a molecule into two offspring.
227+
* Mutates two copies, accepts if total surplus (after optional borrowing
228+
* from the container) is non-negative, and distributes kinetic energy
229+
* and resets collisions.
230+
*/
231+
private fun decomposition(
232+
parent: Molecule<T>,
233+
energy: EnergyContext,
234+
): List<Molecule<T>>? {
235+
val parentPotential = computePotential(parent.suite)
236+
val parentKinetic = parent.kineticEnergy
237+
238+
val first = Molecule(parent.suite.copy(), kineticEnergy = 0.0, numCollisions = 0)
239+
val second = Molecule(parent.suite.copy(), kineticEnergy = 0.0, numCollisions = 0)
240+
241+
applyMutation(first.suite)
242+
applyMutation(second.suite)
243+
244+
val firstPotential = computePotential(first.suite)
245+
val secondPotential = computePotential(second.suite)
246+
247+
var netEnergyToDistribute = computeEnergySurplus(parentPotential, parentKinetic, firstPotential + secondPotential)
248+
if (netEnergyToDistribute < 0) {
249+
val covered = tryBorrowFromContainerToCoverDeficit(netEnergyToDistribute, energy)
250+
if (covered == null) {
251+
parent.numCollisions += 1
252+
return null
253+
}
254+
netEnergyToDistribute = covered
255+
}
256+
257+
val energySplitFraction = randomness.nextDouble()
258+
first.kineticEnergy = netEnergyToDistribute * energySplitFraction
259+
second.kineticEnergy = netEnergyToDistribute * (1.0 - energySplitFraction)
260+
first.numCollisions = 0
261+
second.numCollisions = 0
262+
return listOf(first, second)
263+
}
264+
265+
/**
266+
* Handles the inter-molecular ineffective collision, mutating both molecules
267+
* and accepting the new pair when energy is conserved or improved, while
268+
* splitting surplus kinetic energy between the offspring.
269+
*/
270+
private fun intermolecularIneffectiveCollision(
271+
first: Molecule<T>,
272+
second: Molecule<T>,
273+
): Pair<Molecule<T>, Molecule<T>>? {
274+
val firstPotential = computePotential(first.suite)
275+
val firstKinetic = first.kineticEnergy
276+
val secondPotential = computePotential(second.suite)
277+
val secondKinetic = second.kineticEnergy
278+
279+
val updatedFirst = first.copy(suite = first.suite.copy(), numCollisions = first.numCollisions + 1)
280+
val updatedSecond = second.copy(suite = second.suite.copy(), numCollisions = second.numCollisions + 1)
281+
applyMutation(updatedFirst.suite)
282+
applyMutation(updatedSecond.suite)
283+
284+
val updatedFirstPotential = computePotential(updatedFirst.suite)
285+
val updatedSecondPotential = computePotential(updatedSecond.suite)
286+
val netInterEnergy = computeEnergySurplus(
287+
firstPotential + secondPotential,
288+
firstKinetic + secondKinetic,
289+
updatedFirstPotential + updatedSecondPotential
290+
)
291+
292+
if (netInterEnergy >= 0) {
293+
val energySplitFraction = randomness.nextDouble()
294+
updatedFirst.kineticEnergy = netInterEnergy * energySplitFraction
295+
updatedSecond.kineticEnergy = netInterEnergy * (1.0 - energySplitFraction)
296+
return Pair(updatedFirst, updatedSecond)
297+
}
298+
return null
299+
}
300+
301+
/**
302+
* Executes synthesis between two molecules: crossover their suites, keep
303+
* the fitter fused offspring if energy allows, and reset the resulting
304+
* molecule’s collisions; otherwise increment collisions on the parents.
305+
*/
306+
private fun synthesis(
307+
first: Molecule<T>,
308+
second: Molecule<T>,
309+
): Molecule<T>? {
310+
val firstPotential = computePotential(first.suite)
311+
val firstKinetic = first.kineticEnergy
312+
val secondPotential = computePotential(second.suite)
313+
val secondKinetic = second.kineticEnergy
314+
315+
val firstOffspring = Molecule(first.suite.copy(), 0.0, 0)
316+
val secondOffspring = Molecule(second.suite.copy(), 0.0, 0)
317+
318+
applyCrossover(firstOffspring.suite, secondOffspring.suite)
319+
320+
val fused = if (firstOffspring.suite.calculateCombinedFitness() >= secondOffspring.suite.calculateCombinedFitness()) firstOffspring else secondOffspring
321+
val fusedPotential = computePotential(fused.suite)
322+
323+
val netSynthesisEnergy = computeEnergySurplus(
324+
firstPotential + secondPotential,
325+
firstKinetic + secondKinetic,
326+
fusedPotential
327+
)
328+
329+
if (netSynthesisEnergy >= 0) {
330+
fused.kineticEnergy = netSynthesisEnergy
331+
fused.numCollisions = 0
332+
return fused
333+
}
334+
335+
first.numCollisions += 1
336+
second.numCollisions += 1
337+
return null
338+
}
339+
}
340+
341+

0 commit comments

Comments
 (0)