diff --git a/optaplanner-examples/data/rocktour/unsolved/48shows.xlsx b/optaplanner-examples/data/rocktour/unsolved/48shows.xlsx index 341c920a09..f90c084908 100644 Binary files a/optaplanner-examples/data/rocktour/unsolved/48shows.xlsx and b/optaplanner-examples/data/rocktour/unsolved/48shows.xlsx differ diff --git a/optaplanner-examples/src/main/java/org/optaplanner/examples/rocktour/domain/RockBus.java b/optaplanner-examples/src/main/java/org/optaplanner/examples/rocktour/domain/RockBus.java index 64d75f0adf..de89487416 100644 --- a/optaplanner-examples/src/main/java/org/optaplanner/examples/rocktour/domain/RockBus.java +++ b/optaplanner-examples/src/main/java/org/optaplanner/examples/rocktour/domain/RockBus.java @@ -47,6 +47,16 @@ public RockTimeOfDay getDepartureTimeOfDay() { return RockTimeOfDay.EARLY; } + @Override + public RockStandstill getHosWeekStart() { + return this; + } + + @Override + public Long getHosWeekDrivingSecondsTotal() { + return 0L; + } + @Override public RockLocation getArrivalLocation() { return endLocation; diff --git a/optaplanner-examples/src/main/java/org/optaplanner/examples/rocktour/domain/RockShow.java b/optaplanner-examples/src/main/java/org/optaplanner/examples/rocktour/domain/RockShow.java index 87e32d964c..604ee420ba 100644 --- a/optaplanner-examples/src/main/java/org/optaplanner/examples/rocktour/domain/RockShow.java +++ b/optaplanner-examples/src/main/java/org/optaplanner/examples/rocktour/domain/RockShow.java @@ -26,7 +26,7 @@ import org.optaplanner.core.api.domain.variable.PlanningVariableGraphType; import org.optaplanner.core.api.domain.variable.PlanningVariableReference; import org.optaplanner.examples.common.domain.AbstractPersistable; -import org.optaplanner.examples.rocktour.domain.solver.RockShowDateUpdatingVariableListener; +import org.optaplanner.examples.rocktour.domain.solver.RockShowVariableListener; import static java.time.temporal.ChronoUnit.*; @@ -49,13 +49,19 @@ public class RockShow extends AbstractPersistable implements RockStandstill { @AnchorShadowVariable(sourceVariableName = "previousStandstill") private RockBus bus; - @CustomShadowVariable(variableListenerClass = RockShowDateUpdatingVariableListener.class, + @CustomShadowVariable(variableListenerClass = RockShowVariableListener.class, sources = {@PlanningVariableReference(variableName = "previousStandstill"), @PlanningVariableReference(variableName = "bus")}) private LocalDate date; @CustomShadowVariable(variableListenerRef = @PlanningVariableReference(variableName = "date")) - private RockTimeOfDay timeOfDay; + private RockTimeOfDay timeOfDay; // There can be 2 shows on the same date (early and late) + + @CustomShadowVariable(variableListenerRef = @PlanningVariableReference(variableName = "date")) + private RockStandstill hosWeekStart; // HOS stands for Hours of Service regulation + + @CustomShadowVariable(variableListenerRef = @PlanningVariableReference(variableName = "date")) + private Long hosWeekDrivingSecondsTotal; // HOS stands for Hours of Service regulation public RockShow() { } @@ -194,4 +200,22 @@ public void setTimeOfDay(RockTimeOfDay timeOfDay) { this.timeOfDay = timeOfDay; } + @Override + public RockStandstill getHosWeekStart() { + return hosWeekStart; + } + + public void setHosWeekStart(RockStandstill hosWeekStart) { + this.hosWeekStart = hosWeekStart; + } + + @Override + public Long getHosWeekDrivingSecondsTotal() { + return hosWeekDrivingSecondsTotal; + } + + public void setHosWeekDrivingSecondsTotal(Long hosWeekDrivingSecondsTotal) { + this.hosWeekDrivingSecondsTotal = hosWeekDrivingSecondsTotal; + } + } diff --git a/optaplanner-examples/src/main/java/org/optaplanner/examples/rocktour/domain/RockStandstill.java b/optaplanner-examples/src/main/java/org/optaplanner/examples/rocktour/domain/RockStandstill.java index 890badc5b4..731ea85c05 100644 --- a/optaplanner-examples/src/main/java/org/optaplanner/examples/rocktour/domain/RockStandstill.java +++ b/optaplanner-examples/src/main/java/org/optaplanner/examples/rocktour/domain/RockStandstill.java @@ -34,8 +34,21 @@ public interface RockStandstill { */ LocalDate getDepartureDate(); + /** + * @return sometimes null; + */ RockTimeOfDay getDepartureTimeOfDay(); + /** + * @return sometimes null; + */ + RockStandstill getHosWeekStart(); + + /** + * @return sometimes null; + */ + Long getHosWeekDrivingSecondsTotal(); + /** * @return never null; */ diff --git a/optaplanner-examples/src/main/java/org/optaplanner/examples/rocktour/domain/RockTourParametrization.java b/optaplanner-examples/src/main/java/org/optaplanner/examples/rocktour/domain/RockTourParametrization.java index 7c6b87f5b4..151924adca 100644 --- a/optaplanner-examples/src/main/java/org/optaplanner/examples/rocktour/domain/RockTourParametrization.java +++ b/optaplanner-examples/src/main/java/org/optaplanner/examples/rocktour/domain/RockTourParametrization.java @@ -22,6 +22,9 @@ public class RockTourParametrization extends AbstractPersistable { public static final String EARLY_LATE_BREAK_DRIVING_SECONDS = "Early late break driving seconds budget"; public static final String NIGHT_DRIVING_SECONDS = "Night driving seconds budget"; + public static final String HOS_WEEK_DRIVING_SECONDS_BUDGET = "HOS week driving seconds budget"; + public static final String HOS_WEEK_CONSECUTIVE_DRIVING_DAYS_BUDGET = "HOS week consecutive driving days budget"; + public static final String HOS_WEEK_REST_DAYS = "HOS week rest days"; public static final String MISSED_SHOW_PENALTY = "Minimize missed shows"; public static final String REVENUE_OPPORTUNITY = "Maximize revenue opportunity"; @@ -29,7 +32,10 @@ public class RockTourParametrization extends AbstractPersistable { public static final String DELAY_COST_PER_DAY = "Visit sooner than later"; private long earlyLateBreakDrivingSecondsBudget = 1L * 60L * 60L; - private long nightDrivingSecondsBudget = 8L * 60L * 60L; + private long nightDrivingSecondsBudget = 7L * 60L * 60L; + private long hosWeekDrivingSecondsBudget = 50L * 60L * 60L; + private int hosWeekConsecutiveDrivingDaysBudget = 7; + private int hosWeekRestDays = 2; private long missedShowPenalty = 0; private long revenueOpportunity = 1; @@ -71,6 +77,30 @@ public void setNightDrivingSecondsBudget(long nightDrivingSecondsBudget) { this.nightDrivingSecondsBudget = nightDrivingSecondsBudget; } + public long getHosWeekDrivingSecondsBudget() { + return hosWeekDrivingSecondsBudget; + } + + public void setHosWeekDrivingSecondsBudget(long hosWeekDrivingSecondsBudget) { + this.hosWeekDrivingSecondsBudget = hosWeekDrivingSecondsBudget; + } + + public int getHosWeekConsecutiveDrivingDaysBudget() { + return hosWeekConsecutiveDrivingDaysBudget; + } + + public void setHosWeekConsecutiveDrivingDaysBudget(int hosWeekConsecutiveDrivingDaysBudget) { + this.hosWeekConsecutiveDrivingDaysBudget = hosWeekConsecutiveDrivingDaysBudget; + } + + public int getHosWeekRestDays() { + return hosWeekRestDays; + } + + public void setHosWeekRestDays(int hosWeekRestDays) { + this.hosWeekRestDays = hosWeekRestDays; + } + public long getRevenueOpportunity() { return revenueOpportunity; } diff --git a/optaplanner-examples/src/main/java/org/optaplanner/examples/rocktour/domain/solver/RockShowDateUpdatingVariableListener.java b/optaplanner-examples/src/main/java/org/optaplanner/examples/rocktour/domain/solver/RockShowVariableListener.java similarity index 51% rename from optaplanner-examples/src/main/java/org/optaplanner/examples/rocktour/domain/solver/RockShowDateUpdatingVariableListener.java rename to optaplanner-examples/src/main/java/org/optaplanner/examples/rocktour/domain/solver/RockShowVariableListener.java index 6043722547..311ae7d5ff 100644 --- a/optaplanner-examples/src/main/java/org/optaplanner/examples/rocktour/domain/solver/RockShowDateUpdatingVariableListener.java +++ b/optaplanner-examples/src/main/java/org/optaplanner/examples/rocktour/domain/solver/RockShowVariableListener.java @@ -19,7 +19,6 @@ import java.time.LocalDate; import java.util.Objects; -import org.apache.commons.lang3.tuple.Pair; import org.optaplanner.core.impl.domain.variable.listener.VariableListener; import org.optaplanner.core.impl.score.director.ScoreDirector; import org.optaplanner.examples.rocktour.domain.RockShow; @@ -27,7 +26,9 @@ import org.optaplanner.examples.rocktour.domain.RockTimeOfDay; import org.optaplanner.examples.rocktour.domain.RockTourSolution; -public class RockShowDateUpdatingVariableListener implements VariableListener { +import static java.time.temporal.ChronoUnit.*; + +public class RockShowVariableListener implements VariableListener { @Override public void beforeEntityAdded(ScoreDirector scoreDirector, RockShow show) { @@ -63,56 +64,103 @@ protected void updateDate(ScoreDirector scoreDirector, RockShow sourceShow) { RockTourSolution solution = (RockTourSolution) scoreDirector.getWorkingSolution(); RockStandstill previousStandstill = sourceShow.getPreviousStandstill(); - Pair arrival = calculateArrival(solution, sourceShow, previousStandstill); + Arrival arrival = calculateArrival(solution, sourceShow, previousStandstill); RockShow shadowShow = sourceShow; while (shadowShow != null - && !(Objects.equals(shadowShow.getDate(), arrival.getLeft()) - && Objects.equals(shadowShow.getTimeOfDay(), arrival.getRight()))) { + && !(Objects.equals(shadowShow.getDate(), arrival.date) + && Objects.equals(shadowShow.getTimeOfDay(), arrival.timeOfDay) + && shadowShow.getHosWeekStart() == arrival.hosWeekStart + && Objects.equals(shadowShow.getHosWeekDrivingSecondsTotal(), arrival.hosWeekDrivingSecondsTotal))) { scoreDirector.beforeVariableChanged(shadowShow, "date"); - shadowShow.setDate(arrival.getLeft()); + shadowShow.setDate(arrival.date); scoreDirector.afterVariableChanged(shadowShow, "date"); scoreDirector.beforeVariableChanged(shadowShow, "timeOfDay"); - shadowShow.setTimeOfDay(arrival.getRight()); + shadowShow.setTimeOfDay(arrival.timeOfDay); scoreDirector.afterVariableChanged(shadowShow, "timeOfDay"); + scoreDirector.beforeVariableChanged(shadowShow, "hosWeekStart"); + shadowShow.setHosWeekStart(arrival.hosWeekStart); + scoreDirector.afterVariableChanged(shadowShow, "hosWeekStart"); + scoreDirector.beforeVariableChanged(shadowShow, "hosWeekDrivingSecondsTotal"); + shadowShow.setHosWeekDrivingSecondsTotal(arrival.hosWeekDrivingSecondsTotal); + scoreDirector.afterVariableChanged(shadowShow, "hosWeekDrivingSecondsTotal"); + RockShow previousShow = shadowShow; shadowShow = shadowShow.getNextShow(); arrival = calculateArrival(solution, shadowShow, previousShow); } } - private Pair calculateArrival(RockTourSolution solution, RockShow show, RockStandstill previousStandstill) { + private Arrival calculateArrival(RockTourSolution solution, RockShow show, RockStandstill previousStandstill) { if (show == null || previousStandstill == null || previousStandstill.getDepartureDate() == null) { - return Pair.of(null, null); + return new Arrival(null, null, null, null); } long earlyLateBreakDrivingSecondsBudget = solution.getParametrization().getEarlyLateBreakDrivingSecondsBudget(); long nightDrivingSecondsBudget = solution.getParametrization().getNightDrivingSecondsBudget(); + long hosWeekDrivingSecondsBudget = solution.getParametrization().getHosWeekDrivingSecondsBudget(); + int hosWeekConsecutiveDrivingDaysBudget = solution.getParametrization().getHosWeekConsecutiveDrivingDaysBudget(); + int hosWeekRestDays = solution.getParametrization().getHosWeekRestDays(); - long drivingSeconds = show.getDrivingTimeFromPreviousStandstill(); RockTimeOfDay timeOfDay = previousStandstill.getDepartureTimeOfDay(); - LocalDate date = previousStandstill.getDepartureDate(); + LocalDate arrivalDate = previousStandstill.getDepartureDate(); + RockStandstill hosWeekStart = previousStandstill.getHosWeekStart(); + Long hosWeekDrivingSecondsTotal = previousStandstill.getHosWeekDrivingSecondsTotal(); + + long drivingSeconds = show.getDrivingTimeFromPreviousStandstill(); + // HOS driving time per day limits while (drivingSeconds >= 0) { if (timeOfDay == RockTimeOfDay.EARLY) { drivingSeconds -= earlyLateBreakDrivingSecondsBudget; timeOfDay = RockTimeOfDay.LATE; } else { drivingSeconds -= nightDrivingSecondsBudget; - date = date.plusDays(1); + arrivalDate = arrivalDate.plusDays(1); timeOfDay = RockTimeOfDay.EARLY; } } + hosWeekDrivingSecondsTotal += drivingSeconds; + // HOS driving time per week limits: add weekend rest period if driving for too many hour or too many days + if (hosWeekDrivingSecondsTotal > hosWeekDrivingSecondsBudget + || hosWeekStart.getDepartureDate().until(arrivalDate, DAYS) > hosWeekConsecutiveDrivingDaysBudget) { + arrivalDate = arrivalDate.plusDays(hosWeekRestDays); + hosWeekStart = show; + hosWeekDrivingSecondsTotal = 0L; + timeOfDay = RockTimeOfDay.EARLY; + } if (show.getDurationInHalfDay() % 2 == 0 && timeOfDay != RockTimeOfDay.EARLY) { // Don't split up full days - date = date.plusDays(1); + arrivalDate = arrivalDate.plusDays(1); timeOfDay = RockTimeOfDay.EARLY; } - LocalDate arrivalDate = show.getAvailableDateSet().ceiling(date); - if (!date.equals(arrivalDate)) { + // Fast forward to next available date of venue + LocalDate showDate = show.getAvailableDateSet().ceiling(arrivalDate); + if (showDate == null || showDate.compareTo(show.getBus().getEndDate()) >= 0) { + return new Arrival(null, null, null, null); + } + if (!arrivalDate.equals(showDate) && timeOfDay != RockTimeOfDay.EARLY) { + // If the show date is later than the arrival date, reset back to the early time of day timeOfDay = RockTimeOfDay.EARLY; } - if (arrivalDate == null || arrivalDate.compareTo(show.getBus().getEndDate()) >= 0) { - return Pair.of(null, null); + // HOS driving time per week limits: reset week + if (arrivalDate.until(showDate, DAYS) >= 2) { + hosWeekStart = show; + hosWeekDrivingSecondsTotal = 0L; + } + return new Arrival(showDate, timeOfDay, hosWeekStart, hosWeekDrivingSecondsTotal); + } + + private class Arrival { + + public LocalDate date; + public RockTimeOfDay timeOfDay; + public RockStandstill hosWeekStart; + public Long hosWeekDrivingSecondsTotal; + + public Arrival(LocalDate date, RockTimeOfDay timeOfDay, RockStandstill hosWeekStart, Long hosWeekDrivingSecondsTotal) { + this.date = date; + this.timeOfDay = timeOfDay; + this.hosWeekStart = hosWeekStart; + this.hosWeekDrivingSecondsTotal = hosWeekDrivingSecondsTotal; } - return Pair.of(arrivalDate, timeOfDay); } } diff --git a/optaplanner-examples/src/main/java/org/optaplanner/examples/rocktour/persistence/RockTourXslxFileIO.java b/optaplanner-examples/src/main/java/org/optaplanner/examples/rocktour/persistence/RockTourXslxFileIO.java index e97bdaecb2..8d5ac6bbef 100644 --- a/optaplanner-examples/src/main/java/org/optaplanner/examples/rocktour/persistence/RockTourXslxFileIO.java +++ b/optaplanner-examples/src/main/java/org/optaplanner/examples/rocktour/persistence/RockTourXslxFileIO.java @@ -96,6 +96,12 @@ private void readConfiguration() { "Maximum driving time in seconds between 2 shows on the same day."); readLongConstraintLine(NIGHT_DRIVING_SECONDS, parametrization::setNightDrivingSecondsBudget, "Maximum driving time in seconds per night between 2 shows."); + readLongConstraintLine(HOS_WEEK_DRIVING_SECONDS_BUDGET, parametrization::setHosWeekDrivingSecondsBudget, + "Maximum driving time in seconds since last weekend rest."); + readIntConstraintLine(HOS_WEEK_CONSECUTIVE_DRIVING_DAYS_BUDGET, parametrization::setHosWeekConsecutiveDrivingDaysBudget, + "Maximum driving days since last weekend rest."); + readIntConstraintLine(HOS_WEEK_REST_DAYS, parametrization::setHosWeekRestDays, + "Minimum weekend rest in days (actually in full night sleeps: 2 days guarantees only 32 hours)."); nextRow(true); readHeaderCell("Constraint"); readHeaderCell("Weight"); @@ -324,7 +330,7 @@ public Workbook write() { } private void writeConfiguration() { - nextSheet("Configuration", 1, 5, false); + nextSheet("Configuration", 1, 8, false); nextRow(); nextHeaderCell("Tour name"); nextCell().setCellValue(solution.getTourName()); @@ -333,6 +339,12 @@ private void writeConfiguration() { "Maximum driving time in seconds between 2 shows on the same day."); writeLongConstraintLine(NIGHT_DRIVING_SECONDS, parametrization::getNightDrivingSecondsBudget, "Maximum driving time in seconds per night between 2 shows."); + writeLongConstraintLine(HOS_WEEK_DRIVING_SECONDS_BUDGET, parametrization::getHosWeekDrivingSecondsBudget, + "Maximum driving time in seconds since last weekend rest."); + writeIntConstraintLine(HOS_WEEK_CONSECUTIVE_DRIVING_DAYS_BUDGET, parametrization::getHosWeekConsecutiveDrivingDaysBudget, + "Maximum driving days since last weekend rest."); + writeIntConstraintLine(HOS_WEEK_REST_DAYS, parametrization::getHosWeekRestDays, + "Minimum weekend rest in days (actually in full night sleeps: 2 days guarantees only 32 hours)."); nextRow(); nextRow(); nextHeaderCell("Constraint");