Skip to content

Commit

Permalink
Spreadout weekends (#39)
Browse files Browse the repository at this point in the history
Spreadout weekends
  • Loading branch information
Express50 authored Jul 17, 2019
2 parents a1e1a31 + 48f3969 commit 03049e9
Show file tree
Hide file tree
Showing 4 changed files with 43 additions and 34 deletions.
2 changes: 1 addition & 1 deletion config/sample_config.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"Michael": {"email": "[email protected]", "divisions": {"A": {"min": 0, "max": 100}, "B": {"min": 0, "max": 100}}}, "Jane": {"email": "", "divisions": {"A": {"min": 0, "max": 100}, "B": {"min": 0, "max": 100}}}, "Bob": {"email": "[email protected]", "divisions": {"A": {"min": 0, "max": 100}, "B": {"min": 0, "max": 100}}}, "Alice": {"email": "[email protected]", "divisions": {"A": {"min": 0, "max": 100}, "B": {"min": 0, "max": 100}}}, "James": {"name": "James", "email": "", "divisions": {"A": {"min": 0, "max": 100}, "B": {"min": 0, "max": 100}}}, "Alex": {"name": "Alex", "email": "", "divisions": {"A": {"min": 1, "max": 100}}}}
{"Michael": {"email": "[email protected]", "divisions": {"A": {"min": 0, "max": 100}, "B": {"min": 0, "max": 100}}}, "Jane": {"email": "", "divisions": {"A": {"min": 0, "max": 100}, "B": {"min": 0, "max": 100}}}, "Bob": {"email": "[email protected]", "divisions": {"A": {"min": 0, "max": 100}, "B": {"min": 0, "max": 100}}}, "Alice": {"email": "[email protected]", "divisions": {"A": {"min": 0, "max": 100}, "B": {"min": 0, "max": 100}}}}
Binary file modified paper/scheduler.pdf
Binary file not shown.
13 changes: 8 additions & 5 deletions paper/scheduler.tex
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
\usepackage{hyperref}
\usepackage{bbm}
\usepackage{physics}
\usepackage{xcolor}

\newcommand{\mc}{\mathcal}
\newcommand{\bb}{\mathbb}
Expand Down Expand Up @@ -44,7 +45,9 @@ \subsection{Variables}
X_{c, b, d} \in \{0, 1\}: &\text{ clinician $c$ covers block $b$ for division $d$} \\
Y_{c, w} \in \{0, 1\}: &\text{ clinician $c$ covers weekend $w$}
\end{align}
Note that weekends are not distinguished according to division, since the clinician assigned covers all divisions.
Note that weekends are not distinguished according to division, since the clinician assigned covers all divisions. \\

\textcolor{red}{\textbf{IMPORTANT: This assumes that each clinician works at each of the divisions $1, \ldots, D$. Some of the formulas below are not directly applicable to the case where some clinician only works a subset of those divisions}}

\subsection{Constraints} \label{constraints}
\begin{itemize}
Expand Down Expand Up @@ -72,14 +75,14 @@ \subsection{Constraints} \label{constraints}
\begin{equation}
\sum_d \left(X_{c, b, d} + X_{c, b+1, d}\right) \leq 1 \text{ for each $c$ and each $b \leq B - 1$}
\end{equation}
\item A clinician cannot work two consecutive weekends
\begin{equation}
Y_{c, w} + Y_{c, w+1} \leq 1 \text{ for each $c$ and each $w \leq W - 1$}
\end{equation}
\item Prevent on-off-on-off-on block assignments (across all divisions) to improve the spread of assignments for any given clinician
\begin{equation}
\sum_d \left(X_{c, b, d} + X_{c, b + 2, d} + X_{c, b + 4, d}\right) \leq 2 \text{ for each $c$ and each $b \leq B - 4$}
\end{equation}
\item Improve spread of weekend assignments by preventing more than 1 assignment for a given clinician in a span of 4 weekends
\begin{equation}
Y_{c, w} + Y_{c, w+1} + Y_{c, w+2} + Y_{c, w+3} \leq 1 \text{ for each $c$ and each $w \leq W - 3$}
\end{equation}
\end{itemize}

\subsection{Objectives} \label{objectives}
Expand Down
62 changes: 34 additions & 28 deletions src/services/scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -408,23 +408,24 @@ def setup_problem(self, shuffle=False):
self._build_clinician_variables(divisions, clinicians)
self._build_coverage_constraints(divisions, clinicians)
self._build_minmax_constraints(divisions)
self._build_consec_constraints(clinicians)
self._build_spread_constraints(clinicians)
self._build_consec_blocks_constraints(clinicians)
self._build_spread_blocks_constraints(clinicians)
self._build_spread_weekends_constraints(clinicians)
self._build_longweekend_constraints(clinicians)
self._build_weekend_constraints(clinicians)
self._build_adjacency_variables(divisions)
appeasement_objs = self._build_appeasement_objectives(clinicians)

num_clin = len(clinicians)
num_div = len(divisions)
block_conflicts_obj = self._build_block_objective(clinicians)
weekend_conflicts_obj = self._build_weekend_objective(clinicians)
adjacency_obj = self._build_adjacency_objective(clinicians)

# make sure to normalize objectives, and weigh them equally
self.problem.setObjective(
(1 / 3) * (1 / (num_clin * self.num_blocks * num_div)) * appeasement_objs[0]
+ (1 / 3) * (1 / (num_clin * self.num_weekends)) * appeasement_objs[1]
+ (1 / 3) * (1 / (num_clin * self.num_blocks * num_div)) * self._build_adjacency_objective(clinicians)
(1 / 3) * (1 / len(block_conflicts_obj)) * block_conflicts_obj
+ (1 / 3) * (1 / len(weekend_conflicts_obj)) * weekend_conflicts_obj
+ (1 / 3) * (1 / len(adjacency_obj)) * adjacency_obj
)

def _build_clinician_variables(self, divisions, clinicians):
# create clinician BlockVariables
for div in divisions:
Expand Down Expand Up @@ -473,7 +474,7 @@ def _build_minmax_constraints(self, divisions):
self.problem.add(sum_ <= max_)
self.problem.add(sum_ >= min_)

def _build_consec_constraints(self, clinicians):
def _build_consec_blocks_constraints(self, clinicians):
for clinician in clinicians:
for block_num in range(1, self.num_blocks):
# if a clinician works a given block, they should not work any
Expand All @@ -484,20 +485,10 @@ def _build_consec_constraints(self, clinicians):
)
self.problem.add(sum_ <= 1)

# at most 1 consecutive weekend of work
for week_num in range(1, self.num_weekends):
sum_ = pulp.lpSum(
[_.get_var() for _ in clinician.get_weekend_vars(
lambda x, week_num=week_num: x.week_num in (
week_num, week_num + 1)
)]
)
self.problem.add(sum_ <= 1)

def _build_spread_constraints(self, clinicians):
def _build_spread_blocks_constraints(self, clinicians):
# on-off-on-off-on constraint for block assignment
# we need at least 5 consecutive blocks to implement this constraint
# on-off-on-off-on
if self.num_blocks <= 5: return
if self.num_blocks < 5: return

for clinician in clinicians:
for block_num in range(1, self.num_blocks - 3):
Expand All @@ -508,6 +499,21 @@ def _build_spread_constraints(self, clinicians):
)
self.problem.add(sum_ <= 2)

def _build_spread_weekends_constraints(self, clinicians):
# spreading out weekend assignments
# needs at least 4 weekends
if self.num_weekends < 4: return

for clinician in clinicians:
for week_num in range(1, self.num_weekends - 2):
# constraint: X_i + X_{i+1} + X_{i+2} + X_{i+3} <= 1
sum_ = pulp.lpSum(
[_.get_var() for _ in clinician.get_weekend_vars(
lambda x, week_num=week_num: x.week_num in list(range(week_num, week_num + 4))
)]
)
self.problem.add(sum_ <= 1)

def _build_longweekend_constraints(self, clinicians):
if self.long_weekends:
# (roughly) equal distribution of long weekends
Expand Down Expand Up @@ -587,7 +593,7 @@ def _build_adjacency_objective(self, clinicians):
)
return pulp.lpSum(adjacency_vars)

def _build_appeasement_objectives(self, clinicians):
def _build_weekend_objective(self, clinicians):
wa_variables = []
for clinician in clinicians:
wa_variables.extend(
Expand All @@ -602,6 +608,9 @@ def _build_appeasement_objectives(self, clinicians):
]
)

return pulp.lpSum(wa_variables)

def _build_block_objective(self, clinicians):
ba_variables = []
for clinician in clinicians:
ba_variables.extend(
Expand All @@ -616,10 +625,7 @@ def _build_appeasement_objectives(self, clinicians):
]
)

return (
pulp.lpSum(wa_variables),
pulp.lpSum(ba_variables)
)
return pulp.lpSum(ba_variables)

def assign_schedule(self):
"""
Expand Down

0 comments on commit 03049e9

Please sign in to comment.