From edc23d0924d8033a3be5e45fa44080671fea553a Mon Sep 17 00:00:00 2001 From: Fred Date: Fri, 28 Jun 2024 19:10:36 -0300 Subject: [PATCH 01/16] feat: add summary to score analysis --- tests/test_solution_manager.py | 18 +++++++++++++ .../src/main/python/score/_score_analysis.py | 27 ++++++++++++++++++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/tests/test_solution_manager.py b/tests/test_solution_manager.py index 3a71113f..388deb5f 100644 --- a/tests/test_solution_manager.py +++ b/tests/test_solution_manager.py @@ -127,6 +127,21 @@ def assert_score_analysis(problem: Solution, score_analysis: ScoreAnalysis): assert_constraint_analysis(problem, constraint_analysis) +def assert_score_analysis_summary(score_analysis: ScoreAnalysis): + summary = score_analysis.summary + assert "Explanation of score (3):" in summary + assert "Constraint matches:" in summary + assert "3: constraint (Maximize Value) has 3 matches:" in summary + assert "1: justified with" in summary + + match = score_analysis.constraint_analyses[0] + match_summary = match.summary + assert "Explanation of score (3):" in match_summary + assert "Constraint matches:" in match_summary + assert "3: constraint (Maximize Value) has 3 matches:" in match_summary + assert "1: justified with" in match_summary + + def assert_solution_manager(solution_manager: SolutionManager[Solution]): problem: Solution = Solution([Entity('A', 1), Entity('B', 1), Entity('C', 1)], [1, 2, 3]) assert problem.score is None @@ -140,6 +155,9 @@ def assert_solution_manager(solution_manager: SolutionManager[Solution]): score_analysis = solution_manager.analyze(problem) assert_score_analysis(problem, score_analysis) + score_analysis = solution_manager.analyze(problem) + assert_score_analysis_summary(score_analysis) + def test_solver_manager_score_manager(): with SolverManager.create(SolverFactory.create(solver_config)) as solver_manager: diff --git a/timefold-solver-python-core/src/main/python/score/_score_analysis.py b/timefold-solver-python-core/src/main/python/score/_score_analysis.py index 06b1874e..f2e71884 100644 --- a/timefold-solver-python-core/src/main/python/score/_score_analysis.py +++ b/timefold-solver-python-core/src/main/python/score/_score_analysis.py @@ -445,7 +445,9 @@ class ConstraintAnalysis(Generic[Score_]): but still non-zero constraint weight; non-empty if constraint has matches. This is a list to simplify access to individual elements, but it contains no duplicates just like `set` wouldn't. - + summary : str + Returns a diagnostic text + that explains part of the score quality through the ConstraintAnalysis API. """ _delegate: '_JavaConstraintAnalysis[Score_]' @@ -453,6 +455,9 @@ def __init__(self, delegate: '_JavaConstraintAnalysis[Score_]'): self._delegate = delegate delegate.constraintRef() + def __str__(self): + return self.summary + @property def constraint_ref(self) -> ConstraintRef: return ConstraintRef(package_name=self._delegate.constraintRef().packageName(), @@ -479,6 +484,9 @@ def matches(self) -> list[MatchAnalysis[Score_]]: def score(self) -> Score_: return to_python_score(self._delegate.score()) + @property + def summary(self) -> str: + return self._delegate.summarize() class ScoreAnalysis: """ @@ -510,6 +518,16 @@ class ScoreAnalysis: constraint_analyses : list[ConstraintAnalysis] Individual ConstraintAnalysis instances that make up this ScoreAnalysis. + summary : str + Returns a diagnostic text + that explains the solution through the `ConstraintMatch` API + to identify which constraints cause that score quality. + + In case of an infeasible solution, this can help diagnose the cause of that. + Do not parse the return value, its format may change without warning. + Instead, to provide this information in a UI or a service, + use `constraint_analyses` and convert those into a domain-specific API. + Notes ----- the constructors of this record are off-limits. @@ -520,6 +538,9 @@ class ScoreAnalysis: def __init__(self, delegate: '_JavaScoreAnalysis'): self._delegate = delegate + def __str__(self): + return self.summary + @property def score(self) -> 'Score': return to_python_score(self._delegate.score()) @@ -541,6 +562,10 @@ def constraint_analyses(self) -> list[ConstraintAnalysis]: list['_JavaConstraintAnalysis[Score]'], self._delegate.constraintAnalyses()) ] + @property + def summary(self) -> str: + return self._delegate.summarize() + __all__ = ['ScoreExplanation', 'ConstraintRef', 'ConstraintMatch', 'ConstraintMatchTotal', From c53c04c7798bbe1d8267dff210c335a65f6d4667 Mon Sep 17 00:00:00 2001 From: Fred Date: Tue, 2 Jul 2024 17:27:17 -0300 Subject: [PATCH 02/16] feat: sync ScoreAnalysis API --- pyproject.toml | 3 +- tests/test_solution_manager.py | 54 ++++++++++- .../src/main/python/score/_score_analysis.py | 96 +++++++++++++++++-- tox.ini | 3 +- 4 files changed, 142 insertions(+), 14 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 66a5388f..534b55ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,6 +3,7 @@ requires = [ "setuptools>=69.1.1", "stubgenj>=0.2.5", "JPype1>=1.5.0", - "wheel" + "wheel", + "multipledispatch>=1.0.0" ] build-backend = "setuptools.build_meta" diff --git a/tests/test_solution_manager.py b/tests/test_solution_manager.py index 388deb5f..d5568fd7 100644 --- a/tests/test_solution_manager.py +++ b/tests/test_solution_manager.py @@ -3,6 +3,19 @@ from timefold.solver.config import * from timefold.solver.score import * +import inspect +import re + +from ai.timefold.solver.core.api.score import ScoreExplanation as JavaScoreExplanation +from ai.timefold.solver.core.api.score.analysis import ( + ConstraintAnalysis as JavaConstraintAnalysis, + MatchAnalysis as JavaMatchAnalysis, + ScoreAnalysis as JavaScoreAnalysis) +from ai.timefold.solver.core.api.score.constraint import Indictment as JavaIndictment +from ai.timefold.solver.core.api.score.constraint import (ConstraintRef as JavaConstraintRef, + ConstraintMatch as JavaConstraintMatch, + ConstraintMatchTotal as JavaConstraintMatchTotal) + from dataclasses import dataclass, field from typing import Annotated, List @@ -128,14 +141,14 @@ def assert_score_analysis(problem: Solution, score_analysis: ScoreAnalysis): def assert_score_analysis_summary(score_analysis: ScoreAnalysis): - summary = score_analysis.summary + summary = score_analysis.summarize assert "Explanation of score (3):" in summary assert "Constraint matches:" in summary assert "3: constraint (Maximize Value) has 3 matches:" in summary assert "1: justified with" in summary match = score_analysis.constraint_analyses[0] - match_summary = match.summary + match_summary = match.summarize assert "Explanation of score (3):" in match_summary assert "Constraint matches:" in match_summary assert "3: constraint (Maximize Value) has 3 matches:" in match_summary @@ -166,3 +179,40 @@ def test_solver_manager_score_manager(): def test_solver_factory_score_manager(): assert_solution_manager(SolutionManager.create(SolverFactory.create(solver_config))) + + +def test_score_manager_solution_initialization(): + solution_manager = SolutionManager.create(SolverFactory.create(solver_config)) + problem: Solution = Solution([Entity('A', 1), Entity('B', 1), Entity('C', 1)], [1, 2, 3]) + score_analysis = solution_manager.analyze(problem) + assert score_analysis.is_solution_initialized + + second_problem: Solution = Solution([Entity('A', None), Entity('B', None), Entity('C', None)], [1, 2, 3]) + second_score_analysis = solution_manager.analyze(second_problem) + assert not second_score_analysis.is_solution_initialized + + +def test_score_manager_diff(): + solution_manager = SolutionManager.create(SolverFactory.create(solver_config)) + problem: Solution = Solution([Entity('A', 1), Entity('B', 1), Entity('C', 1)], [1, 2, 3]) + score_analysis = solution_manager.analyze(problem) + second_problem: Solution = Solution([Entity('A', 1), Entity('B', 1), Entity('C', 1), Entity('D', 1)], [1, 2, 3]) + second_score_analysis = solution_manager.analyze(second_problem) + diff = score_analysis.diff(second_score_analysis) + assert diff.score.score == -1 + + constraint_analyses = score_analysis.constraint_analyses + assert len(constraint_analyses) == 1 + +def test_score_manager_constraint_analysis_map(): + solution_manager = SolutionManager.create(SolverFactory.create(solver_config)) + problem: Solution = Solution([Entity('A', 1), Entity('B', 1), Entity('C', 1)], [1, 2, 3]) + score_analysis = solution_manager.analyze(problem) + constraints = score_analysis.constraint_analyses + assert len(constraints) == 1 + + constraint_analysis = score_analysis.constraint_analysis('package', 'Maximize Value') + assert constraint_analysis.constraint_name == 'Maximize Value' + + constraint_analysis = score_analysis.constraint_analysis(ConstraintRef('package', 'Maximize Value')) + assert constraint_analysis.constraint_name == 'Maximize Value' diff --git a/timefold-solver-python-core/src/main/python/score/_score_analysis.py b/timefold-solver-python-core/src/main/python/score/_score_analysis.py index f2e71884..f13da991 100644 --- a/timefold-solver-python-core/src/main/python/score/_score_analysis.py +++ b/timefold-solver-python-core/src/main/python/score/_score_analysis.py @@ -2,6 +2,7 @@ from .._jpype_type_conversions import to_python_score from _jpyinterpreter import unwrap_python_like_object, add_java_interface from dataclasses import dataclass +from multipledispatch import dispatch from typing import TypeVar, Generic, Union, TYPE_CHECKING, Any, cast, Optional, Type @@ -456,7 +457,7 @@ def __init__(self, delegate: '_JavaConstraintAnalysis[Score_]'): delegate.constraintRef() def __str__(self): - return self.summary + return self.summarize @property def constraint_ref(self) -> ConstraintRef: @@ -485,7 +486,7 @@ def score(self) -> Score_: return to_python_score(self._delegate.score()) @property - def summary(self) -> str: + def summarize(self) -> str: return self._delegate.summarize() class ScoreAnalysis: @@ -518,15 +519,19 @@ class ScoreAnalysis: constraint_analyses : list[ConstraintAnalysis] Individual ConstraintAnalysis instances that make up this ScoreAnalysis. - summary : str - Returns a diagnostic text - that explains the solution through the `ConstraintMatch` API - to identify which constraints cause that score quality. + summarize : str + Returns a diagnostic text that explains the solution through the `ConstraintAnalysis` API to identify which + Constraints cause that score quality. + The string is built fresh every time the method is called. In case of an infeasible solution, this can help diagnose the cause of that. + Do not parse the return value, its format may change without warning. - Instead, to provide this information in a UI or a service, - use `constraint_analyses` and convert those into a domain-specific API. + Instead, provide this information in a UI or a service, + use `constraintAnalyses()` + and convert those into a domain-specific API. + + is_solution_initialized : bool Notes ----- @@ -539,7 +544,7 @@ def __init__(self, delegate: '_JavaScoreAnalysis'): self._delegate = delegate def __str__(self): - return self.summary + return self.summarize @property def score(self) -> 'Score': @@ -562,10 +567,81 @@ def constraint_analyses(self) -> list[ConstraintAnalysis]: list['_JavaConstraintAnalysis[Score]'], self._delegate.constraintAnalyses()) ] + @dispatch(str, str) + def constraint_analysis(self, constraint_package: str, constraint_name: str) -> ConstraintAnalysis: + """ + Performs a lookup on `constraint_map`. + + Parameters + ---------- + constraint_package : str + constraint_name : str + + Returns + ------- + ConstraintAnalysis + None if no constraint matches of such constraint are present + """ + return ConstraintAnalysis(self._delegate.getConstraintAnalysis(constraint_package, constraint_name)) + + @dispatch(ConstraintRef) + def constraint_analysis(self, constraint_ref: ConstraintRef) -> ConstraintAnalysis: + """ + Performs a lookup on `constraint_map`. + + Parameters + ---------- + constraint_ref : ConstraintRef + + Returns + ------- + ConstraintAnalysis + None if no constraint matches of such constraint are present + """ + return self.constraint_analysis(constraint_ref.package_name, constraint_ref.constraint_name) + @property - def summary(self) -> str: + def summarize(self) -> str: return self._delegate.summarize() + @property + def is_solution_initialized(self) -> bool: + return self._delegate.isSolutionInitialized() + + + def diff(self, other: 'ScoreAnalysis') -> 'ScoreAnalysis': + """ + Compare this `ScoreAnalysis to another `ScoreAnalysis` + and retrieve the difference between them. + The comparison is in the direction of `this - other`. + + Example: if `this` has a score of 100 and `other` has a score of 90, + the returned score will be 10. + If this and other were inverted, the score would have been -10. + The same applies to all other properties of `ScoreAnalysis`. + + In order to properly diff `MatchAnalysis` against each other, + we rely on the user implementing `ConstraintJustification` equality correctly. + In other words, the diff will consider two justifications equal if the user says they are equal, + and it expects the hash code to be consistent with equals. + + If one `ScoreAnalysis` provides `MatchAnalysis` and the other doesn't, exception is thrown. + Such `ScoreAnalysis` instances are mutually incompatible. + + Parameters + ---------- + other : ScoreAnalysis + + Returns + ------- + ScoreExplanation + The `ScoreAnalysis` corresponding to the diff. + """ + return ScoreAnalysis(self._delegate.diff(other._delegate)) + + + + __all__ = ['ScoreExplanation', 'ConstraintRef', 'ConstraintMatch', 'ConstraintMatchTotal', diff --git a/tox.ini b/tox.ini index 007b69b6..37d1d27f 100644 --- a/tox.ini +++ b/tox.ini @@ -13,8 +13,9 @@ deps = pytest-cov>=4.1.0 coverage>=7.4.3 JPype1>=1.5.0 + multipledispatch>=1.0.0 commands = - pytest --import-mode=importlib {posargs} tests + pytest -s --import-mode=importlib {posargs} tests [coverage:paths] source = From 0c1f6ccaa7132117e10ed0d6f10f38140d30915c Mon Sep 17 00:00:00 2001 From: Fred Date: Tue, 2 Jul 2024 17:33:59 -0300 Subject: [PATCH 03/16] feat: sync ConstraintAnalysis API --- tests/test_solution_manager.py | 40 +++++++++++++++++++ .../src/main/python/score/_score_analysis.py | 8 +++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/tests/test_solution_manager.py b/tests/test_solution_manager.py index d5568fd7..7d563af9 100644 --- a/tests/test_solution_manager.py +++ b/tests/test_solution_manager.py @@ -216,3 +216,43 @@ def test_score_manager_constraint_analysis_map(): constraint_analysis = score_analysis.constraint_analysis(ConstraintRef('package', 'Maximize Value')) assert constraint_analysis.constraint_name == 'Maximize Value' + assert constraint_analysis.match_count == 3 + + +ignored_java_functions = { + 'equals', + 'getClass', + 'hashCode', + 'notify', + 'notifyAll', + 'toString', + 'wait', + 'compareTo', +} + + +def test_has_all_methods(): + missing = [] + for python_type, java_type in ((ScoreExplanation, JavaScoreExplanation), + (ScoreAnalysis, JavaScoreAnalysis), + (ConstraintAnalysis, JavaConstraintAnalysis), + (ScoreExplanation, JavaScoreExplanation), + (ConstraintMatch, JavaConstraintMatch), + (ConstraintMatchTotal, JavaConstraintMatchTotal), + (Indictment, JavaIndictment)): + for function_name, function_impl in inspect.getmembers(java_type, inspect.isfunction): + if function_name in ignored_java_functions: + continue + + snake_case_name = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', function_name) + snake_case_name = re.sub('([a-z0-9])([A-Z])', r'\1_\2', snake_case_name).lower() + snake_case_name_without_prefix = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', function_name[3:] if function_name.startswith("get") else function_name) + snake_case_name_without_prefix = re.sub('([a-z0-9])([A-Z])', r'\1_\2', snake_case_name_without_prefix).lower() + if not hasattr(python_type, snake_case_name) and not hasattr(python_type, snake_case_name_without_prefix): + missing.append((java_type, python_type, snake_case_name)) + + if missing: + assertion_msg = '' + for java_type, python_type, snake_case_name in missing: + assertion_msg += f'{python_type} is missing a method ({snake_case_name}) from java_type ({java_type}).)\n' + raise AssertionError(assertion_msg) diff --git a/timefold-solver-python-core/src/main/python/score/_score_analysis.py b/timefold-solver-python-core/src/main/python/score/_score_analysis.py index f13da991..c6ac98a2 100644 --- a/timefold-solver-python-core/src/main/python/score/_score_analysis.py +++ b/timefold-solver-python-core/src/main/python/score/_score_analysis.py @@ -446,9 +446,11 @@ class ConstraintAnalysis(Generic[Score_]): but still non-zero constraint weight; non-empty if constraint has matches. This is a list to simplify access to individual elements, but it contains no duplicates just like `set` wouldn't. - summary : str + summarize : str Returns a diagnostic text that explains part of the score quality through the ConstraintAnalysis API. + match_count : int + Return the match count of the constraint. """ _delegate: '_JavaConstraintAnalysis[Score_]' @@ -481,6 +483,10 @@ def matches(self) -> list[MatchAnalysis[Score_]]: return [MatchAnalysis(match_analysis) for match_analysis in cast(list['_JavaMatchAnalysis[Score_]'], self._delegate.matches())] + @property + def match_count(self) -> int: + return self._delegate.matchCount() + @property def score(self) -> Score_: return to_python_score(self._delegate.score()) From 78aec72628755288384f0641e1a1cb024a08a65a Mon Sep 17 00:00:00 2001 From: Fred Date: Tue, 2 Jul 2024 18:04:07 -0300 Subject: [PATCH 04/16] feat: sync Indictment API --- tests/test_solution_manager.py | 24 +++++++++++++------ .../src/main/python/score/_score_analysis.py | 1 + 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/tests/test_solution_manager.py b/tests/test_solution_manager.py index 7d563af9..2c03ed51 100644 --- a/tests/test_solution_manager.py +++ b/tests/test_solution_manager.py @@ -31,8 +31,8 @@ class Entity: def my_constraints(constraint_factory: ConstraintFactory): return [ constraint_factory.for_each(Entity) - .reward(SimpleScore.ONE, lambda entity: entity.value) - .as_constraint('package', 'Maximize Value'), + .reward(SimpleScore.ONE, lambda entity: entity.value) + .as_constraint('package', 'Maximize Value'), ] @@ -204,6 +204,7 @@ def test_score_manager_diff(): constraint_analyses = score_analysis.constraint_analyses assert len(constraint_analyses) == 1 + def test_score_manager_constraint_analysis_map(): solution_manager = SolutionManager.create(SolverFactory.create(solver_config)) problem: Solution = Solution([Entity('A', 1), Entity('B', 1), Entity('C', 1)], [1, 2, 3]) @@ -230,6 +231,10 @@ def test_score_manager_constraint_analysis_map(): 'compareTo', } +ignored_java_functions_per_class = { + 'Indictment': {'getJustification'} # deprecated +} + def test_has_all_methods(): missing = [] @@ -237,17 +242,22 @@ def test_has_all_methods(): (ScoreAnalysis, JavaScoreAnalysis), (ConstraintAnalysis, JavaConstraintAnalysis), (ScoreExplanation, JavaScoreExplanation), - (ConstraintMatch, JavaConstraintMatch), - (ConstraintMatchTotal, JavaConstraintMatchTotal), (Indictment, JavaIndictment)): + type_name = python_type.__name__ + ignored_java_functions_type = ignored_java_functions_per_class[ + type_name] if type_name in ignored_java_functions_per_class else {} + for function_name, function_impl in inspect.getmembers(java_type, inspect.isfunction): - if function_name in ignored_java_functions: + if function_name in ignored_java_functions or function_name in ignored_java_functions_type: continue snake_case_name = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', function_name) snake_case_name = re.sub('([a-z0-9])([A-Z])', r'\1_\2', snake_case_name).lower() - snake_case_name_without_prefix = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', function_name[3:] if function_name.startswith("get") else function_name) - snake_case_name_without_prefix = re.sub('([a-z0-9])([A-Z])', r'\1_\2', snake_case_name_without_prefix).lower() + snake_case_name_without_prefix = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', + function_name[3:] if function_name.startswith( + "get") else function_name) + snake_case_name_without_prefix = re.sub('([a-z0-9])([A-Z])', r'\1_\2', + snake_case_name_without_prefix).lower() if not hasattr(python_type, snake_case_name) and not hasattr(python_type, snake_case_name_without_prefix): missing.append((java_type, python_type, snake_case_name)) diff --git a/timefold-solver-python-core/src/main/python/score/_score_analysis.py b/timefold-solver-python-core/src/main/python/score/_score_analysis.py index c6ac98a2..4b535cae 100644 --- a/timefold-solver-python-core/src/main/python/score/_score_analysis.py +++ b/timefold-solver-python-core/src/main/python/score/_score_analysis.py @@ -243,6 +243,7 @@ class Indictment(Generic[Score_]): The object that was involved in causing the constraints to match. It is part of `ConstraintMatch.indicted_objects` of every `ConstraintMatch` in `constraint_match_set`. + """ def __init__(self, delegate: '_JavaIndictment[Score_]'): self._delegate = delegate From f8ef4af3f5680899f6bf4bc26e2d87bf6bc2d11e Mon Sep 17 00:00:00 2001 From: Fred Date: Tue, 2 Jul 2024 18:14:14 -0300 Subject: [PATCH 05/16] chore: fix tox.ini --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 37d1d27f..5acf3ed8 100644 --- a/tox.ini +++ b/tox.ini @@ -15,7 +15,7 @@ deps = JPype1>=1.5.0 multipledispatch>=1.0.0 commands = - pytest -s --import-mode=importlib {posargs} tests + pytest --import-mode=importlib {posargs} tests [coverage:paths] source = From 8e6c3541bd5a5bd384ff521c7fbbfa2c7f381442 Mon Sep 17 00:00:00 2001 From: Fred Date: Tue, 2 Jul 2024 18:29:24 -0300 Subject: [PATCH 06/16] chore: fix CI --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index ccd897b1..6b7aeb49 100644 --- a/setup.py +++ b/setup.py @@ -146,6 +146,7 @@ def find_stub_files(stub_root: str): python_requires='>=3.10', install_requires=[ 'JPype1>=1.5.0', + 'multipledispatch>=1.0.0' ], cmdclass={'build_py': FetchDependencies}, package_data={ From 6015a1963d7045282f2f85d3f00f3d63d44c81bd Mon Sep 17 00:00:00 2001 From: Fred Date: Wed, 3 Jul 2024 09:23:44 -0300 Subject: [PATCH 07/16] feat: sync ConstraintRef API --- tests/test_solution_manager.py | 11 +++- .../ConstraintRefPythonJavaTypeMapping.java | 60 +++++++++++++++++++ .../src/main/python/_timefold_java_interop.py | 21 ++++++- .../src/main/python/score/_score_analysis.py | 33 ++++++---- 4 files changed, 110 insertions(+), 15 deletions(-) create mode 100644 timefold-solver-python-core/src/main/java/ai/timefold/solver/python/score/constraint/ConstraintRefPythonJavaTypeMapping.java diff --git a/tests/test_solution_manager.py b/tests/test_solution_manager.py index 2c03ed51..5d7b98a8 100644 --- a/tests/test_solution_manager.py +++ b/tests/test_solution_manager.py @@ -220,6 +220,13 @@ def test_score_manager_constraint_analysis_map(): assert constraint_analysis.match_count == 3 +def test_score_manager_constraint_ref(): + constraint_ref = ConstraintRef.parse_id('package/Maximize Value') + + assert constraint_ref.package_name == 'package' + assert constraint_ref.constraint_name == 'Maximize Value' + + ignored_java_functions = { 'equals', 'getClass', @@ -232,7 +239,8 @@ def test_score_manager_constraint_analysis_map(): } ignored_java_functions_per_class = { - 'Indictment': {'getJustification'} # deprecated + 'Indictment': {'getJustification'}, # deprecated + 'ConstraintRef': {'of', 'packageName', 'constraintName'} # built-in constructor and properties with @dataclass } @@ -242,6 +250,7 @@ def test_has_all_methods(): (ScoreAnalysis, JavaScoreAnalysis), (ConstraintAnalysis, JavaConstraintAnalysis), (ScoreExplanation, JavaScoreExplanation), + (ConstraintRef, JavaConstraintRef), (Indictment, JavaIndictment)): type_name = python_type.__name__ ignored_java_functions_type = ignored_java_functions_per_class[ diff --git a/timefold-solver-python-core/src/main/java/ai/timefold/solver/python/score/constraint/ConstraintRefPythonJavaTypeMapping.java b/timefold-solver-python-core/src/main/java/ai/timefold/solver/python/score/constraint/ConstraintRefPythonJavaTypeMapping.java new file mode 100644 index 00000000..16544143 --- /dev/null +++ b/timefold-solver-python-core/src/main/java/ai/timefold/solver/python/score/constraint/ConstraintRefPythonJavaTypeMapping.java @@ -0,0 +1,60 @@ +package ai.timefold.solver.python.score.constraint; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; + +import ai.timefold.jpyinterpreter.PythonLikeObject; +import ai.timefold.jpyinterpreter.types.PythonJavaTypeMapping; +import ai.timefold.jpyinterpreter.types.PythonLikeType; +import ai.timefold.jpyinterpreter.types.PythonString; +import ai.timefold.solver.core.api.score.constraint.ConstraintRef; + +public final class ConstraintRefPythonJavaTypeMapping implements PythonJavaTypeMapping { + private final PythonLikeType type; + private final Constructor constructor; + private final Field packageNameField; + private final Field constraintNameField; + + public ConstraintRefPythonJavaTypeMapping(PythonLikeType type) + throws ClassNotFoundException, NoSuchFieldException, NoSuchMethodException { + this.type = type; + Class clazz = type.getJavaClass(); + constructor = clazz.getConstructor(); + packageNameField = clazz.getField("package_name"); + constraintNameField = clazz.getField("constraint_name"); + } + + @Override + public PythonLikeType getPythonType() { + return type; + } + + @Override + public Class getJavaType() { + return ConstraintRef.class; + } + + @Override + public PythonLikeObject toPythonObject(ConstraintRef javaObject) { + try { + var instance = constructor.newInstance(); + packageNameField.set(instance, PythonString.valueOf(javaObject.packageName())); + constraintNameField.set(instance, PythonString.valueOf(javaObject.constraintName())); + return (PythonLikeObject) instance; + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + @Override + public ConstraintRef toJavaObject(PythonLikeObject pythonObject) { + try { + var packageName = ((PythonString) packageNameField.get(pythonObject)).value.toString(); + var constraintName = ((PythonString) constraintNameField.get(pythonObject)).value.toString(); + return ConstraintRef.of(packageName, constraintName); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } +} diff --git a/timefold-solver-python-core/src/main/python/_timefold_java_interop.py b/timefold-solver-python-core/src/main/python/_timefold_java_interop.py index 05a7f833..87ea54a6 100644 --- a/timefold-solver-python-core/src/main/python/_timefold_java_interop.py +++ b/timefold-solver-python-core/src/main/python/_timefold_java_interop.py @@ -98,13 +98,14 @@ def update_log_level() -> None: PythonLoggingToLogbackAdapter.setLevel(logger.getEffectiveLevel()) -def register_score_python_java_type_mappings(): +def register_python_java_type_mappings(): global _scores_registered, _java_score_mapping_dict, _python_score_mapping_dict if _scores_registered: return _scores_registered = True + # score types from .score._score import SimpleScore, HardSoftScore, HardMediumSoftScore, BendableScore from ai.timefold.solver.core.api.score.buildin.simplelong import SimpleLongScore as _SimpleScore from ai.timefold.solver.core.api.score.buildin.hardsoftlong import HardSoftLongScore as _HardSoftScore @@ -137,6 +138,20 @@ def register_score_python_java_type_mappings(): add_python_java_type_mapping(HardMediumSoftScorePythonJavaTypeMapping(HardMediumSoftScoreType)) add_python_java_type_mapping(BendableScorePythonJavaTypeMapping(BendableScoreType)) + # score analysis types + from .score._score_analysis import ConstraintRef + from ai.timefold.solver.core.api.score.constraint import ConstraintRef as _ConstraintRef + + from ai.timefold.solver.python.score.constraint import ConstraintRefPythonJavaTypeMapping + + _python_score_mapping_dict['ConstraintRef'] = ConstraintRef + + _java_score_mapping_dict['ConstraintRef'] = _ConstraintRef + + ConstraintRefType = translate_python_class_to_java_class(ConstraintRef) + + add_python_java_type_mapping(ConstraintRefPythonJavaTypeMapping(ConstraintRefType)) + def forward_logging_events(event: 'PythonLoggingEvent') -> None: logger.log(event.level().getPythonLevelNumber(), @@ -301,7 +316,7 @@ def _add_to_compilation_queue(python_class: type | PythonSupplier) -> None: def _process_compilation_queue() -> None: global _compilation_queue - register_score_python_java_type_mappings() + register_python_java_type_mappings() while len(_compilation_queue) > 0: python_class = _compilation_queue.pop(0) @@ -324,7 +339,7 @@ def _generate_constraint_provider_class(original_function: Callable[['_Constrain wrapped_constraint_provider: Callable[['_ConstraintFactory'], list['_Constraint']]) -> JClass: ensure_init() - register_score_python_java_type_mappings() + register_python_java_type_mappings() from ai.timefold.solver.python import PythonWrapperGenerator # noqa from ai.timefold.solver.core.api.score.stream import ConstraintProvider class_identifier = _get_class_identifier_for_object(original_function) diff --git a/timefold-solver-python-core/src/main/python/score/_score_analysis.py b/timefold-solver-python-core/src/main/python/score/_score_analysis.py index 4b535cae..08ab0983 100644 --- a/timefold-solver-python-core/src/main/python/score/_score_analysis.py +++ b/timefold-solver-python-core/src/main/python/score/_score_analysis.py @@ -1,5 +1,6 @@ from .._timefold_java_interop import get_class from .._jpype_type_conversions import to_python_score +from .._timefold_java_interop import _java_score_mapping_dict from _jpyinterpreter import unwrap_python_like_object, add_java_interface from dataclasses import dataclass from multipledispatch import dispatch @@ -42,17 +43,26 @@ class ConstraintRef: The constraint name. It might not be unique, but `constraint_id` is unique. When using a `constraint_configuration`, it is equal to the `ConstraintWeight.constraint_name`. + + constraint_id : str + Always derived from `packageName` and `constraintName`. """ package_name: str constraint_name: str @property def constraint_id(self) -> str: - """ - Always derived from packageName and constraintName. - """ return f'{self.package_name}/{self.constraint_name}' + @staticmethod + def parse_id(constraint_id: str): + slash_index = constraint_id.rfind('/') + if slash_index == -1: raise RuntimeError( + f'The constraint_id {constraint_id} is invalid as it does not contain a package separator \'/\'.') + package_name = constraint_id[:slash_index] + constraint_name = constraint_id[slash_index + 1:] + return ConstraintRef(package_name, constraint_name) + @staticmethod def compose_constraint_id(solution_type_or_package: Union[type, str], constraint_name: str) -> str: """ @@ -78,6 +88,9 @@ def compose_constraint_id(solution_type_or_package: Union[type, str], constraint return ConstraintRef(package_name=package, constraint_name=constraint_name).constraint_id + def _to_java(self): + return _java_score_mapping_dict['ConstraintRef'].of(self.package_name, self.constraint_name) + def _safe_hash(obj: Any) -> int: try: @@ -201,7 +214,7 @@ def _map_constraint_match_set(constraint_match_set: set['_JavaConstraintMatch']) .getConstraintRef().constraintName()), justification=_unwrap_justification(constraint_match.getJustification()), indicted_objects=tuple([unwrap_python_like_object(indicted) - for indicted in cast(list, constraint_match.getIndictedObjectList())]), + for indicted in cast(list, constraint_match.getIndictedObjectList())]), score=to_python_score(constraint_match.getScore()) ) for constraint_match in constraint_match_set @@ -214,7 +227,7 @@ def _unwrap_justification(justification: Any) -> ConstraintJustification: if isinstance(justification, _JavaDefaultConstraintJustification): fact_list = justification.getFacts() return DefaultConstraintJustification(facts=tuple([unwrap_python_like_object(fact) - for fact in cast(list, fact_list)]), + for fact in cast(list, fact_list)]), impact=to_python_score(justification.getImpact())) else: return unwrap_python_like_object(justification) @@ -245,6 +258,7 @@ class Indictment(Generic[Score_]): in `constraint_match_set`. """ + def __init__(self, delegate: '_JavaIndictment[Score_]'): self._delegate = delegate @@ -496,6 +510,7 @@ def score(self) -> Score_: def summarize(self) -> str: return self._delegate.summarize() + class ScoreAnalysis: """ Represents the breakdown of a `Score` into individual `ConstraintAnalysis` instances, @@ -592,7 +607,7 @@ def constraint_analysis(self, constraint_package: str, constraint_name: str) -> return ConstraintAnalysis(self._delegate.getConstraintAnalysis(constraint_package, constraint_name)) @dispatch(ConstraintRef) - def constraint_analysis(self, constraint_ref: ConstraintRef) -> ConstraintAnalysis: + def constraint_analysis(self, constraint_ref: 'ConstraintRef') -> ConstraintAnalysis: """ Performs a lookup on `constraint_map`. @@ -605,7 +620,7 @@ def constraint_analysis(self, constraint_ref: ConstraintRef) -> ConstraintAnalys ConstraintAnalysis None if no constraint matches of such constraint are present """ - return self.constraint_analysis(constraint_ref.package_name, constraint_ref.constraint_name) + return ConstraintAnalysis(self._delegate.getConstraintAnalysis(constraint_ref._to_java())) @property def summarize(self) -> str: @@ -615,7 +630,6 @@ def summarize(self) -> str: def is_solution_initialized(self) -> bool: return self._delegate.isSolutionInitialized() - def diff(self, other: 'ScoreAnalysis') -> 'ScoreAnalysis': """ Compare this `ScoreAnalysis to another `ScoreAnalysis` @@ -647,9 +661,6 @@ def diff(self, other: 'ScoreAnalysis') -> 'ScoreAnalysis': return ScoreAnalysis(self._delegate.diff(other._delegate)) - - - __all__ = ['ScoreExplanation', 'ConstraintRef', 'ConstraintMatch', 'ConstraintMatchTotal', 'ConstraintJustification', 'DefaultConstraintJustification', 'Indictment', From d42ef22c2b857df2d6452a486f5d5e18ab54eda4 Mon Sep 17 00:00:00 2001 From: Fred Date: Wed, 3 Jul 2024 10:32:24 -0300 Subject: [PATCH 08/16] chore: Sonar fixes --- .../score/constraint/ConstraintRefPythonJavaTypeMapping.java | 4 ++-- .../src/main/python/score/_score_analysis.py | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/timefold-solver-python-core/src/main/java/ai/timefold/solver/python/score/constraint/ConstraintRefPythonJavaTypeMapping.java b/timefold-solver-python-core/src/main/java/ai/timefold/solver/python/score/constraint/ConstraintRefPythonJavaTypeMapping.java index 16544143..15dc8def 100644 --- a/timefold-solver-python-core/src/main/java/ai/timefold/solver/python/score/constraint/ConstraintRefPythonJavaTypeMapping.java +++ b/timefold-solver-python-core/src/main/java/ai/timefold/solver/python/score/constraint/ConstraintRefPythonJavaTypeMapping.java @@ -50,8 +50,8 @@ public PythonLikeObject toPythonObject(ConstraintRef javaObject) { @Override public ConstraintRef toJavaObject(PythonLikeObject pythonObject) { try { - var packageName = ((PythonString) packageNameField.get(pythonObject)).value.toString(); - var constraintName = ((PythonString) constraintNameField.get(pythonObject)).value.toString(); + var packageName = ((PythonString) packageNameField.get(pythonObject)).value; + var constraintName = ((PythonString) constraintNameField.get(pythonObject)).value; return ConstraintRef.of(packageName, constraintName); } catch (IllegalAccessException e) { throw new RuntimeException(e); diff --git a/timefold-solver-python-core/src/main/python/score/_score_analysis.py b/timefold-solver-python-core/src/main/python/score/_score_analysis.py index 08ab0983..d5cf7860 100644 --- a/timefold-solver-python-core/src/main/python/score/_score_analysis.py +++ b/timefold-solver-python-core/src/main/python/score/_score_analysis.py @@ -57,8 +57,9 @@ def constraint_id(self) -> str: @staticmethod def parse_id(constraint_id: str): slash_index = constraint_id.rfind('/') - if slash_index == -1: raise RuntimeError( - f'The constraint_id {constraint_id} is invalid as it does not contain a package separator \'/\'.') + if slash_index == -1: + raise IndexError( + f'The constraint_id {constraint_id} is invalid as it does not contain a package separator \'/\'.') package_name = constraint_id[:slash_index] constraint_name = constraint_id[slash_index + 1:] return ConstraintRef(package_name, constraint_name) From f77d4e51a4479ffbbdd8b8a765d0ae3bd5ae71bd Mon Sep 17 00:00:00 2001 From: Fred Date: Wed, 3 Jul 2024 10:55:48 -0300 Subject: [PATCH 09/16] feat: sync ConstraintMatch and ConstraintRef API --- tests/test_solution_manager.py | 12 +++- .../ConstraintRefPythonJavaTypeMapping.java | 60 ------------------- .../src/main/python/_timefold_java_interop.py | 21 +------ .../src/main/python/score/_score_analysis.py | 7 ++- 4 files changed, 20 insertions(+), 80 deletions(-) delete mode 100644 timefold-solver-python-core/src/main/java/ai/timefold/solver/python/score/constraint/ConstraintRefPythonJavaTypeMapping.java diff --git a/tests/test_solution_manager.py b/tests/test_solution_manager.py index 5d7b98a8..e74de27f 100644 --- a/tests/test_solution_manager.py +++ b/tests/test_solution_manager.py @@ -240,7 +240,16 @@ def test_score_manager_constraint_ref(): ignored_java_functions_per_class = { 'Indictment': {'getJustification'}, # deprecated - 'ConstraintRef': {'of', 'packageName', 'constraintName'} # built-in constructor and properties with @dataclass + 'ConstraintRef': {'of', 'packageName', 'constraintName'}, # built-in constructor and properties with @dataclass + 'ConstraintMatch': { + 'getConstraintRef', # built-in constructor and properties with @dataclass + 'getConstraintPackage', # deprecated + 'getConstraintName', # deprecated + 'getConstraintId', # deprecated + 'getJustificationList', # deprecated + 'getJustification', # built-in constructor and properties with @dataclass + 'getScore', # built-in constructor and properties with @dataclass + } } @@ -250,6 +259,7 @@ def test_has_all_methods(): (ScoreAnalysis, JavaScoreAnalysis), (ConstraintAnalysis, JavaConstraintAnalysis), (ScoreExplanation, JavaScoreExplanation), + (ConstraintMatch, JavaConstraintMatch), (ConstraintRef, JavaConstraintRef), (Indictment, JavaIndictment)): type_name = python_type.__name__ diff --git a/timefold-solver-python-core/src/main/java/ai/timefold/solver/python/score/constraint/ConstraintRefPythonJavaTypeMapping.java b/timefold-solver-python-core/src/main/java/ai/timefold/solver/python/score/constraint/ConstraintRefPythonJavaTypeMapping.java deleted file mode 100644 index 15dc8def..00000000 --- a/timefold-solver-python-core/src/main/java/ai/timefold/solver/python/score/constraint/ConstraintRefPythonJavaTypeMapping.java +++ /dev/null @@ -1,60 +0,0 @@ -package ai.timefold.solver.python.score.constraint; - -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; - -import ai.timefold.jpyinterpreter.PythonLikeObject; -import ai.timefold.jpyinterpreter.types.PythonJavaTypeMapping; -import ai.timefold.jpyinterpreter.types.PythonLikeType; -import ai.timefold.jpyinterpreter.types.PythonString; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; - -public final class ConstraintRefPythonJavaTypeMapping implements PythonJavaTypeMapping { - private final PythonLikeType type; - private final Constructor constructor; - private final Field packageNameField; - private final Field constraintNameField; - - public ConstraintRefPythonJavaTypeMapping(PythonLikeType type) - throws ClassNotFoundException, NoSuchFieldException, NoSuchMethodException { - this.type = type; - Class clazz = type.getJavaClass(); - constructor = clazz.getConstructor(); - packageNameField = clazz.getField("package_name"); - constraintNameField = clazz.getField("constraint_name"); - } - - @Override - public PythonLikeType getPythonType() { - return type; - } - - @Override - public Class getJavaType() { - return ConstraintRef.class; - } - - @Override - public PythonLikeObject toPythonObject(ConstraintRef javaObject) { - try { - var instance = constructor.newInstance(); - packageNameField.set(instance, PythonString.valueOf(javaObject.packageName())); - constraintNameField.set(instance, PythonString.valueOf(javaObject.constraintName())); - return (PythonLikeObject) instance; - } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { - throw new RuntimeException(e); - } - } - - @Override - public ConstraintRef toJavaObject(PythonLikeObject pythonObject) { - try { - var packageName = ((PythonString) packageNameField.get(pythonObject)).value; - var constraintName = ((PythonString) constraintNameField.get(pythonObject)).value; - return ConstraintRef.of(packageName, constraintName); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } - } -} diff --git a/timefold-solver-python-core/src/main/python/_timefold_java_interop.py b/timefold-solver-python-core/src/main/python/_timefold_java_interop.py index 87ea54a6..05a7f833 100644 --- a/timefold-solver-python-core/src/main/python/_timefold_java_interop.py +++ b/timefold-solver-python-core/src/main/python/_timefold_java_interop.py @@ -98,14 +98,13 @@ def update_log_level() -> None: PythonLoggingToLogbackAdapter.setLevel(logger.getEffectiveLevel()) -def register_python_java_type_mappings(): +def register_score_python_java_type_mappings(): global _scores_registered, _java_score_mapping_dict, _python_score_mapping_dict if _scores_registered: return _scores_registered = True - # score types from .score._score import SimpleScore, HardSoftScore, HardMediumSoftScore, BendableScore from ai.timefold.solver.core.api.score.buildin.simplelong import SimpleLongScore as _SimpleScore from ai.timefold.solver.core.api.score.buildin.hardsoftlong import HardSoftLongScore as _HardSoftScore @@ -138,20 +137,6 @@ def register_python_java_type_mappings(): add_python_java_type_mapping(HardMediumSoftScorePythonJavaTypeMapping(HardMediumSoftScoreType)) add_python_java_type_mapping(BendableScorePythonJavaTypeMapping(BendableScoreType)) - # score analysis types - from .score._score_analysis import ConstraintRef - from ai.timefold.solver.core.api.score.constraint import ConstraintRef as _ConstraintRef - - from ai.timefold.solver.python.score.constraint import ConstraintRefPythonJavaTypeMapping - - _python_score_mapping_dict['ConstraintRef'] = ConstraintRef - - _java_score_mapping_dict['ConstraintRef'] = _ConstraintRef - - ConstraintRefType = translate_python_class_to_java_class(ConstraintRef) - - add_python_java_type_mapping(ConstraintRefPythonJavaTypeMapping(ConstraintRefType)) - def forward_logging_events(event: 'PythonLoggingEvent') -> None: logger.log(event.level().getPythonLevelNumber(), @@ -316,7 +301,7 @@ def _add_to_compilation_queue(python_class: type | PythonSupplier) -> None: def _process_compilation_queue() -> None: global _compilation_queue - register_python_java_type_mappings() + register_score_python_java_type_mappings() while len(_compilation_queue) > 0: python_class = _compilation_queue.pop(0) @@ -339,7 +324,7 @@ def _generate_constraint_provider_class(original_function: Callable[['_Constrain wrapped_constraint_provider: Callable[['_ConstraintFactory'], list['_Constraint']]) -> JClass: ensure_init() - register_python_java_type_mappings() + register_score_python_java_type_mappings() from ai.timefold.solver.python import PythonWrapperGenerator # noqa from ai.timefold.solver.core.api.score.stream import ConstraintProvider class_identifier = _get_class_identifier_for_object(original_function) diff --git a/timefold-solver-python-core/src/main/python/score/_score_analysis.py b/timefold-solver-python-core/src/main/python/score/_score_analysis.py index d5cf7860..c83fb7b0 100644 --- a/timefold-solver-python-core/src/main/python/score/_score_analysis.py +++ b/timefold-solver-python-core/src/main/python/score/_score_analysis.py @@ -90,7 +90,8 @@ def compose_constraint_id(solution_type_or_package: Union[type, str], constraint constraint_name=constraint_name).constraint_id def _to_java(self): - return _java_score_mapping_dict['ConstraintRef'].of(self.package_name, self.constraint_name) + from ai.timefold.solver.core.api.score.constraint import ConstraintRef as JavaConstraintRef + return JavaConstraintRef.of(self.package_name, self.constraint_name) def _safe_hash(obj: Any) -> int: @@ -118,6 +119,10 @@ class ConstraintMatch(Generic[Score_]): def identification_string(self) -> str: return self.constraint_ref.constraint_id + @property + def get_indicted_object_list(self): + return self.indicted_objects + def __hash__(self) -> int: combined_hash = hash(self.constraint_ref) combined_hash ^= _safe_hash(self.justification) From 002566b4ca7960f07c9146526c9b1de73cc4da81 Mon Sep 17 00:00:00 2001 From: Fred Date: Wed, 3 Jul 2024 11:57:23 -0300 Subject: [PATCH 10/16] feat: sync ConstraintMatchTotal API --- tests/test_solution_manager.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/test_solution_manager.py b/tests/test_solution_manager.py index e74de27f..41ba916f 100644 --- a/tests/test_solution_manager.py +++ b/tests/test_solution_manager.py @@ -249,7 +249,18 @@ def test_score_manager_constraint_ref(): 'getJustificationList', # deprecated 'getJustification', # built-in constructor and properties with @dataclass 'getScore', # built-in constructor and properties with @dataclass - } + }, + 'ConstraintMatchTotal': { + 'getConstraintRef', # built-in constructor and properties with @dataclass + 'composeConstraintId', # deprecated + 'getConstraintPackage', # deprecated + 'getConstraintName', # deprecated + 'getConstraintId', # deprecated + 'getConstraintMatchCount', # built-in constructor and properties with @dataclass + 'getConstraintMatchSet', # built-in constructor and properties with @dataclass + 'getConstraintWeight', # built-in constructor and properties with @dataclass + 'getScore', # built-in constructor and properties with @dataclass + }, } @@ -260,6 +271,7 @@ def test_has_all_methods(): (ConstraintAnalysis, JavaConstraintAnalysis), (ScoreExplanation, JavaScoreExplanation), (ConstraintMatch, JavaConstraintMatch), + (ConstraintMatchTotal, JavaConstraintMatchTotal), (ConstraintRef, JavaConstraintRef), (Indictment, JavaIndictment)): type_name = python_type.__name__ From 0441b9e8d842a74c712082c1a31f9a8f9de29f1f Mon Sep 17 00:00:00 2001 From: Fred Date: Wed, 3 Jul 2024 14:40:02 -0300 Subject: [PATCH 11/16] fix: address PR comments --- pyproject.toml | 3 +- setup.py | 3 +- tests/test_solution_manager.py | 10 +++- .../src/main/python/score/_score_analysis.py | 48 +++++++++---------- tox.ini | 1 - 5 files changed, 33 insertions(+), 32 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 534b55ac..66a5388f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,6 @@ requires = [ "setuptools>=69.1.1", "stubgenj>=0.2.5", "JPype1>=1.5.0", - "wheel", - "multipledispatch>=1.0.0" + "wheel" ] build-backend = "setuptools.build_meta" diff --git a/setup.py b/setup.py index 6b7aeb49..e9087d73 100644 --- a/setup.py +++ b/setup.py @@ -145,8 +145,7 @@ def find_stub_files(stub_root: str): test_suite='tests', python_requires='>=3.10', install_requires=[ - 'JPype1>=1.5.0', - 'multipledispatch>=1.0.0' + 'JPype1>=1.5.0' ], cmdclass={'build_py': FetchDependencies}, package_data={ diff --git a/tests/test_solution_manager.py b/tests/test_solution_manager.py index 41ba916f..3c53a931 100644 --- a/tests/test_solution_manager.py +++ b/tests/test_solution_manager.py @@ -141,14 +141,14 @@ def assert_score_analysis(problem: Solution, score_analysis: ScoreAnalysis): def assert_score_analysis_summary(score_analysis: ScoreAnalysis): - summary = score_analysis.summarize + summary = score_analysis.summary assert "Explanation of score (3):" in summary assert "Constraint matches:" in summary assert "3: constraint (Maximize Value) has 3 matches:" in summary assert "1: justified with" in summary match = score_analysis.constraint_analyses[0] - match_summary = match.summarize + match_summary = match.summary assert "Explanation of score (3):" in match_summary assert "Constraint matches:" in match_summary assert "3: constraint (Maximize Value) has 3 matches:" in match_summary @@ -201,6 +201,10 @@ def test_score_manager_diff(): diff = score_analysis.diff(second_score_analysis) assert diff.score.score == -1 + diff_operation = score_analysis - second_score_analysis + assert diff_operation.score.score == -1 + print(diff, diff_operation) + constraint_analyses = score_analysis.constraint_analyses assert len(constraint_analyses) == 1 @@ -241,6 +245,8 @@ def test_score_manager_constraint_ref(): ignored_java_functions_per_class = { 'Indictment': {'getJustification'}, # deprecated 'ConstraintRef': {'of', 'packageName', 'constraintName'}, # built-in constructor and properties with @dataclass + 'ConstraintAnalysis': {'summarize'}, # using summary instead + 'ScoreAnalysis': {'summarize'}, # using summary instead 'ConstraintMatch': { 'getConstraintRef', # built-in constructor and properties with @dataclass 'getConstraintPackage', # deprecated diff --git a/timefold-solver-python-core/src/main/python/score/_score_analysis.py b/timefold-solver-python-core/src/main/python/score/_score_analysis.py index c83fb7b0..a9df10f4 100644 --- a/timefold-solver-python-core/src/main/python/score/_score_analysis.py +++ b/timefold-solver-python-core/src/main/python/score/_score_analysis.py @@ -3,9 +3,8 @@ from .._timefold_java_interop import _java_score_mapping_dict from _jpyinterpreter import unwrap_python_like_object, add_java_interface from dataclasses import dataclass -from multipledispatch import dispatch -from typing import TypeVar, Generic, Union, TYPE_CHECKING, Any, cast, Optional, Type +from typing import overload, TypeVar, Generic, Union, TYPE_CHECKING, Any, cast, Optional, Type if TYPE_CHECKING: # These imports require a JVM to be running, so only import if type checking @@ -120,7 +119,7 @@ def identification_string(self) -> str: return self.constraint_ref.constraint_id @property - def get_indicted_object_list(self): + def indicted_object_list(self): return self.indicted_objects def __hash__(self) -> int: @@ -467,7 +466,7 @@ class ConstraintAnalysis(Generic[Score_]): but still non-zero constraint weight; non-empty if constraint has matches. This is a list to simplify access to individual elements, but it contains no duplicates just like `set` wouldn't. - summarize : str + summary : str Returns a diagnostic text that explains part of the score quality through the ConstraintAnalysis API. match_count : int @@ -513,7 +512,7 @@ def score(self) -> Score_: return to_python_score(self._delegate.score()) @property - def summarize(self) -> str: + def summary(self) -> str: return self._delegate.summarize() @@ -547,7 +546,7 @@ class ScoreAnalysis: constraint_analyses : list[ConstraintAnalysis] Individual ConstraintAnalysis instances that make up this ScoreAnalysis. - summarize : str + summary : str Returns a diagnostic text that explains the solution through the `ConstraintAnalysis` API to identify which Constraints cause that score quality. The string is built fresh every time the method is called. @@ -572,7 +571,10 @@ def __init__(self, delegate: '_JavaScoreAnalysis'): self._delegate = delegate def __str__(self): - return self.summarize + return self.summary + + def __sub__(self, other): + return self.diff(other) @property def score(self) -> 'Score': @@ -595,30 +597,22 @@ def constraint_analyses(self) -> list[ConstraintAnalysis]: list['_JavaConstraintAnalysis[Score]'], self._delegate.constraintAnalyses()) ] - @dispatch(str, str) + @overload def constraint_analysis(self, constraint_package: str, constraint_name: str) -> ConstraintAnalysis: - """ - Performs a lookup on `constraint_map`. - - Parameters - ---------- - constraint_package : str - constraint_name : str + ... - Returns - ------- - ConstraintAnalysis - None if no constraint matches of such constraint are present - """ - return ConstraintAnalysis(self._delegate.getConstraintAnalysis(constraint_package, constraint_name)) - - @dispatch(ConstraintRef) + @overload def constraint_analysis(self, constraint_ref: 'ConstraintRef') -> ConstraintAnalysis: + ... + + def constraint_analysis(self, *args) -> ConstraintAnalysis: """ Performs a lookup on `constraint_map`. Parameters ---------- + constraint_package : str + constraint_name : str constraint_ref : ConstraintRef Returns @@ -626,10 +620,14 @@ def constraint_analysis(self, constraint_ref: 'ConstraintRef') -> ConstraintAnal ConstraintAnalysis None if no constraint matches of such constraint are present """ - return ConstraintAnalysis(self._delegate.getConstraintAnalysis(constraint_ref._to_java())) + print(args) + if len(args) == 1: + return ConstraintAnalysis(self._delegate.getConstraintAnalysis(args[0]._to_java())) + else: + return ConstraintAnalysis(self._delegate.getConstraintAnalysis(args[0], args[1])) @property - def summarize(self) -> str: + def summary(self) -> str: return self._delegate.summarize() @property diff --git a/tox.ini b/tox.ini index 5acf3ed8..007b69b6 100644 --- a/tox.ini +++ b/tox.ini @@ -13,7 +13,6 @@ deps = pytest-cov>=4.1.0 coverage>=7.4.3 JPype1>=1.5.0 - multipledispatch>=1.0.0 commands = pytest --import-mode=importlib {posargs} tests From 3798b11be68a7002a538c77ace0c3f013a866119 Mon Sep 17 00:00:00 2001 From: Fred Date: Wed, 3 Jul 2024 15:38:59 -0300 Subject: [PATCH 12/16] fix: address PR comments --- tests/test_solution_manager.py | 7 ++++++- .../src/main/python/score/_score_analysis.py | 10 +++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/tests/test_solution_manager.py b/tests/test_solution_manager.py index 3c53a931..0fad076a 100644 --- a/tests/test_solution_manager.py +++ b/tests/test_solution_manager.py @@ -147,6 +147,9 @@ def assert_score_analysis_summary(score_analysis: ScoreAnalysis): assert "3: constraint (Maximize Value) has 3 matches:" in summary assert "1: justified with" in summary + summary_str = str(score_analysis) + assert summary == summary_str + match = score_analysis.constraint_analyses[0] match_summary = match.summary assert "Explanation of score (3):" in match_summary @@ -154,6 +157,9 @@ def assert_score_analysis_summary(score_analysis: ScoreAnalysis): assert "3: constraint (Maximize Value) has 3 matches:" in match_summary assert "1: justified with" in match_summary + match_summary_str = str(match) + assert match_summary == match_summary_str + def assert_solution_manager(solution_manager: SolutionManager[Solution]): problem: Solution = Solution([Entity('A', 1), Entity('B', 1), Entity('C', 1)], [1, 2, 3]) @@ -203,7 +209,6 @@ def test_score_manager_diff(): diff_operation = score_analysis - second_score_analysis assert diff_operation.score.score == -1 - print(diff, diff_operation) constraint_analyses = score_analysis.constraint_analyses assert len(constraint_analyses) == 1 diff --git a/timefold-solver-python-core/src/main/python/score/_score_analysis.py b/timefold-solver-python-core/src/main/python/score/_score_analysis.py index a9df10f4..535e82c2 100644 --- a/timefold-solver-python-core/src/main/python/score/_score_analysis.py +++ b/timefold-solver-python-core/src/main/python/score/_score_analysis.py @@ -479,7 +479,7 @@ def __init__(self, delegate: '_JavaConstraintAnalysis[Score_]'): delegate.constraintRef() def __str__(self): - return self.summarize + return self.summary @property def constraint_ref(self) -> ConstraintRef: @@ -611,16 +611,16 @@ def constraint_analysis(self, *args) -> ConstraintAnalysis: Parameters ---------- - constraint_package : str - constraint_name : str - constraint_ref : ConstraintRef + *args: *tuple[str, str] | *tuple[ConstraintRef] + Either two strings or a single ConstraintRef can be passed as positional arguments. + If two strings are passed, they are taken to be the constraint package and constraint name, respectively. + If a ConstraintRef is passed, it is used to perform the lookup. Returns ------- ConstraintAnalysis None if no constraint matches of such constraint are present """ - print(args) if len(args) == 1: return ConstraintAnalysis(self._delegate.getConstraintAnalysis(args[0]._to_java())) else: From d965051fbd211358d0643094aede16a4861f78b4 Mon Sep 17 00:00:00 2001 From: Fred Date: Wed, 3 Jul 2024 18:29:58 -0300 Subject: [PATCH 13/16] fix: address PR comments --- tests/test_solution_manager.py | 5 +++-- .../src/main/python/score/_score_analysis.py | 16 ++++++---------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/tests/test_solution_manager.py b/tests/test_solution_manager.py index 0fad076a..482b2389 100644 --- a/tests/test_solution_manager.py +++ b/tests/test_solution_manager.py @@ -141,7 +141,7 @@ def assert_score_analysis(problem: Solution, score_analysis: ScoreAnalysis): def assert_score_analysis_summary(score_analysis: ScoreAnalysis): - summary = score_analysis.summary + summary = score_analysis.summarize assert "Explanation of score (3):" in summary assert "Constraint matches:" in summary assert "3: constraint (Maximize Value) has 3 matches:" in summary @@ -151,7 +151,7 @@ def assert_score_analysis_summary(score_analysis: ScoreAnalysis): assert summary == summary_str match = score_analysis.constraint_analyses[0] - match_summary = match.summary + match_summary = match.summarize assert "Explanation of score (3):" in match_summary assert "Constraint matches:" in match_summary assert "3: constraint (Maximize Value) has 3 matches:" in match_summary @@ -260,6 +260,7 @@ def test_score_manager_constraint_ref(): 'getJustificationList', # deprecated 'getJustification', # built-in constructor and properties with @dataclass 'getScore', # built-in constructor and properties with @dataclass + 'getIndictedObjectList', # built-in constructor and properties with @dataclass }, 'ConstraintMatchTotal': { 'getConstraintRef', # built-in constructor and properties with @dataclass diff --git a/timefold-solver-python-core/src/main/python/score/_score_analysis.py b/timefold-solver-python-core/src/main/python/score/_score_analysis.py index 535e82c2..c1a2e361 100644 --- a/timefold-solver-python-core/src/main/python/score/_score_analysis.py +++ b/timefold-solver-python-core/src/main/python/score/_score_analysis.py @@ -118,10 +118,6 @@ class ConstraintMatch(Generic[Score_]): def identification_string(self) -> str: return self.constraint_ref.constraint_id - @property - def indicted_object_list(self): - return self.indicted_objects - def __hash__(self) -> int: combined_hash = hash(self.constraint_ref) combined_hash ^= _safe_hash(self.justification) @@ -466,7 +462,7 @@ class ConstraintAnalysis(Generic[Score_]): but still non-zero constraint weight; non-empty if constraint has matches. This is a list to simplify access to individual elements, but it contains no duplicates just like `set` wouldn't. - summary : str + summarize : str Returns a diagnostic text that explains part of the score quality through the ConstraintAnalysis API. match_count : int @@ -479,7 +475,7 @@ def __init__(self, delegate: '_JavaConstraintAnalysis[Score_]'): delegate.constraintRef() def __str__(self): - return self.summary + return self.summarize @property def constraint_ref(self) -> ConstraintRef: @@ -512,7 +508,7 @@ def score(self) -> Score_: return to_python_score(self._delegate.score()) @property - def summary(self) -> str: + def summarize(self) -> str: return self._delegate.summarize() @@ -546,7 +542,7 @@ class ScoreAnalysis: constraint_analyses : list[ConstraintAnalysis] Individual ConstraintAnalysis instances that make up this ScoreAnalysis. - summary : str + summarize : str Returns a diagnostic text that explains the solution through the `ConstraintAnalysis` API to identify which Constraints cause that score quality. The string is built fresh every time the method is called. @@ -571,7 +567,7 @@ def __init__(self, delegate: '_JavaScoreAnalysis'): self._delegate = delegate def __str__(self): - return self.summary + return self.summarize def __sub__(self, other): return self.diff(other) @@ -627,7 +623,7 @@ def constraint_analysis(self, *args) -> ConstraintAnalysis: return ConstraintAnalysis(self._delegate.getConstraintAnalysis(args[0], args[1])) @property - def summary(self) -> str: + def summarize(self) -> str: return self._delegate.summarize() @property From 358d7f55f23b32793dbdac55f851cc36b3e4c1a5 Mon Sep 17 00:00:00 2001 From: Fred Date: Fri, 5 Jul 2024 13:11:35 -0300 Subject: [PATCH 14/16] fix: address PR comments --- tests/test_solution_manager.py | 4 ++-- .../src/main/python/score/_score_analysis.py | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/test_solution_manager.py b/tests/test_solution_manager.py index 482b2389..7b3822d5 100644 --- a/tests/test_solution_manager.py +++ b/tests/test_solution_manager.py @@ -141,7 +141,7 @@ def assert_score_analysis(problem: Solution, score_analysis: ScoreAnalysis): def assert_score_analysis_summary(score_analysis: ScoreAnalysis): - summary = score_analysis.summarize + summary = score_analysis.summarize() assert "Explanation of score (3):" in summary assert "Constraint matches:" in summary assert "3: constraint (Maximize Value) has 3 matches:" in summary @@ -151,7 +151,7 @@ def assert_score_analysis_summary(score_analysis: ScoreAnalysis): assert summary == summary_str match = score_analysis.constraint_analyses[0] - match_summary = match.summarize + match_summary = match.summarize() assert "Explanation of score (3):" in match_summary assert "Constraint matches:" in match_summary assert "3: constraint (Maximize Value) has 3 matches:" in match_summary diff --git a/timefold-solver-python-core/src/main/python/score/_score_analysis.py b/timefold-solver-python-core/src/main/python/score/_score_analysis.py index c1a2e361..27b97e04 100644 --- a/timefold-solver-python-core/src/main/python/score/_score_analysis.py +++ b/timefold-solver-python-core/src/main/python/score/_score_analysis.py @@ -475,7 +475,7 @@ def __init__(self, delegate: '_JavaConstraintAnalysis[Score_]'): delegate.constraintRef() def __str__(self): - return self.summarize + return self.summarize() @property def constraint_ref(self) -> ConstraintRef: @@ -507,7 +507,6 @@ def match_count(self) -> int: def score(self) -> Score_: return to_python_score(self._delegate.score()) - @property def summarize(self) -> str: return self._delegate.summarize() @@ -567,7 +566,7 @@ def __init__(self, delegate: '_JavaScoreAnalysis'): self._delegate = delegate def __str__(self): - return self.summarize + return self.summarize() def __sub__(self, other): return self.diff(other) @@ -622,7 +621,6 @@ def constraint_analysis(self, *args) -> ConstraintAnalysis: else: return ConstraintAnalysis(self._delegate.getConstraintAnalysis(args[0], args[1])) - @property def summarize(self) -> str: return self._delegate.summarize() From 1f742feb990d4ea5def0d6cc869e852e04945e06 Mon Sep 17 00:00:00 2001 From: Fred Date: Fri, 5 Jul 2024 14:56:25 -0300 Subject: [PATCH 15/16] fix: address PR comments --- tests/test_solution_manager.py | 4 ++-- .../src/main/python/score/_score_analysis.py | 14 ++++++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/tests/test_solution_manager.py b/tests/test_solution_manager.py index 7b3822d5..82784bf5 100644 --- a/tests/test_solution_manager.py +++ b/tests/test_solution_manager.py @@ -141,7 +141,7 @@ def assert_score_analysis(problem: Solution, score_analysis: ScoreAnalysis): def assert_score_analysis_summary(score_analysis: ScoreAnalysis): - summary = score_analysis.summarize() + summary = score_analysis.summary assert "Explanation of score (3):" in summary assert "Constraint matches:" in summary assert "3: constraint (Maximize Value) has 3 matches:" in summary @@ -151,7 +151,7 @@ def assert_score_analysis_summary(score_analysis: ScoreAnalysis): assert summary == summary_str match = score_analysis.constraint_analyses[0] - match_summary = match.summarize() + match_summary = match.summary assert "Explanation of score (3):" in match_summary assert "Constraint matches:" in match_summary assert "3: constraint (Maximize Value) has 3 matches:" in match_summary diff --git a/timefold-solver-python-core/src/main/python/score/_score_analysis.py b/timefold-solver-python-core/src/main/python/score/_score_analysis.py index 27b97e04..d54532c1 100644 --- a/timefold-solver-python-core/src/main/python/score/_score_analysis.py +++ b/timefold-solver-python-core/src/main/python/score/_score_analysis.py @@ -462,7 +462,7 @@ class ConstraintAnalysis(Generic[Score_]): but still non-zero constraint weight; non-empty if constraint has matches. This is a list to simplify access to individual elements, but it contains no duplicates just like `set` wouldn't. - summarize : str + summary : str Returns a diagnostic text that explains part of the score quality through the ConstraintAnalysis API. match_count : int @@ -475,7 +475,7 @@ def __init__(self, delegate: '_JavaConstraintAnalysis[Score_]'): delegate.constraintRef() def __str__(self): - return self.summarize() + return self.summary @property def constraint_ref(self) -> ConstraintRef: @@ -507,7 +507,8 @@ def match_count(self) -> int: def score(self) -> Score_: return to_python_score(self._delegate.score()) - def summarize(self) -> str: + @property + def summary(self) -> str: return self._delegate.summarize() @@ -541,7 +542,7 @@ class ScoreAnalysis: constraint_analyses : list[ConstraintAnalysis] Individual ConstraintAnalysis instances that make up this ScoreAnalysis. - summarize : str + summary : str Returns a diagnostic text that explains the solution through the `ConstraintAnalysis` API to identify which Constraints cause that score quality. The string is built fresh every time the method is called. @@ -566,7 +567,7 @@ def __init__(self, delegate: '_JavaScoreAnalysis'): self._delegate = delegate def __str__(self): - return self.summarize() + return self.summary def __sub__(self, other): return self.diff(other) @@ -621,7 +622,8 @@ def constraint_analysis(self, *args) -> ConstraintAnalysis: else: return ConstraintAnalysis(self._delegate.getConstraintAnalysis(args[0], args[1])) - def summarize(self) -> str: + @property + def summary(self) -> str: return self._delegate.summarize() @property From d88ad1344b02529a6f2779f82dd08d32194485b1 Mon Sep 17 00:00:00 2001 From: Fred Date: Fri, 5 Jul 2024 15:07:55 -0300 Subject: [PATCH 16/16] fix: address PR comments --- .../src/main/python/score/_score_analysis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/timefold-solver-python-core/src/main/python/score/_score_analysis.py b/timefold-solver-python-core/src/main/python/score/_score_analysis.py index d54532c1..5288f8f0 100644 --- a/timefold-solver-python-core/src/main/python/score/_score_analysis.py +++ b/timefold-solver-python-core/src/main/python/score/_score_analysis.py @@ -57,7 +57,7 @@ def constraint_id(self) -> str: def parse_id(constraint_id: str): slash_index = constraint_id.rfind('/') if slash_index == -1: - raise IndexError( + raise ValueError( f'The constraint_id {constraint_id} is invalid as it does not contain a package separator \'/\'.') package_name = constraint_id[:slash_index] constraint_name = constraint_id[slash_index + 1:]