Skip to content
This repository was archived by the owner on Jul 17, 2024. It is now read-only.

Commit 2c443dc

Browse files
committed
feat: introduce fairness
1 parent 5aa2cbd commit 2c443dc

File tree

2 files changed

+276
-2
lines changed

2 files changed

+276
-2
lines changed

timefold-solver-python-core/src/main/python/score/_constraint_stream.py

Lines changed: 142 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,20 @@ def concat(self, other):
454454
else:
455455
raise RuntimeError(f'Unhandled constraint stream type {type(other)}.')
456456

457+
def complement(self, cls: type[A]) -> 'UniConstraintStream[A]':
458+
"""
459+
Adds to the stream all instances of a given class which are not yet present in it.
460+
These instances must be present in the solution,
461+
which means the class needs to be either a planning entity or a problem fact.
462+
463+
Parameters
464+
----------
465+
cls : Type[A]
466+
the type of the instances to add to the stream.
467+
"""
468+
result = self.delegate.complement(get_class(cls))
469+
return TriConstraintCollector(result, self.package, self.a_type)
470+
457471
def penalize(self, constraint_weight: ScoreType, match_weigher: Callable[[A], int] = None) -> \
458472
'UniConstraintBuilder[A, ScoreType]':
459473
"""
@@ -1000,6 +1014,40 @@ def concat(self, other):
10001014
else:
10011015
raise RuntimeError(f'Unhandled constraint stream type {type(other)}.')
10021016

1017+
@overload
1018+
def complement(self, cls: type[A]) -> 'BiConstraintStream[A, B]':
1019+
...
1020+
1021+
@overload
1022+
def complement(self, cls: type[A], padding: Callable[[A], B]) -> 'BiConstraintStream[A, B]':
1023+
...
1024+
1025+
def complement(self, cls: type[A], padding=None):
1026+
"""
1027+
Adds to the stream all instances of a given class which are not yet present in it.
1028+
These instances must be present in the solution,
1029+
which means the class needs to be either a planning entity or a problem fact.
1030+
1031+
The instances will be read from the first element of the input tuple.
1032+
When an output tuple needs to be created for the newly inserted instances,
1033+
the first element will be the new instance.
1034+
The rest of the tuple will be padded with the result of the padding function.
1035+
1036+
Parameters
1037+
----------
1038+
cls : Type[A]
1039+
the type of the instances to add to the stream.
1040+
1041+
padding : Callable[[A], B]
1042+
a function that computes the padding value for the second fact in the new tuple.
1043+
"""
1044+
if None == padding:
1045+
result = self.delegate.complement(get_class(cls))
1046+
return TriConstraintCollector(result, self.package, self.a_type, self.b_type)
1047+
java_padding = function_cast(padding, self.a_type)
1048+
result = self.delegate.complement(get_class(cls), java_padding)
1049+
return TriConstraintCollector(result, self.package, self.a_type, self.b_type)
1050+
10031051
def penalize(self, constraint_weight: ScoreType, match_weigher: Callable[[A, B], int] = None) -> \
10041052
'BiConstraintBuilder[A, B, ScoreType]':
10051053
"""
@@ -1544,6 +1592,51 @@ def concat(self, other):
15441592
else:
15451593
raise RuntimeError(f'Unhandled constraint stream type {type(other)}.')
15461594

1595+
@overload
1596+
def complement(self, cls: type[A]) -> 'TriConstraintStream[A, B, C]':
1597+
...
1598+
1599+
@overload
1600+
def complement(self, cls: type[A], padding_b: Callable[[A], B], padding_c: Callable[[A], C]) \
1601+
-> 'TriConstraintStream[A, B, C]':
1602+
...
1603+
1604+
def complement(self, cls: type[A], padding_b=None, padding_c=None):
1605+
"""
1606+
Adds to the stream all instances of a given class which are not yet present in it.
1607+
These instances must be present in the solution,
1608+
which means the class needs to be either a planning entity or a problem fact.
1609+
1610+
The instances will be read from the first element of the input tuple.
1611+
When an output tuple needs to be created for the newly inserted instances,
1612+
the first element will be the new instance.
1613+
The rest of the tuple will be padded with the result of the padding function,
1614+
applied on the new instance.
1615+
1616+
Padding functions are optional, but if one is provided, then both must-be provided.
1617+
1618+
Parameters
1619+
----------
1620+
cls : Type[A]
1621+
the type of the instances to add to the stream.
1622+
1623+
padding_b : Callable[[A], B]
1624+
a function that computes the padding value for the second fact in the new tuple.
1625+
1626+
padding_c : Callable[[A], C]
1627+
a function that computes the padding value for the third fact in the new tuple.
1628+
"""
1629+
if None == padding_b == padding_c:
1630+
result = self.delegate.complement(get_class(cls))
1631+
return TriConstraintCollector(result, self.package, self.a_type, self.b_type, self.c_type)
1632+
specified_count = sum(x is not None for x in [padding_b, padding_c])
1633+
if specified_count != 0:
1634+
raise ValueError(f'If a padding function is provided, both are expected, got {specified_count} instead.')
1635+
java_padding_b = function_cast(padding_b, self.a_type)
1636+
java_padding_c = function_cast(padding_c, self.a_type)
1637+
result = self.delegate.complement(get_class(cls), java_padding_b, java_padding_c)
1638+
return TriConstraintCollector(result, self.package, self.a_type, self.b_type, self.c_type)
1639+
15471640
def penalize(self, constraint_weight: ScoreType,
15481641
match_weigher: Callable[[A, B, C], int] = None) -> 'TriConstraintBuilder[A, B, C, ScoreType]':
15491642
"""
@@ -2016,7 +2109,6 @@ def map(self, *mapping_functions):
20162109
JClass('java.lang.Object'))
20172110
if len(mapping_functions) == 4:
20182111
return QuadConstraintStream(self.delegate.map(*translated_functions), self.package,
2019-
20202112
JClass('java.lang.Object'), JClass('java.lang.Object'),
20212113
JClass('java.lang.Object'), JClass('java.lang.Object'))
20222114
raise RuntimeError(f'Impossible state: missing case for {len(mapping_functions)}.')
@@ -2027,7 +2119,6 @@ def flatten_last(self, flattening_function) -> 'QuadConstraintStream[A,B,C,D]':
20272119
"""
20282120
translated_function = function_cast(flattening_function, self.d_type)
20292121
return QuadConstraintStream(self.delegate.flattenLast(translated_function), self.package,
2030-
20312122
self.a_type, self.b_type, self.c_type, JClass('java.lang.Object'))
20322123

20332124
def distinct(self) -> 'QuadConstraintStream[A,B,C,D]':
@@ -2083,6 +2174,55 @@ def concat(self, other):
20832174
else:
20842175
raise RuntimeError(f'Unhandled constraint stream type {type(other)}.')
20852176

2177+
@overload
2178+
def complement(self, cls: type[A]) -> 'QuadConstraintStream[A, B, C, D]':
2179+
...
2180+
2181+
@overload
2182+
def complement(self, cls: type[A], padding_b: Callable[[A], B], padding_c: Callable[[A], C],
2183+
padding_d: Callable[[A], D]) -> 'QuadConstraintStream[A, B, C, D]':
2184+
...
2185+
2186+
def complement(self, cls: type[A], padding_b=None, padding_c=None, padding_d=None):
2187+
"""
2188+
Adds to the stream all instances of a given class which are not yet present in it.
2189+
These instances must be present in the solution,
2190+
which means the class needs to be either a planning entity or a problem fact.
2191+
2192+
The instances will be read from the first element of the input tuple.
2193+
When an output tuple needs to be created for the newly inserted instances,
2194+
the first element will be the new instance.
2195+
The rest of the tuple will be padded with the result of the padding function,
2196+
applied on the new instance.
2197+
2198+
Padding functions are optional, but if one is provided, then all three must-be provided.
2199+
2200+
Parameters
2201+
----------
2202+
cls : Type[A]
2203+
the type of the instances to add to the stream.
2204+
2205+
padding_b : Callable[[A], B]
2206+
a function that computes the padding value for the second fact in the new tuple.
2207+
2208+
padding_c : Callable[[A], C]
2209+
a function that computes the padding value for the third fact in the new tuple.
2210+
2211+
padding_d : Callable[[A], D]
2212+
a function that computes the padding value for the fourth fact in the new tuple.
2213+
"""
2214+
if None == padding_b == padding_c == padding_d:
2215+
result = self.delegate.complement(get_class(cls))
2216+
return QuadConstraintCollector(result, self.package, self.a_type, self.b_type, self.c_type, self.d_type)
2217+
specified_count = sum(x is not None for x in [padding_b, padding_c, padding_d])
2218+
if specified_count != 0:
2219+
raise ValueError(f'If a padding function is provided, all 3 are expected, got {specified_count} instead.')
2220+
java_padding_b = function_cast(padding_b, self.a_type)
2221+
java_padding_c = function_cast(padding_c, self.a_type)
2222+
java_padding_d = function_cast(padding_d, self.a_type)
2223+
result = self.delegate.complement(get_class(cls), java_padding_b, java_padding_c, java_padding_d)
2224+
return QuadConstraintCollector(result, self.package, self.a_type, self.b_type, self.c_type, self.d_type)
2225+
20862226
def penalize(self, constraint_weight: ScoreType,
20872227
match_weigher: Callable[[A, B, C, D], int] = None) -> 'QuadConstraintBuilder[A, B, C, D, ScoreType]':
20882228
"""

timefold-solver-python-core/src/main/python/score/_group_by.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import Callable, Any, Sequence, TypeVar, List, Set, Dict, TYPE_CHECKING, overload
44
if TYPE_CHECKING:
55
from ai.timefold.solver.core.api.score.stream.common import SequenceChain
6+
from ai.timefold.solver.core.api.score.stream.common import LoadBalance
67
from ai.timefold.solver.core.api.score.stream.uni import UniConstraintCollector
78
from ai.timefold.solver.core.api.score.stream.bi import BiConstraintCollector
89
from ai.timefold.solver.core.api.score.stream.tri import TriConstraintCollector
@@ -61,6 +62,14 @@ class CollectAndThenCollector:
6162
mapping_function: Callable
6263

6364

65+
@dataclasses.dataclass
66+
class LoadBalanceCollector:
67+
collector_creator: Callable
68+
balanced_item_function: Callable
69+
load_function: Callable | None
70+
initial_load_function: Callable | None
71+
72+
6473
def extract_collector(collector_info, *type_arguments):
6574
if isinstance(collector_info, NoArgsConstraintCollector):
6675
return collector_info.collector_creator()
@@ -89,6 +98,15 @@ def extract_collector(collector_info, *type_arguments):
8998
delegate_collector = extract_collector(collector_info.delegate_collector, *type_arguments)
9099
mapping_function = function_cast(collector_info.mapping_function, JClass('java.lang.Object'))
91100
return collector_info.collector_creator(delegate_collector, mapping_function)
101+
elif isinstance(collector_info, LoadBalanceCollector):
102+
balanced_item_function = function_cast(collector_info.balanced_item_function, *type_arguments)
103+
if collector_info.load_function is None:
104+
return collector_info.collector_creator(balanced_item_function)
105+
load_function = function_cast(collector_info.load_function, *type_arguments)
106+
if collector_info.initial_load_function is None:
107+
return collector_info.collector_creator(balanced_item_function, load_function)
108+
initial_load_function = function_cast(collector_info.initial_load_function, *type_arguments)
109+
return collector_info.collector_creator(balanced_item_function, load_function, initial_load_function)
92110
else:
93111
raise ValueError(f'Invalid Collector: {collector_info}. '
94112
f'Create Collectors via timefold.solver.constraint.ConstraintCollectors.')
@@ -135,13 +153,15 @@ class ConstraintCollectors:
135153
C = TypeVar('C')
136154
D = TypeVar('D')
137155
E = TypeVar('E')
156+
Balanced = TypeVar('Balanced')
138157

139158
# Method return type variables
140159
A_ = TypeVar('A_')
141160
B_ = TypeVar('B_')
142161
C_ = TypeVar('C_')
143162
D_ = TypeVar('D_')
144163
E_ = TypeVar('E_')
164+
Balanced_ = TypeVar('Balanced_')
145165

146166
@staticmethod
147167
def _delegate():
@@ -993,6 +1013,120 @@ def to_sorted_map(key_mapper, value_mapper, merge_function_or_set_creator=None):
9931013
else:
9941014
raise ValueError
9951015

1016+
@overload
1017+
@staticmethod
1018+
def load_balance(balanced_item_function: Callable[[A], Balanced_]) -> \
1019+
'UniConstraintCollector[A, Any, LoadBalance[Balanced_]]':
1020+
...
1021+
1022+
@overload
1023+
@staticmethod
1024+
def load_balance(balanced_item_function: Callable[[A], Balanced_], load_function: Callable[[A], int]) -> \
1025+
'UniConstraintCollector[A, Any, LoadBalance[Balanced_]]':
1026+
...
1027+
1028+
@overload
1029+
@staticmethod
1030+
def load_balance(balanced_item_function: Callable[[A], Balanced_], load_function: Callable[[A], int],
1031+
initial_load_function: Callable[[A], int]) -> \
1032+
'UniConstraintCollector[A, Any, LoadBalance[Balanced_]]':
1033+
...
1034+
1035+
@overload
1036+
@staticmethod
1037+
def load_balance(balanced_item_function: Callable[[A, B], Balanced_]) -> \
1038+
'BiConstraintCollector[A, B, Any, LoadBalance[Balanced_]]':
1039+
...
1040+
1041+
@overload
1042+
@staticmethod
1043+
def load_balance(balanced_item_function: Callable[[A, B], Balanced_], load_function: Callable[[A, B], int]) -> \
1044+
'BiConstraintCollector[A, B, Any, LoadBalance[Balanced_]]':
1045+
...
1046+
1047+
@overload
1048+
@staticmethod
1049+
def load_balance(balanced_item_function: Callable[[A, B], Balanced_], load_function: Callable[[A, B], int],
1050+
initial_load_function: Callable[[A, B], int]) -> \
1051+
'BiConstraintCollector[A, B, Any, LoadBalance[Balanced_]]':
1052+
...
1053+
1054+
@overload
1055+
@staticmethod
1056+
def load_balance(balanced_item_function: Callable[[A, B, C], Balanced_]) -> \
1057+
'TriConstraintCollector[A, B, C, Any, LoadBalance[Balanced_]]':
1058+
...
1059+
1060+
@overload
1061+
@staticmethod
1062+
def load_balance(balanced_item_function: Callable[[A, B, C], Balanced_],
1063+
load_function: Callable[[A, B, C], int]) -> \
1064+
'TriConstraintCollector[A, B, C, Any, LoadBalance[Balanced_]]':
1065+
...
1066+
1067+
@overload
1068+
@staticmethod
1069+
def load_balance(balanced_item_function: Callable[[A, B, C], Balanced_], load_function: Callable[[A, B, C], int],
1070+
initial_load_function: Callable[[A, B, C], int]) -> \
1071+
'TriConstraintCollector[A, B, C, Any, LoadBalance[Balanced_]]':
1072+
...
1073+
1074+
@overload
1075+
@staticmethod
1076+
def load_balance(balanced_item_function: Callable[[A, B, C, D], Balanced_]) -> \
1077+
'QuadConstraintCollector[A, B, C, D, Any, LoadBalance[Balanced_]]':
1078+
...
1079+
1080+
@overload
1081+
@staticmethod
1082+
def load_balance(balanced_item_function: Callable[[A, B, C, D], Balanced_],
1083+
load_function: Callable[[A, B, C, D], int]) -> \
1084+
'QuadConstraintCollector[A, B, C, D, Any, LoadBalance[Balanced_]]':
1085+
...
1086+
1087+
@overload
1088+
@staticmethod
1089+
def load_balance(balanced_item_function: Callable[[A, B, C, D], Balanced_],
1090+
load_function: Callable[[A, B, C, D], int],
1091+
initial_load_function: Callable[[A, B, C, D], int]) -> \
1092+
'QuadConstraintCollector[A, B, C, D, Any, LoadBalance[Balanced_]]':
1093+
...
1094+
1095+
@staticmethod
1096+
def load_balance(balanced_item_function, load_function=None, initial_load_function=None):
1097+
"""
1098+
Returns a collector that takes a stream of items and calculates the unfairness measure from them.
1099+
The load for every item is provided by the load_function,
1100+
with the starting load provided by the initial_load_function.
1101+
1102+
When this collector is used in a constraint stream,
1103+
it is recommended to use a score type which supports real numbers.
1104+
This is so that the unfairness measure keeps its precision
1105+
without forcing the other constraints to be multiplied by a large constant,
1106+
which would otherwise be required to implement fixed-point arithmetic.
1107+
1108+
Parameters
1109+
----------
1110+
balanced_item_function:
1111+
The function that returns the item which should be load-balanced.
1112+
load_function:
1113+
How much the item should count for in the formula.
1114+
initial_load_function:
1115+
The initial value of the metric, allowing to provide initial state
1116+
without requiring the entire previous planning windows in the working memory.
1117+
If this function is provided, load_function must be provided as well.
1118+
"""
1119+
if None == load_function == initial_load_function:
1120+
return LoadBalanceCollector(ConstraintCollectors._delegate().loadBalance, balanced_item_function, None,
1121+
None)
1122+
elif None == initial_load_function:
1123+
return LoadBalanceCollector(ConstraintCollectors._delegate().loadBalance, balanced_item_function,
1124+
load_function, None)
1125+
elif None == load_function:
1126+
raise ValueError("load_function cannot be None if initial_load_function is not None")
1127+
else:
1128+
return LoadBalanceCollector(ConstraintCollectors._delegate().loadBalance, balanced_item_function,
1129+
load_function, initial_load_function)
9961130

9971131
# Must be at the bottom, constraint_stream depends on this module
9981132
from ._constraint_stream import *

0 commit comments

Comments
 (0)