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

Commit 6015a19

Browse files
committed
feat: sync ConstraintRef API
1 parent 8e6c354 commit 6015a19

File tree

4 files changed

+110
-15
lines changed

4 files changed

+110
-15
lines changed

tests/test_solution_manager.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,13 @@ def test_score_manager_constraint_analysis_map():
220220
assert constraint_analysis.match_count == 3
221221

222222

223+
def test_score_manager_constraint_ref():
224+
constraint_ref = ConstraintRef.parse_id('package/Maximize Value')
225+
226+
assert constraint_ref.package_name == 'package'
227+
assert constraint_ref.constraint_name == 'Maximize Value'
228+
229+
223230
ignored_java_functions = {
224231
'equals',
225232
'getClass',
@@ -232,7 +239,8 @@ def test_score_manager_constraint_analysis_map():
232239
}
233240

234241
ignored_java_functions_per_class = {
235-
'Indictment': {'getJustification'} # deprecated
242+
'Indictment': {'getJustification'}, # deprecated
243+
'ConstraintRef': {'of', 'packageName', 'constraintName'} # built-in constructor and properties with @dataclass
236244
}
237245

238246

@@ -242,6 +250,7 @@ def test_has_all_methods():
242250
(ScoreAnalysis, JavaScoreAnalysis),
243251
(ConstraintAnalysis, JavaConstraintAnalysis),
244252
(ScoreExplanation, JavaScoreExplanation),
253+
(ConstraintRef, JavaConstraintRef),
245254
(Indictment, JavaIndictment)):
246255
type_name = python_type.__name__
247256
ignored_java_functions_type = ignored_java_functions_per_class[
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package ai.timefold.solver.python.score.constraint;
2+
3+
import java.lang.reflect.Constructor;
4+
import java.lang.reflect.Field;
5+
import java.lang.reflect.InvocationTargetException;
6+
7+
import ai.timefold.jpyinterpreter.PythonLikeObject;
8+
import ai.timefold.jpyinterpreter.types.PythonJavaTypeMapping;
9+
import ai.timefold.jpyinterpreter.types.PythonLikeType;
10+
import ai.timefold.jpyinterpreter.types.PythonString;
11+
import ai.timefold.solver.core.api.score.constraint.ConstraintRef;
12+
13+
public final class ConstraintRefPythonJavaTypeMapping implements PythonJavaTypeMapping<PythonLikeObject, ConstraintRef> {
14+
private final PythonLikeType type;
15+
private final Constructor<?> constructor;
16+
private final Field packageNameField;
17+
private final Field constraintNameField;
18+
19+
public ConstraintRefPythonJavaTypeMapping(PythonLikeType type)
20+
throws ClassNotFoundException, NoSuchFieldException, NoSuchMethodException {
21+
this.type = type;
22+
Class<?> clazz = type.getJavaClass();
23+
constructor = clazz.getConstructor();
24+
packageNameField = clazz.getField("package_name");
25+
constraintNameField = clazz.getField("constraint_name");
26+
}
27+
28+
@Override
29+
public PythonLikeType getPythonType() {
30+
return type;
31+
}
32+
33+
@Override
34+
public Class<? extends ConstraintRef> getJavaType() {
35+
return ConstraintRef.class;
36+
}
37+
38+
@Override
39+
public PythonLikeObject toPythonObject(ConstraintRef javaObject) {
40+
try {
41+
var instance = constructor.newInstance();
42+
packageNameField.set(instance, PythonString.valueOf(javaObject.packageName()));
43+
constraintNameField.set(instance, PythonString.valueOf(javaObject.constraintName()));
44+
return (PythonLikeObject) instance;
45+
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
46+
throw new RuntimeException(e);
47+
}
48+
}
49+
50+
@Override
51+
public ConstraintRef toJavaObject(PythonLikeObject pythonObject) {
52+
try {
53+
var packageName = ((PythonString) packageNameField.get(pythonObject)).value.toString();
54+
var constraintName = ((PythonString) constraintNameField.get(pythonObject)).value.toString();
55+
return ConstraintRef.of(packageName, constraintName);
56+
} catch (IllegalAccessException e) {
57+
throw new RuntimeException(e);
58+
}
59+
}
60+
}

timefold-solver-python-core/src/main/python/_timefold_java_interop.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,13 +98,14 @@ def update_log_level() -> None:
9898
PythonLoggingToLogbackAdapter.setLevel(logger.getEffectiveLevel())
9999

100100

101-
def register_score_python_java_type_mappings():
101+
def register_python_java_type_mappings():
102102
global _scores_registered, _java_score_mapping_dict, _python_score_mapping_dict
103103
if _scores_registered:
104104
return
105105

106106
_scores_registered = True
107107

108+
# score types
108109
from .score._score import SimpleScore, HardSoftScore, HardMediumSoftScore, BendableScore
109110
from ai.timefold.solver.core.api.score.buildin.simplelong import SimpleLongScore as _SimpleScore
110111
from ai.timefold.solver.core.api.score.buildin.hardsoftlong import HardSoftLongScore as _HardSoftScore
@@ -137,6 +138,20 @@ def register_score_python_java_type_mappings():
137138
add_python_java_type_mapping(HardMediumSoftScorePythonJavaTypeMapping(HardMediumSoftScoreType))
138139
add_python_java_type_mapping(BendableScorePythonJavaTypeMapping(BendableScoreType))
139140

141+
# score analysis types
142+
from .score._score_analysis import ConstraintRef
143+
from ai.timefold.solver.core.api.score.constraint import ConstraintRef as _ConstraintRef
144+
145+
from ai.timefold.solver.python.score.constraint import ConstraintRefPythonJavaTypeMapping
146+
147+
_python_score_mapping_dict['ConstraintRef'] = ConstraintRef
148+
149+
_java_score_mapping_dict['ConstraintRef'] = _ConstraintRef
150+
151+
ConstraintRefType = translate_python_class_to_java_class(ConstraintRef)
152+
153+
add_python_java_type_mapping(ConstraintRefPythonJavaTypeMapping(ConstraintRefType))
154+
140155

141156
def forward_logging_events(event: 'PythonLoggingEvent') -> None:
142157
logger.log(event.level().getPythonLevelNumber(),
@@ -301,7 +316,7 @@ def _add_to_compilation_queue(python_class: type | PythonSupplier) -> None:
301316
def _process_compilation_queue() -> None:
302317
global _compilation_queue
303318

304-
register_score_python_java_type_mappings()
319+
register_python_java_type_mappings()
305320
while len(_compilation_queue) > 0:
306321
python_class = _compilation_queue.pop(0)
307322

@@ -324,7 +339,7 @@ def _generate_constraint_provider_class(original_function: Callable[['_Constrain
324339
wrapped_constraint_provider: Callable[['_ConstraintFactory'],
325340
list['_Constraint']]) -> JClass:
326341
ensure_init()
327-
register_score_python_java_type_mappings()
342+
register_python_java_type_mappings()
328343
from ai.timefold.solver.python import PythonWrapperGenerator # noqa
329344
from ai.timefold.solver.core.api.score.stream import ConstraintProvider
330345
class_identifier = _get_class_identifier_for_object(original_function)

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

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from .._timefold_java_interop import get_class
22
from .._jpype_type_conversions import to_python_score
3+
from .._timefold_java_interop import _java_score_mapping_dict
34
from _jpyinterpreter import unwrap_python_like_object, add_java_interface
45
from dataclasses import dataclass
56
from multipledispatch import dispatch
@@ -42,17 +43,26 @@ class ConstraintRef:
4243
The constraint name.
4344
It might not be unique, but `constraint_id` is unique.
4445
When using a `constraint_configuration`, it is equal to the `ConstraintWeight.constraint_name`.
46+
47+
constraint_id : str
48+
Always derived from `packageName` and `constraintName`.
4549
"""
4650
package_name: str
4751
constraint_name: str
4852

4953
@property
5054
def constraint_id(self) -> str:
51-
"""
52-
Always derived from packageName and constraintName.
53-
"""
5455
return f'{self.package_name}/{self.constraint_name}'
5556

57+
@staticmethod
58+
def parse_id(constraint_id: str):
59+
slash_index = constraint_id.rfind('/')
60+
if slash_index == -1: raise RuntimeError(
61+
f'The constraint_id {constraint_id} is invalid as it does not contain a package separator \'/\'.')
62+
package_name = constraint_id[:slash_index]
63+
constraint_name = constraint_id[slash_index + 1:]
64+
return ConstraintRef(package_name, constraint_name)
65+
5666
@staticmethod
5767
def compose_constraint_id(solution_type_or_package: Union[type, str], constraint_name: str) -> str:
5868
"""
@@ -78,6 +88,9 @@ def compose_constraint_id(solution_type_or_package: Union[type, str], constraint
7888
return ConstraintRef(package_name=package,
7989
constraint_name=constraint_name).constraint_id
8090

91+
def _to_java(self):
92+
return _java_score_mapping_dict['ConstraintRef'].of(self.package_name, self.constraint_name)
93+
8194

8295
def _safe_hash(obj: Any) -> int:
8396
try:
@@ -201,7 +214,7 @@ def _map_constraint_match_set(constraint_match_set: set['_JavaConstraintMatch'])
201214
.getConstraintRef().constraintName()),
202215
justification=_unwrap_justification(constraint_match.getJustification()),
203216
indicted_objects=tuple([unwrap_python_like_object(indicted)
204-
for indicted in cast(list, constraint_match.getIndictedObjectList())]),
217+
for indicted in cast(list, constraint_match.getIndictedObjectList())]),
205218
score=to_python_score(constraint_match.getScore())
206219
)
207220
for constraint_match in constraint_match_set
@@ -214,7 +227,7 @@ def _unwrap_justification(justification: Any) -> ConstraintJustification:
214227
if isinstance(justification, _JavaDefaultConstraintJustification):
215228
fact_list = justification.getFacts()
216229
return DefaultConstraintJustification(facts=tuple([unwrap_python_like_object(fact)
217-
for fact in cast(list, fact_list)]),
230+
for fact in cast(list, fact_list)]),
218231
impact=to_python_score(justification.getImpact()))
219232
else:
220233
return unwrap_python_like_object(justification)
@@ -245,6 +258,7 @@ class Indictment(Generic[Score_]):
245258
in `constraint_match_set`.
246259
247260
"""
261+
248262
def __init__(self, delegate: '_JavaIndictment[Score_]'):
249263
self._delegate = delegate
250264

@@ -496,6 +510,7 @@ def score(self) -> Score_:
496510
def summarize(self) -> str:
497511
return self._delegate.summarize()
498512

513+
499514
class ScoreAnalysis:
500515
"""
501516
Represents the breakdown of a `Score` into individual `ConstraintAnalysis` instances,
@@ -592,7 +607,7 @@ def constraint_analysis(self, constraint_package: str, constraint_name: str) ->
592607
return ConstraintAnalysis(self._delegate.getConstraintAnalysis(constraint_package, constraint_name))
593608

594609
@dispatch(ConstraintRef)
595-
def constraint_analysis(self, constraint_ref: ConstraintRef) -> ConstraintAnalysis:
610+
def constraint_analysis(self, constraint_ref: 'ConstraintRef') -> ConstraintAnalysis:
596611
"""
597612
Performs a lookup on `constraint_map`.
598613
@@ -605,7 +620,7 @@ def constraint_analysis(self, constraint_ref: ConstraintRef) -> ConstraintAnalys
605620
ConstraintAnalysis
606621
None if no constraint matches of such constraint are present
607622
"""
608-
return self.constraint_analysis(constraint_ref.package_name, constraint_ref.constraint_name)
623+
return ConstraintAnalysis(self._delegate.getConstraintAnalysis(constraint_ref._to_java()))
609624

610625
@property
611626
def summarize(self) -> str:
@@ -615,7 +630,6 @@ def summarize(self) -> str:
615630
def is_solution_initialized(self) -> bool:
616631
return self._delegate.isSolutionInitialized()
617632

618-
619633
def diff(self, other: 'ScoreAnalysis') -> 'ScoreAnalysis':
620634
"""
621635
Compare this `ScoreAnalysis to another `ScoreAnalysis`
@@ -647,9 +661,6 @@ def diff(self, other: 'ScoreAnalysis') -> 'ScoreAnalysis':
647661
return ScoreAnalysis(self._delegate.diff(other._delegate))
648662

649663

650-
651-
652-
653664
__all__ = ['ScoreExplanation',
654665
'ConstraintRef', 'ConstraintMatch', 'ConstraintMatchTotal',
655666
'ConstraintJustification', 'DefaultConstraintJustification', 'Indictment',

0 commit comments

Comments
 (0)