Constraints and Score
1. Score terminology
1.1. What is a score?
Every @PlanningSolution
class has a score.
The score is an objective way to compare two solutions.
The solution with the higher score is better.
The Solver
aims to find the solution with the highest Score
of all possible solutions.
The best solution is the solution with the highest Score
that Solver
has encountered during solving,
which might be the optimal solution.
Timefold Solver cannot automatically know which solution is best for your business,
so you need to tell it how to calculate the score of a given @PlanningSolution
instance according to your business needs.
If you forget or are unable to implement an important business constraint, the solution is probably useless:

1.2. Formalize the business constraints
To implement a verbal business constraint, it needs to be formalized as a score constraint. Luckily, defining constraints in Timefold Solver is very flexible through the following score techniques:
-
Score signum (positive or negative): maximize or minimize a constraint type
-
Score weight: put a cost/profit on a constraint type
-
Score level (hard, soft, …): prioritize a group of constraint types
-
Pareto scoring (rarely used)
Take the time to acquaint yourself with the first three techniques. Once you understand them, formalizing most business constraints becomes straightforward.
Do not presume that your business knows all its score constraints in advance. Expect score constraints to be added, changed or removed after the first releases. |
1.3. Score constraint signum (positive or negative)
All score techniques are based on constraints. A constraint can be a simple pattern (such as Maximize the apple harvest in the solution) or a more complex pattern. A positive constraint is a constraint you want to maximize. A negative constraint is a constraint you want to minimize

The image above illustrates that the optimal solution always has the highest score, regardless if the constraints are positive or negative.
Most planning problems have only negative constraints and therefore have a negative score. In that case, the score is the sum of the weight of the negative constraints being broken, with a perfect score of 0. For example in n queens, the score is the negative of the number of queen pairs which can attack each other.
Negative and positive constraints can be combined, even in the same score level.
When a constraint activates (because the negative constraint is broken or the positive constraint is fulfilled) on a certain planning entity set, it is called a constraint match.
1.4. Score constraint weight
Not all score constraints are equally important. If breaking one constraint is equally bad as breaking another constraint x times, then those two constraints have a different weight (but they are in the same score level). For example in vehicle routing, you can make one unhappy driver constraint match count as much as two fuel tank usage constraint matches:

Score weighting is easy in use cases where you can put a price tag on everything. In that case, the positive constraints maximize revenue and the negative constraints minimize expenses, so together they maximize profit. Alternatively, score weighting is also often used to create social fairness. For example, a nurse, who requests a free day, pays a higher weight on New Years eve than on a normal day.
The weight of a constraint match can depend on the planning entities involved.
For example in cloud balancing, the weight of the soft constraint match for an active Computer
is the maintenance cost
of that Computer
(which differs per computer).
Putting a good weight on a constraint is often a difficult analytical decision, because it is about making choices and trade-offs against other constraints. Different stakeholders have different priorities. Don’t waste time with constraint weight discussions at the start of an implementation, instead add a constraint configuration and allow users to change them through a UI. A non-accurate weight is less damaging than mediocre algorithms:

Most use cases use a Score
with int
weights, such as HardSoftScore.
1.5. Score constraint level (hard, soft, …)
Sometimes a score constraint outranks another score constraint, no matter how many times the latter is broken. In that case, those score constraints are in different levels. For example, a nurse cannot do two shifts at the same time (due to the constraints of physical reality), so this outranks all nurse happiness constraints.
Most use cases have only two score levels, hard and soft.
The levels of two scores are compared lexicographically.
The first score level gets compared first.
If those differ, the remaining score levels are ignored.
For example, a score that breaks 0
hard constraints and 1000000
soft constraints is better
than a score that breaks 1
hard constraint and 0
soft constraints.

If there are two (or more) score levels, for example HardSoftScore, then a score is feasible if no hard constraints are broken.
By default, Timefold Solver will always assign all planning variables a planning value. If there is no feasible solution, this means the best solution will be infeasible. To instead leave some of the planning entities unassigned, apply overconstrained planning. |
For each constraint, you need to pick a score level, a score weight and a score signum.
For example: -1soft
which has score level of soft
, a weight of 1
and a negative signum.
Do not use a big constraint weight when your business actually wants different score levels.
That hack, known as score folding, is broken:

Your business might tell you that your hard constraints all have the same weight, because they cannot be broken (so the weight does not matter). This is not true because if no feasible solution exists for a specific dataset, the least infeasible solution allows the business to estimate how many business resources they are lacking. For example in cloud balancing, how many new computers to buy. Furthermore, it will likely create a score trap.
For example in cloud balance if a |
Three or more score levels are also supported. For example: a company might decide that profit outranks employee satisfaction (or vice versa), while both are outranked by the constraints of physical reality.
To model fairness or load balancing, there is no need to use lots of score levels, even though Timefold Solver can handle many score levels. |
Most use cases use a Score
with two or three weights,
such as HardSoftScore and HardMediumSoftScore.
1.6. Pareto scoring (AKA multi-objective optimization scoring)
Far less common is the use case of pareto optimization, which is also known as multi-objective optimization. In pareto scoring, score constraints are in the same score level, yet they are not weighted against each other. When two scores are compared, each of the score constraints are compared individually and the score with the most dominating score constraints wins. Pareto scoring can even be combined with score levels and score constraint weighting.
Consider this example with positive constraints, where we want to get the most apples and oranges. Since it is impossible to compare apples and oranges, we cannot weigh them against each other. Yet, despite that we cannot compare them, we can state that two apples are better than one apple. Similarly, we can state that two apples and one orange are better than just one orange. So despite our inability to compare some Scores conclusively (at which point we declare them equal), we can find a set of optimal scores. Those are called pareto optimal.

Scores are considered equal far more often. It is left up to a human to choose the better out of a set of best solutions (with equal scores) found by Timefold Solver. In the example above, the user must choose between solution A (three apples and one orange) and solution B (one apple and six oranges). It is guaranteed that Timefold Solver has not found another solution which has more apples or more oranges or even a better combination of both (such as two apples and three oranges).
Pareto scoring is currently not supported in Timefold Solver.
A pareto |
1.7. Combining score techniques
All the score techniques mentioned above, can be combined seamlessly:

1.8. Score
interface
A score is represented by the Score
interface, which naturally extends Comparable
:
public interface Score<...> extends Comparable<...> {
...
}
The Score
implementation to use depends on your use case.
Your score might not efficiently fit in a single long
value.
Timefold Solver has several built-in Score
implementations.
Most use cases tend to use HardSoftScore
.

All Score implementations also have an initScore
(which is an int
).
It is mostly intended for internal use in Timefold Solver: it is the negative number of uninitialized planning variables.
From a user’s perspective this is 0
, unless a Construction Heuristic is terminated before it could initialize all planning variables (in which case Score.isSolutionInitialized()
returns false
).
The Score
implementation (for example HardSoftScore
) must be the same throughout a Solver
runtime.
The Score
implementation is configured in the solution domain class:
@PlanningSolution
public class CloudBalance {
...
@PlanningScore
private HardSoftScore score;
}
1.9. Avoid floating point numbers in score calculation
Avoid the use of float
or double
in score calculation.
Use BigDecimal
or scaled long
instead.
Floating point numbers (float
and double
) cannot represent a decimal number correctly.
For example: a double
cannot hold the value 0.05
correctly.
Instead, it holds the nearest representable value.
Arithmetic (including addition and subtraction) with floating point numbers, especially for planning problems, leads to incorrect decisions:

Additionally, floating point number addition is not associative:
System.out.println( ((0.01 + 0.02) + 0.03) == (0.01 + (0.02 + 0.03)) ); // returns false
This leads to score corruption.
Decimal numbers (BigDecimal
) have none of these problems.
BigDecimal arithmetic is considerably slower than Therefore, in many cases, it can be worthwhile to multiply all numbers for a single score weight by a plural of ten, so the score weight fits in a scaled |
2. Choose a score type
Depending on the number of score levels and type of score weights you need, choose a Score
type.
Most use cases use a HardSoftScore
.
To properly write a |
2.1. SimpleScore
A SimpleScore
has a single int
value, for example -123
.
It has a single score level.
@PlanningScore
private SimpleScore score;
Variants of this Score
type:
-
SimpleLongScore
uses along
value instead of anint
value. -
SimpleBigDecimalScore
uses aBigDecimal
value instead of anint
value.
2.2. HardSoftScore
(Recommended)
A HardSoftScore
has a hard int
value and a soft int
value, for example -123hard/-456soft
.
It has two score levels (hard and soft).
@PlanningScore
private HardSoftScore score;
Variants of this Score
type:
-
HardSoftLongScore
useslong
values instead ofint
values. -
HardSoftBigDecimalScore
usesBigDecimal
values instead ofint
values.
2.3. HardMediumSoftScore
A HardMediumSoftScore
which has a hard int
value, a medium int
value and a soft int
value, for example -123hard/-456medium/-789soft
.
It has three score levels (hard, medium and soft).
The hard level determines if the solution is feasible,
and the medium level and soft level score values determine
how well the solution meets business goals.
Higher medium values take precedence over soft values irrespective of the soft value.
@PlanningScore
private HardMediumSoftScore score;
Variants of this Score
type:
-
HardMediumSoftLongScore
useslong
values instead ofint
values. -
HardMediumSoftBigDecimalScore
usesBigDecimal
values instead ofint
values.
2.4. BendableScore
A BendableScore
has a configurable number of score levels.
It has an array of hard int
values and an array of soft int
values,
for example with two hard levels and three soft levels, the score can be [-123/-456]hard/[-789/-012/-345]soft
.
In that case, it has five score levels.
A solution is feasible if all hard levels are at least zero.
A BendableScore with one hard level and one soft level is equivalent to a HardSoftScore, while a BendableScore with one hard level and two soft levels is equivalent to a HardMediumSoftScore.
@PlanningScore(bendableHardLevelsSize = 2, bendableSoftLevelsSize = 3)
private BendableScore score;
The number of hard and soft score levels need to be set at compilation time. It is not flexible to change during solving.
Do not use a Usually, multiple constraints share the same level and are weighted against each other. Use explaining the score to get the weight of individual constraints in the same level. |
Variants of this Score
type:
-
BendableLongScore
useslong
values instead ofint
values. -
BendableBigDecimalScore
usesBigDecimal
values instead ofint
values.
3. Calculate the Score
3.1. Score calculation types
There are several ways to calculate the Score
of a solution in Hava or another JVM language:
-
Constraint streams: Implement each constraint as a separate Constraint Stream. Fast and scalable.
-
Incremental Java score calculation (not recommended): Implement multiple low-level methods. Fast and scalable. Very difficult to implement and maintain. Supports score explanations with extra effort.
-
Easy Java score calculation (not recommended): Implement all constraints together in a single method. Does not scale. Does not support score explanations.
Every score calculation type can work with any Score definition (such as HardSoftScore
or HardMediumSoftScore
).
All score calculation types are Object Oriented and can reuse existing Java code.
The score calculation must be read-only. It must not change the planning entities or the problem facts in any way. For example, it must not call a setter method on a planning entity in the score calculation. Timefold Solver does not recalculate the score of a solution if it can predict it (unless an environmentMode assertion is enabled). For example, after a winning step is done, there is no need to calculate the score because that move was done and undone earlier. As a result, there is no guarantee that changes applied during score calculation actually happen. To update planning entities when the planning variable change, use shadow variables instead. |
3.2. InitializingScoreTrend
The InitializingScoreTrend
specifies how the Score will change as more and more variables are initialized (while the already initialized variables do not change). Some optimization algorithms (such Construction Heuristics and Exhaustive Search) run faster if they have such information.
For the Score (or each score level separately), specify a trend:
-
ANY
(default): Initializing an extra variable can change the score positively or negatively. Gives no performance gain. -
ONLY_UP
(rare): Initializing an extra variable can only change the score positively. Implies that:-
There are only positive constraints
-
And initializing the next variable cannot unmatch a positive constraint that was matched by a previous initialized variable.
-
-
ONLY_DOWN
: Initializing an extra variable can only change the score negatively. Implies that:-
There are only negative constraints
-
And initializing the next variable cannot unmatch a negative constraint that was matched by a previous initialized variable.
-
Most use cases only have negative constraints.
Many of those have an InitializingScoreTrend
that only goes down:
<scoreDirectorFactory>
<constraintProviderClass>ai.timefold.solver.examples.cloudbalancing.score.CloudBalancingConstraintProvider</constraintProviderClass>
<initializingScoreTrend>ONLY_DOWN</initializingScoreTrend>
</scoreDirectorFactory>
Alternatively, you can also specify the trend for each score level separately:
<scoreDirectorFactory>
<constraintProviderClass>ai.timefold.solver.examples.cloudbalancing.score.CloudBalancingConstraintProvider</constraintProviderClass>
<initializingScoreTrend>ONLY_DOWN/ONLY_DOWN</initializingScoreTrend>
</scoreDirectorFactory>
3.3. Invalid score detection
When you put the environmentMode
in FULL_ASSERT
(or FAST_ASSERT
),
it will detect score corruption in the incremental score calculation.
However, that will not verify that your score calculator actually implements your score constraints as your business desires.
For example, one constraint might consistently match the wrong pattern.
To verify the constraints against an independent implementation, configure a assertionScoreDirectorFactory
:
<environmentMode>FAST_ASSERT</environmentMode>
...
<scoreDirectorFactory>
<constraintProviderClass>ai.timefold.solver.examples.nqueens.optional.score.NQueensConstraintProvider</constraintProviderClass>
<assertionScoreDirectorFactory>
<easyScoreCalculatorClass>ai.timefold.solver.examples.nqueens.optional.score.NQueensEasyScoreCalculator</easyScoreCalculatorClass>
</assertionScoreDirectorFactory>
</scoreDirectorFactory>
This way, the NQueensConstraintProvider
implementation is validated by the EasyScoreCalculator
.
This works well to isolate score corruption, but to verify that the constraint implement the real business needs, a unit test with a ConstraintVerifier is usually better. |
4. Constraint streams
Constraint streams are a Functional Programming form of incremental score calculation in plain Java that is easy to read, write and debug. The API should feel familiar if you’re familiar with Java Streams or SQL.
Using Java’s Streams API, we could implement an easy score calculator that uses a functional approach:
private int doNotAssignAnn() {
int softScore = 0;
schedule.getShiftList().stream()
.filter(Shift::isEmployeeAnn)
.forEach(shift -> {
softScore -= 1;
});
return softScore;
}
However, that scales poorly because it doesn’t do an incremental calculation:
When the planning variable of a single Shift
changes, to recalculate the score,
the normal Streams API has to execute the entire stream from scratch.
The ConstraintStreams API enables you to write similar code in pure Java, while reaping the performance benefits of
incremental score calculation.
This is an example of the same code, using the Constraint Streams API:
private Constraint doNotAssignAnn(ConstraintFactory factory) {
return factory.forEach(Shift.class)
.filter(Shift::isEmployeeAnn)
.penalize(HardSoftScore.ONE_SOFT)
.asConstraint("Don't assign Ann");
}
This constraint stream iterates over all instances of class Shift
in the problem facts and
planning entities in the planning problem.
It finds every Shift
which is assigned to employee Ann
and for every such instance (also called a match), it adds a
soft penalty of 1
to the overall score.
The following figure illustrates this process on a problem with 4 different shifts:

If any of the instances change during solving, the constraint stream automatically detects the change and only recalculates the minimum necessary portion of the problem that is affected by the change. The following figure illustrates this incremental score calculation:

ConstraintStreams API also has advanced support for score explanation through custom justifications and indictments.

4.1. Creating a constraint stream
To use the ConstraintStreams API in your project, first write a pure Java ConstraintProvider
implementation similar
to the following example.
public class MyConstraintProvider implements ConstraintProvider {
@Override
public Constraint[] defineConstraints(ConstraintFactory factory) {
return new Constraint[] {
penalizeEveryShift(factory)
};
}
private Constraint penalizeEveryShift(ConstraintFactory factory) {
return factory.forEach(Shift.class)
.penalize(HardSoftScore.ONE_SOFT)
.asConstraint("Penalize a shift");
}
}
This example contains one constraint, |
Add the following code to your solver configuration:
<solver xmlns="https://timefold.ai/xsd/solver" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://timefold.ai/xsd/solver https://timefold.ai/xsd/solver/solver.xsd">
<scoreDirectorFactory>
<constraintProviderClass>org.acme.schooltimetabling.solver.TimeTableConstraintProvider</constraintProviderClass>
</scoreDirectorFactory>
...
</solver>
4.2. Constraint stream cardinality
Constraint stream cardinality is a measure of how many objects a single constraint match consists of.
The simplest constraint stream has a cardinality of 1, meaning each constraint match only consists of 1 object.
Therefore, it is called a UniConstraintStream
:
private Constraint doNotAssignAnn(ConstraintFactory factory) {
return factory.forEach(Shift.class) // Returns UniStream<Shift>.
...
}
Some constraint stream building blocks can increase stream cardinality, such as join or groupBy:
private Constraint doNotAssignAnn(ConstraintFactory factory) {
return factory.forEach(Shift.class) // Returns Uni<Shift>.
.join(Employee.class) // Returns Bi<Shift, Employee>.
.join(DayOff.class) // Returns Tri<Shift, Employee, DayOff>.
.join(Country.class) // Returns Quad<Shift, Employee, DayOff, Country>.
...
}
The latter can also decrease stream cardinality:
private Constraint doNotAssignAnn(ConstraintFactory factory) {
return factory.forEach(Shift.class) // Returns UniStream<Shift>.
.join(Employee.class) // Returns BiStream<Shift, Employee>.
.groupBy((shift, employee) -> employee) // Returns UniStream<Employee>.
...
}
The following constraint stream cardinalities are currently supported:
Cardinality |
Prefix |
Defining interface |
1 |
Uni |
|
2 |
Bi |
|
3 |
Tri |
|
4 |
Quad |
|
4.2.1. Achieving higher cardinalities
Timefold Solver currently does not support constraint stream cardinalities higher than 4. However, with tuple mapping effectively infinite cardinality is possible:
private Constraint pentaStreamExample(ConstraintFactory factory) {
return factory.forEach(Shift.class) // UniConstraintStream<Shift>
.join(Shift.class) // BiConstraintStream<Shift, Shift>
.join(Shift.class) // TriConstraintStream<Shift, Shift, Shift>
.join(Shift.class) // QuadConstraintStream<Shift, Shift, Shift, Shift>
.map(MyTuple::of) // UniConstraintStream<MyTuple<Shift, Shift, Shift, Shift>>
.join(Shift.class) // BiConstraintStream<MyTuple<Shift, Shift, Shift, Shift>, Shift>
... // This BiConstraintStream carries 5 Shift elements.
}
Timefold Solver does not provide any tuple implementations out of the box. It’s recommended to use one of the freely available 3rd party implementations. Should a custom implementation be necessary, see guidelines for mapping functions. |
4.3. Building blocks
Constraint streams are chains of different operations, called building blocks.
Each constraint stream starts with a forEach(…)
building block and is terminated by either a penalty or a reward.
The following example shows the simplest possible constraint stream:
private Constraint penalizeInitializedShifts(ConstraintFactory factory) {
return factory.forEach(Shift.class)
.penalize(HardSoftScore.ONE_SOFT)
.asConstraint("Initialized shift");
}
This constraint stream penalizes each known and initialized instance of Shift
.
4.3.1. ForEach
The .forEach(T)
building block selects every T
instance that
is in a problem fact collection
or a planning entity collection
and has no null
genuine planning variables.
To include instances with a null
genuine planning variable,
replace the forEach()
building block by forEachIncludingNullVars()
:
private Constraint penalizeAllShifts(ConstraintFactory factory) {
return factory.forEachIncludingNullVars(Shift.class)
.penalize(HardSoftScore.ONE_SOFT)
.asConstraint("A shift");
}
The |
4.3.2. Penalties and rewards
The purpose of constraint streams is to build up a score for a solution.
To do this, every constraint stream must contain a call to either a penalize()
or a reward()
building block.
The penalize()
building block makes the score worse and the reward()
building block improves the score.
Each constraint stream is then terminated by calling asConstraint()
method, which finally builds the constraint. Constraints have several components:
-
Constraint package is the Java package that contains the constraint. The default value is the package that contains the
ConstraintProvider
implementation or the value from constraint configuration, if implemented. -
Constraint name is the human-readable descriptive name for the constraint, which (together with the constraint package) must be unique within the entire
ConstraintProvider
implementation. -
Constraint weight is a constant score value indicating how much every breach of the constraint affects the score. Valid examples include
SimpleScore.ONE
,HardSoftScore.ONE_HARD
andHardMediumSoftScore.of(1, 2, 3)
. -
Constraint match weigher is an optional function indicating how many times the constraint weight should be applied in the score. The penalty or reward score impact is the constraint weight multiplied by the match weight. The default value is
1
.
Constraints with zero constraint weight are automatically disabled and do not impose any performance penalty. |
The ConstraintStreams API supports many different types of penalties. Browse the API in your IDE for the full list of method overloads. Here are some examples:
-
Simple penalty (
penalize(SimpleScore.ONE)
) makes the score worse by1
per every match in the constraint stream. The score type must be the same type as used on the@PlanningScore
annotated member on the planning solution. -
Dynamic penalty (
penalize(SimpleScore.ONE, Shift::getHours)
) makes the score worse by the number of hours in every matchingShift
in the constraint stream. This is an example of using a constraint match weigher. -
Configurable penalty (
penalizeConfigurable()
) makes the score worse using constraint weights defined in constraint configuration. -
Configurable dynamic penalty(
penalizeConfigurable(Shift::getHours)
) makes the score worse using constraint weights defined in constraint configuration, multiplied by the number of hours in every matchingShift
in the constraint stream.
By replacing the keyword penalize
by reward
in the name of these building blocks, you get operations that
affect score in the opposite direction.
Customizing justifications and indictments
One of important Timefold Solver features is its ability to explain the score of solutions it produced through the use of justifications and indictments.
By default, each constraint is justified with ai.timefold.solver.core.api.score.stream.DefaultConstraintJustification
, and the final tuple makes up the indicted objects.
For example, in the following constraint, the indicted objects will be of type Vehicle
and an Integer
:
protected Constraint vehicleCapacity(ConstraintFactory factory) {
return factory.forEach(Customer.class)
.filter(customer -> customer.getVehicle() != null)
.groupBy(Customer::getVehicle, sum(Customer::getDemand))
.filter((vehicle, demand) -> demand > vehicle.getCapacity())
.penalizeLong(HardSoftLongScore.ONE_HARD,
(vehicle, demand) -> demand - vehicle.getCapacity())
.asConstraint("vehicleCapacity");
}
For the purposes of creating a heat map, the Vehicle
is very important, but the naked Integer
carries no semantics.
We can remove it by providing the `indictWith(…) method with a custom indictment mapping:
protected Constraint vehicleCapacity(ConstraintFactory factory) {
return factory.forEach(Customer.class)
.filter(customer -> customer.getVehicle() != null)
.groupBy(Customer::getVehicle, sum(Customer::getDemand))
.filter((vehicle, demand) -> demand > vehicle.getCapacity())
.penalizeLong(HardSoftLongScore.ONE_HARD,
(vehicle, demand) -> demand - vehicle.getCapacity())
.indictWith((vehicle, demand) -> List.of(vehicle))
.asConstraint("vehicleCapacity");
}
The same mechanism can also be used to transform any of the indicted objects to any other object.
To present the constraint matches to the user or to send them over the wire where they can be further processed, use the justifyWith(…)
method to provide a custom constraint justification:
protected Constraint vehicleCapacity(ConstraintFactory factory) {
return factory.forEach(Customer.class)
.filter(customer -> customer.getVehicle() != null)
.groupBy(Customer::getVehicle, sum(Customer::getDemand))
.filter((vehicle, demand) -> demand > vehicle.getCapacity())
.penalizeLong(HardSoftLongScore.ONE_HARD,
(vehicle, demand) -> demand - vehicle.getCapacity())
.justifyWith((vehicle, demand, score) ->
new VehicleDemandOveruse(vehicle, demand, score))
.indictWith((vehicle, demand) -> List.of(vehicle))
.asConstraint("vehicleCapacity");
}
VehicleDemandOveruse
is a custom type you have to implement.
You have complete control over the type, its name or methods exposed.
If you choose to decorate it with the proper annotations,
you will be able to send it over HTTP or store it in a database.
The only limitation is that it must implement the ai.timefold.solver.core.api.score.stream.ConstraintJustification
marker interface.
4.3.3. Filtering
Filtering enables you to reduce the number of constraint matches in your stream.
It first enumerates all constraint matches and then applies a predicate to filter some matches out.
The predicate is a function that only returns true
if the match is to continue in the stream.
The following constraint stream removes all of Beth’s shifts from all Shift
matches:
private Constraint penalizeAnnShifts(ConstraintFactory factory) {
return factory.forEach(Shift.class)
.filter(shift -> shift.getEmployeeName().equals("Ann"))
.penalize(SimpleScore.ONE)
.asConstraint("Ann's shift");
}
The following example retrieves a list of shifts where an employee has asked for a day off from a bi-constraint match
of Shift
and DayOff
:
private Constraint penalizeShiftsOnOffDays(ConstraintFactory factory) {
return factory.forEach(Shift.class)
.join(DayOff.class)
.filter((shift, dayOff) -> shift.date == dayOff.date && shift.employee == dayOff.employee)
.penalize(SimpleScore.ONE)
.asConstraint("Shift on an off-day");
}
The following figure illustrates both these examples:

For performance reasons, using the join building block with the appropriate |
The following functions are required for filtering constraint streams of different cardinality:
Cardinality |
Filtering Predicate |
1 |
|
2 |
|
3 |
|
4 |
|
4.3.4. Joining
Joining is a way to increase stream cardinality and it is similar to the inner join
operation in SQL. As the following figure illustrates,
a join()
creates a cartesian product of the streams being joined:

Doing this is inefficient if the resulting stream contains a lot of constraint matches that need to be filtered out immediately.
Instead, use a Joiner
condition to restrict the joined matches only to those that are interesting:

For example:
import static ai.timefold.solver.core.api.score.stream.Joiners.*;
...
private Constraint shiftOnDayOff(ConstraintFactory constraintFactory) {
return constraintFactory.forEach(Shift.class)
.join(DayOff.class,
equal(Shift::getDate, DayOff::getDate),
equal(Shift::getEmployee, DayOff::getEmployee))
.penalize(HardSoftScore.ONE_HARD)
.asConstraint("Shift on an off-day");
}
Through the Joiners
class, the following Joiner
conditions are supported to join two streams,
pairing a match from each side:
-
equal()
: the paired matches have a property that areequals()
. This relies onhashCode()
. -
greaterThan()
,greaterThanOrEqual()
,lessThan()
andlessThanOrEqual()
: the paired matches have aComparable
property following the prescribed ordering. -
overlapping()
: the paired matches have two properties (a start and an end property) of the sameComparable
type that both represent an interval which overlap.
All Joiners
methods have an overloaded method to use the same property of the same class on both stream sides.
For example, calling equal(Shift::getEmployee)
is the same as calling equal(Shift::getEmployee, Shift::getEmployee)
.
If the other stream might match multiple times, but it must only impact the score once (for each element of the original stream), use ifExists instead. It does not create cartesian products and therefore generally performs better. |
Evaluation of multiple joiners
When using multiple joiners, there are some important considerations to keep in mind. Consider the following example:
factory.forEach(VehicleShift.class)
.join(Visit.class,
Joiners.equal(Function.identity(), Visit::getVehicleShift), // Visit's VehicleShift is not null...
Joiners.lessThan(
vehicleShift -> vehicleShift.getMaxTravelTime(),
visit -> visit.getVehicleShift().getMaxTravelTime() // ... yet NPE may be thrown here.
))
When indexing joiners (such as equal()
and lessThan()
) check their indexes,
they take the input tuple and create a set of keys that will enter the index.
These keys are different for the left and right side of the joiner.
In the above example, from the left side,
the key is [VehicleShift instance && result of calling VehicleShift.getMaxTravelTime()]
.
(Using the first mapping function of each joiner.)
From the right side,
the key is [the result of calling Visit.getVehicleShift() && result of calling Visit.getVehicleShift().getMaxTravelTime()]
.
(Using the second mapping function of each joiner.)
However, both of the key mapping functions are calculated independently of the other,
and therefore the lessThan()
joiner’s mapping functions will be executed even in cases
when the equal()
joiner would not match.
This leads to a NullPointerException
being thrown in the example above,
where the lessThan()
joiner’s mapping functions are executed on a Visit
instance
that has a null
vehicleShift
property which wasn’t (yet) filtered out by the equal()
joiner.
The filtering only happens inside the joiner’s indexes and to access them,
we need these keys to be generated first.
To avoid these issues, do not assume that subsequent joiners' mapping functions only apply after the previous joiners have matched. Alternatively (and possibly at the cost of reduced performance) use the filtering joiner, which is processed differently and does not suffer from this issue:
factory.forEach(VehicleShift.class)
.join(Visit.class,
Joiners.equal(Function.identity(), Visit::getVehicleShift), // Visit's VehicleShift is not null...
Joiners.filtering((vehicleShift, visit) ->
vehicleShift.getMaxTravelTime() < visit.getVehicleShift().getMaxTravelTime()
))
4.3.5. Grouping and collectors
Grouping collects items in a stream according to user-provider criteria (also called "group key"), similar to what a
GROUP BY
SQL clause does. Additionally, some grouping operations also accept one or more Collector
instances, which
provide various aggregation functions. The following figure illustrates a simple groupBy()
operation:

Objects used as group key must obey the general contract of For this reason, it is not recommended to use mutable objects (especially mutable collections) as group keys. If planning entities are used as group keys, their hashCode must not be computed off of planning variables. Failure to follow this recommendation may result in runtime exceptions being thrown. |
For example, the following code snippet first groups all processes by the computer they run on, sums up all the power
required by the processes on that computer using the ConstraintCollectors.sum(…)
collector, and finally penalizes
every computer whose processes consume more power than is available.
import static ai.timefold.solver.core.api.score.stream.ConstraintCollectors.*;
...
private Constraint requiredCpuPowerTotal(ConstraintFactory constraintFactory) {
return constraintFactory.forEach(CloudProcess.class)
.groupBy(CloudProcess::getComputer, sum(CloudProcess::getRequiredCpuPower))
.filter((computer, requiredCpuPower) -> requiredCpuPower > computer.getCpuPower())
.penalize(HardSoftScore.ONE_HARD,
(computer, requiredCpuPower) -> requiredCpuPower - computer.getCpuPower())
.asConstraint("requiredCpuPowerTotal");
}
Information might be lost during grouping.
In the previous example, |
There are several collectors available out of the box. You can also provide your own collectors by implementing the
ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollector
interface, or its Bi…
, Tri…
and Quad…
counterparts.
count()
collector
The ConstraintCollectors.count(…)
counts all elements per group. For example, the following use of the collector
gives a number of items for two separate groups - one where the talks have unavailable speakers, and one where they
don’t.
private Constraint speakerAvailability(ConstraintFactory factory) {
return factory.forEach(Talk.class)
.groupBy(Talk::hasAnyUnavailableSpeaker, count())
.penalize(HardSoftScore.ONE_HARD,
(hasUnavailableSpeaker, count) -> ...)
.asConstraint("speakerAvailability");
}
The count is collected in an int
. Variants of this collector:
-
countLong()
collects along
value instead of anint
value.
To count a bi, tri or quad stream, use countBi()
, countTri()
or countQuad()
respectively,
because - unlike the other built-in collectors - they aren’t overloaded methods due to Java’s generics erasure.
countDistinct()
collector
The ConstraintCollectors.countDistinct(…)
counts any element per group once, regardless of how many times it
occurs. For example, the following use of the collector gives a number of talks in each unique room.
private Constraint roomCount(ConstraintFactory factory) {
return factory.forEach(Talk.class)
.groupBy(Talk::getRoom, countDistinct())
.penalize(HardSoftScore.ONE_SOFT,
(room, count) -> ...)
.asConstraint("roomCount");
}
The distinct count is collected in an int
. Variants of this collector:
-
countDistinctLong()
collects along
value instead of anint
value.
sum()
collector
To sum the values of a particular property of all elements per group, use the ConstraintCollectors.sum(…)
collector. The following code snippet first groups all processes by the computer they run on and sums up all the power
required by the processes on that computer using the ConstraintCollectors.sum(…)
collector.
private Constraint requiredCpuPowerTotal(ConstraintFactory constraintFactory) {
return constraintFactory.forEach(CloudProcess.class)
.groupBy(CloudProcess::getComputer, sum(CloudProcess::getRequiredCpuPower))
.penalize(HardSoftScore.ONE_SOFT,
(computer, requiredCpuPower) -> requiredCpuPower)
.asConstraint("requiredCpuPowerTotal");
}
The sum is collected in an int
. Variants of this collector:
-
sumLong()
collects along
value instead of anint
value. -
sumBigDecimal()
collects ajava.math.BigDecimal
value instead of anint
value. -
sumBigInteger()
collects ajava.math.BigInteger
value instead of anint
value. -
sumDuration()
collects ajava.time.Duration
value instead of anint
value. -
sumPeriod()
collects ajava.time.Period
value instead of anint
value. -
a generic
sum()
variant for summing up custom types
average()
collector
To calculate the average of a particular property of all elements per group, use the ConstraintCollectors.average(…)
collector.
The following code snippet first groups all processes by the computer they run on and averages all the power
required by the processes on that computer using the ConstraintCollectors.average(…)
collector.
private Constraint requiredCpuPowerTotal(ConstraintFactory constraintFactory) {
return constraintFactory.forEach(CloudProcess.class)
.groupBy(CloudProcess::getComputer, average(CloudProcess::getRequiredCpuPower))
.penalize(HardSoftScore.ONE_SOFT,
(computer, averageCpuPower) -> averageCpuPower)
.asConstraint("averageCpuPower");
}
The average is collected as a double
, and the average of no elements is null
.
Variants of this collector:
-
averageLong()
collects along
value instead of anint
value. -
averageBigDecimal()
collects ajava.math.BigDecimal
value instead of anint
value, resulting in aBigDecimal
average. -
averageBigInteger()
collects ajava.math.BigInteger
value instead of anint
value, resulting in aBigDecimal
average. -
averageDuration()
collects ajava.time.Duration
value instead of anint
value, resulting in aDuration
average.
min()
and max()
collectors
To extract the minimum or maximum per group, use the ConstraintCollectors.min(…)
and
ConstraintCollectors.max(…)
collectors respectively.
These collectors operate on values of properties which are Comparable
(such as Integer
, String
or Duration
),
although there are also variants of these collectors which allow you to provide your own Comparator
.
The following example finds a computer which runs the most power-demanding process:
private Constraint computerWithBiggestProcess(ConstraintFactory constraintFactory) {
return constraintFactory.forEach(CloudProcess.class)
.groupBy(CloudProcess::getComputer, max(CloudProcess::getRequiredCpuPower))
.penalize(HardSoftScore.ONE_HARD,
(computer, biggestProcess) -> ...)
.asConstraint("computerWithBiggestProcess");
}
|
toList()
, toSet()
and toMap()
collectors
To extract all elements per group into a collection, use the ConstraintCollectors.toList(…)
.
The following example retrieves all processes running on a computer in a List
:
private Constraint computerWithBiggestProcess(ConstraintFactory constraintFactory) {
return constraintFactory.forEach(CloudProcess.class)
.groupBy(CloudProcess::getComputer, toList())
.penalize(HardSoftScore.ONE_HARD,
(computer, processList) -> ...)
.asConstraint("computerAndItsProcesses");
}
Variants of this collector:
-
toList()
collects aList
value. -
toSet()
collects aSet
value. -
toSortedSet()
collects aSortedSet
value. -
toMap()
collects aMap
value. -
toSortedMap()
collects aSortedMap
value.
The iteration order of elements in the resulting collection is not guaranteed to be stable,
unless it is a sorted collector such as |
Conditional collectors
The constraint collector framework enables you to create constraint collectors which will only collect in certain circumstances.
This is achieved using the ConstraintCollectors.conditionally(…)
constraint collector.
This collector accepts a predicate, and another collector to which it will delegate if the predicate is true. The following example returns a count of long-running processes assigned to a given computer, excluding processes which are not long-running:
private Constraint computerWithLongRunningProcesses(ConstraintFactory constraintFactory) {
return constraintFactory.forEach(CloudProcess.class)
.groupBy(CloudProcess::getComputer, conditionally(
CloudProcess::isLongRunning,
count()
))
.penalize(HardSoftScore.ONE_HARD,
(computer, longRunningProcessCount) -> ...)
.asConstraint("longRunningProcesses");
}
This is useful in situations where multiple collectors are used and only some of them need to be restricted.
If all of them needed to be restricted in the same way,
then applying a filter()
before the grouping is preferable.
Composing collectors
The constraint collector framework enables you to create complex collectors utilizing simpler ones.
This is achieved using the ConstraintCollectors.compose(…)
constraint collector.
This collector accepts 2 to 4 other constraint collectors,
and a function to merge their results into one.
The following example builds an average()
constraint collector
using the count
constraint collector and sum()
constraint collector:
public static <A> UniConstraintCollector<A, ?, Double>
average(ToIntFunction<A> groupValueMapping) {
return compose(count(), sum(groupValueMapping), (count, sum) -> {
if (count == 0) {
return null;
} else {
return sum / (double) count;
}
});
}
Similarly, the compose()
collector enables you to work around the limitation of Constraint Stream cardinality
and use as many as 4 collectors in your groupBy()
statements:
UniConstraintCollector<A, ?, Triple<Integer, Integer, Integer>> collector =
compose(count(),
min(),
max(),
(count, min, max) -> Triple.of(count, min, max));
}
Such a composite collector returns a Triple
instance which allows you to access
each of the sub collectors individually.
Timefold Solver does not provide any |
4.3.6. Conditional propagation
Conditional propagation enables you to exclude constraint matches from the constraint stream based on the presence or absence of some other object.

The following example penalizes computers which have at least one process running:
private Constraint runningComputer(ConstraintFactory constraintFactory) {
return constraintFactory.forEach(CloudComputer.class)
.ifExists(CloudProcess.class, Joiners.equal(Function.identity(), CloudProcess::getComputer))
.penalize(HardSoftScore.ONE_SOFT,
computer -> ...)
.asConstraint("runningComputer");
}
Note the use of the ifExists()
building block.
On UniConstraintStream
, the ifExistsOther()
building block is also available which is useful in situations where the
forEach()
constraint match type is the same as the ifExists()
type.
Conversely, if the ifNotExists()
building block is used (as well as the ifNotExistsOther()
building block on
UniConstraintStream
) you can achieve the opposite effect:
private Constraint unusedComputer(ConstraintFactory constraintFactory) {
return constraintFactory.forEach(CloudComputer.class)
.ifNotExists(CloudProcess.class, Joiners.equal(Function.identity(), CloudProcess::getComputer))
.penalize(HardSoftScore.ONE_HARD,
computer -> ...)
.asConstraint("unusedComputer");
}
Here, only the computers without processes running are penalized.
Also note the use of the Joiner
class to limit the constraint matches.
For a description of available joiners, see joining.
Conditional propagation operates much like joining, with the exception of not increasing the
stream cardinality.
Matches from these building blocks are not available further down the stream.
For performance reasons, using conditional propagation with the appropriate |
4.3.7. Mapping tuples
Mapping enables you to transform each tuple in a constraint stream by applying a mapping function to it. The result of such mapping is another constraint stream of the mapped tuples.
private Constraint computerWithBiggestProcess(ConstraintFactory constraintFactory) {
return constraintFactory.forEach(CloudProcess.class) // UniConstraintStream<CloudProcess>
.map(CloudProcess::getComputer) // UniConstraintStream<CloudComputer>
...
}
In the example above, the mapping function produces duplicate tuples if two different |
Mapping can be used to transform streams of all cardinalities.
The following example maps a pair of CloudProcess
instances to a pair of CloudComputer
instances running them:
private Constraint computerWithBiggestProcess(ConstraintFactory constraintFactory) {
return constraintFactory.forEachUniquePair(CloudProcess.class) // BiConstraintStream<CloudProcess, CloudProcess>
.map(CloudProcess::getComputer, CloudProcess::getComputer) // BiConstraintStream<CloudComputer, CloudComputer>
...
}
Designing the mapping function
When designing the mapping function, follow these guidelines for optimal performance:
-
Keep the function pure. The mapping function should only depend on its input. That is, given the same input, it always returns the same output.
-
Keep the function bijective. No two input tuples should map to the same output tuple, or to tuples that are equal. Not following this recommendation creates a constraint stream with duplicate tuples, and may force you to use
distinct()
later. -
Use immutable data carriers. The tuples returned by the mapping function should be immutable and identified by their contents and nothing else. If two tuples carry objects which equal one another, those two tuples should likewise equal and preferably be the same instance.
Dealing with duplicate tuples using distinct()
As a general rule, tuples in constraint streams are distinct. That is, no two tuples that equal one another. However, certain operations such as tuple mapping may produce constraint streams where that is not true.
If a constraint stream produces duplicate tuples, you can use the distinct()
building block
to have the duplicate copies eliminated.
private Constraint computerWithBiggestProcess(ConstraintFactory constraintFactory) {
return constraintFactory.forEach(CloudProcess.class) // UniConstraintStream<CloudProcess>
.map(CloudProcess::getComputer) // UniConstraintStream<CloudComputer>
.distinct() // The same, each CloudComputer just once.
...
}
There is a performance cost to |
Expanding tuples
Tuple expansion is a special case of tuple mapping which only increases stream cardinality and can not introduce duplicate tuples. It enables you to add extra facts to each tuple in a constraint stream by applying a mapping function to it. This is useful in situations where an expensive computations needs to be cached for use later in the stream.
In the following example,
the method Talk.prevailingSpeakerUndesiredTimeslotTagCount()
internally iterates over collections to find overlapping tags
and returns the number of such tags.
It is expensive and it is called for each Talk
in the stream,
possibly being called many thousands of times per second.
Importantly, it is first called to filter out talks that have zero overlap,
and then again to penalize overlap on talks which suffer from it.
Constraint speakerUndesiredTimeslotTags(ConstraintFactory factory) {
return factory.forEach(Talk.class)
.filter(talk -> talk.prevailingSpeakerUndesiredTimeslotTagCount() > 0)
.penalizeConfigurable(talk -> talk.prevailingSpeakerUndesiredTimeslotTagCount() * talk.getDurationInMinutes())
.asConstraint(SPEAKER_UNDESIRED_TIMESLOT_TAGS);
}
We can improve this by using tuple expansion to cache the result of the expensive computation, possibly significantly reducing the number of times it is called.
Constraint speakerUndesiredTimeslotTags(ConstraintFactory factory) {
return factory.forEach(Talk.class)
.expand(Talk::prevailingSpeakerUndesiredTimeslotTagCount)
.filter((talk, undesiredTagCount) -> undesiredTagCount > 0)
.penalizeConfigurable((talk, undesiredTagCount) -> undesiredTagCount * talk.getDurationInMinutes())
.asConstraint(SPEAKER_UNDESIRED_TIMESLOT_TAGS);
}
Once the tuple for a Talk
has been created and passed through the filter,
the expensive computation will not be reevaluated again unless the Talk
itself changes.
There is a performance cost to |
4.3.8. Flattening
Flattening enables you to transform any Java Iterable
(such as List
or Set
)
into a set of tuples, which are sent downstream.
(Similar to Java Stream’s flatMap(…)
.)
This is done by applying a mapping function to the final element in the source tuple.
private Constraint requiredJobRoles(ConstraintFactory constraintFactory) {
return constraintFactory.forEach(Person.class) // UniConstraintStream<Person>
.join(Job.class,
equal(Function.identity(), Job::getAssignee)) // BiConstraintStream<Person, Job>
.flattenLast(Job::getRequiredRoles) // BiConstraintStream<Person, Role>
.filter((person, requiredRole) -> ...)
...
}
In the example above, the mapping function produces duplicate tuples
if |
4.4. Testing a constraint stream
We recommend that you test your constraints to ensure that they behave as expected.
Constraint streams include the Constraint Verifier unit testing harness.
To use it, first add a test scoped dependency to the timefold-solver-test
JAR.
4.4.1. Testing constraints in isolation
Consider the following constraint stream:
protected Constraint horizontalConflict(ConstraintFactory factory) {
return factory
.forEachUniquePair(Queen.class, equal(Queen::getRowIndex))
.penalize(SimpleScore.ONE)
.asConstraint("Horizontal conflict");
}
The following example uses the Constraint Verifier API to create a simple unit test for the preceding constraint stream:
private ConstraintVerifier<NQueensConstraintProvider, NQueens> constraintVerifier
= ConstraintVerifier.build(new NQueensConstraintProvider(), NQueens.class, Queen.class);
@Test
public void horizontalConflictWithTwoQueens() {
Row row1 = new Row(0);
Column column1 = new Column(0);
Column column2 = new Column(1);
Queen queen1 = new Queen(0, row1, column1);
Queen queen2 = new Queen(1, row1, column2);
constraintVerifier.verifyThat(NQueensConstraintProvider::horizontalConflict)
.given(queen1, queen2)
.penalizesBy(1);
}
This test ensures that the horizontal conflict constraint assigns a penalty of 1
when there are two queens on the same
row.
The following line creates a shared ConstraintVerifier
instance and initializes the instance with the
NQueensConstraintProvider
:
private ConstraintVerifier<NQueensConstraintProvider, NQueens> constraintVerifier
= ConstraintVerifier.build(new NQueensConstraintProvider(), NQueens.class, Queen.class);
The @Test
annotation indicates that the method is a unit test in a testing framework of your choice.
Constraint Verifier works with many testing frameworks including JUnit and AssertJ.
The first part of the test prepares the test data.
In this case, the test data includes two instances of the Queen
planning entity and their dependencies
(Row
, Column
):
Row row1 = new Row(0);
Column column1 = new Column(0);
Column column2 = new Column(1);
Queen queen1 = new Queen(0, row1, column1);
Queen queen2 = new Queen(1, row1, column2);
Further down, the following code tests the constraint:
constraintVerifier.verifyThat(NQueensConstraintProvider::horizontalConflict)
.given(queen1, queen2)
.penalizesBy(1);
The verifyThat(…)
call is used to specify a method on the NQueensConstraintProvider
class which is under test.
This method must be visible to the test class, which the Java compiler enforces.
The given(…)
call is used to enumerate all the facts that the constraint stream operates on.
In this case, the given(…)
call takes the queen1
and queen2
instances previously created.
Alternatively, you can use a givenSolution(…)
method here and provide a planning solution instead.
Finally, the penalizesBy(…)
call completes the test, making sure that the horizontal conflict constraint, given
one Queen
, results in a penalty of 1
.
This number is a product of multiplying the match weight, as defined in the constraint stream, by the number of matches.
Alternatively, you can use a rewardsWith(…)
call to check for rewards instead of penalties.
The method to use here depends on whether the constraint stream in question is terminated with a penalize
or a
reward
building block.
|
4.4.2. Testing all constraints together
In addition to testing individual constraints, you can test the entire ConstraintProvider
instance.
Consider the following test:
@Test
public void givenFactsMultipleConstraints() {
Queen queen1 = new Queen(0, row1, column1);
Queen queen2 = new Queen(1, row2, column2);
Queen queen3 = new Queen(2, row3, column3);
constraintVerifier.verifyThat()
.given(queen1, queen2, queen3)
.scores(SimpleScore.of(-3));
}
There are only two notable differences to the previous example.
First, the verifyThat()
call takes no argument here, signifying that the entire ConstraintProvider
instance is
being tested.
Second, instead of either a penalizesBy()
or rewardsWith()
call, the scores(…)
method is used.
This runs the ConstraintProvider
on the given facts and returns a sum of Score
s of all constraint matches resulting
from the given facts.
Using this method, you ensure that the constraint provider does not miss any constraints and that the scoring function
remains consistent as your code base evolves.
It is therefore necessary for the given(…)
method to list all planning entities and problem facts,
or provide the entire planning solution instead.
|
4.5. Other types of score calculation
4.5.1. Easy Java score calculation
An easy way to implement your score calculation in Java.
-
Advantages:
-
Plain old Java: no learning curve
-
Opportunity to delegate score calculation to an existing code base or legacy system
-
-
Disadvantages:
-
Slower
-
Does not scale because there is no incremental score calculation
-
Implement the one method of the interface EasyScoreCalculator
:
public interface EasyScoreCalculator<Solution_, Score_ extends Score<Score_>> {
Score_ calculateScore(Solution_ solution);
}
For example in n queens:
public class NQueensEasyScoreCalculator
implements EasyScoreCalculator<NQueens, SimpleScore> {
@Override
public SimpleScore calculateScore(NQueens nQueens) {
int n = nQueens.getN();
List<Queen> queenList = nQueens.getQueenList();
int score = 0;
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
Queen leftQueen = queenList.get(i);
Queen rightQueen = queenList.get(j);
if (leftQueen.getRow() != null && rightQueen.getRow() != null) {
if (leftQueen.getRowIndex() == rightQueen.getRowIndex()) {
score--;
}
if (leftQueen.getAscendingDiagonalIndex() == rightQueen.getAscendingDiagonalIndex()) {
score--;
}
if (leftQueen.getDescendingDiagonalIndex() == rightQueen.getDescendingDiagonalIndex()) {
score--;
}
}
}
}
return SimpleScore.valueOf(score);
}
}
Configure it in the solver configuration:
<scoreDirectorFactory>
<easyScoreCalculatorClass>ai.timefold.solver.examples.nqueens.optional.score.NQueensEasyScoreCalculator</easyScoreCalculatorClass>
</scoreDirectorFactory>
To configure values of an EasyScoreCalculator
dynamically in the solver configuration
(so the Benchmarker can tweak those parameters),
add the easyScoreCalculatorCustomProperties
element and use custom properties:
<scoreDirectorFactory>
<easyScoreCalculatorClass>...MyEasyScoreCalculator</easyScoreCalculatorClass>
<easyScoreCalculatorCustomProperties>
<property name="myCacheSize" value="1000" />
</easyScoreCalculatorCustomProperties>
</scoreDirectorFactory>
4.5.2. Incremental Java score calculation
A way to implement your score calculation incrementally in Java.
-
Advantages:
-
Very fast and scalable
-
Currently the fastest if implemented correctly
-
-
-
Disadvantages:
-
Hard to write
-
A scalable implementation heavily uses maps, indexes, … (things ConstraintStreams does for you)
-
You have to learn, design, write and improve all these performance optimizations yourself
-
-
Hard to read
-
Regular score constraint changes can lead to a high maintenance cost
-
-
Implement all the methods of the interface IncrementalScoreCalculator
:
public interface IncrementalScoreCalculator<Solution_, Score_ extends Score<Score_>> {
void resetWorkingSolution(Solution_ workingSolution);
void beforeEntityAdded(Object entity);
void afterEntityAdded(Object entity);
void beforeVariableChanged(Object entity, String variableName);
void afterVariableChanged(Object entity, String variableName);
void beforeEntityRemoved(Object entity);
void afterEntityRemoved(Object entity);
Score_ calculateScore();
}

For example in n queens:
public class NQueensAdvancedIncrementalScoreCalculator
implements IncrementalScoreCalculator<NQueens, SimpleScore> {
private Map<Integer, List<Queen>> rowIndexMap;
private Map<Integer, List<Queen>> ascendingDiagonalIndexMap;
private Map<Integer, List<Queen>> descendingDiagonalIndexMap;
private int score;
public void resetWorkingSolution(NQueens nQueens) {
int n = nQueens.getN();
rowIndexMap = new HashMap<Integer, List<Queen>>(n);
ascendingDiagonalIndexMap = new HashMap<Integer, List<Queen>>(n * 2);
descendingDiagonalIndexMap = new HashMap<Integer, List<Queen>>(n * 2);
for (int i = 0; i < n; i++) {
rowIndexMap.put(i, new ArrayList<Queen>(n));
ascendingDiagonalIndexMap.put(i, new ArrayList<Queen>(n));
descendingDiagonalIndexMap.put(i, new ArrayList<Queen>(n));
if (i != 0) {
ascendingDiagonalIndexMap.put(n - 1 + i, new ArrayList<Queen>(n));
descendingDiagonalIndexMap.put((-i), new ArrayList<Queen>(n));
}
}
score = 0;
for (Queen queen : nQueens.getQueenList()) {
insert(queen);
}
}
public void beforeEntityAdded(Object entity) {
// Do nothing
}
public void afterEntityAdded(Object entity) {
insert((Queen) entity);
}
public void beforeVariableChanged(Object entity, String variableName) {
retract((Queen) entity);
}
public void afterVariableChanged(Object entity, String variableName) {
insert((Queen) entity);
}
public void beforeEntityRemoved(Object entity) {
retract((Queen) entity);
}
public void afterEntityRemoved(Object entity) {
// Do nothing
}
private void insert(Queen queen) {
Row row = queen.getRow();
if (row != null) {
int rowIndex = queen.getRowIndex();
List<Queen> rowIndexList = rowIndexMap.get(rowIndex);
score -= rowIndexList.size();
rowIndexList.add(queen);
List<Queen> ascendingDiagonalIndexList = ascendingDiagonalIndexMap.get(queen.getAscendingDiagonalIndex());
score -= ascendingDiagonalIndexList.size();
ascendingDiagonalIndexList.add(queen);
List<Queen> descendingDiagonalIndexList = descendingDiagonalIndexMap.get(queen.getDescendingDiagonalIndex());
score -= descendingDiagonalIndexList.size();
descendingDiagonalIndexList.add(queen);
}
}
private void retract(Queen queen) {
Row row = queen.getRow();
if (row != null) {
List<Queen> rowIndexList = rowIndexMap.get(queen.getRowIndex());
rowIndexList.remove(queen);
score += rowIndexList.size();
List<Queen> ascendingDiagonalIndexList = ascendingDiagonalIndexMap.get(queen.getAscendingDiagonalIndex());
ascendingDiagonalIndexList.remove(queen);
score += ascendingDiagonalIndexList.size();
List<Queen> descendingDiagonalIndexList = descendingDiagonalIndexMap.get(queen.getDescendingDiagonalIndex());
descendingDiagonalIndexList.remove(queen);
score += descendingDiagonalIndexList.size();
}
}
public SimpleScore calculateScore() {
return SimpleScore.valueOf(score);
}
}
Configure it in the solver configuration:
<scoreDirectorFactory>
<incrementalScoreCalculatorClass>ai.timefold.solver.examples.nqueens.optional.score.NQueensAdvancedIncrementalScoreCalculator</incrementalScoreCalculatorClass>
</scoreDirectorFactory>
A piece of incremental score calculator code can be difficult to write and to review.
Assert its correctness by using an |
To configure values of an IncrementalScoreCalculator
dynamically in the solver configuration
(so the Benchmarker can tweak those parameters),
add the incrementalScoreCalculatorCustomProperties
element and use custom properties:
<scoreDirectorFactory>
<incrementalScoreCalculatorClass>...MyIncrementalScoreCalculator</incrementalScoreCalculatorClass>
<incrementalScoreCalculatorCustomProperties>
<property name="myCacheSize" value="1000"/>
</incrementalScoreCalculatorCustomProperties>
</scoreDirectorFactory>
ConstraintMatchAwareIncrementalScoreCalculator
Optionally, also implement the ConstraintMatchAwareIncrementalScoreCalculator
interface to:
-
Explain a score by splitting it up per score constraint with
ScoreExplanation.getConstraintMatchTotalMap()
. -
Visualize or sort planning entities by how many constraints each one breaks with
ScoreExplanation.getIndictmentMap()
. -
Receive a detailed analysis if the
IncrementalScoreCalculator
is corrupted inFAST_ASSERT
orFULL_ASSERT
environmentMode
,
public interface ConstraintMatchAwareIncrementalScoreCalculator<Solution_, Score_ extends Score<Score_>> {
void resetWorkingSolution(Solution_ workingSolution, boolean constraintMatchEnabled);
Collection<ConstraintMatchTotal<Score_>> getConstraintMatchTotals();
Map<Object, Indictment<Score_>> getIndictmentMap();
}
For example in machine reassignment, create one ConstraintMatchTotal
per constraint type and call addConstraintMatch()
for each constraint match:
public class MachineReassignmentIncrementalScoreCalculator
implements ConstraintMatchAwareIncrementalScoreCalculator<MachineReassignment, HardSoftLongScore> {
...
@Override
public void resetWorkingSolution(MachineReassignment workingSolution, boolean constraintMatchEnabled) {
resetWorkingSolution(workingSolution);
// ignore constraintMatchEnabled, it is always presumed enabled
}
@Override
public Collection<ConstraintMatchTotal<HardSoftLongScore>> getConstraintMatchTotals() {
ConstraintMatchTotal<HardSoftLongScore> maximumCapacityMatchTotal = new DefaultConstraintMatchTotal<>(CONSTRAINT_PACKAGE,
"maximumCapacity", HardSoftLongScore.ZERO);
...
for (MrMachineScorePart machineScorePart : machineScorePartMap.values()) {
for (MrMachineCapacityScorePart machineCapacityScorePart : machineScorePart.machineCapacityScorePartList) {
if (machineCapacityScorePart.maximumAvailable < 0L) {
maximumCapacityMatchTotal.addConstraintMatch(
Arrays.asList(machineCapacityScorePart.machineCapacity),
HardSoftLongScore.valueOf(machineCapacityScorePart.maximumAvailable, 0));
}
}
}
...
List<ConstraintMatchTotal<HardSoftLongScore>> constraintMatchTotalList = new ArrayList<>(4);
constraintMatchTotalList.add(maximumCapacityMatchTotal);
...
return constraintMatchTotalList;
}
@Override
public Map<Object, Indictment<HardSoftLongScore>> getIndictmentMap() {
return null; // Calculate it non-incrementally from getConstraintMatchTotals()
}
}
That getConstraintMatchTotals()
code often duplicates some of the logic of the normal IncrementalScoreCalculator
methods.
Constraint Streams doesn’t have this disadvantage, because they are constraint match aware automatically when needed,
without any extra domain-specific code.
5. Score calculation performance tricks
5.1. Overview
The Solver
will normally spend most of its execution time running the score calculation
(which is called in its deepest loops).
Faster score calculation will return the same solution in less time with the same algorithm,
which normally means a better solution in equal time.
5.2. Score calculation speed
After solving a problem, the Solver
will log the score calculation speed per second.
This is a good measurement of Score calculation performance,
despite that it is affected by non score calculation execution time.
It depends on the problem scale of the problem dataset.
Normally, even for high scale problems, it is higher than 1000
, except if you are using an EasyScoreCalculator
.
When improving your score calculation, focus on maximizing the score calculation speed, instead of maximizing the best score. A big improvement in score calculation can sometimes yield little or no best score improvement, for example when the algorithm is stuck in a local or global optima. If you are watching the calculation speed instead, score calculation improvements are far more visible. Furthermore, watching the calculation speed allows you to remove or add score constraints, and still compare it with the original’s calculation speed. Comparing the best score with the original’s best score is pointless: it’s comparing apples and oranges. |
5.3. Incremental score calculation (with deltas)
When a solution changes, incremental score calculation (AKA delta based score calculation)
calculates the delta with the previous state to find the new Score
,
instead of recalculating the entire score on every solution evaluation.
For example, when a single queen A moves from row 1
to 2
,
it will not bother to check if queen B and C can attack each other, since neither of them changed:

Similarly in employee rostering:

This is a huge performance and scalability gain. Constraint Streams gives you this huge scalability gain without forcing you to write a complicated incremental score calculation algorithm.* Just let the rule engine do the hard work.
Notice that the speedup is relative to the size of your planning problem (your n), making incremental score calculation far more scalable.
5.4. Avoid calling remote services during score calculation
Do not call remote services in your score calculation (except if you are bridging EasyScoreCalculator
to a legacy system). The network latency will kill your score calculation performance.
Cache the results of those remote services if possible.
If some parts of a constraint can be calculated once, when the Solver
starts, and never change during solving,
then turn them into cached problem facts.
5.5. Pointless constraints
If you know a certain constraint can never be broken (or it is always broken), do not write a score constraint for it.
For example in n queens, the score calculation does not check if multiple queens occupy the same column,
because a Queen
's column
never changes and every solution starts with each Queen
on a different column
.
Do not go overboard with this. If some datasets do not use a specific constraint but others do, just return out of the constraint as soon as you can. There is no need to dynamically change your score calculation based on the dataset. |
5.6. Built-in hard constraint
Instead of implementing a hard constraint, it can sometimes be built in.
For example, if Lecture
A should never be assigned to Room
X, but it uses ValueRangeProvider
on Solution,
so the Solver
will often try to assign it to Room
X too (only to find out that it breaks a hard constraint).
Use a ValueRangeProvider on the planning entity or filtered selection to define that Course A should only be assigned a Room
different than X.
This can give a good performance gain in some use cases, not just because the score calculation is faster, but mainly because most optimization algorithms will spend less time evaluating infeasible solutions. However, usually this is not a good idea because there is a real risk of trading short term benefits for long term harm:
-
Many optimization algorithms rely on the freedom to break hard constraints when changing planning entities, to get out of local optima.
-
Both implementation approaches have limitations (feature compatibility, disabling automatic performance optimizations), as explained in their documentation.
5.7. Other score calculation performance tricks
-
Verify that your score calculation happens in the correct
Number
type. If you are making the sum ofint
values, do not sum it in adouble
which takes longer. -
For optimal performance, always use server mode (
java -server
). We have seen performance increases of 50% by turning on server mode. -
For optimal performance, use the latest Java version. For example, in the past we have seen performance increases of 30% by switching from java 1.5 to 1.6.
-
Always remember that premature optimization is the root of all evil. Make sure your design is flexible enough to allow configuration based tweaking.
5.8. Score trap
Make sure that none of your score constraints cause a score trap. A trapped score constraint uses the same weight for different constraint matches, when it could just as easily use a different weight. It effectively lumps its constraint matches together, which creates a flatlined score function for that constraint. This can cause a solution state in which several moves need to be done to resolve or lower the weight of that single constraint. Some examples of score traps:
-
You need two doctors at each table, but you are only moving one doctor at a time. So the solver has no incentive to move a doctor to a table with no doctors. Punish a table with no doctors more than a table with only one doctor in that score constraint in the score function.
-
Two exams need to be conducted at the same time, but you are only moving one exam at a time. So the solver has to move one of those exams to another timeslot without moving the other in the same move. Add a coarse-grained move that moves both exams at the same time.
For example, consider this score trap. If the blue item moves from an overloaded computer to an empty computer, the hard score should improve. The trapped score implementation fails to do that:

The Solver should eventually get out of this trap, but it will take a lot of effort (especially if there are even more processes on the overloaded computer). Before they do that, they might actually start moving more processes into that overloaded computer, as there is no penalty for doing so.
Avoiding score traps does not mean that your score function should be smart enough to avoid local optima. Leave it to the optimization algorithms to deal with the local optima. Avoiding score traps means to avoid, for each score constraint individually, a flatlined score function. |
Always specify the degree of infeasibility. The business will often say "if the solution is infeasible, it does not matter how infeasible it is." While that is true for the business, it is not true for score calculation as it benefits from knowing how infeasible it is. In practice, soft constraints usually do this naturally and it is just a matter of doing it for the hard constraints too. |
There are several ways to deal with a score trap:
-
Improve the score constraint to make a distinction in the score weight. For example, penalize
-1hard
for every missing CPU, instead of just-1hard
if any CPU is missing. -
If changing the score constraint is not allowed from the business perspective, add a lower score level with a score constraint that makes such a distinction. For example, penalize
-1subsoft
for every missing CPU, on top of-1hard
if any CPU is missing. The business ignores the subsoft score level. -
Add coarse-grained moves and union select them with the existing fine-grained moves. A coarse-grained move effectively does multiple moves to directly get out of a score trap with a single move. For example, move multiple items from the same container to another container.
5.9. stepLimit
benchmark
Not all score constraints have the same performance cost. Sometimes one score constraint can kill the score calculation performance outright. Use the Benchmarker to do a one minute run and check what happens to the score calculation speed if you comment out all but one of the score constraints.
5.10. Fairness score constraints
Some use cases have a business requirement to provide a fair schedule (usually as a soft score constraint), for example:
-
Fairly distribute the workload amongst the employees, to avoid envy.
-
Evenly distribute the workload amongst assets, to improve reliability.
Implementing such a constraint can seem difficult (especially because there are different ways to formalize fairness), but usually the squared workload implementation behaves most desirable.
For each employee/asset, count the workload w
and subtract w²
from the score.

As shown above, the squared workload implementation guarantees that if you select two employees from a given solution and make their distribution between those two employees fairer, then the resulting new solution will have a better overall score. Do not just use the difference from the average workload, as that can lead to unfairness, as demonstrated below.

Instead of the squared workload, it is also possible to use the variance (squared difference to the average) or the standard deviation (square root of the variance). This has no effect on the score comparison, because the average will not change during planning. It is just more work to implement (because the average needs to be known) and trivially slower (because the calculation is a bit longer). |
When the workload is perfectly balanced, the user often likes to see a 0
score, instead of the distracting -34soft
in the image above (for the last solution which is almost perfectly balanced).
To nullify this, either add the average multiplied by the number of entities to the score or instead show the variance or standard deviation in the UI.
6. Constraint configuration: adjust constraint weights dynamically
Deciding the correct weight and level for each constraint is not easy. It often involves negotiating with different stakeholders and their priorities. Furthermore, quantifying the impact of soft constraints is often a new experience for business managers, so they’ll need a number of iterations to get it right.
Don’t get stuck between a rock and a hard place. Provide a UI to adjust the constraint weights and visualize the resulting solution, so the business managers can tweak the constraint weights themselves:

6.1. Create a constraint configuration
First, create a new class to hold the constraint weights and other constraint parameters.
Annotate it with @ConstraintConfiguration
:
@ConstraintConfiguration
public class ConferenceConstraintConfiguration {
...
}
There will be exactly one instance of this class per planning solution.
The planning solution and the constraint configuration have a one-to-one relationship,
but they serve a different purpose, so they aren’t merged into a single class.
A @ConstraintConfiguration
class can extend a parent @ConstraintConfiguration
class,
which can be useful in international use cases with many regional constraints.
Add the constraint configuration on the planning solution
and annotate that field or property with @ConstraintConfigurationProvider
:
@PlanningSolution
public class ConferenceSolution {
@ConstraintConfigurationProvider
private ConferenceConstraintConfiguration constraintConfiguration;
...
}
The @ConstraintConfigurationProvider
annotation automatically exposes the constraint configuration
as a problem fact, there is no need to add a @ProblemFactProperty
annotation.
The constraint configuration class holds the constraint weights,
but it can also hold constraint parameters.
For example in conference scheduling, the minimum pause constraint has a constraint weight (like any other constraint),
but it also has a constraint parameter that defines the length of the minimum pause between two talks of the same speaker.
That pause length depends on the conference (= the planning problem):
in some big conferences 20 minutes isn’t enough to go from one room to the other.
That pause length is a field in the constraint configuration without a @ConstraintWeight
annotation.
6.2. Add a constraint weight for each constraint
In the constraint configuration class, add a @ConstraintWeight
field or property for each constraint:
@ConstraintConfiguration(constraintPackage = "...conferencescheduling.score")
public class ConferenceConstraintConfiguration {
@ConstraintWeight("Speaker conflict")
private HardMediumSoftScore speakerConflict = HardMediumSoftScore.ofHard(10);
@ConstraintWeight("Theme track conflict")
private HardMediumSoftScore themeTrackConflict = HardMediumSoftScore.ofSoft(10);
@ConstraintWeight("Content conflict")
private HardMediumSoftScore contentConflict = HardMediumSoftScore.ofSoft(100);
...
}
The type of the constraint weights must be the same score class as the planning solution’s score member.
For example in conference scheduling, ConferenceSolution.getScore()
and ConferenceConstraintConfiguration.getSpeakerConflict()
both return a HardMediumSoftScore
.
A constraint weight cannot be null.
Give each constraint weight a default value, but expose them in a UI so the business users can tweak them.
The example above uses the ofHard()
, ofMedium()
and ofSoft()
methods to do that.
Notice how it defaults the content conflict constraint as ten times more important than the theme track conflict constraint.
Normally, a constraint weight only uses one score level,
but it’s possible to use multiple score levels (at a small performance cost).
Each constraint has a constraint package and a constraint name, together they form the constraint id. These connect the constraint weight with the constraint implementation. For each constraint weight, there must be a constraint implementation with the same package and the same name.
-
The
@ConstraintConfiguration
annotation has aconstraintPackage
property that defaults to the package of the constraint configuration class. Cases with Constraint streams normally don’t need to specify it. -
The
@ConstraintWeight
annotation has avalue
which is the constraint name (for example "Speaker conflict"). It inherits the constraint package from the@ConstraintConfiguration
, but it can override that, for example@ConstraintWeight(constraintPackage = "…region.france", …)
to use a different constraint package than some other weights.
So every constraint weight ends up with a constraint package and a constraint name. Each constraint weight links with a constraint implementation, for example in Constraint streams:
public final class ConferenceSchedulingConstraintProvider implements ConstraintProvider {
@Override
public Constraint[] defineConstraints(ConstraintFactory factory) {
return new Constraint[] {
speakerConflict(factory),
themeTrackConflict(factory),
contentConflict(factory),
...
};
}
protected Constraint speakerConflict(ConstraintFactory factory) {
return factory.forEachUniquePair(...)
...
.penalizeConfigurable("Speaker conflict", ...);
}
protected Constraint themeTrackConflict(ConstraintFactory factory) {
return factory.forEachUniquePair(...)
...
.penalizeConfigurable("Theme track conflict", ...);
}
protected Constraint contentConflict(ConstraintFactory factory) {
return factory.forEachUniquePair(...)
...
.penalizeConfigurable("Content conflict", ...);
}
...
}
Each of the constraint weights defines the score level and score weight of their constraint.
The constraint implementation calls rewardConfigurable()
or penalizeConfigurable()
and the constraint weight is automatically applied.
If the constraint implementation provides a match weight, that match weight is multiplied with the constraint weight.
For example, the content conflict constraint weight defaults to 100soft
and the constraint implementation penalizes each match based on the number of shared content tags and the overlapping duration of the two talks:
@ConstraintWeight("Content conflict")
private HardMediumSoftScore contentConflict = HardMediumSoftScore.ofSoft(100);
Constraint contentConflict(ConstraintFactory factory) {
return factory.forEachUniquePair(Talk.class,
overlapping(t -> t.getTimeslot().getStartDateTime(),
t -> t.getTimeslot().getEndDateTime()),
filtering((talk1, talk2) -> talk1.overlappingContentCount(talk2) > 0))
.penalizeConfigurable("Content conflict",
(talk1, talk2) -> talk1.overlappingContentCount(talk2)
* talk1.overlappingDurationInMinutes(talk2));
}
So when 2 overlapping talks share only 1 content tag and overlap by 60 minutes, the score is impacted by -6000soft
.
But when 2 overlapping talks share 3 content tags, the match weight is 180, so the score is impacted by -18000soft
.
7. Explaining the score: which constraints are broken?
The easiest way to explain the score during development is to print the return value of getSummary()
, but only use that method for diagnostic purposes:
System.out.println(scoreManager.getSummary(solution));
For example in conference scheduling, this prints that talk S51
is responsible for breaking the hard constraint Speaker required room tag
:
Explanation of score (-1hard/-806soft): Constraint match totals: -1hard: constraint (Speaker required room tag) has 1 matches: -1hard: justifications ([S51]) -340soft: constraint (Theme track conflict) has 32 matches: -20soft: justifications ([S68, S66]) -20soft: justifications ([S61, S44]) ... ... Indictments (top 5 of 72): -1hard/-22soft: justification (S51) has 12 matches: -1hard: constraint (Speaker required room tag) -10soft: constraint (Theme track conflict) ... ...
Do not attempt to parse this string or use it in your UI or exposed services. Instead use the ConstraintMatch API below and do it properly. |
In the string above, there are two previously unexplained concepts.
Justifications are user-defined objects that implement the ai.timefold.solver.core.api.score.stream.ConstraintJustification
interface,
which carry meaningful information about a constraint match, such as its package, name and score.
On the other hand, indicted objects are objects which were directly involved in causing a constraint to match. For example, if your constraints penalize each vehicle, then there will be one ai.timefold.solver.core.api.score.constraint.Indictment
instance per vehicle, carrying the vehicle as an indicted object. Indictments are typically used for heat map visualization.
7.1. Using score calculation outside the Solver
If other parts of your application, for example your webUI, need to calculate the score of a solution, use the SolutionManager
API:
SolutionManager<CloudBalance, HardSoftScore> scoreManager = SolutionManager.create(solverFactory);
ScoreExplanation<CloudBalance, HardSoftScore> scoreExplanation = scoreManager.explainScore(cloudBalance);
Then use it when you need to calculate the Score
of a solution:
HardSoftScore score = scoreExplanation.getScore();
Furthermore, the ScoreExplanation
can help explain the score through constraint match totals and/or indictments:

7.2. Break down the score by constraint justification
Each constraint may be justified by a different ConstraintJustification
implementation, but you can also choose to share them among constraints.
To receive all constraint justifications regardless of their type, call:
List<ConstraintJustification> constraintJustificationlist = scoreExplanation.getJustificationList();
...
In Constraint streams, justifications can be customized, so that it can be easily serialized and sent over the wire. Such custom justifications can be queried like so:
List<MyConstraintJustification> constraintJustificationlist = scoreExplanation.getJustificationList(MyConstraintJustification.class);
...
7.3. Break down the score by constraint
To break down the score per constraint, get the ConstraintMatchTotal
s from the ScoreExplanation
:
Collection<ConstraintMatchTotal<HardSoftScore>> constraintMatchTotals = scoreExplanation.getConstraintMatchTotalMap().values();
for (ConstraintMatchTotal<HardSoftScore> constraintMatchTotal : constraintMatchTotals) {
String constraintName = constraintMatchTotal.getConstraintName();
// The score impact of that constraint
HardSoftScore totalScore = constraintMatchTotal.getScore();
for (ConstraintMatch<HardSoftScore> constraintMatch : constraintMatchTotal.getConstraintMatchSet()) {
ConstraintJustification justification = constraintMatch.getJustification();
HardSoftScore score = constraintMatch.getScore();
...
}
}
Each ConstraintMatchTotal
represents one constraint and has a part of the overall score.
The sum of all the ConstraintMatchTotal.getScore()
equals the overall score.
Constraint streams supports constraint matches automatically, but incremental Java score calculation requires implementing an extra interface. |
7.4. Indictment heat map: visualize the hot planning entities
To show a heat map in the UI that highlights the planning entities and problem facts have an impact on the Score
, get the Indictment
map from the ScoreExplanation
:
Map<Object, Indictment<HardSoftScore>> indictmentMap = scoreExplanation.getIndictmentMap();
for (CloudProcess process : cloudBalance.getProcessList()) {
Indictment<HardSoftScore> indictment = indictmentMap.get(process);
if (indictment == null) {
continue;
}
// The score impact of that planning entity
HardSoftScore totalScore = indictment.getScore();
for (ConstraintMatch<HardSoftScore> constraintMatch : indictment.getConstraintMatchSet()) {
String constraintName = constraintMatch.getConstraintName();
HardSoftScore score = constraintMatch.getScore();
...
}
}
Each Indictment
is the sum of all constraints where that justification object is involved with.
The sum of all the Indictment.getScoreTotal()
differs from the overall score, because multiple Indictment
s can share the same ConstraintMatch
.
Constraint streams support constraint matches automatically, but incremental Java score calculation requires implementing an extra interface. |