Skip to content

chore: create filtering value range selectors as early as possible #1738

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Aug 20, 2025
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessor;
import ai.timefold.solver.core.impl.domain.policy.DescriptorPolicy;
import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor;
import ai.timefold.solver.core.impl.domain.valuerange.descriptor.CompositeValueRangeDescriptor;
import ai.timefold.solver.core.impl.domain.variable.anchor.AnchorShadowVariableDescriptor;
import ai.timefold.solver.core.impl.domain.variable.cascade.CascadingUpdateShadowVariableDescriptor;
import ai.timefold.solver.core.impl.domain.variable.custom.CustomShadowVariableDescriptor;
Expand Down Expand Up @@ -321,8 +322,7 @@ private void processValueRangeProviderAnnotation(DescriptorPolicy descriptorPoli
if (((AnnotatedElement) member).isAnnotationPresent(ValueRangeProvider.class)) {
var memberAccessor = descriptorPolicy.getMemberAccessorFactory().buildAndCacheMemberAccessor(member,
FIELD_OR_READ_METHOD, ValueRangeProvider.class, descriptorPolicy.getDomainAccessType());
descriptorPolicy.addFromEntityValueRangeProvider(
memberAccessor);
descriptorPolicy.addFromEntityValueRangeProvider(memberAccessor);
}
}

Expand Down Expand Up @@ -740,6 +740,20 @@ public long getGenuineVariableCount() {
return effectiveGenuineVariableDescriptorList.size();
}

public int getValueRangeCount() {
var count = 0;
for (var genuineVariableDescriptor : effectiveGenuineVariableDescriptorList) {
if (genuineVariableDescriptor
.getValueRangeDescriptor() instanceof CompositeValueRangeDescriptor<Solution_> compositeValueRangeDescriptor) {
// sum the child descriptors size
// and add one more unit to the composite descriptor itself at the end of the iter
count += compositeValueRangeDescriptor.getValueRangeCount();
}
count++;
}
return count;
}

public Collection<ShadowVariableDescriptor<Solution_>> getShadowVariableDescriptors() {
return effectiveShadowVariableDescriptorMap.values();
}
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,43 +1,14 @@
package ai.timefold.solver.core.impl.domain.score.descriptor;

import static ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorFactory.MemberAccessorType.FIELD_OR_GETTER_METHOD_WITH_SETTER;

import java.lang.reflect.Member;
import java.util.Objects;

import ai.timefold.solver.core.api.domain.solution.PlanningScore;
import ai.timefold.solver.core.api.score.IBendableScore;
import ai.timefold.solver.core.api.score.Score;
import ai.timefold.solver.core.api.score.buildin.bendable.BendableScore;
import ai.timefold.solver.core.api.score.buildin.bendablebigdecimal.BendableBigDecimalScore;
import ai.timefold.solver.core.api.score.buildin.bendablelong.BendableLongScore;
import ai.timefold.solver.core.api.score.buildin.hardmediumsoft.HardMediumSoftScore;
import ai.timefold.solver.core.api.score.buildin.hardmediumsoftbigdecimal.HardMediumSoftBigDecimalScore;
import ai.timefold.solver.core.api.score.buildin.hardmediumsoftlong.HardMediumSoftLongScore;
import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore;
import ai.timefold.solver.core.api.score.buildin.hardsoftbigdecimal.HardSoftBigDecimalScore;
import ai.timefold.solver.core.api.score.buildin.hardsoftlong.HardSoftLongScore;
import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore;
import ai.timefold.solver.core.api.score.buildin.simplebigdecimal.SimpleBigDecimalScore;
import ai.timefold.solver.core.api.score.buildin.simplelong.SimpleLongScore;
import ai.timefold.solver.core.config.util.ConfigUtils;
import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessor;
import ai.timefold.solver.core.impl.domain.policy.DescriptorPolicy;
import ai.timefold.solver.core.impl.score.buildin.BendableBigDecimalScoreDefinition;
import ai.timefold.solver.core.impl.score.buildin.BendableLongScoreDefinition;
import ai.timefold.solver.core.impl.score.buildin.BendableScoreDefinition;
import ai.timefold.solver.core.impl.score.buildin.HardMediumSoftBigDecimalScoreDefinition;
import ai.timefold.solver.core.impl.score.buildin.HardMediumSoftLongScoreDefinition;
import ai.timefold.solver.core.impl.score.buildin.HardMediumSoftScoreDefinition;
import ai.timefold.solver.core.impl.score.buildin.HardSoftBigDecimalScoreDefinition;
import ai.timefold.solver.core.impl.score.buildin.HardSoftLongScoreDefinition;
import ai.timefold.solver.core.impl.score.buildin.HardSoftScoreDefinition;
import ai.timefold.solver.core.impl.score.buildin.SimpleBigDecimalScoreDefinition;
import ai.timefold.solver.core.impl.score.buildin.SimpleLongScoreDefinition;
import ai.timefold.solver.core.impl.score.buildin.SimpleScoreDefinition;
import ai.timefold.solver.core.impl.score.definition.ScoreDefinition;

public class ScoreDescriptor<Score_ extends Score<Score_>> {
public final class ScoreDescriptor<Score_ extends Score<Score_>> {

// Used to obtain default @PlanningScore attribute values from a score member that was auto-discovered,
// as if it had an empty @PlanningScore annotation on it.
Expand All @@ -47,150 +18,11 @@ public class ScoreDescriptor<Score_ extends Score<Score_>> {
private final MemberAccessor scoreMemberAccessor;
private final ScoreDefinition<Score_> scoreDefinition;

private ScoreDescriptor(MemberAccessor scoreMemberAccessor, ScoreDefinition<Score_> scoreDefinition) {
public ScoreDescriptor(MemberAccessor scoreMemberAccessor, ScoreDefinition<Score_> scoreDefinition) {
this.scoreMemberAccessor = scoreMemberAccessor;
this.scoreDefinition = scoreDefinition;
}

public static <Score_ extends Score<Score_>> ScoreDescriptor<Score_> buildScoreDescriptor(DescriptorPolicy descriptorPolicy,
Member member, Class<?> solutionClass) {
MemberAccessor scoreMemberAccessor = buildScoreMemberAccessor(descriptorPolicy, member);
Class<Score_> scoreType = extractScoreType(scoreMemberAccessor, solutionClass);
PlanningScore annotation = extractPlanningScoreAnnotation(scoreMemberAccessor);
ScoreDefinition<Score_> scoreDefinition =
buildScoreDefinition(solutionClass, scoreMemberAccessor, scoreType, annotation);
return new ScoreDescriptor<>(scoreMemberAccessor, scoreDefinition);
}

private static MemberAccessor buildScoreMemberAccessor(DescriptorPolicy descriptorPolicy, Member member) {
return descriptorPolicy.getMemberAccessorFactory().buildAndCacheMemberAccessor(
member,
FIELD_OR_GETTER_METHOD_WITH_SETTER,
PlanningScore.class,
descriptorPolicy.getDomainAccessType());
}

@SuppressWarnings("unchecked")
private static <Score_ extends Score<Score_>> Class<Score_> extractScoreType(MemberAccessor scoreMemberAccessor,
Class<?> solutionClass) {
Class<?> memberType = scoreMemberAccessor.getType();
if (!Score.class.isAssignableFrom(memberType)) {
throw new IllegalStateException("The solutionClass (" + solutionClass
+ ") has a @" + PlanningScore.class.getSimpleName()
+ " annotated member (" + scoreMemberAccessor + ") that does not return a subtype of Score.");
}
if (memberType == Score.class) {
throw new IllegalStateException("The solutionClass (" + solutionClass
+ ") has a @" + PlanningScore.class.getSimpleName()
+ " annotated member (" + scoreMemberAccessor
+ ") that doesn't return a non-abstract " + Score.class.getSimpleName() + " class.\n"
+ "Maybe make it return " + HardSoftScore.class.getSimpleName()
+ " or another specific " + Score.class.getSimpleName() + " implementation.");
}
return (Class<Score_>) memberType;
}

private static PlanningScore extractPlanningScoreAnnotation(MemberAccessor scoreMemberAccessor) {
PlanningScore annotation = scoreMemberAccessor.getAnnotation(PlanningScore.class);
if (annotation != null) {
return annotation;
}
// The member was auto-discovered.
try {
return ScoreDescriptor.class.getDeclaredField("PLANNING_SCORE").getAnnotation(PlanningScore.class);
} catch (NoSuchFieldException e) {
throw new IllegalStateException("Impossible situation: the field (PLANNING_SCORE) must exist.", e);
}
}

@SuppressWarnings("unchecked")
private static <Score_ extends Score<Score_>, ScoreDefinition_ extends ScoreDefinition<Score_>> ScoreDefinition_
buildScoreDefinition(Class<?> solutionClass,
MemberAccessor scoreMemberAccessor, Class<Score_> scoreType, PlanningScore annotation) {
Class<ScoreDefinition_> scoreDefinitionClass = (Class<ScoreDefinition_>) annotation.scoreDefinitionClass();
int bendableHardLevelsSize = annotation.bendableHardLevelsSize();
int bendableSoftLevelsSize = annotation.bendableSoftLevelsSize();
if (!Objects.equals(scoreDefinitionClass, PlanningScore.NullScoreDefinition.class)) {
if (bendableHardLevelsSize != PlanningScore.NO_LEVEL_SIZE
|| bendableSoftLevelsSize != PlanningScore.NO_LEVEL_SIZE) {
throw new IllegalArgumentException("The solutionClass (" + solutionClass
+ ") has a @" + PlanningScore.class.getSimpleName()
+ " annotated member (" + scoreMemberAccessor
+ ") that has a scoreDefinition (" + scoreDefinitionClass
+ ") that must not have a bendableHardLevelsSize (" + bendableHardLevelsSize
+ ") or a bendableSoftLevelsSize (" + bendableSoftLevelsSize + ").");
}
return ConfigUtils.newInstance(() -> scoreMemberAccessor + " with @" + PlanningScore.class.getSimpleName(),
"scoreDefinitionClass", scoreDefinitionClass);
}
if (!IBendableScore.class.isAssignableFrom(scoreType)) {
if (bendableHardLevelsSize != PlanningScore.NO_LEVEL_SIZE
|| bendableSoftLevelsSize != PlanningScore.NO_LEVEL_SIZE) {
throw new IllegalArgumentException("The solutionClass (" + solutionClass
+ ") has a @" + PlanningScore.class.getSimpleName()
+ " annotated member (" + scoreMemberAccessor
+ ") that returns a scoreType (" + scoreType
+ ") that must not have a bendableHardLevelsSize (" + bendableHardLevelsSize
+ ") or a bendableSoftLevelsSize (" + bendableSoftLevelsSize + ").");
}
if (scoreType.equals(SimpleScore.class)) {
return (ScoreDefinition_) new SimpleScoreDefinition();
} else if (scoreType.equals(SimpleLongScore.class)) {
return (ScoreDefinition_) new SimpleLongScoreDefinition();
} else if (scoreType.equals(SimpleBigDecimalScore.class)) {
return (ScoreDefinition_) new SimpleBigDecimalScoreDefinition();
} else if (scoreType.equals(HardSoftScore.class)) {
return (ScoreDefinition_) new HardSoftScoreDefinition();
} else if (scoreType.equals(HardSoftLongScore.class)) {
return (ScoreDefinition_) new HardSoftLongScoreDefinition();
} else if (scoreType.equals(HardSoftBigDecimalScore.class)) {
return (ScoreDefinition_) new HardSoftBigDecimalScoreDefinition();
} else if (scoreType.equals(HardMediumSoftScore.class)) {
return (ScoreDefinition_) new HardMediumSoftScoreDefinition();
} else if (scoreType.equals(HardMediumSoftLongScore.class)) {
return (ScoreDefinition_) new HardMediumSoftLongScoreDefinition();
} else if (scoreType.equals(HardMediumSoftBigDecimalScore.class)) {
return (ScoreDefinition_) new HardMediumSoftBigDecimalScoreDefinition();
} else {
throw new IllegalArgumentException("The solutionClass (" + solutionClass
+ ") has a @" + PlanningScore.class.getSimpleName()
+ " annotated member (" + scoreMemberAccessor
+ ") that returns a scoreType (" + scoreType
+ ") that is not recognized as a default " + Score.class.getSimpleName() + " implementation.\n"
+ " If you intend to use a custom implementation,"
+ " maybe set a scoreDefinition in the @" + PlanningScore.class.getSimpleName()
+ " annotation.");
}
} else {
if (bendableHardLevelsSize == PlanningScore.NO_LEVEL_SIZE
|| bendableSoftLevelsSize == PlanningScore.NO_LEVEL_SIZE) {
throw new IllegalArgumentException("The solutionClass (" + solutionClass
+ ") has a @" + PlanningScore.class.getSimpleName()
+ " annotated member (" + scoreMemberAccessor
+ ") that returns a scoreType (" + scoreType
+ ") that must have a bendableHardLevelsSize (" + bendableHardLevelsSize
+ ") and a bendableSoftLevelsSize (" + bendableSoftLevelsSize + ").");
}
if (scoreType.equals(BendableScore.class)) {
return (ScoreDefinition_) new BendableScoreDefinition(bendableHardLevelsSize, bendableSoftLevelsSize);
} else if (scoreType.equals(BendableLongScore.class)) {
return (ScoreDefinition_) new BendableLongScoreDefinition(bendableHardLevelsSize,
bendableSoftLevelsSize);
} else if (scoreType.equals(BendableBigDecimalScore.class)) {
return (ScoreDefinition_) new BendableBigDecimalScoreDefinition(bendableHardLevelsSize,
bendableSoftLevelsSize);
} else {
throw new IllegalArgumentException("The solutionClass (" + solutionClass
+ ") has a @" + PlanningScore.class.getSimpleName()
+ " annotated member (" + scoreMemberAccessor
+ ") that returns a bendable scoreType (" + scoreType
+ ") that is not recognized as a default " + Score.class.getSimpleName() + " implementation.\n"
+ " If you intend to use a custom implementation,"
+ " maybe set a scoreDefinition in the annotation.");
}
}
}

public ScoreDefinition<Score_> getScoreDefinition() {
return scoreDefinition;
}
Expand All @@ -209,7 +41,7 @@ public void setScore(Object solution, Score_ score) {
}

public void failFastOnDuplicateMember(DescriptorPolicy descriptorPolicy, Member member, Class<?> solutionClass) {
MemberAccessor memberAccessor = buildScoreMemberAccessor(descriptorPolicy, member);
MemberAccessor memberAccessor = descriptorPolicy.buildScoreMemberAccessor(member);
// A solution class cannot have more than one score field or bean property (name check), and the @PlanningScore
// annotation cannot appear on both the score field and its getter (member accessor class check).
if (!scoreMemberAccessor.getName().equals(memberAccessor.getName())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,14 @@
import org.slf4j.LoggerFactory;

/**
* @param <Solution_> the solution type, the class with the {@link ai.timefold.solver.core.api.domain.solution.PlanningSolution}
* @param <Solution_> the solution type, the class with the {@link PlanningSolution}
* annotation
*/
public class SolutionDescriptor<Solution_> {
public final class SolutionDescriptor<Solution_> {

private static final Logger LOGGER = LoggerFactory.getLogger(SolutionDescriptor.class);
private static final EntityDescriptor<?> NULL_ENTITY_DESCRIPTOR = new EntityDescriptor<>(-1, null, PlanningEntity.class);
protected static final Class[] ANNOTATED_MEMBERS_CLASSES = {
private static final Class[] ANNOTATED_MEMBERS_CLASSES = {
ProblemFactCollectionProperty.class,
ValueRangeProvider.class,
PlanningEntityCollectionProperty.class,
Expand Down Expand Up @@ -143,7 +143,6 @@ public static <Solution_> SolutionDescriptor<Solution_> buildSolutionDescriptor(

solutionDescriptor.processUnannotatedFieldsAndMethods(descriptorPolicy);
solutionDescriptor.processAnnotations(descriptorPolicy, entityClassList);
var ordinal = 0;
// Before iterating over the entity classes, we need to read the inheritance chain,
// add all parent and child classes, and sort them.
var updatedEntityClassList = new ArrayList<>(entityClassList);
Expand All @@ -154,8 +153,7 @@ public static <Solution_> SolutionDescriptor<Solution_> buildSolutionDescriptor(
updatedEntityClassList.addAll(filteredInheritedEntityClasses);
}
for (var entityClass : sortEntityClassList(updatedEntityClassList)) {
var entityDescriptor = new EntityDescriptor<>(ordinal++, solutionDescriptor, entityClass);
solutionDescriptor.addEntityDescriptor(entityDescriptor);
var entityDescriptor = descriptorPolicy.buildEntityDescriptor(solutionDescriptor, entityClass);
entityDescriptor.processAnnotations(descriptorPolicy);
}
solutionDescriptor.afterAnnotationsProcessed(descriptorPolicy);
Expand Down Expand Up @@ -451,7 +449,7 @@ private void processFactEntityOrScoreAnnotation(DescriptorPolicy descriptorPolic
} else if (annotationClass.equals(PlanningScore.class)) {
if (scoreDescriptor == null) {
// Bottom class wins. Bottom classes are parsed first due to ConfigUtil.getAllAnnotatedLineageClasses().
scoreDescriptor = ScoreDescriptor.buildScoreDescriptor(descriptorPolicy, member, solutionClass);
scoreDescriptor = descriptorPolicy.buildScoreDescriptor(member, solutionClass);
} else {
scoreDescriptor.failFastOnDuplicateMember(descriptorPolicy, member, solutionClass);
}
Expand Down Expand Up @@ -1213,6 +1211,14 @@ public List<ShadowVariableDescriptor<Solution_>> getAllShadowVariableDescriptors
return out;
}

public int getValueRangeDescriptorCount() {
var count = 0;
for (var entityDescriptor : entityDescriptorMap.values()) {
count += entityDescriptor.getValueRangeCount();
}
return count;
}

public List<DeclarativeShadowVariableDescriptor<Solution_>> getDeclarativeShadowVariableDescriptors() {
var out = new HashSet<DeclarativeShadowVariableDescriptor<Solution_>>();
for (var entityDescriptor : entityDescriptorMap.values()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ public abstract non-sealed class AbstractFromPropertyValueRangeDescriptor<Soluti
// Field related to the generic type of the value range, e.g., List<String> -> String
private final boolean isGenericTypeImmutable;

protected AbstractFromPropertyValueRangeDescriptor(GenuineVariableDescriptor<Solution_> variableDescriptor,
protected AbstractFromPropertyValueRangeDescriptor(int ordinalId, GenuineVariableDescriptor<Solution_> variableDescriptor,
MemberAccessor memberAccessor) {
super(variableDescriptor);
super(ordinalId, variableDescriptor);
this.memberAccessor = memberAccessor;
ValueRangeProvider valueRangeProviderAnnotation = memberAccessor.getAnnotation(ValueRangeProvider.class);
if (valueRangeProviderAnnotation == null) {
Expand Down
Loading
Loading