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

Commit 4396e2d

Browse files
authored
feat: Improve ScoreAnalysis debug information (#105)
1 parent 81bbd40 commit 4396e2d

File tree

3 files changed

+290
-10
lines changed

3 files changed

+290
-10
lines changed

Diff for: setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ def find_stub_files(stub_root: str):
145145
test_suite='tests',
146146
python_requires='>=3.10',
147147
install_requires=[
148-
'JPype1>=1.5.0',
148+
'JPype1>=1.5.0'
149149
],
150150
cmdclass={'build_py': FetchDependencies},
151151
package_data={

Diff for: tests/test_solution_manager.py

+163-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,19 @@
33
from timefold.solver.config import *
44
from timefold.solver.score import *
55

6+
import inspect
7+
import re
8+
9+
from ai.timefold.solver.core.api.score import ScoreExplanation as JavaScoreExplanation
10+
from ai.timefold.solver.core.api.score.analysis import (
11+
ConstraintAnalysis as JavaConstraintAnalysis,
12+
MatchAnalysis as JavaMatchAnalysis,
13+
ScoreAnalysis as JavaScoreAnalysis)
14+
from ai.timefold.solver.core.api.score.constraint import Indictment as JavaIndictment
15+
from ai.timefold.solver.core.api.score.constraint import (ConstraintRef as JavaConstraintRef,
16+
ConstraintMatch as JavaConstraintMatch,
17+
ConstraintMatchTotal as JavaConstraintMatchTotal)
18+
619
from dataclasses import dataclass, field
720
from typing import Annotated, List
821

@@ -18,8 +31,8 @@ class Entity:
1831
def my_constraints(constraint_factory: ConstraintFactory):
1932
return [
2033
constraint_factory.for_each(Entity)
21-
.reward(SimpleScore.ONE, lambda entity: entity.value)
22-
.as_constraint('package', 'Maximize Value'),
34+
.reward(SimpleScore.ONE, lambda entity: entity.value)
35+
.as_constraint('package', 'Maximize Value'),
2336
]
2437

2538

@@ -127,6 +140,27 @@ def assert_score_analysis(problem: Solution, score_analysis: ScoreAnalysis):
127140
assert_constraint_analysis(problem, constraint_analysis)
128141

129142

143+
def assert_score_analysis_summary(score_analysis: ScoreAnalysis):
144+
summary = score_analysis.summary
145+
assert "Explanation of score (3):" in summary
146+
assert "Constraint matches:" in summary
147+
assert "3: constraint (Maximize Value) has 3 matches:" in summary
148+
assert "1: justified with" in summary
149+
150+
summary_str = str(score_analysis)
151+
assert summary == summary_str
152+
153+
match = score_analysis.constraint_analyses[0]
154+
match_summary = match.summary
155+
assert "Explanation of score (3):" in match_summary
156+
assert "Constraint matches:" in match_summary
157+
assert "3: constraint (Maximize Value) has 3 matches:" in match_summary
158+
assert "1: justified with" in match_summary
159+
160+
match_summary_str = str(match)
161+
assert match_summary == match_summary_str
162+
163+
130164
def assert_solution_manager(solution_manager: SolutionManager[Solution]):
131165
problem: Solution = Solution([Entity('A', 1), Entity('B', 1), Entity('C', 1)], [1, 2, 3])
132166
assert problem.score is None
@@ -140,6 +174,9 @@ def assert_solution_manager(solution_manager: SolutionManager[Solution]):
140174
score_analysis = solution_manager.analyze(problem)
141175
assert_score_analysis(problem, score_analysis)
142176

177+
score_analysis = solution_manager.analyze(problem)
178+
assert_score_analysis_summary(score_analysis)
179+
143180

144181
def test_solver_manager_score_manager():
145182
with SolverManager.create(SolverFactory.create(solver_config)) as solver_manager:
@@ -148,3 +185,127 @@ def test_solver_manager_score_manager():
148185

149186
def test_solver_factory_score_manager():
150187
assert_solution_manager(SolutionManager.create(SolverFactory.create(solver_config)))
188+
189+
190+
def test_score_manager_solution_initialization():
191+
solution_manager = SolutionManager.create(SolverFactory.create(solver_config))
192+
problem: Solution = Solution([Entity('A', 1), Entity('B', 1), Entity('C', 1)], [1, 2, 3])
193+
score_analysis = solution_manager.analyze(problem)
194+
assert score_analysis.is_solution_initialized
195+
196+
second_problem: Solution = Solution([Entity('A', None), Entity('B', None), Entity('C', None)], [1, 2, 3])
197+
second_score_analysis = solution_manager.analyze(second_problem)
198+
assert not second_score_analysis.is_solution_initialized
199+
200+
201+
def test_score_manager_diff():
202+
solution_manager = SolutionManager.create(SolverFactory.create(solver_config))
203+
problem: Solution = Solution([Entity('A', 1), Entity('B', 1), Entity('C', 1)], [1, 2, 3])
204+
score_analysis = solution_manager.analyze(problem)
205+
second_problem: Solution = Solution([Entity('A', 1), Entity('B', 1), Entity('C', 1), Entity('D', 1)], [1, 2, 3])
206+
second_score_analysis = solution_manager.analyze(second_problem)
207+
diff = score_analysis.diff(second_score_analysis)
208+
assert diff.score.score == -1
209+
210+
diff_operation = score_analysis - second_score_analysis
211+
assert diff_operation.score.score == -1
212+
213+
constraint_analyses = score_analysis.constraint_analyses
214+
assert len(constraint_analyses) == 1
215+
216+
217+
def test_score_manager_constraint_analysis_map():
218+
solution_manager = SolutionManager.create(SolverFactory.create(solver_config))
219+
problem: Solution = Solution([Entity('A', 1), Entity('B', 1), Entity('C', 1)], [1, 2, 3])
220+
score_analysis = solution_manager.analyze(problem)
221+
constraints = score_analysis.constraint_analyses
222+
assert len(constraints) == 1
223+
224+
constraint_analysis = score_analysis.constraint_analysis('package', 'Maximize Value')
225+
assert constraint_analysis.constraint_name == 'Maximize Value'
226+
227+
constraint_analysis = score_analysis.constraint_analysis(ConstraintRef('package', 'Maximize Value'))
228+
assert constraint_analysis.constraint_name == 'Maximize Value'
229+
assert constraint_analysis.match_count == 3
230+
231+
232+
def test_score_manager_constraint_ref():
233+
constraint_ref = ConstraintRef.parse_id('package/Maximize Value')
234+
235+
assert constraint_ref.package_name == 'package'
236+
assert constraint_ref.constraint_name == 'Maximize Value'
237+
238+
239+
ignored_java_functions = {
240+
'equals',
241+
'getClass',
242+
'hashCode',
243+
'notify',
244+
'notifyAll',
245+
'toString',
246+
'wait',
247+
'compareTo',
248+
}
249+
250+
ignored_java_functions_per_class = {
251+
'Indictment': {'getJustification'}, # deprecated
252+
'ConstraintRef': {'of', 'packageName', 'constraintName'}, # built-in constructor and properties with @dataclass
253+
'ConstraintAnalysis': {'summarize'}, # using summary instead
254+
'ScoreAnalysis': {'summarize'}, # using summary instead
255+
'ConstraintMatch': {
256+
'getConstraintRef', # built-in constructor and properties with @dataclass
257+
'getConstraintPackage', # deprecated
258+
'getConstraintName', # deprecated
259+
'getConstraintId', # deprecated
260+
'getJustificationList', # deprecated
261+
'getJustification', # built-in constructor and properties with @dataclass
262+
'getScore', # built-in constructor and properties with @dataclass
263+
'getIndictedObjectList', # built-in constructor and properties with @dataclass
264+
},
265+
'ConstraintMatchTotal': {
266+
'getConstraintRef', # built-in constructor and properties with @dataclass
267+
'composeConstraintId', # deprecated
268+
'getConstraintPackage', # deprecated
269+
'getConstraintName', # deprecated
270+
'getConstraintId', # deprecated
271+
'getConstraintMatchCount', # built-in constructor and properties with @dataclass
272+
'getConstraintMatchSet', # built-in constructor and properties with @dataclass
273+
'getConstraintWeight', # built-in constructor and properties with @dataclass
274+
'getScore', # built-in constructor and properties with @dataclass
275+
},
276+
}
277+
278+
279+
def test_has_all_methods():
280+
missing = []
281+
for python_type, java_type in ((ScoreExplanation, JavaScoreExplanation),
282+
(ScoreAnalysis, JavaScoreAnalysis),
283+
(ConstraintAnalysis, JavaConstraintAnalysis),
284+
(ScoreExplanation, JavaScoreExplanation),
285+
(ConstraintMatch, JavaConstraintMatch),
286+
(ConstraintMatchTotal, JavaConstraintMatchTotal),
287+
(ConstraintRef, JavaConstraintRef),
288+
(Indictment, JavaIndictment)):
289+
type_name = python_type.__name__
290+
ignored_java_functions_type = ignored_java_functions_per_class[
291+
type_name] if type_name in ignored_java_functions_per_class else {}
292+
293+
for function_name, function_impl in inspect.getmembers(java_type, inspect.isfunction):
294+
if function_name in ignored_java_functions or function_name in ignored_java_functions_type:
295+
continue
296+
297+
snake_case_name = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', function_name)
298+
snake_case_name = re.sub('([a-z0-9])([A-Z])', r'\1_\2', snake_case_name).lower()
299+
snake_case_name_without_prefix = re.sub('(.)([A-Z][a-z]+)', r'\1_\2',
300+
function_name[3:] if function_name.startswith(
301+
"get") else function_name)
302+
snake_case_name_without_prefix = re.sub('([a-z0-9])([A-Z])', r'\1_\2',
303+
snake_case_name_without_prefix).lower()
304+
if not hasattr(python_type, snake_case_name) and not hasattr(python_type, snake_case_name_without_prefix):
305+
missing.append((java_type, python_type, snake_case_name))
306+
307+
if missing:
308+
assertion_msg = ''
309+
for java_type, python_type, snake_case_name in missing:
310+
assertion_msg += f'{python_type} is missing a method ({snake_case_name}) from java_type ({java_type}).)\n'
311+
raise AssertionError(assertion_msg)

0 commit comments

Comments
 (0)