Reputation: 89
In my project, Assigning some tasks to the production line and calculated their start and end times. Here are the main logics.
So, The start time of job 2 must be later than the end time of job 1 and job 3.
I use the PlanningListVariable to implement this model. I created a planning list variable - jobList on class ProductionLine and specified the method that needs to update the start time in the Job class with the @CascadingupdateShadowVariable annotation.
The code for the Job class is as follows:
@PlanningEntity
public class Job {
@PlanningId
private Long id;
private String code;
// which production lines can this task be assigned to
private List<ProductionLine> availableProductionLineList;
// a duration map that contains job duration on each resource
private Map<ProductionLine, Duration> durationMap;
// the predecessor job list
private List<Job> predecessorJobList;
// the successor job list
private List<Job> successorJobList;
@InverseRelationShadowVariable(sourceVariableName = "jobList")
private ProductionLine productionLine;
@PreviousElementShadowVariable(sourceVariableName = "jobList")
private Job previousJob;
@NextElementShadowVariable(sourceVariableName = "jobList")
private Job nextJob;
// a method that updates the job start time
@CascadingUpdateShadowVariable(targetMethodName = "updateStartTimes")
private LocalDateTime startTime;
public Job() {
}
public void updateStartTimes() {
if (this.productionLine == null) {
if (this.getStartTime() != null) {
this.setStartTime(null);
}
} else {
this.setStartTime(this.calculateStartTime());
}
}
private LocalDateTime calculateStartTime() {
// the previous job end time
LocalDateTime previousEndTime = this.previousJob == null ? this.productionLine.getStartTime() : this.previousJob.getEndTime();
// the predecessor job end time
LocalDateTime predecessorEndTime = null;
if(!this.predecessorJobList.isEmpty()) {
predecessorEndTime = Collections.max(this.predecessorJobList, Comparator.comparing(Job::getEndTime)).getEndTime();
}
// the later between predecessor end time and previous end time
return this.getMaxDateTime(predecessorEndTime, previousEndTime);
}
private LocalDateTime getMaxDateTime(LocalDateTime dateTime1, LocalDateTime dateTime2) {
if (dateTime1 == null && dateTime2 == null) {
return null;
} else if (dateTime1 == null) {
return dateTime2;
} else if (dateTime2 == null) {
return dateTime1;
} else {
return dateTime1.isAfter(dateTime2) ? dateTime1 : dateTime2;
}
}
public Duration getDuration() {
return this.durationMap.getOrDefault(this.productionLine, null);
}
public LocalDateTime getStartTime() {
return startTime;
}
public void setStartTime(LocalDateTime startTime) {
this.startTime = startTime;
}
public LocalDateTime getEndTime() {
if(this.startTime == null) {
return null;
}
return this.startTime.plus(this.getDuration());
}
// others getter, setter .....
}
But I got a Variable Listener disruption exception.
16:18:12.147 [main ] TRACE Model annotations parsed for solution JobScheduling:
16:18:12.148 [main ] TRACE Entity Job:
16:18:12.149 [main ] TRACE Shadow variable nextJob (reflection)
16:18:12.149 [main ] TRACE Shadow variable previousJob (reflection)
16:18:12.149 [main ] TRACE Shadow variable productionLine (reflection)
16:18:12.149 [main ] TRACE Shadow variable startTime (reflection)
16:18:12.149 [main ] TRACE Entity ProductionLine:
16:18:12.149 [main ] TRACE Genuine variable jobList (reflection)
16:18:12.259 [main ] DEBUG Constraint weights for solution (1):
16:18:12.276 [main ] DEBUG Constraint weights for solution (1):
16:18:12.296 [main ] INFO Solving started: time spent (43), best score (-4init/0hard/0medium/0soft), environment mode (TRACKED_FULL_ASSERT), move thread count (NONE), random (JDK with seed 0).
16:18:12.301 [main ] INFO Problem scale: entity count (3), variable count (3), approximate value count (4), approximate problem scale (360).
16:18:12.313 [main ] DEBUG Constraint weights for solution (1):
16:18:12.315 [main ] TRACE Move index (0), score (-3init/0hard/0medium/0soft), move (Order1,Job1[] {null -> Line1[0]}).
16:18:12.318 [main ] DEBUG Constraint weights for solution (1):
16:18:12.318 [main ] TRACE Move index (1), score (-3init/-1hard/0medium/0soft), move (Order1,Job1[] {null -> Line2[0]}).
16:18:12.319 [main ] DEBUG Constraint weights for solution (1):
16:18:12.319 [main ] TRACE Move index (2), score (-3init/-1hard/0medium/0soft), move (Order1,Job1[] {null -> Line3[0]}).
16:18:12.320 [main ] DEBUG Constraint weights for solution (1):
16:18:12.320 [main ] DEBUG CH step (0), time spent (67), score (-3init/0hard/0medium/0soft), selected move count (3), picked move (Order1,Job1[] {null -> Line1[0]}).
16:18:12.324 [main ] DEBUG Constraint weights for solution (1):
16:18:12.324 [main ] TRACE Move index (0), score (-2init/-1hard/0medium/0soft), move (Order1,Job2[] {null -> Line1[0]}).
16:18:12.326 [main ] DEBUG Constraint weights for solution (1):
16:18:12.326 [main ] TRACE Move index (1), score (-2init/0hard/0medium/0soft), move (Order1,Job2[] {null -> Line2[0]}).
16:18:12.326 [main ] DEBUG Constraint weights for solution (1):
16:18:12.327 [main ] TRACE Move index (2), score (-2init/-1hard/0medium/0soft), move (Order1,Job2[] {null -> Line3[0]}).
16:18:12.327 [main ] DEBUG Constraint weights for solution (1):
16:18:12.327 [main ] TRACE Move index (3), score (-2init/-1hard/0medium/0soft), move (Order1,Job2[] {null -> Line1[1]}).
16:18:12.328 [main ] DEBUG Constraint weights for solution (1):
16:18:12.328 [main ] DEBUG CH step (1), time spent (75), score (-2init/0hard/0medium/0soft), selected move count (4), picked move (Order1,Job2[] {null -> Line2[0]}).
16:18:12.328 [main ] DEBUG Constraint weights for solution (1):
16:18:12.329 [main ] TRACE Move index (0), score (-1init/0hard/0medium/0soft), move (Order2,Job1[] {null -> Line1[0]}).
16:18:12.329 [main ] DEBUG Constraint weights for solution (1):
16:18:12.329 [main ] TRACE Move index (1), score (-1init/-1hard/0medium/0soft), move (Order2,Job1[] {null -> Line2[0]}).
16:18:12.330 [main ] DEBUG Constraint weights for solution (1):
16:18:12.330 [main ] TRACE Move index (2), score (-1init/-1hard/0medium/0soft), move (Order2,Job1[] {null -> Line3[0]}).
16:18:12.330 [main ] DEBUG Constraint weights for solution (1):
16:18:12.330 [main ] TRACE Move index (3), score (-1init/0hard/0medium/0soft), move (Order2,Job1[] {null -> Line1[1]}).
16:18:12.331 [main ] DEBUG Constraint weights for solution (1):
16:18:12.331 [main ] TRACE Move index (4), score (-1init/-1hard/0medium/0soft), move (Order2,Job1[] {null -> Line2[1]}).
16:18:12.331 [main ] DEBUG Constraint weights for solution (1):
Exception in thread "main" java.lang.IllegalStateException: VariableListener corruption after completedAction (Order2,Job1[Line1] {null -> Line1[0]}):
The entity (Order1,Job2[Line2])'s shadow variable (Job.startTime)'s corrupted value (2024-10-01T05:00) changed to uncorrupted value (2024-10-01T10:00) after all variable listeners were triggered without changes to the genuine variables.
Maybe one of the listeners ([]) for that shadow variable (Job.startTime) forgot to update it when one of its sourceVariables ([]) changed.
Or vice versa, maybe one of the listeners computes this shadow variable using a planning variable that is not declared as its source. Use the repeatable @ShadowVariable annotation for each source variable that is used to compute this shadow variable.
at ai.timefold.solver.core.impl.score.director.AbstractScoreDirector.assertShadowVariablesAreNotStale(AbstractScoreDirector.java:568)
at ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope.assertShadowVariablesAreNotStale(AbstractPhaseScope.java:186)
at ai.timefold.solver.core.impl.phase.AbstractPhase.predictWorkingStepScore(AbstractPhase.java:152)
at ai.timefold.solver.core.impl.constructionheuristic.DefaultConstructionHeuristicPhase.doStep(DefaultConstructionHeuristicPhase.java:97)
at ai.timefold.solver.core.impl.constructionheuristic.DefaultConstructionHeuristicPhase.solve(DefaultConstructionHeuristicPhase.java:83)
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 com.easyplan.Main.main(Main.java:284)
And there are no complex constraints implemented in ConstraintStream, only matching the production line allocation of tasks, similar to Skill Match in TaskAssignment:
protected Constraint resourceMatch(ConstraintFactory factory) {
Constraint constraint = factory.forEach(Job.class)
.filter(job -> job.getProductionLine() != null && !job.getAvailableResourceList().contains(job.getProductionLine()))
.penalizeLong(HardMediumSoftLongScore.ONE_HARD, Job::getDefaultScore)
.asConstraint("SkillMissing");
return constraint;
}
This should be a simple problem, but I don't have any idea about this Listener corruption. Has anyone encountered this? Or any suggestions?
I have tried the Chained Through Time pattern to implement this scenario, but PlanningListVariable is simpler and more user-friendly.
Upvotes: 0
Views: 10