Skip to content

Commit bedd35d

Browse files
docs: add docs for precompute (#1905)
Co-authored-by: Lukáš Petrovický <[email protected]>
1 parent 3531f19 commit bedd35d

File tree

1 file changed

+103
-0
lines changed

1 file changed

+103
-0
lines changed

docs/src/modules/ROOT/pages/constraints-and-score/score-calculation.adoc

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1622,6 +1622,109 @@ This stream now contains all `Employee` instances, both with and without shifts
16221622
and can be passed to the <<collectorsLoadBalance,load balancing constraint collector>>,
16231623
which will compute the unfairness metric.
16241624

1625+
[#constraintStreamsPrecompute]
1626+
=== Precomputing a stream
1627+
1628+
When a stream depends only on properties of problem facts or entities,
1629+
and not on any planning variables (genuine or shadow),
1630+
that stream can be precomputed to improve performance.
1631+
Consider the following `No overlapping shifts` constraint:
1632+
1633+
[tabs]
1634+
====
1635+
Java::
1636+
+
1637+
[source,java,options="nowrap"]
1638+
----
1639+
Constraint noOverlappingShifts(ConstraintFactory constraintFactory) {
1640+
return constraintFactory.forEachUniquePair(Shift.class,
1641+
Joiners.overlapping(Shift::getStart, Shift::getEnd),
1642+
Joiners.equal(Shift::getEmployee))
1643+
.penalize(HardSoftScore.ONE_HARD, (shift1, shift2) -> shift1.getOverlap(shift2))
1644+
.asConstraint("No overlapping shifts");
1645+
}
1646+
----
1647+
====
1648+
1649+
Whenever any variable changes, this constraint will recalculate what shifts overlap and their overlap amount.
1650+
Since `start` and `end` are not variables (genuine or shadow) and `getOverlap()` does not depend on any variables,
1651+
some parts of the constraint can be precomputed to improve performance:
1652+
1653+
[tabs]
1654+
====
1655+
Java::
1656+
+
1657+
[source,java,options="nowrap"]
1658+
----
1659+
TriConstraintStream<Shift, Shift, Integer> overlappingShifts(PrecomputeFactory precomputeFactory) {
1660+
return precomputeFactory.forEachUnfilteredUniquePair(Shift.class,
1661+
Joiners.overlapping(Shift::getStart, Shift::getEnd))
1662+
.expand((shift1, shift2) -> shift1.getOverlap(shift2));
1663+
}
1664+
1665+
Constraint noOverlappingShifts(ConstraintFactory constraintFactory) {
1666+
return constraintFactory.precompute(this::overlappingShifts)
1667+
.filter((shift1, shift2, overlap) ->
1668+
shift1.getEmployee() != null &&
1669+
shift1.getEmployee() == shift2.getEmployee())
1670+
.penalize(HardSoftScore.ONE_HARD, (shift1, shift2, overlap) -> overlap)
1671+
.asConstraint("No overlapping shifts");
1672+
}
1673+
----
1674+
====
1675+
1676+
This will precompute and store the overlapping shifts, and their amount of overlap.
1677+
When any of the overlapping shifts' variables change, only the filter will be re-evaluated.
1678+
That filter also won't be evaluated for non-overlapping shifts,
1679+
since they were already eliminated by the precomputed stream.
1680+
1681+
==== Precomputing and planning variables
1682+
1683+
Precomputed streams are unfiltered and will include both unassigned and inconsistent planning entities.
1684+
If only assigned entities are desired,
1685+
add a filter after the `precompute` call to check if the entities' variables are assigned.
1686+
1687+
[IMPORTANT]
1688+
====
1689+
A score corruption will occur if a precomputed stream depends on variables (genuine or shadow).
1690+
For example, if `employee` is a planning variable, then the following constraint will cause a score corruption:
1691+
1692+
[tabs]
1693+
=====
1694+
Java::
1695+
+
1696+
[source,java,options="nowrap"]
1697+
----
1698+
TriConstraintStream<Shift, Shift, Integer> overlappingShifts(PrecomputeFactory precomputeFactory) {
1699+
// This references Shift::getEmployee, which depends
1700+
// on a planning variable and thus is not valid to
1701+
// use in a precomputed stream
1702+
return precomputeFactory.forEachUnfilteredUniquePair(Shift.class,
1703+
Joiners.overlapping(Shift::getStart, Shift::getEnd),
1704+
Joiners.equal(Shift::getEmployee))
1705+
.expand((shift1, shift2) -> shift1.getOverlap(shift2));
1706+
}
1707+
1708+
Constraint noOverlappingShifts(ConstraintFactory constraintFactory) {
1709+
return constraintFactory.precompute(this::overlappingShifts)
1710+
.penalize(HardSoftScore.ONE_HARD, (shift1, shift2, overlap) -> overlap)
1711+
.asConstraint("No overlapping shifts");
1712+
}
1713+
----
1714+
=====
1715+
====
1716+
1717+
==== When to use precomputing
1718+
1719+
Constraint streams are already very efficient, so precomputing them may result in little change.
1720+
It can be useful in cases where the constraint stream needs to execute expensive operations,
1721+
such as filters which look up data in large collections or which compute complex calculations.
1722+
It also helps with large joins where the join condition doesn't depend on any variables.
1723+
1724+
However, only the biggest of datasets typically struggle with these issues.
1725+
Always measure move evaluation speed before and after implementing precomputing
1726+
to determine if using it brings benefits for your use case.
1727+
16251728

16261729
[#constraintStreamsTesting]
16271730
== Testing a constraint stream

0 commit comments

Comments
 (0)