Download
/**
* @author: Dr. rer. nat. George Assaf (Brandenburg University of Technology (BTU), Cottbus, Germany)
* @version: 1.0
* @date: 2025-07-20
* @description: This class implements a Medical Appointment Scheduling Problem (MASP) using the Choco solver.
*/
package com.example;
// Java imports data structures
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
// Choco solver imports
import org.chocosolver.solver.Model;
import org.chocosolver.solver.Solver;
import org.chocosolver.solver.constraints.Constraint;
import org.chocosolver.solver.variables.BoolVar;
import org.chocosolver.solver.variables.IntVar;
class PreferedTime{
int weekday;//0=Monday, 1=Tuesday, ..., 6=Sunday
int stratSlotIndex;// start slot index in the global calendar
int endSlotIndex;// end slot index in the global calendar
public PreferedTime(int weekday, int stratSlot, int endSlot) {
this.weekday = weekday;
this.stratSlotIndex = stratSlot;
this.endSlotIndex = endSlot;
}
/*getters and setters*/
public int getWeekday() {
return weekday;
}
public int getStratSlotIndex() {
return stratSlotIndex;
}
public int getEndSlotIndex() {
return endSlotIndex;
}
}
public class MedicalAppointmentSchedulingProblem {
public enum Resource {
CARDIOLOGY,
NEUROLOGIE
}
private final Model model;
/*Resource domain definitions*/
private final int[] CardiologyDomain = MASPConfig.CARDIOLOGY_DOMAIN;// Identifers of resources in the Cardiology domain
private final int[] NeurologyDomain = MASPConfig.NEUROLOGY_DOMAIN;// Identifers of resources in the Neurology domain
private final int M = MASPConfig.SLOTS_PER_DAY;//Number of time slots per day
/*Time slot domain definitions*/
/* Resource calendars, each resource calendar is represented by an integer array of available slot identifers*/
/*Resource calendar = 9 days x 24 slots */
/*Each calendar starts with Monday (0) at 8:00 -15:00,*/
private final int[] resourceCalendars = MASPConfig.RESOURCE_CALENDARS;
// Global calendar, which is a combination of all resource calendars, and determines the facility availability
private final int[] globalCalendars = MASPConfig.GLOBAL_CALENDAR;
/*model parameters*/
private final int[] preferredDatesByPatient = MASPConfig.PREFERRED_DATES;//{3,8}; /*Identifiers of preferred doctors (0=Cardiology, 1=Neurology)*/
/* Add undesired weekdays for the patient (example: exclude Monday (0) and Friday (4))*/
private final int[] undesiredWeekDaysByPatient = MASPConfig.UNDESIRED_WEEKDAYS;//
/*Identifiers of preferred doctors for each required resource*/
private final Map<Integer, List<Integer>> preferredDoctorsByPatient = MASPConfig.PREFERRED_DOCTORS;
List<PreferedTime> preferredTimesByPatient = new ArrayList<>(MASPConfig.PREFERRED_TIMES);
private int durationInSlots;/*Appointment duration in slot*/
private Resource[] resourcesArray;/* Required resource types for the appointment */
//decion variables
private IntVar[] appSlots; /* each deciion variable encodes one slot in the schedule*/
private IntVar[] appResources; /* resources to be assigned*/
private IntVar[] auxVarCalendars; /* each variabel encodes one resource calendar*/
/**
* Constructor for the Medical Appointment Scheduling Problem (MASP).
* Initializes the model, decision variables, and constraints based on the provided duration and resources.
*
* @param duration The duration of the scheduling problem in time slots, e.g. 4 means 1 hour appointment.
* @param resourcesArray An array of resources (e.g., CARDIOLOGY, NEUROLOGIE) to be scheduled.
*/
public MedicalAppointmentSchedulingProblem(int duration, Resource[] resourcesArray) {
this.model = new Model("MASP Scheduling Problem");
this.durationInSlots = duration;
this.resourcesArray = resourcesArray;
//declaer decision variables and assign domains
createVariables();
//create auxiliary variables
createAuxiliaryVariables();
//define constraints (hard and soft)
MASPConstraints();
//optimize soft constraints
optimizeSoftConstraints();
}
/**
* Creates decision variables for the appointment slots and resources.
* Each appointment slot is represented as an integer variable over the global calendar domain.
* Resources are assigned to each appointment based on their respective domains.
*/
private void createVariables() {
/* Create decision variables for appointment slots over global calendar domain */
appSlots = model.intVarArray("appSlots", durationInSlots, globalCalendars);
this.appResources = new IntVar[resourcesArray.length];
/* Create decision variables for resources assigned to each appointment */
for(int i=0;i<resourcesArray.length;i++) {
Resource resource = resourcesArray[i];
if (resource == null) {
throw new IllegalArgumentException("Resource cannot be null");
}
if (resource != Resource.CARDIOLOGY && resource != Resource.NEUROLOGIE) {
throw new IllegalArgumentException("Resource must be either CARDIOLOGY or NEUROLOGIE");
}
if(resourcesArray[i]==Resource.CARDIOLOGY) {/* Cardiology medical specialization */
this.appResources[i] = model.intVar("appResource_" + resource, CardiologyDomain);
}
else if(resourcesArray[i]==Resource.NEUROLOGIE) {/* Neurology medical specialization */
this.appResources[i] = model.intVar("appResource_" + resource, NeurologyDomain);
} else {
throw new IllegalArgumentException("Unknown resource type: " + resource);
}
}
}
/**
* Creates auxiliary variables for resource calendars.
* Each auxiliary variable represents the first time slot for the appointment assigned to a specific resource.
* Each variable is declared over the corrsponding resourceCalendar domain.
*/
private void createAuxiliaryVariables() {
// Create auxiliary variables for resource calendars, each nominates the first time slot for the appointment
this.auxVarCalendars = model.intVarArray("auxVarCalendars", CardiologyDomain.length + NeurologyDomain.length, resourceCalendars);
}
/**
* Defines the constraints for the appointment scheduling problem.
* This method includes hard constraints to ensure valid scheduling and soft constraints to optimize patient preferences.
*/
private void MASPConstraints() {
//Eliminate slots that fit with the undesired dates for the patient
eleminiatePatientUndesiredDates();
// Link appSlots to auxVarCalendars based on the resources assigned
linkResoucesToAppointmentFirstSlotConstraint();
//appSlots are not allowed to be negative
for (IntVar slot : appSlots) {
model.arithm(slot, ">=", 0).post();
}
//app slots must be different
model.allDifferent(appSlots).post();
//app slots mmust be sequential
for (int i = 0; i < appSlots.length - 1; i++) {
model.arithm(appSlots[i + 1], "=", appSlots[i], "+", 1).post();
}
//app resources must be different
model.allDifferent(appResources).post();
for (IntVar auxVar : auxVarCalendars) {
// Ensure that auxiliary variables are not negative
model.arithm(auxVar, ">=", 0).post();
}
}
/**
* Optimizes soft constraints
* This method calculates the total violation count for soft constraints and then posts it as an objective to minimize.
*/
private void optimizeSoftConstraints() {
// Soft constraint: Optimize preferred dates for the patient
IntVar optimizeDateSoft = calaculatePreferredDatesViolation();
IntVar preferredDoctorsSoft = setPreferedDoctorsByPatient();
IntVar optimizePreferredTimesSoft = optimizePreferredtimes1();// optimizePreferredtimes();
IntVar optimizeSoftConstraints= model.intVar("Optimize Soft Constraints", 0, optimizeDateSoft.getUB() + preferredDoctorsSoft.getUB()+ optimizePreferredTimesSoft.getUB());
model.sum(new IntVar[]{optimizeDateSoft, preferredDoctorsSoft, optimizePreferredTimesSoft}, "=", optimizeSoftConstraints).post();
model.setObjective(Model.MINIMIZE, optimizeSoftConstraints);
}
/**
* Links resources to the first slot of the appointment.
* If a resource is assigned, it will determine the first time slot for the appointment.
* @return void
*/
private void linkResoucesToAppointmentFirstSlotConstraint() {
for (int i = 0; i < appResources.length; i++) {
Resource resource = resourcesArray[i];
//IntVar resourceVar = appResources[i];
if (resource == Resource.CARDIOLOGY) {
for(int j= 0; j < CardiologyDomain.length; j++) {
model.ifThen(model.arithm(appResources[i], "=", CardiologyDomain[j]),
model.arithm(appSlots[0], "=", auxVarCalendars[j]));
}
} else if (resource == Resource.NEUROLOGIE) {
for(int j= 0; j < NeurologyDomain.length; j++) {
model.ifThen(model.arithm(appResources[i], "=", NeurologyDomain[j]),
model.arithm(appSlots[0], "=", auxVarCalendars[j+ CardiologyDomain.length]));
}
}
}
}
/**
* Soft constraint: Calculates the violation of preferred dates for the patient.
* This method creates a list of boolean variables representing whether each preferred date is violated by aeach potential resouce, i.e. Physican.
* @return An IntVar variable representing the total number of violations across all preferred dates by all potential resources.
*/
private IntVar calaculatePreferredDatesViolation()
{
List<BoolVar> violationVars= new ArrayList<>();
//init viloation variabel per patient preferred date
for(int i=0;i<CardiologyDomain.length+NeurologyDomain.length;i++) {
BoolVar prefDateViolation = model.boolVar("prefDateViolation_" + i, false);
violationVars.add(prefDateViolation);
}
for (int i = 0; i < appResources.length; i++) {
Resource resource = resourcesArray[i];
if (resource == Resource.CARDIOLOGY) {
for(int j= 0; j < CardiologyDomain.length; j++) {
//calculaet violation status for the thsi resource
BoolVar prefDateViolation = setPreferredDatesByPatient(auxVarCalendars[j]);
//add the violation variable to the list, if the resource is assigned to the appointment
model.ifThen(model.arithm(appResources[i], "=", CardiologyDomain[j]),
model.arithm(violationVars.get(j), "=", prefDateViolation));
}
} else if (resource == Resource.NEUROLOGIE) {
for(int j= 0; j < NeurologyDomain.length; j++) {
BoolVar prefDateViolation = setPreferredDatesByPatient(auxVarCalendars[j+ CardiologyDomain.length]);
model.ifThen(model.arithm(appResources[i], "=", NeurologyDomain[j]),
model.arithm(violationVars.get(j), "=", prefDateViolation));
}
}
}
// Combine all violations into a single variable
IntVar optimizeDateSoft = model.intVar("Optimize Date Soft", 0, violationVars.size());
// Convert list to array and sum up all violations
model.sum(violationVars.toArray(new BoolVar[0]), "=", optimizeDateSoft).post();
return optimizeDateSoft;
}
/**
* Soft constraint: Sets preferred dates for the patient.
* This method posts constraints to ensure that the appointment slots fall on preferred dates if possible.
* @param resourceCalendaar The calendar variable representing the resource's availability.
* @return A boolean variable indicating whether the preferred date constraint is satisfied.
*/
private BoolVar setPreferredDatesByPatient(IntVar resourceCalendaar){
BoolVar prefDateViolationcheckVar= model.boolVar("prefDateViolation", false);
BoolVar[] dateConstraints = new BoolVar[preferredDatesByPatient.length];
for(int prefDateInd = 0; prefDateInd < preferredDatesByPatient.length; prefDateInd++) {
dateConstraints[prefDateInd] = model.arithm(resourceCalendaar.div(M).intVar(), "=",preferredDatesByPatient[prefDateInd] ).reify();
}
prefDateViolationcheckVar = model.not(model.or(dateConstraints)).reify();
return prefDateViolationcheckVar;
}
/**
* Eliminates undesired dates for patient.
* This method posts constraints to ensure that the appointment slots do not fall on undesired weekdays.
*/
private void eleminiatePatientUndesiredDates(){
for(int badDateInd = 0; badDateInd < undesiredWeekDaysByPatient.length; badDateInd++) {
for(IntVar calendar: auxVarCalendars) {
model.arithm(calendar.div(M).intVar(), "!=", undesiredWeekDaysByPatient[badDateInd]).post();
}
}
}
/**
* Soft constraint: Sets preferred doctors chosen by the patient.
* For each preferred doctor, this method checks if the assigned resource matches the doctor's identifier, and tracks violations per prefered doctor.
* @return An IntVar variable representing the total number of violations across all preferred doctors to be optimized.
*/
private IntVar setPreferedDoctorsByPatient(){
int[] preferredDoctorViolations = new int[appResources.length];
BoolVar[][] allResourcesCheck = model.boolVarMatrix(appResources.length, preferredDoctorsByPatient.size());
BoolVar[] b_preferredSpecialIsBroken = model.boolVarArray(appResources.length);//to record total violation occurance per required resource
int upperBoundPrefferedResources = 0;// simple counter to track the number of preferred resources
for (int i =0; i< appResources.length; i++) {
Resource resource = resourcesArray[i];
List<Integer> preferredDoctors = preferredDoctorsByPatient.get(i);
if (preferredDoctors != null) {
/* Reification of resource assignment*/
for (int j = 0; j < preferredDoctors.size(); j++) {
int doctorId = preferredDoctors.get(j);
if (resource == Resource.CARDIOLOGY) {
allResourcesCheck[i][j] = model.arithm(appResources[i], "=", doctorId).reify();
preferredDoctorViolations[i] ++;
upperBoundPrefferedResources++;
} else if (resource == Resource.NEUROLOGIE) {
allResourcesCheck[i][j] = model.arithm(appResources[i], "=", doctorId).reify();
preferredDoctorViolations[i] ++;
upperBoundPrefferedResources++;
}
}
}
}
// store violation check variables
BoolVar[] isPreferedDoctor = new BoolVar[appResources.length ];
for(int i=0;i<allResourcesCheck.length;i++) {
if( preferredDoctorViolations[i] > 0) {
isPreferedDoctor[i] = model.or(Arrays.copyOf(allResourcesCheck[i], preferredDoctorViolations[i])).reify();
} else {
isPreferedDoctor[i] = model.boolVar("isPreferedDoctor_AppResource" + i, true);//no violation occcurred
}
}
for (int i = 0; i < isPreferedDoctor.length; i++) {
model.ifThenElse(
model.arithm(isPreferedDoctor[i], "=", 0),
model.arithm(b_preferredSpecialIsBroken[i], "=", 1),
model.arithm(b_preferredSpecialIsBroken[i], "=", 0));
}
// Store sum up all violations for preferred resources
IntVar sumViolations = model.intVar("total violations for prefered resolureces", 0, upperBoundPrefferedResources);
// post the constraint to sum up all violations
model.sum(b_preferredSpecialIsBroken, "=", sumViolations).post();
return sumViolations;
}
/**
* Soft constraint: Sets preferred times for the patient.
* This method checks if the appointment slots fall within the patient's preferred time ranges and returns violations.
* @param auxCalendarVar The auxiliary calendar variable representing the resource's availability.
* @return An array of boolean variables indicating whether each preferred time constraint is violated.
*/
private BoolVar[] setPrefferedtimes1(IntVar auxCalendarVar) {
// Check if there are any preferred times defined for the patient
if (preferredTimesByPatient == null || preferredTimesByPatient.isEmpty()) {
return new BoolVar[0];
}
BoolVar[] violations = new BoolVar[preferredTimesByPatient.size()];
IntVar dayVar = auxCalendarVar.div(M).mod(7).intVar();/*day = floor(auxVar div M) mod 7*/
IntVar slotVar = auxCalendarVar.mod(M).intVar(); /*slotIndex = auxVar mod M*/
for (int i = 0; i < preferredTimesByPatient.size(); i++) {
PreferedTime pref = preferredTimesByPatient.get(i);
// Create constraints (not yet reified)
Constraint matchesDayConstraint = model.arithm(dayVar, "=", pref.getWeekday());
Constraint afterStartConstraint = model.arithm(slotVar, ">=", pref.getStratSlotIndex());
Constraint beforeEndConstraint = model.arithm(slotVar, "<=", pref.getEndSlotIndex());
// Create the "not within time range" constraint
Constraint withinTimeRangeConstraint = model.and(afterStartConstraint, beforeEndConstraint);
Constraint notWithinTimeRangeConstraint = model.not(withinTimeRangeConstraint);
// Combine into final constraint: matches day AND not within time range
Constraint violationConstraint = model.and(matchesDayConstraint, notWithinTimeRangeConstraint);
// Reify the final constraint to get a BoolVar
violations[i] = violationConstraint.reify();
}
return violations;
}
/**
* Soft constraint: Optimizes preferred times for the patient.
* This method aggregates violations across all resources and returns a single IntVar representing the total number of violations.
* @return An IntVar variable representing the total number of violations across all preferred times.
*/
private IntVar optimizePreferredtimes1() {
if (preferredTimesByPatient == null || preferredTimesByPatient.isEmpty()) {
return model.intVar(0);
}
List<BoolVar> allViolations = new ArrayList<>();
for (int i = 0; i < appResources.length; i++) {
Resource resource = resourcesArray[i];
int domainOffset = (resource == Resource.CARDIOLOGY) ? 0 : CardiologyDomain.length;
int domainSize = (resource == Resource.CARDIOLOGY) ? CardiologyDomain.length : NeurologyDomain.length;
for (int j = 0; j < domainSize; j++) {
BoolVar[] resourceViolations = setPrefferedtimes1(auxVarCalendars[domainOffset + j]);
int resourceId = (resource == Resource.CARDIOLOGY) ? CardiologyDomain[j] : NeurologyDomain[j];
// Create resource selection constraint (not reified yet)
Constraint resourceSelectedConstraint = model.arithm(appResources[i], "=", resourceId);
for (BoolVar violation : resourceViolations) {
// The violation is already a BoolVar (reified)
// Create a new BoolVar that is true iff both resourceSelectedConstraint and violation are true
BoolVar combinedViolation = model.boolVar();
// If the resource is selected, then combinedViolation == violation; otherwise, combinedViolation == 0
model.ifThenElse(
resourceSelectedConstraint,
model.arithm(combinedViolation, "=", violation),
model.arithm(combinedViolation, "=", 0)
);
allViolations.add(combinedViolation);
}
}
}
if (allViolations.isEmpty()) {
return model.intVar(0);
}
IntVar totalViolations = model.intVar("totalTimeViolations", 0, allViolations.size());
model.sum(allViolations.toArray(new BoolVar[0]), "=", totalViolations).post();
return totalViolations;
}
/**
* Solves the Medical Appointment Scheduling Problem (MASP).
* This method initializes the solver, sets a limit on the number of solutions, and iterates through the solutions,
* printing the values of appointment slots and resources for each solution found.
*/
public void solve() {
Solver solver = model.getSolver();
solver.limitSolution(100); /*time limit critirion*/
while (solver.solve()) {
System.out.println("Solution found:");
System.out.println("Appointment Slots:");
// Print the values of appointment slots (decison variables )
for (IntVar slot : appSlots) {
System.out.print(slot.getName() + "=" + slot.getValue() + " ");
}
for (IntVar resource : appResources) {
System.out.print(resource.getName() + "=" + resource.getValue() + " ");
}
System.out.println();
}
solver.printStatistics(); /* Print solver statistics */
}
}