Surgeries scheduling

Principles learned

  • Add multiple list decision variables

  • Use the find operator

  • Order interval decision variables by pairing them up with a list variable

Problem

../_images/fjsp.svg

A hospital, with a fixed amount of rooms, has a set of surgeries to be done. Each surgery has a given processing time and a given number of nurses needed. A surgery can only start when the room and the number of nurses needed are available. The nurses’ shifts have an earliest start and latest end. They also have a maximum duration.

The goal is to find a sequence of surgeries that minimizes the makespan: the time when all surgeries have been processed.

Download the example


Data

The format of the data files is as follows:

  • First line: number of rooms, number of nurses, number of surgeries

  • Second line: minimum starting time of each surgery (in hours)

  • Third line: maximum ending time of each surgery (in hours)

  • Fourth line: for each surgery, its duration (in minutes)

  • Fifth line: for each surgery, the number of nurses needed

  • Sixth line: for each nurse, the earliest possible shift (in hours)

  • Seventh line: for each nurse, the latest possible end of the shift (in hours)

  • Eigth line: the maximum duration of a shift (in hours)

  • The following lines:

    • For each surgery (representing a line), for each room (representing a

      column) 0 if the room is compatible with the surgery, 1 otherwise.

Program

We use interval decision variables to model the time ranges for each surgery. The length of these intervals is constrained by the processing time required for each surgery.

In addition to interval decision variables representing the surgery time ranges, we also use list decision variables. These lists represent the ordering of surgeries within each room.

Every surgery must be assigned to a room, and the partition() operator on the list ensures that each surgery is allocated to exactly one room.

The find() operator takes an array of lists and an integer value as arguments. It returns the position of the list containing the specified value within the array, if such a list exists. Here, this operator is used to identify the ID of the room where each surgery will occur.

To ensure that no more than one surgery is performed in a room at the same time, we impose a time constraint: a surgery cannot start in a room until the previous surgery in that room has finished.

For each nurse, we create a list to represent the surgeries they are responsible for, in the order they will perform them. Nurses are restricted to starting only after their earliest possible shift and finishing before their latest possible shift. Additionally, they cannot work longer than the maximum duration of a shift. A nurse is also limited to working on one surgery at a time, which translates to the constraint that the start time of any surgery assigned to a nurse must be after the end of their previous surgery.

Each surgery has a minimum nurse requirement. This constraint is modeled by summing, for each surgery s, the lists of nurses’ assigned surgeries in which s appears. The contains() operator takes a list and an integer value as arguments and returns 1 if the integer value is in the list, and 0 otherwise. Therefore, if a nurse is assigned to surgery s, the operator will return 1. Summing the results of this operator across all nurses provides the total number of nurses assigned to a surgery.

The objective is to minimize the makespan, defined as the time at which all surgeries are completed.

Execution:
hexaly surgeries_scheduling.hxm inFileName=instances/instancesurgery.txt [outFileName=] [hxTimeLimit=]
use io;

function input() {
    local usage = "Usage: hexaly surgeries_scheduling.hxm inFileName=instanceFile"
            + " [outFileName=outputFile] [hxTimeLimit=timeLimit]";

    if (inFileName == nil) throw usage;

    inFile = io.openRead(inFileName);
    // Number of rooms
    numRooms = inFile.readInt();
    // Number of nurses
    numNurses = inFile.readInt();
    // Number of surgeries
    numSurgeries = inFile.readInt();
    // Minimum start of each surgery
    for [s in 0...numSurgeries] {
        minStart[s] = inFile.readInt();
    }
    // Maximum end of each surgery
    for [s in 0...numSurgeries] {
        maxEnd[s] = inFile.readInt() * 60;
    }
    // Duration of each surgery
    for [s in 0...numSurgeries] {
        duration[s] = inFile.readInt();
    }
    // Number of nurses needed for each surgery
    for [s in 0...numSurgeries] {
        neededNurses[s] = inFile.readInt();
    }
    // Earliest starting shift for each nurse
    for [n in 0...numNurses] {
        shiftEarliestStart[n] = inFile.readInt() * 60;
    }
    // Latest ending shift for each nurse
    for [n in 0...numNurses] {
        shiftLatestEnd[n] = inFile.readInt() * 60;
    }
    // Maximum duration of each nurse's shift
    maxShiftDuration = inFile.readInt() * 60;

    // Incompatible rooms for each surgery
    incompatibleRooms = {};
    for [s in 0...numSurgeries] {
        for [r in 0...numRooms] {
            incompatibleRooms[s][r] = inFile.readInt();
        }
    }
}

/* Declare the optimization model */
function model() {
    // Sequence of surgeries for each room
    surgeryOrder[r in 0...numRooms] <- list(numSurgeries);

    // Each surgery is scheduled in a room
    constraint partition[r in 0...numRooms](surgeryOrder[r]);

    // Only compatible rooms can be selected for a surgery
    for [s in 0...numSurgeries][r in incompatibleRooms[s]]
        constraint contains(surgeryOrder[r], s) == 0;

    // For each surgery, the selected room
    // This variable is only used to export the solution
    selectedRoom[s in 0...numSurgeries] <- find(surgeryOrder, s);

    // Interval decisions: time range of each surgery
    // Each surgery cannot start before and end after a certain time
    surgeries[s in 0...numSurgeries] <- interval(minStart[s], maxEnd[s]);

    for [s in 0...numSurgeries] {
        // Each surgery has a specific duration 
        constraint length(surgeries[s]) == duration[s];
    }
    // A room can only have one surgery at a time
    for [r in 0...numRooms] {
        constraint and(0...count(surgeryOrder[r])-1, 
                s => start(surgeries[surgeryOrder[r][s+1]]) 
                    >= end(surgeries[surgeryOrder[r][s]]));
    }

    // Each surgery needs a specific amount of nurse
    for [n in 0...numNurses] {
        // Sequence of surgery for each nurse
        nurseOrder[n] <- list(numSurgeries);
        firstSurgeryStart <- count(nurseOrder[n]) > 0 
                                ? start(surgeries[nurseOrder[n][0]]) 
                                : shiftEarliestStart[n];
        lastSurgeryEnd <- count(nurseOrder[n]) > 0 
                            ? end(surgeries[nurseOrder[n][count(nurseOrder[n])-1]]) 
                            : shiftEarliestStart[n];
        
        // Each nurse has an earliest starting shift and latest ending shift to be respected
        constraint firstSurgeryStart >= shiftEarliestStart[n];
        constraint lastSurgeryEnd <= shiftLatestEnd[n];

        // Each nurse cannot work more than a certain amount of hours in a row
        constraint lastSurgeryEnd - firstSurgeryStart <= maxShiftDuration;
        
        // Each nurse can only be at one operation at a time and stays all along the surgery
        constraint and(0...count(nurseOrder[n])-1, 
                    s => surgeries[nurseOrder[n][s+1]] > surgeries[nurseOrder[n][s]]);
    }

    // Each surgery needs a certain amount of nurses
    for [s in 0...numSurgeries] {
        constraint sum(0...numNurses, n => contains(nurseOrder[n], s)) >= neededNurses[s];
    }
    
    // Minimize the makespan: end of the last surgery
    makespan <- max[s in 0...numSurgeries](end(surgeries[s]));
    minimize makespan;

}

/* Parameterize the solver */
function param() {
    if (hxTimeLimit == nil) hxTimeLimit = 20;
}

/* Write the solution in a file with the following format:
- for each surgery, the selected room, the start and end dates 
  the nurses working on this operation*/

function output() {
    if (outFileName != nil) {
        outFile = io.openWrite(outFileName);
        println("Solution written in file ", outFileName);
        listNurses = {};
        for [n in 0...numNurses] {
            surgNurse = nurseOrder[n].value;
            for [s in surgNurse] {
                if (listNurses[s] == nil) {
                    listNurses[s] = {n};
                } else {
                    listNurses[s].add(n);
                }
            }
        }
        for [s in 0...numSurgeries] {
            outFile.print(s, "\t");
            outFile.print(selectedRoom[s].value, "\t");
            outFile.print(surgeries[s].value.start, "\t");
            outFile.print(surgeries[s].value.end, "\t");
            outFile.print(listNurses[s]);
            outFile.println();
        }
    }
}
Execution (Windows)
set PYTHONPATH=%HX_HOME%\bin\python
python surgeries_scheduling.py instances\instancesurgery.txt
Execution (Linux)
export PYTHONPATH=/opt/hexaly_13_5/bin/python
python surgeries_scheduling.py instances/instancesurgery.txt
import hexaly.optimizer
import sys

def read_instance(filename):
    with open(filename) as f:
        lines = f.readlines()

    first_line = lines[0].split()
    # Number of rooms
    num_rooms = int(first_line[0])
    # Number of nurses
    num_nurses = int(first_line[1])
    # Number of surgeries
    num_surgeries = int(first_line[2])

    # Minimum start of each surgery
    line_min_start = lines[1].split()
    min_start = [int(line_min_start[s]) * 60 for s in range(num_surgeries)]

    # Maximum end of each surgery
    line_max_end = lines[2].split()
    max_end = [int(line_max_end[s]) * 60 for s in range(num_surgeries)]

    # Duration of each surgery
    line_duration = lines[3].split()
    duration = [int(line_duration[s]) for s in range(num_surgeries)]

    # Number of nurses needed for each surgery
    line_nurse_needed = lines[4].split()
    needed_nurses = [int(line_nurse_needed[s]) for s in range(num_surgeries)]

    # Earliest starting shift for each nurse
    line_earliest_shift = lines[5].split()
    shift_earliest_start = [int(line_earliest_shift[s]) * 60 for s in range(num_nurses)]

    # Latest ending shift for each nurse
    line_latest_shift = lines[6].split()
    shift_latest_end = [int(line_latest_shift[s]) * 60 for s in range(num_nurses)]

    # Maximum duration of each nurse's shift
    max_shift_duration = int(lines[7].split()[0]) * 60
    
    #Incompatible rooms for each surgery
    incompatible_rooms = [[0 for r in range(num_rooms)] for s in range(num_surgeries)]
    for s in range(num_surgeries):
        line = lines[8+s].split()
        for r in range(num_rooms):
            incompatible_rooms[s][r] = int(line[r])

    return (num_rooms, num_nurses, num_surgeries, min_start, max_end, 
            needed_nurses, shift_earliest_start, shift_latest_end, 
            max_shift_duration, incompatible_rooms, duration)

def main(instance_file, output_file, time_limit):
    num_rooms, num_nurses, num_surgeries, min_start, max_end, needed_nurses, \
    shift_earliest_start, shift_latest_end, max_shift_duration, \
    incompatible_rooms, duration = read_instance(instance_file)
    
    with hexaly.optimizer.HexalyOptimizer() as optimizer:
        #
        # Declare the optimization model
        #
        model = optimizer.model

        # Sequence of surgery for each room
        surgery_order = [model.list(num_surgeries) for _ in range(num_rooms)]
        rooms = model.array(surgery_order)

        # Each surgery is scheduled in a room
        model.constraint(model.partition(rooms))

        # Only compatible rooms can be selected for a surgery
        for s in range(num_surgeries):
            for r in incompatible_rooms[s]:
                model.constraint(model.contains(surgery_order[r], s) == 0)

        # For each surgery, the selected room
        # This variable is only used to export the solution
        selected_room = [model.find(rooms, s) for s in range(num_surgeries)]

        # Interval decisions: time range of each surgery
        # Each surgery cannot start before and end after a certain time
        surgeries = [model.interval(min_start[s], max_end[s]) for s in range(num_surgeries)]

        for s in range(num_surgeries):
            # Each surgery has a specific duration
            model.constraint(model.length(surgeries[s]) == duration[s])

        surgery_array = model.array(surgeries)

        # A room can only have one surgery at a time
        for r in range(num_rooms):
            sequence = surgery_order[r]
            sequence_lambda = model.lambda_function(
                lambda s: surgery_array[sequence[s]] < surgery_array[sequence[s + 1]])
            model.constraint(
                model.and_(model.range(0, model.count(sequence) - 1), sequence_lambda)
            )

        # Each surgery needs a specific amount of nurse
        nurse_order = [model.list(num_surgeries) for _ in range(num_nurses)]

        for n in range(num_nurses):
            # Each nurse has an earliest starting shift and latest ending shift to be respected
            sequence = nurse_order[n]
            first_surgery_start = model.iif( 
                model.count(sequence) > 0, 
                model.start(surgery_array[sequence[0]]),
                shift_earliest_start[n]
            )
            last_surgery_end = model.iif(
                model.count(sequence) > 0, 
                model.end(surgery_array[sequence[model.count(sequence)-1]]),
                shift_earliest_start[n]
            )
            
            model.constraint(first_surgery_start >= shift_earliest_start[n])
            model.constraint(last_surgery_end <= shift_latest_end[n])

            # Each nurse cannot work more than a certain amount of hours
            model.constraint(last_surgery_end - first_surgery_start <= max_shift_duration)
            
            # Each nurse can only be at one operation at a time and stays all along the surgery
            sequence_lambda = model.lambda_function(
                lambda s: surgery_array[sequence[s]] < surgery_array[sequence[s + 1]])
            model.constraint(model.and_(
                model.range(0, model.count(sequence) - 1), sequence_lambda))
        
        # Each surgery needs a certain amount of nurses 
        nurse_order_array = model.array(nurse_order)
        for s in range(num_surgeries):
            model.constraint(
                model.sum(
                    model.range(0, num_nurses), 
                    model.lambda_function(lambda n : model.contains(nurse_order_array[n], s))
                )
                >= needed_nurses[s]
            )

        # Minimize the makespan: end of the last task
        makespan = model.max([model.end(surgeries[s]) for s in range(num_surgeries)])
        model.minimize(makespan)

        model.close()

        # Parameterize the optimizer
        optimizer.param.time_limit = time_limit

        optimizer.solve()

        # Write the solution in a file with the following format:
        # - for each surgery, the selected room, the start and end dates, 
        # the nurses working on this operation
        if output_file != None:
            with open(output_file, "w") as f:
                print("Solution written in file", output_file)
                list_nurses = {}
                for n in range(num_nurses):
                    surg_nurse = nurse_order[n].value
                    for s in surg_nurse:
                        if s not in list_nurses:
                            list_nurses[s] = [n]
                        else:
                            list_nurses[s].append(n)
                for s in range(num_surgeries):
                    f.write(str(s) + "\t"
                        + "\t" + str(selected_room[s].value)
                        + "\t" + str(surgeries[s].value.start())
                        + "\t" + str(surgeries[s].value.end()) 
                        + "\t" + str(list_nurses[s]) + "\n")



if __name__ == '__main__':
    if len(sys.argv) < 2:
        print("Usage: python surgeries_scheduling.py instance_file [output_file] [time_limit]")
        sys.exit(1)

    instance_file = sys.argv[1]
    output_file = sys.argv[2] if len(sys.argv) >= 3 else None
    time_limit = int(sys.argv[3]) if len(sys.argv) >= 4 else 20
    main(instance_file, output_file, time_limit)


Compilation / Execution (Windows)
cl /EHsc surgeries_scheduling.cpp -I%HX_HOME%\include /link %HX_HOME%\bin\hexaly135.lib
surgeries_scheduling instances\instancesurgery.txt
Compilation / Execution (Linux)
g++ surgeries_scheduling.cpp -I/opt/hexaly_13_5/include -lhexaly135 -lpthread -o surgeries_scheduling
./surgeries_scheduling instances/instancesurgery.txt
#include "optimizer/hexalyoptimizer.h"
#include <algorithm>
#include <fstream>
#include <iostream>
#include <limits>
#include <numeric>
#include <vector>

using namespace hexaly;

class SurgeriesScheduling {
private:
    // Number of surgeries 
    int numSurgeries;
    // Number of rooms
    int numRooms;
    // Number of nurses
    int numNurses;
    // Minimum start of each surgery
    std::vector<int> minStart;
    // Maximum end of each surgery
    std::vector<int> maxEnd;
    // Duration of each surgery
    std::vector<int> duration;
    // Number of nurses needed for each surgery
    std::vector<int> neededNurses;
    // Earliest starting shift for each nurse
    std::vector<int> shiftEarliestStart;
    // Latest ending shift for each nurse
    std::vector<int> shiftLatestEnd;
    // Maximum duration of each nurse's shift
    int maxShiftDuration;
    // Incompatible rooms for each surgery
    std::vector<std::vector<int>> incompatibleRooms;

    // Hexaly Optimizer
    HexalyOptimizer optimizer;
    // Decision variables: time range of each surgery
    std::vector<HxExpression> surgeries;
    // Decision variables: sequence of surgery on each room
    std::vector<HxExpression> surgeryOrder;
    // For each surgery, the selected room
    // This variable is only used to export the solution
    std::vector<HxExpression> selectedRoom;
    // Decision variables: for each nurse, their surgeries order
    std::vector<HxExpression> nurseOrder;
    // Objective = minimize the makespan: end of the last surgery
    HxExpression makespan;

public:
    SurgeriesScheduling() : optimizer() {}

    void readInstance(const std::string& fileName) {
        std::ifstream infile;
        infile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
        infile.open(fileName.c_str());

        infile >> numRooms;
        infile >> numNurses;
        infile >> numSurgeries;

        minStart.resize(numSurgeries);
        for (int s = 0; s < numSurgeries; ++s) {
            int start;
            infile >> start;
            minStart[s] = start * 60;
        }
        
        maxEnd.resize(numSurgeries);
        for (int s = 0; s < numSurgeries; ++s) {
            int end;
            infile >> end;
            maxEnd[s] = end * 60;
        }
        
        duration.resize(numSurgeries);
        for (int s = 0; s < numSurgeries; ++s)
            infile >> duration[s];
        
        neededNurses.resize(numSurgeries);
        for (int s = 0; s < numSurgeries; ++s)
            infile >> neededNurses[s];

        shiftEarliestStart.resize(numNurses);
        for (int s = 0; s < numNurses; ++s) {
            int shift;
            infile >> shift;
            shiftEarliestStart[s] = shift * 60 ;
        }

        shiftLatestEnd.resize(numNurses);
        for (int s = 0; s < numNurses; ++s) {
            int shift;
            infile >> shift;
            shiftLatestEnd[s] = shift * 60;
        }
        int maxShiftDurationHeure;
        infile >> maxShiftDurationHeure;
        maxShiftDuration = maxShiftDurationHeure * 60;

        incompatibleRooms.resize(numSurgeries);
        for (int s = 0; s < numSurgeries; ++s) {
            incompatibleRooms[s].resize(numRooms);
            for (int r = 0; r < numRooms; ++r) {
                int room;
                infile >> room;
                incompatibleRooms[s][r] = room;
            }
        }
        infile.close();
    }

    void solve(int timeLimit) {
        // Declare the optimization model
        HxModel model = optimizer.getModel();

        // Sequence of surgery on each room
        surgeryOrder.resize(numRooms);
        HxExpression room = model.array();
        for (unsigned int r = 0; r < numRooms; ++r) {
            surgeryOrder[r] = model.listVar(numSurgeries);
            room.addOperand(surgeryOrder[r]);
        }

        // Each surgery is scheduled in a room
        model.constraint(model.partition(room));

        // Only compatible rooms can be selected for a surgery
        for (int s = 0; s < numSurgeries; ++s) {
            for (int r : incompatibleRooms[s]) {
                model.constraint(model.contains(surgeryOrder[r], s) == 0);
            }
        }

        // For each surgery, the selected room
        selectedRoom.resize(numSurgeries);
        for (int s = 0; s < numSurgeries; ++s) {
            selectedRoom[s] = model.find(room, s);
        }

        // Interval decisions: time range of each surgery
        surgeries.resize(numSurgeries);

        for (int s = 0; s < numSurgeries; ++s) {
            // Each surgery cannot start before and end after a certain time
            surgeries[s] = model.intervalVar(minStart[s], maxEnd[s]);

            // Each surgery has a specific duration
            model.constraint(model.length(surgeries[s]) == duration[s]);
        }
        HxExpression surgeryArray = model.array(surgeries.begin(), surgeries.end());

        // A room can only have one surgery at a time
        for (int r = 0; r < numRooms; ++r) {
            HxExpression sequence = surgeryOrder[r];
            HxExpression sequenceLambda = model.createLambdaFunction([&](HxExpression s) { 
                return surgeryArray[sequence[s]] < surgeryArray[sequence[s + 1]]; });
            model.constraint(model.and_(model.range(0, model.count(sequence) - 1), sequenceLambda));
        }

        // Sequence of surgery for each nurse
        nurseOrder.resize(numNurses);
        
        for (int n = 0; n < numNurses; ++n) {
            nurseOrder[n] = model.listVar(numSurgeries);
            HxExpression sequence = nurseOrder[n];
            HxExpression firstSurgeryStart = model.iif(
                model.count(sequence) > 0, 
                model.start(surgeryArray[sequence[0]]),
                shiftEarliestStart[n]);
            HxExpression lastSurgeryEnd = model.iif(
                model.count(sequence) > 0,
                model.end(surgeryArray[sequence[model.count(sequence)-1]]),
                shiftEarliestStart[n]);

            // Each nurse has an earliest starting shift and latest ending shift to be respected
            model.constraint(firstSurgeryStart >= shiftEarliestStart[n]);
            model.constraint(lastSurgeryEnd <= shiftLatestEnd[n]);

            // Each nurse cannot work more than a certain amount of hours in a row
            model.constraint(lastSurgeryEnd - firstSurgeryStart <= maxShiftDuration);

            // Each nurse can only be at one operation at a time and stays all along the surgery
            HxExpression sequenceLambda = model.createLambdaFunction([&](HxExpression s) { 
                return surgeryArray[sequence[s]] < surgeryArray[sequence[s + 1]]; });
            model.constraint(model.and_(model.range(0, model.count(sequence) - 1), sequenceLambda));
        }
        
        HxExpression nurseOrderArray = model.array(nurseOrder.begin(), nurseOrder.end());
        
        // Each surgery needs a specific amount of nurse
        for (int s = 0; s < numSurgeries; ++s) {
            model.constraint(model.sum(model.range(0, numNurses), 
                    model.createLambdaFunction([&](HxExpression n) { 
                        return model.contains(nurseOrderArray[n], s);})) 
                >= neededNurses[s]);
        } 

        // Minimize the makespan: end of the last surgery
        makespan = model.max();
        for (int s = 0; s < numSurgeries; ++s) {
            makespan.addOperand(model.end(surgeries[s]));
        }
        model.minimize(makespan);

        model.close();

        // Parameterize the optimizer
        optimizer.getParam().setTimeLimit(timeLimit);

        optimizer.solve();
    }

    /* Write the solution in a file with the following format:
     *  - for each surgery, the selected room, the start and end dates, 
          the nurses working on this operation */
    void writeSolution(const std::string& fileName) {
        std::ofstream outfile(fileName.c_str());
        if (!outfile.is_open()) {
            std::cerr << "File " << fileName << " cannot be opened." << std::endl;
            exit(1);
        }
        std::cout << "Solution written in file " << fileName << std::endl;

        std::vector<std::vector<int>> listNurses;
        listNurses.resize(numSurgeries);
        for (unsigned int n = 0; n < numNurses; ++n) {
            HxCollection surgNurse = nurseOrder[n].getCollectionValue();
            for (int s = 0; s < surgNurse.count(); ++s) {
                listNurses[surgNurse.get(s)].push_back(n);
            }
        }

        for (unsigned int s = 0; s < numSurgeries; ++s) {
            outfile << s << "\t" << selectedRoom[s].getValue() << "\t"
                    << surgeries[s].getIntervalValue().getStart() << "\t"
                    << surgeries[s].getIntervalValue().getEnd() << "\t";
            outfile << "[";
            bool first = true;
            for (int n : listNurses[s]) {
                if (!first) {
                    outfile << ", ";
                }
                outfile << n;
                first = false;
            }
            outfile << "]" << std::endl;
        }
        outfile.close();
    }
};

int main(int argc, char** argv) {
    if (argc < 2) {
        std::cout << "Usage: surgeries_scheduling instanceFile [outputFile] [timeLimit]" << std::endl;
        exit(1);
    }

    const char* instanceFile = argv[1];
    const char* outputFile = argc > 2 ? argv[2] : NULL;
    const char* strTimeLimit = argc > 3 ? argv[3] : "20";

    SurgeriesScheduling model;
    try {
        model.readInstance(instanceFile);
        const int timeLimit = atoi(strTimeLimit);
        model.solve(timeLimit);
        if (outputFile != NULL)
            model.writeSolution(outputFile);
        return 0;
    } catch (const std::exception& e) {
        std::cerr << "An error occurred: " << e.what() << std::endl;
        return 1;
    }
}
Compilation / Execution (Windows)
copy %HX_HOME%\bin\Hexaly.NET.dll .
csc SurgeriesScheduling.cs /reference:Hexaly.NET.dll
SurgeriesScheduling instances\instancesurgery.txt
using System;
using System.IO;
using System.Linq;
using Hexaly.Optimizer;
using System.Collections.Generic;

public class SurgeriesScheduling : IDisposable
{
    // Number of surgeries
    private int numSurgeries;
    // Number of rooms
    private int numRooms;
    // Number of nurses
    private int numNurses;
    // Minimum start of each surgery
    private int[] minStart;
    // Maximum end of each surgery
    private int[] maxEnd;
    // Duration of each surgery
    private int[] duration;
    // Number of nurses needed for each surgery
    private int [] neededNurses;
    // Earliest starting shift for each nurse
    private int [] shiftEarliestStart;
    // Latest ending shift for each nurse
    private int [] shiftLatestEnd;
    // Maximum duration of each nurse's shift
    private int maxShiftDuration;
    // Incompatible rooms for each surgery
    private int[][] incompatibleRooms;

    // Hexaly Optimizer
    private HexalyOptimizer optimizer;

    // Decision variables: time range of each surgery
    private HxExpression[] surgeries;

    // Decision variables: sequence of surgery on each room
    private HxExpression[] surgeryOrder;

    // For each surgery, the selected room
    // This variable is only used to export the solution
    private HxExpression[] selectedRoom;

    // Decision variables: the surgeries order of each nurse
    private HxExpression[] nurseOrder;

    // Objective = minimize the makespan: end of the last surgery
    private HxExpression makespan;

    public SurgeriesScheduling()
    {
        optimizer = new HexalyOptimizer();
    }

    public void ReadInstance(string fileName)
    {
        using (StreamReader input = new StreamReader(fileName))
        {
            char[] separators = new char[] { '\t', ' ' };
            string[] splitted = input
                .ReadLine()
                .Split(separators, StringSplitOptions.RemoveEmptyEntries);
            numRooms = int.Parse(splitted[0]);
            numNurses = int.Parse(splitted[1]);
            numSurgeries = int.Parse(splitted[2]);

            minStart = new int[numSurgeries];
            splitted = input
                .ReadLine()
                .Split(separators, StringSplitOptions.RemoveEmptyEntries);
            for (int s = 0; s < numSurgeries; ++s)
            {
                minStart[s] = int.Parse(splitted[s]) * 60;
            }

            maxEnd = new int[numSurgeries];
            splitted = input
                .ReadLine()
                .Split(separators, StringSplitOptions.RemoveEmptyEntries);
            for (int s = 0; s < numSurgeries; ++s)
            {
                maxEnd[s] = int.Parse(splitted[s]) * 60;
            }

            duration = new int[numSurgeries];
            splitted = input
                .ReadLine()
                .Split(separators, StringSplitOptions.RemoveEmptyEntries);
            for (int s = 0; s < numSurgeries; ++s)
            {
                duration[s] = int.Parse(splitted[s]);
            }

            neededNurses = new int[numSurgeries];
            splitted = input
                .ReadLine()
                .Split(separators, StringSplitOptions.RemoveEmptyEntries);
            for (int s = 0; s < numSurgeries; ++s)
            {
                neededNurses[s] = int.Parse(splitted[s]);
            }

            shiftEarliestStart = new int[numNurses];
            splitted = input
                .ReadLine()
                .Split(separators, StringSplitOptions.RemoveEmptyEntries);
            for (int n = 0; n < numNurses; ++n)
            {
                shiftEarliestStart[n] = int.Parse(splitted[n]) * 60;
            }

            shiftLatestEnd = new int[numNurses];
            splitted = input
                .ReadLine()
                .Split(separators, StringSplitOptions.RemoveEmptyEntries);
            for (int n = 0; n < numNurses; ++n)
            {
                shiftLatestEnd[n] = int.Parse(splitted[n]) * 60;
            }

            splitted = input
                .ReadLine()
                .Split(separators, StringSplitOptions.RemoveEmptyEntries);
            
            maxShiftDuration = int.Parse(splitted[0]) * 60;

            incompatibleRooms = new int[numSurgeries][];
            for (int s = 0; s < numSurgeries; ++s) 
            {
                splitted = input
                    .ReadLine()
                    .Split(separators, StringSplitOptions.RemoveEmptyEntries);
                incompatibleRooms[s] = new int[numRooms];
                for (int r = 0; r < numRooms; ++r) 
                {
                    incompatibleRooms[s][r] = int.Parse(splitted[r]);
                }
            } 
        }
    }

    public void Dispose()
    {
        optimizer.Dispose();
    }

    public void Solve(int timeLimit)
    {
        // Declare the optimization model
        HxModel model = optimizer.GetModel();

        // Sequence of surgery for each room
        surgeryOrder = new HxExpression[numRooms];
        HxExpression room = model.Array();
        for (int r = 0; r < numRooms; ++r)
        {
            surgeryOrder[r] = model.List(numSurgeries);
            room.AddOperand(surgeryOrder[r]);
        }

        // Each surgery is scheduled in a room
        model.Constraint(model.Partition(room));

        // Only compatible rooms can be selected for a surgery
        for (int s = 0; s < numSurgeries; ++s)
        {
            foreach (int r in incompatibleRooms[s]) 
            {
                model.Constraint(model.Contains(surgeryOrder[r], s) == 0);
            }
        }

        // For each surgery, the selected room
        selectedRoom = new HxExpression[numSurgeries];
        for (int s = 0; s < numSurgeries; ++s)
        {
            selectedRoom[s] = model.Find(room, s);
        }

        // Interval decisions: time range of each surgery
        surgeries = new HxExpression[numSurgeries];
        for (int s = 0; s < numSurgeries; ++s)
        {
            // Each surgery cannot start before and end after a certain time
            surgeries[s] = model.Interval(minStart[s], maxEnd[s]);

            // Each surgery has a specific duration
            model.Constraint(model.Length(surgeries[s]) == duration[s]);
        }
        HxExpression surgeryArray = model.Array(surgeries);

        // A room can only have one surgery at a time
        for (int r = 0; r < numRooms; ++r)
        {
            HxExpression sequence = surgeryOrder[r];
            HxExpression sequenceLambda = model.LambdaFunction(
                s => surgeryArray[sequence[s]] < surgeryArray[sequence[s + 1]]
            );
            model.Constraint(model.And(model.Range(0, model.Count(sequence) - 1), sequenceLambda));
        }

        // Sequence of surgery for each nurse
        nurseOrder = new HxExpression[numNurses];

        for (int n = 0; n < numNurses; ++n)
        {
            nurseOrder[n] = model.List(numSurgeries);
            HxExpression sequence = nurseOrder[n];
            HxExpression firstSurgeryStart = model.If(
                model.Count(sequence) > 0, 
                model.Start(surgeryArray[sequence[0]]),
                shiftEarliestStart[n]);
            HxExpression lastSurgeryEnd = model.If(
                model.Count(sequence) > 0, 
                model.End(surgeryArray[sequence[model.Count(sequence)-1]]),
                shiftEarliestStart[n]);
            // Each nurse has an earliest starting shift and latest ending shift to be respected
            model.Constraint(firstSurgeryStart >= shiftEarliestStart[n]);
            model.Constraint(lastSurgeryEnd <= shiftLatestEnd[n]);
            // Each nurse cannot work more than a certain amount of hours in a row
            model.Constraint(lastSurgeryEnd - firstSurgeryStart <= maxShiftDuration);
            // Each nurse can only be at one operation at a time and stays all along the surgery
            HxExpression sequenceLambda = model.LambdaFunction(
                s => surgeryArray[sequence[s]] < surgeryArray[sequence[s + 1]]
            );
            model.Constraint(model.And(model.Range(0, model.Count(sequence) - 1), sequenceLambda));
        }

        HxExpression nurseOrderArray = model.Array(nurseOrder);

        // Each surgery needs a specific amount of nurse
        for (int s = 0; s < numSurgeries; ++s)
        {
            model.Constraint(model.Sum(model.Range(0, numNurses), model.LambdaFunction(
                n => model.Contains(nurseOrderArray[n], s))) >= neededNurses[s]);
        }

        // Minimize the makespan: end of the last surgery
        makespan = model.Max();
        for (int s = 0; s < numSurgeries; ++s)
        {
            makespan.AddOperand(model.End(surgeries[s]));
        }
        model.Minimize(makespan);

        model.Close();

        // Parameterize the optimizer
        optimizer.GetParam().SetTimeLimit(timeLimit);

        optimizer.Solve();
    }

    /* Write the solution in a file with the following format:
     *  - for each surgery, the selected room, the start and end dates 
          the nurses working on this operation*/
    public void WriteSolution(string fileName)
    {
        using (StreamWriter output = new StreamWriter(fileName))
        {
            Console.WriteLine("Solution written in file " + fileName);
            List<List<int>> listNurses = new List<List<int>>();
            for (int s = 0; s < numSurgeries; ++s)
            {
                listNurses.Add(new List<int>());
            }
            for (int n = 0; n < numNurses; ++n)
            {
                HxCollection surgNurse = nurseOrder[n].GetCollectionValue();
                foreach (int s in surgNurse) 
                {
                    listNurses[s].Add(n);
                }
            }
            for (int s = 0; s < numSurgeries; ++s)
            {
                string nurseListString = "[" + string.Join(", ", listNurses[s]) + "]";
                output.WriteLine(
                    s
                        + "\t"
                        + selectedRoom[s].GetValue() 
                        + "\t"
                        + surgeries[s].GetIntervalValue().Start()
                        + "\t"
                        + surgeries[s].GetIntervalValue().End()
                        + "\t"
                        + nurseListString
                );
            }
        }
    }

    public static void Main(string[] args)
    {
        if (args.Length < 1)
        {
            Console.WriteLine("Usage: SurgeriesScheduling instanceFile [outputFile] [timeLimit]");
            System.Environment.Exit(1);
        }

        string instanceFile = args[0];
        string outputFile = args.Length > 1 ? args[1] : null;
        string strTimeLimit = args.Length > 2 ? args[2] : "20";

        using (SurgeriesScheduling model = new SurgeriesScheduling())
        {
            model.ReadInstance(instanceFile);
            model.Solve(int.Parse(strTimeLimit));
            if (outputFile != null)
                model.WriteSolution(outputFile);
        }
    }
}
Compilation / Execution (Windows)
javac SurgeriesScheduling.java -cp %HX_HOME%\bin\hexaly.jar
java -cp %HX_HOME%\bin\hexaly.jar;. SurgeriesScheduling instances\instancesurgery.txt
Compilation / Execution (Linux)
javac SurgeriesScheduling.java -cp /opt/hexaly_13_5/bin/hexaly.jar
java -cp /opt/hexaly_13_5/bin/hexaly.jar:. SurgeriesScheduling instances/instancesurgery.txt
import java.util.*;
import java.io.*;
import com.hexaly.optimizer.*;

public class SurgeriesScheduling {
    // Number of surgeries
    private int numSurgeries;
    // Number of rooms
    private int numRooms;
    // Number of nurses
    private int numNurses;
    // Minimum start of each surgery
    private int[] minStart;
    // Maximum end of each surgery
    private int[] maxEnd;
    // Duration of each surgery
    private int[] duration;
    // Number of nurses needed for each surgery
    private int [] neededNurses;
    // Earliest starting shift for each nurse
    private int [] shiftEarliestStart;
    // Latest ending shift for each nurse
    private int [] shiftLatestEnd;
    // Maximum duration of each nurse's shift
    private int maxShiftDuration;
    // Incompatible rooms for each surgery
    private int[][] incompatibleRooms;

    // Hexaly Optimizer
    final HexalyOptimizer optimizer;
    // Decision variables: time range of each surgery
    private HxExpression[] surgeries;
    // Decision variables: sequence of surgery on each room
    private HxExpression[] surgeryOrder;
    // For each surgery, the selected room
    // This variable is only used to export the solution
    private HxExpression[] selectedRoom;
    // Decision variables: the surgeries order of each nurse
    private HxExpression[] nurseOrder;
    // Objective = minimize the makespan: end of the last surgery
    private HxExpression makespan;

    public SurgeriesScheduling(HexalyOptimizer optimizer) throws IOException {
        this.optimizer = optimizer;
    }

    public void readInstance(String fileName) throws IOException {
        try (Scanner input = new Scanner(new File(fileName))) {
            input.useLocale(Locale.ROOT);
            numRooms = input.nextInt();
            numNurses = input.nextInt();
            numSurgeries = input.nextInt();

            minStart = new int[numSurgeries];
            for (int s = 0; s < numSurgeries; ++s) {
                minStart[s] = input.nextInt() * 60;
            }

            maxEnd = new int[numSurgeries];
            for (int s = 0; s < numSurgeries; ++s) {
                maxEnd[s] = input.nextInt() * 60;
            }

            duration = new int[numSurgeries];
            for (int s = 0; s < numSurgeries; ++s) {
                duration[s] = input.nextInt();
            }

            neededNurses = new int[numSurgeries];
            for (int s = 0; s < numSurgeries; ++s) {
                neededNurses[s] = input.nextInt();
            }

            shiftEarliestStart = new int[numNurses];
            for (int n = 0; n < numNurses; ++n) {
                shiftEarliestStart[n] = input.nextInt() * 60;
            }

            shiftLatestEnd = new int[numNurses];
            for (int n = 0; n < numNurses; ++n) {
                shiftLatestEnd[n] = input.nextInt() * 60;
            }

            maxShiftDuration = input.nextInt() * 60;
            
            incompatibleRooms = new int[numSurgeries][];
            for (int s = 0; s < numSurgeries; ++s) {
                incompatibleRooms[s] = new int[numRooms];
                for (int r = 0; r < numRooms; ++r) {
                    incompatibleRooms[s][r] = input.nextInt();
                }
            } 
        }
    }

    public void solve(int timeLimit) {
        // Declare the optimization model
        HxModel model = optimizer.getModel();

        // Sequence of surgery for each room
        surgeryOrder = new HxExpression[numRooms];
        HxExpression room = model.array();
        for (int r = 0; r < numRooms; ++r) {
            surgeryOrder[r] = model.listVar(numSurgeries);
            room.addOperand(surgeryOrder[r]);
        }

        // Each surgery is scheduled in a room
        model.constraint(model.partition(room));

        // Only compatible rooms can be selected for a surgery
        for (int s = 0; s < numSurgeries; ++s) {
            for (int r : incompatibleRooms[s]) {
                model.constraint(model.eq(model.contains(surgeryOrder[r], s), 0));
            }
        }

        // For each surgery, the selected room
        selectedRoom = new HxExpression[numSurgeries];
        for (int s = 0; s < numSurgeries; ++s) {
            selectedRoom[s] = model.find(room, s);
        }

        // Interval decisions: time range of each surgery
        surgeries = new HxExpression[numSurgeries];
        for (int s = 0; s < numSurgeries; ++s) {
            // Each surgery cannot start before and end after a certain time
            surgeries[s] = model.intervalVar(minStart[s], maxEnd[s]);

            // Each surgery has a specific duration
            model.constraint(model.eq(model.length(surgeries[s]), duration[s]));
        }

        HxExpression surgeryArray = model.array(surgeries);

        // A room can only have one surgery at a time
        for (int r = 0; r < numRooms; ++r) {
            HxExpression sequence = surgeryOrder[r];
            HxExpression sequenceLambda = model.lambdaFunction(s -> model
                        .lt(model.at(surgeryArray, model.at(sequence, s)),
                                model.at(surgeryArray, model.at(sequence, model.sum(s, 1)))));
            model.constraint(model.and(
                model.range(0, model.sub(model.count(sequence), 1)), 
                sequenceLambda));
        }
        // Sequence of surgery for each nurse
        nurseOrder = new HxExpression[numNurses];

        for (int n = 0; n < numNurses; ++n) {
            nurseOrder[n] = model.listVar(numSurgeries);
            HxExpression sequence = nurseOrder[n];
            HxExpression firstSurgeryStart = model.iif(
                model.gt(model.count(sequence),0), 
                model.start(model.at(surgeryArray,model.at(sequence,0))),
                shiftEarliestStart[n]
            );
            HxExpression lastSurgeryEnd = model.iif(
                model.gt(model.count(sequence),0), 
                model.end(model.at(surgeryArray, model.at(sequence,model.sub(model.count(sequence),1)))),
                shiftEarliestStart[n]
            );
            // Each nurse has an earliest starting shift and latest ending shift to be respected
            model.constraint(model.geq(firstSurgeryStart, shiftEarliestStart[n]));
            model.constraint(model.leq(lastSurgeryEnd, shiftLatestEnd[n]));
            // Each nurse cannot work more than a certain amount of hours in a row
            model.constraint(model.leq(
                model.sub(lastSurgeryEnd, firstSurgeryStart), 
                maxShiftDuration));
            // Each nurse can only be at one operation at a time and stays all along the surgery
            HxExpression sequenceLambda = model.lambdaFunction(s -> model
                        .lt(model.at(surgeryArray, model.at(sequence, s)),
                            model.at(surgeryArray, model.at(sequence, model.sum(s, 1)))));
            model.constraint(model.and(
                model.range(0, model.sub(model.count(sequence), 1)), sequenceLambda));

        }

        HxExpression nurseOrderArray = model.array(nurseOrder);

        // Each surgery needs a specific amount of nurse
        for (int s = 0; s < numSurgeries; ++s) {
            int surgery = s;
            model.constraint(model.geq(model.sum(
                model.range(0, numNurses), 
                model.lambdaFunction(
                    n -> model.contains(model.at(nurseOrderArray, n), surgery))), neededNurses[s]));
        }
        
        // Minimize the makespan: end of the last task
        makespan = model.max();
        for (int s = 0; s < numSurgeries; ++s) {
            makespan.addOperand(model.end(surgeries[s]));
        }
        model.minimize(makespan);

        model.close();

        // Parameterize the optimizer
        optimizer.getParam().setTimeLimit(timeLimit);

        optimizer.solve();
    }

    /*
     * Write the solution in a file with the following format:
     * - for each operation of each job, the selected machine, the start and end
     * dates, the nurses working on this operation
     */
    public void writeSolution(String fileName) throws IOException {
        try (PrintWriter output = new PrintWriter(fileName)) {
            System.out.println("Solution written in file " + fileName);

            List<List<Integer>> listNurses = new ArrayList<List<Integer>>();
            for (int s = 0; s < numSurgeries; ++s) {
                listNurses.add(new ArrayList<Integer>());
            }
            for (int n = 0; n < numNurses; ++n) {
                HxCollection surgNurse = nurseOrder[n].getCollectionValue();
                for (long s : surgNurse) {
                    listNurses.get((int) s).add(n);
                }
            }

            for (int s = 0; s < numSurgeries; ++s) {
                output.write(s + "\t"
                        + "\t" + selectedRoom[s].getValue()
                        + "\t" + surgeries[s].getIntervalValue().getStart()
                        + "\t" + surgeries[s].getIntervalValue().getEnd() 
                        + "\t" + listNurses.get(s) + "\n");
            }
        }
    }
    public static void main(String[] args) {
        if (args.length < 1) {
            System.out.println("Usage: java SurgeriesScheduling instanceFile [outputFile] [timeLimit]");
            System.exit(1);
        }

        String instanceFile = args[0];
        String outputFile = args.length > 1 ? args[1] : null;
        String strTimeLimit = args.length > 2 ? args[2] : "20";

        try (HexalyOptimizer optimizer = new HexalyOptimizer()) {
            SurgeriesScheduling model = new SurgeriesScheduling(optimizer);
            model.readInstance(instanceFile);
            model.solve(Integer.parseInt(strTimeLimit));
            if (outputFile != null) {
                model.writeSolution(outputFile);
            }
        } catch (Exception ex) {
            System.err.println(ex);
            ex.printStackTrace();
            System.exit(1);
        }
    }
}