Skip to content

Commit bdad980

Browse files
authored
Merge pull request #1368 from WebFuzzing/feature/mu-plus-lambda
Mu Plus Lambda EA
2 parents b8de7e7 + 654f6f7 commit bdad980

File tree

5 files changed

+240
-2
lines changed

5 files changed

+240
-2
lines changed

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

Lines changed: 4 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 // GA variants still work-in-progress.
1165+
StandardGA, MonotonicGA, SteadyStateGA, BreederGA, CellularGA, OnePlusLambdaLambdaGA, MuLambdaEA, MuPlusLambdaEA // 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.")
@@ -1556,6 +1556,9 @@ class EMConfig {
15561556
@Probability
15571557
var fixedRateMutation = 0.04
15581558

1559+
@Cfg("Define the number of offspring (λ) generated per generation in (μ+λ) Evolutionary Algorithm")
1560+
@Min(1.0)
1561+
var muPlusLambdaOffspringSize = 30
15591562
@Cfg("Define the number of offspring (λ) generated per generation in (μ,λ) Evolutionary Algorithm")
15601563
@Min(1.0)
15611564
var muLambdaOffspringSize = 30

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,9 @@ class Main {
645645
EMConfig.Algorithm.StandardGA ->
646646
Key.get(object : TypeLiteral<StandardGeneticAlgorithm<GraphQLIndividual>>() {})
647647

648+
EMConfig.Algorithm.MuPlusLambdaEA ->
649+
Key.get(object : TypeLiteral<MuPlusLambdaEvolutionaryAlgorithm<GraphQLIndividual>>() {})
650+
648651
EMConfig.Algorithm.MuLambdaEA ->
649652
Key.get(object : TypeLiteral<org.evomaster.core.search.algorithms.MuLambdaEvolutionaryAlgorithm<GraphQLIndividual>>(){})
650653
EMConfig.Algorithm.BreederGA ->
@@ -681,6 +684,9 @@ class Main {
681684

682685
EMConfig.Algorithm.RW ->
683686
Key.get(object : TypeLiteral<RandomWalkAlgorithm<RPCIndividual>>() {})
687+
688+
EMConfig.Algorithm.MuPlusLambdaEA ->
689+
Key.get(object : TypeLiteral<MuPlusLambdaEvolutionaryAlgorithm<RPCIndividual>>() {})
684690
EMConfig.Algorithm.MuLambdaEA ->
685691
Key.get(object : TypeLiteral<org.evomaster.core.search.algorithms.MuLambdaEvolutionaryAlgorithm<RPCIndividual>>(){})
686692

@@ -716,6 +722,9 @@ class Main {
716722

717723
EMConfig.Algorithm.RW ->
718724
Key.get(object : TypeLiteral<RandomWalkAlgorithm<WebIndividual>>() {})
725+
726+
EMConfig.Algorithm.MuPlusLambdaEA ->
727+
Key.get(object : TypeLiteral<MuPlusLambdaEvolutionaryAlgorithm<WebIndividual>>() {})
719728
EMConfig.Algorithm.MuLambdaEA ->
720729
Key.get(object : TypeLiteral<org.evomaster.core.search.algorithms.MuLambdaEvolutionaryAlgorithm<WebIndividual>>(){})
721730

@@ -760,6 +769,8 @@ class Main {
760769

761770
EMConfig.Algorithm.RW ->
762771
Key.get(object : TypeLiteral<RandomWalkAlgorithm<RestIndividual>>() {})
772+
EMConfig.Algorithm.MuPlusLambdaEA ->
773+
Key.get(object : TypeLiteral<MuPlusLambdaEvolutionaryAlgorithm<RestIndividual>>() {})
763774
EMConfig.Algorithm.MuLambdaEA ->
764775
Key.get(object : TypeLiteral<org.evomaster.core.search.algorithms.MuLambdaEvolutionaryAlgorithm<RestIndividual>>(){})
765776

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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+
7+
/**
8+
* (μ + λ) Evolutionary Algorithm.
9+
* Population P of size μ is evolved by generating λ offspring via mutation of each parent,
10+
* then selecting the best μ individuals from parents ∪ offspring.
11+
*/
12+
class MuPlusLambdaEvolutionaryAlgorithm<T> : AbstractGeneticAlgorithm<T>() where T : Individual {
13+
14+
override fun getType(): EMConfig.Algorithm = EMConfig.Algorithm.MuPlusLambdaEA
15+
16+
override fun searchOnce() {
17+
beginGeneration()
18+
// Freeze targets for current generation
19+
frozenTargets = archive.notCoveredTargets()
20+
21+
val mu = config.populationSize
22+
val lambda = config.muPlusLambdaOffspringSize
23+
24+
val offspring: MutableList<WtsEvalIndividual<T>> = mutableListOf()
25+
26+
// For each parent, generate λ/μ offspring by mutation (rounded up)
27+
val perParent = lambda / mu
28+
for (p in population) {
29+
for (i in 0 until perParent) {
30+
beginStep()
31+
val o = p.copy()
32+
if (randomness.nextBoolean(config.fixedRateMutation)) {
33+
mutate(o)
34+
}
35+
offspring.add(o)
36+
endStep()
37+
}
38+
}
39+
40+
// Select best μ from parents ∪ offspring
41+
val merged = (population + offspring).sortedByDescending { score(it) }
42+
val next = merged.take(mu).map { it.copy() }.toMutableList()
43+
44+
population.clear()
45+
population.addAll(next)
46+
endGeneration()
47+
}
48+
}
49+
50+
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
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.observer.GARecorder
12+
import org.evomaster.core.search.algorithms.onemax.OneMaxIndividual
13+
import org.evomaster.core.search.algorithms.onemax.OneMaxModule
14+
import org.evomaster.core.search.algorithms.onemax.OneMaxSampler
15+
import org.evomaster.core.search.service.ExecutionPhaseController
16+
import org.junit.jupiter.api.Assertions.*
17+
import org.junit.jupiter.api.BeforeEach
18+
import org.junit.jupiter.api.Test
19+
20+
class MuPlusLambdaEvolutionaryAlgorithmTest {
21+
22+
private lateinit var injector: Injector
23+
24+
@BeforeEach
25+
fun setUp() {
26+
injector = LifecycleInjector.builder()
27+
.withModules(* arrayOf<Module>(OneMaxModule(), BaseModule()))
28+
.build().createInjector()
29+
}
30+
31+
// Verifies that the (μ+λ) EA can find the optimal solution for the OneMax problem
32+
@Test
33+
fun testMuPlusLambdaEAFindsOptimum() {
34+
TestUtils.handleFlaky {
35+
val ea = injector.getInstance(
36+
Key.get(object : TypeLiteral<MuPlusLambdaEvolutionaryAlgorithm<OneMaxIndividual>>() {})
37+
)
38+
39+
val config = injector.getInstance(EMConfig::class.java)
40+
config.populationSize = 5
41+
config.muPlusLambdaOffspringSize = 10
42+
config.maxEvaluations = 10000
43+
config.stoppingCriterion = EMConfig.StoppingCriterion.ACTION_EVALUATIONS
44+
45+
val epc = injector.getInstance(ExecutionPhaseController::class.java)
46+
epc.startSearch()
47+
val solution = ea.search()
48+
epc.finishSearch()
49+
50+
assertEquals(1, solution.individuals.size)
51+
assertEquals(OneMaxSampler.DEFAULT_N.toDouble(), solution.overall.computeFitnessScore(), 0.001)
52+
}
53+
}
54+
55+
// Edge Case: CrossoverProbability=0 and MutationProbability=1
56+
@Test
57+
fun testNoCrossoverWhenProbabilityZero_MuPlusEA() {
58+
TestUtils.handleFlaky {
59+
val ea = injector.getInstance(
60+
Key.get(object : TypeLiteral<MuPlusLambdaEvolutionaryAlgorithm<OneMaxIndividual>>() {})
61+
)
62+
63+
val rec = GARecorder<OneMaxIndividual>()
64+
ea.addObserver(rec)
65+
66+
val config = injector.getInstance(EMConfig::class.java)
67+
config.gaSolutionSource = EMConfig.GASolutionSource.POPULATION
68+
config.maxEvaluations = 100_000
69+
config.stoppingCriterion = EMConfig.StoppingCriterion.ACTION_EVALUATIONS
70+
config.populationSize = 5
71+
config.muPlusLambdaOffspringSize = 10 // divisible by mu
72+
config.xoverProbability = 0.0 // disable crossover
73+
config.fixedRateMutation = 1.0 // force mutation
74+
75+
ea.setupBeforeSearch()
76+
ea.searchOnce()
77+
78+
val nextPop = ea.getViewOfPopulation()
79+
// population remains of size mu in (μ+λ) EA
80+
assertEquals(config.populationSize, nextPop.size)
81+
82+
// crossover disabled (and not used by this EA anyway)
83+
assertEquals(0, rec.xoCalls.size)
84+
// λ offspring mutated
85+
assertEquals(config.muPlusLambdaOffspringSize, rec.mutated.size)
86+
}
87+
}
88+
89+
// Edge Case: MutationProbability=0 and CrossoverProbability=1
90+
@Test
91+
fun testNoMutationWhenProbabilityZero_MuPlusEA() {
92+
TestUtils.handleFlaky {
93+
val ea = injector.getInstance(
94+
Key.get(object : TypeLiteral<MuPlusLambdaEvolutionaryAlgorithm<OneMaxIndividual>>() {})
95+
)
96+
97+
val rec = GARecorder<OneMaxIndividual>()
98+
ea.addObserver(rec)
99+
100+
val config = injector.getInstance(EMConfig::class.java)
101+
config.gaSolutionSource = EMConfig.GASolutionSource.POPULATION
102+
config.maxEvaluations = 100_000
103+
config.stoppingCriterion = EMConfig.StoppingCriterion.ACTION_EVALUATIONS
104+
config.populationSize = 5
105+
config.muPlusLambdaOffspringSize = 10 // divisible by mu
106+
config.xoverProbability = 1.0 // force crossover (not used in this EA)
107+
config.fixedRateMutation = 0.0 // disable mutation
108+
109+
ea.setupBeforeSearch()
110+
ea.searchOnce()
111+
112+
val nextPop = ea.getViewOfPopulation()
113+
// population remains of size mu in (μ+λ) EA
114+
assertEquals(config.populationSize, nextPop.size)
115+
116+
// crossovers are not used in (μ+λ) EA
117+
assertEquals(0, rec.xoCalls.size)
118+
119+
// mutations disabled
120+
assertEquals(0, rec.mutated.size)
121+
}
122+
}
123+
124+
// One iteration properties: population size, best-µ selection, mutation count
125+
@Test
126+
fun testNextGenerationIsTheBestMuOfParentsUnionOffspring() {
127+
TestUtils.handleFlaky {
128+
val ea = injector.getInstance(
129+
Key.get(object : TypeLiteral<MuPlusLambdaEvolutionaryAlgorithm<OneMaxIndividual>>() {})
130+
)
131+
132+
val rec = GARecorder<OneMaxIndividual>()
133+
ea.addObserver(rec)
134+
135+
val config = injector.getInstance(EMConfig::class.java)
136+
config.populationSize = 5
137+
config.muPlusLambdaOffspringSize = 10 // divisible by mu -> perParent = 2
138+
config.xoverProbability = 0.0 // not used in (µ+λ)
139+
config.fixedRateMutation = 1.0 // force mutation on all offspring
140+
141+
// initialize population and snapshot parents
142+
ea.setupBeforeSearch()
143+
val parents = ea.getViewOfPopulation().toList()
144+
145+
// run a single generation
146+
ea.searchOnce()
147+
148+
val finalPop = ea.getViewOfPopulation()
149+
val mu = config.populationSize
150+
151+
// 1) population size remains µ
152+
assertEquals(mu, finalPop.size)
153+
154+
// 2) final population equals best-µ of parents ∪ offspring (compare scores)
155+
val offspring = rec.mutated.toList()
156+
val expectedScores = (parents + offspring)
157+
.map { ea.score(it) }
158+
.sortedDescending()
159+
.take(mu)
160+
val finalScores = finalPop
161+
.map { ea.score(it) }
162+
.sortedDescending()
163+
assertEquals(expectedScores, finalScores)
164+
165+
// 3) with fixedRateMutation=1, mutations equal number of created offspring
166+
val perParent = config.muPlusLambdaOffspringSize / config.populationSize
167+
val expectedMutations = perParent * config.populationSize
168+
assertEquals(expectedMutations, rec.mutated.size)
169+
}
170+
}
171+
}
172+
173+

docs/options.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ There are 3 types of options:
6868
|`addPreDefinedTests`| __Boolean__. Add predefined tests at the end of the search. An example is a test to fetch the schema of RESTful APIs. *Default value*: `true`.|
6969
|`addTestComments`| __Boolean__. Add summary comments on each test. *Default value*: `true`.|
7070
|`advancedBlackBoxCoverage`| __Boolean__. Apply more advanced coverage criteria for black-box testing. This can result in larger generated test suites. *Default value*: `true`.|
71-
|`algorithm`| __Enum__. The algorithm used to generate test cases. The default depends on whether black-box or white-box testing is done. *Valid values*: `DEFAULT, SMARTS, MIO, RANDOM, WTS, MOSA, RW, StandardGA, MonotonicGA, SteadyStateGA, BreederGA, CellularGA, OnePlusLambdaLambdaGA, MuLambdaEA`. *Default value*: `DEFAULT`.|
71+
|`algorithm`| __Enum__. The algorithm used to generate test cases. The default depends on whether black-box or white-box testing is done. *Valid values*: `DEFAULT, SMARTS, MIO, RANDOM, WTS, MOSA, RW, StandardGA, MonotonicGA, SteadyStateGA, BreederGA, CellularGA, OnePlusLambdaLambdaGA, MuLambdaEA, MuPlusLambdaEA`. *Default value*: `DEFAULT`.|
7272
|`allowInvalidData`| __Boolean__. When generating data, allow in some cases to use invalid values on purpose. *Default value*: `true`.|
7373
|`appendToStatisticsFile`| __Boolean__. Whether should add to an existing statistics file, instead of replacing it. *Default value*: `false`.|
7474
|`archiveAfterMutationFile`| __String__. Specify a path to save archive after each mutation during search, only useful for debugging. *DEBUG option*. *Default value*: `archive.csv`.|
@@ -164,6 +164,7 @@ There are 3 types of options:
164164
|`minimizeTimeout`| __Int__. Maximum number of minutes that will be dedicated to the minimization phase. A negative number mean no timeout is considered. A value of 0 means minimization will be skipped, even if minimize=true. *Default value*: `5`.|
165165
|`minimumSizeControl`| __Int__. Specify minimum size when bloatControlForSecondaryObjective. *Constraints*: `min=0.0`. *Default value*: `2`.|
166166
|`muLambdaOffspringSize`| __Int__. Define the number of offspring (λ) generated per generation in (μ,λ) Evolutionary Algorithm. *Constraints*: `min=1.0`. *Default value*: `30`.|
167+
|`muPlusLambdaOffspringSize`| __Int__. Define the number of offspring (λ) generated per generation in (μ+λ) Evolutionary Algorithm. *Constraints*: `min=1.0`. *Default value*: `30`.|
167168
|`mutatedGeneFile`| __String__. Specify a path to save mutation details which is useful for debugging mutation. *DEBUG option*. *Default value*: `mutatedGeneInfo.csv`.|
168169
|`nameWithQueryParameters`| __Boolean__. Specify if true boolean query parameters are included in the test case name. Used for test case naming disambiguation. Only valid for Action based naming strategy. *Default value*: `true`.|
169170
|`namingStrategy`| __Enum__. Specify the naming strategy for test cases. *Valid values*: `NUMBERED, ACTION`. *Default value*: `ACTION`.|

0 commit comments

Comments
 (0)