timefold-solver icon indicating copy to clipboard operation
timefold-solver copied to clipboard

ConstraintCollectors.toConnectedRanges: "this.splitPoint" is null

Open lukaskirner opened this issue 1 year ago • 1 comments

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)

lukaskirner avatar Jul 16 '24 12:07 lukaskirner

Hello @lukaskirner and thank you for reporting the issue! We'll investigate when time permits.

triceo avatar Jul 16 '24 13:07 triceo