14
14
15
15
16
16
class 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
+ """
17
26
_delegate : '_ProblemChangeDirector'
18
27
_java_solution : Solution_
19
28
_python_solution : Solution_
@@ -38,13 +47,33 @@ def _replace_solution_in_callable(self, callable: Callable):
38
47
return callable
39
48
40
49
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
+ """
41
60
from java .util .function import Consumer
42
61
converted_modifier = translate_python_bytecode_to_java_bytecode (self ._replace_solution_in_callable (modifier ),
43
62
Consumer )
44
63
self ._delegate .addEntity (convert_to_java_python_like_object (entity ), converted_modifier )
45
64
update_python_object_from_java (self ._java_solution )
46
65
47
66
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
+ """
48
77
from java .util .function import Consumer
49
78
converted_modifier = translate_python_bytecode_to_java_bytecode (self ._replace_solution_in_callable (modifier ),
50
79
Consumer )
@@ -53,6 +82,18 @@ def add_problem_fact(self, fact: ProblemFact, modifier: Callable[[ProblemFact],
53
82
54
83
def change_problem_property (self , problem_fact_or_entity : EntityOrProblemFact ,
55
84
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
+ """
56
97
from java .util .function import Consumer
57
98
converted_modifier = translate_python_bytecode_to_java_bytecode (self ._replace_solution_in_callable (modifier ),
58
99
Consumer )
@@ -62,43 +103,177 @@ def change_problem_property(self, problem_fact_or_entity: EntityOrProblemFact,
62
103
63
104
def change_variable (self , entity : Entity , variable : str ,
64
105
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
+ """
65
120
from java .util .function import Consumer
66
121
converted_modifier = translate_python_bytecode_to_java_bytecode (self ._replace_solution_in_callable (modifier ),
67
122
Consumer )
68
123
self ._delegate .changeVariable (convert_to_java_python_like_object (entity ), variable , converted_modifier )
69
124
update_python_object_from_java (self ._java_solution )
70
125
71
126
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
+ """
72
148
out = self ._delegate .lookUpWorkingObject (convert_to_java_python_like_object (external_object )).orElse (None )
73
149
if out is None :
74
150
return None
75
151
return unwrap_python_like_object (out )
76
152
77
153
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
+ """
78
171
return unwrap_python_like_object (self ._delegate .lookUpWorkingObjectOrFail (external_object ))
79
172
80
173
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
+ """
81
186
from java .util .function import Consumer
82
187
converted_modifier = translate_python_bytecode_to_java_bytecode (self ._replace_solution_in_callable (modifier ),
83
188
Consumer )
84
189
self ._delegate .removeEntity (convert_to_java_python_like_object (entity ), converted_modifier )
85
190
update_python_object_from_java (self ._java_solution )
86
191
87
192
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
+ """
88
205
from java .util .function import Consumer
89
206
converted_modifier = translate_python_bytecode_to_java_bytecode (self ._replace_solution_in_callable (modifier ),
90
207
Consumer )
91
208
self ._delegate .removeProblemFact (convert_to_java_python_like_object (fact ), converted_modifier )
92
209
update_python_object_from_java (self ._java_solution )
93
210
94
211
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
+ """
95
217
self ._delegate .updateShadowVariables ()
96
218
update_python_object_from_java (self ._java_solution )
97
219
98
220
99
221
class 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
+ """
100
263
@abstractmethod
101
264
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
+ """
102
277
...
103
278
104
279
0 commit comments