Skip to content

feat: add environment mode per phase - #2595

Open
zepfred wants to merge 17 commits into
TimefoldAI:mainfrom
zepfred:feat/environment
Open

feat: add environment mode per phase#2595
zepfred wants to merge 17 commits into
TimefoldAI:mainfrom
zepfred:feat/environment

Conversation

@zepfred

@zepfred zepfred commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Allows each solver phase (Construction Heuristic, Local Search, Exhaustive Search, Partitioned Search, Custom) to override the solver's environmentMode with a stricter mode of its own via PhaseConfig.withEnvironmentMode(...). This makes it possible to run a suspect phase under FULL_ASSERT (or another stricter mode) for debugging, without paying that performance cost for the whole solving run.

  • A phase's environment mode must be at least as strict as the solver's; it can never be looser.
  • If the solver's mode is NON_REPRODUCIBLE, no phase can override it (every other mode is reproducible, hence stricter).
  • At least one phase must still run in the solver's own environment mode.

Key changes

  • PhaseConfig: new environmentMode field/getter/setter/withEnvironmentMode(...), added to the @XmlType propOrder, and wired into solver.xsd / benchmark.xsd. Includes a revapi-differences.json ignore entry for the resulting @XmlType.propOrder change.
  • ScoreDirectorFactoryFactoryDelegateScoreDirectorFactory: renamed and reworked to lazily build a ScoreDirectorFactory per distinct environment mode encountered across the solver config and its phases, instead of a single factory built once from the solver's mode. DefaultSolverFactory keeps one default factory (built from the solver's own environment mode) for consumers that are decoupled from the solving lifecycle (e.g. SolverManager, Quarkus DI injecting ConstraintMetaModel), while phases needing a stricter mode get their own score director from the delegate.
  • AbstractSolver / DefaultSolver / phase factories: restructured so each phase can be built with (and run under) its own score director/environment mode rather than always reusing the solver-level one.
  • Bundled fix — avoid stale references of ListVariableStateSupply in list move selectors (ElementDestinationSelector, RandomSubListSelector, ListChangeMoveSelector, ListSwapMoveSelector, KOptListMoveSelector, ListRuinRecreateMoveSelector): these previously cached the supply once at selector construction time; now a new ListVariableStateSupplyHolder fetches it fresh, so a rebuilt score director (as introduced by the per-phase change) doesn't leave selectors pointing at a stale supply.
  • Docs: new "Using different environment modes per phase" section in solver-diagnostics.adoc with a config example.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces per-phase EnvironmentMode overrides so individual solver phases can run with stricter assertion modes (e.g., FULL_ASSERT) without imposing that cost on the entire solve, and it refactors score director factory creation/lifecycle to support per-phase score directors.

Changes:

  • Add environmentMode to PhaseConfig, wire it into XSDs, and document configuration usage.
  • Refactor solver/phase construction so each phase can run under its own environment mode and corresponding score director (via DelegateScoreDirectorFactory).
  • Fix list move selectors to avoid stale ListVariableStateSupply references when score directors are rebuilt, by introducing ListVariableStateSupplyHolder.

Reviewed changes

Copilot reviewed 61 out of 61 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tools/benchmark/src/main/resources/benchmark.xsd Adds environmentMode element to phase config schema.
docs/src/modules/ROOT/pages/running-timefold-solver/solver-diagnostics.adoc Documents per-phase environment mode overrides with an example.
core/src/test/java/ai/timefold/solver/core/impl/solver/SolverMetricsIT.java Updates latch-await assertions to JUnit assertDoesNotThrow.
core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java Adds tests covering per-phase environment mode behavior and context restoration.
core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverFactoryTest.java Adds validation tests for environment mode constraints across phases.
core/src/test/java/ai/timefold/solver/core/impl/score/director/stream/ConstraintStreamsBavetScoreDirectorSemanticsTest.java Updates to use DelegateScoreDirectorFactory.
core/src/test/java/ai/timefold/solver/core/impl/score/director/incremental/IncrementalScoreDirectorTest.java Mocks getEnvironmentMode() and adjusts no-op listener methods.
core/src/test/java/ai/timefold/solver/core/impl/score/director/incremental/IncrementalScoreDirectorSemanticsTest.java Updates to use DelegateScoreDirectorFactory.
core/src/test/java/ai/timefold/solver/core/impl/score/director/easy/EasyScoreDirectorSemanticsTest.java Updates to use DelegateScoreDirectorFactory.
core/src/test/java/ai/timefold/solver/core/impl/score/director/DelegateScoreDirectorFactoryTest.java Renames/extends tests for the new delegate factory behavior.
core/src/test/java/ai/timefold/solver/core/impl/neighborhood/NeighborhoodsTest.java Adapts acceptor/phase builder APIs to pass environment mode.
core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactoryTest.java Updates acceptor factory API usage to include environment mode.
core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/RandomSubListSwapMoveSelectorTest.java Ensures selectors receive phaseStarted lifecycle for fresh supplies.
core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/RandomSubListChangeMoveSelectorTest.java Ensures selectors receive phaseStarted lifecycle for fresh supplies.
core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/RandomListChangeIteratorTest.java Ensures destination selector receives phaseStarted lifecycle.
core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListSwapMoveSelectorTest.java Ensures selector receives phaseStarted lifecycle for fresh supplies.
core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListChangeMoveSelectorTest.java Ensures selector receives phaseStarted lifecycle for fresh supplies.
core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/list/RandomSubListSelectorTest.java Ensures selector receives phaseStarted lifecycle for fresh supplies.
core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/list/ElementDestinationSelectorTest.java Ensures selector receives phaseStarted lifecycle for fresh supplies.
core/src/test/java/ai/timefold/solver/core/impl/domain/variable/ListVariableStateSupplyHolderTest.java Adds unit test for ListVariableStateSupplyHolder demand/cancel behavior.
core/src/main/resources/solver.xsd Adds environmentMode element to phase config schema.
core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java Routes assertScoreFromScratch through score director instance.
core/src/main/java/ai/timefold/solver/core/impl/solver/recaller/BestSolutionRecallerFactory.java Delegates assertion enabling to BestSolutionRecaller.
core/src/main/java/ai/timefold/solver/core/impl/solver/recaller/BestSolutionRecaller.java Adds enableAssertions(EnvironmentMode) method.
core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverFactory.java Introduces default environment mode, delegate factory, and env-mode validation across phases.
core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolver.java Refactors construction to carry default context and delegate factory; logs default env mode.
core/src/main/java/ai/timefold/solver/core/impl/solver/AbstractSolver.java Adds per-phase context swapping to run phases under different environment modes.
core/src/main/java/ai/timefold/solver/core/impl/score/director/ScoreDirectorFactory.java Adds getEnvironmentMode() and adjusts builder generics; removes factory-level assert method.
core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirector.java Adds assertScoreFromScratch and a counted increment method.
core/src/main/java/ai/timefold/solver/core/impl/score/director/DelegateScoreDirectorFactory.java Replaces ScoreDirectorFactoryFactory and centralizes score director creation per env mode.
core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirectorFactory.java Implements getEnvironmentMode() and moves score-from-scratch assertion off the factory.
core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java Stores environment mode, adjusts tracking/assert logic, and implements assertScoreFromScratch.
core/src/main/java/ai/timefold/solver/core/impl/phase/Phase.java Adds getEnvironmentMode() to phase API.
core/src/main/java/ai/timefold/solver/core/impl/phase/custom/DefaultCustomPhaseFactory.java Resolves per-phase environment mode and passes it into phase builder.
core/src/main/java/ai/timefold/solver/core/impl/phase/custom/DefaultCustomPhase.java Logs environment mode and threads it through the builder hierarchy.
core/src/main/java/ai/timefold/solver/core/impl/phase/AbstractPossiblyInitializingPhase.java Threads environment mode into initializing phase builder base.
core/src/main/java/ai/timefold/solver/core/impl/phase/AbstractPhaseFactory.java Adds shared resolveEnvironmentMode helper for phase factories.
core/src/main/java/ai/timefold/solver/core/impl/phase/AbstractPhase.java Stores phase environment mode and uses it for assertion enabling.
core/src/main/java/ai/timefold/solver/core/impl/partitionedsearch/DefaultPartitionedSearchPhaseFactory.java Passes resolved environment mode into enterprise partitioned search builder.
core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java Propagates environment mode into decider/acceptor construction.
core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java Logs environment mode and threads it through phase builder.
core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/tabu/AbstractTabuAcceptor.java Enables tabu assertions based on environment mode.
core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactory.java Adds environment mode parameter and enables assertions accordingly.
core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateConstructionHeuristicPhaseFactory.java Passes environment mode into decider construction (root mode by default).
core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateConstructionHeuristicPhaseBuilder.java Threads environment mode into builder construction/copying.
core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ruin/ListRuinRecreateMoveSelector.java Switches to ListVariableStateSupplyHolder to avoid stale supply across phase swaps.
core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListSwapMoveSelector.java Switches to ListVariableStateSupplyHolder and phase lifecycle hooks.
core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListChangeMoveSelector.java Switches to ListVariableStateSupplyHolder and phase lifecycle hooks.
core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/kopt/KOptListMoveSelector.java Switches to ListVariableStateSupplyHolder and phase lifecycle hooks.
core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/list/RandomSubListSelector.java Switches to ListVariableStateSupplyHolder and phase lifecycle hooks.
core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/list/ElementDestinationSelector.java Switches to ListVariableStateSupplyHolder and phase lifecycle hooks.
core/src/main/java/ai/timefold/solver/core/impl/heuristic/HeuristicConfigPolicy.java Renames/adjusts copying methods used by phase/child-thread policy creation.
core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/DefaultExhaustiveSearchPhaseFactory.java Resolves per-phase environment mode and propagates it into decider and phase builder.
core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/DefaultExhaustiveSearchPhase.java Logs environment mode and threads it through phase builder.
core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/decider/AbstractExhaustiveSearchDecider.java Enables decider assertions based on environment mode.
core/src/main/java/ai/timefold/solver/core/impl/domain/variable/ListVariableStateSupplyHolder.java Introduces helper to demand/cancel list state supply per phase start/end.
core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseFactory.java Resolves per-phase environment mode and propagates it into decider and phase builder.
core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhase.java Logs environment mode and threads it through phase builder.
core/src/main/java/ai/timefold/solver/core/enterprise/TimefoldSolverEnterpriseService.java Extends enterprise partitioned search API to accept environment mode.
core/src/main/java/ai/timefold/solver/core/config/phase/PhaseConfig.java Adds per-phase environmentMode config with JAXB/XSD support and inheritance.
core/src/build/revapi-differences.json Ignores the JAXB @XmlType.propOrder change for PhaseConfig.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread core/src/main/java/ai/timefold/solver/core/impl/solver/AbstractSolver.java Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 61 out of 61 changed files in this pull request and generated 1 comment.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 61 out of 61 changed files in this pull request and generated no new comments.

Suppressed comments (5)

core/src/test/java/ai/timefold/solver/core/impl/solver/SolverMetricsIT.java:228

  • Using JUnit's assertDoesNotThrow violates the repository test convention requiring AssertJ assertions (see CONSTITUTION.md), so switch this to AssertJ and remove the JUnit import.
    core/src/test/java/ai/timefold/solver/core/impl/solver/SolverMetricsIT.java:291
  • Using JUnit's assertDoesNotThrow violates the repository test convention requiring AssertJ assertions (see CONSTITUTION.md), so switch this to AssertJ and remove the JUnit import.
    core/src/test/java/ai/timefold/solver/core/impl/solver/SolverMetricsIT.java:440
  • Using JUnit's assertDoesNotThrow violates the repository test convention requiring AssertJ assertions (see CONSTITUTION.md), so switch this to AssertJ and remove the JUnit import.
    core/src/main/java/ai/timefold/solver/core/impl/solver/AbstractSolver.java:133
  • AbstractSolver.preparePhase() always builds a new ScoreDirectorFactory/ScoreDirector when switching into a non-default environment mode, which contradicts the PR description of lazily reusing factories per distinct mode and may add avoidable overhead when phases switch modes repeatedly.
    core/src/test/java/ai/timefold/solver/core/impl/solver/SolverMetricsIT.java:138
  • Using JUnit's assertDoesNotThrow violates the repository test convention requiring AssertJ assertions (see CONSTITUTION.md), so switch this to AssertJ and remove the JUnit import.

This issue also appears in the following locations of the same file:

  • line 228
  • line 291
  • line 440

Copilot AI review requested due to automatic review settings August 19, 2026 19:18

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 61 out of 61 changed files in this pull request and generated no new comments.

Suppressed comments (5)

core/src/test/java/ai/timefold/solver/core/impl/solver/SolverMetricsIT.java:138

  • This uses JUnit's assertDoesNotThrow, but CONSTITUTION.md requires AssertJ assertions instead of JUnit assertions.
    core/src/test/java/ai/timefold/solver/core/impl/solver/SolverMetricsIT.java:228
  • This uses JUnit's assertDoesNotThrow, but CONSTITUTION.md requires AssertJ assertions instead of JUnit assertions.
    core/src/test/java/ai/timefold/solver/core/impl/solver/SolverMetricsIT.java:291
  • This uses JUnit's assertDoesNotThrow, but CONSTITUTION.md requires AssertJ assertions instead of JUnit assertions.
    core/src/test/java/ai/timefold/solver/core/impl/solver/SolverMetricsIT.java:440
  • This uses JUnit's assertDoesNotThrow, but CONSTITUTION.md requires AssertJ assertions instead of JUnit assertions.
    core/src/test/java/ai/timefold/solver/core/impl/solver/SolverMetricsIT.java:6
  • This test introduces a static import of JUnit Assertions (assertDoesNotThrow), but CONSTITUTION.md forbids JUnit assertions in favor of AssertJ assertions.

This issue also appears in the following locations of the same file:

  • line 138
  • line 228
  • line 291
  • line 440

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 84 out of 84 changed files in this pull request and generated 5 comments.

Suppressed comments (3)

core/src/test/java/ai/timefold/solver/core/impl/solver/SolverMetricsIT.java:228

  • CONSTITUTION.md forbids JUnit assertions in tests, so replace this assertDoesNotThrow call with an AssertJ equivalent.
    core/src/test/java/ai/timefold/solver/core/impl/solver/SolverMetricsIT.java:291
  • CONSTITUTION.md forbids JUnit assertions in tests, so replace this assertDoesNotThrow call with an AssertJ equivalent.
    core/src/test/java/ai/timefold/solver/core/impl/solver/SolverMetricsIT.java:440
  • CONSTITUTION.md forbids JUnit assertions in tests, so replace this assertDoesNotThrow call with an AssertJ equivalent.

} catch (InterruptedException e) {
Assertions.fail("Failed waiting for the event to happen.", e);
}
assertDoesNotThrow(() -> latch.await(10, TimeUnit.SECONDS), "Failed waiting for the event to happen.");
Comment on lines 4 to +6
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.fail;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
Comment on lines +84 to +86
var originalScoreDirector = solverScope.getScoreDirector();
var workingSolution = originalScoreDirector.getWorkingSolution();

// The working solution carries over rather than being re-cloned.
assertThat(newScoreDirector.getWorkingSolution()).isSameAs(workingSolution);
// The problem change director follows the score director, so problem changes hit the live one.
assertThat(solverScope.getProblemChangeDirector()).isNotSameAs(originalScoreDirector);
void assertPhaseEnvironmentMode() {
var solverConfig = PlannerTestUtils.buildSolverConfig(TestdataSolution.class, TestdataEntity.class);
solverConfig.setEnvironmentMode(EnvironmentMode.FULL_ASSERT);
// LS with NO_ASSERT
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants