optapy / optapy

OptaPy is an AI constraint solver for Python to optimize planning and scheduling problems.

Home Page:https://www.optapy.org/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

TypeError: Ambiguous overloads ... when using ConstraintCollectors.compose

MrGreenTea opened this issue · comments

commented

I am currently trying to optimize some shifts. As these shifts start every 8 hours I want to make sure that no person does more than 2 shifts in a row and has to work 24h at once.

My idea was to group all shift assignments by person and then use a composed collector to get the earliest and latest shift and then make sure these shifts do not fill the entire 24 hours (because another constraint is that every person gets exactly 3 shifts).

ONE_DAY_IN_SECONDS = 24 * 60 * 60
def diff_in_seconds(earliest, latest):
    # I tried timedelta before but that did not work either
    return (latest - earliest).total_seconds()

def no_24h_marathon(constraint_factory):
    return constraint_factory.forEach(assignment_class).groupBy(
        lambda a: a.person.name,
        ConstraintCollectors.compose(
            ConstraintCollectors.min(lambda a: a.shift.start),
            ConstraintCollectors.max(lambda a: a.shift.end),
            more_than_16h,
        )
    ).penalize("No 24h marathon", HardSoftScore.ONE_HARD, lambda _, c: c > ONE_DAY_IN_SECONDS )

When I do this I get this TypError about overloads when trying to solve:

TypeError: Ambiguous overloads found for org.optaplanner.core.api.score.stream.ConstraintCollectors.min(function) between:
	public static org.optaplanner.core.api.score.stream.uni.UniConstraintCollector org.optaplanner.core.api.score.stream.ConstraintCollectors.min(java.util.function.Function)
	public static org.optaplanner.core.api.score.stream.tri.TriConstraintCollector org.optaplanner.core.api.score.stream.ConstraintCollectors.min(org.optaplanner.core.api.function.TriFunction)
	public static org.optaplanner.core.api.score.stream.quad.QuadConstraintCollector org.optaplanner.core.api.score.stream.ConstraintCollectors.min(org.optaplanner.core.api.function.QuadFunction)
	public static org.optaplanner.core.api.score.stream.bi.BiConstraintCollector org.optaplanner.core.api.score.stream.ConstraintCollectors.min(java.util.function.BiFunction)

Should I use a different approach? Any and every help would be greatly appreciated.
Thanks for this python wrapper, it helps a lot!

This is caused by jpype-project/jpype#1016 , which is fixed in the latest version of JPype (which main uses, but not in the current latest release on PyPI). However, it probably still not work on main, since I need to modify the function return value so Java will understand it; this is done automatically for Joiners and constraint stream methods, but not yet for ConstraintCollectors. As a workaround, you can cast the lambda used in min/max to java.util.function.Function as shown in this SO answer: https://stackoverflow.com/a/70714798/9698517 .

commented

Thanks for the quick reply!
Sadly I now get another TypeError:

TypeError: No matching overloads found for *static* org.optaplanner.core.api.score.stream.ConstraintCollectors.min(java.util.function.ToIntFunction), options are:
...

My function looks like this now:

def no_24h_marathon(constraint_factory):
    def more_than_16h(earliest, latest):
        return latest - earliest
    
    def shift_start(assign):
        return assign.shift.start.timestamp()
    
    return (
        constraint_factory.forEach(assignment_class)
        .groupBy(
            lambda a: a.person.name,
            ConstraintCollectors.compose(
                # https://stackoverflow.com/questions/70507127/optapy-having-problems-with-constraints-groupby-and-sum/70714798#70714798
                ConstraintCollectors.min(
                    ToIntFunction @ shift_start
                ),
                ConstraintCollectors.max(
                    ToIntFunction @ shift_start
                ),
                more_than_16h,
            ),
        )
        .penalize(
            "No 24h marathon",
            HardSoftScore.ONE_HARD,
            lambda _, c: c <= _DIFF_IN_SECONDS,
        )
    )
commented

My bad, I overlooked that I have to use Function not ToIntFunction 😓

I still haven't got it to work, as I can't really calculate anything with the PythonComparable that this creates.

The code now looks like this:

_DIFF_IN_SECONDS = 16 * 60 * 60


def no_24h_marathon(constraint_factory):
    def time_difference(earliest, latest):
        return (latest - earliest).total_seconds()
    
    return (
        constraint_factory.forEach(assignment_class)
        .groupBy(
            lambda a: a.person.name,
            ConstraintCollectors.compose(
                # https://stackoverflow.com/questions/70507127/optapy-having-problems-with-constraints-groupby-and-sum/70714798#70714798
                ConstraintCollectors.min(
                    Function @ (lambda assign: assign.shift.start.timestamp())
                ),
                ConstraintCollectors.max(
                    Function @ (lambda assign: assign.shift.start.timestamp())
                ),
                time_difference,
            ),
        )
        .penalize(
            "No 24h marathon",
            HardSoftScore.ONE_HARD,
            lambda _, c: c <= _DIFF_IN_SECONDS,
        )
    )

With this I get:

TypeError: unsupported operand type(s) for -: 'org.optaplanner.optapy.PythonComparable' and 'org.optaplanner.optapy.PythonComparable'

I've tried doing this too:

def time_difference(earliest, latest):
        return int(latest) - int(earliest)

but get this error:

TypeError: int() argument must be a string, a bytes-like object or a number, not 'org.optaplanner.optapy.PythonComparable'

Sorry for bugging you like this and I am immensly grateful for your help!

That should be fixed by 7c8c1b0, which is in the latest release (8.19.0a1). I recommend upgrading your optapy version.

commented

I did that and tried around a lot. I still get the TypeError that PythonComparable is an unsupported operand type(s) for -.

It seems to work fine when using int but when I try to use datetime the error happens. I can't get around it by for example using datetime.timestamp() or trying to convert to int somewhere.

I have attached a minimal example of the error happening (GitHub does not allow adding .py files, so I had to change the extension to txt)
test_groupby.txt

When you change value_codes to for example [1, 2, 3] it works without issue though.

This is what I get running your code using optapy-8.19.0a1:

1009861200 - 946702800 = 63158400
11:22:55.777 [main        ] INFO  Solving started: time spent (44), best score (1), environment mode (REPRODUCIBLE), move thread count (NONE), random (JDK with seed 0).
11:22:55.782 [main        ] INFO  Solving ended: time spent (47), best score (1), score calculation speed (21/sec), phase total (2), environment mode (REPRODUCIBLE), move thread count (NONE).
<jpype._jproxy.proxy.Solution object at 0x7ff6424263e0>

I was able to reproduce it by changing the group by lambda to:

@ (lambda e: e.value.code)

The issue seems to be a typecheck that happens inside datetime.__sub__ which cause it to return NotImplemented. I probably need to update the wrapper methods to also unwrap the arguments to binary dunder methods.

commented

awesome, I now got it to work with converting the datetime to a int timestamp as I don't need float precision anyways :)

Look like the error was caused by this code: lambda _, c: c > 1, which make sense, as a timedelta is not comparable to an integer. Since ConstraintCollectors are now automatically cast, I'll close this issue.