33from timefold .solver .config import *
44from 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+
619from dataclasses import dataclass , field
720from typing import Annotated , List
821
@@ -18,8 +31,8 @@ class Entity:
1831def 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+
130164def 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
144181def 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
149186def 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