Skip to content

Commit e063898

Browse files
authored
Merge pull request #1358 from WebFuzzing/feature/steady-state-ga
Steady State GA: Lifecycle Hooks & Tests
2 parents 418c33d + 6a10952 commit e063898

File tree

2 files changed

+232
-6
lines changed

2 files changed

+232
-6
lines changed

core/src/main/kotlin/org/evomaster/core/search/algorithms/SteadyStateGeneticAlgorithm.kt

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,13 @@ import org.evomaster.core.search.Individual
55
import org.evomaster.core.search.algorithms.wts.WtsEvalIndividual
66
import org.evomaster.core.search.service.SearchAlgorithm
77
import kotlin.math.max
8-
//TODO: Note that this class is not fully tested.
9-
// It needs to be thoroughly verified whether this truly adheres to the intended algorithm.
108
/**
11-
* An implementation of the Steady-State Genetic Algorithm (SSGA).
9+
* An implementation of the Steady-State Genetic Algorithm (SSGA).
1210
*
1311
* Unlike Standard GA, which replaces the entire population in each generation,
1412
* Steady-State GA updates the population incrementally by replacing a small number
15-
* of individuals at a time (typically just 1 or 2).
13+
* of individuals at a time:
14+
* Only replaces selected parents with offspring if the offspring are better
1615
*
1716
* This class inherits from StandardGeneticAlgorithm to reuse shared components,
1817
* but overrides search behavior to follow steady-state principles.
@@ -33,6 +32,12 @@ class SteadyStateGeneticAlgorithm<T> : StandardGeneticAlgorithm<T>() where T : I
3332
* - Replace the parents with the offspring only if the offspring are fitter.
3433
*/
3534
override fun searchOnce() {
35+
// Lifecycle: start generation
36+
beginGeneration()
37+
// Freeze objectives for this generation
38+
frozenTargets = archive.notCoveredTargets()
39+
// Start single steady-state step
40+
beginStep()
3641
// Select two parents from the population
3742
val p1 = tournamentSelection()
3843
val p2 = tournamentSelection()
@@ -55,14 +60,17 @@ class SteadyStateGeneticAlgorithm<T> : StandardGeneticAlgorithm<T>() where T : I
5560
}
5661

5762
// Only replace parents with offspring if the offspring are better
58-
if (max(o1.calculateCombinedFitness(), o2.calculateCombinedFitness()) >
59-
max(p1.calculateCombinedFitness(), p2.calculateCombinedFitness())) {
63+
if (max(score(o1), score(o2)) >
64+
max(score(p1), score(p2))) {
6065

6166
// Replace both parents in the population
6267
population.remove(p1)
6368
population.remove(p2)
6469
population.add(o1)
6570
population.add(o2)
6671
}
72+
// End step and generation
73+
endStep()
74+
endGeneration()
6775
}
6876
}
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
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

Comments
 (0)