From 883cfbe16385f29c8f53b12f351a834516b4eb82 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Fri, 31 Jul 2026 00:44:48 -0400 Subject: [PATCH 1/2] chore: log number of entities and estimated value count by entity/variable. --- .../api/solver/ProblemSizeStatistics.java | 3 ++ .../entity/descriptor/EntityDescriptor.java | 3 ++ .../score/director/ValueRangeStatistics.java | 45 +++++++++++++++++-- .../core/impl/solver/DefaultSolver.java | 16 +++++++ .../api/solver/ProblemSizeStatisticsTest.java | 36 +++++++++------ 5 files changed, 85 insertions(+), 18 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/api/solver/ProblemSizeStatistics.java b/core/src/main/java/ai/timefold/solver/core/api/solver/ProblemSizeStatistics.java index f59ec245dc3..2a23cb9ba86 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/solver/ProblemSizeStatistics.java +++ b/core/src/main/java/ai/timefold/solver/core/api/solver/ProblemSizeStatistics.java @@ -1,6 +1,7 @@ package ai.timefold.solver.core.api.solver; import java.util.Locale; +import java.util.SequencedMap; import ai.timefold.solver.core.impl.util.MathUtils; @@ -17,8 +18,10 @@ */ @NullMarked public record ProblemSizeStatistics(long entityCount, + SequencedMap, Long> genuineEntityClassToEntityCount, long variableCount, long approximateValueCount, + SequencedMap, SequencedMap> genuineEntityClassToVariableToValueCount, double approximateProblemSizeLog) { /** diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/entity/descriptor/EntityDescriptor.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/entity/descriptor/EntityDescriptor.java index 9b6d760cbb2..770368a3118 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/entity/descriptor/EntityDescriptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/entity/descriptor/EntityDescriptor.java @@ -814,4 +814,7 @@ public String toString() { return "%s(%s)".formatted(getClass().getSimpleName(), entityClass.getCanonicalName()); } + public int getMaxVariableOrdinal() { + return effectiveVariableDescriptorMap.size(); + } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/ValueRangeStatistics.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/ValueRangeStatistics.java index 783cee9cd26..b9cb61ccd25 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/ValueRangeStatistics.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/ValueRangeStatistics.java @@ -1,6 +1,8 @@ package ai.timefold.solver.core.impl.score.director; +import java.util.LinkedHashMap; import java.util.Objects; +import java.util.SequencedMap; import java.util.function.Consumer; import ai.timefold.solver.core.api.solver.ProblemSizeStatistics; @@ -28,6 +30,8 @@ final class ValueRangeStatistics { // Negative if not calculated, non-negative if cached private long cachedApproximateValueCount = -1L; + private long @Nullable [][] cachedValueCountByEntityAndVariableOrdinal; + private long @Nullable [] cachedEntityCountByEntityOrdinal; private double cachedProblemScale = -1.0; ValueRangeStatistics(ValueRangeManager valueRangeManager, SolutionDescriptor solutionDescriptor, @@ -61,6 +65,12 @@ SolutionInitializationStatistics computeInitializationStatistics(@Nullable Consu var approximateValueCount = new MutableLong(); var maxValueRangeSize = new MutableLong(0L); + cachedEntityCountByEntityOrdinal = new long[solutionDescriptor.getEntityDescriptors().size()]; + cachedValueCountByEntityAndVariableOrdinal = new long[cachedEntityCountByEntityOrdinal.length][]; + for (var entityDescriptor : solutionDescriptor.getGenuineEntityDescriptors()) { + cachedValueCountByEntityAndVariableOrdinal[entityDescriptor.getOrdinal()] = new long[entityDescriptor.getMaxVariableOrdinal()]; + } + var listVariableDescriptor = solutionDescriptor.getListVariableDescriptor(); if (listVariableDescriptor != null) { var countOnSolution = @@ -69,6 +79,8 @@ SolutionInitializationStatistics computeInitializationStatistics(@Nullable Consu maxValueRangeSize.setValue(countOnSolution); if (listVariableDescriptor.canExtractValueRangeFromSolution()) { approximateValueCount.add(countOnSolution); + cachedValueCountByEntityAndVariableOrdinal[listVariableDescriptor.getEntityDescriptor().getOrdinal()] + [listVariableDescriptor.getOrdinal()] += countOnSolution; } if (!listVariableDescriptor.allowsUnassignedValues()) { // We count every possibly unassigned element in every list variable. @@ -81,6 +93,8 @@ SolutionInitializationStatistics computeInitializationStatistics(@Nullable Consu if (basicVariable.canExtractValueRangeFromSolution()) { var countOnSolution = valueRangeManager.countOnSolution(basicVariable.getValueRangeDescriptor(), solution); approximateValueCount.add(countOnSolution); + cachedValueCountByEntityAndVariableOrdinal[basicVariable.getEntityDescriptor().getOrdinal()] + [basicVariable.getOrdinal()] += countOnSolution; if (maxValueRangeSize.longValue() < countOnSolution) { maxValueRangeSize.setValue(countOnSolution); } @@ -94,6 +108,9 @@ SolutionInitializationStatistics computeInitializationStatistics(@Nullable Consu var entityDescriptor = solutionDescriptor.findEntityDescriptorOrFail(entity.getClass()); if (entityDescriptor.isGenuine()) { genuineEntityCount.increment(); + // Should we include immovable entities for the count? + // Problem scale ignores them. + cachedEntityCountByEntityOrdinal[entityDescriptor.getOrdinal()]++; var uninitializedVariableCountForEntity = entityDescriptor.countUninitializedVariables(entity); if (uninitializedVariableCountForEntity > 0) { uninitializedEntityCount.increment(); @@ -110,8 +127,10 @@ SolutionInitializationStatistics computeInitializationStatistics(@Nullable Consu for (var genuineVariable : entityDescriptor.getGenuineVariableDescriptorList()) { if (genuineVariable instanceof BasicVariableDescriptor basicVariableDescriptor && !basicVariableDescriptor.canExtractValueRangeFromSolution()) { - approximateValueCount - .add(valueRangeManager.countOnEntity(basicVariableDescriptor.getValueRangeDescriptor(), entity)); + var rangeValueCount = valueRangeManager.countOnEntity(basicVariableDescriptor.getValueRangeDescriptor(), entity); + approximateValueCount.add(rangeValueCount); + cachedValueCountByEntityAndVariableOrdinal[entityDescriptor.getOrdinal()][genuineVariable.getOrdinal()] + += rangeValueCount; } } if (!entityDescriptor.hasAnyListVariables()) { @@ -124,8 +143,10 @@ SolutionInitializationStatistics computeInitializationStatistics(@Nullable Consu unassignedValueCount.subtract(countOnEntity); } if (!listVariableDescriptor.canExtractValueRangeFromSolution()) { - approximateValueCount - .add(valueRangeManager.countOnEntity(listVariableDescriptor.getValueRangeDescriptor(), entity)); + var listValueCount = valueRangeManager.countOnEntity(listVariableDescriptor.getValueRangeDescriptor(), entity); + approximateValueCount.add(listValueCount); + cachedValueCountByEntityAndVariableOrdinal[entityDescriptor.getOrdinal()] + [listVariableDescriptor.getOrdinal()] += listValueCount; } // TODO maybe detect duplicates and elements that are outside the value range }); @@ -152,10 +173,26 @@ public ProblemSizeStatistics getProblemSizeStatistics() { computeInitializationStatistics(null, false); } if (cachedProblemSizeStatistics == null) { + var entityClassToEntityCount = new LinkedHashMap, Long>(); + var entityClassToVariableToValueCount = new LinkedHashMap, SequencedMap>(); + for (var entityDescriptor : solutionDescriptor.getGenuineEntityDescriptors()) { + entityClassToEntityCount.put(entityDescriptor.getEntityClass(), cachedEntityCountByEntityOrdinal[entityDescriptor.getOrdinal()]); + var variableToValueCount = new LinkedHashMap(); + for (var variableDescriptor : entityDescriptor.getBasicVariableDescriptorList()) { + variableToValueCount.put(variableDescriptor.getVariableName(), cachedValueCountByEntityAndVariableOrdinal[entityDescriptor.getOrdinal()][variableDescriptor.getOrdinal()]); + } + if (entityDescriptor.hasAnyListVariables()) { + variableToValueCount.put(entityDescriptor.getListVariableDescriptor().getVariableName(), cachedValueCountByEntityAndVariableOrdinal[entityDescriptor.getOrdinal()] + [entityDescriptor.getListVariableDescriptor().getOrdinal()]); + } + entityClassToVariableToValueCount.put(entityDescriptor.getEntityClass(), variableToValueCount); + } cachedProblemSizeStatistics = new ProblemSizeStatistics( solutionDescriptor.getGenuineEntityCount(solution), + entityClassToEntityCount, solutionDescriptor.getGenuineVariableCount(solution), cachedApproximateValueCount, + entityClassToVariableToValueCount, cachedProblemScale); } return cachedProblemSizeStatistics; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolver.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolver.java index 46c3190064b..75baf00c379 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolver.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolver.java @@ -220,6 +220,22 @@ public void solvingStarted(SolverScope solverScope) { problemSizeStatistics.entityCount(), problemSizeStatistics.variableCount(), problemSizeStatistics.approximateValueCount(), problemSizeStatistics.approximateProblemScaleAsFormattedString()); + if (logger.isDebugEnabled()) { + var genuineEntityClassCountEntries = problemSizeStatistics.genuineEntityClassToEntityCount().entrySet(); + for (var genuineEntityCountEntry : genuineEntityClassCountEntries) { + var geninueEntityClass = genuineEntityCountEntry.getKey(); + logger.debug(" Entity ({}) count: {}", + geninueEntityClass.getCanonicalName(), + genuineEntityCountEntry.getValue()); + for (var geninueVariableEntry : problemSizeStatistics + .genuineEntityClassToVariableToValueCount() + .get(geninueEntityClass).entrySet()) { + var genuineVariable = geninueVariableEntry.getKey(); + logger.debug(" Variable ({}) estimated value count: {}", + genuineVariable, geninueVariableEntry.getValue()); + } + } + } } } diff --git a/core/src/test/java/ai/timefold/solver/core/api/solver/ProblemSizeStatisticsTest.java b/core/src/test/java/ai/timefold/solver/core/api/solver/ProblemSizeStatisticsTest.java index c79a710f9d6..51150683aae 100644 --- a/core/src/test/java/ai/timefold/solver/core/api/solver/ProblemSizeStatisticsTest.java +++ b/core/src/test/java/ai/timefold/solver/core/api/solver/ProblemSizeStatisticsTest.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.util.Collections; import java.util.Locale; import ai.timefold.solver.core.impl.util.MathUtils; @@ -12,11 +13,18 @@ class ProblemSizeStatisticsTest { - private static ProblemSizeStatistics getProblemSizeStatistics(long scale) { - return new ProblemSizeStatistics(0L, 0L, 0L, + private static ProblemSizeStatistics getProblemSizeStatisticsFromCountLong(long scale) { + return new ProblemSizeStatistics(0L, Collections.emptySortedMap(), 0L, 0L, + Collections.emptySortedMap(), Math.log10(scale)); } + private static ProblemSizeStatistics getProblemSizeStatisticsFromDoubleLog(double scale) { + return new ProblemSizeStatistics(0L, Collections.emptySortedMap(), 0L, 0L, + Collections.emptySortedMap(), + scale); + } + private static Locale defaultLocaleToRestore; @BeforeAll @@ -33,58 +41,58 @@ public static void restoreLocale() { @Test void getApproximateProblemScaleLogAsFixedPointLong() { - var statistics = getProblemSizeStatistics(100L); + var statistics = getProblemSizeStatisticsFromCountLong(100L); assertThat(statistics.approximateProblemScaleLogAsFixedPointLong()) .isEqualTo(MathUtils.getScaledApproximateLog(MathUtils.LOG_PRECISION, 10L, 100L)); - statistics = getProblemSizeStatistics(250L); + statistics = getProblemSizeStatisticsFromCountLong(250L); assertThat(statistics.approximateProblemScaleLogAsFixedPointLong()) .isEqualTo(MathUtils.getScaledApproximateLog(MathUtils.LOG_PRECISION, 10L, 250L)); } @Test void formatApproximateProblemScale() { - var statistics = getProblemSizeStatistics(100L); + var statistics = getProblemSizeStatisticsFromCountLong(100L); assertThat(statistics.approximateProblemScaleAsFormattedString()) .isEqualTo("100"); - statistics = getProblemSizeStatistics(250L); + statistics = getProblemSizeStatisticsFromCountLong(250L); assertThat(statistics.approximateProblemScaleAsFormattedString()) .isEqualTo("250"); - statistics = getProblemSizeStatistics(1_234_567L); + statistics = getProblemSizeStatisticsFromCountLong(1_234_567L); assertThat(statistics.approximateProblemScaleAsFormattedString()) .isEqualTo("1,234,567"); - statistics = getProblemSizeStatistics(123_456_789L); + statistics = getProblemSizeStatisticsFromCountLong(123_456_789L); assertThat(statistics.approximateProblemScaleAsFormattedString()) .isEqualTo("123,456,789"); - statistics = getProblemSizeStatistics(1_123_456_789L); + statistics = getProblemSizeStatisticsFromCountLong(1_123_456_789L); assertThat(statistics.approximateProblemScaleAsFormattedString()) .isEqualTo("1,123,456,789"); - statistics = getProblemSizeStatistics(321_123_456_789L); + statistics = getProblemSizeStatisticsFromCountLong(321_123_456_789L); assertThat(statistics.approximateProblemScaleAsFormattedString()) .isEqualTo("3.211235 × 10^11"); // scale = -infinity - statistics = new ProblemSizeStatistics(0L, 0L, 0L, Double.NEGATIVE_INFINITY); + statistics = getProblemSizeStatisticsFromDoubleLog(Double.NEGATIVE_INFINITY); assertThat(statistics.approximateProblemScaleAsFormattedString()) .isEqualTo("0"); // scale = +infinity - statistics = new ProblemSizeStatistics(0L, 0L, 0L, Double.POSITIVE_INFINITY); + statistics = getProblemSizeStatisticsFromDoubleLog(Double.POSITIVE_INFINITY); assertThat(statistics.approximateProblemScaleAsFormattedString()) .isEqualTo("0"); // scale = NaN - statistics = new ProblemSizeStatistics(0L, 0L, 0L, Double.NaN); + statistics = getProblemSizeStatisticsFromDoubleLog(Double.NaN); assertThat(statistics.approximateProblemScaleAsFormattedString()) .isEqualTo("0"); // scale = 0 - statistics = new ProblemSizeStatistics(0L, 0L, 0L, 0); + statistics = getProblemSizeStatisticsFromDoubleLog(0); assertThat(statistics.approximateProblemScaleAsFormattedString()) .isEqualTo("1"); } From 61cfddb81f368ed7f283e30e5d0e3db505cfa089 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Fri, 31 Jul 2026 01:18:33 -0400 Subject: [PATCH 2/2] chore: add tests for the breakdown of ProblemSizeStatistics --- core/src/build/revapi-differences.json | 7 + .../score/director/ValueRangeStatistics.java | 41 +++-- .../core/impl/solver/DefaultSolver.java | 2 +- .../api/solver/ProblemSizeStatisticsTest.java | 162 ++++++++++++++++++ 4 files changed, 196 insertions(+), 16 deletions(-) diff --git a/core/src/build/revapi-differences.json b/core/src/build/revapi-differences.json index caa59d0ea5a..e0b9f8792e9 100644 --- a/core/src/build/revapi-differences.json +++ b/core/src/build/revapi-differences.json @@ -44,6 +44,13 @@ "old": "field ai.timefold.solver.core.config.heuristic.selector.move.generic.AbstractPillarMoveSelectorConfig>.subPillarSequenceComparatorClass", "new": "field ai.timefold.solver.core.config.heuristic.selector.move.generic.AbstractPillarMoveSelectorConfig>.subPillarSequenceComparatorClass", "justification": "Internal protected fields; safe." + }, + { + "ignore": true, + "code": "java.method.numberOfParametersChanged", + "old": "method void ai.timefold.solver.core.api.solver.ProblemSizeStatistics::(long, long, long, double)", + "new": "method void ai.timefold.solver.core.api.solver.ProblemSizeStatistics::(long, java.util.SequencedMap, java.lang.Long>, long, long, java.util.SequencedMap, java.util.SequencedMap>, double)", + "justification": "Type is not supposed to be constructed by user; safe." } ] } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/ValueRangeStatistics.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/ValueRangeStatistics.java index b9cb61ccd25..4c32a6caa6f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/ValueRangeStatistics.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/ValueRangeStatistics.java @@ -67,8 +67,14 @@ SolutionInitializationStatistics computeInitializationStatistics(@Nullable Consu cachedEntityCountByEntityOrdinal = new long[solutionDescriptor.getEntityDescriptors().size()]; cachedValueCountByEntityAndVariableOrdinal = new long[cachedEntityCountByEntityOrdinal.length][]; - for (var entityDescriptor : solutionDescriptor.getGenuineEntityDescriptors()) { - cachedValueCountByEntityAndVariableOrdinal[entityDescriptor.getOrdinal()] = new long[entityDescriptor.getMaxVariableOrdinal()]; + for (var entityDescriptor : solutionDescriptor.getEntityDescriptors()) { + // TimefoldTestResourceTest in Quarkus has an entity class with a basic variable + // that was not considered a genuine entity. + // (i.e. solutionDescriptor.getGenuineEntityDescriptors() did not have it). + // Are subclass entities that do not add new basic/list variable not considered + // genuine entities? + cachedValueCountByEntityAndVariableOrdinal[entityDescriptor.getOrdinal()] = + new long[entityDescriptor.getMaxVariableOrdinal()]; } var listVariableDescriptor = solutionDescriptor.getListVariableDescriptor(); @@ -79,8 +85,8 @@ SolutionInitializationStatistics computeInitializationStatistics(@Nullable Consu maxValueRangeSize.setValue(countOnSolution); if (listVariableDescriptor.canExtractValueRangeFromSolution()) { approximateValueCount.add(countOnSolution); - cachedValueCountByEntityAndVariableOrdinal[listVariableDescriptor.getEntityDescriptor().getOrdinal()] - [listVariableDescriptor.getOrdinal()] += countOnSolution; + cachedValueCountByEntityAndVariableOrdinal[listVariableDescriptor.getEntityDescriptor() + .getOrdinal()][listVariableDescriptor.getOrdinal()] += countOnSolution; } if (!listVariableDescriptor.allowsUnassignedValues()) { // We count every possibly unassigned element in every list variable. @@ -93,8 +99,8 @@ SolutionInitializationStatistics computeInitializationStatistics(@Nullable Consu if (basicVariable.canExtractValueRangeFromSolution()) { var countOnSolution = valueRangeManager.countOnSolution(basicVariable.getValueRangeDescriptor(), solution); approximateValueCount.add(countOnSolution); - cachedValueCountByEntityAndVariableOrdinal[basicVariable.getEntityDescriptor().getOrdinal()] - [basicVariable.getOrdinal()] += countOnSolution; + cachedValueCountByEntityAndVariableOrdinal[basicVariable.getEntityDescriptor().getOrdinal()][basicVariable + .getOrdinal()] += countOnSolution; if (maxValueRangeSize.longValue() < countOnSolution) { maxValueRangeSize.setValue(countOnSolution); } @@ -127,10 +133,11 @@ SolutionInitializationStatistics computeInitializationStatistics(@Nullable Consu for (var genuineVariable : entityDescriptor.getGenuineVariableDescriptorList()) { if (genuineVariable instanceof BasicVariableDescriptor basicVariableDescriptor && !basicVariableDescriptor.canExtractValueRangeFromSolution()) { - var rangeValueCount = valueRangeManager.countOnEntity(basicVariableDescriptor.getValueRangeDescriptor(), entity); + var rangeValueCount = + valueRangeManager.countOnEntity(basicVariableDescriptor.getValueRangeDescriptor(), entity); approximateValueCount.add(rangeValueCount); - cachedValueCountByEntityAndVariableOrdinal[entityDescriptor.getOrdinal()][genuineVariable.getOrdinal()] - += rangeValueCount; + cachedValueCountByEntityAndVariableOrdinal[entityDescriptor.getOrdinal()][genuineVariable.getOrdinal()] += + rangeValueCount; } } if (!entityDescriptor.hasAnyListVariables()) { @@ -145,8 +152,8 @@ SolutionInitializationStatistics computeInitializationStatistics(@Nullable Consu if (!listVariableDescriptor.canExtractValueRangeFromSolution()) { var listValueCount = valueRangeManager.countOnEntity(listVariableDescriptor.getValueRangeDescriptor(), entity); approximateValueCount.add(listValueCount); - cachedValueCountByEntityAndVariableOrdinal[entityDescriptor.getOrdinal()] - [listVariableDescriptor.getOrdinal()] += listValueCount; + cachedValueCountByEntityAndVariableOrdinal[entityDescriptor.getOrdinal()][listVariableDescriptor + .getOrdinal()] += listValueCount; } // TODO maybe detect duplicates and elements that are outside the value range }); @@ -176,14 +183,18 @@ public ProblemSizeStatistics getProblemSizeStatistics() { var entityClassToEntityCount = new LinkedHashMap, Long>(); var entityClassToVariableToValueCount = new LinkedHashMap, SequencedMap>(); for (var entityDescriptor : solutionDescriptor.getGenuineEntityDescriptors()) { - entityClassToEntityCount.put(entityDescriptor.getEntityClass(), cachedEntityCountByEntityOrdinal[entityDescriptor.getOrdinal()]); + entityClassToEntityCount.put(entityDescriptor.getEntityClass(), + cachedEntityCountByEntityOrdinal[entityDescriptor.getOrdinal()]); var variableToValueCount = new LinkedHashMap(); for (var variableDescriptor : entityDescriptor.getBasicVariableDescriptorList()) { - variableToValueCount.put(variableDescriptor.getVariableName(), cachedValueCountByEntityAndVariableOrdinal[entityDescriptor.getOrdinal()][variableDescriptor.getOrdinal()]); + variableToValueCount.put(variableDescriptor.getVariableName(), + cachedValueCountByEntityAndVariableOrdinal[entityDescriptor.getOrdinal()][variableDescriptor + .getOrdinal()]); } if (entityDescriptor.hasAnyListVariables()) { - variableToValueCount.put(entityDescriptor.getListVariableDescriptor().getVariableName(), cachedValueCountByEntityAndVariableOrdinal[entityDescriptor.getOrdinal()] - [entityDescriptor.getListVariableDescriptor().getOrdinal()]); + variableToValueCount.put(entityDescriptor.getListVariableDescriptor().getVariableName(), + cachedValueCountByEntityAndVariableOrdinal[entityDescriptor.getOrdinal()][entityDescriptor + .getListVariableDescriptor().getOrdinal()]); } entityClassToVariableToValueCount.put(entityDescriptor.getEntityClass(), variableToValueCount); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolver.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolver.java index 75baf00c379..c115f434352 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolver.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolver.java @@ -227,7 +227,7 @@ public void solvingStarted(SolverScope solverScope) { logger.debug(" Entity ({}) count: {}", geninueEntityClass.getCanonicalName(), genuineEntityCountEntry.getValue()); - for (var geninueVariableEntry : problemSizeStatistics + for (var geninueVariableEntry : problemSizeStatistics .genuineEntityClassToVariableToValueCount() .get(geninueEntityClass).entrySet()) { var genuineVariable = geninueVariableEntry.getKey(); diff --git a/core/src/test/java/ai/timefold/solver/core/api/solver/ProblemSizeStatisticsTest.java b/core/src/test/java/ai/timefold/solver/core/api/solver/ProblemSizeStatisticsTest.java index 51150683aae..3fb6e07fd1b 100644 --- a/core/src/test/java/ai/timefold/solver/core/api/solver/ProblemSizeStatisticsTest.java +++ b/core/src/test/java/ai/timefold/solver/core/api/solver/ProblemSizeStatisticsTest.java @@ -4,8 +4,24 @@ import java.util.Collections; import java.util.Locale; +import java.util.Map; +import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; +import ai.timefold.solver.core.impl.score.director.ValueRangeManager; import ai.timefold.solver.core.impl.util.MathUtils; +import ai.timefold.solver.core.testdomain.TestdataEntity; +import ai.timefold.solver.core.testdomain.TestdataSolution; +import ai.timefold.solver.core.testdomain.cascade.single.TestdataSingleCascadingEntity; +import ai.timefold.solver.core.testdomain.cascade.single.TestdataSingleCascadingSolution; +import ai.timefold.solver.core.testdomain.composite.TestdataCompositeEntity; +import ai.timefold.solver.core.testdomain.composite.TestdataCompositeSolution; +import ai.timefold.solver.core.testdomain.constraintverifier.TestdataConstraintVerifierExtendedSolution; +import ai.timefold.solver.core.testdomain.constraintverifier.TestdataConstraintVerifierFirstEntity; +import ai.timefold.solver.core.testdomain.constraintverifier.TestdataConstraintVerifierSecondEntity; +import ai.timefold.solver.core.testdomain.equals.list.TestdataEqualsByCodeListEntity; +import ai.timefold.solver.core.testdomain.equals.list.TestdataEqualsByCodeListSolution; +import ai.timefold.solver.core.testdomain.record.TestdataRecordEntity; +import ai.timefold.solver.core.testdomain.record.TestdataRecordSolution; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -96,4 +112,150 @@ void formatApproximateProblemScale() { assertThat(statistics.approximateProblemScaleAsFormattedString()) .isEqualTo("1"); } + + @Test + void simpleSolutionEntityAndVariableCount() { + var solution = TestdataSolution.generateSolution(5, 7); + var valueRangeManager = ValueRangeManager.of(TestdataSolution.buildSolutionDescriptor(), solution); + var statistics = valueRangeManager.getProblemSizeStatistics(); + + assertThat(statistics.entityCount()).isEqualTo(7L); + assertThat(statistics.variableCount()).isEqualTo(7L); + assertThat(statistics.approximateValueCount()).isEqualTo(5L); + assertThat(statistics.genuineEntityClassToEntityCount()) + .containsExactlyEntriesOf(Map.of(TestdataEntity.class, 7L)); + assertThat(statistics.genuineEntityClassToVariableToValueCount().get(TestdataEntity.class)) + .containsEntry("value", 5L); + } + + @Test + void emptySolutionEntityAndVariableCount() { + var solution = TestdataSolution.generateSolution(3, 0); + var valueRangeManager = ValueRangeManager.of(TestdataSolution.buildSolutionDescriptor(), solution); + var statistics = valueRangeManager.getProblemSizeStatistics(); + + assertThat(statistics.entityCount()).isEqualTo(0L); + assertThat(statistics.variableCount()).isEqualTo(0L); + assertThat(statistics.approximateValueCount()).isEqualTo(3L); + assertThat(statistics.genuineEntityClassToEntityCount()) + .containsExactlyEntriesOf(Map.of(TestdataEntity.class, 0L)); + assertThat(statistics.genuineEntityClassToVariableToValueCount().get(TestdataEntity.class)) + .containsEntry("value", 3L); + } + + @Test + void compositeValueRangesEntityAndVariableCount() { + var solution = TestdataCompositeSolution.generateSolution(4, 6); + var valueRangeManager = ValueRangeManager.of(TestdataCompositeSolution.buildSolutionDescriptor(), solution); + var statistics = valueRangeManager.getProblemSizeStatistics(); + + assertThat(statistics.entityCount()).isEqualTo(6L); + assertThat(statistics.variableCount()).isEqualTo(6L); + assertThat(statistics.approximateValueCount()).isEqualTo(8L); + assertThat(statistics.genuineEntityClassToEntityCount()) + .containsExactlyEntriesOf(Map.of(TestdataCompositeEntity.class, 6L)); + assertThat(statistics.genuineEntityClassToVariableToValueCount().get(TestdataCompositeEntity.class)) + .containsEntry("value", 8L); + } + + @Test + void multipleEntityTypesEntityAndVariableCount() { + var solution = TestdataConstraintVerifierExtendedSolution.generateSolution(5, 8); + var valueRangeManager = ValueRangeManager.of( + SolutionDescriptor.buildSolutionDescriptor(TestdataConstraintVerifierExtendedSolution.class, + TestdataConstraintVerifierFirstEntity.class, + TestdataConstraintVerifierSecondEntity.class), + solution); + var statistics = valueRangeManager.getProblemSizeStatistics(); + + var firstEntityCount = solution.getEntityList().size(); + var secondEntityCount = solution.getSecondEntityList().size(); + + assertThat(statistics.entityCount()).isEqualTo(firstEntityCount + secondEntityCount); + assertThat(statistics.variableCount()).isEqualTo(firstEntityCount + secondEntityCount); + assertThat(statistics.approximateValueCount()).isEqualTo(10L); + assertThat(statistics.genuineEntityClassToEntityCount()) + .containsEntry(TestdataConstraintVerifierFirstEntity.class, (long) firstEntityCount); + assertThat(statistics.genuineEntityClassToEntityCount()) + .containsEntry(TestdataConstraintVerifierSecondEntity.class, (long) secondEntityCount); + assertThat(statistics.genuineEntityClassToVariableToValueCount().get(TestdataConstraintVerifierFirstEntity.class)) + .containsEntry("value", 5L); + assertThat(statistics.genuineEntityClassToVariableToValueCount().get(TestdataConstraintVerifierSecondEntity.class)) + .containsEntry("value", 5L); + } + + @Test + void listVariableEntityAndVariableCount() { + var solution = TestdataEqualsByCodeListSolution.generateSolution(4, 6); + var valueRangeManager = ValueRangeManager.of(TestdataEqualsByCodeListSolution.buildSolutionDescriptor(), solution); + var statistics = valueRangeManager.getProblemSizeStatistics(); + + assertThat(statistics.entityCount()).isEqualTo(6L); + assertThat(statistics.variableCount()).isEqualTo(6L); + assertThat(statistics.approximateValueCount()).isEqualTo(4L); + assertThat(statistics.genuineEntityClassToEntityCount()) + .containsExactlyEntriesOf(Map.of(TestdataEqualsByCodeListEntity.class, 6L)); + assertThat(statistics.genuineEntityClassToVariableToValueCount().get(TestdataEqualsByCodeListEntity.class)) + .containsEntry("valueList", 4L); + } + + @Test + void cascadingListVariableEntityAndVariableCount() { + var solution = TestdataSingleCascadingSolution.generateUninitializedSolution(3, 5); + var valueRangeManager = ValueRangeManager.of(TestdataSingleCascadingSolution.buildSolutionDescriptor(), solution); + var statistics = valueRangeManager.getProblemSizeStatistics(); + + assertThat(statistics.entityCount()).isEqualTo(5L); + assertThat(statistics.variableCount()).isEqualTo(5L); + assertThat(statistics.approximateValueCount()).isEqualTo(3L); + assertThat(statistics.genuineEntityClassToEntityCount()) + .containsExactlyEntriesOf(Map.of(TestdataSingleCascadingEntity.class, 5L)); + assertThat(statistics.genuineEntityClassToVariableToValueCount().get(TestdataSingleCascadingEntity.class)) + .containsEntry("valueList", 3L); + } + + @Test + void recordEntityAndVariableCount() { + var solution = TestdataRecordSolution.generateSolution(4, 9); + var valueRangeManager = ValueRangeManager.of(TestdataRecordSolution.buildSolutionDescriptor(), solution); + var statistics = valueRangeManager.getProblemSizeStatistics(); + + assertThat(statistics.entityCount()).isEqualTo(9L); + assertThat(statistics.variableCount()).isEqualTo(9L); + assertThat(statistics.approximateValueCount()).isEqualTo(4L); + assertThat(statistics.genuineEntityClassToEntityCount()) + .containsExactlyEntriesOf(Map.of(TestdataRecordEntity.class, 9L)); + assertThat(statistics.genuineEntityClassToVariableToValueCount().get(TestdataRecordEntity.class)) + .containsEntry("value", 4L); + } + + @Test + void uninitializedSolutionEntityAndVariableCount() { + var solution = TestdataSolution.generateUninitializedSolution(6, 10); + var valueRangeManager = ValueRangeManager.of(TestdataSolution.buildSolutionDescriptor(), solution); + var statistics = valueRangeManager.getProblemSizeStatistics(); + + assertThat(statistics.entityCount()).isEqualTo(10L); + assertThat(statistics.variableCount()).isEqualTo(10L); + assertThat(statistics.approximateValueCount()).isEqualTo(6L); + assertThat(statistics.genuineEntityClassToEntityCount()) + .containsExactlyEntriesOf(Map.of(TestdataEntity.class, 10L)); + assertThat(statistics.genuineEntityClassToVariableToValueCount().get(TestdataEntity.class)) + .containsEntry("value", 6L); + } + + @Test + void singleEntityAndValue() { + var solution = TestdataSolution.generateSolution(1, 1); + var valueRangeManager = ValueRangeManager.of(TestdataSolution.buildSolutionDescriptor(), solution); + var statistics = valueRangeManager.getProblemSizeStatistics(); + + assertThat(statistics.entityCount()).isEqualTo(1L); + assertThat(statistics.variableCount()).isEqualTo(1L); + assertThat(statistics.approximateValueCount()).isEqualTo(1L); + assertThat(statistics.genuineEntityClassToEntityCount()) + .containsExactlyEntriesOf(Map.of(TestdataEntity.class, 1L)); + assertThat(statistics.genuineEntityClassToVariableToValueCount().get(TestdataEntity.class)) + .containsEntry("value", 1L); + } }