yanweiyuan3
2023-08-09 588bc7829387dfc761cc25f06f77d4c81818bd10
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
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' }
}