Skip to content

Commit 104579d

Browse files
committed
chore: improve reproducibility
1 parent 28c4966 commit 104579d

15 files changed

Lines changed: 92 additions & 90 deletions

File tree

core/src/main/java/ai/timefold/solver/core/impl/evolutionaryalgorithm/DefaultEvolutionaryAlgorithmPhaseFactory.java

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
import ai.timefold.solver.core.impl.phase.AbstractPhaseFactory;
7676
import ai.timefold.solver.core.impl.phase.Phase;
7777
import ai.timefold.solver.core.impl.phase.PhaseFactory;
78+
import ai.timefold.solver.core.impl.solver.random.DelegatingSplittableRandomGenerator;
7879
import ai.timefold.solver.core.impl.solver.recaller.BestSolutionRecaller;
7980
import ai.timefold.solver.core.impl.solver.termination.PhaseTermination;
8081
import ai.timefold.solver.core.impl.solver.termination.SolverTermination;
@@ -178,19 +179,19 @@ public EvolutionaryAlgorithmPhase<Solution_> buildPhase(int phaseIndex, boolean
178179
disableLogging(buildRefinmentPhase(solverConfigPolicy, solverTermination, isListVariable));
179180

180181
ConstructionIndividualStrategy<Solution_, Score_> exploratoryConstructionIndividualStrategy =
181-
buildConstructionIndividualPhase(workerConfig, workerConfig.getIndividualGeneratorConfig(),
182+
buildConstructionIndividualPhase(solverConfigPolicy, workerConfig, workerConfig.getIndividualGeneratorConfig(),
182183
deterministicBestFitConstructionPhase, shuffledFirstFitConstructionPhase, fasterLocalSearchPhase,
183184
refinmentPhase, solutionStateManager, individualBuilder, exploratoryInheritanceRate, isListVariable);
184185
ConstructionIndividualStrategy<Solution_, Score_> conservativeConstructionIndividualStrategy =
185-
buildConstructionIndividualPhase(workerConfig, workerConfig.getIndividualGeneratorConfig(),
186+
buildConstructionIndividualPhase(solverConfigPolicy, workerConfig, workerConfig.getIndividualGeneratorConfig(),
186187
deterministicBestFitConstructionPhase, shuffledFirstFitConstructionPhase, regularLocalSearchPhase,
187188
refinmentPhase, solutionStateManager, individualBuilder, conservativeInheritanceRate, isListVariable);
188189

189190
CrossoverStrategy<Solution_, Score_> exploratoryCrossoverStrategy =
190-
buildCrossoverStrategy(fasterLocalSearchPhase, refinmentPhase, false, exploratoryInheritanceRate,
191-
isListVariable);
192-
CrossoverStrategy<Solution_, Score_> conservativeCrossoverStrategy = buildCrossoverStrategy(regularLocalSearchPhase,
193-
refinmentPhase, true, conservativeInheritanceRate, isListVariable);
191+
buildCrossoverStrategy(solverConfigPolicy, fasterLocalSearchPhase, refinmentPhase, false,
192+
exploratoryInheritanceRate, isListVariable);
193+
CrossoverStrategy<Solution_, Score_> conservativeCrossoverStrategy = buildCrossoverStrategy(solverConfigPolicy,
194+
regularLocalSearchPhase, refinmentPhase, true, conservativeInheritanceRate, isListVariable);
194195

195196
if (workerConfig.getExploratoryRate() != null && (workerConfig.getExploratoryRate() < 0.0
196197
|| workerConfig.getExploratoryRate() > 1.0)) {
@@ -224,12 +225,14 @@ SolutionStateManager<Solution_, Score_, State_> buildSolutionStateManager(boolea
224225
}
225226

226227
private static <Solution_, Score_ extends Score<Score_>> CrossoverStrategy<Solution_, Score_> buildCrossoverStrategy(
227-
Phase<Solution_> localSearchPhase, @Nullable Phase<Solution_> refinementPhase, boolean isComplex,
228-
double inheritanceRate, boolean isListVariable) {
228+
HeuristicConfigPolicy<Solution_> solverConfigPolicy, Phase<Solution_> localSearchPhase,
229+
@Nullable Phase<Solution_> refinementPhase, boolean isComplex, double inheritanceRate, boolean isListVariable) {
230+
var mainRandomGenerator = (DelegatingSplittableRandomGenerator) solverConfigPolicy.getRandom();
229231
if (isListVariable) {
230-
return new ListOXCrossover<>(localSearchPhase, refinementPhase, inheritanceRate, !isComplex);
232+
return new ListOXCrossover<>(localSearchPhase, refinementPhase, inheritanceRate, !isComplex,
233+
mainRandomGenerator.split());
231234
} else {
232-
return new BasicOXCrossover<>(localSearchPhase, refinementPhase, inheritanceRate);
235+
return new BasicOXCrossover<>(localSearchPhase, refinementPhase, inheritanceRate, mainRandomGenerator.split());
233236
}
234237
}
235238

@@ -268,22 +271,24 @@ private static <Solution_> Phase<Solution_> buildShuffledConstructionHeuristicPh
268271

269272
private static <Solution_, Score_ extends Score<Score_>, State_ extends SolutionState<Solution_, Score_>>
270273
ConstructionIndividualStrategy<Solution_, Score_>
271-
buildConstructionIndividualPhase(EvolutionaryWorkerConfig workerConfig,
274+
buildConstructionIndividualPhase(HeuristicConfigPolicy<Solution_> solverConfigPolicy,
275+
EvolutionaryWorkerConfig workerConfig,
272276
@Nullable EvolutionaryIndividualGeneratorConfig individualGeneratorConfig,
273277
Phase<Solution_> deterministicBestFitConstructionPhase, Phase<Solution_> shuffledFirstFitConstructionPhase,
274278
Phase<Solution_> localSearchPhase, @Nullable Phase<Solution_> refinementPhase,
275279
SolutionStateManager<Solution_, Score_, State_> solutionStateManager,
276280
IndividualBuilder<Solution_, Score_> individualBuilder, double inheritanceRate, boolean isListVariable) {
277281
List<PhaseCommand<Solution_>> customIndividualPhaseCommandList =
278282
buildPhaseCommandList(workerConfig, individualGeneratorConfig);
283+
var mainRandomGenerator = (DelegatingSplittableRandomGenerator) solverConfigPolicy.getRandom();
279284
if (isListVariable) {
280285
return new ListRuinRecreateIndividualStrategy<>(customIndividualPhaseCommandList,
281286
deterministicBestFitConstructionPhase, localSearchPhase, refinementPhase, solutionStateManager,
282-
individualBuilder, inheritanceRate);
287+
individualBuilder, inheritanceRate, mainRandomGenerator.split());
283288
} else {
284289
return new BasicRuinRecreateIndividualStrategy<>(customIndividualPhaseCommandList,
285290
deterministicBestFitConstructionPhase, shuffledFirstFitConstructionPhase, localSearchPhase, refinementPhase,
286-
solutionStateManager, individualBuilder, inheritanceRate);
291+
solutionStateManager, individualBuilder, inheritanceRate, mainRandomGenerator.split());
287292
}
288293
}
289294

core/src/main/java/ai/timefold/solver/core/impl/evolutionaryalgorithm/crossover/basic/BasicOXCrossover.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,15 @@
3636
*/
3737
@NullMarked
3838
public record BasicOXCrossover<Solution_, Score_ extends Score<Score_>>(Phase<Solution_> localSearchPhase,
39-
@Nullable Phase<Solution_> refinementPhase, double inheritanceRate) implements CrossoverStrategy<Solution_, Score_> {
39+
@Nullable Phase<Solution_> refinementPhase, double inheritanceRate,
40+
RandomGenerator workingRandom) implements CrossoverStrategy<Solution_, Score_> {
4041

4142
@Override
4243
public CrossoverResult<Solution_, Score_> apply(CrossoverContext<Solution_, Score_> context) {
4344
var phaseScope = context.phaseScope();
4445
var solverScope = phaseScope.getSolverScope();
4546
var scoreDirector = phaseScope.<Score_> getScoreDirector();
46-
generateOffspring(scoreDirector, context.firstIndividual(), context.secondIndividual(),
47-
inheritanceRate, phaseScope.getWorkingRandom());
47+
generateOffspring(scoreDirector, context.firstIndividual(), context.secondIndividual(), inheritanceRate, workingRandom);
4848
updateScope(phaseScope);
4949
applyPhases(phaseScope, localSearchPhase, refinementPhase);
5050
return new CrossoverResult<>(scoreDirector.cloneSolution(solverScope.getBestSolution()),

core/src/main/java/ai/timefold/solver/core/impl/evolutionaryalgorithm/crossover/list/AbstractListCrossover.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import java.util.Objects;
44
import java.util.Set;
5+
import java.util.random.RandomGenerator;
56

67
import ai.timefold.solver.core.api.score.Score;
78
import ai.timefold.solver.core.impl.domain.variable.ListVariableStateSupply;
@@ -27,12 +28,14 @@ public abstract sealed class AbstractListCrossover<Solution_, Score_ extends Sco
2728
final Phase<Solution_> localSearchPhase;
2829
final @Nullable Phase<Solution_> refinementPhase;
2930
final double inheritanceRate;
31+
final RandomGenerator workingRandom;
3032

31-
AbstractListCrossover(Phase<Solution_> localSearchPhase, @Nullable Phase<Solution_> refinementPhase,
32-
double inheritanceRate) {
33+
AbstractListCrossover(Phase<Solution_> localSearchPhase, @Nullable Phase<Solution_> refinementPhase, double inheritanceRate,
34+
RandomGenerator workingRandom) {
3335
this.localSearchPhase = localSearchPhase;
3436
this.refinementPhase = refinementPhase;
3537
this.inheritanceRate = inheritanceRate;
38+
this.workingRandom = workingRandom;
3639
}
3740

3841
/**

core/src/main/java/ai/timefold/solver/core/impl/evolutionaryalgorithm/crossover/list/ListOXCrossover.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ public final class ListOXCrossover<Solution_, Score_ extends Score<Score_>>
4545
private final boolean applyBestFitFirstPhase;
4646

4747
public ListOXCrossover(Phase<Solution_> localSearchPhase, @Nullable Phase<Solution_> refinementPhase,
48-
double inheritanceRate, boolean applyBestFitFirstPhase) {
49-
super(localSearchPhase, refinementPhase, inheritanceRate);
48+
double inheritanceRate, boolean applyBestFitFirstPhase, RandomGenerator workingRandom) {
49+
super(localSearchPhase, refinementPhase, inheritanceRate, workingRandom);
5050
this.applyBestFitFirstPhase = applyBestFitFirstPhase;
5151
}
5252

@@ -63,7 +63,7 @@ public CrossoverResult<Solution_, Score_> apply(CrossoverContext<Solution_, Scor
6363
// Produce the offspring based on the two parents.
6464
generateOffspring(scoreDirector, listVariableStateSupply, listVariableDescriptor, listVariableModel,
6565
valueRangeManager, context.firstIndividual(), context.secondIndividual(), inheritanceRate,
66-
applyBestFitFirstPhase, phaseScope.getWorkingRandom());
66+
applyBestFitFirstPhase, workingRandom);
6767
// We need to update the best solution, best score,
6868
// and initialized score to avoid inconsistencies in the next phases
6969
updateScope(phaseScope);

core/src/main/java/ai/timefold/solver/core/impl/evolutionaryalgorithm/crossover/list/ListRXCrossover.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ public final class ListRXCrossover<Solution_, Score_ extends Score<Score_>>
4545
private final boolean applyBestFitFirstPhase;
4646

4747
public ListRXCrossover(Phase<Solution_> localSearchPhase, @Nullable Phase<Solution_> refinementPhase,
48-
double inheritanceRage, boolean applyBestFitFirstPhase) {
49-
super(localSearchPhase, refinementPhase, inheritanceRage);
48+
double inheritanceRage, boolean applyBestFitFirstPhase, RandomGenerator randomGenerator) {
49+
super(localSearchPhase, refinementPhase, inheritanceRage, randomGenerator);
5050
this.applyBestFitFirstPhase = applyBestFitFirstPhase;
5151
}
5252

@@ -64,8 +64,8 @@ public CrossoverResult<Solution_, Score_> apply(CrossoverContext<Solution_, Scor
6464
// The offspring is expected to inherit approximately 90% of their planning values from the first parent.
6565
// Some experiments have demonstrated that this approach is more effective in overconstrained models.
6666
generateOffspring(scoreDirector, listVariableStateSupply, listVariableDescriptor, listVariableMetaModel,
67-
valueRangeManager, context.firstIndividual(), context.secondIndividual(), phaseScope.getWorkingRandom(),
68-
inheritanceRate, applyBestFitFirstPhase);
67+
valueRangeManager, context.firstIndividual(), context.secondIndividual(), workingRandom, inheritanceRate,
68+
applyBestFitFirstPhase);
6969
// We need to update the best solution, best score,
7070
// and initialized score to avoid inconsistencies in the next phases
7171
updateScope(phaseScope);

core/src/main/java/ai/timefold/solver/core/impl/evolutionaryalgorithm/decider/HybridGeneticSearchDecider.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import ai.timefold.solver.core.impl.evolutionaryalgorithm.common.state.SolutionState;
1212
import ai.timefold.solver.core.impl.evolutionaryalgorithm.population.DefaultPopulation;
1313
import ai.timefold.solver.core.impl.evolutionaryalgorithm.population.Population;
14+
import ai.timefold.solver.core.impl.solver.random.DelegatingSplittableRandomGenerator;
1415

1516
import org.jspecify.annotations.NullMarked;
1617
import org.jspecify.annotations.Nullable;
@@ -144,7 +145,8 @@ public void phaseStarted(EvolutionaryAlgorithmPhaseScope<Solution_> phaseScope)
144145
exploratoryRate = scaleLog < 427.0 ? 0.9 : 0.1;
145146
}
146147
this.worker = new HybridGeneticSearchWorker<>(HybridGeneticSearchWorkerContext.of(exploratoryRate, context),
147-
bestSolutionUpdater, workerSolverScope.getWorkingRandom(), workerSolverScope);
148+
bestSolutionUpdater, (DelegatingSplittableRandomGenerator) workerSolverScope.getWorkingRandom(),
149+
workerSolverScope);
148150
this.worker.phaseStarted(phaseScope);
149151
}
150152

core/src/main/java/ai/timefold/solver/core/impl/evolutionaryalgorithm/decider/HybridGeneticSearchWorker.java

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import ai.timefold.solver.core.impl.evolutionaryalgorithm.population.individual.generator.ConstructionIndividualStrategy;
1818
import ai.timefold.solver.core.impl.phase.Phase;
1919
import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope;
20+
import ai.timefold.solver.core.impl.solver.random.DelegatingSplittableRandomGenerator;
2021
import ai.timefold.solver.core.impl.solver.scope.SolverScope;
2122

2223
import org.jspecify.annotations.NullMarked;
@@ -45,18 +46,18 @@ public class HybridGeneticSearchWorker<Solution_, Score_ extends Score<Score_>,
4546

4647
private final HybridGeneticSearchWorkerContext<Solution_, Score_, State_> context;
4748
private final BestSolutionUpdater<Solution_> bestSolutionUpdater;
48-
private final RandomGenerator workingRandom;
49+
private final RandomGenerator workerRandom;
4950
private final SolverScope<Solution_> ownSolverScope;
5051

5152
@Nullable
5253
private State_ initialState;
5354

5455
public HybridGeneticSearchWorker(HybridGeneticSearchWorkerContext<Solution_, Score_, State_> context,
55-
BestSolutionUpdater<Solution_> bestSolutionUpdater, RandomGenerator workingRandom,
56+
BestSolutionUpdater<Solution_> bestSolutionUpdater, DelegatingSplittableRandomGenerator workingRandom,
5657
SolverScope<Solution_> ownSolverScope) {
5758
this.context = context;
5859
this.bestSolutionUpdater = bestSolutionUpdater;
59-
this.workingRandom = workingRandom;
60+
this.workerRandom = workingRandom.split();
6061
this.ownSolverScope = ownSolverScope;
6162
}
6263

@@ -84,8 +85,7 @@ public void generateIndividual(EvolutionaryAlgorithmPhaseScope<Solution_> shared
8485
var newIndividual = constructionStrategy.apply(stepScope);
8586
var addIndividual = true;
8687
var oldScore = newIndividual.getScore();
87-
if (!newIndividual.getScore().raw().isFeasible()
88-
&& restoredPhaseScope.getSolverScope().getWorkingRandom().nextBoolean()) {
88+
if (!newIndividual.getScore().raw().isFeasible() && workerRandom.nextBoolean()) {
8989
var clonedIndividual = newIndividual.clone(ownSolverScope.getScoreDirector());
9090
individualConsumer.accept(clonedIndividual);
9191
applyPhases(restoredPhaseScope, constructionStrategy.getLocalSearchPhase(),
@@ -124,8 +124,7 @@ public void applyCrossover(EvolutionaryAlgorithmStepScope<Solution_> sharedStepS
124124
offspringResult.firstParentScore(), offspringResult.secondParentScore(), ownSolverScope.getScoreDirector());
125125
var addIndividual = true;
126126
var oldScore = offspringIndividual.getScore();
127-
if (!offspringIndividual.getScore().raw().isFeasible()
128-
&& restoredPhaseScope.getSolverScope().getWorkingRandom().nextBoolean()) {
127+
if (!offspringIndividual.getScore().raw().isFeasible() && workerRandom.nextBoolean()) {
129128
individualConsumer.accept(offspringIndividual.clone(ownSolverScope.getScoreDirector()));
130129
applyPhases(restoredPhaseScope, crossoverStrategy.getLocalSearchPhase(), crossoverStrategy.getRefinementPhase());
131130
if (restoredPhaseScope.<Score_> getBestScore().compareTo(oldScore) == 0) {
@@ -287,25 +286,21 @@ public static <Solution_> void applyPhases(AbstractPhaseScope<Solution_> phaseSc
287286
}
288287

289288
private ConstructionIndividualStrategy<Solution_, Score_> pickConstructionIndividualStrategy() {
290-
if (workingRandom.nextDouble(1) < context.exploratoryRate()) {
289+
if (workerRandom.nextDouble(1) < context.exploratoryRate()) {
291290
return context.exploratoryConstructionIndividualStrategy();
292291
} else {
293292
return context.conservativeConstructionIndividualStrategy();
294293
}
295294
}
296295

297296
private CrossoverStrategy<Solution_, Score_> pickCrossoverStrategy() {
298-
if (workingRandom.nextDouble(1) < context.exploratoryRate()) {
297+
if (workerRandom.nextDouble(1) < context.exploratoryRate()) {
299298
return context.exploratoryCrossoverStrategy();
300299
} else {
301300
return context.conservativeCrossoverStrategy();
302301
}
303302
}
304303

305-
protected RandomGenerator getWorkingRandom() {
306-
return workingRandom;
307-
}
308-
309304
// ************************************************************************
310305
// Lifecycle methods
311306
// ************************************************************************

core/src/main/java/ai/timefold/solver/core/impl/evolutionaryalgorithm/population/AbstractPopulation.java

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -289,31 +289,25 @@ private double averageDiff(Individual<Solution_, Score_> individual, int size) {
289289
if (otherIndividualsCount == 0) {
290290
return 0.0;
291291
}
292+
// Load and sort the diffs for deterministic floating-point summation
293+
var diffs = new double[otherIndividualsCount];
294+
var i = 0;
295+
for (var diff : individualDiffMap.values()) {
296+
diffs[i++] = diff;
297+
}
298+
Arrays.sort(diffs);
292299
// Hot path for a size of one
293300
if (size == 1) {
294-
var min = Double.MAX_VALUE;
295-
for (var diff : individualDiffMap.values()) {
296-
if (diff < min) {
297-
min = diff;
298-
}
299-
}
300-
return min;
301+
return diffs[0];
301302
}
302-
// All other individuals fit within the limit, so we can calculate the average without sorting
303303
if (otherIndividualsCount <= size) {
304304
var result = 0.d;
305-
for (var diff : individualDiffMap.values()) {
305+
for (var diff : diffs) {
306306
result += diff;
307307
}
308308
return result / (double) otherIndividualsCount;
309309
}
310-
// Sort the individuals ascending and compute only k nearst ones
311-
var diffs = new double[otherIndividualsCount];
312-
var i = 0;
313-
for (var diff : individualDiffMap.values()) {
314-
diffs[i++] = diff;
315-
}
316-
Arrays.sort(diffs);
310+
// Compute only k nearest ones
317311
var result = 0.d;
318312
for (var j = 0; j < size; j++) {
319313
result += diffs[j];

0 commit comments

Comments
 (0)