public Schedule CalcSchedule(FilledBaseline fb)
 {
     if (!_fbCache.ContainsKey(fb))
     {
         var optimizer = new LPOptimizerIndescrete();
         var schedule  = optimizer.Generate(Scenario, fb);
         _fbCache.Add(fb, schedule);
     }
     return(_fbCache[fb]);
 }
Exemplo n.º 2
0
        /// <summary>
        /// Fill the projects chronologically
        /// </summary>
        /// <param name="projects"></param>
        private void Fill(Project[] projects, List <float> monthlyOverload, int currentMonth, int combinationCount)
        {
            int maxAmount = Math.Max(maxCombinations / combinationCount, 1);

            // find unused projects which start this month
            var thisMonthsFillers = potentialFillers.Where(p => Utils.GetMonthIndex(p.DeliveryDate) == currentMonth).Except(projects).ToArray();
            // create combinations with those projects added
            var combinations = CombinationOfProjectsForMonth(thisMonthsFillers, (1.1 - monthlyOverload[currentMonth]) * Utils.DaysInMonthIndex(currentMonth) * 24, currentMonth);

            // limit the amount we take to the best combinations
            combinations = LimitCombinationsByParams(thisMonthsFillers, combinations, maxAmount);

            // Do we need to do the next month too? If yes, make another recursion
            if (currentMonth < numberOfMonths - 1)
            {
                if (combinations.Length > 0)
                {
                    foreach (var combination in combinations)
                    {
                        var newOverload = monthlyOverload.ToList();
                        // add this combination's added projects to the overload
                        AddToMonthlyOverload(newOverload, combination);
                        Fill(projects.Union(combination).ToArray(), newOverload, currentMonth + 1, combinationCount * combinations.Length);
                    }
                }
                else
                {
                    Fill(projects, monthlyOverload, currentMonth + 1, combinationCount);
                }
            }
            else
            {
                // If we have all the months done, create a schedule for each combination and add them to the schedules list
                if (combinations.Length > 0)
                {
                    foreach (var combination in combinations)
                    {
                        var fb = new FilledBaseline()
                        {
                            Projects = projects.Union(combination).ToArray()
                        };
                        FilledBaselines.Add(fb);
                    }
                }
                else
                {
                    var fb = new FilledBaseline()
                    {
                        Projects = projects.ToArray()
                    };
                    FilledBaselines.Add(fb);
                }
            }
        }
Exemplo n.º 3
0
        public override Schedule Generate(Scenario scenario, FilledBaseline baseline = null)
        {
            List <Schedule.BatchAllocation> schedule = new List <Schedule.BatchAllocation>();

            var projL1 = baseline.Projects.Where(p => p.Batches[0].Compatibility != Batch.LineCompatibility.Line2).Select(p => p).ToList();
            var projL2 = baseline.Projects.Where(p => p.Batches[0].Compatibility == Batch.LineCompatibility.Line2).Select(p => p).ToList();
            var offrL1 = baseline.Baseline.Projects.Where(p => p.Batches[0].Compatibility != Batch.LineCompatibility.Line2).Select(p => p).ToList();
            var offrL2 = baseline.Baseline.Projects.Where(p => p.Batches[0].Compatibility == Batch.LineCompatibility.Line2).Select(p => p).ToList();

            // LINE 1 optimization
            Console.Write("Initializing Line 1... ");
            schedule.AddRange(allocateSchedule(projL1, offrL1, getLowerBound(scenario), getAmountScheduleHours(scenario)));
            Console.WriteLine("Done");

            // LINE 2 optimization
            Console.Write("Initializing Line 2... ");
            schedule.AddRange(allocateSchedule(projL2, offrL2, getLowerBound(scenario), getAmountScheduleHours(scenario)));
            Console.WriteLine("Done");

            schedule.ToArray();
            // TODO: implement the simulated anealing for the Optimization
            // TODO: figure out how to return the schedule to Programs
            throw new NotImplementedException();
        }
Exemplo n.º 4
0
        public override Schedule Generate(Scenario scenario, FilledBaseline baseline = null)
        {
            var batches          = scenario.Projects.SelectMany(p => p.Batches).OrderBy(b => b.Compatibility).ToArray();
            var batchesBothLines = batches.Where(b => b.Compatibility == Batch.LineCompatibility.Both).ToArray();
            var batchesLine1     = batches.Where(b => b.Compatibility == Batch.LineCompatibility.Line1).ToArray();
            var batchesLine2     = batches.Where(b => b.Compatibility == Batch.LineCompatibility.Line2).ToArray();

            int       bBc          = batchesBothLines.Count();
            var       earliestDate = scenario.Projects.Min(p => p.DeliveryDate);
            var       lastDate     = scenario.Projects.Max(p => p.DeliveryDate.AddDays(7 * p.Batches.Count())).AddDays(7 * 3); // add an additional buffer of 3 weeks.
            int       maxWeek      = (lastDate - earliestDate).Days / 7;                                                       // 0 to 200
            const int weeksBuffer  = 3;

            var delayPenaltyMultiplier    = 0.01;            // 1% of revenue per week
            var interestPenaltyMultiplier = (0.045 / 52.14); // 4.5%  / 52.14 * (revenue – margin)


            var env = new GRBEnv();

            env.Set(GRB.IntParam.LogToConsole, 1);
            //env.Set(GRB.DoubleParam.TimeLimit, _config.TimeLimit.Value);
            //env.Set(GRB.DoubleParam.MIPGap, 0.02);

            var m = new GRBModel(env);

            // decide: which batch is when, on which line
            // TODO: Batch can be over two weeks
            var varBatchWeekLine1 = m.AddVars(batchesBothLines.Count() + batchesLine1.Count(), maxWeek, 0, 1, GRB.BINARY, "batchWeekLine1");
            var varBatchWeekLine2 = m.AddVars(batchesBothLines.Count() + batchesLine2.Count(), maxWeek, 0, 1, GRB.BINARY, "batchWeekLine2");

            m.Update();

            // line constraints:

            // assign batch only once (constraint for single lines then both lines)
            // if it's a fixed project though, then it must be allocated
            for (int l = 0; l < 2; l++)
            {
                var line = l == 0 ? varBatchWeekLine1 : varBatchWeekLine2;
                for (int bi = bBc; bi < line.GetLength(0); bi++)
                {
                    var batchTotal = new GRBLinExpr();
                    for (int w = 0; w < maxWeek; w++)
                    {
                        batchTotal += line[bi, w];
                    }
                    var  batch          = (l == 0 ? batchesLine1 : batchesLine2)[bi - bBc];
                    bool isFixedProject = scenario.Projects.First(p => p.Batches.Contains(batch)) is FixedProject;
                    if (!isFixedProject)
                    {
                        m.AddConstr(batchTotal <= 1, "assign batch only once");
                    }
                    else
                    {
                        m.AddConstr(batchTotal == 1, "assign batch exactly once");
                    }
                }
            }
            for (int bi = 0; bi < bBc; bi++)
            {
                var batchTotal = new GRBLinExpr();
                for (int w = 0; w < maxWeek; w++)
                {
                    batchTotal += varBatchWeekLine1[bi, w];
                    batchTotal += varBatchWeekLine2[bi, w];
                }
                var  batch          = batchesBothLines[bi];
                bool isFixedProject = scenario.Projects.First(p => p.Batches.Contains(batch)) is FixedProject;
                if (!isFixedProject)
                {
                    m.AddConstr(batchTotal <= 1, "assign batch only once");
                }
                else
                {
                    m.AddConstr(batchTotal == 1, "assign batch exactly once");
                }
            }


            // for all batches which aren't assigned to a line yet, limit the allocation of yet unassigned batches of a project to one line
            // TODO: If project has e.g. line1 and both lines, limit both lines to line 1?
            var varLineDecision = m.AddVars(scenario.Projects.Count(), 0, 1, GRB.BINARY, "varLineDecision");

            m.Update();
            for (int pi = 0; pi < scenario.Projects.Count(); pi++)
            {
                var p = scenario.Projects[pi];

                if (!p.Batches.Any(b => b.Compatibility == Batch.LineCompatibility.Both))
                {
                    continue;
                }

                var sumBatchLine1 = new GRBLinExpr();
                var sumBatchLine2 = new GRBLinExpr();
                int i             = batches.IndexOf(p.Batches.First());
                for (int j = 0; j < p.Batches.Count(); j++)
                {
                    if (p.Batches[j].Compatibility != Batch.LineCompatibility.Both)
                    {
                        continue;
                    }

                    for (int w = 0; w < maxWeek; w++)
                    {
                        sumBatchLine1 += varBatchWeekLine1[i + j, w];
                        sumBatchLine2 += varBatchWeekLine2[i + j, w];
                    }
                }
                m.AddConstr(sumBatchLine1 <= (1 - varLineDecision[pi]) * GRB.INFINITY);
                m.AddConstr(sumBatchLine2 <= varLineDecision[pi] * GRB.INFINITY);
            }

            // on each line the total of a week must not exceed 24*7
            for (int l = 0; l < 2; l++)
            {
                var lineVars = l == 0 ? varBatchWeekLine1 : varBatchWeekLine2;
                var line     = l == 0 ? batchesLine1 : batchesLine2;
                int bBC      = batchesBothLines.Count();

                for (int w = 0; w < maxWeek; w++)
                {
                    var weekTotal = new GRBLinExpr();
                    for (int b = 0; b < lineVars.GetLength(0); b++)
                    {
                        var batch = b < bBC ? batchesBothLines[b] : line[b - bBC];

                        weekTotal += lineVars[b, w] * batch.UsedWorkHours;

                        /*var test = new GRBLinExpr();
                         * test += (lineVars[b] == w) * 0.1;
                         * (l[b] - w) * batch.UsedWorkHours*/
                    }
                    m.AddConstr(weekTotal <= 24 * 7);
                }
            }

            // for each project, either all or no batches must be assigned
            var totalBatchesOfProject = new Dictionary <Project, List <GRBLinExpr> >();

            foreach (var p in scenario.Projects)
            {
                var allBatches = new List <GRBLinExpr>();

                // gather the total of all batches
                GRBLinExpr previousBatchTotal = null;
                for (int bi = 0; bi < p.Batches.Count(); bi++)
                {
                    var b          = p.Batches[bi];
                    var batchTotal = new GRBLinExpr();
                    if (b.Compatibility == Batch.LineCompatibility.Line1)
                    {
                        var bIndex = bBc + batchesLine1.IndexOf(b);
                        for (int w = 0; w < maxWeek; w++)
                        {
                            batchTotal += varBatchWeekLine1[bIndex, w];
                        }
                    }
                    else if (b.Compatibility == Batch.LineCompatibility.Line2)
                    {
                        var bIndex = bBc + batchesLine2.IndexOf(b);
                        for (int w = 0; w < maxWeek; w++)
                        {
                            batchTotal += varBatchWeekLine2[bIndex, w];
                        }
                    }
                    else
                    {
                        var bIndex = batchesBothLines.IndexOf(b);
                        for (int w = 0; w < maxWeek; w++)
                        {
                            batchTotal += varBatchWeekLine1[bIndex, w];
                            batchTotal += varBatchWeekLine2[bIndex, w];
                        }
                    }

                    // the sum of this batch over all weeks (0 or 1) has to be the same as the sum of the previous one
                    if (bi > 0)
                    {
                        m.AddConstr(previousBatchTotal == batchTotal);
                    }

                    previousBatchTotal = batchTotal;
                    allBatches.Add(batchTotal);
                }
                totalBatchesOfProject.Add(p, allBatches);
            }

            // DEPRECATED: penalty/interest term adds this constraint with slack
            // in-between batches of the same project, 3 weeks distance should exist, otherwise there will be a penalty

            /*foreach (var p in scenario.Projects)
             * {
             *  var bCompatibility = p.Batches.First().Compatibility;
             *
             *  // for all batches of this project make sure that the previous one is at least (3 + 1) weeks away
             *  // in other words: the total of batches within (3 + 1) weeks must be 4 at least.
             *  for (int w = 0; w < maxWeek - weeksBuffer; w++)
             *  {
             *      var batchTotalOver4Weeks = new GRBLinExpr();
             *      // sum all batches over a 4 week period
             *      for (int i = 0; i <= weeksBuffer; i++)
             *      {
             *          foreach (var b in p.Batches)
             *          {
             *              if (bCompatibility == Batch.LineCompatibility.Line1)
             *              {
             *                  batchTotalOver4Weeks += varBatchWeekLine1[bBc + batchesLine1.IndexOf(b), w + i];
             *              }
             *              else if (bCompatibility == Batch.LineCompatibility.Line2)
             *              {
             *                  batchTotalOver4Weeks += varBatchWeekLine2[bBc + batchesLine2.IndexOf(b), w + i];
             *              }
             *              else
             *              {
             *                  var bIndex = batchesBothLines.IndexOf(b);
             *                  batchTotalOver4Weeks += varBatchWeekLine1[bIndex, w + i];
             *                  batchTotalOver4Weeks += varBatchWeekLine2[bIndex, w + i];
             *              }
             *          }
             *      }
             *      m.AddConstr(batchTotalOver4Weeks <= 1);
             *  }
             * }*/

            // Tbd: Only half of the batch slots of line 1 may be occupied. Sometimes existst as internal projects.
            // fill gap between 50% and internal projects, monthly resolution



            // Maximize the margin (including delay and interest penalties) and the workload
            // TODO: Only first one important? no delay otherwise?
            var margin = new GRBLinExpr();

            foreach (var p in scenario.Projects)
            {
                // if the project is used add the margin
                margin += totalBatchesOfProject[p].First() * p.Margin;

                // deduct the delay penalty for each batch.
                int startWeekOfProject = (p.DeliveryDate - earliestDate).Days / 7;
                var varMaxedValue      = m.AddVars(p.Batches.Count(), 0, maxWeek, GRB.CONTINUOUS, "penaltyIndicator" + p.Description);
                var varDecisionVar     = m.AddVars(p.Batches.Count(), 0, 1, GRB.BINARY, "penaltyIndicator" + p.Description);
                m.Update();
                GRBLinExpr previousWeekValue = new GRBLinExpr();
                for (int bi = 0; bi < p.Batches.Count(); bi++)
                {
                    var b = p.Batches[bi];
                    // compare the minimal batch time (3 + 1 weeks) to the actual delivery time
                    var weekValue = new GRBLinExpr();
                    if (b.Compatibility == Batch.LineCompatibility.Line1)
                    {
                        var bIndex = bBc + batchesLine1.IndexOf(b);
                        for (int w = 0; w < maxWeek; w++)
                        {
                            weekValue += varBatchWeekLine1[bIndex, w] * w;
                        }
                    }
                    else if (b.Compatibility == Batch.LineCompatibility.Line2)
                    {
                        var bIndex = bBc + batchesLine2.IndexOf(b);
                        for (int w = 0; w < maxWeek; w++)
                        {
                            weekValue += varBatchWeekLine2[bIndex, w] * w;
                        }
                    }
                    else
                    {
                        var bIndex = batchesBothLines.IndexOf(b);
                        for (int w = 0; w < maxWeek; w++)
                        {
                            weekValue += varBatchWeekLine1[bIndex, w] * w;
                            weekValue += varBatchWeekLine2[bIndex, w] * w;
                        }
                    }

                    if (bi < p.Batches.Count() - 1)
                    {
                        // for positive difference add delay penalty, for negative difference add interest penalty
                        // var = max(0, x)
                        // var * delay
                        // (x - var) * interest
                        var plannedWeek = startWeekOfProject + bi * (weeksBuffer + 1);
                        var weekDiff    = weekValue - plannedWeek * totalBatchesOfProject[p].First(); // here we multiply the planned week with the (0/1)-allocation-indicator to avoid penalties when the project is not assigned (and therefore weekValue = 0)
                        m.AddConstr(varMaxedValue[bi] >= weekDiff);
                        m.AddConstr(varMaxedValue[bi] >= 0);
                        m.AddConstr(varMaxedValue[bi] <= weekDiff + maxWeek * (varDecisionVar[bi]));
                        m.AddConstr(varMaxedValue[bi] <= 0 + maxWeek * (1 - varDecisionVar[bi]));

                        double firstBatchImportance = bi == 0 ? 1 : 0.2; // mainly the first batch is important, the rest is meh

                        margin += varMaxedValue[bi] * delayPenaltyMultiplier * firstBatchImportance;
                        margin += (weekDiff - varMaxedValue[bi]) * interestPenaltyMultiplier * firstBatchImportance;
                    }

                    // constraint: batches of a project have to be in succession, i.e. batch2 can't come after batch3 chronologically
                    if (bi > 0)
                    {
                        m.AddConstr(weekValue - previousWeekValue >= 0);
                    }
                    previousWeekValue = weekValue;
                }
            }



            m.SetObjective(margin, GRB.MAXIMIZE);
            m.Update();

            m.Optimize();

            //env.Set(GRB.IntParam.IISMethod, 0); // makes IIS computation fast but potentially inaccurate
            //m.ComputeIIS();
            //m.Write("ilp.ilp");


            // build the solution from the optimization
            var sol = new Schedule();

            foreach (var p in scenario.Projects)
            {
                List <Schedule.BatchAllocation> allocatedBatches = new List <Schedule.BatchAllocation>();
                for (int bi = 0; bi < p.Batches.Count(); bi++)
                {
                    var b = p.Batches[bi];
                    // figure out on which week and which line the batch is allocated
                    int weekLine1 = 0;
                    int weekLine2 = 0;
                    if (b.Compatibility == Batch.LineCompatibility.Line1)
                    {
                        for (int w = 1; w <= maxWeek; w++)
                        {
                            weekLine1 += (int)varBatchWeekLine1[bBc + batchesLine1.IndexOf(b), w - 1].Get(GRB.DoubleAttr.X) * w;
                        }
                    }
                    else if (b.Compatibility == Batch.LineCompatibility.Line2)
                    {
                        for (int w = 1; w <= maxWeek; w++)
                        {
                            weekLine2 += (int)varBatchWeekLine2[bBc + batchesLine2.IndexOf(b), w - 1].Get(GRB.DoubleAttr.X) * w;
                        }
                    }
                    else
                    {
                        for (int w = 1; w <= maxWeek; w++)
                        {
                            weekLine1 += (int)varBatchWeekLine1[batchesBothLines.IndexOf(b), w - 1].Get(GRB.DoubleAttr.X) * w;
                            weekLine2 += (int)varBatchWeekLine2[batchesBothLines.IndexOf(b), w - 1].Get(GRB.DoubleAttr.X) * w;
                        }
                    }

                    if (weekLine1 > 0 && weekLine2 > 0)
                    {
                        throw new InvalidOperationException();
                    }

                    var alloc = Schedule.LineAllocation.None;
                    if (weekLine1 > 0)
                    {
                        alloc = Schedule.LineAllocation.Line1;
                    }
                    if (weekLine2 > 0)
                    {
                        alloc = Schedule.LineAllocation.Line2;
                    }

                    allocatedBatches.Add(new Schedule.BatchAllocation(b, alloc, earliestDate.AddDays(weekLine1 + weekLine2 - 1)));
                }
                sol.Add(new Schedule.ProjectAllocation(p, allocatedBatches));
            }

            return(sol);
        }
Exemplo n.º 5
0
        public override Schedule Generate(Scenario scenario, FilledBaseline fillerBaseline = null)
        {
            var projects = fillerBaseline == null ? scenario.Projects : fillerBaseline.Projects;
            // TODO: With fillerBaseline use ALL OF the projects

            var opportunities    = projects.Where(p => p is Opportunity).ToArray();
            var batches          = projects.SelectMany(p => p.Batches).ToArray();
            var batchesBothLines = batches.Where(b => b.Compatibility == Batch.LineCompatibility.Both).ToArray();
            var batchesLine1     = batches.Where(b => b.Compatibility == Batch.LineCompatibility.Line1).ToArray();
            var batchesLine2     = batches.Where(b => b.Compatibility == Batch.LineCompatibility.Line2).ToArray();

            int       bBc          = batchesBothLines.Count();
            var       earliestDate = projects.Min(p => p.DeliveryDate);
            var       lastDate     = projects.Max(p => p.DeliveryDate.AddDays(7 * p.Batches.Count())).AddDays(7 * 3); // add an additional buffer of 3 weeks.
            double    maxDays      = (lastDate - earliestDate).Days;
            var       maxWeek      = maxDays / 7 + 1;
            const int weeksBuffer  = 3;


            var delayPenaltyMultiplier    = 0.01;            // 1% of revenue per week
            var interestPenaltyMultiplier = (0.045 / 52.14); // 4.5%  / 52.14 * (revenue – margin)


            var env = new GRBEnv();

            env.Set(GRB.IntParam.UpdateMode, 1);
            env.Set(GRB.IntParam.LogToConsole, 1);
            //env.Set(GRB.IntParam.Threads, 1);
            //env.Set(GRB.IntParam.NumericFocus, 3);
            //env.Set(GRB.DoubleParam.TimeLimit, 30);
            env.Set(GRB.DoubleParam.MIPGap, 0.06);


            //env.Set(GRB.IntParam.Presolve,2);
            //env.Set(GRB.IntParam.ScaleFlag, 2);
            //env.Set(GRB.IntParam.Method, 2);



            var m = new GRBModel(env);

            // decide: which batch is when, on which line
            var varBatchTimeLine1    = m.AddVars(bBc + batchesLine1.Count(), 0, maxDays, GRB.CONTINUOUS, "varBatchTimeLine1");
            var varBatchTimeLine2    = m.AddVars(bBc + batchesLine2.Count(), 0, maxDays, GRB.CONTINUOUS, "varBatchTimeLine2");
            var varOpportunityIsUsed = m.AddVars(opportunities.Count(), 0, 1, GRB.BINARY, "varOpportunityIsUsed");
            var varLineDecision      = m.AddVars(bBc, 0, 1, GRB.BINARY, "varLineDecision"); // 0 = Line1, 1 = Line2

            //m.Update();

            //TODO: Start heuristic
            //m.Update();
            //varMaxedValue[0].Set(GRB.DoubleAttr.Start, 5);

            // baseline constraint:
            // we want some opportunities to be used for sure

            /*if (baseline  != null)
             * {
             *  var mustWinOpportunitites = new List<Opportunity>();
             *  for (int oi = 0; oi < opportunities.Count(); oi++)
             *  {
             *      if (baseline.Projects.Contains(opportunities[oi]))
             *      {
             *          mustWinOpportunitites.Add((Opportunity)opportunities[oi]);
             *          m.AddConstr(varOpportunityIsUsed[opportunities.IndexOf(opportunities[oi])] == 1);
             *      }
             *  }
             *
             * }*/


            // line constraints:
            // if it's a fixed project though, then it must be allocated
            // for all batches which aren't assigned to a line yet, limit the allocation of yet unassigned batches of a project to one line
            // TODO: If project has e.g. line1 and both lines, limit both lines to line 1?
            for (int pi = 0; pi < projects.Count(); pi++)
            {
                var p = projects[pi];

                if (!p.Batches.Any(b => b.Compatibility == Batch.LineCompatibility.Both))
                {
                    continue;
                }

                int lineRestriction = -1;
                if (p.Batches.Any(b => b.Compatibility != Batch.LineCompatibility.Both))
                {
                    lineRestriction = p.Batches.First(b => b.Compatibility != Batch.LineCompatibility.Both).Compatibility == Batch.LineCompatibility.Line1 ? 0 : 1;
                }

                int i_prev = -1;
                for (int j = 0; j < p.Batches.Count(); j++)
                {
                    if (p.Batches[j].Compatibility != Batch.LineCompatibility.Both)
                    {
                        continue;
                    }

                    var i = batchesBothLines.IndexOf(p.Batches[j]);
                    if (lineRestriction == -1)
                    {
                        if (i_prev != -1)
                        {
                            m.AddConstr(varLineDecision[i] == varLineDecision[i_prev]);
                        }
                    }
                    else
                    {
                        // if there are other batches on this project which are already on a specific line, limit to the same line
                        m.AddConstr(varLineDecision[i] == lineRestriction);
                    }
                    i_prev = i;
                }
            }

            // scheduling formulation
            for (int l = 0; l < 2; l++)
            {
                var lineVars = l == 0 ? varBatchTimeLine1 : varBatchTimeLine2;
                var line     = batchesBothLines.ToList();
                line.AddRange(l == 0 ? batchesLine1 : batchesLine2);

                for (int bi1 = 0; bi1 < line.Count() - 1; bi1++)
                {
                    var batch1    = line[bi1];
                    var o1_exists = opportunities.FirstOrDefault(o => o.Batches.Contains(batch1));
                    var oi1       = o1_exists != null ? (1 - varOpportunityIsUsed[opportunities.IndexOf(o1_exists)]) : new GRBLinExpr();
                    var s1        = lineVars[bi1];
                    for (int bi2 = bi1 + 1; bi2 < line.Count(); bi2++)
                    {
                        var batch2    = line[bi2];
                        var o2_exists = opportunities.FirstOrDefault(o => o.Batches.Contains(batch2));
                        var oi2       = o2_exists != null ? (1 - varOpportunityIsUsed[opportunities.IndexOf(o2_exists)]) : new GRBLinExpr();
                        var s2        = lineVars[bi2];

                        // S1 - E2 >= 0 OR S2 - E1 >= 0
                        // IF both batches are used
                        var decisionVar             = m.AddVar(0, 1, GRB.BINARY, "schedulingORvar");
                        var opportunityNotUsedSlack = oi1 + oi2;
                        m.AddConstr(s1 - (s2 + batch2.UsedWorkHours / 24d) >= -maxDays * (decisionVar + opportunityNotUsedSlack)); //TODO: varLineDecisionSlack for both lines batches
                        m.AddConstr(s2 - (s1 + batch1.UsedWorkHours / 24d) >= -maxDays * (1 - decisionVar + opportunityNotUsedSlack));
                    }
                }
            }

            // Tbd: Only half of the batch slots of line 1 may be occupied. Sometimes existst as internal projects.
            // fill gap between 50% and internal projects, monthly resolution



            // Maximize the margin (including delay and interest penalties) and the workload
            // Only the delivery of the first batch is really important. The rest is less important.
            var margin     = new GRBLinExpr();
            var delays     = new List <GRBLinExpr>();
            var interests  = new List <GRBLinExpr>();
            var weekValues = new List <GRBLinExpr>();
            var weekDiffs  = new List <GRBLinExpr>();

            foreach (var p in projects)
            {
                // if the project is used add the margin
                if (p is Opportunity)
                {
                    margin += varOpportunityIsUsed[opportunities.IndexOf(p)] * p.Margin;
                }
                else
                {
                    margin += p.Margin;
                }

                // deduct the delay penalty for each batch.
                var startDayOfProject = (p.DeliveryDate - earliestDate).TotalDays;
                var varMaxedValue     = m.AddVars(p.Batches.Count(), 0, maxWeek, GRB.CONTINUOUS, "penaltyIndicator" + p.Description);
                var varDecisionVar    = m.AddVars(p.Batches.Count(), 0, 1, GRB.BINARY, "penaltyIndicator" + p.Description);
                //m.Update();
                GRBLinExpr previousWeekValue = new GRBLinExpr();
                for (int pbi = 0; pbi < p.Batches.Count(); pbi++)
                {
                    var b = p.Batches[pbi];
                    // compare the minimal batch time (3 + 1 weeks) to the actual delivery time

                    GRBLinExpr weekValue;
                    if (batchesLine1.Contains(b))
                    {
                        var bi = batchesLine1.IndexOf(b) + bBc;
                        weekValue = varBatchTimeLine1[bi] / 7d;
                    }
                    else if (batchesLine2.Contains(b))
                    {
                        var bi = batchesLine2.IndexOf(b) + bBc;
                        weekValue = varBatchTimeLine2[bi] / 7d;
                    }
                    else
                    {
                        var bi = batchesBothLines.IndexOf(b);
                        // create a new var
                        var bothLineWeekVar = m.AddVar(0, maxWeek, GRB.CONTINUOUS, "weekValueBothLines");
                        //m.Update();
                        m.AddConstr(bothLineWeekVar >= varBatchTimeLine1[bi] / 7d - (varLineDecision[bi]) * maxWeek);
                        m.AddConstr(bothLineWeekVar <= varBatchTimeLine1[bi] / 7d + (varLineDecision[bi]) * maxWeek);
                        m.AddConstr(bothLineWeekVar >= varBatchTimeLine2[bi] / 7d - (1 - varLineDecision[bi]) * maxWeek);
                        m.AddConstr(bothLineWeekVar <= varBatchTimeLine2[bi] / 7d + (1 - varLineDecision[bi]) * maxWeek);
                        weekValue = bothLineWeekVar;
                    }


                    if (true || pbi < p.Batches.Count() - 1)
                    {
                        // for positive difference add delay penalty, for negative difference add interest penalty
                        // x = opportunity used ? x : 0
                        // var = max(0, x)
                        // margin += var * delay
                        // margin += (x - var) * interest
                        var        plannedWeek = startDayOfProject / 7d + pbi * (weeksBuffer + 1);
                        GRBLinExpr weekDiff;
                        if (p is Opportunity)
                        {
                            var weekDiffIfUsed = m.AddVar(0, maxWeek, GRB.CONTINUOUS, "weekValueBothLines");
                            //m.Update();
                            var wD = weekValue - plannedWeek;
                            m.AddConstr(weekDiffIfUsed >= wD - (1 - varOpportunityIsUsed[opportunities.IndexOf(p)]) * maxWeek);
                            m.AddConstr(weekDiffIfUsed <= wD + (1 - varOpportunityIsUsed[opportunities.IndexOf(p)]) * maxWeek);
                            m.AddConstr(weekDiffIfUsed <= (varOpportunityIsUsed[opportunities.IndexOf(p)]) * maxWeek);
                            m.AddConstr(weekDiffIfUsed >= -(varOpportunityIsUsed[opportunities.IndexOf(p)]) * maxWeek);
                            weekDiff = weekDiffIfUsed;
                        }
                        else
                        {
                            weekDiff = weekValue - plannedWeek;
                        }

                        m.AddConstr(varMaxedValue[pbi] >= weekDiff);
                        m.AddConstr(varMaxedValue[pbi] >= 0);
                        m.AddConstr(varMaxedValue[pbi] <= weekDiff + maxWeek * (varDecisionVar[pbi]));
                        m.AddConstr(varMaxedValue[pbi] <= 0 + maxWeek * (1 - varDecisionVar[pbi]));


                        double firstBatchImportance = pbi == 0 ? 1 : 0.2; // mainly the first batch is important, the rest is meh
                        weekValues.Add(weekValue);
                        weekDiffs.Add(weekDiff);
                        double revenueBase = p.Revenue == 0 ? 999999999 : p.Revenue;
                        delays.Add(varMaxedValue[pbi] * delayPenaltyMultiplier * revenueBase * firstBatchImportance);
                        interests.Add(-(weekDiff - varMaxedValue[pbi]) * interestPenaltyMultiplier * revenueBase * firstBatchImportance);
                        margin -= delays.Last() + interests.Last();
                    }

                    // constraint: batches of a project have to be in succession, i.e. batch2 can't come after batch3 chronologically
                    if (pbi > 0)
                    {
                        m.AddConstr(weekValue - previousWeekValue >= 0);
                    }
                    previousWeekValue = weekValue;
                }
            }



            m.SetObjective(margin, GRB.MAXIMIZE);

            //m.Tune();
            //m.GetTuneResult(0);
            m.Optimize();

            //env.Set(GRB.IntParam.IISMethod, 0); // makes IIS computation fast but potentially inaccurate
            //m.ComputeIIS();
            //m.Write("ilp.ilp");


            // TODO: Max 5 weeks delay per project


            // build the solution from the optimization
            var    sol                = new Schedule();
            int    batchCount         = 0;
            double cumulatedDelays    = 0;
            double cumulatedInterests = 0;

            foreach (var p in projects)
            {
                List <Schedule.BatchAllocation> allocatedBatches = new List <Schedule.BatchAllocation>();
                var data = new List <double[]>();
                for (int bi = 0; bi < p.Batches.Count(); bi++)
                {
                    var b        = p.Batches[bi];
                    var delay    = delays[batchCount].Value;
                    var interest = interests[batchCount].Value;
                    cumulatedDelays    += delay;
                    cumulatedInterests += interest;
                    var weekValue = weekValues[batchCount].Value;
                    var weekDiff  = weekDiffs[batchCount].Value;
                    // figure out on which week and which line the batch is allocated
                    double dayLine1 = 0;
                    double dayLine2 = 0;
                    if (b.Compatibility == Batch.LineCompatibility.Line1)
                    {
                        dayLine1 = varBatchTimeLine1[bBc + batchesLine1.IndexOf(b)].Get(GRB.DoubleAttr.X);
                    }
                    else if (b.Compatibility == Batch.LineCompatibility.Line2)
                    {
                        dayLine2 = varBatchTimeLine2[bBc + batchesLine2.IndexOf(b)].Get(GRB.DoubleAttr.X);
                    }
                    else
                    {
                        var bbi          = batchesBothLines.IndexOf(b);
                        var lineDecision = varLineDecision[bbi].Get(GRB.DoubleAttr.X);
                        dayLine1 = varBatchTimeLine1[bbi].Get(GRB.DoubleAttr.X) * (1 - lineDecision);
                        dayLine2 = varBatchTimeLine2[bbi].Get(GRB.DoubleAttr.X) * lineDecision;
                    }

                    data.Add(new double[] { delay, interest, weekValue, weekDiff, dayLine1 + dayLine2 });

                    if (dayLine1 > 0 && dayLine2 > 0)
                    {
                        throw new InvalidOperationException();
                    }

                    var alloc = Schedule.LineAllocation.None;
                    if (p is FixedProject || (p is Opportunity && varOpportunityIsUsed[opportunities.IndexOf(p)].Get(GRB.DoubleAttr.X) > 0.5))
                    {
                        if (b.Compatibility == Batch.LineCompatibility.Both)
                        {
                            alloc = varLineDecision[batchesBothLines.IndexOf(b)].Get(GRB.DoubleAttr.X) > 0.5 ? Schedule.LineAllocation.Line2 : Schedule.LineAllocation.Line1;
                        }
                        else
                        {
                            alloc = b.Compatibility == Batch.LineCompatibility.Line1 ? Schedule.LineAllocation.Line1 : Schedule.LineAllocation.Line2;
                        }
                    }
                    //if (dayLine1 > 0) alloc = Solution.LineAllocation.Line1;
                    //if (dayLine2 > 0) alloc = Solution.LineAllocation.Line2;

                    allocatedBatches.Add(new Schedule.BatchAllocation(b, alloc, earliestDate.AddDays(dayLine1 + dayLine2)));
                    batchCount++;
                }
                sol.Add(new Schedule.ProjectAllocation(p, allocatedBatches));
            }

            return(sol);
        }
Exemplo n.º 6
0
 public abstract Schedule Generate(Scenario scenario, FilledBaseline baseline = null);
        public override Schedule Generate(Scenario scenario, FilledBaseline baseline = null)
        {
            var opportunities    = scenario.Projects.Where(p => p is Opportunity).ToArray();
            var batches          = scenario.Projects.SelectMany(p => p.Batches).ToArray();
            var batchesBothLines = batches.Where(b => b.Compatibility == Batch.LineCompatibility.Both).ToArray();
            var batchesLine1     = batches.Where(b => b.Compatibility == Batch.LineCompatibility.Line1).ToArray();
            var batchesLine2     = batches.Where(b => b.Compatibility == Batch.LineCompatibility.Line2).ToArray();

            int       bBc          = batchesBothLines.Count();
            var       earliestDate = scenario.Projects.Min(p => p.DeliveryDate);
            var       lastDate     = scenario.Projects.Max(p => p.DeliveryDate.AddDays(7 * p.Batches.Count())).AddDays(7 * 3); // add an additional buffer of 3 weeks.
            double    maxDays      = (lastDate - earliestDate).Days;
            var       maxWeek      = maxDays / 7 + 2;
            const int weeksBuffer  = 3;

            const double numericScale = 100d;

            const double delayPenaltyMultiplier    = 0.01;            // 1% of revenue per week
            const double interestPenaltyMultiplier = (0.045 / 52.14); // 4.5%  / 52.14 * (revenue – margin)


            var env = new GRBEnv();

            env.Set(GRB.IntParam.UpdateMode, 1);
            env.Set(GRB.IntParam.LogToConsole, 1);
            //env.Set(GRB.IntParam.ScaleFlag, 2);
            //env.Set(GRB.IntParam.Presolve, 0);
            //env.Set(GRB.DoubleParam.TimeLimit, _config.TimeLimit.Value);
            //env.Set(GRB.DoubleParam.MIPGap, 0.02);

            var m = new GRBModel(env);

            // decide: which batch is when, on which line
            // [batch, time]
            var varBatchTimeLine1 = m.AddVars(bBc + batchesLine1.Count(), bBc + batchesLine1.Count(), 0, 1, GRB.BINARY, "varBatchTimeLine1");
            var varBatchTimeLine2 = m.AddVars(bBc + batchesLine2.Count(), bBc + batchesLine2.Count(), 0, 1, GRB.BINARY, "varBatchTimeLine2");
            // what are the time differences between times
            var varBatchTimeDiffsLine1 = m.AddVars(bBc + batchesLine1.Count() - 1, 0, maxDays, GRB.CONTINUOUS, "varBatchTimeDiffsLine1");
            var varBatchTimeDiffsLine2 = m.AddVars(bBc + batchesLine2.Count() - 1, 0, maxDays, GRB.CONTINUOUS, "varBatchTimeDiffsLine2");
            var varLineDecision        = m.AddVars(bBc, 0, 1, GRB.BINARY, "varLineDecision"); // 0 = Line1, 1 = Line2


            // make stupid solution:
            // assign fixed projects, set the rest as unused
            m.Update();
            for (int l = 0; l < 2; l++)
            {
                var line  = l == 0 ? varBatchTimeLine1 : varBatchTimeLine2;
                var diffs = l == 0 ? varBatchTimeDiffsLine1 : varBatchTimeDiffsLine2;
                for (int i = 0; i < diffs.GetLength(0); i++)
                {
                    diffs[i].Set(GRB.DoubleAttr.Start, 0);
                }
                //m.AddConstr(diffs[i] == 0, "initial solution diffs");

                int lineT = 0;
                for (int bi = bBc; bi < line.GetLength(0); bi++)
                {
                    var  batch          = (l == 0 ? batchesLine1 : batchesLine2)[bi - bBc];
                    bool isFixedProject = scenario.Projects.First(p => p.Batches.Contains(batch)) is FixedProject;
                    if (isFixedProject)
                    {
                        line[bi, lineT].Set(GRB.DoubleAttr.Start, 1);
                        //m.AddConstr(line[bi, lineT] == 1, "assign batches of project in succession");
                        for (int j = 0; j < line.GetLength(1); j++)
                        {
                            if (j != lineT)
                            {
                                line[bi, j].Set(GRB.DoubleAttr.Start, 0);
                            }
                        }
                        //m.AddConstr(line[bi, j] == 0, "initial solution");
                        lineT++;
                    }
                    else
                    {
                        for (int j = 0; j < line.GetLength(1); j++)
                        {
                            line[bi, j].Set(GRB.DoubleAttr.Start, 0);
                        }
                        //m.AddConstr(line[bi, j] == 0, "initial solution");
                    }
                }

                // make zeros also for both line batches
                for (int i = 0; i < bBc; i++)
                {
                    for (int j = 0; j < line.GetLength(1); j++)
                    {
                        line[i, j].Set(GRB.DoubleAttr.Start, 0);
                    }
                }
                //m.AddConstr(line[i, j] == 0, "initial solution");
            }


            // line constraints:

            // assign batch only once (constraint for single lines then both lines)
            // if it's a fixed project though, then it must be allocated
            for (int l = 0; l < 2; l++)
            {
                var line = l == 0 ? varBatchTimeLine1 : varBatchTimeLine2;
                for (int bi = bBc; bi < line.GetLength(0); bi++)
                {
                    var batchTotal = new GRBLinExpr();
                    for (int t = 0; t < line.GetLength(1); t++)
                    {
                        batchTotal += line[bi, t];
                    }
                    var  batch          = (l == 0 ? batchesLine1 : batchesLine2)[bi - bBc];
                    bool isFixedProject = scenario.Projects.First(p => p.Batches.Contains(batch)) is FixedProject;
                    if (!isFixedProject)
                    {
                        m.AddConstr(batchTotal <= 1, "assign batch only once");
                    }
                    else
                    {
                        m.AddConstr(batchTotal == 1, "assign batch exactly once");
                    }
                }
            }
            for (int bi = 0; bi < bBc; bi++)
            {
                var batchTotal = new GRBLinExpr();
                for (int t = 0; t < varBatchTimeLine1.GetLength(1); t++)
                {
                    batchTotal += varBatchTimeLine1[bi, t];
                }
                for (int t = 0; t < varBatchTimeLine2.GetLength(1); t++)
                {
                    batchTotal += varBatchTimeLine2[bi, t];
                }

                var  batch          = batchesBothLines[bi];
                bool isFixedProject = scenario.Projects.First(p => p.Batches.Contains(batch)) is FixedProject;
                if (!isFixedProject)
                {
                    m.AddConstr(batchTotal <= 1, "assign batch only once");
                }
                else
                {
                    m.AddConstr(batchTotal == 1, "assign batch exactly once");
                }
            }

            // a time slot can only be used once on each line
            for (int l = 0; l < 2; l++)
            {
                var line = l == 0 ? varBatchTimeLine1 : varBatchTimeLine2;
                for (int t = 0; t < line.GetLength(1); t++)
                {
                    var timeTotal = new GRBLinExpr();
                    for (int bi = 0; bi < line.GetLength(0); bi++)
                    {
                        timeTotal += line[bi, t];
                    }
                    m.AddConstr(timeTotal <= 1, "assign time slot only once on line " + (l + 1).ToString());
                }
            }


            // for all batches which aren't assigned to a line yet, limit the allocation of yet unassigned batches of a project to one line
            // TODO: If project has e.g. line1 and both lines, limit both lines to line 1?
            for (int pi = 0; pi < scenario.Projects.Count(); pi++)
            {
                var p = scenario.Projects[pi];

                if (!p.Batches.Any(b => b.Compatibility == Batch.LineCompatibility.Both))
                {
                    continue;
                }

                int lineRestriction = -1;
                if (p.Batches.Any(b => b.Compatibility != Batch.LineCompatibility.Both))
                {
                    lineRestriction = p.Batches.First(b => b.Compatibility != Batch.LineCompatibility.Both).Compatibility == Batch.LineCompatibility.Line1 ? 0 : 1;
                }

                int i_prev = -1;
                for (int j = 0; j < p.Batches.Count(); j++)
                {
                    if (p.Batches[j].Compatibility != Batch.LineCompatibility.Both)
                    {
                        continue;
                    }

                    var i = batchesBothLines.IndexOf(p.Batches[j]);
                    if (lineRestriction == -1)
                    {
                        if (i_prev != -1)
                        {
                            m.AddConstr(varLineDecision[i] == varLineDecision[i_prev], "lineDecisionRestrictionAllSame");
                        }
                    }
                    else
                    {
                        // if there are other batches on this project which are already on a specific line, limit to the same line
                        m.AddConstr(varLineDecision[i] == lineRestriction, "lineDecisionRestrictionSpecific");
                    }
                    i_prev = i;
                }
            }


            // for each project, either all or no batches must be assigned
            var totalBatchesOfProject = new Dictionary <Project, List <GRBLinExpr> >();

            foreach (var p in scenario.Projects)
            {
                var allBatches = new List <GRBLinExpr>();

                // gather the total of all batches
                GRBLinExpr previousBatchTotal = null;
                for (int bi = 0; bi < p.Batches.Count(); bi++)
                {
                    var b          = p.Batches[bi];
                    var batchTotal = new GRBLinExpr();
                    if (b.Compatibility == Batch.LineCompatibility.Line1)
                    {
                        var bIndex = bBc + batchesLine1.IndexOf(b);
                        for (int ti = 0; ti < varBatchTimeLine1.GetLength(1); ti++)
                        {
                            batchTotal += varBatchTimeLine1[bIndex, ti];
                        }
                    }
                    else if (b.Compatibility == Batch.LineCompatibility.Line2)
                    {
                        var bIndex = bBc + batchesLine2.IndexOf(b);
                        for (int ti = 0; ti < varBatchTimeLine2.GetLength(1); ti++)
                        {
                            batchTotal += varBatchTimeLine2[bIndex, ti];
                        }
                    }
                    else
                    {
                        var bIndex = batchesBothLines.IndexOf(b);
                        for (int t = 0; t < varBatchTimeLine1.GetLength(1); t++)
                        {
                            batchTotal += varBatchTimeLine1[bIndex, t];
                        }
                        for (int t = 0; t < varBatchTimeLine2.GetLength(1); t++)
                        {
                            batchTotal += varBatchTimeLine2[bIndex, t];
                        }
                    }

                    // the sum of this batch over all times (0 or 1) has to be the same as the sum of the previous one
                    if (bi > 0)
                    {
                        //m.AddConstr(previousBatchTotal == batchTotal, "allBatchesAllocatedOrNot");
                    }

                    previousBatchTotal = batchTotal;
                    allBatches.Add(batchTotal);
                }
                totalBatchesOfProject.Add(p, allBatches);
            }

            // Tbd: Only half of the batch slots of line 1 may be occupied. Sometimes existst as internal projects.
            // fill gap between 50% and internal projects, monthly resolution

            // gather the time of time slots
            GRBLinExpr[] startTimesLine1    = new GRBLinExpr[varBatchTimeLine1.GetLength(1)];
            GRBLinExpr[] startTimesLine2    = new GRBLinExpr[varBatchTimeLine2.GetLength(1)];
            var          varStartTimesLine1 = m.AddVars(startTimesLine1.Count(), 0, maxDays, GRB.CONTINUOUS, "varStartTimesLine1");
            var          varStartTimesLine2 = m.AddVars(startTimesLine2.Count(), 0, maxDays, GRB.CONTINUOUS, "varStartTimesLine2");

            for (int l = 0; l < 2; l++)
            {
                var batchLine      = l == 0 ? varBatchTimeLine1 : varBatchTimeLine2;
                var startTimes     = l == 0 ? startTimesLine1 : startTimesLine2;
                var batchesLine    = l == 0 ? batchesLine1 : batchesLine2;
                var batchTimeDiffs = l == 0 ? varBatchTimeDiffsLine1 : varBatchTimeDiffsLine2;

                for (int t = 0; t < batchLine.GetLength(1); t++)
                {
                    startTimes[t] = new GRBLinExpr();
                    // sum up all durations and buffer times before this time instance
                    for (int previous_t = 0; previous_t < t; previous_t++)
                    {
                        var duration_prev_t = new GRBLinExpr();
                        for (int bi = 0; bi < batchLine.GetLength(0); bi++)
                        {
                            var batch    = bi < bBc ? batchesBothLines[bi] : batchesLine[bi];
                            var duration = batch.UsedWorkHours / 24d;
                            duration_prev_t += batchLine[bi, previous_t] * duration;
                        }
                        duration_prev_t += batchTimeDiffs[previous_t];

                        startTimes[t] += duration_prev_t;
                    }
                }

                var varStartTime = l == 0 ? varStartTimesLine1 : varStartTimesLine2;
                for (int i = 0; i < varStartTime.Count(); i++)
                {
                    m.AddConstr(varStartTime[i] == startTimes[i], "varStartTimeAssignment");
                }
            }


            // gather the start time of the batches
            var varStartTimesBatches1 = m.AddVars(varBatchTimeLine1.GetLength(0), 0, maxDays, GRB.CONTINUOUS, "varBatchStartTimesLine1");
            var varStartTimesBatches2 = m.AddVars(varBatchTimeLine2.GetLength(0), 0, maxDays, GRB.CONTINUOUS, "varBatchStartTimesLine2");

            for (int l = 0; l < 2; l++)
            {
                var batchLine       = l == 0 ? varBatchTimeLine1 : varBatchTimeLine2;
                var batchStartTimes = l == 0 ? varStartTimesBatches1 : varStartTimesBatches2;
                var batchesLine     = l == 0 ? batchesLine1 : batchesLine2;
                var varStartTimes   = l == 0 ? varStartTimesLine1 : varStartTimesLine2;

                for (int bi = 0; bi < batchLine.GetLength(0); bi++)
                {
                    // make the batch take the value of its time slot
                    for (int ti = 0; ti < batchLine.GetLength(1); ti++)
                    {
                        m.AddConstr(batchStartTimes[bi] <= varStartTimes[ti] + maxDays * (1 - batchLine[bi, ti]), "batchTimeAssignment");
                        m.AddConstr(batchStartTimes[bi] >= varStartTimes[ti] - maxDays * (1 - batchLine[bi, ti]), "batchTimeAssignment");
                    }
                }
            }


            // scheduling formulation
            for (int l = 0; l < 2; l++)
            {
                var varBatchStartTimes = l == 0 ? varStartTimesBatches1 : varStartTimesBatches2;
                var line = batchesBothLines.ToList();
                line.AddRange(l == 0 ? batchesLine1 : batchesLine2);

                for (int bi1 = 0; bi1 < line.Count() - 1; bi1++)
                {
                    var batch1         = line[bi1];
                    var o1_exists      = opportunities.FirstOrDefault(o => o.Batches.Contains(batch1));
                    var oi1            = o1_exists != null ? (1 - totalBatchesOfProject[o1_exists][0]) : new GRBLinExpr();
                    var s1             = varBatchStartTimes[bi1];
                    var bothLineSlack1 = (bi1 < bBc) ? (l == 0 ? varLineDecision[bi1] - 0 : 1 - varLineDecision[bi1]) : new GRBLinExpr();
                    for (int bi2 = bi1 + 1; bi2 < line.Count(); bi2++)
                    {
                        var batch2         = line[bi2];
                        var o2_exists      = opportunities.FirstOrDefault(o => o.Batches.Contains(batch2));
                        var oi2            = o2_exists != null ? (1 - totalBatchesOfProject[o1_exists][0]) : new GRBLinExpr();
                        var s2             = varBatchStartTimes[bi2];
                        var bothLineSlack2 = (bi2 < bBc) ? (l == 0 ? varLineDecision[bi2] - l : l - varLineDecision[bi2]) : new GRBLinExpr();

                        // S1 - E2 >= 0 OR S2 - E1 >= 0
                        // IF both batches are used
                        var decisionVar                   = m.AddVar(0, 1, GRB.BINARY, "schedulingORvar");
                        var opportunityNotUsedSlack       = oi1 + oi2;
                        var varBothLinesLineDecisionSlack = bothLineSlack1 + bothLineSlack2; // LineDecisionSlack for both lines batches, i.e. if a bothline batch is used, don't restrict unused line.
                        m.AddConstr(s1 - (s2 + batch2.UsedWorkHours / 24d) >= -maxDays * (decisionVar + opportunityNotUsedSlack + varBothLinesLineDecisionSlack), "batchesScheduling");
                        m.AddConstr(s2 - (s1 + batch1.UsedWorkHours / 24d) >= -maxDays * (1 - decisionVar + opportunityNotUsedSlack + varBothLinesLineDecisionSlack), "batchesScheduling");
                    }
                }
            }


            // Maximize the margin (including delay and interest penalties) and the workload
            // Only the delivery of the first batch is really important. The rest is less important.

            var margin     = new GRBLinExpr();
            var delays     = new List <GRBLinExpr>();
            var interests  = new List <GRBLinExpr>();
            var weekValues = new List <GRBLinExpr>();
            var weekDiffs  = new List <GRBLinExpr>();

            foreach (var p in scenario.Projects)
            {
                // if the project is used add the margin
                if (p is Opportunity)
                {
                    margin += totalBatchesOfProject[p][0] * p.Margin * (1d / numericScale);
                }
                else
                {
                    margin += p.Margin / numericScale;
                }

                // deduct the delay penalty for each batch.
                var startDayOfProject = (p.DeliveryDate - earliestDate).TotalDays;
                var varMaxedValue     = m.AddVars(p.Batches.Count(), 0, maxWeek, GRB.CONTINUOUS, "penaltyIndicator" + p.Description);
                //var varDecisionVar = m.AddVars(p.Batches.Count(), 0, 1, GRB.BINARY, "penaltyIndicator" + p.Description);

                //m.Update();
                GRBLinExpr previousWeekValue = new GRBLinExpr();
                for (int pbi = 0; pbi < p.Batches.Count(); pbi++)
                {
                    var b = p.Batches[pbi];
                    // compare the minimal batch time (3 + 1 weeks) to the actual delivery time

                    GRBLinExpr weekValue;
                    if (batchesLine1.Contains(b))
                    {
                        var bi = batchesLine1.IndexOf(b) + bBc;
                        weekValue = varStartTimesBatches1[bi] / 7d;
                    }
                    else if (batchesLine2.Contains(b))
                    {
                        var bi = batchesLine2.IndexOf(b) + bBc;
                        weekValue = varStartTimesBatches2[bi] / 7d;
                    }
                    else
                    {
                        var bi = batchesBothLines.IndexOf(b);
                        // create a new var
                        var bothLineWeekVar = m.AddVar(0, maxWeek, GRB.CONTINUOUS, "weekValueBothLines");
                        //m.Update();
                        m.AddConstr(bothLineWeekVar >= varStartTimesBatches1[bi] / 7d - (varLineDecision[bi]) * maxWeek, "weekValueBothLinesDefinitionByConstraints");
                        m.AddConstr(bothLineWeekVar <= varStartTimesBatches1[bi] / 7d + (varLineDecision[bi]) * maxWeek, "weekValueBothLinesDefinitionByConstraints");
                        m.AddConstr(bothLineWeekVar >= varStartTimesBatches2[bi] / 7d - (1 - varLineDecision[bi]) * maxWeek, "weekValueBothLinesDefinitionByConstraints");
                        m.AddConstr(bothLineWeekVar <= varStartTimesBatches2[bi] / 7d + (1 - varLineDecision[bi]) * maxWeek, "weekValueBothLinesDefinitionByConstraints");
                        weekValue = bothLineWeekVar;
                    }


                    if (true || pbi < p.Batches.Count() - 1)
                    {
                        // for positive difference add delay penalty, for negative difference add interest penalty
                        // x = opportunity used ? x : 0
                        // var = max(0, x)
                        // margin += var * delay
                        // margin += (x - var) * interest
                        var        plannedWeek = startDayOfProject / 7d + pbi * (weeksBuffer + 1);
                        GRBLinExpr weekDiff;
                        if (p is Opportunity)
                        {
                            var weekDiffIfUsed = m.AddVar(-maxWeek, maxWeek, GRB.CONTINUOUS, "weekDiffIfUsed");
                            //m.Update();
                            var wD = weekValue - plannedWeek;
                            m.AddConstr(weekDiffIfUsed >= wD - (1 - totalBatchesOfProject[p][0]) * maxWeek, "weekDiffConstraintDefinition1");
                            m.AddConstr(weekDiffIfUsed <= wD + (1 - totalBatchesOfProject[p][0]) * maxWeek, "weekDiffConstraintDefinition2");
                            m.AddConstr(weekDiffIfUsed <= (totalBatchesOfProject[p][0]) * maxWeek, "weekDiffConstraintDefinition3");
                            m.AddConstr(weekDiffIfUsed >= -(totalBatchesOfProject[p][0]) * maxWeek, "weekDiffConstraintDefinition4");
                            weekDiff = weekDiffIfUsed;
                        }
                        else
                        {
                            weekDiff = weekValue - plannedWeek;
                        }

                        m.AddConstr(varMaxedValue[pbi] >= weekDiff, "maxedWeekDiffValueConstraintDefinition");
                        //m.AddConstr(varMaxedValue[pbi] >= 0, "maxedWeekDiffValueConstraintDefinition");
                        //m.AddConstr(varMaxedValue[pbi] <= weekDiff + maxWeek * (varDecisionVar[pbi]), "maxedWeekDiffValueConstraintDefinition");
                        //m.AddConstr(varMaxedValue[pbi] <= 0 + maxWeek * (1 - varDecisionVar[pbi]), "maxedWeekDiffValueConstraintDefinition");


                        double firstBatchImportance = pbi == 0 ? 1 : 0.2; // mainly the first batch is important, the rest is meh
                        weekValues.Add(weekValue);
                        weekDiffs.Add(weekDiff);
                        double revenueBase = p.Revenue == 0 ? 1E+8 : p.Revenue; // if it's an internal project it must not be shifted, therefore we make the penalty high
                        revenueBase /= numericScale;
                        delays.Add(varMaxedValue[pbi] * delayPenaltyMultiplier * revenueBase * firstBatchImportance);
                        interests.Add(-(weekDiff - varMaxedValue[pbi]) * interestPenaltyMultiplier * revenueBase * firstBatchImportance);
                        margin -= delays.Last() + interests.Last();
                    }

                    // constraint: batches of a project have to be in succession, i.e. batch2 can't come after batch3 chronologically
                    if (pbi > 0)
                    {
                        m.AddConstr(weekValue - previousWeekValue >= 0, "batchesOfProjectHaveToBeInSuccession");
                    }
                    previousWeekValue = weekValue;
                }
            }



            //m.SetObjective(margin, GRB.MAXIMIZE);

            m.Update();
            m.Write("ilproman3.mps");
            m.Write("ilproman3.mst");

            //m.Tune();
            //m.GetTuneResult(0);
            m.Optimize();

            //env.Set(GRB.IntParam.IISMethod, 0); // makes IIS computation fast but potentially inaccurate
            m.ComputeIIS();
            m.Write("ilp.ilp");


            // TODO: Max 5 weeks delay per project


            // build the solution from the optimization
            var    sol                = new Schedule();
            int    batchCount         = 0;
            double cumulatedDelays    = 0;
            double cumulatedInterests = 0;

            foreach (var p in scenario.Projects)
            {
                List <Schedule.BatchAllocation> allocatedBatches = new List <Schedule.BatchAllocation>();
                var data = new List <double[]>();
                for (int bi = 0; bi < p.Batches.Count(); bi++)
                {
                    var b        = p.Batches[bi];
                    var delay    = delays[batchCount].Value;
                    var interest = interests[batchCount].Value;
                    cumulatedDelays    += delay;
                    cumulatedInterests += interest;
                    var weekValue = weekValues[batchCount].Value;
                    var weekDiff  = weekDiffs[batchCount].Value;
                    // figure out on which week and which line the batch is allocated
                    double dayLine1 = 0;
                    double dayLine2 = 0;
                    if (b.Compatibility == Batch.LineCompatibility.Line1)
                    {
                        dayLine1 = varStartTimesBatches1[bBc + batchesLine1.IndexOf(b)].Get(GRB.DoubleAttr.X);
                    }
                    else if (b.Compatibility == Batch.LineCompatibility.Line2)
                    {
                        dayLine2 = varStartTimesBatches2[bBc + batchesLine2.IndexOf(b)].Get(GRB.DoubleAttr.X);
                    }
                    else
                    {
                        var bbi          = batchesBothLines.IndexOf(b);
                        var lineDecision = varLineDecision[bbi].Get(GRB.DoubleAttr.X);
                        dayLine1 = varStartTimesBatches1[bbi].Get(GRB.DoubleAttr.X) * (1 - lineDecision);
                        dayLine2 = varStartTimesBatches2[bbi].Get(GRB.DoubleAttr.X) * lineDecision;
                    }

                    data.Add(new double[] { delay, interest, weekValue, weekDiff, dayLine1 + dayLine2 });

                    if (dayLine1 > 0 && dayLine2 > 0)
                    {
                        throw new InvalidOperationException();
                    }

                    var alloc = Schedule.LineAllocation.None;
                    if (p is FixedProject || (p is Opportunity && totalBatchesOfProject[p][0].Value > 0.5))
                    {
                        if (b.Compatibility == Batch.LineCompatibility.Both)
                        {
                            alloc = varLineDecision[batchesBothLines.IndexOf(b)].Get(GRB.DoubleAttr.X) > 0.5 ? Schedule.LineAllocation.Line2 : Schedule.LineAllocation.Line1;
                        }
                        else
                        {
                            alloc = b.Compatibility == Batch.LineCompatibility.Line1 ? Schedule.LineAllocation.Line1 : Schedule.LineAllocation.Line2;
                        }
                    }
                    //if (dayLine1 > 0) alloc = Solution.LineAllocation.Line1;
                    //if (dayLine2 > 0) alloc = Solution.LineAllocation.Line2;

                    allocatedBatches.Add(new Schedule.BatchAllocation(b, alloc, earliestDate.AddDays(dayLine1 + dayLine2)));
                    batchCount++;
                }
                sol.Add(new Schedule.ProjectAllocation(p, allocatedBatches));
            }

            /*var maxMustWinDate = mustWinOpportunitites.Max(p => p.DeliveryDate.AddDays(7 * p.Batches.Count())).AddDays(7 * 3);
             * var minMustWinDate = mustWinOpportunitites.Min(p2 => p2.DeliveryDate);
             * var sevenmonthsrange = sol.Where(p => p.Project.DeliveryDate >= minMustWinDate && p.Project.DeliveryDate <= maxMustWinDate).ToArray();
             * var ops = sevenmonthsrange.Where(p => mustWinOpportunitites.Contains(p.Project) || p.Project is FixedProject);
             * var s = ops.Sum(p => p.Project.Revenue);*/

            return(sol);
        }