├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── config.yml ├── dependabot.yml ├── pull_request_template.md ├── scripts │ ├── change_versions.sh │ └── run_python_tests.sh ├── semantic.yml └── workflows │ ├── pull_request_gradle.yml │ ├── pull_request_maven.yml │ ├── pull_request_maven_long_running.yml │ ├── pull_request_python.yml │ ├── release-pr-body.md │ └── release.yml ├── .gitignore ├── CONTRIBUTING.adoc ├── LICENSE.txt ├── README.md ├── java ├── bed-allocation │ ├── README.adoc │ ├── bed-scheduling-screenshot.png │ ├── pom.xml │ └── src │ │ ├── main │ │ ├── java │ │ │ └── org │ │ │ │ └── acme │ │ │ │ └── bedallocation │ │ │ │ ├── domain │ │ │ │ ├── Bed.java │ │ │ │ ├── BedPlan.java │ │ │ │ ├── Department.java │ │ │ │ ├── Gender.java │ │ │ │ ├── GenderLimitation.java │ │ │ │ ├── Room.java │ │ │ │ └── Stay.java │ │ │ │ ├── rest │ │ │ │ ├── BedSchedulingDemoResource.java │ │ │ │ ├── BedSchedulingResource.java │ │ │ │ ├── DemoDataGenerator.java │ │ │ │ └── exception │ │ │ │ │ ├── ErrorInfo.java │ │ │ │ │ ├── ScheduleSolverException.java │ │ │ │ │ └── ScheduleSolverExceptionMapper.java │ │ │ │ └── solver │ │ │ │ └── BedAllocationConstraintProvider.java │ │ └── resources │ │ │ ├── META-INF │ │ │ └── resources │ │ │ │ ├── app.js │ │ │ │ └── index.html │ │ │ └── application.properties │ │ └── test │ │ └── java │ │ └── org │ │ └── acme │ │ └── bedallocation │ │ ├── rest │ │ ├── BedSchedulingEnvironmentTest.java │ │ ├── BedSchedulingResourceIT.java │ │ └── BedSchedulingResourceTest.java │ │ └── solver │ │ └── BedAllocationConstraintProviderTest.java ├── conference-scheduling │ ├── README.adoc │ ├── conference-scheduling-screenshot.png │ ├── pom.xml │ └── src │ │ ├── main │ │ ├── java │ │ │ └── org │ │ │ │ └── acme │ │ │ │ └── conferencescheduling │ │ │ │ ├── domain │ │ │ │ ├── ConferenceConstraintProperties.java │ │ │ │ ├── ConferenceSchedule.java │ │ │ │ ├── Room.java │ │ │ │ ├── Speaker.java │ │ │ │ ├── Talk.java │ │ │ │ ├── TalkType.java │ │ │ │ └── Timeslot.java │ │ │ │ ├── rest │ │ │ │ ├── ConferenceSchedulingDemoResource.java │ │ │ │ ├── ConferenceSchedulingResource.java │ │ │ │ ├── DemoDataGenerator.java │ │ │ │ └── exception │ │ │ │ │ ├── ConferenceScheduleSolverException.java │ │ │ │ │ ├── ConferenceScheduleSolverExceptionMapper.java │ │ │ │ │ └── ErrorInfo.java │ │ │ │ └── solver │ │ │ │ ├── ConferenceSchedulingConstraintProvider.java │ │ │ │ └── justifications │ │ │ │ ├── ConferenceSchedulingJustification.java │ │ │ │ ├── ConflictTalkJustification.java │ │ │ │ ├── DiversityTalkJustification.java │ │ │ │ ├── PreferredTagsJustification.java │ │ │ │ ├── ProhibitedTagsJustification.java │ │ │ │ ├── RequiredTagsJustification.java │ │ │ │ ├── UnavailableTimeslotJustification.java │ │ │ │ └── UndesiredTagsJustification.java │ │ └── resources │ │ │ ├── META-INF │ │ │ └── resources │ │ │ │ ├── app.js │ │ │ │ └── index.html │ │ │ └── application.properties │ │ └── test │ │ └── java │ │ └── org │ │ └── acme │ │ └── conferencescheduling │ │ ├── rest │ │ ├── ConferenceScheduleResourceTest.java │ │ ├── ConferenceSchedulingEnvironmentTest.java │ │ └── ConferenceSchedulingResourceIT.java │ │ └── solver │ │ └── ConferenceSchedulingConstraintProviderTest.java ├── employee-scheduling │ ├── README.MD │ ├── employee-scheduling-screenshot.png │ ├── pom.xml │ └── src │ │ ├── main │ │ ├── java │ │ │ └── org │ │ │ │ └── acme │ │ │ │ └── employeescheduling │ │ │ │ ├── domain │ │ │ │ ├── Employee.java │ │ │ │ ├── EmployeeSchedule.java │ │ │ │ └── Shift.java │ │ │ │ ├── rest │ │ │ │ ├── DemoDataGenerator.java │ │ │ │ ├── EmployeeScheduleDemoResource.java │ │ │ │ ├── EmployeeScheduleResource.java │ │ │ │ └── exception │ │ │ │ │ ├── EmployeeScheduleSolverException.java │ │ │ │ │ ├── EmployeeScheduleSolverExceptionMapper.java │ │ │ │ │ └── ErrorInfo.java │ │ │ │ └── solver │ │ │ │ └── EmployeeSchedulingConstraintProvider.java │ │ └── resources │ │ │ ├── META-INF │ │ │ └── resources │ │ │ │ ├── app.js │ │ │ │ └── index.html │ │ │ └── application.properties │ │ └── test │ │ └── java │ │ └── org │ │ └── acme │ │ └── employeescheduling │ │ ├── rest │ │ ├── EmployeeScheduleResourceTest.java │ │ ├── EmployeeSchedulingEnvironmentTest.java │ │ └── EmployeeSchedulingResourceIT.java │ │ └── solver │ │ └── EmployeeSchedulingConstraintProviderTest.java ├── facility-location │ ├── README.adoc │ ├── facility-location-screenshot.png │ ├── pom.xml │ └── src │ │ ├── main │ │ ├── java │ │ │ └── org │ │ │ │ └── acme │ │ │ │ └── facilitylocation │ │ │ │ ├── bootstrap │ │ │ │ ├── DemoDataBuilder.java │ │ │ │ └── DemoDataGenerator.java │ │ │ │ ├── domain │ │ │ │ ├── Consumer.java │ │ │ │ ├── Facility.java │ │ │ │ ├── FacilityLocationProblem.java │ │ │ │ └── Location.java │ │ │ │ ├── persistence │ │ │ │ └── FacilityLocationProblemRepository.java │ │ │ │ ├── rest │ │ │ │ ├── SolverResource.java │ │ │ │ └── Status.java │ │ │ │ └── solver │ │ │ │ └── FacilityLocationConstraintProvider.java │ │ └── resources │ │ │ ├── META-INF │ │ │ └── resources │ │ │ │ ├── app.js │ │ │ │ └── index.html │ │ │ └── application.properties │ │ └── test │ │ └── java │ │ └── org │ │ └── acme │ │ └── facilitylocation │ │ ├── bootstrap │ │ └── DemoDataBuilderTest.java │ │ └── solver │ │ ├── FacilityLocationConstraintProviderTest.java │ │ ├── FacilityLocationEnvironmentTest.java │ │ ├── FacilityLocationResourceIT.java │ │ └── SolverManagerTest.java ├── flight-crew-scheduling │ ├── README.adoc │ ├── flight-crew-scheduling-screenshot.png │ ├── pom.xml │ └── src │ │ ├── main │ │ ├── java │ │ │ └── org │ │ │ │ └── acme │ │ │ │ └── flighcrewscheduling │ │ │ │ ├── domain │ │ │ │ ├── Airport.java │ │ │ │ ├── Employee.java │ │ │ │ ├── Flight.java │ │ │ │ ├── FlightAssignment.java │ │ │ │ └── FlightCrewSchedule.java │ │ │ │ ├── rest │ │ │ │ ├── DemoDataGenerator.java │ │ │ │ ├── FlightCrewSchedulingDemoResource.java │ │ │ │ ├── FlightCrewSchedulingResource.java │ │ │ │ └── exception │ │ │ │ │ ├── ErrorInfo.java │ │ │ │ │ ├── ScheduleSolverException.java │ │ │ │ │ └── ScheduleSolverExceptionMapper.java │ │ │ │ └── solver │ │ │ │ └── FlightCrewSchedulingConstraintProvider.java │ │ └── resources │ │ │ ├── META-INF │ │ │ └── resources │ │ │ │ ├── app.js │ │ │ │ └── index.html │ │ │ └── application.properties │ │ └── test │ │ └── java │ │ └── org │ │ └── acme │ │ └── flighcrewscheduling │ │ ├── rest │ │ ├── FlightCrewSchedulingEnvironmentTest.java │ │ ├── FlightCrewSchedulingResourceIT.java │ │ └── FlightCrewSchedulingResourceTest.java │ │ └── solver │ │ └── FlightCrewSchedulingConstraintProviderTest.java ├── food-packaging │ ├── README.adoc │ ├── food-packaging-screenshot.png │ ├── pom.xml │ └── src │ │ ├── main │ │ ├── java │ │ │ └── org │ │ │ │ └── acme │ │ │ │ └── foodpackaging │ │ │ │ ├── bootstrap │ │ │ │ └── DemoDataGenerator.java │ │ │ │ ├── domain │ │ │ │ ├── Job.java │ │ │ │ ├── Line.java │ │ │ │ ├── PackagingSchedule.java │ │ │ │ ├── Product.java │ │ │ │ └── WorkCalendar.java │ │ │ │ ├── persistence │ │ │ │ └── PackagingScheduleRepository.java │ │ │ │ ├── rest │ │ │ │ └── PackagingScheduleResource.java │ │ │ │ └── solver │ │ │ │ └── FoodPackagingConstraintProvider.java │ │ └── resources │ │ │ ├── META-INF │ │ │ └── resources │ │ │ │ ├── app.js │ │ │ │ └── index.html │ │ │ └── application.properties │ │ └── test │ │ └── java │ │ └── org │ │ └── acme │ │ └── foodpackaging │ │ └── solver │ │ ├── FoodPackagingConstraintProviderTest.java │ │ ├── FoodPackagingResourceIT.java │ │ └── FoodPackingEnvironmentTest.java ├── hello-world │ ├── README.adoc │ ├── build.gradle │ ├── pom.xml │ └── src │ │ ├── assembly │ │ └── jar-with-dependencies-and-services.xml │ │ ├── main │ │ ├── java │ │ │ └── org │ │ │ │ └── acme │ │ │ │ └── schooltimetabling │ │ │ │ ├── TimetableApp.java │ │ │ │ ├── domain │ │ │ │ ├── Lesson.java │ │ │ │ ├── Room.java │ │ │ │ ├── Timeslot.java │ │ │ │ └── Timetable.java │ │ │ │ └── solver │ │ │ │ ├── TimetableConstraintProvider.java │ │ │ │ └── justifications │ │ │ │ ├── RoomConflictJustification.java │ │ │ │ ├── StudentGroupConflictJustification.java │ │ │ │ ├── StudentGroupSubjectVarietyJustification.java │ │ │ │ ├── TeacherConflictJustification.java │ │ │ │ ├── TeacherRoomStabilityJustification.java │ │ │ │ └── TeacherTimeEfficiencyJustification.java │ │ └── resources │ │ │ └── logback.xml │ │ └── test │ │ └── java │ │ └── org │ │ └── acme │ │ └── schooltimetabling │ │ ├── JarWithDependenciesIT.java │ │ └── solver │ │ ├── TimetableConstraintProviderTest.java │ │ └── TimetableEnvironmentTest.java ├── maintenance-scheduling │ ├── README.adoc │ ├── maintenance-scheduling-screenshot.png │ ├── pom.xml │ └── src │ │ ├── main │ │ ├── java │ │ │ └── org │ │ │ │ └── acme │ │ │ │ └── maintenancescheduling │ │ │ │ ├── domain │ │ │ │ ├── Crew.java │ │ │ │ ├── Job.java │ │ │ │ ├── MaintenanceSchedule.java │ │ │ │ └── WorkCalendar.java │ │ │ │ ├── rest │ │ │ │ ├── DemoDataGenerator.java │ │ │ │ ├── MaintenanceScheduleDemoResource.java │ │ │ │ ├── MaintenanceScheduleResource.java │ │ │ │ └── exception │ │ │ │ │ ├── ErrorInfo.java │ │ │ │ │ ├── MaintenanceScheduleSolverException.java │ │ │ │ │ └── MaintenanceScheduleSolverExceptionMapper.java │ │ │ │ └── solver │ │ │ │ ├── EndDateUpdatingVariableListener.java │ │ │ │ └── MaintenanceScheduleConstraintProvider.java │ │ └── resources │ │ │ ├── META-INF │ │ │ └── resources │ │ │ │ ├── app.js │ │ │ │ └── index.html │ │ │ └── application.properties │ │ └── test │ │ └── java │ │ └── org │ │ └── acme │ │ └── maintenancescheduling │ │ ├── rest │ │ ├── MaintenanceSchedulingEnvironmentTest.java │ │ ├── MaintenanceSchedulingResourceIT.java │ │ └── MaintenanceSchedulingResourceTest.java │ │ └── solver │ │ └── MaintenanceSchedulingConstraintProviderTest.java ├── meeting-scheduling │ ├── README.adoc │ ├── meeting-scheduling-screenshot.png │ ├── pom.xml │ └── src │ │ ├── main │ │ ├── java │ │ │ └── org │ │ │ │ └── acme │ │ │ │ └── meetingschedule │ │ │ │ ├── domain │ │ │ │ ├── Attendance.java │ │ │ │ ├── Meeting.java │ │ │ │ ├── MeetingAssignment.java │ │ │ │ ├── MeetingSchedule.java │ │ │ │ ├── Person.java │ │ │ │ ├── PreferredAttendance.java │ │ │ │ ├── RequiredAttendance.java │ │ │ │ ├── Room.java │ │ │ │ └── TimeGrain.java │ │ │ │ ├── rest │ │ │ │ ├── DemoDataGenerator.java │ │ │ │ ├── MeetingSchedulingDemoResource.java │ │ │ │ ├── MeetingSchedulingResource.java │ │ │ │ └── exception │ │ │ │ │ ├── ErrorInfo.java │ │ │ │ │ ├── ScheduleSolverException.java │ │ │ │ │ └── ScheduleSolverExceptionMapper.java │ │ │ │ └── solver │ │ │ │ └── MeetingSchedulingConstraintProvider.java │ │ └── resources │ │ │ ├── META-INF │ │ │ └── resources │ │ │ │ ├── app.js │ │ │ │ └── index.html │ │ │ └── application.properties │ │ └── test │ │ └── java │ │ └── org │ │ └── acme │ │ └── meetscheduling │ │ ├── rest │ │ ├── MeetingSchedulingEnvironmentTest.java │ │ ├── MeetingSchedulingResourceIT.java │ │ └── MeetingSchedulingResourceTest.java │ │ └── solver │ │ └── MeetingSchedulingConstraintProviderTest.java ├── order-picking │ ├── README.adoc │ ├── order-picking-screenshot.png │ ├── pom.xml │ └── src │ │ ├── main │ │ ├── java │ │ │ └── org │ │ │ │ └── acme │ │ │ │ └── orderpicking │ │ │ │ ├── bootstrap │ │ │ │ └── DemoDataGenerator.java │ │ │ │ ├── domain │ │ │ │ ├── Order.java │ │ │ │ ├── OrderItem.java │ │ │ │ ├── OrderPickingPlanning.java │ │ │ │ ├── OrderPickingSolution.java │ │ │ │ ├── Product.java │ │ │ │ ├── Shelving.java │ │ │ │ ├── Trolley.java │ │ │ │ ├── TrolleyOrTrolleyStep.java │ │ │ │ ├── TrolleyStep.java │ │ │ │ ├── Warehouse.java │ │ │ │ └── WarehouseLocation.java │ │ │ │ ├── persistence │ │ │ │ └── OrderPickingRepository.java │ │ │ │ ├── rest │ │ │ │ └── OrderPickingSolverResource.java │ │ │ │ └── solver │ │ │ │ └── OrderPickingConstraintProvider.java │ │ └── resources │ │ │ ├── META-INF │ │ │ └── resources │ │ │ │ ├── app.js │ │ │ │ ├── index.html │ │ │ │ ├── style.css │ │ │ │ └── warehouse-api.js │ │ │ └── application.properties │ │ └── test │ │ └── java │ │ └── org │ │ └── acme │ │ └── orderpicking │ │ ├── domain │ │ └── WarehouseTest.java │ │ ├── rest │ │ ├── OrderPickingEnvironmentTest.java │ │ └── OrderPickingResourceIT.java │ │ └── solver │ │ └── OrderPickingConstraintProviderTest.java ├── project-job-scheduling │ ├── README.adoc │ ├── pom.xml │ ├── project-job-scheduling-screenshot.png │ └── src │ │ ├── main │ │ ├── java │ │ │ └── org │ │ │ │ └── acme │ │ │ │ └── projectjobschedule │ │ │ │ ├── domain │ │ │ │ ├── Allocation.java │ │ │ │ ├── ExecutionMode.java │ │ │ │ ├── Job.java │ │ │ │ ├── JobType.java │ │ │ │ ├── Project.java │ │ │ │ ├── ProjectJobSchedule.java │ │ │ │ ├── ResourceRequirement.java │ │ │ │ ├── resource │ │ │ │ │ ├── GlobalResource.java │ │ │ │ │ ├── LocalResource.java │ │ │ │ │ └── Resource.java │ │ │ │ └── solver │ │ │ │ │ ├── DelayStrengthComparator.java │ │ │ │ │ └── PredecessorsDoneDateUpdatingVariableListener.java │ │ │ │ ├── rest │ │ │ │ ├── DemoDataGenerator.java │ │ │ │ ├── ProjectJobSchedulingDemoResource.java │ │ │ │ ├── ProjectJobSchedulingResource.java │ │ │ │ └── exception │ │ │ │ │ ├── ErrorInfo.java │ │ │ │ │ ├── ScheduleSolverException.java │ │ │ │ │ └── ScheduleSolverExceptionMapper.java │ │ │ │ └── solver │ │ │ │ └── ProjectJobSchedulingConstraintProvider.java │ │ └── resources │ │ │ ├── META-INF │ │ │ └── resources │ │ │ │ ├── app.js │ │ │ │ └── index.html │ │ │ └── application.properties │ │ └── test │ │ └── java │ │ └── org │ │ └── acme │ │ └── projectjobschedule │ │ ├── rest │ │ ├── ProjectJobSchedulingEnvironmentTest.java │ │ ├── ProjectJobSchedulingResourceIT.java │ │ └── ProjectJobSchedulingResourceTest.java │ │ └── solver │ │ └── ProjectJobSchedulingConstraintProviderTest.java ├── school-timetabling │ ├── README.adoc │ ├── build.gradle │ ├── pom.xml │ ├── school-timetabling-screenshot.png │ └── src │ │ ├── main │ │ ├── java │ │ │ └── org │ │ │ │ └── acme │ │ │ │ └── schooltimetabling │ │ │ │ ├── domain │ │ │ │ ├── Lesson.java │ │ │ │ ├── Room.java │ │ │ │ ├── Timeslot.java │ │ │ │ └── Timetable.java │ │ │ │ ├── rest │ │ │ │ ├── TimetableDemoResource.java │ │ │ │ ├── TimetableResource.java │ │ │ │ └── exception │ │ │ │ │ ├── ErrorInfo.java │ │ │ │ │ ├── TimetableSolverException.java │ │ │ │ │ └── TimetableSolverExceptionMapper.java │ │ │ │ └── solver │ │ │ │ └── TimetableConstraintProvider.java │ │ └── resources │ │ │ ├── META-INF │ │ │ └── resources │ │ │ │ ├── app.js │ │ │ │ └── index.html │ │ │ └── application.properties │ │ └── test │ │ └── java │ │ └── org │ │ └── acme │ │ └── schooltimetabling │ │ ├── rest │ │ ├── TimetableEnvironmentTest.java │ │ ├── TimetableResourceIT.java │ │ └── TimetableResourceTest.java │ │ └── solver │ │ └── TimetableConstraintProviderTest.java ├── sports-league-scheduling │ ├── README.adoc │ ├── pom.xml │ ├── sports-league-scheduling-screenshot.png │ └── src │ │ ├── main │ │ ├── java │ │ │ └── org │ │ │ │ └── acme │ │ │ │ └── sportsleagueschedule │ │ │ │ ├── domain │ │ │ │ ├── LeagueSchedule.java │ │ │ │ ├── Match.java │ │ │ │ ├── Round.java │ │ │ │ └── Team.java │ │ │ │ ├── rest │ │ │ │ ├── DemoDataGenerator.java │ │ │ │ ├── SportsLeagueSchedulingDemoResource.java │ │ │ │ ├── SportsLeagueSchedulingResource.java │ │ │ │ └── exception │ │ │ │ │ ├── ErrorInfo.java │ │ │ │ │ ├── ScheduleSolverException.java │ │ │ │ │ └── ScheduleSolverExceptionMapper.java │ │ │ │ └── solver │ │ │ │ └── SportsLeagueSchedulingConstraintProvider.java │ │ └── resources │ │ │ ├── META-INF │ │ │ └── resources │ │ │ │ ├── app.js │ │ │ │ └── index.html │ │ │ └── application.properties │ │ └── test │ │ └── java │ │ └── org │ │ └── acme │ │ └── sportsleagueschedule │ │ ├── rest │ │ ├── SportsLeagheSchedulingEnvironmentTest.java │ │ ├── SportsLeagueSchedulingResourceIT.java │ │ └── SportsLeagueSchedulingResourceTest.java │ │ └── solver │ │ └── SportsLeagueSchedulingConstraintProviderTest.java ├── spring-boot-integration │ ├── README.adoc │ ├── build.gradle │ ├── pom.xml │ ├── school-timetabling-screenshot.png │ └── src │ │ ├── main │ │ ├── java │ │ │ └── org │ │ │ │ └── acme │ │ │ │ └── schooltimetabling │ │ │ │ ├── TimetableSpringBootApp.java │ │ │ │ ├── domain │ │ │ │ ├── Lesson.java │ │ │ │ ├── Room.java │ │ │ │ ├── Timeslot.java │ │ │ │ └── Timetable.java │ │ │ │ ├── rest │ │ │ │ ├── TimetableController.java │ │ │ │ ├── TimetableDemoController.java │ │ │ │ └── exception │ │ │ │ │ ├── ErrorInfo.java │ │ │ │ │ ├── TimetableSolverException.java │ │ │ │ │ └── TimetableSolverExceptionHandler.java │ │ │ │ └── solver │ │ │ │ ├── TimetableConstraintProvider.java │ │ │ │ └── justifications │ │ │ │ ├── RoomConflictJustification.java │ │ │ │ ├── StudentGroupConflictJustification.java │ │ │ │ ├── StudentGroupSubjectVarietyJustification.java │ │ │ │ ├── TeacherConflictJustification.java │ │ │ │ ├── TeacherRoomStabilityJustification.java │ │ │ │ └── TeacherTimeEfficiencyJustification.java │ │ └── resources │ │ │ ├── application-enterprise.properties │ │ │ ├── application.properties │ │ │ └── static │ │ │ ├── app.js │ │ │ └── index.html │ │ └── test │ │ └── java │ │ └── org │ │ └── acme │ │ └── schooltimetabling │ │ ├── config │ │ └── ConstraintConfig.java │ │ ├── rest │ │ ├── TimetableControllerTest.java │ │ └── TimetableEnvironmentTest.java │ │ └── solver │ │ └── TimetableConstraintProviderTest.java ├── task-assigning │ ├── README.adoc │ ├── pom.xml │ ├── src │ │ ├── main │ │ │ ├── java │ │ │ │ └── org │ │ │ │ │ └── acme │ │ │ │ │ └── taskassigning │ │ │ │ │ ├── domain │ │ │ │ │ ├── Affinity.java │ │ │ │ │ ├── Customer.java │ │ │ │ │ ├── Employee.java │ │ │ │ │ ├── Priority.java │ │ │ │ │ ├── Task.java │ │ │ │ │ ├── TaskAssigningSolution.java │ │ │ │ │ └── TaskType.java │ │ │ │ │ ├── rest │ │ │ │ │ ├── DemoDataGenerator.java │ │ │ │ │ ├── TaskAssigningDemoResource.java │ │ │ │ │ ├── TaskAssigningResource.java │ │ │ │ │ └── exception │ │ │ │ │ │ ├── ErrorInfo.java │ │ │ │ │ │ ├── ScheduleSolverException.java │ │ │ │ │ │ └── ScheduleSolverExceptionMapper.java │ │ │ │ │ └── solver │ │ │ │ │ └── TaskAssigningConstraintProvider.java │ │ │ └── resources │ │ │ │ ├── META-INF │ │ │ │ └── resources │ │ │ │ │ ├── app.js │ │ │ │ │ └── index.html │ │ │ │ └── application.properties │ │ └── test │ │ │ └── java │ │ │ └── org │ │ │ └── acme │ │ │ └── taskassigning │ │ │ ├── rest │ │ │ ├── TaskAssigningEnvironmentTest.java │ │ │ ├── TaskAssigningResourceTest.java │ │ │ └── TaskAssigningSchedulingResourceIT.java │ │ │ └── solver │ │ │ └── TaskAssigningConstraintProviderTest.java │ └── task-assigning-screenshot.png ├── tournament-scheduling │ ├── README.adoc │ ├── pom.xml │ ├── src │ │ ├── main │ │ │ ├── java │ │ │ │ └── org │ │ │ │ │ └── acme │ │ │ │ │ └── tournamentschedule │ │ │ │ │ ├── domain │ │ │ │ │ ├── Day.java │ │ │ │ │ ├── Team.java │ │ │ │ │ ├── TeamAssignment.java │ │ │ │ │ ├── TournamentSchedule.java │ │ │ │ │ └── UnavailabilityPenalty.java │ │ │ │ │ ├── rest │ │ │ │ │ ├── DemoDataGenerator.java │ │ │ │ │ ├── TournamentSchedulingDemoResource.java │ │ │ │ │ ├── TournamentSchedulingResource.java │ │ │ │ │ └── exception │ │ │ │ │ │ ├── ErrorInfo.java │ │ │ │ │ │ ├── ScheduleSolverException.java │ │ │ │ │ │ └── ScheduleSolverExceptionMapper.java │ │ │ │ │ └── solver │ │ │ │ │ └── TournamentScheduleConstraintProvider.java │ │ │ └── resources │ │ │ │ ├── META-INF │ │ │ │ └── resources │ │ │ │ │ ├── app.js │ │ │ │ │ └── index.html │ │ │ │ └── application.properties │ │ └── test │ │ │ └── java │ │ │ └── org │ │ │ └── acme │ │ │ └── tournamentschedule │ │ │ ├── rest │ │ │ ├── TournamentSchedulingEnvironmentTest.java │ │ │ ├── TournamentSchedulingResourceIT.java │ │ │ └── TournamentSchedulingResourceTest.java │ │ │ └── solver │ │ │ └── TournamentScheduleConstraintProviderTest.java │ └── tournament-scheduling-screenshot.png └── vehicle-routing │ ├── README.MD │ ├── pom.xml │ ├── src │ ├── main │ │ ├── java │ │ │ └── org │ │ │ │ └── acme │ │ │ │ └── vehiclerouting │ │ │ │ ├── domain │ │ │ │ ├── Location.java │ │ │ │ ├── LocationAware.java │ │ │ │ ├── LocationDistanceMeter.java │ │ │ │ ├── Vehicle.java │ │ │ │ ├── VehicleRoutePlan.java │ │ │ │ ├── Visit.java │ │ │ │ ├── dto │ │ │ │ │ ├── ApplyRecommendationRequest.java │ │ │ │ │ ├── RecommendationRequest.java │ │ │ │ │ └── VehicleRecommendation.java │ │ │ │ └── geo │ │ │ │ │ ├── DrivingTimeCalculator.java │ │ │ │ │ └── HaversineDrivingTimeCalculator.java │ │ │ │ ├── rest │ │ │ │ ├── VehicleRouteDemoResource.java │ │ │ │ ├── VehicleRoutePlanResource.java │ │ │ │ └── exception │ │ │ │ │ ├── ErrorInfo.java │ │ │ │ │ ├── VehicleRoutingSolverException.java │ │ │ │ │ └── VehicleRoutingSolverExceptionMapper.java │ │ │ │ └── solver │ │ │ │ └── VehicleRoutingConstraintProvider.java │ │ └── resources │ │ │ ├── META-INF │ │ │ └── resources │ │ │ │ ├── app.js │ │ │ │ ├── index.html │ │ │ │ ├── recommended-fit.js │ │ │ │ └── score-analysis.js │ │ │ └── application.properties │ └── test │ │ ├── java │ │ └── org │ │ │ └── acme │ │ │ └── vehiclerouting │ │ │ ├── domain │ │ │ ├── geo │ │ │ │ └── HaversineDrivingTimeCalculatorTest.java │ │ │ └── jackson │ │ │ │ ├── VRPScoreAnalysisJacksonDeserializer.java │ │ │ │ └── VRPScoreAnalysisJacksonModule.java │ │ │ ├── rest │ │ │ ├── VehicleRoutingEnvironmentTest.java │ │ │ ├── VehicleRoutingPlanResourceTest.java │ │ │ └── VehicleRoutingResourceIT.java │ │ │ └── solver │ │ │ └── VehicleRoutingConstraintProviderTest.java │ │ └── resources │ │ └── META-INF │ │ └── services │ │ └── com.fasterxml.jackson.databind.Module │ └── vehicle-routing-screenshot.png ├── kotlin └── school-timetabling │ ├── README.adoc │ ├── pom.xml │ ├── school-timetabling-screenshot.png │ └── src │ ├── main │ ├── kotlin │ │ └── org │ │ │ └── acme │ │ │ └── kotlin │ │ │ └── schooltimetabling │ │ │ ├── domain │ │ │ ├── Lesson.kt │ │ │ ├── Room.kt │ │ │ ├── Timeslot.kt │ │ │ └── Timetable.kt │ │ │ ├── rest │ │ │ ├── TimetableDemoResource.kt │ │ │ ├── TimetableResource.kt │ │ │ └── exception │ │ │ │ ├── ErrorInfo.kt │ │ │ │ ├── TimetableSolverException.kt │ │ │ │ └── TimetableSolverExceptionMapper.kt │ │ │ └── solver │ │ │ ├── TimeTableConstraintProvider.kt │ │ │ └── justifications │ │ │ ├── RoomConflictJustification.kt │ │ │ ├── StudentGroupConflictJustification.kt │ │ │ ├── StudentGroupSubjectVarietyJustification.kt │ │ │ ├── TeacherConflictJustification.kt │ │ │ ├── TeacherRoomStabilityJustification.kt │ │ │ └── TeacherTimeEfficiencyJustification.kt │ └── resources │ │ ├── META-INF │ │ └── resources │ │ │ ├── app.js │ │ │ └── index.html │ │ └── application.properties │ └── test │ └── kotlin │ └── org │ └── acme │ └── kotlin │ └── schooltimetabling │ ├── rest │ ├── TimetableEnvironmentTest.kt │ ├── TimetableResourceIT.kt │ └── TimetableResourceTest.kt │ └── solver │ └── TimetableConstraintProviderTest.kt ├── pom.xml ├── python ├── employee-scheduling │ ├── README.MD │ ├── employee-scheduling-screenshot.png │ ├── logging.conf │ ├── pyproject.toml │ ├── src │ │ └── employee_scheduling │ │ │ ├── __init__.py │ │ │ ├── constraints.py │ │ │ ├── demo_data.py │ │ │ ├── domain.py │ │ │ ├── json_serialization.py │ │ │ ├── rest_api.py │ │ │ └── solver.py │ ├── static │ │ ├── app.js │ │ ├── index.html │ │ └── webjars │ │ │ └── timefold │ │ │ ├── css │ │ │ └── timefold-webui.css │ │ │ ├── img │ │ │ ├── timefold-favicon.svg │ │ │ ├── timefold-logo-horizontal-negative.svg │ │ │ ├── timefold-logo-horizontal-positive.svg │ │ │ └── timefold-logo-stacked-positive.svg │ │ │ └── js │ │ │ └── timefold-webui.js │ └── tests │ │ ├── test_constraints.py │ │ └── test_feasible.py ├── flight-crew-scheduling │ ├── README.adoc │ ├── flight-crew-scheduling-screenshot.png │ ├── logging.conf │ ├── pyproject.toml │ ├── src │ │ └── flight_crew_scheduling │ │ │ ├── __init__.py │ │ │ ├── constraints.py │ │ │ ├── demo_data.py │ │ │ ├── domain.py │ │ │ ├── json_serialization.py │ │ │ ├── rest_api.py │ │ │ ├── score_analysis.py │ │ │ └── solver.py │ ├── static │ │ ├── app.js │ │ ├── index.html │ │ └── webjars │ │ │ └── timefold │ │ │ ├── css │ │ │ └── timefold-webui.css │ │ │ ├── img │ │ │ ├── timefold-favicon.svg │ │ │ ├── timefold-logo-horizontal-negative.svg │ │ │ ├── timefold-logo-horizontal-positive.svg │ │ │ └── timefold-logo-stacked-positive.svg │ │ │ └── js │ │ │ └── timefold-webui.js │ └── tests │ │ ├── test_constraints.py │ │ └── test_feasible.py ├── hello-world │ ├── README.adoc │ ├── pyproject.toml │ ├── src │ │ └── hello_world │ │ │ ├── __init__.py │ │ │ ├── constraints.py │ │ │ ├── domain.py │ │ │ └── main.py │ └── tests │ │ ├── test_constraints.py │ │ └── test_feasible.py ├── school-timetabling │ ├── README.adoc │ ├── logging.conf │ ├── pyproject.toml │ ├── school-timetabling-screenshot.png │ ├── src │ │ └── school_timetabling │ │ │ ├── __init__.py │ │ │ ├── constraints.py │ │ │ ├── demo_data.py │ │ │ ├── domain.py │ │ │ ├── json_serialization.py │ │ │ ├── rest_api.py │ │ │ ├── score_analysis.py │ │ │ └── solver.py │ ├── static │ │ ├── app.js │ │ ├── index.html │ │ └── webjars │ │ │ └── timefold │ │ │ ├── css │ │ │ └── timefold-webui.css │ │ │ ├── img │ │ │ ├── timefold-favicon.svg │ │ │ ├── timefold-logo-horizontal-negative.svg │ │ │ ├── timefold-logo-horizontal-positive.svg │ │ │ └── timefold-logo-stacked-positive.svg │ │ │ └── js │ │ │ └── timefold-webui.js │ └── tests │ │ ├── test_constraints.py │ │ └── test_feasible.py ├── sports-league-scheduling │ ├── README.adoc │ ├── logging.conf │ ├── pyproject.toml │ ├── sports-league-scheduling-screenshot.png │ ├── src │ │ └── sports_league_scheduling │ │ │ ├── __init__.py │ │ │ ├── constraints.py │ │ │ ├── demo_data.py │ │ │ ├── domain.py │ │ │ ├── json_serialization.py │ │ │ ├── rest_api.py │ │ │ ├── score_analysis.py │ │ │ └── solver.py │ ├── static │ │ ├── app.js │ │ ├── index.html │ │ └── webjars │ │ │ └── timefold │ │ │ ├── css │ │ │ └── timefold-webui.css │ │ │ ├── img │ │ │ ├── timefold-favicon.svg │ │ │ ├── timefold-logo-horizontal-negative.svg │ │ │ ├── timefold-logo-horizontal-positive.svg │ │ │ └── timefold-logo-stacked-positive.svg │ │ │ └── js │ │ │ └── timefold-webui.js │ └── tests │ │ ├── test_constraints.py │ │ └── test_feasible.py ├── tournament-scheduling │ ├── README.adoc │ ├── logging.conf │ ├── pyproject.toml │ ├── src │ │ └── tournament_scheduling │ │ │ ├── __init__.py │ │ │ ├── constraints.py │ │ │ ├── demo_data.py │ │ │ ├── domain.py │ │ │ ├── json_serialization.py │ │ │ ├── rest_api.py │ │ │ ├── score_analysis.py │ │ │ └── solver.py │ ├── static │ │ ├── app.js │ │ ├── index.html │ │ └── webjars │ │ │ └── timefold │ │ │ ├── css │ │ │ └── timefold-webui.css │ │ │ ├── img │ │ │ ├── timefold-favicon.svg │ │ │ ├── timefold-logo-horizontal-negative.svg │ │ │ ├── timefold-logo-horizontal-positive.svg │ │ │ └── timefold-logo-stacked-positive.svg │ │ │ └── js │ │ │ └── timefold-webui.js │ ├── tests │ │ ├── test_constraints.py │ │ └── test_feasible.py │ └── tournament-scheduling-screenshot.png └── vehicle-routing │ ├── README.MD │ ├── logging.conf │ ├── pyproject.toml │ ├── src │ └── vehicle_routing │ │ ├── __init__.py │ │ ├── constraints.py │ │ ├── demo_data.py │ │ ├── domain.py │ │ ├── json_serialization.py │ │ ├── rest_api.py │ │ ├── score_analysis.py │ │ └── solver.py │ ├── static │ ├── app.js │ ├── index.html │ ├── score-analysis.js │ └── webjars │ │ └── timefold │ │ ├── css │ │ └── timefold-webui.css │ │ ├── img │ │ ├── timefold-favicon.svg │ │ ├── timefold-logo-horizontal-negative.svg │ │ ├── timefold-logo-horizontal-positive.svg │ │ └── timefold-logo-stacked-positive.svg │ │ └── js │ │ └── timefold-webui.js │ ├── tests │ ├── test_constraints.py │ └── test_feasible.py │ └── vehicle-routing-screenshot.png ├── settings.gradle └── timefold-logo.png /.gitattributes: -------------------------------------------------------------------------------- 1 | # Default to linux endings 2 | * text eol=lf 3 | 4 | # OS specific files 5 | ################### 6 | 7 | *.sh eol=lf 8 | *.bat eol=crlf 9 | 10 | # Binary files 11 | ############## 12 | 13 | # Image files 14 | *.png binary 15 | *.jpg binary 16 | *.gif binary 17 | *.bmp binary 18 | *.ico binary 19 | 20 | # Audio files 21 | *.wav binary 22 | *.mp3 binary 23 | *.ogg binary 24 | 25 | # Video files 26 | *.mp4 binary 27 | *.mov binary 28 | *.wmv binary 29 | *.avi binary 30 | *.flv binary 31 | *.mkv binary 32 | *.webm binary 33 | 34 | # Other binary files 35 | *.pdf binary 36 | *.xls binary 37 | *.xlsx binary 38 | *.odp binary 39 | *.zip binary 40 | *.jar binary 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: 'Bug: ' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | Describe the issue you are experiencing here. 12 | Tell us what you were trying to do and what happened. 13 | 14 | **Expected behavior** 15 | Describe clearly and concisely what you expected to happen. 16 | 17 | **Actual behavior** 18 | Describe clearly and concisely what actually happened. 19 | 20 | **To Reproduce** 21 | Link to a small reproducer or attach an archive containing the reproducer to the issue. 22 | Alternatively, provide clear and concise steps to reproduce the behavior. 23 | 24 | ## Environment 25 | 26 | **Timefold Solver Version or Git ref**: 27 | 28 | **Output of `java -version`:** 29 | 30 | **Output of `uname -a` or `ver`:** 31 | 32 | ## Additional information 33 | 34 | Provide any and all other information which might be relevant to the issue. 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Feature request or enhancement 4 | url: https://github.com/TimefoldAI/timefold-solver/discussions/new?category=ideas 5 | about: Propose a new Timefold Quickstart or an enhancement to an existing one. 6 | - name: Question 7 | url: https://stackoverflow.com/questions/ask?tags=timefold 8 | about: Ask a question about how to use Timefold Quickstarts or Timefold Solver. 9 | - name: Discussions 10 | url: https://github.com/TimefoldAI/timefold-solver/discussions/new?category=general 11 | about: Open Community discussions related to Timefold Quickstarts or Timefold Solver. 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: maven 4 | target-branch: "development" 5 | directory: "/" 6 | schedule: 7 | interval: weekly 8 | time: '05:00' # Otherwise it picks a random time. 9 | open-pull-requests-limit: 10 10 | commit-message: 11 | prefix: "deps: " 12 | - package-ecosystem: gradle 13 | target-branch: "development" 14 | directory: "/" 15 | schedule: 16 | interval: weekly 17 | time: '05:00' # Otherwise it picks a random time. 18 | open-pull-requests-limit: 10 19 | commit-message: 20 | prefix: "deps: " 21 | - package-ecosystem: "github-actions" 22 | target-branch: "development" 23 | directory: "/" 24 | schedule: 25 | interval: weekly 26 | time: '05:00' # Otherwise it picks a random time. 27 | commit-message: 28 | prefix: "deps: " 29 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description of the change 2 | 3 | > Description here 4 | 5 | ## Checklist 6 | 7 | ### Development 8 | 9 | - [ ] The changes have been covered with tests, if necessary. 10 | - [ ] You have a green build, with the exception of the flaky tests. 11 | - [ ] UI and JS files are fully tested, the user interface works for all modules affected by your changes (e.g., solve and analyze buttons). 12 | - [ ] The network calls work for all modules affected by your changes (e.g., solving a problem). 13 | - [ ] The console messages are validated for all modules affected by your changes. 14 | 15 | ### Code Review 16 | 17 | - [ ] This pull request includes an explanatory title and description. 18 | - [ ] The GitHub issue is linked. 19 | - [ ] At least one other engineer has approved the changes. 20 | - [ ] After PR is merged, inform the reporter. -------------------------------------------------------------------------------- /.github/scripts/change_versions.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Expects the following environment variables to be set: 4 | # $OLD_JAVA_VERSION (Example: "1.1.0") 5 | # $NEW_JAVA_VERSION (Example: "1.2.0") 6 | # $OLD_PYTHON_VERSION (Example: "1.1.0b0") 7 | # $NEW_PYTHON_VERSION (Example: "1.2.0b0") 8 | 9 | echo "Old Java version: $OLD_JAVA_VERSION" 10 | echo "Old Python version: $OLD_PYTHON_VERSION" 11 | echo "New Java version: $NEW_JAVA_VERSION" 12 | echo "New Python version: $NEW_PYTHON_VERSION" 13 | 14 | # Replaces the old version by the new version. 15 | find . -name pom.xml | xargs sed -i "s/>$OLD_JAVA_VERSION$NEW_JAVA_VERSION { 10 | 11 | @Override 12 | public Response toResponse(ScheduleSolverException exception) { 13 | return Response 14 | .status(exception.getStatus()) 15 | .type(MediaType.APPLICATION_JSON) 16 | .entity(new ErrorInfo(exception.getJobId(), exception.getMessage())) 17 | .build(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /java/conference-scheduling/conference-scheduling-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimefoldAI/timefold-quickstarts/f3e83432b4a142110e6aad7e26dc9fd37937ec25/java/conference-scheduling/conference-scheduling-screenshot.png -------------------------------------------------------------------------------- /java/conference-scheduling/src/main/java/org/acme/conferencescheduling/rest/ConferenceSchedulingDemoResource.java: -------------------------------------------------------------------------------- 1 | package org.acme.conferencescheduling.rest; 2 | 3 | import jakarta.inject.Inject; 4 | import jakarta.ws.rs.GET; 5 | import jakarta.ws.rs.Path; 6 | import jakarta.ws.rs.core.MediaType; 7 | import jakarta.ws.rs.core.Response; 8 | 9 | import org.acme.conferencescheduling.domain.ConferenceSchedule; 10 | import org.eclipse.microprofile.openapi.annotations.Operation; 11 | import org.eclipse.microprofile.openapi.annotations.media.Content; 12 | import org.eclipse.microprofile.openapi.annotations.media.Schema; 13 | import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; 14 | import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; 15 | import org.eclipse.microprofile.openapi.annotations.tags.Tag; 16 | 17 | @Tag(name = "Demo data", description = "Timefold-provided demo conference scheduling data.") 18 | @Path("demo-data") 19 | public class ConferenceSchedulingDemoResource { 20 | 21 | private final DemoDataGenerator dataGenerator; 22 | 23 | @Inject 24 | public ConferenceSchedulingDemoResource(DemoDataGenerator dataGenerator) { 25 | this.dataGenerator = dataGenerator; 26 | } 27 | 28 | @APIResponses(value = { 29 | @APIResponse(responseCode = "200", description = "Unsolved demo schedule.", 30 | content = @Content(mediaType = MediaType.APPLICATION_JSON, 31 | schema = @Schema(implementation = ConferenceSchedule.class))) }) 32 | @Operation(summary = "Find an unsolved demo schedule by ID.") 33 | @GET 34 | public Response generate() { 35 | return Response.ok(dataGenerator.generateDemoData()).build(); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /java/conference-scheduling/src/main/java/org/acme/conferencescheduling/rest/exception/ConferenceScheduleSolverException.java: -------------------------------------------------------------------------------- 1 | package org.acme.conferencescheduling.rest.exception; 2 | 3 | import jakarta.ws.rs.core.Response; 4 | 5 | public class ConferenceScheduleSolverException extends RuntimeException { 6 | 7 | private final String jobId; 8 | 9 | private final Response.Status status; 10 | 11 | public ConferenceScheduleSolverException(String jobId, Response.Status status, String message) { 12 | super(message); 13 | this.jobId = jobId; 14 | this.status = status; 15 | } 16 | 17 | public ConferenceScheduleSolverException(String jobId, Throwable cause) { 18 | super(cause.getMessage(), cause); 19 | this.jobId = jobId; 20 | this.status = Response.Status.INTERNAL_SERVER_ERROR; 21 | } 22 | 23 | public String getJobId() { 24 | return jobId; 25 | } 26 | 27 | public Response.Status getStatus() { 28 | return status; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /java/conference-scheduling/src/main/java/org/acme/conferencescheduling/rest/exception/ConferenceScheduleSolverExceptionMapper.java: -------------------------------------------------------------------------------- 1 | package org.acme.conferencescheduling.rest.exception; 2 | 3 | import jakarta.ws.rs.core.MediaType; 4 | import jakarta.ws.rs.core.Response; 5 | import jakarta.ws.rs.ext.ExceptionMapper; 6 | import jakarta.ws.rs.ext.Provider; 7 | 8 | @Provider 9 | public class ConferenceScheduleSolverExceptionMapper implements ExceptionMapper { 10 | 11 | @Override 12 | public Response toResponse(ConferenceScheduleSolverException exception) { 13 | return Response 14 | .status(exception.getStatus()) 15 | .type(MediaType.APPLICATION_JSON) 16 | .entity(new ErrorInfo(exception.getJobId(), exception.getMessage())) 17 | .build(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /java/conference-scheduling/src/main/java/org/acme/conferencescheduling/rest/exception/ErrorInfo.java: -------------------------------------------------------------------------------- 1 | package org.acme.conferencescheduling.rest.exception; 2 | 3 | public record ErrorInfo(String jobId, String message) { 4 | } 5 | -------------------------------------------------------------------------------- /java/conference-scheduling/src/main/java/org/acme/conferencescheduling/solver/justifications/ConferenceSchedulingJustification.java: -------------------------------------------------------------------------------- 1 | package org.acme.conferencescheduling.solver.justifications; 2 | 3 | import ai.timefold.solver.core.api.score.stream.ConstraintJustification; 4 | 5 | public record ConferenceSchedulingJustification(String description) implements ConstraintJustification { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /java/conference-scheduling/src/main/java/org/acme/conferencescheduling/solver/justifications/DiversityTalkJustification.java: -------------------------------------------------------------------------------- 1 | package org.acme.conferencescheduling.solver.justifications; 2 | 3 | import static java.util.stream.Collectors.joining; 4 | 5 | import java.util.Collection; 6 | 7 | import ai.timefold.solver.core.api.score.stream.ConstraintJustification; 8 | 9 | import org.acme.conferencescheduling.domain.Talk; 10 | 11 | public record DiversityTalkJustification(String description) implements ConstraintJustification { 12 | 13 | public DiversityTalkJustification(String type, Talk talk, Collection values, Talk talk2, 14 | Collection values2) { 15 | this("Talks [%s, %s] match %s [%s] at same time.".formatted(talk.getCode(), talk2.getCode(), type, 16 | values.stream().filter(values2::contains).collect(joining(", ")))); 17 | } 18 | 19 | public DiversityTalkJustification(String type, Talk talk, String value, Talk talk2, 20 | String value2) { 21 | this("Talks [%s, %s] have different %s [%s, %s].".formatted(talk.getCode(), talk2.getCode(), type, value, value2)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /java/conference-scheduling/src/main/java/org/acme/conferencescheduling/solver/justifications/PreferredTagsJustification.java: -------------------------------------------------------------------------------- 1 | package org.acme.conferencescheduling.solver.justifications; 2 | 3 | import static java.util.stream.Collectors.joining; 4 | 5 | import java.util.Collection; 6 | 7 | import ai.timefold.solver.core.api.score.stream.ConstraintJustification; 8 | 9 | import org.acme.conferencescheduling.domain.Speaker; 10 | import org.acme.conferencescheduling.domain.Talk; 11 | 12 | public record PreferredTagsJustification(String description) implements ConstraintJustification { 13 | 14 | public PreferredTagsJustification(String type, Talk talk, Collection expectedTags, Collection actualTags) { 15 | this("Missing preferred %s tags [%s] for talk %s." 16 | .formatted(type, 17 | expectedTags.stream().filter(t -> !actualTags.contains(t)).collect(joining(", ")), 18 | talk.getCode())); 19 | } 20 | 21 | public PreferredTagsJustification(String type, Collection speakers, Collection expectedTags, 22 | Collection actualTags) { 23 | this("Missing preferred %s tags [%s] for speakers [%s].".formatted(type, 24 | expectedTags.stream().filter(t -> !actualTags.contains(t)).collect(joining(", ")), 25 | speakers.stream().map(Speaker::getName).collect(joining(", ")))); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /java/conference-scheduling/src/main/java/org/acme/conferencescheduling/solver/justifications/ProhibitedTagsJustification.java: -------------------------------------------------------------------------------- 1 | package org.acme.conferencescheduling.solver.justifications; 2 | 3 | import static java.util.stream.Collectors.joining; 4 | 5 | import java.util.Collection; 6 | 7 | import ai.timefold.solver.core.api.score.stream.ConstraintJustification; 8 | 9 | import org.acme.conferencescheduling.domain.Speaker; 10 | import org.acme.conferencescheduling.domain.Talk; 11 | 12 | public record ProhibitedTagsJustification(String description) implements ConstraintJustification { 13 | 14 | public ProhibitedTagsJustification(String type, Talk talk, Collection prohibitedTags, 15 | Collection actualTags) { 16 | this("Talk %s has prohibited %s tags [%s]".formatted(talk.getCode(), type, 17 | prohibitedTags.stream().filter(actualTags::contains).collect(joining(", ")))); 18 | } 19 | 20 | public ProhibitedTagsJustification(String type, Collection speakers, Collection prohibitedTags, 21 | Collection actualTags) { 22 | this("Speakers [%s] have prohibited %s tags [%s]".formatted(speakers.stream().map(Speaker::getName).collect(joining(", ")), 23 | type, prohibitedTags.stream().filter(actualTags::contains).collect(joining(", ")))); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /java/conference-scheduling/src/main/java/org/acme/conferencescheduling/solver/justifications/RequiredTagsJustification.java: -------------------------------------------------------------------------------- 1 | package org.acme.conferencescheduling.solver.justifications; 2 | 3 | import static java.util.stream.Collectors.joining; 4 | 5 | import java.util.Collection; 6 | 7 | import ai.timefold.solver.core.api.score.stream.ConstraintJustification; 8 | 9 | import org.acme.conferencescheduling.domain.Speaker; 10 | import org.acme.conferencescheduling.domain.Talk; 11 | 12 | public record RequiredTagsJustification(String description) implements ConstraintJustification { 13 | 14 | public RequiredTagsJustification(String type, Talk talk, Collection expectedTags, Collection actualTags) { 15 | this("Missing required %s tags [%s] for talk %s." 16 | .formatted(type, 17 | expectedTags.stream().filter(t -> !actualTags.contains(t)).collect(joining(", ")), 18 | talk.getCode())); 19 | } 20 | 21 | public RequiredTagsJustification(String type, Collection speakers, Collection expectedTags, 22 | Collection actualTags) { 23 | this("Missing required %s tags [%s] for speakers [%s].".formatted(type, 24 | expectedTags.stream().filter(t -> !actualTags.contains(t)).collect(joining(", ")), 25 | speakers.stream().map(Speaker::getName).collect(joining(", ")))); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /java/conference-scheduling/src/main/java/org/acme/conferencescheduling/solver/justifications/UnavailableTimeslotJustification.java: -------------------------------------------------------------------------------- 1 | package org.acme.conferencescheduling.solver.justifications; 2 | 3 | import static java.util.stream.Collectors.joining; 4 | 5 | import ai.timefold.solver.core.api.score.stream.ConstraintJustification; 6 | 7 | import org.acme.conferencescheduling.domain.Speaker; 8 | import org.acme.conferencescheduling.domain.Talk; 9 | import org.acme.conferencescheduling.domain.Timeslot; 10 | 11 | public record UnavailableTimeslotJustification(String description) implements ConstraintJustification { 12 | 13 | public UnavailableTimeslotJustification(Talk talk) { 14 | this("The timeslot %s of Talk %s has been marked as unavailable for room %s [%s].".formatted(talk.getTimeslot().getId(), 15 | talk.getCode(), talk.getRoom().getId(), 16 | talk.getRoom().getUnavailableTimeslots().stream() 17 | .map(Timeslot::getId) 18 | .collect(joining(", ")))); 19 | } 20 | 21 | public UnavailableTimeslotJustification(Talk talk, Speaker speaker) { 22 | this("The timeslot %s of Talk %s has been marked as unavailable for speaker %s [%s].".formatted( 23 | talk.getTimeslot().getId(), talk.getCode(), speaker.getId(), 24 | speaker.getUnavailableTimeslots().stream() 25 | .map(Timeslot::getId) 26 | .collect(joining(", ")))); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /java/conference-scheduling/src/main/java/org/acme/conferencescheduling/solver/justifications/UndesiredTagsJustification.java: -------------------------------------------------------------------------------- 1 | package org.acme.conferencescheduling.solver.justifications; 2 | 3 | import static java.util.stream.Collectors.joining; 4 | 5 | import java.util.Collection; 6 | 7 | import ai.timefold.solver.core.api.score.stream.ConstraintJustification; 8 | 9 | import org.acme.conferencescheduling.domain.Speaker; 10 | import org.acme.conferencescheduling.domain.Talk; 11 | 12 | public record UndesiredTagsJustification(String description) implements ConstraintJustification { 13 | 14 | public UndesiredTagsJustification(String type, Talk talk, Collection undesiredTags, 15 | Collection actualTags) { 16 | this("Talk %s has undesired %s tags [%s]".formatted(talk.getCode(), type, 17 | undesiredTags.stream().filter(actualTags::contains).collect(joining(", ")))); 18 | } 19 | 20 | public UndesiredTagsJustification(String type, Collection speakers, Collection undesiredTags, 21 | Collection actualTags) { 22 | this("Speakers [%s] have undesired %s tags [%s]".formatted(speakers.stream().map(Speaker::getName).collect(joining(", ")), 23 | type, undesiredTags.stream().filter(actualTags::contains).collect(joining(", ")))); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /java/employee-scheduling/employee-scheduling-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimefoldAI/timefold-quickstarts/f3e83432b4a142110e6aad7e26dc9fd37937ec25/java/employee-scheduling/employee-scheduling-screenshot.png -------------------------------------------------------------------------------- /java/employee-scheduling/src/main/java/org/acme/employeescheduling/rest/exception/EmployeeScheduleSolverException.java: -------------------------------------------------------------------------------- 1 | package org.acme.employeescheduling.rest.exception; 2 | 3 | import jakarta.ws.rs.core.Response; 4 | 5 | public class EmployeeScheduleSolverException extends RuntimeException { 6 | 7 | private final String jobId; 8 | 9 | private final Response.Status status; 10 | 11 | public EmployeeScheduleSolverException(String jobId, Response.Status status, String message) { 12 | super(message); 13 | this.jobId = jobId; 14 | this.status = status; 15 | } 16 | 17 | public EmployeeScheduleSolverException(String jobId, Throwable cause) { 18 | super(cause.getMessage(), cause); 19 | this.jobId = jobId; 20 | this.status = Response.Status.INTERNAL_SERVER_ERROR; 21 | } 22 | 23 | public String getJobId() { 24 | return jobId; 25 | } 26 | 27 | public Response.Status getStatus() { 28 | return status; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /java/employee-scheduling/src/main/java/org/acme/employeescheduling/rest/exception/EmployeeScheduleSolverExceptionMapper.java: -------------------------------------------------------------------------------- 1 | package org.acme.employeescheduling.rest.exception; 2 | 3 | import jakarta.ws.rs.core.MediaType; 4 | import jakarta.ws.rs.core.Response; 5 | import jakarta.ws.rs.ext.ExceptionMapper; 6 | import jakarta.ws.rs.ext.Provider; 7 | 8 | @Provider 9 | public class EmployeeScheduleSolverExceptionMapper implements ExceptionMapper { 10 | 11 | @Override 12 | public Response toResponse(EmployeeScheduleSolverException exception) { 13 | return Response 14 | .status(exception.getStatus()) 15 | .type(MediaType.APPLICATION_JSON) 16 | .entity(new ErrorInfo(exception.getJobId(), exception.getMessage())) 17 | .build(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /java/employee-scheduling/src/main/java/org/acme/employeescheduling/rest/exception/ErrorInfo.java: -------------------------------------------------------------------------------- 1 | package org.acme.employeescheduling.rest.exception; 2 | 3 | public record ErrorInfo(String jobId, String message) { 4 | } 5 | -------------------------------------------------------------------------------- /java/employee-scheduling/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | ######################## 2 | # Timefold Solver properties 3 | ######################## 4 | 5 | # The solver runs for 30 seconds. To run for 5 minutes use "5m" and for 2 hours use "2h". 6 | quarkus.timefold.solver.termination.spent-limit=30s 7 | 8 | # To change how many solvers to run in parallel 9 | # timefold.solver-manager.parallel-solver-count=4 10 | 11 | # Temporary comment this out to detect bugs in your code (lowers performance) 12 | # quarkus.timefold.solver.environment-mode=FULL_ASSERT 13 | 14 | # Temporary comment this out to return a feasible solution as soon as possible 15 | # quarkus.timefold.solver.termination.best-score-limit=0hard/*soft 16 | 17 | # To see what Timefold is doing, turn on DEBUG or TRACE logging. 18 | quarkus.log.category."ai.timefold.solver".level=INFO 19 | %test.quarkus.log.category."ai.timefold.solver".level=INFO 20 | %prod.quarkus.log.category."ai.timefold.solver".level=INFO 21 | 22 | # XML file for power tweaking, defaults to solverConfig.xml (directly under src/main/resources) 23 | # quarkus.timefold.solver-config-xml=org/.../maintenanceScheduleSolverConfig.xml 24 | 25 | ######################## 26 | # Timefold Solver Enterprise properties 27 | ######################## 28 | 29 | # To run increase CPU cores usage per solver 30 | %enterprise.quarkus.timefold.solver.move-thread-count=AUTO 31 | 32 | ######################## 33 | # Native build properties 34 | ######################## 35 | 36 | # Enable Swagger UI also in the native mode 37 | quarkus.swagger-ui.always-include=true 38 | 39 | ######################## 40 | # Test overrides 41 | ######################## 42 | 43 | %test.quarkus.timefold.solver.termination.spent-limit=10s 44 | -------------------------------------------------------------------------------- /java/facility-location/facility-location-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimefoldAI/timefold-quickstarts/f3e83432b4a142110e6aad7e26dc9fd37937ec25/java/facility-location/facility-location-screenshot.png -------------------------------------------------------------------------------- /java/facility-location/src/main/java/org/acme/facilitylocation/bootstrap/DemoDataGenerator.java: -------------------------------------------------------------------------------- 1 | package org.acme.facilitylocation.bootstrap; 2 | 3 | import jakarta.enterprise.context.ApplicationScoped; 4 | import jakarta.enterprise.event.Observes; 5 | 6 | import org.acme.facilitylocation.domain.FacilityLocationProblem; 7 | import org.acme.facilitylocation.domain.Location; 8 | import org.acme.facilitylocation.persistence.FacilityLocationProblemRepository; 9 | 10 | import io.quarkus.runtime.StartupEvent; 11 | 12 | @ApplicationScoped 13 | public class DemoDataGenerator { 14 | 15 | private final FacilityLocationProblemRepository repository; 16 | 17 | public DemoDataGenerator(FacilityLocationProblemRepository repository) { 18 | this.repository = repository; 19 | } 20 | 21 | public void generateDemoData(@Observes StartupEvent startupEvent) { 22 | FacilityLocationProblem problem = DemoDataBuilder.builder() 23 | .setCapacity(4500) 24 | .setDemand(900) 25 | .setFacilityCount(30) 26 | .setConsumerCount(60) 27 | .setSouthWestCorner(new Location(51.44, -0.16)) 28 | .setNorthEastCorner(new Location(51.56, -0.01)) 29 | .setAverageSetupCost(50_000) 30 | .setSetupCostStandardDeviation(10_000) 31 | .build(); 32 | repository.update(problem); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /java/facility-location/src/main/java/org/acme/facilitylocation/domain/Location.java: -------------------------------------------------------------------------------- 1 | package org.acme.facilitylocation.domain; 2 | 3 | import static java.lang.Math.ceil; 4 | import static java.lang.Math.sqrt; 5 | 6 | import com.fasterxml.jackson.annotation.JsonFormat; 7 | 8 | @JsonFormat(shape = JsonFormat.Shape.ARRAY) 9 | public class Location { 10 | 11 | // Approximate Metric Equivalents for Degrees. At the equator for longitude and for latitude anywhere, 12 | // the following approximations are valid: 1° = 111 km (or 60 nautical miles) 0.1° = 11.1 km. 13 | public static final double METERS_PER_DEGREE = 111_000; 14 | 15 | private final double latitude; 16 | private final double longitude; 17 | 18 | public Location(double latitude, double longitude) { 19 | this.latitude = latitude; 20 | this.longitude = longitude; 21 | } 22 | 23 | @Override 24 | public String toString() { 25 | return String.format("[%.4fN, %.4fE]", latitude, longitude); 26 | } 27 | 28 | public long getDistanceTo(Location other) { 29 | double latitudeDiff = other.latitude - this.latitude; 30 | double longitudeDiff = other.longitude - this.longitude; 31 | return (long) ceil(sqrt(latitudeDiff * latitudeDiff + longitudeDiff * longitudeDiff) * METERS_PER_DEGREE); 32 | } 33 | 34 | public double getLatitude() { 35 | return latitude; 36 | } 37 | 38 | public double getLongitude() { 39 | return longitude; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /java/facility-location/src/main/java/org/acme/facilitylocation/persistence/FacilityLocationProblemRepository.java: -------------------------------------------------------------------------------- 1 | package org.acme.facilitylocation.persistence; 2 | 3 | import java.util.Optional; 4 | 5 | import jakarta.enterprise.context.ApplicationScoped; 6 | 7 | import org.acme.facilitylocation.domain.FacilityLocationProblem; 8 | 9 | @ApplicationScoped 10 | public class FacilityLocationProblemRepository { 11 | 12 | private FacilityLocationProblem facilityLocationProblem; 13 | 14 | public Optional solution() { 15 | return Optional.ofNullable(facilityLocationProblem); 16 | } 17 | 18 | public void update(FacilityLocationProblem facilityLocationProblem) { 19 | this.facilityLocationProblem = facilityLocationProblem; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /java/facility-location/src/main/java/org/acme/facilitylocation/rest/Status.java: -------------------------------------------------------------------------------- 1 | package org.acme.facilitylocation.rest; 2 | 3 | import org.acme.facilitylocation.domain.FacilityLocationProblem; 4 | import ai.timefold.solver.core.api.solver.SolverStatus; 5 | 6 | class Status { 7 | public final FacilityLocationProblem solution; 8 | public final String scoreExplanation; 9 | public final boolean isSolving; 10 | 11 | Status(FacilityLocationProblem solution, String scoreExplanation, SolverStatus solverStatus) { 12 | this.solution = solution; 13 | this.scoreExplanation = scoreExplanation; 14 | this.isSolving = solverStatus != SolverStatus.NOT_SOLVING; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /java/facility-location/src/test/java/org/acme/facilitylocation/solver/FacilityLocationResourceIT.java: -------------------------------------------------------------------------------- 1 | package org.acme.facilitylocation.solver; 2 | 3 | import static io.restassured.RestAssured.get; 4 | import static io.restassured.RestAssured.post; 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | import static org.awaitility.Awaitility.await; 7 | 8 | import java.time.Duration; 9 | 10 | import org.junit.jupiter.api.Test; 11 | 12 | import io.quarkus.test.junit.QuarkusIntegrationTest; 13 | 14 | @QuarkusIntegrationTest 15 | class FacilityLocationResourceIT { 16 | 17 | @Test 18 | void solveNative() { 19 | post("/flp/solve") 20 | .then() 21 | .statusCode(204) 22 | .extract(); 23 | 24 | await() 25 | .atMost(Duration.ofMinutes(1)) 26 | .pollInterval(Duration.ofMillis(500L)) 27 | .until(() -> !get("/flp/status").jsonPath().getBoolean("isSolving")); 28 | 29 | String score = get("/flp/status").jsonPath().get("solution.score"); 30 | assertThat(score).isNotNull(); 31 | } 32 | } -------------------------------------------------------------------------------- /java/facility-location/src/test/java/org/acme/facilitylocation/solver/SolverManagerTest.java: -------------------------------------------------------------------------------- 1 | package org.acme.facilitylocation.solver; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import java.util.concurrent.ExecutionException; 6 | 7 | import jakarta.inject.Inject; 8 | 9 | import ai.timefold.solver.core.api.solver.SolverManager; 10 | 11 | import org.acme.facilitylocation.bootstrap.DemoDataBuilder; 12 | import org.acme.facilitylocation.domain.FacilityLocationProblem; 13 | import org.acme.facilitylocation.domain.Location; 14 | import org.junit.jupiter.api.Test; 15 | 16 | import io.quarkus.test.junit.QuarkusTest; 17 | 18 | @QuarkusTest 19 | class SolverManagerTest { 20 | 21 | @Inject 22 | SolverManager solverManager; 23 | 24 | @Test 25 | void solve() throws ExecutionException, InterruptedException { 26 | FacilityLocationProblem problem = DemoDataBuilder.builder() 27 | .setCapacity(1200) 28 | .setDemand(900) 29 | .setAverageSetupCost(1000).setSetupCostStandardDeviation(200) 30 | .setFacilityCount(10) 31 | .setConsumerCount(150) 32 | .setSouthWestCorner(new Location(-10, -10)) 33 | .setNorthEastCorner(new Location(10, 10)) 34 | .build(); 35 | 36 | FacilityLocationProblem solution = solverManager.solveBuilder() 37 | .withProblemId(0L) 38 | .withProblemFinder(id -> problem) 39 | .run() 40 | .getFinalBestSolution(); 41 | assertThat(solution.getScore().isFeasible()).isTrue(); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /java/flight-crew-scheduling/flight-crew-scheduling-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimefoldAI/timefold-quickstarts/f3e83432b4a142110e6aad7e26dc9fd37937ec25/java/flight-crew-scheduling/flight-crew-scheduling-screenshot.png -------------------------------------------------------------------------------- /java/flight-crew-scheduling/src/main/java/org/acme/flighcrewscheduling/rest/FlightCrewSchedulingDemoResource.java: -------------------------------------------------------------------------------- 1 | package org.acme.flighcrewscheduling.rest; 2 | 3 | import jakarta.inject.Inject; 4 | import jakarta.ws.rs.GET; 5 | import jakarta.ws.rs.Path; 6 | import jakarta.ws.rs.core.MediaType; 7 | import jakarta.ws.rs.core.Response; 8 | 9 | import org.acme.flighcrewscheduling.domain.FlightCrewSchedule; 10 | import org.eclipse.microprofile.openapi.annotations.Operation; 11 | import org.eclipse.microprofile.openapi.annotations.media.Content; 12 | import org.eclipse.microprofile.openapi.annotations.media.Schema; 13 | import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; 14 | import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; 15 | import org.eclipse.microprofile.openapi.annotations.tags.Tag; 16 | 17 | @Tag(name = "Demo data", description = "Timefold-provided demo flight crew scheduling data.") 18 | @Path("demo-data") 19 | public class FlightCrewSchedulingDemoResource { 20 | 21 | private final DemoDataGenerator dataGenerator; 22 | 23 | @Inject 24 | public FlightCrewSchedulingDemoResource(DemoDataGenerator dataGenerator) { 25 | this.dataGenerator = dataGenerator; 26 | } 27 | 28 | @APIResponses(value = { 29 | @APIResponse(responseCode = "200", description = "Unsolved demo schedule.", 30 | content = @Content(mediaType = MediaType.APPLICATION_JSON, 31 | schema = @Schema(implementation = FlightCrewSchedule.class))) }) 32 | @Operation(summary = "Find an unsolved demo schedule by ID.") 33 | @GET 34 | public Response generate() { 35 | return Response.ok(dataGenerator.generateDemoData()).build(); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /java/flight-crew-scheduling/src/main/java/org/acme/flighcrewscheduling/rest/exception/ErrorInfo.java: -------------------------------------------------------------------------------- 1 | package org.acme.flighcrewscheduling.rest.exception; 2 | 3 | public record ErrorInfo(String jobId, String message) { 4 | } 5 | -------------------------------------------------------------------------------- /java/flight-crew-scheduling/src/main/java/org/acme/flighcrewscheduling/rest/exception/ScheduleSolverException.java: -------------------------------------------------------------------------------- 1 | package org.acme.flighcrewscheduling.rest.exception; 2 | 3 | import jakarta.ws.rs.core.Response; 4 | 5 | public class ScheduleSolverException extends RuntimeException { 6 | 7 | private final String jobId; 8 | 9 | private final Response.Status status; 10 | 11 | public ScheduleSolverException(String jobId, Response.Status status, String message) { 12 | super(message); 13 | this.jobId = jobId; 14 | this.status = status; 15 | } 16 | 17 | public ScheduleSolverException(String jobId, Throwable cause) { 18 | super(cause.getMessage(), cause); 19 | this.jobId = jobId; 20 | this.status = Response.Status.INTERNAL_SERVER_ERROR; 21 | } 22 | 23 | public String getJobId() { 24 | return jobId; 25 | } 26 | 27 | public Response.Status getStatus() { 28 | return status; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /java/flight-crew-scheduling/src/main/java/org/acme/flighcrewscheduling/rest/exception/ScheduleSolverExceptionMapper.java: -------------------------------------------------------------------------------- 1 | package org.acme.flighcrewscheduling.rest.exception; 2 | 3 | import jakarta.ws.rs.core.MediaType; 4 | import jakarta.ws.rs.core.Response; 5 | import jakarta.ws.rs.ext.ExceptionMapper; 6 | import jakarta.ws.rs.ext.Provider; 7 | 8 | @Provider 9 | public class ScheduleSolverExceptionMapper implements ExceptionMapper { 10 | 11 | @Override 12 | public Response toResponse(ScheduleSolverException exception) { 13 | return Response 14 | .status(exception.getStatus()) 15 | .type(MediaType.APPLICATION_JSON) 16 | .entity(new ErrorInfo(exception.getJobId(), exception.getMessage())) 17 | .build(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /java/food-packaging/food-packaging-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimefoldAI/timefold-quickstarts/f3e83432b4a142110e6aad7e26dc9fd37937ec25/java/food-packaging/food-packaging-screenshot.png -------------------------------------------------------------------------------- /java/food-packaging/src/main/java/org/acme/foodpackaging/domain/WorkCalendar.java: -------------------------------------------------------------------------------- 1 | package org.acme.foodpackaging.domain; 2 | 3 | import java.time.LocalDate; 4 | 5 | public class WorkCalendar { 6 | 7 | private LocalDate fromDate; // Inclusive 8 | private LocalDate toDate; // Exclusive 9 | 10 | public WorkCalendar() { 11 | } 12 | 13 | public WorkCalendar(LocalDate fromDate, LocalDate toDate) { 14 | this.fromDate = fromDate; 15 | this.toDate = toDate; 16 | } 17 | 18 | @Override 19 | public String toString() { 20 | return fromDate + " - " + toDate; 21 | } 22 | 23 | // ************************************************************************ 24 | // Getters and setters 25 | // ************************************************************************ 26 | 27 | public LocalDate getFromDate() { 28 | return fromDate; 29 | } 30 | 31 | public LocalDate getToDate() { 32 | return toDate; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /java/food-packaging/src/main/java/org/acme/foodpackaging/persistence/PackagingScheduleRepository.java: -------------------------------------------------------------------------------- 1 | package org.acme.foodpackaging.persistence; 2 | 3 | import java.util.concurrent.atomic.AtomicReference; 4 | 5 | import jakarta.enterprise.context.ApplicationScoped; 6 | 7 | import org.acme.foodpackaging.domain.PackagingSchedule; 8 | 9 | @ApplicationScoped 10 | public class PackagingScheduleRepository { 11 | 12 | private final AtomicReference solutionReference = new AtomicReference<>(); 13 | 14 | public PackagingSchedule read() { 15 | return solutionReference.get(); 16 | } 17 | 18 | public void write(PackagingSchedule schedule) { 19 | solutionReference.set(schedule); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /java/food-packaging/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | ######################## 2 | # Timefold Solver properties 3 | ######################## 4 | 5 | # The solver runs for 30 seconds. To run for 5 minutes use "5m" and for 2 hours use "2h". 6 | quarkus.timefold.solver.termination.spent-limit=30s 7 | 8 | # To change how many solvers to run in parallel 9 | # timefold.solver-manager.parallel-solver-count=4 10 | 11 | # Temporary comment this out to detect bugs in your code (lowers performance) 12 | # quarkus.timefold.solver.environment-mode=FULL_ASSERT 13 | 14 | # Temporary comment this out to return a feasible solution as soon as possible 15 | # quarkus.timefold.solver.termination.best-score-limit=0hard/0medium/*soft 16 | 17 | # To see what Timefold is doing, turn on DEBUG or TRACE logging. 18 | quarkus.log.category."ai.timefold.solver".level=INFO 19 | %test.quarkus.log.category."ai.timefold.solver".level=INFO 20 | %prod.quarkus.log.category."ai.timefold.solver".level=INFO 21 | 22 | ######################## 23 | # Timefold Solver Enterprise properties 24 | ######################## 25 | 26 | # To run increase CPU cores usage per solver 27 | %enterprise.quarkus.timefold.solver.move-thread-count=AUTO 28 | 29 | ######################## 30 | # Native build properties 31 | ######################## 32 | 33 | # Enable Swagger UI also in the native mode 34 | quarkus.swagger-ui.always-include=true 35 | 36 | ######################## 37 | # Test overrides 38 | ######################## 39 | 40 | # Effectively disable spent-time termination in favor of the best-score-limit 41 | %test.quarkus.timefold.solver.termination.spent-limit=1h 42 | %test.quarkus.timefold.solver.termination.best-score-limit=0hard/0medium/*soft -------------------------------------------------------------------------------- /java/food-packaging/src/test/java/org/acme/foodpackaging/solver/FoodPackagingResourceIT.java: -------------------------------------------------------------------------------- 1 | package org.acme.foodpackaging.solver; 2 | 3 | import static io.restassured.RestAssured.get; 4 | import static io.restassured.RestAssured.post; 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | import static org.awaitility.Awaitility.await; 7 | 8 | import java.time.Duration; 9 | 10 | import ai.timefold.solver.core.api.solver.SolverStatus; 11 | 12 | import org.junit.jupiter.api.Test; 13 | 14 | import io.quarkus.test.junit.QuarkusIntegrationTest; 15 | 16 | @QuarkusIntegrationTest 17 | class FoodPackagingResourceIT { 18 | 19 | @Test 20 | void solveNative() { 21 | post("/schedule/solve") 22 | .then() 23 | .statusCode(204) 24 | .extract(); 25 | 26 | await() 27 | .atMost(Duration.ofMinutes(1)) 28 | .pollInterval(Duration.ofMillis(500L)) 29 | .until(() -> !SolverStatus.NOT_SOLVING.name().equals(get("/schedule").jsonPath().get("solverStatus"))); 30 | 31 | String score = get("/schedule").jsonPath().get("score"); 32 | assertThat(score).isNotNull(); 33 | } 34 | } -------------------------------------------------------------------------------- /java/hello-world/README.adoc: -------------------------------------------------------------------------------- 1 | = School Timetabling (Java, Maven or Gradle) 2 | 3 | Assign lessons to timeslots and rooms to produce a better schedule for teachers and students. 4 | 5 | * <> 6 | 7 | == Prerequisites 8 | 9 | . Install Java and Maven, for example with https://sdkman.io[Sdkman]: 10 | + 11 | ---- 12 | $ sdk install java 13 | $ sdk install maven 14 | ---- 15 | 16 | [[run]] 17 | == Run the application 18 | 19 | . Git clone the timefold-quickstarts repo: 20 | + 21 | [source, shell] 22 | ---- 23 | $ git clone https://github.com/TimefoldAI/timefold-quickstarts.git 24 | ... 25 | $ cd timefold-quickstarts/java/hello-world 26 | ---- 27 | 28 | . Start the application with Maven: 29 | + 30 | [source, shell] 31 | ---- 32 | $ mvn verify 33 | ... 34 | $ java -jar target/hello-world-run.jar 35 | ---- 36 | + 37 | or with Gradle: 38 | + 39 | [source, shell] 40 | ---- 41 | $ gradle run 42 | ---- 43 | 44 | Look for the planning solution in the console log. 45 | 46 | == More information 47 | 48 | Visit https://timefold.ai[timefold.ai]. 49 | -------------------------------------------------------------------------------- /java/hello-world/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "java" 3 | id "application" 4 | } 5 | 6 | def timefoldVersion = "1.22.1" 7 | def logbackVersion = "1.5.18" 8 | def junitVersion = "5.12.2" 9 | def assertjVersion = "3.27.3" 10 | def profile = System.properties['profile'] ?: '' 11 | 12 | group = "org.acme" 13 | version = "1.0-SNAPSHOT" 14 | 15 | repositories { 16 | mavenCentral() 17 | mavenLocal() 18 | } 19 | 20 | dependencies { 21 | implementation platform("ai.timefold.solver:timefold-solver-bom:${timefoldVersion}") 22 | implementation "ai.timefold.solver:timefold-solver-core" 23 | runtimeOnly "ch.qos.logback:logback-classic:${logbackVersion}" 24 | 25 | // Testing 26 | testImplementation platform("org.junit:junit-bom:${junitVersion}") 27 | testImplementation "ai.timefold.solver:timefold-solver-test" 28 | testImplementation "org.junit.jupiter:junit-jupiter" 29 | testImplementation "org.assertj:assertj-core:${assertjVersion}" 30 | testRuntimeOnly "org.junit.platform:junit-platform-launcher" 31 | 32 | } 33 | 34 | java { 35 | sourceCompatibility = JavaVersion.VERSION_17 36 | targetCompatibility = JavaVersion.VERSION_17 37 | } 38 | 39 | compileJava { 40 | options.encoding = "UTF-8" 41 | options.compilerArgs << "-parameters" 42 | } 43 | 44 | compileTestJava { 45 | options.encoding = "UTF-8" 46 | } 47 | 48 | application { 49 | mainClass = "org.acme.schooltimetabling.TimetableApp" 50 | } 51 | 52 | test { 53 | // Log the test execution results. 54 | testLogging { 55 | events "passed", "skipped", "failed" 56 | } 57 | 58 | if (profile == 'slowly') { 59 | useJUnitPlatform() 60 | } else { 61 | useJUnitPlatform { 62 | excludeTags "slowly" 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /java/hello-world/src/assembly/jar-with-dependencies-and-services.xml: -------------------------------------------------------------------------------- 1 | 4 | jar-with-dependencies-and-services 5 | 6 | jar 7 | 8 | 9 | 10 | metaInf-services 11 | 12 | 13 | false 14 | 15 | 16 | / 17 | true 18 | true 19 | runtime 20 | 21 | 22 | -------------------------------------------------------------------------------- /java/hello-world/src/main/java/org/acme/schooltimetabling/domain/Room.java: -------------------------------------------------------------------------------- 1 | package org.acme.schooltimetabling.domain; 2 | 3 | import ai.timefold.solver.core.api.domain.lookup.PlanningId; 4 | 5 | public class Room { 6 | 7 | @PlanningId 8 | private String id; 9 | 10 | private String name; 11 | 12 | public Room() { 13 | } 14 | 15 | public Room(String id, String name) { 16 | this.id = id; 17 | this.name = name; 18 | } 19 | 20 | @Override 21 | public String toString() { 22 | return name; 23 | } 24 | 25 | // ************************************************************************ 26 | // Getters and setters 27 | // ************************************************************************ 28 | 29 | public String getId() { 30 | return id; 31 | } 32 | 33 | public String getName() { 34 | return name; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /java/hello-world/src/main/java/org/acme/schooltimetabling/domain/Timeslot.java: -------------------------------------------------------------------------------- 1 | package org.acme.schooltimetabling.domain; 2 | 3 | import ai.timefold.solver.core.api.domain.lookup.PlanningId; 4 | 5 | import java.time.DayOfWeek; 6 | import java.time.LocalTime; 7 | 8 | public class Timeslot { 9 | 10 | @PlanningId 11 | private String id; 12 | 13 | private DayOfWeek dayOfWeek; 14 | private LocalTime startTime; 15 | private LocalTime endTime; 16 | 17 | public Timeslot() { 18 | } 19 | 20 | public Timeslot(String id, DayOfWeek dayOfWeek, LocalTime startTime, LocalTime endTime) { 21 | this.id = id; 22 | this.dayOfWeek = dayOfWeek; 23 | this.startTime = startTime; 24 | this.endTime = endTime; 25 | } 26 | 27 | public Timeslot(String id, DayOfWeek dayOfWeek, LocalTime startTime) { 28 | this(id, dayOfWeek, startTime, startTime.plusMinutes(50)); 29 | } 30 | 31 | @Override 32 | public String toString() { 33 | return dayOfWeek + " " + startTime; 34 | } 35 | 36 | // ************************************************************************ 37 | // Getters and setters 38 | // ************************************************************************ 39 | 40 | public String getId() { 41 | return id; 42 | } 43 | 44 | public DayOfWeek getDayOfWeek() { 45 | return dayOfWeek; 46 | } 47 | 48 | public LocalTime getStartTime() { 49 | return startTime; 50 | } 51 | 52 | public LocalTime getEndTime() { 53 | return endTime; 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /java/hello-world/src/main/java/org/acme/schooltimetabling/solver/justifications/RoomConflictJustification.java: -------------------------------------------------------------------------------- 1 | package org.acme.schooltimetabling.solver.justifications; 2 | 3 | import ai.timefold.solver.core.api.score.stream.ConstraintJustification; 4 | import org.acme.schooltimetabling.domain.Lesson; 5 | import org.acme.schooltimetabling.domain.Room; 6 | 7 | public record RoomConflictJustification(Room room, Lesson lesson1, Lesson lesson2, String description) 8 | implements 9 | ConstraintJustification { 10 | 11 | public RoomConflictJustification(Room room, Lesson lesson1, Lesson lesson2) { 12 | this(room, lesson1, lesson2, 13 | "Room '%s' is used for lesson '%s' for student group '%s' and lesson '%s' for student group '%s' at '%s %s'" 14 | .formatted(room, lesson1.getSubject(), lesson1.getStudentGroup(), lesson2.getSubject(), 15 | lesson2.getStudentGroup(), lesson1.getTimeslot().getDayOfWeek(), 16 | lesson1.getTimeslot().getStartTime())); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /java/hello-world/src/main/java/org/acme/schooltimetabling/solver/justifications/StudentGroupConflictJustification.java: -------------------------------------------------------------------------------- 1 | package org.acme.schooltimetabling.solver.justifications; 2 | 3 | import ai.timefold.solver.core.api.score.stream.ConstraintJustification; 4 | import org.acme.schooltimetabling.domain.Lesson; 5 | 6 | public record StudentGroupConflictJustification(String studentGroup, Lesson lesson1, Lesson lesson2, String description) 7 | implements 8 | ConstraintJustification { 9 | public StudentGroupConflictJustification(String studentGroup, Lesson lesson1, Lesson lesson2) { 10 | this(studentGroup, lesson1, lesson2, 11 | "Student group '%s' has lesson '%s' and lesson '%s' at '%s %s'" 12 | .formatted(studentGroup, lesson1.getSubject(), lesson2.getSubject(), 13 | lesson1.getTimeslot().getDayOfWeek(), lesson1.getTimeslot().getStartTime())); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /java/hello-world/src/main/java/org/acme/schooltimetabling/solver/justifications/StudentGroupSubjectVarietyJustification.java: -------------------------------------------------------------------------------- 1 | package org.acme.schooltimetabling.solver.justifications; 2 | 3 | import ai.timefold.solver.core.api.score.stream.ConstraintJustification; 4 | import org.acme.schooltimetabling.domain.Lesson; 5 | 6 | public record StudentGroupSubjectVarietyJustification(String studentGroup, Lesson lesson1, Lesson lesson2, String description) 7 | implements 8 | ConstraintJustification { 9 | 10 | public StudentGroupSubjectVarietyJustification(String studentGroup, Lesson lesson1, Lesson lesson2) { 11 | this(studentGroup, lesson1, lesson2, 12 | "Student Group '%s' has two consecutive lessons on '%s' at '%s %s' and at '%s %s'" 13 | .formatted(studentGroup, lesson1.getSubject(), lesson1.getTimeslot().getDayOfWeek(), 14 | lesson1.getTimeslot().getStartTime(), lesson2.getTimeslot().getDayOfWeek(), 15 | lesson2.getTimeslot().getStartTime())); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /java/hello-world/src/main/java/org/acme/schooltimetabling/solver/justifications/TeacherConflictJustification.java: -------------------------------------------------------------------------------- 1 | package org.acme.schooltimetabling.solver.justifications; 2 | 3 | import ai.timefold.solver.core.api.score.stream.ConstraintJustification; 4 | import org.acme.schooltimetabling.domain.Lesson; 5 | 6 | public record TeacherConflictJustification(String teacher, Lesson lesson1, Lesson lesson2, String description) 7 | implements 8 | ConstraintJustification { 9 | 10 | public TeacherConflictJustification(String teacher, Lesson lesson1, Lesson lesson2) { 11 | this(teacher, lesson1, lesson2, 12 | "Teacher '%s' needs to teach lesson '%s' for student group '%s' and lesson '%s' for student group '%s' at '%s %s'" 13 | .formatted(teacher, lesson1.getSubject(), lesson1.getStudentGroup(), lesson2.getSubject(), 14 | lesson2.getStudentGroup(), lesson1.getTimeslot().getDayOfWeek(), 15 | lesson1.getTimeslot().getStartTime())); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /java/hello-world/src/main/java/org/acme/schooltimetabling/solver/justifications/TeacherRoomStabilityJustification.java: -------------------------------------------------------------------------------- 1 | package org.acme.schooltimetabling.solver.justifications; 2 | 3 | import ai.timefold.solver.core.api.score.stream.ConstraintJustification; 4 | import org.acme.schooltimetabling.domain.Lesson; 5 | 6 | public record TeacherRoomStabilityJustification(String teacher, Lesson lesson1, Lesson lesson2, String description) 7 | implements 8 | ConstraintJustification { 9 | 10 | public TeacherRoomStabilityJustification(String teacher, Lesson lesson1, Lesson lesson2) { 11 | this(teacher, lesson1, lesson2, 12 | "Teacher '%s' has two lessons in different rooms: room '%s' at '%s %s' and room '%s' at '%s %s'" 13 | .formatted(teacher, lesson1.getRoom(), lesson1.getTimeslot().getDayOfWeek(), 14 | lesson1.getTimeslot().getStartTime(), lesson2.getRoom(), lesson2.getTimeslot().getDayOfWeek(), 15 | lesson2.getTimeslot().getStartTime())); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /java/hello-world/src/main/java/org/acme/schooltimetabling/solver/justifications/TeacherTimeEfficiencyJustification.java: -------------------------------------------------------------------------------- 1 | package org.acme.schooltimetabling.solver.justifications; 2 | 3 | import ai.timefold.solver.core.api.score.stream.ConstraintJustification; 4 | import org.acme.schooltimetabling.domain.Lesson; 5 | 6 | public record TeacherTimeEfficiencyJustification(String teacher, Lesson lesson1, Lesson lesson2, String description) 7 | implements 8 | ConstraintJustification { 9 | 10 | public TeacherTimeEfficiencyJustification(String teacher, Lesson lesson1, Lesson lesson2) { 11 | this(teacher, lesson1, lesson2, 12 | "Teacher '%s' has 2 consecutive lessons: lesson '%s' for student group '%s' at '%s %s' and lesson '%s' for student group '%s' at '%s %s' (gap)" 13 | .formatted(teacher, lesson1.getSubject(), lesson1.getStudentGroup(), 14 | lesson1.getTimeslot().getDayOfWeek(), lesson1.getTimeslot().getStartTime(), 15 | lesson2.getSubject(), lesson2.getStudentGroup(), lesson2.getTimeslot().getDayOfWeek(), 16 | lesson2.getTimeslot().getStartTime())); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /java/hello-world/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %d{HH:mm:ss.SSS} [%-12.12t] %-5p %m%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /java/maintenance-scheduling/maintenance-scheduling-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimefoldAI/timefold-quickstarts/f3e83432b4a142110e6aad7e26dc9fd37937ec25/java/maintenance-scheduling/maintenance-scheduling-screenshot.png -------------------------------------------------------------------------------- /java/maintenance-scheduling/src/main/java/org/acme/maintenancescheduling/domain/Crew.java: -------------------------------------------------------------------------------- 1 | package org.acme.maintenancescheduling.domain; 2 | 3 | import java.util.Objects; 4 | 5 | import ai.timefold.solver.core.api.domain.lookup.PlanningId; 6 | 7 | public class Crew { 8 | 9 | @PlanningId 10 | private String id; 11 | 12 | private String name; 13 | 14 | public Crew() { 15 | } 16 | 17 | public Crew(String name) { 18 | this.name = name; 19 | } 20 | 21 | public Crew(String id, String name) { 22 | this.id = id; 23 | this.name = name; 24 | } 25 | 26 | @Override 27 | public String toString() { 28 | return name; 29 | } 30 | 31 | // ************************************************************************ 32 | // Getters and setters 33 | // ************************************************************************ 34 | 35 | public String getId() { 36 | return id; 37 | } 38 | 39 | public String getName() { 40 | return name; 41 | } 42 | 43 | @Override 44 | public boolean equals(Object o) { 45 | if (this == o) { 46 | return true; 47 | } 48 | if (!(o instanceof Crew crew)) { 49 | return false; 50 | } 51 | return Objects.equals(getId(), crew.getId()); 52 | } 53 | 54 | @Override 55 | public int hashCode() { 56 | return getId().hashCode(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /java/maintenance-scheduling/src/main/java/org/acme/maintenancescheduling/domain/WorkCalendar.java: -------------------------------------------------------------------------------- 1 | package org.acme.maintenancescheduling.domain; 2 | 3 | import java.time.LocalDate; 4 | 5 | import ai.timefold.solver.core.api.domain.lookup.PlanningId; 6 | 7 | public class WorkCalendar { 8 | 9 | @PlanningId 10 | private String id; 11 | 12 | private LocalDate fromDate; // Inclusive 13 | private LocalDate toDate; // Exclusive 14 | 15 | public WorkCalendar() { 16 | } 17 | 18 | public WorkCalendar(String id, LocalDate fromDate, LocalDate toDate) { 19 | this.id = id; 20 | this.fromDate = fromDate; 21 | this.toDate = toDate; 22 | } 23 | 24 | @Override 25 | public String toString() { 26 | return fromDate + " - " + toDate; 27 | } 28 | 29 | // ************************************************************************ 30 | // Getters and setters 31 | // ************************************************************************ 32 | 33 | public String getId() { 34 | return id; 35 | } 36 | 37 | public LocalDate getFromDate() { 38 | return fromDate; 39 | } 40 | 41 | public LocalDate getToDate() { 42 | return toDate; 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /java/maintenance-scheduling/src/main/java/org/acme/maintenancescheduling/rest/exception/ErrorInfo.java: -------------------------------------------------------------------------------- 1 | package org.acme.maintenancescheduling.rest.exception; 2 | 3 | public record ErrorInfo(String jobId, String message) { 4 | } 5 | -------------------------------------------------------------------------------- /java/maintenance-scheduling/src/main/java/org/acme/maintenancescheduling/rest/exception/MaintenanceScheduleSolverException.java: -------------------------------------------------------------------------------- 1 | package org.acme.maintenancescheduling.rest.exception; 2 | 3 | import jakarta.ws.rs.core.Response; 4 | 5 | public class MaintenanceScheduleSolverException extends RuntimeException { 6 | 7 | private final String jobId; 8 | 9 | private final Response.Status status; 10 | 11 | public MaintenanceScheduleSolverException(String jobId, Response.Status status, String message) { 12 | super(message); 13 | this.jobId = jobId; 14 | this.status = status; 15 | } 16 | 17 | public MaintenanceScheduleSolverException(String jobId, Throwable cause) { 18 | super(cause.getMessage(), cause); 19 | this.jobId = jobId; 20 | this.status = Response.Status.INTERNAL_SERVER_ERROR; 21 | } 22 | 23 | public String getJobId() { 24 | return jobId; 25 | } 26 | 27 | public Response.Status getStatus() { 28 | return status; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /java/maintenance-scheduling/src/main/java/org/acme/maintenancescheduling/rest/exception/MaintenanceScheduleSolverExceptionMapper.java: -------------------------------------------------------------------------------- 1 | package org.acme.maintenancescheduling.rest.exception; 2 | 3 | import jakarta.ws.rs.core.MediaType; 4 | import jakarta.ws.rs.core.Response; 5 | import jakarta.ws.rs.ext.ExceptionMapper; 6 | import jakarta.ws.rs.ext.Provider; 7 | 8 | @Provider 9 | public class MaintenanceScheduleSolverExceptionMapper implements ExceptionMapper { 10 | 11 | @Override 12 | public Response toResponse(MaintenanceScheduleSolverException exception) { 13 | return Response 14 | .status(exception.getStatus()) 15 | .type(MediaType.APPLICATION_JSON) 16 | .entity(new ErrorInfo(exception.getJobId(), exception.getMessage())) 17 | .build(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /java/meeting-scheduling/meeting-scheduling-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimefoldAI/timefold-quickstarts/f3e83432b4a142110e6aad7e26dc9fd37937ec25/java/meeting-scheduling/meeting-scheduling-screenshot.png -------------------------------------------------------------------------------- /java/meeting-scheduling/src/main/java/org/acme/meetingschedule/domain/Attendance.java: -------------------------------------------------------------------------------- 1 | package org.acme.meetingschedule.domain; 2 | 3 | import ai.timefold.solver.core.api.domain.lookup.PlanningId; 4 | 5 | public abstract class Attendance { 6 | 7 | @PlanningId 8 | private String id; 9 | private Person person; 10 | private Meeting meeting; 11 | 12 | protected Attendance() { 13 | } 14 | 15 | protected Attendance(String id) { 16 | this.id = id; 17 | } 18 | 19 | protected Attendance(String id, Meeting meeting) { 20 | this(id); 21 | this.meeting = meeting; 22 | } 23 | 24 | public String getId() { 25 | return id; 26 | } 27 | 28 | public void setId(String id) { 29 | this.id = id; 30 | } 31 | 32 | public Person getPerson() { 33 | return person; 34 | } 35 | 36 | public void setPerson(Person person) { 37 | this.person = person; 38 | } 39 | 40 | public Meeting getMeeting() { 41 | return meeting; 42 | } 43 | 44 | public void setMeeting(Meeting meeting) { 45 | this.meeting = meeting; 46 | } 47 | 48 | @Override 49 | public String toString() { 50 | return person + "-" + meeting; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /java/meeting-scheduling/src/main/java/org/acme/meetingschedule/domain/Person.java: -------------------------------------------------------------------------------- 1 | package org.acme.meetingschedule.domain; 2 | 3 | import java.util.Objects; 4 | 5 | import com.fasterxml.jackson.annotation.JsonIdentityInfo; 6 | import com.fasterxml.jackson.annotation.ObjectIdGenerators; 7 | 8 | @JsonIdentityInfo(scope = Person.class, generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") 9 | public class Person { 10 | 11 | private String id; 12 | private String fullName; 13 | 14 | public Person() { 15 | } 16 | 17 | public Person(String id) { 18 | this.id = id; 19 | } 20 | 21 | public Person(String id, String fullName) { 22 | this(id); 23 | this.fullName = fullName; 24 | } 25 | 26 | public String getId() { 27 | return id; 28 | } 29 | 30 | public void setId(String id) { 31 | this.id = id; 32 | } 33 | 34 | public String getFullName() { 35 | return fullName; 36 | } 37 | 38 | public void setFullName(String fullName) { 39 | this.fullName = fullName; 40 | } 41 | 42 | @Override 43 | public String toString() { 44 | return fullName; 45 | } 46 | 47 | @Override 48 | public boolean equals(Object o) { 49 | if (this == o) 50 | return true; 51 | if (!(o instanceof Person person)) 52 | return false; 53 | return Objects.equals(getId(), person.getId()); 54 | } 55 | 56 | @Override 57 | public int hashCode() { 58 | return getId().hashCode(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /java/meeting-scheduling/src/main/java/org/acme/meetingschedule/domain/PreferredAttendance.java: -------------------------------------------------------------------------------- 1 | package org.acme.meetingschedule.domain; 2 | 3 | public class PreferredAttendance extends Attendance { 4 | 5 | public PreferredAttendance() { 6 | } 7 | 8 | public PreferredAttendance(String id, Meeting meeting) { 9 | super(id, meeting); 10 | } 11 | 12 | public PreferredAttendance(String id, Meeting meeting, Person person) { 13 | super(id, meeting); 14 | setPerson(person); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /java/meeting-scheduling/src/main/java/org/acme/meetingschedule/domain/RequiredAttendance.java: -------------------------------------------------------------------------------- 1 | package org.acme.meetingschedule.domain; 2 | 3 | public class RequiredAttendance extends Attendance { 4 | 5 | public RequiredAttendance() { 6 | } 7 | 8 | public RequiredAttendance(String id, Meeting meeting) { 9 | super(id, meeting); 10 | } 11 | 12 | public RequiredAttendance(String id, Meeting meeting, Person person) { 13 | super(id, meeting); 14 | setPerson(person); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /java/meeting-scheduling/src/main/java/org/acme/meetingschedule/rest/MeetingSchedulingDemoResource.java: -------------------------------------------------------------------------------- 1 | package org.acme.meetingschedule.rest; 2 | 3 | import jakarta.inject.Inject; 4 | import jakarta.ws.rs.GET; 5 | import jakarta.ws.rs.Path; 6 | import jakarta.ws.rs.core.MediaType; 7 | import jakarta.ws.rs.core.Response; 8 | 9 | import org.acme.meetingschedule.domain.MeetingSchedule; 10 | import org.eclipse.microprofile.openapi.annotations.Operation; 11 | import org.eclipse.microprofile.openapi.annotations.media.Content; 12 | import org.eclipse.microprofile.openapi.annotations.media.Schema; 13 | import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; 14 | import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; 15 | import org.eclipse.microprofile.openapi.annotations.tags.Tag; 16 | 17 | @Tag(name = "Demo data", description = "Timefold-provided demo meeting scheduling data.") 18 | @Path("demo-data") 19 | public class MeetingSchedulingDemoResource { 20 | 21 | private final DemoDataGenerator dataGenerator; 22 | 23 | @Inject 24 | public MeetingSchedulingDemoResource(DemoDataGenerator dataGenerator) { 25 | this.dataGenerator = dataGenerator; 26 | } 27 | 28 | @APIResponses(value = { 29 | @APIResponse(responseCode = "200", description = "Unsolved demo schedule.", 30 | content = @Content(mediaType = MediaType.APPLICATION_JSON, 31 | schema = @Schema(implementation = MeetingSchedule.class))) }) 32 | @Operation(summary = "Find an unsolved demo schedule by ID.") 33 | @GET 34 | public Response generate() { 35 | return Response.ok(dataGenerator.generateDemoData()).build(); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /java/meeting-scheduling/src/main/java/org/acme/meetingschedule/rest/exception/ErrorInfo.java: -------------------------------------------------------------------------------- 1 | package org.acme.meetingschedule.rest.exception; 2 | 3 | public record ErrorInfo(String jobId, String message) { 4 | } 5 | -------------------------------------------------------------------------------- /java/meeting-scheduling/src/main/java/org/acme/meetingschedule/rest/exception/ScheduleSolverException.java: -------------------------------------------------------------------------------- 1 | package org.acme.meetingschedule.rest.exception; 2 | 3 | import jakarta.ws.rs.core.Response; 4 | 5 | public class ScheduleSolverException extends RuntimeException { 6 | 7 | private final String jobId; 8 | 9 | private final Response.Status status; 10 | 11 | public ScheduleSolverException(String jobId, Response.Status status, String message) { 12 | super(message); 13 | this.jobId = jobId; 14 | this.status = status; 15 | } 16 | 17 | public ScheduleSolverException(String jobId, Throwable cause) { 18 | super(cause.getMessage(), cause); 19 | this.jobId = jobId; 20 | this.status = Response.Status.INTERNAL_SERVER_ERROR; 21 | } 22 | 23 | public String getJobId() { 24 | return jobId; 25 | } 26 | 27 | public Response.Status getStatus() { 28 | return status; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /java/meeting-scheduling/src/main/java/org/acme/meetingschedule/rest/exception/ScheduleSolverExceptionMapper.java: -------------------------------------------------------------------------------- 1 | package org.acme.meetingschedule.rest.exception; 2 | 3 | import jakarta.ws.rs.core.MediaType; 4 | import jakarta.ws.rs.core.Response; 5 | import jakarta.ws.rs.ext.ExceptionMapper; 6 | import jakarta.ws.rs.ext.Provider; 7 | 8 | @Provider 9 | public class ScheduleSolverExceptionMapper implements ExceptionMapper { 10 | 11 | @Override 12 | public Response toResponse(ScheduleSolverException exception) { 13 | return Response 14 | .status(exception.getStatus()) 15 | .type(MediaType.APPLICATION_JSON) 16 | .entity(new ErrorInfo(exception.getJobId(), exception.getMessage())) 17 | .build(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /java/order-picking/order-picking-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimefoldAI/timefold-quickstarts/f3e83432b4a142110e6aad7e26dc9fd37937ec25/java/order-picking/order-picking-screenshot.png -------------------------------------------------------------------------------- /java/order-picking/src/main/java/org/acme/orderpicking/domain/Order.java: -------------------------------------------------------------------------------- 1 | package org.acme.orderpicking.domain; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.Objects; 6 | 7 | /** 8 | * Represents the order submitted by a customer. 9 | */ 10 | public class Order { 11 | 12 | private String id; 13 | private List items = new ArrayList<>(); 14 | 15 | public Order() { 16 | //marshalling constructor 17 | } 18 | 19 | public Order(String id, List items) { 20 | this.id = id; 21 | this.items = items; 22 | } 23 | 24 | public String getId() { 25 | return id; 26 | } 27 | 28 | public void setId(String id) { 29 | this.id = id; 30 | } 31 | 32 | public List getItems() { 33 | return items; 34 | } 35 | 36 | public void setItems(List items) { 37 | this.items = Objects.requireNonNullElseGet(items, ArrayList::new); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /java/order-picking/src/main/java/org/acme/orderpicking/domain/OrderItem.java: -------------------------------------------------------------------------------- 1 | package org.acme.orderpicking.domain; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | 5 | /** 6 | * In the order picking problem context, the order item represents an indivisible product that was added to the order. 7 | * 8 | * @see Product 9 | */ 10 | public class OrderItem { 11 | 12 | private String id; 13 | @JsonIgnore 14 | private Order order; 15 | private Product product; 16 | 17 | public OrderItem() { 18 | //marshalling constructor 19 | } 20 | 21 | public OrderItem(String id, Order order, Product product) { 22 | this.id = id; 23 | this.order = order; 24 | this.product = product; 25 | } 26 | 27 | public String getId() { 28 | return id; 29 | } 30 | 31 | public void setId(String id) { 32 | this.id = id; 33 | } 34 | 35 | public Order getOrder() { 36 | return order; 37 | } 38 | 39 | public void setOrder(Order order) { 40 | this.order = order; 41 | } 42 | 43 | public Product getProduct() { 44 | return product; 45 | } 46 | 47 | public void setProduct(Product product) { 48 | this.product = product; 49 | } 50 | 51 | public int getVolume() { 52 | return product.getVolume(); 53 | } 54 | 55 | /** 56 | * Helper method, facilitates UI building. 57 | */ 58 | public String getOrderId() { 59 | return order != null ? order.getId() : null; 60 | } 61 | 62 | @Override 63 | public String toString() { 64 | return "OrderItem{" + 65 | "id='" + id + '\'' + 66 | ", order=" + order + 67 | ", product=" + product + 68 | '}'; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /java/order-picking/src/main/java/org/acme/orderpicking/domain/OrderPickingPlanning.java: -------------------------------------------------------------------------------- 1 | package org.acme.orderpicking.domain; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | import ai.timefold.solver.core.api.solver.SolverStatus; 7 | 8 | /** 9 | * Helper class for sending information to the UI. 10 | */ 11 | public class OrderPickingPlanning { 12 | 13 | private SolverStatus solverStatus; 14 | private OrderPickingSolution solution; 15 | private boolean solverWasNeverStarted; 16 | private Map distanceToTravelByTrolley = new HashMap<>(); 17 | 18 | public OrderPickingPlanning() { 19 | //marshalling constructor 20 | } 21 | 22 | public OrderPickingPlanning(SolverStatus solverStatus, OrderPickingSolution solution, boolean solverWasNeverStarted) { 23 | this.solverStatus = solverStatus; 24 | this.solution = solution; 25 | this.solverWasNeverStarted = solverWasNeverStarted; 26 | for (Trolley trolley : solution.getTrolleys()) { 27 | distanceToTravelByTrolley.put(trolley.getId(), Warehouse.calculateDistanceToTravel(trolley)); 28 | } 29 | } 30 | 31 | public SolverStatus getSolverStatus() { 32 | return solverStatus; 33 | } 34 | 35 | public OrderPickingSolution getSolution() { 36 | return solution; 37 | } 38 | 39 | public boolean getSolverWasNeverStarted() { 40 | return solverWasNeverStarted; 41 | } 42 | 43 | public Map getDistanceToTravelByTrolley() { 44 | return distanceToTravelByTrolley; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /java/order-picking/src/main/java/org/acme/orderpicking/domain/Shelving.java: -------------------------------------------------------------------------------- 1 | package org.acme.orderpicking.domain; 2 | 3 | /** 4 | * Represents the products container. In the order picking problem the warehouse is represented as a set of shelvings 5 | * that are organized into columns and rows. Each shelving has two sides where the products can be stored, and 6 | * a number of rows. 7 | * 8 | * @see Warehouse 9 | */ 10 | public class Shelving { 11 | 12 | public static final int ROWS_SIZE = 10; 13 | 14 | /** 15 | * Available shelving sides where the products can be located. 16 | */ 17 | public enum Side { 18 | LEFT, 19 | RIGHT 20 | } 21 | 22 | private String id; 23 | 24 | /** 25 | * Absolute x position of the shelving's left bottom corner within the warehouse. 26 | */ 27 | private int x; 28 | /** 29 | * Absolute y position of the shelving's left bottom corner within the warehouse. 30 | */ 31 | private int y; 32 | 33 | Shelving(String id, int x, int y) { 34 | this.id = id; 35 | this.x = x; 36 | this.y = y; 37 | } 38 | 39 | public static String newShelvingId(Warehouse.Column column, Warehouse.Row row) { 40 | return "(" + column.toString() + "," + row.toString() + ")"; 41 | } 42 | 43 | public String getId() { 44 | return id; 45 | } 46 | 47 | public int getX() { 48 | return x; 49 | } 50 | 51 | public int getY() { 52 | return y; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /java/order-picking/src/main/java/org/acme/orderpicking/domain/Trolley.java: -------------------------------------------------------------------------------- 1 | package org.acme.orderpicking.domain; 2 | 3 | import ai.timefold.solver.core.api.domain.lookup.PlanningId; 4 | 5 | /** 6 | * Represents the trolley that will be filled with the order items. 7 | * 8 | * @see TrolleyStep for more information about the model constructed by the Solver. 9 | */ 10 | public class Trolley extends TrolleyOrTrolleyStep { 11 | 12 | @PlanningId 13 | private String id; 14 | private int bucketCount; 15 | private int bucketCapacity; 16 | private WarehouseLocation location; 17 | 18 | public Trolley() { 19 | //marshalling constructor 20 | } 21 | 22 | public Trolley(String id, int bucketCount, int bucketCapacity, WarehouseLocation location) { 23 | this.id = id; 24 | this.bucketCount = bucketCount; 25 | this.bucketCapacity = bucketCapacity; 26 | this.location = location; 27 | } 28 | 29 | public String getId() { 30 | return id; 31 | } 32 | 33 | public void setId(String id) { 34 | this.id = id; 35 | } 36 | 37 | public int getBucketCount() { 38 | return bucketCount; 39 | } 40 | 41 | public void setBucketCount(int bucketCount) { 42 | this.bucketCount = bucketCount; 43 | } 44 | 45 | public int getBucketCapacity() { 46 | return bucketCapacity; 47 | } 48 | 49 | public void setBucketCapacity(int bucketCapacity) { 50 | this.bucketCapacity = bucketCapacity; 51 | } 52 | 53 | @Override 54 | public WarehouseLocation getLocation() { 55 | return location; 56 | } 57 | 58 | public void setLocation(WarehouseLocation location) { 59 | this.location = location; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /java/order-picking/src/main/java/org/acme/orderpicking/domain/TrolleyOrTrolleyStep.java: -------------------------------------------------------------------------------- 1 | package org.acme.orderpicking.domain; 2 | 3 | import ai.timefold.solver.core.api.domain.entity.PlanningEntity; 4 | import ai.timefold.solver.core.api.domain.variable.InverseRelationShadowVariable; 5 | 6 | /** 7 | * Base class for implementing the CHAINED graph modelling strategy. 8 | * 9 | * @see Trolley 10 | * @see TrolleyStep 11 | */ 12 | @PlanningEntity 13 | public abstract class TrolleyOrTrolleyStep { 14 | 15 | public static final String PREVIOUS_ELEMENT = "previousElement"; 16 | 17 | /** 18 | * Shadow variable: Is automatically set by the Solver and facilitates that all the elements in the chain, the 19 | * Trolley and the TrolleyStep, can have a reference to the next element in that chain. 20 | */ 21 | @InverseRelationShadowVariable(sourceVariableName = PREVIOUS_ELEMENT) 22 | protected TrolleyStep nextElement; 23 | 24 | protected TrolleyOrTrolleyStep() { 25 | //marshalling constructor 26 | } 27 | 28 | public abstract WarehouseLocation getLocation(); 29 | 30 | public TrolleyStep getNextElement() { 31 | return nextElement; 32 | } 33 | 34 | public void setNextElement(TrolleyStep nextElement) { 35 | this.nextElement = nextElement; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /java/order-picking/src/main/java/org/acme/orderpicking/domain/WarehouseLocation.java: -------------------------------------------------------------------------------- 1 | package org.acme.orderpicking.domain; 2 | 3 | /** 4 | * Represents a location in the warehouse where a product can be stored. In the context of the order picking problem 5 | * the warehouse is modeled as set of shelvings. For picking a particular product the employees walks to the indicated 6 | * shelving side and row. 7 | * 8 | * @see Warehouse 9 | * @see Shelving 10 | */ 11 | public class WarehouseLocation { 12 | 13 | private String shelvingId; 14 | private Shelving.Side side; 15 | private int row; 16 | 17 | public WarehouseLocation() { 18 | //marshalling constructor 19 | } 20 | 21 | public WarehouseLocation(String shelvingId, Shelving.Side side, int row) { 22 | this.shelvingId = shelvingId; 23 | this.side = side; 24 | this.row = row; 25 | } 26 | 27 | public String getShelvingId() { 28 | return shelvingId; 29 | } 30 | 31 | public void setShelvingId(String shelvingId) { 32 | this.shelvingId = shelvingId; 33 | } 34 | 35 | public Shelving.Side getSide() { 36 | return side; 37 | } 38 | 39 | public void setSide(Shelving.Side side) { 40 | this.side = side; 41 | } 42 | 43 | public int getRow() { 44 | return row; 45 | } 46 | 47 | public void setRow(int row) { 48 | this.row = row; 49 | } 50 | 51 | @Override 52 | public String toString() { 53 | return "WarehouseLocation{" + 54 | "shelvingId='" + shelvingId + '\'' + 55 | ", side=" + side + 56 | ", row=" + row + 57 | '}'; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /java/order-picking/src/main/java/org/acme/orderpicking/persistence/OrderPickingRepository.java: -------------------------------------------------------------------------------- 1 | package org.acme.orderpicking.persistence; 2 | 3 | import jakarta.enterprise.context.ApplicationScoped; 4 | 5 | import org.acme.orderpicking.domain.OrderPickingSolution; 6 | 7 | @ApplicationScoped 8 | public class OrderPickingRepository { 9 | private OrderPickingSolution orderPickingSolution; 10 | 11 | public OrderPickingSolution find() { 12 | return orderPickingSolution; 13 | } 14 | 15 | public void save(OrderPickingSolution orderPickingSolution) { 16 | this.orderPickingSolution = orderPickingSolution; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /java/order-picking/src/main/resources/META-INF/resources/style.css: -------------------------------------------------------------------------------- 1 | .agent-row { 2 | border: 2px solid grey; 3 | } 4 | 5 | .trolley-checkbox-rectangle { 6 | vertical-align: middle; 7 | width: 20px; 8 | max-width: 20px; 9 | height: 20px; 10 | max-height: 20px; 11 | } -------------------------------------------------------------------------------- /java/order-picking/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | ######################## 2 | # Timefold Solver properties 3 | ######################## 4 | 5 | # The solver runs for 30 seconds. To run for 5 minutes use "5m" and for 2 hours use "2h". 6 | quarkus.timefold.solver.termination.spent-limit=30s 7 | 8 | # To change how many solvers to run in parallel 9 | # timefold.solver-manager.parallel-solver-count=4 10 | 11 | # Temporary comment this out to detect bugs in your code (lowers performance) 12 | # quarkus.timefold.solver.environment-mode=FULL_ASSERT 13 | 14 | # Temporary comment this out to return a feasible solution as soon as possible 15 | # quarkus.timefold.solver.termination.best-score-limit=0hard/*soft 16 | 17 | # XML file for power tweaking, defaults to solverConfig.xml (directly under src/main/resources) 18 | # quarkus.timefold.solver-config-xml=org/.../orderPickingSolverConfig.xml 19 | 20 | ######################## 21 | # Timefold Solver Enterprise properties 22 | ######################## 23 | 24 | # To run increase CPU cores usage per solver 25 | %enterprise.quarkus.timefold.solver.move-thread-count=AUTO 26 | 27 | ######################## 28 | # Native build properties 29 | ######################## 30 | 31 | # Enable Swagger UI also in the native mode 32 | quarkus.swagger-ui.always-include=true 33 | 34 | ######################## 35 | # Test overrides 36 | ######################## 37 | 38 | # Effectively disable spent-time termination in favor of the best-score-limit 39 | %test.quarkus.timefold.solver.termination.spent-limit=1h 40 | %test.quarkus.timefold.solver.termination.best-score-limit=0hard/*soft 41 | -------------------------------------------------------------------------------- /java/order-picking/src/test/java/org/acme/orderpicking/rest/OrderPickingResourceIT.java: -------------------------------------------------------------------------------- 1 | package org.acme.orderpicking.rest; 2 | 3 | import static io.restassured.RestAssured.get; 4 | import static io.restassured.RestAssured.post; 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | import static org.awaitility.Awaitility.await; 7 | 8 | import java.time.Duration; 9 | 10 | import ai.timefold.solver.core.api.solver.SolverStatus; 11 | 12 | import org.junit.jupiter.api.Test; 13 | 14 | import io.quarkus.test.junit.QuarkusIntegrationTest; 15 | 16 | @QuarkusIntegrationTest 17 | class OrderPickingResourceIT { 18 | 19 | @Test 20 | void solveNative() { 21 | post("/orderPicking/solve") 22 | .then() 23 | .statusCode(204) 24 | .extract(); 25 | 26 | await() 27 | .atMost(Duration.ofMinutes(1)) 28 | .pollInterval(Duration.ofMillis(500L)) 29 | .until(() -> !SolverStatus.NOT_SOLVING.name().equals(get("/orderPicking").jsonPath().get("solverStatus"))); 30 | 31 | String score = get("/orderPicking").jsonPath().getString("solution.score"); 32 | assertThat(score).isNotNull(); 33 | } 34 | } -------------------------------------------------------------------------------- /java/project-job-scheduling/project-job-scheduling-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimefoldAI/timefold-quickstarts/f3e83432b4a142110e6aad7e26dc9fd37937ec25/java/project-job-scheduling/project-job-scheduling-screenshot.png -------------------------------------------------------------------------------- /java/project-job-scheduling/src/main/java/org/acme/projectjobschedule/domain/JobType.java: -------------------------------------------------------------------------------- 1 | package org.acme.projectjobschedule.domain; 2 | 3 | public enum JobType { 4 | SOURCE, 5 | STANDARD, 6 | SINK; 7 | } 8 | -------------------------------------------------------------------------------- /java/project-job-scheduling/src/main/java/org/acme/projectjobschedule/domain/resource/GlobalResource.java: -------------------------------------------------------------------------------- 1 | package org.acme.projectjobschedule.domain.resource; 2 | 3 | public class GlobalResource extends Resource { 4 | 5 | public GlobalResource() { 6 | } 7 | 8 | public GlobalResource(String id, int capacity) { 9 | super(id, capacity); 10 | } 11 | 12 | // ************************************************************************ 13 | // Complex methods 14 | // ************************************************************************ 15 | 16 | @Override 17 | public boolean isRenewable() { 18 | return true; 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /java/project-job-scheduling/src/main/java/org/acme/projectjobschedule/domain/resource/LocalResource.java: -------------------------------------------------------------------------------- 1 | package org.acme.projectjobschedule.domain.resource; 2 | 3 | import org.acme.projectjobschedule.domain.Project; 4 | 5 | public class LocalResource extends Resource { 6 | 7 | private Project project; 8 | private boolean renewable; 9 | 10 | public LocalResource() { 11 | } 12 | 13 | public LocalResource(String id, Project project, int capacity, boolean renewable) { 14 | super(id, capacity); 15 | this.project = project; 16 | this.renewable = renewable; 17 | } 18 | 19 | public Project getProject() { 20 | return project; 21 | } 22 | 23 | public void setProject(Project project) { 24 | this.project = project; 25 | } 26 | 27 | @Override 28 | public boolean isRenewable() { 29 | return renewable; 30 | } 31 | 32 | public void setRenewable(boolean renewable) { 33 | this.renewable = renewable; 34 | } 35 | 36 | // ************************************************************************ 37 | // Complex methods 38 | // ************************************************************************ 39 | 40 | } 41 | -------------------------------------------------------------------------------- /java/project-job-scheduling/src/main/java/org/acme/projectjobschedule/domain/solver/DelayStrengthComparator.java: -------------------------------------------------------------------------------- 1 | package org.acme.projectjobschedule.domain.solver; 2 | 3 | import java.util.Comparator; 4 | 5 | public class DelayStrengthComparator implements Comparator { 6 | 7 | @Override 8 | public int compare(Integer a, Integer b) { 9 | return a.compareTo(b); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /java/project-job-scheduling/src/main/java/org/acme/projectjobschedule/rest/ProjectJobSchedulingDemoResource.java: -------------------------------------------------------------------------------- 1 | package org.acme.projectjobschedule.rest; 2 | 3 | import jakarta.inject.Inject; 4 | import jakarta.ws.rs.GET; 5 | import jakarta.ws.rs.Path; 6 | import jakarta.ws.rs.core.MediaType; 7 | import jakarta.ws.rs.core.Response; 8 | 9 | import org.acme.projectjobschedule.domain.ProjectJobSchedule; 10 | import org.eclipse.microprofile.openapi.annotations.Operation; 11 | import org.eclipse.microprofile.openapi.annotations.media.Content; 12 | import org.eclipse.microprofile.openapi.annotations.media.Schema; 13 | import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; 14 | import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; 15 | import org.eclipse.microprofile.openapi.annotations.tags.Tag; 16 | 17 | @Tag(name = "Demo data", description = "Timefold-provided demo project job scheduling data.") 18 | @Path("demo-data") 19 | public class ProjectJobSchedulingDemoResource { 20 | 21 | private final DemoDataGenerator dataGenerator; 22 | 23 | @Inject 24 | public ProjectJobSchedulingDemoResource(DemoDataGenerator dataGenerator) { 25 | this.dataGenerator = dataGenerator; 26 | } 27 | 28 | @APIResponses(value = { 29 | @APIResponse(responseCode = "200", description = "Unsolved demo schedule.", 30 | content = @Content(mediaType = MediaType.APPLICATION_JSON, 31 | schema = @Schema(implementation = ProjectJobSchedule.class))) }) 32 | @Operation(summary = "Find an unsolved demo schedule by ID.") 33 | @GET 34 | public Response generate() { 35 | return Response.ok(dataGenerator.generateDemoData()).build(); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /java/project-job-scheduling/src/main/java/org/acme/projectjobschedule/rest/exception/ErrorInfo.java: -------------------------------------------------------------------------------- 1 | package org.acme.projectjobschedule.rest.exception; 2 | 3 | public record ErrorInfo(String jobId, String message) { 4 | } 5 | -------------------------------------------------------------------------------- /java/project-job-scheduling/src/main/java/org/acme/projectjobschedule/rest/exception/ScheduleSolverException.java: -------------------------------------------------------------------------------- 1 | package org.acme.projectjobschedule.rest.exception; 2 | 3 | import jakarta.ws.rs.core.Response; 4 | 5 | public class ScheduleSolverException extends RuntimeException { 6 | 7 | private final String jobId; 8 | 9 | private final Response.Status status; 10 | 11 | public ScheduleSolverException(String jobId, Response.Status status, String message) { 12 | super(message); 13 | this.jobId = jobId; 14 | this.status = status; 15 | } 16 | 17 | public ScheduleSolverException(String jobId, Throwable cause) { 18 | super(cause.getMessage(), cause); 19 | this.jobId = jobId; 20 | this.status = Response.Status.INTERNAL_SERVER_ERROR; 21 | } 22 | 23 | public String getJobId() { 24 | return jobId; 25 | } 26 | 27 | public Response.Status getStatus() { 28 | return status; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /java/project-job-scheduling/src/main/java/org/acme/projectjobschedule/rest/exception/ScheduleSolverExceptionMapper.java: -------------------------------------------------------------------------------- 1 | package org.acme.projectjobschedule.rest.exception; 2 | 3 | import jakarta.ws.rs.core.MediaType; 4 | import jakarta.ws.rs.core.Response; 5 | import jakarta.ws.rs.ext.ExceptionMapper; 6 | import jakarta.ws.rs.ext.Provider; 7 | 8 | @Provider 9 | public class ScheduleSolverExceptionMapper implements ExceptionMapper { 10 | 11 | @Override 12 | public Response toResponse(ScheduleSolverException exception) { 13 | return Response 14 | .status(exception.getStatus()) 15 | .type(MediaType.APPLICATION_JSON) 16 | .entity(new ErrorInfo(exception.getJobId(), exception.getMessage())) 17 | .build(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /java/school-timetabling/school-timetabling-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimefoldAI/timefold-quickstarts/f3e83432b4a142110e6aad7e26dc9fd37937ec25/java/school-timetabling/school-timetabling-screenshot.png -------------------------------------------------------------------------------- /java/school-timetabling/src/main/java/org/acme/schooltimetabling/domain/Room.java: -------------------------------------------------------------------------------- 1 | package org.acme.schooltimetabling.domain; 2 | 3 | import ai.timefold.solver.core.api.domain.lookup.PlanningId; 4 | import com.fasterxml.jackson.annotation.JsonIdentityInfo; 5 | import com.fasterxml.jackson.annotation.ObjectIdGenerators; 6 | 7 | @JsonIdentityInfo(scope = Room.class, generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") 8 | public class Room { 9 | 10 | @PlanningId 11 | private String id; 12 | 13 | private String name; 14 | 15 | public Room() { 16 | } 17 | 18 | public Room(String id, String name) { 19 | this.id = id; 20 | this.name = name; 21 | } 22 | 23 | @Override 24 | public String toString() { 25 | return name; 26 | } 27 | 28 | // ************************************************************************ 29 | // Getters and setters 30 | // ************************************************************************ 31 | 32 | public String getId() { 33 | return id; 34 | } 35 | 36 | public String getName() { 37 | return name; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /java/school-timetabling/src/main/java/org/acme/schooltimetabling/domain/Timeslot.java: -------------------------------------------------------------------------------- 1 | package org.acme.schooltimetabling.domain; 2 | 3 | import java.time.DayOfWeek; 4 | import java.time.LocalTime; 5 | 6 | import ai.timefold.solver.core.api.domain.lookup.PlanningId; 7 | import com.fasterxml.jackson.annotation.JsonIdentityInfo; 8 | import com.fasterxml.jackson.annotation.ObjectIdGenerators; 9 | 10 | @JsonIdentityInfo(scope = Timeslot.class, generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") 11 | public class Timeslot { 12 | 13 | @PlanningId 14 | private String id; 15 | 16 | private DayOfWeek dayOfWeek; 17 | private LocalTime startTime; 18 | private LocalTime endTime; 19 | 20 | public Timeslot() { 21 | } 22 | 23 | public Timeslot(String id, DayOfWeek dayOfWeek, LocalTime startTime, LocalTime endTime) { 24 | this.id = id; 25 | this.dayOfWeek = dayOfWeek; 26 | this.startTime = startTime; 27 | this.endTime = endTime; 28 | } 29 | 30 | public Timeslot(String id, DayOfWeek dayOfWeek, LocalTime startTime) { 31 | this(id, dayOfWeek, startTime, startTime.plusMinutes(50)); 32 | } 33 | 34 | @Override 35 | public String toString() { 36 | return dayOfWeek + " " + startTime; 37 | } 38 | 39 | // ************************************************************************ 40 | // Getters and setters 41 | // ************************************************************************ 42 | 43 | public String getId() { 44 | return id; 45 | } 46 | 47 | public DayOfWeek getDayOfWeek() { 48 | return dayOfWeek; 49 | } 50 | 51 | public LocalTime getStartTime() { 52 | return startTime; 53 | } 54 | 55 | public LocalTime getEndTime() { 56 | return endTime; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /java/school-timetabling/src/main/java/org/acme/schooltimetabling/rest/exception/ErrorInfo.java: -------------------------------------------------------------------------------- 1 | package org.acme.schooltimetabling.rest.exception; 2 | 3 | public record ErrorInfo(String jobId, String message) { 4 | } 5 | -------------------------------------------------------------------------------- /java/school-timetabling/src/main/java/org/acme/schooltimetabling/rest/exception/TimetableSolverException.java: -------------------------------------------------------------------------------- 1 | package org.acme.schooltimetabling.rest.exception; 2 | 3 | import jakarta.ws.rs.core.Response; 4 | 5 | public class TimetableSolverException extends RuntimeException { 6 | 7 | private final String jobId; 8 | 9 | private final Response.Status status; 10 | 11 | public TimetableSolverException(String jobId, Response.Status status, String message) { 12 | super(message); 13 | this.jobId = jobId; 14 | this.status = status; 15 | } 16 | 17 | public TimetableSolverException(String jobId, Throwable cause) { 18 | super(cause.getMessage(), cause); 19 | this.jobId = jobId; 20 | this.status = Response.Status.INTERNAL_SERVER_ERROR; 21 | } 22 | 23 | public String getJobId() { 24 | return jobId; 25 | } 26 | 27 | public Response.Status getStatus() { 28 | return status; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /java/school-timetabling/src/main/java/org/acme/schooltimetabling/rest/exception/TimetableSolverExceptionMapper.java: -------------------------------------------------------------------------------- 1 | package org.acme.schooltimetabling.rest.exception; 2 | 3 | import jakarta.ws.rs.core.MediaType; 4 | import jakarta.ws.rs.core.Response; 5 | import jakarta.ws.rs.ext.ExceptionMapper; 6 | import jakarta.ws.rs.ext.Provider; 7 | 8 | @Provider 9 | public class TimetableSolverExceptionMapper implements ExceptionMapper { 10 | 11 | @Override 12 | public Response toResponse(TimetableSolverException exception) { 13 | return Response 14 | .status(exception.getStatus()) 15 | .type(MediaType.APPLICATION_JSON) 16 | .entity(new ErrorInfo(exception.getJobId(), exception.getMessage())) 17 | .build(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /java/sports-league-scheduling/sports-league-scheduling-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimefoldAI/timefold-quickstarts/f3e83432b4a142110e6aad7e26dc9fd37937ec25/java/sports-league-scheduling/sports-league-scheduling-screenshot.png -------------------------------------------------------------------------------- /java/sports-league-scheduling/src/main/java/org/acme/sportsleagueschedule/rest/SportsLeagueSchedulingDemoResource.java: -------------------------------------------------------------------------------- 1 | package org.acme.sportsleagueschedule.rest; 2 | 3 | import jakarta.inject.Inject; 4 | import jakarta.ws.rs.GET; 5 | import jakarta.ws.rs.Path; 6 | import jakarta.ws.rs.core.MediaType; 7 | import jakarta.ws.rs.core.Response; 8 | 9 | import org.acme.sportsleagueschedule.domain.LeagueSchedule; 10 | import org.eclipse.microprofile.openapi.annotations.Operation; 11 | import org.eclipse.microprofile.openapi.annotations.media.Content; 12 | import org.eclipse.microprofile.openapi.annotations.media.Schema; 13 | import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; 14 | import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; 15 | import org.eclipse.microprofile.openapi.annotations.tags.Tag; 16 | 17 | @Tag(name = "Demo data", description = "Timefold-provided demo sports league scheduling data.") 18 | @Path("demo-data") 19 | public class SportsLeagueSchedulingDemoResource { 20 | 21 | private final DemoDataGenerator dataGenerator; 22 | 23 | @Inject 24 | public SportsLeagueSchedulingDemoResource(DemoDataGenerator dataGenerator) { 25 | this.dataGenerator = dataGenerator; 26 | } 27 | 28 | @APIResponses(value = { 29 | @APIResponse(responseCode = "200", description = "Unsolved demo schedule.", 30 | content = @Content(mediaType = MediaType.APPLICATION_JSON, 31 | schema = @Schema(implementation = LeagueSchedule.class))) }) 32 | @Operation(summary = "Find an unsolved demo schedule by ID.") 33 | @GET 34 | public Response generate() { 35 | return Response.ok(dataGenerator.generateDemoData()).build(); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /java/sports-league-scheduling/src/main/java/org/acme/sportsleagueschedule/rest/exception/ErrorInfo.java: -------------------------------------------------------------------------------- 1 | package org.acme.sportsleagueschedule.rest.exception; 2 | 3 | public record ErrorInfo(String jobId, String message) { 4 | } 5 | -------------------------------------------------------------------------------- /java/sports-league-scheduling/src/main/java/org/acme/sportsleagueschedule/rest/exception/ScheduleSolverException.java: -------------------------------------------------------------------------------- 1 | package org.acme.sportsleagueschedule.rest.exception; 2 | 3 | import jakarta.ws.rs.core.Response; 4 | 5 | public class ScheduleSolverException extends RuntimeException { 6 | 7 | private final String jobId; 8 | 9 | private final Response.Status status; 10 | 11 | public ScheduleSolverException(String jobId, Response.Status status, String message) { 12 | super(message); 13 | this.jobId = jobId; 14 | this.status = status; 15 | } 16 | 17 | public ScheduleSolverException(String jobId, Throwable cause) { 18 | super(cause.getMessage(), cause); 19 | this.jobId = jobId; 20 | this.status = Response.Status.INTERNAL_SERVER_ERROR; 21 | } 22 | 23 | public String getJobId() { 24 | return jobId; 25 | } 26 | 27 | public Response.Status getStatus() { 28 | return status; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /java/sports-league-scheduling/src/main/java/org/acme/sportsleagueschedule/rest/exception/ScheduleSolverExceptionMapper.java: -------------------------------------------------------------------------------- 1 | package org.acme.sportsleagueschedule.rest.exception; 2 | 3 | import jakarta.ws.rs.core.MediaType; 4 | import jakarta.ws.rs.core.Response; 5 | import jakarta.ws.rs.ext.ExceptionMapper; 6 | import jakarta.ws.rs.ext.Provider; 7 | 8 | @Provider 9 | public class ScheduleSolverExceptionMapper implements ExceptionMapper { 10 | 11 | @Override 12 | public Response toResponse(ScheduleSolverException exception) { 13 | return Response 14 | .status(exception.getStatus()) 15 | .type(MediaType.APPLICATION_JSON) 16 | .entity(new ErrorInfo(exception.getJobId(), exception.getMessage())) 17 | .build(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /java/spring-boot-integration/school-timetabling-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimefoldAI/timefold-quickstarts/f3e83432b4a142110e6aad7e26dc9fd37937ec25/java/spring-boot-integration/school-timetabling-screenshot.png -------------------------------------------------------------------------------- /java/spring-boot-integration/src/main/java/org/acme/schooltimetabling/TimetableSpringBootApp.java: -------------------------------------------------------------------------------- 1 | package org.acme.schooltimetabling; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class TimetableSpringBootApp { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(TimetableSpringBootApp.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /java/spring-boot-integration/src/main/java/org/acme/schooltimetabling/domain/Room.java: -------------------------------------------------------------------------------- 1 | package org.acme.schooltimetabling.domain; 2 | 3 | import ai.timefold.solver.core.api.domain.lookup.PlanningId; 4 | import com.fasterxml.jackson.annotation.JsonIdentityInfo; 5 | import com.fasterxml.jackson.annotation.ObjectIdGenerators; 6 | 7 | @JsonIdentityInfo(scope = Room.class, generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") 8 | public class Room { 9 | 10 | @PlanningId 11 | private String id; 12 | 13 | private String name; 14 | 15 | public Room() { 16 | } 17 | 18 | public Room(String id, String name) { 19 | this.id = id; 20 | this.name = name; 21 | } 22 | 23 | @Override 24 | public String toString() { 25 | return name; 26 | } 27 | 28 | // ************************************************************************ 29 | // Getters and setters 30 | // ************************************************************************ 31 | 32 | public String getId() { 33 | return id; 34 | } 35 | 36 | public String getName() { 37 | return name; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /java/spring-boot-integration/src/main/java/org/acme/schooltimetabling/rest/exception/ErrorInfo.java: -------------------------------------------------------------------------------- 1 | package org.acme.schooltimetabling.rest.exception; 2 | 3 | public record ErrorInfo(String jobId, String message) { 4 | } 5 | -------------------------------------------------------------------------------- /java/spring-boot-integration/src/main/java/org/acme/schooltimetabling/rest/exception/TimetableSolverException.java: -------------------------------------------------------------------------------- 1 | package org.acme.schooltimetabling.rest.exception; 2 | 3 | import org.springframework.http.HttpStatus; 4 | 5 | public class TimetableSolverException extends RuntimeException { 6 | 7 | private final String jobId; 8 | 9 | private final HttpStatus status; 10 | 11 | public TimetableSolverException(String jobId, HttpStatus status, String message) { 12 | super(message); 13 | this.jobId = jobId; 14 | this.status = status; 15 | } 16 | 17 | public TimetableSolverException(String jobId, Throwable cause) { 18 | super(cause.getMessage(), cause); 19 | this.jobId = jobId; 20 | this.status = HttpStatus.INTERNAL_SERVER_ERROR; 21 | } 22 | 23 | public String getJobId() { 24 | return jobId; 25 | } 26 | 27 | public HttpStatus getStatus() { 28 | return status; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /java/spring-boot-integration/src/main/java/org/acme/schooltimetabling/rest/exception/TimetableSolverExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package org.acme.schooltimetabling.rest.exception; 2 | 3 | import org.springframework.http.ResponseEntity; 4 | import org.springframework.web.bind.annotation.ControllerAdvice; 5 | import org.springframework.web.bind.annotation.ExceptionHandler; 6 | 7 | @ControllerAdvice 8 | public class TimetableSolverExceptionHandler { 9 | 10 | @ExceptionHandler({TimetableSolverException.class}) 11 | public ResponseEntity handleTimetableSolverException(TimetableSolverException exception) { 12 | return new ResponseEntity<>(new ErrorInfo(exception.getJobId(), exception.getMessage()), exception.getStatus()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /java/spring-boot-integration/src/main/java/org/acme/schooltimetabling/solver/justifications/RoomConflictJustification.java: -------------------------------------------------------------------------------- 1 | package org.acme.schooltimetabling.solver.justifications; 2 | 3 | import ai.timefold.solver.core.api.score.stream.ConstraintJustification; 4 | import org.acme.schooltimetabling.domain.Lesson; 5 | import org.acme.schooltimetabling.domain.Room; 6 | 7 | public record RoomConflictJustification(Room room, Lesson lesson1, Lesson lesson2, String description) 8 | implements 9 | ConstraintJustification { 10 | 11 | public RoomConflictJustification(Room room, Lesson lesson1, Lesson lesson2) { 12 | this(room, lesson1, lesson2, 13 | "Room '%s' is used for lesson '%s' for student group '%s' and lesson '%s' for student group '%s' at '%s %s'" 14 | .formatted(room, lesson1.getSubject(), lesson1.getStudentGroup(), lesson2.getSubject(), 15 | lesson2.getStudentGroup(), lesson1.getTimeslot().getDayOfWeek(), 16 | lesson1.getTimeslot().getStartTime())); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /java/spring-boot-integration/src/main/java/org/acme/schooltimetabling/solver/justifications/StudentGroupConflictJustification.java: -------------------------------------------------------------------------------- 1 | package org.acme.schooltimetabling.solver.justifications; 2 | 3 | import ai.timefold.solver.core.api.score.stream.ConstraintJustification; 4 | import org.acme.schooltimetabling.domain.Lesson; 5 | 6 | public record StudentGroupConflictJustification(String studentGroup, Lesson lesson1, Lesson lesson2, String description) 7 | implements 8 | ConstraintJustification { 9 | public StudentGroupConflictJustification(String studentGroup, Lesson lesson1, Lesson lesson2) { 10 | this(studentGroup, lesson1, lesson2, 11 | "Student group '%s' has lesson '%s' and lesson '%s' at '%s %s'" 12 | .formatted(studentGroup, lesson1.getSubject(), lesson2.getSubject(), 13 | lesson1.getTimeslot().getDayOfWeek(), lesson1.getTimeslot().getStartTime())); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /java/spring-boot-integration/src/main/java/org/acme/schooltimetabling/solver/justifications/StudentGroupSubjectVarietyJustification.java: -------------------------------------------------------------------------------- 1 | package org.acme.schooltimetabling.solver.justifications; 2 | 3 | import ai.timefold.solver.core.api.score.stream.ConstraintJustification; 4 | import org.acme.schooltimetabling.domain.Lesson; 5 | 6 | public record StudentGroupSubjectVarietyJustification(String studentGroup, Lesson lesson1, Lesson lesson2, String description) 7 | implements 8 | ConstraintJustification { 9 | 10 | public StudentGroupSubjectVarietyJustification(String studentGroup, Lesson lesson1, Lesson lesson2) { 11 | this(studentGroup, lesson1, lesson2, 12 | "Student Group '%s' has two consecutive lessons on '%s' at '%s %s' and at '%s %s'" 13 | .formatted(studentGroup, lesson1.getSubject(), lesson1.getTimeslot().getDayOfWeek(), 14 | lesson1.getTimeslot().getStartTime(), lesson2.getTimeslot().getDayOfWeek(), 15 | lesson2.getTimeslot().getStartTime())); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /java/spring-boot-integration/src/main/java/org/acme/schooltimetabling/solver/justifications/TeacherConflictJustification.java: -------------------------------------------------------------------------------- 1 | package org.acme.schooltimetabling.solver.justifications; 2 | 3 | import ai.timefold.solver.core.api.score.stream.ConstraintJustification; 4 | import org.acme.schooltimetabling.domain.Lesson; 5 | 6 | public record TeacherConflictJustification(String teacher, Lesson lesson1, Lesson lesson2, String description) 7 | implements 8 | ConstraintJustification { 9 | 10 | public TeacherConflictJustification(String teacher, Lesson lesson1, Lesson lesson2) { 11 | this(teacher, lesson1, lesson2, 12 | "Teacher '%s' needs to teach lesson '%s' for student group '%s' and lesson '%s' for student group '%s' at '%s %s'" 13 | .formatted(teacher, lesson1.getSubject(), lesson1.getStudentGroup(), lesson2.getSubject(), 14 | lesson2.getStudentGroup(), lesson1.getTimeslot().getDayOfWeek(), 15 | lesson1.getTimeslot().getStartTime())); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /java/spring-boot-integration/src/main/java/org/acme/schooltimetabling/solver/justifications/TeacherRoomStabilityJustification.java: -------------------------------------------------------------------------------- 1 | package org.acme.schooltimetabling.solver.justifications; 2 | 3 | import ai.timefold.solver.core.api.score.stream.ConstraintJustification; 4 | import org.acme.schooltimetabling.domain.Lesson; 5 | 6 | public record TeacherRoomStabilityJustification(String teacher, Lesson lesson1, Lesson lesson2, String description) 7 | implements 8 | ConstraintJustification { 9 | 10 | public TeacherRoomStabilityJustification(String teacher, Lesson lesson1, Lesson lesson2) { 11 | this(teacher, lesson1, lesson2, 12 | "Teacher '%s' has two lessons in different rooms: room '%s' at '%s %s' and room '%s' at '%s %s'" 13 | .formatted(teacher, lesson1.getRoom(), lesson1.getTimeslot().getDayOfWeek(), 14 | lesson1.getTimeslot().getStartTime(), lesson2.getRoom(), lesson2.getTimeslot().getDayOfWeek(), 15 | lesson2.getTimeslot().getStartTime())); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /java/spring-boot-integration/src/main/java/org/acme/schooltimetabling/solver/justifications/TeacherTimeEfficiencyJustification.java: -------------------------------------------------------------------------------- 1 | package org.acme.schooltimetabling.solver.justifications; 2 | 3 | import ai.timefold.solver.core.api.score.stream.ConstraintJustification; 4 | import org.acme.schooltimetabling.domain.Lesson; 5 | 6 | public record TeacherTimeEfficiencyJustification(String teacher, Lesson lesson1, Lesson lesson2, String description) 7 | implements 8 | ConstraintJustification { 9 | 10 | public TeacherTimeEfficiencyJustification(String teacher, Lesson lesson1, Lesson lesson2) { 11 | this(teacher, lesson1, lesson2, 12 | "Teacher '%s' has 2 consecutive lessons: lesson '%s' for student group '%s' at '%s %s' and lesson '%s' for student group '%s' at '%s %s' (gap)" 13 | .formatted(teacher, lesson1.getSubject(), lesson1.getStudentGroup(), 14 | lesson1.getTimeslot().getDayOfWeek(), lesson1.getTimeslot().getStartTime(), 15 | lesson2.getSubject(), lesson2.getStudentGroup(), lesson2.getTimeslot().getDayOfWeek(), 16 | lesson2.getTimeslot().getStartTime())); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /java/spring-boot-integration/src/main/resources/application-enterprise.properties: -------------------------------------------------------------------------------- 1 | ######################## 2 | # Timefold Solver Enterprise properties 3 | ######################## 4 | 5 | # To run increase CPU cores usage per solver 6 | timefold.solver.move-thread-count=AUTO 7 | -------------------------------------------------------------------------------- /java/spring-boot-integration/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | ######################## 2 | # Timefold Solver properties 3 | ######################## 4 | 5 | # The solver runs for 30 seconds. To run for 5 minutes use "5m" and for 2 hours use "2h". 6 | timefold.solver.termination.spent-limit=30s 7 | 8 | # When benchmarking, each individual solver runs for 15 seconds. To run for 5 minutes use "5m" and for 2 hours use "2h". 9 | timefold.benchmark.solver.termination.spent-limit=15s 10 | 11 | # To change how many solvers to run in parallel 12 | # timefold.solver-manager.parallel-solver-count=4 13 | 14 | # Temporary comment this out to detect bugs in your code (lowers performance) 15 | # timefold.solver.environment-mode=FULL_ASSERT 16 | 17 | # Temporary comment this out to return a feasible solution as soon as possible 18 | # quarkus.timefold.solver.termination.best-score-limit=0hard/*soft 19 | 20 | # To see what Timefold is doing, turn on DEBUG or TRACE logging. 21 | logging.level.ai.timefold.solver=INFO 22 | 23 | # XML file for power tweaking, defaults to solverConfig.xml (directly under src/main/resources) 24 | # timefold.solver-config-xml=org/.../timeTableSolverConfig.xml 25 | 26 | ######################## 27 | # Spring Boot properties 28 | ######################## 29 | 30 | # Make it easier to read Timefold logging 31 | logging.pattern.console=%d{HH:mm:ss.SSS} %clr(${LOG_LEVEL_PATTERN:%5p}) %blue([%-15.15t]) %m%n 32 | -------------------------------------------------------------------------------- /java/spring-boot-integration/src/test/java/org/acme/schooltimetabling/config/ConstraintConfig.java: -------------------------------------------------------------------------------- 1 | package org.acme.schooltimetabling.config; 2 | 3 | import ai.timefold.solver.test.api.score.stream.ConstraintVerifier; 4 | import org.acme.schooltimetabling.domain.Lesson; 5 | import org.acme.schooltimetabling.domain.Timetable; 6 | import org.acme.schooltimetabling.solver.TimetableConstraintProvider; 7 | import org.springframework.boot.test.context.TestConfiguration; 8 | import org.springframework.context.annotation.Bean; 9 | 10 | @TestConfiguration 11 | public class ConstraintConfig { 12 | 13 | @Bean 14 | public ConstraintVerifier buildConstraintVerifier() { 15 | return ConstraintVerifier.build(new TimetableConstraintProvider(), Timetable.class, Lesson.class); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /java/task-assigning/src/main/java/org/acme/taskassigning/domain/Affinity.java: -------------------------------------------------------------------------------- 1 | package org.acme.taskassigning.domain; 2 | 3 | public enum Affinity { 4 | NONE(4), 5 | LOW(3), 6 | MEDIUM(2), 7 | HIGH(1); 8 | 9 | private final int durationMultiplier; 10 | 11 | Affinity(int durationMultiplier) { 12 | this.durationMultiplier = durationMultiplier; 13 | } 14 | 15 | public int getDurationMultiplier() { 16 | return durationMultiplier; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /java/task-assigning/src/main/java/org/acme/taskassigning/domain/Customer.java: -------------------------------------------------------------------------------- 1 | package org.acme.taskassigning.domain; 2 | 3 | import java.util.Objects; 4 | 5 | import com.fasterxml.jackson.annotation.JsonIdentityInfo; 6 | import com.fasterxml.jackson.annotation.ObjectIdGenerators; 7 | 8 | @JsonIdentityInfo(scope = Customer.class, generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") 9 | public class Customer { 10 | 11 | private String id; 12 | private String name; 13 | 14 | public Customer() { 15 | } 16 | 17 | public Customer(String id) { 18 | this.id = id; 19 | } 20 | 21 | public Customer(String id, String name) { 22 | this(id); 23 | this.name = name; 24 | } 25 | 26 | public String getId() { 27 | return id; 28 | } 29 | 30 | public void setId(String id) { 31 | this.id = id; 32 | } 33 | 34 | public String getName() { 35 | return name; 36 | } 37 | 38 | public void setName(String name) { 39 | this.name = name; 40 | } 41 | 42 | @Override 43 | public boolean equals(Object o) { 44 | if (this == o) 45 | return true; 46 | if (!(o instanceof Customer customer)) 47 | return false; 48 | return Objects.equals(getId(), customer.getId()); 49 | } 50 | 51 | @Override 52 | public int hashCode() { 53 | return getId().hashCode(); 54 | } 55 | 56 | @Override 57 | public String toString() { 58 | return getId(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /java/task-assigning/src/main/java/org/acme/taskassigning/domain/Priority.java: -------------------------------------------------------------------------------- 1 | package org.acme.taskassigning.domain; 2 | 3 | public enum Priority { 4 | MINOR, 5 | MAJOR, 6 | CRITICAL; 7 | } 8 | -------------------------------------------------------------------------------- /java/task-assigning/src/main/java/org/acme/taskassigning/rest/TaskAssigningDemoResource.java: -------------------------------------------------------------------------------- 1 | package org.acme.taskassigning.rest; 2 | 3 | import jakarta.inject.Inject; 4 | import jakarta.ws.rs.GET; 5 | import jakarta.ws.rs.Path; 6 | import jakarta.ws.rs.core.MediaType; 7 | import jakarta.ws.rs.core.Response; 8 | 9 | import org.acme.taskassigning.domain.TaskAssigningSolution; 10 | import org.eclipse.microprofile.openapi.annotations.Operation; 11 | import org.eclipse.microprofile.openapi.annotations.media.Content; 12 | import org.eclipse.microprofile.openapi.annotations.media.Schema; 13 | import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; 14 | import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; 15 | import org.eclipse.microprofile.openapi.annotations.tags.Tag; 16 | 17 | @Tag(name = "Demo data", description = "Timefold-provided demo task assigning data.") 18 | @Path("demo-data") 19 | public class TaskAssigningDemoResource { 20 | 21 | private final DemoDataGenerator dataGenerator; 22 | 23 | @Inject 24 | public TaskAssigningDemoResource(DemoDataGenerator dataGenerator) { 25 | this.dataGenerator = dataGenerator; 26 | } 27 | 28 | @APIResponses(value = { 29 | @APIResponse(responseCode = "200", description = "Unsolved demo schedule.", 30 | content = @Content(mediaType = MediaType.APPLICATION_JSON, 31 | schema = @Schema(implementation = TaskAssigningSolution.class))) }) 32 | @Operation(summary = "Find an unsolved demo schedule by ID.") 33 | @GET 34 | public Response generate() { 35 | return Response.ok(dataGenerator.generateDemoData()).build(); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /java/task-assigning/src/main/java/org/acme/taskassigning/rest/exception/ErrorInfo.java: -------------------------------------------------------------------------------- 1 | package org.acme.taskassigning.rest.exception; 2 | 3 | public record ErrorInfo(String jobId, String message) { 4 | } 5 | -------------------------------------------------------------------------------- /java/task-assigning/src/main/java/org/acme/taskassigning/rest/exception/ScheduleSolverException.java: -------------------------------------------------------------------------------- 1 | package org.acme.taskassigning.rest.exception; 2 | 3 | import jakarta.ws.rs.core.Response; 4 | 5 | public class ScheduleSolverException extends RuntimeException { 6 | 7 | private final String jobId; 8 | 9 | private final Response.Status status; 10 | 11 | public ScheduleSolverException(String jobId, Response.Status status, String message) { 12 | super(message); 13 | this.jobId = jobId; 14 | this.status = status; 15 | } 16 | 17 | public ScheduleSolverException(String jobId, Throwable cause) { 18 | super(cause.getMessage(), cause); 19 | this.jobId = jobId; 20 | this.status = Response.Status.INTERNAL_SERVER_ERROR; 21 | } 22 | 23 | public String getJobId() { 24 | return jobId; 25 | } 26 | 27 | public Response.Status getStatus() { 28 | return status; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /java/task-assigning/src/main/java/org/acme/taskassigning/rest/exception/ScheduleSolverExceptionMapper.java: -------------------------------------------------------------------------------- 1 | package org.acme.taskassigning.rest.exception; 2 | 3 | import jakarta.ws.rs.core.MediaType; 4 | import jakarta.ws.rs.core.Response; 5 | import jakarta.ws.rs.ext.ExceptionMapper; 6 | import jakarta.ws.rs.ext.Provider; 7 | 8 | @Provider 9 | public class ScheduleSolverExceptionMapper implements ExceptionMapper { 10 | 11 | @Override 12 | public Response toResponse(ScheduleSolverException exception) { 13 | return Response 14 | .status(exception.getStatus()) 15 | .type(MediaType.APPLICATION_JSON) 16 | .entity(new ErrorInfo(exception.getJobId(), exception.getMessage())) 17 | .build(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /java/task-assigning/task-assigning-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimefoldAI/timefold-quickstarts/f3e83432b4a142110e6aad7e26dc9fd37937ec25/java/task-assigning/task-assigning-screenshot.png -------------------------------------------------------------------------------- /java/tournament-scheduling/src/main/java/org/acme/tournamentschedule/domain/Day.java: -------------------------------------------------------------------------------- 1 | package org.acme.tournamentschedule.domain; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIdentityInfo; 4 | import com.fasterxml.jackson.annotation.ObjectIdGenerators; 5 | 6 | @JsonIdentityInfo(scope = Day.class, generator = ObjectIdGenerators.PropertyGenerator.class, property = "dateIndex") 7 | public class Day { 8 | 9 | private int dateIndex; 10 | 11 | public Day() { 12 | } 13 | 14 | public Day(int dateIndex) { 15 | this.dateIndex = dateIndex; 16 | } 17 | 18 | public int getDateIndex() { 19 | return dateIndex; 20 | } 21 | 22 | public void setDateIndex(int dateIndex) { 23 | this.dateIndex = dateIndex; 24 | } 25 | 26 | @Override 27 | public boolean equals(Object o) { 28 | if (this == o) 29 | return true; 30 | if (!(o instanceof Day day)) 31 | return false; 32 | return getDateIndex() == day.getDateIndex(); 33 | } 34 | 35 | @Override 36 | public int hashCode() { 37 | return 31 * dateIndex; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /java/tournament-scheduling/src/main/java/org/acme/tournamentschedule/domain/Team.java: -------------------------------------------------------------------------------- 1 | package org.acme.tournamentschedule.domain; 2 | 3 | import java.util.Objects; 4 | 5 | import ai.timefold.solver.core.api.domain.lookup.PlanningId; 6 | 7 | import com.fasterxml.jackson.annotation.JsonIdentityInfo; 8 | import com.fasterxml.jackson.annotation.ObjectIdGenerators; 9 | 10 | @JsonIdentityInfo(scope = Team.class, generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") 11 | public class Team { 12 | 13 | @PlanningId 14 | private long id; 15 | private String name; 16 | 17 | public Team() { 18 | } 19 | 20 | public Team(long id) { 21 | this.id = id; 22 | } 23 | 24 | public Team(long id, String name) { 25 | this(id); 26 | this.name = name; 27 | } 28 | 29 | public long getId() { 30 | return id; 31 | } 32 | 33 | public void setId(long id) { 34 | this.id = id; 35 | } 36 | 37 | public String getName() { 38 | return name; 39 | } 40 | 41 | public void setName(String name) { 42 | this.name = name; 43 | } 44 | 45 | @Override 46 | public String toString() { 47 | return name == null ? super.toString() : name; 48 | } 49 | 50 | @Override 51 | public boolean equals(Object o) { 52 | if (this == o) 53 | return true; 54 | if (!(o instanceof Team team)) 55 | return false; 56 | return Objects.equals(getId(), team.getId()); 57 | } 58 | 59 | @Override 60 | public int hashCode() { 61 | return (int) (31 * getId()); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /java/tournament-scheduling/src/main/java/org/acme/tournamentschedule/domain/UnavailabilityPenalty.java: -------------------------------------------------------------------------------- 1 | package org.acme.tournamentschedule.domain; 2 | 3 | public class UnavailabilityPenalty { 4 | 5 | private Team team; 6 | private Day day; 7 | 8 | public UnavailabilityPenalty() { 9 | } 10 | 11 | public UnavailabilityPenalty(Team team, Day day) { 12 | this.team = team; 13 | this.day = day; 14 | } 15 | 16 | public Team getTeam() { 17 | return team; 18 | } 19 | 20 | public void setTeam(Team team) { 21 | this.team = team; 22 | } 23 | 24 | public Day getDay() { 25 | return day; 26 | } 27 | 28 | public void setDay(Day day) { 29 | this.day = day; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /java/tournament-scheduling/src/main/java/org/acme/tournamentschedule/rest/TournamentSchedulingDemoResource.java: -------------------------------------------------------------------------------- 1 | package org.acme.tournamentschedule.rest; 2 | 3 | import jakarta.inject.Inject; 4 | import jakarta.ws.rs.GET; 5 | import jakarta.ws.rs.Path; 6 | import jakarta.ws.rs.core.MediaType; 7 | import jakarta.ws.rs.core.Response; 8 | 9 | import org.acme.tournamentschedule.domain.TournamentSchedule; 10 | import org.eclipse.microprofile.openapi.annotations.Operation; 11 | import org.eclipse.microprofile.openapi.annotations.media.Content; 12 | import org.eclipse.microprofile.openapi.annotations.media.Schema; 13 | import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; 14 | import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; 15 | import org.eclipse.microprofile.openapi.annotations.tags.Tag; 16 | 17 | @Tag(name = "Demo data", description = "Timefold-provided demo tournament scheduling data.") 18 | @Path("demo-data") 19 | public class TournamentSchedulingDemoResource { 20 | 21 | private final DemoDataGenerator dataGenerator; 22 | 23 | @Inject 24 | public TournamentSchedulingDemoResource(DemoDataGenerator dataGenerator) { 25 | this.dataGenerator = dataGenerator; 26 | } 27 | 28 | @APIResponses(value = { 29 | @APIResponse(responseCode = "200", description = "Unsolved demo schedule.", 30 | content = @Content(mediaType = MediaType.APPLICATION_JSON, 31 | schema = @Schema(implementation = TournamentSchedule.class))) }) 32 | @Operation(summary = "Find an unsolved demo schedule by ID.") 33 | @GET 34 | public Response generate() { 35 | return Response.ok(dataGenerator.generateDemoData()).build(); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /java/tournament-scheduling/src/main/java/org/acme/tournamentschedule/rest/exception/ErrorInfo.java: -------------------------------------------------------------------------------- 1 | package org.acme.tournamentschedule.rest.exception; 2 | 3 | public record ErrorInfo(String jobId, String message) { 4 | } 5 | -------------------------------------------------------------------------------- /java/tournament-scheduling/src/main/java/org/acme/tournamentschedule/rest/exception/ScheduleSolverException.java: -------------------------------------------------------------------------------- 1 | package org.acme.tournamentschedule.rest.exception; 2 | 3 | import jakarta.ws.rs.core.Response; 4 | 5 | public class ScheduleSolverException extends RuntimeException { 6 | 7 | private final String jobId; 8 | 9 | private final Response.Status status; 10 | 11 | public ScheduleSolverException(String jobId, Response.Status status, String message) { 12 | super(message); 13 | this.jobId = jobId; 14 | this.status = status; 15 | } 16 | 17 | public ScheduleSolverException(String jobId, Throwable cause) { 18 | super(cause.getMessage(), cause); 19 | this.jobId = jobId; 20 | this.status = Response.Status.INTERNAL_SERVER_ERROR; 21 | } 22 | 23 | public String getJobId() { 24 | return jobId; 25 | } 26 | 27 | public Response.Status getStatus() { 28 | return status; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /java/tournament-scheduling/src/main/java/org/acme/tournamentschedule/rest/exception/ScheduleSolverExceptionMapper.java: -------------------------------------------------------------------------------- 1 | package org.acme.tournamentschedule.rest.exception; 2 | 3 | import jakarta.ws.rs.core.MediaType; 4 | import jakarta.ws.rs.core.Response; 5 | import jakarta.ws.rs.ext.ExceptionMapper; 6 | import jakarta.ws.rs.ext.Provider; 7 | 8 | @Provider 9 | public class ScheduleSolverExceptionMapper implements ExceptionMapper { 10 | 11 | @Override 12 | public Response toResponse(ScheduleSolverException exception) { 13 | return Response 14 | .status(exception.getStatus()) 15 | .type(MediaType.APPLICATION_JSON) 16 | .entity(new ErrorInfo(exception.getJobId(), exception.getMessage())) 17 | .build(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /java/tournament-scheduling/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | ######################## 2 | # Timefold Solver properties 3 | ######################## 4 | 5 | # The solver runs for 30 seconds. To run for 5 minutes use "5m" and for 2 hours use "2h". 6 | quarkus.timefold.solver.termination.spent-limit=30s 7 | 8 | # To change how many solvers to run in parallel 9 | # timefold.solver-manager.parallel-solver-count=4 10 | 11 | # Temporary comment this out to detect bugs in your code (lowers performance) 12 | # quarkus.timefold.solver.environment-mode=FULL_ASSERT 13 | 14 | # Temporary comment this out to return a feasible solution as soon as possible 15 | # quarkus.timefold.solver.termination.best-score-limit=0hard/0medium/*soft 16 | 17 | # To see what Timefold is doing, turn on DEBUG or TRACE logging. 18 | quarkus.log.category."ai.timefold.solver".level=INFO 19 | %test.quarkus.log.category."ai.timefold.solver".level=INFO 20 | %prod.quarkus.log.category."ai.timefold.solver".level=INFO 21 | # XML file for power tweaking, defaults to solverConfig.xml (directly under src/main/resources) 22 | # quarkus.timefold.solver-config-xml=org/.../tournamentScheduleSolverConfig.xml 23 | 24 | ######################## 25 | # Timefold Solver Enterprise properties 26 | ######################## 27 | 28 | # To run increase CPU cores usage per solver 29 | %enterprise.quarkus.timefold.solver.move-thread-count=AUTO 30 | 31 | ######################## 32 | # Native build properties 33 | ######################## 34 | 35 | # Enable Swagger UI also in the native mode 36 | quarkus.swagger-ui.always-include=true 37 | 38 | ######################## 39 | # Test overrides 40 | ######################## 41 | 42 | %test.quarkus.timefold.solver.termination.spent-limit=10s 43 | -------------------------------------------------------------------------------- /java/tournament-scheduling/tournament-scheduling-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimefoldAI/timefold-quickstarts/f3e83432b4a142110e6aad7e26dc9fd37937ec25/java/tournament-scheduling/tournament-scheduling-screenshot.png -------------------------------------------------------------------------------- /java/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/LocationAware.java: -------------------------------------------------------------------------------- 1 | package org.acme.vehiclerouting.domain; 2 | 3 | public interface LocationAware { 4 | 5 | Location getLocation(); 6 | } 7 | -------------------------------------------------------------------------------- /java/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/LocationDistanceMeter.java: -------------------------------------------------------------------------------- 1 | package org.acme.vehiclerouting.domain; 2 | 3 | import ai.timefold.solver.core.impl.heuristic.selector.common.nearby.NearbyDistanceMeter; 4 | 5 | public class LocationDistanceMeter implements NearbyDistanceMeter { 6 | 7 | @Override 8 | public double getNearbyDistance(Visit origin, LocationAware destination) { 9 | return origin.getLocation().getDrivingTimeTo(destination.getLocation()); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /java/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/dto/ApplyRecommendationRequest.java: -------------------------------------------------------------------------------- 1 | package org.acme.vehiclerouting.domain.dto; 2 | 3 | import org.acme.vehiclerouting.domain.VehicleRoutePlan; 4 | 5 | public record ApplyRecommendationRequest(VehicleRoutePlan solution, String visitId, String vehicleId, int index) { 6 | } 7 | -------------------------------------------------------------------------------- /java/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/dto/RecommendationRequest.java: -------------------------------------------------------------------------------- 1 | package org.acme.vehiclerouting.domain.dto; 2 | 3 | import org.acme.vehiclerouting.domain.VehicleRoutePlan; 4 | 5 | public record RecommendationRequest(VehicleRoutePlan solution, String visitId) { 6 | } 7 | -------------------------------------------------------------------------------- /java/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/dto/VehicleRecommendation.java: -------------------------------------------------------------------------------- 1 | package org.acme.vehiclerouting.domain.dto; 2 | 3 | public record VehicleRecommendation(String vehicleId, int index) { 4 | } 5 | -------------------------------------------------------------------------------- /java/vehicle-routing/src/main/java/org/acme/vehiclerouting/rest/exception/ErrorInfo.java: -------------------------------------------------------------------------------- 1 | package org.acme.vehiclerouting.rest.exception; 2 | 3 | public record ErrorInfo(String jobId, String message) { 4 | } 5 | -------------------------------------------------------------------------------- /java/vehicle-routing/src/main/java/org/acme/vehiclerouting/rest/exception/VehicleRoutingSolverException.java: -------------------------------------------------------------------------------- 1 | package org.acme.vehiclerouting.rest.exception; 2 | 3 | import jakarta.ws.rs.core.Response; 4 | 5 | public class VehicleRoutingSolverException extends RuntimeException { 6 | 7 | private final String jobId; 8 | 9 | private final Response.Status status; 10 | 11 | public VehicleRoutingSolverException(String jobId, Response.Status status, String message) { 12 | super(message); 13 | this.jobId = jobId; 14 | this.status = status; 15 | } 16 | 17 | public VehicleRoutingSolverException(String jobId, Throwable cause) { 18 | super(cause.getMessage(), cause); 19 | this.jobId = jobId; 20 | this.status = Response.Status.INTERNAL_SERVER_ERROR; 21 | } 22 | 23 | public String getJobId() { 24 | return jobId; 25 | } 26 | 27 | public Response.Status getStatus() { 28 | return status; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /java/vehicle-routing/src/main/java/org/acme/vehiclerouting/rest/exception/VehicleRoutingSolverExceptionMapper.java: -------------------------------------------------------------------------------- 1 | package org.acme.vehiclerouting.rest.exception; 2 | 3 | import jakarta.ws.rs.core.MediaType; 4 | import jakarta.ws.rs.core.Response; 5 | import jakarta.ws.rs.ext.ExceptionMapper; 6 | import jakarta.ws.rs.ext.Provider; 7 | 8 | @Provider 9 | public class VehicleRoutingSolverExceptionMapper implements ExceptionMapper { 10 | 11 | @Override 12 | public Response toResponse(VehicleRoutingSolverException exception) { 13 | return Response 14 | .status(exception.getStatus()) 15 | .type(MediaType.APPLICATION_JSON) 16 | .entity(new ErrorInfo(exception.getJobId(), exception.getMessage())) 17 | .build(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /java/vehicle-routing/src/test/java/org/acme/vehiclerouting/domain/geo/HaversineDrivingTimeCalculatorTest.java: -------------------------------------------------------------------------------- 1 | package org.acme.vehiclerouting.domain.geo; 2 | 3 | import org.acme.vehiclerouting.domain.Location; 4 | import org.assertj.core.api.Assertions; 5 | import org.junit.jupiter.api.Test; 6 | 7 | class HaversineDrivingTimeCalculatorTest { 8 | 9 | private final DrivingTimeCalculator drivingTimeCalculator = HaversineDrivingTimeCalculator.getInstance(); 10 | 11 | // Results have been verified with the help of https://latlongdata.com/. 12 | @Test 13 | void calculateDrivingTime() { 14 | Location Gent = new Location(51.0441461, 3.7336349); 15 | Location Brno = new Location(49.1913945, 16.6122723); 16 | Assertions.assertThat(drivingTimeCalculator.calculateDrivingTime(Gent, Brno)) 17 | .isEqualTo(HaversineDrivingTimeCalculator.metersToDrivingSeconds(939748)); 18 | 19 | // Close to the North Pole. 20 | Location Svolvaer = new Location(68.2359953, 14.5644379); 21 | Location Lulea = new Location(65.5887708, 22.1518707); 22 | Assertions.assertThat(drivingTimeCalculator.calculateDrivingTime(Svolvaer, Lulea)) 23 | .isEqualTo(HaversineDrivingTimeCalculator.metersToDrivingSeconds(442297)); 24 | } 25 | } -------------------------------------------------------------------------------- /java/vehicle-routing/src/test/java/org/acme/vehiclerouting/domain/jackson/VRPScoreAnalysisJacksonDeserializer.java: -------------------------------------------------------------------------------- 1 | package org.acme.vehiclerouting.domain.jackson; 2 | 3 | import ai.timefold.solver.core.api.score.buildin.hardsoftlong.HardSoftLongScore; 4 | import ai.timefold.solver.core.api.score.constraint.ConstraintRef; 5 | import ai.timefold.solver.core.api.score.stream.ConstraintJustification; 6 | import ai.timefold.solver.core.api.score.stream.DefaultConstraintJustification; 7 | import ai.timefold.solver.jackson.api.score.analysis.AbstractScoreAnalysisJacksonDeserializer; 8 | 9 | public class VRPScoreAnalysisJacksonDeserializer extends AbstractScoreAnalysisJacksonDeserializer { 10 | 11 | @Override 12 | protected HardSoftLongScore parseScore(String scoreString) { 13 | return HardSoftLongScore.parseScore(scoreString); 14 | } 15 | 16 | @Override 17 | @SuppressWarnings("unchecked") 18 | protected ConstraintJustification_ parseConstraintJustification( 19 | ConstraintRef constraintRef, String constraintJustificationString, HardSoftLongScore score) { 20 | return (ConstraintJustification_) DefaultConstraintJustification.of(score); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /java/vehicle-routing/src/test/java/org/acme/vehiclerouting/domain/jackson/VRPScoreAnalysisJacksonModule.java: -------------------------------------------------------------------------------- 1 | package org.acme.vehiclerouting.domain.jackson; 2 | 3 | import ai.timefold.solver.core.api.score.analysis.ScoreAnalysis; 4 | import com.fasterxml.jackson.databind.module.SimpleModule; 5 | 6 | public class VRPScoreAnalysisJacksonModule extends SimpleModule { 7 | public VRPScoreAnalysisJacksonModule() { 8 | super("VRP Custom Jackson Module for Score Analysis"); 9 | addDeserializer(ScoreAnalysis.class, new VRPScoreAnalysisJacksonDeserializer()); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /java/vehicle-routing/src/test/resources/META-INF/services/com.fasterxml.jackson.databind.Module: -------------------------------------------------------------------------------- 1 | org.acme.vehiclerouting.domain.jackson.VRPScoreAnalysisJacksonModule -------------------------------------------------------------------------------- /java/vehicle-routing/vehicle-routing-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimefoldAI/timefold-quickstarts/f3e83432b4a142110e6aad7e26dc9fd37937ec25/java/vehicle-routing/vehicle-routing-screenshot.png -------------------------------------------------------------------------------- /kotlin/school-timetabling/school-timetabling-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimefoldAI/timefold-quickstarts/f3e83432b4a142110e6aad7e26dc9fd37937ec25/kotlin/school-timetabling/school-timetabling-screenshot.png -------------------------------------------------------------------------------- /kotlin/school-timetabling/src/main/kotlin/org/acme/kotlin/schooltimetabling/domain/Lesson.kt: -------------------------------------------------------------------------------- 1 | package org.acme.kotlin.schooltimetabling.domain 2 | 3 | import ai.timefold.solver.core.api.domain.entity.PlanningEntity 4 | import ai.timefold.solver.core.api.domain.lookup.PlanningId 5 | import ai.timefold.solver.core.api.domain.variable.PlanningVariable 6 | import com.fasterxml.jackson.annotation.JsonIdentityReference 7 | 8 | 9 | @PlanningEntity 10 | data class Lesson ( 11 | @PlanningId 12 | val id: String, 13 | val subject: String, 14 | val teacher: String, 15 | val studentGroup: String) { 16 | 17 | @JsonIdentityReference 18 | @PlanningVariable 19 | var timeslot: Timeslot? = null 20 | 21 | @JsonIdentityReference 22 | @PlanningVariable 23 | var room: Room? = null 24 | 25 | constructor(id: String, subject: String, teacher: String, studentGroup: String, timeslot: Timeslot?, room: Room?) 26 | : this(id, subject, teacher, studentGroup) { 27 | this.timeslot = timeslot 28 | this.room = room 29 | } 30 | 31 | override fun toString(): String = "$subject($id)" 32 | 33 | } 34 | -------------------------------------------------------------------------------- /kotlin/school-timetabling/src/main/kotlin/org/acme/kotlin/schooltimetabling/domain/Room.kt: -------------------------------------------------------------------------------- 1 | package org.acme.kotlin.schooltimetabling.domain 2 | 3 | import ai.timefold.solver.core.api.domain.lookup.PlanningId 4 | import com.fasterxml.jackson.annotation.JsonIdentityInfo 5 | import com.fasterxml.jackson.annotation.ObjectIdGenerators 6 | 7 | @JsonIdentityInfo( 8 | scope = Room::class, 9 | generator = ObjectIdGenerators.PropertyGenerator::class, 10 | property = "id" 11 | ) 12 | data class Room( 13 | @PlanningId 14 | val id: String, 15 | val name: String) { 16 | 17 | override fun toString(): String = name 18 | 19 | } 20 | -------------------------------------------------------------------------------- /kotlin/school-timetabling/src/main/kotlin/org/acme/kotlin/schooltimetabling/domain/Timeslot.kt: -------------------------------------------------------------------------------- 1 | package org.acme.kotlin.schooltimetabling.domain 2 | 3 | import ai.timefold.solver.core.api.domain.lookup.PlanningId 4 | import com.fasterxml.jackson.annotation.JsonIdentityInfo 5 | import com.fasterxml.jackson.annotation.ObjectIdGenerators 6 | import java.time.DayOfWeek 7 | import java.time.LocalTime 8 | 9 | @JsonIdentityInfo( 10 | scope = Timeslot::class, 11 | generator = ObjectIdGenerators.PropertyGenerator::class, 12 | property = "id" 13 | ) 14 | data class Timeslot( 15 | @PlanningId 16 | val id: String, 17 | val dayOfWeek: DayOfWeek, 18 | val startTime: LocalTime, 19 | val endTime: LocalTime = startTime.plusMinutes(50)) { 20 | 21 | override fun toString(): String = "$dayOfWeek $startTime" 22 | 23 | } 24 | -------------------------------------------------------------------------------- /kotlin/school-timetabling/src/main/kotlin/org/acme/kotlin/schooltimetabling/domain/Timetable.kt: -------------------------------------------------------------------------------- 1 | package org.acme.kotlin.schooltimetabling.domain 2 | 3 | import ai.timefold.solver.core.api.domain.solution.PlanningEntityCollectionProperty 4 | import ai.timefold.solver.core.api.domain.solution.PlanningScore 5 | import ai.timefold.solver.core.api.domain.solution.PlanningSolution 6 | import ai.timefold.solver.core.api.domain.solution.ProblemFactCollectionProperty 7 | import ai.timefold.solver.core.api.domain.valuerange.ValueRangeProvider 8 | import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore 9 | import ai.timefold.solver.core.api.solver.SolverStatus 10 | 11 | @PlanningSolution 12 | data class Timetable ( 13 | val name: String, 14 | @ProblemFactCollectionProperty 15 | @ValueRangeProvider 16 | val timeslots: List, 17 | @ProblemFactCollectionProperty 18 | @ValueRangeProvider 19 | val rooms: List, 20 | @PlanningEntityCollectionProperty 21 | val lessons: List, 22 | @PlanningScore 23 | var score: HardSoftScore? = null, 24 | // Ignored by Timefold, used by the UI to display solve or stop solving button 25 | var solverStatus: SolverStatus? = null) { 26 | 27 | constructor(name: String, score: HardSoftScore?, solverStatus: SolverStatus) 28 | : this(name, emptyList(), emptyList(), emptyList(), score, solverStatus) 29 | 30 | override fun toString(): String = name 31 | 32 | } 33 | -------------------------------------------------------------------------------- /kotlin/school-timetabling/src/main/kotlin/org/acme/kotlin/schooltimetabling/rest/exception/ErrorInfo.kt: -------------------------------------------------------------------------------- 1 | package org.acme.kotlin.schooltimetabling.rest.exception 2 | 3 | data class ErrorInfo(val jobId: String, val message: String) 4 | -------------------------------------------------------------------------------- /kotlin/school-timetabling/src/main/kotlin/org/acme/kotlin/schooltimetabling/rest/exception/TimetableSolverException.kt: -------------------------------------------------------------------------------- 1 | package org.acme.kotlin.schooltimetabling.rest.exception 2 | 3 | import jakarta.ws.rs.core.Response 4 | 5 | class TimetableSolverException : RuntimeException { 6 | 7 | var jobId: String 8 | 9 | var status: Response.Status 10 | 11 | constructor(jobId: String, status: Response.Status, message: String?) : super(message) { 12 | this.jobId = jobId 13 | this.status = status 14 | } 15 | 16 | constructor(jobId: String, cause: Throwable) : super(cause.message, cause) { 17 | this.jobId = jobId 18 | this.status = Response.Status.INTERNAL_SERVER_ERROR 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /kotlin/school-timetabling/src/main/kotlin/org/acme/kotlin/schooltimetabling/rest/exception/TimetableSolverExceptionMapper.kt: -------------------------------------------------------------------------------- 1 | package org.acme.kotlin.schooltimetabling.rest.exception 2 | 3 | import jakarta.ws.rs.core.MediaType 4 | import jakarta.ws.rs.core.Response 5 | import jakarta.ws.rs.ext.ExceptionMapper 6 | import jakarta.ws.rs.ext.Provider 7 | 8 | @Provider 9 | class TimetableSolverExceptionMapper : 10 | ExceptionMapper { 11 | override fun toResponse(exception: TimetableSolverException): Response { 12 | return Response 13 | .status(exception.status) 14 | .type(MediaType.APPLICATION_JSON) 15 | .entity(ErrorInfo(exception.jobId, exception.message ?: "")) 16 | .build() 17 | } 18 | } -------------------------------------------------------------------------------- /kotlin/school-timetabling/src/main/kotlin/org/acme/kotlin/schooltimetabling/solver/justifications/RoomConflictJustification.kt: -------------------------------------------------------------------------------- 1 | package org.acme.kotlin.schooltimetabling.solver.justifications 2 | 3 | import ai.timefold.solver.core.api.score.stream.ConstraintJustification 4 | import org.acme.kotlin.schooltimetabling.domain.Lesson 5 | import org.acme.kotlin.schooltimetabling.domain.Room 6 | 7 | 8 | data class RoomConflictJustification( 9 | val room: Room, 10 | val lesson1: Lesson, 11 | val lesson2: Lesson, 12 | val description: String 13 | ) : 14 | ConstraintJustification { 15 | 16 | constructor( 17 | room: Room, 18 | lesson1: Lesson, 19 | lesson2: Lesson 20 | ) : this( 21 | room, lesson1, lesson2, 22 | "Room '%s' is used for lesson '%s' for student group '%s' and lesson '%s' for student group '%s' at '%s %s'" 23 | .format( 24 | room, 25 | lesson1.subject, 26 | lesson1.studentGroup, 27 | lesson2.subject, 28 | lesson2.studentGroup, 29 | lesson1.timeslot?.dayOfWeek, 30 | lesson1.timeslot?.startTime 31 | ) 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /kotlin/school-timetabling/src/main/kotlin/org/acme/kotlin/schooltimetabling/solver/justifications/StudentGroupConflictJustification.kt: -------------------------------------------------------------------------------- 1 | package org.acme.kotlin.schooltimetabling.solver.justifications 2 | 3 | import ai.timefold.solver.core.api.score.stream.ConstraintJustification 4 | import org.acme.kotlin.schooltimetabling.domain.Lesson 5 | 6 | 7 | data class StudentGroupConflictJustification( 8 | val studentGroup: String, 9 | val lesson1: Lesson, 10 | val lesson2: Lesson, 11 | val description: String 12 | ) : 13 | ConstraintJustification { 14 | 15 | constructor( 16 | studentGroup: String, 17 | lesson1: Lesson, 18 | lesson2: Lesson 19 | ) : this( 20 | studentGroup, lesson1, lesson2, 21 | "Student group '%s' has lesson '%s' and lesson '%s' at '%s %s'" 22 | .format( 23 | studentGroup, 24 | lesson1.subject, 25 | lesson2.subject, 26 | lesson2.studentGroup, 27 | lesson1.timeslot?.dayOfWeek, 28 | lesson1.timeslot?.startTime 29 | ) 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /kotlin/school-timetabling/src/main/kotlin/org/acme/kotlin/schooltimetabling/solver/justifications/StudentGroupSubjectVarietyJustification.kt: -------------------------------------------------------------------------------- 1 | package org.acme.kotlin.schooltimetabling.solver.justifications 2 | 3 | import ai.timefold.solver.core.api.score.stream.ConstraintJustification 4 | import org.acme.kotlin.schooltimetabling.domain.Lesson 5 | 6 | data class StudentGroupSubjectVarietyJustification( 7 | val studentGroup: String, 8 | val lesson1: Lesson, 9 | val lesson2: Lesson, 10 | val description: String 11 | ) : 12 | ConstraintJustification { 13 | 14 | constructor( 15 | studentGroup: String, 16 | lesson1: Lesson, 17 | lesson2: Lesson 18 | ) : this( 19 | studentGroup, lesson1, lesson2, 20 | "Student Group '%s' has two consecutive lessons on '%s' at '%s %s' and at '%s %s'" 21 | .format( 22 | studentGroup, 23 | lesson1.subject, 24 | lesson1.timeslot?.dayOfWeek, 25 | lesson1.timeslot?.startTime, 26 | lesson2.timeslot?.dayOfWeek, 27 | lesson2.timeslot?.startTime 28 | ) 29 | ) 30 | } -------------------------------------------------------------------------------- /kotlin/school-timetabling/src/main/kotlin/org/acme/kotlin/schooltimetabling/solver/justifications/TeacherConflictJustification.kt: -------------------------------------------------------------------------------- 1 | package org.acme.kotlin.schooltimetabling.solver.justifications 2 | 3 | import ai.timefold.solver.core.api.score.stream.ConstraintJustification 4 | import org.acme.kotlin.schooltimetabling.domain.Lesson 5 | 6 | data class TeacherConflictJustification( 7 | val teacher: String, 8 | val lesson1: Lesson, 9 | val lesson2: Lesson, 10 | val description: String 11 | ) : 12 | ConstraintJustification { 13 | 14 | constructor( 15 | teacher: String, 16 | lesson1: Lesson, 17 | lesson2: Lesson 18 | ) : this( 19 | teacher, lesson1, lesson2, 20 | "Teacher '%s' needs to teach lesson '%s' for student group '%s' and lesson '%s' for student group '%s' at '%s %s'" 21 | .format( 22 | teacher, 23 | lesson1.subject, 24 | lesson1.studentGroup, 25 | lesson2.subject, 26 | lesson2.studentGroup, 27 | lesson1.timeslot?.dayOfWeek, 28 | lesson1.timeslot?.startTime 29 | ) 30 | ) 31 | } -------------------------------------------------------------------------------- /kotlin/school-timetabling/src/main/kotlin/org/acme/kotlin/schooltimetabling/solver/justifications/TeacherRoomStabilityJustification.kt: -------------------------------------------------------------------------------- 1 | package org.acme.kotlin.schooltimetabling.solver.justifications 2 | 3 | import ai.timefold.solver.core.api.score.stream.ConstraintJustification 4 | import org.acme.kotlin.schooltimetabling.domain.Lesson 5 | 6 | data class TeacherRoomStabilityJustification( 7 | val teacher: String, 8 | val lesson1: Lesson, 9 | val lesson2: Lesson, 10 | val description: String 11 | ) : 12 | ConstraintJustification { 13 | 14 | constructor( 15 | teacher: String, 16 | lesson1: Lesson, 17 | lesson2: Lesson 18 | ) : this( 19 | teacher, lesson1, lesson2, 20 | "Teacher '%s' has two lessons in different rooms: room '%s' at '%s %s' and room '%s' at '%s %s'" 21 | .format( 22 | teacher, 23 | lesson1.room, 24 | lesson1.studentGroup, 25 | lesson1.timeslot?.dayOfWeek, 26 | lesson1.timeslot?.startTime, 27 | lesson2.room, 28 | lesson2.timeslot?.dayOfWeek, 29 | lesson2.timeslot?.startTime 30 | ) 31 | ) 32 | } -------------------------------------------------------------------------------- /kotlin/school-timetabling/src/main/kotlin/org/acme/kotlin/schooltimetabling/solver/justifications/TeacherTimeEfficiencyJustification.kt: -------------------------------------------------------------------------------- 1 | package org.acme.kotlin.schooltimetabling.solver.justifications 2 | 3 | import ai.timefold.solver.core.api.score.stream.ConstraintJustification 4 | import org.acme.kotlin.schooltimetabling.domain.Lesson 5 | 6 | data class TeacherTimeEfficiencyJustification( 7 | val teacher: String, 8 | val lesson1: Lesson, 9 | val lesson2: Lesson, 10 | val description: String 11 | ) : 12 | ConstraintJustification { 13 | 14 | constructor( 15 | teacher: String, 16 | lesson1: Lesson, 17 | lesson2: Lesson 18 | ) : this( 19 | teacher, lesson1, lesson2, 20 | "Teacher '%s' has 2 consecutive lessons: lesson '%s' for student group '%s' at '%s %s' and lesson '%s' for student group '%s' at '%s %s' (gap)" 21 | .format( 22 | teacher, 23 | lesson1.subject, 24 | lesson1.studentGroup, 25 | lesson1.timeslot?.dayOfWeek, 26 | lesson1.timeslot?.startTime, 27 | lesson2.subject, 28 | lesson2.studentGroup, 29 | lesson2.timeslot?.dayOfWeek, 30 | lesson2.timeslot?.startTime 31 | ) 32 | ) 33 | } -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 4.0.0 6 | 7 | ai.timefold.solver 8 | timefold-solver-build-parent 9 | 1.22.1 10 | 11 | 12 | 13 | 14 | timefold-solver-quickstarts-parent 15 | pom 16 | Timefold Solver Quickstarts 17 | 18 | 19 | false 20 | 21 | 22 | 23 | java/hello-world 24 | java/school-timetabling 25 | java/facility-location 26 | java/maintenance-scheduling 27 | java/vehicle-routing 28 | java/order-picking 29 | java/employee-scheduling 30 | java/food-packaging 31 | java/conference-scheduling 32 | java/bed-allocation 33 | java/flight-crew-scheduling 34 | java/meeting-scheduling 35 | java/sports-league-scheduling 36 | java/task-assigning 37 | java/project-job-scheduling 38 | java/tournament-scheduling 39 | java/spring-boot-integration 40 | kotlin/school-timetabling 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /python/employee-scheduling/employee-scheduling-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimefoldAI/timefold-quickstarts/f3e83432b4a142110e6aad7e26dc9fd37937ec25/python/employee-scheduling/employee-scheduling-screenshot.png -------------------------------------------------------------------------------- /python/employee-scheduling/logging.conf: -------------------------------------------------------------------------------- 1 | [loggers] 2 | keys=root,timefold_solver 3 | 4 | [handlers] 5 | keys=consoleHandler 6 | 7 | [formatters] 8 | keys=simpleFormatter 9 | 10 | [logger_root] 11 | level=INFO 12 | handlers=consoleHandler 13 | 14 | [logger_timefold_solver] 15 | level=INFO 16 | qualname=timefold.solver 17 | handlers=consoleHandler 18 | propagate=0 19 | 20 | [handler_consoleHandler] 21 | class=StreamHandler 22 | level=INFO 23 | formatter=simpleFormatter 24 | args=(sys.stdout,) 25 | 26 | [formatter_simpleFormatter] 27 | class=uvicorn.logging.ColourizedFormatter 28 | format={levelprefix:<8} @ {name} : {message} 29 | style={ 30 | use_colors=True 31 | -------------------------------------------------------------------------------- /python/employee-scheduling/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | 6 | [project] 7 | name = "employee_scheduling" 8 | version = "1.0.0" 9 | requires-python = ">=3.11" 10 | dependencies = [ 11 | 'timefold == 1.22.1b0', 12 | 'fastapi == 0.111.0', 13 | 'pydantic == 2.7.3', 14 | 'uvicorn == 0.30.1', 15 | 'pytest == 8.2.2', 16 | ] 17 | 18 | 19 | [project.scripts] 20 | run-app = "employee_scheduling:main" 21 | -------------------------------------------------------------------------------- /python/employee-scheduling/src/employee_scheduling/__init__.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | 3 | from .rest_api import app 4 | 5 | 6 | def main(): 7 | config = uvicorn.Config("employee_scheduling:app", 8 | port=8080, 9 | log_config="logging.conf", 10 | use_colors=True) 11 | server = uvicorn.Server(config) 12 | server.run() 13 | 14 | 15 | if __name__ == "__main__": 16 | main() 17 | -------------------------------------------------------------------------------- /python/employee-scheduling/src/employee_scheduling/domain.py: -------------------------------------------------------------------------------- 1 | from timefold.solver import SolverStatus 2 | from timefold.solver.domain import * 3 | from timefold.solver.score import HardSoftDecimalScore 4 | from datetime import datetime, date 5 | from typing import Annotated 6 | from pydantic import Field 7 | 8 | from .json_serialization import * 9 | 10 | 11 | class Employee(JsonDomainBase): 12 | name: Annotated[str, PlanningId] 13 | skills: Annotated[set[str], Field(default_factory=set)] 14 | unavailable_dates: Annotated[set[date], Field(default_factory=set)] 15 | undesired_dates: Annotated[set[date], Field(default_factory=set)] 16 | desired_dates: Annotated[set[date], Field(default_factory=set)] 17 | 18 | 19 | @planning_entity 20 | class Shift(JsonDomainBase): 21 | id: Annotated[str, PlanningId] 22 | start: datetime 23 | end: datetime 24 | location: str 25 | required_skill: str 26 | employee: Annotated[Employee | None, 27 | PlanningVariable, 28 | Field(default=None)] 29 | 30 | 31 | @planning_solution 32 | class EmployeeSchedule(JsonDomainBase): 33 | employees: Annotated[list[Employee], ProblemFactCollectionProperty, ValueRangeProvider] 34 | shifts: Annotated[list[Shift], PlanningEntityCollectionProperty] 35 | score: Annotated[HardSoftDecimalScore | None, 36 | PlanningScore, ScoreSerializer, ScoreValidator, Field(default=None)] 37 | solver_status: Annotated[SolverStatus | None, Field(default=None)] 38 | -------------------------------------------------------------------------------- /python/employee-scheduling/src/employee_scheduling/json_serialization.py: -------------------------------------------------------------------------------- 1 | from timefold.solver.score import HardSoftDecimalScore 2 | from typing import Any 3 | from pydantic import BaseModel, ConfigDict, PlainSerializer, BeforeValidator 4 | from pydantic.alias_generators import to_camel 5 | 6 | ScoreSerializer = PlainSerializer(lambda score: str(score) if score is not None else None, return_type=str | None) 7 | 8 | 9 | def validate_score(v: Any) -> Any: 10 | if isinstance(v, HardSoftDecimalScore) or v is None: 11 | return v 12 | if isinstance(v, str): 13 | return HardSoftDecimalScore.parse(v) 14 | raise ValueError('"score" should be a string') 15 | 16 | 17 | ScoreValidator = BeforeValidator(validate_score) 18 | 19 | 20 | class JsonDomainBase(BaseModel): 21 | model_config = ConfigDict( 22 | alias_generator=to_camel, 23 | populate_by_name=True, 24 | from_attributes=True, 25 | ) 26 | -------------------------------------------------------------------------------- /python/employee-scheduling/src/employee_scheduling/solver.py: -------------------------------------------------------------------------------- 1 | from timefold.solver import SolverManager, SolverFactory, SolutionManager 2 | from timefold.solver.config import (SolverConfig, ScoreDirectorFactoryConfig, 3 | TerminationConfig, Duration) 4 | 5 | from .domain import * 6 | from .constraints import define_constraints 7 | 8 | 9 | solver_config = SolverConfig( 10 | solution_class=EmployeeSchedule, 11 | entity_class_list=[Shift], 12 | score_director_factory_config=ScoreDirectorFactoryConfig( 13 | constraint_provider_function=define_constraints 14 | ), 15 | termination_config=TerminationConfig( 16 | spent_limit=Duration(seconds=30) 17 | ) 18 | ) 19 | 20 | solver_manager = SolverManager.create(SolverFactory.create(solver_config)) 21 | solution_manager = SolutionManager.create(solver_manager) 22 | -------------------------------------------------------------------------------- /python/employee-scheduling/static/webjars/timefold/img/timefold-favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.14, written by Peter Selinger 2001-2017 9 | 10 | 12 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /python/employee-scheduling/tests/test_feasible.py: -------------------------------------------------------------------------------- 1 | from timefold.solver import SolverFactory 2 | from timefold.solver.config import (SolverConfig, ScoreDirectorFactoryConfig, TerminationConfig, Duration, 3 | TerminationCompositionStyle) 4 | 5 | from employee_scheduling.rest_api import app 6 | from employee_scheduling.domain import * 7 | 8 | from fastapi.testclient import TestClient 9 | from time import sleep 10 | from pytest import fail 11 | 12 | client = TestClient(app) 13 | 14 | 15 | def test_feasible(): 16 | demo_data_response = client.get("/demo-data/SMALL") 17 | assert demo_data_response.status_code == 200 18 | 19 | job_id_response = client.post("/schedules", json=demo_data_response.json()) 20 | assert job_id_response.status_code == 200 21 | job_id = job_id_response.text[1:-1] 22 | 23 | ATTEMPTS = 1_000 24 | for _ in range(ATTEMPTS): 25 | sleep(0.1) 26 | schedule_response = client.get(f"/schedules/{job_id}") 27 | schedule_json = schedule_response.json() 28 | schedule = EmployeeSchedule.model_validate(schedule_json) 29 | if schedule.score is not None and schedule.score.is_feasible: 30 | stop_solving_response = client.delete(f"/schedules/{job_id}") 31 | assert stop_solving_response.status_code == 200 32 | return 33 | 34 | client.delete(f"/schedules/{job_id}") 35 | fail('solution is not feasible') 36 | -------------------------------------------------------------------------------- /python/flight-crew-scheduling/README.adoc: -------------------------------------------------------------------------------- 1 | = Flight Crew Scheduling (Python) 2 | 3 | Assign crew to flights to produce a better schedule for flight assignments. 4 | 5 | image::./flight-crew-scheduling-screenshot.png[] 6 | 7 | * <> 8 | * <> 9 | * <> 10 | 11 | [[prerequisites]] 12 | == Prerequisites 13 | 14 | . Install https://www.python.org/downloads/[Python 3.11+] 15 | 16 | . Install JDK 17+, for example with https://sdkman.io[Sdkman]: 17 | + 18 | ---- 19 | $ sdk install java 20 | ---- 21 | 22 | [[run]] 23 | == Run the application 24 | 25 | . Git clone the timefold-quickstarts repo and navigate to this directory: 26 | + 27 | [source, shell] 28 | ---- 29 | $ git clone https://github.com/TimefoldAI/timefold-quickstarts.git 30 | ... 31 | $ cd timefold-quickstarts/python/flight-crew-scheduling 32 | ---- 33 | 34 | . Create a virtual environment 35 | + 36 | [source, shell] 37 | ---- 38 | $ python -m venv .venv 39 | ---- 40 | 41 | . Activate the virtual environment 42 | + 43 | [source, shell] 44 | ---- 45 | $ . .venv/bin/activate 46 | ---- 47 | 48 | . Install the application 49 | + 50 | [source, shell] 51 | ---- 52 | $ pip install -e . 53 | ---- 54 | 55 | . Run the application 56 | + 57 | [source, shell] 58 | ---- 59 | $ run-app 60 | ---- 61 | 62 | . Visit http://localhost:8080 in your browser. 63 | 64 | . Click on the *Solve* button. 65 | 66 | 67 | [[test]] 68 | == Test the application 69 | 70 | . Run tests 71 | + 72 | [source, shell] 73 | ---- 74 | $ pytest 75 | ---- 76 | 77 | == More information 78 | 79 | Visit https://timefold.ai[timefold.ai]. 80 | -------------------------------------------------------------------------------- /python/flight-crew-scheduling/flight-crew-scheduling-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimefoldAI/timefold-quickstarts/f3e83432b4a142110e6aad7e26dc9fd37937ec25/python/flight-crew-scheduling/flight-crew-scheduling-screenshot.png -------------------------------------------------------------------------------- /python/flight-crew-scheduling/logging.conf: -------------------------------------------------------------------------------- 1 | [loggers] 2 | keys=root,timefold_solver 3 | 4 | [handlers] 5 | keys=consoleHandler 6 | 7 | [formatters] 8 | keys=simpleFormatter 9 | 10 | [logger_root] 11 | level=INFO 12 | handlers=consoleHandler 13 | 14 | [logger_timefold_solver] 15 | level=INFO 16 | qualname=timefold.solver 17 | handlers=consoleHandler 18 | propagate=0 19 | 20 | [handler_consoleHandler] 21 | class=StreamHandler 22 | level=INFO 23 | formatter=simpleFormatter 24 | args=(sys.stdout,) 25 | 26 | [formatter_simpleFormatter] 27 | class=uvicorn.logging.ColourizedFormatter 28 | format={levelprefix:<8} @ {name} : {message} 29 | style={ 30 | use_colors=True 31 | -------------------------------------------------------------------------------- /python/flight-crew-scheduling/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "flight_crew_scheduling" 7 | version = "1.0.0" 8 | requires-python = ">=3.11" 9 | dependencies = [ 10 | 'timefold == 1.22.1b0', 11 | 'fastapi == 0.111.0', 12 | 'pydantic == 2.7.3', 13 | 'uvicorn == 0.30.1', 14 | 'pytest == 8.2.2', 15 | 'httpx == 0.27.0', 16 | ] 17 | 18 | 19 | [project.scripts] 20 | run-app = "flight_crew_scheduling:main" 21 | -------------------------------------------------------------------------------- /python/flight-crew-scheduling/src/flight_crew_scheduling/__init__.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | 3 | from .rest_api import app 4 | 5 | 6 | def main(): 7 | config = uvicorn.Config("flight_crew_scheduling:app", 8 | port=8080, 9 | log_config="logging.conf", 10 | use_colors=True) 11 | server = uvicorn.Server(config) 12 | server.run() 13 | 14 | 15 | if __name__ == "__main__": 16 | main() 17 | -------------------------------------------------------------------------------- /python/flight-crew-scheduling/src/flight_crew_scheduling/score_analysis.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | 3 | from .json_serialization import * 4 | from .domain import * 5 | 6 | 7 | class MatchAnalysisDTO(JsonDomainBase): 8 | name: str 9 | score: Annotated[HardSoftScore, ScoreSerializer] 10 | justification: object 11 | 12 | 13 | class ConstraintAnalysisDTO(JsonDomainBase): 14 | name: str 15 | weight: Annotated[HardSoftScore, ScoreSerializer] 16 | matches: list[MatchAnalysisDTO] 17 | score: Annotated[HardSoftScore, ScoreSerializer] 18 | -------------------------------------------------------------------------------- /python/flight-crew-scheduling/src/flight_crew_scheduling/solver.py: -------------------------------------------------------------------------------- 1 | from timefold.solver import SolverManager, SolverFactory, SolutionManager 2 | from timefold.solver.config import (SolverConfig, ScoreDirectorFactoryConfig, 3 | TerminationConfig, Duration) 4 | 5 | from .domain import * 6 | from .constraints import define_constraints 7 | 8 | 9 | solver_config = SolverConfig( 10 | solution_class=FlightCrewSchedule, 11 | entity_class_list=[FlightAssignment], 12 | score_director_factory_config=ScoreDirectorFactoryConfig( 13 | constraint_provider_function=define_constraints 14 | ), 15 | termination_config=TerminationConfig( 16 | spent_limit=Duration(seconds=30) 17 | ) 18 | ) 19 | 20 | solver_manager = SolverManager.create(SolverFactory.create(solver_config)) 21 | solution_manager = SolutionManager.create(solver_manager) 22 | -------------------------------------------------------------------------------- /python/flight-crew-scheduling/static/webjars/timefold/img/timefold-favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.14, written by Peter Selinger 2001-2017 9 | 10 | 12 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /python/hello-world/README.adoc: -------------------------------------------------------------------------------- 1 | = School Timetabling (Python) 2 | 3 | Assign lessons to timeslots and rooms to produce a better schedule for teachers and students. 4 | 5 | == Prerequisites 6 | 7 | . Install https://www.python.org/downloads/[Python 3.10+] 8 | 9 | . Install JDK 17+, for example with https://sdkman.io[Sdkman]: 10 | + 11 | ---- 12 | $ sdk install java 13 | ---- 14 | 15 | == Run the application 16 | 17 | . Git clone the timefold-quickstarts repo: 18 | + 19 | [source, shell] 20 | ---- 21 | $ git clone https://github.com/TimefoldAI/timefold-quickstarts.git 22 | ... 23 | $ cd timefold-quickstarts/python/hello-world 24 | ---- 25 | 26 | . Create a virtual environment 27 | + 28 | [source, shell] 29 | ---- 30 | $ python -m venv .venv 31 | ---- 32 | 33 | . Activate the virtual environment 34 | + 35 | [source, shell] 36 | ---- 37 | $ . .venv/bin/activate 38 | ---- 39 | 40 | . Install the application 41 | + 42 | [source, shell] 43 | ---- 44 | $ pip install -e . 45 | ---- 46 | 47 | . Run the application 48 | + 49 | [source, shell] 50 | ---- 51 | $ run-app 52 | ---- 53 | 54 | . Run tests 55 | + 56 | [source, shell] 57 | ---- 58 | $ pytest 59 | ---- 60 | 61 | == More information 62 | 63 | Visit https://timefold.ai[timefold.ai]. 64 | -------------------------------------------------------------------------------- /python/hello-world/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | 6 | [project] 7 | name = "hello_world" 8 | version = "1.0.0" 9 | requires-python = ">=3.10" 10 | dependencies = [ 11 | 'timefold == 1.22.1b0', 12 | 'pytest == 8.2.2' 13 | ] 14 | 15 | 16 | [project.scripts] 17 | run-app = "hello_world:main" 18 | -------------------------------------------------------------------------------- /python/hello-world/src/hello_world/__init__.py: -------------------------------------------------------------------------------- 1 | from .main import main 2 | 3 | __all__ = ['main'] 4 | -------------------------------------------------------------------------------- /python/hello-world/src/hello_world/domain.py: -------------------------------------------------------------------------------- 1 | from timefold.solver.domain import (planning_entity, planning_solution, PlanningId, PlanningVariable, 2 | PlanningEntityCollectionProperty, 3 | ProblemFactCollectionProperty, ValueRangeProvider, 4 | PlanningScore) 5 | from timefold.solver.score import HardSoftScore 6 | from dataclasses import dataclass, field 7 | from datetime import time 8 | from typing import Annotated 9 | 10 | 11 | @dataclass 12 | class Timeslot: 13 | day_of_week: str 14 | start_time: time 15 | end_time: time 16 | 17 | def __str__(self): 18 | return f'{self.day_of_week} {self.start_time.strftime("%H:%M")}' 19 | 20 | 21 | @dataclass 22 | class Room: 23 | name: str 24 | 25 | def __str__(self): 26 | return f'{self.name}' 27 | 28 | 29 | @planning_entity 30 | @dataclass 31 | class Lesson: 32 | id: Annotated[str, PlanningId] 33 | subject: str 34 | teacher: str 35 | student_group: str 36 | timeslot: Annotated[Timeslot | None, PlanningVariable] = field(default=None) 37 | room: Annotated[Room | None, PlanningVariable] = field(default=None) 38 | 39 | 40 | @planning_solution 41 | @dataclass 42 | class Timetable: 43 | id: str 44 | timeslots: Annotated[list[Timeslot], 45 | ProblemFactCollectionProperty, 46 | ValueRangeProvider] 47 | rooms: Annotated[list[Room], 48 | ProblemFactCollectionProperty, 49 | ValueRangeProvider] 50 | lessons: Annotated[list[Lesson], 51 | PlanningEntityCollectionProperty] 52 | score: Annotated[HardSoftScore, PlanningScore] = field(default=None) 53 | -------------------------------------------------------------------------------- /python/hello-world/tests/test_feasible.py: -------------------------------------------------------------------------------- 1 | from timefold.solver import SolverFactory 2 | from timefold.solver.config import (SolverConfig, ScoreDirectorFactoryConfig, 3 | TerminationConfig, Duration, TerminationCompositionStyle) 4 | 5 | from hello_world.domain import * 6 | from hello_world.constraints import define_constraints 7 | from hello_world.main import generate_demo_data, DemoData 8 | 9 | 10 | def test_feasible(): 11 | solver_factory = SolverFactory.create( 12 | SolverConfig( 13 | solution_class=Timetable, 14 | entity_class_list=[Lesson], 15 | score_director_factory_config=ScoreDirectorFactoryConfig( 16 | constraint_provider_function=define_constraints 17 | ), 18 | termination_config=TerminationConfig( 19 | termination_config_list=[ 20 | TerminationConfig(best_score_feasible=True), 21 | TerminationConfig(spent_limit=Duration(seconds=30)), 22 | ], 23 | termination_composition_style=TerminationCompositionStyle.OR 24 | ) 25 | )) 26 | solver = solver_factory.build_solver() 27 | solution = solver.solve(generate_demo_data(DemoData.SMALL)) 28 | assert solution.score.is_feasible 29 | -------------------------------------------------------------------------------- /python/school-timetabling/README.adoc: -------------------------------------------------------------------------------- 1 | = School Timetabling (Python) 2 | 3 | Assign lessons to timeslots and rooms to produce a better schedule for teachers and students. 4 | 5 | image::./school-timetabling-screenshot.png[] 6 | 7 | * <> 8 | * <> 9 | * <> 10 | 11 | [[prerequisites]] 12 | == Prerequisites 13 | 14 | . Install https://www.python.org/downloads/[Python 3.11+] 15 | 16 | . Install JDK 17+, for example with https://sdkman.io[Sdkman]: 17 | + 18 | ---- 19 | $ sdk install java 20 | ---- 21 | 22 | [[run]] 23 | == Run the application 24 | 25 | . Git clone the timefold-quickstarts repo and navigate to this directory: 26 | + 27 | [source, shell] 28 | ---- 29 | $ git clone https://github.com/TimefoldAI/timefold-quickstarts.git 30 | ... 31 | $ cd timefold-quickstarts/python/school-timetabling 32 | ---- 33 | 34 | . Create a virtual environment 35 | + 36 | [source, shell] 37 | ---- 38 | $ python -m venv .venv 39 | ---- 40 | 41 | . Activate the virtual environment 42 | + 43 | [source, shell] 44 | ---- 45 | $ . .venv/bin/activate 46 | ---- 47 | 48 | . Install the application 49 | + 50 | [source, shell] 51 | ---- 52 | $ pip install -e . 53 | ---- 54 | 55 | . Run the application 56 | + 57 | [source, shell] 58 | ---- 59 | $ run-app 60 | ---- 61 | 62 | . Visit http://localhost:8080 in your browser. 63 | 64 | . Click on the *Solve* button. 65 | 66 | 67 | [[test]] 68 | == Test the application 69 | 70 | . Run tests 71 | + 72 | [source, shell] 73 | ---- 74 | $ pytest 75 | ---- 76 | 77 | == More information 78 | 79 | Visit https://timefold.ai[timefold.ai]. 80 | -------------------------------------------------------------------------------- /python/school-timetabling/logging.conf: -------------------------------------------------------------------------------- 1 | [loggers] 2 | keys=root,timefold_solver 3 | 4 | [handlers] 5 | keys=consoleHandler 6 | 7 | [formatters] 8 | keys=simpleFormatter 9 | 10 | [logger_root] 11 | level=INFO 12 | handlers=consoleHandler 13 | 14 | [logger_timefold_solver] 15 | level=INFO 16 | qualname=timefold.solver 17 | handlers=consoleHandler 18 | propagate=0 19 | 20 | [handler_consoleHandler] 21 | class=StreamHandler 22 | level=INFO 23 | formatter=simpleFormatter 24 | args=(sys.stdout,) 25 | 26 | [formatter_simpleFormatter] 27 | class=uvicorn.logging.ColourizedFormatter 28 | format={levelprefix:<8} @ {name} : {message} 29 | style={ 30 | use_colors=True 31 | -------------------------------------------------------------------------------- /python/school-timetabling/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | 6 | [project] 7 | name = "school_timetabling" 8 | version = "1.0.0" 9 | requires-python = ">=3.11" 10 | dependencies = [ 11 | 'timefold == 1.22.1b0', 12 | 'fastapi == 0.111.0', 13 | 'pydantic == 2.7.3', 14 | 'uvicorn == 0.30.1', 15 | 'pytest == 8.2.2', 16 | 'httpx == 0.27.0', 17 | ] 18 | 19 | 20 | [project.scripts] 21 | run-app = "school_timetabling:main" 22 | -------------------------------------------------------------------------------- /python/school-timetabling/school-timetabling-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimefoldAI/timefold-quickstarts/f3e83432b4a142110e6aad7e26dc9fd37937ec25/python/school-timetabling/school-timetabling-screenshot.png -------------------------------------------------------------------------------- /python/school-timetabling/src/school_timetabling/__init__.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | 3 | from .rest_api import app 4 | 5 | 6 | def main(): 7 | config = uvicorn.Config("school_timetabling:app", 8 | port=8080, 9 | log_config="logging.conf", 10 | use_colors=True) 11 | server = uvicorn.Server(config) 12 | server.run() 13 | 14 | 15 | if __name__ == "__main__": 16 | main() 17 | -------------------------------------------------------------------------------- /python/school-timetabling/src/school_timetabling/json_serialization.py: -------------------------------------------------------------------------------- 1 | from timefold.solver.score import HardSoftScore 2 | from pydantic import BaseModel, ConfigDict, Field, PlainSerializer, BeforeValidator, PlainValidator, ValidationInfo 3 | from pydantic.alias_generators import to_camel 4 | from typing import Annotated, Any 5 | 6 | 7 | def make_list_item_validator(key: str): 8 | def validator(v: Any, info: ValidationInfo) -> Any: 9 | if v is None: 10 | return None 11 | 12 | if not isinstance(v, str) or not info.context: 13 | return v 14 | 15 | return info.context.get(key)[v] 16 | 17 | return BeforeValidator(validator) 18 | 19 | 20 | RoomDeserializer = make_list_item_validator('rooms') 21 | TimeslotDeserializer = make_list_item_validator('timeslots') 22 | 23 | IdSerializer = PlainSerializer(lambda item: item.id if item is not None else None, 24 | return_type=str | None) 25 | ScoreSerializer = PlainSerializer(lambda score: str(score) if score is not None else None, 26 | return_type=str | None) 27 | 28 | 29 | def validate_score(v: Any, info: ValidationInfo) -> Any: 30 | if isinstance(v, HardSoftScore) or v is None: 31 | return v 32 | if isinstance(v, str): 33 | return HardSoftScore.parse(v) 34 | raise ValueError('"score" should be a string') 35 | 36 | 37 | ScoreValidator = BeforeValidator(validate_score) 38 | 39 | 40 | class JsonDomainBase(BaseModel): 41 | model_config = ConfigDict( 42 | alias_generator=to_camel, 43 | populate_by_name=True, 44 | from_attributes=True, 45 | ) 46 | -------------------------------------------------------------------------------- /python/school-timetabling/src/school_timetabling/score_analysis.py: -------------------------------------------------------------------------------- 1 | from timefold.solver.score import ConstraintJustification 2 | from dataclasses import dataclass, field 3 | 4 | from .json_serialization import * 5 | from .domain import * 6 | 7 | 8 | class MatchAnalysisDTO(JsonDomainBase): 9 | name: str 10 | score: Annotated[HardSoftScore, ScoreSerializer] 11 | justification: object 12 | 13 | 14 | class ConstraintAnalysisDTO(JsonDomainBase): 15 | name: str 16 | weight: Annotated[HardSoftScore, ScoreSerializer] 17 | matches: list[MatchAnalysisDTO] 18 | score: Annotated[HardSoftScore, ScoreSerializer] 19 | -------------------------------------------------------------------------------- /python/school-timetabling/src/school_timetabling/solver.py: -------------------------------------------------------------------------------- 1 | from timefold.solver import SolverManager, SolverFactory, SolutionManager 2 | from timefold.solver.config import (SolverConfig, ScoreDirectorFactoryConfig, 3 | TerminationConfig, Duration) 4 | 5 | from .domain import * 6 | from .constraints import define_constraints 7 | 8 | 9 | solver_config = SolverConfig( 10 | solution_class=Timetable, 11 | entity_class_list=[Lesson], 12 | score_director_factory_config=ScoreDirectorFactoryConfig( 13 | constraint_provider_function=define_constraints 14 | ), 15 | termination_config=TerminationConfig( 16 | spent_limit=Duration(seconds=30) 17 | ) 18 | ) 19 | 20 | solver_manager = SolverManager.create(SolverFactory.create(solver_config)) 21 | solution_manager = SolutionManager.create(solver_manager) 22 | -------------------------------------------------------------------------------- /python/school-timetabling/static/webjars/timefold/img/timefold-favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.14, written by Peter Selinger 2001-2017 9 | 10 | 12 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /python/sports-league-scheduling/README.adoc: -------------------------------------------------------------------------------- 1 | = Sports League Scheduling (Python) 2 | 3 | Assign rounds to matches to produce a better schedule for league matches. 4 | 5 | image::./sports-league-scheduling-screenshot.png[] 6 | 7 | * <> 8 | * <> 9 | * <> 10 | 11 | [[prerequisites]] 12 | == Prerequisites 13 | 14 | . Install https://www.python.org/downloads/[Python 3.11+] 15 | 16 | . Install JDK 17+, for example with https://sdkman.io[Sdkman]: 17 | + 18 | ---- 19 | $ sdk install java 20 | ---- 21 | 22 | [[run]] 23 | == Run the application 24 | 25 | . Git clone the timefold-quickstarts repo and navigate to this directory: 26 | + 27 | [source, shell] 28 | ---- 29 | $ git clone https://github.com/TimefoldAI/timefold-quickstarts.git 30 | ... 31 | $ cd timefold-quickstarts/python/sports-league-scheduling 32 | ---- 33 | 34 | . Create a virtual environment 35 | + 36 | [source, shell] 37 | ---- 38 | $ python -m venv .venv 39 | ---- 40 | 41 | . Activate the virtual environment 42 | + 43 | [source, shell] 44 | ---- 45 | $ . .venv/bin/activate 46 | ---- 47 | 48 | . Install the application 49 | + 50 | [source, shell] 51 | ---- 52 | $ pip install -e . 53 | ---- 54 | 55 | . Run the application 56 | + 57 | [source, shell] 58 | ---- 59 | $ run-app 60 | ---- 61 | 62 | . Visit http://localhost:8080 in your browser. 63 | 64 | . Click on the *Solve* button. 65 | 66 | 67 | [[test]] 68 | == Test the application 69 | 70 | . Run tests 71 | + 72 | [source, shell] 73 | ---- 74 | $ pytest 75 | ---- 76 | 77 | == More information 78 | 79 | Visit https://timefold.ai[timefold.ai]. 80 | -------------------------------------------------------------------------------- /python/sports-league-scheduling/logging.conf: -------------------------------------------------------------------------------- 1 | [loggers] 2 | keys=root,timefold_solver 3 | 4 | [handlers] 5 | keys=consoleHandler 6 | 7 | [formatters] 8 | keys=simpleFormatter 9 | 10 | [logger_root] 11 | level=INFO 12 | handlers=consoleHandler 13 | 14 | [logger_timefold_solver] 15 | level=INFO 16 | qualname=timefold.solver 17 | handlers=consoleHandler 18 | propagate=0 19 | 20 | [handler_consoleHandler] 21 | class=StreamHandler 22 | level=INFO 23 | formatter=simpleFormatter 24 | args=(sys.stdout,) 25 | 26 | [formatter_simpleFormatter] 27 | class=uvicorn.logging.ColourizedFormatter 28 | format={levelprefix:<8} @ {name} : {message} 29 | style={ 30 | use_colors=True 31 | -------------------------------------------------------------------------------- /python/sports-league-scheduling/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "sports_league_scheduling" 7 | version = "1.0.0" 8 | requires-python = ">=3.11" 9 | dependencies = [ 10 | 'timefold == 1.22.1b0', 11 | 'fastapi == 0.111.0', 12 | 'pydantic == 2.7.3', 13 | 'uvicorn == 0.30.1', 14 | 'pytest == 8.2.2', 15 | 'httpx == 0.27.0', 16 | ] 17 | 18 | 19 | [project.scripts] 20 | run-app = "sports_league_scheduling:main" 21 | -------------------------------------------------------------------------------- /python/sports-league-scheduling/sports-league-scheduling-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimefoldAI/timefold-quickstarts/f3e83432b4a142110e6aad7e26dc9fd37937ec25/python/sports-league-scheduling/sports-league-scheduling-screenshot.png -------------------------------------------------------------------------------- /python/sports-league-scheduling/src/sports_league_scheduling/__init__.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | 3 | from .rest_api import app 4 | 5 | 6 | def main(): 7 | config = uvicorn.Config("sports_league_scheduling:app", 8 | port=8080, 9 | log_config="logging.conf", 10 | use_colors=True) 11 | server = uvicorn.Server(config) 12 | server.run() 13 | 14 | 15 | if __name__ == "__main__": 16 | main() 17 | -------------------------------------------------------------------------------- /python/sports-league-scheduling/src/sports_league_scheduling/score_analysis.py: -------------------------------------------------------------------------------- 1 | from timefold.solver.score import ConstraintJustification 2 | from dataclasses import dataclass, field 3 | 4 | from .json_serialization import * 5 | from .domain import * 6 | 7 | 8 | class MatchAnalysisDTO(JsonDomainBase): 9 | name: str 10 | score: Annotated[HardSoftScore, ScoreSerializer] 11 | justification: object 12 | 13 | 14 | class ConstraintAnalysisDTO(JsonDomainBase): 15 | name: str 16 | weight: Annotated[HardSoftScore, ScoreSerializer] 17 | matches: list[MatchAnalysisDTO] 18 | score: Annotated[HardSoftScore, ScoreSerializer] 19 | -------------------------------------------------------------------------------- /python/sports-league-scheduling/src/sports_league_scheduling/solver.py: -------------------------------------------------------------------------------- 1 | from timefold.solver import SolverManager, SolverFactory, SolutionManager 2 | from timefold.solver.config import (SolverConfig, ScoreDirectorFactoryConfig, 3 | TerminationConfig, Duration) 4 | 5 | from .domain import * 6 | from .constraints import define_constraints 7 | 8 | 9 | solver_config = SolverConfig( 10 | solution_class=LeagueSchedule, 11 | entity_class_list=[Match], 12 | score_director_factory_config=ScoreDirectorFactoryConfig( 13 | constraint_provider_function=define_constraints 14 | ), 15 | termination_config=TerminationConfig( 16 | spent_limit=Duration(seconds=30) 17 | ) 18 | ) 19 | 20 | solver_manager = SolverManager.create(SolverFactory.create(solver_config)) 21 | solution_manager = SolutionManager.create(solver_manager) 22 | -------------------------------------------------------------------------------- /python/sports-league-scheduling/static/webjars/timefold/img/timefold-favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.14, written by Peter Selinger 2001-2017 9 | 10 | 12 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /python/tournament-scheduling/README.adoc: -------------------------------------------------------------------------------- 1 | = Tournament Scheduling (Python) 2 | 3 | Tournament Scheduling service assigning teams to tournament matches. 4 | 5 | image::./tournament-scheduling-screenshot.png[] 6 | 7 | * <> 8 | * <> 9 | * <> 10 | 11 | [[prerequisites]] 12 | == Prerequisites 13 | 14 | . Install https://www.python.org/downloads/[Python 3.11+] 15 | 16 | . Install JDK 17+, for example with https://sdkman.io[Sdkman]: 17 | + 18 | ---- 19 | $ sdk install java 20 | ---- 21 | 22 | [[run]] 23 | == Run the application 24 | 25 | . Git clone the timefold-quickstarts repo and navigate to this directory: 26 | + 27 | [source, shell] 28 | ---- 29 | $ git clone https://github.com/TimefoldAI/timefold-quickstarts.git 30 | ... 31 | $ cd timefold-quickstarts/python/tournament-scheduling 32 | ---- 33 | 34 | . Create a virtual environment 35 | + 36 | [source, shell] 37 | ---- 38 | $ python -m venv .venv 39 | ---- 40 | 41 | . Activate the virtual environment 42 | + 43 | [source, shell] 44 | ---- 45 | $ . .venv/bin/activate 46 | ---- 47 | 48 | . Install the application 49 | + 50 | [source, shell] 51 | ---- 52 | $ pip install -e . 53 | ---- 54 | 55 | . Run the application 56 | + 57 | [source, shell] 58 | ---- 59 | $ run-app 60 | ---- 61 | 62 | . Visit http://localhost:8080 in your browser. 63 | 64 | . Click on the *Solve* button. 65 | 66 | 67 | [[test]] 68 | == Test the application 69 | 70 | . Run tests 71 | + 72 | [source, shell] 73 | ---- 74 | $ pytest 75 | ---- 76 | 77 | == More information 78 | 79 | Visit https://timefold.ai[timefold.ai]. 80 | -------------------------------------------------------------------------------- /python/tournament-scheduling/logging.conf: -------------------------------------------------------------------------------- 1 | [loggers] 2 | keys=root,timefold_solver 3 | 4 | [handlers] 5 | keys=consoleHandler 6 | 7 | [formatters] 8 | keys=simpleFormatter 9 | 10 | [logger_root] 11 | level=INFO 12 | handlers=consoleHandler 13 | 14 | [logger_timefold_solver] 15 | level=INFO 16 | qualname=timefold.solver 17 | handlers=consoleHandler 18 | propagate=0 19 | 20 | [handler_consoleHandler] 21 | class=StreamHandler 22 | level=INFO 23 | formatter=simpleFormatter 24 | args=(sys.stdout,) 25 | 26 | [formatter_simpleFormatter] 27 | class=uvicorn.logging.ColourizedFormatter 28 | format={levelprefix:<8} @ {name} : {message} 29 | style={ 30 | use_colors=True 31 | -------------------------------------------------------------------------------- /python/tournament-scheduling/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "tournament_scheduling" 7 | version = "1.0.0" 8 | requires-python = ">=3.11" 9 | dependencies = [ 10 | 'timefold == 1.22.1b0', 11 | 'fastapi == 0.111.0', 12 | 'pydantic == 2.7.3', 13 | 'uvicorn == 0.30.1', 14 | 'pytest == 8.2.2', 15 | 'httpx == 0.27.0', 16 | ] 17 | 18 | 19 | [project.scripts] 20 | run-app = "tournament_scheduling:main" 21 | -------------------------------------------------------------------------------- /python/tournament-scheduling/src/tournament_scheduling/__init__.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | 3 | from .rest_api import app 4 | 5 | 6 | def main(): 7 | config = uvicorn.Config("tournament_scheduling:app", 8 | port=8080, 9 | log_config="logging.conf", 10 | use_colors=True) 11 | server = uvicorn.Server(config) 12 | server.run() 13 | 14 | 15 | if __name__ == "__main__": 16 | main() 17 | -------------------------------------------------------------------------------- /python/tournament-scheduling/src/tournament_scheduling/json_serialization.py: -------------------------------------------------------------------------------- 1 | from timefold.solver.score import HardMediumSoftDecimalScore, ConstraintJustification 2 | from pydantic import BaseModel, ConfigDict, Field, PlainSerializer, BeforeValidator, ValidationInfo 3 | from pydantic.alias_generators import to_camel 4 | from typing import Any, Dict 5 | 6 | 7 | def make_list_item_validator(key: str): 8 | def validator(v: Any, info: ValidationInfo) -> Any: 9 | if v is None: 10 | return None 11 | 12 | if not isinstance(v, int) or not info.context: 13 | return v 14 | 15 | return info.context.get(key)[v] 16 | 17 | return BeforeValidator(validator) 18 | 19 | 20 | TeamDeserializer = make_list_item_validator('teams') 21 | DayDeserializer = make_list_item_validator('days') 22 | 23 | 24 | IdSerializer = PlainSerializer( 25 | lambda item: getattr(item, 'id', getattr(item, 'date_index', None)) if item is not None else None, 26 | return_type=int | None 27 | ) 28 | ScoreSerializer = PlainSerializer(lambda score: str(score) if score is not None else None, 29 | return_type=str | None) 30 | 31 | 32 | def validate_score(v: Any, info: ValidationInfo) -> Any: 33 | if isinstance(v, HardMediumSoftDecimalScore) or v is None: 34 | return v 35 | if isinstance(v, str): 36 | return HardMediumSoftDecimalScore.parse(v) 37 | raise ValueError('"score" should be a string') 38 | 39 | 40 | ScoreValidator = BeforeValidator(validate_score) 41 | 42 | class JsonDomainBase(BaseModel): 43 | model_config = ConfigDict( 44 | alias_generator=to_camel, 45 | populate_by_name=True, 46 | from_attributes=True, 47 | ) 48 | -------------------------------------------------------------------------------- /python/tournament-scheduling/src/tournament_scheduling/score_analysis.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from decimal import Decimal 3 | from .json_serialization import * 4 | from .domain import * 5 | 6 | 7 | @dataclass 8 | class LoadBalanceJustification(ConstraintJustification): 9 | unfairness: Decimal 10 | 11 | 12 | class MatchAnalysisDTO(JsonDomainBase): 13 | name: str 14 | score: Annotated[HardMediumSoftDecimalScore, ScoreSerializer] 15 | justification: object 16 | 17 | 18 | class ConstraintAnalysisDTO(JsonDomainBase): 19 | name: str 20 | weight: Annotated[HardMediumSoftDecimalScore, ScoreSerializer] 21 | matches: list[MatchAnalysisDTO] 22 | score: Annotated[HardMediumSoftDecimalScore, ScoreSerializer] -------------------------------------------------------------------------------- /python/tournament-scheduling/src/tournament_scheduling/solver.py: -------------------------------------------------------------------------------- 1 | from timefold.solver import SolverManager, SolverFactory, SolutionManager 2 | from timefold.solver.config import (SolverConfig, ScoreDirectorFactoryConfig, 3 | TerminationConfig, Duration) 4 | 5 | from .domain import * 6 | from .constraints import define_constraints 7 | 8 | 9 | solver_config = SolverConfig( 10 | solution_class=TournamentSchedule, 11 | entity_class_list=[TeamAssignment], 12 | score_director_factory_config=ScoreDirectorFactoryConfig( 13 | constraint_provider_function=define_constraints 14 | ), 15 | termination_config=TerminationConfig( 16 | spent_limit=Duration(seconds=30) 17 | ) 18 | ) 19 | 20 | solver_manager = SolverManager.create(SolverFactory.create(solver_config)) 21 | solution_manager = SolutionManager.create(solver_manager) 22 | -------------------------------------------------------------------------------- /python/tournament-scheduling/static/webjars/timefold/img/timefold-favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.14, written by Peter Selinger 2001-2017 9 | 10 | 12 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /python/tournament-scheduling/tournament-scheduling-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimefoldAI/timefold-quickstarts/f3e83432b4a142110e6aad7e26dc9fd37937ec25/python/tournament-scheduling/tournament-scheduling-screenshot.png -------------------------------------------------------------------------------- /python/vehicle-routing/logging.conf: -------------------------------------------------------------------------------- 1 | [loggers] 2 | keys=root,timefold_solver 3 | 4 | [handlers] 5 | keys=consoleHandler 6 | 7 | [formatters] 8 | keys=simpleFormatter 9 | 10 | [logger_root] 11 | level=INFO 12 | handlers=consoleHandler 13 | 14 | [logger_timefold_solver] 15 | level=INFO 16 | qualname=timefold.solver 17 | handlers=consoleHandler 18 | propagate=0 19 | 20 | [handler_consoleHandler] 21 | class=StreamHandler 22 | level=INFO 23 | formatter=simpleFormatter 24 | args=(sys.stdout,) 25 | 26 | [formatter_simpleFormatter] 27 | class=uvicorn.logging.ColourizedFormatter 28 | format={levelprefix:<8} @ {name} : {message} 29 | style={ 30 | use_colors=True 31 | -------------------------------------------------------------------------------- /python/vehicle-routing/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | 6 | [project] 7 | name = "vehicle_routing" 8 | version = "1.0.0" 9 | requires-python = ">=3.10" 10 | dependencies = [ 11 | 'timefold == 1.22.1b0', 12 | 'fastapi == 0.111.0', 13 | 'pydantic == 2.7.3', 14 | 'uvicorn == 0.30.1', 15 | 'pytest == 8.2.2', 16 | ] 17 | 18 | 19 | [project.scripts] 20 | run-app = "vehicle_routing:main" 21 | -------------------------------------------------------------------------------- /python/vehicle-routing/src/vehicle_routing/__init__.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | 3 | from .rest_api import app 4 | 5 | 6 | def main(): 7 | config = uvicorn.Config("vehicle_routing:app", 8 | port=8080, 9 | log_config="logging.conf", 10 | use_colors=True) 11 | server = uvicorn.Server(config) 12 | server.run() 13 | 14 | 15 | if __name__ == "__main__": 16 | main() 17 | -------------------------------------------------------------------------------- /python/vehicle-routing/src/vehicle_routing/score_analysis.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Annotated 3 | 4 | from .json_serialization import * 5 | 6 | 7 | @dataclass 8 | class MatchAnalysisDTO: 9 | name: str 10 | score: Annotated[HardSoftScore, ScoreSerializer] 11 | justification: object 12 | 13 | 14 | @dataclass 15 | class ConstraintAnalysisDTO: 16 | name: str 17 | weight: Annotated[HardSoftScore, ScoreSerializer] 18 | matches: list[MatchAnalysisDTO] 19 | score: Annotated[HardSoftScore, ScoreSerializer] 20 | -------------------------------------------------------------------------------- /python/vehicle-routing/src/vehicle_routing/solver.py: -------------------------------------------------------------------------------- 1 | from timefold.solver import SolverManager, SolutionManager 2 | from timefold.solver.config import (SolverConfig, ScoreDirectorFactoryConfig, 3 | TerminationConfig, Duration) 4 | 5 | from .domain import * 6 | from .constraints import define_constraints 7 | 8 | 9 | solver_config = SolverConfig( 10 | solution_class=VehicleRoutePlan, 11 | entity_class_list=[Vehicle, Visit], 12 | score_director_factory_config=ScoreDirectorFactoryConfig( 13 | constraint_provider_function=define_constraints 14 | ), 15 | termination_config=TerminationConfig( 16 | spent_limit=Duration(seconds=30) 17 | ) 18 | ) 19 | 20 | solver_manager = SolverManager.create(solver_config) 21 | solution_manager = SolutionManager.create(solver_manager) 22 | -------------------------------------------------------------------------------- /python/vehicle-routing/static/webjars/timefold/img/timefold-favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.14, written by Peter Selinger 2001-2017 9 | 10 | 12 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /python/vehicle-routing/tests/test_feasible.py: -------------------------------------------------------------------------------- 1 | from vehicle_routing.rest_api import json_to_vehicle_route_plan, app 2 | 3 | from fastapi.testclient import TestClient 4 | from time import sleep 5 | from pytest import fail 6 | 7 | client = TestClient(app) 8 | 9 | 10 | def test_feasible(): 11 | demo_data_response = client.get("/demo-data/PHILADELPHIA") 12 | assert demo_data_response.status_code == 200 13 | 14 | job_id_response = client.post("/route-plans", json=demo_data_response.json()) 15 | assert job_id_response.status_code == 200 16 | job_id = job_id_response.text[1:-1] 17 | 18 | ATTEMPTS = 1_000 19 | for _ in range(ATTEMPTS): 20 | sleep(0.1) 21 | route_plan_response = client.get(f"/route-plans/{job_id}") 22 | route_plan_json = route_plan_response.json() 23 | timetable = json_to_vehicle_route_plan(route_plan_json) 24 | if timetable.score is not None and timetable.score.is_feasible: 25 | stop_solving_response = client.delete(f"/route-plans/{job_id}") 26 | assert stop_solving_response.status_code == 200 27 | return 28 | 29 | client.delete(f"/route-plans/{job_id}") 30 | fail('solution is not feasible') 31 | -------------------------------------------------------------------------------- /python/vehicle-routing/vehicle-routing-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimefoldAI/timefold-quickstarts/f3e83432b4a142110e6aad7e26dc9fd37937ec25/python/vehicle-routing/vehicle-routing-screenshot.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'Timefold Solver Quickstarts' 2 | include ':java:hello-world' 3 | include ':java:school-timetabling' 4 | include ':java:spring-boot-integration' -------------------------------------------------------------------------------- /timefold-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimefoldAI/timefold-quickstarts/f3e83432b4a142110e6aad7e26dc9fd37937ec25/timefold-logo.png --------------------------------------------------------------------------------