Skip to content

feat: improve the Nearby logic for entity ranges #1729

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Aug 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public void phaseStarted(AbstractPhaseScope<Solution_> phaseScope) {
super.phaseStarted(phaseScope);
this.entitiesSize = childEntitySelector.getEntityDescriptor().extractEntities(phaseScope.getWorkingSolution()).size();
this.reachableValues = phaseScope.getScoreDirector().getValueRangeManager()
.getReachableValeMatrix(childEntitySelector.getEntityDescriptor().getGenuineListVariableDescriptor());
.getReachableValues(phaseScope.getScoreDirector().getSolutionDescriptor().getListVariableDescriptor());
this.childEntitySelector.phaseStarted(phaseScope);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public void phaseStarted(AbstractPhaseScope<Solution_> phaseScope) {
this.nonReplayingValueSelector.phaseStarted(phaseScope);
this.replayingValueSelector.phaseStarted(phaseScope);
this.reachableValues = phaseScope.getScoreDirector().getValueRangeManager()
.getReachableValeMatrix(listVariableStateSupply.getSourceVariableDescriptor());
.getReachableValues(listVariableStateSupply.getSourceVariableDescriptor());
valuesSize = reachableValues.getSize();
}

Expand Down Expand Up @@ -129,20 +129,31 @@ public Iterator<Object> iterator(Object entity) {
@Override
public Iterator<Object> iterator() {
if (randomSelection) {
return new RandomFilteringValueRangeIterator(replayingValueSelector.iterator(), listVariableStateSupply,
reachableValues, workingRandom, (int) getSize(), checkSourceAndDestination, true);
// If the nonReplayingValueSelector does not have any additional configuration,
// we can bypass it and only use reachable values,
// which helps optimize the number of evaluations.
// However, if the nonReplayingValueSelector includes custom configurations,
// such as filtering,
// we will first evaluate its values and then filter out those that are not reachable.
if (nonReplayingValueSelector instanceof IterableFromEntityPropertyValueSelector<Solution_>) {
return new OptimizedRandomFilteringValueRangeIterator(replayingValueSelector.iterator(),
listVariableStateSupply,
reachableValues, workingRandom, (int) getSize(), checkSourceAndDestination);
} else {
return new RandomFilteringValueRangeIterator(replayingValueSelector.iterator(),
nonReplayingValueSelector.iterator(), listVariableStateSupply, reachableValues, (int) getSize(),
checkSourceAndDestination);
}
} else {
return new OriginalFilteringValueRangeIterator(replayingValueSelector.iterator(),
nonReplayingValueSelector.iterator(), listVariableStateSupply, reachableValues, checkSourceAndDestination,
false);
nonReplayingValueSelector.iterator(), listVariableStateSupply, reachableValues, checkSourceAndDestination);
}
}

@Override
public Iterator<Object> endingIterator(Object entity) {
return new OriginalFilteringValueRangeIterator(replayingValueSelector.iterator(),
nonReplayingValueSelector.iterator(), listVariableStateSupply, reachableValues, checkSourceAndDestination,
false);
nonReplayingValueSelector.iterator(), listVariableStateSupply, reachableValues, checkSourceAndDestination);
}

@Override
Expand Down Expand Up @@ -255,25 +266,21 @@ boolean isValueOrEntityReachable(Object destinationValue) {
}
}

private class OriginalFilteringValueRangeIterator extends AbstractFilteringValueRangeIterator {
private abstract class AbstractUpcomingValueRangeIterator extends AbstractFilteringValueRangeIterator {
// The value iterator that only replays the current selected value
private final Iterator<Object> replayingValueIterator;
// The value iterator returns all possible values based on its settings.
// However,
// it may include invalid values that need to be filtered out.
// This iterator must be used to ensure that all positions are included in the CH phase.
// This does not apply to the LS phase.
private final Iterator<Object> valueIterator;
final Iterator<Object> replayingValueIterator;
// The value iterator returns all possible values based on the outer selector settings.
final Iterator<Object> valueIterator;

private OriginalFilteringValueRangeIterator(Iterator<Object> replayingValueIterator, Iterator<Object> valueIterator,
private AbstractUpcomingValueRangeIterator(Iterator<Object> replayingValueIterator, Iterator<Object> valueIterator,
ListVariableStateSupply<Solution_> listVariableStateSupply, ReachableValues reachableValues,
boolean checkSourceAndDestination, boolean useValueList) {
super(listVariableStateSupply, reachableValues, checkSourceAndDestination, useValueList);
this.replayingValueIterator = replayingValueIterator;
this.valueIterator = valueIterator;
}

private void initialize() {
void initialize() {
if (initialized) {
return;
}
Expand All @@ -288,6 +295,16 @@ private void initialize() {
noData();
}
}
}

private class OriginalFilteringValueRangeIterator extends AbstractUpcomingValueRangeIterator {

private OriginalFilteringValueRangeIterator(Iterator<Object> replayingValueIterator, Iterator<Object> valueIterator,
ListVariableStateSupply<Solution_> listVariableStateSupply, ReachableValues reachableValues,
boolean checkSourceAndDestination) {
super(replayingValueIterator, valueIterator, listVariableStateSupply, reachableValues, checkSourceAndDestination,
false);
}

@Override
protected Object createUpcomingSelection() {
Expand All @@ -306,16 +323,51 @@ protected Object createUpcomingSelection() {
}
}

private class RandomFilteringValueRangeIterator extends AbstractFilteringValueRangeIterator {
private class RandomFilteringValueRangeIterator extends AbstractUpcomingValueRangeIterator {
private final int maxBailoutSize;

private RandomFilteringValueRangeIterator(Iterator<Object> replayingValueIterator, Iterator<Object> valueIterator,
ListVariableStateSupply<Solution_> listVariableStateSupply, ReachableValues reachableValues,
int maxBailoutSize, boolean checkSourceAndDestination) {
super(replayingValueIterator, valueIterator, listVariableStateSupply, reachableValues, checkSourceAndDestination,
false);
this.maxBailoutSize = maxBailoutSize;
}

@Override
protected Object createUpcomingSelection() {
initialize();
if (!hasData) {
return noUpcomingSelection();
}
Object next;
var bailoutSize = maxBailoutSize;
do {
if (bailoutSize <= 0 || !valueIterator.hasNext()) {
return noUpcomingSelection();
}
bailoutSize--;
next = valueIterator.next();
} while (!isValueOrEntityReachable(next));
return next;
}
}

/**
* The optimized iterator only traverses reachable values from the current selection.
* Unlike {@link RandomFilteringValueRangeIterator},
* it does not use an outer iterator to filter out non-reachable values.
*/
private class OptimizedRandomFilteringValueRangeIterator extends AbstractFilteringValueRangeIterator {

private final Iterator<Object> replayingValueIterator;
private final Random workingRandom;
private final int maxBailoutSize;

private RandomFilteringValueRangeIterator(Iterator<Object> replayingValueIterator,
private OptimizedRandomFilteringValueRangeIterator(Iterator<Object> replayingValueIterator,
ListVariableStateSupply<Solution_> listVariableStateSupply, ReachableValues reachableValues,
Random workingRandom, int maxBailoutSize, boolean checkSourceAndDestination, boolean useValueList) {
super(listVariableStateSupply, reachableValues, checkSourceAndDestination, useValueList);
Random workingRandom, int maxBailoutSize, boolean checkSourceAndDestination) {
super(listVariableStateSupply, reachableValues, checkSourceAndDestination, true);
this.replayingValueIterator = replayingValueIterator;
this.workingRandom = workingRandom;
this.maxBailoutSize = maxBailoutSize;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ public final class ValueRangeManager<Solution_> {
private final Map<ValueRangeDescriptor<Solution_>, CountableValueRange<?>> fromSolutionMap = new IdentityHashMap<>();
private final Map<Object, Map<ValueRangeDescriptor<Solution_>, CountableValueRange<?>>> fromEntityMap =
new IdentityHashMap<>();

private @Nullable ReachableValues reachableValues = null;

private @Nullable Solution_ cachedWorkingSolution = null;
private @Nullable SolutionInitializationStatistics cachedInitializationStatistics = null;
private @Nullable ProblemSizeStatistics cachedProblemSizeStatistics = null;
Expand Down Expand Up @@ -414,32 +414,30 @@ public long countOnEntity(ValueRangeDescriptor<Solution_> valueRangeDescriptor,
.getSize();
}

public ReachableValues getReachableValeMatrix(ListVariableDescriptor<Solution_> listVariableDescriptor) {
public ReachableValues getReachableValues(ListVariableDescriptor<Solution_> listVariableDescriptor) {
if (reachableValues == null) {
if (cachedWorkingSolution == null) {
throw new IllegalStateException(
"Impossible state: the matrix %s requested before the working solution is known."
.formatted(ReachableValues.class.getSimpleName()));
"Impossible state: value reachability requested before the working solution is known.");
}
var entityDescriptor = listVariableDescriptor.getEntityDescriptor();
var valueRangeDescriptor = listVariableDescriptor.getValueRangeDescriptor();
var entityList = entityDescriptor.extractEntities(cachedWorkingSolution);
var allValues = getFromSolution(valueRangeDescriptor);
var allValues = getFromSolution(listVariableDescriptor.getValueRangeDescriptor());
var valuesSize = allValues.getSize();
if (valuesSize > Integer.MAX_VALUE) {
throw new IllegalStateException(
"The matrix %s cannot be built for the entity %s (%s) because value range has a size (%d) which is higher than Integer.MAX_VALUE."
.formatted(ReachableValues.class.getSimpleName(),
entityDescriptor.getEntityClass().getSimpleName(),
valueRangeDescriptor.getVariableDescriptor().getVariableName(), valuesSize));
listVariableDescriptor.getVariableName(), valuesSize));
}
// list of entities reachable for a value
var entityMatrix = new IdentityHashMap<Object, Set<Object>>((int) valuesSize);
// list of values reachable for a value
var valueMatrix = new IdentityHashMap<Object, Set<Object>>((int) valuesSize);
for (var entity : entityList) {
var valuesIterator = allValues.createOriginalIterator();
var range = getFromEntity(valueRangeDescriptor, entity);
var range = getFromEntity(listVariableDescriptor.getValueRangeDescriptor(), entity);
while (valuesIterator.hasNext()) {
var value = valuesIterator.next();
if (range.contains(value)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,8 @@ void solveWithEntityValueRangeBasicVariable() {
@Test
void solveWithEntityValueRangeListVariable() {
var solverConfig = PlannerTestUtils
.buildSolverConfig(TestdataListEntityProvidingSolution.class, TestdataListEntityProvidingEntity.class)
.buildSolverConfig(TestdataListEntityProvidingSolution.class, TestdataListEntityProvidingEntity.class,
TestdataListEntityProvidingValue.class)
.withEasyScoreCalculatorClass(TestdataListEntityProvidingScoreCalculator.class)
.withPhases(new ConstructionHeuristicPhaseConfig());

Expand All @@ -381,8 +382,10 @@ void solveWithEntityValueRangeListVariable() {
var bestSolution = PlannerTestUtils.solve(solverConfig, solution, true);
assertThat(bestSolution).isNotNull();
// Only one entity should provide the value list and assign the values.
assertThat(bestSolution.getEntityList().get(0).getValueList()).hasSameElementsAs(List.of(value1, value2));
assertThat(bestSolution.getEntityList().get(1).getValueList()).hasSameElementsAs(List.of(value3));
assertThat(bestSolution.getEntityList().get(0).getValueList().stream().map(TestdataListEntityProvidingValue::getCode))
.hasSameElementsAs(List.of("v1", "v2"));
assertThat(bestSolution.getEntityList().get(1).getValueList().stream().map(TestdataListEntityProvidingValue::getCode))
.hasSameElementsAs(List.of("v3"));
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ void testReachableEntities() {
var solutionDescriptor = scoreDirector.getSolutionDescriptor();
var entityDescriptor = solutionDescriptor.findEntityDescriptor(TestdataListEntityProvidingEntity.class);
var reachableValues = scoreDirector.getValueRangeManager()
.getReachableValeMatrix(entityDescriptor.getGenuineListVariableDescriptor());
.getReachableValues(entityDescriptor.getGenuineListVariableDescriptor());

assertThat(reachableValues.extractEntities(v1)).containsExactlyInAnyOrder(a);
assertThat(reachableValues.extractEntities(v2)).containsExactlyInAnyOrder(a, b);
Expand Down Expand Up @@ -60,7 +60,7 @@ void testReachableValues() {
var solutionDescriptor = scoreDirector.getSolutionDescriptor();
var entityDescriptor = solutionDescriptor.findEntityDescriptor(TestdataListEntityProvidingEntity.class);
var reachableValues = scoreDirector.getValueRangeManager()
.getReachableValeMatrix(entityDescriptor.getGenuineListVariableDescriptor());
.getReachableValues(entityDescriptor.getGenuineListVariableDescriptor());

assertThat(reachableValues.extractValues(v1)).containsExactlyInAnyOrder(v2, v3);
assertThat(reachableValues.extractValues(v2)).containsExactlyInAnyOrder(v1, v3);
Expand Down
Loading
Loading