<commit_before>[[constraintStreams]]
= Constraint Streams Score Calculation
:doctype: book
:imagesdir: ..
:sectnums:
:toc: left
:icons: font
:experimental:


Constraint streams are an Functional Programming form of incremental score calculation in plain Java 8+
that is fast, scalable and debuggable.
The API should feel familiar if you've worked with Java 8 Streams or SQL.

[WARNING]
====
The ConstraintStreams/ConstraintProvider API is TECH PREVIEW.
It works but it has many API gaps.
Therefore, it is not rich enough yet to handle complex constraints.
====

[[constraintStreamsIntroduction]]
== Introduction

Using Java 8's Streams API, we could implement an <<easyJavaScoreCalculation,easy score calculator>>
that uses a functional approach:

[source,java,options="nowrap"]
----
    private int speakerUnavailableTimeslot() {
        int softScore = 0;
        schedule.getTalkList().stream()
                .filter(Talk::hasAnyUnavailableSpeaker)
                .forEach(talk -> {
                    softScore -= talk.getDurationInMinutes();
                });
        return softScore;
    }
----

However, that scales poorly because it doesn't do an <<incrementalScoreCalculation,incremental calculation>>:
When the planning variable of a single `Talk` changes, to recalculate the score,
the normal Streams API has to execute the entire stream from scratch.
So it doesn't do <<incrementalScoreCalculation,deltas>>.
Nor does it support <<explainingTheScore,justifications>>.

On the other hand, the ConstraintStreams API uses <<incrementalScoreCalculation,deltas>>
and supports <<explainingTheScore,justifications>>:

[source,java,options="nowrap"]
----
    private Constraint speakerUnavailableTimeslot(ConstraintFactory factory) {
        return factory.from(Talk.class)
                .filter(Talk::hasAnyUnavailableSpeaker)
                .penalizeConfigurable("Speaker unavailable timeslot",
                        HardSoftScore.ONE_SOFT,
                        Talk::getDurationInMinutes);
    }
----<commit_msg>Constraint stream docs: grouping and collectors (#561)<commit_after>[[constraintStreams]]
= Constraint Streams Score Calculation
:doctype: book
:imagesdir: ..
:sectnums:
:toc: left
:icons: font
:experimental:


Constraint streams are an Functional Programming form of incremental score calculation in plain Java 8+
that is fast, scalable and debuggable.
The API should feel familiar if you've worked with Java 8 Streams or SQL.

[WARNING]
====
The ConstraintStreams/ConstraintProvider API is TECH PREVIEW.
It works but it has many API gaps.
Therefore, it is not rich enough yet to handle complex constraints.
====

[[constraintStreamsIntroduction]]
== Introduction

Using Java 8's Streams API, we could implement an <<easyJavaScoreCalculation,easy score calculator>>
that uses a functional approach:

[source,java,options="nowrap"]
----
    private int speakerUnavailableTimeslot() {
        int softScore = 0;
        schedule.getTalkList().stream()
                .filter(Talk::hasAnyUnavailableSpeaker)
                .forEach(talk -> {
                    softScore -= talk.getDurationInMinutes();
                });
        return softScore;
    }
----

However, that scales poorly because it doesn't do an <<incrementalScoreCalculation,incremental calculation>>:
When the planning variable of a single `Talk` changes, to recalculate the score,
the normal Streams API has to execute the entire stream from scratch.
So it doesn't do <<incrementalScoreCalculation,deltas>>.
Nor does it support <<explainingTheScore,justifications>>.

On the other hand, the ConstraintStreams API uses <<incrementalScoreCalculation,deltas>>
and supports <<explainingTheScore,justifications>>:

[source,java,options="nowrap"]
----
    private Constraint speakerUnavailableTimeslot(ConstraintFactory factory) {
        return factory.from(Talk.class)
                .filter(Talk::hasAnyUnavailableSpeaker)
                .penalizeConfigurable("Speaker unavailable timeslot",
                        HardSoftScore.ONE_SOFT,
                        Talk::getDurationInMinutes);
    }
----

[[constraintStreamsGroupingAndCollectors]]
== 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`s, which provide
various aggregation functions.

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.

[source,java,options="nowrap"]
----
    private Constraint requiredCpuPowerTotal(ConstraintFactory constraintFactory) {
        return constraintFactory.from(CloudProcess.class)
                .groupBy(CloudProcess::getComputer, sum(CloudProcess::getRequiredCpuPower))
                .filter((computer, requiredCpuPower) -> requiredCpuPower > computer.getCpuPower())
                .penalize("requiredCpuPowerTotal",
                        HardSoftScore.ONE_HARD,
                        (computer, requiredCpuPower) -> requiredCpuPower - computer.getCpuPower());
    }
----

There are several collectors available out of the box. You can also provide your own collectors by implementing the
`org.optaplanner.core.api.score.stream.uni.UniConstraintCollector` interface, or its `Bi...`, `Tri...` counterparts.

[collectorsOutOfTheBox]
=== Out-of-the-box Collectors

The following section focuses on the collector implementations provided out of the box. This section only describes the
`int`-based variants of the collectors in detail. Many of the collectors also provide variants for other applicable
result data types, such as `long`, `BigDecimal` or `Duration`. You can find a complete list by exploring the
`org.optaplanner.core.api.score.stream.ConstraintCollectors` class.

[collectorsCount]
==== Collecting `count()`

The `ConstraintCollectors.count(...)` counts all elements in a 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.

[source,java,options="nowrap"]
----
    private Constraint speakerAvailability(ConstraintFactory factory) {
        return factory.from(Talk.class)
                .groupBy(Talk::hasAnyUnavailableSpeaker, count())
                .penalize("speakerAvailability",
                        HardSoftScore.ONE_HARD,
                        (hasUnavailableSpeaker, count) -> ...);
    }
----

The return value for this collector is a 32-bit signed integer (`int`). There is also a 64-bit variant, `countLong()`.

[collectorsCountDistinct]
==== Collecting `countDistinct()`

The `ConstraintCollectors.countDistinct(...)` counts any element in a 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.

[source,java,options="nowrap"]
----
    private Constraint roomCount(ConstraintFactory factory) {
        return factory.from(Talk.class)
                .groupBy(Talk::getRoom, countDistinct())
                .penalize("roomCount",
                        HardSoftScore.ONE_SOFT,
                        (room, count) -> ...);
    }
----

The return value for this collector is a 32-bit signed integer (`int`). There is also a 64-bit variant, `countLong()`.

[collectorsSum]
==== Collecting `sum()`

To sum the values of a particular property of all elements in the 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.

[source,java,options="nowrap"]
----
    private Constraint requiredCpuPowerTotal(ConstraintFactory constraintFactory) {
        return constraintFactory.from(CloudProcess.class)
                .groupBy(CloudProcess::getComputer, sum(CloudProcess::getRequiredCpuPower))
                .penalize("requiredCpuPowerTotal",
                        HardSoftScore.ONE_SOFT,
                        (computer, requiredCpuPower) -> requiredCpuPower);
    }
----

The return value for this collector is a 32-bit signed integer (`int`). There is also a 64-bit variant `sumLong()`, as
well as a `BigDecimal`-based variant `sumBigDecimal()`.

[collectorsMinMax]
==== Minimums and maximums

To extract the minimum or maximum of a 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:

[source,java,options="nowrap"]
----
    private Constraint computerWithBiggestProcess(ConstraintFactory constraintFactory) {
        return constraintFactory.from(CloudProcess.class)
                .groupBy(CloudProcess::getComputer, max(CloudProcess::getRequiredCpuPower))
                .penalize("computerWithBiggestProcess",
                        HardSoftScore.ONE_HARD,
                        computer -> ...);
    }
----

{'fraction_non_alphanumeric': 0.09101430976430977, 'fraction_numerical': 0.00242003367003367, 'mean_word_length': 3.500473484848485, 'pattern_counts': {'":': 0, '<': 27, '<?xml version=': 0, '>': 36, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4234024', 'n_tokens_mistral': 2531, 'n_tokens_neox': 2351, 'n_words': 862}