Skip to content

Commit 087c5e9

Browse files
chore: Add docstrings, remove old classes/functions, fix future wrapper (#52)
- Use Numpy docstrings - Remove completable_future wrapper since Python deprecated getting the event loop if there is no current one, and made the future wrapper not use a coroutine so it does not raise a warning if it is not awaited - Remove classes/functions that were either replaced by another one or deprecated. --------- Co-authored-by: Lukáš Petrovický <lukas@petrovicky.net>
1 parent 5b65cf7 commit 087c5e9

30 files changed

+3194
-1014
lines changed

tests/test_constraint_streams.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,7 @@ def define_constraints(constraint_factory: ConstraintFactory):
600600
'ifNotExistsIncludingNullVars',
601601
'ifExistsOtherIncludingNullVars',
602602
'ifNotExistsOtherIncludingNullVars',
603+
'toCollection',
603604
}
604605

605606

timefold-solver-python-core/src/main/python/__init__.py

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,44 @@
11
"""
2-
This module wraps Timefold and allow Python Objects
3-
to be used as the domain and Python functions to be used
4-
as the constraints.
2+
`Timefold Solver <https://timefold.ai/>`_ is a lightweight,
3+
embeddable constraint satisfaction engine which optimizes planning problems.
54
6-
Using any decorators in this module will automatically start
7-
the JVM. If you want to pass custom arguments to the JVM,
8-
use init before decorators and any timefold.solver.types imports.
5+
It solves use cases such as:
6+
7+
- Employee shift rostering: timetabling nurses, repairmen, ...
8+
9+
- Vehicle routing: planning vehicle routes for moving freight and/or passengers through
10+
multiple destinations using known mapping tools ...
11+
12+
- Agenda scheduling: scheduling meetings, appointments, maintenance jobs, advertisements, ...
13+
14+
15+
Planning problems are defined using Python classes and functions.
16+
17+
Examples
18+
--------
19+
>>> from timefold.solver import Solver, SolverFactory
20+
>>> from timefold.solver.config import (SolverConfig, ScoreDirectorFactoryConfig,
21+
... TerminationConfig, Duration)
22+
>>> from domain import Timetable, Lesson, generate_problem
23+
>>> from constraints import my_constraints
24+
...
25+
>>> solver_config = SolverConfig(solution_class=Timetable, entity_class_list=[Lesson],
26+
... score_director_factory_config=ScoreDirectorFactoryConfig(
27+
... constraint_provider_function=my_constraints
28+
... ),
29+
... termination_config=TerminationConfig(
30+
... spent_limit=Duration(seconds=30))
31+
... )
32+
>>> solver = SolverFactory.create(solver_config).build_solver()
33+
>>> problem = generate_problem()
34+
>>> solution = solver.solve(problem)
35+
36+
See Also
37+
--------
38+
:mod:`timefold.solver.config`
39+
:mod:`timefold.solver.domain`
40+
:mod:`timefold.solver.score`
41+
:mod:`timefold.solver.test`
942
"""
1043
from ._problem_change import *
1144
from ._solution_manager import *
Lines changed: 14 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,30 @@
1-
from ._jpype_type_conversions import PythonBiFunction
21
from typing import Awaitable, TypeVar, TYPE_CHECKING
3-
from asyncio import Future, get_event_loop, CancelledError
42

53
if TYPE_CHECKING:
6-
from java.util.concurrent import (Future as JavaFuture,
7-
CompletableFuture as JavaCompletableFuture)
4+
from java.util.concurrent import Future as JavaFuture
85

96

107
Result = TypeVar('Result')
118

129

13-
def wrap_future(future: 'JavaFuture[Result]') -> Awaitable[Result]:
14-
async def get_result() -> Result:
15-
nonlocal future
16-
return future.get()
10+
class JavaFutureAwaitable(Awaitable[Result]):
11+
_future: 'JavaFuture[Result]'
1712

18-
return get_result()
13+
def __init__(self, future: 'JavaFuture[Result]') -> None:
14+
self._future = future
1915

16+
def __await__(self) -> Result:
17+
return self
2018

21-
def wrap_completable_future(future: 'JavaCompletableFuture[Result]') -> Future[Result]:
22-
loop = get_event_loop()
23-
out = loop.create_future()
19+
def __iter__(self):
20+
return self
2421

25-
def result_handler(result, error):
26-
nonlocal out
27-
if error is not None:
28-
out.set_exception(error)
29-
else:
30-
out.set_result(result)
22+
def __next__(self):
23+
raise StopIteration(self._future.get())
3124

32-
def cancel_handler(python_future: Future):
33-
nonlocal future
34-
if isinstance(python_future.exception(), CancelledError):
35-
future.cancel(True)
3625

37-
future.handle(PythonBiFunction(result_handler))
38-
out.add_done_callback(cancel_handler)
39-
return out
26+
def wrap_future(future: 'JavaFuture[Result]') -> Awaitable[Result]:
27+
return JavaFutureAwaitable(future)
4028

4129

42-
__all__ = ['wrap_future', 'wrap_completable_future']
30+
__all__ = ['wrap_future']

timefold-solver-python-core/src/main/python/_problem_change.py

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,15 @@
1414

1515

1616
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+
"""
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

99221
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+
"""
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

Comments
 (0)