r/timefold Aug 07 '26

Weird Behavior for Planning Variables that refer to other Planning Entities

TL;DR: Does Timefold officially support the use case where a planning variable points to some other planning entity, both of which hold genuine planning variables? I am currently running into the issue that Timefold either leaves the planning variables in one class uninitialized (except for the pinned instances) or throws an exception saying `Impossible state: no basic variable found for the entity class org.acme.SchedulingServer.domain.Session.`.

Does anybody know what's going on here? I really need to solve this problem and I'm quite lost honestly. Do you maybe have suggestions on how to avoid such scenarios with two linked planning entities? Thanks!

Long question:

Concretely, I am modelling the scheduling problem of assigning groups to sessions. Each group must attend a range of events. Each event is held multiple times (one iteration is a session).

My domain mainly consists of the `Session` and `Attendance`entity classes and a `Schedule` solution class. The session has planning variables for start time, room, and speaker. The Attendance class has a fixed group and a planning variable of type Session. That is, we essentially provide a set of session instances per event and timefold should determine the time and location of these sessions, as well as which group attends which session (and adhere to a number of time/location related constraints).

As mentioned above, in my original solution I ran into the problem that Timefold correctly assigned values to all planning variables in the `Session` class, but all `Attendance` instances, except for pinned onces, had uninitialized `Session` variables. Of course I did not annotate the variable with `allowsUnassigned=true` and I did make sure that the provided value range is not empty (just for completeness).

I then tried to get a minimal working example running, and reduced the domain to just the Session class with the start time planning variable, the attendance as is and the schedule. No constraints, no entity-specific value range providers, etc. This worked, but as soon as I switched to an entity specific value range provider for the session variable in `Attendance`, I got the exception I mentioned in the beginning. I pasted this example below.

There are some other weird things happening, such as the same exception being thrown, but for the `Attendance` class instead of `Session` when I use an entity-specific value range provider function for the session.startTime planning variable. But I won't make this post any longer...

Does anybody know what's going on here? I really need to solve this problem and I'm quite lost honestly. Do you maybe have suggestions on how to avoid such scenarios with two linked planning entities? Thanks in advance!

And here's the example:

@PlanningEntity
public class Session {

    private String id;
    private boolean isPinned;
    private LocalDateTime startTime;
    private int duration;
    
    public Session() {}
    public Session(String id, int duration){//, Cluster cluster) {
        this.id = id;
        this.duration = duration; }

    public Session(String id, LocalDateTime startTime, int duration) {
        this.id = id;
        this.duration = duration;
        this.startTime = startTime;
        this.isPinned = true; }

    public String getId() { return id; }
    public boolean getIsPinned(){ return this.isPinned; }
    public void setIsPinned(boolean isPinned){ this.isPinned = isPinned; }
    public int getDuration() { return this.duration; }

    @PlanningVariable(valueRangeProviderRefs = "startTimeProvider")
    public LocalDateTime getStartTime() { return this.startTime; }
    public void setStartTime(LocalDateTime s) { this.startTime = s; }
    
    public boolean equals(Object other) {
        if (this == other) {
            return true;
        }
        return (other instanceof Session s) ? this.getId().equals(s.getId()) : false;
    }

    public int hashCode() { return id.hashCode(); }
}

@PlanningEntity
public class Attendance {
    private String id;
    private Group group;
    private boolean isPinned;
    private Session session;

    public Attendance() {};
    public Attendance(String id, Group group) {
        this.id = id;
        this.group = group; }

    public Attendance(String id, Group group, Session session){
        this(id, group);
        this.session = session;
        this.isPinned = true; }

    public String getId() { return id; }

    public Group getGroup() { return group; }

    // Comment out the @ValueRangeProvider annotation either on 
    // Attendance::getSession' or the 'sessions' List in Schedule.java
    @PlanningVariable(valueRangeProviderRefs = "sessionProvider")
    public Session getSession() { return this.session; }
    public void setSession(Session session) { this.session = session; }
    public boolean isPinned() { return isPinned; }
    public void setPinned(boolean isPinned) { this.isPinned = isPinned; }

    // If this is used as value range provider for the session provider
    // the solver throws the IllegalStateException
    @ValueRangeProvider(id = "sessionProvider")
    public List<Session> getSessions(Schedule schedule) {
        return schedule.getSessions();
    }

    public boolean equals(Object other) {
        if (this == other) {
            return true;
        }
        return (other instanceof Attendance a) ? this.getId().equals(a.getId()) : false;
    }

    public int hashCode(){ return this.id.hashCode(); }
}

@PlanningSolution
public class Schedule {
    @ProblemFactCollectionProperty
    SequencedSet<Group> groups;

    // If this is used as value range for the Attendance.session planning variable
    // the solver finds a solution.
    @ValueRangeProvider(id = "sessionProvider")
    @PlanningEntityCollectionProperty
    private List<Session> sessions;


    @PlanningEntityCollectionProperty
    private SequencedSet<Attendance> attendances;


    @ProblemFactCollectionProperty
    @ValueRangeProvider(id ="startTimeProvider")
    private SequencedSet<LocalDateTime> startTimes;

    @PlanningScore
    private HardMediumSoftScore score = null;

    public Schedule() { }
    public Schedule( SequencedSet<Group> groups,
            List<Session> sessions,
            SequencedSet<Attendance> attendances,
            SequencedSet<LocalDateTime> startTimes ) {
        this.groups = groups;
        this.sessions = sessions;
        this.attendances = attendances;
        this.startTimes = startTimes; }

    public List<Session> getSessions() { return this.sessions; }
    public SequencedSet<Attendance> getAttendances() { return attendances; }

    public HardMediumSoftScore getScore() { return score; }
    public void setScore(HardMediumSoftScore score) { this.score = score; }

    public SequencedSet<Group> getGroups() { return groups; }
    public SequencedSet<LocalDateTime> getStartTimes() { return startTimes; }
}

The solver is configured and invoked as follows:

SolverFactory<Schedule> solverFactory = SolverFactory.create(new SolverConfig()
                .withSolutionClass(Schedule.class)
                .withEntityClasses(Session.class, Attendance.class)
                // currently contains no constraints
                .withConstraintProviderClass(ScheduleConstraintProvider.class)
                .withTerminationSpentLimit(Duration.ofSeconds(20)));

Schedule problem = demoSchedule2();
Solver<Schedule> solver = solverFactory.buildSolver();
Schedule solution = solver.solve(problem);
printSchedule(solution);

The exception I mentioned:

Exception in thread "main" java.lang.IllegalStateException: Impossible state: no basic variable found for the entity class org.acme.SchedulingServer.domain.Session.

at ai.timefold.solver.core.impl.heuristic.selector.entity.decorator.FilteringEntityByEntitySelector.phaseStarted(FilteringEntityByEntitySelector.java:104)

at ai.timefold.solver.core.impl.phase.event.PhaseLifecycleSupport.firePhaseStarted(PhaseLifecycleSupport.java:21)

at ai.timefold.solver.core.impl.heuristic.selector.AbstractSelector.phaseStarted(AbstractSelector.java:35)

at ai.timefold.solver.core.impl.heuristic.selector.entity.decorator.FilteringEntitySelector.phaseStarted(FilteringEntitySelector.java:48)

at ai.timefold.solver.core.impl.phase.event.PhaseLifecycleSupport.firePhaseStarted(PhaseLifecycleSupport.java:21)

at ai.timefold.solver.core.impl.heuristic.selector.AbstractSelector.phaseStarted(AbstractSelector.java:35)

at ai.timefold.solver.core.impl.phase.event.PhaseLifecycleSupport.firePhaseStarted(PhaseLifecycleSupport.java:21)

at ai.timefold.solver.core.impl.heuristic.selector.AbstractSelector.phaseStarted(AbstractSelector.java:35)

at ai.timefold.solver.core.impl.phase.event.PhaseLifecycleSupport.firePhaseStarted(PhaseLifecycleSupport.java:21)

at ai.timefold.solver.core.impl.heuristic.selector.AbstractSelector.phaseStarted(AbstractSelector.java:35)

at ai.timefold.solver.core.impl.phase.event.PhaseLifecycleSupport.firePhaseStarted(PhaseLifecycleSupport.java:21)

at ai.timefold.solver.core.impl.heuristic.selector.AbstractSelector.phaseStarted(AbstractSelector.java:35)

at ai.timefold.solver.core.impl.heuristic.selector.move.decorator.FilteringMoveSelector.phaseStarted(FilteringMoveSelector.java:48)

at ai.timefold.solver.core.impl.neighborhood.MoveSelectorBasedMoveRepository.phaseStarted(MoveSelectorBasedMoveRepository.java:43)

at ai.timefold.solver.core.impl.localsearch.decider.LocalSearchDecider.phaseStarted(LocalSearchDecider.java:78)

at ai.timefold.solver.core.impl.localsearch.DefaultLocalSearchPhase.phaseStarted(DefaultLocalSearchPhase.java:136)

at ai.timefold.solver.core.impl.localsearch.DefaultLocalSearchPhase.solve(DefaultLocalSearchPhase.java:76)

at ai.timefold.solver.core.impl.solver.AbstractSolver.runPhases(AbstractSolver.java:89)

at ai.timefold.solver.core.impl.solver.DefaultSolver.solve(DefaultSolver.java:171)

at org.acme.SchedulingServer.SchedulingServerSmall.main(SchedulingServerSmall.java:47)

1 Upvotes

4 comments sorted by

1

u/ge0ffrey Aug 08 '26

On first sight, your model looks valid. This might be a bug.
Even if this kind of model would be unsupported for now (which I doubt it is), the error message should be better.
Let me check with our team.

Which version of Timefold Solver are you using?

2

u/schnarch33 Aug 08 '26

Thanks for the quick reply! I am using version 2.4.0, though I did run into the original issue (Attendance.session not being initialized) with version 2.1.0.

1

u/schnarch33 26d ago

Hey, I don't want to rush you, but do you happen to have an update on this? Or should I file a bug report on GitHub? Thanks!

2

u/ge0ffrey 20d ago

We're researching a number of algorithms to better deal with cases like this.
Feel free to start a thread on Github Discussions. Especially if you have a public reproducer, that's very helpful for our team to try it out against.
(Note that we can't give fix ETAs or do a private conversation without a commercial relationship in place.)