From 90cc10333d643107699e155509bd8778fd98ce57 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Wed, 25 Mar 2026 15:04:03 -0400 Subject: [PATCH 1/8] perf: make Random splittable --- .../solver/random/DefaultRandomFactory.java | 2 +- .../DelegatingSplittableRandomGenerator.java | 99 +++++++++++++++++++ core/src/main/java/module-info.java | 1 + 3 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/solver/random/DelegatingSplittableRandomGenerator.java diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/random/DefaultRandomFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/random/DefaultRandomFactory.java index d1a06ef2113..66469098215 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/random/DefaultRandomFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/random/DefaultRandomFactory.java @@ -32,7 +32,7 @@ public DefaultRandomFactory(RandomType randomType, Long randomSeed) { public RandomGenerator createRandom() { switch (randomType) { case JDK: - return randomSeed == null ? new Random() : new Random(randomSeed); + return randomSeed == null ? new DelegatingSplittableRandomGenerator(new Random().nextLong()) : new DelegatingSplittableRandomGenerator(randomSeed); case MERSENNE_TWISTER: return new RandomAdaptor(randomSeed == null ? new MersenneTwister() : new MersenneTwister(randomSeed)); case WELL512A: diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/random/DelegatingSplittableRandomGenerator.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/random/DelegatingSplittableRandomGenerator.java new file mode 100644 index 00000000000..9880bad0632 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/random/DelegatingSplittableRandomGenerator.java @@ -0,0 +1,99 @@ +package ai.timefold.solver.core.impl.solver.random; + +import java.util.SplittableRandom; +import java.util.random.RandomGenerator; + +public final class DelegatingSplittableRandomGenerator implements RandomGenerator { + RandomGenerator.SplittableGenerator delegate; + + DelegatingSplittableRandomGenerator(long seed) { + this.delegate = new SplittableRandom(seed); + } + + public RandomGenerator.SplittableGenerator split() { + return delegate.split(); + } + + public void setDelegate(RandomGenerator.SplittableGenerator delegate) { + this.delegate = delegate; + } + + // ***************************************** + // RandomGenerator methods + // ***************************************** + + @Override + public long nextLong() { + return delegate.nextLong(); + } + + @Override + public int nextInt() { + return delegate.nextInt(); + } + + @Override + public int nextInt(int bound) { + return delegate.nextInt(bound); + } + + @Override + public int nextInt(int origin, int bound) { + return delegate.nextInt(origin, bound); + } + + @Override + public long nextLong(long bound) { + return delegate.nextLong(bound); + } + + @Override + public long nextLong(long origin, long bound) { + return delegate.nextLong(origin, bound); + } + + @Override + public double nextDouble() { + return delegate.nextDouble(); + } + + @Override + public double nextDouble(double bound) { + return delegate.nextDouble(bound); + } + + @Override + public double nextDouble(double origin, double bound) { + return delegate.nextDouble(origin, bound); + } + + @Override + public float nextFloat() { + return delegate.nextFloat(); + } + + @Override + public float nextFloat(float bound) { + return delegate.nextFloat(bound); + } + + @Override + public float nextFloat(float origin, float bound) { + return delegate.nextFloat(origin, bound); + } + + @Override + public double nextGaussian() { + return delegate.nextGaussian(); + } + + @Override + public boolean nextBoolean() { + return delegate.nextBoolean(); + } + + @Override + public void nextBytes(byte[] bytes) { + delegate.nextBytes(bytes); + } +} diff --git a/core/src/main/java/module-info.java b/core/src/main/java/module-info.java index ed35977a9ca..e6fac5a290f 100644 --- a/core/src/main/java/module-info.java +++ b/core/src/main/java/module-info.java @@ -202,6 +202,7 @@ exports ai.timefold.solver.core.impl.neighborhood to ai.timefold.solver.enterprise.core; exports ai.timefold.solver.core.impl.partitionedsearch to ai.timefold.solver.enterprise.core; exports ai.timefold.solver.core.impl.phase to ai.timefold.solver.enterprise.core; + exports ai.timefold.solver.core.impl.solver.random to ai.timefold.solver.enterprise.core; exports ai.timefold.solver.core.impl.solver.recaller to ai.timefold.solver.enterprise.core; exports ai.timefold.solver.core.impl.solver.event to ai.timefold.solver.enterprise.core; From 0f742396ce1f9dac0d1571fefb783aefe77c7577 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Wed, 22 Apr 2026 11:03:29 -0400 Subject: [PATCH 2/8] chore: save split random on step start so we can restore it This allows move iterators to generate additional moves without affecting the random (and thus reach the same result when those moves are not generated provided the same number of steps). --- .../ai/timefold/solver/core/impl/solver/AbstractSolver.java | 6 ++++++ .../core/impl/solver/random/DefaultRandomFactory.java | 3 ++- .../timefold/solver/core/impl/solver/DefaultSolverTest.java | 4 ++-- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/AbstractSolver.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/AbstractSolver.java index a94d8ac841c..da91c1eb2d1 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/AbstractSolver.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/AbstractSolver.java @@ -2,6 +2,7 @@ import java.util.Iterator; import java.util.List; +import java.util.random.RandomGenerator; import ai.timefold.solver.core.api.domain.solution.PlanningSolution; import ai.timefold.solver.core.api.solver.Solver; @@ -12,6 +13,7 @@ import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; import ai.timefold.solver.core.impl.phase.scope.AbstractStepScope; import ai.timefold.solver.core.impl.solver.event.SolverEventSupport; +import ai.timefold.solver.core.impl.solver.random.DelegatingSplittableRandomGenerator; import ai.timefold.solver.core.impl.solver.recaller.BestSolutionRecaller; import ai.timefold.solver.core.impl.solver.scope.SolverScope; import ai.timefold.solver.core.impl.solver.termination.UniversalTermination; @@ -44,6 +46,8 @@ public abstract class AbstractSolver implements Solver { protected final UniversalTermination globalTermination; protected final List> phaseList; + private RandomGenerator.SplittableGenerator savedRandom; + // ************************************************************************ // Constructors and simple getters/setters // ************************************************************************ @@ -123,10 +127,12 @@ public void stepStarted(AbstractStepScope stepScope) { bestSolutionRecaller.stepStarted(stepScope); phaseLifecycleSupport.fireStepStarted(stepScope); globalTermination.stepStarted(stepScope); + savedRandom = ((DelegatingSplittableRandomGenerator) stepScope.getWorkingRandom()).split(); // Do not propagate to phases; the active phase does that for itself and they should not propagate further. } public void stepEnded(AbstractStepScope stepScope) { + ((DelegatingSplittableRandomGenerator) stepScope.getWorkingRandom()).setDelegate(savedRandom); bestSolutionRecaller.stepEnded(stepScope); phaseLifecycleSupport.fireStepEnded(stepScope); globalTermination.stepEnded(stepScope); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/random/DefaultRandomFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/random/DefaultRandomFactory.java index 66469098215..3b55f630bb8 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/random/DefaultRandomFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/random/DefaultRandomFactory.java @@ -32,7 +32,8 @@ public DefaultRandomFactory(RandomType randomType, Long randomSeed) { public RandomGenerator createRandom() { switch (randomType) { case JDK: - return randomSeed == null ? new DelegatingSplittableRandomGenerator(new Random().nextLong()) : new DelegatingSplittableRandomGenerator(randomSeed); + return randomSeed == null ? new DelegatingSplittableRandomGenerator(new Random().nextLong()) + : new DelegatingSplittableRandomGenerator(randomSeed); case MERSENNE_TWISTER: return new RandomAdaptor(randomSeed == null ? new MersenneTwister() : new MersenneTwister(randomSeed)); case WELL512A: diff --git a/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java b/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java index 3cb55c1b602..30373066432 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java @@ -1397,8 +1397,8 @@ void solveStaleBuiltinShadows() { var solution = PlannerTestUtils.solve(solverConfig, problem); - assertThat(solution.getEntityList().getFirst().getValue().getCode()).isEqualTo("v1"); - assertThat(solution.getEntityList().get(1).getValue().getCode()).isEqualTo("v2"); + assertThat(solution.getEntityList().getFirst().getValue().getCode()).isEqualTo("v2"); + assertThat(solution.getEntityList().get(1).getValue().getCode()).isEqualTo("v1"); assertThat(solution.getScore()).isEqualTo(SimpleScore.of(-2)); } From b39b4d35104115d4907f7db73291e726941a34bb Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Tue, 28 Apr 2026 10:44:35 -0400 Subject: [PATCH 3/8] chore: remove RandomFactory and RandomType --- .../core/config/solver/SolverConfig.java | 38 +----------- .../core/impl/solver/DefaultSolver.java | 11 ++-- .../impl/solver/DefaultSolverFactory.java | 34 ++++------- .../solver/random/DefaultRandomFactory.java | 61 ------------------- .../DelegatingSplittableRandomGenerator.java | 31 +++++++++- .../impl/solver/random/RandomFactory.java | 15 ----- core/src/main/java/module-info.java | 1 - core/src/main/resources/solver.xsd | 52 ++++++++-------- .../config/solver/EnvironmentModeTest.java | 23 +++---- .../impl/solver/DefaultSolverFactoryTest.java | 12 ---- .../core/impl/solver/SolverMetricsIT.java | 2 +- .../src/test/resources/solver-full.xml | 2 - .../src/main/resources/benchmark.xsd | 42 ------------- .../result/PlannerBenchmarkResultTest.java | 7 +-- 14 files changed, 88 insertions(+), 243 deletions(-) delete mode 100644 core/src/main/java/ai/timefold/solver/core/impl/solver/random/DefaultRandomFactory.java delete mode 100644 core/src/main/java/ai/timefold/solver/core/impl/solver/random/RandomFactory.java diff --git a/core/src/main/java/ai/timefold/solver/core/config/solver/SolverConfig.java b/core/src/main/java/ai/timefold/solver/core/config/solver/SolverConfig.java index b9fa907fdc9..edbd1554371 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/solver/SolverConfig.java +++ b/core/src/main/java/ai/timefold/solver/core/config/solver/SolverConfig.java @@ -41,7 +41,6 @@ import ai.timefold.solver.core.config.score.director.ScoreDirectorFactoryConfig; import ai.timefold.solver.core.config.solver.monitoring.MonitoringConfig; import ai.timefold.solver.core.config.solver.monitoring.SolverMetric; -import ai.timefold.solver.core.config.solver.random.RandomType; import ai.timefold.solver.core.config.solver.termination.TerminationConfig; import ai.timefold.solver.core.config.util.ConfigUtils; import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessor; @@ -49,7 +48,6 @@ import ai.timefold.solver.core.impl.io.jaxb.SolverConfigIO; import ai.timefold.solver.core.impl.io.jaxb.TimefoldXmlSerializationException; import ai.timefold.solver.core.impl.phase.PhaseFactory; -import ai.timefold.solver.core.impl.solver.random.RandomFactory; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; @@ -63,9 +61,7 @@ "enablePreviewFeatureSet", "environmentMode", "daemon", - "randomType", "randomSeed", - "randomFactoryClass", "moveThreadCount", "moveThreadBufferSize", "threadFactoryClass", @@ -215,9 +211,7 @@ public final class SolverConfig extends AbstractConfig { private Set enablePreviewFeatureSet = null; private EnvironmentMode environmentMode = null; private Boolean daemon = null; - private RandomType randomType = null; private Long randomSeed = null; - private Class randomFactoryClass = null; private String moveThreadCount = null; private Integer moveThreadBufferSize = null; private Class threadFactoryClass = null; @@ -332,14 +326,6 @@ public void setDaemon(@Nullable Boolean daemon) { this.daemon = daemon; } - public @Nullable RandomType getRandomType() { - return randomType; - } - - public void setRandomType(@Nullable RandomType randomType) { - this.randomType = randomType; - } - public @Nullable Long getRandomSeed() { return randomSeed; } @@ -348,14 +334,6 @@ public void setRandomSeed(@Nullable Long randomSeed) { this.randomSeed = randomSeed; } - public @Nullable Class getRandomFactoryClass() { - return randomFactoryClass; - } - - public void setRandomFactoryClass(@Nullable Class randomFactoryClass) { - this.randomFactoryClass = randomFactoryClass; - } - public @Nullable String getMoveThreadCount() { return moveThreadCount; } @@ -471,21 +449,11 @@ public void setMonitoringConfig(@Nullable MonitoringConfig monitoringConfig) { return this; } - public @NonNull SolverConfig withRandomType(@NonNull RandomType randomType) { - this.randomType = randomType; - return this; - } - public @NonNull SolverConfig withRandomSeed(@NonNull Long randomSeed) { this.randomSeed = randomSeed; return this; } - public @NonNull SolverConfig withRandomFactoryClass(@NonNull Class randomFactoryClass) { - this.randomFactoryClass = randomFactoryClass; - return this; - } - public @NonNull SolverConfig withMoveThreadCount(@NonNull String moveThreadCount) { this.moveThreadCount = moveThreadCount; return this; @@ -648,7 +616,7 @@ public boolean canTerminate() { // ************************************************************************ public void offerRandomSeedFromSubSingleIndex(long subSingleIndex) { - if ((environmentMode == null || environmentMode.isReproducible()) && randomFactoryClass == null && randomSeed == null) { + if ((environmentMode == null || environmentMode.isReproducible()) && randomSeed == null) { randomSeed = subSingleIndex; } } @@ -665,10 +633,7 @@ public void offerRandomSeedFromSubSingleIndex(long subSingleIndex) { inheritedConfig.getEnablePreviewFeatureSet()); environmentMode = ConfigUtils.inheritOverwritableProperty(environmentMode, inheritedConfig.getEnvironmentMode()); daemon = ConfigUtils.inheritOverwritableProperty(daemon, inheritedConfig.getDaemon()); - randomType = ConfigUtils.inheritOverwritableProperty(randomType, inheritedConfig.getRandomType()); randomSeed = ConfigUtils.inheritOverwritableProperty(randomSeed, inheritedConfig.getRandomSeed()); - randomFactoryClass = ConfigUtils.inheritOverwritableProperty(randomFactoryClass, - inheritedConfig.getRandomFactoryClass()); moveThreadCount = ConfigUtils.inheritOverwritableProperty(moveThreadCount, inheritedConfig.getMoveThreadCount()); moveThreadBufferSize = ConfigUtils.inheritOverwritableProperty(moveThreadBufferSize, @@ -700,7 +665,6 @@ public void offerRandomSeedFromSubSingleIndex(long subSingleIndex) { @Override public void visitReferencedClasses(@NonNull Consumer> classVisitor) { - classVisitor.accept(randomFactoryClass); classVisitor.accept(threadFactoryClass); classVisitor.accept(solutionClass); if (entityClassList != null) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolver.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolver.java index 8f9b11b94ff..83d78e46e19 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolver.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolver.java @@ -5,6 +5,8 @@ import java.util.Map; import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; +import java.util.random.RandomGenerator; import ai.timefold.solver.core.api.domain.common.PlanningId; import ai.timefold.solver.core.api.domain.solution.PlanningSolution; @@ -16,7 +18,6 @@ import ai.timefold.solver.core.impl.phase.Phase; import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; import ai.timefold.solver.core.impl.score.director.ScoreDirectorFactory; -import ai.timefold.solver.core.impl.solver.random.RandomFactory; import ai.timefold.solver.core.impl.solver.recaller.BestSolutionRecaller; import ai.timefold.solver.core.impl.solver.scope.SolverScope; import ai.timefold.solver.core.impl.solver.termination.BasicPlumbingTermination; @@ -38,7 +39,7 @@ public class DefaultSolver extends AbstractSolver { protected final EnvironmentMode environmentMode; - protected final RandomFactory randomFactory; + protected final Supplier randomFactory; protected final BasicPlumbingTermination basicPlumbingTermination; protected final AtomicBoolean solving = new AtomicBoolean(false); protected final SolverScope solverScope; @@ -48,7 +49,7 @@ public class DefaultSolver extends AbstractSolver { // Constructors and simple getters/setters // ************************************************************************ - public DefaultSolver(EnvironmentMode environmentMode, RandomFactory randomFactory, + public DefaultSolver(EnvironmentMode environmentMode, Supplier randomFactory, BestSolutionRecaller bestSolutionRecaller, BasicPlumbingTermination basicPlumbingTermination, UniversalTermination termination, List> phaseList, SolverScope solverScope, String moveThreadCountDescription) { @@ -65,7 +66,7 @@ public EnvironmentMode getEnvironmentMode() { return environmentMode; } - public RandomFactory getRandomFactory() { + public Supplier getRandomSupplier() { return randomFactory; } @@ -187,7 +188,7 @@ public void outerSolvingStarted(SolverScope solverScope) { solving.set(true); basicPlumbingTermination.resetTerminateEarly(); solverScope.setStartingSolverCount(0); - solverScope.setWorkingRandom(randomFactory.createRandom()); + solverScope.setWorkingRandom(randomFactory.get()); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverFactory.java index be9e52e5eef..dc2ab5d9481 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverFactory.java @@ -7,6 +7,8 @@ import java.util.List; import java.util.Objects; import java.util.OptionalInt; +import java.util.function.Supplier; +import java.util.random.RandomGenerator; import ai.timefold.solver.core.api.domain.solution.PlanningSolution; import ai.timefold.solver.core.api.score.Score; @@ -21,7 +23,6 @@ import ai.timefold.solver.core.config.solver.PreviewFeature; import ai.timefold.solver.core.config.solver.SolverConfig; import ai.timefold.solver.core.config.solver.monitoring.SolverMetric; -import ai.timefold.solver.core.config.solver.random.RandomType; import ai.timefold.solver.core.config.solver.termination.TerminationConfig; import ai.timefold.solver.core.config.util.ConfigUtils; import ai.timefold.solver.core.impl.AbstractFromConfigFactory; @@ -36,8 +37,7 @@ import ai.timefold.solver.core.impl.score.director.ScoreDirectorFactory; import ai.timefold.solver.core.impl.score.director.ScoreDirectorFactoryFactory; import ai.timefold.solver.core.impl.solver.change.DefaultProblemChangeDirector; -import ai.timefold.solver.core.impl.solver.random.DefaultRandomFactory; -import ai.timefold.solver.core.impl.solver.random.RandomFactory; +import ai.timefold.solver.core.impl.solver.random.DelegatingSplittableRandomGenerator; import ai.timefold.solver.core.impl.solver.recaller.BestSolutionRecaller; import ai.timefold.solver.core.impl.solver.recaller.BestSolutionRecallerFactory; import ai.timefold.solver.core.impl.solver.scope.SolverScope; @@ -133,7 +133,7 @@ public Solver buildSolver(SolverConfigOverride configOverride) { var moveThreadCount = resolveMoveThreadCount(true); var bestSolutionRecaller = BestSolutionRecallerFactory.create(). buildBestSolutionRecaller(environmentMode); - var randomFactory = buildRandomFactory(environmentMode); + var randomFactory = buildRandomSupplier(environmentMode); var previewFeaturesEnabled = solverConfig.getEnablePreviewFeatureSet(); var scoreDirectorFactoryConfig = solverConfig.getScoreDirectorFactoryConfig(); @@ -153,7 +153,7 @@ public Solver buildSolver(SolverConfigOverride configOverride) { .withMoveThreadBufferSize(solverConfig.getMoveThreadBufferSize()) .withThreadFactoryClass(solverConfig.getThreadFactoryClass()) .withNearbyDistanceMeterClass(solverConfig.getNearbyDistanceMeterClass()) - .withRandom(randomFactory.createRandom()) + .withRandom(randomFactory.get()) .withInitializingScoreTrend(scoreDirectorFactory.getInitializingScoreTrend()) .withSolutionDescriptor(solutionDescriptor) .withClassInstanceCache(ClassInstanceCache.create()) @@ -212,25 +212,15 @@ private > ScoreDirectorFactory b return scoreDirectorFactoryFactory.buildScoreDirectorFactory(environmentMode, solutionDescriptor); } - public RandomFactory buildRandomFactory(EnvironmentMode environmentMode_) { - var randomFactoryClass = solverConfig.getRandomFactoryClass(); - if (randomFactoryClass != null) { - var randomType = solverConfig.getRandomType(); - var randomSeed = solverConfig.getRandomSeed(); - if (randomType != null || randomSeed != null) { - throw new IllegalArgumentException( - "The solverConfig with randomFactoryClass (%s) has a non-null randomType (%s) or a non-null randomSeed (%s)." - .formatted(randomFactoryClass, randomType, randomSeed)); - } - return ConfigUtils.newInstance(solverConfig, "randomFactoryClass", randomFactoryClass); + public Supplier buildRandomSupplier(EnvironmentMode environmentMode_) { + var randomSeed_ = solverConfig.getRandomSeed(); + if (randomSeed_ == null && environmentMode_ != EnvironmentMode.NON_REPRODUCIBLE) { + randomSeed_ = DEFAULT_RANDOM_SEED; } else { - var randomType_ = Objects.requireNonNullElse(solverConfig.getRandomType(), RandomType.JDK); - var randomSeed_ = solverConfig.getRandomSeed(); - if (solverConfig.getRandomSeed() == null && environmentMode_ != EnvironmentMode.NON_REPRODUCIBLE) { - randomSeed_ = DEFAULT_RANDOM_SEED; - } - return new DefaultRandomFactory(randomType_, randomSeed_); + randomSeed_ = RandomGenerator.getDefault().nextLong(); } + Long finalRandomSeed_ = randomSeed_; + return () -> new DelegatingSplittableRandomGenerator(finalRandomSeed_); } public List> buildPhaseList(HeuristicConfigPolicy configPolicy, diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/random/DefaultRandomFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/random/DefaultRandomFactory.java deleted file mode 100644 index 3b55f630bb8..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/random/DefaultRandomFactory.java +++ /dev/null @@ -1,61 +0,0 @@ -package ai.timefold.solver.core.impl.solver.random; - -import java.util.Random; -import java.util.random.RandomGenerator; - -import ai.timefold.solver.core.config.solver.random.RandomType; - -import org.apache.commons.math3.random.MersenneTwister; -import org.apache.commons.math3.random.RandomAdaptor; -import org.apache.commons.math3.random.Well1024a; -import org.apache.commons.math3.random.Well19937a; -import org.apache.commons.math3.random.Well19937c; -import org.apache.commons.math3.random.Well44497a; -import org.apache.commons.math3.random.Well44497b; -import org.apache.commons.math3.random.Well512a; - -public class DefaultRandomFactory implements RandomFactory { - - protected final RandomType randomType; - protected final Long randomSeed; - - /** - * @param randomType never null - * @param randomSeed null if no seed - */ - public DefaultRandomFactory(RandomType randomType, Long randomSeed) { - this.randomType = randomType; - this.randomSeed = randomSeed; - } - - @Override - public RandomGenerator createRandom() { - switch (randomType) { - case JDK: - return randomSeed == null ? new DelegatingSplittableRandomGenerator(new Random().nextLong()) - : new DelegatingSplittableRandomGenerator(randomSeed); - case MERSENNE_TWISTER: - return new RandomAdaptor(randomSeed == null ? new MersenneTwister() : new MersenneTwister(randomSeed)); - case WELL512A: - return new RandomAdaptor(randomSeed == null ? new Well512a() : new Well512a(randomSeed)); - case WELL1024A: - return new RandomAdaptor(randomSeed == null ? new Well1024a() : new Well1024a(randomSeed)); - case WELL19937A: - return new RandomAdaptor(randomSeed == null ? new Well19937a() : new Well19937a(randomSeed)); - case WELL19937C: - return new RandomAdaptor(randomSeed == null ? new Well19937c() : new Well19937c(randomSeed)); - case WELL44497A: - return new RandomAdaptor(randomSeed == null ? new Well44497a() : new Well44497a(randomSeed)); - case WELL44497B: - return new RandomAdaptor(randomSeed == null ? new Well44497b() : new Well44497b(randomSeed)); - default: - throw new IllegalStateException("The randomType (" + randomType + ") is not implemented."); - } - } - - @Override - public String toString() { - return randomType.name() + (randomSeed == null ? "" : " with seed " + randomSeed); - } - -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/random/DelegatingSplittableRandomGenerator.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/random/DelegatingSplittableRandomGenerator.java index 9880bad0632..3b7e0de2b68 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/random/DelegatingSplittableRandomGenerator.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/random/DelegatingSplittableRandomGenerator.java @@ -4,17 +4,29 @@ import java.util.random.RandomGenerator; public final class DelegatingSplittableRandomGenerator implements RandomGenerator { - RandomGenerator.SplittableGenerator delegate; + private RandomGenerator.SplittableGenerator delegate; + private final Thread ownerThread; - DelegatingSplittableRandomGenerator(long seed) { + public DelegatingSplittableRandomGenerator(long seed) { this.delegate = new SplittableRandom(seed); + this.ownerThread = Thread.currentThread(); + } + + private void assertIsOwnedByCurrentThread() { + if (Thread.currentThread() != ownerThread) { + throw new IllegalStateException( + "The calling thread (%s) is not the owner thread (%s). Maybe create your own RandomGenerator instance?" + .formatted(Thread.currentThread(), ownerThread)); + } } public RandomGenerator.SplittableGenerator split() { + assertIsOwnedByCurrentThread(); return delegate.split(); } public void setDelegate(RandomGenerator.SplittableGenerator delegate) { + assertIsOwnedByCurrentThread(); this.delegate = delegate; } @@ -24,76 +36,91 @@ public void setDelegate(RandomGenerator.SplittableGenerator delegate) { @Override public long nextLong() { + assertIsOwnedByCurrentThread(); return delegate.nextLong(); } @Override public int nextInt() { + assertIsOwnedByCurrentThread(); return delegate.nextInt(); } @Override public int nextInt(int bound) { + assertIsOwnedByCurrentThread(); return delegate.nextInt(bound); } @Override public int nextInt(int origin, int bound) { + assertIsOwnedByCurrentThread(); return delegate.nextInt(origin, bound); } @Override public long nextLong(long bound) { + assertIsOwnedByCurrentThread(); return delegate.nextLong(bound); } @Override public long nextLong(long origin, long bound) { + assertIsOwnedByCurrentThread(); return delegate.nextLong(origin, bound); } @Override public double nextDouble() { + assertIsOwnedByCurrentThread(); return delegate.nextDouble(); } @Override public double nextDouble(double bound) { + assertIsOwnedByCurrentThread(); return delegate.nextDouble(bound); } @Override public double nextDouble(double origin, double bound) { + assertIsOwnedByCurrentThread(); return delegate.nextDouble(origin, bound); } @Override public float nextFloat() { + assertIsOwnedByCurrentThread(); return delegate.nextFloat(); } @Override public float nextFloat(float bound) { + assertIsOwnedByCurrentThread(); return delegate.nextFloat(bound); } @Override public float nextFloat(float origin, float bound) { + assertIsOwnedByCurrentThread(); return delegate.nextFloat(origin, bound); } @Override public double nextGaussian() { + assertIsOwnedByCurrentThread(); return delegate.nextGaussian(); } @Override public boolean nextBoolean() { + assertIsOwnedByCurrentThread(); return delegate.nextBoolean(); } @Override public void nextBytes(byte[] bytes) { + assertIsOwnedByCurrentThread(); delegate.nextBytes(bytes); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/random/RandomFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/random/RandomFactory.java deleted file mode 100644 index cbe8c4ef724..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/random/RandomFactory.java +++ /dev/null @@ -1,15 +0,0 @@ -package ai.timefold.solver.core.impl.solver.random; - -import java.util.random.RandomGenerator; - -/** - * @see DefaultRandomFactory - */ -public interface RandomFactory { - - /** - * @return never null - */ - RandomGenerator createRandom(); - -} diff --git a/core/src/main/java/module-info.java b/core/src/main/java/module-info.java index e6fac5a290f..ed35977a9ca 100644 --- a/core/src/main/java/module-info.java +++ b/core/src/main/java/module-info.java @@ -202,7 +202,6 @@ exports ai.timefold.solver.core.impl.neighborhood to ai.timefold.solver.enterprise.core; exports ai.timefold.solver.core.impl.partitionedsearch to ai.timefold.solver.enterprise.core; exports ai.timefold.solver.core.impl.phase to ai.timefold.solver.enterprise.core; - exports ai.timefold.solver.core.impl.solver.random to ai.timefold.solver.enterprise.core; exports ai.timefold.solver.core.impl.solver.recaller to ai.timefold.solver.enterprise.core; exports ai.timefold.solver.core.impl.solver.event to ai.timefold.solver.enterprise.core; diff --git a/core/src/main/resources/solver.xsd b/core/src/main/resources/solver.xsd index 8e4c489f223..0699ee0b12c 100644 --- a/core/src/main/resources/solver.xsd +++ b/core/src/main/resources/solver.xsd @@ -17,12 +17,8 @@ - - - - @@ -1471,30 +1467,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - @@ -1832,5 +1804,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/src/test/java/ai/timefold/solver/core/config/solver/EnvironmentModeTest.java b/core/src/test/java/ai/timefold/solver/core/config/solver/EnvironmentModeTest.java index fd9d131c42f..5917943769c 100644 --- a/core/src/test/java/ai/timefold/solver/core/config/solver/EnvironmentModeTest.java +++ b/core/src/test/java/ai/timefold/solver/core/config/solver/EnvironmentModeTest.java @@ -8,6 +8,8 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.function.Supplier; +import java.util.random.RandomGenerator; import java.util.stream.IntStream; import ai.timefold.solver.core.api.score.SimpleScore; @@ -29,7 +31,6 @@ import ai.timefold.solver.core.impl.phase.event.PhaseLifecycleListenerAdapter; import ai.timefold.solver.core.impl.phase.scope.AbstractStepScope; import ai.timefold.solver.core.impl.solver.DefaultSolver; -import ai.timefold.solver.core.impl.solver.random.RandomFactory; import ai.timefold.solver.core.preview.api.move.builtin.Moves; import ai.timefold.solver.core.testdomain.TestdataEntity; import ai.timefold.solver.core.testdomain.TestdataSolution; @@ -186,14 +187,14 @@ void corruptedConstraints(EnvironmentMode environmentMode) { } private void assertReproducibility(Solver solver1, Solver solver2) { - assertGeneratingSameNumbers(((DefaultSolver) solver1).getRandomFactory(), - ((DefaultSolver) solver2).getRandomFactory()); + assertGeneratingSameNumbers(((DefaultSolver) solver1).getRandomSupplier(), + ((DefaultSolver) solver2).getRandomSupplier()); assertSameScoreSeries(solver1, solver2); } private void assertNonReproducibility(Solver solver1, Solver solver2) { - assertGeneratingDifferentNumbers(((DefaultSolver) solver1).getRandomFactory(), - ((DefaultSolver) solver2).getRandomFactory()); + assertGeneratingDifferentNumbers(((DefaultSolver) solver1).getRandomSupplier(), + ((DefaultSolver) solver2).getRandomSupplier()); assertDifferentScoreSeries(solver1, solver2); } @@ -241,9 +242,9 @@ private void assertDifferentScoreSeries(Solver solver1, Solver })); } - private void assertGeneratingSameNumbers(RandomFactory factory1, RandomFactory factory2) { - var random = factory1.createRandom(); - var random2 = factory2.createRandom(); + private void assertGeneratingSameNumbers(Supplier factory1, Supplier factory2) { + var random = factory1.get(); + var random2 = factory2.get(); assertSoftly(softly -> IntStream.range(0, NUMBER_OF_RANDOM_NUMBERS_GENERATED) .forEach(i -> softly.assertThat(random.nextInt()) @@ -252,9 +253,9 @@ private void assertGeneratingSameNumbers(RandomFactory factory1, RandomFactory f .isEqualTo(random2.nextInt()))); } - private void assertGeneratingDifferentNumbers(RandomFactory factory1, RandomFactory factory2) { - var random = factory1.createRandom(); - var random2 = factory2.createRandom(); + private void assertGeneratingDifferentNumbers(Supplier factory1, Supplier factory2) { + var random = factory1.get(); + var random2 = factory2.get(); assertSoftly(softly -> IntStream.range(0, NUMBER_OF_RANDOM_NUMBERS_GENERATED) .forEach(i -> softly.assertThat(random.nextInt()) diff --git a/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverFactoryTest.java b/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverFactoryTest.java index 2a48d6b1d96..a84f5b67bdc 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverFactoryTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverFactoryTest.java @@ -10,7 +10,6 @@ import ai.timefold.solver.core.config.solver.SolverConfig; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; import ai.timefold.solver.core.impl.score.director.ScoreDirectorFactory; -import ai.timefold.solver.core.impl.solver.random.RandomFactory; import ai.timefold.solver.core.testdomain.TestdataConstraintProvider; import ai.timefold.solver.core.testdomain.TestdataEntity; import ai.timefold.solver.core.testdomain.TestdataSolution; @@ -121,17 +120,6 @@ void testNoEntityConfiguration() { "If you're using the Quarkus extension or Spring Boot starter, it should have been filled in already."); } - @Test - void testInvalidRandomConfiguration() { - SolverConfig solverConfig = - SolverConfig.createFromXmlResource("ai/timefold/solver/core/config/solver/testdataSolverConfig.xml") - .withRandomFactoryClass(RandomFactory.class) - .withRandomSeed(1000L); - assertThatCode(() -> new DefaultSolverFactory<>(solverConfig).buildSolver(new SolverConfigOverride())) - .hasMessageContaining("The solverConfig with randomFactoryClass ") - .hasMessageContaining("has a non-null randomType (null) or a non-null randomSeed (1000)."); - } - @Test void testInvalidMoveThreadCountConfiguration() { SolverConfig solverConfig = diff --git a/core/src/test/java/ai/timefold/solver/core/impl/solver/SolverMetricsIT.java b/core/src/test/java/ai/timefold/solver/core/impl/solver/SolverMetricsIT.java index 7aa40f7d9b5..cd8203b55c6 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/solver/SolverMetricsIT.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/solver/SolverMetricsIT.java @@ -836,7 +836,7 @@ void lsWithListVariableAndCustomMetrics() { var solverConfig = PlannerTestUtils .buildSolverConfig(TestdataListSolution.class, TestdataListEntity.class, TestdataListValue.class); var phaseConfig = new LocalSearchPhaseConfig(); - phaseConfig.setTerminationConfig(new TerminationConfig().withScoreCalculationCountLimit(10L)); + phaseConfig.setTerminationConfig(new TerminationConfig().withScoreCalculationCountLimit(20L)); solverConfig.withPhases(phaseConfig) .withMonitoringConfig(new MonitoringConfig().withSolverMetricList(List.of(SolverMetric.MOVE_COUNT_PER_TYPE))); diff --git a/spring-integration/spring-boot-integration-test/src/test/resources/solver-full.xml b/spring-integration/spring-boot-integration-test/src/test/resources/solver-full.xml index 136ebe5aeed..66be4df42be 100644 --- a/spring-integration/spring-boot-integration-test/src/test/resources/solver-full.xml +++ b/spring-integration/spring-boot-integration-test/src/test/resources/solver-full.xml @@ -1,9 +1,7 @@ TRACKED_FULL_ASSERT false - WELL1024A 10 - java.lang.Object 1 3 java.lang.Object diff --git a/tools/benchmark/src/main/resources/benchmark.xsd b/tools/benchmark/src/main/resources/benchmark.xsd index 0f9e9f1f713..87ef47a9121 100644 --- a/tools/benchmark/src/main/resources/benchmark.xsd +++ b/tools/benchmark/src/main/resources/benchmark.xsd @@ -326,15 +326,9 @@ - - - - - - @@ -2468,42 +2462,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tools/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/result/PlannerBenchmarkResultTest.java b/tools/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/result/PlannerBenchmarkResultTest.java index cd996f5c971..fd35acdbc83 100644 --- a/tools/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/result/PlannerBenchmarkResultTest.java +++ b/tools/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/result/PlannerBenchmarkResultTest.java @@ -15,7 +15,6 @@ import ai.timefold.solver.core.api.score.SimpleScore; import ai.timefold.solver.core.api.score.calculator.EasyScoreCalculator; import ai.timefold.solver.core.config.solver.SolverConfig; -import ai.timefold.solver.core.config.solver.random.RandomType; import ai.timefold.solver.core.impl.heuristic.selector.common.nearby.NearbyDistanceMeter; import ai.timefold.solver.core.testdomain.TestdataEntity; import ai.timefold.solver.core.testdomain.TestdataSolution; @@ -37,19 +36,19 @@ void createMergedResult() { var p1SolverX = new SolverBenchmarkResult(p1); p1SolverX.setName("Solver X"); var p1SolverConfigX = new SolverConfig(); - p1SolverConfigX.setRandomType(RandomType.JDK); + p1SolverConfigX.setRandomSeed(0L); p1SolverX.setSolverConfig(p1SolverConfigX); p1SolverX.setSingleBenchmarkResultList(new ArrayList<>()); var p1SolverY = new SolverBenchmarkResult(p1); p1SolverY.setName("Solver Y"); var p1SolverConfigY = new SolverConfig(); - p1SolverConfigY.setRandomType(RandomType.MERSENNE_TWISTER); + p1SolverConfigY.setRandomSeed(1L); p1SolverY.setSolverConfig(p1SolverConfigY); p1SolverY.setSingleBenchmarkResultList(new ArrayList<>()); var p2SolverZ = new SolverBenchmarkResult(p2); p2SolverZ.setName("Solver Z"); var p2SolverConfigZ = new SolverConfig(); - p2SolverConfigZ.setRandomType(RandomType.WELL1024A); + p2SolverConfigZ.setRandomSeed(2L); p2SolverZ.setSolverConfig(p2SolverConfigZ); p2SolverZ.setSingleBenchmarkResultList(new ArrayList<>()); From 02649a3766044582227aa2e26b7621efda090778 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Tue, 28 Apr 2026 11:08:15 -0400 Subject: [PATCH 4/8] chore: export solver.random to enterprise --- .../timefold/solver/core/impl/solver/scope/SolverScope.java | 5 ++--- core/src/main/java/module-info.java | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java index 0f008e6f405..9d37a543bc8 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java @@ -7,7 +7,6 @@ import java.util.EnumSet; import java.util.Map; import java.util.Objects; -import java.util.Random; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Semaphore; @@ -28,6 +27,7 @@ import ai.timefold.solver.core.impl.solver.AbstractSolver; import ai.timefold.solver.core.impl.solver.change.DefaultProblemChangeDirector; import ai.timefold.solver.core.impl.solver.monitoring.ScoreLevels; +import ai.timefold.solver.core.impl.solver.random.DelegatingSplittableRandomGenerator; import ai.timefold.solver.core.impl.solver.termination.PhaseTermination; import ai.timefold.solver.core.impl.solver.thread.ChildThreadType; @@ -353,9 +353,8 @@ public SolverScope createChildThreadSolverScope(ChildThreadType child childThreadSolverScope.monitoringTags = monitoringTags; childThreadSolverScope.solverMetricSet = solverMetricSet; childThreadSolverScope.startingSolverCount = startingSolverCount; - // TODO FIXME use RandomFactory // Experiments show that this trick to attain reproducibility doesn't break uniform distribution - childThreadSolverScope.workingRandom = new Random(workingRandom.nextLong()); + childThreadSolverScope.workingRandom = new DelegatingSplittableRandomGenerator(workingRandom.nextLong()); childThreadSolverScope.scoreDirector = scoreDirector.createChildThreadScoreDirector(childThreadType); childThreadSolverScope.startingSystemTimeMillis.set(startingSystemTimeMillis.get()); resetAtomicLongTimeMillis(childThreadSolverScope.endingSystemTimeMillis); diff --git a/core/src/main/java/module-info.java b/core/src/main/java/module-info.java index ed35977a9ca..e6fac5a290f 100644 --- a/core/src/main/java/module-info.java +++ b/core/src/main/java/module-info.java @@ -202,6 +202,7 @@ exports ai.timefold.solver.core.impl.neighborhood to ai.timefold.solver.enterprise.core; exports ai.timefold.solver.core.impl.partitionedsearch to ai.timefold.solver.enterprise.core; exports ai.timefold.solver.core.impl.phase to ai.timefold.solver.enterprise.core; + exports ai.timefold.solver.core.impl.solver.random to ai.timefold.solver.enterprise.core; exports ai.timefold.solver.core.impl.solver.recaller to ai.timefold.solver.enterprise.core; exports ai.timefold.solver.core.impl.solver.event to ai.timefold.solver.enterprise.core; From ddf7271c56c544875186243c58515497c2727236 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Tue, 28 Apr 2026 14:08:15 -0400 Subject: [PATCH 5/8] chore: log "effective move evaluation speed" when multithreaded solving is used --- .../DefaultConstructionHeuristicPhase.java | 13 +++++--- .../DefaultExhaustiveSearchPhase.java | 5 +-- .../localsearch/DefaultLocalSearchPhase.java | 31 ++++++++++++------- 3 files changed, 31 insertions(+), 18 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhase.java b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhase.java index 4695f0bde28..cae734d261f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhase.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhase.java @@ -104,9 +104,9 @@ public void solve(SolverScope solverScope) { stepScope.getPhaseScope().calculateSolverTimeMillisSpentUpToNow()); } } else { - throw new IllegalStateException("The step index (" + stepScope.getStepIndex() - + ") has selected move count (" + stepScope.getSelectedMoveCount() - + ") but failed to pick a nextStep (" + stepScope.getStep() + ")."); + throw new IllegalStateException( + "The step index (%d) has selected move count (%d) but failed to pick a nextStep (%s).".formatted( + stepScope.getStepIndex(), stepScope.getSelectedMoveCount(), stepScope.getStep())); } // Although stepStarted has been called, stepEnded is not called for this step. earlyTerminationStatus = TerminationStatus.early(phaseScope.getNextStepIndex()); @@ -190,11 +190,16 @@ public void phaseEnded(ConstructionHeuristicPhaseScope phaseScope) { phaseScope.endingNow(); if (decider.isLoggingEnabled() && logger.isInfoEnabled()) { logger.info( - "{}Construction Heuristic phase ({}) ended: time spent ({}), best score ({}), move evaluation speed ({}/sec), step total ({}).", + """ + {}Construction Heuristic phase ({}) ended: time spent ({}), best score ({}), \ + {}move evaluation speed ({}/sec), step total ({}).""", logIndentation, phaseIndex, phaseScope.calculateSolverTimeMillisSpentUpToNow(), phaseScope.getBestScore().raw(), + // Multithreaded solving uses "effective" move evaluation speed, since not all evaluated moves + // are foraged + (decider.getClass().equals(ConstructionHeuristicDecider.class)) ? "" : "effective ", phaseScope.getPhaseMoveEvaluationSpeed(), phaseScope.getNextStepIndex()); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/DefaultExhaustiveSearchPhase.java b/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/DefaultExhaustiveSearchPhase.java index e59d229a952..419903bcdbf 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/DefaultExhaustiveSearchPhase.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/DefaultExhaustiveSearchPhase.java @@ -100,8 +100,9 @@ private void phaseEnded(ExhaustiveSearchPhaseScope phaseScope) { super.phaseEnded(phaseScope); decider.phaseEnded(phaseScope); phaseScope.endingNow(); - logger.info("{}Exhaustive Search phase ({}) ended: time spent ({}), best score ({})," - + " move evaluation speed ({}/sec), step total ({}).", + logger.info(""" + {}Exhaustive Search phase ({}) ended: time spent ({}), best score ({}),\ + move evaluation speed ({}/sec), step total ({}).""", logIndentation, phaseIndex, phaseScope.calculateSolverTimeMillisSpentUpToNow(), diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java index b0ca9722402..c3bfe0dbd95 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java @@ -94,16 +94,17 @@ public void solve(SolverScope solverScope) { stepScope.getStepIndex(), stepScope.getPhaseScope().calculateSolverTimeMillisSpentUpToNow()); } else if (stepScope.getSelectedMoveCount() == 0L) { - logger.warn("{} No doable selected move at step index ({}), time spent ({})." - + " Terminating phase early.", + logger.warn(""" + {} No doable selected move at step index ({}), time spent ({}). \ + Terminating phase early.""", logIndentation, stepScope.getStepIndex(), stepScope.getPhaseScope().calculateSolverTimeMillisSpentUpToNow()); } else { - throw new IllegalStateException("The step index (" + stepScope.getStepIndex() - + ") has accepted/selected move count (" + stepScope.getAcceptedMoveCount() + "/" - + stepScope.getSelectedMoveCount() - + ") but failed to pick a nextStep (" + stepScope.getStep() + ")."); + throw new IllegalStateException( + "The step index (%d) has accepted/selected move count (%d/%d) but failed to pick a nextStep (%s)." + .formatted(stepScope.getStepIndex(), stepScope.getAcceptedMoveCount(), + stepScope.getSelectedMoveCount(), stepScope.getStep())); } // Although stepStarted has been called, stepEnded is not called for this step break; @@ -151,8 +152,9 @@ public void stepEnded(LocalSearchStepScope stepScope) { if (logger.isDebugEnabled()) { if (stepScope.getAcceptedMoveCount() == 0 && phaseTermination.isPhaseTerminated(phaseScope)) { // Terminated early - logger.debug("{} LS step ({}), time spent ({}), score ({}), {} best score ({})," + - " terminated prematurely after selecting {} moves.", + logger.debug(""" + {} LS step ({}), time spent ({}), score ({}), {} best score ({}), \ + terminated prematurely after selecting {} moves.""", logIndentation, stepScope.getStepIndex(), phaseScope.calculateSolverTimeMillisSpentUpToNow(), @@ -160,8 +162,9 @@ public void stepEnded(LocalSearchStepScope stepScope) { (stepScope.getBestScoreImproved() ? "new" : " "), phaseScope.getBestScore().raw(), stepScope.getSelectedMoveCount()); } else { - logger.debug("{} LS step ({}), time spent ({}), score ({}), {} best score ({})," + - " accepted/selected move count ({}/{}), picked move ({}).", + logger.debug(""" + {} LS step ({}), time spent ({}), score ({}), {} best score ({}), \ + accepted/selected move count ({}/{}), picked move ({}).""", logIndentation, stepScope.getStepIndex(), phaseScope.calculateSolverTimeMillisSpentUpToNow(), @@ -224,12 +227,16 @@ public void phaseEnded(LocalSearchPhaseScope phaseScope) { super.phaseEnded(phaseScope); decider.phaseEnded(phaseScope); phaseScope.endingNow(); - logger.info("{}Local Search phase ({}) ended: time spent ({}), best score ({})," - + " move evaluation speed ({}/sec), step total ({}).", + logger.info(""" + {}Local Search phase ({}) ended: time spent ({}), best score ({}), \ + {}move evaluation speed ({}/sec), step total ({}).""", logIndentation, phaseIndex, phaseScope.calculateSolverTimeMillisSpentUpToNow(), phaseScope.getBestScore().raw(), + // Multithreaded solving uses "effective" move evaluation speed, since not all evaluated moves + // are foraged + (decider.getClass().equals(LocalSearchDecider.class)) ? "" : "effective ", phaseScope.getPhaseMoveEvaluationSpeed(), phaseScope.getNextStepIndex()); } From 379c639997552eef761039eaeddc86ca7b27e4c9 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Wed, 29 Apr 2026 11:56:00 -0400 Subject: [PATCH 6/8] chore: review comments --- .../core/impl/solver/AbstractSolver.java | 14 ++++- .../core/impl/solver/DefaultSolver.java | 4 +- .../impl/solver/DefaultSolverFactory.java | 3 +- .../DelegatingSplittableRandomGenerator.java | 63 +++++++++++++++++++ .../core/impl/solver/scope/SolverScope.java | 4 +- .../config/solver/EnvironmentModeTest.java | 26 +++----- .../core/impl/solver/DefaultSolverTest.java | 11 ++-- .../running-the-solver.adoc | 24 +------ 8 files changed, 98 insertions(+), 51 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/AbstractSolver.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/AbstractSolver.java index da91c1eb2d1..d2b1b96afc3 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/AbstractSolver.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/AbstractSolver.java @@ -2,6 +2,7 @@ import java.util.Iterator; import java.util.List; +import java.util.Objects; import java.util.random.RandomGenerator; import ai.timefold.solver.core.api.domain.solution.PlanningSolution; @@ -19,6 +20,7 @@ import ai.timefold.solver.core.impl.solver.termination.UniversalTermination; import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -46,7 +48,7 @@ public abstract class AbstractSolver implements Solver { protected final UniversalTermination globalTermination; protected final List> phaseList; - private RandomGenerator.SplittableGenerator savedRandom; + private RandomGenerator.@Nullable SplittableGenerator savedRandom; // ************************************************************************ // Constructors and simple getters/setters @@ -127,12 +129,18 @@ public void stepStarted(AbstractStepScope stepScope) { bestSolutionRecaller.stepStarted(stepScope); phaseLifecycleSupport.fireStepStarted(stepScope); globalTermination.stepStarted(stepScope); - savedRandom = ((DelegatingSplittableRandomGenerator) stepScope.getWorkingRandom()).split(); + // To ensure reproducibility even when the number of random calls is not deterministic, + // split the random at step start. + var delegatingRandom = ((DelegatingSplittableRandomGenerator) stepScope.getWorkingRandom()); + savedRandom = delegatingRandom.getDelegate(); + delegatingRandom.setDelegate(delegatingRandom.split()); // Do not propagate to phases; the active phase does that for itself and they should not propagate further. } public void stepEnded(AbstractStepScope stepScope) { - ((DelegatingSplittableRandomGenerator) stepScope.getWorkingRandom()).setDelegate(savedRandom); + // Restore from the split random + var delegatingRandom = ((DelegatingSplittableRandomGenerator) stepScope.getWorkingRandom()); + delegatingRandom.setDelegate(Objects.requireNonNull(savedRandom)); bestSolutionRecaller.stepEnded(stepScope); phaseLifecycleSupport.fireStepEnded(stepScope); globalTermination.stepEnded(stepScope); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolver.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolver.java index 83d78e46e19..8541e0e6c01 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolver.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolver.java @@ -66,8 +66,8 @@ public EnvironmentMode getEnvironmentMode() { return environmentMode; } - public Supplier getRandomSupplier() { - return randomFactory; + public RandomGenerator getRandomGenerator() { + return randomFactory.get(); } public ScoreDirectorFactory getScoreDirectorFactory() { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverFactory.java index dc2ab5d9481..5dbb662e5e4 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverFactory.java @@ -219,8 +219,7 @@ public Supplier buildRandomSupplier(EnvironmentMode environment } else { randomSeed_ = RandomGenerator.getDefault().nextLong(); } - Long finalRandomSeed_ = randomSeed_; - return () -> new DelegatingSplittableRandomGenerator(finalRandomSeed_); + return DelegatingSplittableRandomGenerator.getSupplier(randomSeed_); } public List> buildPhaseList(HeuristicConfigPolicy configPolicy, diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/random/DelegatingSplittableRandomGenerator.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/random/DelegatingSplittableRandomGenerator.java index 3b7e0de2b68..cbd893fd227 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/random/DelegatingSplittableRandomGenerator.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/random/DelegatingSplittableRandomGenerator.java @@ -1,15 +1,39 @@ package ai.timefold.solver.core.impl.solver.random; import java.util.SplittableRandom; +import java.util.function.Supplier; import java.util.random.RandomGenerator; +import ai.timefold.solver.core.impl.heuristic.selector.move.MoveSelector; +import ai.timefold.solver.core.impl.solver.AbstractSolver; + +import org.jspecify.annotations.NullMarked; + +/** + * A {@link RandomGenerator} that delegates to another {@link RandomGenerator.SplittableGenerator} + * instance. This allows us to change the {@link RandomGenerator} used even when + * {@link MoveSelector} and other classes to cache the {@link RandomGenerator} in a field. + * + * @apiNote To ensure reproducibility, this class can only be used by the {@link Thread} + * that created it. Attempting to call any method from another thread will + * throw an {@link IllegalStateException}. + */ +@NullMarked public final class DelegatingSplittableRandomGenerator implements RandomGenerator { private RandomGenerator.SplittableGenerator delegate; private final Thread ownerThread; + private final long seed; public DelegatingSplittableRandomGenerator(long seed) { this.delegate = new SplittableRandom(seed); this.ownerThread = Thread.currentThread(); + this.seed = seed; + } + + public DelegatingSplittableRandomGenerator(long seed, RandomGenerator.SplittableGenerator delegate) { + this.delegate = delegate; + this.ownerThread = Thread.currentThread(); + this.seed = seed; } private void assertIsOwnedByCurrentThread() { @@ -25,6 +49,15 @@ public RandomGenerator.SplittableGenerator split() { return delegate.split(); } + public long getSeed() { + return seed; + } + + public RandomGenerator.SplittableGenerator getDelegate() { + assertIsOwnedByCurrentThread(); + return delegate; + } + public void setDelegate(RandomGenerator.SplittableGenerator delegate) { assertIsOwnedByCurrentThread(); this.delegate = delegate; @@ -123,4 +156,34 @@ public void nextBytes(byte[] bytes) { assertIsOwnedByCurrentThread(); delegate.nextBytes(bytes); } + + /** + * Return a supplier of {@link DelegatingSplittableRandomGenerator} with the specified + * seed with the seed included in {@link Object#toString()}. + *

+ * Required so the {@link RandomGenerator} is created in the thread {@link AbstractSolver#solve} + * is called. + * + * @param seed The seed to use for the {@link RandomGenerator}. + * @return A supplier include the seed in its {@link Object#toString()} + */ + public static Supplier getSupplier(long seed) { + return new Supplier<>() { + @Override + public RandomGenerator get() { + return new DelegatingSplittableRandomGenerator(seed); + } + + @Override + public String toString() { + return "seed %d".formatted(seed); + } + }; + } + + @Override + public String toString() { + return "%s (%s) with seed %d".formatted(delegate.getClass().getSimpleName(), + delegate, seed); + } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java index 9d37a543bc8..096557360de 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java @@ -354,7 +354,9 @@ public SolverScope createChildThreadSolverScope(ChildThreadType child childThreadSolverScope.solverMetricSet = solverMetricSet; childThreadSolverScope.startingSolverCount = startingSolverCount; // Experiments show that this trick to attain reproducibility doesn't break uniform distribution - childThreadSolverScope.workingRandom = new DelegatingSplittableRandomGenerator(workingRandom.nextLong()); + var delegatingRandom = (DelegatingSplittableRandomGenerator) workingRandom; + childThreadSolverScope.workingRandom = + new DelegatingSplittableRandomGenerator(delegatingRandom.getSeed(), delegatingRandom.split()); childThreadSolverScope.scoreDirector = scoreDirector.createChildThreadScoreDirector(childThreadType); childThreadSolverScope.startingSystemTimeMillis.set(startingSystemTimeMillis.get()); resetAtomicLongTimeMillis(childThreadSolverScope.endingSystemTimeMillis); diff --git a/core/src/test/java/ai/timefold/solver/core/config/solver/EnvironmentModeTest.java b/core/src/test/java/ai/timefold/solver/core/config/solver/EnvironmentModeTest.java index 5917943769c..d22a080eae4 100644 --- a/core/src/test/java/ai/timefold/solver/core/config/solver/EnvironmentModeTest.java +++ b/core/src/test/java/ai/timefold/solver/core/config/solver/EnvironmentModeTest.java @@ -8,7 +8,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.function.Supplier; import java.util.random.RandomGenerator; import java.util.stream.IntStream; @@ -187,14 +186,14 @@ void corruptedConstraints(EnvironmentMode environmentMode) { } private void assertReproducibility(Solver solver1, Solver solver2) { - assertGeneratingSameNumbers(((DefaultSolver) solver1).getRandomSupplier(), - ((DefaultSolver) solver2).getRandomSupplier()); + assertGeneratingSameNumbers(((DefaultSolver) solver1).getRandomGenerator(), + ((DefaultSolver) solver2).getRandomGenerator()); assertSameScoreSeries(solver1, solver2); } private void assertNonReproducibility(Solver solver1, Solver solver2) { - assertGeneratingDifferentNumbers(((DefaultSolver) solver1).getRandomSupplier(), - ((DefaultSolver) solver2).getRandomSupplier()); + assertGeneratingDifferentNumbers(((DefaultSolver) solver1).getRandomGenerator(), + ((DefaultSolver) solver2).getRandomGenerator()); assertDifferentScoreSeries(solver1, solver2); } @@ -242,10 +241,7 @@ private void assertDifferentScoreSeries(Solver solver1, Solver })); } - private void assertGeneratingSameNumbers(Supplier factory1, Supplier factory2) { - var random = factory1.get(); - var random2 = factory2.get(); - + private void assertGeneratingSameNumbers(RandomGenerator random, RandomGenerator random2) { assertSoftly(softly -> IntStream.range(0, NUMBER_OF_RANDOM_NUMBERS_GENERATED) .forEach(i -> softly.assertThat(random.nextInt()) .as("Random factories should generate the same results " @@ -253,15 +249,13 @@ private void assertGeneratingSameNumbers(Supplier factory1, Sup .isEqualTo(random2.nextInt()))); } - private void assertGeneratingDifferentNumbers(Supplier factory1, Supplier factory2) { - var random = factory1.get(); - var random2 = factory2.get(); - + private void assertGeneratingDifferentNumbers(RandomGenerator random, RandomGenerator random2) { assertSoftly(softly -> IntStream.range(0, NUMBER_OF_RANDOM_NUMBERS_GENERATED) .forEach(i -> softly.assertThat(random.nextInt()) - .as("Random factories should not generate exactly the same results " - + "in the non-reproducible environment mode. " - + "It can happen but the probability is very low. Run test again") + .as(""" + Random factories should not generate exactly the same results \ + in the non-reproducible environment mode. \ + It can happen but the probability is very low. Run test again""") .isNotEqualTo(random2.nextInt()))); } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java b/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java index 30373066432..dad0aa1a369 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java @@ -1397,8 +1397,9 @@ void solveStaleBuiltinShadows() { var solution = PlannerTestUtils.solve(solverConfig, problem); - assertThat(solution.getEntityList().getFirst().getValue().getCode()).isEqualTo("v2"); - assertThat(solution.getEntityList().get(1).getValue().getCode()).isEqualTo("v1"); + assertThat(solution.getEntityList()) + .map(entity -> entity.getValue().getCode()) + .containsExactlyInAnyOrder("v1", "v2"); assertThat(solution.getScore()).isEqualTo(SimpleScore.of(-2)); } @@ -1438,9 +1439,9 @@ void solveStaleDeclarativeShadows() { var solution = PlannerTestUtils.solve(solverConfig, problem); - assertThat(solution.getEntities().getFirst().getValues()).map(TestdataConcurrentValue::getId).containsExactly("a1", - "b1"); - assertThat(solution.getEntities().get(1).getValues()).map(TestdataConcurrentValue::getId).containsExactly("a2", "b2"); + assertThat(solution.getEntities().getFirst().getValues()).map(TestdataConcurrentValue::getId).containsExactly("b1", + "a2"); + assertThat(solution.getEntities().get(1).getValues()).map(TestdataConcurrentValue::getId).containsExactly("b2", "a1"); assertThat(solution.getScore()).isEqualTo(HardSoftScore.of(0, -240)); } diff --git a/docs/src/modules/ROOT/pages/using-timefold-solver/running-the-solver.adoc b/docs/src/modules/ROOT/pages/using-timefold-solver/running-the-solver.adoc index 80d2f0738f4..318a71d6e27 100644 --- a/docs/src/modules/ROOT/pages/using-timefold-solver/running-the-solver.adoc +++ b/docs/src/modules/ROOT/pages/using-timefold-solver/running-the-solver.adoc @@ -913,9 +913,9 @@ there are `timefold.solver.move.type.step.score.diff.hard.score` and `timefold.s [#randomNumberGenerator] == Random number generator -Many heuristics and metaheuristics depend on a pseudorandom number generator for move selection, to resolve score ties, probability based move acceptance, ... During solving, the same `Random` instance is reused to improve reproducibility, performance and uniform distribution of random values. +Many heuristics and metaheuristics depend on a pseudorandom number generator for move selection, to resolve score ties, probability based move acceptance, ... During solving, the same `RandomGenerator` instance is reused to improve reproducibility, performance and uniform distribution of random values. -To change the random seed of that `Random` instance, specify a ``randomSeed``: +To change the random seed of that `RandomGenerator` instance, specify a ``randomSeed``: [source,xml,options="nowrap"] ---- @@ -926,26 +926,6 @@ To change the random seed of that `Random` instance, specify a ``randomSeed``: ---- -To change the pseudorandom number generator implementation, specify a ``randomType``: - -[source,xml,options="nowrap"] ----- - - MERSENNE_TWISTER - ... - ----- - -The following types are supported: - -* `JDK` (default): Standard implementation (``java.util.Random``). -* ``MERSENNE_TWISTER``: Implementation by http://commons.apache.org/proper/commons-math/userguide/random.html[Commons Math]. -* ``WELL512A``, ``WELL1024A``, ``WELL19937A``, ``WELL19937C``, `WELL44497A` and ``WELL44497B``: Implementation by http://commons.apache.org/proper/commons-math/userguide/random.html[Commons Math]. - -For most use cases, the randomType has no significant impact on the average quality of the best solution on multiple datasets. -If you want to confirm this on your use case, use the xref:using-timefold-solver/benchmarking-and-tweaking.adoc#benchmarker[benchmarker]. - [#solverManager] == `SolverManager` From f23c69611ec501d53dafa7841d03f2c1afb38140 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Wed, 29 Apr 2026 12:04:09 -0400 Subject: [PATCH 7/8] chore: remove RandomType enum --- .../core/config/solver/random/RandomType.java | 23 ------------------ .../config/solver/random/package-info.java | 9 ------- core/src/main/java/module-info.java | 2 -- core/src/main/resources/solver.xsd | 24 ------------------- 4 files changed, 58 deletions(-) delete mode 100644 core/src/main/java/ai/timefold/solver/core/config/solver/random/RandomType.java delete mode 100644 core/src/main/java/ai/timefold/solver/core/config/solver/random/package-info.java diff --git a/core/src/main/java/ai/timefold/solver/core/config/solver/random/RandomType.java b/core/src/main/java/ai/timefold/solver/core/config/solver/random/RandomType.java deleted file mode 100644 index 4b8c20b06ae..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/config/solver/random/RandomType.java +++ /dev/null @@ -1,23 +0,0 @@ -package ai.timefold.solver.core.config.solver.random; - -import jakarta.xml.bind.annotation.XmlEnum; - -/** - * Defines the pseudo random number generator. - * See the PRNG - * documentation in commons-math. - */ -@XmlEnum -public enum RandomType { - /** - * This is the default. - */ - JDK, - MERSENNE_TWISTER, - WELL512A, - WELL1024A, - WELL19937A, - WELL19937C, - WELL44497A, - WELL44497B; -} diff --git a/core/src/main/java/ai/timefold/solver/core/config/solver/random/package-info.java b/core/src/main/java/ai/timefold/solver/core/config/solver/random/package-info.java deleted file mode 100644 index c1998dd1769..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/config/solver/random/package-info.java +++ /dev/null @@ -1,9 +0,0 @@ -@XmlSchema( - namespace = SolverConfig.XML_NAMESPACE, - elementFormDefault = XmlNsForm.QUALIFIED) -package ai.timefold.solver.core.config.solver.random; - -import jakarta.xml.bind.annotation.XmlNsForm; -import jakarta.xml.bind.annotation.XmlSchema; - -import ai.timefold.solver.core.config.solver.SolverConfig; diff --git a/core/src/main/java/module-info.java b/core/src/main/java/module-info.java index e6fac5a290f..b6581b85c62 100644 --- a/core/src/main/java/module-info.java +++ b/core/src/main/java/module-info.java @@ -55,7 +55,6 @@ exports ai.timefold.solver.core.config.score.trend; exports ai.timefold.solver.core.config.solver; exports ai.timefold.solver.core.config.solver.monitoring; - exports ai.timefold.solver.core.config.solver.random; exports ai.timefold.solver.core.config.solver.termination; exports ai.timefold.solver.core.config.util; exports ai.timefold.solver.core.enterprise; @@ -245,7 +244,6 @@ opens ai.timefold.solver.core.config.score.trend to jakarta.xml.bind, org.glassfish.jaxb.runtime; opens ai.timefold.solver.core.config.solver to jakarta.xml.bind, org.glassfish.jaxb.runtime; opens ai.timefold.solver.core.config.solver.monitoring to jakarta.xml.bind, org.glassfish.jaxb.runtime; - opens ai.timefold.solver.core.config.solver.random to jakarta.xml.bind, org.glassfish.jaxb.runtime; opens ai.timefold.solver.core.config.solver.termination to jakarta.xml.bind, org.glassfish.jaxb.runtime; opens ai.timefold.solver.core.config.util to jakarta.xml.bind, org.glassfish.jaxb.runtime; diff --git a/core/src/main/resources/solver.xsd b/core/src/main/resources/solver.xsd index 0699ee0b12c..a5d10b20c96 100644 --- a/core/src/main/resources/solver.xsd +++ b/core/src/main/resources/solver.xsd @@ -1804,29 +1804,5 @@ - - - - - - - - - - - - - - - - - - - - - - - - From ab2c90e4b5a94df96ef32a825f2355b4487ccd6d Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Wed, 29 Apr 2026 12:26:32 -0400 Subject: [PATCH 8/8] fix: javadoc issue --- .../random/DelegatingSplittableRandomGenerator.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/random/DelegatingSplittableRandomGenerator.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/random/DelegatingSplittableRandomGenerator.java index cbd893fd227..d6a62185fa1 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/random/DelegatingSplittableRandomGenerator.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/random/DelegatingSplittableRandomGenerator.java @@ -13,10 +13,10 @@ * A {@link RandomGenerator} that delegates to another {@link RandomGenerator.SplittableGenerator} * instance. This allows us to change the {@link RandomGenerator} used even when * {@link MoveSelector} and other classes to cache the {@link RandomGenerator} in a field. - * - * @apiNote To ensure reproducibility, this class can only be used by the {@link Thread} - * that created it. Attempting to call any method from another thread will - * throw an {@link IllegalStateException}. + *

+ * To ensure reproducibility, this class can only be used by the {@link Thread} + * that created it. Attempting to call any method from another thread will + * throw an {@link IllegalStateException}. */ @NullMarked public final class DelegatingSplittableRandomGenerator implements RandomGenerator {