diff --git a/java/food-packaging/src/main/java/org/acme/foodpackaging/domain/Job.java b/java/food-packaging/src/main/java/org/acme/foodpackaging/domain/Job.java index 31cb1a2f09..17ebd0bc9e 100644 --- a/java/food-packaging/src/main/java/org/acme/foodpackaging/domain/Job.java +++ b/java/food-packaging/src/main/java/org/acme/foodpackaging/domain/Job.java @@ -3,14 +3,17 @@ import ai.timefold.solver.core.api.domain.entity.PlanningEntity; import ai.timefold.solver.core.api.domain.entity.PlanningPin; import ai.timefold.solver.core.api.domain.lookup.PlanningId; -import ai.timefold.solver.core.api.domain.variable.CascadingUpdateShadowVariable; import ai.timefold.solver.core.api.domain.variable.InverseRelationShadowVariable; import ai.timefold.solver.core.api.domain.variable.NextElementShadowVariable; import ai.timefold.solver.core.api.domain.variable.PreviousElementShadowVariable; +import ai.timefold.solver.core.api.domain.variable.ShadowSources; import ai.timefold.solver.core.api.domain.variable.ShadowVariable; +import ai.timefold.solver.core.api.domain.variable.ShadowVariablesInconsistent; + import com.fasterxml.jackson.annotation.JsonIgnore; import java.time.Duration; +import java.time.LocalDate; import java.time.LocalDateTime; @PlanningEntity @@ -34,13 +37,13 @@ public class Job { @InverseRelationShadowVariable(sourceVariableName = "jobs") private Line line; - @ShadowVariable( - variableListenerClass = LineOperatorUpdatingVariableListener.class, - sourceEntityClass = Line.class, - sourceVariableName = "operator") - @ShadowVariable( - variableListenerClass = JobOperatorUpdatingVariableListener.class, - sourceVariableName = "line") + + // TODO: Remove me when solver has supplier + @JsonIgnore + @ShadowVariablesInconsistent + private boolean isInconsistent; + + @ShadowVariable(supplierName = "lineOperatorSupplier") private Operator lineOperator; @JsonIgnore @PreviousElementShadowVariable(sourceVariableName = "jobs") @@ -52,11 +55,11 @@ public class Job { /** * Start is after cleanup. */ - @CascadingUpdateShadowVariable(targetMethodName = "updateStartCleaningDateTime") + @ShadowVariable(supplierName = "startCleaningDateTimeSupplier") private LocalDateTime startCleaningDateTime; - @CascadingUpdateShadowVariable(targetMethodName = "updateStartCleaningDateTime") + @ShadowVariable(supplierName = "startProductionDateTimeSupplier") private LocalDateTime startProductionDateTime; - @CascadingUpdateShadowVariable(targetMethodName = "updateStartCleaningDateTime") + @ShadowVariable(supplierName = "endDateTimeSupplier") private LocalDateTime endDateTime; // No-arg constructor required for Timefold @@ -188,30 +191,44 @@ public void setEndDateTime(LocalDateTime endDateTime) { // ************************************************************************ // Complex methods // ************************************************************************ + @SuppressWarnings("unused") + @ShadowSources({"line", "line.operator"}) + private Operator lineOperatorSupplier() { + if (line == null) { + return null; + } + return line.getOperator(); + } + + @SuppressWarnings("unused") + @ShadowSources({"line", "previousJob.endDateTime"}) + private LocalDateTime startCleaningDateTimeSupplier() { + if (line == null) { + return null; + } + if (previousJob == null) { + return line.getStartDateTime(); + } else { + return previousJob.getEndDateTime(); + } + } @SuppressWarnings("unused") - private void updateStartCleaningDateTime() { - if (getLine() == null) { - if (getStartCleaningDateTime() != null) { - setStartCleaningDateTime(null); - setStartProductionDateTime(null); - setEndDateTime(null); - } - return; + @ShadowSources({"line", "startCleaningDateTime"}) + private LocalDateTime startProductionDateTimeSupplier() { + if (line == null) { + return null; } - Job previous = getPreviousJob(); - LocalDateTime startCleaning; - LocalDateTime startProduction; - if (previous == null) { - startCleaning = line.getStartDateTime(); - startProduction = line.getStartDateTime(); + if (previousJob == null) { + return line.getStartDateTime(); } else { - startCleaning = previous.getEndDateTime(); - startProduction = startCleaning == null ? null : startCleaning.plus(getProduct().getCleanupDuration(previous.getProduct())); + return startCleaningDateTime == null ? null : startCleaningDateTime.plus(getProduct().getCleanupDuration(previousJob.getProduct())); } - setStartCleaningDateTime(startCleaning); - setStartProductionDateTime(startProduction); - var endTime = startProduction == null ? null : startProduction.plus(getDuration()); - setEndDateTime(endTime); + } + + @SuppressWarnings("unused") + @ShadowSources({"startProductionDateTime"}) + private LocalDateTime endDateTimeSupplier() { + return startProductionDateTime == null ? null : startProductionDateTime.plus(getDuration()); } } diff --git a/java/food-packaging/src/main/java/org/acme/foodpackaging/domain/JobOperatorUpdatingVariableListener.java b/java/food-packaging/src/main/java/org/acme/foodpackaging/domain/JobOperatorUpdatingVariableListener.java deleted file mode 100644 index ffd2c0e082..0000000000 --- a/java/food-packaging/src/main/java/org/acme/foodpackaging/domain/JobOperatorUpdatingVariableListener.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.acme.foodpackaging.domain; - -import ai.timefold.solver.core.api.domain.variable.VariableListener; -import ai.timefold.solver.core.api.score.director.ScoreDirector; -import org.jspecify.annotations.NonNull; - -import java.util.Objects; - -public class JobOperatorUpdatingVariableListener implements VariableListener { - - private static final String LINE_OPERATOR_FIELD = "lineOperator"; - - @Override - public void beforeVariableChanged(@NonNull ScoreDirector scoreDirector, @NonNull Job job) { - // No need to do anything. - } - - @Override - public void afterVariableChanged(@NonNull ScoreDirector scoreDirector, @NonNull Job job) { - var line = job.getLine(); - var operator = line != null ? line.getOperator() : null; - var lineOperator = job.getLineOperator(); - if (line == null && lineOperator != null) { - scoreDirector.beforeVariableChanged(job, LINE_OPERATOR_FIELD); - job.setLineOperator(null); - scoreDirector.afterVariableChanged(job, LINE_OPERATOR_FIELD); - } else if (!Objects.equals(operator, lineOperator)) { - scoreDirector.beforeVariableChanged(job, LINE_OPERATOR_FIELD); - job.setLineOperator(operator); - scoreDirector.afterVariableChanged(job, LINE_OPERATOR_FIELD); - } - } - - @Override - public void beforeEntityAdded(@NonNull ScoreDirector scoreDirector, @NonNull Job job) { - // No need to do anything. - } - - @Override - public void afterEntityAdded(@NonNull ScoreDirector scoreDirector, @NonNull Job job) { - // No need to do anything. - } - - @Override - public void beforeEntityRemoved(@NonNull ScoreDirector scoreDirector, @NonNull Job job) { - // No need to do anything. - } - - @Override - public void afterEntityRemoved(@NonNull ScoreDirector scoreDirector, @NonNull Job job) { - // No need to do anything. - } -} diff --git a/java/food-packaging/src/main/java/org/acme/foodpackaging/domain/LineOperatorUpdatingVariableListener.java b/java/food-packaging/src/main/java/org/acme/foodpackaging/domain/LineOperatorUpdatingVariableListener.java deleted file mode 100644 index 014880cb3d..0000000000 --- a/java/food-packaging/src/main/java/org/acme/foodpackaging/domain/LineOperatorUpdatingVariableListener.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.acme.foodpackaging.domain; - -import ai.timefold.solver.core.api.domain.variable.VariableListener; -import ai.timefold.solver.core.api.score.director.ScoreDirector; -import org.jspecify.annotations.NonNull; - -import java.util.Objects; - -public class LineOperatorUpdatingVariableListener implements VariableListener { - @Override - public void beforeVariableChanged(@NonNull ScoreDirector scoreDirector, @NonNull Line line) { - // No need to do anything. - } - - @Override - public void afterVariableChanged(@NonNull ScoreDirector scoreDirector, @NonNull Line line) { - for (var job : line.getJobs()) { - if (!Objects.equals(job.getLineOperator(), line.getOperator())) { - scoreDirector.beforeVariableChanged(job, "lineOperator"); - job.setLineOperator(line.getOperator()); - scoreDirector.afterVariableChanged(job, "lineOperator"); - } - } - } - - @Override - public void beforeEntityAdded(@NonNull ScoreDirector scoreDirector, @NonNull Line line) { - // No need to do anything. - } - - @Override - public void afterEntityAdded(@NonNull ScoreDirector scoreDirector, @NonNull Line line) { - // No need to do anything. - } - - @Override - public void beforeEntityRemoved(@NonNull ScoreDirector scoreDirector, @NonNull Line line) { - // No need to do anything. - } - - @Override - public void afterEntityRemoved(@NonNull ScoreDirector scoreDirector, @NonNull Line line) { - // No need to do anything. - } -} diff --git a/java/maintenance-scheduling/src/main/java/org/acme/maintenancescheduling/domain/Job.java b/java/maintenance-scheduling/src/main/java/org/acme/maintenancescheduling/domain/Job.java index 6ab14ce4fa..2638e538ea 100644 --- a/java/maintenance-scheduling/src/main/java/org/acme/maintenancescheduling/domain/Job.java +++ b/java/maintenance-scheduling/src/main/java/org/acme/maintenancescheduling/domain/Job.java @@ -6,9 +6,11 @@ import ai.timefold.solver.core.api.domain.entity.PlanningEntity; import ai.timefold.solver.core.api.domain.lookup.PlanningId; import ai.timefold.solver.core.api.domain.variable.PlanningVariable; +import ai.timefold.solver.core.api.domain.variable.ShadowSources; import ai.timefold.solver.core.api.domain.variable.ShadowVariable; +import ai.timefold.solver.core.api.domain.variable.ShadowVariablesInconsistent; -import org.acme.maintenancescheduling.solver.EndDateUpdatingVariableListener; +import com.fasterxml.jackson.annotation.JsonIgnore; @PlanningEntity public class Job { @@ -29,9 +31,14 @@ public class Job { // Follows the TimeGrain Design Pattern @PlanningVariable private LocalDate startDate; // Inclusive - @ShadowVariable(variableListenerClass = EndDateUpdatingVariableListener.class, sourceVariableName = "startDate") + @ShadowVariable(supplierName = "endDateSupplier") private LocalDate endDate; // Exclusive + // TODO: Remove me when solver has supplier + @JsonIgnore + @ShadowVariablesInconsistent + private boolean isInconsistent; + public Job() { } @@ -56,7 +63,7 @@ public Job(String id, String name, int durationInDays, LocalDate minStartDate, L this.tags = tags; this.crew = crew; this.startDate = startDate; - this.endDate = EndDateUpdatingVariableListener.calculateEndDate(startDate, durationInDays); + this.endDate = calculateEndDate(startDate, durationInDays); } @Override @@ -119,4 +126,25 @@ public LocalDate getEndDate() { public void setEndDate(LocalDate endDate) { this.endDate = endDate; } + + // ************************************************************************ + // Complex methods + // ************************************************************************ + @SuppressWarnings("unused") + @ShadowSources("startDate") + public LocalDate endDateSupplier() { + return calculateEndDate(startDate, durationInDays); + } + + public static LocalDate calculateEndDate(LocalDate startDate, int durationInDays) { + if (startDate == null) { + return null; + } else { + // Skip weekends. Does not work for holidays. + // To skip holidays too, cache all working days in WorkCalendar. + // Keep in sync with MaintenanceSchedule.createStartDateList(). + int weekendPadding = 2 * ((durationInDays + (startDate.getDayOfWeek().getValue() - 1)) / 5); + return startDate.plusDays(durationInDays + weekendPadding); + } + } } diff --git a/java/maintenance-scheduling/src/main/java/org/acme/maintenancescheduling/domain/MaintenanceSchedule.java b/java/maintenance-scheduling/src/main/java/org/acme/maintenancescheduling/domain/MaintenanceSchedule.java index 0b4a0d5f1e..e2772326b1 100644 --- a/java/maintenance-scheduling/src/main/java/org/acme/maintenancescheduling/domain/MaintenanceSchedule.java +++ b/java/maintenance-scheduling/src/main/java/org/acme/maintenancescheduling/domain/MaintenanceSchedule.java @@ -50,7 +50,7 @@ public MaintenanceSchedule(HardSoftLongScore score, SolverStatus solverStatus) { public List createStartDateList() { return workCalendar.getFromDate().datesUntil(workCalendar.getToDate()) // Skip weekends. Does not work for holidays. - // Keep in sync with EndDateUpdatingVariableListener.updateEndDate(). + // Keep in sync with Job.calculateEndDate(). // To skip holidays too, cache all working days in WorkCalendar. .filter(date -> date.getDayOfWeek() != DayOfWeek.SATURDAY && date.getDayOfWeek() != DayOfWeek.SUNDAY) diff --git a/java/maintenance-scheduling/src/main/java/org/acme/maintenancescheduling/rest/DemoDataGenerator.java b/java/maintenance-scheduling/src/main/java/org/acme/maintenancescheduling/rest/DemoDataGenerator.java index 1aa2697002..4fb8171ad0 100644 --- a/java/maintenance-scheduling/src/main/java/org/acme/maintenancescheduling/rest/DemoDataGenerator.java +++ b/java/maintenance-scheduling/src/main/java/org/acme/maintenancescheduling/rest/DemoDataGenerator.java @@ -14,7 +14,6 @@ import org.acme.maintenancescheduling.domain.Job; import org.acme.maintenancescheduling.domain.MaintenanceSchedule; import org.acme.maintenancescheduling.domain.WorkCalendar; -import org.acme.maintenancescheduling.solver.EndDateUpdatingVariableListener; @ApplicationScoped public class DemoDataGenerator { @@ -63,9 +62,9 @@ public MaintenanceSchedule generateDemoData(DemoData demoData) { + random.nextInt(workdayTotal - (durationInDays + 5)); int minWorkdayOffset = random.nextInt(workdayTotal - minMaxBetweenWorkdays + 1); int minIdealEndBetweenWorkdays = minMaxBetweenWorkdays - 1 - random.nextInt(4); - LocalDate minStartDate = EndDateUpdatingVariableListener.calculateEndDate(fromDate, minWorkdayOffset); - LocalDate maxEndDate = EndDateUpdatingVariableListener.calculateEndDate(minStartDate, minMaxBetweenWorkdays); - LocalDate idealEndDate = EndDateUpdatingVariableListener.calculateEndDate(minStartDate, minIdealEndBetweenWorkdays); + LocalDate minStartDate = Job.calculateEndDate(fromDate, minWorkdayOffset); + LocalDate maxEndDate = Job.calculateEndDate(minStartDate, minMaxBetweenWorkdays); + LocalDate idealEndDate = Job.calculateEndDate(minStartDate, minIdealEndBetweenWorkdays); Set tags = random.nextDouble() < 0.1 ? Set.of(jobArea, "Subway") : Set.of(jobArea); jobs.add(new Job(Integer.toString(i), jobArea + " " + jobTarget, durationInDays, minStartDate, maxEndDate, idealEndDate, tags)); diff --git a/java/maintenance-scheduling/src/main/java/org/acme/maintenancescheduling/solver/EndDateUpdatingVariableListener.java b/java/maintenance-scheduling/src/main/java/org/acme/maintenancescheduling/solver/EndDateUpdatingVariableListener.java deleted file mode 100644 index a3616d7739..0000000000 --- a/java/maintenance-scheduling/src/main/java/org/acme/maintenancescheduling/solver/EndDateUpdatingVariableListener.java +++ /dev/null @@ -1,60 +0,0 @@ -package org.acme.maintenancescheduling.solver; - -import java.time.LocalDate; - -import org.acme.maintenancescheduling.domain.Job; -import org.acme.maintenancescheduling.domain.MaintenanceSchedule; -import ai.timefold.solver.core.api.domain.variable.VariableListener; -import ai.timefold.solver.core.api.score.director.ScoreDirector; - -public class EndDateUpdatingVariableListener implements VariableListener { - - @Override - public void beforeEntityAdded(ScoreDirector scoreDirector, Job job) { - // Do nothing - } - - @Override - public void afterEntityAdded(ScoreDirector scoreDirector, Job job) { - updateEndDate(scoreDirector, job); - } - - @Override - public void beforeVariableChanged(ScoreDirector scoreDirector, Job job) { - // Do nothing - } - - @Override - public void afterVariableChanged(ScoreDirector scoreDirector, Job job) { - updateEndDate(scoreDirector, job); - } - - @Override - public void beforeEntityRemoved(ScoreDirector scoreDirector, Job job) { - // Do nothing - } - - @Override - public void afterEntityRemoved(ScoreDirector scoreDirector, Job job) { - // Do nothing - } - - protected void updateEndDate(ScoreDirector scoreDirector, Job job) { - scoreDirector.beforeVariableChanged(job, "endDate"); - job.setEndDate(calculateEndDate(job.getStartDate(), job.getDurationInDays())); - scoreDirector.afterVariableChanged(job, "endDate"); - } - - public static LocalDate calculateEndDate(LocalDate startDate, int durationInDays) { - if (startDate == null) { - return null; - } else { - // Skip weekends. Does not work for holidays. - // To skip holidays too, cache all working days in scoreDirector.getWorkingSolution().getWorkCalendar(). - // Keep in sync with MaintenanceSchedule.createStartDateList(). - int weekendPadding = 2 * ((durationInDays + (startDate.getDayOfWeek().getValue() - 1)) / 5); - return startDate.plusDays(durationInDays + weekendPadding); - } - } - -} diff --git a/java/project-job-scheduling/src/main/java/org/acme/projectjobschedule/domain/Allocation.java b/java/project-job-scheduling/src/main/java/org/acme/projectjobschedule/domain/Allocation.java index 96b91d41da..fd9fb4b7c7 100644 --- a/java/project-job-scheduling/src/main/java/org/acme/projectjobschedule/domain/Allocation.java +++ b/java/project-job-scheduling/src/main/java/org/acme/projectjobschedule/domain/Allocation.java @@ -1,7 +1,6 @@ package org.acme.projectjobschedule.domain; import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.Objects; @@ -12,10 +11,11 @@ import ai.timefold.solver.core.api.domain.valuerange.ValueRangeFactory; import ai.timefold.solver.core.api.domain.valuerange.ValueRangeProvider; import ai.timefold.solver.core.api.domain.variable.PlanningVariable; +import ai.timefold.solver.core.api.domain.variable.ShadowSources; import ai.timefold.solver.core.api.domain.variable.ShadowVariable; +import ai.timefold.solver.core.api.domain.variable.ShadowVariablesInconsistent; import org.acme.projectjobschedule.domain.solver.DelayStrengthComparator; -import org.acme.projectjobschedule.domain.solver.PredecessorsDoneDateUpdatingVariableListener; import com.fasterxml.jackson.annotation.JsonIdentityInfo; import com.fasterxml.jackson.annotation.JsonIdentityReference; @@ -50,14 +50,20 @@ public class Allocation { private Integer delay; // In days // Shadow variables - @ShadowVariable(variableListenerClass = PredecessorsDoneDateUpdatingVariableListener.class, - sourceVariableName = "executionMode") - @ShadowVariable(variableListenerClass = PredecessorsDoneDateUpdatingVariableListener.class, sourceVariableName = "delay") + // TODO: Remove me when solver has supplier + @JsonIgnore + @ShadowVariablesInconsistent + private boolean isInconsistent; + + @ShadowVariable(supplierName = "predecessorsDoneDateSupplier") private Integer predecessorsDoneDate; // Filled from shadow variables + @ShadowVariable(supplierName = "startDateSupplier") private Integer startDate; + @ShadowVariable(supplierName = "endDateSupplier") private Integer endDate; + @ShadowVariable(supplierName = "busyDatesSupplier") private List busyDates; public Allocation() { @@ -175,7 +181,6 @@ public ExecutionMode getExecutionMode() { public void setExecutionMode(ExecutionMode executionMode) { this.executionMode = executionMode; - invalidateComputedVariables(); } public Integer getDelay() { @@ -184,7 +189,6 @@ public Integer getDelay() { public void setDelay(Integer delay) { this.delay = delay; - invalidateComputedVariables(); } public Integer getPredecessorsDoneDate() { @@ -193,51 +197,69 @@ public Integer getPredecessorsDoneDate() { public void setPredecessorsDoneDate(Integer predecessorsDoneDate) { this.predecessorsDoneDate = predecessorsDoneDate; - invalidateComputedVariables(); - } - - // ************************************************************************ - // Complex methods - // ************************************************************************ - - public void invalidateComputedVariables() { - this.startDate = null; - this.endDate = null; - this.busyDates = null; } public Integer getStartDate() { - if (predecessorsDoneDate != null) { - startDate = predecessorsDoneDate + Objects.requireNonNullElse(delay, 0); - } return startDate; } public Integer getEndDate() { - if (predecessorsDoneDate != null) { - endDate = getStartDate() + (executionMode == null ? 0 : executionMode.getDuration()); - } return endDate; } @JsonIgnore public List getBusyDates() { - if (busyDates == null) { - if (predecessorsDoneDate == null) { - busyDates = Collections.emptyList(); - } else { - var start = getStartDate(); - var end = getEndDate(); - var dates = new Integer[end - start]; - for (int i = 0; i < dates.length; i++) { - dates[i] = start + i; - } - busyDates = Arrays.asList(dates); - } - } return busyDates; } + // ************************************************************************ + // Complex methods + // ************************************************************************ + @SuppressWarnings("unused") + @ShadowSources("predecessorAllocations[].endDate") + public Integer predecessorsDoneDateSupplier() { + // For the source the doneDate must be 0. + int doneDate = 0; + if (predecessorAllocations == null) { + return doneDate; + } + for (Allocation predecessorAllocation : predecessorAllocations) { + int endDate = predecessorAllocation.getEndDate(); + doneDate = Math.max(doneDate, endDate); + } + return doneDate; + } + + @SuppressWarnings("unused") + @ShadowSources({"predecessorsDoneDate", "delay"}) + public Integer startDateSupplier() { + return predecessorsDoneDate + Objects.requireNonNullElse(delay, 0); + } + + @SuppressWarnings("unused") + @ShadowSources({"startDate", "executionMode"}) + public Integer endDateSupplier() { + return getStartDate() + (executionMode == null ? 0 : executionMode.getDuration()); + } + + @SuppressWarnings("unused") + @ShadowSources({"startDate", "endDate"}) + public List busyDatesSupplier() { + var start = getStartDate(); + var end = getEndDate(); + var dates = new Integer[end - start]; + for (int i = 0; i < dates.length; i++) { + dates[i] = start + i; + } + return Arrays.asList(dates); + } + + public void updateShadowsAfterPredecessorDoneDate() { + startDate = startDateSupplier(); + endDate = endDateSupplier(); + busyDates = busyDatesSupplier(); + } + @JsonIgnore public Project getProject() { return job.getProject(); diff --git a/java/project-job-scheduling/src/main/java/org/acme/projectjobschedule/domain/solver/PredecessorsDoneDateUpdatingVariableListener.java b/java/project-job-scheduling/src/main/java/org/acme/projectjobschedule/domain/solver/PredecessorsDoneDateUpdatingVariableListener.java deleted file mode 100644 index a8344e1442..0000000000 --- a/java/project-job-scheduling/src/main/java/org/acme/projectjobschedule/domain/solver/PredecessorsDoneDateUpdatingVariableListener.java +++ /dev/null @@ -1,80 +0,0 @@ -package org.acme.projectjobschedule.domain.solver; - -import java.util.ArrayDeque; -import java.util.Objects; -import java.util.Queue; - -import ai.timefold.solver.core.api.domain.variable.VariableListener; -import ai.timefold.solver.core.api.score.director.ScoreDirector; - -import org.acme.projectjobschedule.domain.Allocation; -import org.acme.projectjobschedule.domain.ProjectJobSchedule; - -public class PredecessorsDoneDateUpdatingVariableListener implements VariableListener { - - @Override - public void beforeEntityAdded(ScoreDirector scoreDirector, Allocation allocation) { - // Do nothing - } - - @Override - public void afterEntityAdded(ScoreDirector scoreDirector, Allocation allocation) { - updateAllocation(scoreDirector, allocation); - } - - @Override - public void beforeVariableChanged(ScoreDirector scoreDirector, Allocation allocation) { - // Do nothing - } - - @Override - public void afterVariableChanged(ScoreDirector scoreDirector, Allocation allocation) { - updateAllocation(scoreDirector, allocation); - } - - @Override - public void beforeEntityRemoved(ScoreDirector scoreDirector, Allocation allocation) { - // Do nothing - } - - @Override - public void afterEntityRemoved(ScoreDirector scoreDirector, Allocation allocation) { - // Do nothing - } - - protected void updateAllocation(ScoreDirector scoreDirector, Allocation originalAllocation) { - // Reset computed variables when a planning variable changes to prevent score corruption - originalAllocation.invalidateComputedVariables(); - Queue uncheckedSuccessorQueue = new ArrayDeque<>(); - uncheckedSuccessorQueue.addAll(originalAllocation.getSuccessorAllocations()); - while (!uncheckedSuccessorQueue.isEmpty()) { - Allocation allocation = uncheckedSuccessorQueue.remove(); - boolean updated = updatePredecessorsDoneDate(scoreDirector, allocation); - if (updated) { - uncheckedSuccessorQueue.addAll(allocation.getSuccessorAllocations()); - } - } - } - - /** - * @param scoreDirector never null - * @param allocation never null - * @return true if the startDate changed - */ - protected boolean updatePredecessorsDoneDate(ScoreDirector scoreDirector, Allocation allocation) { - // For the source the doneDate must be 0. - Integer doneDate = 0; - for (Allocation predecessorAllocation : allocation.getPredecessorAllocations()) { - int endDate = predecessorAllocation.getEndDate(); - doneDate = Math.max(doneDate, endDate); - } - if (Objects.equals(doneDate, allocation.getPredecessorsDoneDate())) { - return false; - } - scoreDirector.beforeVariableChanged(allocation, "predecessorsDoneDate"); - allocation.setPredecessorsDoneDate(doneDate); - scoreDirector.afterVariableChanged(allocation, "predecessorsDoneDate"); - return true; - } - -} diff --git a/java/project-job-scheduling/src/main/java/org/acme/projectjobschedule/rest/DemoDataGenerator.java b/java/project-job-scheduling/src/main/java/org/acme/projectjobschedule/rest/DemoDataGenerator.java index 18922dbb51..d0fa0f9a7d 100644 --- a/java/project-job-scheduling/src/main/java/org/acme/projectjobschedule/rest/DemoDataGenerator.java +++ b/java/project-job-scheduling/src/main/java/org/acme/projectjobschedule/rest/DemoDataGenerator.java @@ -12,6 +12,8 @@ import jakarta.enterprise.context.ApplicationScoped; +import ai.timefold.solver.core.api.solver.SolutionManager; + import org.acme.projectjobschedule.domain.Allocation; import org.acme.projectjobschedule.domain.ExecutionMode; import org.acme.projectjobschedule.domain.Job; @@ -56,6 +58,7 @@ public ProjectJobSchedule generateDemoData() { projectJobSchedule.setResourceRequirements( projectJobSchedule.getExecutionModes().stream().flatMap(e -> e.getResourceRequirements().stream()).toList()); + SolutionManager.updateShadowVariables(projectJobSchedule); return projectJobSchedule; } diff --git a/java/project-job-scheduling/src/main/java/org/acme/projectjobschedule/rest/ProjectJobSchedulingResource.java b/java/project-job-scheduling/src/main/java/org/acme/projectjobschedule/rest/ProjectJobSchedulingResource.java index 7bdbfb5c2f..cfe4aef04b 100644 --- a/java/project-job-scheduling/src/main/java/org/acme/projectjobschedule/rest/ProjectJobSchedulingResource.java +++ b/java/project-job-scheduling/src/main/java/org/acme/projectjobschedule/rest/ProjectJobSchedulingResource.java @@ -88,6 +88,8 @@ public Collection list() { @Produces(MediaType.TEXT_PLAIN) public String solve(ProjectJobSchedule problem) { String jobId = UUID.randomUUID().toString(); + // Need to update the shadow variables since their default is not null + SolutionManager.updateShadowVariables(problem); jobIdToJob.put(jobId, Job.ofSchedule(problem)); solverManager.solveBuilder() .withProblemId(jobId) diff --git a/java/project-job-scheduling/src/test/java/org/acme/projectjobschedule/rest/ProjectJobSchedulingEnvironmentTest.java b/java/project-job-scheduling/src/test/java/org/acme/projectjobschedule/rest/ProjectJobSchedulingEnvironmentTest.java index 515016fd16..52194cdf34 100644 --- a/java/project-job-scheduling/src/test/java/org/acme/projectjobschedule/rest/ProjectJobSchedulingEnvironmentTest.java +++ b/java/project-job-scheduling/src/test/java/org/acme/projectjobschedule/rest/ProjectJobSchedulingEnvironmentTest.java @@ -7,6 +7,7 @@ import jakarta.inject.Inject; +import ai.timefold.solver.core.api.solver.SolutionManager; import ai.timefold.solver.core.api.solver.Solver; import ai.timefold.solver.core.api.solver.SolverFactory; import ai.timefold.solver.core.config.solver.EnvironmentMode; @@ -52,6 +53,8 @@ void solve(EnvironmentMode environmentMode) { SolverFactory solverFactory = SolverFactory.create(updatedConfig); // Solve the problem + // Need to update the shadow variables since their default is not null + SolutionManager.updateShadowVariables(problem); Solver solver = solverFactory.buildSolver(); ProjectJobSchedule solution = solver.solve(problem); assertThat(solution.getScore()).isNotNull(); diff --git a/java/project-job-scheduling/src/test/java/org/acme/projectjobschedule/solver/ProjectJobSchedulingConstraintProviderTest.java b/java/project-job-scheduling/src/test/java/org/acme/projectjobschedule/solver/ProjectJobSchedulingConstraintProviderTest.java index 5e396781f8..44d7ea2058 100644 --- a/java/project-job-scheduling/src/test/java/org/acme/projectjobschedule/solver/ProjectJobSchedulingConstraintProviderTest.java +++ b/java/project-job-scheduling/src/test/java/org/acme/projectjobschedule/solver/ProjectJobSchedulingConstraintProviderTest.java @@ -7,6 +7,7 @@ import jakarta.inject.Inject; +import ai.timefold.solver.core.api.solver.SolutionManager; import ai.timefold.solver.test.api.score.stream.ConstraintVerifier; import org.acme.projectjobschedule.domain.Allocation; @@ -52,6 +53,11 @@ void nonRenewableResourceCapacity() { secondAllocation.setExecutionMode(secondExecutionMode); secondAllocation.setDelay(100); + // Cannot use SolutionManager.updateShadows since that would set + // predecessorDoneDate to 0 (since the two allocations have no predecessors) + firstAllocation.updateShadowsAfterPredecessorDoneDate(); + secondAllocation.updateShadowsAfterPredecessorDoneDate(); + constraintVerifier.verifyThat(ProjectJobSchedulingConstraintProvider::nonRenewableResourceCapacity) .given(firstResourceRequirement, secondResourceRequirement, firstAllocation, secondAllocation) .penalizesBy(3); @@ -80,6 +86,11 @@ void renewableResourceCapacity() { secondAllocation.setDelay(100); secondAllocation.setPredecessorsDoneDate(2); + // Cannot use SolutionManager.updateShadows since that would set + // predecessorDoneDate to 0 (since the two allocations have no predecessors) + firstAllocation.updateShadowsAfterPredecessorDoneDate(); + secondAllocation.updateShadowsAfterPredecessorDoneDate(); + constraintVerifier.verifyThat(ProjectJobSchedulingConstraintProvider::renewableResourceCapacity) .given(firstResourceRequirement, secondResourceRequirement, firstAllocation, secondAllocation) .penalizesBy(18); @@ -109,6 +120,11 @@ void totalProjectDelay() { secondAllocation.setDelay(100); secondAllocation.setPredecessorsDoneDate(2); + // Cannot use SolutionManager.updateShadows since that would set + // predecessorDoneDate to 0 (since the two allocations have no predecessors) + firstAllocation.updateShadowsAfterPredecessorDoneDate(); + secondAllocation.updateShadowsAfterPredecessorDoneDate(); + constraintVerifier.verifyThat(ProjectJobSchedulingConstraintProvider::totalProjectDelay) .given(firstAllocation, secondAllocation) .penalizesBy(23); @@ -138,6 +154,11 @@ void totalMakespan() { secondAllocation.setDelay(100); secondAllocation.setPredecessorsDoneDate(2); + // Cannot use SolutionManager.updateShadows since that would set + // predecessorDoneDate to 0 (since the two allocations have no predecessors) + firstAllocation.updateShadowsAfterPredecessorDoneDate(); + secondAllocation.updateShadowsAfterPredecessorDoneDate(); + constraintVerifier.verifyThat(ProjectJobSchedulingConstraintProvider::totalMakespan) .given(firstAllocation, secondAllocation) .penalizesBy(112); diff --git a/java/task-assigning/src/main/java/org/acme/taskassigning/domain/Task.java b/java/task-assigning/src/main/java/org/acme/taskassigning/domain/Task.java index 20ba718805..542d968664 100644 --- a/java/task-assigning/src/main/java/org/acme/taskassigning/domain/Task.java +++ b/java/task-assigning/src/main/java/org/acme/taskassigning/domain/Task.java @@ -2,9 +2,11 @@ import ai.timefold.solver.core.api.domain.entity.PlanningEntity; import ai.timefold.solver.core.api.domain.lookup.PlanningId; -import ai.timefold.solver.core.api.domain.variable.CascadingUpdateShadowVariable; import ai.timefold.solver.core.api.domain.variable.InverseRelationShadowVariable; import ai.timefold.solver.core.api.domain.variable.PreviousElementShadowVariable; +import ai.timefold.solver.core.api.domain.variable.ShadowSources; +import ai.timefold.solver.core.api.domain.variable.ShadowVariable; +import ai.timefold.solver.core.api.domain.variable.ShadowVariablesInconsistent; import com.fasterxml.jackson.annotation.JsonIdentityInfo; import com.fasterxml.jackson.annotation.JsonIgnore; @@ -23,6 +25,11 @@ public class Task { private Priority priority; // Shadow variables + // TODO: Remove me when solver has supplier + @JsonIgnore + @ShadowVariablesInconsistent + private boolean isInconsistent; + @JsonIgnore @InverseRelationShadowVariable(sourceVariableName = "tasks") private Employee employee; @@ -30,7 +37,7 @@ public class Task { @PreviousElementShadowVariable(sourceVariableName = "tasks") private Task previousTask; @JsonIgnore - @CascadingUpdateShadowVariable(targetMethodName = "updateStartTime") + @ShadowVariable(supplierName = "startTimeSupplier") private Integer startTime; // In minutes public Task() { @@ -132,14 +139,15 @@ public void setStartTime(Integer startTime) { // ************************************************************************ @SuppressWarnings("unused") - private void updateStartTime() { + @ShadowSources({"employee", "previousTask.startTime"}) + private Integer startTimeSupplier() { if (employee == null) { - startTime = null; + return null; } else if (previousTask == null) { - startTime = minStartTime; + return minStartTime; } else { var previousEndTime = previousTask.getEndTime(); - startTime = Math.max(previousEndTime, minStartTime); + return Math.max(previousEndTime, minStartTime); } } diff --git a/java/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/Visit.java b/java/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/Visit.java index 8f6970373a..cece42ba52 100644 --- a/java/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/Visit.java +++ b/java/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/Visit.java @@ -6,9 +6,11 @@ import ai.timefold.solver.core.api.domain.entity.PlanningEntity; import ai.timefold.solver.core.api.domain.lookup.PlanningId; -import ai.timefold.solver.core.api.domain.variable.CascadingUpdateShadowVariable; import ai.timefold.solver.core.api.domain.variable.InverseRelationShadowVariable; import ai.timefold.solver.core.api.domain.variable.PreviousElementShadowVariable; +import ai.timefold.solver.core.api.domain.variable.ShadowSources; +import ai.timefold.solver.core.api.domain.variable.ShadowVariable; +import ai.timefold.solver.core.api.domain.variable.ShadowVariablesInconsistent; import com.fasterxml.jackson.annotation.JsonIdentityInfo; import com.fasterxml.jackson.annotation.JsonIdentityReference; @@ -29,13 +31,18 @@ public class Visit implements LocationAware { private LocalDateTime maxEndTime; private Duration serviceDuration; + // TODO: Remove me when solver has supplier + @JsonIgnore + @ShadowVariablesInconsistent + private boolean isInconsistent; + @JsonIdentityReference(alwaysAsId = true) @InverseRelationShadowVariable(sourceVariableName = "visits") private Vehicle vehicle; @JsonIdentityReference(alwaysAsId = true) @PreviousElementShadowVariable(sourceVariableName = "visits") private Visit previousVisit; - @CascadingUpdateShadowVariable(targetMethodName = "updateArrivalTime") + @ShadowVariable(supplierName = "arrivalTimeSupplier") private LocalDateTime arrivalTime; public Visit() { @@ -122,13 +129,13 @@ public void setArrivalTime(LocalDateTime arrivalTime) { // ************************************************************************ @SuppressWarnings("unused") - private void updateArrivalTime() { + @ShadowSources({"vehicle", "previousVisit.arrivalTime"}) + private LocalDateTime arrivalTimeSupplier() { if (previousVisit == null && vehicle == null) { - arrivalTime = null; - return; + return null; } LocalDateTime departureTime = previousVisit == null ? vehicle.getDepartureTime() : previousVisit.getDepartureTime(); - arrivalTime = departureTime != null ? departureTime.plusSeconds(getDrivingTimeSecondsFromPreviousStandstill()) : null; + return departureTime != null ? departureTime.plusSeconds(getDrivingTimeSecondsFromPreviousStandstill()) : null; } @JsonProperty(access = JsonProperty.Access.READ_ONLY)