Skip to content

Commit 1e39882

Browse files
committed
feat: allow to analyze uninitialized solutions
1 parent 1de9105 commit 1e39882

File tree

3 files changed

+174
-12
lines changed

3 files changed

+174
-12
lines changed

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

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -342,16 +342,8 @@ default ScoreAnalysis<Score_> buildScoreAnalysis(boolean analyzeConstraintMatche
342342
*/
343343
default ScoreAnalysis<Score_> buildScoreAnalysis(boolean analyzeConstraintMatches, ScoreAnalysisMode mode) {
344344
var score = calculateScore();
345-
switch (Objects.requireNonNull(mode)) {
346-
case RECOMMENDATION_API -> score = score.withInitScore(0);
347-
case DEFAULT -> {
348-
if (!score.isSolutionInitialized()) {
349-
throw new IllegalArgumentException("""
350-
Cannot analyze solution (%s) as it is not initialized (%s).
351-
Maybe run the solver first?"""
352-
.formatted(getWorkingSolution(), score));
353-
}
354-
}
345+
if (Objects.requireNonNull(mode) == ScoreAnalysisMode.RECOMMENDATION_API) {
346+
score = score.withInitScore(0);
355347
}
356348
var constraintAnalysisMap = new TreeMap<ConstraintRef, ConstraintAnalysis<Score_>>();
357349
for (var constraintMatchTotal : getConstraintMatchTotalMap().values()) {

core/src/test/java/ai/timefold/solver/core/api/score/analysis/ScoreAnalysisTest.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,42 @@ Explanation of score (67):
100100
""");
101101
}
102102

103+
@Test
104+
void summarizeUninitializedSolution() {
105+
var constraintPackage = "constraintPackage";
106+
var constraintName1 = "constraint1";
107+
var constraintName2 = "constraint2";
108+
var constraintId1 = ConstraintRef.of(constraintPackage, constraintName1);
109+
var constraintId2 = ConstraintRef.of(constraintPackage, constraintName2);
110+
111+
var constraintMatchTotal = new DefaultConstraintMatchTotal<>(constraintId1, SimpleScore.of(0));
112+
var constraintMatchTotal2 = new DefaultConstraintMatchTotal<>(constraintId2, SimpleScore.of(0));
113+
var constraintAnalysisMap = Map.of(
114+
constraintMatchTotal.getConstraintRef(), getConstraintAnalysis(constraintMatchTotal, true),
115+
constraintMatchTotal2.getConstraintRef(), getConstraintAnalysis(constraintMatchTotal2, true));
116+
var scoreAnalysis = new ScoreAnalysis<>(SimpleScore.ofUninitialized(3, 0), constraintAnalysisMap);
117+
118+
// Single constraint analysis
119+
var constraintSummary = constraintAnalysisMap.get(constraintMatchTotal.getConstraintRef()).summarize();
120+
assertThat(constraintSummary)
121+
.isEqualTo("""
122+
Explanation of score (0):
123+
Constraint matches:
124+
0: constraint (constraint1) has 0 matches:
125+
""");
126+
127+
// Complete score analysis
128+
var summary = scoreAnalysis.summarize();
129+
assertThat(scoreAnalysis.getConstraintAnalysis(constraintPackage, constraintName1).matchCount()).isEqualTo(0);
130+
assertThat(summary)
131+
.isEqualTo("""
132+
Explanation of score (3init/0):
133+
Constraint matches:
134+
0: constraint (constraint1) has 0 matches:
135+
0: constraint (constraint2) has 0 matches:
136+
""");
137+
}
138+
103139
@Test
104140
void failFastSummarize() {
105141
var constraintPackage = "constraintPackage";

core/src/test/java/ai/timefold/solver/core/api/solver/SolutionManagerTest.java

Lines changed: 136 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -299,8 +299,7 @@ void analyzeNonNullableWithNullValue(SolutionManagerSource SolutionManagerSource
299299
var solutionManager = SolutionManagerSource.createSolutionManager(SOLVER_FACTORY);
300300
assertThat(solutionManager).isNotNull();
301301

302-
assertThatThrownBy(() -> solutionManager.analyze(solution))
303-
.hasMessageContaining("not initialized");
302+
assertThat(solutionManager.analyze(solution)).isNotNull();
304303
}
305304

306305
@ParameterizedTest
@@ -320,6 +319,23 @@ void analyzeNullableWithNullValue(SolutionManagerSource SolutionManagerSource) {
320319
});
321320
}
322321

322+
@ParameterizedTest
323+
@EnumSource(SolutionManagerSource.class)
324+
void analyzeWithUninitializedSolution(SolutionManagerSource SolutionManagerSource) {
325+
var uninitializedSolution = TestdataShadowedSolution.generateSolution(3, 3);
326+
uninitializedSolution.getEntityList().forEach(e -> e.setValue(null));
327+
328+
var solutionManager = SolutionManagerSource.createSolutionManager(SOLVER_FACTORY);
329+
assertThat(solutionManager).isNotNull();
330+
331+
var scoreAnalysis = solutionManager.analyze(uninitializedSolution);
332+
assertThat(scoreAnalysis).isNotNull();
333+
assertSoftly(softly -> {
334+
softly.assertThat(scoreAnalysis.score()).isNotNull();
335+
softly.assertThat(scoreAnalysis.constraintMap()).isNotEmpty();
336+
});
337+
}
338+
323339
@ParameterizedTest
324340
@EnumSource(SolutionManagerSource.class)
325341
void recommendFit(SolutionManagerSource SolutionManagerSource) {
@@ -368,6 +384,21 @@ void recommendFit(SolutionManagerSource SolutionManagerSource) {
368384
});
369385
}
370386

387+
@ParameterizedTest
388+
@EnumSource(SolutionManagerSource.class)
389+
void recommendFitUninitializedSolution(SolutionManagerSource SolutionManagerSource) {
390+
int valueSize = 3;
391+
var uninitializedSolution = TestdataShadowedSolution.generateSolution(valueSize, 3);
392+
uninitializedSolution.getEntityList().forEach(e -> e.setValue(null));
393+
var uninitializedEntity = uninitializedSolution.getEntityList().get(2);
394+
395+
var solutionManager = SolutionManagerSource.createSolutionManager(SOLVER_FACTORY_SHADOWED);
396+
assertThat(solutionManager).isNotNull();
397+
assertThatThrownBy(() -> solutionManager.recommendFit(uninitializedSolution, uninitializedEntity,
398+
TestdataShadowedEntity::getValue))
399+
.hasMessageContaining("Solution (Generated Solution 0) has (3) uninitialized elements.");
400+
}
401+
371402
@ParameterizedTest
372403
@EnumSource(SolutionManagerSource.class)
373404
void recommendFitWithUnassigned(SolutionManagerSource SolutionManagerSource) {
@@ -426,6 +457,22 @@ void recommendFitWithUnassigned(SolutionManagerSource SolutionManagerSource) {
426457
});
427458
}
428459

460+
@ParameterizedTest
461+
@EnumSource(SolutionManagerSource.class)
462+
void recommendFitUninitializedSolutionWithUnassigned(SolutionManagerSource SolutionManagerSource) {
463+
int valueSize = 3;
464+
var uninitializedSolution = TestdataAllowsUnassignedSolution.generateSolution(valueSize, 3);
465+
uninitializedSolution.getEntityList().forEach(e -> e.setValue(null));
466+
var uninitializedEntity = uninitializedSolution.getEntityList().get(2);
467+
var solutionManager = SolutionManagerSource.createSolutionManager(SOLVER_FACTORY_UNASSIGNED);
468+
assertThat(solutionManager).isNotNull();
469+
var recommendationList = solutionManager.recommendFit(uninitializedSolution, uninitializedEntity,
470+
TestdataAllowsUnassignedEntity::getValue);
471+
472+
// Three values means there need to be four recommendations, one extra for unassigned.
473+
assertThat(recommendationList).hasSize(valueSize + 1);
474+
}
475+
429476
@ParameterizedTest
430477
@EnumSource(SolutionManagerSource.class)
431478
void recommendFitMultivar(SolutionManagerSource SolutionManagerSource) {
@@ -517,6 +564,26 @@ void recommendFitMultivar(SolutionManagerSource SolutionManagerSource) {
517564
});
518565
}
519566

567+
@ParameterizedTest
568+
@EnumSource(SolutionManagerSource.class)
569+
void recommendFitUninitializedSolutionWithMultivar(SolutionManagerSource SolutionManagerSource) {
570+
var solution = new TestdataMultiVarSolution("solution");
571+
var firstValue = new TestdataValue("firstValue");
572+
var secondValue = new TestdataValue("secondValue");
573+
solution.setValueList(List.of(firstValue, secondValue));
574+
var firstOtherValue = new TestdataOtherValue("firstOtherValue");
575+
solution.setOtherValueList(List.of(firstOtherValue));
576+
var uninitializedEntity = new TestdataMultiVarEntity("uninitialized");
577+
var secondUninitializedEntity = new TestdataMultiVarEntity("uninitialized2");
578+
solution.setMultiVarEntityList(List.of(uninitializedEntity, secondUninitializedEntity));
579+
580+
var solutionManager = SolutionManagerSource.createSolutionManager(SOLVER_FACTORY_MULTIVAR);
581+
assertThatThrownBy(() -> solutionManager.recommendFit(solution, uninitializedEntity,
582+
entity -> new Triple<>(entity.getPrimaryValue(), entity.getSecondaryValue(),
583+
entity.getTertiaryValueAllowedUnassigned())))
584+
.hasMessageContaining("Solution (solution) has (2) uninitialized elements.");
585+
}
586+
520587
record Triple<A, B, C>(A a, B b, C c) {
521588

522589
}
@@ -589,6 +656,27 @@ void recommendFitChained(SolutionManagerSource SolutionManagerSource) {
589656
});
590657
}
591658

659+
@ParameterizedTest
660+
@EnumSource(SolutionManagerSource.class)
661+
void recommendFitTwoUninitializedEntityWithChained(SolutionManagerSource SolutionManagerSource) {
662+
var a0 = new TestdataShadowingChainedAnchor("a0");
663+
var b0 = new TestdataShadowingChainedAnchor("b0");
664+
var b1 = new TestdataShadowingChainedEntity("b1", b0);
665+
var c0 = new TestdataShadowingChainedAnchor("c0");
666+
var c1 = new TestdataShadowingChainedEntity("c1", c0);
667+
var c2 = new TestdataShadowingChainedEntity("c2", c1);
668+
var uninitializedEntity = new TestdataShadowingChainedEntity("uninitialized");
669+
var uninitializedEntity2 = new TestdataShadowingChainedEntity("uninitialized2");
670+
var uninitializedSolution = new TestdataShadowingChainedSolution("solution");
671+
uninitializedSolution.setChainedAnchorList(Arrays.asList(a0, b0, c0));
672+
uninitializedSolution.setChainedEntityList(Arrays.asList(b1, c1, c2, uninitializedEntity, uninitializedEntity2));
673+
674+
var solutionManager = SolutionManagerSource.createSolutionManager(SOLVER_FACTORY_CHAINED);
675+
assertThatThrownBy(() -> solutionManager.recommendFit(uninitializedSolution, uninitializedEntity,
676+
TestdataShadowingChainedEntity::getChainedObject))
677+
.hasMessageContaining("Solution (solution) has (2) uninitialized elements.");
678+
}
679+
592680
@ParameterizedTest
593681
@EnumSource(SolutionManagerSource.class)
594682
void recommendFitList(SolutionManagerSource SolutionManagerSource) {
@@ -659,6 +747,27 @@ void recommendFitList(SolutionManagerSource SolutionManagerSource) {
659747
});
660748
}
661749

750+
@ParameterizedTest
751+
@EnumSource(SolutionManagerSource.class)
752+
void recommendFitTwoUninitializedEntityWithList(SolutionManagerSource SolutionManagerSource) {
753+
var a = new TestdataListEntityWithShadowHistory("a");
754+
var b0 = new TestdataListValueWithShadowHistory("b0");
755+
var b = new TestdataListEntityWithShadowHistory("b", b0);
756+
var c0 = new TestdataListValueWithShadowHistory("c0");
757+
var c1 = new TestdataListValueWithShadowHistory("c1");
758+
var c = new TestdataListEntityWithShadowHistory("c", c0, c1);
759+
var d = new TestdataListEntityWithShadowHistory("d");
760+
var solution = new TestdataListSolutionWithShadowHistory();
761+
TestdataListValueWithShadowHistory uninitializedValue = new TestdataListValueWithShadowHistory("uninitialized");
762+
solution.setEntityList(Arrays.asList(a, b, c, d));
763+
solution.setValueList(Arrays.asList(b0, c0, c1, uninitializedValue));
764+
765+
var solutionManager = SolutionManagerSource.createSolutionManager(SOLVER_FACTORY_LIST);
766+
var recommendationList =
767+
solutionManager.recommendFit(solution, uninitializedValue, v -> new Pair<>(v.getEntity(), v.getIndex()));
768+
assertThat(recommendationList).hasSize(7);
769+
}
770+
662771
@ParameterizedTest
663772
@EnumSource(SolutionManagerSource.class)
664773
void recommendFitListPinned(SolutionManagerSource SolutionManagerSource) {
@@ -721,6 +830,31 @@ void recommendFitListPinned(SolutionManagerSource SolutionManagerSource) {
721830
});
722831
}
723832

833+
@ParameterizedTest
834+
@EnumSource(SolutionManagerSource.class)
835+
void recommendFitTwoUninitializedEntityWithListPinned(SolutionManagerSource SolutionManagerSource) {
836+
var a = new TestdataPinnedWithIndexListEntity("a");
837+
var b0 = new TestdataPinnedWithIndexListValue("b0");
838+
var b = new TestdataPinnedWithIndexListEntity("b", b0);
839+
b.setPinned(true); // Entity will be unavailable.
840+
var c0 = new TestdataPinnedWithIndexListValue("c0");
841+
var c1 = new TestdataPinnedWithIndexListValue("c1");
842+
var c = new TestdataPinnedWithIndexListEntity("c", c0, c1);
843+
var d = new TestdataPinnedWithIndexListEntity("d");
844+
c.setPinned(false);
845+
c.setPlanningPinToIndex(1); // Destination c[0] will be unavailable.
846+
var solution = new TestdataPinnedWithIndexListSolution();
847+
var uninitializedValue = new TestdataPinnedWithIndexListValue("uninitialized");
848+
solution.setEntityList(Arrays.asList(a, b, c, d));
849+
solution.setValueList(Arrays.asList(b0, c0, c1, uninitializedValue));
850+
851+
var solutionManager = SolutionManagerSource.createSolutionManager(SOLVER_FACTORY_LIST_PINNED);
852+
var recommendationList =
853+
solutionManager.recommendFit(solution, uninitializedValue,
854+
v -> new Pair<>(v.getEntity(), v.getEntity().getValueList().indexOf(v)));
855+
assertThat(recommendationList).hasSize(4);
856+
}
857+
724858
public enum SolutionManagerSource {
725859

726860
FROM_SOLVER_FACTORY(SolutionManager::create),

0 commit comments

Comments
 (0)