Skip to content
Open
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
19 changes: 18 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 // These 3 are still work-in-progress
StandardGA, MonotonicGA, SteadyStateGA, BreederGA // 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 @@ -2742,6 +2742,23 @@ class EMConfig {
@Min(0.0)
var elitesCount: Int = 1

/**
* Breeder GA: truncation fraction to build parents pool P'. Range (0,1].
*/
@Experimental
@Min(0.0)
@Max(1.0)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could be @PercentageAsProbability

@Cfg("Breeder GA: fraction of top individuals to keep in parents pool (truncation).")
var breederTruncationFraction: Double = 0.5

/**
* Breeder GA: minimum number of parents to keep after truncation.
*/
@Experimental
@Min(1.0)
@Cfg("Breeder GA: minimum number of individuals in parents pool after truncation.")
var breederParentsMin: Int = 2

@Experimental
@Cfg("In REST APIs, when request Content-Type is JSON, POJOs are used instead of raw JSON string. " +
"Only available for JVM languages")
Expand Down
12 changes: 12 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.BreederGA ->
Key.get(object : TypeLiteral<BreederGeneticAlgorithm<GraphQLIndividual>>() {})


else -> throw IllegalStateException("Unrecognized algorithm ${config.algorithm}")
}
Expand All @@ -670,6 +673,9 @@ class Main {

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

EMConfig.Algorithm.BreederGA ->
Key.get(object : TypeLiteral<BreederGeneticAlgorithm<RPCIndividual>>() {})
else -> throw IllegalStateException("Unrecognized algorithm ${config.algorithm}")
}
}
Expand All @@ -694,6 +700,9 @@ class Main {

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

EMConfig.Algorithm.BreederGA ->
Key.get(object : TypeLiteral<BreederGeneticAlgorithm<WebIndividual>>() {})
else -> throw IllegalStateException("Unrecognized algorithm ${config.algorithm}")
}
}
Expand Down Expand Up @@ -728,6 +737,9 @@ class Main {
EMConfig.Algorithm.RW ->
Key.get(object : TypeLiteral<RandomWalkAlgorithm<RestIndividual>>() {})

EMConfig.Algorithm.BreederGA ->
Key.get(object : TypeLiteral<BreederGeneticAlgorithm<RestIndividual>>() {})

else -> throw IllegalStateException("Unrecognized algorithm ${config.algorithm}")
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
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
import kotlin.math.max

/**
* Breeder Genetic Algorithm (BGA)
*
* Differences vs Standard GA:
* - Uses truncation selection to build a parents pool P'.
* - At each step, creates two offspring from two random parents in P',
* then randomly selects ONE of the 2 offspring to add to the next population.
*/
class BreederGeneticAlgorithm<T> : AbstractGeneticAlgorithm<T>() where T : Individual {

override fun getType(): EMConfig.Algorithm {
return EMConfig.Algorithm.BreederGA
}

override fun searchOnce() {
beginGeneration()
frozenTargets = archive.notCoveredTargets()
val n = config.populationSize

// Elitism base for next generation
val nextPop = formTheNextPopulation(population)

// Build parents pool P' by truncation on current population
val parentsPool = buildParentsPoolByTruncation(population)

while (nextPop.size < n) {
beginStep()
val p1 = randomness.choose(parentsPool)
val p2 = randomness.choose(parentsPool)

// Work on copies
val o1 = p1.copy()
val o2 = p2.copy()

if (randomness.nextBoolean(config.xoverProbability)) {
xover(o1, o2)
}
if (randomness.nextBoolean(config.fixedRateMutation)) {
mutate(o1)
}
if (randomness.nextBoolean(config.fixedRateMutation)) {
mutate(o2)
}

// Randomly pick one child to carry over
var chosen = o1
if (!randomness.nextBoolean()) {
chosen = o2
}
nextPop.add(chosen)

if (!time.shouldContinueSearch()) {
endStep()
break
}
endStep()
}

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

private fun buildParentsPoolByTruncation(pop: List<WtsEvalIndividual<T>>): List<WtsEvalIndividual<T>> {
if (pop.isEmpty()) {
return pop
}

val sorted = pop.sortedByDescending { score(it) }
val k = max(config.breederParentsMin, (sorted.size * config.breederTruncationFraction).toInt())
return sorted.take(k)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
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.onemax.OneMaxIndividual
import org.evomaster.core.search.algorithms.onemax.OneMaxModule
import org.evomaster.core.search.algorithms.onemax.OneMaxSampler
import org.evomaster.core.search.algorithms.observer.GARecorder
import org.evomaster.core.search.service.ExecutionPhaseController
import org.evomaster.core.search.service.Randomness
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

import org.junit.jupiter.api.Assertions.*

class BreederGeneticAlgorithmTest {

private lateinit var injector: Injector

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

// Verifies that the Breeder GA can find the optimal solution for the OneMax problem
@Test
fun testBreederGeneticAlgorithmFindsOptimum() {
TestUtils.handleFlaky {
val breederGA = injector.getInstance(
Key.get(
object : TypeLiteral<BreederGeneticAlgorithm<OneMaxIndividual>>() {})
)

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

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

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

// Verifies that BGA forms next generation as elites + chosen children from truncation
@Test
fun testNextGenerationIsElitesPlusTruncationChildren() {
TestUtils.handleFlaky {
val breederGA = injector.getInstance(
Key.get(
object : TypeLiteral<BreederGeneticAlgorithm<OneMaxIndividual>>() {})
)

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

val config = injector.getInstance(EMConfig::class.java)
injector.getInstance(Randomness::class.java).updateSeed(42)

config.populationSize = 4
config.elitesCount = 2
config.xoverProbability = 1.0
config.fixedRateMutation = 1.0
config.gaSolutionSource = EMConfig.GASolutionSource.POPULATION
config.maxEvaluations = 100_000
config.stoppingCriterion = EMConfig.StoppingCriterion.ACTION_EVALUATIONS

breederGA.setupBeforeSearch()

val pop = breederGA.getViewOfPopulation()
val expectedElites = pop.sortedByDescending { it.calculateCombinedFitness() }.take(2)

breederGA.searchOnce()

val nextPop = breederGA.getViewOfPopulation()

// population size preserved
assertEquals(config.populationSize, nextPop.size)

// elites are present in next population
assertTrue(nextPop.any { it === expectedElites[0] })
assertTrue(nextPop.any { it === expectedElites[1] })

// number of iterations equals children added = populationSize - elites
val iterations = config.populationSize - config.elitesCount
assertEquals(iterations, rec.xoCalls.size)

// each iteration produced (o1,o2); exactly one should be carried over
rec.xoCalls.forEach { (o1, o2) ->
assertTrue(nextPop.any { it === o1 } || nextPop.any { it === o2 })
}

// two mutations per iteration (one per offspring)
assertEquals(2 * iterations, rec.mutated.size)
}
}

// Edge Case: CrossoverProbability=0 and MutationProbability=1 on BGA
@Test
fun testNoCrossoverWhenProbabilityZero_BGA() {
TestUtils.handleFlaky {
val breederGA = injector.getInstance(
Key.get(
object : TypeLiteral<BreederGeneticAlgorithm<OneMaxIndividual>>() {})
)

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

val config = injector.getInstance(EMConfig::class.java)
config.populationSize = 4
config.elitesCount = 0
config.xoverProbability = 0.0 // disable crossover
config.fixedRateMutation = 1.0 // force mutation
config.gaSolutionSource = EMConfig.GASolutionSource.POPULATION
config.maxEvaluations = 100_000
config.stoppingCriterion = EMConfig.StoppingCriterion.ACTION_EVALUATIONS

breederGA.setupBeforeSearch()
breederGA.searchOnce()

val nextPop = breederGA.getViewOfPopulation()
assertEquals(config.populationSize, nextPop.size)

// crossover disabled
assertEquals(0, rec.xoCalls.size)
// should apply two mutations per iteration (mutation probability = 1)
assertEquals(2 * config.populationSize, rec.mutated.size)
}
}

// Edge Case: MutationProbability=0 and CrossoverProbability=1 on BGA
@Test
fun testNoMutationWhenProbabilityZero_BGA() {
TestUtils.handleFlaky {
val breederGA = injector.getInstance(
Key.get(
object : TypeLiteral<BreederGeneticAlgorithm<OneMaxIndividual>>() {})
)

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

val config = injector.getInstance(EMConfig::class.java)
config.populationSize = 4
config.elitesCount = 0
config.xoverProbability = 1.0 // force crossover
config.fixedRateMutation = 0.0 // disable mutation
config.gaSolutionSource = EMConfig.GASolutionSource.POPULATION
config.maxEvaluations = 100_000
config.stoppingCriterion = EMConfig.StoppingCriterion.ACTION_EVALUATIONS

breederGA.setupBeforeSearch()
breederGA.searchOnce()

val nextPop = breederGA.getViewOfPopulation()
assertEquals(config.populationSize, nextPop.size)

// crossovers happen once per iteration (mutation probability = 1)
assertEquals(config.populationSize, rec.xoCalls.size)

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


}
4 changes: 3 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`. *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`. *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 @@ -246,6 +246,8 @@ There are 3 types of options:
|`aiResponseClassifierWarmup`| __Int__. Number of training iterations required to update classifier parameters. For example, in the Gaussian model this affects mean and variance updates. For neural network (NN) models, the warm-up should typically be larger than 1000. *Default value*: `10`.|
|`appendToTargetHeuristicsFile`| __Boolean__. Whether should add to an existing target heuristics file, instead of replacing it. It is only used when processFormat is TARGET_HEURISTIC. *Default value*: `false`.|
|`bbProbabilityUseDataPool`| __Double__. Specify the probability of using the data pool when sampling test cases. This is for black-box (bb) mode. *Constraints*: `probability 0.0-1.0`. *Default value*: `0.8`.|
|`breederParentsMin`| __Int__. Breeder GA: minimum number of individuals in parents pool after truncation. *Constraints*: `min=1.0`. *Default value*: `2`.|
|`breederTruncationFraction`| __Double__. Breeder GA: fraction of top individuals to keep in parents pool (truncation). *Constraints*: `min=0.0, max=1.0`. *Default value*: `0.5`.|
|`callbackURLHostname`| __String__. HTTP callback verifier hostname. Default is set to 'localhost'. If the SUT is running inside a container (i.e., Docker), 'localhost' will refer to the container. This can be used to change the hostname. *Default value*: `localhost`.|
|`classificationRepairThreshold`| __Double__. If using THRESHOLD for AI Classification Repair, specify its value. All classifications with probability equal or above such threshold value will be accepted. *Constraints*: `probability 0.0-1.0`. *Default value*: `0.8`.|
|`discoveredInfoRewardedInFitness`| __Boolean__. If there is new discovered information from a test execution, reward it in the fitness function. *Default value*: `false`.|
Expand Down