Skip to content

Commit 02e2c8b

Browse files
committed
fix: fully notify neighborhoods when internal state changes
1 parent 64f2cf0 commit 02e2c8b

File tree

34 files changed

+562
-348
lines changed

34 files changed

+562
-348
lines changed

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

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,29 +13,31 @@ public ExternalizedIndexVariableProcessor(IndexShadowVariableDescriptor<Solution
1313
this.shadowVariableDescriptor = shadowVariableDescriptor;
1414
}
1515

16-
public void addElement(InnerScoreDirector<Solution_, ?> scoreDirector, Object element, Integer index) {
17-
updateIndex(scoreDirector, element, index);
16+
public boolean addElement(InnerScoreDirector<Solution_, ?> scoreDirector, Object element, Integer index) {
17+
return updateIndex(scoreDirector, element, index);
1818
}
1919

20-
public void removeElement(InnerScoreDirector<Solution_, ?> scoreDirector, Object element) {
21-
updateIndex(scoreDirector, element, null);
20+
public boolean removeElement(InnerScoreDirector<Solution_, ?> scoreDirector, Object element) {
21+
return updateIndex(scoreDirector, element, null);
2222
}
2323

24-
public void unassignElement(InnerScoreDirector<Solution_, ?> scoreDirector, Object element) {
25-
removeElement(scoreDirector, element);
24+
public boolean unassignElement(InnerScoreDirector<Solution_, ?> scoreDirector, Object element) {
25+
return removeElement(scoreDirector, element);
2626
}
2727

28-
public void changeElement(InnerScoreDirector<Solution_, ?> scoreDirector, Object element, Integer index) {
29-
updateIndex(scoreDirector, element, index);
28+
public boolean changeElement(InnerScoreDirector<Solution_, ?> scoreDirector, Object element, Integer index) {
29+
return updateIndex(scoreDirector, element, index);
3030
}
3131

32-
private void updateIndex(InnerScoreDirector<Solution_, ?> scoreDirector, Object element, Integer index) {
32+
private boolean updateIndex(InnerScoreDirector<Solution_, ?> scoreDirector, Object element, Integer index) {
3333
var oldIndex = getIndex(element);
3434
if (!Objects.equals(oldIndex, index)) {
3535
scoreDirector.beforeVariableChanged(shadowVariableDescriptor, element);
3636
shadowVariableDescriptor.setValue(element, index);
3737
scoreDirector.afterVariableChanged(shadowVariableDescriptor, element);
38+
return true;
3839
}
40+
return false;
3941
}
4042

4143
public Integer getIndex(Object planningValue) {

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

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,15 @@ public ExternalizedListInverseVariableProcessor(
1616
this.sourceVariableDescriptor = sourceVariableDescriptor;
1717
}
1818

19-
public void addElement(InnerScoreDirector<Solution_, ?> scoreDirector, Object entity, Object element) {
20-
setInverseAsserted(scoreDirector, element, entity, null);
19+
public boolean addElement(InnerScoreDirector<Solution_, ?> scoreDirector, Object entity, Object element) {
20+
return setInverseAsserted(scoreDirector, element, entity, null);
2121
}
2222

23-
private void setInverseAsserted(InnerScoreDirector<Solution_, ?> scoreDirector, Object element, Object inverseEntity,
23+
private boolean setInverseAsserted(InnerScoreDirector<Solution_, ?> scoreDirector, Object element, Object inverseEntity,
2424
Object expectedOldInverseEntity) {
2525
var oldInverseEntity = getInverseSingleton(element);
2626
if (oldInverseEntity == inverseEntity) {
27-
return;
27+
return false;
2828
}
2929
if (scoreDirector.expectShadowVariablesInCorrectState() && oldInverseEntity != expectedOldInverseEntity) {
3030
throw new IllegalStateException("""
@@ -34,27 +34,25 @@ has an oldInverseEntity (%s) which is not that entity.
3434
.formatted(inverseEntity, sourceVariableDescriptor.getVariableName(), element,
3535
shadowVariableDescriptor.getVariableName(), oldInverseEntity));
3636
}
37-
setInverse(scoreDirector, inverseEntity, element);
37+
return setInverse(scoreDirector, inverseEntity, element);
3838
}
3939

40-
private void setInverse(InnerScoreDirector<Solution_, ?> scoreDirector, Object entity, Object element) {
40+
private boolean setInverse(InnerScoreDirector<Solution_, ?> scoreDirector, Object entity, Object element) {
4141
scoreDirector.beforeVariableChanged(shadowVariableDescriptor, element);
4242
shadowVariableDescriptor.setValue(element, entity);
4343
scoreDirector.afterVariableChanged(shadowVariableDescriptor, element);
44+
return true;
4445
}
4546

46-
public void removeElement(InnerScoreDirector<Solution_, ?> scoreDirector, Object entity, Object element) {
47-
setInverseAsserted(scoreDirector, element, null, entity);
47+
public boolean unassignElement(InnerScoreDirector<Solution_, ?> scoreDirector, Object element) {
48+
return changeElement(scoreDirector, null, element);
4849
}
4950

50-
public void unassignElement(InnerScoreDirector<Solution_, ?> scoreDirector, Object element) {
51-
changeElement(scoreDirector, null, element);
52-
}
53-
54-
public void changeElement(InnerScoreDirector<Solution_, ?> scoreDirector, Object entity, Object element) {
51+
public boolean changeElement(InnerScoreDirector<Solution_, ?> scoreDirector, Object entity, Object element) {
5552
if (getInverseSingleton(element) != entity) {
56-
setInverse(scoreDirector, entity, element);
53+
return setInverse(scoreDirector, entity, element);
5754
}
55+
return false;
5856
}
5957

6058
public Object getInverseSingleton(Object planningValue) {

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package ai.timefold.solver.core.impl.domain.variable;
22

33
import java.util.Objects;
4+
import java.util.function.Consumer;
45

56
import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor;
67
import ai.timefold.solver.core.impl.domain.variable.index.IndexShadowVariableDescriptor;
@@ -27,9 +28,11 @@ final class ExternalizedListVariableStateSupply<Solution_, Entity_>
2728
@Nullable
2829
private Solution_ workingSolution;
2930

30-
public ExternalizedListVariableStateSupply(ListVariableDescriptor<Solution_> sourceVariableDescriptor) {
31+
public ExternalizedListVariableStateSupply(ListVariableDescriptor<Solution_> sourceVariableDescriptor,
32+
Consumer<Object> notifier) {
3133
this.sourceVariableDescriptor = sourceVariableDescriptor;
32-
this.listVariableState = new ListVariableState<>(sourceVariableDescriptor);
34+
this.listVariableState = new ListVariableState<>(sourceVariableDescriptor,
35+
notifier);
3336
}
3437

3538
@Override

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

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,30 +29,32 @@ private ExternalizedNextPrevElementVariableProcessor(ShadowVariableDescriptor<So
2929
this.modifier = modifier;
3030
}
3131

32-
public void setElement(InnerScoreDirector<Solution_, ?> scoreDirector, List<Object> listVariable, Object element,
32+
public boolean setElement(InnerScoreDirector<Solution_, ?> scoreDirector, List<Object> listVariable, Object element,
3333
int index) {
3434
var target = index + modifier;
3535
if (target < 0 || target >= listVariable.size()) {
36-
setValue(scoreDirector, element, null);
36+
return setValue(scoreDirector, element, null);
3737
} else {
38-
setValue(scoreDirector, element, listVariable.get(target));
38+
return setValue(scoreDirector, element, listVariable.get(target));
3939
}
4040
}
4141

42-
private void setValue(InnerScoreDirector<Solution_, ?> scoreDirector, Object element, Object value) {
42+
private boolean setValue(InnerScoreDirector<Solution_, ?> scoreDirector, Object element, Object value) {
4343
if (getElement(element) != value) {
4444
scoreDirector.beforeVariableChanged(shadowVariableDescriptor, element);
4545
shadowVariableDescriptor.setValue(element, value);
4646
scoreDirector.afterVariableChanged(shadowVariableDescriptor, element);
47+
return true;
4748
}
49+
return false;
4850
}
4951

5052
public Object getElement(Object element) {
5153
return shadowVariableDescriptor.getValue(element);
5254
}
5355

54-
public void unsetElement(InnerScoreDirector<Solution_, ?> scoreDirector, Object element) {
55-
setValue(scoreDirector, element, null);
56+
public boolean unsetElement(InnerScoreDirector<Solution_, ?> scoreDirector, Object element) {
57+
return setValue(scoreDirector, element, null);
5658
}
5759

5860
}

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

Lines changed: 35 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import java.util.List;
44
import java.util.Map;
5+
import java.util.function.Consumer;
56

67
import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor;
78
import ai.timefold.solver.core.impl.domain.variable.index.IndexShadowVariableDescriptor;
@@ -16,6 +17,7 @@
1617
final class ListVariableState<Solution_> {
1718

1819
private final ListVariableDescriptor<Solution_> sourceVariableDescriptor;
20+
private final Consumer<Object> notifier;
1921

2022
private ExternalizedIndexVariableProcessor<Solution_> externalizedIndexProcessor = null;
2123
private ExternalizedListInverseVariableProcessor<Solution_> externalizedInverseProcessor = null;
@@ -27,8 +29,10 @@ final class ListVariableState<Solution_> {
2729
private int unassignedCount = 0;
2830
private Map<Object, MutablePosition> elementPositionMap;
2931

30-
public ListVariableState(ListVariableDescriptor<Solution_> sourceVariableDescriptor) {
32+
public ListVariableState(ListVariableDescriptor<Solution_> sourceVariableDescriptor,
33+
Consumer<Object> notifier) {
3134
this.sourceVariableDescriptor = sourceVariableDescriptor;
35+
this.notifier = notifier;
3236
}
3337

3438
public void linkShadowVariable(IndexShadowVariableDescriptor<Solution_> shadowVariableDescriptor) {
@@ -121,49 +125,26 @@ public void addElement(Object entity, List<Object> elements, Object element, int
121125
.formatted(sourceVariableDescriptor, element, index, oldPosition));
122126
}
123127
}
128+
var elementUpdateSent = false;
124129
if (externalizedIndexProcessor != null) {
125-
externalizedIndexProcessor.addElement(scoreDirector, element, index);
130+
elementUpdateSent = externalizedIndexProcessor.addElement(scoreDirector, element, index);
126131
}
127132
if (externalizedInverseProcessor != null) {
128-
externalizedInverseProcessor.addElement(scoreDirector, entity, element);
133+
elementUpdateSent = externalizedInverseProcessor.addElement(scoreDirector, entity, element) || elementUpdateSent;
129134
}
130135
if (externalizedPreviousElementProcessor != null) {
131-
externalizedPreviousElementProcessor.setElement(scoreDirector, elements, element, index);
136+
elementUpdateSent = externalizedPreviousElementProcessor.setElement(scoreDirector, elements, element, index)
137+
|| elementUpdateSent;
132138
}
133139
if (externalizedNextElementProcessor != null) {
134-
externalizedNextElementProcessor.setElement(scoreDirector, elements, element, index);
140+
elementUpdateSent =
141+
externalizedNextElementProcessor.setElement(scoreDirector, elements, element, index) || elementUpdateSent;
135142
}
136143
unassignedCount--;
137-
}
138-
139-
public void removeElement(Object entity, Object element, int index) {
140-
if (requiresPositionMap) {
141-
var oldPosition = elementPositionMap.remove(element);
142-
if (oldPosition == null) {
143-
throw new IllegalStateException(
144-
"The supply for list variable (%s) is corrupted, because the element (%s) at index (%d) was already unassigned (%s)."
145-
.formatted(sourceVariableDescriptor, element, index, oldPosition));
146-
}
147-
var oldIndex = oldPosition.getIndex();
148-
if (oldIndex != index) {
149-
throw new IllegalStateException(
150-
"The supply for list variable (%s) is corrupted, because the element (%s) at index (%d) had an old index (%d) which is not the current index (%d)."
151-
.formatted(sourceVariableDescriptor, element, index, oldIndex, index));
152-
}
144+
// Trigger notifier if none of the previous methods triggered a shadow var update for this element.
145+
if (!elementUpdateSent) {
146+
notifier.accept(element);
153147
}
154-
if (externalizedIndexProcessor != null) {
155-
externalizedIndexProcessor.removeElement(scoreDirector, element);
156-
}
157-
if (externalizedInverseProcessor != null) {
158-
externalizedInverseProcessor.removeElement(scoreDirector, entity, element);
159-
}
160-
if (externalizedPreviousElementProcessor != null) {
161-
externalizedPreviousElementProcessor.unsetElement(scoreDirector, element);
162-
}
163-
if (externalizedNextElementProcessor != null) {
164-
externalizedNextElementProcessor.unsetElement(scoreDirector, element);
165-
}
166-
unassignedCount++;
167148
}
168149

169150
public void unassignElement(Object element) {
@@ -175,37 +156,49 @@ public void unassignElement(Object element) {
175156
.formatted(sourceVariableDescriptor, element));
176157
}
177158
}
159+
var elementUpdateSent = false;
178160
if (externalizedIndexProcessor != null) {
179-
externalizedIndexProcessor.unassignElement(scoreDirector, element);
161+
elementUpdateSent = externalizedIndexProcessor.unassignElement(scoreDirector, element);
180162
}
181163
if (externalizedInverseProcessor != null) {
182-
externalizedInverseProcessor.unassignElement(scoreDirector, element);
164+
elementUpdateSent = externalizedInverseProcessor.unassignElement(scoreDirector, element) || elementUpdateSent;
183165
}
184166
if (externalizedPreviousElementProcessor != null) {
185-
externalizedPreviousElementProcessor.unsetElement(scoreDirector, element);
167+
elementUpdateSent = externalizedPreviousElementProcessor.unsetElement(scoreDirector, element) || elementUpdateSent;
186168
}
187169
if (externalizedNextElementProcessor != null) {
188-
externalizedNextElementProcessor.unsetElement(scoreDirector, element);
170+
elementUpdateSent = externalizedNextElementProcessor.unsetElement(scoreDirector, element) || elementUpdateSent;
189171
}
190172
unassignedCount++;
173+
// Trigger notifier if none of the previous methods triggered a shadow var update for this element.
174+
if (!elementUpdateSent) {
175+
notifier.accept(element);
176+
}
191177
}
192178

193179
public boolean changeElement(Object entity, List<Object> elements, int index) {
194180
var element = elements.get(index);
195181
var difference = processElementPosition(entity, element, index);
182+
var elementUpdateSent = false;
196183
if (difference.indexChanged && externalizedIndexProcessor != null) {
197-
externalizedIndexProcessor.changeElement(scoreDirector, element, index);
184+
elementUpdateSent = externalizedIndexProcessor.changeElement(scoreDirector, element, index);
198185
}
199186
if (difference.entityChanged && externalizedInverseProcessor != null) {
200-
externalizedInverseProcessor.changeElement(scoreDirector, entity, element);
187+
elementUpdateSent = externalizedInverseProcessor.changeElement(scoreDirector, entity, element) || elementUpdateSent;
201188
}
202189
// Next and previous still might have changed, even if the index and entity did not.
203190
// Those are based on what happened elsewhere in the list.
204191
if (externalizedPreviousElementProcessor != null) {
205-
externalizedPreviousElementProcessor.setElement(scoreDirector, elements, element, index);
192+
elementUpdateSent = externalizedPreviousElementProcessor.setElement(scoreDirector, elements, element, index)
193+
|| elementUpdateSent;
206194
}
207195
if (externalizedNextElementProcessor != null) {
208-
externalizedNextElementProcessor.setElement(scoreDirector, elements, element, index);
196+
elementUpdateSent =
197+
externalizedNextElementProcessor.setElement(scoreDirector, elements, element, index) || elementUpdateSent;
198+
}
199+
// Trigger notifier if none of the previous methods triggered a shadow var update for this element.
200+
if (!elementUpdateSent) {
201+
notifier.accept(element);
209202
}
210203
return difference.anythingChanged;
211204
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public ListVariableStateDemand(ListVariableDescriptor<Solution_> variableDescrip
1414
@Override
1515
public ListVariableStateSupply<Solution_, Object, Object> createExternalizedSupply(SupplyManager supplyManager) {
1616
var listVariableDescriptor = (ListVariableDescriptor<Solution_>) variableDescriptor;
17-
return new ExternalizedListVariableStateSupply<>(listVariableDescriptor);
17+
return new ExternalizedListVariableStateSupply<>(listVariableDescriptor, supplyManager.getStateChangeNotifier());
1818
}
1919

2020
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ public AnchorVariableDemand(VariableDescriptor<Solution_> sourceVariableDescript
2121
public AnchorVariableSupply createExternalizedSupply(SupplyManager supplyManager) {
2222
SingletonInverseVariableSupply inverseVariableSupply = supplyManager
2323
.demand(new SingletonInverseVariableDemand<>(variableDescriptor));
24-
return new ExternalizedAnchorVariableSupply<>(variableDescriptor, inverseVariableSupply);
24+
return new ExternalizedAnchorVariableSupply<>(variableDescriptor, inverseVariableSupply,
25+
supplyManager.getStateChangeNotifier());
2526
}
2627

2728
}

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import java.util.IdentityHashMap;
44
import java.util.Map;
5+
import java.util.function.Consumer;
56

67
import ai.timefold.solver.core.impl.domain.variable.BasicVariableChangeEvent;
78
import ai.timefold.solver.core.impl.domain.variable.descriptor.VariableDescriptor;
@@ -23,12 +24,15 @@ public class ExternalizedAnchorVariableSupply<Solution_> implements
2324
protected final VariableDescriptor<Solution_> previousVariableDescriptor;
2425
protected final SingletonInverseVariableSupply nextVariableSupply;
2526
protected final Map<Object, @Nullable Object> anchorMap;
27+
private final Consumer<Object> notifier;
2628

2729
public ExternalizedAnchorVariableSupply(VariableDescriptor<Solution_> previousVariableDescriptor,
28-
SingletonInverseVariableSupply nextVariableSupply) {
30+
SingletonInverseVariableSupply nextVariableSupply,
31+
Consumer<Object> notifier) {
2932
this.previousVariableDescriptor = previousVariableDescriptor;
3033
this.nextVariableSupply = nextVariableSupply;
3134
this.anchorMap = new IdentityHashMap<>();
35+
this.notifier = notifier;
3236
}
3337

3438
@Override
@@ -72,6 +76,7 @@ protected void insert(Object entity) {
7276
Object nextEntity = entity;
7377
while (nextEntity != null && anchorMap.get(nextEntity) != anchor) {
7478
anchorMap.put(nextEntity, anchor);
79+
notifier.accept(nextEntity);
7580
nextEntity = nextVariableSupply.getInverseSingleton(nextEntity);
7681
}
7782
}

core/src/main/java/ai/timefold/solver/core/impl/domain/variable/inverserelation/CollectionInverseVariableDemand.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public CollectionInverseVariableDemand(VariableDescriptor<Solution_> sourceVaria
2121

2222
@Override
2323
public CollectionInverseVariableSupply createExternalizedSupply(SupplyManager supplyManager) {
24-
return new ExternalizedCollectionInverseVariableSupply<>(variableDescriptor);
24+
return new ExternalizedCollectionInverseVariableSupply<>(variableDescriptor, supplyManager.getStateChangeNotifier());
2525
}
2626

2727
}

0 commit comments

Comments
 (0)