Skip to content

Commit 4ec768f

Browse files
chore: throw if pinned entity is inconsistent
1 parent 1c27293 commit 4ec768f

4 files changed

Lines changed: 74 additions & 9 deletions

File tree

core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,12 @@ public void unassignInconsistentEntities() {
353353
if (listElementClass.isInstance(inconsistentEntity)) {
354354
var inverse = Objects.requireNonNull(listVariableStateSupply.getInverseSingleton(inconsistentEntity));
355355
int index = Objects.requireNonNull(listVariableStateSupply.getIndex(inconsistentEntity));
356+
357+
if (listVariableDescriptor.isElementPinned(Objects.requireNonNull(workingSolution), inverse, index)) {
358+
throw new IllegalStateException(
359+
"Entity (%s) is pinned but is involved in a dependency loop".formatted(inconsistentEntity));
360+
}
361+
356362
beforeListVariableElementUnassigned(listVariableDescriptor, inconsistentEntity);
357363
beforeListVariableChanged(listVariableDescriptor, inverse, index, index + 1);
358364
listVariableDescriptor.removeElement(inverse, index);
@@ -375,6 +381,11 @@ private void unassignPlainEntity(Object inconsistentEntity) {
375381
if (entityDescriptor == null) {
376382
throw new IllegalStateException("Object (%s) is not an entity but is inconsistent".formatted(inconsistentEntity));
377383
}
384+
if (entityDescriptor.isGenuine()
385+
&& !entityDescriptor.isMovable(Objects.requireNonNull(workingSolution), inconsistentEntity)) {
386+
throw new IllegalStateException(
387+
"Entity (%s) is pinned but is involved in a dependency loop".formatted(inconsistentEntity));
388+
}
378389
for (var genuineVariableDescriptor : entityDescriptor.getGenuineVariableDescriptorList()) {
379390
beforeVariableChanged(genuineVariableDescriptor, inconsistentEntity);
380391
genuineVariableDescriptor.setValue(inconsistentEntity, null);

core/src/main/java/ai/timefold/solver/core/impl/solver/recaller/BestSolutionRecaller.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ public void solvingStarted(SolverScope<Solution_> solverScope) {
6060
scoreDirector.unassignInconsistentEntities();
6161
innerScore = scoreDirector.calculateScore();
6262
if (innerScore.isInvalid()) {
63-
throw new IllegalStateException("The initial solution passed to the solver is inconsistent even after unassigning involved entities.");
63+
throw new IllegalStateException(
64+
"The initial solution passed to the solver is inconsistent even after unassigning involved entities.");
6465
}
6566
}
6667
var score = innerScore.raw();

core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1454,15 +1454,16 @@ void solveStaleDeclarativeShadows() {
14541454
}
14551455

14561456
@Test
1457-
void solveWhenIgnoringInconsistentSolutionsThrowsIfInitialSolutionInconsistent() {
1457+
void solveWhenIgnoringInconsistentSolutionsUnassignsIfInitialSolutionInconsistent() {
14581458
// Solver config
14591459
var solverConfig = PlannerTestUtils.buildSolverConfig(
14601460
TestdataDependencyNoInconsistentFieldSolution.class, TestdataDependencyNoInconsistentFieldEntity.class,
14611461
TestdataDependencyNoInconsistentFieldValue.class)
14621462
.withEasyScoreCalculatorClass(null)
14631463
.withConstraintProviderClass(TestdataDependencyNoInconsistentFieldConstraintProvider.class)
14641464
.withPhases(new CustomPhaseConfig()
1465-
.withCustomPhaseCommands(command -> {}));
1465+
.withCustomPhaseCommands(command -> {
1466+
}));
14661467

14671468
var e1 = new TestdataDependencyNoInconsistentFieldEntity("a");
14681469
var e2 = new TestdataDependencyNoInconsistentFieldEntity("b");
@@ -1476,7 +1477,7 @@ void solveWhenIgnoringInconsistentSolutionsThrowsIfInitialSolutionInconsistent()
14761477
b2.setDependencies(List.of(b1));
14771478

14781479
e1.setValues(List.of(b2, b1));
1479-
e2.setValues(List.of(a2, a1));
1480+
e2.setValues(List.of(a1, a2));
14801481

14811482
var entities = List.of(e1, e2);
14821483
var values = List.of(a1, a2, b1, b2);
@@ -1487,19 +1488,21 @@ void solveWhenIgnoringInconsistentSolutionsThrowsIfInitialSolutionInconsistent()
14871488
problem.setValues(values);
14881489

14891490
var solution = PlannerTestUtils.solve(solverConfig, problem, false);
1490-
assertThat(solution.getEntities().getFirst().getValues().isEmpty());
1491-
assertThat(solution.getEntities().getLast().getValues().isEmpty());
1491+
assertThat(solution.getEntities().getFirst().getValues()).isEmpty();
1492+
assertThat(solution.getEntities().getLast().getValues()).hasSize(2);
1493+
1494+
var sE2 = solution.getEntities().getLast();
14921495

14931496
var sA1 = solution.getValues().get(0);
14941497
var sA2 = solution.getValues().get(1);
14951498
var sB1 = solution.getValues().get(2);
14961499
var sB2 = solution.getValues().get(3);
14971500

1498-
assertThat(sA1.getEntity()).isNull();
1501+
assertThat(sA1.getEntity()).isEqualTo(sE2);
14991502
assertThat(sA1.getPreviousValue()).isNull();
15001503

1501-
assertThat(sA2.getEntity()).isNull();
1502-
assertThat(sA2.getPreviousValue()).isNull();
1504+
assertThat(sA2.getEntity()).isEqualTo(sE2);
1505+
assertThat(sA2.getPreviousValue()).isEqualTo(sA1);
15031506

15041507
assertThat(sB1.getEntity()).isNull();
15051508
assertThat(sB1.getPreviousValue()).isNull();
@@ -1508,6 +1511,44 @@ void solveWhenIgnoringInconsistentSolutionsThrowsIfInitialSolutionInconsistent()
15081511
assertThat(sB2.getPreviousValue()).isNull();
15091512
}
15101513

1514+
@Test
1515+
void solveWhenIgnoringInconsistentSolutionsThrowsIfInconsistentEntityPinned() {
1516+
// Solver config
1517+
var solverConfig = PlannerTestUtils.buildSolverConfig(
1518+
TestdataDependencyNoInconsistentFieldSolution.class, TestdataDependencyNoInconsistentFieldEntity.class,
1519+
TestdataDependencyNoInconsistentFieldValue.class)
1520+
.withEasyScoreCalculatorClass(null)
1521+
.withConstraintProviderClass(TestdataDependencyNoInconsistentFieldConstraintProvider.class);
1522+
1523+
var e1 = new TestdataDependencyNoInconsistentFieldEntity("a");
1524+
var e2 = new TestdataDependencyNoInconsistentFieldEntity("b");
1525+
1526+
var a1 = new TestdataDependencyNoInconsistentFieldValue("a1");
1527+
var a2 = new TestdataDependencyNoInconsistentFieldValue("a2");
1528+
var b1 = new TestdataDependencyNoInconsistentFieldValue("b1");
1529+
var b2 = new TestdataDependencyNoInconsistentFieldValue("b2");
1530+
1531+
a2.setDependencies(List.of(a1));
1532+
b2.setDependencies(List.of(b1));
1533+
1534+
e1.setValues(List.of(b2, b1));
1535+
e2.setValues(List.of(a2, a1));
1536+
1537+
var entities = List.of(e1, e2);
1538+
var values = List.of(a1, a2, b1, b2);
1539+
1540+
e1.setPinnedToIndex(1);
1541+
1542+
var problem = new TestdataDependencyNoInconsistentFieldSolution();
1543+
1544+
problem.setEntities(entities);
1545+
problem.setValues(values);
1546+
1547+
assertThatCode(() -> PlannerTestUtils.solve(solverConfig, problem, false))
1548+
.isInstanceOf(IllegalStateException.class)
1549+
.hasMessageContainingAll("Entity", "b2", "is pinned but is involved in a dependency loop");
1550+
}
1551+
15111552
@Test
15121553
void solveIgnoreInconsistent() {
15131554
// Solver config

core/src/test/java/ai/timefold/solver/core/testdomain/shadow/no_inconsistent_field/TestdataDependencyNoInconsistentFieldEntity.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import ai.timefold.solver.core.api.domain.common.PlanningId;
99
import ai.timefold.solver.core.api.domain.entity.PlanningEntity;
10+
import ai.timefold.solver.core.api.domain.entity.PlanningPinToIndex;
1011
import ai.timefold.solver.core.api.domain.variable.PlanningListVariable;
1112

1213
@PlanningEntity
@@ -17,6 +18,9 @@ public class TestdataDependencyNoInconsistentFieldEntity {
1718
@PlanningListVariable
1819
List<TestdataDependencyNoInconsistentFieldValue> values;
1920

21+
@PlanningPinToIndex
22+
int pinnedToIndex;
23+
2024
LocalDateTime startTime;
2125

2226
public TestdataDependencyNoInconsistentFieldEntity() {
@@ -57,6 +61,14 @@ public void setStartTime(LocalDateTime startTime) {
5761
this.startTime = startTime;
5862
}
5963

64+
public int getPinnedToIndex() {
65+
return pinnedToIndex;
66+
}
67+
68+
public void setPinnedToIndex(int pinnedToIndex) {
69+
this.pinnedToIndex = pinnedToIndex;
70+
}
71+
6072
@Override
6173
public String toString() {
6274
return "TestdataPredecessorEntity{id=%s, values=%s, startTime=%s}".formatted(id, values, startTime);

0 commit comments

Comments
 (0)