Skip to content

Commit 4316a5e

Browse files
authored
feat: Support deep_planning_clone class decorator (#1271)
This brings parity with the Java version.
1 parent 0f2da1e commit 4316a5e

File tree

3 files changed

+130
-5
lines changed

3 files changed

+130
-5
lines changed

docs/src/modules/ROOT/pages/using-timefold-solver/modeling-planning-problems.adoc

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2659,6 +2659,10 @@ If any of your problem facts needs to be deep cloned for a planning clone,
26592659
for example if the problem fact references a planning entity or the planning solution,
26602660
mark its class with a `@DeepPlanningClone` annotation:
26612661
2662+
[tabs]
2663+
===
2664+
Java::
2665+
+
26622666
[source,java,options="nowrap"]
26632667
----
26642668
@DeepPlanningClone
@@ -2669,13 +2673,42 @@ public class SeatDesignationDependency {
26692673
}
26702674
----
26712675
2676+
Python::
2677+
+
2678+
[source,python,options="nowrap"]
2679+
----
2680+
@deep_planning_clone
2681+
class SeatDesignationDependency:
2682+
leftSeatDesignation: SeatDesignation # planning entity
2683+
rightSeatDesignation: SeatDesignation # planning entity
2684+
----
2685+
====
2686+
26722687
In the example above, because `SeatDesignationDependency` references the planning entity `SeatDesignation`
26732688
(which is deep planning cloned automatically), it should also be deep planning cloned.
26742689
26752690
Alternatively, the `@DeepPlanningClone` annotation also works on a getter method or a field to planning clone it.
26762691
If that property is a `Collection` or a `Map`, it will shallow clone it and deep planning clone
26772692
any element thereof that is an instance of a class that has a `@DeepPlanningClone` annotation.
26782693
2694+
[tabs]
2695+
====
2696+
Java::
2697+
+
2698+
[source,java,options="nowrap"]
2699+
----
2700+
@DeepPlanningClone
2701+
private SeatDesignationDependency seatDesignationDependency;
2702+
----
2703+
2704+
Python::
2705+
+
2706+
[source,python,options="nowrap"]
2707+
----
2708+
seat_designation_dependency: Annotated[SeatDesignationDependency, DeepPlanningClone]
2709+
----
2710+
====
2711+
26792712
[NOTE]
26802713
====
26812714
Values of Java's `enum` and `record` types are never deep-cloned.

python/python-core/src/main/python/domain/_annotations.py

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
Solution_ = TypeVar('Solution_')
1010
Entity_ = TypeVar('Entity_')
1111

12-
1312
class PlanningId(JavaAnnotation):
1413
"""
1514
Specifies that an attribute is the id to match when locating
@@ -654,13 +653,14 @@ def __init__(self, *,
654653

655654
class DeepPlanningClone(JavaAnnotation):
656655
"""
657-
Marks a problem fact class as being required to be deep planning cloned.
658-
Not needed for a `planning_solution` or `planning_entity` because those are automatically deep cloned.
659-
It can also mark an attribute as being required to be deep planning cloned.
656+
Marks an attribute as being required to be deep planning cloned.
657+
Not needed for `planning_solution` or `planning_entity` attributes because those are automatically deep cloned.
660658
This is especially useful for `list` (or `dict`) properties.
661659
Not needed for a `list` (or `dist`) attribute with a generic type of `planning_entity`,
662660
because those are automatically deep cloned.
663661
662+
To annotate a class, use @deep_planning_clone
663+
664664
Notes
665665
-----
666666
If it annotates an attribute returning `list` (or `dict`),
@@ -878,6 +878,32 @@ def constraint_configuration(constraint_configuration_class: Type[Solution_]) ->
878878
out = add_class_annotation(JavaConstraintConfiguration)(constraint_configuration_class)
879879
return out
880880

881+
def deep_planning_clone(entity_class: Type[Entity_] = None) -> Type[Entity_]:
882+
"""
883+
Marks a problem fact class as being required to be deep planning cloned.
884+
Not needed for a `planning_solution` or `planning_entity` because those are automatically deep cloned.
885+
886+
To annotate an attribute, use DeepPlanningClone
887+
888+
Examples
889+
--------
890+
>>> from timefold.solver.domain import deep_planning_clone
891+
>>>
892+
>>> @deep_planning_clone
893+
... @dataclass
894+
... class Timeslot:
895+
... day_of_week: str
896+
... start_time: time
897+
... end_time: time
898+
"""
899+
ensure_init()
900+
from _jpyinterpreter import add_class_annotation
901+
from .._timefold_java_interop import _add_to_compilation_queue
902+
from ai.timefold.solver.core.api.domain.solution.cloner import (
903+
DeepPlanningClone as JavaDeepPlanningClone)
904+
out = add_class_annotation(JavaDeepPlanningClone)(entity_class)
905+
_add_to_compilation_queue(entity_class)
906+
return out
881907

882908
__all__ = ['PlanningId', 'PlanningScore', 'PlanningPin', 'PlanningPinToIndex',
883909
'PlanningVariable', 'PlanningVariableGraphType', 'PlanningListVariable',
@@ -888,4 +914,4 @@ def constraint_configuration(constraint_configuration_class: Type[Solution_]) ->
888914
'PlanningEntityProperty', 'PlanningEntityCollectionProperty',
889915
'ValueRangeProvider', 'DeepPlanningClone', 'ConstraintConfigurationProvider',
890916
'ConstraintWeight',
891-
'planning_entity', 'planning_solution', 'constraint_configuration']
917+
'planning_entity', 'planning_solution', 'constraint_configuration', 'deep_planning_clone']

python/python-core/tests/test_domain.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -954,3 +954,69 @@ class Solution:
954954
solution = solver.solve(problem)
955955
assert solution.score.score == 0
956956
assert solution.entity.value == [1, 2, 3]
957+
958+
def test_deep_clone_class():
959+
@deep_planning_clone
960+
@dataclass
961+
class Code:
962+
value: str
963+
parent_entity: 'Entity' = field(default=None)
964+
965+
@dataclass
966+
class Value:
967+
code: Code
968+
969+
@planning_entity
970+
@dataclass
971+
class Entity:
972+
code: Code
973+
value: Annotated[Value, PlanningVariable] = field(default=None)
974+
975+
def assign_to_v1(constraint_factory: ConstraintFactory):
976+
return (constraint_factory.for_each(Entity)
977+
.filter(lambda e: e.value.code.value == 'v1')
978+
.reward(SimpleScore.ONE)
979+
.as_constraint('assign to v1')
980+
)
981+
982+
@constraint_provider
983+
def my_constraints(constraint_factory: ConstraintFactory):
984+
return [
985+
assign_to_v1(constraint_factory)
986+
]
987+
988+
@planning_solution
989+
@dataclass
990+
class Solution:
991+
entities: Annotated[List[Entity], PlanningEntityCollectionProperty]
992+
values: Annotated[List[Value], ProblemFactCollectionProperty, ValueRangeProvider]
993+
codes: Annotated[List[Code], ProblemFactCollectionProperty]
994+
score: Annotated[SimpleScore, PlanningScore] = field(default=None)
995+
996+
solver_config = SolverConfig(
997+
solution_class=Solution,
998+
entity_class_list=[Entity],
999+
score_director_factory_config=ScoreDirectorFactoryConfig(
1000+
constraint_provider_function=my_constraints
1001+
),
1002+
termination_config=TerminationConfig(
1003+
best_score_limit='2'
1004+
)
1005+
)
1006+
1007+
e1 = Entity(Code('e1'))
1008+
e1.code.parent_entity = e1
1009+
e2 = Entity(Code('e2'))
1010+
e2.code.parent_entity = e2
1011+
1012+
v1 = Value(Code('v1'))
1013+
v2 = Value(Code('v2'))
1014+
1015+
problem = Solution([e1, e2], [v1, v2], [e1.code, e2.code, v1.code, v2.code])
1016+
solver = SolverFactory.create(solver_config).build_solver()
1017+
solution = solver.solve(problem)
1018+
1019+
assert solution.score.score == 2
1020+
assert solution.entities[0].value == v1
1021+
assert solution.codes[0].parent_entity == solution.entities[0]
1022+
assert solution.codes[0] is not e1.code

0 commit comments

Comments
 (0)