Package: or-shifty
OR-ShiftyOR-Shiftyis a CLI app to automate assignment of on-call shifts via constraint solving by use of theOR-toolslibrary.It is designed to accept two JSON files as input:a config file which describes the desired on-call rota, the people to assign, and any constraintsa history file which describes past shifts per person, to be used by certain constraints described belowIt will then attempt to create a rota allocation given these inputs and print it either on terminal or as JSON to an
output file if provided.The given constraints can be prioritised so if a solution cannot be found that satisfies all of them the tool will
drop the least important constraint and try again, repeatedly, until a solution is found or no more constraints can
be dropped. If any constraints had to be dropped the tool will also print to the terminal which of the original
constraints were violated, for which person, and on which day.InstallationYou will need Python 3.7 or greater.pipinstallor-shiftyUseHelp:shifty--helpSolver modeFor using shifty in its default solver mode.An output file path can optionally be provided in which case shifty will write the solution to the file in JSON
format. If a file already exists at that path then it will overwritten.shifty\--config<path_to_config.json>\--history<path_to_history.json>\[--output<path_to_optional_output.json>]Evaluation modeShifty can also be run in evaluation mode.In this mode an output with an existing solution must be provided. Shifty will then evaluate this solution against
the constraints, objective function, and history provided and it will print which (if any) constraints are violated
by the solution and the score of the objective function. This is intended as a diagnostic tool to inspect shifty's
decisions.When in this mode the output file must contain exactly an assigned shift for every shift specified in config otherwise
shifty will exit with an error.shifty\--config<path_to_config.json>\--history<path_to_history.json>\--output<path_to_optional_output.json>\--evaluateShift typesAny shift can be assigned one of three types. These types have no intrinsic meaning and only exist so different
constraints can be applied to different shifts, e.g. it might be desired to have more unassigned days after having a
Saturday shift or similar.The currently supported shift types are:STANDARDSPECIAL_ASPECIAL_BConstraintsSolver constraints fall into one of two categories:Mandatory, that are always includedUser selected, that can be included and configured using the config input fileFor each user selected constraint there will be an example of the JSON configuration needed to include it.User selected constraints can also be given a priority. If a solution can not be found the constraints with the highest
priority number will be dropped and the solver will retry, then the constraints with the second highest priority
number will be dropped and so on. Priority must be a positive integer. A priority of 0 means the constraint cannot ever
be dropped. If multiple constraints have the same priority and that priority is due to be dropped then all these
constraints will be dropped together.Constraints can also be named to make them easier to manager by providing"name": "constraint name"in the JSON
config.It is valid to include the same type of constraint multiple times in the config, e.g. by having a stricter constraint
with a higher priority number, followed by a more permissive one with a lower priority number. This way the solver
will try to meet the strict one first, but if it cannot it will then at least try to meet the more permissive one.The mandatory constraints are mostly common sense and can be seen in the code, e.g. you can't assign Eve's first shift
twice, etc.The user selected constraints are as follows:Each person works at most X shifts per assignment period"constraints":[{"type":"EachPersonWorksAtMostXShiftsPerAssignmentPeriod","priority":0,"params":{"x":1}}]An assignment period is one run of the program with given input files. This constraints that a person can only be
assigned X of these shifts.There should be at least X days between ops"constraints":[{"type":"ThereShouldBeAtLeastXDaysBetweenOps","priority":0,"params":{"x":4}}]Subtract the last date on ops from the date assigned on a shift. The resulting number of days must be greater that X.There should be at least X days between ops of shift types"constraints":[{"type":"ThereShouldBeAtLeastXDaysBetweenOpsOfShiftTypes","priority":0,"params":{"x":8,"shift_types":["SPECIAL_A","SPECIAL_B"]}}]Subtract the last date on ops of any the given types from the date assigned on a shift of one of the given types. The
resulting number of days must be grater than X. In the example configuration someone cannot be assigned to a
SPECIAL_A or SPECIAL_B type shift if they have had a SPECIAL_A or SPECIAL_B shift in the last 8 days.Respect person restrictions per shift type"constraints":[{"type":"RespectPersonRestrictionsPerShiftType","priority":0,"params":{"forbidden_by_shift_type":{"special_a":["Alice"]}}}]Disallow particular people from being assigned any shifts of a given type.Respect person restrictions per day"constraints":[{"type":"RespectPersonRestrictionsPerDay","name":"Holidays","priority":0,"params":{"restrictions":{"Alice":["2019-11-01"]}}}]Disallow particular people from being assigned any shifts on a given date.Objective functionWhen scheduling the solver will choose between possible solutions that satisfy the constraints by using an objective
function. Each possible solution will be assigned a score using this function and the solution with the highest score
will be chosen.There is currently support for only one type of objective function calledRankingWeight.RankingWeightThis objective function will rank people for each shift type based on the following criteria:How many shifts of the given type have they done beforeHow recently have they been on shiftTheir name (just to ensure there is a total ordering)In addition it will also rank someone being assigned a second shift below everyone else being assigned their first shift
and so on for possible additional shifts on the same person.e.g. we have Bob, Alice, and Eve. Bob has done 10 shifts, Alice and Eve have done 8 each. Alice was last on call 2 days
ago and Even was 4 days ago. Everyone can be assigned a maximum of two shifts. The ranking would be:Eve, 1st shiftAlice, 1st shiftBob, 1st shiftEve, 2nd shiftAlice, 2nd shiftBob, 2nd shiftEve is given the highest priority because she is tied for least number of shifts done, but has not been on call as
recently. Second shifts are ranked the same way, but after all first shifts.Each level of this rank is then assigned a weight that is higher the further up in the ranking.There is a large gap in the weights when switching from the first shift to the second and so on in order to discourage
assigning multiple shifts before exhausting other possibilities.This process is repeated for each shift type. The resulting weights are then added together to produce the final score.
This final score is then given as a reward to the solver for assigning the corresponding person/shift.ConfigExamples can be found underexamples.The config file describes the desired on-call rota, the people to assign, and any constraints. The meaning of its
sections are as follows:shifts: contains one entry for each shift to assign, along with its name, and typepeople: the people to assignmax_shifts_per_person: a hard limit on how many shifts a single person can be assignedobjective: the chosen objective functionconstraints: the chosen constraintsHistoryExamples can be found underexamples.The history file describes past shifts per person, to be used by certain constraints. The meaning of its sections
are as follows:shifts: one entry per past shift a person has done, including its typeoffset: offsets to be added to number of shifts done per type for each personDevelopmentOR-Shifty is Python3 application developed usingPoetry. The minimum required
Python version, project dependencies, and other project information can be found in thepyproject.tomlfile.For ease of development there is aMakefile.makehelp# Get a list of all supported make targetsmakefreeze# Update and freeze python requirementsmaketest# Run testsmakeformat# Run formatters and lintersmakeverify# Run formatters and linters in check only modemakeinstall# Install project dependencies from poetry.lock, project module, and `shifty` scriptmakebuild# Build source and wheelsBefore submitting any pull requestsmake testandmake verifymust both be run an be passing.LicenseSee the LICENSE file that is included with this repository.