removed initial formulation
This commit is contained in:
parent
22182ec3a9
commit
58d8ce1bcf
|
@ -1,263 +0,0 @@
|
||||||
import pandas as pd
|
|
||||||
import pyomo.environ as pe
|
|
||||||
import pyomo.gdp as pyogdp
|
|
||||||
import os
|
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
import matplotlib.cm as cm
|
|
||||||
from itertools import product
|
|
||||||
|
|
||||||
|
|
||||||
# check with Taha if code is too similar to Alstom?
|
|
||||||
class TheatreScheduler:
|
|
||||||
|
|
||||||
def __init__(self, case_file_path, session_file_path):
|
|
||||||
"""
|
|
||||||
Read case and session data into Pandas DataFrames
|
|
||||||
Args:
|
|
||||||
case_file_path (str): path to case data in CSV format
|
|
||||||
session_file_path (str): path to theatre session data in CSV format
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
self.df_cases = pd.read_csv(case_file_path)
|
|
||||||
except FileNotFoundError:
|
|
||||||
print("Case data not found.")
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.df_sessions = pd.read_csv(session_file_path)
|
|
||||||
except FileNotFoundError:
|
|
||||||
print("Session data not found")
|
|
||||||
|
|
||||||
self.model = self.create_model()
|
|
||||||
|
|
||||||
def _generate_case_durations(self):
|
|
||||||
"""
|
|
||||||
Generate mapping of cases IDs to median case time for the procedure
|
|
||||||
Returns:
|
|
||||||
(dict): dictionary with CaseID as key and median case time (mins) for procedure as value
|
|
||||||
"""
|
|
||||||
return pd.Series(self.df_cases["Median Duration"].values, index=self.df_cases["CaseID"]).to_dict()
|
|
||||||
|
|
||||||
def _generate_session_durations(self):
|
|
||||||
"""
|
|
||||||
Generate mapping of all theatre sessions IDs to session duration in minutes
|
|
||||||
Returns:
|
|
||||||
(dict): dictionary with SessionID as key and session duration as value
|
|
||||||
"""
|
|
||||||
return pd.Series(self.df_sessions["Duration"].values, index=self.df_sessions["SessionID"]).to_dict()
|
|
||||||
|
|
||||||
def _generate_session_start_times(self):
|
|
||||||
"""
|
|
||||||
Generate mapping from SessionID to session start time
|
|
||||||
Returns:
|
|
||||||
(dict): dictionary with SessionID as key and start time in minutes since midnight as value
|
|
||||||
"""
|
|
||||||
# Convert session start time from HH:MM:SS format into seconds elapsed since midnight
|
|
||||||
self.df_sessions.loc[:, "Start"] = pd.to_timedelta(self.df_sessions["Start"])
|
|
||||||
self.df_sessions.loc[:, "Start"] = self.df_sessions["Start"].dt.total_seconds() / 60
|
|
||||||
return pd.Series(self.df_sessions["Start"].values, index=self.df_sessions["SessionID"]).to_dict()
|
|
||||||
|
|
||||||
def _get_ordinal_case_deadlines(self):
|
|
||||||
"""
|
|
||||||
#TODO
|
|
||||||
Returns:
|
|
||||||
|
|
||||||
"""
|
|
||||||
self.df_cases.loc[:, "TargetDeadline"] = pd.to_datetime(self.df_cases["TargetDeadline"], format="%d/%m/%Y")
|
|
||||||
self.df_cases.loc[:, "TargetDeadline"] = self.df_cases["TargetDeadline"].apply(lambda date: date.toordinal())
|
|
||||||
return pd.Series(self.df_cases["TargetDeadline"].values, index=self.df_cases["CaseID"]).to_dict()
|
|
||||||
|
|
||||||
def _get_ordinal_session_dates(self):
|
|
||||||
"""
|
|
||||||
#TODO
|
|
||||||
Returns:
|
|
||||||
|
|
||||||
"""
|
|
||||||
self.df_sessions.loc[:, "Date"] = pd.to_datetime(self.df_sessions["Date"], format="%d/%m/%Y")
|
|
||||||
self.df_sessions.loc[:, "Date"] = self.df_sessions["Date"].apply(lambda date: date.toordinal())
|
|
||||||
return pd.Series(self.df_sessions["Date"].values, index=self.df_sessions["SessionID"]).to_dict()
|
|
||||||
|
|
||||||
def _generate_disjunctions(self):
|
|
||||||
"""
|
|
||||||
#TODO
|
|
||||||
Returns:
|
|
||||||
disjunctions (list): list of tuples containing disjunctions
|
|
||||||
"""
|
|
||||||
cases = self.df_cases["CaseID"].to_list()
|
|
||||||
sessions = self.df_sessions["SessionID"].to_list()
|
|
||||||
disjunctions = []
|
|
||||||
for (case1, case2, session) in product(cases, cases, sessions):
|
|
||||||
if (case1 != case2) and (case2, case1, session) not in disjunctions:
|
|
||||||
disjunctions.append((case1, case2, session))
|
|
||||||
|
|
||||||
return disjunctions
|
|
||||||
|
|
||||||
def create_model(self):
|
|
||||||
model = pe.ConcreteModel()
|
|
||||||
|
|
||||||
# Model Data
|
|
||||||
|
|
||||||
# List of case IDs in surgical waiting list
|
|
||||||
model.CASES = pe.Set(initialize=self.df_cases["CaseID"].tolist())
|
|
||||||
# List of sessions IDs
|
|
||||||
model.SESSIONS = pe.Set(initialize=self.df_sessions["SessionID"].tolist())
|
|
||||||
# List of tasks - all possible (caseID, sessionID) combination
|
|
||||||
model.TASKS = pe.Set(initialize=model.CASES * model.SESSIONS, dimen=2)
|
|
||||||
# The duration (median case time) for each operation
|
|
||||||
model.CASE_DURATION = pe.Param(model.CASES, initialize=self._generate_case_durations())
|
|
||||||
# The duration of each theatre session
|
|
||||||
model.SESSION_DURATION = pe.Param(model.SESSIONS, initialize=self._generate_session_durations())
|
|
||||||
# The start time of each theatre session
|
|
||||||
model.SESSION_START_TIME = pe.Param(model.SESSIONS, initialize=self._generate_session_start_times())
|
|
||||||
# The deadline of each case
|
|
||||||
model.CASE_DEADLINES = pe.Param(model.CASES, initialize=self._get_ordinal_case_deadlines())
|
|
||||||
# The date of each theatre session
|
|
||||||
model.SESSION_DATES = pe.Param(model.SESSIONS, initialize=self._get_ordinal_session_dates())
|
|
||||||
|
|
||||||
|
|
||||||
model.DISJUNCTIONS = pe.Set(initialize=self._generate_disjunctions(), dimen=3)
|
|
||||||
|
|
||||||
ub = 1440 # seconds in a day
|
|
||||||
model.M = pe.Param(initialize=1e3*ub) # big M
|
|
||||||
max_util = 0.85
|
|
||||||
num_cases = self.df_cases.shape[0]
|
|
||||||
|
|
||||||
# Decision Variables
|
|
||||||
model.SESSION_ASSIGNED = pe.Var(model.TASKS, domain=pe.Binary)
|
|
||||||
model.CASE_START_TIME = pe.Var(model.TASKS, bounds=(0, ub), within=pe.PositiveReals)
|
|
||||||
model.CASES_IN_SESSION = pe.Var(model.SESSIONS, bounds=(0, num_cases), within=pe.PositiveReals)
|
|
||||||
|
|
||||||
# Objective
|
|
||||||
def objective_function(model):
|
|
||||||
return pe.summation(model.CASES_IN_SESSION)
|
|
||||||
#return sum([model.SESSION_ASSIGNED[case, session] for case in model.CASES for session in model.SESSIONS])
|
|
||||||
model.OBJECTIVE = pe.Objective(rule=objective_function, sense=pe.maximize)
|
|
||||||
|
|
||||||
# Constraints
|
|
||||||
|
|
||||||
# Case start time must be after start time of assigned theatre session
|
|
||||||
def case_start_time(model, case, session):
|
|
||||||
return model.CASE_START_TIME[case, session] >= model.SESSION_START_TIME[session] - \
|
|
||||||
((1 - model.SESSION_ASSIGNED[(case, session)])*model.M)
|
|
||||||
model.CASE_START = pe.Constraint(model.TASKS, rule=case_start_time)
|
|
||||||
|
|
||||||
# Case end time must be before end time of assigned theatre session
|
|
||||||
def case_end_time(model, case, session):
|
|
||||||
return model.CASE_START_TIME[case, session] + model.CASE_DURATION[case] <= model.SESSION_START_TIME[session] + \
|
|
||||||
model.SESSION_DURATION[session]*max_util + ((1 - model.SESSION_ASSIGNED[(case, session)]) * model.M)
|
|
||||||
model.CASE_END_TIME = pe.Constraint(model.TASKS, rule=case_end_time)
|
|
||||||
|
|
||||||
# Cases can be assigned to a maximum of one session
|
|
||||||
def session_assignment(model, case):
|
|
||||||
return sum([model.SESSION_ASSIGNED[(case, session)] for session in model.SESSIONS]) <= 1
|
|
||||||
model.SESSION_ASSIGNMENT = pe.Constraint(model.CASES, rule=session_assignment)
|
|
||||||
|
|
||||||
def set_deadline_condition(model, case, session):
|
|
||||||
return model.SESSION_DATES[session] <= model.CASE_DEADLINES[case] + ((1 - model.SESSION_ASSIGNED[case, session])*model.M)
|
|
||||||
model.APPLY_DEADLINE = pe.Constraint(model.TASKS, rule=set_deadline_condition)
|
|
||||||
|
|
||||||
def no_case_overlap(model, case1, case2, session):
|
|
||||||
return [model.CASE_START_TIME[case1, session] + model.CASE_DURATION[case1] <= model.CASE_START_TIME[case2, session] + \
|
|
||||||
((2 - model.SESSION_ASSIGNED[case1, session] - model.SESSION_ASSIGNED[case2, session])*model.M),
|
|
||||||
model.CASE_START_TIME[case2, session] + model.CASE_DURATION[case2] <= model.CASE_START_TIME[case1, session] + \
|
|
||||||
((2 - model.SESSION_ASSIGNED[case1, session] - model.SESSION_ASSIGNED[case2, session])*model.M)]
|
|
||||||
|
|
||||||
model.DISJUNCTIONS_RULE = pyogdp.Disjunction(model.DISJUNCTIONS, rule=no_case_overlap)
|
|
||||||
|
|
||||||
def theatre_util(model, session):
|
|
||||||
return model.CASES_IN_SESSION[session] == \
|
|
||||||
sum([model.SESSION_ASSIGNED[case, session] for case in model.CASES])
|
|
||||||
|
|
||||||
model.THEATRE_UTIL = pe.Constraint(model.SESSIONS, rule=theatre_util)
|
|
||||||
|
|
||||||
pe.TransformationFactory("gdp.bigm").apply_to(model)
|
|
||||||
|
|
||||||
return model
|
|
||||||
|
|
||||||
def solve(self, solver_name, options=None, solver_path=None, local=True):
|
|
||||||
|
|
||||||
if solver_path is not None:
|
|
||||||
solver = pe.SolverFactory(solver_name, executable=solver_path)
|
|
||||||
else:
|
|
||||||
solver = pe.SolverFactory(solver_name)
|
|
||||||
|
|
||||||
# TODO remove - too similar to alstom
|
|
||||||
if options is not None:
|
|
||||||
for key, value in options.items():
|
|
||||||
solver.options[key] = value
|
|
||||||
|
|
||||||
if local:
|
|
||||||
solver_results = solver.solve(self.model, tee=True)
|
|
||||||
else:
|
|
||||||
solver_manager = pe.SolverManagerFactory("neos")
|
|
||||||
solver_results = solver_manager.solve(self.model, opt=solver)
|
|
||||||
|
|
||||||
results = [{"Case": case,
|
|
||||||
"Session": session,
|
|
||||||
"Session Date": self.model.SESSION_DATES[session],
|
|
||||||
"Case Deadline": self.model.CASE_DEADLINES[case],
|
|
||||||
"Days before deadline": self.model.CASE_DEADLINES[case] - self.model.SESSION_DATES[session],
|
|
||||||
"Start": self.model.CASE_START_TIME[case, session](),
|
|
||||||
"Assignment": self.model.SESSION_ASSIGNED[case, session]()}
|
|
||||||
for (case, session) in self.model.TASKS]
|
|
||||||
|
|
||||||
self.df_times = pd.DataFrame(results)
|
|
||||||
|
|
||||||
all_cases = self.model.CASES.value_list
|
|
||||||
cases_assigned = []
|
|
||||||
for (case, session) in self.model.SESSION_ASSIGNED:
|
|
||||||
if self.model.SESSION_ASSIGNED[case, session] == 1:
|
|
||||||
cases_assigned.append(case)
|
|
||||||
|
|
||||||
cases_missed = list(set(all_cases).difference(cases_assigned))
|
|
||||||
print("Number of cases assigned = {} out of {}:".format(len(cases_assigned), len(all_cases)))
|
|
||||||
print("Cases assigned: ", cases_assigned)
|
|
||||||
print("Number of cases missed = {} out of {}:".format(len(cases_missed), len(all_cases)))
|
|
||||||
print("Cases missed: ", cases_missed)
|
|
||||||
self.model.CASES_IN_SESSION.pprint()
|
|
||||||
print("Total Objective = {}".format(sum(self.model.CASES_IN_SESSION.get_values().values())))
|
|
||||||
print("Number of constraints = {}".format(solver_results["Problem"].__getitem__(0)["Number of constraints"]))
|
|
||||||
#self.model.SESSION_ASSIGNED.pprint()
|
|
||||||
print(self.df_times[self.df_times["Assignment"] == 1].to_string())
|
|
||||||
self.draw_gantt()
|
|
||||||
|
|
||||||
def draw_gantt(self):
|
|
||||||
|
|
||||||
df = self.df_times[self.df_times["Assignment"] == 1]
|
|
||||||
cases = sorted(list(df['Case'].unique()))
|
|
||||||
sessions = sorted(list(df['Session'].unique()))
|
|
||||||
|
|
||||||
bar_style = {'alpha': 1.0, 'lw': 25, 'solid_capstyle': 'butt'}
|
|
||||||
text_style = {'color': 'white', 'weight': 'bold', 'ha': 'center', 'va': 'center'}
|
|
||||||
colors = cm.Dark2.colors
|
|
||||||
|
|
||||||
df.sort_values(by=['Case', 'Session'])
|
|
||||||
df.set_index(['Case', 'Session'], inplace=True)
|
|
||||||
|
|
||||||
fig, ax = plt.subplots(1, 1)
|
|
||||||
for c_ix, c in enumerate(cases, 1):
|
|
||||||
for s_ix, s in enumerate(sessions, 1):
|
|
||||||
if (c, s) in df.index:
|
|
||||||
xs = df.loc[(c, s), 'Start']
|
|
||||||
xf = df.loc[(c, s), 'Start'] + \
|
|
||||||
self.df_cases[self.df_cases["CaseID"] == c]["Median Duration"]
|
|
||||||
ax.plot([xs, xf], [s] * 2, c=colors[c_ix % 7], **bar_style)
|
|
||||||
ax.text((xs + xf) / 2, s, c, **text_style)
|
|
||||||
|
|
||||||
ax.set_title('Assigning Ophthalmology Cases to Theatre Sessions')
|
|
||||||
ax.set_xlabel('Time')
|
|
||||||
ax.set_ylabel('Sessions')
|
|
||||||
ax.grid(True)
|
|
||||||
|
|
||||||
fig.tight_layout()
|
|
||||||
plt.show()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
case_path = os.path.join(os.path.dirname(os.getcwd()), "data", "cases.csv")
|
|
||||||
session_path = os.path.join(os.path.dirname(os.getcwd()), "data", "sessions.csv")
|
|
||||||
cbc_path = "C:\\Users\\LONLW15\\Documents\\Linear Programming\\Solvers\\cbc.exe"
|
|
||||||
|
|
||||||
options = {"seconds": 300}
|
|
||||||
scheduler = TheatreScheduler(case_file_path=case_path, session_file_path=session_path)
|
|
||||||
scheduler.solve(solver_name="cbc", solver_path=cbc_path, options=options)
|
|
||||||
#scheduler.solve(solver_name="cbc", local=False, options=None)
|
|
Loading…
Reference in New Issue