1

I am trying to implement a predecessor constraint similar to the Job Scheduling example given in java.

But I struggle with the ordering constraint definition to consider predecessors.

I have defined my time slots simply as ordered integers :

@dataclass
class Timeslot:
    slot : int

And my operations as planning entities with few boolean operations and predecessors as a list of other operations id :

@planning_entity
@dataclass
class Operation:
    id: Annotated[int, PlanningId]
    name: str
    predecessors: list[int]
    timeslot: Annotated[Timeslot, PlanningVariable] = field(default=None)

    def isPred(self,other):
        return other.id in self.predecessors

    def isAfter(self,other):
        return self.timeslot.slot < other.timeslot.slot

And then my constraint as :

def precondition_conflict(constraint_factory: ConstraintFactory) -> Constraint:
    # Respect order constraints
    return (
        constraint_factory.for_each_unique_pair(Operation)
        .filter(lambda op1, op2 : op1.isPred(op2))
        .filter(lambda op1, op2 : op1.isAfter(op2))
        .penalize(HardSoftScore.ONE_HARD)
        .as_constraint("Order conflict")
    )

Then I instanciate my problem :

 operations.append(Operation(1,"OP N°1 : Bake the cake",[3]))
 operations.append(Operation(2,"OP N°2 : Enjoy your meal",[4]))
 operations.append(Operation(3,"OP N°3 : Mix flour, eggs and whatever they say in the recipe",[]))
 operations.append(Operation(4,"OP N°4 : take it out of the oven",[1]))

But the solver keeps giving me solutions where the operations order is incorrect, with 0 hard constraint violated. For instance :

INFO:timefold.solver:Solving ended: time spent (30056), best score (0hard/0soft), move evaluation speed (219895/sec), phase total (2), environment mode (PHASE_ASSERT), move thread count (NONE).
INFO:app:+------------------+------------------+
INFO:app:|0                 |OP N°4 : take it out of the oven|
INFO:app:+------------------+------------------+
INFO:app:|1                 |OP N°2 : Enjoy your meal|
INFO:app:+------------------+------------------+
INFO:app:|2                 |OP N°3 : Mix flour, eggs and whatever they say in the recipe|
INFO:app:+------------------+------------------+
INFO:app:|3                 |OP N°1 : Bake the cake|
INFO:app:+------------------+------------------+

Where obviously I expected 3 -> 1 -> 4 -> 2

Where did I do it wrong?

SOLUTION (thanks to Lukáš Petrovický):

def precondition_conflict(constraint_factory: ConstraintFactory) -> Constraint:
    # Respect order constraints
    return (
        constraint_factory.for_each(Operation)
        .join(Operation)
        .filter(lambda op1, op2 : op1.isPred(op2))
        .filter(lambda op1, op2 : op1.isAfter(op2))
        .penalize(HardSoftScore.ONE_HARD)
        .as_constraint("Order conflict")
    )

1 Answer 1

2

Unique pairs are tricky. In this case, you probably want to avoid using them, and instead go for a standard join.

Consider three operations: A, B, C. Unique pairs will give you A+B, A+C and B+C. It assumes that whatever is true for A+B, it is also true for B+A and therefore processing both would be redundant. But this is not true in your case.

A standard join would fix it, enumerating all possible pairs. Or a better filter, which checks the condition from both sides.

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks a lot! It solved it! I will update the post with the solution following your answer

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.