Constraint weights (optional)
When implementing a model, it’s important to set up the weights of the constraints correctly. Having good default weights makes the model easily reusable and is an expression of your modeling expertise.
|
This page describes features which are only relevant when running Timefold Solver as a service. The information on these pages may describe functionality which may be changed or even removed in a future release. |
Constraint weights are always an interpretation by the modeler. It might be that the consumer of the model would like to see the constraints weighed differently.
ModelConfigOverrides allows consumers of a model to tailor constraint weights and parameters to their use case.
| Be careful not to make your model overly configurable as that impacts usability. |
1. Adjusting constraint weights
Implement the ModelConfigOverrides interface. This is a marker interface, meaning it has no methods but can be discovered by the SDK.
The implementation should have fields that refer to specific constraints using the @ConstraintReference annotation.
To ensure both the constraint and this reference are the same, use a static field to keep the name of the constraint.
-
Java
-
Kotlin
public class TimetableConstraintProvider implements ConstraintProvider {
public static final String TEACHER_CONFLICT = "Teacher conflict";
public static final String ROOM_CONFLICT = "Room conflict";
Constraint roomConflict(ConstraintFactory constraintFactory) {
return constraintFactory
// constraint implementation excluded
.asConstraint(ROOM_CONFLICT);
}
Constraint teacherConflict(ConstraintFactory constraintFactory) {
return constraintFactory
// constraint implementation excluded
.asConstraint(TEACHER_CONFLICT);
}
// other constraints excluded
}
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
}
-
Java
-
Kotlin
public final class TimetableConfigOverrides implements ModelConfigOverrides {
public static final long DEFAULT_WEIGHT_ZERO = 0L;
public static final long DEFAULT_WEIGHT_ONE = 1L;
@ConstraintReference(TimetableConstraintProvider.TEACHER_CONFLICT)
private long teacherConflictWeight = DEFAULT_WEIGHT_ONE;
@ConstraintReference(TimetableConstraintProvider.ROOM_CONFLICT)
private long roomConflictWeight = DEFAULT_WEIGHT_ONE;
// getter/setter excluded
}
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:
{
"config": {
"run": {
"name": "run name",
<some fields excluded>
},
"model": {
"overrides": {
"teacherConflictWeight": 10
}
}
},
"modelInput" : "<ModelInput class as JSON>"
}
| Only allow weight overrides if it makes sense. Usually, it doesn’t make sense to allow weight overrides for hard constraints. |
Next, in the model converter, make sure to map these overrides to a solver specific ConstraintWeightOverrides object that must be on the @PlanningSolution class.
-
Java
-
Kotlin
TimetableConfigOverrides modelConfigOverrides = modelConfig.overrides();
ConstraintWeightOverrides<HardMediumSoftLongScore> constraintWeightOverrides = ConstraintWeightOverrides.of(
Map.ofEntries(
Map.entry(TimetableConstraintProvider.TEACHER_CONFLICT,
HardMediumSoftLongScore.ofHard(modelConfigOverrides.getTeacherConflictWeight())),
Map.entry(TimetableConstraintProvider.ROOM_CONFLICT,
HardMediumSoftLongScore.ofSoft(modelConfigOverrides.getRoomConflictWeight()))
)
);
solverModel.setConstraintWeightOverrides(constraintWeightOverrides);
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 Adjusting constraints at runtime.