in core/optaplanner-core-impl/src/main/java/org/optaplanner/core/impl/score/director/AbstractScoreDirector.java [695:785]
protected String buildScoreCorruptionAnalysis(InnerScoreDirector<Solution_, Score_> uncorruptedScoreDirector,
boolean predicted) {
if (!isConstraintMatchEnabled() || !uncorruptedScoreDirector.isConstraintMatchEnabled()) {
return "Score corruption analysis could not be generated because"
+ " either corrupted constraintMatchEnabled (" + isConstraintMatchEnabled()
+ ") or uncorrupted constraintMatchEnabled (" + uncorruptedScoreDirector.isConstraintMatchEnabled()
+ ") is disabled.\n"
+ " Check your score constraints manually.";
}
Map<String, ConstraintMatchTotal<Score_>> constraintMatchTotalMap = getConstraintMatchTotalMap();
Map<Object, Set<ConstraintMatch<Score_>>> corruptedMap =
createConstraintMatchMap(constraintMatchTotalMap.values());
Map<String, ConstraintMatchTotal<Score_>> uncorruptedConstraintMatchTotalMap =
uncorruptedScoreDirector.getConstraintMatchTotalMap();
Map<Object, Set<ConstraintMatch<Score_>>> uncorruptedMap =
createConstraintMatchMap(uncorruptedConstraintMatchTotalMap.values());
Set<ConstraintMatch<Score_>> excessSet = new LinkedHashSet<>();
Set<ConstraintMatch<Score_>> missingSet = new LinkedHashSet<>();
uncorruptedMap.forEach((key, uncorruptedMatches) -> {
Set<ConstraintMatch<Score_>> corruptedMatches = corruptedMap.getOrDefault(key, Collections.emptySet());
if (corruptedMatches.isEmpty()) {
missingSet.addAll(uncorruptedMatches);
return;
}
updateExcessAndMissingConstraintMatches(uncorruptedMatches, corruptedMatches, excessSet, missingSet);
});
corruptedMap.forEach((key, corruptedMatches) -> {
Set<ConstraintMatch<Score_>> uncorruptedMatches = uncorruptedMap.getOrDefault(key, Collections.emptySet());
if (uncorruptedMatches.isEmpty()) {
excessSet.addAll(corruptedMatches);
return;
}
updateExcessAndMissingConstraintMatches(uncorruptedMatches, corruptedMatches, excessSet, missingSet);
});
final int CONSTRAINT_MATCH_DISPLAY_LIMIT = 8;
StringBuilder analysis = new StringBuilder();
analysis.append("Score corruption analysis:\n");
// If predicted, the score calculation might have happened on another thread, so a different ScoreDirector
// so there is no guarantee that the working ScoreDirector is the corrupted ScoreDirector
String workingLabel = predicted ? "working" : "corrupted";
if (excessSet.isEmpty()) {
analysis.append(" The ").append(workingLabel)
.append(" scoreDirector has no ConstraintMatch(s) which are in excess.\n");
} else {
analysis.append(" The ").append(workingLabel).append(" scoreDirector has ").append(excessSet.size())
.append(" ConstraintMatch(s) which are in excess (and should not be there):\n");
excessSet.stream().sorted().limit(CONSTRAINT_MATCH_DISPLAY_LIMIT)
.forEach(constraintMatch -> analysis.append(" ").append(constraintMatch).append("\n"));
if (excessSet.size() >= CONSTRAINT_MATCH_DISPLAY_LIMIT) {
analysis.append(" ... ").append(excessSet.size() - CONSTRAINT_MATCH_DISPLAY_LIMIT)
.append(" more\n");
}
}
if (missingSet.isEmpty()) {
analysis.append(" The ").append(workingLabel)
.append(" scoreDirector has no ConstraintMatch(s) which are missing.\n");
} else {
analysis.append(" The ").append(workingLabel).append(" scoreDirector has ").append(missingSet.size())
.append(" ConstraintMatch(s) which are missing:\n");
missingSet.stream().sorted().limit(CONSTRAINT_MATCH_DISPLAY_LIMIT)
.forEach(constraintMatch -> analysis.append(" ").append(constraintMatch).append("\n"));
if (missingSet.size() >= CONSTRAINT_MATCH_DISPLAY_LIMIT) {
analysis.append(" ... ").append(missingSet.size() - CONSTRAINT_MATCH_DISPLAY_LIMIT)
.append(" more\n");
}
}
if (!missingSet.isEmpty() || !excessSet.isEmpty()) {
analysis.append(" Maybe there is a bug in the score constraints of those ConstraintMatch(s).\n");
analysis.append(
" Maybe a score constraint doesn't select all the entities it depends on, but finds some through a reference in a selected entity."
+ " This corrupts incremental score calculation, because the constraint is not re-evaluated if such a non-selected entity changes.");
} else {
if (predicted) {
analysis.append(" If multithreaded solving is active,"
+ " the working scoreDirector is probably not the corrupted scoreDirector.\n");
analysis.append(" If multithreaded solving is active, maybe the rebase() method of the move is bugged.\n");
analysis.append(" If multithreaded solving is active,"
+ " maybe a VariableListener affected the moveThread's workingSolution after doing and undoing a move,"
+ " but this didn't happen here on the solverThread, so we can't detect it.");
} else {
analysis.append(" Impossible state. Maybe this is a bug in the scoreDirector (").append(getClass())
.append(").");
}
}
return analysis.toString();
}