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

Commit 55c3f85

Browse files
committed
feat: introduce fairness
1 parent 5aa2cbd commit 55c3f85

File tree

3 files changed

+317
-2
lines changed

3 files changed

+317
-2
lines changed

tests/test_constraint_verifier.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,16 @@
55
from timefold.solver.config import *
66
from timefold.solver.test import *
77

8+
import inspect
9+
import re
810
from typing import Annotated, List
911
from dataclasses import dataclass, field
1012

13+
from ai.timefold.solver.test.api.score.stream import (ConstraintVerifier as JavaConstraintVerifier,
14+
SingleConstraintAssertion as JavaSingleConstraintAssertion,
15+
SingleConstraintVerification as JavaSingleConstraintVerification,
16+
MultiConstraintAssertion as JavaMultiConstraintAssertion,
17+
MultiConstraintVerification as JavaMultiConstraintVerification)
1118

1219
def verifier_suite(verifier: ConstraintVerifier, same_value, is_value_one,
1320
solution, e1, e2, e3, v1, v2, v3):
@@ -268,3 +275,37 @@ class Solution:
268275

269276
verifier_suite(verifier, same_value, is_value_one,
270277
solution, e1, e2, e3, v1, v2, v3)
278+
279+
280+
ignored_java_functions = {
281+
'equals',
282+
'getClass',
283+
'hashCode',
284+
'notify',
285+
'notifyAll',
286+
'toString',
287+
'wait',
288+
'with_constraint_stream_impl_type'
289+
}
290+
291+
292+
def test_has_all_methods():
293+
for python_type, java_type in ((ConstraintVerifier, JavaConstraintVerifier),
294+
(SingleConstraintAssertion, JavaSingleConstraintAssertion),
295+
(SingleConstraintVerification, JavaSingleConstraintVerification),
296+
(MultiConstraintAssertion, JavaMultiConstraintAssertion),
297+
(MultiConstraintVerification, JavaMultiConstraintVerification)):
298+
missing = []
299+
for function_name, function_impl in inspect.getmembers(java_type, inspect.isfunction):
300+
if function_name in ignored_java_functions:
301+
continue
302+
snake_case_name = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', function_name)
303+
# change h_t_t_p -> http
304+
snake_case_name = re.sub('([a-z0-9])([A-Z])', r'\1_\2', snake_case_name).lower()
305+
if not hasattr(python_type, snake_case_name):
306+
missing.append(snake_case_name)
307+
308+
if missing:
309+
raise AssertionError(f'{python_type} is missing methods ({missing}) '
310+
f'from java_type ({java_type}).)')
311+

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

0 commit comments

Comments
 (0)