Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

Swap move selector applies entity value range filtering to entity classes without entity-level ranges

Open
#2,761 0 comments 0 reactions 1 assignee View on GitHub

Maintainers usually reply within 1 day

@zepfred is already working on this.

Since Oct 15, 2026.

Assessment

This issue has not been assessed yet.

Description

process/needs triage

Summary

Once any basic variable in the model has an entity-level value range, the default swap move selector
turns on entity value range filtering for every entity class with basic variables, including classes
whose variables only have solution-level ranges. Local search then crashes when it starts.

List variables are not affected: only basic variables are checked.

Error

java.lang.IllegalStateException: Impossible state: no basic variable found for the entity class org.acme.projectjobschedule.domain.ProjectLead.
	at ai.timefold.solver.core.impl.heuristic.selector.entity.decorator.FilteringEntityByEntitySelector.phaseStarted(FilteringEntityByEntitySelector.java:104)
	...
	at ai.timefold.solver.core.impl.neighborhood.MoveSelectorBasedMoveRepository.phaseStarted(MoveSelectorBasedMoveRepository.java:43)
	at ai.timefold.solver.core.impl.localsearch.decider.LocalSearchDecider.phaseStarted(LocalSearchDecider.java:78)
	at ai.timefold.solver.core.impl.localsearch.DefaultLocalSearchPhase.phaseStarted(DefaultLocalSearchPhase.java:136)
	at ai.timefold.solver.core.impl.localsearch.DefaultLocalSearchPhase.solve(DefaultLocalSearchPhase.java:76)

Root cause

SwapMoveSelectorFactory.java (lines 44-47) decides whether to enable entity value range filtering by
looking at all basic variables in the solution, not just those of the entity class being swapped:

var entityDescriptor = deduceEntityDescriptor(configPolicy, entitySelectorConfig.getEntityClass());
var enableEntityValueRangeFilter = entityDescriptor.getSolutionDescriptor().getBasicVariableDescriptorList()
        .stream()
        .anyMatch(v -> !v.canExtractValueRangeFromSolution());

The default swap move selector is unfolded into one child selector per entity class with basic variables.
For an entity class whose variables all have solution-level ranges, the check above still returns true
because of another entity class, so a recorder ID is set and the right entity selector is wrapped in
FilteringEntityByEntitySelector. That selector then finds no entity-level range on this class and throws.

Suggested fix

Scope the check to the entity class being swapped (ideally to the variables the selector actually uses,
honoring variableNameIncludeList):

var enableEntityValueRangeFilter = deduceBasicVariableDescriptorList(entityDescriptor, config.getVariableNameIncludeList())
        .stream()
        .anyMatch(v -> !v.canExtractValueRangeFromSolution());

Reproducer

Quickstart use-cases/project-job-scheduling, branch bug/reproduce-multi-entity-value-range:

  • Allocation (existing) has basic variables with entity-level ranges (executionMode, delay).
  • ProjectLead (new) has a basic variable resource whose range comes from the solution
    (@ValueRangeProvider on ProjectJobSchedule.resources).
  • MultiEntityValueRangeReproducerTest solves for 5 seconds without a best score limit, so the
    local search phase runs and crashes.

Note: the existing SolverManagerTest does not catch this because the test profile's best score limit
ends the solve before local search starts.

Workaround

Give every other entity class with basic variables an entity-level range too, even if it just lists every
value:

@ValueRangeProvider
public List<Resource> getResourceRange() {
    return resourceRange; // all resources
}

With this in place, the reproducer passes and local search runs normally.

Environment

  • Reproduced with the local timefold-solver 999-SNAPSHOT (built 2026-10-12); the same code is on
    solver main. Reported against 2.7.
Dominant language
Java
Stars
1.8k
Forks
238
Avg merge
3d 1h
Merged PRs (30d)
55

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from TimefoldAI/timefold-solver

All issues in TimefoldAI/timefold-solver

Similar issues

More Java issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.