Skip to content

feat(neighborhoods): introduce coarse-grained generic moves - #2610

Open
triceo wants to merge 16 commits into
TimefoldAI:mainfrom
triceo:pillars
Open

feat(neighborhoods): introduce coarse-grained generic moves#2610
triceo wants to merge 16 commits into
TimefoldAI:mainfrom
triceo:pillars

Conversation

@triceo

@triceo triceo commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator

Introduces:

  • Basic var pillar, subpillar and mass moves.
  • List var sublist and mass moves.
  • 2opt move and tail swap.

Documentation will only be written after we agree on all the things that are in here. The chances of having to redo the docs because of review changes are too big.

My recommendation for review: start with the moves and move providers. Understand them. Before we discuss the implementation, we need to be in agreement that we want these moves, and not some others. (The implementation is dictated by the requirements of the moves - therefore if we decide to change the moves, we will change the implementation.)

@triceo
triceo deployed to internal August 23, 2026 07:39 — with GitHub Actions Active
@triceo
triceo deployed to internal August 23, 2026 07:39 — with GitHub Actions Active
@triceo
triceo deployed to internal August 23, 2026 07:39 — with GitHub Actions Active
@triceo
triceo deployed to internal August 23, 2026 12:11 — with GitHub Actions Active
@triceo
triceo deployed to internal August 23, 2026 12:11 — with GitHub Actions Active
@triceo
triceo deployed to internal August 23, 2026 12:11 — with GitHub Actions Active
@triceo triceo changed the title feat(neighborhoods): Implement pillars+subpillars for basic vars feat(neighborhoods): introduce coarse-grained generic moves Aug 23, 2026
@triceo triceo self-assigned this Aug 23, 2026
@triceo triceo linked an issue Aug 23, 2026 that may be closed by this pull request
@triceo
triceo requested a lite review from Copilot August 23, 2026 12:22

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

Introduces coarse-grained generic neighborhood moves for basic and list variables, with supporting preview APIs, migration recipes, tests, and documentation.

Changes:

  • Adds pillar, subpillar, mass, list, and sublist moves and providers.
  • Adds sampling, dataset, range, and neighborhood-stream infrastructure.
  • Adds preview migration support and upgrade documentation.

Four moderate findings remain unresolved.

Reviewed changes

Copilot reviewed 155 out of 155 changed files in this pull request and generated 4 comments.

Show a summary per file
File Reviewed change
tools/migration/src/test/java/ai/timefold/solver/migration/preview/NeighborhoodsMigrationRecipeTest.java Migration recipe tests
tools/migration/src/main/java/module-info.java Migration module exports
tools/migration/src/main/java/ai/timefold/solver/migration/v2/ToLatestV2Recipe.java V2 migration composition
tools/migration/src/main/java/ai/timefold/solver/migration/v1/ToLatestV1Recipe.java V1 migration composition
tools/migration/src/main/java/ai/timefold/solver/migration/ToLatestRecipe.java Aggregate migration composition
tools/migration/src/main/java/ai/timefold/solver/migration/preview/PreviewToLatestRecipe.java Preview migration aggregation
tools/migration/src/main/java/ai/timefold/solver/migration/preview/package-info.java Preview migration documentation
tools/migration/src/main/java/ai/timefold/solver/migration/preview/NeighborhoodsMigrationRecipe.java Neighborhood API migration
docs/src/modules/ROOT/pages/upgrading-timefold-solver/upgrade-to-latest.adoc Latest upgrade guide
docs/src/modules/ROOT/pages/upgrading-timefold-solver/upgrade-from-v1.adoc V1 upgrade documentation
docs/src/modules/ROOT/pages/upgrading-timefold-solver/backwards-compatibility.adoc Compatibility documentation
docs/src/modules/ROOT/pages/upgrading-timefold-solver/.upgrading-timefold-solver.adoc Upgrade page inclusion
docs/src/modules/ROOT/nav.adoc Documentation navigation
core/src/test/java/ai/timefold/solver/core/testutil/NeighborhoodTestUtils.java Neighborhood test utilities
core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/entityproviding/multivar/TestdataAllowsUnassignedMultiVarEntityProvidingSolution.java Multivariable test data
core/src/test/java/ai/timefold/solver/core/preview/api/neighborhood/stream/dataset/sample/SampleTest.java Sample API tests
core/src/test/java/ai/timefold/solver/core/preview/api/neighborhood/stream/dataset/DatasetTest.java Dataset API tests
core/src/test/java/ai/timefold/solver/core/preview/api/move/builtin/SwapMoveTest.java Swap move tests
core/src/test/java/ai/timefold/solver/core/preview/api/move/builtin/SubPillarUnassignMoveProviderTest.java Subpillar unassignment tests
core/src/test/java/ai/timefold/solver/core/preview/api/move/builtin/SampleValueRangesBiasIT.java Value-range bias tests
core/src/test/java/ai/timefold/solver/core/preview/api/move/builtin/MassUnassignMoveProviderTest.java Mass unassignment tests
core/src/test/java/ai/timefold/solver/core/preview/api/move/builtin/MassAssignMoveProviderTest.java Mass assignment tests
core/src/test/java/ai/timefold/solver/core/preview/api/move/builtin/ListSwapMoveProviderTest.java List swap provider tests
core/src/test/java/ai/timefold/solver/core/preview/api/move/builtin/ListChangeMoveProviderTest.java List change provider tests
core/src/test/java/ai/timefold/solver/core/preview/api/move/builtin/ChangeMoveTest.java Change move tests
core/src/test/java/ai/timefold/solver/core/preview/api/move/builtin/ChangeMoveProviderTest.java Change provider tests
core/src/test/java/ai/timefold/solver/core/impl/util/TriangularNumbersTest.java Triangular-number tests
core/src/test/java/ai/timefold/solver/core/impl/util/TriangleElementFactoryTest.java Triangle factory tests
core/src/test/java/ai/timefold/solver/core/impl/neighborhood/bias/SamplingIteratorBiasIT.java Sampling bias tests
core/src/test/java/ai/timefold/solver/core/impl/neighborhood/bias/PairFairnessBiasIT.java Pair fairness tests
core/src/test/java/ai/timefold/solver/core/impl/neighborhood/bias/DatasetBucketBiasIT.java Dataset bucket bias tests
core/src/test/java/ai/timefold/solver/core/impl/neighborhood/bias/BiasReport.java Bias reporting
core/src/test/java/ai/timefold/solver/core/impl/neighborhood/bias/AbstractBiasIT.java Shared bias test support
core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/RandomSubListSwapMoveSelectorTest.java Random sublist swap selector tests
core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/RandomSubListChangeMoveSelectorTest.java Random sublist change selector tests
core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/list/RandomSubListSelectorTest.java Random sublist selector tests
core/src/main/java/module-info.java Core module exports
core/src/main/java/ai/timefold/solver/core/preview/api/neighborhood/UniMoveConstructor.java Unary move construction
core/src/main/java/ai/timefold/solver/core/preview/api/neighborhood/stream/picking/UniPickingStream.java Unary picking stream
core/src/main/java/ai/timefold/solver/core/preview/api/neighborhood/stream/picking/PickingStream.java Picking stream API
core/src/main/java/ai/timefold/solver/core/preview/api/neighborhood/stream/picking/BiPickingStream.java Binary picking stream
core/src/main/java/ai/timefold/solver/core/preview/api/neighborhood/stream/MoveStreamFactory.java Move stream factory
core/src/main/java/ai/timefold/solver/core/preview/api/neighborhood/stream/enumerating/UniEnumeratingStream.java Unary enumeration stream
core/src/main/java/ai/timefold/solver/core/preview/api/neighborhood/stream/enumerating/BiEnumeratingStream.java Binary enumeration stream
core/src/main/java/ai/timefold/solver/core/preview/api/neighborhood/stream/dataset/UniDatasetInstance.java Unary dataset instances
core/src/main/java/ai/timefold/solver/core/preview/api/neighborhood/stream/dataset/sample/SubListSampler.java Sublist sampling
core/src/main/java/ai/timefold/solver/core/preview/api/neighborhood/stream/dataset/sample/Sampler.java Sampler API
core/src/main/java/ai/timefold/solver/core/preview/api/neighborhood/stream/dataset/sample/Sample.java Sample API
core/src/main/java/ai/timefold/solver/core/preview/api/neighborhood/stream/dataset/sample/Range.java Range API
core/src/main/java/ai/timefold/solver/core/preview/api/neighborhood/stream/dataset/sample/package-info.java Sample API documentation
core/src/main/java/ai/timefold/solver/core/preview/api/neighborhood/stream/dataset/sample/DefaultSample.java Sample implementation
core/src/main/java/ai/timefold/solver/core/preview/api/neighborhood/stream/dataset/sample/DefaultRange.java Range implementation
core/src/main/java/ai/timefold/solver/core/preview/api/neighborhood/stream/dataset/BiDatasetInstance.java Binary dataset instances
core/src/main/java/ai/timefold/solver/core/preview/api/move/MutableSolutionView.java Mutable solution access
core/src/main/java/ai/timefold/solver/core/preview/api/move/Move.java Move contract
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/UnassignMoveProvider.java Unassignment provider
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/SwapMoveProvider.java Swap move provider
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/SwapMove.java Swap move implementation
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/SubListUnassignMove.java Sublist unassignment
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/SubListChangeMove.java Sublist change move
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/PillarUnassignMoveProvider.java Pillar unassignment provider
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/package-info.java Built-in move documentation
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/MassUnassignMoveProvider.java Mass unassignment provider
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/MassListUnassignMoveProvider.java Mass list unassignment provider
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/MassListDestinationMoveIterator.java Mass list destination iteration
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/MassListChangeMoveProvider.java Mass list change provider
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/MassListChangeMove.java Mass list change move
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/MassListAssignMoveProvider.java Mass list assignment provider
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/MassChangeMoveProvider.java Mass change provider
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/MassChangeMove.java Mass change move
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/MassAssignMoveProvider.java Mass assignment provider
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/ListUnassignMoveProvider.java List unassignment provider
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/ListUnassignMove.java List unassignment move
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/ListSwapMoveProvider.java List swap provider
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/ListAssignMoveProvider.java List assignment provider
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/ListAssignMove.java List assignment move
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/ChangeMoveProvider.java Change move provider
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/ChangeMove.java Change move implementation
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/AssignMoveProvider.java Assignment provider
core/src/main/java/ai/timefold/solver/core/preview/api/domain/metamodel/VariableMetaModel.java Variable metadata
core/src/main/java/ai/timefold/solver/core/preview/api/domain/metamodel/ShadowVariableMetaModel.java Shadow variable metadata
core/src/main/java/ai/timefold/solver/core/preview/api/domain/metamodel/ShadowEntityMetaModel.java Shadow entity metadata
core/src/main/java/ai/timefold/solver/core/preview/api/domain/metamodel/PlanningEntityMetaModel.java Planning entity metadata
core/src/main/java/ai/timefold/solver/core/preview/api/domain/metamodel/GenuineVariableMetaModel.java Genuine variable metadata
core/src/main/java/ai/timefold/solver/core/preview/api/domain/metamodel/ElementPosition.java Element position metadata
core/src/main/java/ai/timefold/solver/core/impl/util/TriangularNumbers.java Triangular-number utilities
core/src/main/java/ai/timefold/solver/core/impl/util/TriangleElementFactory.java Triangle element factory
core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/sampling/InnerUniSamplingStream.java Unary sampling implementation
core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/sampling/InnerSamplingStream.java Sampling implementation
core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/picking/InnerUniPickingStream.java Unary picking implementation
core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/picking/InnerPickingStream.java Picking implementation
core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/picking/DefaultUniPickingStream.java Default unary picking
core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/picking/DefaultBiPickingStream.java Default binary picking
core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/enumerating/common/AbstractLeftDatasetInstance.java Dataset join support
core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/DefaultMoveStreamFactory.java Move stream construction
core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/dataset/sample/SampleAssembler.java Sample assembly
core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/dataset/JustInTimeBiDatasetInstance.java Just-in-time binary datasets
core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/dataset/DefaultUniDatasetInstance.java Default unary datasets
core/src/main/java/ai/timefold/solver/core/impl/neighborhood/DefaultNeighborhoodTestContext.java Neighborhood test context
core/src/main/java/ai/timefold/solver/core/impl/neighborhood/DefaultNeighborhoodProvider.java Neighborhood provider
core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/list/TriangleElementFactory.java List selection factory
core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/list/RandomSubListSelector.java Random sublist selection
core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/NullAllowingValueRange.java Null-allowing value ranges
core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/SolutionDescriptor.java Solution metadata
core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/InnerPlanningEntityMetaModel.java Planning entity metadata construction
core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/DefaultShadowVariableMetaModel.java Shadow variable metadata construction
core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/DefaultShadowEntityMetaModel.java Shadow entity metadata construction
core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/DefaultPlanningVariableMetaModel.java Planning variable metadata construction
core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/DefaultPlanningListVariableMetaModel.java Planning list metadata construction
core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/DefaultGenuineEntityMetaModel.java Genuine entity metadata construction
core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/RetiringRandomIterator.java Random iterator support
core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/MappingRetiringRandomIterator.java Mapped random iterator support
CONSTITUTION.md Repository guidance
Suppressed comments (7)

core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/MassListChangeMove.java:79

  • Sample explicitly allows null members, but this move dereferences every member here (and again in getPlanningValues()), so a valid Sample can make the public list mass-change move fail with an unhelpful NullPointerException; either reject null members when constructing the move or document a non-null-member precondition.
    core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/MoveProviderUtil.java:232
  • The provider groups all entities, including those whose variable is null, but containsInEvery is called on ranges with nullable wrappers removed, so legal swaps involving an unassigned value are always rejected; handle null as legal when variableMetaModel.allowsUnassigned() is true.
    core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/PillarChangeMoveProvider.java:42
  • The new Javadoc contains the truncated word w instead of would, making the explanation grammatically incorrect.
    core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/SampleValueRanges.java:126
  • ValueRange.getSize() is a long, but admittedCount is an int, so pickExactly overflows after 2^31−1 admitted values and eventually passes a non-positive bound to nextInt; use a long counter and nextLong.
    core/src/main/java/ai/timefold/solver/core/preview/api/neighborhood/stream/dataset/sample/DefaultSample.java:48
  • Sample documents value-based equality for any two samples, but this implementation only compares DefaultSample, so a valid custom Sample with the same members is considered unequal; compare against the Sample interface and preserve the same member-based hash contract.
    core/src/main/java/ai/timefold/solver/core/preview/api/neighborhood/stream/dataset/sample/Sample.java:45
  • first() is annotated nullable and DefaultSample returns null when null is the first member, but this Javadoc promises a never-null result, which gives callers an unsafe contract; document that the result may be null.
    docs/src/modules/ROOT/pages/upgrading-timefold-solver/upgrade-to-latest.adoc:9
  • This unconditional promise contradicts the preview API migration documented later on this page and the preview compatibility policy: users on preview APIs can be broken by exactly the sampling-to-picking rename; qualify the guarantee to stable APIs or explicitly exclude preview APIs.

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

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 199 out of 199 changed files in this pull request and generated 1 comment.

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/ListSwapMoveProvider.java:58

  • ListSwapMoveProvider reuses a PositionInList obtained before unassigning as the subsequent assign target even though ElementPosition is documented as only valid when obtained; pass entity and index to Moves.assign(...) instead of reusing the PositionInList object.

Copilot AI review requested due to automatic review settings August 26, 2026 06:36

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 199 out of 199 changed files in this pull request and generated 2 comments.

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 199 out of 199 changed files in this pull request and generated no new comments.

Suppressed comments (2)

Previously missed (2) — in code that hasn't changed since the last review.

tools/migration/src/main/java/ai/timefold/solver/migration/ToLatestRecipe.java:36

  • ToLatestRecipe runs RemoveUnusedImports even though ToLatestV1Recipe and ToLatestV2Recipe already do, which redundantly applies the same cleanup multiple times during a single migration run.
    core/src/main/java/ai/timefold/solver/core/preview/api/neighborhood/stream/dataset/sample/SubListSampler.java:24
  • The caveat says fully @PlanningPin entities report getFirstUnpinnedIndex as 0, which contradicts SolutionView#getFirstUnpinnedIndex Javadoc claiming the value is at most countValues when the entire list is pinned.

Copilot AI review requested due to automatic review settings August 26, 2026 10:08
@triceo
triceo deployed to internal August 26, 2026 10:09 — with GitHub Actions Active
@triceo
triceo deployed to internal August 26, 2026 10:09 — with GitHub Actions Active
@triceo
triceo deployed to internal August 26, 2026 10:09 — with GitHub Actions Active

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 203 out of 203 changed files in this pull request and generated 2 comments.

Suppressed comments (2)

core/src/main/java/ai/timefold/solver/core/preview/api/neighborhood/stream/enumerating/BiEnumeratingStream.java:35

  • The new concat(UniEnumeratingStream, Function) method is missing @param/@return Javadoc tags required by CONSTITUTION.md for public APIs, so please add them.
    core/src/main/java/ai/timefold/solver/core/preview/api/neighborhood/stream/enumerating/UniEnumeratingStream.java:48
  • The new concat(BiEnumeratingStream, Function) method is missing @param/@return Javadoc tags required by CONSTITUTION.md for public APIs, so please add them.

Comment on lines +26 to +29
/**
* As defined by {@link UniEnumeratingStream#concat(UniEnumeratingStream)}.
*/
BiEnumeratingStream<Solution_, A, B> concat(BiEnumeratingStream<Solution_, A, B> otherStream);
Comment on lines +37 to +40
*
* @return a stream containing every tuple of both streams
*/
UniEnumeratingStream<Solution_, A> concat(UniEnumeratingStream<Solution_, A> otherStream);

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 204 out of 204 changed files in this pull request and generated 2 comments.

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

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 204 out of 204 changed files in this pull request and generated no new comments.

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

core/src/main/java/ai/timefold/solver/core/api/domain/common/Lookup.java:40

  • lookUpNonNullWorkingObject(T) can still return null because it doesn’t null-check the lookup result, which breaks its contract and can cause unexpected NPEs in Move.rebase callers; wrap the lookUpWorkingObject(...) result in Objects.requireNonNull(...).
    /**
     * As defined by {@link #lookUpWorkingObject(Object)},
     * but does not accept null arguments and cannot return null.
     */
    default <T> T lookUpNonNullWorkingObject(T problemFactOrPlanningEntity) {
        return lookUpWorkingObject(Objects.requireNonNull(problemFactOrPlanningEntity));
    }

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 204 out of 204 changed files in this pull request and generated 1 comment.

Comment on lines +38 to +40
default <T> T lookUpNonNullWorkingObject(T problemFactOrPlanningEntity) {
return lookUpWorkingObject(Objects.requireNonNull(problemFactOrPlanningEntity));
}
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.

Feat: Neighborhoods - Pillar move support

3 participants