|
| 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). Potential energy (PE) |
| 12 | + * is defined as the negative of the combined fitness of the suite, so that minimization |
| 13 | + * semantics of the CRO equations align with EvoMaster's higher-is-better fitness. |
| 14 | + */ |
| 15 | +class CroAlgorithm<T> : AbstractGeneticAlgorithm<T>() where T : Individual { |
| 16 | + |
| 17 | + private data class Molecule<T : Individual>( |
| 18 | + var suite: WtsEvalIndividual<T>, |
| 19 | + var kineticEnergy: Double, |
| 20 | + var numCollisions: Int |
| 21 | + ) |
| 22 | + |
| 23 | + private val molecules: MutableList<Molecule<T>> = mutableListOf() |
| 24 | + |
| 25 | + private var buffer: Double = 0.0 |
| 26 | + private var initialEnergy: Double = 0.0 |
| 27 | + |
| 28 | + override fun getType(): EMConfig.Algorithm = EMConfig.Algorithm.CRO |
| 29 | + |
| 30 | + override fun setupBeforeSearch() { |
| 31 | + // Reuse GA population initialization to sample and evaluate initial suites |
| 32 | + molecules.clear() |
| 33 | + buffer = 0.0 |
| 34 | + |
| 35 | + // Initialize the underlying GA population to reuse sampling utilities |
| 36 | + super.setupBeforeSearch() |
| 37 | + |
| 38 | + // Convert GA population to CRO molecules with initial KE |
| 39 | + getViewOfPopulation().forEach { w -> |
| 40 | + molecules.add(Molecule(w.copy(), config.croInitialKineticEnergy, 0)) |
| 41 | + } |
| 42 | + |
| 43 | + initialEnergy = getCurrentEnergy() |
| 44 | + } |
| 45 | + |
| 46 | + override fun searchOnce() { |
| 47 | + if (molecules.isEmpty()) return |
| 48 | + |
| 49 | + val binary = randomness.nextDouble() <= config.croMolecularCollisionRate && molecules.size > 1 |
| 50 | + |
| 51 | + if (!binary) { |
| 52 | + // Uni-molecular collision |
| 53 | + val idx = randomness.nextInt(molecules.size) |
| 54 | + val m = molecules[idx] |
| 55 | + |
| 56 | + if (decompositionCheck(m)) { |
| 57 | + decomposition(m)?.let { offsprings -> |
| 58 | + molecules.removeAt(idx) |
| 59 | + molecules.addAll(offsprings) |
| 60 | + } |
| 61 | + } else { |
| 62 | + onWallIneffectiveCollision(m)?.let { nm -> |
| 63 | + molecules[idx] = nm |
| 64 | + } |
| 65 | + } |
| 66 | + } else { |
| 67 | + // Inter-molecular collision |
| 68 | + val i1 = randomness.nextInt(molecules.size) |
| 69 | + var i2 = randomness.nextInt(molecules.size) |
| 70 | + while (i2 == i1) i2 = randomness.nextInt(molecules.size) |
| 71 | + |
| 72 | + val m1 = molecules[i1] |
| 73 | + val m2 = molecules[i2] |
| 74 | + |
| 75 | + if (synthesisCheck(m1) && synthesisCheck(m2)) { |
| 76 | + synthesis(m1, m2)?.let { off -> |
| 77 | + val low = minOf(i1, i2) |
| 78 | + val high = maxOf(i1, i2) |
| 79 | + molecules[low] = off |
| 80 | + molecules.removeAt(high) |
| 81 | + } |
| 82 | + } else { |
| 83 | + intermolecularIneffectiveCollision(m1, m2)?.let { (n1, n2) -> |
| 84 | + molecules[i1] = n1 |
| 85 | + molecules[i2] = n2 |
| 86 | + } |
| 87 | + } |
| 88 | + } |
| 89 | + |
| 90 | + // Adjust buffer if external factors changed fitness values, to conserve energy |
| 91 | + val current = getCurrentEnergy() |
| 92 | + if (abs(current - initialEnergy) > 1e-9) { |
| 93 | + val delta = current - initialEnergy |
| 94 | + buffer -= delta |
| 95 | + } |
| 96 | + } |
| 97 | + |
| 98 | + private fun potential(w: WtsEvalIndividual<T>): Double = -w.calculateCombinedFitness() |
| 99 | + |
| 100 | + private fun decompositionCheck(m: Molecule<T>): Boolean = m.numCollisions > config.croDecompositionThreshold |
| 101 | + |
| 102 | + private fun synthesisCheck(m: Molecule<T>): Boolean = m.kineticEnergy <= config.croSynthesisThreshold |
| 103 | + |
| 104 | + private fun onWallIneffectiveCollision(m: Molecule<T>): Molecule<T>? { |
| 105 | + val pe = potential(m.suite) |
| 106 | + val ke = m.kineticEnergy |
| 107 | + val newM = m.copy(suite = m.suite.copy(), numCollisions = m.numCollisions + 1) |
| 108 | + |
| 109 | + // mutate and evaluate in-place |
| 110 | + mutate(newM.suite) |
| 111 | + |
| 112 | + val peNew = potential(newM.suite) |
| 113 | + return if (pe + ke >= peNew) { |
| 114 | + val a = randomness.nextDouble(config.croKineticEnergyLossRate, 1.0) |
| 115 | + val surplus = (pe - peNew + ke) |
| 116 | + newM.kineticEnergy = surplus * a |
| 117 | + buffer += surplus * (1.0 - a) |
| 118 | + newM |
| 119 | + } else null |
| 120 | + } |
| 121 | + |
| 122 | + private fun decomposition(m: Molecule<T>): List<Molecule<T>>? { |
| 123 | + val pe = potential(m.suite) |
| 124 | + val ke = m.kineticEnergy |
| 125 | + |
| 126 | + val o1 = Molecule(m.suite.copy(), ke = 0.0, numCollisions = 0) |
| 127 | + val o2 = Molecule(m.suite.copy(), ke = 0.0, numCollisions = 0) |
| 128 | + |
| 129 | + mutate(o1.suite) |
| 130 | + mutate(o2.suite) |
| 131 | + |
| 132 | + val pe1 = potential(o1.suite) |
| 133 | + val pe2 = potential(o2.suite) |
| 134 | + |
| 135 | + var eDec = (pe + ke) - (pe1 + pe2) |
| 136 | + if (eDec < 0) { |
| 137 | + val d1 = randomness.nextDouble() |
| 138 | + val d2 = randomness.nextDouble() |
| 139 | + val canBorrow = d1 * d2 * buffer |
| 140 | + if (eDec + canBorrow >= 0) { |
| 141 | + buffer *= (1.0 - d1 * d2) |
| 142 | + eDec += canBorrow |
| 143 | + } else { |
| 144 | + m.numCollisions += 1 |
| 145 | + return null |
| 146 | + } |
| 147 | + } |
| 148 | + |
| 149 | + // distribute kinetic energy and reset collisions |
| 150 | + val d3 = randomness.nextDouble() |
| 151 | + o1.kineticEnergy = eDec * d3 |
| 152 | + o2.kineticEnergy = eDec * (1.0 - d3) |
| 153 | + o1.numCollisions = 0 |
| 154 | + o2.numCollisions = 0 |
| 155 | + return listOf(o1, o2) |
| 156 | + } |
| 157 | + |
| 158 | + private fun intermolecularIneffectiveCollision(m1: Molecule<T>, m2: Molecule<T>): Pair<Molecule<T>, Molecule<T>>? { |
| 159 | + val pe1 = potential(m1.suite) |
| 160 | + val ke1 = m1.kineticEnergy |
| 161 | + val pe2 = potential(m2.suite) |
| 162 | + val ke2 = m2.kineticEnergy |
| 163 | + |
| 164 | + val n1 = m1.copy(suite = m1.suite.copy(), numCollisions = m1.numCollisions + 1) |
| 165 | + val n2 = m2.copy(suite = m2.suite.copy(), numCollisions = m2.numCollisions + 1) |
| 166 | + |
| 167 | + mutate(n1.suite) |
| 168 | + mutate(n2.suite) |
| 169 | + |
| 170 | + val pe1n = potential(n1.suite) |
| 171 | + val pe2n = potential(n2.suite) |
| 172 | + |
| 173 | + val eInter = (pe1 + pe2 + ke1 + ke2) - (pe1n + pe2n) |
| 174 | + return if (eInter >= 0) { |
| 175 | + val d4 = randomness.nextDouble() |
| 176 | + n1.kineticEnergy = eInter * d4 |
| 177 | + n2.kineticEnergy = eInter * (1.0 - d4) |
| 178 | + Pair(n1, n2) |
| 179 | + } else null |
| 180 | + } |
| 181 | + |
| 182 | + private fun synthesis(m1: Molecule<T>, m2: Molecule<T>): Molecule<T>? { |
| 183 | + val pe1 = potential(m1.suite) |
| 184 | + val ke1 = m1.kineticEnergy |
| 185 | + val pe2 = potential(m2.suite) |
| 186 | + val ke2 = m2.kineticEnergy |
| 187 | + |
| 188 | + val o1 = Molecule(m1.suite.copy(), 0.0, 0) |
| 189 | + val o2 = Molecule(m2.suite.copy(), 0.0, 0) |
| 190 | + |
| 191 | + // crossover suites |
| 192 | + xover(o1.suite, o2.suite) |
| 193 | + |
| 194 | + // choose the better offspring (higher combined fitness => lower potential energy) |
| 195 | + val best = if (o1.suite.calculateCombinedFitness() >= o2.suite.calculateCombinedFitness()) o1 else o2 |
| 196 | + val pe = potential(best.suite) |
| 197 | + return if (pe1 + pe2 + ke1 + ke2 >= pe) { |
| 198 | + best.kineticEnergy = (pe1 + pe2 + ke1 + ke2) - pe |
| 199 | + best.numCollisions = 0 |
| 200 | + best |
| 201 | + } else { |
| 202 | + m1.numCollisions += 1 |
| 203 | + m2.numCollisions += 1 |
| 204 | + null |
| 205 | + } |
| 206 | + } |
| 207 | + |
| 208 | + private fun getCurrentEnergy(): Double { |
| 209 | + var energy = buffer |
| 210 | + molecules.forEach { energy += potential(it.suite) + it.kineticEnergy } |
| 211 | + return energy |
| 212 | + } |
| 213 | +} |
| 214 | + |
| 215 | + |
0 commit comments