Spring Boot集成Timefold Solver实现课程表编排

1. 什么是Timefold Solver?

每个组织都面临规划问题:使用一组有限的资源(员工、资产、时间和金钱)提供产品或服务。Timefold Solver 优化了此类规划,以更少的资源开展更多业务。这被称为约束满足编程(属于运筹学学科的一部分)。

Timefold Solver是一个轻量级、可嵌入的约束满足引擎,可优化规划问题。它解决了以下用例:

  • 员工轮班排班:安排护士、修理工等的时间
  • 议程安排:安排会议、约会、维护工作、广告等
  • 教育时间表:安排课程、课程、考试、会议演示等
  • 车辆路线:使用已知的地图工具规划车辆路线(卡车、火车、轮船、飞机等),以便将货物和/或乘客运送到多个目的地……
  • 装箱:用物品填充集装箱、卡车、轮船和仓库,同时也跨计算机资源打包信息,例如云计算……
  • 车间调度:汽车装配线规划、机器队列规划、劳动力任务规划……
  • 切割库存:切割纸张、钢材、地毯等时尽量减少浪费……
  • 体育赛事安排:规划足球联赛、棒球联赛等的比赛和训练日程……
  • 财务优化:投资组合优化、风险分散……

useCaseOverview

1.1什么是规划问题?

whatIsAPlanningProblem

规划问题具有基于有限资源和特定约束的最优目标。最优目标可以是任意数量的事物,例如:

  • 利润最大化——最佳目标带来最高的利润。
  • 最小化生态足迹——最佳目标是对环境的影响最小。
  • 最大限度地提高员工或客户的满意度——最佳目标优先考虑员工或客户的需求。

实现这些目标的能力取决于可用资源的数量,例如:

  • 人的数量。
  • 多少时间。
  • 预算。
  • 实物资产,例如机械、车辆、计算机、建筑物等。

还必须考虑与这些资源相关的具体限制,例如一个人的工作时间、他们使用某些机器的能力或设备之间的兼容性。

Timefold Solver 可帮助 Java TM、Kotlin 和 Python 程序员高效地解决约束满足问题。在底层,它将优化启发式和元启发式与非常高效的分数计算相结合。

1.2规划问题是 NP 完全问题还是 NP 难题

所有上述用例可能是 NP-complete/NP-hard,用外行人的话来说就是:

  • 在合理的时间内验证问题的给定解决方案很容易。
  • 没有什么灵丹妙药可以在合理的时间内找到问题的最佳解决方案(*)。

(*) 至少,世界上最聪明的计算机科学家还没有找到这样的灵丹妙药。但如果他们能找到一个解决 NP 完全问题的灵丹妙药,那么它就能解决所有 NP 完全问题。

事实上,如果有人能证明这种灵丹妙药是否真的存在,就可以获得 1,000,000 美元的奖励。

这意味着非常可怕的事情:解决你的问题可能比你想象的要难,因为两种常用的技术是不够的:

  • 暴力算法(即使是更智能的变体)也会花费太长时间。
  • 一种快速算法,例如在装箱中,首先放入最大的物品,将返回远非最优的解决方案。

通过使用先进的优化算法,Timefold Solver 确实能够在合理的时间内为此类规划问题找到接近最优的解决方案。

1.3. 规划问题有(硬和软)约束

通常,规划问题至少有两个层次的约束:

  • (负面)硬约束不能被打破。例如:1 位老师不能同时教授 2 门不同的课程
  • 如果可以避免,则不应打破(负面)软约束。例如: A 老师不喜欢在星期五下午上课

有些问题也有积极的约束:

  • 如果可能的话,应该满足积极的软约束(或奖励)。例如:B老师喜欢在周一早上上课

有些基本问题只有硬约束,有些问题有三级或更多级的约束——硬约束、中等约束和软约束。

这些约束定义了规划问题的得分计算(又称适应度函数)。规划问题的每个解决方案都可以用分数来评分。 使用 Timefold Solver,问题可以在 Java TM、Kotlin 或 Python等语言中以面向对象范式建模。这样的代码简单、灵活且可扩展。

1.4. 规划问题具有巨大的搜索空间

规划问题有多种解决方案。解决方案有以下几类:

  • 任何解决方案都是可能的解决方案,无论它是否打破了多少限制。规划问题往往有大量的可能解决方案。其中许多解决方案毫无价值。
  • 可行是指不打破任何(负面)硬约束的解。可行解的数量往往与可能解的数量有关。有时没有可行解。每个可行解都是一个可能解。
  • 最优是得分最高的解。规划问题往往有 1 个或几个最优解。即使在没有可行解且最优解不可行的情况下,也总是至少有 1 个最优解。
  • 找到的最佳解决方案是实现在给定时间内找到的得分最高的解决方案。找到的最佳解决方案很可能是可行的,并且如果有足够的时间,它就是最优解决方案。

与直觉相反的是,即使数据集很小,可能的解决方案数量也是巨大的(如果计算正确的话)。正如您在示例中看到的,大多数情况下的可能解决方案比已知宇宙中原子的最小数量(10^80)要多得多。由于没有灵丹妙药可以找到最佳解决方案,因此任何实现都必须评估所有这些可能解决方案的至少一个子集。

Timefold Solver 支持多种优化算法,可高效地处理数量惊人的可能解决方案。根据用例,某些优化算法的性能优于其他算法,但无法提前知道。 使用 Timefold Solver,只需在几行代码中更改求解器配置,即可轻松切换优化算法。

2.代码工程

实验目的:

实现课程表编排,Lesson实例分配给TimeslotRoom实例以遵守硬调度和软调度约束,例如以下示例:

  • 一个房间最多可以同时上一节课。
  • 一位老师最多可以同时讲授一节课。
  • 一名学生最多可同时参加一节课。
  • 一位老师喜欢在同一个教室里讲授所有课程。
  • 一位老师喜欢教授连续的课程并且不喜欢课程之间有间隙。
  • 学生不喜欢连续学习同一科目。

从数学上讲,学校时间表是一个NP难题。这意味着它很难扩展。对于一个非平凡的数据集,简单地用蛮力迭代所有可能的组合需要数百万年的时间,即使在超级计算机上也是如此。幸运的是,Timefold Solver 等 AI 约束求解器拥有先进的算法,可以在合理的时间内提供接近最优的解决方案。

pom.xml

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><!-- Use spring boot parent to use its native profile --><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.2.1</version></parent><groupId>org.acme</groupId><artifactId>spring-boot-school-timetabling</artifactId><version>1.0-SNAPSHOT</version><properties><maven.compiler.release>17</maven.compiler.release><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><version.ai.timefold.solver>1.10.0</version.ai.timefold.solver><version.org.springframework.boot>${project.parent.version}</version.org.springframework.boot><version.compiler.plugin>3.13.0</version.compiler.plugin><version.surefire.plugin>3.2.5</version.surefire.plugin></properties><dependencyManagement><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>${version.org.springframework.boot}</version><type>pom</type><scope>import</scope></dependency><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><!-- Spring boot --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>ai.timefold.solver</groupId><artifactId>timefold-solver-spring-boot-starter</artifactId></dependency><!-- Swagger --><dependency><groupId>org.springdoc</groupId><artifactId>springdoc-openapi-starter-webmvc-ui</artifactId><version>2.5.0</version></dependency><!-- Testing --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>ai.timefold.solver</groupId><artifactId>timefold-solver-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-webflux</artifactId><scope>test</scope></dependency><dependency><groupId>org.awaitility</groupId><artifactId>awaitility</artifactId><scope>test</scope></dependency><!-- UI --><!-- No webjar locator; incompatible in native mode;see https://github.com/spring-projects/spring-framework/issues/27619and https://github.com/webjars/webjars-locator-core/issues/96--><dependency><groupId>ai.timefold.solver</groupId><artifactId>timefold-solver-webui</artifactId><scope>runtime</scope></dependency><dependency><groupId>org.webjars</groupId><artifactId>bootstrap</artifactId><version>5.2.3</version><scope>runtime</scope></dependency><dependency><groupId>org.webjars</groupId><artifactId>jquery</artifactId><version>3.6.4</version><scope>runtime</scope></dependency><dependency><groupId>org.webjars</groupId><artifactId>font-awesome</artifactId><version>5.15.1</version><scope>runtime</scope></dependency><dependency><groupId>org.webjars.npm</groupId><artifactId>js-joda</artifactId><version>1.11.0</version><scope>runtime</scope></dependency></dependencies><build><plugins><plugin><artifactId>maven-compiler-plugin</artifactId><version>${version.compiler.plugin}</version></plugin><plugin><artifactId>maven-surefire-plugin</artifactId><version>${version.surefire.plugin}</version></plugin><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><version>${version.org.springframework.boot}</version><executions><!-- Repackage the archive produced by maven-jar-plugin into an executable JAR file. --><execution><goals><goal>repackage</goal></goals></execution></executions><configuration><!-- optimizedLaunch disables the C2 compiler, which has a massive performance impact --><optimizedLaunch>false</optimizedLaunch></configuration></plugin><plugin><groupId>org.graalvm.buildtools</groupId><artifactId>native-maven-plugin</artifactId></plugin></plugins></build></project>

时间段

课程Timeslot代表授课的时间间隔,例如Monday 10:30 - 11:30Tuesday 13:30 - 14:30。为简单起见,所有时间段的持续时间相同,午餐或其他休息期间没有时间段。

package org.acme.schooltimetabling.domain;import ai.timefold.solver.core.api.domain.lookup.PlanningId;
import com.fasterxml.jackson.annotation.JsonIdentityInfo;
import com.fasterxml.jackson.annotation.ObjectIdGenerators;import java.time.DayOfWeek;
import java.time.LocalTime;@JsonIdentityInfo(scope = Timeslot.class, generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
public class Timeslot {@PlanningIdprivate String id;private DayOfWeek dayOfWeek;private LocalTime startTime;private LocalTime endTime;public Timeslot() {}public Timeslot(String id, DayOfWeek dayOfWeek, LocalTime startTime, LocalTime endTime) {this.id = id;this.dayOfWeek = dayOfWeek;this.startTime = startTime;this.endTime = endTime;}public Timeslot(String id, DayOfWeek dayOfWeek, LocalTime startTime) {this(id, dayOfWeek, startTime, startTime.plusMinutes(50));}@Overridepublic String toString() {return dayOfWeek + " " + startTime;}// ************************************************************************// Getters and setters// ************************************************************************public String getId() {return id;}public DayOfWeek getDayOfWeek() {return dayOfWeek;}public LocalTime getStartTime() {return startTime;}public LocalTime getEndTime() {return endTime;}
}

房间

教室Room代表授课地点,例如Room ARoom B。为简单起见,所有房间均无容量限制,可容纳所有课程。

package org.acme.schooltimetabling.domain;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 {@PlanningIdprivate String id;private String name;public Room() {}public Room(String id, String name) {this.id = id;this.name = name;}@Overridepublic String toString() {return name;}// ************************************************************************// Getters and setters// ************************************************************************public String getId() {return id;}public String getName() {return name;}
}

课程

在课堂上Lesson,一位老师向一组学生讲授一门课程,例如Math by A.Turing for 9th gradeChemistry by M.Curie for 10th grade。如果同一老师每周向同一组学生讲授同一门课程多次,则存在多个Lesson实例,只能通过 来区分id。例如,9 年级每周有 6 节数学课。

在解决过程中,Timefold Solver 会更改课程的timeslotroom字段Lesson,以将每节课分配到时间段和房间。由于 Timefold Solver 会更改这些字段,因此Lesson是一个规划实体

schoolTimetablingClassDiagramAnnotated

package org.acme.schooltimetabling.domain;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.JsonIdentityReference;@PlanningEntity
public class Lesson {@PlanningIdprivate String id;private String subject;private String teacher;private String studentGroup;@JsonIdentityReference@PlanningVariableprivate Timeslot timeslot;@JsonIdentityReference@PlanningVariableprivate Room room;public Lesson() {}public Lesson(String id, String subject, String teacher, String studentGroup) {this.id = id;this.subject = subject;this.teacher = teacher;this.studentGroup = studentGroup;}public Lesson(String id, String subject, String teacher, String studentGroup, Timeslot timeslot, Room room) {this(id, subject, teacher, studentGroup);this.timeslot = timeslot;this.room = room;}@Overridepublic String toString() {return subject + "(" + id + ")";}// ************************************************************************// Getters and setters// ************************************************************************public String getId() {return id;}public String getSubject() {return subject;}public String getTeacher() {return teacher;}public String getStudentGroup() {return studentGroup;}public Timeslot getTimeslot() {return timeslot;}public void setTimeslot(Timeslot timeslot) {this.timeslot = timeslot;}public Room getRoom() {return room;}public void setRoom(Room room) {this.room = room;}
}

定义约束并计算分数

分数代表特定解决方案的质量。分数越高越好。Timefold Solver 寻找最佳解决方案,即在可用时间内找到的得分最高的解决方案。它可能是最佳解决方案。

由于此用例有硬约束和软约束,因此使用HardSoftScore类来表示分数:

  • 不能打破硬性约束。例如:一个房间最多只能同时上一节课。
  • 软约束不能被打破。例如:老师喜欢在单人教室上课。

硬约束相对于其他硬约束具有权重。软约束相对于其他软约束也具有权重。 无论各自的权重如何,硬约束总是比软约束更重要。

package org.acme.schooltimetabling.solver;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 ai.timefold.solver.core.api.score.stream.Joiners;
import org.acme.schooltimetabling.domain.Lesson;
import org.acme.schooltimetabling.solver.justifications.*;import java.time.Duration;public class TimetableConstraintProvider implements ConstraintProvider {@Overridepublic Constraint[] defineConstraints(ConstraintFactory constraintFactory) {return new Constraint[] {// Hard constraintsroomConflict(constraintFactory),teacherConflict(constraintFactory),studentGroupConflict(constraintFactory),// Soft constraintsteacherRoomStability(constraintFactory),teacherTimeEfficiency(constraintFactory),studentGroupSubjectVariety(constraintFactory)};}Constraint roomConflict(ConstraintFactory constraintFactory) {// A room can accommodate at most one lesson at the same time.return constraintFactory// Select each pair of 2 different lessons ....forEachUniquePair(Lesson.class,// ... in the same timeslot ...Joiners.equal(Lesson::getTimeslot),// ... in the same room ...Joiners.equal(Lesson::getRoom))// ... and penalize each pair with a hard weight..penalize(HardSoftScore.ONE_HARD).justifyWith((lesson1, lesson2, score) -> new RoomConflictJustification(lesson1.getRoom(), lesson1, lesson2)).asConstraint("Room conflict");}Constraint teacherConflict(ConstraintFactory constraintFactory) {// A teacher can teach at most one lesson at the same time.return constraintFactory.forEachUniquePair(Lesson.class,Joiners.equal(Lesson::getTimeslot),Joiners.equal(Lesson::getTeacher)).penalize(HardSoftScore.ONE_HARD).justifyWith((lesson1, lesson2, score) -> new TeacherConflictJustification(lesson1.getTeacher(), lesson1, lesson2)).asConstraint("Teacher conflict");}Constraint studentGroupConflict(ConstraintFactory constraintFactory) {// A student can attend at most one lesson at the same time.return constraintFactory.forEachUniquePair(Lesson.class,Joiners.equal(Lesson::getTimeslot),Joiners.equal(Lesson::getStudentGroup)).penalize(HardSoftScore.ONE_HARD).justifyWith((lesson1, lesson2, score) -> new StudentGroupConflictJustification(lesson1.getStudentGroup(), lesson1, lesson2)).asConstraint("Student group conflict");}Constraint teacherRoomStability(ConstraintFactory constraintFactory) {// A teacher prefers to teach in a single room.return constraintFactory.forEachUniquePair(Lesson.class,Joiners.equal(Lesson::getTeacher)).filter((lesson1, lesson2) -> lesson1.getRoom() != lesson2.getRoom()).penalize(HardSoftScore.ONE_SOFT).justifyWith((lesson1, lesson2, score) -> new TeacherRoomStabilityJustification(lesson1.getTeacher(), lesson1, lesson2)).asConstraint("Teacher room stability");}Constraint teacherTimeEfficiency(ConstraintFactory constraintFactory) {// A teacher prefers to teach sequential lessons and dislikes gaps between lessons.return constraintFactory.forEach(Lesson.class).join(Lesson.class, Joiners.equal(Lesson::getTeacher),Joiners.equal((lesson) -> lesson.getTimeslot().getDayOfWeek())).filter((lesson1, lesson2) -> {Duration between = Duration.between(lesson1.getTimeslot().getEndTime(),lesson2.getTimeslot().getStartTime());return !between.isNegative() && between.compareTo(Duration.ofMinutes(30)) <= 0;}).reward(HardSoftScore.ONE_SOFT).justifyWith((lesson1, lesson2, score) -> new TeacherTimeEfficiencyJustification(lesson1.getTeacher(), lesson1, lesson2)).asConstraint("Teacher time efficiency");}Constraint studentGroupSubjectVariety(ConstraintFactory constraintFactory) {// A student group dislikes sequential lessons on the same subject.return constraintFactory.forEach(Lesson.class).join(Lesson.class,Joiners.equal(Lesson::getSubject),Joiners.equal(Lesson::getStudentGroup),Joiners.equal((lesson) -> lesson.getTimeslot().getDayOfWeek())).filter((lesson1, lesson2) -> {Duration between = Duration.between(lesson1.getTimeslot().getEndTime(),lesson2.getTimeslot().getStartTime());return !between.isNegative() && between.compareTo(Duration.ofMinutes(30)) <= 0;}).penalize(HardSoftScore.ONE_SOFT).justifyWith((lesson1, lesson2, score) -> new StudentGroupSubjectVarietyJustification(lesson1.getStudentGroup(), lesson1, lesson2)).asConstraint("Student group subject variety");}}

在规划解决方案中收集领域对象

ATimetable包装单个数据集的所有TimeslotRoom和实例。此外,由于它包含所有课程,每个课程都有特定的规划变量状态,因此它是一个规划解决方案,并且具有分数:Lesson

  • 如果课程仍未分配,那么它就是一个未初始化的解决方案,例如分数为 的解决方案-4init/0hard/0soft
  • 如果它打破了硬约束,那么它就是一个不可行的解决方案,例如得分为 的解决方案-2hard/-3soft
  • 如果它遵守所有硬性约束,那么它就是一个可行的解决方案,例如分数为 的解决方案0hard/-7soft
package org.acme.schooltimetabling.domain;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.solution.ProblemFactCollectionProperty;
import ai.timefold.solver.core.api.domain.valuerange.ValueRangeProvider;
import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore;
import ai.timefold.solver.core.api.solver.SolverStatus;import java.util.List;@PlanningSolution
public class Timetable {private String name;@ProblemFactCollectionProperty@ValueRangeProviderprivate List<Timeslot> timeslots;@ProblemFactCollectionProperty@ValueRangeProviderprivate List<Room> rooms;@PlanningEntityCollectionPropertyprivate List<Lesson> lessons;@PlanningScoreprivate HardSoftScore score;// Ignored by Timefold, used by the UI to display solve or stop solving buttonprivate SolverStatus solverStatus;// No-arg constructor required for Timefoldpublic Timetable() {}public Timetable(String name, HardSoftScore score, SolverStatus solverStatus) {this.name = name;this.score = score;this.solverStatus = solverStatus;}public Timetable(String name, List<Timeslot> timeslots, List<Room> rooms, List<Lesson> lessons) {this.name = name;this.timeslots = timeslots;this.rooms = rooms;this.lessons = lessons;}// ************************************************************************// Getters and setters// ************************************************************************public String getName() {return name;}public List<Timeslot> getTimeslots() {return timeslots;}public List<Room> getRooms() {return rooms;}public List<Lesson> getLessons() {return lessons;}public HardSoftScore getScore() {return score;}public SolverStatus getSolverStatus() {return solverStatus;}public void setSolverStatus(SolverStatus solverStatus) {this.solverStatus = solverStatus;}}

controller

package org.acme.schooltimetabling.rest;import java.util.Collection;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;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.schooltimetabling.domain.Timetable;
import org.acme.schooltimetabling.rest.exception.ErrorInfo;
import org.acme.schooltimetabling.rest.exception.TimetableSolverException;
import org.acme.schooltimetabling.solver.justifications.RoomConflictJustification;
import org.acme.schooltimetabling.solver.justifications.StudentGroupConflictJustification;
import org.acme.schooltimetabling.solver.justifications.StudentGroupSubjectVarietyJustification;
import org.acme.schooltimetabling.solver.justifications.TeacherConflictJustification;
import org.acme.schooltimetabling.solver.justifications.TeacherRoomStabilityJustification;
import org.acme.schooltimetabling.solver.justifications.TeacherTimeEfficiencyJustification;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aot.hint.annotation.RegisterReflectionForBinding;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;@Tag(name = "School Timetables", description = "School timetable service assigning lessons to rooms and timeslots.")
@RestController
@RequestMapping("/timetables")
public class TimetableController {private static final Logger LOGGER = LoggerFactory.getLogger(TimetableController.class);private final SolverManager<Timetable, String> solverManager;private final SolutionManager<Timetable, HardSoftScore> solutionManager;// TODO: Without any "time to live", the map may eventually grow out of memory.private final ConcurrentMap<String, Job> jobIdToJob = new ConcurrentHashMap<>();public TimetableController(SolverManager<Timetable, String> solverManager,SolutionManager<Timetable, HardSoftScore> solutionManager) {this.solverManager = solverManager;this.solutionManager = solutionManager;}@Operation(summary = "List the job IDs of all submitted timetables.")@ApiResponses(value = {@ApiResponse(responseCode = "200", description = "List of all job IDs.",content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE,schema = @Schema(type = "array", implementation = String.class))) })@GetMappingpublic Collection<String> list() {return jobIdToJob.keySet();}@Operation(summary = "Submit a timetable 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_VALUE,schema = @Schema(implementation = String.class))) })@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.TEXT_PLAIN_VALUE)public String solve(@RequestBody Timetable problem) {String jobId = UUID.randomUUID().toString();jobIdToJob.put(jobId, Job.ofTimetable(problem));solverManager.solveBuilder().withProblemId(jobId).withProblemFinder(jobId_ -> jobIdToJob.get(jobId).timetable).withBestSolutionConsumer(solution -> jobIdToJob.put(jobId, Job.ofTimetable(solution))).withExceptionHandler((jobId_, exception) -> {jobIdToJob.put(jobId, Job.ofException(exception));LOGGER.error("Failed solving jobId ({}).", jobId, exception);}).run();return jobId;}@Operation(summary = "Submit a timetable to analyze its score.")@ApiResponses(value = {@ApiResponse(responseCode = "202",description = "Resulting score analysis, optionally without constraint matches.",content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE,schema = @Schema(implementation = ScoreAnalysis.class))) })@PutMapping(value = "/analyze", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)@RegisterReflectionForBinding({RoomConflictJustification.class,StudentGroupConflictJustification.class,StudentGroupSubjectVarietyJustification.class,TeacherConflictJustification.class,TeacherRoomStabilityJustification.class,TeacherTimeEfficiencyJustification.class})public ScoreAnalysis<HardSoftScore> analyze(@RequestBody Timetable problem,@RequestParam(name = "fetchPolicy", required = false) 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 timetable so far.",content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE,schema = @Schema(implementation = Timetable.class))),@ApiResponse(responseCode = "404", description = "No timetable found.",content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE,schema = @Schema(implementation = ErrorInfo.class))),@ApiResponse(responseCode = "500", description = "Exception during solving a timetable.",content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE,schema = @Schema(implementation = ErrorInfo.class)))})@GetMapping(value = "/{jobId}", produces = MediaType.APPLICATION_JSON_VALUE)public Timetable getTimeTable(@Parameter(description = "The job ID returned by the POST method.") @PathVariable("jobId") String jobId) {Timetable timetable = getTimetableAndCheckForExceptions(jobId);SolverStatus solverStatus = solverManager.getSolverStatus(jobId);timetable.setSolverStatus(solverStatus);return timetable;}@Operation(summary = "Get the timetable status and score for a given job ID.")@ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The timetable status and the best score so far.",content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE,schema = @Schema(implementation = Timetable.class))),@ApiResponse(responseCode = "404", description = "No timetable found.",content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE,schema = @Schema(implementation = ErrorInfo.class))),@ApiResponse(responseCode = "500", description = "Exception during solving a timetable.",content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE,schema = @Schema(implementation = ErrorInfo.class)))})@GetMapping(value = "/{jobId}/status", produces = MediaType.APPLICATION_JSON_VALUE)public Timetable getStatus(@Parameter(description = "The job ID returned by the POST method.") @PathVariable("jobId") String jobId) {Timetable timetable = getTimetableAndCheckForExceptions(jobId);SolverStatus solverStatus = solverManager.getSolverStatus(jobId);return new Timetable(timetable.getName(), timetable.getScore(), solverStatus);}private Timetable getTimetableAndCheckForExceptions(String jobId) {Job job = jobIdToJob.get(jobId);if (job == null) {throw new TimetableSolverException(jobId, HttpStatus.NOT_FOUND, "No timetable found.");}if (job.exception != null) {throw new TimetableSolverException(jobId, job.exception);}return job.timetable;}@Operation(summary = "Terminate solving for a given job ID. Returns the best solution of the timetable so far, as it might still be running or not even started.")@ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The best solution of the timetable so far.",content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE,schema = @Schema(implementation = Timetable.class))),@ApiResponse(responseCode = "404", description = "No timetable found.",content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE,schema = @Schema(implementation = ErrorInfo.class))),@ApiResponse(responseCode = "500", description = "Exception during solving a timetable.",content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE,schema = @Schema(implementation = ErrorInfo.class)))})@DeleteMapping(value = "/{jobId}", produces = MediaType.APPLICATION_JSON_VALUE)public Timetable terminateSolving(@Parameter(description = "The job ID returned by the POST method.") @PathVariable("jobId") String jobId) {// TODO: Replace with .terminateEarlyAndWait(... [, timeout]); see https://github.com/TimefoldAI/timefold-solver/issues/77solverManager.terminateEarly(jobId);return getTimeTable(jobId);}private record Job(Timetable timetable, Throwable exception) {static Job ofTimetable(Timetable timetable) {return new Job(timetable, null);}static Job ofException(Throwable error) {return new Job(null, error);}}
}

设置终止时间

如果没有终止设置或terminationEarly()事件,求解器将永远运行。为避免这种情况,请将求解时间限制为五秒。这足够短,可以避免 HTTP 超时。

创建src/main/resources/application.properties文件:

# The solver runs only for 5 seconds to avoid a HTTP timeout in this simple implementation.
# It's recommended to run for at least 5 minutes ("5m") otherwise.
timefold.solver.termination.spent-limit=5s

Timefold Solver 返回在可用终止时间内找到的最佳解决方案。由于NP-hard 问题的性质,最佳解决方案可能不是最优的,尤其是对于较大的数据集。增加终止时间可能会找到更好的解决方案。

以上只是一些关键代码,所有代码请参见下面代码仓库

代码仓库

  • https://github.com/Harries/springboot-demo

3.测试

启动Spring Boot应用程序

时间表生成

访问http://127.0.0.1:8080/,点击solve

111

4.引用

  • Optimization algorithms :: Timefold Documentation
  • Spring Boot集成Timefold Solver实现课程表编排 | Harries Blog™

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/1451866.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

QT QFileDialog文件选择对话框

QT QFileDialog文件选择对话框 选择txt或者cpp文件&#xff0c;读取内容并显示 参考&#xff1a; QT写入文件与读取文件内容_qt往一个文件写东西-CSDN博客 #include "QtFilePreview.h" #include "qfiledialog.h" #include "qfile.h" #includ…

怎样收集企业名单?

收集企业名单的方法按照不同维度有不同的方式&#xff0c; 通过人工一个个收集&#xff0c;通过技术手段收集&#xff0c;通过第三方进行购买。 按照来源渠道&#xff0c;可以分为官方和非官方网站&#xff0c;官方的有公示系统&#xff0c;年报等。此外一些相对于官方的平台…

redis+lua实现分布式限流

redislua实现分布式限流 文章目录 redislua实现分布式限流为什么使用redislua实现分布式限流使用ZSET也可以实现限流&#xff0c;为什么选择lua的方式实现依赖lua脚本yaml代码实现 Jmeter压测 为什么使用redislua实现分布式限流 原子性&#xff1a;通过Lua脚本执行限流逻辑&am…

【计算机视觉】人脸算法之图像处理基础知识(三)

图像处理基础知识&#xff08;三&#xff09; 1.图像二值化 顾名思义&#xff0c;图像二值化是指一张图像上只有两种大小的像素值&#xff0c;常用的是0和255&#xff0c;0表示背景&#xff0c;255表示前景。这种处理方式是非常重要的&#xff0c;大部分的图像处理都会经历该…

简单Mesh多线程合并,使用什么库性能更高

1&#xff09;简单Mesh多线程合并&#xff0c;使用什么库性能更高 2&#xff09;Unity Semaphore.WaitForSignal耗时高 3&#xff09;VS编辑的C#代码注释的中文部分乱码 4&#xff09;变量IntPtr m_cachePtr切换线程后变空 这是第389篇UWA技术知识分享的推送&#xff0c;精选了…

【复旦邱锡鹏教授《神经网络与深度学习公开课》笔记】前馈神经网络

前馈神经网络又叫全连接神经网络、多层感知器&#xff0c;在网络中信息由输入到输出单向传递&#xff0c;具体特点有&#xff1a; 个神经元分别属于不同的层&#xff0c;层内无连接相邻两层之间的神经元全部两两连接整个网络中无反馈&#xff0c;信号从输入层像输出层单向传播…

【复旦邱锡鹏教授《神经网络与深度学习公开课》笔记】梯度的反向传播算法

矩阵微积分&#xff08;Matrix Calculus&#xff09; 在开始之前&#xff0c;需要先了解矩阵微积分的一些计算规则。 首先&#xff0c;对于矩阵微积分的表示&#xff0c;通常由两种符号约定&#xff1a; 分母布局 标量关于向量的导数为列向量 向量关于标量的导数为行向量 N维…

【LeetCode:2786. 访问数组中的位置使分数最大 + 递归 + 记忆化缓存 + dp】

&#x1f680; 算法题 &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&#xff0c;…

Real3D:利用真实世界图像扩展3D重建模型

原理&#xff1a; 在3D重建领域&#xff0c;单视图重建任务由于其固有的不确定性而充满挑战。为了克服这一难题&#xff0c;研究者们一直在探索如何利用大型数据集训练模型以学习形状和纹理的通用先验知识。然而&#xff0c;现有训练方法依赖于合成数据或多视图捕获&#xff0c…

U-Mail国产信创邮件系统,让企业通信更加安全可控

信息技术应用创新产业&#xff0c;即信创产业&#xff0c;是信息化建设的核心&#xff0c;它涵盖了从硬件到软件的一系列关键技术。信创产业的目标是通过自主研发&#xff0c;减少对外部技术的依赖&#xff0c;增强信息安全&#xff0c;并提升国内产业的全球竞争力。该产业主要…

java打印99乘法表

public class NineNineMulTable{public static void main(String[] args){for(int i 1; i < 9; i ){for(int j 1; j < i; j ){System.out.print(j " * " i " " i * j "\t");//再次先输出j在输出i是打印出来是1*2&#xff0c;2*2}S…

Allegro X PCB设计小诀窍--如何在Allegro X中快速设置快捷键

背景介绍&#xff1a;我们在进行PCB设计时&#xff0c;经常会用到一些高频次操作&#xff0c;例如移动、复制、删除、旋转、绘制走线、铺铜等&#xff0c;这些操作在软件中通常需要点击对应命令菜单来实现。为了点击这些菜单&#xff0c;设计人员需要通过鼠标频繁的在设计界面进…

积木搭建游戏-第13届蓝桥杯省赛Python真题精选

[导读]&#xff1a;超平老师的Scratch蓝桥杯真题解读系列在推出之后&#xff0c;受到了广大老师和家长的好评&#xff0c;非常感谢各位的认可和厚爱。作为回馈&#xff0c;超平老师计划推出《Python蓝桥杯真题解析100讲》&#xff0c;这是解读系列的第83讲。 积木搭建游戏&…

三款有3D效果的js图表库

1、G2简洁的渐进式可视化语法。https://g2.antv.antgroup.com/manual/extra-topics/3d-charts 2、 https://www.highcharts.com/https://www.highcharts.com/ 3、https://www.fusioncharts.com/charts/pie-doughnut-charts/donut-chart-in-3d?frameworkjavascripthttps://www…

30V转5V3.5A大电流芯片 30降压12V3.5A DCDC低功耗恒压IC-H4012-车充芯片

H4012芯片是一款同步降压型DC-DC转换器&#xff0c;为高效率和大电流应用设计。它内置了30V耐压的MOS&#xff0c;并支持3.5A的持续输出电流&#xff0c;使得它在需要高功率输出的应用中表现出色。此外&#xff0c;H4012的输出电压可调&#xff0c;可支持100%占空比&#xff0c…

el-cascader 支持多层级,多选(可自定义限制数量),保留最后一级

多功能的 el-cascader 序言&#xff1a;最近遇到一个需求关于级联的&#xff0c;有点东西&#xff0c;这里是要获取某个产品类型下的产品&#xff0c;会存在产品类型和产品在同一级的情况&#xff0c;但是产品类型不能勾选&#xff1b; 情况1&#xff08;二级菜单是产品&…

深度学习笔记: 最详尽估算送达时间系统设计

欢迎收藏Star我的Machine Learning Blog:https://github.com/purepisces/Wenqing-Machine_Learning_Blog。如果收藏star, 有问题可以随时与我交流, 谢谢大家&#xff01; 估算送达时间 1. 问题陈述 构建一个模型来估算在给定订单详情、市场条件和交通状况下的总送达时间。 为…

python-jupyter notebook安装教程

&#x1f308;所属专栏&#xff1a;【python】✨作者主页&#xff1a; Mr.Zwq✔️个人简介&#xff1a;一个正在努力学技术的Python领域创作者&#xff0c;擅长爬虫&#xff0c;逆向&#xff0c;全栈方向&#xff0c;专注基础和实战分享&#xff0c;欢迎咨询&#xff01; 您的…

Java内存模型,堆、栈和方法区的区别

Java内存管理是Java虚拟机&#xff08;JVM&#xff09;技术的核心之一。了解Java内存管理对于提高程序性能、解决内存泄漏和优化资源利用至关重要。 一、Java内存模型&#xff08;Java Memory Model, JMM&#xff09; Java内存模型描述了Java程序中变量&#xff08;包括实例字…

5.1 Python 函数的参数类型

1. 实参与形参 形参: 函数定义阶段, 括号内定义的参数名(变量名), 形参的表现形式只有一种就是参数命. 实参: 函数调用阶段, 括号内传入的参数值(变量值), 实参的表现形式有很多种(核心: 可以引用到值).两者之间的关系: 函数调用阶段 --> 实参的值绑定给形参名. 函数调用完…