Skip to content

Commit 0be77ae

Browse files
authored
chore: improve entity range for list variables (#1722)
1 parent 79806ca commit 0be77ae

File tree

44 files changed

+1576
-389
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1576
-389
lines changed

core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/NearbyUtil.java

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.ListSwapMoveSelectorConfig;
1313
import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.kopt.KOptListMoveSelectorConfig;
1414
import ai.timefold.solver.core.config.heuristic.selector.value.ValueSelectorConfig;
15+
import ai.timefold.solver.core.config.util.ConfigUtils;
1516
import ai.timefold.solver.core.impl.heuristic.selector.common.nearby.NearbyDistanceMeter;
1617

1718
import org.jspecify.annotations.NonNull;
@@ -31,7 +32,7 @@ private static EntitySelectorConfig configureEntitySelector(EntitySelectorConfig
3132
if (entitySelectorConfig == null) {
3233
entitySelectorConfig = new EntitySelectorConfig();
3334
}
34-
var entitySelectorId = addRandomSuffix("entitySelector", random);
35+
var entitySelectorId = ConfigUtils.addRandomSuffix("entitySelector", random);
3536
entitySelectorConfig.withId(entitySelectorId);
3637
return entitySelectorConfig;
3738
}
@@ -128,7 +129,7 @@ private static ValueSelectorConfig configureValueSelector(ValueSelectorConfig va
128129
if (valueSelectorConfig == null) {
129130
valueSelectorConfig = new ValueSelectorConfig();
130131
}
131-
var valueSelectorId = addRandomSuffix("valueSelector", random);
132+
var valueSelectorId = ConfigUtils.addRandomSuffix("valueSelector", random);
132133
valueSelectorConfig.withId(valueSelectorId);
133134
return valueSelectorConfig;
134135
}
@@ -162,15 +163,6 @@ private static ValueSelectorConfig configureSecondaryValueSelector(ValueSelector
162163
.withValueSelectorConfig(valueConfig);
163164
}
164165

165-
public static String addRandomSuffix(String name, Random random) {
166-
var value = new StringBuilder(name);
167-
value.append("-");
168-
random.ints(97, 122) // ['a', 'z']
169-
.limit(4) // 4 letters
170-
.forEach(value::appendCodePoint);
171-
return value.toString();
172-
}
173-
174166
private NearbyUtil() {
175167
// No instances.
176168
}

core/src/main/java/ai/timefold/solver/core/config/util/ConfigUtils.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.util.Map;
2626
import java.util.Objects;
2727
import java.util.Optional;
28+
import java.util.Random;
2829
import java.util.Set;
2930
import java.util.function.Supplier;
3031
import java.util.stream.Collectors;
@@ -606,6 +607,15 @@ private static List<Member> getMembers(List<Member> memberList, boolean needMeth
606607
return abbreviate(list, 3);
607608
}
608609

610+
public static String addRandomSuffix(String name, Random random) {
611+
var value = new StringBuilder(name);
612+
value.append("-");
613+
random.ints(97, 122) // ['a', 'z']
614+
.limit(4) // 4 letters
615+
.forEach(value::appendCodePoint);
616+
return value.toString();
617+
}
618+
609619
// ************************************************************************
610620
// Private constructor
611621
// ************************************************************************

core/src/main/java/ai/timefold/solver/core/impl/domain/variable/descriptor/GenuineVariableDescriptor.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,6 @@ private ValueRangeDescriptor<Solution_> buildValueRangeDescriptor(DescriptorPoli
150150
if (descriptorPolicy.isFromSolutionValueRangeProvider(valueRangeProviderMemberAccessor)) {
151151
return new FromSolutionPropertyValueRangeDescriptor<>(this, valueRangeProviderMemberAccessor);
152152
} else if (descriptorPolicy.isFromEntityValueRangeProvider(valueRangeProviderMemberAccessor)) {
153-
if (isListVariable()) {
154-
throw new IllegalStateException("Entity value ranges for list variables are not supported.");
155-
}
156153
return new FromEntityPropertyValueRangeDescriptor<>(this, valueRangeProviderMemberAccessor);
157154
} else {
158155
throw new IllegalStateException("Impossible state: member accessor (%s) is not a value range provider."
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package ai.timefold.solver.core.impl.heuristic.selector.common;
2+
3+
import java.util.ArrayList;
4+
import java.util.Collections;
5+
import java.util.IdentityHashMap;
6+
import java.util.List;
7+
import java.util.Map;
8+
import java.util.Objects;
9+
import java.util.Set;
10+
11+
import ai.timefold.solver.core.impl.domain.valuerange.descriptor.FromEntityPropertyValueRangeDescriptor;
12+
13+
import org.jspecify.annotations.NullMarked;
14+
import org.jspecify.annotations.Nullable;
15+
16+
/**
17+
* This class records the relationship between each planning value and all entities that include the related value
18+
* within its value range.
19+
*
20+
* @see FromEntityPropertyValueRangeDescriptor
21+
*/
22+
@NullMarked
23+
public final class ReachableValues {
24+
25+
private final Map<Object, Set<Object>> valueToEntityMap;
26+
private final Map<Object, Set<Object>> valueToValueMap;
27+
private final Map<Object, List<Object>> randomAccessValueToEntityMap;
28+
private final Map<Object, List<Object>> randomAccessValueToValueMap;
29+
private final @Nullable Class<?> valueClass;
30+
31+
public ReachableValues(Map<Object, Set<Object>> valueToEntityMap, Map<Object, Set<Object>> valueToValueMap) {
32+
this.valueToEntityMap = valueToEntityMap;
33+
this.randomAccessValueToEntityMap = new IdentityHashMap<>(this.valueToEntityMap.size());
34+
this.valueToValueMap = valueToValueMap;
35+
this.randomAccessValueToValueMap = new IdentityHashMap<>(this.valueToValueMap.size());
36+
var first = valueToEntityMap.entrySet().stream().findFirst();
37+
this.valueClass = first.<Class<?>> map(entry -> entry.getKey().getClass()).orElse(null);
38+
}
39+
40+
/**
41+
* @return all reachable values for the given value.
42+
*/
43+
public @Nullable Set<Object> extractEntities(Object value) {
44+
return valueToEntityMap.get(value);
45+
}
46+
47+
/**
48+
* @return all reachable entities for the given value.
49+
*/
50+
public @Nullable Set<Object> extractValues(Object value) {
51+
return valueToValueMap.get(value);
52+
}
53+
54+
public List<Object> extractEntitiesAsList(Object value) {
55+
var result = randomAccessValueToEntityMap.get(value);
56+
if (result == null) {
57+
var entitySet = this.valueToEntityMap.get(value);
58+
if (entitySet != null) {
59+
result = new ArrayList<>(entitySet);
60+
} else {
61+
result = Collections.emptyList();
62+
}
63+
randomAccessValueToEntityMap.put(value, result);
64+
}
65+
return result;
66+
}
67+
68+
public List<Object> extractValuesAsList(Object value) {
69+
var result = randomAccessValueToValueMap.get(value);
70+
if (result == null) {
71+
var valueSet = this.valueToValueMap.get(value);
72+
if (valueSet != null) {
73+
result = new ArrayList<>(valueSet);
74+
} else {
75+
result = Collections.emptyList();
76+
}
77+
randomAccessValueToValueMap.put(value, result);
78+
}
79+
return result;
80+
}
81+
82+
public int getSize() {
83+
return valueToEntityMap.size();
84+
}
85+
86+
public boolean isValidValueClass(Object value) {
87+
if (valueToEntityMap.isEmpty()) {
88+
return false;
89+
}
90+
return Objects.requireNonNull(value).getClass().equals(valueClass);
91+
}
92+
93+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
package ai.timefold.solver.core.impl.heuristic.selector.entity.decorator;
2+
3+
import java.util.Iterator;
4+
import java.util.List;
5+
import java.util.ListIterator;
6+
import java.util.Objects;
7+
import java.util.Random;
8+
9+
import ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor;
10+
import ai.timefold.solver.core.impl.heuristic.selector.AbstractDemandEnabledSelector;
11+
import ai.timefold.solver.core.impl.heuristic.selector.common.ReachableValues;
12+
import ai.timefold.solver.core.impl.heuristic.selector.common.iterator.UpcomingSelectionIterator;
13+
import ai.timefold.solver.core.impl.heuristic.selector.entity.EntitySelector;
14+
import ai.timefold.solver.core.impl.heuristic.selector.value.IterableValueSelector;
15+
import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope;
16+
import ai.timefold.solver.core.impl.solver.scope.SolverScope;
17+
18+
/**
19+
* The decorator returns a list of reachable entities for a specific value.
20+
* It enables the creation of a filtering tier when using entity value selectors,
21+
* ensuring only valid and reachable entities are returned.
22+
*
23+
* e1 = entity_range[v1, v2, v3]
24+
* e2 = entity_range[v1, v4]
25+
*
26+
* v1 = [e1, e2]
27+
* v2 = [e1]
28+
* v3 = [e1]
29+
* v4 = [e2]
30+
*
31+
* @param <Solution_> the solution type
32+
*/
33+
public final class FilteringEntityValueRangeSelector<Solution_> extends AbstractDemandEnabledSelector<Solution_>
34+
implements EntitySelector<Solution_> {
35+
36+
private final IterableValueSelector<Solution_> replayingValueSelector;
37+
private final EntitySelector<Solution_> childEntitySelector;
38+
private final boolean randomSelection;
39+
40+
private ReachableValues reachableValues;
41+
private long entitiesSize;
42+
43+
public FilteringEntityValueRangeSelector(EntitySelector<Solution_> childEntitySelector,
44+
IterableValueSelector<Solution_> replayingValueSelector, boolean randomSelection) {
45+
this.replayingValueSelector = replayingValueSelector;
46+
this.childEntitySelector = childEntitySelector;
47+
this.randomSelection = randomSelection;
48+
}
49+
50+
// ************************************************************************
51+
// Lifecycle methods
52+
// ************************************************************************
53+
54+
@Override
55+
public void solvingStarted(SolverScope<Solution_> solverScope) {
56+
super.solvingStarted(solverScope);
57+
this.childEntitySelector.solvingStarted(solverScope);
58+
}
59+
60+
@Override
61+
public void phaseStarted(AbstractPhaseScope<Solution_> phaseScope) {
62+
super.phaseStarted(phaseScope);
63+
this.entitiesSize = childEntitySelector.getEntityDescriptor().extractEntities(phaseScope.getWorkingSolution()).size();
64+
this.reachableValues = phaseScope.getScoreDirector().getValueRangeManager()
65+
.getReachableValeMatrix(childEntitySelector.getEntityDescriptor().getGenuineListVariableDescriptor());
66+
this.childEntitySelector.phaseStarted(phaseScope);
67+
}
68+
69+
@Override
70+
public void phaseEnded(AbstractPhaseScope<Solution_> phaseScope) {
71+
super.phaseEnded(phaseScope);
72+
this.reachableValues = null;
73+
}
74+
75+
// ************************************************************************
76+
// Worker methods
77+
// ************************************************************************
78+
79+
public EntitySelector<Solution_> getChildEntitySelector() {
80+
return childEntitySelector;
81+
}
82+
83+
@Override
84+
public EntityDescriptor<Solution_> getEntityDescriptor() {
85+
return childEntitySelector.getEntityDescriptor();
86+
}
87+
88+
@Override
89+
public long getSize() {
90+
return entitiesSize;
91+
}
92+
93+
@Override
94+
public boolean isCountable() {
95+
return childEntitySelector.isCountable();
96+
}
97+
98+
@Override
99+
public boolean isNeverEnding() {
100+
return childEntitySelector.isNeverEnding();
101+
}
102+
103+
@Override
104+
public Iterator<Object> endingIterator() {
105+
return new OriginalFilteringValueRangeIterator(replayingValueSelector.iterator(), reachableValues);
106+
}
107+
108+
@Override
109+
public Iterator<Object> iterator() {
110+
if (randomSelection) {
111+
return new EntityRandomFilteringValueRangeIterator(replayingValueSelector.iterator(), reachableValues,
112+
workingRandom);
113+
} else {
114+
return new OriginalFilteringValueRangeIterator(replayingValueSelector.iterator(), reachableValues);
115+
}
116+
}
117+
118+
@Override
119+
public ListIterator<Object> listIterator() {
120+
throw new UnsupportedOperationException();
121+
}
122+
123+
@Override
124+
public ListIterator<Object> listIterator(int index) {
125+
throw new UnsupportedOperationException();
126+
}
127+
128+
@Override
129+
public boolean equals(Object other) {
130+
return other instanceof FilteringEntityValueRangeSelector<?> that
131+
&& Objects.equals(childEntitySelector, that.childEntitySelector)
132+
&& Objects.equals(replayingValueSelector, that.replayingValueSelector);
133+
}
134+
135+
@Override
136+
public int hashCode() {
137+
return Objects.hash(childEntitySelector, replayingValueSelector);
138+
}
139+
140+
private static class OriginalFilteringValueRangeIterator extends UpcomingSelectionIterator<Object> {
141+
142+
private final Iterator<Object> valueIterator;
143+
private final ReachableValues reachableValues;
144+
private Iterator<Object> otherIterator;
145+
146+
private OriginalFilteringValueRangeIterator(Iterator<Object> valueIterator, ReachableValues reachableValues) {
147+
this.valueIterator = valueIterator;
148+
this.reachableValues = Objects.requireNonNull(reachableValues);
149+
}
150+
151+
private void initialize() {
152+
if (otherIterator != null) {
153+
return;
154+
}
155+
var allValues = reachableValues.extractEntitiesAsList(Objects.requireNonNull(valueIterator.next()));
156+
this.otherIterator = Objects.requireNonNull(allValues).iterator();
157+
}
158+
159+
@Override
160+
protected Object createUpcomingSelection() {
161+
initialize();
162+
if (!otherIterator.hasNext()) {
163+
return noUpcomingSelection();
164+
}
165+
return otherIterator.next();
166+
}
167+
}
168+
169+
private static class EntityRandomFilteringValueRangeIterator extends UpcomingSelectionIterator<Object> {
170+
171+
private final Iterator<Object> valueIterator;
172+
private final ReachableValues reachableValues;
173+
private final Random workingRandom;
174+
private Object currentUpcomingValue;
175+
private List<Object> entityList;
176+
177+
private EntityRandomFilteringValueRangeIterator(Iterator<Object> valueIterator,
178+
ReachableValues reachableValues, Random workingRandom) {
179+
this.valueIterator = valueIterator;
180+
this.reachableValues = Objects.requireNonNull(reachableValues);
181+
this.workingRandom = workingRandom;
182+
}
183+
184+
private void initialize() {
185+
if (entityList != null) {
186+
return;
187+
}
188+
this.currentUpcomingValue = Objects.requireNonNull(valueIterator.next());
189+
loadValues();
190+
}
191+
192+
private void loadValues() {
193+
upcomingCreated = false;
194+
this.entityList = reachableValues.extractEntitiesAsList(currentUpcomingValue);
195+
}
196+
197+
@Override
198+
public boolean hasNext() {
199+
if (valueIterator.hasNext() && currentUpcomingValue != null) {
200+
var updatedUpcomingValue = valueIterator.next();
201+
if (updatedUpcomingValue != currentUpcomingValue) {
202+
// The iterator is reused in the ElementPositionRandomIterator,
203+
// even if the value has changed.
204+
// Therefore,
205+
// we need to update the value list to ensure it is consistent.
206+
this.currentUpcomingValue = updatedUpcomingValue;
207+
loadValues();
208+
}
209+
}
210+
return super.hasNext();
211+
}
212+
213+
@Override
214+
protected Object createUpcomingSelection() {
215+
initialize();
216+
if (entityList.isEmpty()) {
217+
return noUpcomingSelection();
218+
}
219+
var index = workingRandom.nextInt(entityList.size());
220+
return entityList.get(index);
221+
}
222+
}
223+
224+
}

0 commit comments

Comments
 (0)