Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion core/src/main/kotlin/org/evomaster/core/EMConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1162,7 +1162,7 @@ class EMConfig {

enum class Algorithm {
DEFAULT, SMARTS, MIO, RANDOM, WTS, MOSA, RW,
StandardGA, MonotonicGA, SteadyStateGA, BreederGA, CellularGA, OnePlusLambdaLambdaGA, MuLambdaEA // GA variants still work-in-progress.
StandardGA, MonotonicGA, SteadyStateGA, BreederGA, CellularGA, OnePlusLambdaLambdaGA, MuLambdaEA, MuPlusLambdaEA // GA variants still work-in-progress.
}

@Cfg("The algorithm used to generate test cases. The default depends on whether black-box or white-box testing is done.")
Expand Down Expand Up @@ -1556,6 +1556,9 @@ class EMConfig {
@Probability
var fixedRateMutation = 0.04

@Cfg("Define the number of offspring (λ) generated per generation in (μ+λ) Evolutionary Algorithm")
@Min(1.0)
var muPlusLambdaOffspringSize = 30
@Cfg("Define the number of offspring (λ) generated per generation in (μ,λ) Evolutionary Algorithm")
@Min(1.0)
var muLambdaOffspringSize = 30
Expand Down
11 changes: 11 additions & 0 deletions core/src/main/kotlin/org/evomaster/core/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,9 @@ class Main {
EMConfig.Algorithm.StandardGA ->
Key.get(object : TypeLiteral<StandardGeneticAlgorithm<GraphQLIndividual>>() {})

EMConfig.Algorithm.MuPlusLambdaEA ->
Key.get(object : TypeLiteral<MuPlusLambdaEvolutionaryAlgorithm<GraphQLIndividual>>() {})

EMConfig.Algorithm.MuLambdaEA ->
Key.get(object : TypeLiteral<org.evomaster.core.search.algorithms.MuLambdaEvolutionaryAlgorithm<GraphQLIndividual>>(){})
EMConfig.Algorithm.BreederGA ->
Expand Down Expand Up @@ -681,6 +684,9 @@ class Main {

EMConfig.Algorithm.RW ->
Key.get(object : TypeLiteral<RandomWalkAlgorithm<RPCIndividual>>() {})

EMConfig.Algorithm.MuPlusLambdaEA ->
Key.get(object : TypeLiteral<MuPlusLambdaEvolutionaryAlgorithm<RPCIndividual>>() {})
EMConfig.Algorithm.MuLambdaEA ->
Key.get(object : TypeLiteral<org.evomaster.core.search.algorithms.MuLambdaEvolutionaryAlgorithm<RPCIndividual>>(){})

Expand Down Expand Up @@ -716,6 +722,9 @@ class Main {

EMConfig.Algorithm.RW ->
Key.get(object : TypeLiteral<RandomWalkAlgorithm<WebIndividual>>() {})

EMConfig.Algorithm.MuPlusLambdaEA ->
Key.get(object : TypeLiteral<MuPlusLambdaEvolutionaryAlgorithm<WebIndividual>>() {})
EMConfig.Algorithm.MuLambdaEA ->
Key.get(object : TypeLiteral<org.evomaster.core.search.algorithms.MuLambdaEvolutionaryAlgorithm<WebIndividual>>(){})

Expand Down Expand Up @@ -760,6 +769,8 @@ class Main {

EMConfig.Algorithm.RW ->
Key.get(object : TypeLiteral<RandomWalkAlgorithm<RestIndividual>>() {})
EMConfig.Algorithm.MuPlusLambdaEA ->
Key.get(object : TypeLiteral<MuPlusLambdaEvolutionaryAlgorithm<RestIndividual>>() {})
EMConfig.Algorithm.MuLambdaEA ->
Key.get(object : TypeLiteral<org.evomaster.core.search.algorithms.MuLambdaEvolutionaryAlgorithm<RestIndividual>>(){})

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package org.evomaster.core.search.algorithms

import org.evomaster.core.EMConfig
import org.evomaster.core.search.Individual
import org.evomaster.core.search.algorithms.wts.WtsEvalIndividual

/**
* (μ + λ) Evolutionary Algorithm.
* Population P of size μ is evolved by generating λ offspring via mutation of each parent,
* then selecting the best μ individuals from parents ∪ offspring.
*/
class MuPlusLambdaEvolutionaryAlgorithm<T> : AbstractGeneticAlgorithm<T>() where T : Individual {

override fun getType(): EMConfig.Algorithm = EMConfig.Algorithm.MuPlusLambdaEA

override fun searchOnce() {
beginGeneration()
// Freeze targets for current generation
frozenTargets = archive.notCoveredTargets()

val mu = config.populationSize
val lambda = config.muPlusLambdaOffspringSize

val offspring: MutableList<WtsEvalIndividual<T>> = mutableListOf()

// For each parent, generate λ/μ offspring by mutation (rounded up)
val perParent = lambda / mu
for (p in population) {
for (i in 0 until perParent) {
beginStep()
val o = p.copy()
if (randomness.nextBoolean(config.fixedRateMutation)) {
mutate(o)
}
offspring.add(o)
endStep()
}
}

// Select best μ from parents ∪ offspring
val merged = (population + offspring).sortedByDescending { score(it) }
val next = merged.take(mu).map { it.copy() }.toMutableList()

population.clear()
population.addAll(next)
endGeneration()
}
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package org.evomaster.core.search.algorithms

import com.google.inject.Injector
import com.google.inject.Key
import com.google.inject.Module
import com.google.inject.TypeLiteral
import com.netflix.governator.guice.LifecycleInjector
import org.evomaster.core.BaseModule
import org.evomaster.core.EMConfig
import org.evomaster.core.TestUtils
import org.evomaster.core.search.algorithms.observer.GARecorder
import org.evomaster.core.search.algorithms.onemax.OneMaxIndividual
import org.evomaster.core.search.algorithms.onemax.OneMaxModule
import org.evomaster.core.search.algorithms.onemax.OneMaxSampler
import org.evomaster.core.search.service.ExecutionPhaseController
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

class MuPlusLambdaEvolutionaryAlgorithmTest {

private lateinit var injector: Injector

@BeforeEach
fun setUp() {
injector = LifecycleInjector.builder()
.withModules(* arrayOf<Module>(OneMaxModule(), BaseModule()))
.build().createInjector()
}

// Verifies that the (μ+λ) EA can find the optimal solution for the OneMax problem
@Test
fun testMuPlusLambdaEAFindsOptimum() {
TestUtils.handleFlaky {
val ea = injector.getInstance(
Key.get(object : TypeLiteral<MuPlusLambdaEvolutionaryAlgorithm<OneMaxIndividual>>() {})
)

val config = injector.getInstance(EMConfig::class.java)
config.populationSize = 5
config.muPlusLambdaOffspringSize = 10
config.maxEvaluations = 10000
config.stoppingCriterion = EMConfig.StoppingCriterion.ACTION_EVALUATIONS

val epc = injector.getInstance(ExecutionPhaseController::class.java)
epc.startSearch()
val solution = ea.search()
epc.finishSearch()

assertEquals(1, solution.individuals.size)
assertEquals(OneMaxSampler.DEFAULT_N.toDouble(), solution.overall.computeFitnessScore(), 0.001)
}
}

// Edge Case: CrossoverProbability=0 and MutationProbability=1
@Test
fun testNoCrossoverWhenProbabilityZero_MuPlusEA() {
TestUtils.handleFlaky {
val ea = injector.getInstance(
Key.get(object : TypeLiteral<MuPlusLambdaEvolutionaryAlgorithm<OneMaxIndividual>>() {})
)

val rec = GARecorder<OneMaxIndividual>()
ea.addObserver(rec)

val config = injector.getInstance(EMConfig::class.java)
config.gaSolutionSource = EMConfig.GASolutionSource.POPULATION
config.maxEvaluations = 100_000
config.stoppingCriterion = EMConfig.StoppingCriterion.ACTION_EVALUATIONS
config.populationSize = 5
config.muPlusLambdaOffspringSize = 10 // divisible by mu
config.xoverProbability = 0.0 // disable crossover
config.fixedRateMutation = 1.0 // force mutation

ea.setupBeforeSearch()
ea.searchOnce()

val nextPop = ea.getViewOfPopulation()
// population remains of size mu in (μ+λ) EA
assertEquals(config.populationSize, nextPop.size)

// crossover disabled (and not used by this EA anyway)
assertEquals(0, rec.xoCalls.size)
// λ offspring mutated
assertEquals(config.muPlusLambdaOffspringSize, rec.mutated.size)
}
}

// Edge Case: MutationProbability=0 and CrossoverProbability=1
@Test
fun testNoMutationWhenProbabilityZero_MuPlusEA() {
TestUtils.handleFlaky {
val ea = injector.getInstance(
Key.get(object : TypeLiteral<MuPlusLambdaEvolutionaryAlgorithm<OneMaxIndividual>>() {})
)

val rec = GARecorder<OneMaxIndividual>()
ea.addObserver(rec)

val config = injector.getInstance(EMConfig::class.java)
config.gaSolutionSource = EMConfig.GASolutionSource.POPULATION
config.maxEvaluations = 100_000
config.stoppingCriterion = EMConfig.StoppingCriterion.ACTION_EVALUATIONS
config.populationSize = 5
config.muPlusLambdaOffspringSize = 10 // divisible by mu
config.xoverProbability = 1.0 // force crossover (not used in this EA)
config.fixedRateMutation = 0.0 // disable mutation

ea.setupBeforeSearch()
ea.searchOnce()

val nextPop = ea.getViewOfPopulation()
// population remains of size mu in (μ+λ) EA
assertEquals(config.populationSize, nextPop.size)

// crossovers are not used in (μ+λ) EA
assertEquals(0, rec.xoCalls.size)

// mutations disabled
assertEquals(0, rec.mutated.size)
}
}

// One iteration properties: population size, best-µ selection, mutation count
@Test
fun testNextGenerationIsTheBestMuOfParentsUnionOffspring() {
TestUtils.handleFlaky {
val ea = injector.getInstance(
Key.get(object : TypeLiteral<MuPlusLambdaEvolutionaryAlgorithm<OneMaxIndividual>>() {})
)

val rec = GARecorder<OneMaxIndividual>()
ea.addObserver(rec)

val config = injector.getInstance(EMConfig::class.java)
config.populationSize = 5
config.muPlusLambdaOffspringSize = 10 // divisible by mu -> perParent = 2
config.xoverProbability = 0.0 // not used in (µ+λ)
config.fixedRateMutation = 1.0 // force mutation on all offspring

// initialize population and snapshot parents
ea.setupBeforeSearch()
val parents = ea.getViewOfPopulation().toList()

// run a single generation
ea.searchOnce()

val finalPop = ea.getViewOfPopulation()
val mu = config.populationSize

// 1) population size remains µ
assertEquals(mu, finalPop.size)

// 2) final population equals best-µ of parents ∪ offspring (compare scores)
val offspring = rec.mutated.toList()
val expectedScores = (parents + offspring)
.map { ea.score(it) }
.sortedDescending()
.take(mu)
val finalScores = finalPop
.map { ea.score(it) }
.sortedDescending()
assertEquals(expectedScores, finalScores)

// 3) with fixedRateMutation=1, mutations equal number of created offspring
val perParent = config.muPlusLambdaOffspringSize / config.populationSize
val expectedMutations = perParent * config.populationSize
assertEquals(expectedMutations, rec.mutated.size)
}
}
}


3 changes: 2 additions & 1 deletion docs/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ There are 3 types of options:
|`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`.|
|`addTestComments`| __Boolean__. Add summary comments on each test. *Default value*: `true`.|
|`advancedBlackBoxCoverage`| __Boolean__. Apply more advanced coverage criteria for black-box testing. This can result in larger generated test suites. *Default value*: `true`.|
|`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`.|
|`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`.|
|`allowInvalidData`| __Boolean__. When generating data, allow in some cases to use invalid values on purpose. *Default value*: `true`.|
|`appendToStatisticsFile`| __Boolean__. Whether should add to an existing statistics file, instead of replacing it. *Default value*: `false`.|
|`archiveAfterMutationFile`| __String__. Specify a path to save archive after each mutation during search, only useful for debugging. *DEBUG option*. *Default value*: `archive.csv`.|
Expand Down Expand Up @@ -164,6 +164,7 @@ There are 3 types of options:
|`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`.|
|`minimumSizeControl`| __Int__. Specify minimum size when bloatControlForSecondaryObjective. *Constraints*: `min=0.0`. *Default value*: `2`.|
|`muLambdaOffspringSize`| __Int__. Define the number of offspring (λ) generated per generation in (μ,λ) Evolutionary Algorithm. *Constraints*: `min=1.0`. *Default value*: `30`.|
|`muPlusLambdaOffspringSize`| __Int__. Define the number of offspring (λ) generated per generation in (μ+λ) Evolutionary Algorithm. *Constraints*: `min=1.0`. *Default value*: `30`.|
|`mutatedGeneFile`| __String__. Specify a path to save mutation details which is useful for debugging mutation. *DEBUG option*. *Default value*: `mutatedGeneInfo.csv`.|
|`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`.|
|`namingStrategy`| __Enum__. Specify the naming strategy for test cases. *Valid values*: `NUMBERED, ACTION`. *Default value*: `ACTION`.|
Expand Down