@@ -12,8 +12,13 @@ import kotlin.math.abs
1212 */
1313class 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