diff --git a/README b/README index 50e72ab..65cc5d2 100644 --- a/README +++ b/README @@ -1,8 +1,3 @@ -# Create EKG Schedule - -based on https://python.plainenglish.io/solving-the-resource-constrained-project-scheduling-problem-rcpsp-with-python-and-pyomo-001cffd5344a - -$ python -m venv ekg -$ source ekg/bin/activate -$ pip install -r requirements.txt - +python -m venv venv +source venv/bin/activate +pip install -r requirements.txt diff --git a/data/blocks.csv b/data/blocks.csv new file mode 100644 index 0000000..c247093 --- /dev/null +++ b/data/blocks.csv @@ -0,0 +1,33 @@ +day,time,available +03/12/2024,08:00:00,1 +03/12/2024,08:20:00,1 +03/12/2024,08:40:00,1 +03/12/2024,09:00:00,1 +03/12/2024,09:20:00,1 +03/12/2024,09:40:00,1 +03/12/2024,10:00:00,1 +03/12/2024,10:20:00,1 +03/12/2024,10:40:00,0 +03/12/2024,11:00:00,1 +03/12/2024,11:20:00,1 +03/12/2024,11:40:00,0 +03/12/2024,12:00:00,1 +03/12/2024,12:20:00,1 +03/12/2024,12:40:00,1 +03/12/2024,13:00:00,1 +04/12/2024,08:00:00,0 +04/12/2024,08:20:00,0 +04/12/2024,08:40:00,0 +04/12/2024,09:00:00,1 +04/12/2024,09:20:00,1 +04/12/2024,09:40:00,1 +04/12/2024,10:00:00,1 +04/12/2024,10:20:00,1 +04/12/2024,10:40:00,1 +04/12/2024,11:00:00,0 +04/12/2024,11:20:00,1 +04/12/2024,11:40:00,1 +04/12/2024,12:00:00,1 +04/12/2024,12:20:00,1 +04/12/2024,12:40:00,1 +04/12/2024,13:00:00,0 diff --git a/data/cases.csv b/data/cases.csv deleted file mode 100644 index a81d985..0000000 --- a/data/cases.csv +++ /dev/null @@ -1,9 +0,0 @@ -CaseID,PatientID,Station,EKT,Erhaltung,Ket,Date -1,21-239,1,15,0,no,28/11/2024 -2,21-237,0,22,1,no,29/11/2024 -3,21-238,3,12,1,no,30/11/2024 -4,21-248,2,12,1,no,30/11/2024 -5,21-243,3,12,1,no,30/11/2024 -6,21-233,3,12,1,no,30/11/2024 -7,21-218,3,12,1,no,30/11/2024 -8,21-208,3,12,1,no,30/11/2024 diff --git a/data/patients.csv b/data/patients.csv index 3662b41..1cd55a7 100644 --- a/data/patients.csv +++ b/data/patients.csv @@ -1,12 +1,17 @@ -Id,PatientID,Patient,Station -1,21-239,Hans Zimmer,1 -2,21-237,Lina Gruber,0 -3,21-238,Victoria J,3 -4,21-229,Hilmario Maggio,3 -5,21-249,Egon Schiele,2 -6,21-219,Hans Landa,1 -7,21-213,Gregor Samsa,1 -8,21-222,Johanne,2 -9,21-223,Angela Merkel,1 -10,21-240,Britney Spears,3 -11,21-241,Madonna,0 +patient,id,station +Hr. Y (9),21-237,08b +Hr. X (5),21-239,16A +Hr. Q (12),21-235,08B +Hr. N (9),21-238,16A +Hr. Li (16/4E),21-228,16A +Hr. J,21-176,16A +Hr. Ä (17/3E),21-223,16A +Fr.O (1),21-242,16b +Fr. S (18/1E),21-227,08B +Fr. R (15/3E),21-220,16B +Fr. P (8E),21-213,16B +Fr. M (7E),21-216,16A +Fr. L (7E),21-214,16B +Fr. F (12/1E),21-234,08b +Fr. E (13),21-233,16B +Fr. A (7),21-236,16A diff --git a/data/sessions.csv b/data/sessions.csv deleted file mode 100644 index 8127527..0000000 --- a/data/sessions.csv +++ /dev/null @@ -1,13 +0,0 @@ -SessionID,Date,Start,End,Duration,ConsultantID,Specialty -1001,29/11/2024,08:00:00,08:20:00,20,11,Ophthalmology -1002,29/11/2024,08:20:00,08:40:00,20,11,Ophthalmology -1003,29/11/2024,08:40:00,09:00:00,20,11,Ophthalmology -1004,29/11/2024,09:00:00,09:20:00,20,11,Ophthalmology -1005,29/11/2024,09:20:00,09:40:00,20,11,Ophthalmology -1006,29/11/2024,09:40:00,10:00:00,20,11,Ophthalmology -1007,29/11/2024,10:00:00,10:20:00,20,11,Ophthalmology -1008,29/11/2024,10:20:00,10:40:00,20,11,Ophthalmology -1009,29/11/2024,10:40:00,11:00:00,20,11,Ophthalmology -1010,29/11/2024,11:00:00,11:20:00,20,11,Ophthalmology -1011,29/11/2024,11:20:00,11:40:00,20,11,Ophthalmology -1012,29/11/2024,11:40:00,12:00:00,20,11,Ophthalmology diff --git a/data/tasks.csv b/data/tasks.csv new file mode 100644 index 0000000..3ceed0e --- /dev/null +++ b/data/tasks.csv @@ -0,0 +1,12 @@ +id,patient,priority,date,time,duration,station +1001,21-237,1,03/12/2024,08:30:00,1,0 +1002,21-239,1,04/12/2024,08:30:00,1,2 +1003,21-235,11,03/12/2024,08:30:00,1,1 +1004,21-227,1,03/12/2024,08:30:00,1,0 +1005,21-238,2,03/12/2024,08:30:00,1,1 +1006,21-240,1,04/12/2024,08:30:00,1,3 +1007,21-236,1,03/12/2024,08:30:00,1,2 +1008,21-228,1,03/12/2024,08:30:00,1,1 +1009,21-249,1,03/12/2024,08:30:00,1,2 +1010,21-241,1,03/12/2024,08:30:00,1,3 +1011,21-247,1,03/12/2024,08:30:00,1,3 diff --git a/opt.py b/opt.py index 0db1345..6ab2532 100644 --- a/opt.py +++ b/opt.py @@ -1,175 +1,121 @@ +import pyomo import pandas as pd -import pyomo.environ as pyo -import datetime -import matplotlib.cm as cm -import matplotlib.pyplot as plt +from pulp import * class Scheduler: - def __init__(self, case_file_path, session_file_path, patient_file_path): + def __init__(self, + task_path='data/tasks.csv', + block_path='data/blocks.csv', + patient_path='data/patients.csv'): """ 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 + session_file_path (str): path to theatre session data in CSV format """ try: - self.df_cases = pd.read_csv(case_file_path) + self.df_tasks = pd.read_csv(task_path) except FileNotFoundError: - print(f"Case data not found. {case_file_path}") + print("Task data not found.") try: - self.df_sessions = pd.read_csv(session_file_path) + self.df_blocks = pd.read_csv(block_path) except FileNotFoundError: print("Session data not found") try: - self.df_patients = pd.read_csv(patient_file_path) + self.df_patients = pd.read_csv(patient_path) except FileNotFoundError: print("Patient data not found") - self.solver = pyo.SolverFactory('glpk') - self.model = self.create_model() - self.build_model() - def solve_model(self): - self.solver_results = self.solver.solve(self.model, tee=True) + self.create_lists() - def extract_results(self): - 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) + def create_lists(self): + self.s = self.df_tasks['priority'].tolist() + self.d = self.df_tasks['duration'].tolist() + self.dates = self.df_tasks['date'].tolist() + self.b = self.df_blocks['available'].tolist() + self.block_dates= self.df_blocks['day'].tolist() - def create_model(self): - return pyo.ConcreteModel() - - def set_constraint(self): - self.model.c = pyo.ConstraintList() - - def solve(self): - result = self.solver.solve(self.model) - print(f'result was {result}') - - def build_model(self): - self.add_cases() - #self.set_options() - self.add_sessions() - self.add_tasks() - self.set_decisions() - self.set_obj() + self.patient_ids = self.df_patients['patient'].tolist() + self.task_patient= self.df_tasks['patient'].tolist() + # convert dates of tasks and blocks to (week,day) + self.convert_dates() return - def add_cases(self): - # List of case IDs in surgical waiting list - self.model.CASES = pyo.Set(initialize=self.df_cases["CaseID"].tolist()) - return + def convert_dates(self): + self.week_day = [self.date2int(date) for date in self.dates] + self.block_week_day = [self.date2int(date) for date in self.block_dates] - def set_options(self): - # Add solver parameters (time limit) - options = {"seconds": 6} - for key, value in options.items(): - self.solver.options[key] = value + def date2int(self, date): + d0 = pd.to_datetime("25/11/2024", dayfirst=True) + delta = (pd.to_datetime(date, dayfirst=True) - d0).days + day = delta%7 + week = int((delta - day)/7) + return week,day - def add_sessions(self): - # List of sessions IDs - # TODO: Generate more sessions based on EKT Erhaltung - self.model.SESSIONS = pyo.Set(initialize=self.df_sessions["SessionID"].tolist()) - return - - # List of job shop tasks - # all possible combinations of cases and sessions - def add_tasks(self): - self.model.TASKS = pyo.Set(initialize=self.model.CASES * self.model.SESSIONS, dimen=2) - return - # Decision Variables - - ## Upper bound (minutes in a day) - #ub = 1440 - ## Upper bound of session utilisation set to 85% - #max_util = 0.85 - def set_decisions(self): - - # Binary flag, 1 if case is assigned to session, 0 otherwise - self.model.SESSION_ASSIGNED = pyo.Var(self.model.TASKS, domain=pyo.Binary) - # Start time of a case - #self.model.CASE_START_TIME = pe.Var(self.model.TASKS, bounds=(0, ub), within=pe.PositiveReals) - # Session utilisation - num_cases = self.df_cases.shape[0] - self.model.CASES_IN_SESSION = pyo.Var(self.model.SESSIONS, bounds=(0, num_cases), within=pyo.PositiveReals) - self.model.UTILISATION = pyo.Var(self.model.SESSIONS, bounds=(0, 1), within=pyo.PositiveReals) - - def set_obj(self): - - # Objective - def objective_function(model): - return pyo.summation(model.CASES_IN_SESSION) - self.model.OBJECTIVE = pyo.Objective(rule=objective_function, sense=pyo.maximize) - - 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() - -def date2int(date): - d0 = pd.to_datetime("25/11/2024", dayfirst=True) - delta = (pd.to_datetime(date, dayfirst=True) - d0).days - day = delta%7 - week = int((delta - day)/7) - return week,day - -def int2date(week, day): - d0 = pd.to_datetime("25/11/2024", dayfirst=True) - delta = datetime.timedelta(days = 7*week + day) - date = d0 + delta - return date - -path = '/home/hmag/git/octopusx/ekt/data' -my = Scheduler(path+'/cases.csv', path+'/sessions.csv', path+'/patients.csv') - -print(my.df_cases) -print(my.df_sessions) -#print(my.df_patients) -#for i in my.df_patients: -# print(i) -print(my.df_cases['Date'][0]) -date = my.df_cases['Date'][0] - -print(date2int(date)) -print(int2date(1,0)) + def int2date(week, day): + d0 = pd.to_datetime("25/11/2024", dayfirst=True) + delta = datetime.timedelta(days = 7*week + day) + date = d0 + delta + return date -#my.set_constraint() -#my.set_obj() -my.solve_model() +sc = Scheduler() +print(sc.df_tasks) +print(sc.df_blocks) +print(sc.df_patients) -my.extract_results() -print(my.df_times) -my.draw_gantt() +# Declare variables for optimization +s = sc.s +print(f's = {s}') + +d = sc.d +print(f'd = {d}') + +b = sc.b +print(f'b = {b}') + +B = len(b) +n = len(s) +A = sum(b) + +# Import PuLP + +# Define the problem +prob = LpProblem("Schedule_Tasks", LpMaximize) + +# Define y +y = LpVariable.dicts('Block', [(i, t) for i in range(n) for t in range(B)], cat = 'Binary') + +# Definite objective function +prob += lpSum(s[i]*b[t]*y[(i,t)] for i in range(n) for t in range(B)) + +# CONSTRAINTS +# Constraint #1 +prob += lpSum(y[(i,t)] for i in range(n) for t in range(B)) <= A + +# Constraint #2 +for i in range(n): + prob += lpSum(y[(i,t)] for t in range(B)) <= d[i] + +# Constraint #3 +for t in range(B): + prob += lpSum(y[(i,t)] for i in range(n)) <= 1 + +prob.solve() + +# Visualize solution +tasks_blocks = pd.DataFrame(columns=['Task', 'Block']) + +for i in range(n): + for t in range(B): + if y[(i,t)].varValue == 1: + tasks_blocks = pd.concat([tasks_blocks, + pd.DataFrame({'Task': [i], 'Block': [t], 'Patient': sc.task_patient[i] })], + ignore_index=True) + +print(tasks_blocks) + +print(sc.dates) +print(sc.week_day) +print(sc.block_dates) +print(sc.block_week_day) diff --git a/requirements.txt b/requirements.txt index f2725ea..5faf037 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ pyomo pandas -matplotlib +pulp