Final best solution can't be used to explain the score
ubersan opened this issue · comments
Hi, it seems the returned object of getFinalBestSolution
can't be used in explainScore
.
You can reproduce this behavior by running this snippet:
import time
import optapy
import optapy.config
import optapy.constraint
import optapy.score
import optapy.types
from org.optaplanner.core.api.solver import SolverStatus
@optapy.problem_fact
class Value:
def __init__(self, number):
self.number = number
@optapy.planning_entity
class Entity:
def __init__(self, code, value=None):
self.code = code
self.value = value
@optapy.planning_variable(Value, ["value_range"])
def get_value(self):
return self.value
def set_value(self, value):
self.value = value
@optapy.planning_solution
class Solution:
def __init__(self, entity_list, value_list, score=None):
self.entity_list = entity_list
self.value_list = value_list
self.score = score
@optapy.planning_entity_collection_property(Entity)
def get_entity_list(self):
return self.entity_list
def set_entity_list(self, entity_list):
self.entity_list = entity_list
@optapy.problem_fact_collection_property(Value)
@optapy.value_range_provider("value_range")
def get_value_list(self):
return self.value_list
def set_value_list(self, value_list):
self.value_list = value_list
@optapy.planning_score(optapy.score.SimpleScore)
def get_score(self):
return self.score
def set_score(self, score):
self.score = score
@optapy.constraint_provider
def define_constraints(constraint_factory: optapy.constraint.ConstraintFactory):
return [
constraint_factory.for_each(Entity)
.group_by(optapy.constraint.ConstraintCollectors.min(lambda entity: entity.value.number))
.reward("Min value", optapy.score.SimpleScore.ONE, lambda min_value: min_value)
]
if __name__ == "__main__":
entity_a: Entity = Entity("A")
entity_b: Entity = Entity("B")
value_1 = Value(1)
value_2 = Value(2)
entity_a.set_value(value_1)
entity_b.set_value(value_1)
problem = Solution([entity_a, entity_b], [value_1, value_2])
solver_config = optapy.config.solver.SolverConfig()
solver_config \
.withEntityClasses(Entity) \
.withSolutionClass(Solution) \
.withConstraintProviderClass(define_constraints) \
.withTerminationSpentLimit(optapy.types.Duration.ofSeconds(3))
with optapy.solver_manager_create(solver_config) as solver_manager:
solver_job = solver_manager.solve(1, problem)
while solver_job.getSolverStatus() != SolverStatus.NOT_SOLVING:
time.sleep(1)
solution: Solution = solver_job.getFinalBestSolution()
# uncomment to make this work
# solution = Solution(entity_list=solution.entity_list, value_list=solution.value_list, score=solution.score)
score_manager = optapy.score_manager_create(optapy.solver_factory_create(solver_config))
explanation = score_manager.explainScore(solution)
print("explanation", explanation)
The output I get is the following:
21:42:42.143 [l-1-thread-1] INFO Solving started: time spent (66), best score (1), environment mode (REPRODUCIBLE), move thread count (NONE), random (JDK with seed 0).
21:42:42.156 [l-1-thread-1] INFO Construction Heuristic phase (0) ended: time spent (80), best score (1), score calculation speed (111/sec), step total (0).
21:42:45.078 [l-1-thread-1] INFO Local Search phase (1) ended: time spent (3002), best score (2), score calculation speed (242072/sec), step total (414).
21:42:45.079 [l-1-thread-1] INFO Solving ended: time spent (3002), best score (2), score calculation speed (235057/sec), phase total (2), environment mode (REPRODUCIBLE), move thread count (NONE).
Traceback (most recent call last):
File "/home/sandro/dev/hourclass/neo/issue.py", line 101, in <module>
explanation = score_manager.explainScore(solution)
File "/home/sandro/.cache/pypoetry/virtualenvs/neo-jZqp7wPN-py3.10/lib/python3.10/site-packages/optapy/optaplanner_api_wrappers.py", line 199, in explainScore
return self._wrap_call(lambda wrapped_solution: self._java_explainScore(wrapped_solution), solution)
File "/home/sandro/.cache/pypoetry/virtualenvs/neo-jZqp7wPN-py3.10/lib/python3.10/site-packages/optapy/optaplanner_api_wrappers.py", line 169, in _wrap_call
wrapped_problem = PythonSolver.wrapProblem(get_class(type(problem)), problem)
TypeError: No matching overloads found for *static* org.optaplanner.optapy.PythonSolver.wrapProblem(_jpype._JClass,org.jpyinterpreter.user.Solution), options are:
public static java.lang.Object org.optaplanner.optapy.PythonSolver.wrapProblem(java.lang.Class,org.optaplanner.jpyinterpreter.types.wrappers.OpaquePythonReference)
This can be fixed by manually creating a Solution
(uncomment line in bottom of the snippet) entity and using this for the explanation. Is this intentional or can/should I do something different?
I'm on
Python 3.10.9
openjdk 17.0.5 2022-10-18
Debian
with a kernel version of6.1.4-1
Thanks for the help 🙌
Sounds like a bug. From the stack trace, I am guessing what happening is ScoreManager.explainScore
is rewrapping an already wrapped problem.
Wrapping is the process of converting a CPython object to a Java compatible object. This involves passing a reference of the CPython object to its corresponding Java class constructor. It appears SolverJob.getFinalBestSolution
is returning the wrapped Java object, instead of the unwrapped CPython object. Thanks to JPype magic, you can interact with the wrapped Java object just fine in CPython, but Type issues will arise if you pass that object to code that expects a CPython reference.
Looking at the code, we return a normal Java SolverJob
and not a Python wrapper around SolverJob
. So the fix would be:
- Create a
SolverJob
Python wrapper, which unwraps the Java object (likeSolver
): - In
_PythonSolverManager
(optapy/optapy-core/src/main/python/optaplanner_api_wrappers.py
Lines 96 to 132 in 2be7988
SolverJob
instead of the JavaSolverJob
I see, thanks for the explanation, yes indeed it seems that the wrapping seems to be the problem.
I can bypass it for now with only minor impact on code and performance so it's not a big issue right now.
Hey,
What is the solution ? How did you bypass it ?
Use _unwrap_java_object
on the solution ?
Thx by advance !
check the commented out code at the bottom of the snippet in the issue description