Timefold rolling timegrain period constraint

98 views Asked by At

In Timefold, is there a standard way to look at an X min rolling window across previous and future timegrains and look for planning changes?

In my use case there is a cost to changes and a hardware limitation on how frequently changes can occur, the former being a soft constraint and latter a hard constraint. I've implemented a version of the soft constraint but the hard constraint implementation is looking to be very inefficient without some thought put into the constraint stream.

The hardware limitation is generally around one change every 5 minutes as a minimum frequency to change. The timegrain duration will very depending on how big / future looking the simulation is that is being optimized for, with very long time period simulations having larger timegrains and very near term, near real time, runs having much shorter timegrains.

Edit: The "user" will be assigned to hardware resources per timegrain. If a single assignment is not sufficient to satisfy the user SLA then multiple entities are assigned for that timegrain to satisfy the SLA. The dynamics of the problem and the SLAs change over time so a single solution cannot always be maintained, but can be maintained for several hours potentially.

There are hard limits on switching frequency such that an assignment that changes every 30 seconds is not possible. The hardware assignment can at most be changed for instance every 5 minutes, this will depend on the hardware in question.

I was thinking to look at the last 5 minutes of timegrains as well as the next 5 minutes of timegrains and as long as there is a 5 minute window, not to penalize. This is possible with a fairly rudimentary approach but requires many comparisons. I think just looking at the previous 5 minutes would also be sufficient as checks on the later timegrain instance would pick up the penalty for a change in the future.

Generally I don't recall seeing any examples in the docs or github with rolling window based constraints. If there is a standard way to do this that is more efficient that would be helpful.

1

There are 1 answers

1
Christopher Chianelli On BEST ANSWER

From your description, I am assuming your entity looks like this:

@PlanningEntity
public class UserResourceAssignment {
    TimeGrain timeGrain;

    User user;

    @PlanningVariable
    Resource resource1;

    @PlanningVariable(nullable=true)
    Resource resource2;

    
    @PlanningVariable(nullable=true)
    Resource resource3;
    
    // ...

    public List<Resource> getResourceTuple() {
        return List.of(Optional.ofNullable(resource1),
                       Optional.ofNullable(resource2),
                       Optional.ofNullable(resource3));
    }
}

And you want to penalize when any of the resources change value too frequently. Or phased another way, when the combined tuple (resource1, resource2, resource3) changes too frequently.

For instance, if the minimum was 3 timegrains before change, you want this to be penalized:

(0, User A, R0, R1, R2),
(1, User A, R0, R1, R2), 
(2, User A, R0, R1, R3)

Since the tuple changed from (R0, R1, R2) to (R0, R1, R3) before 3 timegrains was spent. Likewise, you do not want this to be penalized:

(0, User A, R0, R1, R2),
(1, User A, R0, R1, R2), 
(2, User A, R0, R1, R2),
(3, User A, R0, R1, R3)

Since the change happens after it spent 3 timegrains with the assignment (R0, R1, R2). So what we are looking for is the length of the consecutive sequence (excluding the last consecutive sequence), using (user, resource1, resource2, resource3) as the group key. If it less than the minimum, penalize. For consecutive sequences and their breaks, there is the experimental consecutive constraint collector (tracker issue for when it stops being experimental). Here how it can be used to implement the above constraint:

Constraint minimumUserResourceAssignmentLength(ConstraintFactory constraintFactory) {
    constraintFactory.forEachIncludingNullVars(UserResourceAssignment.class)
        .groupBy(UserResourceAssignment::getUser,
                 UserResourceAssignment::getResourceTuple,
                 ExperimentalConstraintCollectors.consecutive(UserResourceAssignment::getTimeGrain, TimeGrain::getIndex))
        .flattenLast(ConsecutiveInfo::getConsecutiveSequences)
        .filter((user, resourceTuple, consecutiveResourceAssignment) -> consecutiveResourceAssignment.getLastItem() != LAST_TIMEGRAIN && consecutiveResourceAssignment.getCount() * consecutiveResourceAssignment.getFirstItem().getTimeGrainLengthInMinutes() < HARD_MINIMUM_LENGTH)
        .penalize(HardSoftScore.ONE_HARD, (user, resourceTuple, consecutiveResourceAssignment) -> HARD_MINIMUM_LENGTH - consecutiveResourceAssignment.getCount() * consecutiveResourceAssignment.getFirstItem().getTimeGrainLengthInMinutes())
        .asConstraint("Minimum Resource Assignment Length");
}

You might want to do an additional join with a singleton problem fact prior to the groupby and add it as a group key to obtain LAST_TIMEGRAIN and HARD_MINIMUM_LENGTH (LAST_TIMEGRAIN is needed since we are looking at consecutive sequences of the tuple (User, resource1, resource2, resource3), so isLast() will only tell us if it the last time resource1, resource2, resource3 was assigned to the User, NOT if it is the final consecutive sequence assigned to the user.)