diff --git a/benchmark/src/main/resources/benchmark.xsd b/benchmark/src/main/resources/benchmark.xsd index 6bb3594d055..7fb34bd4308 100644 --- a/benchmark/src/main/resources/benchmark.xsd +++ b/benchmark/src/main/resources/benchmark.xsd @@ -674,7 +674,7 @@ - + diff --git a/core/src/build/revapi-differences.json b/core/src/build/revapi-differences.json index 21d6761d887..47a22bfe78f 100644 --- a/core/src/build/revapi-differences.json +++ b/core/src/build/revapi-differences.json @@ -331,6 +331,23 @@ "new": "class ai.timefold.solver.core.api.score.buildin.simplelong.SimpleLongScore", "annotation": "@org.jspecify.annotations.NullMarked", "justification": "@NonNull replaced by @NullMarked" + }, + { + "ignore": true, + "code": "java.field.removed", + "old": "field ai.timefold.solver.core.config.constructionheuristic.ConstructionHeuristicPhaseConfig.entityPlacerConfig", + "justification": "New CH configuration with multiple placers" + }, + { + "ignore": true, + "code": "java.annotation.attributeValueChanged", + "old": "class ai.timefold.solver.core.config.constructionheuristic.ConstructionHeuristicPhaseConfig", + "new": "class ai.timefold.solver.core.config.constructionheuristic.ConstructionHeuristicPhaseConfig", + "annotationType": "jakarta.xml.bind.annotation.XmlType", + "attribute": "propOrder", + "oldValue": "{\"constructionHeuristicType\", \"entitySorterManner\", \"valueSorterManner\", \"entityPlacerConfig\", \"moveSelectorConfigList\", \"foragerConfig\"}", + "newValue": "{\"constructionHeuristicType\", \"entitySorterManner\", \"valueSorterManner\", \"entityPlacerConfigList\", \"moveSelectorConfigList\", \"foragerConfig\"}", + "justification": "New CH configuration with multiple placers" } ] } diff --git a/core/src/main/java/ai/timefold/solver/core/config/constructionheuristic/ConstructionHeuristicPhaseConfig.java b/core/src/main/java/ai/timefold/solver/core/config/constructionheuristic/ConstructionHeuristicPhaseConfig.java index e8a46a4d9b5..fca788048fb 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/constructionheuristic/ConstructionHeuristicPhaseConfig.java +++ b/core/src/main/java/ai/timefold/solver/core/config/constructionheuristic/ConstructionHeuristicPhaseConfig.java @@ -1,5 +1,6 @@ package ai.timefold.solver.core.config.constructionheuristic; +import java.util.Arrays; import java.util.List; import java.util.function.Consumer; @@ -36,7 +37,7 @@ "constructionHeuristicType", "entitySorterManner", "valueSorterManner", - "entityPlacerConfig", + "entityPlacerConfigList", "moveSelectorConfigList", "foragerConfig" }) @@ -56,9 +57,9 @@ public class ConstructionHeuristicPhaseConfig extends PhaseConfig entityPlacerConfigList = null; - /** Simpler alternative for {@link #entityPlacerConfig}. */ + /** Simpler alternative for {@link #entityPlacerConfigList}. */ @XmlElements({ @XmlElement(name = CartesianProductMoveSelectorConfig.XML_ELEMENT_NAME, type = CartesianProductMoveSelectorConfig.class), @@ -110,12 +111,35 @@ public void setValueSorterManner(@Nullable ValueSorterManner valueSorterManner) this.valueSorterManner = valueSorterManner; } - public @Nullable EntityPlacerConfig getEntityPlacerConfig() { - return entityPlacerConfig; + public List getEntityPlacerConfigList() { + return entityPlacerConfigList; + } + + public void setEntityPlacerConfigList(List entityPlacerConfigList) { + this.entityPlacerConfigList = entityPlacerConfigList; } - public void setEntityPlacerConfig(@Nullable EntityPlacerConfig entityPlacerConfig) { - this.entityPlacerConfig = entityPlacerConfig; + /** + * @deprecated Use {@link #setEntityPlacerConfigList(List)}} instead. + */ + @Deprecated(forRemoval = true, since = "1.22.0") + public void setEntityPlacerConfig(EntityPlacerConfig entityPlacerConfig) { + setEntityPlacerConfigList(List.of(entityPlacerConfig)); + } + + /** + * @deprecated Use {@link #getEntityPlacerConfigList()} instead. + */ + @Deprecated(forRemoval = true, since = "1.22.0") + public @Nullable EntityPlacerConfig getEntityPlacerConfig() { + if (entityPlacerConfigList == null || entityPlacerConfigList.isEmpty()) { + return null; + } + if (entityPlacerConfigList.size() > 1) { + throw new IllegalStateException( + "Returning a single entity placer configuration is not possible. Maybe use getEntityPlacerConfigList instead."); + } + return entityPlacerConfigList.get(0); } public @Nullable List<@NonNull MoveSelectorConfig> getMoveSelectorConfigList() { @@ -154,8 +178,19 @@ public void setForagerConfig(@Nullable ConstructionHeuristicForagerConfig forage return this; } - public @NonNull ConstructionHeuristicPhaseConfig withEntityPlacerConfig(@NonNull EntityPlacerConfig entityPlacerConfig) { - this.entityPlacerConfig = entityPlacerConfig; + public @NonNull ConstructionHeuristicPhaseConfig + withEntityPlacerConfigList(@NonNull EntityPlacerConfig... entityPlacerConfig) { + setEntityPlacerConfigList(Arrays.asList(entityPlacerConfig)); + return this; + } + + /** + * @deprecated use {@link #withEntityPlacerConfigList(EntityPlacerConfig[])} instead. + */ + @Deprecated(forRemoval = true, since = "1.22.0") + public @NonNull ConstructionHeuristicPhaseConfig + withEntityPlacerConfig(@NonNull EntityPlacerConfig entityPlacerConfig) { + setEntityPlacerConfigList(List.of(entityPlacerConfig)); return this; } @@ -180,8 +215,8 @@ public void setForagerConfig(@Nullable ConstructionHeuristicForagerConfig forage inheritedConfig.getEntitySorterManner()); valueSorterManner = ConfigUtils.inheritOverwritableProperty(valueSorterManner, inheritedConfig.getValueSorterManner()); - setEntityPlacerConfig(ConfigUtils.inheritOverwritableProperty( - getEntityPlacerConfig(), inheritedConfig.getEntityPlacerConfig())); + entityPlacerConfigList = ConfigUtils.inheritMergeableListConfig( + entityPlacerConfigList, inheritedConfig.getEntityPlacerConfigList()); moveSelectorConfigList = ConfigUtils.inheritMergeableListConfig( moveSelectorConfigList, inheritedConfig.getMoveSelectorConfigList()); foragerConfig = ConfigUtils.inheritConfig(foragerConfig, inheritedConfig.getForagerConfig()); @@ -198,8 +233,8 @@ public void visitReferencedClasses(@NonNull Consumer> classVisitor) { if (terminationConfig != null) { terminationConfig.visitReferencedClasses(classVisitor); } - if (entityPlacerConfig != null) { - entityPlacerConfig.visitReferencedClasses(classVisitor); + if (entityPlacerConfigList != null) { + entityPlacerConfigList.forEach(entityPlacerConfig -> entityPlacerConfig.visitReferencedClasses(classVisitor)); } if (moveSelectorConfigList != null) { moveSelectorConfigList.forEach(ms -> ms.visitReferencedClasses(classVisitor)); diff --git a/core/src/main/java/ai/timefold/solver/core/config/constructionheuristic/placer/QueuedEntityPlacerConfig.java b/core/src/main/java/ai/timefold/solver/core/config/constructionheuristic/placer/QueuedEntityPlacerConfig.java index 2e4a19bc8c9..b9996f53a5b 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/constructionheuristic/placer/QueuedEntityPlacerConfig.java +++ b/core/src/main/java/ai/timefold/solver/core/config/constructionheuristic/placer/QueuedEntityPlacerConfig.java @@ -32,6 +32,8 @@ }) public class QueuedEntityPlacerConfig extends EntityPlacerConfig { + public static final String XML_ELEMENT_NAME = "queuedEntityPlacer"; + @XmlElement(name = "entitySelector") protected EntitySelectorConfig entitySelectorConfig = null; diff --git a/core/src/main/java/ai/timefold/solver/core/config/constructionheuristic/placer/QueuedValuePlacerConfig.java b/core/src/main/java/ai/timefold/solver/core/config/constructionheuristic/placer/QueuedValuePlacerConfig.java index 3ff0ce62728..791790e72f5 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/constructionheuristic/placer/QueuedValuePlacerConfig.java +++ b/core/src/main/java/ai/timefold/solver/core/config/constructionheuristic/placer/QueuedValuePlacerConfig.java @@ -31,6 +31,8 @@ }) public class QueuedValuePlacerConfig extends EntityPlacerConfig { + public static final String XML_ELEMENT_NAME = "queuedValuePlacer"; + protected Class entityClass = null; @XmlElement(name = "valueSelector") diff --git a/core/src/main/java/ai/timefold/solver/core/impl/AbstractFromConfigFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/AbstractFromConfigFactory.java index 73c5e5a9ac9..ff691c260a8 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/AbstractFromConfigFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/AbstractFromConfigFactory.java @@ -3,7 +3,6 @@ import java.util.Collection; import java.util.List; import java.util.Objects; -import java.util.stream.Collectors; import ai.timefold.solver.core.api.domain.solution.PlanningSolution; import ai.timefold.solver.core.config.AbstractConfig; @@ -19,7 +18,7 @@ public abstract class AbstractFromConfigFactory getEntityDescriptorForClass(SolutionDescript Class entityClass) { EntityDescriptor entityDescriptor = solutionDescriptor.getEntityDescriptorStrict(entityClass); if (entityDescriptor == null) { - throw new IllegalArgumentException("The config (" + config - + ") has an entityClass (" + entityClass + ") that is not a known planning entity.\n" - + "Check your solver configuration. If that class (" + entityClass.getSimpleName() - + ") is not in the entityClassSet (" + solutionDescriptor.getEntityClassSet() - + "), check your @" + PlanningSolution.class.getSimpleName() - + " implementation's annotated methods too."); + throw new IllegalArgumentException( + """ + The config (%s) has an entityClass (%s) that is not a known planning entity. + Check your solver configuration. If that class (%s) is not in the entityClassSet (%s), check your @%s implementation's annotated methods too.""" + .formatted(config, entityClass, entityClass.getSimpleName(), solutionDescriptor.getEntityClassSet(), + PlanningSolution.class.getSimpleName())); } return entityDescriptor; } @@ -68,10 +67,23 @@ private EntityDescriptor getEntityDescriptorForClass(SolutionDescript protected EntityDescriptor getTheOnlyEntityDescriptor(SolutionDescriptor solutionDescriptor) { Collection> entityDescriptors = solutionDescriptor.getGenuineEntityDescriptors(); if (entityDescriptors.size() != 1) { - throw new IllegalArgumentException("The config (" + config - + ") has no entityClass configured and because there are multiple in the entityClassSet (" - + solutionDescriptor.getEntityClassSet() - + "), it cannot be deduced automatically."); + throw new IllegalArgumentException( + "The config (%s) has no entityClass configured and because there are multiple in the entityClassSet (%s), it cannot be deduced automatically." + .formatted(config, solutionDescriptor.getEntityClassSet())); + } + return entityDescriptors.iterator().next(); + } + + protected EntityDescriptor + getTheOnlyEntityDescriptorWithBasicVariables(SolutionDescriptor solutionDescriptor) { + Collection> entityDescriptors = solutionDescriptor.getGenuineEntityDescriptors() + .stream() + .filter(EntityDescriptor::hasAnyGenuineBasicVariables) + .toList(); + if (entityDescriptors.size() != 1) { + throw new IllegalArgumentException( + "The config (%s) has no entityClass configured and because there are multiple in the entityClassSet (%s) defining basic variables, it cannot be deduced automatically." + .formatted(config, solutionDescriptor.getEntityClassSet())); } return entityDescriptors.iterator().next(); } @@ -87,11 +99,11 @@ protected GenuineVariableDescriptor getVariableDescriptorForName(Enti String variableName) { GenuineVariableDescriptor variableDescriptor = entityDescriptor.getGenuineVariableDescriptor(variableName); if (variableDescriptor == null) { - throw new IllegalArgumentException("The config (" + config - + ") has a variableName (" + variableName - + ") which is not a valid planning variable on entityClass (" - + entityDescriptor.getEntityClass() + ").\n" - + entityDescriptor.buildInvalidVariableNameExceptionMessage(variableName)); + throw new IllegalArgumentException( + """ + The config (%s) has a variableName (%s) which is not a valid planning variable on entityClass (%s). + %s""".formatted(config, variableName, entityDescriptor.getEntityClass(), + entityDescriptor.buildInvalidVariableNameExceptionMessage(variableName))); } return variableDescriptor; } @@ -100,11 +112,10 @@ protected GenuineVariableDescriptor getTheOnlyVariableDescriptor(Enti List> variableDescriptorList = entityDescriptor.getGenuineVariableDescriptorList(); if (variableDescriptorList.size() != 1) { - throw new IllegalArgumentException("The config (" + config - + ") has no configured variableName for entityClass (" + entityDescriptor.getEntityClass() - + ") and because there are multiple variableNames (" - + entityDescriptor.getGenuineVariableNameSet() - + "), it cannot be deduced automatically."); + throw new IllegalArgumentException( + "The config (%s) has no configured variableName for entityClass (%s) and because there are multiple variableNames (%s), it cannot be deduced automatically." + .formatted(config, entityDescriptor.getEntityClass(), + entityDescriptor.getGenuineVariableNameSet())); } return variableDescriptorList.iterator().next(); } @@ -122,10 +133,10 @@ protected List> deduceVariableDescriptorLis .map(variableNameInclude -> variableDescriptorList.stream() .filter(variableDescriptor -> variableDescriptor.getVariableName().equals(variableNameInclude)) .findFirst() - .orElseThrow(() -> new IllegalArgumentException("The config (" + config - + ") has a variableNameInclude (" + variableNameInclude - + ") which does not exist in the entity (" + entityDescriptor.getEntityClass() - + ")'s variableDescriptorList (" + variableDescriptorList + ")."))) - .collect(Collectors.toList()); + .orElseThrow(() -> new IllegalArgumentException( + "The config (%s) has a variableNameInclude (%s) which does not exist in the entity (%s)'s variableDescriptorList (%s)." + .formatted(config, variableNameInclude, entityDescriptor.getEntityClass(), + variableDescriptorList)))) + .toList(); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhase.java b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhase.java index 797fa7f9834..e16e8477b9c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhase.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhase.java @@ -57,15 +57,14 @@ public void solve(SolverScope solverScope) { phaseStarted(phaseScope); var solutionDescriptor = solverScope.getSolutionDescriptor(); - var listVariableDescriptor = solutionDescriptor.getListVariableDescriptor(); - var hasListVariable = listVariableDescriptor != null; + var hasListVariable = solutionDescriptor.hasListVariable(); var maxStepCount = -1; if (hasListVariable) { // In case of list variable with support for unassigned values, the placer will iterate indefinitely. // (When it exhausts all values, it will start over from the beginning.) // To prevent that, we need to limit the number of steps to the number of unassigned values. var workingSolution = phaseScope.getWorkingSolution(); - maxStepCount = listVariableDescriptor.countUnassigned(workingSolution); + maxStepCount = solutionDescriptor.getListVariableDescriptor().countUnassigned(workingSolution); } TerminationStatus earlyTerminationStatus = null; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseFactory.java index 9fdfb4cda3d..9f561892594 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseFactory.java @@ -1,5 +1,6 @@ package ai.timefold.solver.core.impl.constructionheuristic; +import java.util.ArrayList; import java.util.Objects; import java.util.Optional; @@ -28,6 +29,7 @@ import ai.timefold.solver.core.impl.constructionheuristic.placer.PooledEntityPlacerFactory; import ai.timefold.solver.core.impl.constructionheuristic.placer.QueuedEntityPlacerFactory; import ai.timefold.solver.core.impl.constructionheuristic.placer.QueuedValuePlacerFactory; +import ai.timefold.solver.core.impl.constructionheuristic.placer.internal.QueuedMultiplePlacerConfig; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; import ai.timefold.solver.core.impl.heuristic.HeuristicConfigPolicy; @@ -85,10 +87,38 @@ public ConstructionHeuristicPhase buildPhase(int phaseIndex, boolean } private Optional> getValidEntityPlacerConfig() { - var entityPlacerConfig = phaseConfig.getEntityPlacerConfig(); - if (entityPlacerConfig == null) { + if (phaseConfig.getEntityPlacerConfigList() == null || phaseConfig.getEntityPlacerConfigList().isEmpty()) { return Optional.empty(); } + + if (phaseConfig.getEntityPlacerConfigList().size() > 2) { + throw new IllegalArgumentException( + "The Construction Heuristic configuration (%s) only support a maximum of two entity placers." + .formatted(phaseConfig)); + } + if (phaseConfig.getEntityPlacerConfigList().stream().anyMatch(PooledEntityPlacerConfig.class::isInstance) + && phaseConfig.getEntityPlacerConfigList().size() == 2) { + throw new IllegalArgumentException( + "The Construction Heuristic configuration (%s) does not support multiple configurations when using the pooled placer configuration %s." + .formatted(phaseConfig, PooledEntityPlacerConfig.class.getSimpleName())); + } + if (phaseConfig.getEntityPlacerConfigList().stream().map(EntityPlacerConfig::getClass).distinct().count() == 1 + && phaseConfig.getEntityPlacerConfigList().size() == 2) { + var message = "The Construction Heuristic configuration (%s) cannot contain duplicate placer configurations." + .formatted(phaseConfig); + if (phaseConfig.getEntityPlacerConfigList().get(0) instanceof QueuedEntityPlacerConfig) { + throw new IllegalArgumentException(""" + %s + Maybe define multiple move selectors if there are more than one basic variables.""".formatted(message)); + } + throw new IllegalArgumentException(message); + } + + var entityPlacerConfig = phaseConfig.getEntityPlacerConfigList().get(0); + if (phaseConfig.getEntityPlacerConfigList().size() == 2) { + entityPlacerConfig = new QueuedMultiplePlacerConfig() + .withPlacerConfigList(phaseConfig.getEntityPlacerConfigList()); + } if (phaseConfig.getConstructionHeuristicType() != null) { throw new IllegalArgumentException( "The constructionHeuristicType (%s) must not be configured if the entityPlacerConfig (%s) is explicitly configured." @@ -100,14 +130,32 @@ private Optional> getValidEntityPlacerConfig() { "The moveSelectorConfigList (%s) cannot be configured if the entityPlacerConfig (%s) is explicitly configured." .formatted(moveSelectorConfigList, entityPlacerConfig)); } + return Optional.of(entityPlacerConfig); } + @SuppressWarnings("rawtypes") private EntityPlacerConfig buildDefaultEntityPlacerConfig(HeuristicConfigPolicy configPolicy, ConstructionHeuristicType constructionHeuristicType) { - return findValidListVariableDescriptor(configPolicy.getSolutionDescriptor()) - .map(listVariableDescriptor -> buildListVariableQueuedValuePlacerConfig(configPolicy, listVariableDescriptor)) - .orElseGet(() -> buildUnfoldedEntityPlacerConfig(configPolicy, constructionHeuristicType)); + var listVariableDescriptor = findValidListVariableDescriptor(configPolicy.getSolutionDescriptor()).orElse(null); + if (configPolicy.getSolutionDescriptor().hasBothBasicAndListVariables()) { + if (listVariableDescriptor == null) { + throw new IllegalStateException("Impossible state: the list variable descriptor is null."); + } + var placerConfigList = new ArrayList(); + // Generate the default configuration for the list variable + placerConfigList.add(buildListVariableQueuedValuePlacerConfig(configPolicy, listVariableDescriptor)); + // Generate a single config for the basic variable(s) + // When multiple basic variables are defined, a Cartesian product is created + placerConfigList.add(buildUnfoldedEntityPlacerConfig(configPolicy, constructionHeuristicType)); + return new QueuedMultiplePlacerConfig().withPlacerConfigList(placerConfigList); + } else { + if (listVariableDescriptor != null) { + return buildListVariableQueuedValuePlacerConfig(configPolicy, listVariableDescriptor); + } else { + return buildUnfoldedEntityPlacerConfig(configPolicy, constructionHeuristicType); + } + } } private Optional> @@ -117,7 +165,6 @@ private EntityPlacerConfig buildDefaultEntityPlacerConfig(HeuristicConfigPoli return Optional.empty(); } failIfConfigured(phaseConfig.getConstructionHeuristicType(), "constructionHeuristicType"); - failIfConfigured(phaseConfig.getEntityPlacerConfig(), "entityPlacerConfig"); failIfConfigured(phaseConfig.getMoveSelectorConfigList(), "moveSelectorConfigList"); return Optional.of(listVariableDescriptor); } @@ -144,7 +191,8 @@ public static EntityPlacerConfig buildListVariableQueuedValuePlacerConfig(Heuris } // Prepare replaying ValueSelector config. var mimicReplayingValueSelectorConfig = new ValueSelectorConfig() - .withMimicSelectorRef(mimicSelectorId); + .withMimicSelectorRef(mimicSelectorId) + .withVariableName(variableDescriptor.getVariableName()); // ListChangeMoveSelector uses the replaying ValueSelector. var listChangeMoveSelectorConfig = new ListChangeMoveSelectorConfig() diff --git a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/EntityPlacerFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/EntityPlacerFactory.java index f7039e2a60f..97837552cd9 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/EntityPlacerFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/EntityPlacerFactory.java @@ -4,17 +4,21 @@ import ai.timefold.solver.core.config.constructionheuristic.placer.PooledEntityPlacerConfig; import ai.timefold.solver.core.config.constructionheuristic.placer.QueuedEntityPlacerConfig; import ai.timefold.solver.core.config.constructionheuristic.placer.QueuedValuePlacerConfig; +import ai.timefold.solver.core.impl.constructionheuristic.placer.internal.QueuedMultiplePlacerConfig; import ai.timefold.solver.core.impl.heuristic.HeuristicConfigPolicy; public interface EntityPlacerFactory { + @SuppressWarnings({ "rawtypes", "unchecked" }) static EntityPlacerFactory create(EntityPlacerConfig entityPlacerConfig) { - if (PooledEntityPlacerConfig.class.isAssignableFrom(entityPlacerConfig.getClass())) { - return new PooledEntityPlacerFactory<>((PooledEntityPlacerConfig) entityPlacerConfig); - } else if (QueuedEntityPlacerConfig.class.isAssignableFrom(entityPlacerConfig.getClass())) { - return new QueuedEntityPlacerFactory<>((QueuedEntityPlacerConfig) entityPlacerConfig); - } else if (QueuedValuePlacerConfig.class.isAssignableFrom(entityPlacerConfig.getClass())) { - return new QueuedValuePlacerFactory<>((QueuedValuePlacerConfig) entityPlacerConfig); + if (entityPlacerConfig instanceof PooledEntityPlacerConfig pooledEntityPlacerConfig) { + return new PooledEntityPlacerFactory<>(pooledEntityPlacerConfig); + } else if (entityPlacerConfig instanceof QueuedEntityPlacerConfig queuedEntityPlacerConfig) { + return new QueuedEntityPlacerFactory<>(queuedEntityPlacerConfig); + } else if (entityPlacerConfig instanceof QueuedValuePlacerConfig queuedValuePlacerConfig) { + return new QueuedValuePlacerFactory<>(queuedValuePlacerConfig); + } else if (entityPlacerConfig instanceof QueuedMultiplePlacerConfig queuedMultiplePlacerConfig) { + return new QueuedMultiplePlacerFactory<>(queuedMultiplePlacerConfig); } else { throw new IllegalArgumentException(String.format("Unknown %s type: (%s).", EntityPlacerConfig.class.getSimpleName(), entityPlacerConfig.getClass().getName())); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/QueuedEntityPlacerFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/QueuedEntityPlacerFactory.java index d013a1ec8b3..5fc5e40793e 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/QueuedEntityPlacerFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/QueuedEntityPlacerFactory.java @@ -87,7 +87,9 @@ public QueuedEntityPlacer buildEntityPlacer(HeuristicConfigPolicy !variableDescriptor.isListVariable()) + .toList(); var subMoveSelectorConfigList = new ArrayList(variableDescriptorList.size()); for (var variableDescriptor : variableDescriptorList) { subMoveSelectorConfigList @@ -104,7 +106,7 @@ public QueuedEntityPlacer buildEntityPlacer(HeuristicConfigPolicy configPolicy) { var entitySelectorConfig = config.getEntitySelectorConfig(); if (entitySelectorConfig == null) { - var entityDescriptor = getTheOnlyEntityDescriptor(configPolicy.getSolutionDescriptor()); + var entityDescriptor = getTheOnlyEntityDescriptorWithBasicVariables(configPolicy.getSolutionDescriptor()); entitySelectorConfig = getDefaultEntitySelectorConfigForEntity(configPolicy, entityDescriptor); } else { // The default phase configuration generates the entity selector config without an updated version of the configuration policy. diff --git a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/QueuedMultiplePlacer.java b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/QueuedMultiplePlacer.java new file mode 100644 index 00000000000..8c051816b1a --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/QueuedMultiplePlacer.java @@ -0,0 +1,249 @@ +package ai.timefold.solver.core.impl.constructionheuristic.placer; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +import ai.timefold.solver.core.impl.heuristic.HeuristicConfigPolicy; +import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionFilter; +import ai.timefold.solver.core.impl.heuristic.selector.common.iterator.UpcomingSelectionIterator; +import ai.timefold.solver.core.impl.heuristic.selector.move.composite.CartesianProductMoveSelector; +import ai.timefold.solver.core.impl.move.generic.CompositeMove; +import ai.timefold.solver.core.impl.phase.event.PhaseLifecycleListener; +import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; +import ai.timefold.solver.core.impl.phase.scope.AbstractStepScope; +import ai.timefold.solver.core.impl.solver.scope.SolverScope; +import ai.timefold.solver.core.preview.api.move.Move; + +public class QueuedMultiplePlacer extends AbstractEntityPlacer + implements EntityPlacer { + + private final List> queuedPlacerList; + + public QueuedMultiplePlacer(EntityPlacerFactory factory, + HeuristicConfigPolicy configPolicy, List> queuedPlacerList) { + super(factory, configPolicy); + this.queuedPlacerList = queuedPlacerList; + this.queuedPlacerList.forEach(queuedPlacer -> phaseLifecycleSupport.addEventListener(queuedPlacer)); + + } + + @Override + public EntityPlacer rebuildWithFilter(SelectionFilter filter) { + var filteredQueuedPlacerList = queuedPlacerList.stream() + .map(placer -> placer.rebuildWithFilter(filter)) + .toList(); + return new QueuedMultiplePlacer<>(factory, configPolicy, filteredQueuedPlacerList); + } + + @Override + public Iterator> iterator() { + var iterator = new MultipleQueuedPlacingIterator(queuedPlacerList); + phaseLifecycleSupport.addEventListener(iterator); + return iterator; + } + + private class MultipleQueuedPlacingIterator extends UpcomingSelectionIterator> + implements PhaseLifecycleListener { + + private final List> queuedPlacerList; + private Iterator>[] moveIterators; + private Iterator>[] placementIterators; + private Move[] previousMove; + private Move cachedMove = null; + + private MultipleQueuedPlacingIterator(List> queuedPlacerList) { + // We expect only the QueuedValuePlacer and a QueuedEntityPlacer + var assertSize = queuedPlacerList.size() == 2; + var assertQueuedValuePlacer = + queuedPlacerList.stream().anyMatch(QueuedValuePlacer.class::isInstance); + var assertQueuedEntityPlacer = + queuedPlacerList.stream().anyMatch(QueuedEntityPlacer.class::isInstance); + if (!assertSize || !assertQueuedValuePlacer || !assertQueuedEntityPlacer) { + throw new IllegalArgumentException( + "Impossible state: the queued placer list must consist exclusively of a QueuedValuePlacer and a QueuedEntityPlacer."); + } + this.queuedPlacerList = new ArrayList<>(); + // We make sure that the QueuedEntityPlacer is added first + this.queuedPlacerList.addAll(queuedPlacerList.stream().filter(QueuedValuePlacer.class::isInstance).toList()); + this.queuedPlacerList.addAll(queuedPlacerList.stream().filter(QueuedEntityPlacer.class::isInstance).toList()); + reset(); + } + + /** + * The method uses a strategy similar to {@link CartesianProductMoveSelector}, + * but it uses placer iterators instead. + */ + private Move nextMove() { + if (cachedMove != null) { + return cachedMove; + } + var childSize = moveIterators.length; + int index; + Move[] move = new Move[childSize]; + if (previousMove == null) { + index = -1; + } else { + index = consumeNextMove(move, previousMove); + if (index == -1) { + // No more moves + return null; + } + } + var updatedMove = updateNextIterators(index, move); + if (updatedMove == null) { + // We stop if one of the placement iterators has no next placement + return null; + } + previousMove = updatedMove; + cachedMove = CompositeMove.buildMove(updatedMove); + return cachedMove; + } + + /** + * Go through the registered iterators and check for any available moves. + * + * @param move the move array to be loaded + * @param previousMove the previous moves + * @return the last index of the iterator that still has available moves; otherwise, the function returns -1 + * when all iterators have no more moves available. + */ + private int consumeNextMove(Move[] move, Move[] previousMove) { + var index = move.length - 1; + // Look for the first iterator that still has available moves to generate + while (index >= 0) { + var moveIterator = moveIterators[index]; + if (moveIterator.hasNext()) { + break; + } + // Check if there are more placements available in the QueuedEntityPlacer + if (index == 1) { + var placementIterator = placementIterators[index]; + if (placementIterator.hasNext()) { + moveIterators[index] = placementIterator.next().iterator(); + continue; + } else { + // Reset the iterator in case the previous placerIterator still has more placements + placementIterators[index] = queuedPlacerList.get(index).iterator(); + } + } + index--; + } + if (index < 0) { + return -1; + } + // Copy the previous move until the next one generated + System.arraycopy(previousMove, 0, move, 0, index); + // Generate and set the new move + move[index] = moveIterators[index].next(); + return index; + } + + /** + * Update the move list and recreate all move iterators starting from #lastValidIteratorIndex. + * + * @param lastValidIteratorIndex the index of the last iterator that generated a valid move + * @param move the move array to be loaded + */ + private Move[] updateNextIterators(int lastValidIteratorIndex, Move[] move) { + var childSize = moveIterators.length; + var updatedMove = new Move[childSize]; + System.arraycopy(move, 0, updatedMove, 0, childSize); + for (int i = lastValidIteratorIndex + 1; i < childSize; i++) { + var placementIterator = placementIterators[i]; + Move next; + if (!placementIterator.hasNext()) { + return null; + } else { + var moveIterator = placementIterator.next().iterator(); + moveIterators[i] = moveIterator; + next = moveIterator.next(); + } + updatedMove[i] = next; + } + return updatedMove; + } + + private void clearCache() { + this.cachedMove = null; + } + + @SuppressWarnings("unchecked") + private void reset() { + if (moveIterators == null) { + moveIterators = new Iterator[queuedPlacerList.size()]; + Arrays.fill(moveIterators, null); + placementIterators = new Iterator[queuedPlacerList.size()]; + for (var i = 0; i < queuedPlacerList.size(); i++) { + var placement = queuedPlacerList.get(i); + placementIterators[i] = placement.iterator(); + } + } else { + Arrays.fill(moveIterators, null); + // We need to reset of the QueuedEntityPlacer or there will be no more moves for the basic variables + placementIterators[1] = queuedPlacerList.get(1).iterator(); + } + previousMove = null; + } + + @Override + protected Placement createUpcomingSelection() { + var nextMove = nextMove(); + if (nextMove == null) { + return noUpcomingSelection(); + } + return new Placement<>(new PlacementToMoveAdapterIterator(this)); + } + + @Override + public void phaseStarted(AbstractPhaseScope phaseScope) { + // Ignore + } + + @Override + public void stepStarted(AbstractStepScope stepScope) { + // Ignore + } + + @Override + public void stepEnded(AbstractStepScope stepScope) { + reset(); + } + + @Override + public void phaseEnded(AbstractPhaseScope phaseScope) { + // Ignore + } + + @Override + public void solvingStarted(SolverScope solverScope) { + // Ignore + } + + @Override + public void solvingEnded(SolverScope solverScope) { + // Ignore + } + } + + private class PlacementToMoveAdapterIterator implements Iterator> { + private final MultipleQueuedPlacingIterator iterator; + + private PlacementToMoveAdapterIterator(MultipleQueuedPlacingIterator iterator) { + this.iterator = iterator; + } + + @Override + public boolean hasNext() { + return iterator.nextMove() != null; + } + + @Override + public Move next() { + var move = iterator.nextMove(); + iterator.clearCache(); + return move; + } + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/QueuedMultiplePlacerFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/QueuedMultiplePlacerFactory.java new file mode 100644 index 00000000000..6a08c8a08de --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/QueuedMultiplePlacerFactory.java @@ -0,0 +1,24 @@ +package ai.timefold.solver.core.impl.constructionheuristic.placer; + +import java.util.ArrayList; + +import ai.timefold.solver.core.impl.constructionheuristic.placer.internal.QueuedMultiplePlacerConfig; +import ai.timefold.solver.core.impl.heuristic.HeuristicConfigPolicy; + +public final class QueuedMultiplePlacerFactory + extends AbstractEntityPlacerFactory { + + public QueuedMultiplePlacerFactory(QueuedMultiplePlacerConfig config) { + super(config); + } + + @Override + public EntityPlacer buildEntityPlacer(HeuristicConfigPolicy configPolicy) { + var queuedPlacerList = new ArrayList>(); + for (var placerConfig : config.getPlacerConfigList()) { + var placer = EntityPlacerFactory. create(placerConfig).buildEntityPlacer(configPolicy); + queuedPlacerList.add(placer); + } + return new QueuedMultiplePlacer<>(this, configPolicy, queuedPlacerList); + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/internal/QueuedMultiplePlacerConfig.java b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/internal/QueuedMultiplePlacerConfig.java new file mode 100644 index 00000000000..a2d597e98f3 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/internal/QueuedMultiplePlacerConfig.java @@ -0,0 +1,57 @@ +package ai.timefold.solver.core.impl.constructionheuristic.placer.internal; + +import java.util.List; +import java.util.function.Consumer; + +import ai.timefold.solver.core.config.constructionheuristic.placer.EntityPlacerConfig; +import ai.timefold.solver.core.config.util.ConfigUtils; + +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +public class QueuedMultiplePlacerConfig extends EntityPlacerConfig { + + protected List placerConfigList = null; + + public List getPlacerConfigList() { + return placerConfigList; + } + + public void setPlacerConfigList(List placerConfigList) { + this.placerConfigList = placerConfigList; + } + + // ************************************************************************ + // With methods + // ************************************************************************ + + public @NonNull QueuedMultiplePlacerConfig + withPlacerConfigList(@NonNull List<@NonNull EntityPlacerConfig> placerConfigList) { + setPlacerConfigList(placerConfigList); + return this; + } + + // ************************************************************************ + // Worker methods + // ************************************************************************ + + @Override + public @NonNull QueuedMultiplePlacerConfig + inherit(@NonNull QueuedMultiplePlacerConfig inheritedConfig) { + placerConfigList = + ConfigUtils.inheritMergeableListConfig(placerConfigList, inheritedConfig.getPlacerConfigList()); + return this; + } + + @Override + public @NonNull QueuedMultiplePlacerConfig copyConfig() { + return new QueuedMultiplePlacerConfig().inherit(this); + } + + @Override + public void visitReferencedClasses(@NonNull Consumer<@Nullable Class> classVisitor) { + if (placerConfigList != null) { + placerConfigList.forEach(placer -> placer.visitReferencedClasses(classVisitor)); + } + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/entity/descriptor/EntityDescriptor.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/entity/descriptor/EntityDescriptor.java index af00c37ffbf..e8b5c2a5404 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/entity/descriptor/EntityDescriptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/entity/descriptor/EntityDescriptor.java @@ -699,6 +699,24 @@ public boolean hasAnyGenuineVariables() { return !effectiveGenuineVariableDescriptorMap.isEmpty(); } + public boolean hasAnyGenuineBasicVariables() { + if (!isGenuine()) { + return false; + } + return getDeclaredGenuineVariableDescriptors().stream() + .anyMatch(descriptor -> !descriptor.isListVariable()); + } + + public boolean hasAnyGenuineChainedVariables() { + if (!isGenuine()) { + return false; + } + return getDeclaredGenuineVariableDescriptors().stream() + .filter(descriptor -> descriptor instanceof BasicVariableDescriptor) + .map(descriptor -> (BasicVariableDescriptor) descriptor) + .anyMatch(BasicVariableDescriptor::isChained); + } + public boolean hasAnyGenuineListVariables() { if (!isGenuine()) { return false; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/SolutionDescriptor.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/SolutionDescriptor.java index 2ab6ec4333f..08f28243c35 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/SolutionDescriptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/SolutionDescriptor.java @@ -727,11 +727,13 @@ private void validateListVariableDescriptors() { var listVariableDescriptor = listVariableDescriptorList.get(0); var listVariableEntityDescriptor = listVariableDescriptor.getEntityDescriptor(); - if (listVariableEntityDescriptor.getGenuineVariableDescriptorList().size() > 1) { + // We will not support chained and list variables at the same entity, + // and the validation can be removed once we discontinue support for chained variables. + if (hasChainedVariable()) { var basicVariableDescriptorList = new ArrayList<>(listVariableEntityDescriptor.getGenuineVariableDescriptorList()); basicVariableDescriptorList.remove(listVariableDescriptor); throw new UnsupportedOperationException( - "Combining basic variables (%s) with list variables (%s) on a single planning entity (%s) is not supported." + "Combining chained variables (%s) with list variables (%s) on a single planning entity (%s) is not supported." .formatted(basicVariableDescriptorList, listVariableDescriptor, listVariableDescriptor.getEntityDescriptor().getEntityClass().getCanonicalName())); } @@ -877,6 +879,22 @@ public PlanningSolutionMetaModel getMetaModel() { return planningSolutionMetaModel; } + public boolean hasBasicVariable() { + return getGenuineEntityDescriptors().stream().anyMatch(EntityDescriptor::hasAnyGenuineBasicVariables); + } + + public boolean hasChainedVariable() { + return getGenuineEntityDescriptors().stream().anyMatch(EntityDescriptor::hasAnyGenuineChainedVariables); + } + + public boolean hasListVariable() { + return getListVariableDescriptor() != null; + } + + public boolean hasBothBasicAndListVariables() { + return hasBasicVariable() && hasListVariable(); + } + /** * @deprecated {@link ConstraintConfiguration} was replaced by {@link ConstraintWeightOverrides}. */ diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/anchor/AnchorShadowVariableDescriptor.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/anchor/AnchorShadowVariableDescriptor.java index e462b74a075..d87db428198 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/anchor/AnchorShadowVariableDescriptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/anchor/AnchorShadowVariableDescriptor.java @@ -92,4 +92,9 @@ public Iterable> buildVariableListeners(S sourceVariableDescriptor).toCollection(); } + @Override + public boolean isListVariableSource() { + return false; + } + } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/cascade/CascadingUpdateShadowVariableDescriptor.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/cascade/CascadingUpdateShadowVariableDescriptor.java index 87b0d1daca9..f61fd950810 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/cascade/CascadingUpdateShadowVariableDescriptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/cascade/CascadingUpdateShadowVariableDescriptor.java @@ -165,6 +165,11 @@ public Iterable> buildVariableListeners(S throw new UnsupportedOperationException("Cascade update element generates no listeners."); } + @Override + public boolean isListVariableSource() { + return false; + } + private record ShadowVariableTarget(EntityDescriptor entityDescriptor, MemberAccessor variableMemberAccessor) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/custom/CustomShadowVariableDescriptor.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/custom/CustomShadowVariableDescriptor.java index 82e50c88c03..27094a7b5ef 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/custom/CustomShadowVariableDescriptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/custom/CustomShadowVariableDescriptor.java @@ -148,4 +148,9 @@ public Iterable> buildVariableListeners(S return new VariableListenerWithSources<>(variableListener, classListEntry.getValue()); }).collect(Collectors.toList()); } + + @Override + public boolean isListVariableSource() { + return false; + } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/custom/LegacyCustomShadowVariableDescriptor.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/custom/LegacyCustomShadowVariableDescriptor.java index fbe233de51f..116efe879dc 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/custom/LegacyCustomShadowVariableDescriptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/custom/LegacyCustomShadowVariableDescriptor.java @@ -231,4 +231,9 @@ public Iterable> buildVariableListeners(S return new VariableListenerWithSources<>(variableListener, sourceVariableDescriptorList).toCollection(); } + @Override + public boolean isListVariableSource() { + return false; + } + } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/custom/PiggybackShadowVariableDescriptor.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/custom/PiggybackShadowVariableDescriptor.java index eb6424f2e15..e5e9c2158fa 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/custom/PiggybackShadowVariableDescriptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/custom/PiggybackShadowVariableDescriptor.java @@ -110,4 +110,9 @@ public Iterable> buildVariableListeners(S throw new UnsupportedOperationException("The piggybackShadowVariableDescriptor (" + this + ") cannot build a variable listener."); } + + @Override + public boolean isListVariableSource() { + return false; + } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/declarative/DeclarativeShadowVariableDescriptor.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/declarative/DeclarativeShadowVariableDescriptor.java index 158037333d0..c01e650956b 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/declarative/DeclarativeShadowVariableDescriptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/declarative/DeclarativeShadowVariableDescriptor.java @@ -113,6 +113,11 @@ public void linkVariableDescriptors(DescriptorPolicy descriptorPolicy) { } } + @Override + public boolean isListVariableSource() { + return false; + } + public MemberAccessor getMemberAccessor() { return variableMemberAccessor; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/declarative/ShadowVariableLoopedVariableDescriptor.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/declarative/ShadowVariableLoopedVariableDescriptor.java index ea98ec66bb2..18921a03851 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/declarative/ShadowVariableLoopedVariableDescriptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/declarative/ShadowVariableLoopedVariableDescriptor.java @@ -61,4 +61,9 @@ public Iterable> buildVariableListeners(S public void linkVariableDescriptors(DescriptorPolicy descriptorPolicy) { // no action needed } + + @Override + public boolean isListVariableSource() { + return false; + } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/descriptor/ShadowVariableDescriptor.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/descriptor/ShadowVariableDescriptor.java index 56de9ed1f2f..121f5d8994f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/descriptor/ShadowVariableDescriptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/descriptor/ShadowVariableDescriptor.java @@ -64,6 +64,11 @@ public boolean hasVariableListener() { return true; } + /** + * return true if the source variable is a list variable; otherwise, return false. + */ + public abstract boolean isListVariableSource(); + /** * @param supplyManager never null * @return never null 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 b07a895260d..618f1b2c76a 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 @@ -114,4 +114,9 @@ public Iterable> buildVariableListeners(S public Integer getValue(Object entity) { return super.getValue(entity); } + + @Override + public boolean isListVariableSource() { + return true; + } } 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 aa58a5e6d38..d29ca6f62ed 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 @@ -17,6 +17,7 @@ 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; @@ -182,4 +183,8 @@ private AbstractVariableListener buildVariableListener() { } } + @Override + public boolean isListVariableSource() { + return sourceVariableDescriptor instanceof ListVariableDescriptor; + } } 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 65f6c3a36ae..44b3c4dcf82 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 @@ -112,14 +112,21 @@ public void linkVariableListeners() { .flatMap(Collection::stream) .filter(ShadowVariableDescriptor::hasVariableListener) .sorted(Comparator.comparingInt(ShadowVariableDescriptor::getGlobalShadowOrder)) - .forEach(d -> { + .forEach(descriptor -> { // 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. + // 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); + processShadowVariableDescriptorWithoutListVariable(descriptor); } else { - processShadowVariableDescriptorWithListVariable(d, listVariableStateSupply); + // When multiple variable types are used, + // the shadow variable process needs to account for each variable + // and process them according to their types. + if (descriptor.isListVariableSource()) { + processShadowVariableDescriptorWithListVariable(descriptor, listVariableStateSupply); + } else { + processShadowVariableDescriptorWithoutListVariable(descriptor); + } } }); } 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 fb7efe38168..e8782be6247 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 @@ -97,4 +97,9 @@ public Demand getProvidedDemand() { throw new UnsupportedOperationException("Impossible state: Handled by %s." .formatted(ListVariableStateSupply.class.getSimpleName())); } + + @Override + public boolean isListVariableSource() { + return true; + } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListChangeMoveSelectorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListChangeMoveSelectorFactory.java index 9c915c34734..7a3fc874114 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListChangeMoveSelectorFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListChangeMoveSelectorFactory.java @@ -12,6 +12,7 @@ import ai.timefold.solver.core.config.heuristic.selector.move.MoveSelectorConfig; import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.ListChangeMoveSelectorConfig; import ai.timefold.solver.core.config.heuristic.selector.value.ValueSelectorConfig; +import ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor; import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; import ai.timefold.solver.core.impl.domain.variable.descriptor.VariableDescriptor; import ai.timefold.solver.core.impl.heuristic.HeuristicConfigPolicy; @@ -71,8 +72,18 @@ protected MoveSelectorConfig buildUnfoldedMoveSelectorConfig(HeuristicConfigP : EntitySelectorFactory. create(destinationEntitySelectorConfig) .extractEntityDescriptor(configPolicy); var entityDescriptors = - onlyEntityDescriptor == null ? configPolicy.getSolutionDescriptor().getGenuineEntityDescriptors() + onlyEntityDescriptor == null ? configPolicy.getSolutionDescriptor().getGenuineEntityDescriptors().stream() + // We need to filter the entity that defines the list variable + .filter(EntityDescriptor::hasAnyGenuineListVariables) + .toList() : Collections.singletonList(onlyEntityDescriptor); + + if (entityDescriptors.isEmpty()) { + throw new IllegalArgumentException( + "The listChangeMoveSelector (%s) cannot unfold because there are no planning list variables." + .formatted(config)); + } + if (entityDescriptors.size() > 1) { throw new IllegalArgumentException(""" The listChangeMoveSelector (%s) cannot unfold when there are multiple entities (%s). @@ -123,11 +134,6 @@ The listChangeMoveSelector (%s) is configured to use a planning variable (%s), \ .map(variableDescriptor -> ((ListVariableDescriptor) variableDescriptor)) .toList()); } - if (variableDescriptorList.isEmpty()) { - throw new IllegalArgumentException( - "The listChangeMoveSelector (%s) cannot unfold because there are no planning list variables." - .formatted(config)); - } if (variableDescriptorList.size() > 1) { throw new IllegalArgumentException( "The listChangeMoveSelector (%s) cannot unfold because there are multiple planning list variables." diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ruin/ListRuinRecreateMoveSelectorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ruin/ListRuinRecreateMoveSelectorFactory.java index fb186201405..b9386ddabd9 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ruin/ListRuinRecreateMoveSelectorFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ruin/ListRuinRecreateMoveSelectorFactory.java @@ -42,7 +42,7 @@ protected MoveSelector buildBaseMoveSelector(HeuristicConfigPolicy(valueSelector, listVariableDescriptor, constructionHeuristicPhaseBuilder, diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/ValueSelectorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/ValueSelectorFactory.java index 6de40c96620..480a215298e 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/ValueSelectorFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/ValueSelectorFactory.java @@ -134,7 +134,6 @@ public ValueSelector buildValueSelector(HeuristicConfigPolicy buildMimicReplaying(HeuristicConfigPolicy configPolicy) { if (config.getId() != null - || config.getVariableName() != null || config.getCacheType() != null || config.getSelectionOrder() != null || config.getNearbySelectionConfig() != null diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java index 6daa0a87c0e..4726ac294b0 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java @@ -68,7 +68,10 @@ public LocalSearchPhase buildPhase(int phaseIndex, boolean lastInitia The solver configuration enabled both move selectors and Move Streams. These are mutually exclusive features, please pick one or the other."""); } - + if (solverConfigPolicy.getSolutionDescriptor().hasBothBasicAndListVariables()) { + throw new UnsupportedOperationException( + "A mixed model using both basic and list variables is not supported yet."); + } var phaseConfigPolicy = solverConfigPolicy.createPhaseConfigPolicy(); var phaseTermination = buildPhaseTermination(phaseConfigPolicy, solverTermination); var decider = moveStreamsEnabled diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverFactory.java index 2993b256210..935319ed64a 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverFactory.java @@ -250,7 +250,7 @@ public List> buildPhaseList(HeuristicConfigPolicy co .getDefaultEntitySelectorConfigForEntity(configPolicy, genuineEntityDescriptor)); } - constructionHeuristicPhaseConfig.setEntityPlacerConfig(entityPlacerConfig); + constructionHeuristicPhaseConfig.setEntityPlacerConfigList(List.of(entityPlacerConfig)); phaseConfigList.add(constructionHeuristicPhaseConfig); } phaseConfigList.add(new LocalSearchPhaseConfig()); diff --git a/core/src/main/java/ai/timefold/solver/core/preview/api/domain/metamodel/PlanningSolutionMetaModel.java b/core/src/main/java/ai/timefold/solver/core/preview/api/domain/metamodel/PlanningSolutionMetaModel.java index a7fbcffb7dd..8de29a2d9e8 100644 --- a/core/src/main/java/ai/timefold/solver/core/preview/api/domain/metamodel/PlanningSolutionMetaModel.java +++ b/core/src/main/java/ai/timefold/solver/core/preview/api/domain/metamodel/PlanningSolutionMetaModel.java @@ -84,5 +84,4 @@ default boolean hasEntity(Class entityClass) { } return false; } - } diff --git a/core/src/main/resources/solver.xsd b/core/src/main/resources/solver.xsd index 74067637912..b3fe9064749 100644 --- a/core/src/main/resources/solver.xsd +++ b/core/src/main/resources/solver.xsd @@ -255,7 +255,7 @@ - + diff --git a/core/src/test/java/ai/timefold/solver/core/api/solver/SolutionManagerTest.java b/core/src/test/java/ai/timefold/solver/core/api/solver/SolutionManagerTest.java index dfa7e87909e..fb32a936fa3 100644 --- a/core/src/test/java/ai/timefold/solver/core/api/solver/SolutionManagerTest.java +++ b/core/src/test/java/ai/timefold/solver/core/api/solver/SolutionManagerTest.java @@ -31,10 +31,10 @@ import ai.timefold.solver.core.testdomain.list.shadowhistory.TestdataListSolutionWithShadowHistory; import ai.timefold.solver.core.testdomain.list.shadowhistory.TestdataListValueWithShadowHistory; import ai.timefold.solver.core.testdomain.list.shadowhistory.TestdataListWithShadowHistoryIncrementalScoreCalculator; -import ai.timefold.solver.core.testdomain.multivar.TestdataMultiVarEntity; -import ai.timefold.solver.core.testdomain.multivar.TestdataMultiVarSolution; -import ai.timefold.solver.core.testdomain.multivar.TestdataMultivarIncrementalScoreCalculator; -import ai.timefold.solver.core.testdomain.multivar.TestdataOtherValue; +import ai.timefold.solver.core.testdomain.multivar.basic.TestdataMultiVarEntity; +import ai.timefold.solver.core.testdomain.multivar.basic.TestdataMultiVarSolution; +import ai.timefold.solver.core.testdomain.multivar.basic.TestdataMultivarIncrementalScoreCalculator; +import ai.timefold.solver.core.testdomain.multivar.basic.TestdataOtherValue; import ai.timefold.solver.core.testdomain.shadow.TestdataShadowedEntity; import ai.timefold.solver.core.testdomain.shadow.TestdataShadowedIncrementalScoreCalculator; import ai.timefold.solver.core.testdomain.shadow.TestdataShadowedSolution; diff --git a/core/src/test/java/ai/timefold/solver/core/config/solver/SolverConfigTest.java b/core/src/test/java/ai/timefold/solver/core/config/solver/SolverConfigTest.java index 77b8fbb2db0..d03a4f6ed55 100644 --- a/core/src/test/java/ai/timefold/solver/core/config/solver/SolverConfigTest.java +++ b/core/src/test/java/ai/timefold/solver/core/config/solver/SolverConfigTest.java @@ -291,19 +291,6 @@ void entityWithTwoPlanningListVariables() { .hasMessageContaining("secondListVariable"); } - @Test - void entityWithMixedBasicAndPlanningListVariables() { - var solverConfig = new SolverConfig() - .withSolutionClass(DummySolutionWithMixedSimpleAndListVariableEntity.class) - .withEntityClasses(DummyEntityWithMixedSimpleAndListVariable.class) - .withEasyScoreCalculatorClass(DummyRecordEasyScoreCalculator.class); - assertThatThrownBy(() -> SolverFactory.create(solverConfig)) - .isExactlyInstanceOf(UnsupportedOperationException.class) - .hasMessageContaining(DummyEntityWithMixedSimpleAndListVariable.class.getSimpleName()) - .hasMessageContaining("listVariable") - .hasMessageContaining("basicVariable"); - } - @PlanningSolution private record DummyRecordSolution( @PlanningEntityCollectionProperty List entities, diff --git a/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseTest.java b/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseTest.java index e86fadf5dd0..cbc6bff7f2f 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseTest.java @@ -2,6 +2,7 @@ import static ai.timefold.solver.core.testutil.PlannerAssert.assertCode; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.SoftAssertions.assertSoftly; @@ -16,8 +17,15 @@ import ai.timefold.solver.core.api.solver.SolverFactory; import ai.timefold.solver.core.config.constructionheuristic.ConstructionHeuristicPhaseConfig; import ai.timefold.solver.core.config.constructionheuristic.ConstructionHeuristicType; +import ai.timefold.solver.core.config.constructionheuristic.placer.PooledEntityPlacerConfig; +import ai.timefold.solver.core.config.constructionheuristic.placer.QueuedEntityPlacerConfig; +import ai.timefold.solver.core.config.constructionheuristic.placer.QueuedValuePlacerConfig; +import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.ListChangeMoveSelectorConfig; +import ai.timefold.solver.core.config.heuristic.selector.value.ValueSelectorConfig; +import ai.timefold.solver.core.config.localsearch.LocalSearchPhaseConfig; import ai.timefold.solver.core.config.solver.monitoring.MonitoringConfig; import ai.timefold.solver.core.config.solver.monitoring.SolverMetric; +import ai.timefold.solver.core.config.solver.termination.TerminationConfig; import ai.timefold.solver.core.impl.phase.event.PhaseLifecycleListenerAdapter; import ai.timefold.solver.core.impl.solver.DefaultSolver; import ai.timefold.solver.core.impl.solver.scope.SolverScope; @@ -27,10 +35,23 @@ import ai.timefold.solver.core.testdomain.list.TestdataListEntity; import ai.timefold.solver.core.testdomain.list.TestdataListSolution; import ai.timefold.solver.core.testdomain.list.TestdataListValue; +import ai.timefold.solver.core.testdomain.list.TestdataListVarEasyScoreCalculator; import ai.timefold.solver.core.testdomain.list.unassignedvar.TestdataAllowsUnassignedValuesListEasyScoreCalculator; import ai.timefold.solver.core.testdomain.list.unassignedvar.TestdataAllowsUnassignedValuesListEntity; import ai.timefold.solver.core.testdomain.list.unassignedvar.TestdataAllowsUnassignedValuesListSolution; import ai.timefold.solver.core.testdomain.list.unassignedvar.TestdataAllowsUnassignedValuesListValue; +import ai.timefold.solver.core.testdomain.multivar.list.multientity.TestdataListMultiEntityEasyScoreCalculator; +import ai.timefold.solver.core.testdomain.multivar.list.multientity.TestdataListMultiEntityFirstEntity; +import ai.timefold.solver.core.testdomain.multivar.list.multientity.TestdataListMultiEntitySecondEntity; +import ai.timefold.solver.core.testdomain.multivar.list.multientity.TestdataListMultiEntitySolution; +import ai.timefold.solver.core.testdomain.multivar.list.singleentity.TestdataListMultiVarEasyScoreCalculator; +import ai.timefold.solver.core.testdomain.multivar.list.singleentity.TestdataListMultiVarEntity; +import ai.timefold.solver.core.testdomain.multivar.list.singleentity.TestdataListMultiVarOtherValue; +import ai.timefold.solver.core.testdomain.multivar.list.singleentity.TestdataListMultiVarSolution; +import ai.timefold.solver.core.testdomain.multivar.list.singleentity.TestdataListMultiVarValue; +import ai.timefold.solver.core.testdomain.multivar.list.singleentity.unassignedvar.TestdataUnassignedListMultiVarEasyScoreCalculator; +import ai.timefold.solver.core.testdomain.multivar.list.singleentity.unassignedvar.TestdataUnassignedListMultiVarEntity; +import ai.timefold.solver.core.testdomain.multivar.list.singleentity.unassignedvar.TestdataUnassignedListMultiVarSolution; import ai.timefold.solver.core.testdomain.pinned.TestdataPinnedEntity; import ai.timefold.solver.core.testdomain.pinned.TestdataPinnedSolution; import ai.timefold.solver.core.testdomain.pinned.unassignedvar.TestdataPinnedAllowsUnassignedEntity; @@ -342,4 +363,275 @@ void constructionHeuristicAllocateToValueFromQueue() { .filter(e -> e.getValue() == null)).isEmpty(); } + @Test + void failWithExceededMultipleQueuedEntityPlacers() { + var solverConfig = PlannerTestUtils.buildSolverConfig(TestdataSolution.class, TestdataEntity.class) + .withPhases(new ConstructionHeuristicPhaseConfig() + .withEntityPlacerConfigList(new QueuedEntityPlacerConfig(), new QueuedEntityPlacerConfig(), + new QueuedEntityPlacerConfig())); + + assertThatCode(() -> PlannerTestUtils.solve(solverConfig, new TestdataSolution("s1"))) + .hasMessageContaining( + "The Construction Heuristic configuration (ConstructionHeuristicPhaseConfig) only support a maximum of two entity placers."); + } + + @Test + void failWithMultipleQueuedEntityPlacers() { + var solverConfig = PlannerTestUtils.buildSolverConfig(TestdataSolution.class, TestdataEntity.class) + .withPhases(new ConstructionHeuristicPhaseConfig() + .withEntityPlacerConfigList(new QueuedEntityPlacerConfig(), new QueuedEntityPlacerConfig())); + + assertThatCode(() -> PlannerTestUtils.solve(solverConfig, new TestdataSolution("s1"))) + .hasMessageContaining( + "The Construction Heuristic configuration (ConstructionHeuristicPhaseConfig) cannot contain duplicate placer configurations.") + .hasMessageContaining("Maybe define multiple move selectors if there are more than one basic variables"); + + var solverConfig2 = PlannerTestUtils.buildSolverConfig(TestdataSolution.class, TestdataEntity.class) + .withPhases(new ConstructionHeuristicPhaseConfig() + .withEntityPlacerConfigList(new QueuedValuePlacerConfig(), new QueuedValuePlacerConfig())); + + assertThatCode(() -> PlannerTestUtils.solve(solverConfig2, new TestdataSolution("s1"))) + .hasMessageContaining( + "The Construction Heuristic configuration (ConstructionHeuristicPhaseConfig) cannot contain duplicate placer configurations."); + } + + @Test + void failWithPooledEntityPlacers() { + var solverConfig = PlannerTestUtils.buildSolverConfig(TestdataSolution.class, TestdataEntity.class) + .withPhases(new ConstructionHeuristicPhaseConfig() + .withEntityPlacerConfigList(new QueuedEntityPlacerConfig(), new PooledEntityPlacerConfig())); + + assertThatCode(() -> PlannerTestUtils.solve(solverConfig, new TestdataSolution("s1"))) + .hasMessageContaining( + "The Construction Heuristic configuration (ConstructionHeuristicPhaseConfig) does not support multiple configurations when using the pooled placer configuration PooledEntityPlacerConfig."); + } + + @Test + void failMultiEntityWithListAndBasicVariables() { + var solverConfig = PlannerTestUtils.buildSolverConfig( + TestdataListMultiEntitySolution.class, TestdataListMultiEntityFirstEntity.class, + TestdataListMultiEntitySecondEntity.class) + .withPhases(new ConstructionHeuristicPhaseConfig() + .withTerminationConfig(new TerminationConfig().withStepCountLimit(16))) + .withEasyScoreCalculatorClass(TestdataListMultiVarEasyScoreCalculator.class); + + var problem = TestdataListMultiEntitySolution.generateUninitializedSolution(2, 2, 2); + assertThatCode(() -> PlannerTestUtils.solve(solverConfig, problem)) + .hasMessageContaining("has no entityClass configured and because there are multiple in the entityClassSet") + .hasMessageContaining("it cannot be deduced automatically"); + } + + @Test + void failLocalSearchWithListAndBasicVariables() { + var solverConfig = PlannerTestUtils.buildSolverConfig( + TestdataListMultiVarSolution.class, TestdataListMultiVarEntity.class, TestdataListMultiVarValue.class, + TestdataListMultiVarOtherValue.class) + .withPhases(new ConstructionHeuristicPhaseConfig() + .withTerminationConfig(new TerminationConfig().withStepCountLimit(16)), + new LocalSearchPhaseConfig()) + .withEasyScoreCalculatorClass(TestdataListMultiVarEasyScoreCalculator.class); + + var problem = TestdataListMultiVarSolution.generateUninitializedSolution(2, 2, 2); + assertThatCode(() -> PlannerTestUtils.solve(solverConfig, problem)) + .hasMessageContaining("A mixed model using both basic and list variables is not supported yet."); + } + + @Test + void solveWithListAndBasicVariables() { + var solverConfig = PlannerTestUtils.buildSolverConfig( + TestdataListMultiVarSolution.class, TestdataListMultiVarEntity.class, TestdataListMultiVarValue.class, + TestdataListMultiVarOtherValue.class) + .withPhases(new ConstructionHeuristicPhaseConfig() + .withTerminationConfig(new TerminationConfig().withStepCountLimit(16))) + .withEasyScoreCalculatorClass(TestdataListMultiVarEasyScoreCalculator.class); + + var problem = TestdataListMultiVarSolution.generateUninitializedSolution(2, 2, 2); + var solution = PlannerTestUtils.solve(solverConfig, problem); + assertThat(solution.getEntityList().stream() + .filter(e -> e.getBasicValue() == null || e.getSecondBasicValue() == null || e.getValueList().isEmpty())) + .isEmpty(); + } + + @Test + void solvePinnedWithListAndBasicVariables() { + var solverConfig = PlannerTestUtils.buildSolverConfig( + TestdataListMultiVarSolution.class, TestdataListMultiVarEntity.class, TestdataListMultiVarValue.class, + TestdataListMultiVarOtherValue.class) + .withPhases(new ConstructionHeuristicPhaseConfig() + .withTerminationConfig(new TerminationConfig().withStepCountLimit(16))) + .withEasyScoreCalculatorClass(TestdataListMultiVarEasyScoreCalculator.class); + + var problem = TestdataListMultiVarSolution.generateUninitializedSolution(2, 2, 2); + // Pin the first entity + problem.getEntityList().get(0).setPinned(true); + problem.getEntityList().get(0).setPinnedIndex(0); + var solution = PlannerTestUtils.solve(solverConfig, problem); + // The first entity should remain unchanged + assertThat(solution.getEntityList().get(0).getBasicValue()).isNull(); + assertThat(solution.getEntityList().get(0).getSecondBasicValue()).isNull(); + assertThat(solution.getEntityList().get(0).getValueList()).isEmpty(); + } + + @Test + void solveUnassignedWithListAndBasicVariables() { + var solverConfig = PlannerTestUtils.buildSolverConfig( + TestdataUnassignedListMultiVarSolution.class, TestdataUnassignedListMultiVarEntity.class) + .withPhases(new ConstructionHeuristicPhaseConfig() + .withTerminationConfig(new TerminationConfig().withStepCountLimit(16))) + .withEasyScoreCalculatorClass(TestdataUnassignedListMultiVarEasyScoreCalculator.class); + + var problem = TestdataUnassignedListMultiVarSolution.generateUninitializedSolution(2, 2, 2); + // Block values and make the basic and list variables unassigned + problem.getValueList().get(0).setBlocked(true); + problem.getValueList().get(1).setBlocked(true); + problem.getOtherValueList().get(0).setBlocked(true); + problem.getOtherValueList().get(1).setBlocked(true); + var solution = PlannerTestUtils.solve(solverConfig, problem); + assertThat(solution.getEntityList().stream() + .filter(e -> e.getBasicValue() == null)) + .hasSize(2); + assertThat(solution.getEntityList().stream() + .filter(e -> e.getSecondBasicValue() != null)) + .hasSize(2); + assertThat(solution.getEntityList().stream() + .filter(e -> e.getValueList().isEmpty())) + .hasSize(2); + } + + @Test + void solvePinnedAndUnassignedWithListAndBasicVariables() { + var solverConfig = PlannerTestUtils.buildSolverConfig( + TestdataUnassignedListMultiVarSolution.class, TestdataUnassignedListMultiVarEntity.class) + .withPhases(new ConstructionHeuristicPhaseConfig() + .withTerminationConfig(new TerminationConfig().withStepCountLimit(16))) + .withEasyScoreCalculatorClass(TestdataUnassignedListMultiVarEasyScoreCalculator.class); + + // Pin the entire first entity + var problem = TestdataUnassignedListMultiVarSolution.generateUninitializedSolution(2, 2, 2); + problem.getEntityList().get(0).setPinned(true); + problem.getEntityList().get(0).setBasicValue(problem.getOtherValueList().get(0)); + problem.getEntityList().get(0).setSecondBasicValue(problem.getOtherValueList().get(0)); + problem.getEntityList().get(0).setValueList(List.of(problem.getValueList().get(0))); + // Block values and make the basic and list variables unassigned + problem.getValueList().get(0).setBlocked(true); + problem.getValueList().get(1).setBlocked(true); + problem.getOtherValueList().get(0).setBlocked(true); + problem.getOtherValueList().get(1).setBlocked(true); + var solution = PlannerTestUtils.solve(solverConfig, problem); + // The first entity should remain unchanged + assertThat(solution.getEntityList().get(0).getBasicValue()).isNotNull(); + assertThat(solution.getEntityList().get(0).getSecondBasicValue()).isNotNull(); + assertThat(solution.getEntityList().get(0).getValueList()).hasSize(1); + assertThat(solution.getEntityList().get(1).getBasicValue()).isNull(); + assertThat(solution.getEntityList().get(1).getSecondBasicValue()).isNotNull(); + assertThat(solution.getEntityList().get(1).getValueList()).isEmpty(); + + // Pin partially the first entity list + problem = TestdataUnassignedListMultiVarSolution.generateUninitializedSolution(2, 4, 2); + problem.getEntityList().get(0).setPinnedIndex(2); + problem.getEntityList().get(0).setValueList(problem.getValueList().subList(1, 3)); + // Block values and make the basic variable unassigned + problem.getOtherValueList().get(0).setBlocked(true); + problem.getOtherValueList().get(1).setBlocked(true); + solution = PlannerTestUtils.solve(solverConfig, problem); + assertThat(solution.getEntityList().get(0).getBasicValue()).isNull(); + assertThat(solution.getEntityList().get(0).getSecondBasicValue()).isNotNull(); + // The pinning index fixed the values 1 and 2. The only remaining option is values are 0 and 3. + // The score is bigger when the list size is 3 + assertThat(solution.getEntityList().get(0).getValueList()).hasSize(3); + assertThat(solution.getEntityList().get(0).getValueList()) + .hasSameElementsAs( + List.of(problem.getValueList().get(1), problem.getValueList().get(2), problem.getValueList().get(0))); + assertThat(solution.getEntityList().get(1).getBasicValue()).isNull(); + assertThat(solution.getEntityList().get(1).getSecondBasicValue()).isNotNull(); + assertThat(solution.getEntityList().get(1).getValueList()).hasSize(1); + assertThat(solution.getEntityList().get(1).getValueList()).hasSameElementsAs(List.of(problem.getValueList().get(3))); + } + + @Test + void solveCustomConfigurationMultiEntityWithListAndBasicVariables() { + var valueSelectorConfig = new ValueSelectorConfig("valueList") + .withId("valueList"); + var mimicReplayingValueSelectorConfig = new ValueSelectorConfig() + .withMimicSelectorRef("valueList") + .withVariableName("valueList"); + var valuePlacerConfig = new QueuedValuePlacerConfig() + .withValueSelectorConfig(valueSelectorConfig) + .withMoveSelectorConfig(new ListChangeMoveSelectorConfig() + .withValueSelectorConfig(mimicReplayingValueSelectorConfig)) + .withEntityClass(TestdataListMultiEntityFirstEntity.class); + var entityPlacerConfig = new QueuedEntityPlacerConfig(); + + var solverConfig = PlannerTestUtils.buildSolverConfig( + TestdataListMultiEntitySolution.class, TestdataListMultiEntityFirstEntity.class, + TestdataListMultiEntitySecondEntity.class) + .withPhases(new ConstructionHeuristicPhaseConfig() + .withEntityPlacerConfigList(valuePlacerConfig, entityPlacerConfig) + .withTerminationConfig(new TerminationConfig().withStepCountLimit(16))) + .withEasyScoreCalculatorClass(TestdataListMultiEntityEasyScoreCalculator.class); + + var problem = TestdataListMultiEntitySolution.generateUninitializedSolution(2, 2, 2); + var solution = PlannerTestUtils.solve(solverConfig, problem); + assertThat(solution.getEntityList().stream() + .filter(e -> e.getValueList().isEmpty())) + .isEmpty(); + assertThat(solution.getOtherEntityList().stream() + .filter(e -> e.getBasicValue() == null || e.getSecondBasicValue() == null)) + .isEmpty(); + } + + @Test + void solveCustomConfigurationWithListAndBasicVariables() { + var valueSelectorConfig = new ValueSelectorConfig("valueList") + .withId("valueList"); + var mimicReplayingValueSelectorConfig = new ValueSelectorConfig() + .withMimicSelectorRef("valueList") + .withVariableName("valueList"); + var valuePlacerConfig = new QueuedValuePlacerConfig() + .withValueSelectorConfig(valueSelectorConfig) + .withMoveSelectorConfig(new ListChangeMoveSelectorConfig() + .withValueSelectorConfig(mimicReplayingValueSelectorConfig)); + var entityPlacerConfig = new QueuedEntityPlacerConfig(); + + var solverConfig = PlannerTestUtils.buildSolverConfig( + TestdataListMultiVarSolution.class, TestdataListMultiVarEntity.class, TestdataListMultiVarValue.class, + TestdataListMultiVarOtherValue.class) + .withPhases(new ConstructionHeuristicPhaseConfig() + .withEntityPlacerConfigList(valuePlacerConfig, entityPlacerConfig) + .withTerminationConfig(new TerminationConfig().withStepCountLimit(16))) + .withEasyScoreCalculatorClass(TestdataListMultiVarEasyScoreCalculator.class); + + var problem = TestdataListMultiVarSolution.generateUninitializedSolution(2, 2, 2); + var solution = PlannerTestUtils.solve(solverConfig, problem); + assertThat(solution.getEntityList().stream() + .filter(e -> e.getBasicValue() == null || e.getSecondBasicValue() == null || e.getValueList().isEmpty())) + .isEmpty(); + } + + @Test + void solveCustomConfigurationWithListVariables() { + var valueSelectorConfig = new ValueSelectorConfig("valueList") + .withId("valueList"); + var mimicReplayingValueSelectorConfig = new ValueSelectorConfig() + .withMimicSelectorRef("valueList") + .withVariableName("valueList"); + var valuePlacerConfig = new QueuedValuePlacerConfig() + .withValueSelectorConfig(valueSelectorConfig) + .withMoveSelectorConfig(new ListChangeMoveSelectorConfig() + .withValueSelectorConfig(mimicReplayingValueSelectorConfig)); + + var solverConfig = PlannerTestUtils.buildSolverConfig( + TestdataListSolution.class, TestdataListEntity.class, TestdataListValue.class) + .withPhases(new ConstructionHeuristicPhaseConfig() + .withEntityPlacerConfigList(valuePlacerConfig) + .withTerminationConfig(new TerminationConfig().withStepCountLimit(16))) + .withEasyScoreCalculatorClass(TestdataListVarEasyScoreCalculator.class); + + var problem = TestdataListSolution.generateUninitializedSolution(2, 2); + var solution = PlannerTestUtils.solve(solverConfig, problem); + assertThat(solution.getEntityList().stream() + .filter(e -> e.getValueList().isEmpty())) + .isEmpty(); + } + } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/placer/entity/QueuedEntityPlacerFactoryTest.java b/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/placer/entity/QueuedEntityPlacerFactoryTest.java index 57b58585853..8b67c05d4ab 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/placer/entity/QueuedEntityPlacerFactoryTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/placer/entity/QueuedEntityPlacerFactoryTest.java @@ -27,8 +27,8 @@ import ai.timefold.solver.core.impl.solver.scope.SolverScope; import ai.timefold.solver.core.testdomain.TestdataValue; import ai.timefold.solver.core.testdomain.difficultyweight.TestdataDifficultyWeightSolution; -import ai.timefold.solver.core.testdomain.multivar.TestdataMultiVarEntity; -import ai.timefold.solver.core.testdomain.multivar.TestdataMultiVarSolution; +import ai.timefold.solver.core.testdomain.multivar.basic.TestdataMultiVarEntity; +import ai.timefold.solver.core.testdomain.multivar.basic.TestdataMultiVarSolution; import org.junit.jupiter.api.Test; diff --git a/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/placer/entity/QueuedEntityPlacerTest.java b/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/placer/entity/QueuedEntityPlacerTest.java index dcd93980d16..eb5fc2d56e7 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/placer/entity/QueuedEntityPlacerTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/placer/entity/QueuedEntityPlacerTest.java @@ -33,8 +33,8 @@ import ai.timefold.solver.core.testdomain.TestdataEntity; import ai.timefold.solver.core.testdomain.TestdataSolution; import ai.timefold.solver.core.testdomain.TestdataValue; -import ai.timefold.solver.core.testdomain.multivar.TestdataMultiVarEntity; -import ai.timefold.solver.core.testdomain.multivar.TestdataMultiVarSolution; +import ai.timefold.solver.core.testdomain.multivar.basic.TestdataMultiVarEntity; +import ai.timefold.solver.core.testdomain.multivar.basic.TestdataMultiVarSolution; import org.junit.jupiter.api.Test; diff --git a/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/placer/entity/QueuedMultiplePlacerFactoryTest.java b/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/placer/entity/QueuedMultiplePlacerFactoryTest.java new file mode 100644 index 00000000000..c636c58a4e8 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/placer/entity/QueuedMultiplePlacerFactoryTest.java @@ -0,0 +1,476 @@ +package ai.timefold.solver.core.impl.constructionheuristic.placer.entity; + +import static ai.timefold.solver.core.config.heuristic.selector.entity.EntitySorterManner.DECREASING_DIFFICULTY_IF_AVAILABLE; +import static ai.timefold.solver.core.config.heuristic.selector.value.ValueSorterManner.INCREASING_STRENGTH_IF_AVAILABLE; +import static ai.timefold.solver.core.config.score.trend.InitializingScoreTrendLevel.ANY; +import static ai.timefold.solver.core.config.solver.EnvironmentMode.PHASE_ASSERT; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Random; + +import ai.timefold.solver.core.config.constructionheuristic.placer.QueuedEntityPlacerConfig; +import ai.timefold.solver.core.config.constructionheuristic.placer.QueuedValuePlacerConfig; +import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.ListChangeMoveSelectorConfig; +import ai.timefold.solver.core.config.heuristic.selector.value.ValueSelectorConfig; +import ai.timefold.solver.core.config.score.trend.InitializingScoreTrendLevel; +import ai.timefold.solver.core.impl.constructionheuristic.placer.EntityPlacerFactory; +import ai.timefold.solver.core.impl.constructionheuristic.placer.internal.QueuedMultiplePlacerConfig; +import ai.timefold.solver.core.impl.domain.variable.listener.support.VariableListenerSupport; +import ai.timefold.solver.core.impl.heuristic.HeuristicConfigPolicy; +import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; +import ai.timefold.solver.core.impl.phase.scope.AbstractStepScope; +import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; +import ai.timefold.solver.core.impl.score.trend.InitializingScoreTrend; +import ai.timefold.solver.core.impl.solver.scope.SolverScope; +import ai.timefold.solver.core.impl.util.MutableInt; +import ai.timefold.solver.core.testdomain.multivar.list.singleentity.TestdataListMultiVarSolution; +import ai.timefold.solver.core.testdomain.multivar.list.singleentity.unassignedvar.TestdataUnassignedListMultiVarSolution; + +import org.junit.jupiter.api.Test; + +class QueuedMultiplePlacerFactoryTest { + + @Test + void testPlacersForConstructionHeuristic() { + var solutionDescriptor = TestdataListMultiVarSolution.buildSolutionDescriptor(); + var configPolicy = new HeuristicConfigPolicy.Builder() + .withEnvironmentMode(PHASE_ASSERT) + .withInitializingScoreTrend(new InitializingScoreTrend(new InitializingScoreTrendLevel[] { ANY })) + .withSolutionDescriptor(solutionDescriptor) + .withEntitySorterManner(DECREASING_DIFFICULTY_IF_AVAILABLE) + .withValueSorterManner(INCREASING_STRENGTH_IF_AVAILABLE) + .withReinitializeVariableFilterEnabled(true) + .withInitializedChainedValueFilterEnabled(true) + .withUnassignedValuesAllowed(true) + .withRandom(new Random(0)) + .build(); + var valueSelectorConfig = new ValueSelectorConfig("valueList") + .withId("valueList"); + var mimicReplayingValueSelectorConfig = new ValueSelectorConfig() + .withMimicSelectorRef("valueList") + .withVariableName("valueList"); + var valuePlacerConfig = new QueuedValuePlacerConfig() + .withValueSelectorConfig(valueSelectorConfig) + .withMoveSelectorConfig(new ListChangeMoveSelectorConfig() + .withValueSelectorConfig(mimicReplayingValueSelectorConfig)); + var entityPlacerConfig = new QueuedEntityPlacerConfig(); + var placerConfig = new QueuedMultiplePlacerConfig() + .withPlacerConfigList(List.of(valuePlacerConfig, entityPlacerConfig)); + var placer = EntityPlacerFactory. create(placerConfig).buildEntityPlacer(configPolicy); + + var problem = TestdataListMultiVarSolution.generateUninitializedSolution(2, 2, 2); + var solverScope = mock(SolverScope.class); + var scoreDirector = mock(InnerScoreDirector.class); + var random = new Random(0L); + when(solverScope.getScoreDirector()).thenReturn(scoreDirector); + when(solverScope.getWorkingRandom()).thenReturn(random); + when(scoreDirector.getWorkingSolution()).thenReturn(problem); + when(scoreDirector.getWorkingSolution()).thenReturn(problem); + when(scoreDirector.getSolutionDescriptor()).thenReturn(solutionDescriptor); + + var supplyManager = VariableListenerSupport.create(scoreDirector); + when(scoreDirector.getSupplyManager()).thenReturn(supplyManager); + supplyManager.linkVariableListeners(); + supplyManager.resetWorkingSolution(); + + placer.solvingStarted(solverScope); + var phaseScope = mock(AbstractPhaseScope.class); + when(phaseScope.getScoreDirector()).thenReturn(scoreDirector); + placer.phaseStarted(phaseScope); + + var placerIterator = placer.iterator(); + + // Step 1 + // 1 = Generated Value 0 -> Entity 0[0] - Entity 0 - Generated Other Value 0 -> basicValue - Generated Other Value 0 -> secondBasicValue + // 2 = Generated Value 0 -> Entity 0[0] - Entity 0 - Generated Other Value 0 -> basicValue - Generated Other Value 1 -> secondBasicValue + // 3 = Generated Value 0 -> Entity 0[0] - Entity 0 - Generated Other Value 1 -> basicValue - Generated Other Value 0 -> secondBasicValue + // 4 = Generated Value 0 -> Entity 0[0] - Entity 0 - Generated Other Value 1 -> basicValue - Generated Other Value 1 -> secondBasicValue + // 5 = Generated Value 0 -> Entity 1[0] - Entity 0 - Generated Other Value 0 -> basicValue - Generated Other Value 0 -> secondBasicValue + // 6 = Generated Value 0 -> Entity 1[0] - Entity 0 - Generated Other Value 0 -> basicValue - Generated Other Value 1 -> secondBasicValue + // 7 = Generated Value 0 -> Entity 1[0] - Entity 0 - Generated Other Value 1 -> basicValue - Generated Other Value 0 -> secondBasicValue + // 8 = Generated Value 0 -> Entity 1[0] - Entity 0 - Generated Other Value 1 -> basicValue - Generated Other Value 1 -> secondBasicValue + // 9 = Generated Value 0 -> Entity 0[0] - Entity 1 - Generated Other Value 0 -> basicValue - Generated Other Value 0 -> secondBasicValue + // 10 = Generated Value 0 -> Entity 0[0] - Entity 1 - Generated Other Value 0 -> basicValue - Generated Other Value 1 -> secondBasicValue + // 11 = Generated Value 0 -> Entity 0[0] - Entity 1 - Generated Other Value 1 -> basicValue - Generated Other Value 0 -> secondBasicValue + // 12 = Generated Value 0 -> Entity 0[0] - Entity 1 - Generated Other Value 1 -> basicValue - Generated Other Value 1 -> secondBasicValue + // 13 = Generated Value 0 -> Entity 1[0] - Entity 1 - Generated Other Value 0 -> basicValue - Generated Other Value 0 -> secondBasicValue + // 14 = Generated Value 0 -> Entity 1[0] - Entity 1 - Generated Other Value 0 -> basicValue - Generated Other Value 1 -> secondBasicValue + // 15 = Generated Value 0 -> Entity 1[0] - Entity 1 - Generated Other Value 1 -> basicValue - Generated Other Value 0 -> secondBasicValue + // 16 = Generated Value 0 -> Entity 1[0] - Entity 1 - Generated Other Value 1 -> basicValue - Generated Other Value 1 -> secondBasicValue + assertThat(placerIterator.hasNext()).isTrue(); + var counter = new MutableInt(); + placerIterator.next().iterator().forEachRemaining(move -> counter.increment()); + assertThat(counter.intValue()).isEqualTo(16); + + // Accept the move -> 1 = Generated Value 0 -> Entity 0[0] - Entity 0 - Generated Other Value 0 -> basicValue - Generated Other Value 0 -> secondBasicValue + problem.getEntityList().get(0).setValueList(List.of(problem.getValueList().get(0))); + problem.getEntityList().get(0).setBasicValue(problem.getOtherValueList().get(0)); + problem.getEntityList().get(0).setSecondBasicValue(problem.getOtherValueList().get(0)); + // Update all variables + supplyManager.resetWorkingSolution(); + var stepScope = mock(AbstractStepScope.class); + placer.stepEnded(stepScope); + + // Step 2 + // 1 = Generated Value 1 -> Entity 0[0] - Entity 1 - Generated Other Value 0 -> basicValue - Generated Other Value 0 -> secondBasicValue + // 2 = Generated Value 1 -> Entity 0[0] - Entity 1 - Generated Other Value 0 -> basicValue - Generated Other Value 1 -> secondBasicValue + // 3 = Generated Value 1 -> Entity 0[0] - Entity 1 - Generated Other Value 1 -> basicValue - Generated Other Value 0 -> secondBasicValue + // 4 = Generated Value 1 -> Entity 0[0] - Entity 1 - Generated Other Value 1 -> basicValue - Generated Other Value 1 -> secondBasicValue + // 5 = Generated Value 1 -> Entity 1[0] - Entity 1 - Generated Other Value 0 -> basicValue - Generated Other Value 0 -> secondBasicValue + // 6 = Generated Value 1 -> Entity 1[0] - Entity 1 - Generated Other Value 0 -> basicValue - Generated Other Value 1 -> secondBasicValue + // 7 = Generated Value 1 -> Entity 1[0] - Entity 1 - Generated Other Value 1 -> basicValue - Generated Other Value 0 -> secondBasicValue + // 8 = Generated Value 1 -> Entity 1[0] - Entity 1 - Generated Other Value 1 -> basicValue - Generated Other Value 1 -> secondBasicValue + // 9 = Generated Value 1 -> Entity 0[1] - Entity 1 - Generated Other Value 0 -> basicValue - Generated Other Value 0 -> secondBasicValue + // 10 = Generated Value 1 -> Entity 0[1] - Entity 1 - Generated Other Value 0 -> basicValue - Generated Other Value 1 -> secondBasicValue + // 11 = Generated Value 1 -> Entity 0[1] - Entity 1 - Generated Other Value 1 -> basicValue - Generated Other Value 0 -> secondBasicValue + // 12 = Generated Value 1 -> Entity 0[1] - Entity 1 - Generated Other Value 1 -> basicValue - Generated Other Value 1 -> secondBasicValue + counter.setValue(0); + assertThat(placerIterator.hasNext()).isTrue(); + placerIterator.next().iterator().forEachRemaining(move -> counter.increment()); + assertThat(counter.intValue()).isEqualTo(12); + + // Accept the move = 5 = Generated Value 1 -> Entity 1[0] - Entity 1 - Generated Other Value 0 -> basicValue - Generated Other Value 0 -> secondBasicValue + problem.getEntityList().get(1).setValueList(List.of(problem.getValueList().get(1))); + problem.getEntityList().get(1).setBasicValue(problem.getOtherValueList().get(0)); + problem.getEntityList().get(1).setSecondBasicValue(problem.getOtherValueList().get(0)); + // Update all variables + supplyManager.resetWorkingSolution(); + placer.stepEnded(stepScope); + // No more placements + assertThat(placerIterator.hasNext()).isFalse(); + } + + @Test + void testPinnedPlacersForConstructionHeuristic() { + var solutionDescriptor = TestdataListMultiVarSolution.buildSolutionDescriptor(); + var configPolicy = new HeuristicConfigPolicy.Builder() + .withEnvironmentMode(PHASE_ASSERT) + .withInitializingScoreTrend(new InitializingScoreTrend(new InitializingScoreTrendLevel[] { ANY })) + .withSolutionDescriptor(solutionDescriptor) + .withEntitySorterManner(DECREASING_DIFFICULTY_IF_AVAILABLE) + .withValueSorterManner(INCREASING_STRENGTH_IF_AVAILABLE) + .withReinitializeVariableFilterEnabled(true) + .withInitializedChainedValueFilterEnabled(true) + .withUnassignedValuesAllowed(true) + .withRandom(new Random(0)) + .build(); + var valueSelectorConfig = new ValueSelectorConfig("valueList") + .withId("valueList"); + var mimicReplayingValueSelectorConfig = new ValueSelectorConfig() + .withMimicSelectorRef("valueList") + .withVariableName("valueList"); + var valuePlacerConfig = new QueuedValuePlacerConfig() + .withValueSelectorConfig(valueSelectorConfig) + .withMoveSelectorConfig(new ListChangeMoveSelectorConfig() + .withValueSelectorConfig(mimicReplayingValueSelectorConfig)); + var entityPlacerConfig = new QueuedEntityPlacerConfig(); + var placerConfig = new QueuedMultiplePlacerConfig() + .withPlacerConfigList(List.of(valuePlacerConfig, entityPlacerConfig)); + var placer = EntityPlacerFactory. create(placerConfig).buildEntityPlacer(configPolicy); + + var problem = TestdataListMultiVarSolution.generateUninitializedSolution(2, 2, 2); + // Pin the first entity + problem.getEntityList().get(0).setPinned(true); + problem.getEntityList().get(0).setPinnedIndex(2); + problem.getEntityList().get(0).setBasicValue(problem.getOtherValueList().get(0)); + problem.getEntityList().get(0).setSecondBasicValue(problem.getOtherValueList().get(0)); + problem.getEntityList().get(0).setValueList(List.of(problem.getValueList().get(0))); + + var solverScope = mock(SolverScope.class); + var scoreDirector = mock(InnerScoreDirector.class); + var random = new Random(0L); + when(solverScope.getScoreDirector()).thenReturn(scoreDirector); + when(solverScope.getWorkingRandom()).thenReturn(random); + when(scoreDirector.getWorkingSolution()).thenReturn(problem); + when(scoreDirector.getWorkingSolution()).thenReturn(problem); + when(scoreDirector.getSolutionDescriptor()).thenReturn(solutionDescriptor); + + var supplyManager = VariableListenerSupport.create(scoreDirector); + when(scoreDirector.getSupplyManager()).thenReturn(supplyManager); + supplyManager.linkVariableListeners(); + supplyManager.resetWorkingSolution(); + + placer.solvingStarted(solverScope); + var phaseScope = mock(AbstractPhaseScope.class); + when(phaseScope.getScoreDirector()).thenReturn(scoreDirector); + placer.phaseStarted(phaseScope); + + var placerIterator = placer.iterator(); + + // Step 1 + // 1 = Generated Value 1 -> Entity 1[0] - Entity 1 - Generated Other Value 0 -> basicValue - Generated Other Value 0 -> secondBasicValue + // 2 = Generated Value 1 -> Entity 1[0] - Entity 1 - Generated Other Value 0 -> basicValue - Generated Other Value 1 -> secondBasicValue + // 3 = Generated Value 1 -> Entity 1[0] - Entity 1 - Generated Other Value 1 -> basicValue - Generated Other Value 0 -> secondBasicValue + // 4 = Generated Value 1 -> Entity 1[0] - Entity 1 - Generated Other Value 1 -> basicValue - Generated Other Value 1 -> secondBasicValue + assertThat(placerIterator.hasNext()).isTrue(); + var counter = new MutableInt(); + placerIterator.next().iterator().forEachRemaining(move -> counter.increment()); + assertThat(counter.intValue()).isEqualTo(4); + + // Accept the move -> 1 = Generated Value 1 -> Entity 1[0] - Entity 1 - Generated Other Value 0 -> basicValue - Generated Other Value 0 -> secondBasicValue + problem.getEntityList().get(1).setValueList(List.of(problem.getValueList().get(1))); + problem.getEntityList().get(1).setBasicValue(problem.getOtherValueList().get(0)); + problem.getEntityList().get(1).setSecondBasicValue(problem.getOtherValueList().get(0)); + // Update all variables + supplyManager.resetWorkingSolution(); + var stepScope = mock(AbstractStepScope.class); + placer.stepEnded(stepScope); + assertThat(placerIterator.hasNext()).isFalse(); + } + + @Test + void testUnassignedPlacersForConstructionHeuristic() { + var solutionDescriptor = TestdataUnassignedListMultiVarSolution.buildSolutionDescriptor(); + var configPolicy = new HeuristicConfigPolicy.Builder() + .withEnvironmentMode(PHASE_ASSERT) + .withInitializingScoreTrend(new InitializingScoreTrend(new InitializingScoreTrendLevel[] { ANY })) + .withSolutionDescriptor(solutionDescriptor) + .withEntitySorterManner(DECREASING_DIFFICULTY_IF_AVAILABLE) + .withValueSorterManner(INCREASING_STRENGTH_IF_AVAILABLE) + .withReinitializeVariableFilterEnabled(true) + .withInitializedChainedValueFilterEnabled(true) + .withUnassignedValuesAllowed(true) + .withRandom(new Random(0)) + .build(); + var valueSelectorConfig = new ValueSelectorConfig("valueList") + .withId("valueList"); + var mimicReplayingValueSelectorConfig = new ValueSelectorConfig() + .withMimicSelectorRef("valueList") + .withVariableName("valueList"); + var valuePlacerConfig = new QueuedValuePlacerConfig() + .withValueSelectorConfig(valueSelectorConfig) + .withMoveSelectorConfig(new ListChangeMoveSelectorConfig() + .withValueSelectorConfig(mimicReplayingValueSelectorConfig)); + var entityPlacerConfig = new QueuedEntityPlacerConfig(); + var placerConfig = new QueuedMultiplePlacerConfig() + .withPlacerConfigList(List.of(valuePlacerConfig, entityPlacerConfig)); + var placer = EntityPlacerFactory. create(placerConfig) + .buildEntityPlacer(configPolicy); + + var problem = TestdataUnassignedListMultiVarSolution.generateUninitializedSolution(2, 2, 2); + var solverScope = mock(SolverScope.class); + var scoreDirector = mock(InnerScoreDirector.class); + var random = new Random(0L); + when(solverScope.getScoreDirector()).thenReturn(scoreDirector); + when(solverScope.getWorkingRandom()).thenReturn(random); + when(scoreDirector.getWorkingSolution()).thenReturn(problem); + when(scoreDirector.getWorkingSolution()).thenReturn(problem); + when(scoreDirector.getSolutionDescriptor()).thenReturn(solutionDescriptor); + + var supplyManager = VariableListenerSupport.create(scoreDirector); + when(scoreDirector.getSupplyManager()).thenReturn(supplyManager); + supplyManager.linkVariableListeners(); + supplyManager.resetWorkingSolution(); + + placer.solvingStarted(solverScope); + var phaseScope = mock(AbstractPhaseScope.class); + when(phaseScope.getScoreDirector()).thenReturn(scoreDirector); + placer.phaseStarted(phaseScope); + + var placerIterator = placer.iterator(); + + // Step 1 + // 1 = Generated Value 0 -> Entity 0[0] - Entity 0 - null -> basicValue - Generated Other Value 0 -> secondBasicValue + // 2 = Generated Value 0 -> Entity 0[0] - Entity 0 - null -> basicValue - Generated Other Value 1 -> secondBasicValue + // 3 = Generated Value 0 -> Entity 0[0] - Entity 0 - Generated Other Value 0 -> basicValue - Generated Other Value 0 -> secondBasicValue + // 4 = Generated Value 0 -> Entity 0[0] - Entity 0 - Generated Other Value 0 -> basicValue - Generated Other Value 1 -> secondBasicValue + // 5 = Generated Value 0 -> Entity 0[0] - Entity 0 - Generated Other Value 1 -> basicValue - Generated Other Value 0 -> secondBasicValue + // 6 = Generated Value 0 -> Entity 0[0] - Entity 0 - Generated Other Value 1 -> basicValue - Generated Other Value 1 -> secondBasicValue + // 7 = Generated Value 0 -> Entity 0[0] - Entity 1 - null -> basicValue - Generated Other Value 0 -> secondBasicValue + // 8 = Generated Value 0 -> Entity 0[0] - Entity 1 - null -> basicValue - Generated Other Value 1 -> secondBasicValue + // 9 = Generated Value 0 -> Entity 0[0] - Entity 1 - Generated Other Value 0 -> basicValue - Generated Other Value 0 -> secondBasicValue + // 10 = Generated Value 0 -> Entity 0[0] - Entity 1 - Generated Other Value 0 -> basicValue - Generated Other Value 1 -> secondBasicValue + // 11 = Generated Value 0 -> Entity 0[0] - Entity 1 - Generated Other Value 1 -> basicValue - Generated Other Value 0 -> secondBasicValue + // 12 = Generated Value 0 -> Entity 0[0] - Entity 1 - Generated Other Value 1 -> basicValue - Generated Other Value 1 -> secondBasicValue + // 13 = Generated Value 0 -> Entity 1[0] - Entity 0 - null -> basicValue - Generated Other Value 0 -> secondBasicValue + // 14 = Generated Value 0 -> Entity 1[0] - Entity 0 - null -> basicValue - Generated Other Value 1 -> secondBasicValue + // 15 = Generated Value 0 -> Entity 1[0] - Entity 0 - Generated Other Value 0 -> basicValue - Generated Other Value 0 -> secondBasicValue + // 16 = Generated Value 0 -> Entity 1[0] - Entity 0 - Generated Other Value 0 -> basicValue - Generated Other Value 1 -> secondBasicValue + // 17 = Generated Value 0 -> Entity 1[0] - Entity 0 - Generated Other Value 1 -> basicValue - Generated Other Value 0 -> secondBasicValue + // 18 = Generated Value 0 -> Entity 1[0] - Entity 0 - Generated Other Value 1 -> basicValue - Generated Other Value 1 -> secondBasicValue + // 19 = Generated Value 0 -> Entity 1[0] - Entity 1 - null -> basicValue - Generated Other Value 0 -> secondBasicValue + // 20 = Generated Value 0 -> Entity 1[0] - Entity 1 - null -> basicValue - Generated Other Value 1 -> secondBasicValue + // 21 = Generated Value 0 -> Entity 1[0] - Entity 1 - Generated Other Value 0 -> basicValue - Generated Other Value 0 -> secondBasicValue + // 22 = Generated Value 0 -> Entity 1[0] - Entity 1 - Generated Other Value 0 -> basicValue - Generated Other Value 1 -> secondBasicValue + // 23 = Generated Value 0 -> Entity 1[0] - Entity 1 - Generated Other Value 1 -> basicValue - Generated Other Value 0 -> secondBasicValue + // 24 = Generated Value 0 -> Entity 1[0] - Entity 1 - Generated Other Value 1 -> basicValue - Generated Other Value 1 -> secondBasicValue + // 25 = NoChange - null -> basicValue - Generated Other Value 0 -> secondBasicValue + // 26 = NoChange - null -> basicValue - Generated Other Value 1 -> secondBasicValue + // 27 = NoChange - Entity 0 - Generated Other Value 0 -> basicValue - Generated Other Value 0 -> secondBasicValue + // 28 = NoChange - Entity 0 - Generated Other Value 0 -> basicValue - Generated Other Value 1 -> secondBasicValue + // 29 = NoChange - Entity 0 - Generated Other Value 1 -> basicValue - Generated Other Value 0 -> secondBasicValue + // 30 = NoChange - Entity 0 - Generated Other Value 1 -> basicValue - Generated Other Value 1 -> secondBasicValue + // 31 = NoChange - Entity 1 - null -> basicValue - Generated Other Value 0 -> secondBasicValue + // 32 = NoChange - Entity 1 - null -> basicValue - Generated Other Value 1 -> secondBasicValue + // 33 = NoChange - Entity 1 - Generated Other Value 0 -> basicValue - Generated Other Value 0 -> secondBasicValue + // 34 = NoChange - Entity 1 - Generated Other Value 0 -> basicValue - Generated Other Value 1 -> secondBasicValue + // 35 = NoChange - Entity 1 - Generated Other Value 1 -> basicValue - Generated Other Value 0 -> secondBasicValue + // 36 = NoChange - Entity 1 - Generated Other Value 1 -> basicValue - Generated Other Value 1 -> secondBasicValue + assertThat(placerIterator.hasNext()).isTrue(); + var counter = new MutableInt(); + placerIterator.next().iterator().forEachRemaining(move -> counter.increment()); + assertThat(counter.intValue()).isEqualTo(36); + + // Accept the move - 31 = NoChange - Entity 1 - null -> basicValue - Generated Other Value 0 -> secondBasicValue + problem.getEntityList().get(0).setSecondBasicValue(problem.getOtherValueList().get(0)); + // Update all variables + supplyManager.resetWorkingSolution(); + var stepScope = mock(AbstractStepScope.class); + placer.stepEnded(stepScope); + + // Step 2 + // 1 = Generated Value 1 -> Entity 0[0] - Entity 0 - null -> basicValue + // 2 = Generated Value 1 -> Entity 0[0] - Entity 0 - Generated Other Value 0 -> basicValue + // 3 = Generated Value 1 -> Entity 0[0] - Entity 0 - Generated Other Value 1 -> basicValue + // 4 = Generated Value 1 -> Entity 0[0] - Entity 1 - null -> basicValue - Generated Other Value 0 -> secondBasicValue + // 5 = Generated Value 1 -> Entity 0[0] - Entity 1 - null -> basicValue - Generated Other Value 1 -> secondBasicValue + // 6 = Generated Value 1 -> Entity 0[0] - Entity 1 - Generated Other Value 0 -> basicValue - Generated Other Value 0 -> secondBasicValue + // 7 = Generated Value 1 -> Entity 0[0] - Entity 1 - Generated Other Value 0 -> basicValue - Generated Other Value 1 -> secondBasicValue + // 8 = Generated Value 1 -> Entity 0[0] - Entity 1 - Generated Other Value 1 -> basicValue - Generated Other Value 0 -> secondBasicValue + // 9 = Generated Value 1 -> Entity 0[0] - Entity 1 - Generated Other Value 1 -> basicValue - Generated Other Value 1 -> secondBasicValue + // 10 = Generated Value 1 -> Entity 1[0] - Entity 0 - null -> basicValue + // 11 = Generated Value 1 -> Entity 1[0] - Entity 0 - Generated Other Value 0 -> basicValue + // 12 = Generated Value 1 -> Entity 1[0] - Entity 0 - Generated Other Value 1 -> basicValue + // 13 = Generated Value 1 -> Entity 1[0] - Entity 1 - null -> basicValue - Generated Other Value 0 -> secondBasicValue + // 14 = Generated Value 1 -> Entity 1[0] - Entity 1 - null -> basicValue - Generated Other Value 1 -> secondBasicValue + // 15 = Generated Value 1 -> Entity 1[0] - Entity 1 - Generated Other Value 0 -> basicValue - Generated Other Value 0 -> secondBasicValue + // 16 = Generated Value 1 -> Entity 1[0] - Entity 1 - Generated Other Value 0 -> basicValue - Generated Other Value 1 -> secondBasicValue + // 17 = Generated Value 1 -> Entity 1[0] - Entity 1 - Generated Other Value 1 -> basicValue - Generated Other Value 0 -> secondBasicValue + // 18 = Generated Value 1 -> Entity 1[0] - Entity 1 - Generated Other Value 1 -> basicValue - Generated Other Value 1 -> secondBasicValue + // 19 = NoChange - Entity 0 - null -> basicValue + // 20 = NoChange - Entity 0 - Generated Other Value 0 -> basicValue + // 21 = NoChange - Entity 0 - Generated Other Value 1 -> basicValue + // 22 = NoChange - Entity 1 - null -> basicValue - Generated Other Value 0 -> secondBasicValue + // 23 = NoChange - Entity 1 - null -> basicValue - Generated Other Value 1 -> secondBasicValue + // 24 = NoChange - Entity 1 - Generated Other Value 0 -> basicValue - Generated Other Value 0 -> secondBasicValue + // 25 = NoChange - Entity 1 - Generated Other Value 0 -> basicValue - Generated Other Value 1 -> secondBasicValue + // 26 = NoChange - Entity 1 - Generated Other Value 1 -> basicValue - Generated Other Value 0 -> secondBasicValue + // 27 = NoChange - Entity 1 - Generated Other Value 1 -> basicValue - Generated Other Value 1 -> secondBasicValue + counter.setValue(0); + assertThat(placerIterator.hasNext()).isTrue(); + placerIterator.next().iterator().forEachRemaining(move -> counter.increment()); + assertThat(counter.intValue()).isEqualTo(27); + + // Accept the move - 22 = NoChange - Entity 1 - null -> basicValue - Generated Other Value 0 -> secondBasicValue + problem.getEntityList().get(1).setSecondBasicValue(problem.getOtherValueList().get(0)); + // Update all variables + supplyManager.resetWorkingSolution(); + placer.stepEnded(stepScope); + // 1 = Generated Value 0 -> Entity 0[0] - Entity 0 - null -> basicValue + // 2 = Generated Value 0 -> Entity 0[0] - Entity 0 - Generated Other Value 0 -> basicValue + // 3 = Generated Value 0 -> Entity 0[0] - Entity 0 - Generated Other Value 1 -> basicValue + // 4 = Generated Value 0 -> Entity 0[0] - Entity 1 - null -> basicValue + // 5 = Generated Value 0 -> Entity 0[0] - Entity 1 - Generated Other Value 0 -> basicValue + // 6 = Generated Value 0 -> Entity 0[0] - Entity 1 - Generated Other Value 1 -> basicValue + // 7 = Generated Value 0 -> Entity 1[0] - Entity 0 - null -> basicValue + // 8 = Generated Value 0 -> Entity 1[0] - Entity 0 - Generated Other Value 0 -> basicValue + // 9 = Generated Value 0 -> Entity 1[0] - Entity 0 - Generated Other Value 1 -> basicValue + // 10 = Generated Value 0 -> Entity 1[0] - Entity 1 - null -> basicValue + // 11 = Generated Value 0 -> Entity 1[0] - Entity 1 - Generated Other Value 0 -> basicValue + // 12 = Generated Value 0 -> Entity 1[0] - Entity 1 - Generated Other Value 1 -> basicValue + // 13 = NoChange - Entity 0 - null -> basicValue + // 14 = NoChange - Entity 0 - Generated Other Value 0 -> basicValue + // 15 = NoChange - Entity 0 - Generated Other Value 1 -> basicValue + // 16 = NoChange - Entity 1 - null -> basicValue + // 17 = NoChange - Entity 1 - Generated Other Value 0 -> basicValue + // 18 = NoChange - Entity 1 - Generated Other Value 1 -> basicValue + counter.setValue(0); + placerIterator.next().iterator().forEachRemaining(move -> counter.increment()); + assertThat(counter.intValue()).isEqualTo(18); + } + + @Test + void testPinnedUnassignedPlacersForConstructionHeuristic() { + var solutionDescriptor = TestdataUnassignedListMultiVarSolution.buildSolutionDescriptor(); + var configPolicy = new HeuristicConfigPolicy.Builder() + .withEnvironmentMode(PHASE_ASSERT) + .withInitializingScoreTrend(new InitializingScoreTrend(new InitializingScoreTrendLevel[] { ANY })) + .withSolutionDescriptor(solutionDescriptor) + .withEntitySorterManner(DECREASING_DIFFICULTY_IF_AVAILABLE) + .withValueSorterManner(INCREASING_STRENGTH_IF_AVAILABLE) + .withReinitializeVariableFilterEnabled(true) + .withInitializedChainedValueFilterEnabled(true) + .withUnassignedValuesAllowed(true) + .withRandom(new Random(0)) + .build(); + var valueSelectorConfig = new ValueSelectorConfig("valueList") + .withId("valueList"); + var mimicReplayingValueSelectorConfig = new ValueSelectorConfig() + .withMimicSelectorRef("valueList") + .withVariableName("valueList"); + var valuePlacerConfig = new QueuedValuePlacerConfig() + .withValueSelectorConfig(valueSelectorConfig) + .withMoveSelectorConfig(new ListChangeMoveSelectorConfig() + .withValueSelectorConfig(mimicReplayingValueSelectorConfig)); + var entityPlacerConfig = new QueuedEntityPlacerConfig(); + var placerConfig = new QueuedMultiplePlacerConfig() + .withPlacerConfigList(List.of(valuePlacerConfig, entityPlacerConfig)); + var placer = EntityPlacerFactory. create(placerConfig) + .buildEntityPlacer(configPolicy); + + var problem = TestdataUnassignedListMultiVarSolution.generateUninitializedSolution(2, 2, 2); + // Pin the first entity + problem.getEntityList().get(0).setPinned(true); + problem.getEntityList().get(0).setPinnedIndex(2); + problem.getEntityList().get(0).setBasicValue(problem.getOtherValueList().get(0)); + problem.getEntityList().get(0).setSecondBasicValue(problem.getOtherValueList().get(0)); + problem.getEntityList().get(0).setValueList(List.of(problem.getValueList().get(0))); + + var solverScope = mock(SolverScope.class); + var scoreDirector = mock(InnerScoreDirector.class); + var random = new Random(0L); + when(solverScope.getScoreDirector()).thenReturn(scoreDirector); + when(solverScope.getWorkingRandom()).thenReturn(random); + when(scoreDirector.getWorkingSolution()).thenReturn(problem); + when(scoreDirector.getWorkingSolution()).thenReturn(problem); + when(scoreDirector.getSolutionDescriptor()).thenReturn(solutionDescriptor); + + var supplyManager = VariableListenerSupport.create(scoreDirector); + when(scoreDirector.getSupplyManager()).thenReturn(supplyManager); + supplyManager.linkVariableListeners(); + supplyManager.resetWorkingSolution(); + + placer.solvingStarted(solverScope); + var phaseScope = mock(AbstractPhaseScope.class); + when(phaseScope.getScoreDirector()).thenReturn(scoreDirector); + placer.phaseStarted(phaseScope); + + var placerIterator = placer.iterator(); + // Step 1 + // 1 = Generated Value 1 -> Entity 1[0] - Entity 1 - null -> basicValue - Generated Other Value 0 -> secondBasicValue + // 2 = Generated Value 1 -> Entity 1[0] - Entity 1 - null -> basicValue - Generated Other Value 1 -> secondBasicValue + // 3 = Generated Value 1 -> Entity 1[0] - Entity 1 - Generated Other Value 0 -> basicValue - Generated Other Value 0 -> secondBasicValue + // 4 = Generated Value 1 -> Entity 1[0] - Entity 1 - Generated Other Value 0 -> basicValue - Generated Other Value 1 -> secondBasicValue + // 5 = Generated Value 1 -> Entity 1[0] - Entity 1 - Generated Other Value 1 -> basicValue - Generated Other Value 0 -> secondBasicValue + // 6 = Generated Value 1 -> Entity 1[0] - Entity 1 - Generated Other Value 1 -> basicValue - Generated Other Value 1 -> secondBasicValue + // 7 = NoChange - Entity 1 - null -> basicValue - Generated Other Value 0 -> secondBasicValue + // 8 = NoChange - Entity 1 - null -> basicValue - Generated Other Value 1 -> secondBasicValue + // 9 = NoChange - Entity 1 - Generated Other Value 0 -> basicValue - Generated Other Value 0 -> secondBasicValue + // 10 = NoChange - Entity 1 - Generated Other Value 0 -> basicValue - Generated Other Value 1 -> secondBasicValue + // 11 = NoChange - Entity 1 - Generated Other Value 1 -> basicValue - Generated Other Value 0 -> secondBasicValue + // 12 = NoChange - Entity 1 - Generated Other Value 1 -> basicValue - Generated Other Value 1 -> secondBasicValue + assertThat(placerIterator.hasNext()).isTrue(); + var counter = new MutableInt(); + placerIterator.next().iterator().forEachRemaining(move -> counter.increment()); + assertThat(counter.intValue()).isEqualTo(12); + + // Accept the move - 7 = NoChange - Entity 1 - null -> basicValue - Generated Other Value 0 -> secondBasicValue + problem.getEntityList().get(1).setSecondBasicValue(problem.getOtherValueList().get(0)); + // Update all variables + supplyManager.resetWorkingSolution(); + var stepScope = mock(AbstractStepScope.class); + placer.stepEnded(stepScope); + counter.setValue(0); + // 1 = Generated Value 1 -> Entity 1[0] - null -> secondBasicValue + // 2 = Generated Value 1 -> Entity 1[0] - Generated Other Value 0 -> secondBasicValue + // 3 = Generated Value 1 -> Entity 1[0] - Generated Other Value 1 -> secondBasicValue + // 4 = NoChange - null -> secondBasicValue + // 5 = NoChange - Generated Other Value 0 -> secondBasicValue + // 6 = NoChange - Generated Other Value 1 -> secondBasicValue + placerIterator.next().iterator().forEachRemaining(move -> counter.increment()); + assertThat(counter.intValue()).isEqualTo(6); + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/impl/domain/solution/descriptor/SolutionDescriptorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/domain/solution/descriptor/SolutionDescriptorTest.java index aa16673ab19..6bae5f871fb 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/domain/solution/descriptor/SolutionDescriptorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/domain/solution/descriptor/SolutionDescriptorTest.java @@ -33,6 +33,7 @@ import ai.timefold.solver.core.testdomain.invalid.constraintconfiguration.TestdataInvalidConfigurationSolution; import ai.timefold.solver.core.testdomain.invalid.constraintweightoverrides.TestdataInvalidConstraintWeightOverridesSolution; import ai.timefold.solver.core.testdomain.invalid.duplicateweightoverrides.TestdataDuplicateWeightConfigurationSolution; +import ai.timefold.solver.core.testdomain.invalid.multivar.TestdataInvalidMultiVarSolution; import ai.timefold.solver.core.testdomain.invalid.nosolution.TestdataNoSolution; import ai.timefold.solver.core.testdomain.invalid.variablemap.TestdataMapConfigurationSolution; import ai.timefold.solver.core.testdomain.list.TestdataListSolution; @@ -641,4 +642,13 @@ void testBadFactCollection() { assertThatCode(TestdataBadFactCollectionSolution::buildSolutionDescriptor) .hasMessageContaining("that does not return a Collection or an array."); } + + @Test + void testBadChainedAndListModel() { + assertThatCode(TestdataInvalidMultiVarSolution::buildSolutionDescriptor) + .hasMessageContaining("Combining chained variables") + .hasMessageContaining("with list variables") + .hasMessageContaining("on a single planning entity") + .hasMessageContaining("is not supported"); + } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/composite/CartesianProductMoveSelectorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/composite/CartesianProductMoveSelectorTest.java index f0cde221813..125cece9919 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/composite/CartesianProductMoveSelectorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/composite/CartesianProductMoveSelectorTest.java @@ -24,7 +24,7 @@ import ai.timefold.solver.core.impl.phase.scope.AbstractStepScope; import ai.timefold.solver.core.impl.solver.scope.SolverScope; import ai.timefold.solver.core.testdomain.TestdataValue; -import ai.timefold.solver.core.testdomain.multivar.TestdataMultiVarEntity; +import ai.timefold.solver.core.testdomain.multivar.basic.TestdataMultiVarEntity; import org.junit.jupiter.api.Test; diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/ChangeMoveSelectorFactoryTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/ChangeMoveSelectorFactoryTest.java index 9597800e068..263b2e7af3c 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/ChangeMoveSelectorFactoryTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/ChangeMoveSelectorFactoryTest.java @@ -24,7 +24,7 @@ import ai.timefold.solver.core.testdomain.list.TestdataListSolution; import ai.timefold.solver.core.testdomain.multientity.TestdataHerdEntity; import ai.timefold.solver.core.testdomain.multientity.TestdataMultiEntitySolution; -import ai.timefold.solver.core.testdomain.multivar.TestdataMultiVarSolution; +import ai.timefold.solver.core.testdomain.multivar.basic.TestdataMultiVarSolution; import org.junit.jupiter.api.Test; diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/ChangeMoveTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/ChangeMoveTest.java index 10158a9ceeb..565cda0bad6 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/ChangeMoveTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/ChangeMoveTest.java @@ -13,8 +13,8 @@ import ai.timefold.solver.core.testdomain.TestdataEntity; import ai.timefold.solver.core.testdomain.TestdataSolution; import ai.timefold.solver.core.testdomain.TestdataValue; -import ai.timefold.solver.core.testdomain.multivar.TestdataMultiVarEntity; -import ai.timefold.solver.core.testdomain.multivar.TestdataOtherValue; +import ai.timefold.solver.core.testdomain.multivar.basic.TestdataMultiVarEntity; +import ai.timefold.solver.core.testdomain.multivar.basic.TestdataOtherValue; import ai.timefold.solver.core.testdomain.valuerange.entityproviding.TestdataEntityProvidingEntity; import ai.timefold.solver.core.testdomain.valuerange.entityproviding.TestdataEntityProvidingSolution; diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/PillarChangeMoveTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/PillarChangeMoveTest.java index 51224744b6b..0947f592cbd 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/PillarChangeMoveTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/PillarChangeMoveTest.java @@ -15,7 +15,7 @@ import ai.timefold.solver.core.testdomain.TestdataEntity; import ai.timefold.solver.core.testdomain.TestdataSolution; import ai.timefold.solver.core.testdomain.TestdataValue; -import ai.timefold.solver.core.testdomain.multivar.TestdataMultiVarEntity; +import ai.timefold.solver.core.testdomain.multivar.basic.TestdataMultiVarEntity; import ai.timefold.solver.core.testdomain.valuerange.entityproviding.TestdataEntityProvidingEntity; import ai.timefold.solver.core.testdomain.valuerange.entityproviding.TestdataEntityProvidingSolution; diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/PillarSwapMoveTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/PillarSwapMoveTest.java index df0a1903580..051cf917448 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/PillarSwapMoveTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/PillarSwapMoveTest.java @@ -14,7 +14,7 @@ import ai.timefold.solver.core.testdomain.TestdataEntity; import ai.timefold.solver.core.testdomain.TestdataSolution; import ai.timefold.solver.core.testdomain.TestdataValue; -import ai.timefold.solver.core.testdomain.multivar.TestdataMultiVarEntity; +import ai.timefold.solver.core.testdomain.multivar.basic.TestdataMultiVarEntity; import ai.timefold.solver.core.testdomain.valuerange.entityproviding.TestdataEntityProvidingEntity; import ai.timefold.solver.core.testdomain.valuerange.entityproviding.TestdataEntityProvidingSolution; diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/SwapMoveSelectorFactoryTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/SwapMoveSelectorFactoryTest.java index e35b4df3894..f751942b036 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/SwapMoveSelectorFactoryTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/SwapMoveSelectorFactoryTest.java @@ -21,7 +21,7 @@ import ai.timefold.solver.core.testdomain.multientity.TestdataHerdEntity; import ai.timefold.solver.core.testdomain.multientity.TestdataLeadEntity; import ai.timefold.solver.core.testdomain.multientity.TestdataMultiEntitySolution; -import ai.timefold.solver.core.testdomain.multivar.TestdataMultiVarSolution; +import ai.timefold.solver.core.testdomain.multivar.basic.TestdataMultiVarSolution; import org.junit.jupiter.api.Test; diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/SwapMoveTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/SwapMoveTest.java index 30355be02db..8f8dfd1af21 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/SwapMoveTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/SwapMoveTest.java @@ -14,8 +14,8 @@ import ai.timefold.solver.core.testdomain.TestdataEntity; import ai.timefold.solver.core.testdomain.TestdataSolution; import ai.timefold.solver.core.testdomain.TestdataValue; -import ai.timefold.solver.core.testdomain.multivar.TestdataMultiVarEntity; -import ai.timefold.solver.core.testdomain.multivar.TestdataOtherValue; +import ai.timefold.solver.core.testdomain.multivar.basic.TestdataMultiVarEntity; +import ai.timefold.solver.core.testdomain.multivar.basic.TestdataOtherValue; import ai.timefold.solver.core.testdomain.valuerange.entityproviding.TestdataEntityProvidingEntity; import ai.timefold.solver.core.testdomain.valuerange.entityproviding.TestdataEntityProvidingSolution; diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/ReinitializeVariableValueSelectorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/ReinitializeVariableValueSelectorTest.java index c24b1d3ca82..01a3c36e8d0 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/ReinitializeVariableValueSelectorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/ReinitializeVariableValueSelectorTest.java @@ -17,7 +17,7 @@ import ai.timefold.solver.core.impl.solver.scope.SolverScope; import ai.timefold.solver.core.testdomain.TestdataEntity; import ai.timefold.solver.core.testdomain.TestdataValue; -import ai.timefold.solver.core.testdomain.multivar.TestdataMultiVarEntity; +import ai.timefold.solver.core.testdomain.multivar.basic.TestdataMultiVarEntity; import org.junit.jupiter.api.Test; diff --git a/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java b/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java index 79abd07d1c4..dc9c509434e 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java @@ -22,7 +22,6 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.function.BooleanSupplier; -import java.util.stream.Collectors; import java.util.stream.IntStream; import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore; @@ -1023,7 +1022,7 @@ void solveRepeatedlyBasicVariable(SoftAssertions softly) { final var entityCount = 5; solution.setEntityList(IntStream.rangeClosed(1, entityCount) .mapToObj(id -> new TestdataEntity("e" + id)) - .collect(Collectors.toList())); + .toList()); var score = SolutionManager.create(solverFactory).update(solution); assertThat(score).isNotNull(); @@ -1199,9 +1198,9 @@ void solveWithMultipleChainedPlanningEntities() { .withTerminationConfig(new TerminationConfig().withBestScoreLimit("0")) .withPhases( // Each planning entity class needs a separate CH phase. - new ConstructionHeuristicPhaseConfig().withEntityPlacerConfig(new QueuedEntityPlacerConfig() + new ConstructionHeuristicPhaseConfig().withEntityPlacerConfigList(new QueuedEntityPlacerConfig() .withEntitySelectorConfig(new EntitySelectorConfig(TestdataChainedBrownEntity.class))), - new ConstructionHeuristicPhaseConfig().withEntityPlacerConfig(new QueuedEntityPlacerConfig() + new ConstructionHeuristicPhaseConfig().withEntityPlacerConfigList(new QueuedEntityPlacerConfig() .withEntitySelectorConfig(new EntitySelectorConfig(TestdataChainedGreenEntity.class))), new LocalSearchPhaseConfig().withMoveSelectorConfig(new UnionMoveSelectorConfig().withMoveSelectors( new ChangeMoveSelectorConfig(), diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/invalid/multivar/TestdataInvalidMultiVarEntity.java b/core/src/test/java/ai/timefold/solver/core/testdomain/invalid/multivar/TestdataInvalidMultiVarEntity.java new file mode 100644 index 00000000000..d918408745e --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/invalid/multivar/TestdataInvalidMultiVarEntity.java @@ -0,0 +1,42 @@ +package ai.timefold.solver.core.testdomain.invalid.multivar; + +import java.util.List; + +import ai.timefold.solver.core.api.domain.entity.PlanningEntity; +import ai.timefold.solver.core.api.domain.variable.PlanningListVariable; +import ai.timefold.solver.core.api.domain.variable.PlanningVariable; +import ai.timefold.solver.core.api.domain.variable.PlanningVariableGraphType; +import ai.timefold.solver.core.testdomain.TestdataObject; +import ai.timefold.solver.core.testdomain.chained.TestdataChainedObject; +import ai.timefold.solver.core.testdomain.multivar.list.singleentity.TestdataListMultiVarValue; + +@PlanningEntity +public class TestdataInvalidMultiVarEntity extends TestdataObject implements TestdataChainedObject { + + @PlanningVariable(valueRangeProviderRefs = { "chainedEntityRange", "chainedAnchorRange" }, + graphType = PlanningVariableGraphType.CHAINED) + private TestdataChainedObject chainedValue; + + @PlanningListVariable(valueRangeProviderRefs = "valueRange") + private List valueList; + + public TestdataInvalidMultiVarEntity(String code) { + super(code); + } + + public TestdataChainedObject getChainedValue() { + return chainedValue; + } + + public void setChainedValue(TestdataChainedObject chainedValue) { + this.chainedValue = chainedValue; + } + + public List getValueList() { + return valueList; + } + + public void setValueList(List valueList) { + this.valueList = valueList; + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/invalid/multivar/TestdataInvalidMultiVarSolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/invalid/multivar/TestdataInvalidMultiVarSolution.java new file mode 100644 index 00000000000..4f303754154 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/invalid/multivar/TestdataInvalidMultiVarSolution.java @@ -0,0 +1,65 @@ +package ai.timefold.solver.core.testdomain.invalid.multivar; + +import java.util.List; + +import ai.timefold.solver.core.api.domain.solution.PlanningEntityCollectionProperty; +import ai.timefold.solver.core.api.domain.solution.PlanningScore; +import ai.timefold.solver.core.api.domain.solution.PlanningSolution; +import ai.timefold.solver.core.api.domain.valuerange.ValueRangeProvider; +import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; +import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; +import ai.timefold.solver.core.testdomain.chained.TestdataChainedAnchor; +import ai.timefold.solver.core.testdomain.multivar.list.singleentity.TestdataListMultiVarValue; + +@PlanningSolution +public class TestdataInvalidMultiVarSolution { + + public static SolutionDescriptor buildSolutionDescriptor() { + return SolutionDescriptor.buildSolutionDescriptor(TestdataInvalidMultiVarSolution.class, + TestdataInvalidMultiVarEntity.class); + } + + @ValueRangeProvider(id = "valueRange") + @PlanningEntityCollectionProperty + private List valueList; + @ValueRangeProvider(id = "chainedAnchorRange") + @PlanningEntityCollectionProperty + private List chainedAnchorList; + @PlanningEntityCollectionProperty + @ValueRangeProvider(id = "chainedEntityRange") + private List entityList; + @PlanningScore + private SimpleScore score; + + public List getValueList() { + return valueList; + } + + public void setValueList(List valueList) { + this.valueList = valueList; + } + + public List getChainedAnchorList() { + return chainedAnchorList; + } + + public void setChainedAnchorList(List chainedAnchorList) { + this.chainedAnchorList = chainedAnchorList; + } + + public List getEntityList() { + return entityList; + } + + public void setEntityList(List entityList) { + this.entityList = entityList; + } + + public SimpleScore getScore() { + return score; + } + + public void setScore(SimpleScore score) { + this.score = score; + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/list/TestdataListVarEasyScoreCalculator.java b/core/src/test/java/ai/timefold/solver/core/testdomain/list/TestdataListVarEasyScoreCalculator.java new file mode 100644 index 00000000000..61210b00ef4 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/list/TestdataListVarEasyScoreCalculator.java @@ -0,0 +1,23 @@ +package ai.timefold.solver.core.testdomain.list; + +import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; +import ai.timefold.solver.core.api.score.calculator.EasyScoreCalculator; + +import org.jspecify.annotations.NonNull; + +public class TestdataListVarEasyScoreCalculator implements EasyScoreCalculator { + + @Override + public @NonNull SimpleScore calculateScore(@NonNull TestdataListSolution solution) { + int score = 0; + for (var entity : solution.getEntityList()) { + if (entity.getValueList().size() == 1) { + score += 2; + } else { + score += 1; + } + } + return SimpleScore.of(score); + } + +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/TestdataMultiVarEntity.java b/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/basic/TestdataMultiVarEntity.java similarity index 97% rename from core/src/test/java/ai/timefold/solver/core/testdomain/multivar/TestdataMultiVarEntity.java rename to core/src/test/java/ai/timefold/solver/core/testdomain/multivar/basic/TestdataMultiVarEntity.java index 7ddf763da91..61e9a3c74f7 100644 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/TestdataMultiVarEntity.java +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/basic/TestdataMultiVarEntity.java @@ -1,4 +1,4 @@ -package ai.timefold.solver.core.testdomain.multivar; +package ai.timefold.solver.core.testdomain.multivar.basic; import ai.timefold.solver.core.api.domain.entity.PlanningEntity; import ai.timefold.solver.core.api.domain.variable.PlanningVariable; diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/TestdataMultiVarSolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/basic/TestdataMultiVarSolution.java similarity index 97% rename from core/src/test/java/ai/timefold/solver/core/testdomain/multivar/TestdataMultiVarSolution.java rename to core/src/test/java/ai/timefold/solver/core/testdomain/multivar/basic/TestdataMultiVarSolution.java index 0c3db5b1978..53f44505d59 100644 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/TestdataMultiVarSolution.java +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/basic/TestdataMultiVarSolution.java @@ -1,4 +1,4 @@ -package ai.timefold.solver.core.testdomain.multivar; +package ai.timefold.solver.core.testdomain.multivar.basic; import java.util.List; diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/TestdataMultivarIncrementalScoreCalculator.java b/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/basic/TestdataMultivarIncrementalScoreCalculator.java similarity index 98% rename from core/src/test/java/ai/timefold/solver/core/testdomain/multivar/TestdataMultivarIncrementalScoreCalculator.java rename to core/src/test/java/ai/timefold/solver/core/testdomain/multivar/basic/TestdataMultivarIncrementalScoreCalculator.java index 3ed0430cc13..7c94edf2f32 100644 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/TestdataMultivarIncrementalScoreCalculator.java +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/basic/TestdataMultivarIncrementalScoreCalculator.java @@ -1,4 +1,4 @@ -package ai.timefold.solver.core.testdomain.multivar; +package ai.timefold.solver.core.testdomain.multivar.basic; import java.util.Collection; import java.util.Collections; diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/TestdataOtherValue.java b/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/basic/TestdataOtherValue.java similarity index 79% rename from core/src/test/java/ai/timefold/solver/core/testdomain/multivar/TestdataOtherValue.java rename to core/src/test/java/ai/timefold/solver/core/testdomain/multivar/basic/TestdataOtherValue.java index 244c2f95c69..eb2b19a3694 100644 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/TestdataOtherValue.java +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/basic/TestdataOtherValue.java @@ -1,4 +1,4 @@ -package ai.timefold.solver.core.testdomain.multivar; +package ai.timefold.solver.core.testdomain.multivar.basic; import ai.timefold.solver.core.testdomain.TestdataObject; diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/list/multientity/TestdataListMultiEntityEasyScoreCalculator.java b/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/list/multientity/TestdataListMultiEntityEasyScoreCalculator.java new file mode 100644 index 00000000000..0501e762793 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/list/multientity/TestdataListMultiEntityEasyScoreCalculator.java @@ -0,0 +1,32 @@ +package ai.timefold.solver.core.testdomain.multivar.list.multientity; + +import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; +import ai.timefold.solver.core.api.score.calculator.EasyScoreCalculator; + +import org.jspecify.annotations.NonNull; + +public class TestdataListMultiEntityEasyScoreCalculator + implements EasyScoreCalculator { + + @Override + public @NonNull SimpleScore calculateScore(@NonNull TestdataListMultiEntitySolution solution) { + int score = 0; + for (var entity : solution.getEntityList()) { + if (entity.getValueList().size() == 1) { + score += 2; + } else { + score += 1; + } + } + for (var entity : solution.getOtherEntityList()) { + if (entity.getBasicValue() != null) { + score++; + } + if (entity.getSecondBasicValue() != null) { + score++; + } + } + return SimpleScore.of(score); + } + +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/list/multientity/TestdataListMultiEntityFirstEntity.java b/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/list/multientity/TestdataListMultiEntityFirstEntity.java new file mode 100644 index 00000000000..389f500d2b8 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/list/multientity/TestdataListMultiEntityFirstEntity.java @@ -0,0 +1,32 @@ +package ai.timefold.solver.core.testdomain.multivar.list.multientity; + +import java.util.ArrayList; +import java.util.List; + +import ai.timefold.solver.core.api.domain.entity.PlanningEntity; +import ai.timefold.solver.core.api.domain.variable.PlanningListVariable; +import ai.timefold.solver.core.testdomain.TestdataObject; + +@PlanningEntity +public class TestdataListMultiEntityFirstEntity extends TestdataObject { + + @PlanningListVariable(valueRangeProviderRefs = "valueRange") + private List valueList; + + public TestdataListMultiEntityFirstEntity() { + // Required for cloner + } + + public TestdataListMultiEntityFirstEntity(String code) { + super(code); + valueList = new ArrayList<>(); + } + + public List getValueList() { + return valueList; + } + + public void setValueList(List valueList) { + this.valueList = valueList; + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/list/multientity/TestdataListMultiEntityFirstValue.java b/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/list/multientity/TestdataListMultiEntityFirstValue.java new file mode 100644 index 00000000000..529874f6045 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/list/multientity/TestdataListMultiEntityFirstValue.java @@ -0,0 +1,16 @@ +package ai.timefold.solver.core.testdomain.multivar.list.multientity; + +import ai.timefold.solver.core.api.domain.entity.PlanningEntity; +import ai.timefold.solver.core.testdomain.TestdataObject; + +@PlanningEntity +public class TestdataListMultiEntityFirstValue extends TestdataObject { + + public TestdataListMultiEntityFirstValue() { + // Required for cloner + } + + public TestdataListMultiEntityFirstValue(String code) { + super(code); + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/list/multientity/TestdataListMultiEntitySecondEntity.java b/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/list/multientity/TestdataListMultiEntitySecondEntity.java new file mode 100644 index 00000000000..8f6acbb79f5 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/list/multientity/TestdataListMultiEntitySecondEntity.java @@ -0,0 +1,39 @@ +package ai.timefold.solver.core.testdomain.multivar.list.multientity; + +import ai.timefold.solver.core.api.domain.entity.PlanningEntity; +import ai.timefold.solver.core.api.domain.variable.PlanningVariable; +import ai.timefold.solver.core.testdomain.TestdataObject; + +@PlanningEntity +public class TestdataListMultiEntitySecondEntity extends TestdataObject { + + @PlanningVariable(valueRangeProviderRefs = "otherValueRange") + private TestdataListMultiEntitySecondValue basicValue; + + @PlanningVariable(valueRangeProviderRefs = "otherValueRange") + private TestdataListMultiEntitySecondValue secondBasicValue; + + public TestdataListMultiEntitySecondEntity() { + // Required for cloner + } + + public TestdataListMultiEntitySecondEntity(String code) { + super(code); + } + + public TestdataListMultiEntitySecondValue getBasicValue() { + return basicValue; + } + + public void setBasicValue(TestdataListMultiEntitySecondValue basicValue) { + this.basicValue = basicValue; + } + + public TestdataListMultiEntitySecondValue getSecondBasicValue() { + return secondBasicValue; + } + + public void setSecondBasicValue(TestdataListMultiEntitySecondValue secondBasicValue) { + this.secondBasicValue = secondBasicValue; + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/list/multientity/TestdataListMultiEntitySecondValue.java b/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/list/multientity/TestdataListMultiEntitySecondValue.java new file mode 100644 index 00000000000..caf87e234aa --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/list/multientity/TestdataListMultiEntitySecondValue.java @@ -0,0 +1,16 @@ +package ai.timefold.solver.core.testdomain.multivar.list.multientity; + +import ai.timefold.solver.core.api.domain.entity.PlanningEntity; +import ai.timefold.solver.core.testdomain.TestdataObject; + +@PlanningEntity +public class TestdataListMultiEntitySecondValue extends TestdataObject { + + public TestdataListMultiEntitySecondValue() { + // Required for cloner + } + + public TestdataListMultiEntitySecondValue(String code) { + super(code); + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/list/multientity/TestdataListMultiEntitySolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/list/multientity/TestdataListMultiEntitySolution.java new file mode 100644 index 00000000000..4edb6904c04 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/list/multientity/TestdataListMultiEntitySolution.java @@ -0,0 +1,94 @@ +package ai.timefold.solver.core.testdomain.multivar.list.multientity; + +import java.util.ArrayList; +import java.util.List; + +import ai.timefold.solver.core.api.domain.solution.PlanningEntityCollectionProperty; +import ai.timefold.solver.core.api.domain.solution.PlanningScore; +import ai.timefold.solver.core.api.domain.solution.PlanningSolution; +import ai.timefold.solver.core.api.domain.solution.ProblemFactCollectionProperty; +import ai.timefold.solver.core.api.domain.valuerange.ValueRangeProvider; +import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; + +@PlanningSolution +public class TestdataListMultiEntitySolution { + + public static TestdataListMultiEntitySolution generateUninitializedSolution(int entityListSize, int valueListSize, + int otherValueListSize) { + var solution = new TestdataListMultiEntitySolution(); + var valueList = new ArrayList(valueListSize); + var otherValueList = new ArrayList(otherValueListSize); + for (int i = 0; i < valueListSize; i++) { + valueList.add(new TestdataListMultiEntityFirstValue("Generated Value " + i)); + } + for (int i = 0; i < otherValueListSize; i++) { + otherValueList.add(new TestdataListMultiEntitySecondValue("Generated Other Value " + i)); + } + solution.setValueList(valueList); + solution.setOtherValueList(otherValueList); + var entityList = new ArrayList(entityListSize); + var otherEntityList = new ArrayList(entityListSize); + for (int i = 0; i < entityListSize; i++) { + var entity = new TestdataListMultiEntityFirstEntity("Entity " + i); + entityList.add(entity); + var otherEntity = new TestdataListMultiEntitySecondEntity("Other Entity " + i); + otherEntityList.add(otherEntity); + } + solution.setEntityList(entityList); + solution.setOtherEntityList(otherEntityList); + return solution; + } + + @ValueRangeProvider(id = "valueRange") + @ProblemFactCollectionProperty + private List valueList; + @ValueRangeProvider(id = "otherValueRange") + @ProblemFactCollectionProperty + private List otherValueList; + @PlanningEntityCollectionProperty + private List entityList; + @PlanningEntityCollectionProperty + private List otherEntityList; + @PlanningScore + private SimpleScore score; + + public List getValueList() { + return valueList; + } + + public void setValueList(List valueList) { + this.valueList = valueList; + } + + public List getOtherValueList() { + return otherValueList; + } + + public void setOtherValueList(List otherValueList) { + this.otherValueList = otherValueList; + } + + public List getEntityList() { + return entityList; + } + + public void setEntityList(List entityList) { + this.entityList = entityList; + } + + public List getOtherEntityList() { + return otherEntityList; + } + + public void setOtherEntityList(List otherEntityList) { + this.otherEntityList = otherEntityList; + } + + public SimpleScore getScore() { + return score; + } + + public void setScore(SimpleScore score) { + this.score = score; + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/list/singleentity/TestdataListMultiVarEasyScoreCalculator.java b/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/list/singleentity/TestdataListMultiVarEasyScoreCalculator.java new file mode 100644 index 00000000000..362ad084855 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/list/singleentity/TestdataListMultiVarEasyScoreCalculator.java @@ -0,0 +1,29 @@ +package ai.timefold.solver.core.testdomain.multivar.list.singleentity; + +import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; +import ai.timefold.solver.core.api.score.calculator.EasyScoreCalculator; + +import org.jspecify.annotations.NonNull; + +public class TestdataListMultiVarEasyScoreCalculator implements EasyScoreCalculator { + + @Override + public @NonNull SimpleScore calculateScore(@NonNull TestdataListMultiVarSolution solution) { + int score = 0; + for (var entity : solution.getEntityList()) { + if (entity.getBasicValue() != null) { + score++; + } + if (entity.getSecondBasicValue() != null) { + score++; + } + if (entity.getValueList().size() == 1) { + score += 2; + } else { + score += 1; + } + } + return SimpleScore.of(score); + } + +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/list/singleentity/TestdataListMultiVarEntity.java b/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/list/singleentity/TestdataListMultiVarEntity.java new file mode 100644 index 00000000000..4847ea19a28 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/list/singleentity/TestdataListMultiVarEntity.java @@ -0,0 +1,79 @@ +package ai.timefold.solver.core.testdomain.multivar.list.singleentity; + +import java.util.ArrayList; +import java.util.List; + +import ai.timefold.solver.core.api.domain.entity.PlanningEntity; +import ai.timefold.solver.core.api.domain.entity.PlanningPin; +import ai.timefold.solver.core.api.domain.entity.PlanningPinToIndex; +import ai.timefold.solver.core.api.domain.variable.PlanningListVariable; +import ai.timefold.solver.core.api.domain.variable.PlanningVariable; +import ai.timefold.solver.core.testdomain.TestdataObject; + +@PlanningEntity +public class TestdataListMultiVarEntity extends TestdataObject { + + @PlanningVariable(valueRangeProviderRefs = "otherValueRange") + private TestdataListMultiVarOtherValue basicValue; + + @PlanningVariable(valueRangeProviderRefs = "otherValueRange") + private TestdataListMultiVarOtherValue secondBasicValue; + + @PlanningListVariable(valueRangeProviderRefs = "valueRange") + private List valueList; + + @PlanningPin + private boolean pinned = false; + + @PlanningPinToIndex + private int pinnedIndex = 0; + + public TestdataListMultiVarEntity() { + // Required for cloner + } + + public TestdataListMultiVarEntity(String code) { + super(code); + valueList = new ArrayList<>(); + } + + public TestdataListMultiVarOtherValue getBasicValue() { + return basicValue; + } + + public void setBasicValue(TestdataListMultiVarOtherValue basicValue) { + this.basicValue = basicValue; + } + + public TestdataListMultiVarOtherValue getSecondBasicValue() { + return secondBasicValue; + } + + public void setSecondBasicValue(TestdataListMultiVarOtherValue secondBasicValue) { + this.secondBasicValue = secondBasicValue; + } + + public List getValueList() { + return valueList; + } + + public void setValueList(List valueList) { + this.valueList = valueList; + } + + public boolean isPinned() { + return pinned; + } + + public void setPinned(boolean pinned) { + this.pinned = pinned; + } + + public int getPinnedIndex() { + return pinnedIndex; + } + + public void setPinnedIndex(int pinnedIndex) { + this.pinnedIndex = pinnedIndex; + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/list/singleentity/TestdataListMultiVarOtherValue.java b/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/list/singleentity/TestdataListMultiVarOtherValue.java new file mode 100644 index 00000000000..8740cdeaa62 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/list/singleentity/TestdataListMultiVarOtherValue.java @@ -0,0 +1,32 @@ +package ai.timefold.solver.core.testdomain.multivar.list.singleentity; + +import java.util.ArrayList; +import java.util.List; + +import ai.timefold.solver.core.api.domain.entity.PlanningEntity; +import ai.timefold.solver.core.api.domain.variable.InverseRelationShadowVariable; +import ai.timefold.solver.core.testdomain.TestdataObject; + +@PlanningEntity +public class TestdataListMultiVarOtherValue extends TestdataObject { + + @InverseRelationShadowVariable(sourceVariableName = "basicValue") + private List entityList; + + public TestdataListMultiVarOtherValue() { + // Required for cloner + } + + public TestdataListMultiVarOtherValue(String code) { + super(code); + entityList = new ArrayList<>(); + } + + public List getEntityList() { + return entityList; + } + + public void setEntityList(List entityList) { + this.entityList = entityList; + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/list/singleentity/TestdataListMultiVarSolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/list/singleentity/TestdataListMultiVarSolution.java new file mode 100644 index 00000000000..491a4d903b8 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/list/singleentity/TestdataListMultiVarSolution.java @@ -0,0 +1,86 @@ +package ai.timefold.solver.core.testdomain.multivar.list.singleentity; + +import java.util.ArrayList; +import java.util.List; + +import ai.timefold.solver.core.api.domain.solution.PlanningEntityCollectionProperty; +import ai.timefold.solver.core.api.domain.solution.PlanningScore; +import ai.timefold.solver.core.api.domain.solution.PlanningSolution; +import ai.timefold.solver.core.api.domain.solution.ProblemFactCollectionProperty; +import ai.timefold.solver.core.api.domain.valuerange.ValueRangeProvider; +import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; +import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; + +@PlanningSolution +public class TestdataListMultiVarSolution { + + public static SolutionDescriptor buildSolutionDescriptor() { + return SolutionDescriptor.buildSolutionDescriptor(TestdataListMultiVarSolution.class, TestdataListMultiVarEntity.class, + TestdataListMultiVarValue.class, TestdataListMultiVarOtherValue.class); + } + + public static TestdataListMultiVarSolution generateUninitializedSolution(int entityListSize, int valueListSize, + int otherValueListSize) { + var solution = new TestdataListMultiVarSolution(); + var valueList = new ArrayList(valueListSize); + var otherValueList = new ArrayList(otherValueListSize); + for (int i = 0; i < valueListSize; i++) { + valueList.add(new TestdataListMultiVarValue("Generated Value " + i)); + } + for (int i = 0; i < otherValueListSize; i++) { + otherValueList.add(new TestdataListMultiVarOtherValue("Generated Other Value " + i)); + } + solution.setValueList(valueList); + solution.setOtherValueList(otherValueList); + var entityList = new ArrayList(entityListSize); + for (int i = 0; i < entityListSize; i++) { + var entity = new TestdataListMultiVarEntity("Entity " + i); + entityList.add(entity); + } + solution.setEntityList(entityList); + return solution; + } + + @ValueRangeProvider(id = "valueRange") + @ProblemFactCollectionProperty + private List valueList; + @ValueRangeProvider(id = "otherValueRange") + @ProblemFactCollectionProperty + private List otherValueList; + @PlanningEntityCollectionProperty + private List entityList; + @PlanningScore + private SimpleScore score; + + public List getValueList() { + return valueList; + } + + public void setValueList(List valueList) { + this.valueList = valueList; + } + + public List getOtherValueList() { + return otherValueList; + } + + public void setOtherValueList(List otherValueList) { + this.otherValueList = otherValueList; + } + + public List getEntityList() { + return entityList; + } + + public void setEntityList(List entityList) { + this.entityList = entityList; + } + + public SimpleScore getScore() { + return score; + } + + public void setScore(SimpleScore score) { + this.score = score; + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/list/singleentity/TestdataListMultiVarValue.java b/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/list/singleentity/TestdataListMultiVarValue.java new file mode 100644 index 00000000000..aef299201e2 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/list/singleentity/TestdataListMultiVarValue.java @@ -0,0 +1,64 @@ +package ai.timefold.solver.core.testdomain.multivar.list.singleentity; + +import ai.timefold.solver.core.api.domain.entity.PlanningEntity; +import ai.timefold.solver.core.api.domain.variable.IndexShadowVariable; +import ai.timefold.solver.core.api.domain.variable.InverseRelationShadowVariable; +import ai.timefold.solver.core.api.domain.variable.NextElementShadowVariable; +import ai.timefold.solver.core.api.domain.variable.PreviousElementShadowVariable; +import ai.timefold.solver.core.testdomain.TestdataObject; + +@PlanningEntity +public class TestdataListMultiVarValue extends TestdataObject { + + @InverseRelationShadowVariable(sourceVariableName = "valueList") + private TestdataListMultiVarEntity entity; + + @PreviousElementShadowVariable(sourceVariableName = "valueList") + private TestdataListMultiVarValue previousElement; + + @NextElementShadowVariable(sourceVariableName = "valueList") + private TestdataListMultiVarValue nextElement; + + @IndexShadowVariable(sourceVariableName = "valueList") + private Integer index; + + public TestdataListMultiVarValue() { + // Required for cloner + } + + public TestdataListMultiVarValue(String code) { + super(code); + } + + public TestdataListMultiVarEntity getEntity() { + return entity; + } + + public void setEntity(TestdataListMultiVarEntity entity) { + this.entity = entity; + } + + public TestdataListMultiVarValue getPreviousElement() { + return previousElement; + } + + public void setPreviousElement(TestdataListMultiVarValue previousElement) { + this.previousElement = previousElement; + } + + public TestdataListMultiVarValue getNextElement() { + return nextElement; + } + + public void setNextElement(TestdataListMultiVarValue nextElement) { + this.nextElement = nextElement; + } + + public Integer getIndex() { + return index; + } + + public void setIndex(Integer index) { + this.index = index; + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/list/singleentity/unassignedvar/TestdataUnassignedListMultiVarEasyScoreCalculator.java b/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/list/singleentity/unassignedvar/TestdataUnassignedListMultiVarEasyScoreCalculator.java new file mode 100644 index 00000000000..6fa8ab013d7 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/list/singleentity/unassignedvar/TestdataUnassignedListMultiVarEasyScoreCalculator.java @@ -0,0 +1,34 @@ +package ai.timefold.solver.core.testdomain.multivar.list.singleentity.unassignedvar; + +import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; +import ai.timefold.solver.core.api.score.calculator.EasyScoreCalculator; + +import org.jspecify.annotations.NonNull; + +public class TestdataUnassignedListMultiVarEasyScoreCalculator + implements EasyScoreCalculator { + + @Override + public @NonNull SimpleScore calculateScore(@NonNull TestdataUnassignedListMultiVarSolution solution) { + int score = 0; + for (var entity : solution.getEntityList()) { + if (entity.getBasicValue() != null && !entity.getBasicValue().isBlocked()) { + score++; + } else if (entity.getBasicValue() != null) { + score -= 10; + } + if (entity.getSecondBasicValue() != null) { + score++; + } + if (entity.getValueList().stream().anyMatch(TestdataUnassignedListMultiVarValue::isBlocked)) { + score -= 10; + } else if (entity.getValueList().size() == 3) { + score += 2; + } else { + score++; + } + } + return SimpleScore.of(score); + } + +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/list/singleentity/unassignedvar/TestdataUnassignedListMultiVarEntity.java b/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/list/singleentity/unassignedvar/TestdataUnassignedListMultiVarEntity.java new file mode 100644 index 00000000000..8c698439bd4 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/list/singleentity/unassignedvar/TestdataUnassignedListMultiVarEntity.java @@ -0,0 +1,79 @@ +package ai.timefold.solver.core.testdomain.multivar.list.singleentity.unassignedvar; + +import java.util.ArrayList; +import java.util.List; + +import ai.timefold.solver.core.api.domain.entity.PlanningEntity; +import ai.timefold.solver.core.api.domain.entity.PlanningPin; +import ai.timefold.solver.core.api.domain.entity.PlanningPinToIndex; +import ai.timefold.solver.core.api.domain.variable.PlanningListVariable; +import ai.timefold.solver.core.api.domain.variable.PlanningVariable; +import ai.timefold.solver.core.testdomain.TestdataObject; + +@PlanningEntity +public class TestdataUnassignedListMultiVarEntity extends TestdataObject { + + @PlanningVariable(valueRangeProviderRefs = "otherValueRange", allowsUnassigned = true) + private TestdataUnassignedListMultiVarOtherValue basicValue; + + @PlanningVariable(valueRangeProviderRefs = "otherValueRange") + private TestdataUnassignedListMultiVarOtherValue secondBasicValue; + + @PlanningListVariable(valueRangeProviderRefs = "valueRange", allowsUnassignedValues = true) + private List valueList; + + @PlanningPin + private boolean pinned = false; + + @PlanningPinToIndex + private int pinnedIndex = 0; + + public TestdataUnassignedListMultiVarEntity() { + // Required for cloner + } + + public TestdataUnassignedListMultiVarEntity(String code) { + super(code); + valueList = new ArrayList<>(); + } + + public TestdataUnassignedListMultiVarOtherValue getBasicValue() { + return basicValue; + } + + public void setBasicValue(TestdataUnassignedListMultiVarOtherValue basicValue) { + this.basicValue = basicValue; + } + + public TestdataUnassignedListMultiVarOtherValue getSecondBasicValue() { + return secondBasicValue; + } + + public void setSecondBasicValue(TestdataUnassignedListMultiVarOtherValue secondBasicValue) { + this.secondBasicValue = secondBasicValue; + } + + public List getValueList() { + return valueList; + } + + public void setValueList(List valueList) { + this.valueList = valueList; + } + + public boolean isPinned() { + return pinned; + } + + public void setPinned(boolean pinned) { + this.pinned = pinned; + } + + public int getPinnedIndex() { + return pinnedIndex; + } + + public void setPinnedIndex(int pinnedIndex) { + this.pinnedIndex = pinnedIndex; + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/list/singleentity/unassignedvar/TestdataUnassignedListMultiVarOtherValue.java b/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/list/singleentity/unassignedvar/TestdataUnassignedListMultiVarOtherValue.java new file mode 100644 index 00000000000..34e5dfa88a5 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/list/singleentity/unassignedvar/TestdataUnassignedListMultiVarOtherValue.java @@ -0,0 +1,24 @@ +package ai.timefold.solver.core.testdomain.multivar.list.singleentity.unassignedvar; + +import ai.timefold.solver.core.testdomain.TestdataObject; + +public class TestdataUnassignedListMultiVarOtherValue extends TestdataObject { + + private boolean blocked = false; + + public TestdataUnassignedListMultiVarOtherValue() { + // Required for cloner + } + + public TestdataUnassignedListMultiVarOtherValue(String code) { + super(code); + } + + public boolean isBlocked() { + return blocked; + } + + public void setBlocked(boolean blocked) { + this.blocked = blocked; + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/list/singleentity/unassignedvar/TestdataUnassignedListMultiVarSolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/list/singleentity/unassignedvar/TestdataUnassignedListMultiVarSolution.java new file mode 100644 index 00000000000..ad9a8bcbb1f --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/list/singleentity/unassignedvar/TestdataUnassignedListMultiVarSolution.java @@ -0,0 +1,86 @@ +package ai.timefold.solver.core.testdomain.multivar.list.singleentity.unassignedvar; + +import java.util.ArrayList; +import java.util.List; + +import ai.timefold.solver.core.api.domain.solution.PlanningEntityCollectionProperty; +import ai.timefold.solver.core.api.domain.solution.PlanningScore; +import ai.timefold.solver.core.api.domain.solution.PlanningSolution; +import ai.timefold.solver.core.api.domain.solution.ProblemFactCollectionProperty; +import ai.timefold.solver.core.api.domain.valuerange.ValueRangeProvider; +import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; +import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; + +@PlanningSolution +public class TestdataUnassignedListMultiVarSolution { + + public static SolutionDescriptor buildSolutionDescriptor() { + return SolutionDescriptor.buildSolutionDescriptor(TestdataUnassignedListMultiVarSolution.class, + TestdataUnassignedListMultiVarEntity.class); + } + + public static TestdataUnassignedListMultiVarSolution generateUninitializedSolution(int entityListSize, int valueListSize, + int otherValueListSize) { + var solution = new TestdataUnassignedListMultiVarSolution(); + var valueList = new ArrayList(valueListSize); + var otherValueList = new ArrayList(otherValueListSize); + for (int i = 0; i < valueListSize; i++) { + valueList.add(new TestdataUnassignedListMultiVarValue("Generated Value " + i)); + } + for (int i = 0; i < otherValueListSize; i++) { + otherValueList.add(new TestdataUnassignedListMultiVarOtherValue("Generated Other Value " + i)); + } + solution.setValueList(valueList); + solution.setOtherValueList(otherValueList); + var entityList = new ArrayList(entityListSize); + for (int i = 0; i < entityListSize; i++) { + var entity = new TestdataUnassignedListMultiVarEntity("Entity " + i); + entityList.add(entity); + } + solution.setEntityList(entityList); + return solution; + } + + @ValueRangeProvider(id = "valueRange") + @ProblemFactCollectionProperty + private List valueList; + @ValueRangeProvider(id = "otherValueRange") + @ProblemFactCollectionProperty + private List otherValueList; + @PlanningEntityCollectionProperty + private List entityList; + @PlanningScore + private SimpleScore score; + + public List getValueList() { + return valueList; + } + + public void setValueList(List valueList) { + this.valueList = valueList; + } + + public List getOtherValueList() { + return otherValueList; + } + + public void setOtherValueList(List otherValueList) { + this.otherValueList = otherValueList; + } + + public List getEntityList() { + return entityList; + } + + public void setEntityList(List entityList) { + this.entityList = entityList; + } + + public SimpleScore getScore() { + return score; + } + + public void setScore(SimpleScore score) { + this.score = score; + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/list/singleentity/unassignedvar/TestdataUnassignedListMultiVarValue.java b/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/list/singleentity/unassignedvar/TestdataUnassignedListMultiVarValue.java new file mode 100644 index 00000000000..a6a64c461ac --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/list/singleentity/unassignedvar/TestdataUnassignedListMultiVarValue.java @@ -0,0 +1,24 @@ +package ai.timefold.solver.core.testdomain.multivar.list.singleentity.unassignedvar; + +import ai.timefold.solver.core.testdomain.TestdataObject; + +public class TestdataUnassignedListMultiVarValue extends TestdataObject { + + private boolean blocked = false; + + public TestdataUnassignedListMultiVarValue() { + // Required for cloner + } + + public TestdataUnassignedListMultiVarValue(String code) { + super(code); + } + + public boolean isBlocked() { + return blocked; + } + + public void setBlocked(boolean blocked) { + this.blocked = blocked; + } +}