Skip to content

Commit 06173f6

Browse files
authored
Merge pull request #1372 from WebFuzzing/feature/mu-lambda-ea
MuLambda EA
2 parents 0d517e9 + 32d6211 commit 06173f6

File tree

5 files changed

+236
-2
lines changed

5 files changed

+236
-2
lines changed

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

Lines changed: 5 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 // GA variants still work-in-progress.
1165+
StandardGA, MonotonicGA, SteadyStateGA, BreederGA, CellularGA, OnePlusLambdaLambdaGA, MuLambdaEA // 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,10 @@ 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 muLambdaOffspringSize = 30
1562+
15591563
@Cfg("Define the maximum number of tests in a suite in the search algorithms that evolve whole suites, e.g. WTS")
15601564
@Min(1.0)
15611565
var maxSearchSuiteSize = 50

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

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

648+
EMConfig.Algorithm.MuLambdaEA ->
649+
Key.get(object : TypeLiteral<org.evomaster.core.search.algorithms.MuLambdaEvolutionaryAlgorithm<GraphQLIndividual>>(){})
648650
EMConfig.Algorithm.BreederGA ->
649651
Key.get(object : TypeLiteral<BreederGeneticAlgorithm<GraphQLIndividual>>() {})
650652

@@ -679,6 +681,8 @@ class Main {
679681

680682
EMConfig.Algorithm.RW ->
681683
Key.get(object : TypeLiteral<RandomWalkAlgorithm<RPCIndividual>>() {})
684+
EMConfig.Algorithm.MuLambdaEA ->
685+
Key.get(object : TypeLiteral<org.evomaster.core.search.algorithms.MuLambdaEvolutionaryAlgorithm<RPCIndividual>>(){})
682686

683687
EMConfig.Algorithm.BreederGA ->
684688
Key.get(object : TypeLiteral<BreederGeneticAlgorithm<RPCIndividual>>() {})
@@ -712,6 +716,8 @@ class Main {
712716

713717
EMConfig.Algorithm.RW ->
714718
Key.get(object : TypeLiteral<RandomWalkAlgorithm<WebIndividual>>() {})
719+
EMConfig.Algorithm.MuLambdaEA ->
720+
Key.get(object : TypeLiteral<org.evomaster.core.search.algorithms.MuLambdaEvolutionaryAlgorithm<WebIndividual>>(){})
715721

716722
EMConfig.Algorithm.BreederGA ->
717723
Key.get(object : TypeLiteral<BreederGeneticAlgorithm<WebIndividual>>() {})
@@ -754,6 +760,8 @@ class Main {
754760

755761
EMConfig.Algorithm.RW ->
756762
Key.get(object : TypeLiteral<RandomWalkAlgorithm<RestIndividual>>() {})
763+
EMConfig.Algorithm.MuLambdaEA ->
764+
Key.get(object : TypeLiteral<org.evomaster.core.search.algorithms.MuLambdaEvolutionaryAlgorithm<RestIndividual>>(){})
757765

758766
EMConfig.Algorithm.BreederGA ->
759767
Key.get(object : TypeLiteral<BreederGeneticAlgorithm<RestIndividual>>() {})
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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.Lazy
6+
import org.evomaster.core.search.algorithms.wts.WtsEvalIndividual
7+
8+
/**
9+
* (μ, λ) Evolutionary Algorithm.
10+
*
11+
* Population P of size μ is evolved by generating exactly λ offspring via mutation
12+
* and selecting the best μ individuals only from the offspring set.
13+
*/
14+
class MuLambdaEvolutionaryAlgorithm<T> : AbstractGeneticAlgorithm<T>() where T : Individual {
15+
16+
override fun getType(): EMConfig.Algorithm = EMConfig.Algorithm.MuLambdaEA
17+
18+
override fun searchOnce() {
19+
beginGeneration()
20+
// Freeze targets for current generation
21+
frozenTargets = archive.notCoveredTargets()
22+
23+
val mu = config.populationSize
24+
val lambda = config.muLambdaOffspringSize
25+
26+
val offspring: MutableList<WtsEvalIndividual<T>> = mutableListOf()
27+
28+
val perParent = lambda / mu
29+
for (p in population) {
30+
for (i in 0 until perParent) {
31+
beginStep()
32+
val o = p.copy()
33+
if (randomness.nextBoolean(config.fixedRateMutation)) {
34+
mutate(o)
35+
}
36+
offspring.add(o)
37+
if (!time.shouldContinueSearch()) {
38+
endStep()
39+
break
40+
}
41+
endStep()
42+
}
43+
if (!time.shouldContinueSearch()) break
44+
}
45+
46+
// Select best μ only from offspring
47+
val next = offspring.sortedByDescending { score(it) }
48+
.take(mu)
49+
.map { it.copy() }
50+
.toMutableList()
51+
52+
population.clear()
53+
population.addAll(next)
54+
endGeneration()
55+
}
56+
}
57+
58+
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
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 MuLambdaEvolutionaryAlgorithmTest {
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 testMuLambdaEAFindsOptimum() {
34+
TestUtils.handleFlaky {
35+
val ea = injector.getInstance(
36+
Key.get(object : TypeLiteral<MuLambdaEvolutionaryAlgorithm<OneMaxIndividual>>() {})
37+
)
38+
39+
val config = injector.getInstance(EMConfig::class.java)
40+
config.populationSize = 5
41+
config.muLambdaOffspringSize = 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_MuLambdaEA() {
58+
TestUtils.handleFlaky {
59+
val ea = injector.getInstance(
60+
Key.get(object : TypeLiteral<MuLambdaEvolutionaryAlgorithm<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.muLambdaOffspringSize = 10 // divisible by mu
72+
config.xoverProbability = 0.0 // no crossover used in (μ,λ)
73+
config.fixedRateMutation = 1.0 // force mutation
74+
75+
ea.setupBeforeSearch()
76+
ea.searchOnce()
77+
78+
val nextPop = ea.getViewOfPopulation()
79+
assertEquals(config.populationSize, nextPop.size)
80+
81+
// crossover unused
82+
assertEquals(0, rec.xoCalls.size)
83+
// offspring mutated: perParent * µ == λ when divisible
84+
val perParent = config.muLambdaOffspringSize / config.populationSize
85+
assertEquals(perParent * config.populationSize, rec.mutated.size)
86+
}
87+
}
88+
89+
// Edge Case: MutationProbability=0 and CrossoverProbability=1
90+
@Test
91+
fun testNoMutationWhenProbabilityZero_MuLambdaEA() {
92+
TestUtils.handleFlaky {
93+
val ea = injector.getInstance(
94+
Key.get(object : TypeLiteral<MuLambdaEvolutionaryAlgorithm<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.muLambdaOffspringSize = 10
106+
config.xoverProbability = 1.0 // irrelevant for (μ,λ)
107+
config.fixedRateMutation = 0.0 // disable mutation
108+
109+
ea.setupBeforeSearch()
110+
ea.searchOnce()
111+
112+
val nextPop = ea.getViewOfPopulation()
113+
assertEquals(config.populationSize, nextPop.size)
114+
assertEquals(0, rec.xoCalls.size)
115+
assertEquals(0, rec.mutated.size)
116+
}
117+
}
118+
119+
// One iteration properties: population size, best-µ selection from offspring, mutation count
120+
@Test
121+
fun testNextGenerationIsTheBestMuFromOffspringOnly() {
122+
TestUtils.handleFlaky {
123+
val ea = injector.getInstance(
124+
Key.get(object : TypeLiteral<MuLambdaEvolutionaryAlgorithm<OneMaxIndividual>>() {})
125+
)
126+
127+
val rec = GARecorder<OneMaxIndividual>()
128+
ea.addObserver(rec)
129+
130+
val config = injector.getInstance(EMConfig::class.java)
131+
config.populationSize = 5
132+
config.muLambdaOffspringSize = 10 // divisible by mu -> perParent = 2
133+
config.xoverProbability = 0.0
134+
config.fixedRateMutation = 1.0
135+
136+
ea.setupBeforeSearch()
137+
ea.searchOnce()
138+
139+
val finalPop = ea.getViewOfPopulation()
140+
val mu = config.populationSize
141+
142+
// 1) population size remains µ
143+
assertEquals(mu, finalPop.size)
144+
145+
// 2) final population equals best-µ from offspring only
146+
val offspring = rec.mutated.toList()
147+
val expectedScores = offspring
148+
.map { ea.score(it) }
149+
.sortedDescending()
150+
.take(mu)
151+
val finalScores = finalPop
152+
.map { ea.score(it) }
153+
.sortedDescending()
154+
assertEquals(expectedScores, finalScores)
155+
156+
// 3) mutation count equals number of created offspring
157+
val perParent = config.muLambdaOffspringSize / config.populationSize
158+
assertEquals(perParent * config.populationSize, rec.mutated.size)
159+
}
160+
}
161+
}
162+
163+

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`. *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`. *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`.|
@@ -163,6 +163,7 @@ There are 3 types of options:
163163
|`minimizeThresholdForLoss`| __Double__. Losing targets when recomputing coverage is expected (e.g., constructors of singletons), but problematic if too much. *Constraints*: `probability 0.0-1.0`. *Default value*: `0.2`.|
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`.|
166+
|`muLambdaOffspringSize`| __Int__. Define the number of offspring (λ) generated per generation in (μ,λ) Evolutionary Algorithm. *Constraints*: `min=1.0`. *Default value*: `30`.|
166167
|`mutatedGeneFile`| __String__. Specify a path to save mutation details which is useful for debugging mutation. *DEBUG option*. *Default value*: `mutatedGeneInfo.csv`.|
167168
|`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`.|
168169
|`namingStrategy`| __Enum__. Specify the naming strategy for test cases. *Valid values*: `NUMBERED, ACTION`. *Default value*: `ACTION`.|

0 commit comments

Comments
 (0)