timefold-solver
timefold-solver copied to clipboard
ConstraintCollectors.toConnectedRanges: "this.splitPoint" is null
Describe the bug
By using the new ConstraintCollectors.toConnectedRanges we always run into the exception where the splitPoint == nullwhich crashes the entire application. To reproduce I recreated the example from the documentation, which has the same issue.
Expected behavior Runs without exception.
Actual behavior Crashes with:
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Comparable.compareTo(Object)" because "this.splitPoint" is null
at ai.timefold.solver.core.impl.score.stream.collector.connected_ranges.RangeSplitPoint.compareTo(RangeSplitPoint.java:109)
at ai.timefold.solver.core.impl.score.stream.collector.connected_ranges.RangeSplitPoint.compareTo(RangeSplitPoint.java:9)
at java.base/java.util.TreeMap.compare(TreeMap.java:1604)
at java.base/java.util.TreeMap.getFloorEntry(TreeMap.java:463)
at java.base/java.util.TreeMap.floorKey(TreeMap.java:1045)
at java.base/java.util.TreeSet.floor(TreeSet.java:426)
at ai.timefold.solver.core.impl.score.stream.collector.connected_ranges.ConnectedRangeTracker.remove(ConnectedRangeTracker.java:80)
at ai.timefold.solver.core.impl.score.stream.collector.ConnectedRangesCalculator.retract(ConnectedRangesCalculator.java:30)
at ai.timefold.solver.core.impl.score.stream.collector.bi.ObjectCalculatorBiCollector.lambda$accumulator$0(ObjectCalculatorBiCollector.java:26)
at ai.timefold.solver.core.impl.score.stream.bavet.common.AbstractGroupNode.retract(AbstractGroupNode.java:261)
at ai.timefold.solver.core.impl.score.stream.bavet.common.StaticPropagationQueue.propagate(StaticPropagationQueue.java:93)
at ai.timefold.solver.core.impl.score.stream.bavet.common.StaticPropagationQueue.propagateRetracts(StaticPropagationQueue.java:83)
at ai.timefold.solver.core.impl.score.stream.bavet.common.Propagator.propagateEverything(Propagator.java:64)
at ai.timefold.solver.core.impl.score.stream.bavet.BavetConstraintSession.calculateScoreInLayer(BavetConstraintSession.java:92)
at ai.timefold.solver.core.impl.score.stream.bavet.BavetConstraintSession.calculateScore(BavetConstraintSession.java:83)
at ai.timefold.solver.core.impl.score.director.stream.BavetConstraintStreamScoreDirector.calculateScore(BavetConstraintStreamScoreDirector.java:49)
at ai.timefold.solver.core.impl.score.director.AbstractScoreDirector.assertExpectedUndoMoveScore(AbstractScoreDirector.java:642)
at ai.timefold.solver.core.impl.constructionheuristic.decider.ConstructionHeuristicDecider.doMove(ConstructionHeuristicDecider.java:138)
at ai.timefold.solver.core.impl.constructionheuristic.decider.ConstructionHeuristicDecider.decideNextStep(ConstructionHeuristicDecider.java:107)
at ai.timefold.solver.core.impl.constructionheuristic.DefaultConstructionHeuristicPhase.solve(DefaultConstructionHeuristicPhase.java:62)
at ai.timefold.solver.core.impl.solver.AbstractSolver.runPhases(AbstractSolver.java:82)
at ai.timefold.solver.core.impl.solver.DefaultSolver.solve(DefaultSolver.java:200)
at <mypackage>.ConnectedRangeDemo.main(ConnectedRangeDemo.java:22)
To Reproduce
public class Equipment {
private int id;
private int capacity;
public Equipment(int id, int capacity) {
this.id = id;
this.capacity = capacity;
}
public int getId() { return id; }
public int getCapacity() { return capacity; }
}
@PlanningEntity
public class Job {
private int id;
private int requiredEquipmentId;
private Integer start;
public Job() {}
public Job(int id, int requiredEquipmentId) {
this.id = id;
this.requiredEquipmentId = requiredEquipmentId;
}
@PlanningId
public int getId() { return id; }
public int getRequiredEquipmentId() { return requiredEquipmentId; }
@PlanningVariable
public Integer getStart() { return start; }
public void setStart(Integer start) { this.start = start; }
public Integer getEnd() { return start == null ? null : start + 10; }
}
@PlanningSolution
public class Planner {
@PlanningScore
private HardMediumSoftScore score;
@ProblemFactCollectionProperty
private final List<Equipment> equipments = new ArrayList<>();
@PlanningEntityCollectionProperty
private final List<Job> jobs = new ArrayList<>();
@ValueRangeProvider
public CountableValueRange<Integer> getStartOffsetRange() {
return ValueRangeFactory.createIntValueRange(0, 100);
}
public Planner() {}
public Planner(List<Equipment> equipments, List<Job> jobs) {
this.equipments.addAll(equipments);
this.jobs.addAll(jobs);
}
}
public class MyConstraintProvider implements ConstraintProvider {
public MyConstraintProvider() {}
@Override
public Constraint[] defineConstraints(ConstraintFactory constraintFactory) {
return new Constraint[]{doNotOverAssignEquipment(constraintFactory)};
}
public Constraint doNotOverAssignEquipment(ConstraintFactory constraintFactory) {
return constraintFactory.forEach(Equipment.class)
.join(Job.class, Joiners.equal(Equipment::getId, Job::getRequiredEquipmentId))
.groupBy((equipment, job) -> equipment, ConstraintCollectors.toConnectedRanges((equipment, job) -> job,
Job::getStart,
Job::getEnd,
(a, b) -> b - a))
.flattenLast(ConnectedRangeChain::getConnectedRanges)
.filter((equipment, connectedRange) -> connectedRange.getMaximumOverlap() > equipment.getCapacity())
.penalize(HardMediumSoftScore.ONE_HARD)
.asConstraint("Concurrent equipment usage over capacity");
}
}
public class ConnectedRangeDemo {
public static void main(String[] args) {
var e1 = new Equipment(1, 1);
var j1 = new Job(1, e1.getId());
var j2 = new Job(2, e1.getId());
var problem = new Planner(List.of(e1), List.of(j1, j2));
var config = new SolverConfig()
.withSolutionClass(Planner.class)
.withEntityClasses(Job.class)
.withConstraintProviderClass(MyConstraintProvider.class)
.withEnvironmentMode(EnvironmentMode.FULL_ASSERT);
var solver = SolverFactory.create(config).buildSolver();
solver.solve(problem);
}
}
Environment
Timefold Solver Version or Git ref:
Tested on 1.12.0 and 1.10.0
Output of java -version:
openjdk version "21.0.1" 2023-10-17 LTS
OpenJDK Runtime Environment Temurin-21.0.1+12 (build 21.0.1+12-LTS)
OpenJDK 64-Bit Server VM Temurin-21.0.1+12 (build 21.0.1+12-LTS, mixed mode)
Hello @lukaskirner and thank you for reporting the issue! We'll investigate when time permits.