|  | 
|  | 1 | +package org.evomaster.core.search.algorithms | 
|  | 2 | + | 
|  | 3 | +import com.google.inject.Injector | 
|  | 4 | +import com.google.inject.Key | 
|  | 5 | +import com.google.inject.Module | 
|  | 6 | +import com.google.inject.TypeLiteral | 
|  | 7 | +import com.netflix.governator.guice.LifecycleInjector | 
|  | 8 | +import org.evomaster.core.BaseModule | 
|  | 9 | +import org.evomaster.core.EMConfig | 
|  | 10 | +import org.evomaster.core.TestUtils | 
|  | 11 | +import org.evomaster.core.search.algorithms.onemax.OneMaxIndividual | 
|  | 12 | +import org.evomaster.core.search.algorithms.onemax.OneMaxModule | 
|  | 13 | +import org.evomaster.core.search.algorithms.onemax.OneMaxSampler | 
|  | 14 | +import org.evomaster.core.search.algorithms.observer.GARecorder | 
|  | 15 | +import org.evomaster.core.search.algorithms.strategy.FixedSelectionStrategy | 
|  | 16 | +import org.evomaster.core.search.service.ExecutionPhaseController | 
|  | 17 | +import org.evomaster.core.search.service.Randomness | 
|  | 18 | +import org.junit.jupiter.api.Assertions.* | 
|  | 19 | +import org.junit.jupiter.api.BeforeEach | 
|  | 20 | +import org.junit.jupiter.api.Test | 
|  | 21 | + | 
|  | 22 | +class SteadyStateGeneticAlgorithmTest { | 
|  | 23 | + | 
|  | 24 | +    private lateinit var injector: Injector | 
|  | 25 | + | 
|  | 26 | +    @BeforeEach | 
|  | 27 | +    fun setUp() { | 
|  | 28 | +        injector = LifecycleInjector.builder() | 
|  | 29 | +            .withModules(* arrayOf<Module>(OneMaxModule(), BaseModule())) | 
|  | 30 | +            .build().createInjector() | 
|  | 31 | +    } | 
|  | 32 | + | 
|  | 33 | +    // Verifies that the Steady-State GA can find the optimal solution for the OneMax problem | 
|  | 34 | +    @Test | 
|  | 35 | +    fun testSteadyStateAlgorithm() { | 
|  | 36 | +        TestUtils.handleFlaky { | 
|  | 37 | +            val steadyStateAlgorithm = injector.getInstance( | 
|  | 38 | +                Key.get( | 
|  | 39 | +                    object : TypeLiteral<SteadyStateGeneticAlgorithm<OneMaxIndividual>>() {}) | 
|  | 40 | +            ) | 
|  | 41 | + | 
|  | 42 | +            val config = injector.getInstance(EMConfig::class.java) | 
|  | 43 | +            config.maxEvaluations = 10000 | 
|  | 44 | +            config.stoppingCriterion = EMConfig.StoppingCriterion.ACTION_EVALUATIONS | 
|  | 45 | + | 
|  | 46 | +            val epc = injector.getInstance(ExecutionPhaseController::class.java) | 
|  | 47 | +            epc.startSearch() | 
|  | 48 | +            val solution = steadyStateAlgorithm.search() | 
|  | 49 | +            epc.finishSearch() | 
|  | 50 | +            assertTrue(solution.individuals.size == 1) | 
|  | 51 | +            assertEquals(OneMaxSampler.DEFAULT_N.toDouble(), solution.overall.computeFitnessScore(), 0.001) | 
|  | 52 | +        } | 
|  | 53 | +    } | 
|  | 54 | + | 
|  | 55 | +     // Verifies steady-state replacement: replace parents only if children are better | 
|  | 56 | +    @Test | 
|  | 57 | +    fun testSteadyStateReplacementIfChildrenBetter() { | 
|  | 58 | +        TestUtils.handleFlaky { | 
|  | 59 | +            val fixedSel = FixedSelectionStrategy() | 
|  | 60 | +            val (ga, localInjector) = createGAWithSelection(fixedSel) | 
|  | 61 | + | 
|  | 62 | +            val rec = GARecorder<OneMaxIndividual>() | 
|  | 63 | +            ga.addObserver(rec) | 
|  | 64 | + | 
|  | 65 | +            val config = localInjector.getInstance(EMConfig::class.java) | 
|  | 66 | +            localInjector.getInstance(Randomness::class.java).updateSeed(42) | 
|  | 67 | + | 
|  | 68 | +            config.populationSize = 4 | 
|  | 69 | +            config.xoverProbability = 1.0 | 
|  | 70 | +            config.fixedRateMutation = 1.0 | 
|  | 71 | +            config.gaSolutionSource = EMConfig.GASolutionSource.POPULATION | 
|  | 72 | +            config.maxEvaluations = 100_000 | 
|  | 73 | +            config.stoppingCriterion = EMConfig.StoppingCriterion.ACTION_EVALUATIONS | 
|  | 74 | + | 
|  | 75 | +            ga.setupBeforeSearch() | 
|  | 76 | + | 
|  | 77 | +            val pop = ga.getViewOfPopulation() | 
|  | 78 | + | 
|  | 79 | +            // Select 2 deterministic parents from the initial population | 
|  | 80 | +            val p1 = pop[0] | 
|  | 81 | +            val p2 = pop[1] | 
|  | 82 | +            fixedSel.setOrder(listOf(p1, p2)) | 
|  | 83 | + | 
|  | 84 | +            ga.searchOnce() | 
|  | 85 | + | 
|  | 86 | +            val nextPop = ga.getViewOfPopulation() | 
|  | 87 | + | 
|  | 88 | +            // Size preserved | 
|  | 89 | +            assertEquals(config.populationSize, nextPop.size) | 
|  | 90 | + | 
|  | 91 | +            // Exactly two selections | 
|  | 92 | +            assertEquals(2, rec.selections.size) | 
|  | 93 | + | 
|  | 94 | +            // Crossover was called, capture offspring | 
|  | 95 | +            assertEquals(1, rec.xoCalls.size) | 
|  | 96 | +            val (o1, o2) = rec.xoCalls[0] | 
|  | 97 | + | 
|  | 98 | +            // Replacement rule: keep offspring only if better than parents | 
|  | 99 | +            val parentBest = kotlin.math.max(ga.score(p1), ga.score(p2)) | 
|  | 100 | +            val childBest = kotlin.math.max(ga.score(o1), ga.score(o2)) | 
|  | 101 | + | 
|  | 102 | +            if (childBest > parentBest) { | 
|  | 103 | +                assertTrue(nextPop.any { it === o1 }) | 
|  | 104 | +                assertTrue(nextPop.any { it === o2 }) | 
|  | 105 | +                assertFalse(nextPop.containsAll(listOf(p1, p2))) | 
|  | 106 | +            } else { | 
|  | 107 | +                assertTrue(nextPop.any { it === p1 }) | 
|  | 108 | +                assertTrue(nextPop.any { it === p2 }) | 
|  | 109 | +                assertFalse(nextPop.containsAll(listOf(o1, o2))) | 
|  | 110 | +            } | 
|  | 111 | + | 
|  | 112 | +            // Mutation applied twice to offspring | 
|  | 113 | +            assertEquals(2, rec.mutated.size) | 
|  | 114 | +            assertTrue(rec.mutated.any { it === o1 }) | 
|  | 115 | +            assertTrue(rec.mutated.any { it === o2 }) | 
|  | 116 | +        } | 
|  | 117 | +    } | 
|  | 118 | + | 
|  | 119 | +    // Edge Case: CrossoverProbability=0 on SSGA | 
|  | 120 | +    @Test | 
|  | 121 | +    fun testNoCrossoverWhenProbabilityZero_SSGA() { | 
|  | 122 | +        TestUtils.handleFlaky { | 
|  | 123 | +            val fixedSel = FixedSelectionStrategy() | 
|  | 124 | +            val (ga, localInjector) = createGAWithSelection(fixedSel) | 
|  | 125 | + | 
|  | 126 | +            val rec = GARecorder<OneMaxIndividual>() | 
|  | 127 | +            ga.addObserver(rec) | 
|  | 128 | + | 
|  | 129 | +            val config = localInjector.getInstance(EMConfig::class.java) | 
|  | 130 | +            localInjector.getInstance(Randomness::class.java).updateSeed(42) | 
|  | 131 | + | 
|  | 132 | +            config.populationSize = 4 | 
|  | 133 | +            config.xoverProbability = 0.0 // disable crossover | 
|  | 134 | +            config.fixedRateMutation = 1.0 // force mutation | 
|  | 135 | +            config.gaSolutionSource = EMConfig.GASolutionSource.POPULATION | 
|  | 136 | +            config.maxEvaluations = 100_000 | 
|  | 137 | +            config.stoppingCriterion = EMConfig.StoppingCriterion.ACTION_EVALUATIONS | 
|  | 138 | + | 
|  | 139 | +            ga.setupBeforeSearch() | 
|  | 140 | +            // Provide a deterministic selection order for the 2 selections in SSGA | 
|  | 141 | +            val init = ga.getViewOfPopulation() | 
|  | 142 | +            fixedSel.setOrder(listOf(init[0], init[1])) | 
|  | 143 | +            ga.searchOnce() | 
|  | 144 | + | 
|  | 145 | +            // population size preserved | 
|  | 146 | +            val nextPop = ga.getViewOfPopulation() | 
|  | 147 | +            assertEquals(config.populationSize, nextPop.size) | 
|  | 148 | + | 
|  | 149 | +            // exactly two selections in one steady-state step | 
|  | 150 | +            assertEquals(2, rec.selections.size) | 
|  | 151 | +            // crossover disabled | 
|  | 152 | +            assertEquals(0, rec.xoCalls.size) | 
|  | 153 | +            // two mutations (one per offspring) | 
|  | 154 | +            assertEquals(2, rec.mutated.size) | 
|  | 155 | +        } | 
|  | 156 | +    } | 
|  | 157 | + | 
|  | 158 | +    // Edge Case: MutationProbability=0 on SSGA | 
|  | 159 | +    @Test | 
|  | 160 | +    fun testNoMutationWhenProbabilityZero_SSGA() { | 
|  | 161 | +        TestUtils.handleFlaky { | 
|  | 162 | +            val fixedSel = FixedSelectionStrategy() | 
|  | 163 | +            val (ga, localInjector) = createGAWithSelection(fixedSel) | 
|  | 164 | + | 
|  | 165 | +            val rec = GARecorder<OneMaxIndividual>() | 
|  | 166 | +            ga.addObserver(rec) | 
|  | 167 | + | 
|  | 168 | +            val config = localInjector.getInstance(EMConfig::class.java) | 
|  | 169 | +            localInjector.getInstance(Randomness::class.java).updateSeed(42) | 
|  | 170 | + | 
|  | 171 | +            config.populationSize = 4 | 
|  | 172 | +            config.xoverProbability = 1.0 // force crossover | 
|  | 173 | +            config.fixedRateMutation = 0.0 // disable mutation | 
|  | 174 | +            config.gaSolutionSource = EMConfig.GASolutionSource.POPULATION | 
|  | 175 | +            config.maxEvaluations = 100_000 | 
|  | 176 | +            config.stoppingCriterion = EMConfig.StoppingCriterion.ACTION_EVALUATIONS | 
|  | 177 | + | 
|  | 178 | +            ga.setupBeforeSearch() | 
|  | 179 | +            val init = ga.getViewOfPopulation() | 
|  | 180 | +            fixedSel.setOrder(listOf(init[0], init[1])) | 
|  | 181 | +            ga.searchOnce() | 
|  | 182 | + | 
|  | 183 | +            val nextPop = ga.getViewOfPopulation() | 
|  | 184 | +            assertEquals(config.populationSize, nextPop.size) | 
|  | 185 | + | 
|  | 186 | +            // two selections, one crossover, zero mutations | 
|  | 187 | +            assertEquals(2, rec.selections.size) | 
|  | 188 | +            assertEquals(1, rec.xoCalls.size) | 
|  | 189 | +            assertEquals(0, rec.mutated.size) | 
|  | 190 | +        } | 
|  | 191 | +    } | 
|  | 192 | + | 
|  | 193 | + | 
|  | 194 | + | 
|  | 195 | +} | 
|  | 196 | + | 
|  | 197 | +// --- Test helpers --- | 
|  | 198 | + | 
|  | 199 | +// --- Test helpers --- | 
|  | 200 | + | 
|  | 201 | +private fun createGAWithSelection( | 
|  | 202 | +    fixedSel: FixedSelectionStrategy | 
|  | 203 | +): Pair<SteadyStateGeneticAlgorithm<OneMaxIndividual>, Injector> { | 
|  | 204 | +    val injector = LifecycleInjector.builder() | 
|  | 205 | +        .withModules(* arrayOf<Module>(OneMaxModule(), BaseModule())) | 
|  | 206 | +        .build().createInjector() | 
|  | 207 | + | 
|  | 208 | +    val ga = injector.getInstance( | 
|  | 209 | +        Key.get(object : TypeLiteral<SteadyStateGeneticAlgorithm<OneMaxIndividual>>() {}) | 
|  | 210 | +    ) | 
|  | 211 | +    // Override selection strategy directly on the GA instance (no DI here) | 
|  | 212 | +    ga.useSelectionStrategy(fixedSel) | 
|  | 213 | +    return ga to injector | 
|  | 214 | +} | 
|  | 215 | + | 
|  | 216 | + | 
|  | 217 | + | 
|  | 218 | + | 
0 commit comments