Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package ai.timefold.solver.core.api.score.stream.common;

import java.util.Collection;
import java.util.function.ToIntBiFunction;
import java.util.function.ToIntFunction;

import org.jspecify.annotations.NonNull;

/**
Expand Down Expand Up @@ -46,6 +50,26 @@ public interface ConnectedRange<Range_, Point_ extends Comparable<Point_>, Diffe
*/
int getMaximumOverlap();

/**
* Get the maximum sum of a function amongst distinct ranges of overlapping values
* amongst all points contained by this {@link ConnectedRange}.
*
* @return get the maximum sum of a function amongst distinct ranges of overlapping values
* for any point contained by this {@link ConnectedRange}.
*/
int getMaximumValue(ToIntFunction<? super Range_> functionSupplier);

/**
* Get the maximum sum of a function amongst distinct ranges of overlapping values
* amongst all points contained by this {@link ConnectedRange}. This method allows you to use
* a function that takes all active ranges as an input. Use {@link ::getMaximumValue} if possible
* for efficiency.
*
* @return get the maximum sum of a function amongst distinct ranges of overlapping values
* for any point contained by this {@link ConnectedRange}.
*/
int getMaximumValueForDistinctRanges(ToIntBiFunction<Collection<? super Range_>, Difference_> functionSupplier);

/**
* Get the length of this {@link ConnectedRange}.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package ai.timefold.solver.core.api.score.stream.common;

import java.util.Collection;
import java.util.function.ToIntBiFunction;
import java.util.function.ToIntFunction;

import org.jspecify.annotations.NonNull;

/**
Expand All @@ -24,4 +28,24 @@ public interface ConnectedRangeChain<Range_, Point_ extends Comparable<Point_>,
*/
@NonNull
Iterable<RangeGap<Point_, Difference_>> getGaps();

/**
* Get the maximum sum of a function amongst distinct ranges of overlapping values
* amongst all points contained by this {@link ConnectedRange}.
*
* @return get the maximum sum of a function amongst distinct ranges of overlapping values
* for any point contained by this {@link ConnectedRange}.
*/
int getMaximumValue(ToIntFunction<? super Range_> functionSupplier);

/**
* Get the maximum sum of a function amongst distinct ranges of overlapping values
* amongst all points contained by this {@link ConnectedRange}. This method allows you to use
* a function that takes all active ranges as an input. Use {@link ::getMaximumValue} if possible
* for efficiency.
*
* @return get the maximum sum of a function amongst distinct ranges of overlapping values
* for any point contained by this {@link ConnectedRange}.
*/
int getMaximumValueForDistinctRanges(ToIntBiFunction<Collection<? super Range_>, Difference_> functionSupplier);
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package ai.timefold.solver.core.impl.score.stream.collector.connected_ranges;

import java.util.Collection;
import java.util.NavigableMap;
import java.util.NavigableSet;
import java.util.Objects;
import java.util.TreeMap;
import java.util.function.BiFunction;
import java.util.function.ToIntBiFunction;
import java.util.function.ToIntFunction;

import ai.timefold.solver.core.api.score.stream.common.ConnectedRange;
import ai.timefold.solver.core.api.score.stream.common.ConnectedRangeChain;
Expand Down Expand Up @@ -251,6 +254,38 @@ void removeRange(Range<Range_, Point_> range) {
return (Iterable) startSplitPointToNextGap.values();
}

/**
* Get the maximum sum of a function amongst distinct ranges of overlapping values
* amongst all points contained by this {@link ConnectedRangeChain}.
*
* @return get the maximum sum of a function amongst distinct ranges of overlapping values
* for any point contained by this {@link ConnectedRangeChain}.
*/
public int getMaximumValue(ToIntFunction<? super Range_> functionSupplier) {
var max = 0;
for (ConnectedRange<Range_, Point_, Difference_> range : getConnectedRanges()) {
max = Math.max(range.getMaximumValue(functionSupplier), max);
}
return max;
}

/**
* Get the maximum sum of a function amongst distinct ranges of overlapping values
* amongst all points contained by this {@link ConnectedRangeChain}. This method allows you to use
* a function that takes all active ranges as an input. Use {@link ::getMaximumValue} if possible
* for efficiency.
*
* @return get the maximum sum of a function amongst distinct ranges of overlapping values
* for any point contained by this {@link ConnectedRangeChain}.
*/
public int getMaximumValueForDistinctRanges(ToIntBiFunction<Collection<? super Range_>, Difference_> functionSupplier) {
var max = 0;
for (ConnectedRange<Range_, Point_, Difference_> range : getConnectedRanges()) {
max = Math.max(range.getMaximumValueForDistinctRanges(functionSupplier), max);
}
return max;
}

@Override
public boolean equals(Object o) {
if (this == o)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package ai.timefold.solver.core.impl.score.stream.collector.connected_ranges;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.NavigableSet;
import java.util.Objects;
import java.util.function.BiFunction;
import java.util.function.ToIntBiFunction;
import java.util.function.ToIntFunction;

import ai.timefold.solver.core.api.score.stream.common.ConnectedRange;

Expand Down Expand Up @@ -134,6 +139,43 @@ public int getMaximumOverlap() {
return maximumOverlap;
}

@Override
public int getMaximumValue(ToIntFunction<? super Range_> functionSupplier) {
var current = startSplitPoint;
var activeRangeCount = 0;
var maxValue = 0;
var activeValue = 0;
do {
activeRangeCount += current.rangesStartingAtSplitPointSet.size() - current.rangesEndingAtSplitPointSet.size();
activeValue += current.rangesStartingAtSplitPointSet.stream().map(Range::getValue).mapToInt(functionSupplier).sum();
activeValue -= current.rangesEndingAtSplitPointSet.stream().map(Range::getValue).mapToInt(functionSupplier).sum();
maxValue = Math.max(maxValue, activeValue);
current = splitPointSet.higher(current);
} while (activeRangeCount > 0 && current != null);
return maxValue;
}

@Override
public int getMaximumValueForDistinctRanges(ToIntBiFunction<Collection<? super Range_>, Difference_> functionSupplier) {
var current = startSplitPoint;
var activeValue = 0;
var activeRanges = new ArrayList<>();
var next = splitPointSet.higher(current);
while (next != null) {
activeRanges.addAll(current.rangesStartingAtSplitPointSet);
activeRanges.removeAll(current.rangesEndingAtSplitPointSet);
activeValue = Math.max(
functionSupplier.applyAsInt(
Collections.unmodifiableList(activeRanges),
differenceFunction.apply(current.splitPoint, next.splitPoint)),
activeValue);
current = next;
next = splitPointSet.higher(current);
}
;
return activeValue;
}

@Override
public @NonNull Point_ getStart() {
return startSplitPoint.splitPoint;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static org.assertj.core.api.Assertions.assertThat;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -67,6 +68,10 @@ private ConnectedRangeTracker<TestRange, Integer, Integer> getIntegerConnectedRa
return new ConnectedRangeTracker<>(TestRange::getStart, TestRange::getEnd, (a, b) -> b - a);
}

static int rangeMaxFunction(Collection<? super TestRange> ranges, int length) {
return 100 * ranges.size() / length;
}

@Test
void testNonConsecutiveRanges() {
ConnectedRangeTracker<TestRange, Integer, Integer> tree = getIntegerConnectedRangeTracker();
Expand Down Expand Up @@ -96,6 +101,10 @@ void testNonConsecutiveRanges() {
assertThat(connectedRangeList.get(2).getMinimumOverlap()).isEqualTo(1);
assertThat(connectedRangeList.get(2).getMaximumOverlap()).isEqualTo(1);

assertThat(connectedRangeList.get(2).getMaximumValue(i -> 1)).isEqualTo(1);
assertThat(connectedRangeList.get(2).getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction))
.isEqualTo(50);

verifyGaps(tree);
}

Expand All @@ -116,6 +125,14 @@ void testConsecutiveRanges() {
assertThat(connectedRangeList.get(0)).containsExactly(new TestRange(0, 2), new TestRange(2, 4), new TestRange(4, 7));
assertThat(connectedRangeList.get(0).getMinimumOverlap()).isEqualTo(1);
assertThat(connectedRangeList.get(0).getMaximumOverlap()).isEqualTo(1);
assertThat(connectedRangeList.get(0).getMaximumValue(i -> 1)).isEqualTo(1);
assertThat(connectedRangeList.get(0).getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction))
.isEqualTo(50);

assertThat(tree.getConnectedRangeChain().getMaximumValue(i -> 1)).isEqualTo(1);
assertThat(tree.getConnectedRangeChain().getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction))
.isEqualTo(50);

verifyGaps(tree);
}

Expand All @@ -135,9 +152,20 @@ void testDuplicateRanges() {
assertThat(connectedRangeList.get(0)).containsExactly(a.getValue(), a.getValue());
assertThat(connectedRangeList.get(0).getMinimumOverlap()).isEqualTo(2);
assertThat(connectedRangeList.get(0).getMaximumOverlap()).isEqualTo(2);
assertThat(connectedRangeList.get(0).getMaximumValue(i -> 1)).isEqualTo(2);
assertThat(connectedRangeList.get(0).getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction))
.isEqualTo(100);
assertThat(connectedRangeList.get(1)).containsExactly(b.getValue());
assertThat(connectedRangeList.get(1).getMinimumOverlap()).isEqualTo(1);
assertThat(connectedRangeList.get(1).getMaximumOverlap()).isEqualTo(1);
assertThat(connectedRangeList.get(1).getMaximumValue(i -> 1)).isEqualTo(1);
assertThat(connectedRangeList.get(1).getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction))
.isEqualTo(33);

assertThat(tree.getConnectedRangeChain().getMaximumValue(i -> 1)).isEqualTo(2);
assertThat(tree.getConnectedRangeChain().getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction))
.isEqualTo(100);

verifyGaps(tree);
}

Expand Down Expand Up @@ -221,16 +249,29 @@ void testOverlappingRange() {
assertThat(connectedRanges.get(0).hasOverlap()).isTrue();
assertThat(connectedRanges.get(0).getMinimumOverlap()).isEqualTo(1);
assertThat(connectedRanges.get(0).getMaximumOverlap()).isEqualTo(2);
assertThat(connectedRanges.get(0).getMaximumValue(i -> 1)).isEqualTo(2);
assertThat(connectedRanges.get(0).getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction))
.isEqualTo(200);

assertThat(connectedRanges.get(1)).containsExactly(d.getValue());
assertThat(connectedRanges.get(1).hasOverlap()).isFalse();
assertThat(connectedRanges.get(1).getMinimumOverlap()).isEqualTo(1);
assertThat(connectedRanges.get(1).getMaximumOverlap()).isEqualTo(1);
assertThat(connectedRanges.get(1).getMaximumValue(i -> 1)).isEqualTo(1);
assertThat(connectedRanges.get(1).getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction))
.isEqualTo(100);

assertThat(connectedRanges.get(2)).containsExactly(e.getValue(), removedTestRange2);
assertThat(connectedRanges.get(2).hasOverlap()).isTrue();
assertThat(connectedRanges.get(2).getMinimumOverlap()).isEqualTo(2);
assertThat(connectedRanges.get(2).getMaximumOverlap()).isEqualTo(2);
assertThat(connectedRanges.get(2).getMaximumValue(i -> 1)).isEqualTo(2);
assertThat(connectedRanges.get(2).getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction))
.isEqualTo(100);

assertThat(tree.getConnectedRangeChain().getMaximumValue(i -> 1)).isEqualTo(2);
assertThat(tree.getConnectedRangeChain().getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction))
.isEqualTo(200);

verifyGaps(tree);

Expand All @@ -247,16 +288,29 @@ void testOverlappingRange() {
assertThat(connectedRanges.get(0).hasOverlap()).isFalse();
assertThat(connectedRanges.get(0).getMinimumOverlap()).isEqualTo(1);
assertThat(connectedRanges.get(0).getMaximumOverlap()).isEqualTo(1);
assertThat(connectedRanges.get(0).getMaximumValue(i -> 1)).isEqualTo(1);
assertThat(connectedRanges.get(0).getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction))
.isEqualTo(100);

assertThat(connectedRanges.get(1)).containsExactly(d.getValue());
assertThat(connectedRanges.get(1).hasOverlap()).isFalse();
assertThat(connectedRanges.get(1).getMinimumOverlap()).isEqualTo(1);
assertThat(connectedRanges.get(1).getMaximumOverlap()).isEqualTo(1);
assertThat(connectedRanges.get(1).getMaximumValue(i -> 1)).isEqualTo(1);
assertThat(connectedRanges.get(1).getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction))
.isEqualTo(100);

assertThat(connectedRanges.get(2)).containsExactly(e.getValue(), removedTestRange2);
assertThat(connectedRanges.get(2).hasOverlap()).isTrue();
assertThat(connectedRanges.get(2).getMinimumOverlap()).isEqualTo(2);
assertThat(connectedRanges.get(2).getMaximumOverlap()).isEqualTo(2);
assertThat(connectedRanges.get(2).getMaximumValue(i -> 1)).isEqualTo(2);
assertThat(connectedRanges.get(2).getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction))
.isEqualTo(100);

assertThat(tree.getConnectedRangeChain().getMaximumValue(i -> 1)).isEqualTo(2);
assertThat(tree.getConnectedRangeChain().getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction))
.isEqualTo(100);

verifyGaps(tree);

Expand All @@ -272,16 +326,29 @@ void testOverlappingRange() {
assertThat(connectedRanges.get(0).hasOverlap()).isFalse();
assertThat(connectedRanges.get(0).getMinimumOverlap()).isEqualTo(1);
assertThat(connectedRanges.get(0).getMaximumOverlap()).isEqualTo(1);
assertThat(connectedRanges.get(0).getMaximumValue(i -> 1)).isEqualTo(1);
assertThat(connectedRanges.get(0).getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction))
.isEqualTo(100);

assertThat(connectedRanges.get(1)).containsExactly(d.getValue());
assertThat(connectedRanges.get(1).hasOverlap()).isFalse();
assertThat(connectedRanges.get(1).getMinimumOverlap()).isEqualTo(1);
assertThat(connectedRanges.get(1).getMaximumOverlap()).isEqualTo(1);
assertThat(connectedRanges.get(1).getMaximumValue(i -> 1)).isEqualTo(1);
assertThat(connectedRanges.get(1).getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction))
.isEqualTo(100);

assertThat(connectedRanges.get(2)).containsExactly(e.getValue());
assertThat(connectedRanges.get(2).hasOverlap()).isFalse();
assertThat(connectedRanges.get(2).getMinimumOverlap()).isEqualTo(1);
assertThat(connectedRanges.get(2).getMaximumOverlap()).isEqualTo(1);
assertThat(connectedRanges.get(2).getMaximumValue(i -> 1)).isEqualTo(1);
assertThat(connectedRanges.get(2).getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction))
.isEqualTo(50);

assertThat(tree.getConnectedRangeChain().getMaximumValue(i -> 1)).isEqualTo(1);
assertThat(tree.getConnectedRangeChain().getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction))
.isEqualTo(100);

verifyGaps(tree);
Range<TestRange, Integer> g = tree.getRange(new TestRange(6, 7));
Expand All @@ -298,6 +365,10 @@ void testOverlappingRange() {
assertThat(connectedRanges.get(1).hasOverlap()).isFalse();
assertThat(connectedRanges.get(1).getMinimumOverlap()).isEqualTo(1);
assertThat(connectedRanges.get(1).getMaximumOverlap()).isEqualTo(1);

assertThat(tree.getConnectedRangeChain().getMaximumValue(i -> 1)).isEqualTo(1);
assertThat(tree.getConnectedRangeChain().getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction))
.isEqualTo(100);
}

void verifyGaps(ConnectedRangeTracker<TestRange, Integer, Integer> tree) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1066,6 +1066,37 @@ Java::
----
====

In the event that data about distinct ranges of overlapping values is needed beyond the total count, `ConnectedRange` provides methods to calculate a max sum
of an integer function over each distinct arrangement of overlapping entities:

[tabs]
====
Java::
+
[source,java,options="nowrap"]
----
.groupBy(Job::getEquipment,
ConstraintCollectors.toConnectedRanges(
Job::getStart,
Job::getEnd
)
).expand(
connectedRangeChain -> connectedRangeChain.getMaximumValueForDistinctRanges(
(activeJobList, duration) -> {
var required = activeJobList.stream().mapToInt(Job::getCapacityRequired).sum()
var capacity = equipment.getCapacity() - (activeJobList.size() - 1); // Lose a little capacity per additional job
return Math.max(0, capacity - required);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could do something like multiplying by the duration here to get a scaled penalty

}
)
)
.filter((connectedRangeChain, amountOverCapacity) -> amountOverCapacity > 0)
.penalize((connectedRangeChain, amountOverCapacity) -> amountOverCapacity)
)
)
----
====


[#collectorsLoadBalance]
==== Load balancing collectors

Expand Down