11import jpype
22
3+ from ..api import VariableListener
34from ..constraint import ConstraintFactory
45from .._timefold_java_interop import ensure_init , _generate_constraint_provider_class , register_java_class
56from jpyinterpreter import JavaAnnotation
1112 from ai .timefold .solver .core .api .score .stream import Constraint as _Constraint
1213 from ai .timefold .solver .core .api .score import Score as _Score
1314 from ai .timefold .solver .core .api .score .calculator import IncrementalScoreCalculator as _IncrementalScoreCalculator
14- from ai .timefold .solver .core .api .domain .variable import PlanningVariableGraphType as _PlanningVariableGraphType , \
15- VariableListener as _VariableListener
15+ from ai .timefold .solver .core .api .domain .variable import PlanningVariableGraphType as _PlanningVariableGraphType
1616
1717
1818Solution_ = TypeVar ('Solution_' )
@@ -35,7 +35,7 @@ class PlanningVariable(JavaAnnotation):
3535 def __init__ (self , * ,
3636 value_range_provider_refs : List [str ] = None ,
3737 allows_unassigned : bool = False ,
38- graph_type : '_PlanningVariableGraphType' = None ):
38+ graph_type = None ):
3939 ensure_init ()
4040 from ai .timefold .solver .core .api .domain .variable import PlanningVariable as JavaPlanningVariable
4141 super ().__init__ (JavaPlanningVariable ,
@@ -75,19 +75,37 @@ def __init__(self, *,
7575
7676class ShadowVariable (JavaAnnotation ):
7777 def __init__ (self , * ,
78- variable_listener_class : Type ['_VariableListener' ] = None ,
78+ variable_listener_class : Type [VariableListener ] = None ,
7979 source_variable_name : str ,
8080 source_entity_class : Type = None ):
8181 ensure_init ()
8282 from .._timefold_java_interop import get_class
83+ from jpyinterpreter import get_java_type_for_python_type
8384 from ai .timefold .jpyinterpreter import PythonClassTranslator
8485 from ai .timefold .solver .core .api .domain .variable import (
85- ShadowVariable as JavaShadowVariable )
86+ ShadowVariable as JavaShadowVariable , VariableListener as JavaVariableListener )
87+
8688 super ().__init__ (JavaShadowVariable ,
8789 {
8890 'variableListenerClass' : get_class (variable_listener_class ),
8991 'sourceVariableName' : PythonClassTranslator .getJavaFieldName (source_variable_name ),
90- 'sourceEntityClass' : source_entity_class ,
92+ 'sourceEntityClass' : get_class (source_entity_class ),
93+ })
94+
95+
96+ class PiggybackShadowVariable (JavaAnnotation ):
97+ def __init__ (self , * ,
98+ shadow_variable_name : str ,
99+ shadow_entity_class : Type = None ):
100+ ensure_init ()
101+ from .._timefold_java_interop import get_class
102+ from ai .timefold .jpyinterpreter import PythonClassTranslator
103+ from ai .timefold .solver .core .api .domain .variable import (
104+ PiggybackShadowVariable as JavaPiggybackShadowVariable )
105+ super ().__init__ (JavaPiggybackShadowVariable ,
106+ {
107+ 'shadowVariableName' : PythonClassTranslator .getJavaFieldName (shadow_variable_name ),
108+ 'shadowEntityClass' : get_class (shadow_entity_class ),
91109 })
92110
93111
@@ -455,100 +473,6 @@ def resetWorkingSolution(self, workingSolution: Solution_, constraintMatchEnable
455473 return register_java_class (incremental_score_calculator , java_class )
456474
457475
458- def variable_listener (variable_listener_class : Type ['_VariableListener' ] = None , / , * ,
459- require_unique_entity_events : bool = False ) -> Type ['_VariableListener' ]:
460- """Changes shadow variables when a genuine planning variable changes.
461- Important: it must only change the shadow variable(s) for which it's configured!
462- It should never change a genuine variable or a problem fact.
463- It can change its shadow variable(s) on multiple entity instances
464- (for example: an arrival_time change affects all trailing entities too).
465-
466- It is recommended that implementations be kept stateless.
467- If state must be implemented, implementations may need to override the default methods
468- resetWorkingSolution(score_director: ScoreDirector) and close().
469-
470- The following methods must exist:
471-
472- def beforeEntityAdded(score_director: ScoreDirector[Solution_], entity: Entity_);
473-
474- def afterEntityAdded(score_director: ScoreDirector[Solution_], entity: Entity_);
475-
476- def beforeEntityRemoved(score_director: ScoreDirector[Solution_], entity: Entity_);
477-
478- def afterEntityRemoved(score_director: ScoreDirector[Solution_], entity: Entity_);
479-
480- def beforeVariableChanged(score_director: ScoreDirector[Solution_], entity: Entity_);
481-
482- def afterVariableChanged(score_director: ScoreDirector[Solution_], entity: Entity_);
483-
484- If the implementation is stateful, then the following methods should also be defined:
485-
486- def resetWorkingSolution(score_director: ScoreDirector)
487-
488- def close()
489-
490- :param require_unique_entity_events: Set to True to guarantee that each of the before/after methods will only be
491- called once per entity instance per operation type (add, change or remove).
492- When set to True, this has a slight performance loss.
493- When set to False, it's often easier to make the listener implementation
494- correct and fast.
495- Defaults to False
496-
497- :type variable_listener_class: '_VariableListener'
498- :type require_unique_entity_events: bool
499- :rtype: Type
500- """
501- ensure_init ()
502-
503- def variable_listener_wrapper (the_variable_listener_class ):
504- from jpyinterpreter import translate_python_class_to_java_class , generate_proxy_class_for_translated_class
505- from ai .timefold .solver .core .api .domain .variable import VariableListener
506- methods = ['beforeEntityAdded' ,
507- 'afterEntityAdded' ,
508- 'beforeVariableChanged' ,
509- 'afterVariableChanged' ,
510- 'beforeEntityRemoved' ,
511- 'afterEntityRemoved' ]
512-
513- missing_method_list = []
514- for method in methods :
515- if not callable (getattr (the_variable_listener_class , method , None )):
516- missing_method_list .append (method )
517- if len (missing_method_list ) != 0 :
518- raise ValueError (f'The following required methods are missing from @variable_listener class '
519- f'{ the_variable_listener_class } : { missing_method_list } ' )
520-
521- method_on_class = getattr (the_variable_listener_class , 'requiresUniqueEntityEvents' , None )
522- if method_on_class is None :
523- def class_requires_unique_entity_events (self ):
524- return require_unique_entity_events
525-
526- setattr (the_variable_listener_class , 'requiresUniqueEntityEvents' , class_requires_unique_entity_events )
527-
528- method_on_class = getattr (the_variable_listener_class , 'close' , None )
529- if method_on_class is None :
530- def close (self ):
531- pass
532-
533- setattr (the_variable_listener_class , 'close' , close )
534-
535- method_on_class = getattr (the_variable_listener_class , 'resetWorkingSolution' , None )
536- if method_on_class is None :
537- def reset_working_solution (self , score_director ):
538- pass
539-
540- setattr (the_variable_listener_class , 'resetWorkingSolution' , reset_working_solution )
541-
542- translated_class = translate_python_class_to_java_class (the_variable_listener_class )
543- java_class = generate_proxy_class_for_translated_class (VariableListener , translated_class )
544- return register_java_class (the_variable_listener_class , java_class )
545-
546- if variable_listener_class : # Called as @variable_listener
547- return variable_listener_wrapper (variable_listener_class )
548- else : # Called as @variable_listener(require_unique_entity_events=True)
549- return variable_listener_wrapper
550-
551-
552476def problem_change (problem_change_class : Type ['_ProblemChange' ]) -> \
553477 Type ['_ProblemChange' ]:
554478 """A ProblemChange represents a change in 1 or more planning entities or problem facts of a PlanningSolution.
@@ -599,6 +523,7 @@ def wrapper_doChange(self, solution, problem_change_director):
599523
600524__all__ = ['PlanningId' , 'PlanningScore' , 'PlanningPin' , 'PlanningVariable' ,
601525 'PlanningListVariable' , 'PlanningVariableReference' , 'ShadowVariable' ,
526+ 'PiggybackShadowVariable' ,
602527 'IndexShadowVariable' , 'AnchorShadowVariable' , 'InverseRelationShadowVariable' ,
603528 'ProblemFactProperty' , 'ProblemFactCollectionProperty' ,
604529 'PlanningEntityProperty' , 'PlanningEntityCollectionProperty' ,
@@ -607,4 +532,4 @@ def wrapper_doChange(self, solution, problem_change_director):
607532 'planning_entity' , 'planning_solution' , 'constraint_configuration' ,
608533 'nearby_distance_meter' ,
609534 'constraint_provider' , 'easy_score_calculator' , 'incremental_score_calculator' ,
610- 'variable_listener' , ' problem_change' ]
535+ 'problem_change' ]
0 commit comments