Adjusting constraint weights
Deciding the correct weight and level for each constraint is not easy. It often involves negotiating with different stakeholders and their priorities. Furthermore, quantifying the impact of soft constraints is often a new experience for business managers, so they’ll need a number of iterations to get it right.
Don’t get stuck between a rock and a hard place. Provide a UI to adjust the constraint weights and visualize the resulting solution, so the business managers can tweak the constraint weights themselves:
1. Defining constraint weights
Let’s define three constraints:
-
Constraint with a name of
Vehicle capacityand a weight of1hard. -
Constraint with a name of
Service finished after max end time, also with a weight of1hard. -
Constraint with a name of
Minimize travel timeand a weight of1soft.
Using the Constraint Streams API, this is done as follows:
-
Java
public class VehicleRoutingConstraintProvider implements ConstraintProvider {
public static final String VEHICLE_CAPACITY = "Vehicle capacity";
public static final String SERVICE_FINISHED_AFTER_MAX_END_TIME = "Service finished after max end time";
public static final String MINIMIZE_TRAVEL_TIME = "Minimize travel time";
...
@Override
public Constraint[] defineConstraints(ConstraintFactory factory) {
return new Constraint[] {
vehicleCapacity(factory),
serviceFinishedAfterMaxEndTime(factory),
minimizeTravelTime(factory)
};
}
Constraint vehicleCapacity(ConstraintFactory factory) {
return factory.forEach(Vehicle.class)
...
.penalize(HardSoftScore.ONE_HARD, ...)
.asConstraint(VEHICLE_CAPACITY);
}
Constraint serviceFinishedAfterMaxEndTime(ConstraintFactory factory) {
return factory.forEach(Visit.class)
...
.penalize(HardSoftScore.ONE_HARD, ...)
.asConstraint(SERVICE_FINISHED_AFTER_MAX_END_TIME);
}
Constraint minimizeTravelTime(ConstraintFactory factory) {
return factory.forEach(Vehicle.class)
...
.penalize(HardSoftScore.ONE_SOFT, ...)
.asConstraint(MINIMIZE_TRAVEL_TIME);
}
}
Using static string constants for constraint names is recommended.
It prevents typos and ensures the constraint name is consistent across the ConstraintProvider
and any code that references constraints by name, such as when applying overrides.
|
2. Enabling weight overrides
Without anything else, the constraint weights are fixed to the values specified in the ConstraintProvider.
To be able to override these weights at runtime, add a ConstraintWeightOverrides field
to the planning solution class:
-
Java
@PlanningSolution
public class VehicleRoutePlan {
...
ConstraintWeightOverrides<HardSoftScore> constraintWeightOverrides;
void setConstraintWeightOverrides(ConstraintWeightOverrides<HardSoftScore> constraintWeightOverrides) {
this.constraintWeightOverrides = constraintWeightOverrides;
}
ConstraintWeightOverrides<HardSoftScore> getConstraintWeightOverrides() {
return constraintWeightOverrides;
}
...
}
The field will be automatically exposed as a problem fact,
there is no need to add a @ProblemFactProperty annotation.
3. Applying overrides
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 to their use case.
| Be careful not to make your model overly configurable as that impacts usability. Usually, it doesn’t make sense to allow weight overrides for hard constraints. |
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,
referencing the static string constants defined in the ConstraintProvider:
-
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 the 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>"
}
Next, in the model converter, map the overrides
to a ConstraintWeightOverrides object and set it on the @PlanningSolution class
as described above:
-
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