diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/AbstractExternalizedNextPrevElementVariableProcessor.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/AbstractExternalizedNextPrevElementVariableProcessor.java new file mode 100644 index 0000000000..304fa1fc93 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/AbstractExternalizedNextPrevElementVariableProcessor.java @@ -0,0 +1,36 @@ +package ai.timefold.solver.core.impl.domain.variable; + +import java.util.List; +import java.util.Objects; + +import ai.timefold.solver.core.impl.domain.variable.descriptor.ShadowVariableDescriptor; +import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; + +abstract sealed class AbstractExternalizedNextPrevElementVariableProcessor + permits ExternalizedNextElementVariableProcessor, ExternalizedPreviousElementVariableProcessor { + + protected final ShadowVariableDescriptor shadowVariableDescriptor; + + protected AbstractExternalizedNextPrevElementVariableProcessor( + ShadowVariableDescriptor shadowVariableDescriptor) { + this.shadowVariableDescriptor = Objects.requireNonNull(shadowVariableDescriptor); + } + + public abstract void setElement(InnerScoreDirector scoreDirector, List listVariable, Object element, + int index); + + public abstract Object getElement(Object element); + + public void unsetElement(InnerScoreDirector scoreDirector, Object element) { + setValue(scoreDirector, element, null); + } + + protected void setValue(InnerScoreDirector scoreDirector, Object element, Object value) { + if (getElement(element) != value) { + scoreDirector.beforeVariableChanged(shadowVariableDescriptor, element); + shadowVariableDescriptor.setValue(element, value); + scoreDirector.afterVariableChanged(shadowVariableDescriptor, element); + } + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/ExternalizedIndexVariableProcessor.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/ExternalizedIndexVariableProcessor.java new file mode 100644 index 0000000000..f3ece2ef83 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/ExternalizedIndexVariableProcessor.java @@ -0,0 +1,45 @@ +package ai.timefold.solver.core.impl.domain.variable; + +import java.util.Objects; + +import ai.timefold.solver.core.impl.domain.variable.index.IndexShadowVariableDescriptor; +import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; + +final class ExternalizedIndexVariableProcessor { + + private final IndexShadowVariableDescriptor shadowVariableDescriptor; + + public ExternalizedIndexVariableProcessor(IndexShadowVariableDescriptor shadowVariableDescriptor) { + this.shadowVariableDescriptor = shadowVariableDescriptor; + } + + public void addElement(InnerScoreDirector scoreDirector, Object element, Integer index) { + updateIndex(scoreDirector, element, index); + } + + public void removeElement(InnerScoreDirector scoreDirector, Object element) { + updateIndex(scoreDirector, element, null); + } + + public void unassignElement(InnerScoreDirector scoreDirector, Object element) { + removeElement(scoreDirector, element); + } + + public void changeElement(InnerScoreDirector scoreDirector, Object element, Integer index) { + updateIndex(scoreDirector, element, index); + } + + private void updateIndex(InnerScoreDirector scoreDirector, Object element, Integer index) { + var oldIndex = getIndex(element); + if (!Objects.equals(oldIndex, index)) { + scoreDirector.beforeVariableChanged(shadowVariableDescriptor, element); + shadowVariableDescriptor.setValue(element, index); + scoreDirector.afterVariableChanged(shadowVariableDescriptor, element); + } + } + + public Integer getIndex(Object planningValue) { + return shadowVariableDescriptor.getValue(planningValue); + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/ExternalizedListInverseVariableProcessor.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/ExternalizedListInverseVariableProcessor.java new file mode 100644 index 0000000000..889e587d2a --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/ExternalizedListInverseVariableProcessor.java @@ -0,0 +1,63 @@ +package ai.timefold.solver.core.impl.domain.variable; + +import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; +import ai.timefold.solver.core.impl.domain.variable.inverserelation.InverseRelationShadowVariableDescriptor; +import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; + +final class ExternalizedListInverseVariableProcessor { + + private final InverseRelationShadowVariableDescriptor shadowVariableDescriptor; + private final ListVariableDescriptor sourceVariableDescriptor; + + public ExternalizedListInverseVariableProcessor( + InverseRelationShadowVariableDescriptor shadowVariableDescriptor, + ListVariableDescriptor sourceVariableDescriptor) { + this.shadowVariableDescriptor = shadowVariableDescriptor; + this.sourceVariableDescriptor = sourceVariableDescriptor; + } + + public void addElement(InnerScoreDirector scoreDirector, Object entity, Object element) { + setInverseAsserted(scoreDirector, element, entity, null); + } + + private void setInverseAsserted(InnerScoreDirector scoreDirector, Object element, Object inverseEntity, + Object expectedOldInverseEntity) { + var oldInverseEntity = getInverseSingleton(element); + if (oldInverseEntity == inverseEntity) { + return; + } + if (scoreDirector.expectShadowVariablesInCorrectState() && oldInverseEntity != expectedOldInverseEntity) { + throw new IllegalStateException(""" + The entity (%s) has a list variable (%s) and one of its elements (%s) which has a shadow variable (%s) \ + has an oldInverseEntity (%s) which is not that entity. + Verify the consistency of your input problem for that shadow variable.""" + .formatted(inverseEntity, sourceVariableDescriptor.getVariableName(), element, + shadowVariableDescriptor.getVariableName(), oldInverseEntity)); + } + setInverse(scoreDirector, inverseEntity, element); + } + + private void setInverse(InnerScoreDirector scoreDirector, Object entity, Object element) { + scoreDirector.beforeVariableChanged(shadowVariableDescriptor, element); + shadowVariableDescriptor.setValue(element, entity); + scoreDirector.afterVariableChanged(shadowVariableDescriptor, element); + } + + public void removeElement(InnerScoreDirector scoreDirector, Object entity, Object element) { + setInverseAsserted(scoreDirector, element, null, entity); + } + + public void unassignElement(InnerScoreDirector scoreDirector, Object element) { + changeElement(scoreDirector, null, element); + } + + public void changeElement(InnerScoreDirector scoreDirector, Object entity, Object element) { + if (getInverseSingleton(element) != entity) { + setInverse(scoreDirector, entity, element); + } + } + + public Object getInverseSingleton(Object planningValue) { + return shadowVariableDescriptor.getValue(planningValue); + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/ExternalizedListVariableStateSupply.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/ExternalizedListVariableStateSupply.java index e85296773f..041d44397c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/ExternalizedListVariableStateSupply.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/ExternalizedListVariableStateSupply.java @@ -1,13 +1,13 @@ package ai.timefold.solver.core.impl.domain.variable; -import java.util.IdentityHashMap; -import java.util.Map; -import java.util.Objects; - import ai.timefold.solver.core.api.score.director.ScoreDirector; import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; +import ai.timefold.solver.core.impl.domain.variable.index.IndexShadowVariableDescriptor; +import ai.timefold.solver.core.impl.domain.variable.inverserelation.InverseRelationShadowVariableDescriptor; +import ai.timefold.solver.core.impl.domain.variable.nextprev.NextElementShadowVariableDescriptor; +import ai.timefold.solver.core.impl.domain.variable.nextprev.PreviousElementShadowVariableDescriptor; +import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; import ai.timefold.solver.core.preview.api.domain.metamodel.ElementLocation; -import ai.timefold.solver.core.preview.api.domain.metamodel.LocationInList; import org.jspecify.annotations.NonNull; @@ -15,161 +15,147 @@ final class ExternalizedListVariableStateSupply implements ListVariableStateSupply { private final ListVariableDescriptor sourceVariableDescriptor; - private Map elementLocationMap; - private int unassignedCount; + private final ListVariableState listVariableState; + + private boolean previousExternalized = false; + private boolean nextExternalized = false; public ExternalizedListVariableStateSupply(ListVariableDescriptor sourceVariableDescriptor) { this.sourceVariableDescriptor = sourceVariableDescriptor; + this.listVariableState = new ListVariableState<>(sourceVariableDescriptor); + } + + @Override + public void externalize(IndexShadowVariableDescriptor shadowVariableDescriptor) { + listVariableState.linkDescriptor(shadowVariableDescriptor); + } + + @Override + public void externalize(InverseRelationShadowVariableDescriptor shadowVariableDescriptor) { + listVariableState.linkDescriptor(shadowVariableDescriptor); + } + + @Override + public void externalize(PreviousElementShadowVariableDescriptor shadowVariableDescriptor) { + listVariableState.linkDescriptor(shadowVariableDescriptor); + previousExternalized = true; + } + + @Override + public void externalize(NextElementShadowVariableDescriptor shadowVariableDescriptor) { + listVariableState.linkDescriptor(shadowVariableDescriptor); + nextExternalized = true; } @Override public void resetWorkingSolution(@NonNull ScoreDirector scoreDirector) { + listVariableState.initialize((InnerScoreDirector) scoreDirector, + (int) sourceVariableDescriptor.getValueRangeSize(scoreDirector.getWorkingSolution(), null)); var workingSolution = scoreDirector.getWorkingSolution(); - if (elementLocationMap == null) { - elementLocationMap = new IdentityHashMap<>((int) sourceVariableDescriptor.getValueRangeSize(workingSolution, null)); - } else { - elementLocationMap.clear(); - } - // Start with everything unassigned. - unassignedCount = (int) sourceVariableDescriptor.getValueRangeSize(workingSolution, null); // Will run over all entities and unmark all present elements as unassigned. - sourceVariableDescriptor.getEntityDescriptor().visitAllEntities(workingSolution, this::insert); + sourceVariableDescriptor.getEntityDescriptor() + .visitAllEntities(workingSolution, this::insert); } private void insert(Object entity) { var assignedElements = sourceVariableDescriptor.getValue(entity); var index = 0; for (var element : assignedElements) { - var oldLocation = elementLocationMap.put(element, ElementLocation.of(entity, index)); - if (oldLocation != null) { - throw new IllegalStateException( - "The supply (%s) is corrupted, because the element (%s) at index (%d) already exists (%s)." - .formatted(this, element, index, oldLocation)); - } + listVariableState.addElement(entity, assignedElements, element, index); index++; - unassignedCount--; } } @Override - public void close() { - elementLocationMap = null; - } - - @Override - public void beforeEntityAdded(@NonNull ScoreDirector scoreDirector, @NonNull Object o) { + public void beforeEntityAdded(@NonNull ScoreDirector scoreDirector, @NonNull Object entity) { // No need to do anything. } @Override - public void afterEntityAdded(@NonNull ScoreDirector scoreDirector, @NonNull Object o) { - insert(o); + public void afterEntityAdded(@NonNull ScoreDirector scoreDirector, @NonNull Object entity) { + insert(entity); } @Override - public void beforeEntityRemoved(@NonNull ScoreDirector scoreDirector, @NonNull Object o) { + public void beforeEntityRemoved(@NonNull ScoreDirector scoreDirector, @NonNull Object entity) { // No need to do anything. } @Override - public void afterEntityRemoved(@NonNull ScoreDirector scoreDirector, @NonNull Object o) { + public void afterEntityRemoved(@NonNull ScoreDirector scoreDirector, @NonNull Object entity) { // When the entity is removed, its values become unassigned. // An unassigned value has no inverse entity and no index. - retract(o); - } - - private void retract(Object entity) { var assignedElements = sourceVariableDescriptor.getValue(entity); for (var index = 0; index < assignedElements.size(); index++) { - var element = assignedElements.get(index); - var oldElementLocation = elementLocationMap.remove(element); - if (oldElementLocation == null) { - throw new IllegalStateException( - "The supply (%s) is corrupted, because the element (%s) at index (%d) was already unassigned (%s)." - .formatted(this, element, index, oldElementLocation)); - } - var oldIndex = oldElementLocation.index(); - if (oldIndex != index) { - throw new IllegalStateException( - "The supply (%s) is corrupted, because the element (%s) at index (%d) had an old index (%d) which is not the current index (%d)." - .formatted(this, element, index, oldIndex, index)); - } - unassignedCount++; + listVariableState.removeElement(entity, assignedElements.get(index), index); } } @Override public void afterListVariableElementUnassigned(@NonNull ScoreDirector scoreDirector, @NonNull Object element) { - var oldLocation = elementLocationMap.remove(element); - if (oldLocation == null) { - throw new IllegalStateException( - "The supply (%s) is corrupted, because the element (%s) did not exist before unassigning." - .formatted(this, element)); - } - unassignedCount++; + listVariableState.unassignElement(element); } @Override - public void beforeListVariableChanged(@NonNull ScoreDirector scoreDirector, @NonNull Object o, int fromIndex, - int toIndex) { + public void beforeListVariableChanged(@NonNull ScoreDirector scoreDirector, @NonNull Object entity, + int fromIndex, int toIndex) { // No need to do anything. } @Override - public void afterListVariableChanged(@NonNull ScoreDirector scoreDirector, @NonNull Object o, int fromIndex, + public void afterListVariableChanged(@NonNull ScoreDirector scoreDirector, @NonNull Object entity, int fromIndex, int toIndex) { - updateIndexes(o, fromIndex, toIndex); - } - - private void updateIndexes(Object entity, int startIndex, int toIndex) { var assignedElements = sourceVariableDescriptor.getValue(entity); - for (var index = startIndex; index < assignedElements.size(); index++) { - var element = assignedElements.get(index); - var newLocation = ElementLocation.of(entity, index); - var oldLocation = elementLocationMap.put(element, newLocation); - if (oldLocation == null) { - unassignedCount--; - } else if (index >= toIndex && newLocation.equals(oldLocation)) { + var elementCount = assignedElements.size(); + // Include the last element of the previous part of the list, if any, for the next element shadow var. + // But only if the next element shadow var is externalized; otherwise, there is nothing to update. + var firstChangeIndex = nextExternalized ? Math.max(0, fromIndex - 1) : fromIndex; + // Include the first element of the next part of the list, if any, for the previous element shadow var. + // But only if the previous element shadow var is externalized; otherwise, there is nothing to update. + var lastChangeIndex = previousExternalized ? Math.min(toIndex + 1, elementCount) : toIndex; + for (var index = firstChangeIndex; index < elementCount; index++) { + var locationsDiffer = listVariableState.changeElement(entity, assignedElements, index); + if (!locationsDiffer && index >= lastChangeIndex) { // Location is unchanged and we are past the part of the list that changed. + // We can terminate the loop prematurely. return; - } else { - // Continue to the next element. } } } @Override public ElementLocation getLocationInList(Object planningValue) { - return Objects.requireNonNullElse(elementLocationMap.get(Objects.requireNonNull(planningValue)), - ElementLocation.unassigned()); + return listVariableState.getLocationInList(planningValue); } @Override public Integer getIndex(Object planningValue) { - var elementLocation = elementLocationMap.get(Objects.requireNonNull(planningValue)); - if (elementLocation == null) { - return null; - } - return elementLocation.index(); + return listVariableState.getIndex(planningValue); } @Override public Object getInverseSingleton(Object planningValue) { - var elementLocation = elementLocationMap.get(Objects.requireNonNull(planningValue)); - if (elementLocation == null) { - return null; - } - return elementLocation.entity(); + return listVariableState.getInverseSingleton(planningValue); } @Override public boolean isAssigned(Object element) { - return getLocationInList(element) instanceof LocationInList; + return getInverseSingleton(element) != null; } @Override public int getUnassignedCount() { - return unassignedCount; + return listVariableState.getUnassignedCount(); + } + + @Override + public Object getPreviousElement(Object element) { + return listVariableState.getPreviousElement(element); + } + + @Override + public Object getNextElement(Object element) { + return listVariableState.getNextElement(element); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/ExternalizedNextElementVariableProcessor.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/ExternalizedNextElementVariableProcessor.java new file mode 100644 index 0000000000..7855980f7d --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/ExternalizedNextElementVariableProcessor.java @@ -0,0 +1,26 @@ +package ai.timefold.solver.core.impl.domain.variable; + +import java.util.List; + +import ai.timefold.solver.core.impl.domain.variable.nextprev.NextElementShadowVariableDescriptor; +import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; + +final class ExternalizedNextElementVariableProcessor + extends AbstractExternalizedNextPrevElementVariableProcessor { + + public ExternalizedNextElementVariableProcessor(NextElementShadowVariableDescriptor shadowVariableDescriptor) { + super(shadowVariableDescriptor); + } + + @Override + public void setElement(InnerScoreDirector scoreDirector, List listVariable, Object element, + int index) { + var next = index == listVariable.size() - 1 ? null : listVariable.get(index + 1); + setValue(scoreDirector, element, next); + } + + public Object getElement(Object element) { + return shadowVariableDescriptor.getValue(element); + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/ExternalizedPreviousElementVariableProcessor.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/ExternalizedPreviousElementVariableProcessor.java new file mode 100644 index 0000000000..5c2f309ef9 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/ExternalizedPreviousElementVariableProcessor.java @@ -0,0 +1,27 @@ +package ai.timefold.solver.core.impl.domain.variable; + +import java.util.List; + +import ai.timefold.solver.core.impl.domain.variable.nextprev.PreviousElementShadowVariableDescriptor; +import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; + +final class ExternalizedPreviousElementVariableProcessor + extends AbstractExternalizedNextPrevElementVariableProcessor { + + public ExternalizedPreviousElementVariableProcessor( + PreviousElementShadowVariableDescriptor shadowVariableDescriptor) { + super(shadowVariableDescriptor); + } + + @Override + public void setElement(InnerScoreDirector scoreDirector, List listVariable, Object element, + int index) { + var previous = index == 0 ? null : listVariable.get(index - 1); + setValue(scoreDirector, element, previous); + } + + public Object getElement(Object element) { + return shadowVariableDescriptor.getValue(element); + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/ListVariableElementStateSupply.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/ListVariableElementStateSupply.java deleted file mode 100644 index 1d27c5cd47..0000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/ListVariableElementStateSupply.java +++ /dev/null @@ -1,33 +0,0 @@ -package ai.timefold.solver.core.impl.domain.variable; - -import ai.timefold.solver.core.api.domain.variable.ListVariableListener; -import ai.timefold.solver.core.impl.domain.variable.listener.SourcedVariableListener; -import ai.timefold.solver.core.preview.api.domain.metamodel.ElementLocation; - -public interface ListVariableElementStateSupply extends - SourcedVariableListener, - ListVariableListener { - - /** - * - * @param element never null - * @return true if the element is contained in a list variable of any entity. - */ - boolean isAssigned(Object element); - - /** - * - * @param value never null - * @return never null - */ - ElementLocation getLocationInList(Object value); - - /** - * Consider colling this before {@link #isAssigned(Object)} to eliminate some map accesses. - * If unassigned count is 0, {@link #isAssigned(Object)} is guaranteed to return true. - * - * @return number of elements for which {@link #isAssigned(Object)} would return false. - */ - int getUnassignedCount(); - -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/ListVariableState.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/ListVariableState.java new file mode 100644 index 0000000000..613ee59f2f --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/ListVariableState.java @@ -0,0 +1,291 @@ +package ai.timefold.solver.core.impl.domain.variable; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; +import ai.timefold.solver.core.impl.domain.variable.index.IndexShadowVariableDescriptor; +import ai.timefold.solver.core.impl.domain.variable.inverserelation.InverseRelationShadowVariableDescriptor; +import ai.timefold.solver.core.impl.domain.variable.nextprev.NextElementShadowVariableDescriptor; +import ai.timefold.solver.core.impl.domain.variable.nextprev.PreviousElementShadowVariableDescriptor; +import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; +import ai.timefold.solver.core.impl.util.CollectionUtils; +import ai.timefold.solver.core.preview.api.domain.metamodel.ElementLocation; +import ai.timefold.solver.core.preview.api.domain.metamodel.LocationInList; + +final class ListVariableState { + + private final ListVariableDescriptor sourceVariableDescriptor; + + private ExternalizedIndexVariableProcessor externalizedIndexProcessor = null; + private ExternalizedListInverseVariableProcessor externalizedInverseProcessor = null; + private AbstractExternalizedNextPrevElementVariableProcessor externalizedPreviousElementProcessor = null; + private AbstractExternalizedNextPrevElementVariableProcessor externalizedNextElementProcessor = null; + + private boolean canUseExternalizedLocation = false; + private boolean requiresLocationMap = true; + private InnerScoreDirector scoreDirector; + private int unassignedCount = 0; + private Map elementLocationMap; + + public ListVariableState(ListVariableDescriptor sourceVariableDescriptor) { + this.sourceVariableDescriptor = sourceVariableDescriptor; + } + + public void linkDescriptor(IndexShadowVariableDescriptor shadowVariableDescriptor) { + this.externalizedIndexProcessor = new ExternalizedIndexVariableProcessor<>(shadowVariableDescriptor); + } + + public void linkDescriptor(InverseRelationShadowVariableDescriptor shadowVariableDescriptor) { + this.externalizedInverseProcessor = + new ExternalizedListInverseVariableProcessor<>(shadowVariableDescriptor, sourceVariableDescriptor); + } + + public void linkDescriptor(PreviousElementShadowVariableDescriptor shadowVariableDescriptor) { + this.externalizedPreviousElementProcessor = + new ExternalizedPreviousElementVariableProcessor<>(shadowVariableDescriptor); + } + + public void linkDescriptor(NextElementShadowVariableDescriptor shadowVariableDescriptor) { + this.externalizedNextElementProcessor = new ExternalizedNextElementVariableProcessor<>(shadowVariableDescriptor); + } + + public void initialize(InnerScoreDirector scoreDirector, int initialUnassignedCount) { + this.scoreDirector = scoreDirector; + this.unassignedCount = initialUnassignedCount; + + this.canUseExternalizedLocation = externalizedIndexProcessor != null && externalizedInverseProcessor != null; + this.requiresLocationMap = !canUseExternalizedLocation + || externalizedPreviousElementProcessor == null || externalizedNextElementProcessor == null; + if (requiresLocationMap) { + if (elementLocationMap == null) { + elementLocationMap = CollectionUtils.newIdentityHashMap(unassignedCount); + } else { + elementLocationMap.clear(); + } + } else { + elementLocationMap = null; + } + } + + public void addElement(Object entity, List elements, Object element, int index) { + if (requiresLocationMap) { + var location = ElementLocation.of(entity, index); + var oldLocation = elementLocationMap.put(element, location); + if (oldLocation != null) { + throw new IllegalStateException( + "The supply for list variable (%s) is corrupted, because the element (%s) at index (%d) already exists (%s)." + .formatted(sourceVariableDescriptor, element, index, oldLocation)); + } + } + if (externalizedIndexProcessor != null) { + externalizedIndexProcessor.addElement(scoreDirector, element, index); + } + if (externalizedInverseProcessor != null) { + externalizedInverseProcessor.addElement(scoreDirector, entity, element); + } + if (externalizedPreviousElementProcessor != null) { + externalizedPreviousElementProcessor.setElement(scoreDirector, elements, element, index); + } + if (externalizedNextElementProcessor != null) { + externalizedNextElementProcessor.setElement(scoreDirector, elements, element, index); + } + unassignedCount--; + } + + public void removeElement(Object entity, Object element, int index) { + if (requiresLocationMap) { + var oldElementLocation = elementLocationMap.remove(element); + if (oldElementLocation == null) { + throw new IllegalStateException( + "The supply for list variable (%s) is corrupted, because the element (%s) at index (%d) was already unassigned (%s)." + .formatted(sourceVariableDescriptor, element, index, oldElementLocation)); + } + var oldIndex = oldElementLocation.index(); + if (oldIndex != index) { + throw new IllegalStateException( + "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)." + .formatted(sourceVariableDescriptor, element, index, oldIndex, index)); + } + } + if (externalizedIndexProcessor != null) { + externalizedIndexProcessor.removeElement(scoreDirector, element); + } + if (externalizedInverseProcessor != null) { + externalizedInverseProcessor.removeElement(scoreDirector, entity, element); + } + if (externalizedPreviousElementProcessor != null) { + externalizedPreviousElementProcessor.unsetElement(scoreDirector, element); + } + if (externalizedNextElementProcessor != null) { + externalizedNextElementProcessor.unsetElement(scoreDirector, element); + } + unassignedCount++; + } + + public void unassignElement(Object element) { + if (requiresLocationMap) { + var oldLocation = elementLocationMap.remove(element); + if (oldLocation == null) { + throw new IllegalStateException( + "The supply for list variable (%s) is corrupted, because the element (%s) did not exist before unassigning." + .formatted(sourceVariableDescriptor, element)); + } + } + if (externalizedIndexProcessor != null) { + externalizedIndexProcessor.unassignElement(scoreDirector, element); + } + if (externalizedInverseProcessor != null) { + externalizedInverseProcessor.unassignElement(scoreDirector, element); + } + if (externalizedPreviousElementProcessor != null) { + externalizedPreviousElementProcessor.unsetElement(scoreDirector, element); + } + if (externalizedNextElementProcessor != null) { + externalizedNextElementProcessor.unsetElement(scoreDirector, element); + } + unassignedCount++; + } + + public boolean changeElement(Object entity, List elements, int index) { + var element = elements.get(index); + var difference = processElementLocation(entity, element, index); + if (difference.indexChanged && externalizedIndexProcessor != null) { + externalizedIndexProcessor.changeElement(scoreDirector, element, index); + } + if (difference.entityChanged && externalizedInverseProcessor != null) { + externalizedInverseProcessor.changeElement(scoreDirector, entity, element); + } + // Next and previous still might have changed, even if the index and entity did not. + // Those are based on what happened elsewhere in the list. + if (externalizedPreviousElementProcessor != null) { + externalizedPreviousElementProcessor.setElement(scoreDirector, elements, element, index); + } + if (externalizedNextElementProcessor != null) { + externalizedNextElementProcessor.setElement(scoreDirector, elements, element, index); + } + return difference.anythingChanged; + } + + private ChangeType processElementLocation(Object entity, Object element, int index) { + if (requiresLocationMap) { // Update the location and figure out if it is different from previous. + var newLocation = ElementLocation.of(entity, index); + var oldLocation = elementLocationMap.put(element, newLocation); + if (oldLocation == null) { + unassignedCount--; + return ChangeType.BOTH; + } + return compareLocations(entity, oldLocation.entity(), index, oldLocation.index()); + } else { // Read the location and figure out if it is different from previous. + var oldEntity = getInverseSingleton(element); + if (oldEntity == null) { + unassignedCount--; + return ChangeType.BOTH; + } + var oldIndex = getIndex(element); + if (oldIndex == null) { // Technically impossible, but we handle it anyway. + return ChangeType.BOTH; + } + return compareLocations(entity, oldEntity, index, oldIndex); + } + } + + private static ChangeType compareLocations(Object entity, Object otherEntity, int index, int otherIndex) { + if (entity != otherEntity) { + return ChangeType.BOTH; // Entity changed, so index changed too. + } else if (index != otherIndex) { + return ChangeType.INDEX; + } else { + return ChangeType.NEITHER; + } + } + + private enum ChangeType { + + BOTH(true, true), + INDEX(false, true), + NEITHER(false, false); + + final boolean anythingChanged; + final boolean entityChanged; + final boolean indexChanged; + + ChangeType(boolean entityChanged, boolean indexChanged) { + this.anythingChanged = entityChanged || indexChanged; + this.entityChanged = entityChanged; + this.indexChanged = indexChanged; + } + + } + + public ElementLocation getLocationInList(Object planningValue) { + if (!canUseExternalizedLocation) { + return Objects.requireNonNullElse(elementLocationMap.get(planningValue), ElementLocation.unassigned()); + } else { + var inverse = getInverseSingleton(planningValue); + if (inverse == null) { + return ElementLocation.unassigned(); + } + return ElementLocation.of(inverse, getIndex(planningValue)); + } + } + + public Integer getIndex(Object planningValue) { + if (externalizedIndexProcessor == null) { + var elementLocation = elementLocationMap.get(planningValue); + if (elementLocation == null) { + return null; + } + return elementLocation.index(); + } + return externalizedIndexProcessor.getIndex(planningValue); + } + + public Object getInverseSingleton(Object planningValue) { + if (externalizedInverseProcessor == null) { + var elementLocation = elementLocationMap.get(planningValue); + if (elementLocation == null) { + return null; + } + return elementLocation.entity(); + } + return externalizedInverseProcessor.getInverseSingleton(planningValue); + } + + public Object getPreviousElement(Object element) { + if (externalizedPreviousElementProcessor == null) { + var elementLocation = getLocationInList(element); + if (!(elementLocation instanceof LocationInList locationInList)) { + return null; + } + var index = locationInList.index(); + if (index == 0) { + return null; + } + return sourceVariableDescriptor.getValue(locationInList.entity()).get(index - 1); + } + return externalizedPreviousElementProcessor.getElement(element); + } + + public Object getNextElement(Object element) { + if (externalizedNextElementProcessor == null) { + var elementLocation = getLocationInList(element); + if (!(elementLocation instanceof LocationInList locationInList)) { + return null; + } + var list = sourceVariableDescriptor.getValue(locationInList.entity()); + var index = locationInList.index(); + if (index == list.size() - 1) { + return null; + } + return list.get(index + 1); + } + return externalizedNextElementProcessor.getElement(element); + } + + public int getUnassignedCount() { + return unassignedCount; + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/ListVariableStateSupply.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/ListVariableStateSupply.java index b673df378b..7292b303ed 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/ListVariableStateSupply.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/ListVariableStateSupply.java @@ -1,18 +1,91 @@ package ai.timefold.solver.core.impl.domain.variable; import ai.timefold.solver.core.api.domain.variable.ListVariableListener; +import ai.timefold.solver.core.api.domain.variable.PlanningListVariable; import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; +import ai.timefold.solver.core.impl.domain.variable.index.IndexShadowVariableDescriptor; import ai.timefold.solver.core.impl.domain.variable.index.IndexVariableSupply; +import ai.timefold.solver.core.impl.domain.variable.inverserelation.InverseRelationShadowVariableDescriptor; import ai.timefold.solver.core.impl.domain.variable.inverserelation.SingletonInverseVariableSupply; import ai.timefold.solver.core.impl.domain.variable.listener.SourcedVariableListener; +import ai.timefold.solver.core.impl.domain.variable.nextprev.NextElementShadowVariableDescriptor; +import ai.timefold.solver.core.impl.domain.variable.nextprev.PreviousElementShadowVariableDescriptor; +import ai.timefold.solver.core.preview.api.domain.metamodel.ElementLocation; +/** + * Single source of truth for all information about elements inside {@link PlanningListVariable list variables}. + * Shadow variables can be connected to this class to save on iteration costs + * that would've been incurred otherwise if using variable listeners for each of them independently. + * This way, there is only one variable listener for all such shadow variables, + * and therefore only a single iteration to update all the information. + * + *

+ * If a particular shadow variable is externalized, + * it means that there is a field on an entity holding the value of the shadow variable. + * In this case, we will attempt to use that value. + * Otherwise, we will keep an internal track of all the possible shadow variables + * ({@link ai.timefold.solver.core.api.domain.variable.IndexShadowVariable}, + * {@link ai.timefold.solver.core.api.domain.variable.InverseRelationShadowVariable}, + * {@link ai.timefold.solver.core.api.domain.variable.PreviousElementShadowVariable}, + * {@link ai.timefold.solver.core.api.domain.variable.NextElementShadowVariable}), + * and use values from this internal representation. + * + * @param + * @see ListVariableState The logic of switching between internal and externalized shadow variables. + * @see ExternalizedListVariableStateSupply The external representation of these shadow variables, + * which doesn't care whether the variable is internal or externalized. + */ public interface ListVariableStateSupply extends SourcedVariableListener, ListVariableListener, SingletonInverseVariableSupply, - IndexVariableSupply, - ListVariableElementStateSupply { + IndexVariableSupply { + + void externalize(IndexShadowVariableDescriptor shadowVariableDescriptor); + + void externalize(InverseRelationShadowVariableDescriptor shadowVariableDescriptor); + + void externalize(PreviousElementShadowVariableDescriptor shadowVariableDescriptor); + + void externalize(NextElementShadowVariableDescriptor shadowVariableDescriptor); @Override ListVariableDescriptor getSourceVariableDescriptor(); + + /** + * + * @param element never null + * @return true if the element is contained in a list variable of any entity. + */ + boolean isAssigned(Object element); + + /** + * + * @param value never null + * @return never null + */ + ElementLocation getLocationInList(Object value); + + /** + * Consider calling this before {@link #isAssigned(Object)} to eliminate some map accesses. + * If unassigned count is 0, {@link #isAssigned(Object)} is guaranteed to return true. + * + * @return number of elements for which {@link #isAssigned(Object)} would return false. + */ + int getUnassignedCount(); + + /** + * + * @param element never null + * @return null if the element is the first element in the list + */ + Object getPreviousElement(Object element); + + /** + * + * @param element never null + * @return null if the element is the last element in the list + */ + Object getNextElement(Object element); + } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/index/IndexShadowVariableDescriptor.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/index/IndexShadowVariableDescriptor.java index 6f216df7aa..3cf942c09a 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/index/IndexShadowVariableDescriptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/index/IndexShadowVariableDescriptor.java @@ -11,10 +11,12 @@ import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessor; import ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor; import ai.timefold.solver.core.impl.domain.policy.DescriptorPolicy; +import ai.timefold.solver.core.impl.domain.variable.ListVariableStateSupply; import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; import ai.timefold.solver.core.impl.domain.variable.descriptor.ShadowVariableDescriptor; import ai.timefold.solver.core.impl.domain.variable.descriptor.VariableDescriptor; import ai.timefold.solver.core.impl.domain.variable.listener.VariableListenerWithSources; +import ai.timefold.solver.core.impl.domain.variable.supply.Demand; import ai.timefold.solver.core.impl.domain.variable.supply.SupplyManager; public final class IndexShadowVariableDescriptor extends ShadowVariableDescriptor { @@ -94,18 +96,20 @@ public List> getSourceVariableDescriptorList() { @Override public Collection> getVariableListenerClasses() { - return Collections.singleton(IndexVariableListener.class); + throw new UnsupportedOperationException("Impossible state: Handled by %s." + .formatted(ListVariableStateSupply.class.getSimpleName())); } @Override - public IndexVariableDemand getProvidedDemand() { - return new IndexVariableDemand<>(sourceVariableDescriptor); + public Demand getProvidedDemand() { + throw new UnsupportedOperationException("Impossible state: Handled by %s." + .formatted(ListVariableStateSupply.class.getSimpleName())); } @Override public Iterable> buildVariableListeners(SupplyManager supplyManager) { - return new VariableListenerWithSources<>(new IndexVariableListener<>(this, sourceVariableDescriptor), - sourceVariableDescriptor).toCollection(); + throw new UnsupportedOperationException("Impossible state: Handled by %s." + .formatted(ListVariableStateSupply.class.getSimpleName())); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/index/IndexVariableDemand.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/index/IndexVariableDemand.java deleted file mode 100644 index 46a4e27419..0000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/index/IndexVariableDemand.java +++ /dev/null @@ -1,23 +0,0 @@ -package ai.timefold.solver.core.impl.domain.variable.index; - -import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; -import ai.timefold.solver.core.impl.domain.variable.supply.AbstractVariableDescriptorBasedDemand; -import ai.timefold.solver.core.impl.domain.variable.supply.SupplyManager; - -public final class IndexVariableDemand - extends AbstractVariableDescriptorBasedDemand { - - public IndexVariableDemand(ListVariableDescriptor sourceVariableDescriptor) { - super(sourceVariableDescriptor); - } - - // ************************************************************************ - // Creation method - // ************************************************************************ - - @Override - public IndexVariableSupply createExternalizedSupply(SupplyManager supplyManager) { - return supplyManager.demand(((ListVariableDescriptor) variableDescriptor).getStateDemand()); - } - -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/index/IndexVariableListener.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/index/IndexVariableListener.java deleted file mode 100644 index 6e7a7df48f..0000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/index/IndexVariableListener.java +++ /dev/null @@ -1,98 +0,0 @@ -package ai.timefold.solver.core.impl.domain.variable.index; - -import java.util.List; -import java.util.Objects; - -import ai.timefold.solver.core.api.domain.variable.ListVariableListener; -import ai.timefold.solver.core.api.score.director.ScoreDirector; -import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; -import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; - -import org.jspecify.annotations.NonNull; - -public class IndexVariableListener implements ListVariableListener, IndexVariableSupply { - - protected final IndexShadowVariableDescriptor shadowVariableDescriptor; - protected final ListVariableDescriptor sourceVariableDescriptor; - - private static final int NEVER_QUIT_EARLY = Integer.MAX_VALUE; - - public IndexVariableListener( - IndexShadowVariableDescriptor shadowVariableDescriptor, - ListVariableDescriptor sourceVariableDescriptor) { - this.shadowVariableDescriptor = shadowVariableDescriptor; - this.sourceVariableDescriptor = sourceVariableDescriptor; - } - - @Override - public void beforeEntityAdded(@NonNull ScoreDirector scoreDirector, @NonNull Object entity) { - // Do nothing - } - - @Override - public void afterEntityAdded(@NonNull ScoreDirector scoreDirector, @NonNull Object entity) { - updateIndexes((InnerScoreDirector) scoreDirector, entity, 0, NEVER_QUIT_EARLY); - } - - @Override - public void beforeEntityRemoved(@NonNull ScoreDirector scoreDirector, @NonNull Object entity) { - // Do nothing - } - - @Override - public void afterEntityRemoved(@NonNull ScoreDirector scoreDirector, @NonNull Object entity) { - InnerScoreDirector innerScoreDirector = (InnerScoreDirector) scoreDirector; - List listVariable = sourceVariableDescriptor.getValue(entity); - for (Object element : listVariable) { - innerScoreDirector.beforeVariableChanged(shadowVariableDescriptor, element); - shadowVariableDescriptor.setValue(element, null); - innerScoreDirector.afterVariableChanged(shadowVariableDescriptor, element); - } - } - - @Override - public void afterListVariableElementUnassigned(@NonNull ScoreDirector scoreDirector, @NonNull Object element) { - InnerScoreDirector innerScoreDirector = (InnerScoreDirector) scoreDirector; - innerScoreDirector.beforeVariableChanged(shadowVariableDescriptor, element); - shadowVariableDescriptor.setValue(element, null); - innerScoreDirector.afterVariableChanged(shadowVariableDescriptor, element); - } - - @Override - public void beforeListVariableChanged(@NonNull ScoreDirector scoreDirector, @NonNull Object entity, - int fromIndex, int toIndex) { - // Do nothing - } - - @Override - public void afterListVariableChanged(@NonNull ScoreDirector scoreDirector, @NonNull Object entity, int fromIndex, - int toIndex) { - InnerScoreDirector innerScoreDirector = (InnerScoreDirector) scoreDirector; - updateIndexes(innerScoreDirector, entity, fromIndex, toIndex); - } - - private void updateIndexes(InnerScoreDirector scoreDirector, Object entity, int fromIndex, int toIndex) { - List listVariable = sourceVariableDescriptor.getValue(entity); - for (int i = fromIndex; i < listVariable.size(); i++) { - Object element = listVariable.get(i); - Integer oldIndex = shadowVariableDescriptor.getValue(element); - if (!Objects.equals(oldIndex, i)) { - scoreDirector.beforeVariableChanged(shadowVariableDescriptor, element); - shadowVariableDescriptor.setValue(element, i); - scoreDirector.afterVariableChanged(shadowVariableDescriptor, element); - } else if (i >= toIndex) { - // Do not quit early while inside the affected subList range. - // Example 1. When X is moved from Ann[3] to Beth[3], we need to start updating Beth's elements at index 3 - // where X already has the expected index, but quitting there would be incorrect because all the elements - // above X need their indexes incremented. - // Example 2. After ListSwapMove(Ann, 5, 9), the listener must not quit at index 6, but it can quit at index 10. - return; - } - } - } - - @Override - public Integer getIndex(Object planningValue) { - return shadowVariableDescriptor.getValue(planningValue); - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/index/IndexVariableSupply.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/index/IndexVariableSupply.java index 5c3561c570..6cfc2e0607 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/index/IndexVariableSupply.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/index/IndexVariableSupply.java @@ -2,12 +2,9 @@ import ai.timefold.solver.core.api.domain.variable.PlanningListVariable; import ai.timefold.solver.core.impl.domain.variable.supply.Supply; -import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; /** * Only supported for {@link PlanningListVariable list variables}. - *

- * To get an instance, demand an {@link IndexVariableDemand} from {@link InnerScoreDirector#getSupplyManager()}. */ public interface IndexVariableSupply extends Supply { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/inverserelation/InverseRelationShadowVariableDescriptor.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/inverserelation/InverseRelationShadowVariableDescriptor.java index 401d91bece..aa58a5e6d3 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/inverserelation/InverseRelationShadowVariableDescriptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/inverserelation/InverseRelationShadowVariableDescriptor.java @@ -15,8 +15,8 @@ import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessor; import ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor; import ai.timefold.solver.core.impl.domain.policy.DescriptorPolicy; +import ai.timefold.solver.core.impl.domain.variable.ListVariableStateSupply; import ai.timefold.solver.core.impl.domain.variable.descriptor.BasicVariableDescriptor; -import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; import ai.timefold.solver.core.impl.domain.variable.descriptor.ShadowVariableDescriptor; import ai.timefold.solver.core.impl.domain.variable.descriptor.VariableDescriptor; import ai.timefold.solver.core.impl.domain.variable.listener.VariableListenerWithSources; @@ -138,7 +138,8 @@ public Collection> getVariableListener if (chained) { return Collections.singleton(SingletonInverseVariableListener.class); } else { - return Collections.singleton(SingletonListInverseVariableListener.class); + throw new UnsupportedOperationException("Impossible state: Handled by %s." + .formatted(ListVariableStateSupply.class.getSimpleName())); } } else { return Collections.singleton(CollectionInverseVariableListener.class); @@ -155,7 +156,8 @@ public Demand getProvidedDemand() { if (chained) { return new SingletonInverseVariableDemand<>(sourceVariableDescriptor); } else { - return new SingletonListInverseVariableDemand<>((ListVariableDescriptor) sourceVariableDescriptor); + throw new UnsupportedOperationException("Impossible state: Handled by %s." + .formatted(ListVariableStateSupply.class.getSimpleName())); } } else { return new CollectionInverseVariableDemand<>(sourceVariableDescriptor); @@ -172,8 +174,8 @@ private AbstractVariableListener buildVariableListener() { if (chained) { return new SingletonInverseVariableListener<>(this, sourceVariableDescriptor); } else { - return new SingletonListInverseVariableListener<>( - this, (ListVariableDescriptor) sourceVariableDescriptor); + throw new UnsupportedOperationException("Impossible state: Handled by %s." + .formatted(ListVariableStateSupply.class.getSimpleName())); } } else { return new CollectionInverseVariableListener<>(this, sourceVariableDescriptor); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/inverserelation/SingletonInverseVariableSupply.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/inverserelation/SingletonInverseVariableSupply.java index adc0d6ed20..6c3f888dc1 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/inverserelation/SingletonInverseVariableSupply.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/inverserelation/SingletonInverseVariableSupply.java @@ -2,14 +2,10 @@ import ai.timefold.solver.core.api.domain.variable.PlanningListVariable; import ai.timefold.solver.core.impl.domain.variable.supply.Supply; -import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; /** * Currently only supported for chained variables and {@link PlanningListVariable list variables}, * which guarantee that no 2 entities use the same planningValue. - *

- * To get an instance, demand a {@link SingletonInverseVariableDemand} (for a chained variable) - * or a {@link SingletonListInverseVariableDemand} (for a list variable) from {@link InnerScoreDirector#getSupplyManager()}. */ public interface SingletonInverseVariableSupply extends Supply { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/inverserelation/SingletonListInverseVariableDemand.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/inverserelation/SingletonListInverseVariableDemand.java deleted file mode 100644 index b3c788987d..0000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/inverserelation/SingletonListInverseVariableDemand.java +++ /dev/null @@ -1,23 +0,0 @@ -package ai.timefold.solver.core.impl.domain.variable.inverserelation; - -import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; -import ai.timefold.solver.core.impl.domain.variable.supply.AbstractVariableDescriptorBasedDemand; -import ai.timefold.solver.core.impl.domain.variable.supply.SupplyManager; - -public final class SingletonListInverseVariableDemand - extends AbstractVariableDescriptorBasedDemand { - - public SingletonListInverseVariableDemand(ListVariableDescriptor sourceVariableDescriptor) { - super(sourceVariableDescriptor); - } - - // ************************************************************************ - // Creation method - // ************************************************************************ - - @Override - public SingletonInverseVariableSupply createExternalizedSupply(SupplyManager supplyManager) { - return supplyManager.demand(((ListVariableDescriptor) variableDescriptor).getStateDemand()); - } - -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/inverserelation/SingletonListInverseVariableListener.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/inverserelation/SingletonListInverseVariableListener.java deleted file mode 100644 index 9169256d56..0000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/inverserelation/SingletonListInverseVariableListener.java +++ /dev/null @@ -1,111 +0,0 @@ -package ai.timefold.solver.core.impl.domain.variable.inverserelation; - -import ai.timefold.solver.core.api.domain.variable.ListVariableListener; -import ai.timefold.solver.core.api.score.director.ScoreDirector; -import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; -import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; - -public class SingletonListInverseVariableListener - implements ListVariableListener, SingletonInverseVariableSupply { - - protected final InverseRelationShadowVariableDescriptor shadowVariableDescriptor; - protected final ListVariableDescriptor sourceVariableDescriptor; - - public SingletonListInverseVariableListener( - InverseRelationShadowVariableDescriptor shadowVariableDescriptor, - ListVariableDescriptor sourceVariableDescriptor) { - this.shadowVariableDescriptor = shadowVariableDescriptor; - this.sourceVariableDescriptor = sourceVariableDescriptor; - } - - @Override - public void resetWorkingSolution(ScoreDirector scoreDirector) { - if (sourceVariableDescriptor.supportsPinning()) { - // Required for variable pinning, otherwise pinned values have their inverse set to null. - var entityDescriptor = sourceVariableDescriptor.getEntityDescriptor(); - entityDescriptor.getSolutionDescriptor() - .visitEntitiesByEntityClass(scoreDirector.getWorkingSolution(), entityDescriptor.getEntityClass(), - entity -> { - beforeEntityAdded(scoreDirector, entity); - afterEntityAdded(scoreDirector, entity); - return false; - }); - } - } - - @Override - public void beforeEntityAdded(ScoreDirector scoreDirector, Object entity) { - // Do nothing - } - - @Override - public void afterEntityAdded(ScoreDirector scoreDirector, Object entity) { - for (var element : sourceVariableDescriptor.getValue(entity)) { - setInverse((InnerScoreDirector) scoreDirector, element, entity, null); - } - } - - @Override - public void beforeEntityRemoved(ScoreDirector scoreDirector, Object entity) { - // Do nothing - } - - @Override - public void afterEntityRemoved(ScoreDirector scoreDirector, Object entity) { - var castScoreDirector = (InnerScoreDirector) scoreDirector; - for (var element : sourceVariableDescriptor.getValue(entity)) { - setInverse(castScoreDirector, element, null, entity); - } - } - - @Override - public void afterListVariableElementUnassigned(ScoreDirector scoreDirector, Object element) { - var castScoreDirector = (InnerScoreDirector) scoreDirector; - castScoreDirector.beforeVariableChanged(shadowVariableDescriptor, element); - shadowVariableDescriptor.setValue(element, null); - castScoreDirector.afterVariableChanged(shadowVariableDescriptor, element); - } - - @Override - public void beforeListVariableChanged(ScoreDirector scoreDirector, Object entity, int fromIndex, int toIndex) { - // Do nothing - } - - @Override - public void afterListVariableChanged(ScoreDirector scoreDirector, Object entity, int fromIndex, int toIndex) { - var castScoreDirector = (InnerScoreDirector) scoreDirector; - var listVariable = sourceVariableDescriptor.getValue(entity); - for (var i = fromIndex; i < toIndex; i++) { - var element = listVariable.get(i); - if (getInverseSingleton(element) != entity) { - castScoreDirector.beforeVariableChanged(shadowVariableDescriptor, element); - shadowVariableDescriptor.setValue(element, entity); - castScoreDirector.afterVariableChanged(shadowVariableDescriptor, element); - } - } - } - - private void setInverse(InnerScoreDirector scoreDirector, - Object element, Object inverseEntity, Object expectedOldInverseEntity) { - var oldInverseEntity = getInverseSingleton(element); - if (oldInverseEntity == inverseEntity) { - return; - } - if (scoreDirector.expectShadowVariablesInCorrectState() && oldInverseEntity != expectedOldInverseEntity) { - throw new IllegalStateException("The entity (" + inverseEntity - + ") has a list variable (" + sourceVariableDescriptor.getVariableName() - + ") and one of its elements (" + element - + ") which has a shadow variable (" + shadowVariableDescriptor.getVariableName() - + ") has an oldInverseEntity (" + oldInverseEntity + ") which is not that entity.\n" - + "Verify the consistency of your input problem for that shadow variable."); - } - scoreDirector.beforeVariableChanged(shadowVariableDescriptor, element); - shadowVariableDescriptor.setValue(element, inverseEntity); - scoreDirector.afterVariableChanged(shadowVariableDescriptor, element); - } - - @Override - public Object getInverseSingleton(Object planningValue) { - return shadowVariableDescriptor.getValue(planningValue); - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/listener/support/VariableListenerSupport.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/listener/support/VariableListenerSupport.java index 8a785be4c2..cb69e86a52 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/listener/support/VariableListenerSupport.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/listener/support/VariableListenerSupport.java @@ -10,12 +10,17 @@ import ai.timefold.solver.core.api.domain.solution.PlanningSolution; import ai.timefold.solver.core.api.score.director.ScoreDirector; import ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor; +import ai.timefold.solver.core.impl.domain.variable.ListVariableStateSupply; import ai.timefold.solver.core.impl.domain.variable.cascade.CascadingUpdateShadowVariableDescriptor; import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; import ai.timefold.solver.core.impl.domain.variable.descriptor.ShadowVariableDescriptor; import ai.timefold.solver.core.impl.domain.variable.descriptor.VariableDescriptor; +import ai.timefold.solver.core.impl.domain.variable.index.IndexShadowVariableDescriptor; +import ai.timefold.solver.core.impl.domain.variable.inverserelation.InverseRelationShadowVariableDescriptor; import ai.timefold.solver.core.impl.domain.variable.listener.SourcedVariableListener; import ai.timefold.solver.core.impl.domain.variable.listener.support.violation.ShadowVariablesAssert; +import ai.timefold.solver.core.impl.domain.variable.nextprev.NextElementShadowVariableDescriptor; +import ai.timefold.solver.core.impl.domain.variable.nextprev.PreviousElementShadowVariableDescriptor; import ai.timefold.solver.core.impl.domain.variable.supply.Demand; import ai.timefold.solver.core.impl.domain.variable.supply.Supply; import ai.timefold.solver.core.impl.domain.variable.supply.SupplyManager; @@ -55,15 +60,41 @@ public static VariableListenerSupport create(InnerScoreDi } public void linkVariableListeners() { + var listVariableStateSupply = listVariableDescriptor == null ? null : demand(listVariableDescriptor.getStateDemand()); scoreDirector.getSolutionDescriptor().getEntityDescriptors().stream() .map(EntityDescriptor::getDeclaredShadowVariableDescriptors) .flatMap(Collection::stream) .filter(ShadowVariableDescriptor::hasVariableListener) .sorted(Comparator.comparingInt(ShadowVariableDescriptor::getGlobalShadowOrder)) - .forEach(this::processShadowVariableDescriptor); + .forEach(d -> { + // All information about elements in all shadow variables is tracked in a centralized place. + // Therefore all list-related shadow variables need to be connected to that centralized place. + // Shadow variables which are not related to a list variable are processed normally. + if (listVariableStateSupply == null) { + processShadowVariableDescriptorWithoutListVariable(d); + } else { + processShadowVariableDescriptorWithListVariable(d, listVariableStateSupply); + } + }); } - private void processShadowVariableDescriptor(ShadowVariableDescriptor shadowVariableDescriptor) { + private void processShadowVariableDescriptorWithListVariable(ShadowVariableDescriptor shadowVariableDescriptor, + ListVariableStateSupply listVariableStateSupply) { + if (shadowVariableDescriptor instanceof IndexShadowVariableDescriptor indexShadowVariableDescriptor) { + listVariableStateSupply.externalize(indexShadowVariableDescriptor); + } else if (shadowVariableDescriptor instanceof InverseRelationShadowVariableDescriptor inverseRelationShadowVariableDescriptor) { + listVariableStateSupply.externalize(inverseRelationShadowVariableDescriptor); + } else if (shadowVariableDescriptor instanceof PreviousElementShadowVariableDescriptor previousElementShadowVariableDescriptor) { + listVariableStateSupply.externalize(previousElementShadowVariableDescriptor); + } else if (shadowVariableDescriptor instanceof NextElementShadowVariableDescriptor nextElementShadowVariableDescriptor) { + listVariableStateSupply.externalize(nextElementShadowVariableDescriptor); + } else { // The list variable supply supports no other shadow variables. + processShadowVariableDescriptorWithoutListVariable(shadowVariableDescriptor); + } + } + + private void + processShadowVariableDescriptorWithoutListVariable(ShadowVariableDescriptor shadowVariableDescriptor) { for (var listenerWithSources : shadowVariableDescriptor.buildVariableListeners(this)) { var variableListener = listenerWithSources.getVariableListener(); if (variableListener instanceof Supply supply) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/nextprev/AbstractNextPrevElementShadowVariableDescriptor.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/nextprev/AbstractNextPrevElementShadowVariableDescriptor.java index fa3600fa89..fb7efe3816 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/nextprev/AbstractNextPrevElementShadowVariableDescriptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/nextprev/AbstractNextPrevElementShadowVariableDescriptor.java @@ -8,6 +8,7 @@ import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessor; import ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor; import ai.timefold.solver.core.impl.domain.policy.DescriptorPolicy; +import ai.timefold.solver.core.impl.domain.variable.ListVariableStateSupply; import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; import ai.timefold.solver.core.impl.domain.variable.descriptor.ShadowVariableDescriptor; import ai.timefold.solver.core.impl.domain.variable.descriptor.VariableDescriptor; @@ -93,7 +94,7 @@ public List> getSourceVariableDescriptorList() { @Override public Demand getProvidedDemand() { - throw new UnsupportedOperationException( - "Not implemented because no subsystems demand previous or next shadow variables."); + throw new UnsupportedOperationException("Impossible state: Handled by %s." + .formatted(ListVariableStateSupply.class.getSimpleName())); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/nextprev/NextElementShadowVariableDescriptor.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/nextprev/NextElementShadowVariableDescriptor.java index 9892ff4ecf..0af1d2cf32 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/nextprev/NextElementShadowVariableDescriptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/nextprev/NextElementShadowVariableDescriptor.java @@ -1,12 +1,12 @@ package ai.timefold.solver.core.impl.domain.variable.nextprev; import java.util.Collection; -import java.util.Collections; import ai.timefold.solver.core.api.domain.variable.AbstractVariableListener; import ai.timefold.solver.core.api.domain.variable.NextElementShadowVariable; import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessor; import ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor; +import ai.timefold.solver.core.impl.domain.variable.ListVariableStateSupply; import ai.timefold.solver.core.impl.domain.variable.listener.VariableListenerWithSources; import ai.timefold.solver.core.impl.domain.variable.supply.SupplyManager; @@ -30,12 +30,13 @@ String getAnnotationName() { @Override public Collection> getVariableListenerClasses() { - return Collections.singleton(NextElementVariableListener.class); + throw new UnsupportedOperationException("Impossible state: Handled by %s." + .formatted(ListVariableStateSupply.class.getSimpleName())); } @Override public Iterable> buildVariableListeners(SupplyManager supplyManager) { - return new VariableListenerWithSources<>(new NextElementVariableListener<>(this, sourceVariableDescriptor), - sourceVariableDescriptor).toCollection(); + throw new UnsupportedOperationException("Impossible state: Handled by %s." + .formatted(ListVariableStateSupply.class.getSimpleName())); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/nextprev/NextElementVariableListener.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/nextprev/NextElementVariableListener.java deleted file mode 100644 index 51da259ee5..0000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/nextprev/NextElementVariableListener.java +++ /dev/null @@ -1,92 +0,0 @@ -package ai.timefold.solver.core.impl.domain.variable.nextprev; - -import java.util.List; - -import ai.timefold.solver.core.api.domain.variable.ListVariableListener; -import ai.timefold.solver.core.api.score.director.ScoreDirector; -import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; -import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; - -import org.jspecify.annotations.NonNull; - -public class NextElementVariableListener - implements ListVariableListener { - - protected final NextElementShadowVariableDescriptor shadowVariableDescriptor; - protected final ListVariableDescriptor sourceVariableDescriptor; - - public NextElementVariableListener( - NextElementShadowVariableDescriptor shadowVariableDescriptor, - ListVariableDescriptor sourceVariableDescriptor) { - this.shadowVariableDescriptor = shadowVariableDescriptor; - this.sourceVariableDescriptor = sourceVariableDescriptor; - } - - @Override - public void beforeEntityAdded(@NonNull ScoreDirector scoreDirector, @NonNull Object entity) { - // Do nothing - } - - @Override - public void afterEntityAdded(@NonNull ScoreDirector scoreDirector, @NonNull Object entity) { - InnerScoreDirector innerScoreDirector = (InnerScoreDirector) scoreDirector; - List listVariable = sourceVariableDescriptor.getValue(entity); - for (int i = 0; i < listVariable.size() - 1; i++) { - Object element = listVariable.get(i); - Object next = listVariable.get(i + 1); - innerScoreDirector.beforeVariableChanged(shadowVariableDescriptor, element); - shadowVariableDescriptor.setValue(element, next); - innerScoreDirector.beforeVariableChanged(shadowVariableDescriptor, element); - } - } - - @Override - public void beforeEntityRemoved(@NonNull ScoreDirector scoreDirector, @NonNull Object entity) { - // Do nothing - } - - @Override - public void afterEntityRemoved(@NonNull ScoreDirector scoreDirector, @NonNull Object entity) { - InnerScoreDirector innerScoreDirector = (InnerScoreDirector) scoreDirector; - List listVariable = sourceVariableDescriptor.getValue(entity); - for (int i = 0; i < listVariable.size() - 1; i++) { - Object element = listVariable.get(i); - innerScoreDirector.beforeVariableChanged(shadowVariableDescriptor, element); - shadowVariableDescriptor.setValue(element, null); - innerScoreDirector.beforeVariableChanged(shadowVariableDescriptor, element); - } - } - - @Override - public void afterListVariableElementUnassigned(@NonNull ScoreDirector scoreDirector, @NonNull Object element) { - InnerScoreDirector innerScoreDirector = (InnerScoreDirector) scoreDirector; - if (shadowVariableDescriptor.getValue(element) != null) { - innerScoreDirector.beforeVariableChanged(shadowVariableDescriptor, element); - shadowVariableDescriptor.setValue(element, null); - innerScoreDirector.afterVariableChanged(shadowVariableDescriptor, element); - } - } - - @Override - public void beforeListVariableChanged(@NonNull ScoreDirector scoreDirector, @NonNull Object entity, - int fromIndex, int toIndex) { - // Do nothing - } - - @Override - public void afterListVariableChanged(@NonNull ScoreDirector scoreDirector, @NonNull Object entity, int fromIndex, - int toIndex) { - InnerScoreDirector innerScoreDirector = (InnerScoreDirector) scoreDirector; - List listVariable = sourceVariableDescriptor.getValue(entity); - Object next = toIndex < listVariable.size() ? listVariable.get(toIndex) : null; - for (int i = toIndex - 1; i >= fromIndex - 1 && i >= 0; i--) { - Object element = listVariable.get(i); - if (next != shadowVariableDescriptor.getValue(element)) { - innerScoreDirector.beforeVariableChanged(shadowVariableDescriptor, element); - shadowVariableDescriptor.setValue(element, next); - innerScoreDirector.afterVariableChanged(shadowVariableDescriptor, element); - } - next = element; - } - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/nextprev/PreviousElementShadowVariableDescriptor.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/nextprev/PreviousElementShadowVariableDescriptor.java index 93a0acf89a..c46fdc8f6c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/nextprev/PreviousElementShadowVariableDescriptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/nextprev/PreviousElementShadowVariableDescriptor.java @@ -1,12 +1,12 @@ package ai.timefold.solver.core.impl.domain.variable.nextprev; import java.util.Collection; -import java.util.Collections; import ai.timefold.solver.core.api.domain.variable.AbstractVariableListener; import ai.timefold.solver.core.api.domain.variable.PreviousElementShadowVariable; import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessor; import ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor; +import ai.timefold.solver.core.impl.domain.variable.ListVariableStateSupply; import ai.timefold.solver.core.impl.domain.variable.listener.VariableListenerWithSources; import ai.timefold.solver.core.impl.domain.variable.supply.SupplyManager; @@ -30,12 +30,13 @@ String getAnnotationName() { @Override public Collection> getVariableListenerClasses() { - return Collections.singleton(PreviousElementVariableListener.class); + throw new UnsupportedOperationException("Impossible state: Handled by %s." + .formatted(ListVariableStateSupply.class.getSimpleName())); } @Override public Iterable> buildVariableListeners(SupplyManager supplyManager) { - return new VariableListenerWithSources<>(new PreviousElementVariableListener<>(this, sourceVariableDescriptor), - sourceVariableDescriptor).toCollection(); + throw new UnsupportedOperationException("Impossible state: Handled by %s." + .formatted(ListVariableStateSupply.class.getSimpleName())); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/nextprev/PreviousElementVariableListener.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/nextprev/PreviousElementVariableListener.java deleted file mode 100644 index bc8e9ae9a6..0000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/nextprev/PreviousElementVariableListener.java +++ /dev/null @@ -1,91 +0,0 @@ -package ai.timefold.solver.core.impl.domain.variable.nextprev; - -import java.util.List; - -import ai.timefold.solver.core.api.domain.variable.ListVariableListener; -import ai.timefold.solver.core.api.score.director.ScoreDirector; -import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; -import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; - -import org.jspecify.annotations.NonNull; - -public class PreviousElementVariableListener implements ListVariableListener { - - protected final PreviousElementShadowVariableDescriptor shadowVariableDescriptor; - protected final ListVariableDescriptor sourceVariableDescriptor; - - public PreviousElementVariableListener( - PreviousElementShadowVariableDescriptor shadowVariableDescriptor, - ListVariableDescriptor sourceVariableDescriptor) { - this.shadowVariableDescriptor = shadowVariableDescriptor; - this.sourceVariableDescriptor = sourceVariableDescriptor; - } - - @Override - public void beforeEntityAdded(@NonNull ScoreDirector scoreDirector, @NonNull Object entity) { - // Do nothing - } - - @Override - public void afterEntityAdded(@NonNull ScoreDirector scoreDirector, @NonNull Object entity) { - InnerScoreDirector innerScoreDirector = (InnerScoreDirector) scoreDirector; - List listVariable = sourceVariableDescriptor.getValue(entity); - for (int i = 1; i < listVariable.size(); i++) { - Object element = listVariable.get(i); - Object previous = listVariable.get(i - 1); - innerScoreDirector.beforeVariableChanged(shadowVariableDescriptor, element); - shadowVariableDescriptor.setValue(element, previous); - innerScoreDirector.beforeVariableChanged(shadowVariableDescriptor, element); - } - } - - @Override - public void beforeEntityRemoved(@NonNull ScoreDirector scoreDirector, @NonNull Object entity) { - // Do nothing - } - - @Override - public void afterEntityRemoved(@NonNull ScoreDirector scoreDirector, @NonNull Object entity) { - InnerScoreDirector innerScoreDirector = (InnerScoreDirector) scoreDirector; - List listVariable = sourceVariableDescriptor.getValue(entity); - for (int i = 1; i < listVariable.size(); i++) { - Object element = listVariable.get(i); - innerScoreDirector.beforeVariableChanged(shadowVariableDescriptor, element); - shadowVariableDescriptor.setValue(element, null); - innerScoreDirector.beforeVariableChanged(shadowVariableDescriptor, element); - } - } - - @Override - public void afterListVariableElementUnassigned(@NonNull ScoreDirector scoreDirector, @NonNull Object element) { - InnerScoreDirector innerScoreDirector = (InnerScoreDirector) scoreDirector; - if (shadowVariableDescriptor.getValue(element) != null) { - innerScoreDirector.beforeVariableChanged(shadowVariableDescriptor, element); - shadowVariableDescriptor.setValue(element, null); - innerScoreDirector.afterVariableChanged(shadowVariableDescriptor, element); - } - } - - @Override - public void beforeListVariableChanged(@NonNull ScoreDirector scoreDirector, @NonNull Object entity, - int fromIndex, int toIndex) { - // Do nothing - } - - @Override - public void afterListVariableChanged(@NonNull ScoreDirector scoreDirector, @NonNull Object entity, int fromIndex, - int toIndex) { - InnerScoreDirector innerScoreDirector = (InnerScoreDirector) scoreDirector; - List listVariable = sourceVariableDescriptor.getValue(entity); - Object previous = fromIndex > 0 ? listVariable.get(fromIndex - 1) : null; - for (int i = fromIndex; i <= toIndex && i < listVariable.size(); i++) { - Object element = listVariable.get(i); - if (previous != shadowVariableDescriptor.getValue(element)) { - innerScoreDirector.beforeVariableChanged(shadowVariableDescriptor, element); - shadowVariableDescriptor.setValue(element, previous); - innerScoreDirector.afterVariableChanged(shadowVariableDescriptor, element); - } - previous = element; - } - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/kopt/KOptDescriptor.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/kopt/KOptDescriptor.java index 82c148ce1f..ba049268e1 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/kopt/KOptDescriptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/kopt/KOptDescriptor.java @@ -165,8 +165,7 @@ public KOptListMove getKOptListMove(ListVariableStateSupp } var combinedList = computeCombinedList(listVariableDescriptor, listVariableStateSupply); - IndexVariableSupply indexVariableSupply = - node -> combinedList.getIndexOfValue(listVariableStateSupply, node); + IndexVariableSupply indexVariableSupply = node -> combinedList.getIndexOfValue(listVariableStateSupply, node); var entityListSize = combinedList.size(); List out = new ArrayList<>(); var originalToCurrentIndexList = new int[entityListSize]; diff --git a/core/src/main/java/ai/timefold/solver/core/preview/api/domain/metamodel/DefaultLocationInList.java b/core/src/main/java/ai/timefold/solver/core/preview/api/domain/metamodel/DefaultLocationInList.java index e5ad52887c..4af729184f 100644 --- a/core/src/main/java/ai/timefold/solver/core/preview/api/domain/metamodel/DefaultLocationInList.java +++ b/core/src/main/java/ai/timefold/solver/core/preview/api/domain/metamodel/DefaultLocationInList.java @@ -23,6 +23,22 @@ record DefaultLocationInList(@NonNull Object entity, int index) implements Locat return this; } + @Override + public boolean equals(Object element) { + if (!(element instanceof DefaultLocationInList that)) { + return false; + } + return index == that.index && entity == that.entity; + } + + @Override + public int hashCode() { + var result = 1; + result = 31 * result + (System.identityHashCode(entity)); + result = 31 * result + (Integer.hashCode(index)); + return result; + } + @Override public String toString() { return entity + "[" + index + "]"; diff --git a/core/src/test/java/ai/timefold/solver/core/impl/domain/variable/ExternalizedListVariableStateSupplyTest.java b/core/src/test/java/ai/timefold/solver/core/impl/domain/variable/ExternalizedListVariableStateSupplyTest.java index 84af3fcccc..8f12b9372b 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/domain/variable/ExternalizedListVariableStateSupplyTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/domain/variable/ExternalizedListVariableStateSupplyTest.java @@ -9,6 +9,7 @@ import java.util.Arrays; import ai.timefold.solver.core.api.score.director.ScoreDirector; +import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; import ai.timefold.solver.core.impl.testdata.domain.list.allows_unassigned.TestdataAllowsUnassignedValuesListEntity; import ai.timefold.solver.core.impl.testdata.domain.list.allows_unassigned.TestdataAllowsUnassignedValuesListSolution; import ai.timefold.solver.core.impl.testdata.domain.list.allows_unassigned.TestdataAllowsUnassignedValuesListValue; @@ -21,7 +22,7 @@ class ExternalizedListVariableStateSupplyTest { @Test void initializeRoundTrip() { var variableDescriptor = TestdataAllowsUnassignedValuesListEntity.buildVariableDescriptorForValueList(); - var scoreDirector = (ScoreDirector) mock(ScoreDirector.class); + var scoreDirector = (ScoreDirector) mock(InnerScoreDirector.class); try (var supply = new ExternalizedListVariableStateSupply<>(variableDescriptor)) { var v1 = new TestdataAllowsUnassignedValuesListValue("1"); @@ -49,7 +50,7 @@ void initializeRoundTrip() { @Test void assignRoundTrip() { var variableDescriptor = TestdataAllowsUnassignedValuesListEntity.buildVariableDescriptorForValueList(); - var scoreDirector = (ScoreDirector) mock(ScoreDirector.class); + var scoreDirector = (ScoreDirector) mock(InnerScoreDirector.class); try (var supply = new ExternalizedListVariableStateSupply<>(variableDescriptor)) { var v1 = new TestdataAllowsUnassignedValuesListValue("1"); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/domain/variable/ListVariableListenerTest.java b/core/src/test/java/ai/timefold/solver/core/impl/domain/variable/ListVariableListenerTest.java index 702a950417..17fe1a8d3f 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/domain/variable/ListVariableListenerTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/domain/variable/ListVariableListenerTest.java @@ -58,8 +58,12 @@ static void assertEmptyPreviousHistory(TestdataListValueWithShadowHistory elemen static void assertPreviousHistory(TestdataListValueWithShadowHistory element, TestdataListValueWithShadowHistory... previousHistory) { - assertThat(element.getPrevious()).isEqualTo(previousHistory[previousHistory.length - 1]); - assertThat(element.getPreviousHistory()).containsExactly(previousHistory); + assertThat(element.getPrevious()) + .as("Previous is incorrect") + .isEqualTo(previousHistory[previousHistory.length - 1]); + assertThat(element.getPreviousHistory()) + .as("History is incorrect") + .containsExactly(previousHistory); } static void assertEmptyNextHistory(TestdataListValueWithShadowHistory element) { @@ -69,8 +73,12 @@ static void assertEmptyNextHistory(TestdataListValueWithShadowHistory element) { static void assertNextHistory(TestdataListValueWithShadowHistory element, TestdataListValueWithShadowHistory... nextHistory) { - assertThat(element.getNext()).isEqualTo(nextHistory[nextHistory.length - 1]); - assertThat(element.getNextHistory()).containsExactly(nextHistory); + assertThat(element.getNext()) + .as("Next is incorrect") + .isEqualTo(nextHistory[nextHistory.length - 1]); + assertThat(element.getNextHistory()) + .as("History is incorrect") + .containsExactly(nextHistory); } void doChangeMove( diff --git a/core/src/test/java/ai/timefold/solver/core/impl/domain/variable/index/IndexVariableListenerTest.java b/core/src/test/java/ai/timefold/solver/core/impl/domain/variable/index/IndexVariableListenerTest.java deleted file mode 100644 index 86c730760d..0000000000 --- a/core/src/test/java/ai/timefold/solver/core/impl/domain/variable/index/IndexVariableListenerTest.java +++ /dev/null @@ -1,97 +0,0 @@ -package ai.timefold.solver.core.impl.domain.variable.index; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - -import ai.timefold.solver.core.api.score.director.ScoreDirector; -import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; -import ai.timefold.solver.core.impl.testdata.domain.list.TestdataListEntity; -import ai.timefold.solver.core.impl.testdata.domain.list.TestdataListSolution; -import ai.timefold.solver.core.impl.testdata.domain.list.TestdataListValue; - -import org.junit.jupiter.api.Test; - -class IndexVariableListenerTest { - - private final ScoreDirector scoreDirector = mock(InnerScoreDirector.class); - - private final IndexVariableListener indexVariableListener = new IndexVariableListener<>( - TestdataListValue.buildVariableDescriptorForIndex(), - TestdataListEntity.buildVariableDescriptorForValueList()); - - @Test - void index() { - TestdataListValue v1 = new TestdataListValue("1"); - TestdataListValue v2 = new TestdataListValue("2"); - TestdataListValue v3 = new TestdataListValue("3"); - TestdataListValue v4 = new TestdataListValue("4"); - TestdataListEntity entity = new TestdataListEntity("a", v1, v2, v3); - - assertIndex(v1, null); - assertIndex(v2, null); - assertIndex(v3, null); - assertIndex(v4, null); - - indexVariableListener.beforeEntityAdded(scoreDirector, entity); - indexVariableListener.afterEntityAdded(scoreDirector, entity); - - assertIndex(v1, 0); - assertIndex(v2, 1); - assertIndex(v3, 2); - - // Assign v4. - indexVariableListener.beforeListVariableChanged(scoreDirector, entity, 2, 2); - entity.getValueList().add(2, v4); - indexVariableListener.afterListVariableChanged(scoreDirector, entity, 2, 3); - - assertIndex(v1, 0); - assertIndex(v2, 1); - assertIndex(v4, 2); - assertIndex(v3, 3); - - // Unassign v1. - indexVariableListener.beforeListVariableChanged(scoreDirector, entity, 0, 1); - entity.getValueList().remove(v1); - indexVariableListener.afterListVariableElementUnassigned(scoreDirector, v1); - indexVariableListener.afterListVariableChanged(scoreDirector, entity, 0, 0); - - assertIndex(v1, null); - assertIndex(v2, 0); - assertIndex(v4, 1); - assertIndex(v3, 2); - - // Move v4 from entity[1] to entity[2]. - indexVariableListener.beforeListVariableChanged(scoreDirector, entity, 1, 3); - entity.getValueList().remove(v4); - entity.getValueList().add(2, v4); - indexVariableListener.afterListVariableChanged(scoreDirector, entity, 1, 3); - - assertIndex(v2, 0); - assertIndex(v3, 1); - assertIndex(v4, 2); - } - - @Test - void removeEntity() { - TestdataListValue v1 = new TestdataListValue("1"); - TestdataListValue v2 = new TestdataListValue("2"); - TestdataListValue v3 = new TestdataListValue("3"); - TestdataListEntity entity = TestdataListEntity.createWithValues("a", v1, v2, v3); - - assertIndex(v1, 0); - assertIndex(v2, 1); - assertIndex(v3, 2); - - indexVariableListener.beforeEntityRemoved(scoreDirector, entity); - indexVariableListener.afterEntityRemoved(scoreDirector, entity); - - assertIndex(v1, null); - assertIndex(v2, null); - assertIndex(v3, null); - } - - void assertIndex(TestdataListValue element, Integer index) { - assertThat(element.getIndex()).isEqualTo(index); - assertThat(indexVariableListener.getIndex(element)).isEqualTo(index); - } -} diff --git a/core/src/test/java/ai/timefold/solver/core/impl/domain/variable/inverserelation/SingletonListInverseVariableListenerTest.java b/core/src/test/java/ai/timefold/solver/core/impl/domain/variable/inverserelation/SingletonListInverseVariableListenerTest.java deleted file mode 100644 index 12de174d1e..0000000000 --- a/core/src/test/java/ai/timefold/solver/core/impl/domain/variable/inverserelation/SingletonListInverseVariableListenerTest.java +++ /dev/null @@ -1,95 +0,0 @@ -package ai.timefold.solver.core.impl.domain.variable.inverserelation; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - -import ai.timefold.solver.core.api.score.director.ScoreDirector; -import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; -import ai.timefold.solver.core.impl.testdata.domain.list.TestdataListEntity; -import ai.timefold.solver.core.impl.testdata.domain.list.TestdataListSolution; -import ai.timefold.solver.core.impl.testdata.domain.list.TestdataListValue; - -import org.junit.jupiter.api.Test; - -class SingletonListInverseVariableListenerTest { - - private final ScoreDirector scoreDirector = mock(InnerScoreDirector.class); - - private final SingletonListInverseVariableListener inverseVariableListener = - new SingletonListInverseVariableListener<>( - TestdataListValue.buildVariableDescriptorForEntity(), - TestdataListEntity.buildVariableDescriptorForValueList()); - - @Test - void inverseRelation() { - TestdataListValue v1 = new TestdataListValue("1"); - TestdataListValue v2 = new TestdataListValue("2"); - TestdataListValue v3 = new TestdataListValue("3"); - TestdataListValue v4 = new TestdataListValue("4"); - TestdataListEntity e1 = new TestdataListEntity("a", v1, v2); - TestdataListEntity e2 = new TestdataListEntity("b", v3); - - assertThat(v1.getEntity()).isNull(); - assertThat(v2.getEntity()).isNull(); - assertThat(v3.getEntity()).isNull(); - - inverseVariableListener.beforeEntityAdded(scoreDirector, e1); - inverseVariableListener.afterEntityAdded(scoreDirector, e1); - inverseVariableListener.beforeEntityAdded(scoreDirector, e2); - inverseVariableListener.afterEntityAdded(scoreDirector, e2); - - assertInverseEntity(v1, e1); - assertInverseEntity(v2, e1); - assertInverseEntity(v3, e2); - - // Move v1 from e1 to e2. - inverseVariableListener.beforeListVariableChanged(scoreDirector, e1, 0, 1); - e1.getValueList().remove(v1); - inverseVariableListener.afterListVariableChanged(scoreDirector, e1, 0, 0); - inverseVariableListener.beforeListVariableChanged(scoreDirector, e2, 1, 1); - e2.getValueList().add(v1); - inverseVariableListener.afterListVariableChanged(scoreDirector, e2, 1, 2); - - assertInverseEntity(v1, e2); - - // Assign v4 to e2[0]. - inverseVariableListener.beforeListVariableChanged(scoreDirector, e2, 0, 0); - e2.getValueList().add(0, v4); - inverseVariableListener.afterListVariableChanged(scoreDirector, e2, 0, 1); - - assertInverseEntity(v4, e2); - - // Unassign v2 from e1. - inverseVariableListener.beforeListVariableChanged(scoreDirector, e1, 0, 1); - e1.getValueList().remove(0); - inverseVariableListener.afterListVariableElementUnassigned(scoreDirector, v2); - inverseVariableListener.afterListVariableChanged(scoreDirector, e1, 0, 0); - - assertInverseEntity(v2, null); - } - - @Test - void removeEntity() { - TestdataListValue v1 = new TestdataListValue("1"); - TestdataListValue v2 = new TestdataListValue("2"); - TestdataListValue v3 = new TestdataListValue("3"); - TestdataListEntity e1 = TestdataListEntity.createWithValues("a", v1, v2); - TestdataListEntity e2 = TestdataListEntity.createWithValues("b", v3); - - assertThat(v1.getEntity()).isEqualTo(e1); - assertThat(v2.getEntity()).isEqualTo(e1); - assertThat(v3.getEntity()).isEqualTo(e2); - - inverseVariableListener.beforeEntityRemoved(scoreDirector, e1); - inverseVariableListener.afterEntityRemoved(scoreDirector, e1); - - assertInverseEntity(v1, null); - assertInverseEntity(v2, null); - assertInverseEntity(v3, e2); - } - - void assertInverseEntity(TestdataListValue element, Object entity) { - assertThat(element.getEntity()).isEqualTo(entity); - assertThat(inverseVariableListener.getInverseSingleton(element)).isEqualTo(entity); - } -}