From 7dba481a075c370000316051e2aeaefae0f78847 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frederico=20Gon=C3=A7alves?= Date: Wed, 27 Mar 2024 11:07:20 -0300 Subject: [PATCH] feat: bed allocation improvement (#366) --- .../acme/bedallocation/domain/BedPlan.java | 41 +++++++----- .../domain/DepartmentSpecialty.java | 62 ------------------- .../org/acme/bedallocation/domain/Stay.java | 10 +++ .../bedallocation/rest/DemoDataGenerator.java | 17 ++--- .../BedAllocationConstraintProvider.java | 14 +---- .../main/resources/META-INF/resources/app.js | 8 ++- .../BedAllocationConstraintProviderTest.java | 22 ++----- 7 files changed, 56 insertions(+), 118 deletions(-) delete mode 100644 use-cases/bed-allocation/src/main/java/org/acme/bedallocation/domain/DepartmentSpecialty.java diff --git a/use-cases/bed-allocation/src/main/java/org/acme/bedallocation/domain/BedPlan.java b/use-cases/bed-allocation/src/main/java/org/acme/bedallocation/domain/BedPlan.java index de1434fbeb..5bd7e5617e 100644 --- a/use-cases/bed-allocation/src/main/java/org/acme/bedallocation/domain/BedPlan.java +++ b/use-cases/bed-allocation/src/main/java/org/acme/bedallocation/domain/BedPlan.java @@ -10,20 +10,21 @@ import ai.timefold.solver.core.api.score.buildin.hardmediumsoft.HardMediumSoftScore; import ai.timefold.solver.core.api.solver.SolverStatus; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; + @PlanningSolution public class BedPlan { @ProblemFactCollectionProperty private List departments; - @ProblemFactCollectionProperty - private List departmentSpecialties; - @ProblemFactCollectionProperty - private List rooms; - @ProblemFactCollectionProperty - @ValueRangeProvider - private List beds; @PlanningEntityCollectionProperty private List stays; + @JsonIgnore + private List rooms; + @JsonIgnore + private List beds; @PlanningScore private HardMediumSoftScore score; @@ -34,6 +35,21 @@ public class BedPlan { public BedPlan() { } + @JsonCreator + public BedPlan(@JsonProperty("departments") List departments, @JsonProperty("stays") List stays) { + this.departments = departments; + this.stays = stays; + this.rooms = departments.stream() + .filter(d -> d.getRooms() != null) + .flatMap(d -> d.getRooms().stream()) + .toList(); + this.beds = departments.stream() + .filter(d -> d.getRooms() != null) + .flatMap(d -> d.getRooms().stream()) + .flatMap(r -> r.getBeds().stream()) + .toList(); + } + public BedPlan(HardMediumSoftScore score, SolverStatus solverStatus) { this.score = score; this.solverStatus = solverStatus; @@ -50,14 +66,7 @@ public void setDepartments(List departments) { this.departments = departments; } - public void setDepartmentSpecialties(List departmentSpecialties) { - this.departmentSpecialties = departmentSpecialties; - } - - public List getDepartmentSpecialties() { - return departmentSpecialties; - } - + @ProblemFactCollectionProperty public List getRooms() { return rooms; } @@ -66,6 +75,8 @@ public void setRooms(List rooms) { this.rooms = rooms; } + @ProblemFactCollectionProperty + @ValueRangeProvider public List getBeds() { return beds; } 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 deleted file mode 100644 index f11b2e3135..0000000000 --- a/use-cases/bed-allocation/src/main/java/org/acme/bedallocation/domain/DepartmentSpecialty.java +++ /dev/null @@ -1,62 +0,0 @@ -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/Stay.java b/use-cases/bed-allocation/src/main/java/org/acme/bedallocation/domain/Stay.java index cddbfb5e45..89ca71437c 100644 --- 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 @@ -60,6 +60,16 @@ public int calculateSameNightCount(Stay other) { return Math.max(0, (int) DAYS.between(maxArrivalDate, minDepartureDate) + 1); // TODO is + 1 still desired? } + @JsonIgnore + public boolean hasDepartmentSpecialty() { + return getDepartment().getSpecialtyToPriority().containsKey(specialty); + } + + @JsonIgnore + public int getSpecialtyPriority() { + return getDepartment().getSpecialtyToPriority().get(specialty); + } + @JsonIgnore public Room getRoom() { if (bed == null) { 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 index df538cf67a..206c1e5cff 100644 --- 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 @@ -25,7 +25,6 @@ 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; @@ -48,21 +47,15 @@ public BedPlan generateDemoData() { 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()); + List rooms = generateRooms(countRooms, departments); + schedule.getDepartments().get(0).setRooms(rooms); + schedule.setRooms(rooms); // Beds - generateBeds(schedule.getRooms()); + generateBeds(rooms); schedule.setBeds(departments.stream() + .filter(d -> d.getRooms() != null) .flatMap(d -> d.getRooms().stream()) .flatMap(r -> r.getBeds().stream()) .toList()); 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 index afe65b536a..4dce5c32e3 100644 --- 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 @@ -13,7 +13,6 @@ 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; @@ -130,22 +129,15 @@ public Constraint preferredMaximumRoomCapacity(ConstraintFactory constraintFacto public Constraint departmentSpecialty(ConstraintFactory constraintFactory) { return constraintFactory.forEach(Stay.class) - .ifNotExists(DepartmentSpecialty.class, - equal(Stay::getDepartment, DepartmentSpecialty::getDepartment), - equal(Stay::getSpecialty, DepartmentSpecialty::getSpecialty)) + .filter(st -> !st.hasDepartmentSpecialty()) .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()) + .filter(st -> st.getSpecialtyPriority() > 1) + .penalize(HardMediumSoftScore.ofSoft(10), stay -> (stay.getSpecialtyPriority() - 1) * stay.getNightCount()) .asConstraint("departmentSpecialtyNotFirstPriority"); } 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 index 376566683b..71276f5cf8 100644 --- 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 @@ -177,7 +177,9 @@ function renderScheduleByRoom(schedule) { unassignedPatientElement.append($("
").prop("class", "d-flex justify-content-end").append($(``) .text(stay.patientPreferredMaximumRoomCapacity))); - unassignedPatients.append($(`
`).append($(`
`).append(unassignedPatientElement))); + const color = stay.patientGender == 'MALE' ? '#729FCF' : '#FCE94F'; + unassignedPatients.append($(`
`).append($(`
`).append(unassignedPatientElement))); + console.log(stay) byRoomItemData.add({ id: stay.id, group: stay.id, @@ -208,13 +210,15 @@ function renderScheduleByRoom(schedule) { } byPatientElement.append($("
").prop("class", "d-flex justify-content-end").append($(``) .text(stay.patientPreferredMaximumRoomCapacity))); + const color = stay.patientGender == 'MALE' ? '#729FCF' : '#FCE94F'; byRoomItemData.add({ id: stay.id, group: stay.bed, content: byPatientElement.html(), start: stay.arrivalDate, - end: stay.departureDate + end: stay.departureDate, + style: `background-color: ${color}` }); } }); 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 index e3b83054df..23512f1feb 100644 --- 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 @@ -11,7 +11,6 @@ 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; @@ -171,7 +170,6 @@ void assignEveryPatientToABed() { @Test void preferredMaximumRoomCapacity() { - Room room = new Room(); room.setCapacity(6); @@ -205,9 +203,10 @@ void preferredPatientEquipment() { } @Test - void departmentSpecialtY() { + void departmentSpecialty() { Department department = new Department("0", "0"); + department.setSpecialtyToPriority(Map.of("spec1", 1)); Room roomInDep = new Room(); roomInDep.setDepartment(department); @@ -225,20 +224,16 @@ void departmentSpecialtY() { 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) + .given(staySpec1, staySpec2) .penalizesBy(6); } @Test - void departmentSpecialtYNotFirstPriorityConstraint() { + void departmentSpecialtyNotFirstPriorityConstraint() { Department department = new Department("0", "0"); - department.setSpecialtyToPriority(Map.of("spec1", 2)); + department.setSpecialtyToPriority(Map.of("spec1", 2, "spec2", 1)); Room roomInDep = new Room("1"); roomInDep.setDepartment(department); @@ -255,13 +250,8 @@ void departmentSpecialtYNotFirstPriorityConstraint() { 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) + .given(stay1, stay2) .penalizesBy(6); }