Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
package ai.timefold.solver.core.api.domain.variable;

import java.util.Collection;
import java.util.List;

import ai.timefold.solver.core.api.score.analysis.VariableLoop;

import org.jspecify.annotations.NullMarked;

@NullMarked
public class InconsistentSolutionException extends RuntimeException {
public final class InconsistentSolutionException extends RuntimeException {
private final Object solution;
private final Collection<Object> involvedEntityCollection;
private final List<VariableLoop> variableLoops;

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

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

@SuppressWarnings("unchecked")
public <T> List<T> getInvolvedEntityCollection() {
return (List<T>) involvedEntityCollection;
public List<VariableLoop> getVariableLoops() {
return variableLoops;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import ai.timefold.solver.core.api.domain.entity.PlanningEntity;
import ai.timefold.solver.core.api.domain.solution.PlanningSolution;
import ai.timefold.solver.core.api.score.Score;
import ai.timefold.solver.core.api.score.stream.Constraint;

/**
Expand Down Expand Up @@ -129,8 +130,13 @@
* Do not use a {@link ShadowVariablesInconsistent} property in a method annotated with {@link ShadowSources}.
* {@link ShadowSources} marked methods do not need to check {@link ShadowVariablesInconsistent} properties,
* since they are only called if all their dependencies are consistent.
*
* @deprecated The introduction of {@link Score#structuralScore()} removed the need for this annotation.
* If you currently have this annotation on a property, you are encouraged to remove
* it to have simpler constraints and faster solve speeds.
*/
@Target({ METHOD, FIELD })
@Retention(RUNTIME)
@Deprecated(since = "2.7.0")
public @interface ShadowVariablesInconsistent {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package ai.timefold.solver.core.api.score.analysis;

import org.jspecify.annotations.NullMarked;

/**
* A pair of an entity and a variable on it.
*
* @param entity The entity.
* @param variableName The variable on the entity.
*/
@NullMarked
public record EntityVariablePair(Object entity, String variableName) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will lead to a very verbose JSON.
I question if we need the variable information at all.
If we do, then arguably we can list entities per variable, as opposed to listing the variable with every entity.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want the individual loops, you need the variables, and the loop might be entity1:a -> entity2:b -> entity1:c.

@Override
public String toString() {
return "%s.%s".formatted(entity, variableName);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,21 @@ public interface ScoreAnalysis<Score_ extends Score<Score_>> {
*/
boolean isSolutionInitialized();

/**
* Indicates whether the solution was structurally flawed at the time of analysis.
*
* @return isSolutionStructurallyFlawed true if the solution was structurally flawed at the time of analysis.
*/
boolean isSolutionStructurallyFlawed();

/**
* Returns an analysis of the structural flaws of a structurally flawed solution.
*
* @return null if the solution was not structurally flawed at the time of analysis.
*/
@Nullable
StructuralFlawAnalysis getStructuralFlawAnalysis();

/**
* Performs a lookup on {@link #constraintMap()}.
* Equivalent to {@code constraintMap().get(constraintRef)}.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package ai.timefold.solver.core.api.score.analysis;

import java.util.List;

import org.jspecify.annotations.NullMarked;

/**
* Represents a breakdown of the structural flaws of a solution.
*/
@NullMarked
public interface StructuralFlawAnalysis {
/**
* Return a list of independent {@link VariableLoop}
* that form cycles and thus cause inconsistencies in the solution.
*/
List<VariableLoop> getVariableLoops();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package ai.timefold.solver.core.api.score.analysis;

import java.util.Set;
import java.util.stream.Collectors;

import org.jspecify.annotations.NullMarked;

/**
* A set of entity-variable pairs that form a cycle.
*
* @param involvedVariableSet
*/
@NullMarked
public record VariableLoop(Set<EntityVariablePair> involvedVariableSet) {
/**
* Get the set of involved entities in the cycle
*/
@SuppressWarnings("unchecked")
public <T> Set<T> getEntitySet() {
return (Set<T>) involvedVariableSet.stream()
.map(EntityVariablePair::entity)
.collect(Collectors.toSet());
}

@Override
public String toString() {
return involvedVariableSet.stream()
.map(EntityVariablePair::toString)
.collect(Collectors.joining(", ", "[", "]"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ public interface PhaseCommandContext<Solution_>
* without recalculating the score for performance reasons.
*
* @param move the move to execute
* @throws IllegalArgumentException if the move causes the solution to have a negative
* {@link Score#structuralScore()}. If you are unsure if a move will result in a structurally valid
* solution, use {@link #executeTemporarily(Move)} to check
* if a move results in a structural flawed solution before executing it.
*/
void execute(Move<Solution_> move);

Expand All @@ -62,6 +66,10 @@ public interface PhaseCommandContext<Solution_>
*
* @param move the move to execute
* @return the new score of the working solution after executing the move
* @throws IllegalArgumentException if the move causes the solution to have a negative
* {@link Score#structuralScore()}. If you are unsure if a move will result in a structurally valid
* solution, use {@link #executeTemporarily(Move)} to check
* if a move results in a structural flawed solution before executing it.
*/
<Score_ extends Score<Score_>> Score_ executeAndCalculateScore(Move<Solution_> move);

Expand All @@ -74,10 +82,28 @@ public interface PhaseCommandContext<Solution_>
* @param temporarySolutionConsumer the consumer to execute with the temporarily modified solution;
* this solution must not be modified any further.
* @return the result of the consumer
* @throws IllegalArgumentException if the move causes the solution to have a negative {@link Score#structuralScore()}.
* Use {@link #executeTemporarily(Move, Function, Function)} instead,
* where structurally flawed solutions are handled by a separate consumer.
*/
<Result_> @Nullable Result_ executeTemporarily(Move<Solution_> move,
Function<Solution_, @Nullable Result_> temporarySolutionConsumer);

/**
* As defined by {@link #executeTemporarily(Move, Function)},
* except having a separate consumer to handle solutions with a negative {@link Score#structuralScore()}.
*
* @param move the move to execute temporarily
* @param temporarySolutionConsumer the consumer to execute with the temporarily modified structurally valid solution;
* this solution must not be modified any further.
* @param structurallyFlawedSolutionConsumer the consumer that is called when a move results in a structurally flawed
* solution. This solution must not be modified any further.
* @return the result of the consumer
*/
<Result_> @Nullable Result_ executeTemporarily(Move<Solution_> move,
Function<Solution_, @Nullable Result_> temporarySolutionConsumer,
Function<Solution_, @Nullable Result_> structurallyFlawedSolutionConsumer);

/**
* Executes the given move temporarily and returns the score of the temporarily modified solution.
* The working solution is reverted to its original state after the consumer has been executed,
Expand All @@ -97,10 +123,29 @@ public interface PhaseCommandContext<Solution_>
/**
* As defined by {@link #executeTemporarily(Move, Function)},
* with the guarantee of a fresh score at the end of the method's invocation.
*
* @param move the move to execute temporarily
* @param temporarySolutionConsumer the consumer to execute with the temporarily modified solution;
* this solution must not be modified any further.
* @throws IllegalArgumentException if the move causes the solution to have a negative {@link Score#structuralScore()}.
*/
<Result_> @Nullable Result_ executeTemporarilyAndCalculateScore(Move<Solution_> move,
Function<Solution_, @Nullable Result_> temporarySolutionConsumer);

/**
* As defined by {@link #executeTemporarily(Move, Function, Function)},
* with the guarantee of a fresh score at the end of the method's invocation.
*
* @param move the move to execute temporarily
* @param temporarySolutionConsumer the consumer to execute with the temporarily modified structurally valid solution;
* this solution must not be modified any further.
* @param structurallyFlawedSolutionConsumer the consumer that is called when a move results in a structurally flawed
* solution. This solution must not be modified any further.
*/
<Result_> @Nullable Result_ executeTemporarilyAndCalculateScore(Move<Solution_> move,
Function<Solution_, @Nullable Result_> temporarySolutionConsumer,
Function<Solution_, @Nullable Result_> structurallyFlawedSolutionConsumer);

@Override
<T> @Nullable T lookUpWorkingObject(@Nullable T problemFactOrPlanningEntity);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import ai.timefold.solver.core.api.score.Score;
import ai.timefold.solver.core.api.score.analysis.ScoreAnalysis;
import ai.timefold.solver.core.api.score.analysis.VariableLoop;
import ai.timefold.solver.core.api.score.stream.ConstraintProvider;
import ai.timefold.solver.core.api.score.stream.ConstraintRef;
import ai.timefold.solver.core.api.solver.RecommendedAssignment;
Expand Down Expand Up @@ -44,6 +45,7 @@
import ai.timefold.solver.core.impl.neighborhood.MoveRepository;
import ai.timefold.solver.core.impl.partitionedsearch.PartitionedSearchPhase;
import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchTotal;
import ai.timefold.solver.core.impl.score.definition.ScoreDefinition;
import ai.timefold.solver.core.impl.score.director.InnerScore;
import ai.timefold.solver.core.impl.score.director.InnerScoreDirector;
import ai.timefold.solver.core.impl.solver.DefaultSolverFactory;
Expand Down Expand Up @@ -225,8 +227,15 @@ <Solution_> DestinationSelector<Solution_> applyNearbySelection(DestinationSelec

InnerConstraintProfiler buildConstraintProfiler();

/**
* @param variableLoops the variable loops in the solution
* @param scoreDefinition can be null if variableLoops is known to be empty
*/
<Score_ extends Score<Score_>> ScoreAnalysis<Score_> analyze(InnerScore<Score_> state,
Map<ConstraintRef, ConstraintMatchTotal<Score_>> constraintMatchTotalMap, ScoreAnalysisFetchPolicy fetchPolicy);
Map<ConstraintRef, ConstraintMatchTotal<Score_>> constraintMatchTotalMap,
List<VariableLoop> variableLoops,
@Nullable ScoreDefinition<Score_> scoreDefinition,
Comment thread
Christopher-Chianelli marked this conversation as resolved.
ScoreAnalysisFetchPolicy fetchPolicy);

<Solution_> PlanningSolutionDiff<Solution_> solutionDiff(PlanningSolutionMetaModel<Solution_> metaModel,
Solution_ oldSolution, Solution_ newSolution);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import java.util.function.IntFunction;

import ai.timefold.solver.core.api.domain.solution.PlanningSolution;
import ai.timefold.solver.core.api.score.analysis.VariableLoop;
import ai.timefold.solver.core.enterprise.TimefoldSolverEnterpriseService;
import ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor;
import ai.timefold.solver.core.impl.domain.variable.cascade.CascadingUpdateShadowVariableDescriptor;
Expand Down Expand Up @@ -306,7 +307,8 @@ public void resetWorkingSolution() {
shadowVariableGraphCreator);
shadowVariableSession =
shadowVariableSessionFactory.forSolution(consistencyTracker,
scoreDirector.getWorkingSolution());
scoreDirector.getWorkingSolution(),
scoreDirector.ignoreInconsistentSolutions());
}
}

Expand Down Expand Up @@ -423,12 +425,11 @@ public boolean updateShadowVariables() {
return true;
}

public Collection<Object> getInconsistentEntities() {
public List<VariableLoop> getVariableLoops() {
if (shadowVariableSession == null) {
throw new IllegalStateException(
"Impossible state: The shadowVariableSession is null. A solution without shadow variables cannot be inconsistent.");
return Collections.emptyList();
}
return shadowVariableSession.getInconsistentEntities();
return shadowVariableSession.getVariableLoops();
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package ai.timefold.solver.core.impl.domain.variable.declarative;

import java.util.Collection;
import java.util.List;

import ai.timefold.solver.core.api.score.analysis.VariableLoop;
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.supply.Supply;
Expand Down Expand Up @@ -53,7 +54,7 @@ public boolean updateVariables() {
return graph.updateChanged();
}

public Collection<Object> getInconsistentEntities() {
return graph.getInconsistentEntities();
public List<VariableLoop> getVariableLoops() {
return graph.getVariableLoops();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,27 +78,34 @@ public int hashCode() {

public record GraphDescriptor<Solution_>(ConsistencyTracker<Solution_> consistencyTracker,
SolutionDescriptor<Solution_> solutionDescriptor,
boolean ignoreInconsistentSolutions,
VariableReferenceGraphBuilder<Solution_> variableReferenceGraphBuilder,
Object[] entities, IntFunction<TopologicalOrderGraph> graphCreator) {

public boolean ignoreInconsistentSolutions() {
return !solutionDescriptor.hasAnyShadowVariablesInconsistentMember();
return ignoreInconsistentSolutions;
}

public GraphDescriptor(SolutionDescriptor<Solution_> solutionDescriptor,
ChangedVariableNotifier<Solution_> changedVariableNotifier,
Object... entities) {
this(new ConsistencyTracker<>(), solutionDescriptor, new VariableReferenceGraphBuilder<>(changedVariableNotifier),
this(new ConsistencyTracker<>(), solutionDescriptor, !solutionDescriptor.hasAnyShadowVariablesInconsistentMember(),
new VariableReferenceGraphBuilder<>(changedVariableNotifier),
entities, DefaultTopologicalOrderGraph::new);
}

public GraphDescriptor<Solution_> withGraphCreator(IntFunction<TopologicalOrderGraph> graphCreator) {
return new GraphDescriptor<>(consistencyTracker, solutionDescriptor,
return new GraphDescriptor<>(consistencyTracker, solutionDescriptor, ignoreInconsistentSolutions,
variableReferenceGraphBuilder, entities, graphCreator);
}

public GraphDescriptor<Solution_> withConsistencyTracker(ConsistencyTracker<Solution_> consistencyTracker) {
return new GraphDescriptor<>(consistencyTracker, solutionDescriptor,
return new GraphDescriptor<>(consistencyTracker, solutionDescriptor, ignoreInconsistentSolutions,
variableReferenceGraphBuilder, entities, graphCreator);
}

public GraphDescriptor<Solution_> withIgnoreInconsistentSolutions(boolean ignoreInconsistentSolutions) {
return new GraphDescriptor<>(consistencyTracker, solutionDescriptor, ignoreInconsistentSolutions,
variableReferenceGraphBuilder, entities, graphCreator);
}

Expand Down Expand Up @@ -741,18 +748,21 @@ private static <Solution_> void createFixedVariableRelationEdges(
}

public DefaultShadowVariableSession<Solution_> forSolution(ConsistencyTracker<Solution_> consistencyTracker,
Solution_ solution) {
Solution_ solution,
boolean ignoreInconsistentSolutions) {
var entities = new ArrayList<>();
solutionDescriptor.visitAllEntities(solution, entities::add);
return forEntities(consistencyTracker, entities.toArray());
return forEntities(consistencyTracker, ignoreInconsistentSolutions, entities.toArray());
}

public DefaultShadowVariableSession<Solution_> forEntities(ConsistencyTracker<Solution_> consistencyTracker,
boolean ignoreInconsistentSolutions,
Object... entities) {
var graph = buildGraph(
new GraphDescriptor<>(solutionDescriptor, ChangedVariableNotifier.of(scoreDirector), entities)
.withConsistencyTracker(consistencyTracker)
.withGraphCreator(graphCreator));
.withGraphCreator(graphCreator)
.withIgnoreInconsistentSolutions(ignoreInconsistentSolutions));
return new DefaultShadowVariableSession<>(graph);
}
}
Loading