/**Run Scheduling Alogirthm after Import*/ // This trigger fires after all of the CaseInfo table has been imported. // The OR start and quiting times were entered by the user through the dashboard window. double orstarttime = timestringtominutes(gettablestr(ModelVariables, 1,1)); double orquittime = timestringtominutes(gettablestr(ModelVariables, 2,1)); int schedulingmethod = gettablenum(ModelVariables, 3,1); // Patients for the first case in each OR suite must arrive before the OR start time in order to be prepped in the PreOP area. // Therefore the arrival times for patients as determined by the scheduling algorithm will be "Allotted PreOp Time" minutes // before their planned surgery start time. I will keep track of the earliest patient arrival time to be able to set the // simulation start time accordingly. double earliestarrivaltime = VERYLARGENUMBER; // Populate an array of cases for each OR suite according to the following rules: // 1. The allotted time for each OR suite is the orquittime minus orstarttime and will NOT include the final room cleaning time. // 2. Fill the allotted time with scheduled block cases ahead of add-on cases, in the order of shortest to longest expected surgery durations. // 3. Do not schedule any cases with an expected duration falling outside the allotted time for the OR. // 4. Cases will be scheduled in the suite designated by the user. When no suite is specified, the case will be added to the first suite with sufficient time remaining in its schedule. int totcases = gettablerows(CaseInfo); int totrooms = getnumlocations(ORArea); int row = 0; //row number in table for loops int index = 0; //index in array for loops // If an entry is found in the ArrivalTime field, then assume the user has manually entered all the patient // arrival times, and therefore, it will not be necessary to calculate any patient arrival times in this function. if(gettablenum(CaseInfo,1,CI_ARRIVALTIME) == 0 && schedulingmethod > 1) { // Make sure the arrival time column is numeric data type (necessary because the automatic data distinction will assign string data to cells containing no data) for(int j = 1; j <= totcases; j++) nodeadddata(gettablecell(CaseInfo, j, CI_ARRIVALTIME), DATATYPE_NUMBER); // An array indicating the status of each OR suite's schedule. A value of 1 means the suite's schedule has been filled up // with surgical cases for the day, and a 0 means there may be time remaining for additional cases to be added to its schedule. // The array entries are ordered according to OR suite number. intarray fullarray = makearray(totrooms); // Initialize array for(index = 1; index <= totrooms; index++) fullarray[index] = 0; // An array of sorted CaseInfo table row numbers for the last case assigned to each OR suite intarray lastcasearray = makearray(totrooms); // Initialize array for(index = 1; index <= totrooms; index++) lastcasearray[index] = 0; // Local variable declarations int roomnr = 0; string roomname = ""; int lastrow = 0; //row number of last case assigned double lastcasefinishtime = 0; //finish time for the last case assigned (includes room cleaning) int allfinished = 0; //flag set to 1 when all rooms are finished being scheduled for a while loop int sortorder = 1; //ascending/descending (+1/-1) if(schedulingmethod == 3) //LPTF should sort in descending order sortorder = -1; // Sort the CaseInfo table cases from the shortest to the longest expected surgery durations multisorttable(CaseInfo, sortorder * CI_EXPECTEDSURGERY); // A while loop is used to continue assigning block cases in round robin fashion from shortest to longest surgical times until no more // block cases can be assigned. A countdown is used as a backup to avoid an infinite loop. int countdown = 1000; while(!allfinished && countdown > 0) { countdown--; // For each OR suite, for(roomnr = 1; roomnr <= totrooms; roomnr++) { // If the schedule is already full for this suite, then skip and continue with the other rooms if(fullarray[roomnr]) continue; roomname = getname(getlocfromarea(ORArea, roomnr)); // The row number in the sorted CaseInfo table of the last case assigned to this suite lastrow = lastcasearray[roomnr]; // If no cases have been assigned to this suite yet, then set the lastcasefinishtime to the starting time for the OR if(lastrow == 0) lastcasefinishtime = orstarttime; else // Otherwise, calculate the finish time for the case: scheduled arrival time plus PreOP time plus planned surgery time plus suite cleaning time lastcasefinishtime = gettablenum(CaseInfo,lastrow,CI_ARRIVALTIME) + gettablenum(CaseInfo,lastrow,CI_PREOP) + gettablenum(CaseInfo,lastrow,CI_EXPECTEDSURGERY) + gettablenum(CaseInfo,lastrow,CI_EXPECTEDTURNOVER); // For each case (row) in the CaseInfo table starting with the row after the last case assigned to this suite for(row = lastrow+1; row <= totcases; row++) { // Only consider unassigned block cases if(gettablenum(CaseInfo,row,CI_ARRIVALTIME) == 0 && comparetext("Block", gettablestr(CaseInfo,row,CI_CASETYPE))) { // Only consider cases with no suite assignment or an explicit assignment to this suite if(stringlen(gettablestr(CaseInfo,row,CI_SUITE)) == 0 || comparetext(gettablestr(CaseInfo,row,CI_SUITE), roomname)) { // If this case will finish (surgery only, not cleaning) before the quiting time of the OR, then record the case in this // suite's lastcasearray; otherwise stop checking for block cases to assign to this suite because no more block cases will fit. if(lastcasefinishtime + gettablenum(CaseInfo,row,CI_EXPECTEDSURGERY) < orquittime) { // Record current row number of the sorted CaseInfo table in the lastcasearray lastcasearray[roomnr] = row; // Record the arrival time for the current case in the CaseInfo table as the lastcasefinishtime minus this case's PreOp time. double arrivaltime = lastcasefinishtime - gettablenum(CaseInfo,row,CI_PREOP); settablenum(CaseInfo,row,CI_ARRIVALTIME, arrivaltime); // Record the assigned room number in the CaseInfo table for cases without room number pre-assigned by the user. settablestr(CaseInfo,row,CI_SUITE, roomname); // Update earliest scheduled arrival time if necessary if(arrivaltime < earliestarrivaltime) earliestarrivaltime = arrivaltime; } // Else if the case cannot fit in this suite's schedule, then set fullarray to 1 for this suite else { fullarray[roomnr] = 1; // Check if all rooms are finished int totfinished = 0; for(index = 1; index <= totrooms; index++) totfinished += fullarray[index]; if(totfinished == totrooms) allfinished = 1; } // Break out of the for loop after finding a valid block case to assign to this suite whether it was assigned or not. If the // case was assigned, then another case may be assigned during the next iteration of the while loop. If no case was assigned, // then there is no need to continue checking for another case to assign to this suite during subsequent iterations of the // while loop because there simply wont be any other cases following this one that will fit in the schedule if this one didn't. break; } //end of if no suite or matching suite } //end of if block case } //end of cases loop // If the cases loop made it to the end of the CaseInfo table, then we are finished assigning block cases to this suite. if(row >= totcases) { fullarray[roomnr] = 1; // Check if all rooms are finished int totfinished = 0; for(index = 1; index <= totrooms; index++) totfinished += fullarray[index]; if(totfinished == totrooms) allfinished = 1; } } //end of suite loop } //end while loop for block cases // Reinitialize the fullarray to zeros indicating we are not finished assigning add-on cases to any of the rooms. // The lastcasearray will be left alone so that it will still contain records of the last assigned block cases to each suite. for(index = 1; index <= totrooms; index++) fullarray[index] = 0; // A flag indicating that the last case assigned was a block case int lastcasewasblock = 1; // Reset allfinished to 0 allfinished = 0; // This while loop assigns add-on cases to the suite schedules where possible countdown = 1000; while(!allfinished && countdown > 0) { countdown--; // For each OR suite, for(roomnr = 1; roomnr <= totrooms; roomnr++) { // If the schedule is already full for this suite, then skip and continue with the other rooms if(fullarray[roomnr]) continue; roomname = getname(getlocfromarea(ORArea, roomnr)); // The row number in the sorted CaseInfo table of the last case assigned to this suite lastrow = lastcasearray[roomnr]; // If no cases have been assigned to this suite yet, then set the lastcasefinishtime to the starting time for the OR if(lastrow == 0) lastcasefinishtime = orstarttime; else // Otherwise, calculate the finish time for the case: scheduled arrival time plus PreOP time plus planned surgery time plus suite cleaning time lastcasefinishtime = gettablenum(CaseInfo,lastrow,CI_ARRIVALTIME) + gettablenum(CaseInfo,lastrow,CI_PREOP) + gettablenum(CaseInfo,lastrow,CI_EXPECTEDSURGERY) + gettablenum(CaseInfo,lastrow,CI_EXPECTEDTURNOVER); // For each case (row) in the CaseInfo table starting with the row after the last add-on case assigned to this suite. If the last case assigned // was a block case, then start at row 1, because there's no guarantee that add-on cases will always follow the last block case found. int startrow = lastrow + 1; if(lastcasewasblock) startrow = 1; for(row = startrow; row <= totcases; row++) { // Only consider unassigned add-on cases if(gettablenum(CaseInfo,row,CI_ARRIVALTIME) == 0 && comparetext("Addon", gettablestr(CaseInfo,row,CI_CASETYPE))) { // Only consider cases with no suite assignment or an explicit assignment to this suite if(stringlen(gettablestr(CaseInfo,row,CI_SUITE)) == 0 || comparetext(gettablestr(CaseInfo,row,CI_SUITE), roomname)) { // If this case will finish (surgery only, not cleaning) before the quiting time of the OR, then record the case in this // suite's lastcasearray; otherwise stop checking for add-on cases to assign to this suite because no more cases will fit. if(lastcasefinishtime + gettablenum(CaseInfo,row,CI_EXPECTEDSURGERY) < orquittime) { // Record current row number of the sorted CaseInfo table in the lastcasearray lastcasearray[roomnr] = row; // Record the arrival time for the current case in the CaseInfo table as the lastcasefinishtime minus this case's PreOp time. double arrivaltime = lastcasefinishtime - gettablenum(CaseInfo,row,CI_PREOP); settablenum(CaseInfo,row,CI_ARRIVALTIME, arrivaltime); // Record the assigned room number in the CaseInfo table for cases without room number pre-assigned by the user. settablestr(CaseInfo,row,CI_SUITE, roomname); // Update earliest scheduled arrival time if necessary if(arrivaltime < earliestarrivaltime) earliestarrivaltime = arrivaltime; } // Else if the case cannot fit in this suite's schedule, then set fullarray to 1 for this suite else { fullarray[roomnr] = 1; // Check if all rooms are finished int totfinished = 0; for(index = 1; index <= totrooms; index++) totfinished += fullarray[index]; if(totfinished == totrooms) allfinished = 1; } // Break out of the for loop after finding a valid add-on case to assign to this suite whether it was assigned or not. If the // case was assigned, then another case may be assigned during the next iteration of the while loop. If no case was assigned, // then there is no need to continue checking for another case to assign to this suite during subsequent iterations of the // while loop because there simply wont be any other cases following this one that will fit in the schedule if this one didn't. break; } //end of if no suite or matching suite } //end of if add-on case } //end of cases loop // If the cases loop made it to the end of the CaseInfo table, then we are finished assigning cases to this suite. if(row >= totcases) { fullarray[roomnr] = 1; // Check if all rooms are finished int totfinished = 0; for(index = 1; index <= totrooms; index++) totfinished += fullarray[index]; if(totfinished == totrooms) allfinished = 1; } } //end of suite loop // Set flag to false because now the last case considered was an add-on case, not a block case lastcasewasblock = 0; } //end while loop for add-on cases // Go through the CaseInfo table and change all blank arrival times to a large number (i.e. 999999) so that when // the table is sorted from earliest to latest arrival time, the cases not associated with a patient arrival will // be moved to the end of the table. for(row = 1; row <= totcases; row++) { if(gettablenum(CaseInfo,row,CI_ARRIVALTIME) == 0) settablenum(CaseInfo,row,CI_ARRIVALTIME, 999999); } // Now sort the CaseInfo table according to arrival time multisorttable(CaseInfo, CI_ARRIVALTIME); } // Populate the Patient Classification table in the Track Manager and the Appointments table in the PatientArrivals // object with information recorded in the CaseInfo table. treenode appointments = getvarnode(InPatientArrivals, VAR_ScheduledArrivals); treenode templaterow = node("MAIN:/project/modules/HC/library/Patients/Arrivals>PatientArrivals>variables/ScheduledArrivals/1"); clearcontents(appointments); treenode classificationtable = getvarnode(PatientBin, "PatientClassifications"); int totcols = gettablecols(classificationtable); settablesize(classificationtable, 1+totcases, totcols); int numappts = 0; for(row = 1; row <= totcases; row++) { double arrivaltime = gettablenum(CaseInfo,row,CI_ARRIVALTIME); if(arrivaltime < 999999) { // Count the number patient arrivals numappts++; // Add appointment to the Appointments table of PatientArrivals object createcopy(templaterow, appointments); treenode rownode = last(appointments); setname(rownode, numtostring(row,0,0)); //row name setnodenum(first(rank(rownode,1)), arrivaltime); //appointment time in minutes setnodestr(rank(rownode,1), timeminutestostring(arrivaltime)); //appointment time as string setnodestr(rank(rownode,2), numtostring(row,0,0)); //appointment PCI (row number in CaseInfo table) // Record cases in the Patient Classification table as unique PCI's (PCI# = row number in the CaseInfo table) setname(rank(classificationtable, row+1), concat("PCI ", numtostring(row,0,0))); settablestr(classificationtable, row+1, 1, gettablestr(CaseInfo,row,CI_PATIENTTYPE)); //Track settablestr(classificationtable, row+1, 2, concat(gettablestr(CaseInfo,row,CI_PATIENTTYPE), "_Visuals")); //Visuals settablenum(classificationtable, row+1, 3, 60); //Speed settablenum(classificationtable, row+1, 4, 1); //Acuity settablenum(classificationtable, row+1, 5, 0); //ApptTime settablenum(classificationtable, row+1, 6, 0); //ArrivalTime settablenum(classificationtable, row+1, 7, gettablenum(CaseInfo,row,CI_CASE)); //Case } //Name the rows of the CaseInfo table in numerical order setname(rank(CaseInfo, row), concat("Row ", numtostring(row,0,0))); //If a surgeon wasn't specifically named, then set staff requirement to SurgeonGroup treenode surgeon = gettablecell(CaseInfo, row, CI_SURGEON); if(stringlen(getnodestr(surgeon)) == 0) setnodestr(surgeon, "SurgeonGroup"); } // Resize the CaseInfo and Patient Classification tables so it only contains scheduled cases settablesize(CaseInfo, numappts, ncols(CaseInfo)); settablesize(classificationtable, 2+numappts, totcols); // +2 because 1 for the datatype row and 1 for the EmerSurgAdmit patient // Add the EmerSurgAdmit track's PCI to the end of the Patient Classification table setname(rank(classificationtable, numappts+2), concat("PCI ", numtostring(numappts+1,0,0))); settablestr(classificationtable, numappts+2, 1, "EmerSurgAdmit"); //Track settablestr(classificationtable, numappts+2, 2, "EmerSurgAdmit_Visuals"); //Visuals settablenum(classificationtable, numappts+2, 3, 60); //Speed settablenum(classificationtable, numappts+2, 4, 5); //Acuity settablenum(classificationtable, numappts+2, 5, 0); //ApptTime settablenum(classificationtable, numappts+2, 6, 0); //ArrivalTime settablenum(classificationtable, numappts+2, 7, 0); //Case // Write the EmerSurgAdmit track's PCI number in the Hourly Arrivals, Appointments and Custom Arrivals tabs setnodestr(node("MAIN:/project/model/ERPatientArrivals1>variables/ScheduledArrivals/1/ PCI"), numtostring(numappts+1,0,0)); setnodestr(node("MAIN:/project/model/ERPatientArrivals1>variables/CustomArrivalsTable/1/PCI"), numtostring(numappts+1,0,0)); setnodestr(node("MAIN:/project/model/ERPatientArrivals1>variables/PatientClassification"), numtostring(numappts+1,0,0)); setnodestr(node("MAIN:/project/model/ERPatientArrivals1>variables/InterarrivalTimes/1/pci"), numtostring(numappts+1,0,0)); // Update any open views that reference the number of arrivals so that if the user apply the guis, it won't delete data! // Update the clock time for start of simulation (OR Start Time minus the maximum number of Alloted PreOp Time for any given case) if(earliestarrivaltime == VERYLARGENUMBER) earliestarrivaltime = 0; // Set the model start time to the earliest arrival time rounded down to the nearest 15 minutes. int newstarttime = trunc(earliestarrivaltime/15) * 15; treenode startday = node("/startday",StartTime); treenode starthour = node("/starthour",StartTime); treenode startampm = node("/startampm",StartTime); treenode starthours = node("/starthours",StartTime); treenode totalstartminutes = node("/totalstartminutes",StartTime); int remaingminutes = newstarttime; int day = remaingminutes / 1440 + 1; remaingminutes -= ((day - 1) * 1440); int hour = remaingminutes / 60; remaingminutes -= (hour * 60); set(startday,day); set(startampm,(hour < 12 ? 1 : 2)); set(starthour,(hour < 12 ? hour : hour - 12)); set(starthours,hour); set(totalstartminutes,newstarttime); // set the warmup time to match the start time treenode execwarmupnode = node("MAIN:/project/exec/warmup", c); treenode warmtimenode = node("/Tools/Experimenter/WarmupTime", model()); treenode subnode = first(warmtimenode); if(stringlen(gets(warmtimenode)) > 0) { if(get(subnode) < newstarttime) { sets(warmtimenode, ""); set(subnode, 0); // Clock Time set(execwarmupnode, 0); // Sim time (offset from the start time) } } repaintall();