GST-4 Timefold Solver

This commit is contained in:
Geoffrey De Smet
2023-11-21 14:09:04 +01:00
parent c10a404f57
commit 1f5f4af22a
9 changed files with 330 additions and 0 deletions
+2
View File
@@ -894,6 +894,7 @@
<module>tensorflow-java</module>
<module>testing-modules</module>
<module>testing-modules/mockito-simple</module>
<module>timefold-solver</module>
<module>vaadin</module>
<module>vavr-modules</module>
<module>vertx-modules</module>
@@ -1139,6 +1140,7 @@
<module>tensorflow-java</module>
<module>testing-modules</module>
<module>testing-modules/mockito-simple</module>
<module>timefold-solver</module>
<module>vaadin</module>
<module>vavr-modules</module>
<module>vertx-modules</module>
+7
View File
@@ -0,0 +1,7 @@
## OptaPlanner
This module contains articles about OptaPlanner.
### Relevant articles
- [A Guide to OptaPlanner](https://www.baeldung.com/opta-planner)
+44
View File
@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>timefold-solver</artifactId>
<name>timefold-solver</name>
<parent>
<groupId>com.baeldung</groupId>
<artifactId>parent-modules</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>ai.timefold.solver</groupId>
<artifactId>timefold-solver-bom</artifactId>
<version>${version.ai.timefold.solver}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>ai.timefold.solver</groupId>
<artifactId>timefold-solver-core</artifactId>
</dependency>
<dependency>
<groupId>ai.timefold.solver</groupId>
<artifactId>timefold-solver-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<version.ai.timefold.solver>1.4.0</version.ai.timefold.solver>
</properties>
</project>
@@ -0,0 +1,32 @@
package com.baeldung.timefoldsolver;
import java.util.Set;
public class Employee {
private String name;
private Set<String> skills;
public Employee(String name, Set<String> skills) {
this.name = name;
this.skills = skills;
}
@Override
public String toString() {
return name;
}
// ************************************************************************
// Getters and setters
// ************************************************************************
public String getName() {
return name;
}
public Set<String> getSkills() {
return skills;
}
}
@@ -0,0 +1,57 @@
package com.baeldung.timefoldsolver;
import java.time.LocalDateTime;
import ai.timefold.solver.core.api.domain.entity.PlanningEntity;
import ai.timefold.solver.core.api.domain.variable.PlanningVariable;
@PlanningEntity
public class Shift {
private LocalDateTime start;
private LocalDateTime end;
private String requiredSkill;
@PlanningVariable
private Employee employee;
// A no-arg constructor is required for @PlanningEntity annotated classes
public Shift() {}
public Shift(LocalDateTime start, LocalDateTime end, String requiredSkill) {
this(start, end, requiredSkill, null);
}
public Shift(LocalDateTime start, LocalDateTime end, String requiredSkill, Employee employee) {
this.start = start;
this.end = end;
this.requiredSkill = requiredSkill;
this.employee = employee;
}
@Override
public String toString() {
return start + " - " + end;
}
// ************************************************************************
// Getters and setters
// ************************************************************************
public LocalDateTime getStart() {
return start;
}
public LocalDateTime getEnd() {
return end;
}
public String getRequiredSkill() {
return requiredSkill;
}
public Employee getEmployee() {
return employee;
}
}
@@ -0,0 +1,47 @@
package com.baeldung.timefoldsolver;
import java.util.List;
import ai.timefold.solver.core.api.domain.solution.PlanningEntityCollectionProperty;
import ai.timefold.solver.core.api.domain.solution.PlanningScore;
import ai.timefold.solver.core.api.domain.solution.PlanningSolution;
import ai.timefold.solver.core.api.domain.valuerange.ValueRangeProvider;
import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore;
@PlanningSolution
public class ShiftSchedule {
@ValueRangeProvider
private List<Employee> employees;
@PlanningEntityCollectionProperty
private List<Shift> shifts;
@PlanningScore
private HardSoftScore score = null;
// A no-arg constructor is required for @PlanningSolution annotated classes
public ShiftSchedule() {
}
public ShiftSchedule(List<Employee> employees, List<Shift> shifts) {
this.employees = employees;
this.shifts = shifts;
}
// ************************************************************************
// Getters and setters
// ************************************************************************
public List<Employee> getEmployees() {
return employees;
}
public List<Shift> getShifts() {
return shifts;
}
public HardSoftScore getScore() {
return score;
}
}
@@ -0,0 +1,54 @@
package com.baeldung.timefoldsolver;
import java.time.Duration;
import java.time.LocalDate;
import java.util.List;
import java.util.Set;
import ai.timefold.solver.core.api.solver.Solver;
import ai.timefold.solver.core.api.solver.SolverFactory;
import ai.timefold.solver.core.config.solver.SolverConfig;
public class ShiftScheduleApp {
public static void main(String[] args) {
SolverFactory<ShiftSchedule> solverFactory = SolverFactory.create(new SolverConfig()
.withSolutionClass(ShiftSchedule.class)
.withEntityClasses(Shift.class)
.withConstraintProviderClass(ShiftScheduleConstraintProvider.class)
// The solver runs only for 5 seconds on this small dataset.
// It's recommended to run for at least 5 minutes ("5m") otherwise.
.withTerminationSpentLimit(Duration.ofSeconds(5)));
Solver<ShiftSchedule> solver = solverFactory.buildSolver();
ShiftSchedule problem = loadProblem();
ShiftSchedule solution = solver.solve(problem);
printSolution(solution);
}
private static ShiftSchedule loadProblem() {
LocalDate monday = LocalDate.of(2030, 4, 1);
LocalDate tuesday = LocalDate.of(2030, 4, 2);
return new ShiftSchedule(List.of(
new Employee("Ann", Set.of("Bartender")),
new Employee("Beth", Set.of("Waiter", "Bartender")),
new Employee("Carl", Set.of("Waiter"))
), List.of(
new Shift(monday.atTime(6, 0), monday.atTime(14, 0), "Waiter"),
new Shift(monday.atTime(9, 0), monday.atTime(17, 0), "Bartender"),
new Shift(monday.atTime(14, 0), monday.atTime(22, 0), "Bartender"),
new Shift(tuesday.atTime(6, 0), tuesday.atTime(14, 0), "Waiter"),
new Shift(tuesday.atTime(14, 0), tuesday.atTime(22, 0), "Bartender")
));
}
private static void printSolution(ShiftSchedule solution) {
System.out.println("Shift assignments");
for (Shift shift : solution.getShifts()) {
System.out.println(" " + shift.getStart().toLocalDate()
+ " " + shift.getStart().toLocalTime() + " - " + shift.getEnd().toLocalTime()
+ ": " + shift.getEmployee().getName());
}
}
}
@@ -0,0 +1,37 @@
package com.baeldung.timefoldsolver;
import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore;
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 static ai.timefold.solver.core.api.score.stream.Joiners.equal;
public class ShiftScheduleConstraintProvider implements ConstraintProvider {
@Override
public Constraint[] defineConstraints(ConstraintFactory constraintFactory) {
return new Constraint[] {
atMostOneShiftPerDay(constraintFactory),
requiredSkill(constraintFactory)
};
}
public Constraint atMostOneShiftPerDay(ConstraintFactory constraintFactory) {
return constraintFactory.forEach(Shift.class)
.join(Shift.class,
equal(shift -> shift.getStart().toLocalDate()),
equal(Shift::getEmployee))
.filter((shift1, shift2) -> shift1 != shift2)
.penalize(HardSoftScore.ONE_HARD)
.asConstraint("At most one shift per day");
}
public Constraint requiredSkill(ConstraintFactory constraintFactory) {
return constraintFactory.forEach(Shift.class)
.filter(shift -> !shift.getEmployee().getSkills().contains(shift.getRequiredSkill()))
.penalize(HardSoftScore.ONE_HARD)
.asConstraint("Required skill");
}
}
@@ -0,0 +1,50 @@
package com.baeldung.timefoldsolver;
import java.time.LocalDate;
import java.util.Set;
import ai.timefold.solver.test.api.score.stream.ConstraintVerifier;
import org.junit.jupiter.api.Test;
class ShiftScheduleConstraintProviderTest {
private static final LocalDate MONDAY = LocalDate.of(2030, 4, 1);
private static final LocalDate TUESDAY = LocalDate.of(2030, 4, 2);
ConstraintVerifier<ShiftScheduleConstraintProvider, ShiftSchedule> constraintVerifier = ConstraintVerifier.build(
new ShiftScheduleConstraintProvider(), ShiftSchedule.class, Shift.class);
@Test
void atMostOneShiftPerDay() {
Employee ann = new Employee("Ann", null);
constraintVerifier.verifyThat(ShiftScheduleConstraintProvider::atMostOneShiftPerDay)
.given(
ann,
new Shift(MONDAY.atTime(6, 0), MONDAY.atTime(14, 0), null, ann),
new Shift(MONDAY.atTime(14, 0), MONDAY.atTime(22, 0), null, ann))
// Penalizes both A-B and B-A. To avoid that, use forEachUniquePair() in the constraint instead.
.penalizesBy(2);
constraintVerifier.verifyThat(ShiftScheduleConstraintProvider::atMostOneShiftPerDay)
.given(
ann,
new Shift(MONDAY.atTime(6, 0), MONDAY.atTime(14, 0), null, ann),
new Shift(TUESDAY.atTime(14, 0), TUESDAY.atTime(22, 0), null, ann))
.penalizesBy(0);
}
@Test
void requiredSkill() {
Employee ann = new Employee("Ann", Set.of("Waiter"));
constraintVerifier.verifyThat(ShiftScheduleConstraintProvider::requiredSkill)
.given(
ann,
new Shift(MONDAY.atTime(6, 0), MONDAY.atTime(14, 0), "Cook", ann))
.penalizesBy(1);
constraintVerifier.verifyThat(ShiftScheduleConstraintProvider::requiredSkill)
.given(
ann,
new Shift(MONDAY.atTime(6, 0), MONDAY.atTime(14, 0), "Waiter", ann))
.penalizesBy(0);
}
}