| Quintiq file version 2.0 | 
| #parent: #root | 
| Method InitConstraintsForShiftPatternMinDuration ( | 
|   CapacityPlanningSuboptimizer_CapacityPlanningAlgorithm program, | 
|   const LibOpt_Scope scope, | 
|   const UnitPeriodTime up, | 
|   const ShiftPattern sp, | 
|   Real scalefactor_spisused_const, | 
|   Real scalefactor_spisfirst_const, | 
|   Real scalefactor_mindurationslack_const, | 
|   Real scalefactor_rhs_minimumdurationconstr | 
| ) const | 
| { | 
|   Description: 'Checks that a shift pattern is used is used consecutively on a unit for at least a certain duration.' | 
|   TextBody: | 
|   [* | 
|     /*  This method ensures that a shift pattern is used consecutively on a unit for at least a certain duration. | 
|         The base constraint is as follows: | 
|         SUM( SPIsUsed [sp][up’] * duration[up’] ) >= minDuration * SPIsFirst[sp][up] | 
|      | 
|         There are 3 possible cases: | 
|           1) The unit period is located within the optimizer scope, and the minimum duration ends in the scope as well: | 
|              For example, if the mininimum shift duration is 2 months (the optimizer scope is in brackets): | 
|                 [up_january, up_february, up_march, up_may] -> for january, february and march we can use the regular constraint. | 
|           2) The unit period is located before the scope, but the minimum duration ends inside the scope: | 
|                 16x5_november, 24x7_december, [up_january, up_february...] | 
|              Here, we need to create a constraint that starts in december, because it can still be influenced by the next months in scope. | 
|              However, the variable SPIsFirst is fixed. | 
|           3) The unit period is located right after the optimizer scope: | 
|                 [..., up_may], 16x5_june, 24x5_july | 
|              Here, we have already created a SPIsFirst variable for june, so we need to create a constraint starting in june in order to force | 
|              the optimizer to set that variable. Since the minimum duration in this example is 2 months, the optimizer can't set SPIsFirst to true | 
|              in june because the shift 16x5 would then last only 1 month. So it has to start that shift in may at the latest. | 
|      | 
|         Note: the optimizer scope is usually not continuous, and might look like this: | 
|         [up_january, up_february], 16x5, 24x5, 24x5, [up_june, up_july,...] | 
|         In that case, the behavior described above still applies. If the gap is shorter than the minimum duration, this falls into case 1. | 
|         If the gap is longer than the minimum duration, this falls into cases 2 and 3. */ | 
|      | 
|     minimumdurationconstr := program.MinShiftPatternDurationConstraints().New( sp, up ); | 
|     minimumdurationconstr.Sense( ">=" ); | 
|      | 
|     // Slack variable for minimum shift pattern duration. | 
|     // A positive slack variable represents a shift assigned to a unit period to even out the constraint. | 
|     minimumdurationconstr.NewTerm( 1.0 * scalefactor_mindurationslack_const, program.ShiftPatternDurationSlackVariables().Get( sp, up ) ); | 
|      | 
|     minduration := up.GetRoundedMinimumShiftPatternDuration( sp.MinimumDuration().HoursAsReal() ); | 
|      | 
|     // Set the following part of the constraint: minDuration * SPIsFirst[sp][up] | 
|     if( up.IsInScopeForShiftOptimization( scope )  | 
|         or guard( up.PreviousPlanningUnitPeriod().IsInScopeForShiftOptimization( scope ), false ) ) | 
|     { | 
|       minimumdurationconstr.NewTerm( -minduration * scalefactor_spisfirst_const, program.ShiftPatternIsFirstVariables().Get( sp, up ) ); | 
|       minimumdurationconstr.RHSValue( 0.0 ); | 
|     } | 
|     else if( up.ShiftPattern() = sp ) | 
|     { | 
|       // If up is not in scope and the previous unit period is not in scope either, then up is the start of a sequence of identical shift patterns | 
|       // that ends right before the scope. So if that unit period uses sp, we need to update the RHS (because SPIsFirst[sp][up] is fixed and holds true). | 
|       minimumdurationconstr.RHSValue( minduration * scalefactor_rhs_minimumdurationconstr ); | 
|     } | 
|      | 
|     // Iterate on every unit period, starting from up, to the unit period that ends after 'minduration' is elapsed. | 
|     up2 := up; | 
|     while( not isnull( up2 ) and ( up2.Start() - up.Start()).HoursAsReal() < minduration ) | 
|     { | 
|       duration := up2.Duration().HoursAsReal(); | 
|        | 
|       // If the unit period is in scope, add a term with the corresponding variable. | 
|       if( up2.IsInScopeForShiftOptimization( scope ) ) | 
|       { | 
|         minimumdurationconstr.NewTerm( duration * scalefactor_spisused_const, program.ShiftPatternIsUsedVariables().Get( sp, up2 ) ); | 
|       } | 
|       else if( up2.ShiftPattern() = sp ) | 
|       { | 
|         // If up2 is not in scope and follows a unit period in scope that has the same shift pattern, update the RHS. | 
|         // In this case, the optimizer can still follow the minimum duration constraint using the last unit period of the scope. | 
|         newrhs := this.GetConstraintRHS( minimumdurationconstr, scalefactor_rhs_minimumdurationconstr ) - duration; | 
|         minimumdurationconstr.RHSValue( newrhs * scalefactor_rhs_minimumdurationconstr ); | 
|       } | 
|        | 
|       up2 := guard( up2.NextPlanningUnitPeriod().astype( UnitPeriodTime ), null( UnitPeriodTime ) ); | 
|     } | 
|   *] | 
|   InterfaceProperties { Accessibility: 'Module' } | 
| } |