diff --git a/core/src/main/kotlin/org/evomaster/core/EMConfig.kt b/core/src/main/kotlin/org/evomaster/core/EMConfig.kt index 9a407d0aea..1511cb2dce 100644 --- a/core/src/main/kotlin/org/evomaster/core/EMConfig.kt +++ b/core/src/main/kotlin/org/evomaster/core/EMConfig.kt @@ -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.") @@ -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 diff --git a/core/src/main/kotlin/org/evomaster/core/Main.kt b/core/src/main/kotlin/org/evomaster/core/Main.kt index 037dc54e30..06dc69d3f7 100644 --- a/core/src/main/kotlin/org/evomaster/core/Main.kt +++ b/core/src/main/kotlin/org/evomaster/core/Main.kt @@ -645,6 +645,9 @@ class Main { EMConfig.Algorithm.StandardGA -> Key.get(object : TypeLiteral>() {}) + EMConfig.Algorithm.MuPlusLambdaEA -> + Key.get(object : TypeLiteral>() {}) + EMConfig.Algorithm.MuLambdaEA -> Key.get(object : TypeLiteral>(){}) EMConfig.Algorithm.BreederGA -> @@ -681,6 +684,9 @@ class Main { EMConfig.Algorithm.RW -> Key.get(object : TypeLiteral>() {}) + + EMConfig.Algorithm.MuPlusLambdaEA -> + Key.get(object : TypeLiteral>() {}) EMConfig.Algorithm.MuLambdaEA -> Key.get(object : TypeLiteral>(){}) @@ -716,6 +722,9 @@ class Main { EMConfig.Algorithm.RW -> Key.get(object : TypeLiteral>() {}) + + EMConfig.Algorithm.MuPlusLambdaEA -> + Key.get(object : TypeLiteral>() {}) EMConfig.Algorithm.MuLambdaEA -> Key.get(object : TypeLiteral>(){}) @@ -760,6 +769,8 @@ class Main { EMConfig.Algorithm.RW -> Key.get(object : TypeLiteral>() {}) + EMConfig.Algorithm.MuPlusLambdaEA -> + Key.get(object : TypeLiteral>() {}) EMConfig.Algorithm.MuLambdaEA -> Key.get(object : TypeLiteral>(){}) diff --git a/core/src/main/kotlin/org/evomaster/core/search/algorithms/MuPlusLambdaEvolutionaryAlgorithm.kt b/core/src/main/kotlin/org/evomaster/core/search/algorithms/MuPlusLambdaEvolutionaryAlgorithm.kt new file mode 100644 index 0000000000..86f213a96f --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/search/algorithms/MuPlusLambdaEvolutionaryAlgorithm.kt @@ -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 : AbstractGeneticAlgorithm() 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> = 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() + } +} + + diff --git a/core/src/test/kotlin/org/evomaster/core/search/algorithms/MuPlusLambdaEvolutionaryAlgorithmTest.kt b/core/src/test/kotlin/org/evomaster/core/search/algorithms/MuPlusLambdaEvolutionaryAlgorithmTest.kt new file mode 100644 index 0000000000..54fb5f7a17 --- /dev/null +++ b/core/src/test/kotlin/org/evomaster/core/search/algorithms/MuPlusLambdaEvolutionaryAlgorithmTest.kt @@ -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(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>() {}) + ) + + 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>() {}) + ) + + val rec = GARecorder() + 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>() {}) + ) + + val rec = GARecorder() + 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>() {}) + ) + + val rec = GARecorder() + 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) + } + } +} + + diff --git a/docs/options.md b/docs/options.md index 1012176b4d..4f724d0f8a 100644 --- a/docs/options.md +++ b/docs/options.md @@ -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`.| @@ -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`.|