Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ConstraintCollectors.toConnectedRanges: "this.splitPoint" is null #953

Closed
lukaskirner opened this issue Jul 16, 2024 · 1 comment · Fixed by #1056
Closed

ConstraintCollectors.toConnectedRanges: "this.splitPoint" is null #953

lukaskirner opened this issue Jul 16, 2024 · 1 comment · Fixed by #1056
Assignees
Labels
bug Something isn't working
Milestone

Comments

@lukaskirner
Copy link

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 lukaskirner added bug Something isn't working process/needs triage Requires initial assessment of validity, priority etc. labels Jul 16, 2024
@triceo
Copy link
Contributor

triceo commented Jul 16, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
3 participants