Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions docs/src/modules/ROOT/pages/service/constraint-weights.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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:
Expand Down Expand Up @@ -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();
Expand All @@ -108,6 +177,28 @@ ConstraintWeightOverrides<HardMediumSoftLongScore> 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].

Expand Down
52 changes: 52 additions & 0 deletions docs/src/modules/ROOT/pages/service/demo-data.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<TimetableInput, TimetableConfigOverrides>
) {
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<TimetableInput, TimetableConfigOverrides> {
return TODO("Generate basic request.")
}

fun generateComplexSet(): ModelRequest<TimetableInput, TimetableConfigOverrides> {
return TODO("Generate complex request.")
}
}

override fun demoMetaData(): List<DemoMetaData> {
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:

Expand Down
143 changes: 143 additions & 0 deletions docs/src/modules/ROOT/pages/service/exposing-metrics.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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
Expand Down Expand Up @@ -65,6 +93,37 @@ public class Timetable implements SolverModel<HardSoftScore>, InputMetricsAware<
// other Getters/Setters/Constructors excluded
}
----
--

Kotlin::
+
--
[source,kotlin,options="nowrap"]
----
@PlanningSolution
class Timetable : SolverModel<HardSoftScore>, InputMetricsAware<TimetableInputMetrics> {

@ProblemFactCollectionProperty
@ValueRangeProvider
val timeslots: List<Timeslot> = emptyList()

@PlanningEntityCollectionProperty
val lessons: List<Lesson> = 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
Expand All @@ -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(
Expand All @@ -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
Expand Down Expand Up @@ -129,12 +216,50 @@ public class Timetable implements SolverModel<HardSoftScore>, OutputMetricsAware
// other Getters/Setters/Constructors excluded
}
----
--

Kotlin::
+
--
[source,kotlin,options="nowrap"]
----
@PlanningSolution
class Timetable : SolverModel<HardSoftScore>, OutputMetricsAware<TimetableOutputMetrics> {

@ProblemFactCollectionProperty
@ValueRangeProvider
val timeslots: List<Timeslot> = emptyList()

@PlanningEntityCollectionProperty
val lessons: List<Lesson> = 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
Expand All @@ -146,3 +271,21 @@ public class Timetable implements SolverModel<HardSoftScore>,

}
----
--

Kotlin::
+
--
[source,kotlin,options="nowrap"]
----
@PlanningSolution
class Timetable : SolverModel<HardSoftScore>,
InputMetricsAware<TimetableInputMetrics>,
OutputMetricsAware<TimetableOutputMetrics> {

// fields, getInputMetrics(), getOutputMetrics() excluded

}
----
--
====
Loading