Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import ai.timefold.solver.core.api.score.Score;
import ai.timefold.solver.core.api.score.constraint.ConstraintMatchTotal;
import ai.timefold.solver.core.api.score.constraint.Indictment;
import ai.timefold.solver.core.config.solver.EnvironmentMode;
import ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor;
import ai.timefold.solver.core.impl.domain.solution.descriptor.DefaultShadowVariableMetaModel;
import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor;
Expand Down Expand Up @@ -338,8 +339,8 @@ private List<BasicVariableDescriptor<Solution_>> fetchBasicDescriptors(EntityDes
private static class InternalScoreDirectorFactory<Solution_, Score_ extends Score<Score_>>
extends AbstractScoreDirectorFactory<Solution_, Score_, InternalScoreDirectorFactory<Solution_, Score_>> {

public InternalScoreDirectorFactory(SolutionDescriptor<Solution_> solutionDescriptor) {
super(solutionDescriptor);
public InternalScoreDirectorFactory(SolutionDescriptor<Solution_> solutionDescriptor, EnvironmentMode environmentMode) {
super(solutionDescriptor, environmentMode);
}

@Override
Expand Down Expand Up @@ -387,7 +388,8 @@ public static final class Builder<Solution_, Score_ extends Score<Score_>>
AbstractScoreDirectorBuilder<Solution_, Score_, InternalScoreDirectorFactory<Solution_, Score_>, InternalScoreDirector.Builder<Solution_, Score_>> {

public Builder(SolutionDescriptor<Solution_> solutionDescriptor) {
super(new InternalScoreDirectorFactory<>(solutionDescriptor));
// We use PHASE_ASSERT by default
super(new InternalScoreDirectorFactory<>(solutionDescriptor, EnvironmentMode.PHASE_ASSERT));
withConstraintMatchPolicy(DISABLED);
withLookUpEnabled(false);
withExpectShadowVariablesInCorrectState(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.util.Objects;

import ai.timefold.solver.core.config.solver.EnvironmentMode;
import ai.timefold.solver.core.impl.domain.solution.descriptor.DefaultPlanningSolutionMetaModel;
import ai.timefold.solver.core.impl.score.director.AbstractScoreDirectorFactory;
import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningSolutionMetaModel;
Expand All @@ -16,9 +17,10 @@ public final class DefaultMoveRunner<Solution_> implements MoveRunner<Solution_>
private final AbstractScoreDirectorFactory<Solution_, ?, ?> scoreDirectorFactory;

public DefaultMoveRunner(PlanningSolutionMetaModel<Solution_> solutionMetaModel) {
// We use PHASE_ASSERT by default
this(new MoveRunnerScoreDirectorFactory<>(
((DefaultPlanningSolutionMetaModel<Solution_>) Objects.requireNonNull(solutionMetaModel))
.solutionDescriptor()));
((DefaultPlanningSolutionMetaModel<Solution_>) Objects.requireNonNull(solutionMetaModel)).solutionDescriptor(),
EnvironmentMode.PHASE_ASSERT));
}

private DefaultMoveRunner(AbstractScoreDirectorFactory<Solution_, ?, ?> scoreDirectorFactory) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ai.timefold.solver.core.impl.move;

import ai.timefold.solver.core.api.score.Score;
import ai.timefold.solver.core.config.solver.EnvironmentMode;
import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor;
import ai.timefold.solver.core.impl.score.director.AbstractScoreDirector;
import ai.timefold.solver.core.impl.score.director.AbstractScoreDirectorFactory;
Expand All @@ -11,8 +12,8 @@
final class MoveRunnerScoreDirectorFactory<Solution_, Score_ extends Score<Score_>>
extends AbstractScoreDirectorFactory<Solution_, Score_, MoveRunnerScoreDirectorFactory<Solution_, Score_>> {

public MoveRunnerScoreDirectorFactory(SolutionDescriptor<Solution_> solutionDescriptor) {
super(solutionDescriptor);
public MoveRunnerScoreDirectorFactory(SolutionDescriptor<Solution_> solutionDescriptor, EnvironmentMode environmentMode) {
super(solutionDescriptor, environmentMode);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor;
import ai.timefold.solver.core.impl.domain.variable.InnerVariableListener;
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.VariableDescriptor;
import ai.timefold.solver.core.impl.domain.variable.listener.support.VariableListenerSupport;
Expand Down Expand Up @@ -100,6 +101,8 @@ public abstract class AbstractScoreDirector<Solution_, Score_ extends Score<Scor
protected Solution_ workingSolution;
private int workingInitScore = 0;

private final boolean isStepAssertOrMore;

private @Nullable MoveRepository<Solution_> moveRepository;

protected AbstractScoreDirector(AbstractScoreDirectorBuilder<Solution_, Score_, Factory_, ?> builder) {
Expand All @@ -125,6 +128,8 @@ protected AbstractScoreDirector(AbstractScoreDirectorBuilder<Solution_, Score_,
this.listVariableStateSupply = getSupplyManager().demand(listVariableDescriptor.getStateDemand());
}
setAllChangesWillBeUndoneBeforeStepEnds(false); // Make sure the notifier is correctly initialized.
this.isStepAssertOrMore =
scoreDirectorFactory.environmentMode != null && scoreDirectorFactory.environmentMode.isStepAssertOrMore();
}

@Override
Expand Down Expand Up @@ -263,6 +268,10 @@ protected void setWorkingSolutionWithoutUpdatingShadows(Solution_ workingSolutio
entityAndFactVisitor = entityAndFactVisitor == null ? workingObjectLookupVisitor
: entityAndFactVisitor.andThen(workingObjectLookupVisitor);
}
// One-time check of value ranges
// to ensure assigned planning values are included in the solution range or any entity value range
entityAndFactVisitor = entityAndFactVisitor == null ? this::assertValueRangeForEntity
: entityAndFactVisitor.andThen(this::assertValueRangeForEntity);
// This visits all the facts, applying the visitor if non-null.
if (entityAndFactVisitor != null) {
solutionDescriptor.visitAllProblemFacts(workingSolution, entityAndFactVisitor);
Expand Down Expand Up @@ -504,6 +513,9 @@ public void afterVariableChanged(VariableDescriptor<Solution_> variableDescripto
}
variableListenerSupport.afterVariableChanged(variableDescriptor, entity);
neighborhoodsElementUpdateNotifier.accept(entity);
if (isStepAssertOrMore) {
assertValueRangeForBasicVariables(entity);
}
}

@Override
Expand Down Expand Up @@ -554,6 +566,10 @@ public void afterListVariableChanged(ListVariableDescriptor<Solution_> variableD
int toIndex) {
variableListenerSupport.afterListVariableChanged(variableDescriptor, entity, fromIndex, toIndex);
neighborhoodsElementUpdateNotifier.accept(entity);
if (isStepAssertOrMore) {
var valueList = variableDescriptor.getValue(entity).subList(fromIndex, toIndex);
assertValueRangeForListVariable(entity, valueList);
}
}

public void beforeEntityRemoved(EntityDescriptor<Solution_> entityDescriptor, Object entity) {
Expand Down Expand Up @@ -816,6 +832,90 @@ that could cause the scoreDifference (%s).""".formatted(scoreDifference, beforeM
}
}

private void assertValueRangeForEntity(Object entity) {
assertValueRangeForBasicVariables(entity);
var listVariableDescriptor = getSolutionDescriptor().getListVariableDescriptor();
if (listVariableDescriptor != null) {
if (!listVariableDescriptor.getEntityDescriptor().matchesEntity(entity)) {
return;
}
var valueList = listVariableDescriptor.getValue(entity);
assertValueRangeForListVariable(entity, valueList);
}
}

private void assertValueRangeForBasicVariables(Object entity) {
var entityDescriptor = getSolutionDescriptor().findEntityDescriptor(entity.getClass());
if (entityDescriptor == null) {
// It may be called for a shadow entity
return;
}
var basicVariableDescriptorList = entityDescriptor.getGenuineBasicVariableDescriptorList().stream()
.map(v -> (BasicVariableDescriptor<Solution_>) v).toList();
assertValueRangeForBasicVariables(this, basicVariableDescriptorList, entity);
}

private void assertValueRangeForListVariable(Object entity, List<Object> valueList) {
var entityDescriptor = getSolutionDescriptor().findEntityDescriptor(entity.getClass());
if (entityDescriptor == null) {
// It may be called for a shadow entity
return;
}
var listVariableDescriptor = entityDescriptor.getGenuineListVariableDescriptor();
if (listVariableDescriptor == null) {
// The entity has no genuine list variable
return;
}
assertValueRangeForListVariable(this, listVariableDescriptor, entity, valueList);
}

private static <Solution_> void assertValueRangeForBasicVariables(InnerScoreDirector<Solution_, ?> scoreDirector,
List<BasicVariableDescriptor<Solution_>> basicVariableDescriptorList, Object entity) {
if (basicVariableDescriptorList == null || basicVariableDescriptorList.isEmpty()) {
return;
}
for (var variableDescriptor : basicVariableDescriptorList) {
var value = variableDescriptor.getValue(entity);
if (value == null) {
continue;
}
var valueRange = scoreDirector.getValueRangeManager()
.getFromEntity(variableDescriptor.getValueRangeDescriptor(), entity);
if (!valueRange.contains(value)) {
if (variableDescriptor.isChained()) {
// We also check the entity list
var allEntities =
variableDescriptor.getEntityDescriptor().extractEntities(scoreDirector.getWorkingSolution());
if (allEntities.contains(value)) {
continue;
}
}
throw new IllegalStateException(
"The value (%s) from the planning variable (%s) has been assigned to the entity (%s), but it is outside of the related value range %s."
.formatted(value, variableDescriptor.getVariableName(), entity, valueRange));
}
}
}

private static <Solution_> void assertValueRangeForListVariable(InnerScoreDirector<Solution_, ?> scoreDirector,
ListVariableDescriptor<Solution_> variableDescriptor, Object entity, List<Object> valueList) {
if (valueList.isEmpty()) {
return;
}
var valueRange = scoreDirector.getValueRangeManager()
.getFromEntity(variableDescriptor.getValueRangeDescriptor(), entity);
for (var value : valueList) {
if (value == null) {
continue;
}
if (!valueRange.contains(value)) {
throw new IllegalStateException(
"The value (%s) from the planning variable (%s) has been assigned to the entity (%s), but it is outside of the related value range %s."
.formatted(value, variableDescriptor.getVariableName(), entity, valueRange));
}
}
}

public SolutionTracker.SolutionCorruptionResult getSolutionCorruptionAfterUndo(Move<Solution_> move,
InnerScore<Score_> undoInnerScore) {
var trackingWorkingSolution = solutionTracker != null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public abstract class AbstractScoreDirectorFactory<Solution_, Score_ extends Sco
protected final transient Logger logger = LoggerFactory.getLogger(getClass());

protected final SolutionDescriptor<Solution_> solutionDescriptor;
protected final EnvironmentMode environmentMode;
protected final ListVariableDescriptor<Solution_> listVariableDescriptor;

protected InitializingScoreTrend initializingScoreTrend;
Expand All @@ -36,8 +37,9 @@ public abstract class AbstractScoreDirectorFactory<Solution_, Score_ extends Sco
protected boolean assertClonedSolution = false;
protected boolean trackingWorkingSolution = false;

public AbstractScoreDirectorFactory(SolutionDescriptor<Solution_> solutionDescriptor) {
public AbstractScoreDirectorFactory(SolutionDescriptor<Solution_> solutionDescriptor, EnvironmentMode environmentMode) {
this.solutionDescriptor = solutionDescriptor;
this.environmentMode = environmentMode;
this.listVariableDescriptor = solutionDescriptor.getListVariableDescriptor();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,9 @@ DRL constraints requested via scoreDrlList (%s), but this is no longer supported

// At this point, we are guaranteed to have at most one score director factory selected.
if (config.getEasyScoreCalculatorClass() != null) {
return EasyScoreDirectorFactory.buildScoreDirectorFactory(solutionDescriptor, config);
return EasyScoreDirectorFactory.buildScoreDirectorFactory(solutionDescriptor, config, environmentMode);
} else if (config.getIncrementalScoreCalculatorClass() != null) {
return IncrementalScoreDirectorFactory.buildScoreDirectorFactory(solutionDescriptor, config);
return IncrementalScoreDirectorFactory.buildScoreDirectorFactory(solutionDescriptor, config, environmentMode);
} else if (config.getConstraintProviderClass() != null) {
return BavetConstraintStreamScoreDirectorFactory.buildScoreDirectorFactory(solutionDescriptor, config,
environmentMode);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import ai.timefold.solver.core.api.score.Score;
import ai.timefold.solver.core.api.score.calculator.EasyScoreCalculator;
import ai.timefold.solver.core.config.score.director.ScoreDirectorFactoryConfig;
import ai.timefold.solver.core.config.solver.EnvironmentMode;
import ai.timefold.solver.core.config.util.ConfigUtils;
import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor;
import ai.timefold.solver.core.impl.score.director.AbstractScoreDirector;
Expand All @@ -22,7 +23,8 @@ public final class EasyScoreDirectorFactory<Solution_, Score_ extends Score<Scor
extends AbstractScoreDirectorFactory<Solution_, Score_, EasyScoreDirectorFactory<Solution_, Score_>> {

public static <Solution_, Score_ extends Score<Score_>> EasyScoreDirectorFactory<Solution_, Score_>
buildScoreDirectorFactory(SolutionDescriptor<Solution_> solutionDescriptor, ScoreDirectorFactoryConfig config) {
buildScoreDirectorFactory(SolutionDescriptor<Solution_> solutionDescriptor, ScoreDirectorFactoryConfig config,
EnvironmentMode environmentMode) {
var easyScoreCalculatorClass = config.getEasyScoreCalculatorClass();
if (easyScoreCalculatorClass == null || !EasyScoreCalculator.class.isAssignableFrom(easyScoreCalculatorClass)) {
throw new IllegalArgumentException(
Expand All @@ -33,14 +35,14 @@ public final class EasyScoreDirectorFactory<Solution_, Score_ extends Score<Scor
ConfigUtils.newInstance(config, "easyScoreCalculatorClass", easyScoreCalculatorClass);
ConfigUtils.applyCustomProperties(easyScoreCalculator, "easyScoreCalculatorClass",
config.getEasyScoreCalculatorCustomProperties(), "easyScoreCalculatorCustomProperties");
return new EasyScoreDirectorFactory<>(solutionDescriptor, easyScoreCalculator);
return new EasyScoreDirectorFactory<>(solutionDescriptor, easyScoreCalculator, environmentMode);
}

private final EasyScoreCalculator<Solution_, Score_> easyScoreCalculator;

public EasyScoreDirectorFactory(SolutionDescriptor<Solution_> solutionDescriptor,
EasyScoreCalculator<Solution_, Score_> easyScoreCalculator) {
super(solutionDescriptor);
EasyScoreCalculator<Solution_, Score_> easyScoreCalculator, EnvironmentMode environmentMode) {
super(solutionDescriptor, environmentMode);
this.easyScoreCalculator = easyScoreCalculator;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import ai.timefold.solver.core.api.score.calculator.ConstraintMatchAwareIncrementalScoreCalculator;
import ai.timefold.solver.core.api.score.calculator.IncrementalScoreCalculator;
import ai.timefold.solver.core.config.score.director.ScoreDirectorFactoryConfig;
import ai.timefold.solver.core.config.solver.EnvironmentMode;
import ai.timefold.solver.core.config.util.ConfigUtils;
import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor;
import ai.timefold.solver.core.impl.score.director.AbstractScoreDirectorFactory;
Expand All @@ -24,7 +25,8 @@ public final class IncrementalScoreDirectorFactory<Solution_, Score_ extends Sco
extends AbstractScoreDirectorFactory<Solution_, Score_, IncrementalScoreDirectorFactory<Solution_, Score_>> {

public static <Solution_, Score_ extends Score<Score_>> IncrementalScoreDirectorFactory<Solution_, Score_>
buildScoreDirectorFactory(SolutionDescriptor<Solution_> solutionDescriptor, ScoreDirectorFactoryConfig config) {
buildScoreDirectorFactory(SolutionDescriptor<Solution_> solutionDescriptor, ScoreDirectorFactoryConfig config,
EnvironmentMode environmentMode) {
if (!IncrementalScoreCalculator.class.isAssignableFrom(config.getIncrementalScoreCalculatorClass())) {
throw new IllegalArgumentException(
"The incrementalScoreCalculatorClass (%s) does not implement %s."
Expand All @@ -37,14 +39,15 @@ public final class IncrementalScoreDirectorFactory<Solution_, Score_ extends Sco
ConfigUtils.applyCustomProperties(incrementalScoreCalculator, "incrementalScoreCalculatorClass",
config.getIncrementalScoreCalculatorCustomProperties(), "incrementalScoreCalculatorCustomProperties");
return incrementalScoreCalculator;
});
}, environmentMode);
}

private final Supplier<IncrementalScoreCalculator<Solution_, Score_>> incrementalScoreCalculatorSupplier;

public IncrementalScoreDirectorFactory(SolutionDescriptor<Solution_> solutionDescriptor,
Supplier<IncrementalScoreCalculator<Solution_, Score_>> incrementalScoreCalculatorSupplier) {
super(solutionDescriptor);
Supplier<IncrementalScoreCalculator<Solution_, Score_>> incrementalScoreCalculatorSupplier,
EnvironmentMode environmentMode) {
super(solutionDescriptor, environmentMode);
this.incrementalScoreCalculatorSupplier = incrementalScoreCalculatorSupplier;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public BavetConstraintStreamScoreDirectorFactory(SolutionDescriptor<Solution_> s

public BavetConstraintStreamScoreDirectorFactory(SolutionDescriptor<Solution_> solutionDescriptor,
ConstraintProvider constraintProvider, EnvironmentMode environmentMode, boolean profilingEnabled) {
super(solutionDescriptor);
super(solutionDescriptor, environmentMode);
var constraintFactory = new BavetConstraintFactory<>(solutionDescriptor, environmentMode);
constraintMetaModel = DefaultConstraintMetaModel.of(constraintFactory.buildConstraints(constraintProvider));
constraintSessionFactory =
Expand Down
Loading
Loading