From 1b14fb452d11aa9bb2508aa4477ca2f31c495d9e Mon Sep 17 00:00:00 2001 From: tungkhanhh Date: Sat, 14 Sep 2024 20:10:20 +1000 Subject: [PATCH 1/7] feat: add the day-of-week to Unit --- .../src/main/java/org/acme/domain/Unit.java | 36 +++++++++++-------- .../solver/TimetableConstraintProvider.java | 6 ++-- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/backend/src/main/java/org/acme/domain/Unit.java b/backend/src/main/java/org/acme/domain/Unit.java index 96314e2..4be353a 100644 --- a/backend/src/main/java/org/acme/domain/Unit.java +++ b/backend/src/main/java/org/acme/domain/Unit.java @@ -4,6 +4,7 @@ import ai.timefold.solver.core.api.domain.lookup.PlanningId; import ai.timefold.solver.core.api.domain.variable.PlanningVariable; +import java.time.DayOfWeek; import java.time.Duration; import java.time.LocalTime; import java.util.List; @@ -17,24 +18,21 @@ @PlanningEntity public class Unit { + List students; @PlanningId - int unitID; - - String name; - - Duration duration; - + private int unitID; + private String name; + private Duration duration; @PlanningVariable - LocalTime start; - List students; + private LocalTime startTime; + @PlanningVariable + private DayOfWeek dayOfWeek; @PlanningVariable private Room room; public Unit() { } - ; - /** * Creates a unit. * @@ -91,16 +89,16 @@ public void setDuration(Duration duration) { this.duration = duration; } - public LocalTime getStart() { - return start; + public LocalTime getStartTime() { + return startTime; } - public void setStart(LocalTime start) { - this.start = start; + public void setStartTime(LocalTime startTime) { + this.startTime = startTime; } public LocalTime getEnd() { - return start.plus(duration); + return startTime.plus(duration); } public List getStudents() { @@ -127,4 +125,12 @@ public Room getRoom() { public void setRoom(Room room) { this.room = room; } + + public DayOfWeek getDayOfWeek() { + return dayOfWeek; + } + + public void setDayOfWeek(DayOfWeek dayOfWeek) { + this.dayOfWeek = dayOfWeek; + } } \ No newline at end of file diff --git a/backend/src/main/java/org/acme/solver/TimetableConstraintProvider.java b/backend/src/main/java/org/acme/solver/TimetableConstraintProvider.java index 393120c..3d301fa 100644 --- a/backend/src/main/java/org/acme/solver/TimetableConstraintProvider.java +++ b/backend/src/main/java/org/acme/solver/TimetableConstraintProvider.java @@ -40,9 +40,9 @@ private Constraint studentConflict(ConstraintFactory constraintFactory) { return constraintFactory.forEach(ConflictingUnit.class) .join(Unit.class, Joiners.equal(ConflictingUnit::getUnit1, Function.identity())) .join(Unit.class, Joiners.equal((conflictingUnit, unit1) -> conflictingUnit.getUnit2(), Function.identity()), - overlapping((conflictingUnit, unit1) -> unit1.getStart(), + overlapping((conflictingUnit, unit1) -> unit1.getStartTime(), (conflictingUnit, unit1) -> unit1.getEnd(), - Unit::getStart, Unit::getEnd)) + Unit::getStartTime, Unit::getEnd)) .penalize(HardSoftScore.ofHard(1), (conflictingUnit, unit1, unit2) -> conflictingUnit.getNumStudent()) .asConstraint("Student conflict"); @@ -57,7 +57,7 @@ Constraint roomConflict(ConstraintFactory constraintFactory) { // Select each pair of 2 different lessons ... .forEachUniquePair(Unit.class, // ... in the same timeslot ... - overlapping(Unit::getStart, Unit::getEnd), + overlapping(Unit::getStartTime, Unit::getEnd), // ... in the same room ... Joiners.equal(Unit::getRoom)) // ... and penalize each pair with a hard weight. From 39756dceed43680b1818f5eb24fa059c9db180ad Mon Sep 17 00:00:00 2001 From: tungkhanhh Date: Sat, 14 Sep 2024 20:16:43 +1000 Subject: [PATCH 2/7] feat: add the day-of-week to Timetable --- .../main/java/org/acme/domain/Timetable.java | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/backend/src/main/java/org/acme/domain/Timetable.java b/backend/src/main/java/org/acme/domain/Timetable.java index b340c92..147edf5 100644 --- a/backend/src/main/java/org/acme/domain/Timetable.java +++ b/backend/src/main/java/org/acme/domain/Timetable.java @@ -7,6 +7,7 @@ import ai.timefold.solver.core.api.domain.valuerange.ValueRangeProvider; import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore; +import java.time.DayOfWeek; import java.time.LocalTime; import java.util.ArrayList; import java.util.List; @@ -20,12 +21,14 @@ public class Timetable { @PlanningEntityCollectionProperty - List units; + private List units; @ValueRangeProvider - List startTimes; + private List daysOfWeek; + @ValueRangeProvider + private List startTimes; @PlanningScore - HardSoftScore score; + private HardSoftScore score; @ProblemFactCollectionProperty @ValueRangeProvider private List rooms; @@ -58,6 +61,29 @@ public Timetable(List units, List startTimes, List rooms) this.rooms = rooms; } + /** + * Creates a timetable. + * + * @param units The list of units to be allocated. + * @param daysOfWeek The list of available days of the week. + * @param startTimes The list of available starting times. + * @param rooms The list of available rooms. + */ + public Timetable(List units, List daysOfWeek, List startTimes, List rooms) { + this.units = units; + this.daysOfWeek = daysOfWeek; + this.startTimes = startTimes; + this.rooms = rooms; + } + + public List getDaysOfWeek() { + return daysOfWeek; + } + + public void setDaysOfWeek(List daysOfWeek) { + this.daysOfWeek = daysOfWeek; + } + public List getStartTimes() { return startTimes; } From 2e1d2e50dd054581b356c854bb7d58efd7b8ca3c Mon Sep 17 00:00:00 2001 From: tungkhanhh Date: Sat, 14 Sep 2024 20:28:53 +1000 Subject: [PATCH 3/7] feat: add day-of-week to constraints, add comments to explain the constraints --- .../main/java/org/acme/domain/Student.java | 2 +- .../solver/TimetableConstraintProvider.java | 28 ++++++++++++++----- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/backend/src/main/java/org/acme/domain/Student.java b/backend/src/main/java/org/acme/domain/Student.java index 2d7cd6d..42093dd 100644 --- a/backend/src/main/java/org/acme/domain/Student.java +++ b/backend/src/main/java/org/acme/domain/Student.java @@ -10,7 +10,7 @@ public class Student { // String studentID; -// @PlanningId + String name; public Student() { diff --git a/backend/src/main/java/org/acme/solver/TimetableConstraintProvider.java b/backend/src/main/java/org/acme/solver/TimetableConstraintProvider.java index 3d301fa..af0f1b8 100644 --- a/backend/src/main/java/org/acme/solver/TimetableConstraintProvider.java +++ b/backend/src/main/java/org/acme/solver/TimetableConstraintProvider.java @@ -10,6 +10,7 @@ import java.util.function.Function; +import static ai.timefold.solver.core.api.score.stream.Joiners.equal; import static ai.timefold.solver.core.api.score.stream.Joiners.overlapping; /** @@ -37,12 +38,21 @@ public Constraint[] defineConstraints(ConstraintFactory constraintFactory) { * Penalize 1 hard score for each student with overlapping units. */ private Constraint studentConflict(ConstraintFactory constraintFactory) { - return constraintFactory.forEach(ConflictingUnit.class) - .join(Unit.class, Joiners.equal(ConflictingUnit::getUnit1, Function.identity())) - .join(Unit.class, Joiners.equal((conflictingUnit, unit1) -> conflictingUnit.getUnit2(), Function.identity()), + // A student can be in at most one unit at the same time. + return constraintFactory + // Select each pair of conflicting units. + .forEach(ConflictingUnit.class) + // Find the first unit. + .join(Unit.class, equal(ConflictingUnit::getUnit1, Function.identity())) + // Find the second unit. + .join(Unit.class, equal((conflictingUnit, unit1) -> conflictingUnit.getUnit2(), Function.identity()), + // Check if the 2 units are on the same weekday ... + equal((conflictingUnit, unit1) -> unit1.getDayOfWeek(), Unit::getDayOfWeek), + // ... in the same timeslot ... overlapping((conflictingUnit, unit1) -> unit1.getStartTime(), (conflictingUnit, unit1) -> unit1.getEnd(), Unit::getStartTime, Unit::getEnd)) + // ... and penalize each pair with a hard weight. .penalize(HardSoftScore.ofHard(1), (conflictingUnit, unit1, unit2) -> conflictingUnit.getNumStudent()) .asConstraint("Student conflict"); @@ -52,14 +62,16 @@ private Constraint studentConflict(ConstraintFactory constraintFactory) { * Penalize 1 hard score for each room with overlapping units. */ Constraint roomConflict(ConstraintFactory constraintFactory) { - // A room can accommodate at most one lesson at the same time. + // A room can accommodate at most one unit at the same time. return constraintFactory - // Select each pair of 2 different lessons ... + // Select each pair of 2 different units ... .forEachUniquePair(Unit.class, + // ... on the same weekday ... + equal(Unit::getDayOfWeek), // ... in the same timeslot ... overlapping(Unit::getStartTime, Unit::getEnd), // ... in the same room ... - Joiners.equal(Unit::getRoom)) + equal(Unit::getRoom)) // ... and penalize each pair with a hard weight. .penalize(HardSoftScore.ofHard(1)) .asConstraint("Room conflict"); @@ -69,7 +81,9 @@ Constraint roomConflict(ConstraintFactory constraintFactory) { * Penalize 1 soft score for each student overflowing the capacity of the room. */ Constraint roomCapacity(ConstraintFactory constraintFactory) { - return constraintFactory.forEach(Unit.class) + // A room cannot accommodate more students than its capacity. + return constraintFactory + .forEach(Unit.class) .filter(unit -> unit.getStudentSize() > unit.getRoom().getCapacity()) .penalize(HardSoftScore.ofSoft(1), unit -> unit.getStudentSize() - unit.getRoom().getCapacity()) .asConstraint("Room capacity conflict"); From bab1a2eec4f8ec393007d15befe1a93fa5c5174f Mon Sep 17 00:00:00 2001 From: tungkhanhh Date: Sat, 14 Sep 2024 20:42:24 +1000 Subject: [PATCH 4/7] feat: add day-of-week to problem in TimetableResource --- .../main/java/org/acme/TimetableResource.java | 16 +++++++++++++--- .../java/org/acme/domain/ConflictingUnit.java | 6 +++--- .../src/main/java/org/acme/domain/Timetable.java | 12 +++++++----- backend/src/main/java/org/acme/domain/Unit.java | 6 +++--- 4 files changed, 26 insertions(+), 14 deletions(-) diff --git a/backend/src/main/java/org/acme/TimetableResource.java b/backend/src/main/java/org/acme/TimetableResource.java index 11e28f4..2202575 100644 --- a/backend/src/main/java/org/acme/TimetableResource.java +++ b/backend/src/main/java/org/acme/TimetableResource.java @@ -11,6 +11,7 @@ import org.acme.domain.Timetable; import org.acme.domain.Unit; +import java.time.DayOfWeek; import java.time.Duration; import java.time.LocalTime; import java.util.List; @@ -56,17 +57,26 @@ public Timetable hello() throws ExecutionException, InterruptedException { // new Unit(5, "5", Duration.ofHours(2), List.of(c, d, e)), // new Unit(6, "6", Duration.ofHours(2), List.of(f, g, h, i)) ), + + List.of( + DayOfWeek.MONDAY, + DayOfWeek.TUESDAY, + DayOfWeek.WEDNESDAY +// DayOfWeek.THURSDAY, +// DayOfWeek.FRIDAY + ), + List.of( - LocalTime.of(15, 0), - LocalTime.of(17, 0) + LocalTime.of(15, 0) +// LocalTime.of(17, 0) // LocalTime.of(16,0), // LocalTime.of(23,0) ), List.of(r1, r2, r3) ); - Timetable solution = solverManager.solve("job 1", problem).getFinalBestSolution(); + return solution; } diff --git a/backend/src/main/java/org/acme/domain/ConflictingUnit.java b/backend/src/main/java/org/acme/domain/ConflictingUnit.java index 20444ff..14be70b 100644 --- a/backend/src/main/java/org/acme/domain/ConflictingUnit.java +++ b/backend/src/main/java/org/acme/domain/ConflictingUnit.java @@ -6,11 +6,11 @@ * @author Jet Edge */ public class ConflictingUnit { - Unit unit1; + private Unit unit1; - Unit unit2; + private Unit unit2; - int numStudent; + private int numStudent; /** * Creates a pair of conflicting units. diff --git a/backend/src/main/java/org/acme/domain/Timetable.java b/backend/src/main/java/org/acme/domain/Timetable.java index 147edf5..b6f9826 100644 --- a/backend/src/main/java/org/acme/domain/Timetable.java +++ b/backend/src/main/java/org/acme/domain/Timetable.java @@ -20,19 +20,21 @@ @PlanningSolution public class Timetable { - @PlanningEntityCollectionProperty - private List units; - @ValueRangeProvider private List daysOfWeek; @ValueRangeProvider private List startTimes; - @PlanningScore - private HardSoftScore score; + @ProblemFactCollectionProperty @ValueRangeProvider private List rooms; + @PlanningEntityCollectionProperty + private List units; + + @PlanningScore + private HardSoftScore score; + public Timetable() { } diff --git a/backend/src/main/java/org/acme/domain/Unit.java b/backend/src/main/java/org/acme/domain/Unit.java index 4be353a..e09a274 100644 --- a/backend/src/main/java/org/acme/domain/Unit.java +++ b/backend/src/main/java/org/acme/domain/Unit.java @@ -18,16 +18,16 @@ @PlanningEntity public class Unit { - List students; + private List students; @PlanningId private int unitID; private String name; private Duration duration; @PlanningVariable - private LocalTime startTime; - @PlanningVariable private DayOfWeek dayOfWeek; @PlanningVariable + private LocalTime startTime; + @PlanningVariable private Room room; public Unit() { From d960b1a644f27490a800df78c98ba3d36dbeb1b7 Mon Sep 17 00:00:00 2001 From: tungkhanhh Date: Sat, 14 Sep 2024 20:52:25 +1000 Subject: [PATCH 5/7] feat: add isLab attribute to Room, whether it is a laboratory --- .../src/main/java/org/acme/TimetableResource.java | 6 +++--- backend/src/main/java/org/acme/domain/Room.java | 14 +++++++++++++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/backend/src/main/java/org/acme/TimetableResource.java b/backend/src/main/java/org/acme/TimetableResource.java index 2202575..0efb26b 100644 --- a/backend/src/main/java/org/acme/TimetableResource.java +++ b/backend/src/main/java/org/acme/TimetableResource.java @@ -44,9 +44,9 @@ public Timetable hello() throws ExecutionException, InterruptedException { Student h = new Student("h"); Student i = new Student("i"); - Room r1 = new Room("Room1", 2); - Room r2 = new Room("Room2", 3); - Room r3 = new Room("Room3", 4); + Room r1 = new Room("Room1", 2, true); + Room r2 = new Room("Room2", 3, false); + Room r3 = new Room("Room3", 4, false); var problem = new Timetable( List.of( diff --git a/backend/src/main/java/org/acme/domain/Room.java b/backend/src/main/java/org/acme/domain/Room.java index ef74552..57c7bbd 100644 --- a/backend/src/main/java/org/acme/domain/Room.java +++ b/backend/src/main/java/org/acme/domain/Room.java @@ -10,7 +10,9 @@ public class Room { @PlanningId private String id; + private int capacity; + private boolean isLab; public Room() { } @@ -20,10 +22,12 @@ public Room() { * * @param id The room’s id. * @param capacity The room's capacity. + * @param isLab Whether the room is a laboratory. */ - public Room(String id, int capacity) { + public Room(String id, int capacity, boolean isLab) { this.id = id; this.capacity = capacity; + this.isLab = isLab; } public String getId() { @@ -41,4 +45,12 @@ public int getCapacity() { public void setCapacity(int capacity) { this.capacity = capacity; } + + public boolean isLab() { + return isLab; + } + + public void setLab(boolean lab) { + isLab = lab; + } } \ No newline at end of file From b82d52f0ee73dd2a8c1a600a01c83c9ff7dd0c37 Mon Sep 17 00:00:00 2001 From: tungkhanhh Date: Sat, 14 Sep 2024 20:57:31 +1000 Subject: [PATCH 6/7] feat: add wantsLab attribute to Unit, whether it prefers a laboratory room --- .../main/java/org/acme/TimetableResource.java | 6 ++--- .../src/main/java/org/acme/domain/Unit.java | 24 +++++++++++++------ 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/backend/src/main/java/org/acme/TimetableResource.java b/backend/src/main/java/org/acme/TimetableResource.java index 0efb26b..dc1ffd9 100644 --- a/backend/src/main/java/org/acme/TimetableResource.java +++ b/backend/src/main/java/org/acme/TimetableResource.java @@ -50,9 +50,9 @@ public Timetable hello() throws ExecutionException, InterruptedException { var problem = new Timetable( List.of( - new Unit(1, "1", Duration.ofHours(2), List.of(a, b)), - new Unit(2, "2", Duration.ofHours(2), List.of(a, c, d, e)), - new Unit(3, "3", Duration.ofHours(2), List.of(f, g, h, i)) + new Unit(1, "1", Duration.ofHours(2), List.of(a, b), true), + new Unit(2, "2", Duration.ofHours(2), List.of(a, c, d, e), true), + new Unit(3, "3", Duration.ofHours(2), List.of(f, g, h, i), false) // new Unit(4, "4", Duration.ofHours(2), List.of(a, b)), // new Unit(5, "5", Duration.ofHours(2), List.of(c, d, e)), // new Unit(6, "6", Duration.ofHours(2), List.of(f, g, h, i)) diff --git a/backend/src/main/java/org/acme/domain/Unit.java b/backend/src/main/java/org/acme/domain/Unit.java index e09a274..570c9b7 100644 --- a/backend/src/main/java/org/acme/domain/Unit.java +++ b/backend/src/main/java/org/acme/domain/Unit.java @@ -30,6 +30,8 @@ public class Unit { @PlanningVariable private Room room; + private boolean wantsLab; + public Unit() { } @@ -55,14 +57,14 @@ public Unit(int unitID, String name, Duration duration, List students) * @param name The unit’s ID. * @param duration The unit’s duration. * @param students The list of students enrolled in the unit. - * @param room The room assigned to the unit. + * @param wantsLab Whether the unit wants a laboratory room. */ - public Unit(int unitID, String name, Duration duration, List students, Room room) { + public Unit(int unitID, String name, Duration duration, List students, boolean wantsLab) { this.unitID = unitID; this.name = name; this.duration = duration; this.students = students; - this.room = room; + this.wantsLab = wantsLab; } public int getUnitID() { @@ -89,6 +91,14 @@ public void setDuration(Duration duration) { this.duration = duration; } + public DayOfWeek getDayOfWeek() { + return dayOfWeek; + } + + public void setDayOfWeek(DayOfWeek dayOfWeek) { + this.dayOfWeek = dayOfWeek; + } + public LocalTime getStartTime() { return startTime; } @@ -126,11 +136,11 @@ public void setRoom(Room room) { this.room = room; } - public DayOfWeek getDayOfWeek() { - return dayOfWeek; + public boolean isWantsLab() { + return wantsLab; } - public void setDayOfWeek(DayOfWeek dayOfWeek) { - this.dayOfWeek = dayOfWeek; + public void setWantsLab(boolean wantsLab) { + this.wantsLab = wantsLab; } } \ No newline at end of file From a23e2a7497667ac25227b697cf29e2745b91a831 Mon Sep 17 00:00:00 2001 From: tungkhanhh Date: Sat, 14 Sep 2024 21:15:13 +1000 Subject: [PATCH 7/7] feat: add lab preference constraint, lab units prefer lab rooms --- .../main/java/org/acme/TimetableResource.java | 6 ++--- .../solver/TimetableConstraintProvider.java | 25 ++++++++++++++++--- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/backend/src/main/java/org/acme/TimetableResource.java b/backend/src/main/java/org/acme/TimetableResource.java index dc1ffd9..93cd1e3 100644 --- a/backend/src/main/java/org/acme/TimetableResource.java +++ b/backend/src/main/java/org/acme/TimetableResource.java @@ -45,15 +45,15 @@ public Timetable hello() throws ExecutionException, InterruptedException { Student i = new Student("i"); Room r1 = new Room("Room1", 2, true); - Room r2 = new Room("Room2", 3, false); + Room r2 = new Room("Room2", 4, false); Room r3 = new Room("Room3", 4, false); var problem = new Timetable( List.of( new Unit(1, "1", Duration.ofHours(2), List.of(a, b), true), new Unit(2, "2", Duration.ofHours(2), List.of(a, c, d, e), true), - new Unit(3, "3", Duration.ofHours(2), List.of(f, g, h, i), false) -// new Unit(4, "4", Duration.ofHours(2), List.of(a, b)), + new Unit(3, "3", Duration.ofHours(2), List.of(f, g, h, i), false), + new Unit(4, "4", Duration.ofHours(2), List.of(a, b), false) // new Unit(5, "5", Duration.ofHours(2), List.of(c, d, e)), // new Unit(6, "6", Duration.ofHours(2), List.of(f, g, h, i)) ), diff --git a/backend/src/main/java/org/acme/solver/TimetableConstraintProvider.java b/backend/src/main/java/org/acme/solver/TimetableConstraintProvider.java index af0f1b8..4e06a01 100644 --- a/backend/src/main/java/org/acme/solver/TimetableConstraintProvider.java +++ b/backend/src/main/java/org/acme/solver/TimetableConstraintProvider.java @@ -4,7 +4,6 @@ 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 ai.timefold.solver.core.api.score.stream.Joiners; import org.acme.domain.ConflictingUnit; import org.acme.domain.Unit; @@ -30,7 +29,8 @@ public Constraint[] defineConstraints(ConstraintFactory constraintFactory) { return new Constraint[]{ studentConflict(constraintFactory), roomConflict(constraintFactory), - roomCapacity(constraintFactory) + roomCapacity(constraintFactory), + labPreference(constraintFactory) }; } @@ -53,7 +53,8 @@ private Constraint studentConflict(ConstraintFactory constraintFactory) { (conflictingUnit, unit1) -> unit1.getEnd(), Unit::getStartTime, Unit::getEnd)) // ... and penalize each pair with a hard weight. - .penalize(HardSoftScore.ofHard(1), (conflictingUnit, unit1, unit2) -> conflictingUnit.getNumStudent()) + .penalize(HardSoftScore.ofHard(1), + (conflictingUnit, unit1, unit2) -> conflictingUnit.getNumStudent()) .asConstraint("Student conflict"); } @@ -85,8 +86,24 @@ Constraint roomCapacity(ConstraintFactory constraintFactory) { return constraintFactory .forEach(Unit.class) .filter(unit -> unit.getStudentSize() > unit.getRoom().getCapacity()) - .penalize(HardSoftScore.ofSoft(1), unit -> unit.getStudentSize() - unit.getRoom().getCapacity()) + .penalize(HardSoftScore.ofSoft(1), + unit -> unit.getStudentSize() - unit.getRoom().getCapacity()) .asConstraint("Room capacity conflict"); } + /** + * Penalize 1 soft score for each laboratory unit not assigned to a laboratory. + */ + Constraint labPreference(ConstraintFactory constraintFactory) { + // Some units prefer to have a laboratory room. + return constraintFactory + .forEach(Unit.class) + // Select a laboratory unit ... + .filter(Unit::isWantsLab) + // ... in a non-lab room ... + .filter(unit -> !unit.getRoom().isLab()) + .penalize(HardSoftScore.ofSoft(1)) + .asConstraint("Unit laboratory preference"); + } + } \ No newline at end of file