From 162e60aa1ce2f693b54910589d0022b095b2979f Mon Sep 17 00:00:00 2001 From: francastagna Date: Thu, 30 Oct 2025 12:58:46 -0300 Subject: [PATCH] added algorithm and tests --- .../kotlin/org/evomaster/core/EMConfig.kt | 6 +- .../main/kotlin/org/evomaster/core/Main.kt | 9 + .../MuLambdaEvolutionaryAlgorithm.kt | 58 +++++++ .../MuLambdaEvolutionaryAlgorithmTest.kt | 163 ++++++++++++++++++ docs/options.md | 3 +- 5 files changed, 237 insertions(+), 2 deletions(-) create mode 100644 core/src/main/kotlin/org/evomaster/core/search/algorithms/MuLambdaEvolutionaryAlgorithm.kt create mode 100644 core/src/test/kotlin/org/evomaster/core/search/algorithms/MuLambdaEvolutionaryAlgorithmTest.kt diff --git a/core/src/main/kotlin/org/evomaster/core/EMConfig.kt b/core/src/main/kotlin/org/evomaster/core/EMConfig.kt index 8a2d0e744d..0078fb64b6 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 // These 3 are still work-in-progress + StandardGA, MonotonicGA, SteadyStateGA, MuLambdaEA } @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 { @Probability var fixedRateMutation = 0.04 + @Cfg("Define the number of offspring (λ) generated per generation in (μ,λ) Evolutionary Algorithm") + @Min(1.0) + var muLambdaOffspringSize = 30 + @Cfg("Define the maximum number of tests in a suite in the search algorithms that evolve whole suites, e.g. WTS") @Min(1.0) var maxSearchSuiteSize = 50 diff --git a/core/src/main/kotlin/org/evomaster/core/Main.kt b/core/src/main/kotlin/org/evomaster/core/Main.kt index f8705bd650..26a37828e5 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.MuLambdaEA -> + Key.get(object : TypeLiteral>(){}) + else -> throw IllegalStateException("Unrecognized algorithm ${config.algorithm}") } @@ -670,6 +673,8 @@ class Main { EMConfig.Algorithm.RW -> Key.get(object : TypeLiteral>() {}) + EMConfig.Algorithm.MuLambdaEA -> + Key.get(object : TypeLiteral>(){}) else -> throw IllegalStateException("Unrecognized algorithm ${config.algorithm}") } } @@ -694,6 +699,8 @@ class Main { EMConfig.Algorithm.RW -> Key.get(object : TypeLiteral>() {}) + EMConfig.Algorithm.MuLambdaEA -> + Key.get(object : TypeLiteral>(){}) else -> throw IllegalStateException("Unrecognized algorithm ${config.algorithm}") } } @@ -727,6 +734,8 @@ class Main { EMConfig.Algorithm.RW -> Key.get(object : TypeLiteral>() {}) + EMConfig.Algorithm.MuLambdaEA -> + Key.get(object : TypeLiteral>(){}) else -> throw IllegalStateException("Unrecognized algorithm ${config.algorithm}") } diff --git a/core/src/main/kotlin/org/evomaster/core/search/algorithms/MuLambdaEvolutionaryAlgorithm.kt b/core/src/main/kotlin/org/evomaster/core/search/algorithms/MuLambdaEvolutionaryAlgorithm.kt new file mode 100644 index 0000000000..f3221cd9a7 --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/search/algorithms/MuLambdaEvolutionaryAlgorithm.kt @@ -0,0 +1,58 @@ +package org.evomaster.core.search.algorithms + +import org.evomaster.core.EMConfig +import org.evomaster.core.search.Individual +import org.evomaster.core.Lazy +import org.evomaster.core.search.algorithms.wts.WtsEvalIndividual + +/** + * (μ, λ) Evolutionary Algorithm. + * + * Population P of size μ is evolved by generating exactly λ offspring via mutation + * and selecting the best μ individuals only from the offspring set. + */ +class MuLambdaEvolutionaryAlgorithm : AbstractGeneticAlgorithm() where T : Individual { + + override fun getType(): EMConfig.Algorithm = EMConfig.Algorithm.MuLambdaEA + + override fun searchOnce() { + beginGeneration() + // Freeze targets for current generation + frozenTargets = archive.notCoveredTargets() + + val mu = config.populationSize + val lambda = config.muLambdaOffspringSize + + val offspring: MutableList> = mutableListOf() + + 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) + if (!time.shouldContinueSearch()) { + endStep() + break + } + endStep() + } + if (!time.shouldContinueSearch()) break + } + + // Select best μ only from offspring + val next = offspring.sortedByDescending { score(it) } + .take(mu) + .map { it.copy() } + .toMutableList() + + population.clear() + population.addAll(next) + endGeneration() + } +} + + diff --git a/core/src/test/kotlin/org/evomaster/core/search/algorithms/MuLambdaEvolutionaryAlgorithmTest.kt b/core/src/test/kotlin/org/evomaster/core/search/algorithms/MuLambdaEvolutionaryAlgorithmTest.kt new file mode 100644 index 0000000000..bcd3053716 --- /dev/null +++ b/core/src/test/kotlin/org/evomaster/core/search/algorithms/MuLambdaEvolutionaryAlgorithmTest.kt @@ -0,0 +1,163 @@ +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 MuLambdaEvolutionaryAlgorithmTest { + + 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 testMuLambdaEAFindsOptimum() { + TestUtils.handleFlaky { + val ea = injector.getInstance( + Key.get(object : TypeLiteral>() {}) + ) + + val config = injector.getInstance(EMConfig::class.java) + config.populationSize = 5 + config.muLambdaOffspringSize = 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_MuLambdaEA() { + 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.muLambdaOffspringSize = 10 // divisible by mu + config.xoverProbability = 0.0 // no crossover used in (μ,λ) + config.fixedRateMutation = 1.0 // force mutation + + ea.setupBeforeSearch() + ea.searchOnce() + + val nextPop = ea.getViewOfPopulation() + assertEquals(config.populationSize, nextPop.size) + + // crossover unused + assertEquals(0, rec.xoCalls.size) + // offspring mutated: perParent * µ == λ when divisible + val perParent = config.muLambdaOffspringSize / config.populationSize + assertEquals(perParent * config.populationSize, rec.mutated.size) + } + } + + // Edge Case: MutationProbability=0 and CrossoverProbability=1 + @Test + fun testNoMutationWhenProbabilityZero_MuLambdaEA() { + 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.muLambdaOffspringSize = 10 + config.xoverProbability = 1.0 // irrelevant for (μ,λ) + config.fixedRateMutation = 0.0 // disable mutation + + ea.setupBeforeSearch() + ea.searchOnce() + + val nextPop = ea.getViewOfPopulation() + assertEquals(config.populationSize, nextPop.size) + assertEquals(0, rec.xoCalls.size) + assertEquals(0, rec.mutated.size) + } + } + + // One iteration properties: population size, best-µ selection from offspring, mutation count + @Test + fun testNextGenerationIsTheBestMuFromOffspringOnly() { + 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.muLambdaOffspringSize = 10 // divisible by mu -> perParent = 2 + config.xoverProbability = 0.0 + config.fixedRateMutation = 1.0 + + ea.setupBeforeSearch() + ea.searchOnce() + + val finalPop = ea.getViewOfPopulation() + val mu = config.populationSize + + // 1) population size remains µ + assertEquals(mu, finalPop.size) + + // 2) final population equals best-µ from offspring only + val offspring = rec.mutated.toList() + val expectedScores = offspring + .map { ea.score(it) } + .sortedDescending() + .take(mu) + val finalScores = finalPop + .map { ea.score(it) } + .sortedDescending() + assertEquals(expectedScores, finalScores) + + // 3) mutation count equals number of created offspring + val perParent = config.muLambdaOffspringSize / config.populationSize + assertEquals(perParent * config.populationSize, rec.mutated.size) + } + } +} + + diff --git a/docs/options.md b/docs/options.md index 4847bbc66e..192540b388 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`. *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, MuLambdaEA`. *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`.| @@ -163,6 +163,7 @@ There are 3 types of options: |`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`.| |`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`.| |`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`.|