Skip to content

Commit

Permalink
rock tour: Hours of Service regulations
Browse files Browse the repository at this point in the history
  • Loading branch information
ge0ffrey committed May 25, 2018
1 parent a68afe4 commit d501924
Show file tree
Hide file tree
Showing 7 changed files with 160 additions and 23 deletions.
Binary file modified optaplanner-examples/data/rocktour/unsolved/48shows.xlsx
Binary file not shown.
Expand Up @@ -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;
Expand Down
Expand Up @@ -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.*;

Expand All @@ -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() {
}
Expand Down Expand Up @@ -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;
}

}
Expand Up @@ -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;
*/
Expand Down
Expand Up @@ -22,14 +22,20 @@ 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";
public static final String DRIVING_TIME_COST_PER_SECOND = "Minimize driving time cost";
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;
Expand Down Expand Up @@ -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;
}
Expand Down
Expand Up @@ -19,15 +19,16 @@
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;
import org.optaplanner.examples.rocktour.domain.RockStandstill;
import org.optaplanner.examples.rocktour.domain.RockTimeOfDay;
import org.optaplanner.examples.rocktour.domain.RockTourSolution;

public class RockShowDateUpdatingVariableListener implements VariableListener<RockShow> {
import static java.time.temporal.ChronoUnit.*;

public class RockShowVariableListener implements VariableListener<RockShow> {

@Override
public void beforeEntityAdded(ScoreDirector scoreDirector, RockShow show) {
Expand Down Expand Up @@ -63,56 +64,103 @@ protected void updateDate(ScoreDirector scoreDirector, RockShow sourceShow) {
RockTourSolution solution = (RockTourSolution) scoreDirector.getWorkingSolution();

RockStandstill previousStandstill = sourceShow.getPreviousStandstill();
Pair<LocalDate, RockTimeOfDay> 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<LocalDate, RockTimeOfDay> 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);
}

}
Expand Up @@ -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");
Expand Down Expand Up @@ -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());
Expand All @@ -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");
Expand Down

0 comments on commit d501924

Please sign in to comment.