protected String buildScoreCorruptionAnalysis()

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();
    }