diff --git a/docs/src/modules/ROOT/pages/service/constraint-weights.adoc b/docs/src/modules/ROOT/pages/service/constraint-weights.adoc index 2c0d1ed2a4..70429c0036 100644 --- a/docs/src/modules/ROOT/pages/service/constraint-weights.adoc +++ b/docs/src/modules/ROOT/pages/service/constraint-weights.adoc @@ -22,6 +22,11 @@ The implementation should have fields that refer to specific constraints using t To ensure both the constraint and this reference are the same, use a static field to keep the name of the constraint. .The example ConstraintProvider class. +[tabs] +==== +Java:: ++ +-- [source,java,options="nowrap"] ---- public class TimetableConstraintProvider implements ConstraintProvider { @@ -44,8 +49,44 @@ public class TimetableConstraintProvider implements ConstraintProvider { // other constraints excluded } ---- +-- + +Kotlin:: ++ +-- +[source,kotlin,options="nowrap"] +---- +class TimetableConstraintProvider : ConstraintProvider { + + companion object { + const val TEACHER_CONFLICT = "Teacher conflict" + const val ROOM_CONFLICT = "Room conflict" + } + + fun roomConflict(constraintFactory: ConstraintFactory): Constraint { + return constraintFactory + // constraint implementation excluded + .asConstraint(ROOM_CONFLICT) + } + + fun teacherConflict(constraintFactory: ConstraintFactory): Constraint { + return constraintFactory + // constraint implementation excluded + .asConstraint(TEACHER_CONFLICT) + } + + // other constraints excluded +} +---- +-- +==== .The ModelConfigOverrides class. +[tabs] +==== +Java:: ++ +-- [source,java,options="nowrap"] ---- public final class TimetableConfigOverrides implements ModelConfigOverrides { @@ -63,6 +104,29 @@ public final class TimetableConfigOverrides implements ModelConfigOverrides { } ---- +-- + +Kotlin:: ++ +-- +[source,kotlin,options="nowrap"] +---- +data class TimetableConfigOverrides( + @ConstraintReference(TimetableConstraintProvider.TEACHER_CONFLICT) + val teacherConflictWeight: Long = DEFAULT_WEIGHT_ONE, + @ConstraintReference(TimetableConstraintProvider.ROOM_CONFLICT) + val roomConflictWeight: Long = DEFAULT_WEIGHT_ONE +) : ModelConfigOverrides { + + companion object { + const val DEFAULT_WEIGHT_ZERO = 0L + const val DEFAULT_WEIGHT_ONE = 1L + } + +} +---- +-- +==== The default constraint weight for these constraints is `1`. This can now be overridden by the consumer by passing in the model overrides object in a request. For example, to make the Teacher conflict 10 times more impactful, override the weight to 10: @@ -93,6 +157,11 @@ Usually, it doesn't make sense to allow weight overrides for _hard_ constraints. Next, in the xref:./rest-api.adoc#modelConverter[model converter], make sure to map these overrides to a solver specific `ConstraintWeightOverrides` object that must be on the `@PlanningSolution` class. .As part of the ModelConvertor +[tabs] +==== +Java:: ++ +-- [source,java,options="nowrap"] ---- TimetableConfigOverrides modelConfigOverrides = modelConfig.overrides(); @@ -108,6 +177,28 @@ ConstraintWeightOverrides constraintWeightOverrides = C solverModel.setConstraintWeightOverrides(constraintWeightOverrides); ---- +-- + +Kotlin:: ++ +-- +[source,kotlin,options="nowrap"] +---- +val modelConfigOverrides = modelConfig.overrides() + +val constraintWeightOverrides = ConstraintWeightOverrides.of( + mapOf( + TimetableConstraintProvider.TEACHER_CONFLICT to + HardMediumSoftLongScore.ofHard(modelConfigOverrides.teacherConflictWeight), + TimetableConstraintProvider.ROOM_CONFLICT to + HardMediumSoftLongScore.ofSoft(modelConfigOverrides.roomConflictWeight) + ) +) + +solverModel.constraintWeightOverrides = constraintWeightOverrides +---- +-- +==== For more information, see xref:../constraints-and-score/constraint-configuration.adoc#constraintConfiguration[Adjusting constraints at runtime]. diff --git a/docs/src/modules/ROOT/pages/service/demo-data.adoc b/docs/src/modules/ROOT/pages/service/demo-data.adoc index b798b70344..e32e6ee85e 100644 --- a/docs/src/modules/ROOT/pages/service/demo-data.adoc +++ b/docs/src/modules/ROOT/pages/service/demo-data.adoc @@ -23,6 +23,11 @@ This interface requires you to implement 2 methods: Implementations of this interface must be dependency free meaning simple instantiation (even with reflection) of this class is enough to generate demo data. ==== +[tabs] +==== +Java:: ++ +-- [source,java,options="nowrap"] ---- @ApplicationScoped @@ -79,6 +84,53 @@ public class TimetableDemoDataGenerator implements DemoDataGenerator { } } ---- +-- + +Kotlin:: ++ +-- +[source,kotlin,options="nowrap"] +---- +@ApplicationScoped +class TimetableDemoDataGenerator : DemoDataGenerator { + + enum class DemoDataKind( + private val metaData: DemoMetaData, + private val requestFunction: (DemoDataKind) -> ModelRequest + ) { + BASIC( + DemoMetaData("BASIC", "SHORT_DESCRIPTION", "LONG_DESCRIPTION", listOf("TAGS"), listOf()), + { it.generateBasicDemoData() } // could also delegate to another class instead + ), + COMPLEX_SET( + DemoMetaData("COMPLEX_SET", "SHORT_DESCRIPTION", "LONG_DESCRIPTION", listOf("TAGS"), listOf()), + { it.generateComplexSet() } + ); + + fun getMetaData(): DemoMetaData = metaData + + fun getDemoData(): DemoData = DemoData(metaData, requestFunction(this)) + + fun generateBasicDemoData(): ModelRequest { + return TODO("Generate basic request.") + } + + fun generateComplexSet(): ModelRequest { + return TODO("Generate complex request.") + } + } + + override fun demoMetaData(): List { + return DemoDataKind.entries.map { it.getMetaData() } + } + + override fun generateDemoData(demoDataId: String): DemoData { + return DemoDataKind.fromString(demoDataId).getDemoData() + } +} +---- +-- +==== With this interface implemented, Timefold Solver will automatically expose these methods as REST endpoints: diff --git a/docs/src/modules/ROOT/pages/service/exposing-metrics.adoc b/docs/src/modules/ROOT/pages/service/exposing-metrics.adoc index 1dd8659cb7..0c7388d0e2 100644 --- a/docs/src/modules/ROOT/pages/service/exposing-metrics.adoc +++ b/docs/src/modules/ROOT/pages/service/exposing-metrics.adoc @@ -22,6 +22,11 @@ It is therefore necessary to add xref:./rest-api.adoc#openAPISpecification[OpenA ==== .Example for School Timetabling +[tabs] +==== +Java:: ++ +-- [source,java,options="nowrap"] ---- public record TimetableInputMetrics( @@ -33,10 +38,33 @@ public record TimetableInputMetrics( type = SchemaType.NUMBER, example = "30", readOnly = true) int timeslots ) implements ModelInputMetrics {} ---- +-- + +Kotlin:: ++ +-- +[source,kotlin,options="nowrap"] +---- +data class TimetableInputMetrics( + @JsonFormat(shape = JsonFormat.Shape.NUMBER_INT) @Schema(name = "lessons", title = "Lessons", + format = DataFormat.Values.NUMBER, description = "The number of lessons submitted in the input dataset.", + type = SchemaType.NUMBER, example = "10", readOnly = true) val lessons: Int, + @JsonFormat(shape = JsonFormat.Shape.NUMBER_INT) @Schema(name = "timeslots", title = "Timeslots", + format = DataFormat.Values.NUMBER, description = "The number of timeslots submitted in the input dataset.", + type = SchemaType.NUMBER, example = "30", readOnly = true) val timeslots: Int +) : ModelInputMetrics +---- +-- +==== Next, the `SolverModel` should implement the `InputMetricsAware` interface and construct the defined `ModelInputMetrics` object. .Example for School Timetabling +[tabs] +==== +Java:: ++ +-- [source,java,options="nowrap"] ---- @PlanningSolution @@ -65,6 +93,37 @@ public class Timetable implements SolverModel, InputMetricsAware< // other Getters/Setters/Constructors excluded } ---- +-- + +Kotlin:: ++ +-- +[source,kotlin,options="nowrap"] +---- +@PlanningSolution +class Timetable : SolverModel, InputMetricsAware { + + @ProblemFactCollectionProperty + @ValueRangeProvider + val timeslots: List = emptyList() + + @PlanningEntityCollectionProperty + val lessons: List = emptyList() + + private var _score: HardSoftScore? = null + + @PlanningScore + override fun getScore(): HardSoftScore? = _score + + override fun getInputMetrics(): TimetableInputMetrics { + return TimetableInputMetrics(lessons.size, timeslots.size) + } + + // other Getters/Setters/Constructors excluded +} +---- +-- +==== [#modelOutputMetrics] == Output metrics @@ -82,6 +141,11 @@ It is therefore necessary to add xref:./rest-api.adoc#openAPISpecification[OpenA ==== .Example for School Timetabling +[tabs] +==== +Java:: ++ +-- [source,java,options="nowrap"] ---- public record TimetableOutputMetrics( @@ -93,10 +157,33 @@ public record TimetableOutputMetrics( type = SchemaType.NUMBER, example = "3", readOnly = true) int maxConsecutiveLessons ) implements ModelOutputMetrics {} ---- +-- + +Kotlin:: ++ +-- +[source,kotlin,options="nowrap"] +---- +data class TimetableOutputMetrics( + @JsonFormat(shape = JsonFormat.Shape.NUMBER_INT) @Schema(name = "unassignedLessons", title = "Unassigned lessons", + format = DataFormat.Values.NUMBER, description = "The number of lessons that could not be assigned a timeslot or room.", + type = SchemaType.NUMBER, example = "0", readOnly = true) val unassignedLessons: Int, + @JsonFormat(shape = JsonFormat.Shape.NUMBER_INT) @Schema(name = "maxConsecutiveLessons", title = "Max consecutive lessons", + format = DataFormat.Values.NUMBER, description = "The maximum number of consecutive lessons assigned to any single teacher.", + type = SchemaType.NUMBER, example = "3", readOnly = true) val maxConsecutiveLessons: Int +) : ModelOutputMetrics +---- +-- +==== Next, the `SolverModel` should implement the `OutputMetricsAware` interface and construct the defined `ModelOutputMetrics` object from the solved state. .Example for School Timetabling +[tabs] +==== +Java:: ++ +-- [source,java,options="nowrap"] ---- @PlanningSolution @@ -129,12 +216,50 @@ public class Timetable implements SolverModel, OutputMetricsAware // other Getters/Setters/Constructors excluded } ---- +-- + +Kotlin:: ++ +-- +[source,kotlin,options="nowrap"] +---- +@PlanningSolution +class Timetable : SolverModel, OutputMetricsAware { + + @ProblemFactCollectionProperty + @ValueRangeProvider + val timeslots: List = emptyList() + + @PlanningEntityCollectionProperty + val lessons: List = emptyList() + + private var _score: HardSoftScore? = null + + @PlanningScore + override fun getScore(): HardSoftScore? = _score + + override fun getOutputMetrics(): TimetableOutputMetrics { + val unassigned = lessons.count { it.timeslot == null || it.room == null } + val maxConsecutive = computeMaxConsecutiveLessons(lessons) + return TimetableOutputMetrics(unassigned, maxConsecutive) + } + + // other Getters/Setters/Constructors excluded +} +---- +-- +==== [#combiningMetrics] == Combining input and output metrics A `SolverModel` can implement both `InputMetricsAware` and `OutputMetricsAware` at the same time. +[tabs] +==== +Java:: ++ +-- [source,java,options="nowrap"] ---- @PlanningSolution @@ -146,3 +271,21 @@ public class Timetable implements SolverModel, } ---- +-- + +Kotlin:: ++ +-- +[source,kotlin,options="nowrap"] +---- +@PlanningSolution +class Timetable : SolverModel, + InputMetricsAware, + OutputMetricsAware { + + // fields, getInputMetrics(), getOutputMetrics() excluded + +} +---- +-- +==== diff --git a/docs/src/modules/ROOT/pages/service/modeling-changes.adoc b/docs/src/modules/ROOT/pages/service/modeling-changes.adoc index 28ecb48f94..82e7c9421f 100644 --- a/docs/src/modules/ROOT/pages/service/modeling-changes.adoc +++ b/docs/src/modules/ROOT/pages/service/modeling-changes.adoc @@ -14,6 +14,11 @@ include::_preview-note.adoc[] Your class which is annotated by `@PlanningSolution` should implement the `SolverModel` interface. .Example for School Timetabling +[tabs] +==== +Java:: ++ +-- [source,java,options="nowrap"] ---- @PlanningSolution @@ -37,6 +42,33 @@ public class Timetable implements SolverModel { // other Getters/Setters/Constructors excluded } ---- +-- + +Kotlin:: ++ +-- +[source,kotlin,options="nowrap"] +---- +@PlanningSolution +class Timetable : SolverModel { + + @ProblemFactCollectionProperty + @ValueRangeProvider + val timeslots: List = emptyList() + + @PlanningEntityCollectionProperty + val lessons: List = emptyList() + + private var _score: HardSoftScore? = null + + @PlanningScore + override fun getScore(): HardSoftScore? = _score + + // other Getters/Setters/Constructors excluded +} +---- +-- +==== In case you don't want control over the `score` class used, you can also extend the `AbstractSimpleModel` as used in the xref:../quickstart/service/getting-started.adoc[getting started guide]. @@ -53,9 +85,13 @@ Especially when the field depends on external information or is difficult to com In this example, we enrich the Timetable PlanningSolution described above by filling in the "isHoliday" field for all Timeslot objects. .Timeslot class for the School Timetabling example +[tabs] +==== +Java:: ++ +-- [source,java,options="nowrap"] ---- - public class Timeslot { private LocalDateTime startTime; @@ -70,10 +106,31 @@ public class Timeslot { // other Getters/Setters/Constructors excluded } ---- +-- + +Kotlin:: ++ +-- +[source,kotlin,options="nowrap"] +---- +data class Timeslot( + val startTime: LocalDateTime? = null, + val endTime: LocalDateTime? = null +) { + var isHoliday: Boolean = false +} +---- +-- +==== Enrichment of the SolverModel is possible by implementing a `SolverModelEnricher`. .Timeslot Enricher for the School Timetabling example +[tabs] +==== +Java:: ++ +-- [source,java,options="nowrap"] ---- @ApplicationScoped @@ -94,8 +151,33 @@ public class TimeslotHolidayEnricher implements SolverModelEnricher { return false; } } +---- +-- + +Kotlin:: ++ +-- +[source,kotlin,options="nowrap"] +---- +@ApplicationScoped +class TimeslotHolidayEnricher : SolverModelEnricher { + + override fun enrich(solverModel: Timetable): Timetable { + for (timeslot in solverModel.timeslots) { + timeslot.isHoliday = overlapsWithKnownHoliday(timeslot.startTime, timeslot.endTime) + } + + return solverModel + } + private fun overlapsWithKnownHoliday(start: LocalDateTime?, end: LocalDateTime?): Boolean { + // Implementation excluded; potentially call external service / database. + return false + } +} ---- +-- +==== Next, register a `SolverModelEnrichmentDirector` implementation. This class allows you to determine the order in which enrichers are executed. @@ -103,6 +185,11 @@ This might be important when 1 of your custom enrichers depends on an enricher p //TODO add link to maps component docs https://github.com/TimefoldAI/timefold-solver/issues/2348 .Timetable Enrichment Director for the School Timetabling example +[tabs] +==== +Java:: ++ +-- [source,java,options="nowrap"] ---- @ApplicationScoped @@ -122,4 +209,25 @@ public class TimetableEnrichmentDirector implements SolverModelEnrichmentDirecto return enrichedModel; } } ----- \ No newline at end of file +---- +-- + +Kotlin:: ++ +-- +[source,kotlin,options="nowrap"] +---- +@ApplicationScoped +class TimetableEnrichmentDirector @Inject constructor( + private val timeslotEnricher: TimeslotHolidayEnricher +) : SolverModelEnrichmentDirector { + + override fun enrich(solverModel: Timetable): Timetable { + val enrichedModel = timeslotEnricher.enrich(solverModel) + // Additional enrichers. + return enrichedModel + } +} +---- +-- +==== \ No newline at end of file diff --git a/docs/src/modules/ROOT/pages/service/rest-api.adoc b/docs/src/modules/ROOT/pages/service/rest-api.adoc index 639d5bfa6c..9bece72bb2 100644 --- a/docs/src/modules/ROOT/pages/service/rest-api.adoc +++ b/docs/src/modules/ROOT/pages/service/rest-api.adoc @@ -18,6 +18,11 @@ The following are expected. - An interface which extends `ModelRest` to define the root path of the REST API. For example, in the case of School Timetabling, those classes could look as follows. +[tabs] +==== +Java:: ++ +-- [source,java,options="nowrap"] ---- // TimetableDto.java @@ -40,6 +45,31 @@ public class TimetableDto implements ModelInput, ModelOutput { public interface TimetableSchedulingResource extends ModelRest { } ---- +-- + +Kotlin:: ++ +-- +[source,kotlin,options="nowrap"] +---- +// TimetableDto.kt +class TimetableDto( + @Schema(required = true, description = "The unique identifier of timetable") + var name: String = "", + @Schema(required = true, description = "List of timeslots") + var timeslots: List = emptyList(), + @Schema(required = true, description = "List of lessons") + var lessons: List = emptyList() +) : ModelInput, ModelOutput + +// TimetableSchedulingResource.kt +@Tag(name = "School Timetabling", + description = "School timetabling service assigning lessons to timeslots.") // OpenAPI documentation annotation +@Path("/v1/timetables") //sets the root path +interface TimetableSchedulingResource : ModelRest +---- +-- +==== Note how the `ModelInput` and `ModelOutput` interface can be placed on the same class. @@ -199,6 +229,11 @@ it's often beneficial to decouple the classes used for the REST API interactions To convert between `ModelInput` / `ModelOutput` and the `SolverModel`, a `ModelConvertor` implementation can be provided. +[tabs] +==== +Java:: ++ +-- [source,java,options="nowrap"] ---- @ApplicationScoped @@ -221,6 +256,32 @@ public class TimetableConvertor implements ModelConvertor { + + override fun toSolverModel(modelInput: TimetableDto, modelConfig: ModelConfig, + lastModelOutput: Optional): Timetable { + return TODO("Mapping logic") + } + + override fun toModelOutput(solverModel: Timetable): TimetableDto { + return TODO("Mapping logic") + } + + override fun applyOutputToInput(modelInput: TimetableDto, modelOutput: TimetableDto): TimetableDto { + return TODO("Mapping logic") + } +} +---- +-- +==== [NOTE] ==== @@ -235,6 +296,11 @@ By default, validation on the input is based on the xref:./rest-api.adoc#openAPI Additional validations can be added by implementing the `ModelValidator` interface. [#timetableValidatorExample] +[tabs] +==== +Java:: ++ +-- [source,java,options="nowrap"] ---- @ApplicationScoped @@ -269,6 +335,39 @@ public enum TimetableValidationIssue { } } ---- +-- + +Kotlin:: ++ +-- +[source,kotlin,options="nowrap"] +---- +@ApplicationScoped +class TimetableValidator : ModelValidator { + + override fun validate(validationBuilder: ValidationBuilder, input: TimetableDto, modelConfig: ModelConfig) { + + //validation logic here, simplified example here. + if (hasDuplicateTeacherNames(input)) { + validationBuilder.addIssue(TimetableValidationIssue.DUPLICATE_TEACHER.asIssueType(), DuplicateTeacherDetail("Ann")) + } + } +} + +enum class TimetableValidationIssue( + private val issueType: IssueType +) { + + DUPLICATE_TEACHER(IssueType(IssueCode.of("DUPLICATE_TEACHER"), IssueSeverity.ERROR, + "Duplicate teacher names found.")); + + fun asIssueType(): IssueType = issueType + + data class DuplicateTeacherDetail(val teacherName: String) : IssueDetail +} +---- +-- +==== The `ValidationBuilder` supports issue types of different severity: - `IssueSeverity.ERROR`: error level: processing cannot continue. Results in a BAD_REQUEST (400) HTTP error. @@ -338,6 +437,11 @@ You can add custom endpoints in the `ModelRest` implementation. For example, if you want to extend the Timetable REST API, you can add custom https://jakarta.ee/learn/docs/jakartaee-tutorial/current/websvcs/rest/rest.html[Jakarta REST endpoints]. These endpoints should also be documented appropriately for the automatically generated xref:#openAPISpecification[OpenAPI specification]. +[tabs] +==== +Java:: ++ +-- [source,java,options="nowrap"] ---- @Tag(name = "School Timetabling", @@ -363,6 +467,38 @@ public interface TimetableSchedulingResource extends ModelRest { } } ---- +-- + +Kotlin:: ++ +-- +[source,kotlin,options="nowrap"] +---- +@Tag(name = "School Timetabling", + description = "School timetabling service assigning lessons to timeslots.") //OpenAPI documentation annotation +@Path("/v1/timetables") //sets the root path +interface TimetableSchedulingResource : ModelRest { + + @APIResponses(value = [ + APIResponse(responseCode = "500", description = "In case of processing errors", + content = [Content(mediaType = MediaType.APPLICATION_JSON, + schema = Schema(implementation = ErrorInfo::class))]), + APIResponse(responseCode = "200", description = "List of all teachers in all optimization runs.", + content = [Content(mediaType = MediaType.APPLICATION_JSON, + schema = Schema(implementation = ListTeachersResponseDto::class))])]) + @Operation(operationId = "list-teachers-in-solver-model", + summary = "Lists all teachers in the solver model.") + @GET + @Path("/insights/teachers") + @Produces(MediaType.APPLICATION_JSON) + fun insightsTeachers(): Response { + val teachers = TODO("Get the list of teachers") + return Response.ok(teachers).build() + } +} +---- +-- +==== [#openAPISpecification] == OpenAPI specification