Skip to content
This repository was archived by the owner on Jul 17, 2024. It is now read-only.

Commit 537b5f0

Browse files
committed
feat: add summary to score analysis
1 parent e6bb0f7 commit 537b5f0

File tree

2 files changed

+45
-1
lines changed

2 files changed

+45
-1
lines changed

Diff for: tests/test_solution_manager.py

+19
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from dataclasses import dataclass, field
77
from typing import Annotated, List
8+
import difflib
89

910

1011
@planning_entity
@@ -127,6 +128,21 @@ def assert_score_analysis(problem: Solution, score_analysis: ScoreAnalysis):
127128
assert_constraint_analysis(problem, constraint_analysis)
128129

129130

131+
def assert_score_analysis_summary(score_analysis: ScoreAnalysis):
132+
summary = score_analysis.summary
133+
assert "Explanation of score (3):" in summary
134+
assert "Constraint matches:" in summary
135+
assert "3: constraint (Maximize Value) has 3 matches:" in summary
136+
assert "1: justified with" in summary
137+
138+
match = score_analysis.constraint_analyses[0]
139+
match_summary = match.summary
140+
assert "Explanation of score (3):" in match_summary
141+
assert "Constraint matches:" in match_summary
142+
assert "3: constraint (Maximize Value) has 3 matches:" in match_summary
143+
assert "1: justified with" in match_summary
144+
145+
130146
def assert_solution_manager(solution_manager: SolutionManager[Solution]):
131147
problem: Solution = Solution([Entity('A', 1), Entity('B', 1), Entity('C', 1)], [1, 2, 3])
132148
assert problem.score is None
@@ -140,6 +156,9 @@ def assert_solution_manager(solution_manager: SolutionManager[Solution]):
140156
score_analysis = solution_manager.analyze(problem)
141157
assert_score_analysis(problem, score_analysis)
142158

159+
score_analysis = solution_manager.analyze(problem)
160+
assert_score_analysis_summary(score_analysis)
161+
143162

144163
def test_solver_manager_score_manager():
145164
with SolverManager.create(SolverFactory.create(solver_config)) as solver_manager:

Diff for: timefold-solver-python-core/src/main/python/score/_score_analysis.py

+26-1
Original file line numberDiff line numberDiff line change
@@ -445,14 +445,19 @@ class ConstraintAnalysis(Generic[Score_]):
445445
but still non-zero constraint weight; non-empty if constraint has matches.
446446
This is a list to simplify access to individual elements,
447447
but it contains no duplicates just like `set` wouldn't.
448-
448+
summary : str
449+
Returns a diagnostic text
450+
that explains part of the score quality through the ConstraintAnalysis API.
449451
"""
450452
_delegate: '_JavaConstraintAnalysis[Score_]'
451453

452454
def __init__(self, delegate: '_JavaConstraintAnalysis[Score_]'):
453455
self._delegate = delegate
454456
delegate.constraintRef()
455457

458+
def __str__(self):
459+
return self.summary
460+
456461
@property
457462
def constraint_ref(self) -> ConstraintRef:
458463
return ConstraintRef(package_name=self._delegate.constraintRef().packageName(),
@@ -479,6 +484,9 @@ def matches(self) -> list[MatchAnalysis[Score_]]:
479484
def score(self) -> Score_:
480485
return to_python_score(self._delegate.score())
481486

487+
@property
488+
def summary(self) -> str:
489+
return self._delegate.summarize()
482490

483491
class ScoreAnalysis:
484492
"""
@@ -510,6 +518,16 @@ class ScoreAnalysis:
510518
constraint_analyses : list[ConstraintAnalysis]
511519
Individual ConstraintAnalysis instances that make up this ScoreAnalysis.
512520
521+
summary : str
522+
Returns a diagnostic text
523+
that explains the solution through the `ConstraintMatch` API
524+
to identify which constraints cause that score quality.
525+
526+
In case of an infeasible solution, this can help diagnose the cause of that.
527+
Do not parse the return value, its format may change without warning.
528+
Instead, to provide this information in a UI or a service,
529+
use `constraint_analyses` and convert those into a domain-specific API.
530+
513531
Notes
514532
-----
515533
the constructors of this record are off-limits.
@@ -520,6 +538,9 @@ class ScoreAnalysis:
520538
def __init__(self, delegate: '_JavaScoreAnalysis'):
521539
self._delegate = delegate
522540

541+
def __str__(self):
542+
return self.summary
543+
523544
@property
524545
def score(self) -> 'Score':
525546
return to_python_score(self._delegate.score())
@@ -541,6 +562,10 @@ def constraint_analyses(self) -> list[ConstraintAnalysis]:
541562
list['_JavaConstraintAnalysis[Score]'], self._delegate.constraintAnalyses())
542563
]
543564

565+
@property
566+
def summary(self) -> str:
567+
return self._delegate.summarize()
568+
544569

545570
__all__ = ['ScoreExplanation',
546571
'ConstraintRef', 'ConstraintMatch', 'ConstraintMatchTotal',

0 commit comments

Comments
 (0)