Skip to content

Commit

Permalink
feat: constraint weight override (TimefoldAI#946)
Browse files Browse the repository at this point in the history
Planning solution gets an optional field of type `Map` (or more likely a
custom map), with keys being the `constraintName` from
`asConstraint(constraintName)`, and the value being the constraint
weight.
Using the map is optional. Constraint weights not present in the map use
their default constraint weights as defined by `penalize(...)`.
Constraints present in the map but not present in the
`ConstraintProvider` trigger a fail-fast.

`asConstraintDescribed(constraintName, constraintDescription)` is
introduced to keep constraint documentation where it belongs, with the
constraints.

`ConstraintWeight`, `ConstraintConfiguration` and
`penalizeConfigurable(...)` are deprecated. They continue working, but
the new system doesn't use them.

---------

Co-authored-by: lee-carlon <[email protected]>
Co-authored-by: Frederico Gonçalves <[email protected]>
Co-authored-by: Christopher Chianelli <[email protected]>
  • Loading branch information
4 people authored Jul 18, 2024
1 parent cc9630f commit 2c74a11
Show file tree
Hide file tree
Showing 111 changed files with 2,542 additions and 1,823 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package ai.timefold.solver.core.api.domain.autodiscover;

import ai.timefold.solver.core.api.domain.constraintweight.ConstraintConfigurationProvider;
import ai.timefold.solver.core.api.domain.solution.ConstraintWeightOverrides;
import ai.timefold.solver.core.api.domain.solution.PlanningEntityCollectionProperty;
import ai.timefold.solver.core.api.domain.solution.PlanningEntityProperty;
import ai.timefold.solver.core.api.domain.solution.PlanningScore;
Expand All @@ -9,10 +9,10 @@
import ai.timefold.solver.core.api.domain.solution.ProblemFactProperty;

/**
* Determines if and how to automatically presume
* {@link ConstraintConfigurationProvider}, {@link ProblemFactCollectionProperty}, {@link ProblemFactProperty},
* {@link PlanningEntityCollectionProperty}, {@link PlanningEntityProperty} and {@link PlanningScore} annotations
* on {@link PlanningSolution} members based from the member type.
* Determines if and how to automatically presume {@link ConstraintWeightOverrides},
* {@link ProblemFactCollectionProperty}, {@link ProblemFactProperty}, {@link PlanningEntityCollectionProperty},
* {@link PlanningEntityProperty} and {@link PlanningScore} annotations on {@link PlanningSolution} members
* based on the member type.
*/
public enum AutoDiscoverMemberType {
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import ai.timefold.solver.core.api.domain.solution.ConstraintWeightOverrides;
import ai.timefold.solver.core.api.domain.solution.PlanningSolution;

/**
Expand All @@ -15,7 +16,10 @@
* <p>
* A {@link PlanningSolution} has at most one field or property annotated with {@link ConstraintConfigurationProvider}
* with returns a type of the {@link ConstraintConfiguration} annotated class.
*
* @deprecated Use {@link ConstraintWeightOverrides} instead.
*/
@Deprecated(forRemoval = true, since = "1.13.0")
@Target({ TYPE })
@Retention(RUNTIME)
public @interface ConstraintConfiguration {
Expand All @@ -26,9 +30,7 @@
* This is the default for every {@link ConstraintWeight#constraintPackage()} in the annotated class.
*
* @return defaults to the annotated class's package.
* @deprecated Leave empty and let the solver provide the default. Do not rely on constraint package in user code.
*/
@Deprecated(forRemoval = true, since = "1.13.0")
String constraintPackage() default "";

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import ai.timefold.solver.core.api.domain.solution.ConstraintWeightOverrides;
import ai.timefold.solver.core.api.domain.solution.PlanningSolution;
import ai.timefold.solver.core.api.domain.solution.ProblemFactProperty;

Expand All @@ -15,7 +16,10 @@
* This property is automatically a {@link ProblemFactProperty} too, so no need to declare that explicitly.
* <p>
* The type of this property (or field) must have a {@link ConstraintConfiguration} annotation.
*
* @deprecated Use {@link ConstraintWeightOverrides} instead.
*/
@Deprecated(forRemoval = true, since = "1.13.0")
@Target({ METHOD, FIELD })
@Retention(RUNTIME)
public @interface ConstraintConfigurationProvider {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import ai.timefold.solver.core.api.domain.solution.ConstraintWeightOverrides;
import ai.timefold.solver.core.api.score.Score;

/**
Expand All @@ -16,7 +17,10 @@
* will result in a {@link Score} of {@code -6soft}.
* <p>
* It is specified on a getter of a java bean property (or directly on a field) of a {@link ConstraintConfiguration} class.
*
* @deprecated Use {@link ConstraintWeightOverrides} instead.
*/
@Deprecated(forRemoval = true, since = "1.13.0")
@Target({ FIELD, METHOD })
@Retention(RUNTIME)
public @interface ConstraintWeight {
Expand All @@ -28,9 +32,7 @@
* concatenated with "/" and {@link #value() the constraint name}.
*
* @return defaults to {@link ConstraintConfiguration#constraintPackage()}
* @deprecated Leave empty and let the solver provide the default. Do not rely on constraint package in user code.
*/
@Deprecated(forRemoval = true, since = "1.13.0")
String constraintPackage() default "";

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package ai.timefold.solver.core.api.domain.solution;

import java.util.Collections;
import java.util.Map;
import java.util.Set;

import ai.timefold.solver.core.api.score.Score;
import ai.timefold.solver.core.api.score.stream.ConstraintProvider;
import ai.timefold.solver.core.api.score.stream.uni.UniConstraintBuilder;
import ai.timefold.solver.core.api.score.stream.uni.UniConstraintStream;
import ai.timefold.solver.core.api.solver.change.ProblemChange;
import ai.timefold.solver.core.impl.domain.solution.DefaultConstraintWeightOverrides;

/**
* Used to override constraint weights defined in Constraint Streams,
* e.g., in {@link UniConstraintStream#penalize(Score)}.
* To use,
* place a member (typically a field) of type {@link ConstraintWeightOverrides}
* in your {@link PlanningSolution}-annotated class.
* <p>
* Users should use {@link #of(Map)} to provide the actual constraint weights.
* Alternatively, a JSON serializers and deserializer may be defined to interact with a solution file.
* Once the constraint weights are set, they must remain constant throughout the solving process,
* or a {@link ProblemChange} needs to be triggered.
* <p>
* Zero-weight will be excluded from processing,
* and the solver will behave as if it did not exist in the {@link ConstraintProvider}.
* <p>
* There is no support for user-defined packages, which is a deprecated feature in itself.
* The constraint is assumed to be in the same package as the top-most class implementing this interface.
* It is therefore required that the constraints be built using {@link UniConstraintBuilder#asConstraint(String)},
* leaving the constraint package to its default value.
*
* @param <Score_>
*/
public interface ConstraintWeightOverrides<Score_ extends Score<Score_>> {

static <Score_ extends Score<Score_>> ConstraintWeightOverrides<Score_> none() {
return of(Collections.<String, Score_> emptyMap());
}

static <Score_ extends Score<Score_>> ConstraintWeightOverrides<Score_> of(Map<String, Score_> constraintWeightMap) {
return new DefaultConstraintWeightOverrides<>(constraintWeightMap);
}

/**
* Return a constraint weight for a particular constraint.
*
* @param constraintName never null
* @return null if the constraint name is not known
*/
Score_ getConstraintWeight(String constraintName);

/**
* Returns all known constraints.
*
* @return All constraint names for which {@link #getConstraintWeight(String)} returns a non-null value.
*/
Set<String> getKnownConstraintNames();

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
import java.lang.annotation.Target;

import ai.timefold.solver.core.api.domain.autodiscover.AutoDiscoverMemberType;
import ai.timefold.solver.core.api.domain.constraintweight.ConstraintConfigurationProvider;
import ai.timefold.solver.core.api.domain.lookup.LookUpStrategyType;
import ai.timefold.solver.core.api.domain.lookup.PlanningId;
import ai.timefold.solver.core.api.domain.solution.cloner.SolutionCloner;
import ai.timefold.solver.core.api.score.stream.ConstraintProvider;

/**
* Specifies that the class is a planning solution.
Expand All @@ -28,7 +28,9 @@
* Each planning solution must have at least 1 {@link PlanningEntityCollectionProperty}
* or {@link PlanningEntityProperty} property.
* <p>
* Each planning solution is recommended to have 1 {@link ConstraintConfigurationProvider} property too.
* Each planning solution is recommended to have 1 {@link ConstraintWeightOverrides} property too.
* This will make it easy for a solution to override constraint weights provided in {@link ConstraintProvider},
* in turn making it possible to run different solutions with a different balance of constraint weights.
* <p>
* Each planning solution used with ConstraintStream score calculation must have at least 1
* {@link ProblemFactCollectionProperty}
Expand All @@ -45,7 +47,7 @@
* Enable reflection through the members of the class
* to automatically assume {@link PlanningScore}, {@link PlanningEntityCollectionProperty},
* {@link PlanningEntityProperty}, {@link ProblemFactCollectionProperty}, {@link ProblemFactProperty}
* and {@link ConstraintConfigurationProvider} annotations based on the member type.
* and {@link ConstraintWeightOverrides} annotations based on the member type.
*
* <p>
* This feature is not supported under Quarkus.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import ai.timefold.solver.core.api.domain.constraintweight.ConstraintConfiguration;
import ai.timefold.solver.core.api.domain.entity.PlanningEntity;
import ai.timefold.solver.core.api.score.stream.ConstraintFactory;
import ai.timefold.solver.core.api.score.stream.ConstraintProvider;
Expand All @@ -19,8 +18,8 @@
* <p>
* The constraints in a {@link ConstraintProvider} rely on problem facts for {@link ConstraintFactory#forEach(Class)}.
* <p>
* Do not annotate a {@link PlanningEntity planning entity} or a {@link ConstraintConfiguration planning paramerization}
* as a problem fact: they are automatically available as facts for {@link ConstraintFactory#forEach(Class)}.
* Do not annotate {@link PlanningEntity} or {@link ConstraintWeightOverrides} fields as a problem fact:
* they are automatically available as facts for {@link ConstraintFactory#forEach(Class)}.
*
* @see ProblemFactCollectionProperty
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

import java.util.Set;

import ai.timefold.solver.core.api.domain.constraintweight.ConstraintConfiguration;
import ai.timefold.solver.core.api.domain.constraintweight.ConstraintWeight;
import ai.timefold.solver.core.api.domain.solution.PlanningSolution;
import ai.timefold.solver.core.api.domain.variable.PlanningVariable;
import ai.timefold.solver.core.api.score.Score;
Expand Down Expand Up @@ -56,11 +54,11 @@ default String getConstraintName() {
}

/**
* The value of the {@link ConstraintWeight} annotated member of the {@link ConstraintConfiguration}.
* It's independent to the state of the {@link PlanningVariable planning variables}.
* The effective value of constraint weight after applying optional overrides.
* It is independent to the state of the {@link PlanningVariable planning variables}.
* Do not confuse with {@link #getScore()}.
*
* @return null if {@link ConstraintWeight} isn't used for this constraint
* @return never null
*/
Score_ getConstraintWeight();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ public interface Constraint {

ConstraintRef getConstraintRef();

/**
* Returns a human-friendly description of the constraint.
* The format of the description is left unspecified and will not be parsed in any way.
*
* @return never null, may be left empty
*/
default String getDescription() {
return "";
}

/**
* @deprecated Prefer {@link #getConstraintRef()}.
* @return never null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,19 @@ public interface ConstraintBuilder {
* @param constraintName never null, shows up in {@link ConstraintMatchTotal} during score justification
* @return never null
*/
Constraint asConstraint(String constraintName);
default Constraint asConstraint(String constraintName) {
return asConstraintDescribed(constraintName, "");
}

/**
* Builds a {@link Constraint} from the constraint stream.
* The {@link ConstraintRef#packageName() constraint package} defaults to the package of the {@link PlanningSolution} class.
*
* @param constraintName never null, shows up in {@link ConstraintMatchTotal} during score justification
* @param constraintDescription never null
* @return never null
*/
Constraint asConstraintDescribed(String constraintName, String constraintDescription);

/**
* Builds a {@link Constraint} from the constraint stream.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@
import java.util.function.Function;
import java.util.function.Predicate;

import ai.timefold.solver.core.api.domain.constraintweight.ConstraintConfiguration;
import ai.timefold.solver.core.api.domain.entity.PlanningEntity;
import ai.timefold.solver.core.api.domain.lookup.PlanningId;
import ai.timefold.solver.core.api.domain.solution.PlanningSolution;
import ai.timefold.solver.core.api.domain.solution.ProblemFactCollectionProperty;
import ai.timefold.solver.core.api.domain.variable.InverseRelationShadowVariable;
import ai.timefold.solver.core.api.domain.variable.PlanningListVariable;
Expand All @@ -23,9 +21,6 @@
public interface ConstraintFactory {

/**
* This is {@link ConstraintConfiguration#constraintPackage()} if available,
* otherwise the package of the {@link PlanningSolution} class.
*
* @return never null
* @deprecated Do not rely on any constraint package in user code.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import ai.timefold.solver.core.api.domain.constraintweight.ConstraintConfiguration;
import ai.timefold.solver.core.api.domain.constraintweight.ConstraintWeight;
import ai.timefold.solver.core.api.domain.entity.PlanningEntity;
import ai.timefold.solver.core.api.domain.solution.ConstraintWeightOverrides;
import ai.timefold.solver.core.api.domain.solution.PlanningSolution;
import ai.timefold.solver.core.api.domain.solution.ProblemFactCollectionProperty;
import ai.timefold.solver.core.api.domain.solution.ProblemFactProperty;
Expand Down Expand Up @@ -114,25 +115,24 @@ public interface ConstraintStream {
* The constraintWeight comes from an {@link ConstraintWeight} annotated member on the {@link ConstraintConfiguration},
* so end users can change the constraint weights dynamically.
* This constraint may be deactivated if the {@link ConstraintWeight} is zero.
* If there is no {@link ConstraintConfiguration}, use {@link #penalize(String, Score)} instead.
* <p>
* The {@link ConstraintRef#packageName() constraint package} defaults to
* {@link ConstraintConfiguration#constraintPackage()}.
*
* @deprecated Prefer {@link UniConstraintStream#penalizeConfigurable()} and equivalent bi/tri/... overloads.
* @param constraintName never null, shows up in {@link ConstraintMatchTotal} during score justification
* @return never null
* @deprecated Prefer {@code penalize()} and {@link ConstraintWeightOverrides}.
*/
@Deprecated(forRemoval = true)
Constraint penalizeConfigurable(String constraintName);

/**
* As defined by {@link #penalizeConfigurable(String)}.
*
* @deprecated Prefer {@link UniConstraintStream#penalizeConfigurable()} and equivalent bi/tri/... overloads.
* @param constraintPackage never null
* @param constraintName never null
* @return never null
* @deprecated Prefer {@code penalize()} and {@link ConstraintWeightOverrides}.
*/
@Deprecated(forRemoval = true)
Constraint penalizeConfigurable(String constraintPackage, String constraintName);
Expand Down Expand Up @@ -171,25 +171,24 @@ public interface ConstraintStream {
* The constraintWeight comes from an {@link ConstraintWeight} annotated member on the {@link ConstraintConfiguration},
* so end users can change the constraint weights dynamically.
* This constraint may be deactivated if the {@link ConstraintWeight} is zero.
* If there is no {@link ConstraintConfiguration}, use {@link #reward(String, Score)} instead.
* <p>
* The {@link ConstraintRef#packageName() constraint package} defaults to
* {@link ConstraintConfiguration#constraintPackage()}.
*
* @deprecated Prefer {@link UniConstraintStream#rewardConfigurable()} and equivalent bi/tri/... overloads.
* @param constraintName never null, shows up in {@link ConstraintMatchTotal} during score justification
* @return never null
* @deprecated Prefer {@code reward()} and {@link ConstraintWeightOverrides}.
*/
@Deprecated(forRemoval = true)
Constraint rewardConfigurable(String constraintName);

/**
* As defined by {@link #rewardConfigurable(String)}.
*
* @deprecated Prefer {@link UniConstraintStream#rewardConfigurable()} and equivalent bi/tri/... overloads.
* @param constraintPackage never null
* @param constraintName never null
* @return never null
* @deprecated Prefer {@code reward()} and {@link ConstraintWeightOverrides}.
*/
@Deprecated(forRemoval = true)
Constraint rewardConfigurable(String constraintPackage, String constraintName);
Expand Down
Loading

0 comments on commit 2c74a11

Please sign in to comment.