OptaPlanner: Stuck in local optima. Issue with domain model? Need better moves? Score traps? etc - optaplanner

Forewarning: This is a very detailed and long question. I wouldn't normally make a post like this unless I was really stuck. I've been working on this problem for a few weeks now and I've hit a breaking point. I cannot find a way for the solver to even solve for, much less optimize around, my hard constraints. I'm a fresh CS grad and I'm doing this freelance project for free. I'm doing it for the hospital my mom works for. Optimizing their provider schedule will make her life a lot easier. She's the RN in charge of staffing (which is hell) and works 11-12+ hour days at 58 because she also has regular RN responsibilities, I really want to help her out! Plus, the experience doesn't hurt and I want something like this on my resume.
I'll try to give a rundown of the problem and what I've done so far as succinctly as possible but that is going to be difficult.
The Problem
I'm writing an auto-scheduler for healthcare providers. This hospital dept. has a static scheduling system and they have around 23 providers. They want to load balance the staff required for the providers working in their dept. for each shift. Providers also have obligations to other departments which have to be met.
Each provider must fulfill, on average, a certain number of shifts in a particular location in a month.
How they lay out their schedule, and what I am kind of beholden to, is this. The shifts can be scheduled in a Slot, a Day, and a Time, each of which are discrete.
Slot: 1st, 2nd, 3rd, 4th
Day: M, T, W, Th, F
Time: AM, PM
So (1st, M, AM) refers to the 1st Monday of the month in the morning. Lets call this a TimeSlot. Thus, all of the possible TimeSlots available then is just the Cartesian product of these three sets. Note there could be a 5th slot since sometimes days appear 5 times in a month but the 5th's schedule can just simply mimic the 3rd for simplicity.
Some shifts must be full days. That is they need to span a slot, a day, and both times. Some shifts must be full days and must occur every week on the same day. That means they span a day, every slot and every time. The last shift type is a half day that occurs on the same day every week. These span a day, a time and every slot.
There are two different location types: In the department and outside the department. Dept. Locations have a limit on the number of rooms in a TimeSlot. Outside Locations have a limit on the number of providers that can be scheduled in a TimeSlot.
Hard constraints:
A provider cannot work two different shifts at the same time.
All the shifts scheduled in a TimeSlot at a dept. location cannot exceed the room availability.
All the shifts scheduled in a TimeSlot at an outside location cannot exceed the provider limit.
Medium Constraints:
Even out the number of RNs in every dept. Location in every TimeSlot.
Even out the number of STs in every dept. Location in every TimeSlot.
Even out the number of MAs in every dept. Location in every TimeSlot.
Soft Constraints:
Left out for now for simplicity
What I've Done
I have 4 planning entities for each of the shift types. I'll outline their planning variables and the types below:
HalfDayShift: Slot, Day, Time
HalfDayNoSplitShift: Day, Time
FullDayShift: Slot, Day
FullDayNoSplitShift: Day
Each of these extend an abstract Shift class so I can interact with them all easily. I could've done this with only one planning entity being the "smallest" shift (HalfDayShift) and instead defined the "larger" shifts as groups of these smaller ones. However I found this way easier for defining custom moves, as the moves are the weirdest part of this problem. I'll try to explain the move mechanics as best I can.
In thinking of the moves for this schedule, the first type applies only to the construction. For each shift type, the only move is the Cartesian product of the change moves for each of that types planning variables. Simple enough.
For Local Search, I figured the simplest move would be to pick a shift, assign it a new value based on the possible values of that type, i.e. a FullDayShift will pick a new Slot/Day and a FullDayNoSplitShift will just pick a new Day, and any shifts occurring on that new value with the same provider will be swapped. However, this only works if a shift is moving to a value where all the shifts there are smaller or equal in size. I.e. a HalfDayShift moving onto a day which a FullDayNosplitShift fills doesn't make sense because all the shifts on the original Day should be swapped too. So once we find a valid set of shifts, to make a move, shifts just swap any planning variables in common. I.e. A FullDayNoSplitShift just swaps a Day with any shifts with the same provider on the other day.
This shows how the different shifts can move on top of eachother
I've only implemented the hard constraints and I cannot generate a feasible solution. A couple notes:
Some shifts are concurrent shifts. This means they occur in two Locations simultaneously (usually in nearby locations). All this really means, is the shift has, and counts towards resources in, a second location.
I believe there are not any score traps here and my move is coarse grained enough that it swaps things together as needed. However, please let me know if I'm missing something.
rule "Provider cannot work two shifts in same timeslot"
when
$leftShift : Shift (
isInitialized(),
$leftProv : provider
)
Shift (
this != $leftShift,
provider == $leftProv,
isInitialized(),
conflictsWith($leftShift)
)
then
scoreHolder.addHardConstraintMatch(kcontext, -100);
end
rule "Cannot exceed available slots resource constraint"
when
$loc : Location(rooms == false, $avail: availMap)
$s : Slot()
$d : Day()
$t : Time()
accumulate (
Shift (
isInitialized(),
occursOn($s,$d,$t),
isConsumingResc(),
primaryAt($loc) || concAt($loc)
);
$c : count();
((Integer)$avail.get($s,$d,$t)) != null &&
((Integer)$avail.get($s,$d,$t)) < $c
)
then
scoreHolder.addHardConstraintMatch(kcontext, (int)(((Integer)$avail.get($s,$d,$t)) - ($c)));
end
rule "Cannot exceed room resource constraint"
when
$loc : Location(rooms == true, $avail: availMap)
$s : Slot()
$d : Day()
$t : Time()
accumulate (
Shift (
isInitialized(),
occursOn($s,$d,$t),
isConsumingResc(),
primaryAt($loc),
$primRI : primaryResc
);
$s1 : sum($primRI.getNumRMs())
)
accumulate (
Shift (
isInitialized(),
occursOn($s,$d,$t),
isConsumingResc(),
concAt($loc),
$concRI : concurrentResc
);
$s2 : sum($concRI.getNumRMs())
)
eval(
((Integer)$avail.get($s,$d,$t)) != null &&
((Integer)$avail.get($s,$d,$t)) < ($s1 + $s2)
)
then
scoreHolder.addHardConstraintMatch(kcontext,(int)(((Integer)$avail.get($s,$d,$t)) - ($s1 + $s2)) );
end
Here's my configuration as well.
<?xml version="1.0" encoding="UTF-8"?>
<solver>
<!--<environmentMode>FAST_ASSERT</environmentMode>-->
<!--<environmentMode>FULL_ASSERT</environmentMode>-->
<scanAnnotatedClasses>
<packageInclude>com.labrador.providerplanner.domain</packageInclude>
</scanAnnotatedClasses>
<scoreDirectorFactory>
<scoreDrl>constraints.drl</scoreDrl>
</scoreDirectorFactory>
<!--<constructionHeuristic>
<constructionHeuristicType>CHEAPEST_INSERTION</constructionHeuristicType>
</constructionHeuristic>-->
<constructionHeuristic>
<queuedEntityPlacer>
<entitySelector id="placerEntitySelector">
<cacheType>PHASE</cacheType>
<entityClass>com.labrador.providerplanner.domain.FullDayNoSplitShift</entityClass>
</entitySelector>
<changeMoveSelector>
<entitySelector mimicSelectorRef="placerEntitySelector"/>
</changeMoveSelector>
</queuedEntityPlacer>
</constructionHeuristic>
<constructionHeuristic>
<queuedEntityPlacer>
<entitySelector id="placerEntitySelector">
<cacheType>PHASE</cacheType>
<entityClass>com.labrador.providerplanner.domain.HalfDayNoSplitShift</entityClass>
</entitySelector>
<cartesianProductMoveSelector>
<changeMoveSelector>
<entitySelector mimicSelectorRef="placerEntitySelector"/>
<valueSelector variableName="time"/>
</changeMoveSelector>
<changeMoveSelector>
<entitySelector mimicSelectorRef="placerEntitySelector"/>
<valueSelector variableName="day"/>
</changeMoveSelector>
</cartesianProductMoveSelector>
</queuedEntityPlacer>
</constructionHeuristic>
<constructionHeuristic>
<queuedEntityPlacer>
<entitySelector id="placerEntitySelector">
<cacheType>PHASE</cacheType>
<entityClass>com.labrador.providerplanner.domain.FullDayShift</entityClass>
</entitySelector>
<cartesianProductMoveSelector>
<changeMoveSelector>
<entitySelector mimicSelectorRef="placerEntitySelector"/>
<valueSelector variableName="slot"/>
</changeMoveSelector>
<changeMoveSelector>
<entitySelector mimicSelectorRef="placerEntitySelector"/>
<valueSelector variableName="day"/>
</changeMoveSelector>
</cartesianProductMoveSelector>
</queuedEntityPlacer>
</constructionHeuristic>
<constructionHeuristic>
<queuedEntityPlacer>
<entitySelector id="placerEntitySelector">
<cacheType>PHASE</cacheType>
<entityClass>com.labrador.providerplanner.domain.HalfDayShift</entityClass>
</entitySelector>
<cartesianProductMoveSelector>
<changeMoveSelector>
<entitySelector mimicSelectorRef="placerEntitySelector"/>
<valueSelector variableName="slot"/>
</changeMoveSelector>
<changeMoveSelector>
<entitySelector mimicSelectorRef="placerEntitySelector"/>
<valueSelector variableName="day"/>
</changeMoveSelector>
<changeMoveSelector>
<entitySelector mimicSelectorRef="placerEntitySelector"/>
<valueSelector variableName="time"/>
</changeMoveSelector>
</cartesianProductMoveSelector>
</queuedEntityPlacer>
</constructionHeuristic>
<localSearch>
<localSearchType>LATE_ACCEPTANCE</localSearchType>
<moveIteratorFactory>
<moveIteratorFactoryClass>com.labrador.providerplanner.solver.SingleProviderShiftSwapMoveIteratorFactory</moveIteratorFactoryClass>
</moveIteratorFactory>
<termination>
<secondsSpentLimit>60</secondsSpentLimit>
</termination>
</localSearch>
</solver>
The Problem With The Problem
Here's the main issue. The following is what the solver spits out. It gets stuck in this local optima. Lets look at, for instance, the Main OR Robot Location which is an outside Location that only three Providers work shifts in. P2 and P3 have 4 FullDayShift's each in the Main OR Robot. P1 has 4 FullDayShifts and 4 HalfDay Shifts. Main OR Robot can only accommodate 1 Provider in every Monday TimeSlot, 1 in Every Tuesday, 1 in Every Thursday and 1 in every Friday AM.
The Main OR Robot shifts are circled. P1 violates the resource constraint on Tuesday. P2 violates another resource constraint on Friday. The arrows show a set of moves which would successfully not break those constraints for Main OR Robot anymore. However, the issue is those spots contain Shifts at FH Cancer. Which is another outside location that is filled to the brim. Thus, moving those causes other constraints to break, probably more. So those have to be shuffled around which could cause problems with other Locations and on... and on.. and on... This really is just the crux of an optimization problem and it seems like the meta-heuristic is simply falling short of breaking out of a this local optima.
So this is where I am. I don't really know how to proceed. Are there coarser grained moves I can make to help the solver bust out of optima like this? I.e. trade location times between two providers. I've tried something like this and it didn't help. Do I need to rethink my domain model? I.e. should Locations have rooms or open slots on certain times based on their availability and should Providers be assigned to these? That might work but how do I encapsulate the larger shifts types in this domain? How do I ensure each provider works the number of shifts they need to in each location?
This is quite a complex problem and a wall of information. So thank you if you actually read all of this! Any help or direction at all is hugely appreciated as I am very stuck right now. If anything is confusing or more info is needed, please let me know and I'll do my best to clear it up.

Related

How to speed up construction phase whilst having an trivial overlapping constraint

We're are trying to put together a proof of concept planning constraints solver using OptaPlanner. However the construction phase seems slow for even a trivial set of constraints i.e. assign to one User with no overlapping Tasks for that User.
Problem overview:
We are assigning Tasks to Users
Only one Task can be assigned to User
The Tasks can be variable length: 1-16 hours
Users can only do one Task at a time
Users have 8 hours per day
We are using the Time Grain pattern - 1 grain = 1 hour.
See constraints configuration below.
This works fine (returns in a 20 seconds) for a small number of Users and Tasks e.g. 30 Users / 1000 Tasks but when we start scaling up the performance rapidly drops off. Simply increasing the number of Users without increasing the number of Tasks (300 Users / 1000 Tasks) increases the solve time to 120 seconds.
But we hope to scale up to 300 Users / 10000 Tasks and incorporate much more elaborate constraints.
Is there a way to optimise the constraints/configuration?
Constraint constraint1 = constraintFactory.forEach(Task.class)
.filter(st -> st.getUser() == null)
.penalize("Assign Task", HardSoftLongScore.ONE_HARD);
Constraint constraint2 = constraintFactory.forEach(Task.class)
.filter(st -> st.getStartDate() == null)
.penalize("Assign Start Date", HardSoftLongScore.ONE_HARD);
Constraint constraint3 = constraintFactory
.forEachUniquePair(Task.class,
equal(Task::getUser),
overlapping(st -> st.getStartDate().getId(),
st -> st.getStartDate().getId() + st.getDurationInHours()))
.penalizeLong("Crew conflict", HardSoftLongScore.ONE_HARD,
(st1, st2) -> {
int x1 = st1.getStartDate().getId() > st2.getStartDate().getId() ? st1.getStartDate().getId(): st2.getStartDate().getId();
int x2 = st1.getStartDate().getId() + st1.getDurationInHours() < st2.getStartDate().getId() + st2.getDurationInHours() ?
st1.getStartDate().getId() + st1.getDurationInHours(): st2.getStartDate().getId() + + st2.getDurationInHours();
return Math.abs(x2-x1);
});
constraint1 and constraint2 seem redundant to me. The Construction Heuristic phase will initialize all planning variables (automatically, without being penalized for not doing so) and Local Search will never set a planning variable to null (unless you're optimizing an over-constrained problem).
You should be able to remove constraint1 and constraint2 without impact on the solution quality.
Other than that, it seems you have two planning variables (Task.user and Task.startDate). By default, in each CH step, both variables of a selected entity are initialized "together". That means OptaPlanner looks for the best initial pair of values for that entity in the Cartesian product of all users and all time grains. This scales poorly.
See the Scaling construction heuristics chapter to learn how to change that default behavior and for other ways how to make Construction Heuristic algorithms scale better.

Optaplanner:Add Dynamic visits without changing the already created visits

I am saving the best solution into the DB, and we display that on the web page. I am looking for some solution where a user can add more visits, but that should not change already published trips.
I have checked the documentation and found ProblemFactChange can be used, but only when the solver is already running.
In my case solver is already terminated and the solution is also published. Now I want to add more visits to the vehicle without modifying the existing visits of the Vehicle. Is this possible with Optaplanner? if yes any example of documentation would be very helpful.
You can use PlanningPin annotation for avoiding unwanted changes.
Optaplanner - Pinned planning entities
If you're not looking for pinning (see Ismail's excellent answer), take a look at the OptaPlanner School Timetabling example, which allows adding lessons between solver runs. The lessons simply get stored in the database and then get loaded when the solver starts.
The difficulty with VRP is the chained model complexity (we're working on an alternative): If you add a visit X between A and B, then make sure that afterwards A.next = X, B.previous = X, X.previous = A, X.next = B and X.vehicle = A.vehicle. Not the mention the arrival times etc.
My suggestion would be to resolve what is left after the changes have been introduced. Let's say you are you visited half of your destinations (A -> B -> C) but not yet (C - > D -> E) when two new possible destinations (D' and E') are introduced. Would not this be the same thing as you are starting in C and trying plan for D, D', E and E'? The solution needs to be updated on the progress though so the remainder + changes can be input to the next solution.
Just my two cent.

Sentinel 1 data gaps in swath overlap (not sequential scenes) in Google Earth Engine

I am working on a project using the Sentinel 1 GRD product in Google Earth Engine and I have found a couple examples of missing data, apparently in swath overlaps in the descending orbit. This is not the issue discussed here and explained on the GEE developers forum. It is a much larger gap and does not appear to be the product of the terrain correction as explained for that other issue.
This gap seems to persist regardless of year changes in the date range or polarization. The gap is resolved by changing the orbit filter param from 'DESCENDING' to 'ASCENDING', presumably because of the different swaths or by increasing the date range. I get that increasing the date range increases revisits and thus coverage but is this then just a byproduct of the orbital geometry? ie it takes more than the standard temporal repeat to image that area? I am just trying to understand where this data gap is coming from.
Code example:
var geometry = ee.Geometry.Polygon(
[[[-123.79472413785096, 46.20720039434629],
[-123.79472413785096, 42.40398120362418],
[-117.19194093472596, 42.40398120362418],
[-117.19194093472596, 46.20720039434629]]], null, false)
var filtered = ee.ImageCollection('COPERNICUS/S1_GRD').filterDate('2019-01-01','2019-04-30')
.filterBounds(geometry)
.filter(ee.Filter.eq('orbitProperties_pass', 'DESCENDING'))
.filter(ee.Filter.listContains('transmitterReceiverPolarisation', 'VH'))
.filter(ee.Filter.listContains('transmitterReceiverPolarisation', 'VV'))
.filter(ee.Filter.eq('instrumentMode', 'IW'))
.select(["VV","VH"])
print(filtered)
var filtered_mean = filtered.mean()
print(filtered_mean)
Map.addLayer(filtered_mean.select('VH'),{min:-25,max:1},'filtered')
You can view an example here: https://code.earthengine.google.com/26556660c352fb25b98ac80667298959

Updating shadow variables of multiple chains

I'm trying to solve a cooperative team orienteering problem and thus implementing an Auto-Delay. I use shadow variables to store arrival and starting time as explained in the documentation.
My chains are as follows :
Vehicule 1 : [Vehicule1, TaskA(1/3), TaskB(2/3),...] Arrival on A : 10, Start of A : 20
Vehicule 2 : [Vehicule2, TaskA(2/3),TaskB(1/3),...] Arrival on A : 20, Start of A : 20
Vehicule 3 : [Vehicule3, TaskB(3/3),...]
The fact that TaskA cannot be completed is punished in the score calculation.
Optaplanner is now adding TaskA(3/3) before TaskB(3/3). Its Arrival time is 30
I wish to change the starting time of TaskA(1/3) and TaskA(2/3) to 30. BUT i also want to change the consequent arrival and starting time of TaskB(1/3) and TaskB(2/3) and tasks that are placed after.
TaskB(3/3) is in the source chain so it will be taken care of normally.
Ending time is based only on starting time in my problem.
What is the best way to do that ?

Unequally spaced vehicles in a flow: SUMO

I have needed an equally spaced vehicle flow. As per the documentation , vehicles should be equally spaced unless someone randomizes the flow. I didn't randomize the flow, but I am experiencing that the vehicles do not have the same headway.
Here is my rou.xml file entry, and I set sigma = 0 as well.
<flow id = "f1" color="1,1,1" begin = "0" type="Car" vehsPerHour="1500" number="100" route="route0" departSpeed="13.9"> </flow>
I am seeing majority of the vehicles have a headway around 27m and some other vehicles around 40m. There is a pattern. The first 2 vehicles of every 5 vehicles travel together (with 27m heading), and other other 3 travel together (with 27m heading) but with 40m gap between 3rd and the 2nd (e.g. V represents a vehicle VVV*****VV*****VVV*****VV****VVV*****V**V)
I tried this as well.
<flow id = "f1" color="1,1,1" begin = "0" type="Car" period="2.4" number="100" route="route0" departSpeed="13.9"> </flow>
But it is the same as the previous.
Is there a workaround for this?
Thanks!
It is a discretization error. Assuming you run with the default step length of one second the vehicles will be emitted at whole seconds only. To avoid this use only multiples of the step length as period (so either using a period of 2 or 3 or reducing the step length to 0.2 should help in your example). There is also a ticket concerning this topic: https://github.com/eclipse/sumo/issues/4277.