commit d608f3fc6fbb9451cde5e2e61703de6648e60cb4 Author: Hilmar Magnusson Date: Wed Nov 27 20:19:03 2024 +0100 commit message diff --git a/README b/README new file mode 100644 index 0000000..50e72ab --- /dev/null +++ b/README @@ -0,0 +1,8 @@ +# 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 + diff --git a/opt.py b/opt.py new file mode 100644 index 0000000..45d5dc6 --- /dev/null +++ b/opt.py @@ -0,0 +1,69 @@ +from pyomo.environ import SolverFactory +solver = SolverFactory('glpk') + +import pandas as pd +import pyomo.environ as pyo +# Create a model +model = pyo.ConcreteModel() + +## Define variables +#model.x = pyo.Var(within=pyo.NonNegativeReals) +#model.y = pyo.Var(within=pyo.NonNegativeReals) +## Define objective +#model.obj = pyo.Objective(expr=model.x + model.y, sense=pyo.minimize) +## Define constraints +#model.con1 = pyo.Constraint(expr=model.x + 2 * model.y >= 4) +#model.con2 = pyo.Constraint(expr=model.x - model.y <= 1) +## Select solver +#solver = pyo.SolverFactory('glpk') +## Solve the problem +#result = solver.solve(model) +## Display results +#print('Status:', result.solver.status) +#print('Termination Condition:', result.solver.termination_condition) +#print('Optimal x:', pyo.value(model.x)) +#print('Optimal y:', pyo.value(model.y)) +#print('Optimal Objective:', pyo.value(model.obj)) + +class Scheduler: + def __init__(self, case_file_path, session_file_path, patient_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") + try: + self.df_patients = pd.read_csv(patient_file_path) + except FileNotFoundError: + print("Patient data not found") + #self.model = self.create_model() + +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 + + +path = '/home/hmag/code/pyomo/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)) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0fec1bd --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +pyomo +pandas diff --git a/scheduler.py b/scheduler.py new file mode 100644 index 0000000..2309f76 --- /dev/null +++ b/scheduler.py @@ -0,0 +1,206 @@ +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 +#import numpy as np + + +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() + return + + 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() + return + + 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 + + print("print df sessions type") + print(self.df_sessions.loc[:, "Start"]) + print('done') + + #self.df_sessions.loc[:, "Start"] = pd.to_datetime(self.df_sessions["Start"]) + self.df_sessions.loc[:, "Start"] = pd.to_timedelta(self.df_sessions["Start"]) + print("print AFTER") + print(self.df_sessions.loc[:, "Start"]) + print('done') + for i in range(0,len(self.df_sessions.loc[:, "Start"])): + print(self.df_sessions.loc[:, "Start"][i]) + print(type(self.df_sessions.loc[:, "Start"][i])) + print(f'total sceconds = {self.df_sessions.loc[:, "Start"][i].total_seconds()}') + self.df_sessions.loc[i, "Start"] = self.df_sessions.loc[:, "Start"][i].total_seconds()/60 + # for session in self.df_sessions.loc[:, "Start"]: + # print(session) + # print(session.total_seconds()) + # session = session.total_seconds() + print('test values;') + for session in self.df_sessions.loc[:, "Start"]: + print(session) + + #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 create_model(self): + model = pe.ConcreteModel() + + return model + # # 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 sessions IDs + # # 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): + return + + def draw_gantt(self): + return + +if __name__ == "__main__": + print('tet') + case_path = os.path.join(os.path.dirname(os.getcwd()), "data", "cases.csv") + #case_path = "/home/hmag/code/theatre-scheduling/data/cases.csv" + patient_path = os.path.join(os.path.dirname(os.getcwd()), "data", "patients.csv") + session_path = os.path.join(os.path.dirname(os.getcwd()), "data", "sessions.csv") + schedule_path = os.path.join(os.path.dirname(os.getcwd()), "data", "EKT-Plan.docx") + cbc_path = "/usr/bin/cbc" + + options = {"seconds": 10} + 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)