From 3be3b53da62dd0148995cccaedb9953c931c17f4 Mon Sep 17 00:00:00 2001 From: Geoffrey De Smet Date: Tue, 26 Mar 2024 17:17:11 +0100 Subject: [PATCH] feat: bed allocation quickstart (#307) --- README.adoc | 18 + pom.xml | 1 + use-cases/bed-allocation/README.adoc | 117 +++++ use-cases/bed-allocation/pom.xml | 223 +++++++++ .../quarkus-bed-scheduling-screenshot.png | Bin 0 -> 119917 bytes .../org/acme/bedallocation/domain/Bed.java | 71 +++ .../acme/bedallocation/domain/BedPlan.java | 101 ++++ .../acme/bedallocation/domain/Department.java | 123 +++++ .../domain/DepartmentSpecialty.java | 62 +++ .../org/acme/bedallocation/domain/Gender.java | 26 + .../domain/GenderLimitation.java | 28 ++ .../org/acme/bedallocation/domain/Room.java | 125 +++++ .../org/acme/bedallocation/domain/Stay.java | 199 ++++++++ .../rest/BedSchedulingDemoResource.java | 38 ++ .../rest/BedSchedulingResource.java | 230 +++++++++ .../bedallocation/rest/DemoDataGenerator.java | 343 +++++++++++++ .../rest/exception/ErrorInfo.java | 4 + .../exception/ScheduleSolverException.java | 30 ++ .../ScheduleSolverExceptionMapper.java | 19 + .../BedAllocationConstraintProvider.java | 161 +++++++ .../main/resources/META-INF/resources/app.js | 454 ++++++++++++++++++ .../resources/META-INF/resources/index.html | 179 +++++++ .../src/main/resources/application.properties | 41 ++ .../rest/BedSchedulingResourceTest.java | 107 +++++ .../BedAllocationConstraintProviderTest.java | 268 +++++++++++ 25 files changed, 2968 insertions(+) create mode 100644 use-cases/bed-allocation/README.adoc create mode 100644 use-cases/bed-allocation/pom.xml create mode 100644 use-cases/bed-allocation/quarkus-bed-scheduling-screenshot.png create mode 100644 use-cases/bed-allocation/src/main/java/org/acme/bedallocation/domain/Bed.java create mode 100644 use-cases/bed-allocation/src/main/java/org/acme/bedallocation/domain/BedPlan.java create mode 100644 use-cases/bed-allocation/src/main/java/org/acme/bedallocation/domain/Department.java create mode 100644 use-cases/bed-allocation/src/main/java/org/acme/bedallocation/domain/DepartmentSpecialty.java create mode 100644 use-cases/bed-allocation/src/main/java/org/acme/bedallocation/domain/Gender.java create mode 100644 use-cases/bed-allocation/src/main/java/org/acme/bedallocation/domain/GenderLimitation.java create mode 100644 use-cases/bed-allocation/src/main/java/org/acme/bedallocation/domain/Room.java create mode 100644 use-cases/bed-allocation/src/main/java/org/acme/bedallocation/domain/Stay.java create mode 100644 use-cases/bed-allocation/src/main/java/org/acme/bedallocation/rest/BedSchedulingDemoResource.java create mode 100644 use-cases/bed-allocation/src/main/java/org/acme/bedallocation/rest/BedSchedulingResource.java create mode 100644 use-cases/bed-allocation/src/main/java/org/acme/bedallocation/rest/DemoDataGenerator.java create mode 100644 use-cases/bed-allocation/src/main/java/org/acme/bedallocation/rest/exception/ErrorInfo.java create mode 100644 use-cases/bed-allocation/src/main/java/org/acme/bedallocation/rest/exception/ScheduleSolverException.java create mode 100644 use-cases/bed-allocation/src/main/java/org/acme/bedallocation/rest/exception/ScheduleSolverExceptionMapper.java create mode 100644 use-cases/bed-allocation/src/main/java/org/acme/bedallocation/solver/BedAllocationConstraintProvider.java create mode 100644 use-cases/bed-allocation/src/main/resources/META-INF/resources/app.js create mode 100644 use-cases/bed-allocation/src/main/resources/META-INF/resources/index.html create mode 100644 use-cases/bed-allocation/src/main/resources/application.properties create mode 100644 use-cases/bed-allocation/src/test/java/org/acme/bedallocation/rest/BedSchedulingResourceTest.java create mode 100644 use-cases/bed-allocation/src/test/java/org/acme/bedallocation/solver/BedAllocationConstraintProviderTest.java diff --git a/README.adoc b/README.adoc index 5b449c7c85..28823081e5 100644 --- a/README.adoc +++ b/README.adoc @@ -15,6 +15,8 @@ a|* <> * <> * <> * <> +* <> +* <> a|* link:hello-world/README.adoc[Java (Hello World)] (Java, Maven or Gradle) * link:use-cases/school-timetabling/README.adoc[Quarkus] (Java, Maven or Gradle, Quarkus) @@ -86,6 +88,22 @@ image::use-cases/facility-location/quarkus-facility-location-screenshot.png[] * link:use-cases/facility-location/README.adoc[Run quarkus-facility-location] (Java, Maven, Quarkus) +=== Conference Scheduling + +Assign conference talks to timeslots and rooms to produce a better schedule for speakers. + +image::use-cases/conference-scheduling/quarkus-conference-scheduling-screenshot.png[] + +* link:use-cases/conference-scheduling/README.adoc[Run quarkus-conference-scheduling] (Java, Maven, Quarkus) + +=== Bed Allocation Scheduling + +Assign beds to patient stays to produce a better schedule for hospitals. + +image::use-cases/bed-allocation/quarkus-bed-scheduling-screenshot.png[] + +* link:use-cases/bed-allocation/README.adoc[Run quarkus-bed-allocation-scheduling] (Java, Maven, Quarkus) + == Legal notice Timefold Quickstarts was https://timefold.ai/blog/2023/optaplanner-fork/[forked] on 20 April 2023 from OptaPlanner Quickstarts, diff --git a/pom.xml b/pom.xml index 0041e79680..13e2ec7287 100644 --- a/pom.xml +++ b/pom.xml @@ -31,6 +31,7 @@ use-cases/employee-scheduling use-cases/food-packaging use-cases/conference-scheduling + use-cases/bed-allocation diff --git a/use-cases/bed-allocation/README.adoc b/use-cases/bed-allocation/README.adoc new file mode 100644 index 0000000000..88959d6c1b --- /dev/null +++ b/use-cases/bed-allocation/README.adoc @@ -0,0 +1,117 @@ += Bed Allocation Scheduling (Java, Quarkus, Maven) + +Assign beds to patient stays to produce a better schedule for hospitals. + +image::./quarkus-bed-scheduling-screenshot.png[] + +* <> +* <> +* <> +* <> + +[[run]] +== Run the application + +. Git clone the timefold-quickstarts repo and navigate to this directory: ++ +[source, shell] +---- +$ git clone https://github.com/TimefoldAI/timefold-quickstarts.git +... +$ cd timefold-quickstarts/use-cases/bed-allocation +---- + +. Start the application with Maven: ++ +[source, shell] +---- +$ mvn quarkus:dev +---- + + +. Visit http://localhost:8080 in your browser. + +. Click on the *Solve* button. + +Then try _live coding_: + +. Make some changes in the source code. +. Refresh your browser (F5). + +Notice that those changes are immediately in effect. + + +[[package]] +== Run the packaged application + +When you're done iterating in `quarkus:dev` mode, +package the application to run as a conventional jar file. + +. Build it with Maven: ++ +[source, shell] +---- +$ mvn package +---- +. Run the Maven output: ++ +[source, shell] +---- +$ java -jar ./target/quarkus-app/quarkus-run.jar +---- ++ +[NOTE] +==== +To run it on port 8081 instead, add `-Dquarkus.http.port=8081`. +==== + +. Visit http://localhost:8080 in your browser. + +. Click on the *Solve* button. + +[[container]] +== Run the application in a container + +. Build a container image: ++ +[source, shell] +---- +$ mvn package -Dcontainer +---- +The container image name +. Run a container: ++ +[source, shell] +---- +$ docker run -p 8080:8080 --rm $USER/timefold-solver-quarkus-bed-allocation-quickstart:1.0-SNAPSHOT +---- + +[[native]] +== Run it native + +To increase startup performance for serverless deployments, +build the application as a native executable: + +. https://quarkus.io/guides/building-native-image#configuring-graalvm[Install GraalVM and gu install the native-image tool] + +. Compile it natively. This takes a few minutes: ++ +[source, shell] +---- +$ mvn package -Dnative +---- + +. Run the native executable: ++ +[source, shell] +---- +$ ./target/*-runner +---- + +. Visit http://localhost:8080 in your browser. + +. Click on the *Solve* button. + +== More information + +Visit https://timefold.ai[timefold.ai]. diff --git a/use-cases/bed-allocation/pom.xml b/use-cases/bed-allocation/pom.xml new file mode 100644 index 0000000000..4ad1a34b0e --- /dev/null +++ b/use-cases/bed-allocation/pom.xml @@ -0,0 +1,223 @@ + + + 4.0.0 + + org.acme + timefold-solver-quarkus-bed-allocation-quickstart + 1.0-SNAPSHOT + + + 17 + UTF-8 + + 3.8.0 + 999-SNAPSHOT + + 3.12.1 + 3.3.1 + 3.2.5 + + + + + + io.quarkus + quarkus-bom + ${version.io.quarkus} + pom + import + + + ai.timefold.solver + timefold-solver-bom + ${version.ai.timefold.solver} + pom + import + + + + + + + io.quarkus + quarkus-resteasy + + + io.quarkus + quarkus-resteasy-jackson + + + io.quarkus + quarkus-smallrye-openapi + + + ai.timefold.solver + timefold-solver-quarkus + + + ai.timefold.solver + timefold-solver-quarkus-jackson + + + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + ai.timefold.solver + timefold-solver-test + test + + + org.awaitility + awaitility + test + + + org.assertj + assertj-core + 3.25.3 + test + + + + + io.quarkus + quarkus-webjars-locator + + + ai.timefold.solver + timefold-solver-webui + runtime + + + org.webjars + bootstrap + 5.2.3 + runtime + + + org.webjars + jquery + 3.6.4 + runtime + + + org.webjars + font-awesome + 5.15.1 + runtime + + + org.webjars.npm + js-joda + 1.11.0 + runtime + + + org.webjars.npm + js-joda__locale_en-us + 3.1.0 + runtime + + + + + + + maven-resources-plugin + ${version.resources.plugin} + + + maven-compiler-plugin + ${version.compiler.plugin} + + + io.quarkus + quarkus-maven-plugin + ${version.io.quarkus} + + + + build + + + + + + maven-surefire-plugin + ${version.surefire.plugin} + + + org.jboss.logmanager.LogManager + + + + + + + + + native + + + native + + + + + + maven-failsafe-plugin + ${version.surefire.plugin} + + + + integration-test + verify + + + + + ${project.build.directory}/${project.build.finalName}-runner + + + + + + + + + + native + + native + + + + container + + + container + + + + + io.quarkus + quarkus-container-image-jib + + + + true + + + + + diff --git a/use-cases/bed-allocation/quarkus-bed-scheduling-screenshot.png b/use-cases/bed-allocation/quarkus-bed-scheduling-screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..def37f09744752ad6fec9539457ff0270cc5543c GIT binary patch literal 119917 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Ð`6S^Zymc|B8EPY) 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%0A(!Pw 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 departments; + @ProblemFactCollectionProperty + private List departmentSpecialties; + @ProblemFactCollectionProperty + private List rooms; + @ProblemFactCollectionProperty + @ValueRangeProvider + private List beds; + @PlanningEntityCollectionProperty + private List stays; + + @PlanningScore + private HardMediumSoftScore score; + + private SolverStatus solverStatus; + + // No-arg constructor required for Timefold + public BedPlan() { + } + + public BedPlan(HardMediumSoftScore score, SolverStatus solverStatus) { + this.score = score; + this.solverStatus = solverStatus; + } + + // ************************************************************************ + // Getters and setters + // ************************************************************************ + public List getDepartments() { + return departments; + } + + public void setDepartments(List departments) { + this.departments = departments; + } + + public void setDepartmentSpecialties(List departmentSpecialties) { + this.departmentSpecialties = departmentSpecialties; + } + + public List getDepartmentSpecialties() { + return departmentSpecialties; + } + + public List getRooms() { + return rooms; + } + + public void setRooms(List rooms) { + this.rooms = rooms; + } + + public List getBeds() { + return beds; + } + + public void setBeds(List beds) { + this.beds = beds; + } + + public List getStays() { + return stays; + } + + public void setStays(List stays) { + this.stays = stays; + } + + public HardMediumSoftScore getScore() { + return score; + } + + public void setScore(HardMediumSoftScore score) { + this.score = score; + } + + public SolverStatus getSolverStatus() { + return solverStatus; + } + + public void setSolverStatus(SolverStatus solverStatus) { + this.solverStatus = solverStatus; + } + +} diff --git a/use-cases/bed-allocation/src/main/java/org/acme/bedallocation/domain/Department.java b/use-cases/bed-allocation/src/main/java/org/acme/bedallocation/domain/Department.java new file mode 100644 index 0000000000..c83e0b9f8d --- /dev/null +++ b/use-cases/bed-allocation/src/main/java/org/acme/bedallocation/domain/Department.java @@ -0,0 +1,123 @@ +package org.acme.bedallocation.domain; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import ai.timefold.solver.core.api.domain.lookup.PlanningId; + +import com.fasterxml.jackson.annotation.JsonIdentityInfo; +import com.fasterxml.jackson.annotation.ObjectIdGenerators; + +@JsonIdentityInfo(scope = Department.class, generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") +public class Department { + + @PlanningId + private String id; + private Map specialtyToPriority; + private String name; + private Integer minimumAge = null; + private Integer maximumAge = null; + private List rooms; + + public Department() { + this.specialtyToPriority = new HashMap<>(); + } + + public Department(String id, String name) { + this.id = id; + this.name = name; + this.specialtyToPriority = new HashMap<>(); + } + + public void addRoom(Room room) { + if (rooms == null) { + rooms = new LinkedList<>(); + } + if (!rooms.contains(room)) { + rooms.add(room); + } + } + + public int countHardDisallowedStay(Stay stay) { + return countDisallowedPatientAge(stay.getPatientAge()); + } + + public int countDisallowedPatientAge(int patientAge) { + int count = 0; + if (minimumAge != null && patientAge < minimumAge) { + count += 100; + } + if (maximumAge != null && patientAge > maximumAge) { + count += 100; + } + return count; + } + + @Override + public String toString() { + return name; + } + + // ************************************************************************ + // Getters and setters + // ************************************************************************ + + public Map getSpecialtyToPriority() { + return specialtyToPriority; + } + + public void setSpecialtyToPriority(Map specialtyToPriority) { + this.specialtyToPriority = specialtyToPriority; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Integer getMinimumAge() { + return minimumAge; + } + + public void setMinimumAge(Integer minimumAge) { + this.minimumAge = minimumAge; + } + + public Integer getMaximumAge() { + return maximumAge; + } + + public void setMaximumAge(Integer maximumAge) { + this.maximumAge = maximumAge; + } + + public List getRooms() { + return rooms; + } + + public void setRooms(List rooms) { + this.rooms = rooms; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Department that)) return false; + return Objects.equals(getId(), that.getId()); + } + + @Override + public int hashCode() { + return getId().hashCode(); + } +} diff --git a/use-cases/bed-allocation/src/main/java/org/acme/bedallocation/domain/DepartmentSpecialty.java b/use-cases/bed-allocation/src/main/java/org/acme/bedallocation/domain/DepartmentSpecialty.java new file mode 100644 index 0000000000..f11b2e3135 --- /dev/null +++ b/use-cases/bed-allocation/src/main/java/org/acme/bedallocation/domain/DepartmentSpecialty.java @@ -0,0 +1,62 @@ +package org.acme.bedallocation.domain; + +import ai.timefold.solver.core.api.domain.lookup.PlanningId; + +public class DepartmentSpecialty { + + @PlanningId + private String id; + + private Department department; + private String specialty; + + private int priority; // AKA choice + + public DepartmentSpecialty() { + } + + public DepartmentSpecialty(String id, Department department, String specialty, int priority) { + this.id = id; + this.department = department; + this.specialty = specialty; + this.priority = priority; + } + + @Override + public String toString() { + return department + "-" + specialty; + } + + // ************************************************************************ + // Getters and setters + // ************************************************************************ + + public String getId() { + return id; + } + + public Department getDepartment() { + return department; + } + + public void setDepartment(Department department) { + this.department = department; + } + + public String getSpecialty() { + return specialty; + } + + public void setSpecialty(String specialty) { + this.specialty = specialty; + } + + public int getPriority() { + return priority; + } + + public void setPriority(int priority) { + this.priority = priority; + } + +} diff --git a/use-cases/bed-allocation/src/main/java/org/acme/bedallocation/domain/Gender.java b/use-cases/bed-allocation/src/main/java/org/acme/bedallocation/domain/Gender.java new file mode 100644 index 0000000000..3da3e5e939 --- /dev/null +++ b/use-cases/bed-allocation/src/main/java/org/acme/bedallocation/domain/Gender.java @@ -0,0 +1,26 @@ +package org.acme.bedallocation.domain; + +public enum Gender { + MALE("M"), + FEMALE("F"); + + public static Gender valueOfCode(String code) { + for (Gender gender : values()) { + if (code.equalsIgnoreCase(gender.getCode())) { + return gender; + } + } + return null; + } + + private final String code; + + Gender(String code) { + this.code = code; + } + + public String getCode() { + return code; + } + +} diff --git a/use-cases/bed-allocation/src/main/java/org/acme/bedallocation/domain/GenderLimitation.java b/use-cases/bed-allocation/src/main/java/org/acme/bedallocation/domain/GenderLimitation.java new file mode 100644 index 0000000000..10974963c8 --- /dev/null +++ b/use-cases/bed-allocation/src/main/java/org/acme/bedallocation/domain/GenderLimitation.java @@ -0,0 +1,28 @@ +package org.acme.bedallocation.domain; + +public enum GenderLimitation { + ANY_GENDER("N"), // mixed + MALE_ONLY("M"), + FEMALE_ONLY("F"), + SAME_GENDER("D"); // dependent on the first + + public static GenderLimitation valueOfCode(String code) { + for (GenderLimitation gender : GenderLimitation.values()) { + if (code.equalsIgnoreCase(gender.getCode())) { + return gender; + } + } + return null; + } + + private final String code; + + GenderLimitation(String code) { + this.code = code; + } + + public String getCode() { + return code; + } + +} diff --git a/use-cases/bed-allocation/src/main/java/org/acme/bedallocation/domain/Room.java b/use-cases/bed-allocation/src/main/java/org/acme/bedallocation/domain/Room.java new file mode 100644 index 0000000000..366afedf44 --- /dev/null +++ b/use-cases/bed-allocation/src/main/java/org/acme/bedallocation/domain/Room.java @@ -0,0 +1,125 @@ +package org.acme.bedallocation.domain; + +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; + +import ai.timefold.solver.core.api.domain.lookup.PlanningId; + +import com.fasterxml.jackson.annotation.JsonIdentityInfo; +import com.fasterxml.jackson.annotation.ObjectIdGenerators; + +@JsonIdentityInfo(scope = Room.class, generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") +public class Room { + + @PlanningId + private String id; + private String name; + private Department department; + private int capacity; + private GenderLimitation genderLimitation; + private List equipments; + private List beds; + + public Room() { + this.equipments = new LinkedList<>(); + this.beds = new LinkedList<>(); + } + + public Room(String id) { + this.id = id; + this.name = id; + this.equipments = new LinkedList<>(); + this.beds = new LinkedList<>(); + } + + public Room(String id, String name, Department department) { + this.id = id; + this.name = name; + this.department = department; + this.department.addRoom(this); + this.equipments = new LinkedList<>(); + this.beds = new LinkedList<>(); + } + + public void addBed(Bed bed) { + if (!beds.contains(bed)) { + beds.add(bed); + } + } + + @Override + public String toString() { + return name; + } + + // ************************************************************************ + // Getters and setters + // ************************************************************************ + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Department getDepartment() { + return department; + } + + public void setDepartment(Department department) { + this.department = department; + } + + public int getCapacity() { + return capacity; + } + + public void setCapacity(int capacity) { + this.capacity = capacity; + } + + public GenderLimitation getGenderLimitation() { + return genderLimitation; + } + + public void setGenderLimitation(GenderLimitation genderLimitation) { + this.genderLimitation = genderLimitation; + } + + public List getEquipments() { + return equipments; + } + + public void setEquipments(List equipments) { + this.equipments = equipments; + } + + public List getBeds() { + return beds; + } + + public void setBeds(List beds) { + this.beds = beds; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (!(o instanceof Room room)) + return false; + return Objects.equals(getId(), room.getId()); + } + + @Override + public int hashCode() { + return getId().hashCode(); + } +} diff --git a/use-cases/bed-allocation/src/main/java/org/acme/bedallocation/domain/Stay.java b/use-cases/bed-allocation/src/main/java/org/acme/bedallocation/domain/Stay.java new file mode 100644 index 0000000000..cddbfb5e45 --- /dev/null +++ b/use-cases/bed-allocation/src/main/java/org/acme/bedallocation/domain/Stay.java @@ -0,0 +1,199 @@ +package org.acme.bedallocation.domain; + +import static java.time.temporal.ChronoUnit.DAYS; + +import java.time.LocalDate; +import java.util.LinkedList; +import java.util.List; + +import ai.timefold.solver.core.api.domain.entity.PlanningEntity; +import ai.timefold.solver.core.api.domain.lookup.PlanningId; +import ai.timefold.solver.core.api.domain.variable.PlanningVariable; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +@PlanningEntity +public class Stay { + + @PlanningId + private String id; + private String patientName; + private Gender patientGender; + private int patientAge; + private Integer patientPreferredMaximumRoomCapacity; + private List patientRequiredEquipments; + private List patientPreferredEquipments; + private LocalDate arrivalDate; + private LocalDate departureDate; + private String specialty; + @PlanningVariable(allowsUnassigned = true) + private Bed bed; + + public Stay() { + } + + public Stay(String id, String patientName) { + this.id = id; + this.patientName = patientName; + this.patientRequiredEquipments = new LinkedList<>(); + this.patientPreferredEquipments = new LinkedList<>(); + } + + public Stay(String id, LocalDate arrivalDate, LocalDate departureDate, String specialty, Bed bed) { + this.id = id; + this.arrivalDate = arrivalDate; + this.departureDate = departureDate; + this.specialty = specialty; + this.bed = bed; + this.patientRequiredEquipments = new LinkedList<>(); + this.patientPreferredEquipments = new LinkedList<>(); + } + + @JsonIgnore + public int getNightCount() { + return (int) DAYS.between(arrivalDate, departureDate) + 1; // TODO is + 1 still desired? + } + + public int calculateSameNightCount(Stay other) { + LocalDate maxArrivalDate = arrivalDate.compareTo(other.arrivalDate) < 0 ? other.arrivalDate : arrivalDate; + LocalDate minDepartureDate = departureDate.compareTo(other.departureDate) < 0 ? departureDate : other.departureDate; + return Math.max(0, (int) DAYS.between(maxArrivalDate, minDepartureDate) + 1); // TODO is + 1 still desired? + } + + @JsonIgnore + public Room getRoom() { + if (bed == null) { + return null; + } + return bed.getRoom(); + } + + @JsonIgnore + public int getRoomCapacity() { + if (bed == null) { + return Integer.MIN_VALUE; + } + return bed.getRoom().getCapacity(); + } + + @JsonIgnore + public Department getDepartment() { + if (bed == null) { + return null; + } + return bed.getRoom().getDepartment(); + } + + @JsonIgnore + public GenderLimitation getRoomGenderLimitation() { + if (bed == null) { + return null; + } + return bed.getRoom().getGenderLimitation(); + } + + public void addRequiredEquipment(String equipment) { + if (!patientRequiredEquipments.contains(equipment)) { + this.patientRequiredEquipments.add(equipment); + } + } + + public void addPreferredEquipment(String equipment) { + if (!patientPreferredEquipments.contains(equipment)) { + this.patientPreferredEquipments.add(equipment); + } + } + + @Override + public String toString() { + return patientName + "(" + arrivalDate + "-" + departureDate + ")"; + } + + // ************************************************************************ + // Getters and setters + // ************************************************************************ + + public String getId() { + return id; + } + + public String getPatientName() { + return patientName; + } + + public void setPatientName(String patientName) { + this.patientName = patientName; + } + + public void setPatientGender(Gender patientGender) { + this.patientGender = patientGender; + } + + public Gender getPatientGender() { + return patientGender; + } + + public void setPatientAge(int patientAge) { + this.patientAge = patientAge; + } + + public int getPatientAge() { + return patientAge; + } + + public Integer getPatientPreferredMaximumRoomCapacity() { + return patientPreferredMaximumRoomCapacity; + } + + public void setPatientPreferredMaximumRoomCapacity(Integer patientPreferredMaximumRoomCapacity) { + this.patientPreferredMaximumRoomCapacity = patientPreferredMaximumRoomCapacity; + } + + public List getPatientRequiredEquipments() { + return patientRequiredEquipments; + } + + public void setPatientRequiredEquipments(List patientRequiredEquipments) { + this.patientRequiredEquipments = patientRequiredEquipments; + } + + public List getPatientPreferredEquipments() { + return patientPreferredEquipments; + } + + public void setPatientPreferredEquipments(List patientPreferredEquipments) { + this.patientPreferredEquipments = patientPreferredEquipments; + } + + public LocalDate getArrivalDate() { + return arrivalDate; + } + + public void setArrivalDate(LocalDate arrivalDate) { + this.arrivalDate = arrivalDate; + } + + public LocalDate getDepartureDate() { + return departureDate; + } + + public void setDepartureDate(LocalDate departureDate) { + this.departureDate = departureDate; + } + + public String getSpecialty() { + return specialty; + } + + public void setSpecialty(String specialty) { + this.specialty = specialty; + } + + public Bed getBed() { + return bed; + } + + public void setBed(Bed bed) { + this.bed = bed; + } +} diff --git a/use-cases/bed-allocation/src/main/java/org/acme/bedallocation/rest/BedSchedulingDemoResource.java b/use-cases/bed-allocation/src/main/java/org/acme/bedallocation/rest/BedSchedulingDemoResource.java new file mode 100644 index 0000000000..b70d16b1af --- /dev/null +++ b/use-cases/bed-allocation/src/main/java/org/acme/bedallocation/rest/BedSchedulingDemoResource.java @@ -0,0 +1,38 @@ +package org.acme.bedallocation.rest; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +import org.acme.bedallocation.domain.BedPlan; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; + +@Tag(name = "Demo data", description = "Timefold-provided demo bed scheduling data.") +@Path("demo-data") +public class BedSchedulingDemoResource { + + private final DemoDataGenerator dataGenerator; + + @Inject + public BedSchedulingDemoResource(DemoDataGenerator dataGenerator) { + this.dataGenerator = dataGenerator; + } + + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "Unsolved demo schedule.", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = BedPlan.class))) }) + @Operation(summary = "Find an unsolved demo schedule by ID.") + @GET + public Response generate() { + return Response.ok(dataGenerator.generateDemoData()).build(); + } + +} diff --git a/use-cases/bed-allocation/src/main/java/org/acme/bedallocation/rest/BedSchedulingResource.java b/use-cases/bed-allocation/src/main/java/org/acme/bedallocation/rest/BedSchedulingResource.java new file mode 100644 index 0000000000..ba61593a6a --- /dev/null +++ b/use-cases/bed-allocation/src/main/java/org/acme/bedallocation/rest/BedSchedulingResource.java @@ -0,0 +1,230 @@ +package org.acme.bedallocation.rest; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; +import java.util.Map.Entry; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import jakarta.inject.Inject; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +import ai.timefold.solver.core.api.score.analysis.ScoreAnalysis; +import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore; +import ai.timefold.solver.core.api.solver.ScoreAnalysisFetchPolicy; +import ai.timefold.solver.core.api.solver.SolutionManager; +import ai.timefold.solver.core.api.solver.SolverManager; +import ai.timefold.solver.core.api.solver.SolverStatus; + +import org.acme.bedallocation.domain.BedPlan; +import org.acme.bedallocation.rest.exception.ErrorInfo; +import org.acme.bedallocation.rest.exception.ScheduleSolverException; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Tag(name = "Bed Scheduling", + description = "Bed Scheduling service assigning beds for patient stays.") +@Path("schedules") +public class BedSchedulingResource { + + private static final Logger LOGGER = LoggerFactory.getLogger(BedSchedulingResource.class); + private static final int MAX_JOBS_CACHE_SIZE = 2; + + private final SolverManager solverManager; + private final SolutionManager solutionManager; + private final ConcurrentMap jobIdToJob = new ConcurrentHashMap<>(); + + // Workaround to make Quarkus CDI happy. Do not use. + public BedSchedulingResource() { + this.solverManager = null; + this.solutionManager = null; + } + + @Inject + public BedSchedulingResource(SolverManager solverManager, + SolutionManager solutionManager) { + this.solverManager = solverManager; + this.solutionManager = solutionManager; + } + + @Operation(summary = "List the job IDs of all submitted schedules.") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "List of all job IDs.", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(type = SchemaType.ARRAY, implementation = String.class))) }) + @GET + @Produces(MediaType.APPLICATION_JSON) + public Collection list() { + return jobIdToJob.keySet(); + } + + @Operation(summary = "Submit a schedule to start solving as soon as CPU resources are available.") + @APIResponses(value = { + @APIResponse(responseCode = "202", + description = "The job ID. Use that ID to get the solution with the other methods.", + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(implementation = String.class))) }) + @POST + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces(MediaType.TEXT_PLAIN) + public String solve(BedPlan problem) { + String jobId = UUID.randomUUID().toString(); + jobIdToJob.put(jobId, Job.ofSchedule(problem)); + solverManager.solveBuilder() + .withProblemId(jobId) + .withProblemFinder(id -> jobIdToJob.get(jobId).schedule) + .withBestSolutionConsumer(solution -> jobIdToJob.put(jobId, Job.ofSchedule(solution))) + .withExceptionHandler((id, exception) -> { + jobIdToJob.put(id, Job.ofException(exception)); + LOGGER.error("Failed solving jobId ({}).", id, exception); + }) + .run(); + cleanJobs(); + return jobId; + } + + @Operation(summary = "Submit a schedule to analyze its score.") + @APIResponses(value = { + @APIResponse(responseCode = "202", + description = "Resulting score analysis, optionally without constraint matches.", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = ScoreAnalysis.class))) }) + @PUT + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces(MediaType.APPLICATION_JSON) + @Path("analyze") + public ScoreAnalysis analyze(BedPlan problem, + @QueryParam("fetchPolicy") ScoreAnalysisFetchPolicy fetchPolicy) { + return fetchPolicy == null ? solutionManager.analyze(problem) : solutionManager.analyze(problem, fetchPolicy); + } + + @Operation( + summary = "Get the solution and score for a given job ID. This is the best solution so far, as it might still be running or not even started.") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "The best solution of the schedule so far.", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = BedPlan.class))), + @APIResponse(responseCode = "404", description = "No schedule found.", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = ErrorInfo.class))), + @APIResponse(responseCode = "500", description = "Exception during solving a schedule.", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = ErrorInfo.class))) + }) + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("{jobId}") + public BedPlan getSchedule( + @Parameter(description = "The job ID returned by the POST method.") @PathParam("jobId") String jobId) { + BedPlan schedule = getScheduleAndCheckForExceptions(jobId); + SolverStatus solverStatus = solverManager.getSolverStatus(jobId); + schedule.setSolverStatus(solverStatus); + return schedule; + } + + @Operation( + summary = "Get the schedule status and score for a given job ID.") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "The schedule status and the best score so far.", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = BedPlan.class))), + @APIResponse(responseCode = "404", description = "No schedule found.", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = ErrorInfo.class))), + @APIResponse(responseCode = "500", description = "Exception during solving a schedule.", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = ErrorInfo.class))) + }) + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("{jobId}/status") + public BedPlan getStatus( + @Parameter(description = "The job ID returned by the POST method.") @PathParam("jobId") String jobId) { + BedPlan schedule = getScheduleAndCheckForExceptions(jobId); + SolverStatus solverStatus = solverManager.getSolverStatus(jobId); + return new BedPlan(schedule.getScore(), solverStatus); + } + + @Operation( + summary = "Terminate solving for a given job ID. Returns the best solution of the schedule so far, as it might still be running or not even started.") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "The best solution of the schedule so far.", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = BedPlan.class))), + @APIResponse(responseCode = "404", description = "No schedule found.", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = ErrorInfo.class))), + @APIResponse(responseCode = "500", description = "Exception during solving a schedule.", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = ErrorInfo.class))) + }) + @DELETE + @Produces(MediaType.APPLICATION_JSON) + @Path("{jobId}") + public BedPlan terminateSolving( + @Parameter(description = "The job ID returned by the POST method.") @PathParam("jobId") String jobId) { + solverManager.terminateEarly(jobId); + return getSchedule(jobId); + } + + private BedPlan getScheduleAndCheckForExceptions(String jobId) { + Job job = jobIdToJob.get(jobId); + if (job == null) { + throw new ScheduleSolverException(jobId, Response.Status.NOT_FOUND, "No schedule found."); + } + if (job.exception != null) { + throw new ScheduleSolverException(jobId, job.exception); + } + return job.schedule; + } + + /** + * The method retains only the records of the last MAX_JOBS_CACHE_SIZE completed jobs by removing the oldest ones. + */ + private void cleanJobs() { + if (jobIdToJob.size() <= MAX_JOBS_CACHE_SIZE) { + return; + } + List jobsToRemove = jobIdToJob.entrySet().stream() + .filter(e -> getStatus(e.getKey()).getSolverStatus() != SolverStatus.NOT_SOLVING) + .filter(e -> jobIdToJob.get(e.getKey()).schedule() != null) + .sorted((j1, j2) -> jobIdToJob.get(j1.getKey()).createdAt().compareTo(jobIdToJob.get(j2.getKey()).createdAt())) + .map(Entry::getKey) + .toList(); + if (jobsToRemove.size() > MAX_JOBS_CACHE_SIZE) { + for (int i = 0; i < jobsToRemove.size() - MAX_JOBS_CACHE_SIZE; i++) { + jobIdToJob.remove(jobsToRemove.get(i)); + } + } + } + + private record Job(BedPlan schedule, LocalDateTime createdAt, Throwable exception) { + + static Job ofSchedule(BedPlan schedule) { + return new Job(schedule, LocalDateTime.now(), null); + } + + static Job ofException(Throwable error) { + return new Job(null, LocalDateTime.now(), error); + } + } +} diff --git a/use-cases/bed-allocation/src/main/java/org/acme/bedallocation/rest/DemoDataGenerator.java b/use-cases/bed-allocation/src/main/java/org/acme/bedallocation/rest/DemoDataGenerator.java new file mode 100644 index 0000000000..df538cf67a --- /dev/null +++ b/use-cases/bed-allocation/src/main/java/org/acme/bedallocation/rest/DemoDataGenerator.java @@ -0,0 +1,343 @@ +package org.acme.bedallocation.rest; + +import static java.time.temporal.TemporalAdjusters.firstInMonth; +import static org.acme.bedallocation.domain.Gender.FEMALE; +import static org.acme.bedallocation.domain.Gender.MALE; +import static org.acme.bedallocation.domain.GenderLimitation.SAME_GENDER; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Random; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.IntStream; + +import jakarta.enterprise.context.ApplicationScoped; + +import ai.timefold.solver.core.impl.util.MutableReference; +import ai.timefold.solver.core.impl.util.Pair; + +import org.acme.bedallocation.domain.Bed; +import org.acme.bedallocation.domain.BedPlan; +import org.acme.bedallocation.domain.Department; +import org.acme.bedallocation.domain.DepartmentSpecialty; +import org.acme.bedallocation.domain.Room; +import org.acme.bedallocation.domain.Stay; + +@ApplicationScoped +public class DemoDataGenerator { + + private static final List SPECIALTIES = List.of("Specialty1", "Specialty2", "Specialty3"); + private static final String TELEMETRY = "telemetry"; + private static final String TELEVISION = "television"; + private static final String OXYGEN = "oxygen"; + private static final String NITROGEN = "nitrogen"; + private static final List EQUIPMENTS = List.of(TELEMETRY, TELEVISION, OXYGEN, NITROGEN); + private final Random random = new Random(0); + + public BedPlan generateDemoData() { + BedPlan schedule = new BedPlan(); + // Department + List departments = List.of(new Department("1", "Department")); + schedule.setDepartments(departments); + schedule.getDepartments().get(0).getSpecialtyToPriority().put(SPECIALTIES.get(0), 1); + schedule.getDepartments().get(0).getSpecialtyToPriority().put(SPECIALTIES.get(1), 2); + schedule.getDepartments().get(0).getSpecialtyToPriority().put(SPECIALTIES.get(2), 2); + schedule.setDepartmentSpecialties(departments.stream() + .flatMap(d -> d.getSpecialtyToPriority().entrySet().stream() + .map(e -> new DepartmentSpecialty("%s-%s".formatted(d.getId(), e.getKey()), d, e.getKey(), + e.getValue())) + .toList() + .stream()) + .toList()); + + // Rooms + int countRooms = 10; + schedule.getDepartments().get(0).setRooms(generateRooms(countRooms, departments)); + schedule.setRooms(departments.stream().flatMap(d -> d.getRooms().stream()).toList()); + // Beds + generateBeds(schedule.getRooms()); + schedule.setBeds(departments.stream() + .flatMap(d -> d.getRooms().stream()) + .flatMap(r -> r.getBeds().stream()) + .toList()); + // Stays + LocalDate firstMonthMonday = LocalDate.now().with(firstInMonth(DayOfWeek.MONDAY)); // First Monday of the month + List dates = new ArrayList<>(7); + dates.add(firstMonthMonday); + int countDays = 28; + for (int i = 1; i < countDays; i++) { + dates.add(firstMonthMonday.with(firstInMonth(DayOfWeek.MONDAY)).plusDays(i)); + } + List stays = generateStays(countDays, schedule.getBeds(), SPECIALTIES); + // Patients + generatePatients(stays); + // Dates + schedule.setStays(generateStayDates(stays, schedule.getRooms(), dates)); + return schedule; + } + + private List generateRooms(int size, List departments) { + List rooms = IntStream.range(0, size) + .mapToObj(i -> new Room(String.valueOf(i), "%s%d".formatted("Room", i), departments.get(0))) + .toList(); + + // Room gender limitation + applyRandomValue(size, rooms, r -> r.getGenderLimitation() == null, + r -> r.setGenderLimitation(SAME_GENDER)); + + // Room capacity + List> capacityValues = List.of( + new Pair<>(0.8f, 1), // 20% for capacity 1 + new Pair<>(0.1f, 2), + new Pair<>(0.1f, 3)); + capacityValues.forEach(c -> applyRandomValue((int) (size * c.key()), rooms, r -> r.getCapacity() == 0, + r -> r.setCapacity(c.value()))); + rooms.stream() + .filter(r -> r.getCapacity() == 0) + .toList() + .forEach(r -> r.setCapacity(1)); + + // Room equipments + // 11% - 1 equipment; 16% 2 equipments; 42% 3 equipments; 31% 4 equipments + List countEquipments = List.of(0.11, 0.27, 0.69, 1d); + Consumer equipmentConsumer = room -> { + double count = random.nextDouble(); + int numEquipments = IntStream.range(0, countEquipments.size()) + .filter(i -> count <= countEquipments.get(i)) + .findFirst() + .getAsInt() + 1; + List roomEquipments = new LinkedList<>(EQUIPMENTS); + Collections.shuffle(roomEquipments, random); + room.setEquipments(roomEquipments.subList(0, numEquipments)); + }; + // Only 76% of rooms have equipment + applyRandomValue((int) (0.76 * size), rooms, r -> r.getEquipments().isEmpty(), equipmentConsumer); + + return rooms; + } + + private void generateBeds(List rooms) { + for (Room room : rooms) { + IntStream.range(0, room.getCapacity()) + .forEach(i -> room.addBed(new Bed("%s-bed%d".formatted(room.getId(), i), room, i))); + } + } + + private List generateStays(int countDays, List beds, List specialties) { + List stays = IntStream.range(0, countDays * beds.size()) + .mapToObj(i -> new Stay("stay-%d".formatted(i), "patient-%d".formatted(i))) + .toList(); + + // specialty - 27% Specialty1; 36% Specialty2; 37% Specialty3 + applyRandomValue((int) (0.27 * stays.size()), stays, s -> s.getSpecialty() == null, + s -> s.setSpecialty(specialties.get(0))); + applyRandomValue((int) (0.36 * stays.size()), stays, s -> s.getSpecialty() == null, + s -> s.setSpecialty(specialties.get(1))); + applyRandomValue((int) (0.37 * stays.size()), stays, s -> s.getSpecialty() == null, + s -> s.setSpecialty(specialties.get(2))); + stays.stream() + .filter(s -> s.getSpecialty() == null) + .toList() + .forEach(s -> s.setSpecialty(specialties.get(0))); + return stays; + } + + private List generateStayDates(List stays, List rooms, List dates) { + List updatedStays = new ArrayList<>(stays); + LocalDate initialDate = dates.get(0); + LocalDate maxDate = dates.get(dates.size() - 1); + List> periodCount = List.of( + new Pair<>(0.05f, 1), // 5% one day + new Pair<>(0.30f, 2), // 25% two days, etc + new Pair<>(0.95f, 3), + new Pair<>(1f, 4)); + for (int i = 0; i < rooms.size(); i++) { + MutableReference currentDate = new MutableReference<>(LocalDate.from(initialDate)); + while (currentDate.getValue().isBefore(maxDate)) { + double countDays = random.nextDouble(); + int numDays = periodCount.stream() + .filter(p -> countDays <= p.key()) + .mapToInt(Pair::value) + .findFirst() + .getAsInt(); + MutableReference nextDate = new MutableReference<>(currentDate.getValue().plusDays(numDays)); + if (nextDate.getValue().isAfter(maxDate)) { + nextDate.setValue(maxDate); + } + applyRandomValue(1, updatedStays, stay -> stay.getArrivalDate() == null, stay -> { + stay.setArrivalDate(currentDate.getValue()); + stay.setDepartureDate(nextDate.getValue()); + }); + currentDate.setValue(nextDate.getValue().plusDays(1)); + } + } + return updatedStays.stream().filter(s -> s.getArrivalDate() != null).toList(); + } + + private void generatePatients(List stays) { + // 50% MALE - 50% FEMALE + applyRandomValue((int) (stays.size() * 0.5), stays, p -> p.getPatientGender() == null, p -> p.setPatientGender(MALE)); + applyRandomValue((int) (stays.size() * 0.5), stays, p -> p.getPatientGender() == null, p -> p.setPatientGender(FEMALE)); + stays.stream().filter(p -> p.getPatientGender() == null).forEach(p -> p.setPatientGender(MALE)); + + // Age group + List> ageValues = List.of( + new Pair<>(0.1f, new Integer[] { 0, 10 }), // 10% for age group [0, 10] + new Pair<>(0.09f, new Integer[] { 11, 20 }), + new Pair<>(0.07f, new Integer[] { 21, 30 }), + new Pair<>(0.1f, new Integer[] { 31, 40 }), + new Pair<>(0.09f, new Integer[] { 41, 50 }), + new Pair<>(0.08f, new Integer[] { 51, 60 }), + new Pair<>(0.08f, new Integer[] { 61, 70 }), + new Pair<>(0.13f, new Integer[] { 71, 80 }), + new Pair<>(0.08f, new Integer[] { 81, 90 }), + new Pair<>(0.09f, new Integer[] { 91, 100 }), + new Pair<>(0.09f, new Integer[] { 101, 109 })); + + ageValues.forEach(ag -> applyRandomValue((int) (ag.key() * stays.size()), stays, a -> a.getPatientAge() == -1, + p -> p.setPatientAge(random.nextInt(ag.value()[0], ag.value()[1] + 1)))); + stays.stream() + .filter(p -> p.getPatientAge() == -1) + .toList() + .forEach(p -> p.setPatientAge(71)); + + // Preferred maximum capacity + List> capacityValues = List.of( + new Pair<>(0.34f, 1), // 34% for capacity 1 + new Pair<>(0.68f, 2), + new Pair<>(1f, 3)); + for (Stay stay : stays) { + double count = random.nextDouble(); + IntStream.range(0, capacityValues.size()) + .filter(i -> count <= capacityValues.get(i).key()) + .map(i -> capacityValues.get(i).value()) + .findFirst() + .ifPresent(stay::setPatientPreferredMaximumRoomCapacity); + } + + // Required equipments - 12% no equipments; 47% one equipment; 41% two equipments + // one required equipment + List> oneEquipmentValues = List.of( + new Pair<>(0.22f, NITROGEN), // 22% for nitrogen + new Pair<>(0.47f, TELEVISION), + new Pair<>(0.72f, OXYGEN), + new Pair<>(1f, TELEMETRY)); + BiConsumer>> oneEquipmentConsumer = (stay, values) -> { + double count = random.nextDouble(); + IntStream.range(0, values.size()) + .filter(i -> count <= values.get(i).key()) + .mapToObj(i -> values.get(i).value()) + .findFirst() + .ifPresent(stay::addRequiredEquipment); + }; + applyRandomValue((int) (stays.size() * 0.47), stays, oneEquipmentValues, + p -> p.getPatientRequiredEquipments().isEmpty(), + oneEquipmentConsumer); + // Two required equipments + List> twoEquipmentValues = List.of( + new Pair<>(0.13f, NITROGEN), // 13% for nitrogen + new Pair<>(0.29f, TELEVISION), + new Pair<>(0.49f, OXYGEN), + new Pair<>(1f, TELEMETRY)); + Consumer twoEquipmentsConsumer = patient -> { + while (patient.getPatientRequiredEquipments().size() < 2) { + oneEquipmentConsumer.accept(patient, twoEquipmentValues); + } + }; + applyRandomValue((int) (stays.size() * 0.41), stays, p -> p.getPatientRequiredEquipments().isEmpty(), + twoEquipmentsConsumer); + + // Preferred equipments - 29% one equipment; 53% two equipments; 16% three equipments; 2% four equipments + // one preferred equipment + List> onePreferredEquipmentValues = List.of( + new Pair<>(0.34f, NITROGEN), // 34% for nitrogen + new Pair<>(0.63f, TELEVISION), + new Pair<>(1f, OXYGEN)); + BiConsumer>> onePreferredEquipmentConsumer = (patient, values) -> { + double count = random.nextDouble(); + IntStream.range(0, values.size()) + .filter(i -> count <= values.get(i).key()) + .mapToObj(i -> values.get(i).value()) + .findFirst() + .ifPresent(patient::addPreferredEquipment); + }; + applyRandomValue((int) (stays.size() * 0.29), stays, onePreferredEquipmentValues, + p -> p.getPatientPreferredEquipments().isEmpty(), + onePreferredEquipmentConsumer); + // two preferred equipments + List> twoPreferredEquipmentValues = List.of( + new Pair<>(0.32f, NITROGEN), // 32% for nitrogen + new Pair<>(0.62f, TELEVISION), + new Pair<>(0.90f, OXYGEN), + new Pair<>(1f, TELEMETRY)); + Consumer twoPreferredEquipmentsConsumer = patient -> { + while (patient.getPatientPreferredEquipments().size() < 2) { + onePreferredEquipmentConsumer.accept(patient, twoPreferredEquipmentValues); + } + }; + applyRandomValue((int) (stays.size() * 0.53), stays, p -> p.getPatientPreferredEquipments().isEmpty(), + twoPreferredEquipmentsConsumer); + // three preferred equipments + List> threePreferredEquipmentValues = List.of( + new Pair<>(0.26f, NITROGEN), // 26% for nitrogen + new Pair<>(0.50f, TELEVISION), + new Pair<>(0.77f, OXYGEN), + new Pair<>(1f, TELEMETRY)); + Consumer threePreferredEquipmentsConsumer = patient -> { + while (patient.getPatientPreferredEquipments().size() < 3) { + onePreferredEquipmentConsumer.accept(patient, threePreferredEquipmentValues); + } + }; + applyRandomValue((int) (stays.size() * 0.16), stays, p -> p.getPatientPreferredEquipments().isEmpty(), + threePreferredEquipmentsConsumer); + // four preferred equipments + Consumer fourPreferredEquipmentsConsumer = patient -> { + patient.addPreferredEquipment(NITROGEN); + patient.addPreferredEquipment(TELEVISION); + patient.addPreferredEquipment(OXYGEN); + patient.addPreferredEquipment(TELEMETRY); + }; + applyRandomValue((int) (stays.size() * 0.02), stays, p -> p.getPatientPreferredEquipments().isEmpty(), + fourPreferredEquipmentsConsumer); + + stays.stream() + .filter(p -> p.getPatientPreferredEquipments().isEmpty()) + .toList() + .forEach(p -> onePreferredEquipmentConsumer.accept(p, onePreferredEquipmentValues)); + } + + private void applyRandomValue(int count, List values, Predicate filter, Consumer consumer) { + int size = (int) values.stream().filter(filter).count(); + for (int i = 0; i < count; i++) { + values.stream() + .filter(filter) + .skip(size > 0 ? random.nextInt(size) : 0).findFirst() + .ifPresent(consumer::accept); + size--; + if (size < 0) { + break; + } + } + } + + private void applyRandomValue(int count, List values, L secondParam, Predicate filter, + BiConsumer consumer) { + int size = (int) values.stream().filter(filter).count(); + for (int i = 0; i < count; i++) { + values.stream() + .filter(filter) + .skip(size > 0 ? random.nextInt(size) : 0).findFirst() + .ifPresent(v -> consumer.accept(v, secondParam)); + size--; + if (size < 0) { + break; + } + } + } +} diff --git a/use-cases/bed-allocation/src/main/java/org/acme/bedallocation/rest/exception/ErrorInfo.java b/use-cases/bed-allocation/src/main/java/org/acme/bedallocation/rest/exception/ErrorInfo.java new file mode 100644 index 0000000000..62993a3112 --- /dev/null +++ b/use-cases/bed-allocation/src/main/java/org/acme/bedallocation/rest/exception/ErrorInfo.java @@ -0,0 +1,4 @@ +package org.acme.bedallocation.rest.exception; + +public record ErrorInfo(String jobId, String message) { +} diff --git a/use-cases/bed-allocation/src/main/java/org/acme/bedallocation/rest/exception/ScheduleSolverException.java b/use-cases/bed-allocation/src/main/java/org/acme/bedallocation/rest/exception/ScheduleSolverException.java new file mode 100644 index 0000000000..7527ee3e9f --- /dev/null +++ b/use-cases/bed-allocation/src/main/java/org/acme/bedallocation/rest/exception/ScheduleSolverException.java @@ -0,0 +1,30 @@ +package org.acme.bedallocation.rest.exception; + +import jakarta.ws.rs.core.Response; + +public class ScheduleSolverException extends RuntimeException { + + private final String jobId; + + private final Response.Status status; + + public ScheduleSolverException(String jobId, Response.Status status, String message) { + super(message); + this.jobId = jobId; + this.status = status; + } + + public ScheduleSolverException(String jobId, Throwable cause) { + super(cause.getMessage(), cause); + this.jobId = jobId; + this.status = Response.Status.INTERNAL_SERVER_ERROR; + } + + public String getJobId() { + return jobId; + } + + public Response.Status getStatus() { + return status; + } +} diff --git a/use-cases/bed-allocation/src/main/java/org/acme/bedallocation/rest/exception/ScheduleSolverExceptionMapper.java b/use-cases/bed-allocation/src/main/java/org/acme/bedallocation/rest/exception/ScheduleSolverExceptionMapper.java new file mode 100644 index 0000000000..b948118133 --- /dev/null +++ b/use-cases/bed-allocation/src/main/java/org/acme/bedallocation/rest/exception/ScheduleSolverExceptionMapper.java @@ -0,0 +1,19 @@ +package org.acme.bedallocation.rest.exception; + +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.Provider; + +@Provider +public class ScheduleSolverExceptionMapper implements ExceptionMapper { + + @Override + public Response toResponse(ScheduleSolverException exception) { + return Response + .status(exception.getStatus()) + .type(MediaType.APPLICATION_JSON) + .entity(new ErrorInfo(exception.getJobId(), exception.getMessage())) + .build(); + } +} diff --git a/use-cases/bed-allocation/src/main/java/org/acme/bedallocation/solver/BedAllocationConstraintProvider.java b/use-cases/bed-allocation/src/main/java/org/acme/bedallocation/solver/BedAllocationConstraintProvider.java new file mode 100644 index 0000000000..afe65b536a --- /dev/null +++ b/use-cases/bed-allocation/src/main/java/org/acme/bedallocation/solver/BedAllocationConstraintProvider.java @@ -0,0 +1,161 @@ +package org.acme.bedallocation.solver; + +import static ai.timefold.solver.core.api.score.stream.Joiners.equal; +import static ai.timefold.solver.core.api.score.stream.Joiners.filtering; +import static ai.timefold.solver.core.api.score.stream.Joiners.greaterThan; +import static ai.timefold.solver.core.api.score.stream.Joiners.lessThan; + +import java.util.function.Function; + +import ai.timefold.solver.core.api.score.buildin.hardmediumsoft.HardMediumSoftScore; +import ai.timefold.solver.core.api.score.stream.Constraint; +import ai.timefold.solver.core.api.score.stream.ConstraintFactory; +import ai.timefold.solver.core.api.score.stream.ConstraintProvider; + +import org.acme.bedallocation.domain.Department; +import org.acme.bedallocation.domain.DepartmentSpecialty; +import org.acme.bedallocation.domain.Gender; +import org.acme.bedallocation.domain.GenderLimitation; +import org.acme.bedallocation.domain.Stay; + +public class BedAllocationConstraintProvider implements ConstraintProvider { + + @Override + public Constraint[] defineConstraints(ConstraintFactory constraintFactory) { + return new Constraint[] { + // Hard constraints + sameBedInSameNight(constraintFactory), + femaleInMaleRoom(constraintFactory), + maleInFemaleRoom(constraintFactory), + differentGenderInSameGenderRoomInSameNight(constraintFactory), + departmentMinimumAge(constraintFactory), + departmentMaximumAge(constraintFactory), + requiredPatientEquipment(constraintFactory), + // Medium constraints + assignEveryPatientToABed(constraintFactory), + // Soft constraints + preferredMaximumRoomCapacity(constraintFactory), + departmentSpecialty(constraintFactory), + departmentSpecialtyNotFirstPriority(constraintFactory), + preferredPatientEquipment(constraintFactory) + }; + } + + public Constraint sameBedInSameNight(ConstraintFactory constraintFactory) { + return constraintFactory.forEachUniquePair(Stay.class, + equal(Stay::getBed)) + .filter((left, right) -> left.calculateSameNightCount(right) > 0) + .penalize(HardMediumSoftScore.ofHard(1000), + Stay::calculateSameNightCount) + .asConstraint("sameBedInSameNight"); + } + + public Constraint femaleInMaleRoom(ConstraintFactory constraintFactory) { + return constraintFactory.forEachIncludingUnassigned(Stay.class) + .filter(st -> st.getPatientGender() == Gender.FEMALE + && st.getRoomGenderLimitation() == GenderLimitation.MALE_ONLY) + .penalize(HardMediumSoftScore.ofHard(50), Stay::getNightCount) + .asConstraint("femaleInMaleRoom"); + } + + public Constraint maleInFemaleRoom(ConstraintFactory constraintFactory) { + return constraintFactory.forEachIncludingUnassigned(Stay.class) + .filter(st -> st.getPatientGender() == Gender.MALE + && st.getRoomGenderLimitation() == GenderLimitation.FEMALE_ONLY) + .penalize(HardMediumSoftScore.ofHard(50), Stay::getNightCount) + .asConstraint("maleInFemaleRoom"); + } + + public Constraint differentGenderInSameGenderRoomInSameNight(ConstraintFactory constraintFactory) { + return constraintFactory.forEach(Stay.class) + .filter(bd -> bd.getRoomGenderLimitation() == GenderLimitation.SAME_GENDER) + .join(constraintFactory.forEach(Stay.class) + .filter(st -> st.getRoomGenderLimitation() == GenderLimitation.SAME_GENDER), + equal(Stay::getRoom), + lessThan(Stay::getId), + filtering((left, right) -> left.getPatientGender() != right.getPatientGender() + && left.calculateSameNightCount(right) > 0)) + .penalize(HardMediumSoftScore.ofHard(1000), + Stay::calculateSameNightCount) + .asConstraint("differentGenderInSameGenderRoomInSameNight"); + } + + public Constraint departmentMinimumAge(ConstraintFactory constraintFactory) { + return constraintFactory.forEachIncludingUnassigned(Department.class) + .filter(d -> d.getMinimumAge() != null) + .join(constraintFactory.forEachIncludingUnassigned(Stay.class), + equal(Function.identity(), Stay::getDepartment), + greaterThan(Department::getMinimumAge, Stay::getPatientAge)) + .penalize(HardMediumSoftScore.ofHard(100), + (d, st) -> st.getNightCount()) + .asConstraint("departmentMinimumAge"); + } + + public Constraint departmentMaximumAge(ConstraintFactory constraintFactory) { + return constraintFactory.forEachIncludingUnassigned(Department.class) + .filter(d -> d.getMaximumAge() != null) + .join(constraintFactory.forEachIncludingUnassigned(Stay.class), + equal(Function.identity(), Stay::getDepartment), + lessThan(Department::getMaximumAge, Stay::getPatientAge)) + .penalize(HardMediumSoftScore.ofHard(100), + (d, st) -> st.getNightCount()) + .asConstraint("departmentMaximumAge"); + } + + public Constraint requiredPatientEquipment(ConstraintFactory constraintFactory) { + return constraintFactory.forEach(Stay.class) + .filter(st -> !st.getRoom().getEquipments().containsAll(st.getPatientRequiredEquipments())) + .penalize(HardMediumSoftScore.ofHard(50), + st -> st.getNightCount() * (int) st.getPatientRequiredEquipments().stream() + .filter(equipment -> st.getRoom().getEquipments().contains(equipment)).count()) + .asConstraint("requiredPatientEquipment"); + } + + //Medium + public Constraint assignEveryPatientToABed(ConstraintFactory constraintFactory) { + return constraintFactory.forEachIncludingUnassigned(Stay.class) + .filter(st -> st.getBed() == null) + .penalize(HardMediumSoftScore.ONE_MEDIUM, Stay::getNightCount) + .asConstraint("assignEveryPatientToABed"); + } + + //Soft + public Constraint preferredMaximumRoomCapacity(ConstraintFactory constraintFactory) { + return constraintFactory.forEach(Stay.class) + .filter(st -> st.getPatientPreferredMaximumRoomCapacity() != null + && st.getPatientPreferredMaximumRoomCapacity() < st.getRoom().getCapacity()) + .penalize(HardMediumSoftScore.ofSoft(8), Stay::getNightCount) + .asConstraint("preferredMaximumRoomCapacity"); + } + + public Constraint departmentSpecialty(ConstraintFactory constraintFactory) { + return constraintFactory.forEach(Stay.class) + .ifNotExists(DepartmentSpecialty.class, + equal(Stay::getDepartment, DepartmentSpecialty::getDepartment), + equal(Stay::getSpecialty, DepartmentSpecialty::getSpecialty)) + .penalize(HardMediumSoftScore.ofSoft(10), Stay::getNightCount) + .asConstraint("departmentSpecialty"); + } + + public Constraint departmentSpecialtyNotFirstPriority(ConstraintFactory constraintFactory) { + return constraintFactory.forEach(Stay.class) + .filter(st -> st.getSpecialty() != null) + .join(constraintFactory.forEach(DepartmentSpecialty.class) + .filter(ds -> ds.getPriority() > 1), + equal(Stay::getDepartment, DepartmentSpecialty::getDepartment), + equal(Stay::getSpecialty, DepartmentSpecialty::getSpecialty)) + .penalize(HardMediumSoftScore.ofSoft(10), + (bd, rs) -> (rs.getPriority() - 1) * bd.getNightCount()) + .asConstraint("departmentSpecialtyNotFirstPriority"); + } + + public Constraint preferredPatientEquipment(ConstraintFactory constraintFactory) { + return constraintFactory.forEach(Stay.class) + .filter(bedDesignation -> !bedDesignation.getRoom().getEquipments().containsAll( + bedDesignation.getPatientPreferredEquipments())) + .penalize(HardMediumSoftScore.ofHard(50), + st -> st.getNightCount() * (int) st.getPatientPreferredEquipments().stream() + .filter(equipment -> st.getRoom().getEquipments().contains(equipment)).count()) + .asConstraint("preferredPatientEquipment"); + } +} diff --git a/use-cases/bed-allocation/src/main/resources/META-INF/resources/app.js b/use-cases/bed-allocation/src/main/resources/META-INF/resources/app.js new file mode 100644 index 0000000000..376566683b --- /dev/null +++ b/use-cases/bed-allocation/src/main/resources/META-INF/resources/app.js @@ -0,0 +1,454 @@ +var autoRefreshIntervalId = null; +const dateFormatter = JSJoda.DateTimeFormatter.ofPattern('MM-dd'); +const roomDateFormatter = JSJoda.DateTimeFormatter.ofPattern('d MMM').withLocale(JSJodaLocale.Locale.ENGLISH); + +const byRoomPanel = document.getElementById("byRoomPanel"); +const byRoomTimelineOptions = { + timeAxis: {scale: "day"}, + orientation: {axis: "top"}, + stack: false, + xss: {disabled: true}, // Items are XSS safe through JQuery + zoomMin: 3 * 1000 * 60 * 60 * 24 // Three day in milliseconds +}; +var byRoomGroupData = new vis.DataSet(); +var byRoomItemData = new vis.DataSet(); +var byRoomTimeline = new vis.Timeline(byRoomPanel, byRoomItemData, byRoomGroupData, byRoomTimelineOptions); + +let scheduleId = null; +let loadedSchedule = null; +let viewType = "R"; + +$(document).ready(function () { + replaceQuickstartTimefoldAutoHeaderFooter(); + + $("#solveButton").click(function () { + solve(); + }); + $("#stopSolvingButton").click(function () { + stopSolving(); + }); + $("#analyzeButton").click(function () { + analyze(); + }); + $("#byRoomTab").click(function () { + viewType = "R"; + byRoomTimeline.redraw(); + refreshSchedule(); + }); + + addImportDropdownItem(); + addExportDropdownItem(); + + setupAjax(); + refreshSchedule(); +}); + +function addImportDropdownItem() { + $("#testDataButton") + .append($('

')) + .append($('Import')); + $("#uploadModalImportButton").click(importLocalFile); + $("#importTestData").click(function () { + scheduleId = null; + demoDataId = null; + $('#uploadModal').modal('show'); + }); +} + +function addExportDropdownItem() { + $("#testDataButton") + .append($('Export')); +} + +function setupAjax() { + $.ajaxSetup({ + headers: { + 'Content-Type': 'application/json', 'Accept': 'application/json,text/plain', // plain text is required by solve() returning UUID of the solver job + } + }); + + // Extend jQuery to support $.put() and $.delete() + jQuery.each(["put", "delete"], function (i, method) { + jQuery[method] = function (url, data, callback, type) { + if (jQuery.isFunction(data)) { + type = type || callback; + callback = data; + data = undefined; + } + return jQuery.ajax({ + url: url, type: method, dataType: type, data: data, success: callback + }); + }; + }); +} + +function refreshSchedule() { + let path = "/schedules/" + scheduleId; + if (scheduleId === null) { + path = "/demo-data"; + } + + $.getJSON(path, function (schedule) { + loadedSchedule = schedule; + $('#exportData').attr('href', 'data:text/plain;charset=utf-8,' + JSON.stringify(loadedSchedule)); + renderSchedule(schedule); + }) + .fail(function (xhr, ajaxOptions, thrownError) { + showError("Getting the schedule has failed.", xhr); + refreshSolvingButtons(false); + }); +} + +function renderSchedule(schedule) { + refreshSolvingButtons(schedule.solverStatus != null && schedule.solverStatus !== "NOT_SOLVING"); + $("#score").text("Score: " + (schedule.score == null ? "?" : schedule.score)); + + if (viewType === "R") { + renderScheduleByRoom(schedule); + } +} + +function renderScheduleByRoom(schedule) { + const unassignedPatients = $("#unassignedPatients"); + unassignedPatients.children().remove(); + let unassignedJobsCount = 0; + byRoomGroupData.clear(); + byRoomItemData.clear(); + + $.each(schedule.departments.flatMap(d => d.rooms), (_, room) => { + let content = `
${room.name}
`; + if (room.equipments.length > 0) { + let equipments = room.equipments.sort().slice(0, Math.min(2, room.equipments.length)); + content += `
`; + equipments.forEach(e => content += `
${e}
`); + content += "
"; + if (room.equipments.length > 2) { + let equipments = room.equipments.sort().slice(2, Math.min(4, room.equipments.length)); + content += `
`; + equipments.forEach(e => content += `
${e}
`); + content += "
"; + } + } + + const roomData = { + id: room.id, + content: content, + treeLevel: 1, + nestedLevels: [...room.beds.map(b => b.id)] + }; + byRoomGroupData.add(roomData); + room.beds.forEach(bed => byRoomGroupData.add({ + id: bed.id, + content: `Bed ${bed.indexInRoom + 1}`, + treeLevel: 2 + })); + }); + + const bedMap = new Map(); + schedule.departments.flatMap(d => d.rooms).flatMap(r => r.beds).forEach(b => bedMap.set(b.id, b)); + + $.each(schedule.stays, (_, stay) => { + if (stay.bed == null) { + unassignedJobsCount++; + const unassignedPatientElement = $(`
`) + .append($(`
`).text(`${stay.patientName} (${stay.patientGender.substring(0, 1)})`)) + .append($(`

`).text(`${JSJoda.LocalDate.parse(stay.arrivalDate) + .until(JSJoda.LocalDate.parse(stay.departureDate), JSJoda.ChronoUnit.DAYS)} day(s)`)) + .append($(`

`).text(`Arrival: ${stay.arrivalDate}`)) + .append($(`

`).text(`Departure: ${stay.departureDate}`)); + + unassignedPatientElement + .append($(`

`).append($(``) + .text(stay.specialty))); + + const equipmentDiv = $("

").prop("class", "col"); + unassignedPatientElement.append(equipmentDiv); + stay.patientRequiredEquipments.sort().forEach(e => { + equipmentDiv.append($(``).text(e)) + }); + const preferredEquipmentDiv = $("
").prop("class", "col"); + unassignedPatientElement.append(preferredEquipmentDiv); + if (stay.patientPreferredEquipments && stay.patientPreferredEquipments.length > 0) { + stay.patientPreferredEquipments + .filter(e => stay.patientRequiredEquipments.indexOf(e) == -1) + .sort() + .forEach(e => preferredEquipmentDiv.append($(``).text(e))); + } + unassignedPatientElement.append($("
").prop("class", "d-flex justify-content-end").append($(``) + .text(stay.patientPreferredMaximumRoomCapacity))); + + unassignedPatients.append($(`
`).append($(`
`).append(unassignedPatientElement))); + byRoomItemData.add({ + id: stay.id, + group: stay.id, + start: stay.arrivalDate, + end: stay.departureDate, + style: "background-color: #EF292999" + }); + } else { + const byPatientElement = $(`
`) + .append($(`
`).text(`${stay.patientName} (${stay.patientGender.substring(0, 1)})`)); + + byPatientElement + .append($(`

`).append($(``) + .text(stay.specialty))); + + const equipmentDiv = $("

").prop("class", "col"); + byPatientElement.append(equipmentDiv); + stay.patientRequiredEquipments.sort().forEach(e => { + equipmentDiv.append($(``).text(e)) + }); + const preferredEquipmentDiv = $("
").prop("class", "col"); + byPatientElement.append(preferredEquipmentDiv); + if (stay.patientPreferredEquipments && stay.patientPreferredEquipments.length > 0) { + stay.patientPreferredEquipments + .filter(e => stay.patientRequiredEquipments.indexOf(e) == -1) + .sort() + .forEach(e => preferredEquipmentDiv.append($(``).text(e))); + } + byPatientElement.append($("
").prop("class", "d-flex justify-content-end").append($(``) + .text(stay.patientPreferredMaximumRoomCapacity))); + + byRoomItemData.add({ + id: stay.id, + group: stay.bed, + content: byPatientElement.html(), + start: stay.arrivalDate, + end: stay.departureDate + }); + } + }); + if (unassignedJobsCount === 0) { + unassignedPatients.append($(`

`).text(`There are no unassigned stays.`)); + } + + const arrivalDates = schedule.stays.map(s => s.arrivalDate); + const departureDates = schedule.stays.map(s => s.departureDate); + const allDates = [...new Set([...arrivalDates, ...departureDates])] + .sort((a, b) => JSJoda.LocalDate.parse(a).compareTo(JSJoda.LocalDate.parse(b))); + byRoomTimeline.setWindow(allDates[0], allDates[allDates.length - 1]); +} + +function solve() { + $.post("/schedules", JSON.stringify(loadedSchedule), function (data) { + scheduleId = data; + refreshSolvingButtons(true); + }).fail(function (xhr, ajaxOptions, thrownError) { + showError("Start solving failed.", xhr); + refreshSolvingButtons(false); + }, "text"); +} + +function analyze() { + new bootstrap.Modal("#scoreAnalysisModal").show() + const scoreAnalysisModalContent = $("#scoreAnalysisModalContent"); + scoreAnalysisModalContent.children().remove(); + if (loadedSchedule.score == null || loadedSchedule.score.indexOf('init') != -1) { + scoreAnalysisModalContent.text("No score to analyze yet, please first press the 'solve' button."); + } else { + $('#scoreAnalysisScoreLabel').text(`(${loadedSchedule.score})`); + $.put("/schedules/analyze", JSON.stringify(loadedSchedule), function (scoreAnalysis) { + let constraints = scoreAnalysis.constraints; + constraints.sort((a, b) => { + let aComponents = getScoreComponents(a.score), bComponents = getScoreComponents(b.score); + if (aComponents.hard < 0 && bComponents.hard > 0) return -1; + if (aComponents.hard > 0 && bComponents.soft < 0) return 1; + if (Math.abs(aComponents.hard) > Math.abs(bComponents.hard)) { + return -1; + } else { + if (aComponents.medium < 0 && bComponents.medium > 0) return -1; + if (aComponents.medium > 0 && bComponents.medium < 0) return 1; + if (Math.abs(aComponents.medium) > Math.abs(bComponents.medium)) { + return -1; + } else { + if (aComponents.soft < 0 && bComponents.soft > 0) return -1; + if (aComponents.soft > 0 && bComponents.soft < 0) return 1; + + return Math.abs(bComponents.soft) - Math.abs(aComponents.soft); + } + } + }); + constraints.map((e) => { + let components = getScoreComponents(e.weight); + e.type = components.hard != 0 ? 'hard' : (components.medium != 0 ? 'medium' : 'soft'); + e.weight = components[e.type]; + let scores = getScoreComponents(e.score); + e.implicitScore = scores.hard != 0 ? scores.hard : (scores.medium != 0 ? scores.medium : scores.soft); + }); + scoreAnalysis.constraints = constraints; + + scoreAnalysisModalContent.children().remove(); + scoreAnalysisModalContent.text(""); + + const analysisTable = $(``).css({textAlign: 'center'}); + const analysisTHead = $(``).append($(``) + .append($(``)) + .append($(``).css({textAlign: 'left'})) + .append($(``)) + .append($(``)) + .append($(``)) + .append($(``)) + .append($(``))); + analysisTable.append(analysisTHead); + const analysisTBody = $(``) + $.each(scoreAnalysis.constraints, (index, constraintAnalysis) => { + let icon = constraintAnalysis.type == "hard" && constraintAnalysis.implicitScore < 0 ? '' : ''; + if (!icon) icon = constraintAnalysis.matches.length == 0 ? '' : ''; + + let row = $(``); + row.append($(`
ConstraintType# MatchesWeightScore
`).html(icon)) + .append($(``).text(constraintAnalysis.name).css({textAlign: 'left'})) + .append($(``).text(constraintAnalysis.type)) + .append($(``).html(`${constraintAnalysis.matches.length}`)) + .append($(``).text(constraintAnalysis.weight)) + .append($(``).text(constraintAnalysis.implicitScore)); + analysisTBody.append(row); + row.append($(``)); + }); + analysisTable.append(analysisTBody); + scoreAnalysisModalContent.append(analysisTable); + }).fail(function (xhr, ajaxOptions, thrownError) { + showError("Analyze failed.", xhr); + }, "text"); + } +} + +function publish() { + $("#publishButton").hide(); + $("#publishLoadingButton").show(); + $.put(`/schedules/${scheduleId}/publish`, function (schedule) { + loadedSchedule = schedule; + renderSchedule(schedule); + }) + .fail(function (xhr, ajaxOptions, thrownError) { + showError("Publish failed.", xhr); + refreshSolvingButtons(false); + }); +} + +function getScoreComponents(score) { + let components = {hard: 0, medium: 0, soft: 0}; + + $.each([...score.matchAll(/(-?[0-9]+)(hard|medium|soft)/g)], (i, parts) => { + components[parts[2]] = parseInt(parts[1], 10); + }); + + return components; +} + +function refreshSolvingButtons(solving) { + if (solving) { + $("#solveButton").hide(); + $("#stopSolvingButton").show(); + if (autoRefreshIntervalId == null) { + autoRefreshIntervalId = setInterval(refreshSchedule, 2000); + } + } else { + $("#solveButton").show(); + $("#stopSolvingButton").hide(); + if (autoRefreshIntervalId != null) { + clearInterval(autoRefreshIntervalId); + autoRefreshIntervalId = null; + } + } +} + +function stopSolving() { + $.delete("/schedules/" + scheduleId, function () { + refreshSolvingButtons(false); + refreshSchedule(); + }).fail(function (xhr, ajaxOptions, thrownError) { + showError("Stop solving failed.", xhr); + }); +} + +function importLocalFile() { + var file = document.querySelector('input[type=file]').files[0]; + var reader = new FileReader(); + + reader.addEventListener("load", function () { + // convert image file to base64 string + var data = atob(reader.result.toString().replace(/^data:(.*,)?/, '')); + $("#importedFile").val(''); + + try { + loadedSchedule = JSON.parse(data); + renderSchedule(loadedSchedule); + $('#exportData').attr('href', 'data:text/plain;charset=utf-8,' + JSON.stringify(loadedSchedule)); + } catch (error) { + console.error(error); + showSimpleError("Failed loading a bed plan.\nCheck if the content of the file represents a valid bed plan."); + } + $('#uploadModal').modal('hide'); + }, false); + + reader.readAsDataURL(file); +} + +function copyTextToClipboard(id) { + var text = $("#" + id).text().trim(); + + var dummy = document.createElement("textarea"); + document.body.appendChild(dummy); + dummy.value = text; + dummy.select(); + document.execCommand("copy"); + document.body.removeChild(dummy); +} + +function compareTimeslots(t1, t2) { + const LocalDateTime = JSJoda.LocalDateTime; + let diff = LocalDateTime.parse(t1.startDateTime).compareTo(LocalDateTime.parse(t2.startDateTime)); + if (diff === 0) { + diff = LocalDateTime.parse(t1.endDateTime).compareTo(LocalDateTime.parse(t2.endDateTime)); + } + return diff; +} + +// TODO: move to the webjar +function replaceQuickstartTimefoldAutoHeaderFooter() { + const timefoldHeader = $("header#timefold-auto-header"); + if (timefoldHeader != null) { + timefoldHeader.addClass("bg-black") + timefoldHeader.append($(`
+ +
`)); + } + + const timefoldFooter = $("footer#timefold-auto-footer"); + if (timefoldFooter != null) { + timefoldFooter.append($(``)); + } +} diff --git a/use-cases/bed-allocation/src/main/resources/META-INF/resources/index.html b/use-cases/bed-allocation/src/main/resources/META-INF/resources/index.html new file mode 100644 index 0000000000..20f41e32a0 --- /dev/null +++ b/use-cases/bed-allocation/src/main/resources/META-INF/resources/index.html @@ -0,0 +1,179 @@ + + + + + Bed Allocation Scheduling - Timefold Quarkus + + + + + + + +
+ +
+
+
+
+
+
+

Bed Allocation Scheduling Solver

+

Generate the optimal schedule for your bed scheduling.

+ +
+ + + Score: ? + + +
+ + +
+
+ +
+
+
+
+
+
+ +

Unassigned stays

+
+
+ +
+

REST API Guide

+ +

Conference Scheduling solver integration via cURL

+ +

1. Download demo data

+
+            
+            curl -X GET -H 'Accept:application/json' http://localhost:8080/demo-data -o sample.json
+    
+ +

2. Post the sample data for solving

+

The POST operation returns a jobId that should be used in subsequent commands.

+
+            
+            curl -X POST -H 'Content-Type:application/json' http://localhost:8080/schedules -d@sample.json
+    
+ +

3. Get the current status and score

+
+            
+            curl -X GET -H 'Accept:application/json' http://localhost:8080/schedules/{jobId}/status
+    
+ +

4. Get the complete solution

+
+            
+            curl -X GET -H 'Accept:application/json' http://localhost:8080/schedules/{jobId} -o solution.json
+    
+ +

5. Fetch the analysis of the solution

+
+            
+            curl -X PUT -H 'Accept:application/json' http://localhost:8080/schedules/analyze?fetchPolicy={FETCH_ALL|FETCH_SHALLOW} -d@solution.json
+    
+ +

6. Terminate solving early

+
+            
+            curl -X DELETE -H 'Accept:application/json' http://localhost:8080/schedules/{jobId}
+    
+
+ +
+

REST API Reference

+
+ + +
+
+
+
+ + + + + + + + + + + + + + diff --git a/use-cases/bed-allocation/src/main/resources/application.properties b/use-cases/bed-allocation/src/main/resources/application.properties new file mode 100644 index 0000000000..f892858292 --- /dev/null +++ b/use-cases/bed-allocation/src/main/resources/application.properties @@ -0,0 +1,41 @@ +######################## +# General properties +######################## + +# Enable CORS for runQuickstartsFromSource.sh +quarkus.http.cors=true +quarkus.http.cors.origins=/http://localhost:.*/ +# Allow all origins in dev-mode +%dev.quarkus.http.cors.origins=/.*/ +# Enable Swagger UI also in the native mode +quarkus.swagger-ui.always-include=true + +######################## +# Timefold properties +######################## + +# The solver runs for 30 seconds. To run for 5 minutes use "5m" and for 2 hours use "2h". +quarkus.timefold.solver.termination.spent-limit=30s + +# To change how many solvers to run in parallel +# timefold.solver-manager.parallel-solver-count=4 +# To run increase CPU cores usage per solver +# quarkus.timefold.solver.move-thread-count=2 + +# Temporary comment this out to detect bugs in your code (lowers performance) +# quarkus.timefold.solver.environment-mode=FULL_ASSERT +# To see what Timefold is doing, turn on DEBUG or TRACE logging. +quarkus.log.category."ai.timefold.solver".level=DEBUG +%test.quarkus.log.category."ai.timefold.solver".level=INFO +%prod.quarkus.log.category."ai.timefold.solver".level=INFO + +# XML file for power tweaking, defaults to solverConfig.xml (directly under src/main/resources) +# quarkus.timefold.solver-config-xml=org/.../bedSchedulingSolverConfig.xml + +######################## +# Test overrides +######################## + +# Effectively disable spent-time termination in favor of the best-score-limit +%test.quarkus.timefold.solver.termination.spent-limit=1h +%test.quarkus.timefold.solver.termination.best-score-limit=0hard/0medium/*soft \ No newline at end of file diff --git a/use-cases/bed-allocation/src/test/java/org/acme/bedallocation/rest/BedSchedulingResourceTest.java b/use-cases/bed-allocation/src/test/java/org/acme/bedallocation/rest/BedSchedulingResourceTest.java new file mode 100644 index 0000000000..5f01492f36 --- /dev/null +++ b/use-cases/bed-allocation/src/test/java/org/acme/bedallocation/rest/BedSchedulingResourceTest.java @@ -0,0 +1,107 @@ +package org.acme.bedallocation.rest; + +import static io.restassured.RestAssured.get; +import static io.restassured.RestAssured.given; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import java.time.Duration; + +import ai.timefold.solver.core.api.solver.SolverStatus; + +import org.acme.bedallocation.domain.BedPlan; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.http.ContentType; + +@QuarkusTest +class BedSchedulingResourceTest { + + @Test + void solveDemoDataUntilFeasible() { + BedPlan schedule = given() + .when().get("/demo-data") + .then() + .statusCode(200) + .extract() + .as(BedPlan.class); + + String jobId = given() + .contentType(ContentType.JSON) + .body(schedule) + .expect().contentType(ContentType.TEXT) + .when().post("/schedules") + .then() + .statusCode(200) + .extract() + .asString(); + + await() + .atMost(Duration.ofMinutes(1)) + .pollInterval(Duration.ofMillis(500L)) + .until(() -> SolverStatus.NOT_SOLVING.name().equals( + get("/schedules/" + jobId + "/status") + .jsonPath().get("solverStatus"))); + + BedPlan solution = get("/schedules/" + jobId).then().extract().as(BedPlan.class); + assertThat(solution.getSolverStatus()).isEqualTo(SolverStatus.NOT_SOLVING); + assertThat(solution.getStays().stream().allMatch(bedDesignation -> bedDesignation.getBed() != null)).isTrue(); + assertThat(solution.getScore().isFeasible()).isTrue(); + } + + @Test + void analyze() { + BedPlan schedule = given() + .when().get("/demo-data") + .then() + .statusCode(200) + .extract() + .as(BedPlan.class); + + String jobId = given() + .contentType(ContentType.JSON) + .body(schedule) + .expect().contentType(ContentType.TEXT) + .when().post("/schedules") + .then() + .statusCode(200) + .extract() + .asString(); + + await() + .atMost(Duration.ofMinutes(1)) + .pollInterval(Duration.ofMillis(500L)) + .until(() -> SolverStatus.NOT_SOLVING.name().equals( + get("/schedules/" + jobId + "/status") + .jsonPath().get("solverStatus"))); + + BedPlan solution = get("/schedules/" + jobId).then().extract().as(BedPlan.class); + + String analysis = given() + .contentType(ContentType.JSON) + .body(solution) + .expect().contentType(ContentType.JSON) + .when() + .put("/schedules/analyze") + .then() + .extract() + .asString(); + // There are too many constraints to validate + assertThat(analysis).isNotNull(); + + String analysis2 = given() + .contentType(ContentType.JSON) + .queryParam("fetchPolicy", "FETCH_SHALLOW") + .body(solution) + .expect().contentType(ContentType.JSON) + .when() + .put("/schedules/analyze") + .then() + .extract() + .asString(); + // There are too many constraints to validate + assertThat(analysis2).isNotNull(); + } + +} \ No newline at end of file diff --git a/use-cases/bed-allocation/src/test/java/org/acme/bedallocation/solver/BedAllocationConstraintProviderTest.java b/use-cases/bed-allocation/src/test/java/org/acme/bedallocation/solver/BedAllocationConstraintProviderTest.java new file mode 100644 index 0000000000..e3b83054df --- /dev/null +++ b/use-cases/bed-allocation/src/test/java/org/acme/bedallocation/solver/BedAllocationConstraintProviderTest.java @@ -0,0 +1,268 @@ +package org.acme.bedallocation.solver; + +import java.time.LocalDate; +import java.util.List; +import java.util.Map; + +import jakarta.inject.Inject; + +import ai.timefold.solver.test.api.score.stream.ConstraintVerifier; + +import org.acme.bedallocation.domain.Bed; +import org.acme.bedallocation.domain.BedPlan; +import org.acme.bedallocation.domain.Department; +import org.acme.bedallocation.domain.DepartmentSpecialty; +import org.acme.bedallocation.domain.Gender; +import org.acme.bedallocation.domain.GenderLimitation; +import org.acme.bedallocation.domain.Room; +import org.acme.bedallocation.domain.Stay; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +class BedAllocationConstraintProviderTest { + + private static final LocalDate ZERO_NIGHT = LocalDate.of(2021, 2, 1); + private static final LocalDate FIVE_NIGHT = ZERO_NIGHT.plusDays(5); + + private static final String DEFAULT_SPECIALTY = "default"; + + @Inject + ConstraintVerifier constraintVerifier; + + @Test + void femaleInMaleRoom() { + Room room = new Room(); + room.setGenderLimitation(GenderLimitation.MALE_ONLY); + + Bed bed = new Bed(); + bed.setRoom(room); + + Stay genderAdmission = new Stay("0", ZERO_NIGHT, FIVE_NIGHT, DEFAULT_SPECIALTY, bed); + genderAdmission.setPatientGender(Gender.FEMALE); + + constraintVerifier.verifyThat(BedAllocationConstraintProvider::femaleInMaleRoom) + .given(genderAdmission) + .penalizesBy(6); + } + + @Test + void maleInFemaleRoom() { + Room room = new Room(); + room.setGenderLimitation(GenderLimitation.FEMALE_ONLY); + + Bed bed = new Bed(); + bed.setRoom(room); + + Stay genderAdmission = new Stay("0", ZERO_NIGHT, FIVE_NIGHT, DEFAULT_SPECIALTY, bed); + genderAdmission.setPatientGender(Gender.MALE); + + constraintVerifier.verifyThat(BedAllocationConstraintProvider::maleInFemaleRoom) + .given(genderAdmission) + .penalizesBy(6); + } + + @Test + void sameBedInSameNight() { + + Bed bed = new Bed("1"); + + Stay stay = new Stay("0", ZERO_NIGHT, FIVE_NIGHT, DEFAULT_SPECIALTY, bed); + + Stay sameBedAndNightsStay = new Stay("2", ZERO_NIGHT, FIVE_NIGHT, DEFAULT_SPECIALTY, bed); + + constraintVerifier.verifyThat(BedAllocationConstraintProvider::sameBedInSameNight) + .given(stay, sameBedAndNightsStay) + .penalizesBy(6); + } + + @Test + void departmentMinimumAge() { + Department department = new Department("1", "Adult department"); + department.setMinimumAge(18); + + Room room = new Room(); + room.setDepartment(department); + + Bed bed = new Bed(); + bed.setRoom(room); + + Stay admission = new Stay("0", ZERO_NIGHT, FIVE_NIGHT, DEFAULT_SPECIALTY, bed); + admission.setPatientAge(5); + + constraintVerifier.verifyThat(BedAllocationConstraintProvider::departmentMinimumAge) + .given(admission, department) + .penalizesBy(6); + } + + @Test + void departmentMaximumAge() { + Department department = new Department("2", "Underage department"); + department.setMaximumAge(18); + + Room room = new Room(); + room.setDepartment(department); + + Bed bed = new Bed(); + bed.setRoom(room); + + Stay admission = new Stay("0", ZERO_NIGHT, FIVE_NIGHT, DEFAULT_SPECIALTY, bed); + admission.setPatientAge(42); + + constraintVerifier.verifyThat(BedAllocationConstraintProvider::departmentMaximumAge) + .given(admission, department) + .penalizesBy(6); + } + + @Test + void requiredPatientEquipment() { + Room room = new Room(); + room.setEquipments(List.of("TELEMETRY")); + + Bed bed = new Bed(); + bed.setRoom(room); + + Stay admission = new Stay("0", ZERO_NIGHT, FIVE_NIGHT, DEFAULT_SPECIALTY, bed); + admission.setPatientRequiredEquipments(List.of("TELEVISION", "TELEMETRY")); + + constraintVerifier.verifyThat(BedAllocationConstraintProvider::requiredPatientEquipment) + .given(admission) + .penalizesBy(6); + } + + @Test + void differentGenderInSameGenderRoomInSameNight() { + + Room room = new Room("1"); + room.setGenderLimitation(GenderLimitation.SAME_GENDER); + + //Assign female + Bed bed1 = new Bed(); + bed1.setRoom(room); + + Stay stayFemale = new Stay("0", ZERO_NIGHT, FIVE_NIGHT, DEFAULT_SPECIALTY, bed1); + stayFemale.setPatientGender(Gender.FEMALE); + + //Assign male + Bed bed2 = new Bed(); + bed2.setRoom(room); + + Stay stayMale = new Stay("1", ZERO_NIGHT, FIVE_NIGHT, DEFAULT_SPECIALTY, bed2); + stayMale.setPatientGender(Gender.MALE); + + constraintVerifier + .verifyThat(BedAllocationConstraintProvider::differentGenderInSameGenderRoomInSameNight) + .given(stayFemale, stayMale) + .penalizesBy(6); + } + + @Test + void assignEveryPatientToABed() { + + Stay stay = new Stay("0", ZERO_NIGHT, FIVE_NIGHT, DEFAULT_SPECIALTY, null); + + constraintVerifier + .verifyThat(BedAllocationConstraintProvider::assignEveryPatientToABed) + .given(stay) + .penalizesBy(6); + } + + @Test + void preferredMaximumRoomCapacity() { + + + Room room = new Room(); + room.setCapacity(6); + + Bed assignedBedInExceedCapacity = new Bed(); + assignedBedInExceedCapacity.setRoom(room); + + Stay stay = new Stay("0", ZERO_NIGHT, FIVE_NIGHT, DEFAULT_SPECIALTY, + assignedBedInExceedCapacity); + stay.setPatientPreferredMaximumRoomCapacity(3); + + constraintVerifier + .verifyThat(BedAllocationConstraintProvider::preferredMaximumRoomCapacity) + .given(stay) + .penalizesBy(6); + } + + @Test + void preferredPatientEquipment() { + Room room = new Room(); + room.setEquipments(List.of("TELEMETRY")); + + Bed bed = new Bed(); + bed.setRoom(room); + + Stay stay = new Stay("0", ZERO_NIGHT, FIVE_NIGHT, DEFAULT_SPECIALTY, bed); + stay.setPatientPreferredEquipments(List.of("TELEVISION", "TELEMETRY")); + + constraintVerifier.verifyThat(BedAllocationConstraintProvider::preferredPatientEquipment) + .given(stay) + .penalizesBy(6); + } + + @Test + void departmentSpecialtY() { + + Department department = new Department("0", "0"); + + Room roomInDep = new Room(); + roomInDep.setDepartment(department); + + Bed bedInRoomInDep = new Bed(); + bedInRoomInDep.setRoom(roomInDep); + + //Designation with 1st spec + String spec1 = "spec1"; + + Stay staySpec1 = new Stay("0", ZERO_NIGHT, FIVE_NIGHT, spec1, bedInRoomInDep); + + //Designation with 2nd spec + String spec2 = "spec2"; + + Stay staySpec2 = new Stay("1", ZERO_NIGHT, FIVE_NIGHT, spec2, bedInRoomInDep); + + DepartmentSpecialty departmentSpecialtyWithOneSpec = new DepartmentSpecialty(); + departmentSpecialtyWithOneSpec.setDepartment(department); + departmentSpecialtyWithOneSpec.setSpecialty(spec1); + + constraintVerifier.verifyThat(BedAllocationConstraintProvider::departmentSpecialty) + .given(staySpec1, staySpec2, departmentSpecialtyWithOneSpec) + .penalizesBy(6); + } + + @Test + void departmentSpecialtYNotFirstPriorityConstraint() { + + Department department = new Department("0", "0"); + department.setSpecialtyToPriority(Map.of("spec1", 2)); + + Room roomInDep = new Room("1"); + roomInDep.setDepartment(department); + department.addRoom(roomInDep); + + Bed bedInDep = new Bed(); + bedInDep.setRoom(roomInDep); + + //Designation with 1st spec + String spec1 = "spec1"; + Stay stay1 = new Stay("0", ZERO_NIGHT, FIVE_NIGHT, spec1, bedInDep); + + //Designation with 2nd spec + String spec2 = "spec2"; + Stay stay2 = new Stay("1", ZERO_NIGHT, FIVE_NIGHT, spec2, bedInDep); + + DepartmentSpecialty departmentSpecialty = new DepartmentSpecialty(); + departmentSpecialty.setDepartment(department); + departmentSpecialty.setSpecialty(spec1); + departmentSpecialty.setPriority(2); + + constraintVerifier.verifyThat(BedAllocationConstraintProvider::departmentSpecialtyNotFirstPriority) + .given(stay1, stay2, departmentSpecialty) + .penalizesBy(6); + } + +}