1414
1515
1616class ProblemChangeDirector :
17+ """
18+ Allows external changes to the working solution.
19+ If the changes are not applied through the `ProblemChangeDirector`,
20+ both internal and custom variable listeners are never notified about them,
21+ resulting to inconsistencies in the working solution.
22+ Should be used only from a `ProblemChange` implementation.
23+
24+ To see an example implementation, please refer to the `ProblemChange` docstring.
25+ """
1726 _delegate : '_ProblemChangeDirector'
1827 _java_solution : Solution_
1928 _python_solution : Solution_
@@ -38,13 +47,33 @@ def _replace_solution_in_callable(self, callable: Callable):
3847 return callable
3948
4049 def add_entity (self , entity : Entity , modifier : Callable [[Entity ], None ]) -> None :
50+ """
51+ Add a new ``planning_entity`` instance into the ``working solution``.
52+
53+ Parameters
54+ ----------
55+ entity : Entity
56+ The ``planning_entity`` instance
57+ modifier : Callable[[Entity], None]
58+ A callable that adds the entity to the working solution.
59+ """
4160 from java .util .function import Consumer
4261 converted_modifier = translate_python_bytecode_to_java_bytecode (self ._replace_solution_in_callable (modifier ),
4362 Consumer )
4463 self ._delegate .addEntity (convert_to_java_python_like_object (entity ), converted_modifier )
4564 update_python_object_from_java (self ._java_solution )
4665
4766 def add_problem_fact (self , fact : ProblemFact , modifier : Callable [[ProblemFact ], None ]) -> None :
67+ """
68+ Add a new problem fact instance into the ``working solution``.
69+
70+ Parameters
71+ ----------
72+ fact : ProblemFact
73+ The problem fact instance
74+ modifier : Callable[[ProblemFact], None]
75+ A callable that adds the fact to the working solution.
76+ """
4877 from java .util .function import Consumer
4978 converted_modifier = translate_python_bytecode_to_java_bytecode (self ._replace_solution_in_callable (modifier ),
5079 Consumer )
@@ -53,6 +82,18 @@ def add_problem_fact(self, fact: ProblemFact, modifier: Callable[[ProblemFact],
5382
5483 def change_problem_property (self , problem_fact_or_entity : EntityOrProblemFact ,
5584 modifier : Callable [[EntityOrProblemFact ], None ]) -> None :
85+ """
86+ Change a property of either a ``planning_entity`` or a problem fact.
87+ Translates the entity or the problem fact to its working solution counterpart
88+ by performing a lookup as defined by `lookup_working_object_or_fail`.
89+
90+ Parameters
91+ ----------
92+ problem_fact_or_entity : EntityOrProblemFact
93+ The ``planning_entity`` or problem fact instance
94+ modifier : Callable[[EntityOrProblemFact], None]
95+ Updates the property of the ``planning_entity`` or the problem fact
96+ """
5697 from java .util .function import Consumer
5798 converted_modifier = translate_python_bytecode_to_java_bytecode (self ._replace_solution_in_callable (modifier ),
5899 Consumer )
@@ -62,43 +103,177 @@ def change_problem_property(self, problem_fact_or_entity: EntityOrProblemFact,
62103
63104 def change_variable (self , entity : Entity , variable : str ,
64105 modifier : Callable [[Entity ], None ]) -> None :
106+ """
107+ Change a ``PlanningVariable`` value of a ``planning_entity``.
108+ Translates the entity to a working planning entity
109+ by performing a lookup as defined by `lookup_working_object_or_fail`.
110+
111+ Parameters
112+ ----------
113+ entity : Entity
114+ The ``planning_entity`` instance
115+ variable : str
116+ Name of the ``PlanningVariable``
117+ modifier : Callable[[Entity], None]
118+ Updates the value of the ``PlanningVariable`` inside the ``planning_entity``
119+ """
65120 from java .util .function import Consumer
66121 converted_modifier = translate_python_bytecode_to_java_bytecode (self ._replace_solution_in_callable (modifier ),
67122 Consumer )
68123 self ._delegate .changeVariable (convert_to_java_python_like_object (entity ), variable , converted_modifier )
69124 update_python_object_from_java (self ._java_solution )
70125
71126 def lookup_working_object (self , external_object : EntityOrProblemFact ) -> Optional [EntityOrProblemFact ]:
127+ """
128+ As defined by `lookup_working_object_or_fail`,
129+ but doesn't fail fast if no working object was ever added for the `external_object`.
130+ It's recommended to use `lookup_working_object_or_fail` instead.
131+
132+ Parameters
133+ ----------
134+ external_object : EntityOrProblemFact
135+ The entity or fact instance to lookup.
136+ Can be ``None``.
137+
138+ Returns
139+ -------
140+ EntityOrProblemFact | None
141+ None if there is no working object for the `external_object`, the looked up object
142+ otherwise.
143+
144+ Raises
145+ ------
146+ If it cannot be looked up or if the `external_object`'s class is not supported.
147+ """
72148 out = self ._delegate .lookUpWorkingObject (convert_to_java_python_like_object (external_object )).orElse (None )
73149 if out is None :
74150 return None
75151 return unwrap_python_like_object (out )
76152
77153 def lookup_working_object_or_fail (self , external_object : EntityOrProblemFact ) -> EntityOrProblemFact :
154+ """
155+ Translate an entity or fact instance (often from another Thread)
156+ to this `ProblemChangeDirector`'s internal working instance.
157+
158+ Matches entities by ``PlanningId``.
159+
160+ Parameters
161+ ----------
162+ external_object : EntityOrProblemFact
163+ The entity or fact instance to lookup.
164+ Can be ``None``.
165+
166+ Raises
167+ ------
168+ If there is no working object for `external_object`,
169+ if it cannot be looked up or if the `external_object`'s class is not supported.
170+ """
78171 return unwrap_python_like_object (self ._delegate .lookUpWorkingObjectOrFail (external_object ))
79172
80173 def remove_entity (self , entity : Entity , modifier : Callable [[Entity ], None ]) -> None :
174+ """
175+ Remove an existing `planning_entity` instance from the ``working solution``.
176+ Translates the entity to its working solution counterpart
177+ by performing a lookup as defined by `lookup_working_object_or_fail`.
178+
179+ Parameters
180+ ----------
181+ entity : Entity
182+ The ``planning_entity`` instance
183+ modifier : Callable[[Entity], None]
184+ Removes the working entity from the ``working solution``.
185+ """
81186 from java .util .function import Consumer
82187 converted_modifier = translate_python_bytecode_to_java_bytecode (self ._replace_solution_in_callable (modifier ),
83188 Consumer )
84189 self ._delegate .removeEntity (convert_to_java_python_like_object (entity ), converted_modifier )
85190 update_python_object_from_java (self ._java_solution )
86191
87192 def remove_problem_fact (self , fact : ProblemFact , modifier : Callable [[ProblemFact ], None ]) -> None :
193+ """
194+ Remove an existing problem fact instance from the ``working solution``.
195+ Translates the problem fact to its working solution counterpart
196+ by performing a lookup as defined by `lookup_working_object_or_fail`.
197+
198+ Parameters
199+ ----------
200+ fact : ProblemFact
201+ The problem fact instance
202+ modifier : Callable[[ProblemFact], None]
203+ Removes the working problem fact from the ``working solution``.
204+ """
88205 from java .util .function import Consumer
89206 converted_modifier = translate_python_bytecode_to_java_bytecode (self ._replace_solution_in_callable (modifier ),
90207 Consumer )
91208 self ._delegate .removeProblemFact (convert_to_java_python_like_object (fact ), converted_modifier )
92209 update_python_object_from_java (self ._java_solution )
93210
94211 def update_shadow_variables (self ) -> None :
212+ """
213+ Calls variable listeners on the external changes submitted so far.
214+ This happens automatically after the entire `ProblemChange` has been processed,
215+ but this method allows the user to specifically request it in the middle of the `ProblemChange`.
216+ """
95217 self ._delegate .updateShadowVariables ()
96218 update_python_object_from_java (self ._java_solution )
97219
98220
99221class ProblemChange (Generic [Solution_ ], ABC ):
222+ """
223+ A `ProblemChange` represents a change in one or more planning entities or problem facts of a `planning_solution`.
224+
225+ The Solver checks the presence of waiting problem changes after every Move evaluation.
226+ If there are waiting problem changes, the Solver:
227+
228+ 1. clones the last best solution and sets the clone as the new working solution
229+ 2. applies every problem change keeping the order in which problem changes have been submitted; after every problem change, variable listeners are triggered
230+ 3. calculates the score and makes the updated working solution the new best solution; note that this solution is not published via the ai. timefold. solver. core. api. solver. event. BestSolutionChangedEvent, as it hasn't been initialized yet
231+ 4. restarts solving to fill potential uninitialized planning entities
232+
233+ Note that the Solver clones a `planning_solution` at will.
234+ Any change must be done on the problem facts and planning entities referenced by the `planning_solution`.
235+
236+ Examples
237+ --------
238+ An example implementation, based on the Cloud balancing problem, looks as follows:
239+ >>> from timefold.solver import ProblemChange
240+ >>> from domain import CloudBalance, CloudComputer
241+ >>>
242+ >>> class DeleteComputerProblemChange(ProblemChange[CloudBalance]):
243+ ... computer: CloudComputer
244+ ...
245+ ... def __init__(self, computer: CloudComputer):
246+ ... self.computer = computer
247+ ...
248+ ... def do_change(self, cloud_balance: CloudBalance, problem_change_director: ProblemChangeDirector):
249+ ... working_computer = problem_change_director.lookup_working_object_or_fail(self.computer)
250+ ... # First remove the problem fact from all planning entities that use it
251+ ... for process in cloud_balance.process_list:
252+ ... if process.computer == working_computer:
253+ ... problem_change_director.change_variable(process, "computer",
254+ ... lambda working_process: setattr(working_process,
255+ ... 'computer', None))
256+ ... # A SolutionCloner does not clone problem fact lists (such as computer_list), only entity lists.
257+ ... # Shallow clone the computer_list so only the working solution is affected.
258+ ... computer_list = cloud_balance.computer_list.copy()
259+ ... cloud_balance.computer_list = computer_list
260+ ... # Remove the problem fact itself
261+ ... problem_change_director.remove_problem_fact(working_computer, computer_list.remove)
262+ """
100263 @abstractmethod
101264 def do_change (self , working_solution : Solution_ , problem_change_director : ProblemChangeDirector ) -> None :
265+ """
266+ Do the change on the `planning_solution`.
267+ Every modification to the `planning_solution` must be done via the `ProblemChangeDirector`,
268+ otherwise the Score calculation will be corrupted.
269+
270+ Parameters
271+ ----------
272+ working_solution : Solution_
273+ the working solution which contains the problem facts (and planning entities) to change
274+ problem_change_director : ProblemChangeDirector
275+ `ProblemChangeDirector` to perform the change through
276+ """
102277 ...
103278
104279
0 commit comments