javatimefoldplanning

simple timefold planning task


Can you please help me to understand the Timefold? I`m trying to setup a pretty simple problem: have 'processes' with the start moment defined with int and the relation 'link' between them. relation means that the 'consuming' process cannot start earlier than the 'producer' starts. but when I do execute it, both processes starts at 0.

@PlanningEntity
public class Process {
   private int id;
   private Integer startDate;

   public Process() {
   }

   public Process(int id, Integer startDate) {
       this.id = id;
       this.startDate = startDate;
   }

   public int getId() {
       return id;
   }

   public void setId(int id) {
       this.id = id;
   }

   @PlanningVariable(valueRangeProviderRefs = "startDateRange")
   public Integer getStartDate() {
       return startDate;
   }

   public void setStartDate(Integer startDate) {
       this.startDate = startDate;
   }
}


public class Link {
   private Process source;
   private Process target;

   public Link() {
   }

   public Link(Process source, Process target) {
       this.source = source;
       this.target = target;
   }

   public Process getSource() {
       return source;
   }

   public void setSource(Process source) {
       this.source = source;
   }

   public Process getTarget() {
       return target;
   }

   public void setTarget(Process target) {
       this.target = target;
   }
}

@PlanningSolution
public class Schedule {
   private List<Process> processList;
   private List<Link> linkList;
   private HardSoftScore score;
   private List<Integer> startDateRange;

   public Schedule() {
   }

   public Schedule(List<Process> processList, List<Link> linkList, List<Integer> startDateRange) {
       this.processList = processList;
       this.linkList = linkList;
       this.startDateRange = startDateRange;
   }

   @PlanningEntityCollectionProperty
   public List<Process> getProcessList() {
       return processList;
   }

   public void setProcessList(List<Process> processList) {
       this.processList = processList;
   }

   @ProblemFactCollectionProperty
   public List<Link> getLinkList() {
       return linkList;
   }

   public void setLinkList(List<Link> linkList) {
       this.linkList = linkList;
   }

   @ValueRangeProvider(id = "startDateRange")
   public List<Integer> getStartDateRange() {
       return startDateRange;
   }

   public void setStartDateRange(List<Integer> startDateRange) {
       this.startDateRange = startDateRange;
   }

   @ai.timefold.solver.core.api.domain.solution.PlanningScore
   public HardSoftScore getScore() {
       return score;
   }

   public void setScore(HardSoftScore score) {
       this.score = score;
   }
}

public class TimetableConstraintProvider implements ConstraintProvider {
   @Override
   public Constraint[] defineConstraints(ConstraintFactory constraintFactory) {
       return new Constraint[]{
           processOrderConstraint(constraintFactory)
       };
   }

   private Constraint processOrderConstraint(ConstraintFactory constraintFactory) {
       return constraintFactory.forEach(Link.class)
           .penalize("Process order constraint", HardSoftScore.ONE_HARD,
               link -> {
                   Integer sourceStartDate = link.getSource().getStartDate();
                   Integer targetStartDate = link.getTarget().getStartDate();
                   if (sourceStartDate == null || targetStartDate == null) {
                       return 0;  // No penalty if either start date is null
                   }
                   return sourceStartDate >= targetStartDate ? 1 : 0;
               });
   }
}

public class TimetableApp {
   public static void main(String[] args) {
       Process p1 = new Process(1, null);
       Process p2 = new Process(2, null);
       Link link = new Link(p1, p2);

       List<Integer> startDateRange = Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

       Schedule unsolvedSchedule = new Schedule(
           Arrays.asList(p1, p2),
           Arrays.asList(link),
           startDateRange
       );

       SolverFactory<Schedule> solverFactory = 
           SolverFactory.create(new SolverConfig()
               .withSolutionClass(Schedule.class)
               .withEntityClasses(Process.class)
               .withConstraintProviderClass(TimetableConstraintProvider.class)
               .withTerminationSpentLimit(Duration.ofSeconds(100)));
       Solver<Schedule> solver = solverFactory.buildSolver();

       Schedule solvedSchedule = solver.solve(unsolvedSchedule);

       for (Process process : solvedSchedule.getProcessList()) {
           System.out.println("Process " + process.getId() + " starts at " + process.getStartDate());
       }
       
   }
}




Solution

  • If you turn on environmentMode FULL_ASSERT (which makes it slow), you'll probably discover a score corruption (it will flush out bugs), because of this code:

      return constraintFactory.forEach(Link.class)
           // SCORE CORRUPTION because this doesn't reevaluate if the Process instances change
           .penalize("Process order constraint", HardSoftScore.ONE_HARD,
               link -> {
                   Integer sourceStartDate = link.getSource().getStartDate();
                   Integer targetStartDate = link.getTarget().getStartDate();
                   if (sourceStartDate == null || targetStartDate == null) {
                       return 0;  // No penalty if either start date is null
                   }
                   return sourceStartDate >= targetStartDate ? 1 : 0;
               });
    

    The way to fix this is to do something like:

    constraintFactory.forEach(Process)
        .join(Process.class,
             filtering(a, b -> a.linkedDestinationProcesses.contains(b))
        .penalize(...)