Is optaplanner scheduling AI suitable for optimising a college schedule? - optaplanner

Optaplanner school timetabling examples show different problems than what I need to solve.
The problem:
Let's have three courses: Math, English, History
All these courses offer multiple timeslots (parallels) and the student gets to choose one of them for each.
Let's say:
Math: Monday: 9:00 - 10:00, Tuesday: 10:00 - 11:00
English: Monday: 9:00 - 10:00, Tuesday: 11:00 - 12:00, 14:00 - 15:00,
Friday: 16:00 - 17:00
History: Tuesday: 10:00 - 11:00, Friday: 10:00 - 11:00
I would like to find the best possible timeslot for each of these courses. By the best I mean the one with the least amount of gaps and, more importantly, the one which lets you have the most free days.
The optimal solution for the example problem might be something like
Tuesday: Math: 9:00 - 10:00, History: 10:00 - 11:00, English: 11:00 -
12:00
leaving no gap and simultaneously giving 4 free days.
Is this possible with OptaPlanner or should I use a different solver? I noticed that the Lesson object only has a single TimeSlot instead of a list, which makes me think that this kind of timetable optimisation is not supported.

From the description, it appears your problem is "Student planning their own schedule" (whereas school timetabling is "Teachers planning the school timetable for all students").
I would model it like this:
#PlanningEntity
public class CourseSection {
private List<TimeSlot> availableTimeslots;
#PlanningId // needed for forEachUniquePair
private String courseSectionIdentifier;
#PlanningVariable(valueRangeProviderRefs = {"timeslotRange"})
private TimeSlot selectedTimeslot;
#ValueRangeProvider(id="timeslotRange")
public List<TimeSlot> getTimeslotRangeForCourse() {
return availableTimeslots;
}
// getters and setters ...
}
This takes advantage of value range provider on a planning entity, meaning each planning entity gets it own value range (and non-existing TimeSlot for the course will not be tried).
I used CourseSection, since courses could be divided into multiple sections (1 lectures + 2 tutorial, 2 lectures, etc.), and you would have one CourseSection for each of those sections. (If the course only one lecture section, then there is only one CourseSection, if the course a lecture + tutorial, there are two CourseSections, etc.).
For the minimize gaps constraint, I would use the experimental consecutive interval constraint collector. To use it, you would either need to add OptaPlanner Examples as a maven dependency or copy it from the source code of examples; it will eventually be moved into ConstraintCollectors once we are certain of the API (which is subject to change as it still in experimental). With the consecutive interval constraint collector, it will look like this:
Constraint minimizeGaps(ConstraintFactory constraintFactory) {
return constraintFactory.forEach(CourseSection.class)
.groupBy(section -> section.getSelectedTimeslot().getDayOfWeek(),
ExperimentalConstraintCollectors.consecutiveTemporalIntervals(
section -> section.getSelectedTimeslot().getStartTime(),
section -> section.getSelectedTimeslot().getEndTime()))
.flattenLast(ConsecutiveIntervalInfo::getBreaks)
.penalize("Minimize Gaps", HardMediumSoft.ONE_SOFT);
}
For the maximize free days constraints, I would write it as "minimize working days" like this:
Constraint minimizeWorkingDays(ConstraintFactory constraintFactory) {
return constraintFactory.forEach(CourseSection.class)
.groupBy(section -> section.getSelectedTimeslot().getDayOfWeek())
.penalize("Minimize Working Days", HardMediumSoft.ONE_MEDIUM);
}
Finally, we need to make sure no two course sections overlap:
Constraint noOverlappingCourseSections(ConstraintFactory constraintFactory) {
return constraintFactory.forEachUniquePair(CourseSection.class,
Joiners.equal(section -> section.getSelectedTimeslot().getDayOfWeek()),
Joiners.overlapping(
section -> section.getSelectedTimeslot().getStartTime(),
section -> section.getSelectedTimeslot().getEndTime()))
.penalize("Overlapping sections", HardMediumSoftScore.ONE_HARD);
}
Note in the constraints I provided, a solution with less working days is ALWAYS preferred to a solution with more working days, even if the solution with less days has a 8 hour gap. You might want to add an additional constraint on the maximum gap allowed (which you can do with a filter that checks IntervalBreak.getLength()).

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.

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 ?

How to define constraint about consecutive periods in optaplanner

In my optaplanner project i have periods with fixed duration.
For some of them there is a medium constraint that they should be scheduled in a row, occupying for example 5 directly adjacent timeslots.
I want to use Java constraints streams but dont manage to define this constraint using the timeslot-pattern.
I know that this constraint can be defined using the time-grain-pattern as suggested by https://stackoverflow.com/a/30702865. I have done this and it works. But I want to compare timeslot-pattern to time-grain-pattern because the have different behaviour when it comes to escape local maxima. The problem with time-grain-pattern is that those 5 periods could also be sheduled in every possible partition of 5 (eg. as 2 + 2 + 1).
Has anyone a hint on how to define the constraint using timeslot-pattern?
You may want to try using the newly added ifExists() building block. Without knowing your actual domain model, I imagine the constraint to look like this:
private Constraint twoConsecutivePeriods(ConstraintFactory constraintFactory) {
return constraintFactory.from(Period.class)
.ifExists(Period.class, equal(Period::getDay, period -> period.getDay() + 1))
.penalize("2 consecutive periods", period -> ...);
}
Consequently, ifNotExists() may be used to achieve the opposite. We have examples of both in Traveling Tournament OptaPlanner example.
Please note that this API is only available since OptaPlanner 7.33.0.Final onward.
I solved the problem using ConstraintCollectors.toList() as follows:
factory.from(Period.class).groupBy(Period::getCourse, ConstraintCollectors.toList())
.penalize(id, score, (course,list)->dayDistributionPenalize(course,list));
and
public int dayDistributionPenalize(Course course, List<Period> list) {
var penalize = 0;
var dayCodes = dayCodesFromPeriodList(list);
for (var dayCode : dayCodes)
if (!getAllowedDayCodes(course).contains(dayCode))
penalize++;
return penalize;
}
Where getAllowedDayCodes() returns for example 0111110000, 0230000000 or 0005000000 etc. from a hash-map if a course has to have 5 consecutive periods.

Convert between arbitrary timezones

I'm trying to find a simple yet robust way to convert time between arbitrary time zones.
This: http://www.cpearson.com/excel/TimeZoneAndDaylightTime.aspx explains only how to convert betwen my (current) TZ and another TZ.
Those two SO articles (Getting Windows Time Zone Information (C++/MFC) and How do you get info for an arbitrary time zone in Windows?) talk about getting the information from the registry.
That sounds a bit too convoluted and time-consuming; moreover, it appears that Windows stores TZs in their "full names" (such as (UTC-08:00) Pacific Time (US & Canada)) and I'd rather refer to TZs using abbreviations (such as EDT). Moreover, relying on Windows registry could also be unsafe: different users might have different versions and some might not be up to date. That would mean a report run by two persons might provide two different results!
Is there a simpler way that will also be robust? Writing a lookup table could work for some time but then it will be broken when a government decides to abolish DST or change anything else.
Maybe get a list of TZs from Internet and parse it? Would that be safe enough?
Update 1
I've made my research and explored the possibilities, but this problem is not as trivial as it might seem. If you think that the function shall look like bTime = aTime + 3, then please reconsider. Timezones and DSTs are in a state of constant flux.
Read this for reference: list of pending / proposed timezone changes. Note that some countries are actually changing their timezones, not just DST settings! And Brazil changed the date on which they change their clocks to winter time! A static lookup table would be broken very quickly by all those changes.
Update 2
I'm not looking into a quick and dirty hack, I can come up with that myself. I'm not wanting to write something and forget about it; I'd like to create a function once that could be safely used by other people for different internal projects without the maintenance nightmare. Hard-coding constants that are known to change once in a while is a very bad software design (think Y2K bug caused by a very, very old piece of code).
Update 3
This database looks good (although I'm not sure if it's stable enough): https://timezonedb.com/api. They even have a TZ conversion call - exactly what I need! I will probably try to parse XML from VBA and share my results.
The API at https://timezonedb.com/references/convert-time-zone is indeed a great place to get the correct worldwide time, timezone, and timezone-offset between two locations, taking into account past/future Daylight Savings changes.
A problem with your suggested method of specifying only the Time Zone Abbreviations (such as "convert PST to EST") is that this API takes your zones literally, even if they are incorrect.
So, if Toronto is currently on EDT but you specify EST, you'll probably get the incorrect time. Using "full names" like (UTC-08:00) Pacific Time (US & Canada) would have the same issue.
A way around that is to specify the time zone names like America/Vancouver (as listed here), or else specify the city, country and/or region name with the appropriate parameters.
I wrote a function to figure it out but it only applies to certain countries (see further down).
What time was it in Toronto last Halloween at 11:11pm Vancouver time?
http://api.timezonedb.com/v2/convert-time-zone?key=94RKE4SAXH67&from=America/Vancouver&to=America/Toronto&time=1509516660
Result: (Default is XML but JSON is also available.)
<result>
<status>OK</status>
<message/>
<fromZoneName>America/Vancouver</fromZoneName>
<fromAbbreviation>PDT</fromAbbreviation>
<fromTimestamp>1509516660</fromTimestamp>
<toZoneName>America/Toronto</toZoneName>
<toAbbreviation>EDT</toAbbreviation>
<toTimestamp>1509527460</toTimestamp>
<offset>10800</offset>
</result>
Getting the data programmatically:
There are plenty of options and lookup methods you will have to decide upon, but here's one example using a VBA Function:
What will be the time difference between Vancouver & Berlin on Christmas Day?
Input Time: 2018-12-25 00:00:00 = Vancouver Local Unix time 1545724800
Function GetTimeZoneOffsetHours(fromZone As String, _
toZone As String, UnixTime As Long) As Single
Const key = "94RKE4SAXH67"
Const returnField = "<offset>"
Dim HTML As String, URL As String
Dim XML As String, pStart As Long, pStop As Long
URL = "http://api.timezonedb.com/v2/convert-time-zone?key=" & key & _
"&from=" & fromZone & "&to=" & toZone & "&time=" & UnixTime
With CreateObject("MSXML2.XMLHTTP")
.Open "GET", URL, False
.Send
XML = .ResponseText
End With
pStart = InStr(XML, returnField)
If pStart = 0 Then
MsgBox "Something went wrong!"
Exit Function
End If
pStart = pStart + Len(returnField) + 1
pStop = InStr(pStart, XML, "</") - 1
GetTimeZoneOffsetHours = Val(Mid(XML, pStart, pStop - pStart)) / 60
End Function
Sub testTZ()
Debug.Print "Time Zone Offset (Vancouver to Berlin) = " & _
GetTimeZoneOffsetHours("America/Vancouver", _
"Europe/Berlin", 1545724800) & " hours"
End Sub
Unix/UTC Timestamps:
Unix time is defined as "the number of seconds that have elapsed since 00:00:00 Coordinated Universal Time (UTC), Thursday, 1 January 1970."
You can convert times between Unix and/or UTC or Local time at: epochconverter.com ... the site also has conversion formulas for several programming languages.
For example, the formua to convert Unix time to GMT/UTC in Excel is:
=(A1 / 86400) + 25569
You could also download static files (in SQL or CSV format) here instead of caling the API, and the page also has sample queries. However use caution: it's easier to make mistakes with Daylight Savings (as mentioned above).
I made a dummy account to get the "demo" used in the examples, but you should get your own (free) key for long-term use. (I'm not responsible if it gets locked out from over-use!)
An good alternative Time Zone API is Google Maps Time Zone API. The difference is that you specify Latitude & Longitude. It seems to work just fine without a key You'll need to register for a key.
What will the Time Zone Offset be on June 1st at the White House?
https://maps.googleapis.com/maps/api/timezone/json?location=38.8976,-77.0365&timestamp=1527811200&key={YourKeyHere}
Result:
{
"dstOffset" : 0,
"rawOffset" : -18000,
"status" : "OK",
"timeZoneId" : "America/Toronto",
"timeZoneName" : "Eastern Standard Time"
}
The Offset will be -18000 seconds (-5 hours).
Determining when Daylight Savings is in effect
Below is a function I put together so I could "trust" the Daylight Savings (DST) values I was getting from a different API, however (as discussed by others) the rules have no pattern plus are constantly changing country by country, even town by town in some parts of the world, so this only will work in countries where:
DST begins on the Second Sunday of March every year
DST end on the First Sunday of November every year
The applicable countries are Bahamas, Bermuda, Canada, Cuba, Haiti, St. Pierre & United States. (Source: Daylight saving time by country**)
Function IsDST(dateTime As Date) As Boolean
'Returns TRUE if Daylight Savings is in effect during the [dateTime]
'DST Start (adjust clocks forward) Second Sunday March at 02:00am
'DST end (adjust clocks backward) First Sunday November at 02:00am
Dim DSTStart As Date, DSTstop As Date
DSTStart = DateSerial(Year(dateTime), 3, _
(14 - Weekday(DateSerial(Year(dateTime), 3, 1), 3))) + (2 / 24)
DSTstop = DateSerial(Year(dateTime), 11, _
(7 - Weekday(DateSerial(Year(dateTime), 11, 1), 3))) + (2 / 24)
IsDST = (dateTime >= DSTStart) And (dateTime < DSTstop)
End Function
And a couple examples of how I could use function IsDST*:
Public Function UTCtoPST(utcDateTime As Date) As Date
'Example for 'PST' time zone, where Offset = -7 during DST, otherwise if -8
If IsDST(utcDateTime) Then
UTCtoPST = utcDateTime - (7 / 24)
Else
UTCtoPST = utcDateTime - (8 / 24)
End If
End Function
Function UTCtimestampMStoPST(ByVal ts As String) As Date
'Example for 'PST', to convert a UTC Unix Time Stamp to 'PST' Time Zone
UTCtimestampMStoPST = UTCtoPST((CLng(Left(ts, 10)) / 86400) + 25569)
End Function
* Note that function IsDST is incomplete: It does not take into account the hours just before/after IsDST takes actually effect at 2am. Specifically when, in spring, the clock jumps forward from the last instant of 01:59 standard time to 03:00 DST and that day has 23 hours, whereas in autumn the clock jumps backward from the last instant of 01:59 DST to 01:00 standard time, repeating that hour, and that day has 25 hours ...but, if someone wants to add that functionality to update the function, feel free! I was having trouble wrapping my head around that last part, and didn't immediately need that level of detail, but I'm sure others would appreciate it!
Finally one more alternative is an API that I use to for polling current/future/historical weather data for various purposes — and also happens to provide Timezone Offset — is DarkSky.
It queries by latitude/longitude and is free (up to 1000 calls/day) and provides "ultra-accurate weather data" (more-so in the USA, where it predicts weather down to the minute and to the square-yard! — but quite accurate I've seen for the unpredictable Canadian West Coast Canada!)
Response is in JSON only, and the very last line is Time Zone Offset versus UTC/GMT time.
DarkSky Sample Call:
https://api.darksky.net/forecast/85b57f827eb89bf903b3a796ef53733c/40.70893,-74.00662
It says it's supposed to rain for the next 60 hours at Stack Overflow's Head Office. ☂
...but I dunno, it looks like a pretty nice day so far! ☀
(flag)
Im afraid anything to do with timezones is never a simple task (ask any web designer and they will say it is a massive challenge)
there are 2 ways to solve your problem
1) The Easy way - Create a central list which all other workbooks are linked to. This can be saved on SharePoint or on a shared drive, then all you have to do is update this one table
2) The hard way - Use a website API to get the latest timezone data. https://www.amdoren.com/ is a good site, you can get a free API key by signing up. The only issue is you then have to parse the Json file from the website. This isn't easy but if you google "vba parse json" you will find some solutions (it generally requires importing some libraries and using other peoples code as a starting point)
Hope you find the right solution, and if you do might be worth sharing it as im sure there will be others with same issue.

Opta planner incorrect best score

I am trying out the optaplanner for a shift assignment problem.
It is a many to many relationship since one shift can have many employees.
In the trial run , I have two employees and three shifts .
One of the shift needs two employees.
So I have created a new ShiftAssignment class to handle the many to many relationship . ShiftAssignment is the planning entity and employee is the planning variable.
I pass the two employees and four shift assignment class ( because one shift needs two employees )
to the planning solution
I have only one hard rule in the score calculator which is basically the employee should
have the necessary skill needed for the shift
When I run the solver , I print the score in my code below ( I dont have any soft constraints so I have hard coded it to zero )
public HardSoftScore calculateScore(AuditAllocationSolution auditAllocationSolution) {
int hardScore = 0;
for (Auditor auditor : auditAllocationSolution.getAuditors()) {
for (AuditAssignment auditAssignment : auditAllocationSolution.getAuditAssignments()) {
if (auditor.equals(auditAssignment.getAuditor())) {
List<String> auditorSkils = auditor.getQualifications().stream().map(skill -> skill.getSkillName())
.collect(Collectors.toList());
String requiredSkillForThisAuditInstance = auditAssignment.getRequiredSkill().getSkillName();
if ( !auditorSkils.contains(requiredSkillForThisAuditInstance))
{
// increement hard score since skill match contraint is violated
hardScore = hardScore + 1;
}
}
}
}
System.out.println(" hardScore " + hardScore);
return HardSoftScore.valueOf(hardScore, 0);
}
When I print the values of the solution class in the score calculator , I can see that there are few solutions where hard score is zero. The solution satisfies the rules and matches the expected results . But it is not accepted as per the logs
08:16:35.549 [main] TRACE o.o.c.i.l.decider.LocalSearchDecider - Move index (0), score (0hard/0soft), accepted (false), move (AuditAssignment-2 {Auditor-1} <-> AuditAssignment-3 {Auditor-0}).
08:16:35.549 [main] TRACE o.o.c.i.l.decider.LocalSearchDecider - Move index (0), score (0hard/0soft), accepted (false), move (AuditAssignment-2 {Auditor-1} <-> AuditAssignment-3 {Auditor-0}).
One another observation which I want to clarify in the logs.
I understand that every new solution , which is the outcome of each step , is passed to score calculator . But sometimes I see that for a single step , score calculator is invoked more than once with different solution. This is my observation from the logs. Assuming this is single threaded and log sequencing is correct , why does that happen ?
The final output is incorrect . The best score that is selected is something with high hard score. And the solutions with the best score are not accepted
I also see the below line in the logs which I am not able to comprehend. Is there anything wrong in my configuration ?
23:53:01.242 [main] DEBUG o.o.c.i.l.DefaultLocalSearchPhase - LS step (26), time spent (121), score (2hard/0soft), best score (4hard/0soft), accepted/selected move count (1/1), picked move (AuditAssignment-2 {Auditor-1} <-> AuditAssignment-0 {Auditor-0}).
23:53:01.242 [main] DEBUG o.o.c.i.l.DefaultLocalSearchPhase - LS step (26), time spent (121), score (2hard/0soft), best score (4hard/0soft), accepted/selected move count (1/1), picked move (AuditAssignment-2 {Auditor-1} <-> AuditAssignment-0 {Auditor-0}).
This is a small problem size and I feel I have not set it up right . Kindly suggest.
Hard Score has to be decremented when a constraint is violated. In the above code , I had incremented the hard score which probably had led to the erroneous result.
It worked as expected once I fixed the above.