Skip to content
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
@@ -1,5 +1,6 @@
package ai.timefold.solver.core.impl.score.director;

import java.util.ArrayList;
import java.util.BitSet;
import java.util.HashMap;
import java.util.IdentityHashMap;
Expand Down Expand Up @@ -35,7 +36,6 @@ final class ValueRangeState<Solution_, Entity_, Value_> {

// Solution related fields
private @Nullable ValueRangeItem<Solution_, Entity_, ValueRange<Value_>, Value_> fromSolutionItem;
private @Nullable Map<Value_, Integer> fromSolutionValueIndexMap;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Was only returned by a method that was never called...


// Entity related fields
private @Nullable Map<Entity_, ValueRangeItem<Solution_, Entity_, ValueRange<Value_>, Value_>> fromEntityMap;
Expand All @@ -54,7 +54,6 @@ public ValueRange<Value_> getFromSolution(Solution_ solution, @Nullable Selectio
if (fromSolutionItem == null) {
var valueRange = fetchValueRangeFromSolution(solution, sorter);
fromSolutionItem = ValueRangeItem.ofLeft(null, valueRange, sorter);
fromSolutionValueIndexMap = buildIndexMap(valueRange.createOriginalIterator(), (int) valueRange.getSize());
return valueRange;
}
var valueRange = pickValueBySorter(fromSolutionItem, sorter, null);
Expand All @@ -66,9 +65,6 @@ public ValueRange<Value_> getFromSolution(Solution_ solution, @Nullable Selectio
var sortedValueRange = sortValueRange(Objects.requireNonNull(fromSolutionItem.leftItem()), sorter);
fromSolutionItem = ValueRangeItem.of(null, sortedValueRange, sorter, fromSolutionItem.rightItem(),
fromSolutionItem.rightSorter());
// We need to update the index map or the positions may become inconsistent
fromSolutionValueIndexMap =
buildIndexMap(sortedValueRange.createOriginalIterator(), (int) sortedValueRange.getSize());
return sortedValueRange;
} else if (fromSolutionItem.rightItem() == null) {
var sortedValueRange = sortValueRange(Objects.requireNonNull(fromSolutionItem.leftItem()), sorter);
Expand Down Expand Up @@ -103,14 +99,6 @@ public ValueRange<Value_> getFromSolution(Solution_ solution, @Nullable Selectio
return null;
}

private Map<Value_, Integer> getIndexMapFromSolution() {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

... this method.

if (fromSolutionValueIndexMap == null) {
// We call getFromSolution to ensure the solution-range is loaded and the related index map is created
getFromSolution(cachedWorkingSolution, null);
}
return fromSolutionValueIndexMap;
}

private ValueRange<Value_> fetchValueRangeFromSolution(Solution_ solution,
@Nullable SelectionSorter<Solution_, Value_> sorter) {
var valueRange = extractValueRange(solution);
Expand Down Expand Up @@ -153,7 +141,7 @@ public ValueRange<Value_> getFromEntity(Entity_ entity,
}
return Objects.requireNonNull(newItem.leftItem());
}
var valueRange = pickValueBySorter(item, sorter, (p, s) -> getFromEntity(p, s));
var valueRange = pickValueBySorter(item, sorter, this::getFromEntity);
if (valueRange != null) {
return valueRange;
}
Expand Down Expand Up @@ -282,10 +270,14 @@ private ReachableValues<Entity_, Value_> fetchReachableValues(GenuineVariableDes
var expectedTypeOfValue = valueRangeDescriptor.getVariableDescriptor().getVariableMetaModel().type();
var reachableValueList = initReachableValueList(valueList, entityList.size());
var valueIndexItem = new ReachableValuesIndex<>(valueIndexMap, reachableValueList);
var entityIndicesByRange = new IdentityHashMap<ValueRange<Value_>, List<Integer>>();
for (var i = 0; i < entityList.size(); i++) {
var entity = entityList.get(i);
var valueRange = getFromEntity(entity, null);
loadEntityValueRange(i, valueIndexMap, valueRange, reachableValueList);
entityIndicesByRange.computeIfAbsent(valueRange, k -> new ArrayList<>()).add(i);
}
for (var entry : entityIndicesByRange.entrySet()) {
loadEntityValueRange(entry.getValue(), valueIndexMap, entry.getKey(), reachableValueList);
}
var sorterAdapter = sorter != null ? SelectionSorterAdapter.of(cachedWorkingSolution, sorter) : null;
return new ReachableValues<>(entityIndexItem, valueIndexItem, expectedTypeOfValue, sorterAdapter,
Expand All @@ -308,31 +300,34 @@ private static <Type_> Map<Type_, Integer> buildIndexMap(Iterator<@Nullable Type
private List<ReachableItemValue<Entity_, Value_>> initReachableValueList(ValueRange<Value_> valueRange,
int entityListSize) {
var valuesSize = (int) valueRange.getSize();
Iterator<@Nullable Value_> iterator = valueRange.createOriginalIterator();
var iterator = valueRange.createOriginalIterator();
var spliterator = Spliterators.spliterator(iterator, valuesSize, Spliterator.ORDERED | Spliterator.IMMUTABLE);
var idx = new MutableInt(-1);
return StreamSupport.stream(spliterator, false).filter(Objects::nonNull)
.map(v -> new ReachableItemValue<Entity_, Value_>(idx.increment(), v, entityListSize, valuesSize)).toList();
}

private static <Entity_, Value_> void loadEntityValueRange(int entityIndex, Map<Value_, Integer> valueIndexMap,
private static <Entity_, Value_> void loadEntityValueRange(List<Integer> entityIndices, Map<Value_, Integer> valueIndexMap,
ValueRange<Value_> valueRange, List<ReachableItemValue<Entity_, Value_>> reachableValueList) {
// We create a bitset containing all possible values from the range to optimize operations
// Build bitset once per distinct range to avoid redundant work for entities sharing a range.
var allValuesBitSet = buildBitSetForValueRange(valueRange, valueIndexMap);
// The second pass need only to iterate over the bits we already set.
// The second pass only iterates over the bits we already set.
var valueIndex = allValuesBitSet.nextSetBit(0);
while (valueIndex >= 0) {
var item = reachableValueList.get(valueIndex);
item.addEntity(entityIndex);
// We unset the current value index to import only the values that are reachable
// Co-values populated once per distinct range; addValuesExcept is idempotent across shared ranges.
item.addValuesExcept(allValuesBitSet, valueIndex);
// Entity membership registered per entity.
for (var entityIndex : entityIndices) {
item.addEntity(entityIndex);
}
valueIndex = allValuesBitSet.nextSetBit(valueIndex + 1);
}
}

private static <Value_> BitSet buildBitSetForValueRange(ValueRange<Value_> valueRange, Map<Value_, Integer> valueIndexMap) {
var valueBitSet = new BitSet((int) valueRange.getSize());
Iterator<@Nullable Value_> iterator = valueRange.createOriginalIterator();
var iterator = valueRange.createOriginalIterator();
while (iterator.hasNext()) {
var value = iterator.next();
if (value == null) {
Expand Down Expand Up @@ -370,7 +365,7 @@ public static <Solution_, Entity_, Type_, Value_> ValueRangeItem<Solution_, Enti
* The record holds a reference to {@link ValueRange},
* a precomputed hash to avoid recalculating it every time.
*/
record HashedValueRange<T>(ValueRange<T> item, int hash) {
private record HashedValueRange<T>(ValueRange<T> item, int hash) {

public static <Value_> HashedValueRange<Value_> of(ValueRange<Value_> valueRange) {
return new HashedValueRange<>(valueRange, valueRange.hashCode());
Expand All @@ -383,10 +378,9 @@ public int hashCode() {

@Override
public boolean equals(Object o) {
if (!(o instanceof HashedValueRange<?> that)) {
return false;
}
return hash == that.hash && Objects.equals(item, that.item);
return o instanceof HashedValueRange<?>(var otherItem, var otherHash)
&& hash == otherHash
&& Objects.equals(item, otherItem);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,43 @@

class ReachableValuesTest {

@Test
void testSharedEntityValueRange() {
var v1 = new TestdataListEntityProvidingValue("V1");
var v2 = new TestdataListEntityProvidingValue("V2");
var v3 = new TestdataListEntityProvidingValue("V3");
var v4 = new TestdataListEntityProvidingValue("V4");
// A and B share an identical value range; C has a distinct one.
var a = new TestdataListEntityProvidingEntity("A", List.of(v1, v2, v3));
var b = new TestdataListEntityProvidingEntity("B", List.of(v1, v2, v3));
var c = new TestdataListEntityProvidingEntity("C", List.of(v3, v4));
var solution = new TestdataListEntityProvidingSolution();
solution.setEntityList(List.of(a, b, c));

var scoreDirector = mockScoreDirector(TestdataListEntityProvidingSolution.buildSolutionDescriptor());
scoreDirector.setWorkingSolution(solution);

var solutionDescriptor = scoreDirector.getSolutionDescriptor();
var entityDescriptor = solutionDescriptor.findEntityDescriptor(TestdataListEntityProvidingEntity.class);
var reachableValues = scoreDirector.getValueRangeManager()
.getReachableValues(entityDescriptor.getListVariableDescriptor());

// Both A and B must appear — addEntity must be called for each entity, not just the canonical one.
assertThat(reachableValues.extractEntitiesAsList(v1)).containsExactlyInAnyOrder(a, b);
assertThat(reachableValues.extractEntitiesAsList(v2)).containsExactlyInAnyOrder(a, b);
assertThat(reachableValues.extractEntitiesAsList(v3)).containsExactlyInAnyOrder(a, b, c);
assertThat(reachableValues.extractEntitiesAsList(v4)).containsExactlyInAnyOrder(c);

assertThat(reachableValues.isEntityReachable(v1, a)).isTrue();
assertThat(reachableValues.isEntityReachable(v1, b)).isTrue();
assertThat(reachableValues.isEntityReachable(v1, c)).isFalse();

// v3 appears in both the shared range [v1,v2,v3] and C's range [v3,v4]; co-values = union minus self.
assertThat(reachableValues.extractValuesAsList(v3)).containsExactlyInAnyOrder(v1, v2, v4);
assertThat(reachableValues.extractValuesAsList(v1)).containsExactlyInAnyOrder(v2, v3);
assertThat(reachableValues.extractValuesAsList(v4)).containsExactlyInAnyOrder(v3);
}

@Test
void testReachableValuesByEntity() {
var v1 = new TestdataListEntityProvidingValue("V1");
Expand Down Expand Up @@ -106,7 +143,7 @@ void testUnassignedReachableValues() {
var solutionDescriptor = scoreDirector.getSolutionDescriptor();
var entityDescriptor = solutionDescriptor.findEntityDescriptor(TestdataAllowsUnassignedEntityProvidingEntity.class);
var reachableValues = scoreDirector.getValueRangeManager()
.getReachableValues(entityDescriptor.getGenuineVariableDescriptorList().get(0));
.getReachableValues(entityDescriptor.getGenuineVariableDescriptorList().getFirst());

assertThat(reachableValues.extractValuesAsList(v1)).containsExactlyInAnyOrder(v2, v3);
assertThat(reachableValues.extractValuesAsList(v2)).containsExactlyInAnyOrder(v1, v3);
Expand Down Expand Up @@ -142,7 +179,7 @@ void sortAscendingReachableValues() {
var solutionDescriptor = scoreDirector.getSolutionDescriptor();
var entityDescriptor = solutionDescriptor.findEntityDescriptor(TestdataAllowsUnassignedEntityProvidingEntity.class);
var reachableValues = scoreDirector.getValueRangeManager()
.getReachableValues(entityDescriptor.getGenuineVariableDescriptorList().get(0),
.getReachableValues(entityDescriptor.getGenuineVariableDescriptorList().getFirst(),
new TestdataObjectSorter<TestdataAllowsUnassignedEntityProvidingSolution, TestdataValue>());

assertThat(reachableValues.extractValuesAsList(v1)).containsExactlyInAnyOrder(v2, v3);
Expand Down Expand Up @@ -171,7 +208,7 @@ void sortDescendingReachableValues() {
var solutionDescriptor = scoreDirector.getSolutionDescriptor();
var entityDescriptor = solutionDescriptor.findEntityDescriptor(TestdataAllowsUnassignedEntityProvidingEntity.class);
var reachableValues = scoreDirector.getValueRangeManager()
.getReachableValues(entityDescriptor.getGenuineVariableDescriptorList().get(0),
.getReachableValues(entityDescriptor.getGenuineVariableDescriptorList().getFirst(),
new TestdataObjectSorter<TestdataAllowsUnassignedEntityProvidingSolution, TestdataValue>(false));

assertThat(reachableValues.extractValuesAsList(v1)).containsExactlyInAnyOrder(v3, v2);
Expand Down Expand Up @@ -203,31 +240,31 @@ void multipleSorters() {

// No sorter
var noSorterValues = scoreDirector.getValueRangeManager()
.getReachableValues(entityDescriptor.getGenuineVariableDescriptorList().get(0), null);
.getReachableValues(entityDescriptor.getGenuineVariableDescriptorList().getFirst(), null);
assertThat(noSorterValues.extractValuesAsList(v2)).containsExactlyInAnyOrder(v3, v1, v4, v5);

// Ascending sorter replaces the no sorter
var ascendingSorter = new TestdataObjectSorter<TestdataAllowsUnassignedEntityProvidingSolution, TestdataValue>(true);
var ascendingValues = scoreDirector.getValueRangeManager()
.getReachableValues(entityDescriptor.getGenuineVariableDescriptorList().get(0), ascendingSorter);
.getReachableValues(entityDescriptor.getGenuineVariableDescriptorList().getFirst(), ascendingSorter);
assertThat(ascendingValues).isNotSameAs(noSorterValues)
.isSameAs(scoreDirector.getValueRangeManager()
.getReachableValues(entityDescriptor.getGenuineVariableDescriptorList().get(0), ascendingSorter));
.getReachableValues(entityDescriptor.getGenuineVariableDescriptorList().getFirst(), ascendingSorter));
assertThat(noSorterValues.extractValuesAsList(v2)).containsExactlyInAnyOrder(v1, v3, v4, v5);

// Descending sorter
var descendingSorter = new TestdataObjectSorter<TestdataAllowsUnassignedEntityProvidingSolution, TestdataValue>(false);
var descendingValues = scoreDirector.getValueRangeManager()
.getReachableValues(entityDescriptor.getGenuineVariableDescriptorList().get(0), descendingSorter);
.getReachableValues(entityDescriptor.getGenuineVariableDescriptorList().getFirst(), descendingSorter);
assertThat(descendingValues).isNotSameAs(ascendingValues)
.isNotSameAs(noSorterValues)
.isSameAs(scoreDirector.getValueRangeManager()
.getReachableValues(entityDescriptor.getGenuineVariableDescriptorList().get(0), descendingSorter));
.getReachableValues(entityDescriptor.getGenuineVariableDescriptorList().getFirst(), descendingSorter));
assertThat(descendingValues.extractValuesAsList(v2)).containsExactlyInAnyOrder(v5, v4, v3, v1);

// Null sorter returns the ascending sorter
var otherNoSorterValues = scoreDirector.getValueRangeManager()
.getReachableValues(entityDescriptor.getGenuineVariableDescriptorList().get(0), ascendingSorter);
.getReachableValues(entityDescriptor.getGenuineVariableDescriptorList().getFirst(), ascendingSorter);
assertThat(otherNoSorterValues).isSameAs(ascendingValues);
}
}
Loading