Skip to content

Commit 209e449

Browse files
authored
Merge pull request #1357 from WebFuzzing/feature/monotonic-ga
MonotonicGA & Lifecycle Hooks
2 parents fd3d8bd + 6e1b773 commit 209e449

6 files changed

Lines changed: 335 additions & 13 deletions

File tree

core/src/main/kotlin/org/evomaster/core/search/algorithms/AbstractGeneticAlgorithm.kt

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,28 @@ abstract class AbstractGeneticAlgorithm<T> : SearchAlgorithm<T>() where T : Indi
5757
observers.remove(observer)
5858
}
5959

60+
/** Call at the start of each searchOnce to begin a new generation scope. */
61+
protected fun beginGeneration() {
62+
observers.forEach { it.onGenerationStart() }
63+
}
64+
65+
/** Call at the end of each searchOnce to report generation aggregates. */
66+
protected fun endGeneration() {
67+
val snapshot = population.toList()
68+
val bestScore = snapshot.maxOfOrNull { score(it) } ?: 0.0
69+
observers.forEach { it.onGenerationEnd(snapshot, bestScore) }
70+
}
71+
72+
/** Start a new step inside current iteration. */
73+
protected fun beginStep() {
74+
observers.forEach { it.onStepStart() }
75+
}
76+
77+
/** End current step and report aggregates. */
78+
protected fun endStep() {
79+
observers.forEach { it.onStepEnd() }
80+
}
81+
6082
/**
6183
* Called once before the search begins. Clears any old population and initializes a new one.
6284
*/
@@ -184,12 +206,9 @@ abstract class AbstractGeneticAlgorithm<T> : SearchAlgorithm<T>() where T : Indi
184206
/**
185207
* Combined fitness of a suite computed only over [frozenTargets] when set; otherwise full combined fitness.
186208
*/
187-
protected fun score(w: WtsEvalIndividual<T>): Double {
188-
if (w.suite.isEmpty()) return 0.0
189-
190-
// Explicitly use full combined fitness when solution source is POPULATION
191-
if (config.gaSolutionSource == EMConfig.GASolutionSource.POPULATION) {
192-
return w.calculateCombinedFitness()
209+
public fun score(w: WtsEvalIndividual<T>): Double {
210+
if (w.suite.isEmpty()) {
211+
return 0.0
193212
}
194213

195214
if (frozenTargets.isEmpty()) {
@@ -202,7 +221,9 @@ abstract class AbstractGeneticAlgorithm<T> : SearchAlgorithm<T>() where T : Indi
202221
var sum = 0.0
203222
frozenTargets.forEach { t ->
204223
val comp = view[t]
205-
if (comp != null) sum += comp.score
224+
if (comp != null){
225+
sum += comp.score
226+
}
206227
}
207228
return sum
208229
}

core/src/main/kotlin/org/evomaster/core/search/algorithms/MonotonicGeneticAlgorithm.kt

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@ import kotlin.math.max
1616
* This is a more conservative variant compared to standard GAs, aiming to preserve
1717
* the quality of solutions across generations and avoid degradation in performance.
1818
*
19-
* This class builds on top of [StandardGeneticAlgorithm] but overrides
20-
* population update logic to enforce the monotonic condition.
19+
* This class relies on the common GA utilities provided by
20+
* [AbstractGeneticAlgorithm] (selection, crossover, mutation, population
21+
* management), and overrides the generation update logic to enforce the
22+
* monotonic condition.
2123
*/
22-
class MonotonicGeneticAlgorithm<T> : StandardGeneticAlgorithm<T>() where T : Individual {
24+
class MonotonicGeneticAlgorithm<T> : AbstractGeneticAlgorithm<T>() where T : Individual {
2325

2426
override fun getType(): EMConfig.Algorithm {
2527
return EMConfig.Algorithm.MonotonicGA
@@ -47,11 +49,16 @@ class MonotonicGeneticAlgorithm<T> : StandardGeneticAlgorithm<T>() where T : Ind
4749
* or the time budget is exhausted.
4850
*/
4951
override fun searchOnce() {
52+
beginGeneration()
53+
// Freeze objectives for this generation
54+
frozenTargets = archive.notCoveredTargets()
5055
val n = config.populationSize
5156

52-
val nextPop: MutableList<WtsEvalIndividual<T>> = mutableListOf()
57+
// Start next population with elites (elitism)
58+
val nextPop: MutableList<WtsEvalIndividual<T>> = formTheNextPopulation(population)
5359

5460
while (nextPop.size < n) {
61+
beginStep()
5562
val p1 = tournamentSelection()
5663
val p2 = tournamentSelection()
5764

@@ -74,8 +81,8 @@ class MonotonicGeneticAlgorithm<T> : StandardGeneticAlgorithm<T>() where T : Ind
7481

7582
// Monotonic replacement rule:
7683
// Keep offspring only if they're better than the parents
77-
if (max(o1.calculateCombinedFitness(), o2.calculateCombinedFitness()) >
78-
max(p1.calculateCombinedFitness(), p2.calculateCombinedFitness())
84+
if (max(score(o1), score(o2)) >
85+
max(score(p1), score(p2))
7986
) {
8087
nextPop.add(o1)
8188
nextPop.add(o2)
@@ -85,12 +92,15 @@ class MonotonicGeneticAlgorithm<T> : StandardGeneticAlgorithm<T>() where T : Ind
8592
}
8693

8794
if (!time.shouldContinueSearch()) {
95+
endStep()
8896
break
8997
}
98+
endStep()
9099
}
91100

92101
// Replace the current population with the newly formed one
93102
population.clear()
94103
population.addAll(nextPop)
104+
endGeneration()
95105
}
96106
}

core/src/main/kotlin/org/evomaster/core/search/algorithms/StandardGeneticAlgorithm.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ open class StandardGeneticAlgorithm<T> : AbstractGeneticAlgorithm<T>() where T :
4242
* Terminates early if the time budget is exceeded.
4343
*/
4444
override fun searchOnce() {
45+
beginGeneration()
4546
// Freeze objectives for this generation
4647
frozenTargets = archive.notCoveredTargets()
4748
val n = config.populationSize
@@ -50,6 +51,7 @@ open class StandardGeneticAlgorithm<T> : AbstractGeneticAlgorithm<T>() where T :
5051
val nextPop = formTheNextPopulation(population)
5152

5253
while (nextPop.size < n) {
54+
beginStep()
5355
val sizeBefore = nextPop.size
5456

5557
// Select two parents
@@ -78,12 +80,15 @@ open class StandardGeneticAlgorithm<T> : AbstractGeneticAlgorithm<T>() where T :
7880

7981
// Stop early if time budget is exhausted
8082
if (!time.shouldContinueSearch()) {
83+
endStep()
8184
break
8285
}
86+
endStep()
8387
}
8488

8589
// Replace current population with the new one
8690
population.clear()
8791
population.addAll(nextPop)
92+
endGeneration()
8893
}
8994
}

core/src/main/kotlin/org/evomaster/core/search/algorithms/observer/GAObserver.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,30 @@ import org.evomaster.core.search.algorithms.wts.WtsEvalIndividual
88
* Default methods are no-ops so listeners can implement only what they need.
99
*/
1010
interface GAObserver<T : Individual> {
11+
/** Called at the start of a generation (one call to searchOnce). */
12+
fun onGenerationStart() {}
13+
14+
/** Called at the start of a step inside a generation. */
15+
fun onStepStart() {}
16+
1117
/** Called when one parent is selected. */
1218
fun onSelection(sel: WtsEvalIndividual<T>) {}
1319
/** Called immediately after crossover is applied to [x] and [y]. */
1420
fun onCrossover(x: WtsEvalIndividual<T>, y: WtsEvalIndividual<T>) {}
1521

1622
/** Called immediately after mutation is applied to [wts]. */
1723
fun onMutation(wts: WtsEvalIndividual<T>) {}
24+
25+
/**
26+
* Called at the end of a generation (one call to searchOnce), with a snapshot of the final population
27+
* and the best score according to the GA's internal scoring (e.g., frozen targets).
28+
*/
29+
fun onGenerationEnd(population: List<WtsEvalIndividual<T>>, bestScore: Double) {}
30+
31+
/**
32+
* Called at the end of a step inside a generation.
33+
*/
34+
fun onStepEnd() {}
1835
}
1936

2037

0 commit comments

Comments
 (0)