This is a follow-up to this question.
I have two PlanningEntity classes like this:
@PlanningEntity
class Location {
private static long nextId = 0;
private long id = nextId++;
@PlanningVariable
Coordinates coordinates;
}
@PlanningEntity
class LocationList {
private static long nextId = 0;
private long id = nextId++;
@ShadowVariable(
variableListenerClass = LocationListUpdatingVariableListener.class,
sourceVariableName = "coordinates",
sourceEntityClass = Location.class)
List<Location> locations = ArrayList<>();
int weight;
}
The solution class looks like this:
@PlanningSolution
class Solution {
@PlanningEntityCollectionProperty
List<Location> locations;
@PlanningEntityCollectionProperty
List<LocationList> locationLists;
@ValueRangeProvider
@ProblemFactCollectionProperty
List<Coordinates> possibleCoordinates;
}
The shadow variable updater looks like this:
class LocationListUpdatingVariableListener implements VariableListener<Solution, Location> {
@Override
public void beforeEntityAdded(ScoreDirector<Solution> scoreDirector, Location location) {
// do nothing
}
@Override
public void afterEntityAdded(ScoreDirector<Solution> scoreDirector, Location location) {
updateLocationLists(scoreDirector, location);
}
@Override
public void beforeEntityRemoved(ScoreDirector<Solution> scoreDirector, Location location) {
// do nothing
}
@Override
public void afterEntityRemoved(ScoreDirector<Solution> scoreDirector, Location location) {
updateLocationLists(scoreDirector, location);
}
@Override
public void beforeVariableChanged(ScoreDirector<Solution> scoreDirector, Location location) {
// do nothing
}
@Override
public void afterVariableChanged(ScoreDirector<Solution> scoreDirector, Location location) {
updateLocationLists(scoreDirector, location);
}
protected void updateLocationLists(ScoreDirector<Solution> scoreDirector, Location location) {
Solution solution = scoreDirector.getWorkingSolution();
solution.locationLists.forEach(locationList -> {
for(int i = 0; i < locationList.locations.size(); i++) {
var locationListLocation = locationList.locations.get(i);
if(locationListLocation.id == location.id) {
scoreDirector.beforeVariableChanged(locationList, "locations");
locationList.locations.set(i, location);
scoreDirector.afterVariableChanged(locationList, "locations");
}
}
});
}
}
I have a constraint like this:
Constraint myConstraint(ConstraintFactory constraintFactory) {
return constraintFactory.forEach(LocationList.class)
.filter(locationList -> locationList.locations.size() == 2)
.join(Location.class)
.filter((locationList, location) -> locationList.locations.contains(location))
.groupby((locationList, location) -> locationList, ConstraintCollectors.toList((locationList, location) -> child))
.penalize(HardSoftScore.ONE_HARD, (locationList, locationsInList) -> weightedDistance(locationList.weight, locationsInList))
.asConstraint("Shorter total distance is better");
}
The weightedDistance
does something like add up all the distances between the assigned coordinates and multiplies by the weight. What it actually does is more complex, but the point is that it needs all the locations at once -- it can't be handled one at a time.
The part where I filter on 2 elements is not part of the actual constraint. I just put it in there to demonstrate the problem -- that the code makes it past there but somehow still only ends up with 1 location in locationsInList
.
I can actually pull the locations out of locationList
directly and it works, but I'm concerned that because locationsInList
doesn't have the right locations in it that this won't be tracked correctly by the solver.
It turns out this was caused by a timefold bug: https://github.com/TimefoldAI/timefold-solver/issues/309
If I run without brute force everything works as expected.