diff --git a/tests/test_solution_manager.py b/tests/test_solution_manager.py index 3a71113..388deb5 100644 --- a/tests/test_solution_manager.py +++ b/tests/test_solution_manager.py @@ -127,6 +127,21 @@ def assert_score_analysis(problem: Solution, score_analysis: ScoreAnalysis): assert_constraint_analysis(problem, constraint_analysis) +def assert_score_analysis_summary(score_analysis: ScoreAnalysis): + summary = score_analysis.summary + assert "Explanation of score (3):" in summary + assert "Constraint matches:" in summary + assert "3: constraint (Maximize Value) has 3 matches:" in summary + assert "1: justified with" in summary + + match = score_analysis.constraint_analyses[0] + match_summary = match.summary + assert "Explanation of score (3):" in match_summary + assert "Constraint matches:" in match_summary + assert "3: constraint (Maximize Value) has 3 matches:" in match_summary + assert "1: justified with" in match_summary + + def assert_solution_manager(solution_manager: SolutionManager[Solution]): problem: Solution = Solution([Entity('A', 1), Entity('B', 1), Entity('C', 1)], [1, 2, 3]) assert problem.score is None @@ -140,6 +155,9 @@ def assert_solution_manager(solution_manager: SolutionManager[Solution]): score_analysis = solution_manager.analyze(problem) assert_score_analysis(problem, score_analysis) + score_analysis = solution_manager.analyze(problem) + assert_score_analysis_summary(score_analysis) + def test_solver_manager_score_manager(): with SolverManager.create(SolverFactory.create(solver_config)) as solver_manager: diff --git a/timefold-solver-python-core/src/main/python/score/_score_analysis.py b/timefold-solver-python-core/src/main/python/score/_score_analysis.py index 06b1874..f2e7188 100644 --- a/timefold-solver-python-core/src/main/python/score/_score_analysis.py +++ b/timefold-solver-python-core/src/main/python/score/_score_analysis.py @@ -445,7 +445,9 @@ class ConstraintAnalysis(Generic[Score_]): but still non-zero constraint weight; non-empty if constraint has matches. This is a list to simplify access to individual elements, but it contains no duplicates just like `set` wouldn't. - + summary : str + Returns a diagnostic text + that explains part of the score quality through the ConstraintAnalysis API. """ _delegate: '_JavaConstraintAnalysis[Score_]' @@ -453,6 +455,9 @@ def __init__(self, delegate: '_JavaConstraintAnalysis[Score_]'): self._delegate = delegate delegate.constraintRef() + def __str__(self): + return self.summary + @property def constraint_ref(self) -> ConstraintRef: return ConstraintRef(package_name=self._delegate.constraintRef().packageName(), @@ -479,6 +484,9 @@ def matches(self) -> list[MatchAnalysis[Score_]]: def score(self) -> Score_: return to_python_score(self._delegate.score()) + @property + def summary(self) -> str: + return self._delegate.summarize() class ScoreAnalysis: """ @@ -510,6 +518,16 @@ class ScoreAnalysis: constraint_analyses : list[ConstraintAnalysis] Individual ConstraintAnalysis instances that make up this ScoreAnalysis. + summary : str + Returns a diagnostic text + that explains the solution through the `ConstraintMatch` API + to identify which constraints cause that score quality. + + In case of an infeasible solution, this can help diagnose the cause of that. + Do not parse the return value, its format may change without warning. + Instead, to provide this information in a UI or a service, + use `constraint_analyses` and convert those into a domain-specific API. + Notes ----- the constructors of this record are off-limits. @@ -520,6 +538,9 @@ class ScoreAnalysis: def __init__(self, delegate: '_JavaScoreAnalysis'): self._delegate = delegate + def __str__(self): + return self.summary + @property def score(self) -> 'Score': return to_python_score(self._delegate.score()) @@ -541,6 +562,10 @@ def constraint_analyses(self) -> list[ConstraintAnalysis]: list['_JavaConstraintAnalysis[Score]'], self._delegate.constraintAnalyses()) ] + @property + def summary(self) -> str: + return self._delegate.summarize() + __all__ = ['ScoreExplanation', 'ConstraintRef', 'ConstraintMatch', 'ConstraintMatchTotal',