Download
/**
 * @author: Dr. rer. nat. George Assaf (Brandenburg University of Technology (BTU), Cottbus, Germany)
 * @version: 1.0
 * @date: 2025-10-26
 * @description: Prototype implementation of the medical appointment sequence scheduling problem(MASSP) 
*/
package com.example.massp;


import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.chocosolver.solver.Model;
import org.chocosolver.solver.Solver;
import org.chocosolver.solver.variables.BoolVar;
import org.chocosolver.solver.variables.IntVar;
import java.util.Arrays;
public class MASSP {
    
    private Model model;
    private ArrayList<ArrayList<CalendarDomain>> potentialResourceSlot;
    private int[][] appID2Resources;
    private IntVar[] resourceVariables;
    private IntVar[][] sequenceSlots;
    private IntVar totalWorkloadPenalty;
    private Map<Integer, Integer> resourceIndex2AppointmentMap;
    private List<Integer>                 similiarResourcesList;
    private Map<Integer, List<Integer>>   resourceToIndices;



    public MASSP() {

        Appointment maxResAppointment = MASPConfigLarge.getAppointmentWithMostResources();


        this.appID2Resources = new int[MASPConfigLarge.sequence.size()][(maxResAppointment.getRequiredResources().size())+1];
        
        this.model = new Model("MASSP");

        this.potentialResourceSlot  = new ArrayList<ArrayList<CalendarDomain>>();

        int max = MASPConfigLarge.getMaxAppointmentDuration();

        this.sequenceSlots = new IntVar[max][MASPConfigLarge.sequence.size()];

    }



    public void solve() {

        initialize();

        //declare decision variables
        assignVariableDomains();

        //different resources per each appointment
        applyAllDifferentConstraints();

        //link resources with appointment slots and ensure sequential slots per appointment
        ensureSequentialSlotsPerAppointment();

        // No overlapping time slots for appointments
        NoTimeSlotOverlapping();

        // Set the chronological order of the sequence
        setChronologicalOrderOfSequence1( MASPConfigLarge.chronologicalOrder);

        ensureMinimalDistanceBetweenAppointments(sequenceSlots, MASPConfigLarge.min_distance );

        ensureMaximalDistanceBetweenAppointments(sequenceSlots, MASPConfigLarge.max_distance );

        //sameResourceconstraint();

        totalWorkloadPenalty = optimizeResourcceWorkload();
       // totalWorkloadPenalty = optimizeResourcecWorkload();

        model.setObjective(Model.MINIMIZE, totalWorkloadPenalty);



        Solver solver = model.getSolver();
 

        while (solver.solve()) {
        System.out.println("=== Solution ===");

        for (Map.Entry<Integer, Appointment> entry : MASPConfigLarge.sequence.entrySet()) {
            int appointmentId = entry.getKey();
            int appIndex = appointmentId - 1;
            Appointment app = entry.getValue();

            // Print all resource assignments for this appointment
            System.out.print("Appointment " + app.getId() + " (" + app.getRequiredResources() + "): ");
            System.out.print("assigned resource IDs = [");
            
            boolean first = true;
            for (int i = 0; i < resourceVariables.length; i++) {
                if (resourceIndex2AppointmentMap.get(i) == appIndex) {
                    if (!first) {
                        System.out.print(", ");
                    }
                    System.out.print(resourceVariables[i].getValue());
                    first = false;
                }
            }
            System.out.println("]");

            // Print slot identifiers for this appointment
            System.out.print("  Slots: ");
            for (int slot = 0; slot < app.getDurationInSlot(); slot++) {
                System.out.print(sequenceSlots[slot][appIndex].getValue() + " ");
            }
            System.out.println();
        }
        System.out.println();
    }
    solver.printStatistics();

     }


    private void initialize() {
        
        int requiredResourceId = 0;
        int maxResources = MASPConfigLarge.getAppointmentWithMostResources().getRequiredResources().size();


        for(int i=0;i<appID2Resources.length;i++) {
            appID2Resources[i][0] = i;
            Appointment appointment = MASPConfigLarge.sequence.get(i+1);
             for(int j=0;j<maxResources;j++) {

               if(appointment.getRequiredResources().size()>requiredResourceId) {
                
                 appID2Resources[i][j+1] = appointment.getRequiredResources().get(requiredResourceId).ordinal();

               } else {
                  appID2Resources[i][j+1] = -1;
               }
                 requiredResourceId++;
            }
            requiredResourceId = 0;
        }


        for(int i = 0; i < appID2Resources.length; i++)
        {
            for(int j = 1; j < appID2Resources[i].length; j++)
            {
                if(appID2Resources[i][j] != -1)
                {
                    potentialResourceSlot.add(new ArrayList<CalendarDomain>());
                }
            }
        }
    }



    private void assignVariableDomains() {

         int noResources = MASPConfigLarge.getTotalNumberOfResources();
        this.resourceVariables= new IntVar[noResources];

        this.resourceIndex2AppointmentMap = new HashMap<>();
        int resourceIndex =0;
        int resourceId=0;


        for(int i=0;i<appID2Resources.length;i++) {

            for(int j=1;j<appID2Resources[i].length;j++) {

            if(appID2Resources[i][j] == ResourceType.CARDIOLOGY.ordinal() && appID2Resources[i][j] !=-1)  {

            resourceVariables[resourceIndex] = model.intVar("Resource_var " + i, MASPConfigLarge.CARDIOLOGY_DOMAIN);

            for (int k = 0; k <  MASPConfigLarge.CARDIOLOGY_DOMAIN.length; k++) {
                CalendarDomain calendarDomain = new CalendarDomain(MASPConfigLarge.CARDIOLOGY_DOMAIN[k],
                        MASPConfigLarge.getFilteredGlobalCalendar(),
                        MASPConfigLarge.getFilteredResourceCalendar(),
                        MASPConfigLarge.SLOTS_PER_DAY,
                        MASPConfigLarge.CURRENT_WORKLOAD[MASPConfigLarge.CARDIOLOGY_DOMAIN[k]]
                );
                
                calendarDomain.findDate(model);

                potentialResourceSlot.get(resourceIndex).add(calendarDomain);
            }
            resourceIndex2AppointmentMap.put(resourceIndex++, i);

            }else if(appID2Resources[i][j] !=-1){
            
            resourceVariables[resourceIndex] = model.intVar("Resource_var " + i, MASPConfigLarge.NEUROLOGY_DOMAIN);

            for (int k = 0; k <  MASPConfigLarge.NEUROLOGY_DOMAIN.length; k++) {
                CalendarDomain calendarDomain = new CalendarDomain(MASPConfigLarge.NEUROLOGY_DOMAIN[k],
                        MASPConfigLarge.getFilteredGlobalCalendar(),
                        MASPConfigLarge.getFilteredResourceCalendar(),
                        MASPConfigLarge.SLOTS_PER_DAY,
                        MASPConfigLarge.CURRENT_WORKLOAD[MASPConfigLarge.NEUROLOGY_DOMAIN[k]]
                );
                calendarDomain.findDate(model);
                potentialResourceSlot.get(resourceIndex).add(calendarDomain);
            }
            resourceIndex2AppointmentMap.put(resourceIndex++, i);

            }
        }
    }


    // Initialize sequence start slots of appointments
    for (int i = 0; i < sequenceSlots[0].length; i++) {
         sequenceSlots[0][i] = model.intVar("SeqSlot_0_App" + (i+1), MASPConfigLarge.getFilteredGlobalCalendar());
    }
}



    private void applyAllDifferentConstraints() {
    // Collect resource variables per appointment
    Map<Integer, List<IntVar>> appointment2Vars = new HashMap<>();

    for (int idx = 0; idx < resourceVariables.length; idx++) {
        int appointmentId = resourceIndex2AppointmentMap.get(idx);
        appointment2Vars
            .computeIfAbsent(appointmentId, k -> new ArrayList<>())
            .add(resourceVariables[idx]);
    }

    // Apply allDifferent per appointment
    for (Map.Entry<Integer, List<IntVar>> entry : appointment2Vars.entrySet()) {
        List<IntVar> vars = entry.getValue();
        if (vars.size() > 1) {
            model.allDifferent(vars.toArray(new IntVar[0])).post();
        }
    }
}

private void ensureSequentialSlotsPerAppointment() {


    // link resources to slots
    for (int i=0;i<potentialResourceSlot.size();i++) {
        int appointmentId = resourceIndex2AppointmentMap.get(i);
        //Appointment appointment = MASPConfigLarge.sequence.get(appointmentId + 1);
        //System.out.println("Linking resource variable " + resourceVariables[i].getName() +
        // " to appointment " + (appointmentId + 1) + " starting slot variable " + sequenceSlots[0][appointmentId].getName());

        for (int j = 0; j < potentialResourceSlot.get(i).size(); j++) {
             model.ifThen(model.arithm(resourceVariables[i], "=", potentialResourceSlot.get(i).get(j).getResourceId()),
              //todo: fix this bug here for mapping resources to correct appointment
              model.arithm(sequenceSlots[0][appointmentId], "=", potentialResourceSlot.get(i).get(j).getCalendarVar()));
        }
    }

     //ensure sequential slots per each appointment
     for(int i=0;i<sequenceSlots[0].length;i++)
    {
           for(int j=1;j<sequenceSlots.length;j++)
        {
            if(j>=MASPConfigLarge.sequence.get(i+1).getDurationInSlot())
            {//when the appointment duration is greater than the number of slots
                sequenceSlots[j][i] = null;//model.intVar(-1*sequenceSlots.length*sequenceSlots.length, -1);
               // System.out.println("Appointment " + (i+1) + " exceeds available slots at "+ j);
            }
            else 
            {
                sequenceSlots[j][i] = model.intVar(MASPConfigLarge.getFilteredGlobalCalendar());
                model.arithm(sequenceSlots[j][i], "=", sequenceSlots[j-1][i].add(1).intVar()).post();
            }
        }
    
    }
}


private void NoTimeSlotOverlapping() {


        int index = 0;
        for(int i = 0; i < sequenceSlots.length; i++)
        {
            for(int j = 0; j < sequenceSlots[0].length; j++)
            {  
                if(sequenceSlots[i][j] == null) continue;

                 
                index++;
            }
        }
         
        IntVar[] unfoldedSequenceSlots = new IntVar[index];

        index = 0;
        for(int i = 0; i < sequenceSlots.length; i++)
        {
            for(int j = 0; j < sequenceSlots[0].length; j++)
            {  
                if(sequenceSlots[i][j] == null) continue;

                unfoldedSequenceSlots[index] = sequenceSlots[i][j];
                index++;
            }
        }

        model.allDifferent(unfoldedSequenceSlots).post();

}


private void setChronologicalOrderOfSequence1(List<Integer> requestedOrderList) {
  //  System.out.println("Applying chronological order: " + requestedOrderList);
    
    for (int i = 0; i < requestedOrderList.size() - 1; i++) {
        int aId = requestedOrderList.get(i);
        int bId = requestedOrderList.get(i + 1);
        int aIndex = aId - 1;
        int bIndex = bId - 1;
        
   /*      System.out.printf("Constraint: slot[%d][%d] (%s) <= slot[%d][%d] (%s)%n",
            aIndex, 0, sequenceSlots[0][aIndex].getName(),
            bIndex, 0, sequenceSlots[0][bIndex].getName()); */
        
        model.arithm(sequenceSlots[0][aIndex], "<=", sequenceSlots[0][bIndex]).post();
    }
}


 /* This method ensures that the minimal distance between two appointments is satisfied
     * @param appointments: The appointments
     * @param minDistanceList: The list of minimal distances
     * @param appointmentDurationsInSlots: The appointment durations in slots
     * @param NoSinD: The number of slots per day
    */
    private void ensureMinimalDistanceBetweenAppointments(IntVar[][] appointments, int[][] minDistanceList) 
    {

        if (minDistanceList.length == 0 || appointments.length == 0) {
            return;  // Nothing to process if minDistance or appointments are empty
        }

        for(int i=0;i<minDistanceList.length;i++)
        {
            for(int j=0;j<minDistanceList[i].length;j++)
            {
                if(minDistanceList[i][j] > 0)
                {
                    int minimalDistanceInDays = minDistanceList[i][j];

                    if (minimalDistanceInDays <= 0) {
                        continue;
                    }

                    // Convert minimal distance from days to slots (days * NoSinD slots per day)
                    int minimalDistanceInSlots = minimalDistanceInDays * MASPConfigLarge.SLOTS_PER_DAY;

                    // Ensure that the minimal distance in slots is enforced
                    if (minimalDistanceInSlots % MASPConfigLarge.SLOTS_PER_DAY == 0)// day case
                    {
                        int appDuration =  MASPConfigLarge.sequence.get(i + 1).getDurationInSlot();//appointmentDurationsInSlots[i];  // Get the duration of the current appointment
                        

                        model.arithm(appointments[0][j].sub(appointments[appDuration - 1][i]).add(appointments[0][i].mod(MASPConfigLarge.SLOTS_PER_DAY)).intVar(), ">=", minimalDistanceInSlots).post();;
                    }
                    else
                    {
                        //todo hours case
                    }
                }
            }
        }
    }



     /**
     * This method ensures that the maximal distance between two appointments is satisfied
     * @param appointments: The appointments
     * @param maxDistanceMatrix: The maximal distance matrix
     * @param appointmentDurationsInSlots: The appointment durations in slots
     * @param NoSinD: The number of slots per day
    */
    private void ensureMaximalDistanceBetweenAppointments(IntVar[][] appointments, int[][] maxDistanceMatrix) 
    {
     

        if (maxDistanceMatrix.length == 0 || appointments.length == 0) {
            return;  // Nothing to process if maxDistance or appointments are empty
        }

        // Iterate over each row in the maximal distance matrix
        for (int i = 0; i < maxDistanceMatrix.length; i++)
        {
            // Iterate over each column in the row
            for (int j = 0; j < maxDistanceMatrix[i].length; j++)
            {

                // Get the maximal distance between the two appointments
                int maximalDistanceInDays = maxDistanceMatrix[i][j];

                // If maximal distance is less than or equal to 0, skip this pair
                if (maximalDistanceInDays <= 0) {
                    continue;
                }

                // Convert maximal distance from days to slots (days * NoSinD slots per day)
                int maximalDistanceInSlots = maximalDistanceInDays * MASPConfigLarge.SLOTS_PER_DAY;

                // Ensure that the maximal distance in slots is enforced
                if (maximalDistanceInSlots % MASPConfigLarge.SLOTS_PER_DAY == 0)// day case
                {

                    int appDuration =  MASPConfigLarge.sequence.get(i + 1).getDurationInSlot();//appointmentDurationsInSlots[i];  // Get the duration of the current appointment

                    
                    // Calculate the end of the first appointment (start time + duration)
                    IntVar endOfFirstAppointment = appointments[0][i].add(appDuration).intVar();

                    // Calculate the constraint to ensure that the second appointment starts within the maximal distance
                    model.arithm(appointments[0][j], "<=", endOfFirstAppointment.add(maximalDistanceInSlots).intVar()).post();

                }
                else
                {
                    //todo hours case
                }
            }
        }
    }

    private void sameResourceconstraint() {

        for (SameResources constraint : MASPConfigLarge.sameResourcesConstraints) {
            int idxA = MASPConfigLarge.getGlobalResourceIndex(constraint.getAppointmentIndex1(), constraint.getResourceIndex1());
            int idxB = MASPConfigLarge.getGlobalResourceIndex(constraint.getAppointmentIndex2(), constraint.getResourceIndex2());
           
            model.arithm(resourceVariables[idxA], "=", resourceVariables[idxB]).post();
           
            //System.out.printf("Constraint: resources[%d] must equal resources[%d]%n", idxA, idxB);
        }


    }

    private IntVar optimizeResourcceWorkload() {

                
        IntVar[] workloadPenalties = new IntVar[resourceVariables.length];

        Map<Integer,IntVar> totalWorkloadperResource = new HashMap<>();

        Map<Integer,IntVar>    totalWorkloadPerResource = new HashMap<>();

        Map<Integer,IntVar>    resourceWorkloadPerResource = new HashMap<>();

            for (int i=0;i<potentialResourceSlot.size();i++) {

            
                int appointmentId = resourceIndex2AppointmentMap.get(i);
                   workloadPenalties[i] = model.intVar("workloadPenalty" + i, 0, 500);

                for (int j = 0; j < potentialResourceSlot.get(i).size(); j++) {

                    int resourceId = potentialResourceSlot.get(i).get(j).getResourceId();
                
                    int initialWorkload = potentialResourceSlot.get(i).get(j).getCurrentWorkload();
                   // System.out.println("Appointment " + (appointmentId+1) + " resource variable " + resourceVariables[i].getName() +
                    // " assigned to resource ID " + resourceId + " with initial workload " + initialWorkload);

                    int thisAppDuration = MASPConfigLarge.sequence.get(appointmentId+1).getDurationInSlot() * 15;//15 minutes per slot

                    totalWorkloadPerResource.putIfAbsent(resourceId, model.intVar(initialWorkload));

                     // Dynamically update workload penalty for this resource
                model.ifThen(
                    model.arithm(resourceVariables[i], "=", resourceId),
                    model.arithm(workloadPenalties[i], "=",
                        model.intOffsetView(totalWorkloadPerResource.get(resourceId), thisAppDuration))
                );

                resourceWorkloadPerResource.put(i, workloadPenalties[i]);

                }
            }

        // Step 2: Aggregate workloads for each resource
        for (int resourceId : totalWorkloadPerResource.keySet()) 
        {
            IntVar cumulativeWorkload = model.intVar("cumulativeWorkload_" + resourceId, 0, 500);
            ArrayList<IntVar> resourceWorkloads = new ArrayList<>();

            for (int i = 0; i < resourceVariables.length; i++)
            {
                IntVar workloadForThisResource = model.intVar("workloadForResource_" + resourceId + "_app" + i, 0, 500);
                model.ifThenElse(
                    model.arithm(resourceVariables[i], "=", resourceId),
                    model.arithm(workloadForThisResource, "=", workloadPenalties[i]),
                    model.arithm(workloadForThisResource, "=", 0)
                );
                resourceWorkloads.add(workloadForThisResource);
            }

            // Sum all workloads for this resource
            model.sum(resourceWorkloads.toArray(new IntVar[0]), "=", cumulativeWorkload).post();

            // Update the cumulative workload in the tracking map
            totalWorkloadPerResource.put(resourceId, cumulativeWorkload);
        }

         // Step 3: Calculate maximum workload
        IntVar maxWorkload = model.intVar("maxWorkload", 0, 500);
        model.max(maxWorkload, totalWorkloadPerResource.values().toArray(new IntVar[0])).post();



        return maxWorkload;
}





    private IntVar optimizeResourcecWorkload() {
    int numResources = MASPConfigLarge.CURRENT_WORKLOAD.length;
    
    // Calculate total duration each resource will work based on assignments
    IntVar[] finalWorkloads = new IntVar[numResources];
    IntVar[] workloadIncreases = new IntVar[numResources];
    
    for (int r = 0; r < numResources; r++) {
        // Start with current workload
        finalWorkloads[r] = model.intVar("final_workload_" + r, 
            MASPConfigLarge.CURRENT_WORKLOAD[r], 
            MASPConfigLarge.CURRENT_WORKLOAD[r] + getMaxPossibleWorkloadIncrease());
        
        // Calculate workload increase for this resource
        workloadIncreases[r] = model.intVar("workload_inc_" + r, 0, getMaxPossibleWorkloadIncrease());
        
        // Sum up all durations where this resource is selected
        List<IntVar> indicators = new ArrayList<>();
        List<Integer> durations = new ArrayList<>();
        
        for (int i = 0; i < resourceVariables.length; i++) {
            int appointmentId = resourceIndex2AppointmentMap.get(i);
            Appointment appointment = MASPConfigLarge.sequence.get(appointmentId + 1);
            int duration = appointment.getDurationInSlot();
            
            // Create indicator variable for whether this resource is selected
            BoolVar indicator = model.boolVar("indicator_r" + r + "_var" + i);
            model.arithm(resourceVariables[i], "=", r).reifyWith(indicator);
            
            indicators.add(indicator);
            durations.add(duration);
        }
        
        // Workload increase = sum of (indicator * duration) for all assignments to this resource
        if (!indicators.isEmpty()) {
            model.scalar(indicators.toArray(new IntVar[0]), 
                        durations.stream().mapToInt(Integer::intValue).toArray(), 
                        "=", workloadIncreases[r]).post();
        } else {
            model.arithm(workloadIncreases[r], "=", 0).post();
        }
        
        // Final workload = current workload + workload increase
        model.arithm(finalWorkloads[r], "=", 
                    model.intVar(MASPConfigLarge.CURRENT_WORKLOAD[r]).add(workloadIncreases[r]).intVar()).post();
    }
    
    // Calculate mean workload
    IntVar totalWorkload = model.intVar("total_workload", 0, 
        Arrays.stream(MASPConfigLarge.CURRENT_WORKLOAD).sum() + getTotalPossibleWorkloadIncrease());
    model.sum(finalWorkloads, "=", totalWorkload).post();
    
    IntVar meanWorkload = model.intVar("mean_workload", 0, 
        Arrays.stream(MASPConfigLarge.CURRENT_WORKLOAD).max().orElse(0) + getMaxPossibleWorkloadIncrease());
    model.arithm(meanWorkload, "=", totalWorkload.div(model.intVar(numResources)).intVar()).post();
    
    // Calculate variance = sum of (workload - mean)^2 for all resources
    IntVar[] squaredDeviations = new IntVar[numResources];
    for (int r = 0; r < numResources; r++) {
        IntVar deviation = model.intVar("dev_" + r, 
            -getMaxPossibleWorkloadIncrease(), 
            getMaxPossibleWorkloadIncrease());
        
        // deviation = finalWorkloads[r] - meanWorkload
        model.arithm(deviation, "=", finalWorkloads[r].sub(meanWorkload).intVar()).post();
        
        // squaredDeviation = deviation * deviation
        squaredDeviations[r] = model.intVar("sq_dev_" + r, 0, 
            (int)Math.pow(getMaxPossibleWorkloadIncrease() * 2, 2));
        model.times(deviation, deviation, squaredDeviations[r]).post();
    }
    
    // Total penalty = sum of squared deviations (variance)
    IntVar totalPenalty = model.intVar("workload_penalty", 0, 
        numResources * (int)Math.pow(getMaxPossibleWorkloadIncrease() * 2, 2));
    model.sum(squaredDeviations, "=", totalPenalty).post();
    
    return totalPenalty;
}

private int getMaxPossibleWorkloadIncrease() {
    // Maximum possible workload increase for a single resource
    // This is the sum of durations of all appointments that could potentially use this resource
    int maxIncrease = 0;
    for (Appointment app : MASPConfigLarge.sequence.values()) {
        maxIncrease += app.getDurationInSlot();
    }
    return maxIncrease;
}

private int getTotalPossibleWorkloadIncrease() {
    // Total possible workload increase across all resources
    int totalIncrease = 0;
    for (Appointment app : MASPConfigLarge.sequence.values()) {
        totalIncrease += app.getDurationInSlot() * app.getRequiredResources().size();
    }
    return totalIncrease;
}





    
}