In the same project, I don't want two jobs to run in parallel. How should I design it?
Is there a rule in the drl file that does not allow two jobs under the same project to run at the same time?
If there is no such thing, how should two jobs under the same project not run simultaneously?
rule "nonrenewableResourceCapacity"
when
$resource : Resource(renewable == false, $capacity : capacity)
accumulate(
ResourceRequirement(resource == $resource,
$executionMode : executionMode,
$requirement : requirement)
and Allocation(executionMode == $executionMode);
$used : sum($requirement);
$used > $capacity
)
then
scoreHolder.addHardConstraintMatch(kcontext, 0, $capacity - $used);
end
rule "renewableResourceUsedDay"
salience 1 // Do these rules first (optional, for performance)
when
ResourceRequirement(resourceRenewable == true, $executionMode : executionMode, $resource : resource)
Allocation(executionMode == $executionMode,
$startDate : startDate, $endDate : endDate)
then
for (int i = $startDate; i < $endDate; i++) {
insertLogical(new RenewableResourceUsedDay($resource, i));
}
end
rule "renewableResourceCapacity"
when
RenewableResourceUsedDay($resource : resource, $capacity : resourceCapacity, $usedDay : usedDay)
accumulate(
ResourceRequirement(resource == $resource,
$executionMode : executionMode,
$requirement : requirement)
and Allocation(executionMode == $executionMode, $usedDay >= startDate, $usedDay < endDate);
$used : sum($requirement);
$used > $capacity
)
then
scoreHolder.addHardConstraintMatch(kcontext, 0, $capacity - $used);
end
// ############################################################################
// Soft constraints
// ############################################################################
rule "totalProjectDelay"
when
Allocation(jobType == JobType.SINK, endDate != null, $endDate : endDate,
$criticalPathEndDate : projectCriticalPathEndDate)
then
scoreHolder.addSoftConstraintMatch(kcontext, 0, $criticalPathEndDate - $endDate);
end
rule "totalMakespan"
when
accumulate(
Allocation(jobType == JobType.SINK, $endDate : endDate);
$maxProjectEndDate : max($endDate)
)
then
scoreHolder.addSoftConstraintMatch(kcontext, 1, - (Integer) $maxProjectEndDate);
end
In task assignment, when you never want to run 2 jobs in parallel (so its a hard constraint, for all jobs), I'd probably make it a build-in hard constraint and basically model it like TSP.
If it's just pairs of specific jobs that shouldn't run in parallel, I'd have the variable listener detect that the 2 jobs would be run at the same time and delay the start time of the job that can start the latest. If they can both start at the same time, the one with the lowest id starts first and the other is delayed. This last bit is to avoid score corruption with incremental calculation.
Related
Optaplanner is being used to plan the routes of a fleet of vehicles and I am optimizing the route times.
I have a scenario where I have one visit with time windows in the morning and the second visit with time window in the afternoon. However the vehicle leaves when the time window opens, makes the first delivery and heads to the second visit. Since the second visit has a time window in the afternoon, the vehicle has to wait for the time window of this visit to open, which introduces downtime. This downtime (idle time) can be reduced by leaving the depot later. So I would like to ask if:
Is there any rule to q backtrack to the depot or to the previous visit, wait longer to continue, and thereby reduce the downtime or waiting time on the second visit?
I have tried different variants:
1- I implemented a constraint to penalize if early to a visit and penalize with customer -> customer.getReadyTime() - customer.getArrivalTime().
This may optimize but it does not roll back the arrivalTime.
2- Modify my listener (ArrivalTimeUpdatingVariableListener, updateArrivalTime method).When calculating the arrival time, if there is idle time, I go to the previous visit and subtract the idle time. However, in some cases it does not recursively update all previous visits correctly, and in other cases it gives me a "VariableListener corruption". I have had no success for this variant either.
Is there any rule to wait or roll back and update all visits again?
I attach my constraint and listener for better context.
ArrivalTimeUpdatingVariableListener.class:
protected void updateArrivalTime(ScoreDirector scoreDirector, TimeWindowedVisit sourceCustomer) {
Standstill previousStandstill = sourceCustomer.getPreviousStandstill();
Long departureTime = previousStandstill == null ? null
: (previousStandstill instanceof TimeWindowedVisit)
? ((TimeWindowedVisit) previousStandstill).getArrivalTime() + ((TimeWindowedVisit) previousStandstill).getServiceDuration()
: ((PlanningVehicle) previousStandstill).getDepot() != null
? ((TimeWindowedDepot) ((PlanningVehicle) previousStandstill).getDepot()).getReadyTime()
: 0;
TimeWindowedVisit shadowCustomer = sourceCustomer;
Long arrivalTime = calculateArrivalTime(shadowCustomer, departureTime);
while (shadowCustomer != null && !Objects.equals(shadowCustomer.getArrivalTime(), arrivalTime)) {
scoreDirector.beforeVariableChanged(shadowCustomer, "arrivalTime");
shadowCustomer.setArrivalTime(arrivalTime);
scoreDirector.afterVariableChanged(shadowCustomer, "arrivalTime");
departureTime = shadowCustomer.getDepartureTime();
shadowCustomer = shadowCustomer.getNextVisit();
arrivalTime = calculateArrivalTime(shadowCustomer, departureTime);
}
}
private Long calculateArrivalTime(TimeWindowedVisit customer, Long previousDepartureTime) {
long arrivalTime = 0;
if (customer == null || customer.getPreviousStandstill() == null) {
return null;
}
if (customer.getPreviousStandstill() instanceof PlanningVehicle) {
arrivalTime = Math.max(customer.getReadyTime(),
previousDepartureTime + customer.distanceFromPreviousStandstill());
} else {
arrivalTime = previousDepartureTime + customer.distanceFromPreviousStandstill();
// to reach backwards and (attempt to) shift the previous arrival time.
Standstill previousStandstill = customer.getPreviousStandstill();
long idle = customer.getReadyTime() - arrivalTime;
if (previousStandstill != null && idle > 0) {
arrivalTime += idle;
if (previousStandstill instanceof TimeWindowedVisit) {
long previousArrival = ((TimeWindowedVisit) previousStandstill).getArrivalTime() + idle;
if (previousArrival > ((TimeWindowedVisit) previousStandstill).getDueTime()){
System.out.println("Arrival es mayor que el duetime");
previousArrival = ((TimeWindowedVisit) previousStandstill).getDueTime() - ((TimeWindowedVisit) previousStandstill).getServiceDuration();
}
((TimeWindowedVisit) previousStandstill).setArrivalTime(previousArrival);
}
}
}
// breaks
return arrivalTime;
}
ConstraintProvider.class:
private Constraint arrivalEarly(ConstraintFactory constraintFactory) {
return constraintFactory.from(TimeWindowedVisit.class)
.filter((customer) -> !customer.getVehicle().isGhost() && customer.getArrivalTime() < customer.getReadyTime())
.penalizeConfigurableLong(
VehicleRoutingConstraintConfiguration.MINIMIZE_IDLE_TIME,
customer -> customer.getReadyTime() - customer.getArrivalTime());
}
Currently, I have a roster of 30 shifts and 25 employees with there availability.
This 25 employees match the shift start and end time with employee availability.
Still, Opta only assigns 19 shifts and left all other shifts as blank and do not assign remaining 6 employees.
Here my assumption was, it should assign all 25 employees as their time matches with the shift.
Do I missed something here or should look at any other aspect as well?
Below is my Opta rules file, I have removed all other rules as they were not required in my case.
Opta employee-rostering version currently using 7.28.0-SNAPSHOT
time given roster for solving 240secs.
// ############################################################################
// Hard constraints
// ############################################################################
rule "Unavailable time slot for an employee"
when
EmployeeAvailability(
$e : employee,
$employeeName : employee.getName(),
$startDateTime : startDateTime,
$endDateTime : endDateTime)
Shift(
employee == $e,
!DateTimeUtils.doTimeslotsMatch($startDateTime,$endDateTime, startDateTime, endDateTime, $employeeName))
then
scoreHolder.addHardConstraintMatch(kcontext, -100);
end
rule "No overlapping shifts for an employee"
when
$s : Shift( employee != null,
$e : employee,
$employeeName : employee.getName(),
$firstStartDateTime: startDateTime,
$firstEndDateTime : endDateTime)
$s2: Shift( employee == $e,
this != $s,
DateTimeUtils.doTimeslotsMatch($firstStartDateTime,$firstEndDateTime, startDateTime, endDateTime, $employeeName))
then
scoreHolder.addHardConstraintMatch(kcontext, -100);
end
// ############################################################################
// Medium constraints
// ############################################################################
rule "Assign every possible shift"
when
Shift(employee == null)
then
scoreHolder.addMediumConstraintMatch(kcontext, -100);
end
// ############################################################################
// Soft constraints
// ############################################################################
rule "available time slot for an employee"
when
$rosterParametrization : RosterParametrization(desiredTimeSlotWeight != 0)
EmployeeAvailability(
$e : employee,
$employeeName : employee.getName(),
$startDateTime : startDateTime,
$endDateTime : endDateTime)
Shift(
employee == $e,
DateTimeUtils.doTimeslotsMatch($startDateTime,$endDateTime, startDateTime, endDateTime, $employeeName))
then
scoreHolder.addSoftConstraintMatch(kcontext, 100);
end
rule "Skill set preference"
when
Shift(employee != null, matchedPreferencedDisciplineCount > 0,$matchedPreferencedDisciplineCount : matchedPreferencedDisciplineCount)
then
scoreHolder.addSoftConstraintMatch(kcontext, + $matchedPreferencedDisciplineCount);
end
Here is my updated solver configuration file.
<?xml version="1.0" encoding="UTF-8"?>
<solver>
<!--<environmentMode>FAST_ASSERT</environmentMode>-->
<solutionClass>org.optaweb.employeerostering.domain.roster.Roster</solutionClass>
<entityClass>org.optaweb.employeerostering.domain.shift.Shift</entityClass>
<scoreDirectorFactory>
<scoreDrl>org/optaweb/employeerostering/service/solver/employeeRosteringScoreRules.drl</scoreDrl>
</scoreDirectorFactory>
<termination>
<secondsSpentLimit>240</secondsSpentLimit>
</termination>
<localSearch>
<unionMoveSelector>
<pillarChangeMoveSelector>
<subPillarType>SEQUENCE</subPillarType>
</pillarChangeMoveSelector>
<pillarSwapMoveSelector>
<subPillarType>SEQUENCE</subPillarType>
</pillarSwapMoveSelector>
</unionMoveSelector>
<acceptor>
<entityTabuSize>7</entityTabuSize>
</acceptor>
<forager>
<acceptedCountLimit>800</acceptedCountLimit>
</forager>
</localSearch>
</solver>
Also it enforces me to implements Comparable on planning entity shift
public class Shift extends AbstractPersistable implements Comparable<Shift> {
private static final Comparator<Shift> PILLAR_SEQUENCE_COMPARATOR = Comparator
.comparing((Shift a) -> a.getStartDateTime())
.thenComparing(a -> a.getEndDateTime());
Will this resolve my problem of solver not assigning employees although they are available and will remove itself from local optima.
Use Pilar based move selectors to escape local optima more efficiently.
datatype CACHE_STATE = I| S| E
datatype MSG_CMD = Empty| ReqS| ReqE| Inv| InvAck| GntS| GntE
type NODE=nat
type DATA=nat
type boolean=bool
class class_0 {
var
Data : DATA,
Cmd : MSG_CMD
}
class class_1 {
var
Data : DATA,
State : CACHE_STATE
}
method n_RecvGntSinv__1_2(
Chan2 : array<class_0 > ,
Cache : array<class_1 > ,i:nat, N1:nat ,p__Inv0:nat,p__Inv2:nat)
modifies Chan2[i]
modifies Cache[i]
requires 0<= i<N1
requires Cache.Length ==N1
requires N1>0
requires Chan2.Length ==N1
requires p__Inv0!=p__Inv2&&p__Inv2<N1&& p__Inv0<N1
requires Chan2[i] != null
requires Cache[i] !=null
requires i!=p__Inv0&&i!=p__Inv2
requires (!((Cache[p__Inv2].State == E) && (!(Cache[p__Inv0].State == I))))
requires (Chan2[i].Cmd == GntS)
ensures Cache==old(Cache)
ensures Chan2==old(Chan2)
ensures (!((Cache[p__Inv2].State == E) && (!(Cache[p__Inv0].State == I))))
{
Cache[i].State := S;
Cache[i].Data := Chan2[i].Data;
Chan2[i].Cmd := Empty;
}
I have placed the requirement i is different from p__Inv2 and p_Inv0, thus the assignments should not disturb the evaluation of the invariant.
It is obvious that the invariant (!((Cache[p__Inv2].State == E) && (!(Cache[p__Inv0].State == I)))) should hold if it holds before execution.
Dafny shows my assertions fail and gives a counterexample I cann't understand.
Your precondition allows the possibility that Cache[i] references the same object as Cache[p__Inv0] or Cache[p__Inv2]. If that's what you intended, then the method body is indeed incorrect, as reported by the verifier. If that's not what you intended, then a precondition like
requires forall j,k :: 0 <= j < k < Cache.Length ==> Cache[j] != Cache[k]
will make your method verify.
My local search part in solver config looks like:
<acceptor>
<lateAcceptanceSize>400</lateAcceptanceSize>
<entityTabuSize>9</entityTabuSize>
</acceptor>
<forager>
<acceptedCountLimit>2000</acceptedCountLimit>
</forager>
and everything works fine but when I change it to(what can cause optimization gain I think):
<acceptor>
<lateAcceptanceSize>600</lateAcceptanceSize>
</acceptor>
<forager>
<acceptedCountLimit>4</acceptedCountLimit>
</forager>
After solver starts working I got exception
Score corruption: the solution's score (-20hard/-8medium/-4soft) is not the uncorruptedScore (-20hard/-8medium/-8soft)
What can cause this problem? (It is only information from FULL_ASSERT mode)
EDIT:
Something can be connected to rule:
// Boundary lessons have to be schedulead at the beginning/end in a day
rule "boundaryLesson"
when
$oddzial : Oddzial()
$boundaryLesson : Lesson(scheduled == true, containsOddzial($oddzial), base.lessonLimits.isBoundaryLesson == true, $base : base)
exists Lesson(scheduled == true, containsOddzial($oddzial), dayLessonNumber.day == $base.day, base.lessonNumberFrom < $base.lessonNumberFrom)
and exists Lesson(scheduled == true, containsOddzial($oddzial), dayLessonNumber.day == $base.day, base.lessonNumberTo > $base.lessonNumberTo)
then
scoreHolder.addHardConstraintMatch(kcontext, -1);
end
because, sometimes I get following error also:
Score corruption: the workingScore (0hard/-2medium/0soft) is not the uncorruptedScore (-1hard/-2medium/0soft) after completedAction (8848-537:Tuesday-3 {com.pbz.plek.model.simple.DayLessonNr#5924af87 -> com.pbz.plek.model.simple.DayLessonNr#5924af87}):
The corrupted scoreDirector has no ConstraintMatch(s) which are in excess.
The corrupted scoreDirector has 1 ConstraintMatch(s) which are missing:
com.praca.mgr.cp.algorytm.solver/boundaryLesson/level0/[8854-537:Tuesday-2, com.krakfin.pbz.plek.model.simple.Oddzial#c9d4]=-1
Check your score constraints.
I know how incremental score calculation works but I cannot see what can be wrong with this rule
In both cases you'll have potential score corruption, but only in the second case it surfaces. For production reliability, you'll definitely want to fix it.
See docs on "incremental score calculation" to understand what score corruption is. Usual causes:
Shadow variable corruption. Use OptaPlanner 6.3.0.Final or later and it will show up as "VariableListener corruption" instead of "Score corruption" and provide more info.
A bad custom Move due to a bad undo move. Normally this will show up as "Undo Move corruption" instead of "Score corruption".
A bad custom Move that acts different the second time it's done on the same solution state. This will be detected during processWorkingSolutionDuringStep().
If you use Drools calculation:
A bad score rule that causes "Score corruption". As of OptaPlanner 6.1 this is unlikely, because it's much harder to write a bad score rule. Try commenting out score rules to figure out which one is to blame.
A bug in Drools. Unlikely, but possible. Create a dedicated reproducer and submit a jira.
If you use incremental score calculation:
A bad Java Incremental score calculator. Use <assertScoreDirectorFactor> with an easy score calculator too. Good luck in this case.
To sum up:
I am using OptaPlanner 6.3.0.Final with FullAssert environment mode
I removed custom move to exclude possibility of bugs in that section
There are two plannig variables: Sala(Room) and DzienNrLekcji(Object consists day and lesson number)
There aren't any shadow variables
Problems with corrupted score exists at every score level so I left
only hard constraints rules section, which looks like that:
// ############################################################################
// Hard constraints
// ############################################################################
// two Lessons at the same time should be in another rooms.
rule "salaOccupancy"
when
$leftLesson : Lesson($id : base.numericId, scheduled == true, $sala : sala)
not Lesson(scheduled == true, timeCollision($leftLesson), sala == $sala, base.numericId < $id)
$rightLesson : Lesson(scheduled == true, timeCollision($leftLesson), sala == $sala, base.numericId > $id)
then
scoreHolder.addHardConstraintMatch(kcontext, -10);
end
// each oddzial and nauczyciel can't have two lessons at the same time
rule "przydzialCollision"
when
$przydzialConflict : PrzydzialConflict($leftPrzydzial : leftPrzydzial, $rightPrzydzial : rightPrzydzial)
$leftLesson : Lesson(scheduled == true, base.przydzial == $leftPrzydzial)
$rightLesson : Lesson(scheduled == true, base.przydzial == $rightPrzydzial, timeCollision($leftLesson), this != $leftLesson)
then
scoreHolder.addHardConstraintMatch(kcontext, -2 * $przydzialConflict.getConflictCount());
end
// sala's capacity shouldn't be exceeded
rule "salaCapacity"
when
$sala : Sala($capacity : ograniczenia.maxLiczbaUczniow.max)
$lesson : Lesson(scheduled == true, sala == $sala)
$limit : LessonStudentLimit(lesson == $lesson, numberOfStudents > $capacity)
then
scoreHolder.addHardConstraintMatch(kcontext, -2);
end
// cannot put lesson into not available time period in Sala or Przydzial
rule "availability"
when
Lesson( scheduled == true , dostepnaSala == false )
or Lesson( scheduled == true , dostepnyPrzydzial == false)
then
scoreHolder.addHardConstraintMatch(kcontext, -2);
end
// Oddzials cannot have gaps between classes during a day
rule "gaps"
when
$oddzial : Oddzial()
$dzien : DzienTygodnia()
$lessonList : ArrayList(LessonBlockCounter.calculateOddzialGaps($lessonList,TimetableSolution.maxLessonNr)>0) from collect (
Lesson(scheduled == true, containsOddzial($oddzial), dzienNrLekcji.dzien == $dzien)
)
then
scoreHolder.addHardConstraintMatch(kcontext, -5*LessonBlockCounter.calculateOddzialGaps($lessonList,TimetableSolution.maxLessonNr));
end
// If Przydzial has blocks distribution defined, only one lesson per day is allowed
rule "blocks"
when
$przydzial : Przydzial( ograniczenia.ograniczeniaBlokiLekcyjnePrzydzialu.czyTylkoJednaLekcjaNaDzien.isAktywne() == true )
$dzien : DzienTygodnia()
$lessonCount : Number( intValue > 1 ) from accumulate (
$lesson : Lesson(scheduled == true, base.przydzial == $przydzial,dzienNrLekcji.dzien == $dzien),
count($lesson)
)
then
scoreHolder.addHardConstraintMatch(kcontext, -2);
end
// Boundary lessons have to be schedulead at the beginning/end in a day
rule "boundaryLesson"
when
$oddzial : Oddzial()
$boundaryLesson : Lesson(scheduled == true, containsOddzial($oddzial), base.ograniczeniaLekcja.czyLekcjaGraniczna.aktywne == true, $base : base)
exists Lesson(scheduled == true, containsOddzial($oddzial), dzienNrLekcji.dzien == $base.dzien, base.lekcjaNrOd < $base.lekcjaNrOd)
and exists Lesson(scheduled == true, containsOddzial($oddzial), dzienNrLekcji.dzien == $base.dzien, base.lekcjaNrDo > $base.lekcjaNrDo)
then
scoreHolder.addHardConstraintMatch(kcontext, -1);
end
// Linked lessons have to take place at the same time
rule "linkedLesson"
when
$linkedLesson : Lesson(scheduled == true, base.ograniczeniaLekcja.lekcjePolaczone.empty == false, $dzienNrLekcji : dzienNrLekcji)
Lesson(scheduled == true, base.ograniczeniaLekcja.lekcjePolaczone contains $linkedLesson.base, dzienNrLekcji != $dzienNrLekcji)
then
scoreHolder.addHardConstraintMatch(kcontext, -5);
end
// Linked lessons have to take place at the same time
rule "scheduledLinkedLesson"
when
$linkedLesson : Lesson(scheduled == false, base.ograniczeniaLekcja.lekcjePolaczone.empty == false)
then
scoreHolder.addHardConstraintMatch(kcontext, -10*$linkedLesson.getBase().getCzasTrwania());
end
// Lessons have to be placed in the school time boundaries
rule "schoolTime"
when
$lesson : Lesson(scheduled == true, base.czasTrwania > 1 , base.lekcjaNrOd > TimetableSolution.maxLessonNr - base.czasTrwania)
then
scoreHolder.addHardConstraintMatch(kcontext, -5);
end
// Lessons have to be scheduled in one of the preferred sala
rule "assignedSalaPrzydzialu"
when
$lesson : Lesson( scheduled == true,
sala not memberOf base.przydzial.ograniczenia.perferowaneSale.preferowaneSale.saleList )
then
scoreHolder.addHardConstraintMatch(kcontext, -1);
end
// ############################################################################
// Medium constraints
// ############################################################################
//lesson have to have sala and day assigned, not assigned lessons are acceptable in overconstrained problem
rule "scheduledLesson"
when
$lesson : Lesson( scheduled == false )
then
scoreHolder.addMediumConstraintMatch(kcontext, -$lesson.getBase().getCzasTrwania());
end
After running algorithm, I'am getting exception:
2015-11-04 10:39:21,493 [http-8080-3] INFO org.optaplanner.core.impl.solver.DefaultSolver - Solving started: time spent (426), best score (uninitialized/-160hard/-165medium/0soft), environment mode (FULL_ASSERT), random (JDK with seed 0).
2015-11-04 10:39:23,969 [http-8080-3] INFO org.optaplanner.core.impl.constructionheuristic.DefaultConstructionHeuristicPhase - Construction Heuristic phase (0) ended: step total (165), time spent (2903), best score (-160hard/-165medium/0soft).
2015-11-04 10:39:24,615 [http-8080-3] ERROR org.apache.struts2.dispatcher.Dispatcher - Exception occurred during processing request: Score corruption: the solution's score (-123hard/-161medium/0soft) is not the uncorruptedScore (-126hard/-160medium/0soft).
java.lang.IllegalStateException: Score corruption: the solution's score (-123hard/-161medium/0soft) is not the uncorruptedScore (-126hard/-160medium/0soft).
at org.optaplanner.core.impl.score.director.AbstractScoreDirectorFactory.assertScoreFromScratch(AbstractScoreDirectorFactory.java:100)
at org.optaplanner.core.impl.solver.scope.DefaultSolverScope.assertScoreFromScratch(DefaultSolverScope.java:127)
at org.optaplanner.core.impl.solver.recaller.BestSolutionRecaller.processWorkingSolutionDuringStep(BestSolutionRecaller.java:107)
...
After studied problem I am pretty sure that it's connected with incremental score calculation and drl file. I thought that problem causes "gaps" rule because "calulateOddzialGaps" method checks day and lesson number of collected $lessonList, but after commented this rule problem still exists. Any other rule doesn't use lessons(planningEntity) at WHEN section inside java method. What can be wrong? I don't have any other ideas...
I am using OptaPlanner to solve what is effectively the Traveling Salesman Problem with Time Windows (TSPTW). I have a working initial solution based on the OptaPlanner provided VRPTW example.
I am now trying to address my requirements that deviate from the standard TSPTW, which are:
I am trying to minimize the total time spent rather than the total distance traveled. Because of this, idle time counts against me.
In additional to the standard time windowed visits I also must support no-later-than (NLT) visits (i.e. don't visit after X time) and no-earlier-than (NET) visits (i.e don't visit before X time).
My current solution always sets the first visit's arrival time to that visit's start time. This has the following problems with respect to my requirements:
This can introduce unnecessary idle time that could be avoided if the visit was arrived at sometime later in its time window.
The behavior with NLT is problematic. If I define an NLT with the start time set to Long.MIN_VALUE (to represent that it is unbounded without resorting to nulls) then that is the time the NLT visit is arrived at (the same problem as #1). I tried addressing this by setting the start time to the NLT time. This resulted in arriving just in time for the NLT visit but overshooting the time windows of subsequent visits.
How should I address this/these problems? I suspect a solution will involve ArrivalTimeUpdatingVariableListener but I don't know what that solution should look like.
In case it's relevant, I've pasted in my current scoring rules below. One thing to note is that "distance" is really travel time. Also, for domain reasons, I am encouraging NLT and NET arrival times to be close to the cutoff time (end time for NLT, start time for NET).
import org.optaplanner.core.api.score.buildin.hardsoftlong.HardSoftLongScoreHolder;
global HardSoftLongScoreHolder scoreHolder;
// Hard Constraints
rule "ArrivalAfterWindowEnd"
when
Visit(arrivalTime > maxStartTime, $arrivalTime : arrivalTime, $maxStartTime : maxStartTime)
then
scoreHolder.addHardConstraintMatch(kcontext, $maxStartTime - $arrivalTime);
end
// Soft Constraints
rule "MinimizeDistanceToPreviousEvent"
when
Visit(previousRouteEvent != null, $distanceFromPreviousRouteEvent : distanceFromPreviousRouteEvent)
then
scoreHolder.addSoftConstraintMatch(kcontext, -$distanceFromPreviousRouteEvent);
end
rule "MinimizeDistanceFromLastEventToHome"
when
$visit : Visit(previousRouteEvent != null)
not Visit(previousRouteEvent == $visit)
$home : Home()
then
scoreHolder.addSoftConstraintMatch(kcontext, -$visit.getDistanceTo($home));
end
rule "MinimizeIdle"
when
Visit(scheduleType != ScheduleType.NLT, arrivalTime < minStartTime, $minStartTime : minStartTime, $arrivalTime : arrivalTime)
then
scoreHolder.addSoftConstraintMatch(kcontext, $arrivalTime - $minStartTime);
end
rule "PreferLatestNLT"
when
Visit(scheduleType == ScheduleType.NLT, arrivalTime < maxStartTime, $maxStartTime : maxStartTime, $arrivalTime : arrivalTime)
then
scoreHolder.addSoftConstraintMatch(kcontext, $arrivalTime - $maxStartTime);
end
rule "PreferEarliestNET"
when
Visit(scheduleType == ScheduleType.NET, arrivalTime > minStartTime, $minStartTime : minStartTime, $arrivalTime : arrivalTime)
then
scoreHolder.addSoftConstraintMatch(kcontext, $minStartTime - $arrivalTime);
end
To see an example that uses real road times instead of road distances: In the examples app, open Vehicle Routing, click button Import, load the file roaddistance/capacitated/belgium-road-time-n50-k10.vrp. Those times were calculated with GraphHopper.
To see an example that uses Time Windows, open the Vehicle Routing and quick open a dataset that is called cvrptw (tw stands for Time Windows). If you look at the academic spec (linked from docs chapter 3 IIRC) for CVRPTW, you'll see it already has a hard constraint "Do not arrive after time window closes" - so you'll see that one in score rules drl. As for arriving too early (and therefore losing the idle time): copy paste that hard constraint, make it a soft, make it use readyTime instead of dueTime and reverse it's comparison and penalty calculation. I actually originally implemented that (as it's the logical thing to have), but because I followed the academic spec (to compare with results of the academics) I had to remove it.
I was able to solve my problem by modifying ArrivalTimeUpdatingVariableListener's updateArrivalTime method to reach backwards and (attempt to) shift the previous arrival time. Additionally, I introduced a getPreferredStartTime() method to support NLT events defaulting to as late as possible. Finally, just for code cleanliness, I moved the updateArrivalTime method from ArrivalTimeUpdatingVariableListener into the Visit class.
Here is the relevant code from the Visit class:
public long getPreferredStartTime()
{
switch(scheduleType)
{
case NLT:
return getMaxStartTime();
default:
return getMinStartTime();
}
}
public Long getStartTime()
{
Long arrivalTime = getArrivalTime();
if (arrivalTime == null)
{
return null;
}
switch(scheduleType)
{
case NLT:
return arrivalTime;
default:
return Math.max(arrivalTime, getMinStartTime());
}
}
public Long getEndTime()
{
Long startTime = getStartTime();
if (startTime == null)
{
return null;
}
return startTime + duration;
}
public void updateArrivalTime(ScoreDirector scoreDirector)
{
if(previousRouteEvent instanceof Visit)
{
updateArrivalTime(scoreDirector, (Visit)previousRouteEvent);
return;
}
long arrivalTime = getPreferredStartTime();
if(Utilities.equal(this.arrivalTime, arrivalTime))
{
return;
}
setArrivalTime(scoreDirector, arrivalTime);
}
private void updateArrivalTime(ScoreDirector scoreDirector, Visit previousVisit)
{
long departureTime = previousVisit.getEndTime();
long arrivalTime = departureTime + getDistanceFromPreviousRouteEvent();
if(Utilities.equal(this.arrivalTime, arrivalTime))
{
return;
}
if(arrivalTime > maxStartTime)
{
if(previousVisit.shiftTimeLeft(scoreDirector, arrivalTime - maxStartTime))
{
return;
}
}
else if(arrivalTime < minStartTime)
{
if(previousVisit.shiftTimeRight(scoreDirector, minStartTime - arrivalTime))
{
return;
}
}
setArrivalTime(scoreDirector, arrivalTime);
}
/**
* Set the arrival time and propagate the change to any following entities.
*/
private void setArrivalTime(ScoreDirector scoreDirector, long arrivalTime)
{
scoreDirector.beforeVariableChanged(this, "arrivalTime");
this.arrivalTime = arrivalTime;
scoreDirector.afterVariableChanged(this, "arrivalTime");
Visit nextEntity = getNextVisit();
if(nextEntity != null)
{
nextEntity.updateArrivalTime(scoreDirector, this);
}
}
/**
* Attempt to shift the arrival time backward by the specified amount.
* #param requested The amount of time that should be subtracted from the arrival time.
* #return Returns true if the arrival time was changed.
*/
private boolean shiftTimeLeft(ScoreDirector scoreDirector, long requested)
{
long available = arrivalTime - minStartTime;
if(available <= 0)
{
return false;
}
requested = Math.min(requested, available);
if(previousRouteEvent instanceof Visit)
{
//Arrival time is inflexible as this is not the first event. Forward to previous event.
return ((Visit)previousRouteEvent).shiftTimeLeft(scoreDirector, requested);
}
setArrivalTime(scoreDirector, arrivalTime - requested);
return true;
}
/**
* Attempt to shift the arrival time forward by the specified amount.
* #param requested The amount of time that should be added to the arrival time.
* #return Returns true if the arrival time was changed.
*/
private boolean shiftTimeRight(ScoreDirector scoreDirector, long requested)
{
long available = maxStartTime - arrivalTime;
if(available <= 0)
{
return false;
}
requested = Math.min(requested, available);
if(previousRouteEvent instanceof Visit)
{
//Arrival time is inflexible as this is not the first event. Forward to previous event.
//Note, we could start later anyways but that won't decrease idle time, which is the purpose of shifting right
return ((Visit)previousRouteEvent).shiftTimeRight(scoreDirector, requested);
}
setArrivalTime(scoreDirector, arrivalTime + requested);
return false;
}