Skip to content

Commit 9661cf8

Browse files
committed
chore: address comments
1 parent 14ec097 commit 9661cf8

27 files changed

Lines changed: 615 additions & 150 deletions

core/src/main/java/ai/timefold/solver/core/impl/AbstractFromConfigFactory.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,9 @@ private EntityDescriptor<Solution_> getEntityDescriptorForClass(SolutionDescript
5555
EntityDescriptor<Solution_> entityDescriptor = solutionDescriptor.getEntityDescriptorStrict(entityClass);
5656
if (entityDescriptor == null) {
5757
throw new IllegalArgumentException(
58-
"The config (%s) has an entityClass (%s) that is not a known planning entity.\nCheck your solver configuration. If that class (%s) is not in the entityClassSet (%s), check your @%s implementation's annotated methods too."
58+
"""
59+
The config (%s) has an entityClass (%s) that is not a known planning entity.
60+
Check your solver configuration. If that class (%s) is not in the entityClassSet (%s), check your @%s implementation's annotated methods too."""
5961
.formatted(config, entityClass, entityClass.getSimpleName(), solutionDescriptor.getEntityClassSet(),
6062
PlanningSolution.class.getSimpleName()));
6163
}
@@ -98,9 +100,10 @@ protected GenuineVariableDescriptor<Solution_> getVariableDescriptorForName(Enti
98100
GenuineVariableDescriptor<Solution_> variableDescriptor = entityDescriptor.getGenuineVariableDescriptor(variableName);
99101
if (variableDescriptor == null) {
100102
throw new IllegalArgumentException(
101-
"The config (%s) has a variableName (%s) which is not a valid planning variable on entityClass (%s).\n%s"
102-
.formatted(config, variableName, entityDescriptor.getEntityClass(),
103-
entityDescriptor.buildInvalidVariableNameExceptionMessage(variableName)));
103+
"""
104+
The config (%s) has a variableName (%s) which is not a valid planning variable on entityClass (%s).
105+
%s""".formatted(config, variableName, entityDescriptor.getEntityClass(),
106+
entityDescriptor.buildInvalidVariableNameExceptionMessage(variableName)));
104107
}
105108
return variableDescriptor;
106109
}

core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhase.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,15 +57,14 @@ public void solve(SolverScope<Solution_> solverScope) {
5757
phaseStarted(phaseScope);
5858

5959
var solutionDescriptor = solverScope.getSolutionDescriptor();
60-
var listVariableDescriptor = solutionDescriptor.getListVariableDescriptor();
61-
var hasOnlyListVariable = listVariableDescriptor != null && !solutionDescriptor.hasBothBasicAndListVariables();
60+
var hasListVariable = solutionDescriptor.hasListVariable();
6261
var maxStepCount = -1;
63-
if (hasOnlyListVariable) {
62+
if (hasListVariable) {
6463
// In case of list variable with support for unassigned values, the placer will iterate indefinitely.
6564
// (When it exhausts all values, it will start over from the beginning.)
6665
// To prevent that, we need to limit the number of steps to the number of unassigned values.
6766
var workingSolution = phaseScope.getWorkingSolution();
68-
maxStepCount = listVariableDescriptor.countUnassigned(workingSolution);
67+
maxStepCount = solutionDescriptor.getListVariableDescriptor().countUnassigned(workingSolution);
6968
}
7069

7170
TerminationStatus earlyTerminationStatus = null;
@@ -102,7 +101,7 @@ public void solve(SolverScope<Solution_> solverScope) {
102101
doStep(stepScope);
103102
stepEnded(stepScope);
104103
phaseScope.setLastCompletedStepScope(stepScope);
105-
if (hasOnlyListVariable && stepScope.getStepIndex() >= maxStepCount) {
104+
if (hasListVariable && stepScope.getStepIndex() >= maxStepCount) {
106105
earlyTerminationStatus = TerminationStatus.regular(phaseScope.getNextStepIndex());
107106
break;
108107
} else if (phaseTermination.isPhaseTerminated(phaseScope)) {

core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/QueuedMultiplePlacer.java

Lines changed: 73 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package ai.timefold.solver.core.impl.constructionheuristic.placer;
22

33
import java.util.ArrayList;
4+
import java.util.Arrays;
45
import java.util.Iterator;
56
import java.util.List;
67

@@ -47,13 +48,26 @@ private class MultipleQueuedPlacingIterator extends UpcomingSelectionIterator<Pl
4748
implements PhaseLifecycleListener<Solution_> {
4849

4950
private final List<EntityPlacer<Solution_>> queuedPlacerList;
50-
private List<Iterator<Placement<Solution_>>> placementIteratorList;
51-
private List<Iterator<Move<Solution_>>> moveIteratorList;
51+
private Iterator<Move<Solution_>>[] moveIterators;
52+
private Iterator<Placement<Solution_>>[] placementIterators;
5253
private Move<Solution_>[] previousMove;
5354
private Move<Solution_> cachedMove = null;
5455

5556
private MultipleQueuedPlacingIterator(List<EntityPlacer<Solution_>> queuedPlacerList) {
56-
this.queuedPlacerList = new ArrayList<>(queuedPlacerList);
57+
// We expect only the QueuedValuePlacer and a QueuedEntityPlacer
58+
var assertSize = queuedPlacerList.size() == 2;
59+
var assertQueuedValuePlacer =
60+
queuedPlacerList.stream().anyMatch(QueuedValuePlacer.class::isInstance);
61+
var assertQueuedEntityPlacer =
62+
queuedPlacerList.stream().anyMatch(QueuedEntityPlacer.class::isInstance);
63+
if (!assertSize || !assertQueuedValuePlacer || !assertQueuedEntityPlacer) {
64+
throw new IllegalArgumentException(
65+
"Impossible state: the queued placer list must consist exclusively of a QueuedValuePlacer and a QueuedEntityPlacer.");
66+
}
67+
this.queuedPlacerList = new ArrayList<>();
68+
// We make sure that the QueuedEntityPlacer is added first
69+
this.queuedPlacerList.addAll(queuedPlacerList.stream().filter(QueuedValuePlacer.class::isInstance).toList());
70+
this.queuedPlacerList.addAll(queuedPlacerList.stream().filter(QueuedEntityPlacer.class::isInstance).toList());
5771
reset();
5872
}
5973

@@ -62,27 +76,28 @@ private MultipleQueuedPlacingIterator(List<EntityPlacer<Solution_>> queuedPlacer
6276
* but it uses placer iterators instead.
6377
*/
6478
private Move<Solution_> nextMove() {
65-
if (cachedMove == null) {
66-
var childSize = moveIteratorList.size();
67-
int index;
68-
Move<Solution_>[] move = new Move[childSize];
69-
if (previousMove == null) {
70-
index = -1;
71-
} else {
72-
index = consumeNextMove(move, previousMove);
73-
if (index == -1) {
74-
// No more moves
75-
return null;
76-
}
77-
}
78-
var recreated = recreateNextIterators(index, move);
79-
if (!recreated) {
80-
// We stop if one of the placement iterators has no next placement
79+
if (cachedMove != null) {
80+
return cachedMove;
81+
}
82+
var childSize = moveIterators.length;
83+
int index;
84+
Move<Solution_>[] move = new Move[childSize];
85+
if (previousMove == null) {
86+
index = -1;
87+
} else {
88+
index = consumeNextMove(move, previousMove);
89+
if (index == -1) {
90+
// No more moves
8191
return null;
8292
}
83-
previousMove = move;
84-
cachedMove = CompositeMove.buildMove(move);
8593
}
94+
var updatedMove = updateNextIterators(index, move);
95+
if (updatedMove == null) {
96+
// We stop if one of the placement iterators has no next placement
97+
return null;
98+
}
99+
previousMove = updatedMove;
100+
cachedMove = CompositeMove.buildMove(updatedMove);
86101
return cachedMove;
87102
}
88103

@@ -98,10 +113,21 @@ private int consumeNextMove(Move<?>[] move, Move<Solution_>[] previousMove) {
98113
var index = move.length - 1;
99114
// Look for the first iterator that still has available moves to generate
100115
while (index >= 0) {
101-
var moveIterator = moveIteratorList.get(index);
116+
var moveIterator = moveIterators[index];
102117
if (moveIterator.hasNext()) {
103118
break;
104119
}
120+
// Check if there are more placements available in the QueuedEntityPlacer
121+
if (index == 1) {
122+
var placementIterator = placementIterators[index];
123+
if (placementIterator.hasNext()) {
124+
moveIterators[index] = placementIterator.next().iterator();
125+
continue;
126+
} else {
127+
// Reset the iterator in case the previous placerIterator still has more placements
128+
placementIterators[index] = queuedPlacerList.get(index).iterator();
129+
}
130+
}
105131
index--;
106132
}
107133
if (index < 0) {
@@ -110,43 +136,53 @@ private int consumeNextMove(Move<?>[] move, Move<Solution_>[] previousMove) {
110136
// Copy the previous move until the next one generated
111137
System.arraycopy(previousMove, 0, move, 0, index);
112138
// Generate and set the new move
113-
move[index] = moveIteratorList.get(index).next();
139+
move[index] = moveIterators[index].next();
114140
return index;
115141
}
116142

117143
/**
118-
* Recreate all move iterators starting from #lastValidIteratorIndex.
144+
* Update the move list and recreate all move iterators starting from #lastValidIteratorIndex.
119145
*
120146
* @param lastValidIteratorIndex the index of the last iterator that generated a valid move
121147
* @param move the move array to be loaded
122148
*/
123-
private boolean recreateNextIterators(int lastValidIteratorIndex, Move<?>[] move) {
124-
for (int i = lastValidIteratorIndex + 1; i < move.length; i++) {
125-
var placementIterator = queuedPlacerList.get(i).iterator();
126-
placementIteratorList.set(i, placementIterator);
149+
private Move<Solution_>[] updateNextIterators(int lastValidIteratorIndex, Move<?>[] move) {
150+
var childSize = moveIterators.length;
151+
var updatedMove = new Move[childSize];
152+
System.arraycopy(move, 0, updatedMove, 0, childSize);
153+
for (int i = lastValidIteratorIndex + 1; i < childSize; i++) {
154+
var placementIterator = placementIterators[i];
127155
Move<Solution_> next;
128156
if (!placementIterator.hasNext()) {
129-
return false;
157+
return null;
130158
} else {
131159
var moveIterator = placementIterator.next().iterator();
132-
moveIteratorList.set(i, moveIterator);
160+
moveIterators[i] = moveIterator;
133161
next = moveIterator.next();
134162
}
135-
move[i] = next;
163+
updatedMove[i] = next;
136164
}
137-
return true;
165+
return updatedMove;
138166
}
139167

140168
private void clearCache() {
141169
this.cachedMove = null;
142170
}
143171

172+
@SuppressWarnings("unchecked")
144173
private void reset() {
145-
placementIteratorList = new ArrayList<>(queuedPlacerList.size());
146-
moveIteratorList = new ArrayList<>(queuedPlacerList.size());
147-
for (EntityPlacer<Solution_> placements : queuedPlacerList) {
148-
moveIteratorList.add(null);
149-
placementIteratorList.add(null);
174+
if (moveIterators == null) {
175+
moveIterators = new Iterator[queuedPlacerList.size()];
176+
Arrays.fill(moveIterators, null);
177+
placementIterators = new Iterator[queuedPlacerList.size()];
178+
for (var i = 0; i < queuedPlacerList.size(); i++) {
179+
var placement = queuedPlacerList.get(i);
180+
placementIterators[i] = placement.iterator();
181+
}
182+
} else {
183+
Arrays.fill(moveIterators, null);
184+
// We need to reset of the QueuedEntityPlacer or there will be no more moves for the basic variables
185+
placementIterators[1] = queuedPlacerList.get(1).iterator();
150186
}
151187
previousMove = null;
152188
}

core/src/main/java/ai/timefold/solver/core/impl/domain/entity/descriptor/EntityDescriptor.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -707,6 +707,16 @@ public boolean hasAnyGenuineBasicVariables() {
707707
.anyMatch(descriptor -> !descriptor.isListVariable());
708708
}
709709

710+
public boolean hasAnyGenuineChainedVariables() {
711+
if (!isGenuine()) {
712+
return false;
713+
}
714+
return getDeclaredGenuineVariableDescriptors().stream()
715+
.filter(descriptor -> descriptor instanceof BasicVariableDescriptor<Solution_>)
716+
.map(descriptor -> (BasicVariableDescriptor<Solution_>) descriptor)
717+
.anyMatch(BasicVariableDescriptor::isChained);
718+
}
719+
710720
public boolean hasAnyGenuineListVariables() {
711721
if (!isGenuine()) {
712722
return false;

core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/SolutionDescriptor.java

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -727,13 +727,9 @@ private void validateListVariableDescriptors() {
727727

728728
var listVariableDescriptor = listVariableDescriptorList.get(0);
729729
var listVariableEntityDescriptor = listVariableDescriptor.getEntityDescriptor();
730-
var hasChained = listVariableEntityDescriptor.getGenuineVariableDescriptorList().stream()
731-
.anyMatch(
732-
variableDescriptor -> variableDescriptor instanceof BasicVariableDescriptor<Solution_> basicVariableDescriptor
733-
&& basicVariableDescriptor.isChained());
734730
// We will not support chained and list variables at the same entity,
735731
// and the validation can be removed once we discontinue support for chained variables.
736-
if (hasChained) {
732+
if (hasChainedVariable()) {
737733
var basicVariableDescriptorList = new ArrayList<>(listVariableEntityDescriptor.getGenuineVariableDescriptorList());
738734
basicVariableDescriptorList.remove(listVariableDescriptor);
739735
throw new UnsupportedOperationException(
@@ -883,8 +879,20 @@ public PlanningSolutionMetaModel<Solution_> getMetaModel() {
883879
return planningSolutionMetaModel;
884880
}
885881

882+
public boolean hasBasicVariable() {
883+
return getGenuineEntityDescriptors().stream().anyMatch(EntityDescriptor::hasAnyGenuineBasicVariables);
884+
}
885+
886+
public boolean hasChainedVariable() {
887+
return getGenuineEntityDescriptors().stream().anyMatch(EntityDescriptor::hasAnyGenuineChainedVariables);
888+
}
889+
890+
public boolean hasListVariable() {
891+
return getListVariableDescriptor() != null;
892+
}
893+
886894
public boolean hasBothBasicAndListVariables() {
887-
return getMetaModel().hasBothBasicAndListVariables();
895+
return hasBasicVariable() && hasListVariable();
888896
}
889897

890898
/**

core/src/main/java/ai/timefold/solver/core/impl/domain/variable/anchor/AnchorShadowVariableDescriptor.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,9 @@ public Iterable<VariableListenerWithSources<Solution_>> buildVariableListeners(S
9292
sourceVariableDescriptor).toCollection();
9393
}
9494

95+
@Override
96+
public boolean isListVariableSource() {
97+
return false;
98+
}
99+
95100
}

core/src/main/java/ai/timefold/solver/core/impl/domain/variable/cascade/CascadingUpdateShadowVariableDescriptor.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,11 @@ public Iterable<VariableListenerWithSources<Solution_>> buildVariableListeners(S
165165
throw new UnsupportedOperationException("Cascade update element generates no listeners.");
166166
}
167167

168+
@Override
169+
public boolean isListVariableSource() {
170+
return false;
171+
}
172+
168173
private record ShadowVariableTarget<Solution_>(EntityDescriptor<Solution_> entityDescriptor,
169174
MemberAccessor variableMemberAccessor) {
170175

core/src/main/java/ai/timefold/solver/core/impl/domain/variable/custom/CustomShadowVariableDescriptor.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,4 +148,9 @@ public Iterable<VariableListenerWithSources<Solution_>> buildVariableListeners(S
148148
return new VariableListenerWithSources<>(variableListener, classListEntry.getValue());
149149
}).collect(Collectors.toList());
150150
}
151+
152+
@Override
153+
public boolean isListVariableSource() {
154+
return false;
155+
}
151156
}

core/src/main/java/ai/timefold/solver/core/impl/domain/variable/custom/LegacyCustomShadowVariableDescriptor.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,4 +231,9 @@ public Iterable<VariableListenerWithSources<Solution_>> buildVariableListeners(S
231231
return new VariableListenerWithSources<>(variableListener, sourceVariableDescriptorList).toCollection();
232232
}
233233

234+
@Override
235+
public boolean isListVariableSource() {
236+
return false;
237+
}
238+
234239
}

core/src/main/java/ai/timefold/solver/core/impl/domain/variable/custom/PiggybackShadowVariableDescriptor.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,4 +110,9 @@ public Iterable<VariableListenerWithSources<Solution_>> buildVariableListeners(S
110110
throw new UnsupportedOperationException("The piggybackShadowVariableDescriptor (" + this
111111
+ ") cannot build a variable listener.");
112112
}
113+
114+
@Override
115+
public boolean isListVariableSource() {
116+
return false;
117+
}
113118
}

0 commit comments

Comments
 (0)