3
3
from timefold .solver .config import *
4
4
from timefold .solver .score import *
5
5
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
+
6
19
from dataclasses import dataclass , field
7
20
from typing import Annotated , List
8
21
@@ -18,8 +31,8 @@ class Entity:
18
31
def my_constraints (constraint_factory : ConstraintFactory ):
19
32
return [
20
33
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' ),
23
36
]
24
37
25
38
@@ -127,6 +140,27 @@ def assert_score_analysis(problem: Solution, score_analysis: ScoreAnalysis):
127
140
assert_constraint_analysis (problem , constraint_analysis )
128
141
129
142
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
+
130
164
def assert_solution_manager (solution_manager : SolutionManager [Solution ]):
131
165
problem : Solution = Solution ([Entity ('A' , 1 ), Entity ('B' , 1 ), Entity ('C' , 1 )], [1 , 2 , 3 ])
132
166
assert problem .score is None
@@ -140,6 +174,9 @@ def assert_solution_manager(solution_manager: SolutionManager[Solution]):
140
174
score_analysis = solution_manager .analyze (problem )
141
175
assert_score_analysis (problem , score_analysis )
142
176
177
+ score_analysis = solution_manager .analyze (problem )
178
+ assert_score_analysis_summary (score_analysis )
179
+
143
180
144
181
def test_solver_manager_score_manager ():
145
182
with SolverManager .create (SolverFactory .create (solver_config )) as solver_manager :
@@ -148,3 +185,127 @@ def test_solver_manager_score_manager():
148
185
149
186
def test_solver_factory_score_manager ():
150
187
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