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
7 changes: 7 additions & 0 deletions core/src/build/revapi-differences.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@
"old": "field ai.timefold.solver.core.config.heuristic.selector.move.generic.AbstractPillarMoveSelectorConfig<Config_ extends ai.timefold.solver.core.config.heuristic.selector.move.generic.AbstractPillarMoveSelectorConfig<Config_>>.subPillarSequenceComparatorClass",
"new": "field ai.timefold.solver.core.config.heuristic.selector.move.generic.AbstractPillarMoveSelectorConfig<Config_ extends ai.timefold.solver.core.config.heuristic.selector.move.generic.AbstractPillarMoveSelectorConfig<Config_>>.subPillarSequenceComparatorClass",
"justification": "Internal protected fields; safe."
},
{
"ignore": true,
"code": "java.method.numberOfParametersChanged",
"old": "method void ai.timefold.solver.core.api.solver.ProblemSizeStatistics::<init>(long, long, long, double)",
"new": "method void ai.timefold.solver.core.api.solver.ProblemSizeStatistics::<init>(long, java.util.SequencedMap<java.lang.Class<?>, java.lang.Long>, long, long, java.util.SequencedMap<java.lang.Class<?>, java.util.SequencedMap<java.lang.String, java.lang.Long>>, double)",
"justification": "Type is not supposed to be constructed by user; safe."
}
]
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -17,8 +18,10 @@
*/
@NullMarked
public record ProblemSizeStatistics(long entityCount,

@triceo triceo Jul 31, 2026

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.

Seeing as this is a public API, we should add a backwards-compatible constructor, and deprecate it.
Maybe even a migration script, this one will be easy.

SequencedMap<Class<?>, Long> genuineEntityClassToEntityCount,
long variableCount,
long approximateValueCount,
SequencedMap<Class<?>, SequencedMap<String, Long>> genuineEntityClassToVariableToValueCount,
double approximateProblemSizeLog) {

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -814,4 +814,7 @@ public String toString() {
return "%s(%s)".formatted(getClass().getSimpleName(), entityClass.getCanonicalName());
}

public int getMaxVariableOrdinal() {
return effectiveVariableDescriptorMap.size();
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -28,6 +30,8 @@

// 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<Solution_> valueRangeManager, SolutionDescriptor<Solution_> solutionDescriptor,
Expand Down Expand Up @@ -61,6 +65,18 @@
var approximateValueCount = new MutableLong();
var maxValueRangeSize = new MutableLong(0L);

cachedEntityCountByEntityOrdinal = new long[solutionDescriptor.getEntityDescriptors().size()];
cachedValueCountByEntityAndVariableOrdinal = new long[cachedEntityCountByEntityOrdinal.length][];
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();
if (listVariableDescriptor != null) {
var countOnSolution =
Expand All @@ -69,6 +85,8 @@
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.
Expand All @@ -81,6 +99,8 @@
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);
}
Expand All @@ -94,6 +114,9 @@
var entityDescriptor = solutionDescriptor.findEntityDescriptorOrFail(entity.getClass());
if (entityDescriptor.isGenuine()) {
genuineEntityCount.increment();
// Should we include immovable entities for the count?
// Problem scale ignores them.
Comment on lines +117 to +118

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.

That's a good question.
IMO we should include them in entity count, but they cannot affect the problem scale, so they don't.

Maybe we log that the entity count includes immovable as well? Not sure. We should definitely log not just "entity count" but "genuine entity count".

cachedEntityCountByEntityOrdinal[entityDescriptor.getOrdinal()]++;
var uninitializedVariableCountForEntity = entityDescriptor.countUninitializedVariables(entity);
if (uninitializedVariableCountForEntity > 0) {
uninitializedEntityCount.increment();
Expand All @@ -110,8 +133,11 @@
for (var genuineVariable : entityDescriptor.getGenuineVariableDescriptorList()) {
if (genuineVariable instanceof BasicVariableDescriptor<Solution_> 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()) {
Expand All @@ -124,8 +150,10 @@
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
});
Expand All @@ -152,10 +180,30 @@
computeInitializationStatistics(null, false);
}
if (cachedProblemSizeStatistics == null) {
var entityClassToEntityCount = new LinkedHashMap<Class<?>, Long>();
var entityClassToVariableToValueCount = new LinkedHashMap<Class<?>, SequencedMap<String, Long>>();
for (var entityDescriptor : solutionDescriptor.getGenuineEntityDescriptors()) {

Check failure on line 185 in core/src/main/java/ai/timefold/solver/core/impl/score/director/ValueRangeStatistics.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Fix this access that may throw a NullPointerException when executed.

See more on https://sonarcloud.io/project/issues?id=ai.timefold%3Atimefold-solver&issues=AZ-2sP2qmOqCDBUmc9kv&open=AZ-2sP2qmOqCDBUmc9kv&pullRequest=2549
entityClassToEntityCount.put(entityDescriptor.getEntityClass(),
cachedEntityCountByEntityOrdinal[entityDescriptor.getOrdinal()]);
var variableToValueCount = new LinkedHashMap<String, Long>();
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,22 @@ public void solvingStarted(SolverScope<Solution_> 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: {}",

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.

LOGGER

geninueEntityClass.getCanonicalName(),
genuineEntityCountEntry.getValue());
for (var geninueVariableEntry : problemSizeStatistics
.genuineEntityClassToVariableToValueCount()
.get(geninueEntityClass).entrySet()) {
var genuineVariable = geninueVariableEntry.getKey();
logger.debug(" Variable ({}) estimated value count: {}",

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.

Maybe we also want to show the variable's value type? Not sure.
I would certainly mention if basic/list.

genuineVariable, geninueVariableEntry.getValue());
}
}
}
}
}

Expand Down
Loading
Loading