Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package ai.timefold.solver.core.api.domain.variable;

import java.util.List;

import org.jspecify.annotations.NullMarked;

@NullMarked
public class InconsistentSolutionException extends RuntimeException {
private final Object solution;
private final List<Object> involvedEntityList;

public InconsistentSolutionException(String feature, Object solution, List<Object> involvedEntityList) {
super("The solution (%s) is inconsistent. %s requires a consistent solution.".formatted(solution, feature));
this.solution = solution;
this.involvedEntityList = involvedEntityList;
}

@SuppressWarnings("unchecked")
public <T> T getSolution() {
return (T) solution;
}

@SuppressWarnings("unchecked")
public <T> List<T> getInvolvedEntityList() {
return (List<T>) involvedEntityList;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package ai.timefold.solver.core.impl.domain.variable.declarative;

import java.util.List;

import ai.timefold.solver.core.impl.domain.variable.descriptor.VariableDescriptor;
import ai.timefold.solver.core.impl.domain.variable.supply.Supply;
import ai.timefold.solver.core.preview.api.domain.metamodel.VariableMetaModel;
Expand Down Expand Up @@ -37,4 +39,8 @@ public void afterVariableChanged(VariableMetaModel<Solution_, ?, ?> variableMeta
public boolean updateVariables() {
return graph.updateChanged();
}

public List<Object> getInconsistentEntities() {
return graph.getInconsistentEntities();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import java.util.List;
import java.util.function.IntFunction;

import ai.timefold.solver.core.impl.util.LinkedIdentityHashSet;

import org.jspecify.annotations.NonNull;

final class DefaultVariableReferenceGraph<Solution_> extends AbstractVariableReferenceGraph<Solution_, BitSet> {
Expand Down Expand Up @@ -71,4 +73,20 @@ public void setUnknownInconsistencyValues() {
graph.commitChanges(changeTracker);
affectedEntitiesUpdater.setUnknownInconsistencyValues();
}

@Override
public List<Object> getInconsistentEntities() {
var out = new LinkedIdentityHashSet<>();
var graphTrackingInconsistentEntities = new DefaultTopologicalOrderGraph(this.nodeTopologicalOrders.length);
graph.forEachEdge(graphTrackingInconsistentEntities::addEdge);
graphTrackingInconsistentEntities.commitChanges(new BitSet());
var loopedComponentList = graphTrackingInconsistentEntities.getLoopedComponentList();
for (var loopedComponent : loopedComponentList) {
for (var nodeId : loopedComponent) {
var node = this.nodeList.get(nodeId);
out.add(node.entity());
}
}
return new ArrayList<>(out);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package ai.timefold.solver.core.impl.domain.variable.declarative;

import java.util.Collections;
import java.util.List;

import ai.timefold.solver.core.preview.api.domain.metamodel.VariableMetaModel;

final class EmptyVariableReferenceGraph implements VariableReferenceGraph {
Expand All @@ -22,6 +25,11 @@ public void afterVariableChanged(VariableMetaModel<?, ?, ?> variableReference, O
// No need to do anything.
}

@Override
public List<Object> getInconsistentEntities() {
return Collections.emptyList();
}

@Override
public String toString() {
return "{}";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package ai.timefold.solver.core.impl.domain.variable.declarative;

import java.util.BitSet;
import java.util.Collections;
import java.util.List;
import java.util.PriorityQueue;
import java.util.Spliterators;
import java.util.function.IntFunction;
Expand Down Expand Up @@ -106,4 +108,9 @@ boolean innerUpdateChanged() {
isChanged.clear();
return true;
}

@Override
public List<Object> getInconsistentEntities() {
return Collections.emptyList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.IdentityHashMap;
Expand Down Expand Up @@ -136,4 +137,9 @@ public void afterVariableChanged(VariableMetaModel<?, ?, ?> variableReference, O
}
}

@Override
public List<Object> getInconsistentEntities() {
return Collections.emptyList();
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package ai.timefold.solver.core.impl.domain.variable.declarative;

import java.util.List;

import ai.timefold.solver.core.preview.api.domain.metamodel.VariableMetaModel;

public sealed interface VariableReferenceGraph
Expand Down Expand Up @@ -34,4 +36,5 @@ public sealed interface VariableReferenceGraph
*/
void afterVariableChanged(VariableMetaModel<?, ?, ?> variableReference, Object entity);

List<Object> getInconsistentEntities();
}
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,14 @@ public boolean triggerVariableListenersInNotificationQueues() {
return true;
}

public List<Object> getInconsistentEntities() {
if (shadowVariableSession == null) {
throw new IllegalStateException(
"The shadowVariableSession is null. A solution without shadow variables cannot be inconsistent.");
}
return shadowVariableSession.getInconsistentEntities();
}

/**
* Triggers all cascading update shadow variable user-logic.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ private void doStep(CustomStepScope<Solution_> stepScope, PhaseCommand<Solution_
() -> phaseTermination.isPhaseTerminated(stepScope.getPhaseScope()));
customPhaseCommand.changeWorkingSolution(commandContext);
calculateWorkingStepScore(stepScope, customPhaseCommand);
if (stepScope.getScore().isInvalid()) {
throw new IllegalStateException("The custom phase command (%s) resulted in an inconsistent solution."
.formatted(customPhaseCommand));
}
var solver = stepScope.getPhaseScope().getSolverScope().getSolver();
solver.getBestSolutionRecaller().processWorkingSolutionDuringStep(stepScope);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,60 @@ protected void afterSetWorkingSolution() {
// Do nothing
}

public List<Object> getInconsistentEntities() {
return variableListenerSupport.getInconsistentEntities();
}

public void unassignInconsistentEntities() {
var inconsistentEntities = getInconsistentEntities();
if (listVariableStateSupply != null) {
var listVariableDescriptor = listVariableStateSupply.getSourceVariableDescriptor();
var listElementClass = listVariableStateSupply.getSourceVariableDescriptor().getElementType();
for (var inconsistentEntity : inconsistentEntities) {
if (listElementClass.isInstance(inconsistentEntity)) {
var inverse = Objects.requireNonNull(listVariableStateSupply.getInverseSingleton(inconsistentEntity));
int index = Objects.requireNonNull(listVariableStateSupply.getIndex(inconsistentEntity));

if (listVariableDescriptor.isElementPinned(Objects.requireNonNull(workingSolution), inverse, index)) {
throw new IllegalStateException(
"Entity (%s) is pinned but is involved in a dependency loop".formatted(inconsistentEntity));
}

beforeListVariableElementUnassigned(listVariableDescriptor, inconsistentEntity);
beforeListVariableChanged(listVariableDescriptor, inverse, index, index + 1);
listVariableDescriptor.removeElement(inverse, index);
afterListVariableChanged(listVariableDescriptor, inverse, index, index);
afterListVariableElementUnassigned(listVariableDescriptor, inconsistentEntity);
triggerVariableListeners();
}
// Unassign any normal @PlanningVariable on the entity too
unassignPlainEntity(inconsistentEntity);
}
} else {
for (var inconsistentEntity : inconsistentEntities) {
unassignPlainEntity(inconsistentEntity);
}
}
}

private void unassignPlainEntity(Object inconsistentEntity) {
var entityDescriptor = getSolutionDescriptor().findEntityDescriptor(inconsistentEntity.getClass());
if (entityDescriptor == null) {
throw new IllegalStateException("Object (%s) is not an entity but is inconsistent".formatted(inconsistentEntity));
}
if (entityDescriptor.isGenuine()
&& !entityDescriptor.isMovable(Objects.requireNonNull(workingSolution), inconsistentEntity)) {
throw new IllegalStateException(
"Entity (%s) is pinned but is involved in a dependency loop".formatted(inconsistentEntity));
}
for (var genuineVariableDescriptor : entityDescriptor.getGenuineVariableDescriptorList()) {
beforeVariableChanged(genuineVariableDescriptor, inconsistentEntity);
genuineVariableDescriptor.setValue(inconsistentEntity, null);
afterVariableChanged(genuineVariableDescriptor, inconsistentEntity);
}
triggerVariableListeners();
}

@Override
public void setMoveRepository(@Nullable MoveRepository<Solution_> moveRepository) {
if (this.moveRepository == moveRepository) { // Prevent double initialization
Expand Down Expand Up @@ -437,6 +491,10 @@ public void triggerVariableListeners() {
lastVariableUpdateWasSuccessful = variableListenerSupport.triggerVariableListenersInNotificationQueues();
}

public boolean isLastVariableUpdateWasSuccessful() {
return lastVariableUpdateWasSuccessful;
}

/**
* This function clears all listener events that have been generated without triggering any of them.
* Using this method requires caution because clearing the event queue can lead to inconsistent states.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,8 @@ default InnerScore<Score_> executeTemporaryMove(Move<Solution_> move, boolean as

boolean ignoreInconsistentSolutions();

void unassignInconsistentEntities();

/**
* @return never null
*/
Expand Down Expand Up @@ -363,5 +365,4 @@ default void afterEntityRemoved(Object entity) {
void beforeProblemFactRemoved(Object problemFact);

void afterProblemFactRemoved(Object problemFact);

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.util.function.Function;

import ai.timefold.solver.core.api.domain.solution.PlanningSolution;
import ai.timefold.solver.core.api.domain.variable.InconsistentSolutionException;
import ai.timefold.solver.core.api.score.Score;
import ai.timefold.solver.core.api.score.analysis.ScoreAnalysis;
import ai.timefold.solver.core.api.solver.RecommendedAssignment;
Expand Down Expand Up @@ -51,20 +52,23 @@ public ScoreDirectorFactory<Solution_, Score_> getScoreDirectorFactory() {
@Override
public Score_ update(Solution_ solution, SolutionUpdatePolicy solutionUpdatePolicy) {
if (solutionUpdatePolicy == SolutionUpdatePolicy.NO_UPDATE) {
throw new IllegalArgumentException("Can not call " + this.getClass().getSimpleName()
+ ".update() with this solutionUpdatePolicy (" + solutionUpdatePolicy + ").");
throw new IllegalArgumentException(
"Can not call %s.update() with this solutionUpdatePolicy (%s)."
.formatted(this.getClass().getSimpleName(), solutionUpdatePolicy));
}
return callScoreDirector(solution, solutionUpdatePolicy,
return callScoreDirector("Solution update", solution, solutionUpdatePolicy,
s -> s.getSolutionDescriptor().getScore(s.getWorkingSolution()), ConstraintMatchPolicy.DISABLED, false);
}

private <Result_> Result_ callScoreDirector(Solution_ solution, SolutionUpdatePolicy solutionUpdatePolicy,
private <Result_> Result_ callScoreDirector(String feature, Solution_ solution, SolutionUpdatePolicy solutionUpdatePolicy,
Function<InnerScoreDirector<Solution_, Score_>, Result_> function, ConstraintMatchPolicy constraintMatchPolicy,
boolean cloneSolution) {
var isShadowVariableUpdateEnabled = solutionUpdatePolicy.isShadowVariableUpdateEnabled();
var nonNullSolution = Objects.requireNonNull(solution);
var allowsInconsistent = getScoreDirectorFactory().getSolutionDescriptor().hasAnyShadowVariablesInconsistentMember();
try (var scoreDirector = getScoreDirectorFactory().createScoreDirectorBuilder().withLookUpEnabled(cloneSolution)
.withConstraintMatchPolicy(constraintMatchPolicy)
.withIgnoreInconsistentSolutions(!allowsInconsistent)
.withExpectShadowVariablesInCorrectState(!isShadowVariableUpdateEnabled).build()) {
nonNullSolution = cloneSolution ? scoreDirector.cloneSolution(nonNullSolution) : nonNullSolution;
if (isShadowVariableUpdateEnabled) {
Expand All @@ -82,7 +86,14 @@ private <Result_> Result_ callScoreDirector(Solution_ solution, SolutionUpdatePo
Maybe use Constraint Streams instead of Easy or Incremental score calculator?""");
}
if (solutionUpdatePolicy.isScoreUpdateEnabled()) {
scoreDirector.calculateScore();
var score = scoreDirector.calculateScore();
if (score.isInvalid()) {
var inconsistentEntities = scoreDirector.getInconsistentEntities();
throw new InconsistentSolutionException(feature, nonNullSolution, inconsistentEntities);
}
} else if (!scoreDirector.isLastVariableUpdateWasSuccessful()) {
var inconsistentEntities = scoreDirector.getInconsistentEntities();
throw new InconsistentSolutionException(feature, nonNullSolution, inconsistentEntities);
}
return function.apply(scoreDirector);
}
Expand Down Expand Up @@ -112,7 +123,7 @@ public ScoreAnalysis<Score_> analyze(Solution_ solution, ScoreAnalysisFetchPolic
var enterpriseService =
TimefoldSolverEnterpriseService.loadOrFail(TimefoldSolverEnterpriseService.Feature.SCORE_ANALYSIS);
var currentScore = (Score_) scoreDirectorFactory.getSolutionDescriptor().getScore(solution);
var analysis = callScoreDirector(solution, solutionUpdatePolicy,
var analysis = callScoreDirector("Solution analysis", solution, solutionUpdatePolicy,
scoreDirector -> enterpriseService.analyze(scoreDirector.calculateScore(),
scoreDirector.getConstraintMatchTotalMap(), fetchPolicy),
ConstraintMatchPolicy.match(fetchPolicy), false);
Expand All @@ -134,7 +145,7 @@ public <In_, Out_> List<RecommendedAssignment<Out_, Score_>> recommendAssignment
ScoreAnalysisFetchPolicy fetchPolicy) {
var enterpriseService =
TimefoldSolverEnterpriseService.loadOrFail(TimefoldSolverEnterpriseService.Feature.RECOMMENDATIONS);
return callScoreDirector(
return callScoreDirector("Recommended assignment",
solution, SolutionUpdatePolicy.UPDATE_ALL, enterpriseService.buildRecommender(solverFactory, solution,
evaluatedEntityOrElement, propositionFunction, fetchPolicy),
ConstraintMatchPolicy.match(fetchPolicy), true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,17 @@
import ai.timefold.solver.core.impl.solver.event.SolverEventSupport;
import ai.timefold.solver.core.impl.solver.scope.SolverScope;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Remembers the {@link PlanningSolution best solution} that a {@link Solver} encounters.
*
* @param <Solution_> the solution type, the class with the {@link PlanningSolution} annotation
*/
public class BestSolutionRecaller<Solution_> extends PhaseLifecycleListenerAdapter<Solution_> {

private static final Logger log = LoggerFactory.getLogger(BestSolutionRecaller.class);
protected boolean assertInitialScoreFromScratch = false;
protected boolean assertShadowVariablesAreNotStale = false;
protected boolean assertBestScoreIsUnmodified = false;
Expand All @@ -44,16 +48,21 @@ public void setSolverEventSupport(SolverEventSupport<Solution_> solverEventSuppo
// Worker methods
// ************************************************************************

@SuppressWarnings({ "unchecked", "rawtypes" })
@Override
@SuppressWarnings("unchecked")
public void solvingStarted(SolverScope<Solution_> solverScope) {
// Starting bestSolution is already set by Solver.solve(Solution)
var scoreDirector = solverScope.getScoreDirector();
@SuppressWarnings("rawtypes")
InnerScore innerScore = scoreDirector.calculateScore();
if (innerScore.isInvalid()) {
throw new IllegalStateException(
"The initial solution passed to the solver (%s) is invalid because it has dependency loops."
.formatted(solverScope.getWorkingSolution()));
log.warn("The initial solution passed to the solver is inconsistent. Unassigning involved entities.");
scoreDirector.unassignInconsistentEntities();
innerScore = scoreDirector.calculateScore();
if (innerScore.isInvalid()) {
throw new IllegalStateException(
"The initial solution passed to the solver is inconsistent even after unassigning involved entities.");
}
}
var score = innerScore.raw();
solverScope.setBestScore(innerScore);
Expand Down
Loading