Beispiel #1
0
void SolverMRP::solve(const BufferInfinite* b, void* v)
{
  SolverMRPdata* data = static_cast<SolverMRPdata*>(v);

  // Call the user exit
  if (userexit_buffer) userexit_buffer.call(b, PythonData(data->constrainedPlanning));

  // Message
  if (data->getSolver()->getLogLevel()>1)
    logger << indent(b->getLevel()) << "  Infinite buffer '" << b << "' is asked: "
        << data->state->q_qty << "  " << data->state->q_date << endl;

  // Reply whatever is requested, regardless of date, quantity or supply.
  // The demand is not propagated upstream either.
  data->state->a_qty = data->state->q_qty;
  data->state->a_date = data->state->q_date;
  if (b->getItem())
    data->state->a_cost += data->state->q_qty * b->getItem()->getPrice();

  // Message
  if (data->getSolver()->getLogLevel()>1)
    logger << indent(b->getLevel()) << "  Infinite buffer '" << b << "' answers: "
        << data->state->a_qty << "  " << data->state->a_date << "  "
        << data->state->a_cost << "  " << data->state->a_penalty << endl;
}
Beispiel #2
0
DECLARE_EXPORT void SolverMRP::solve(const Flow* fl, void* v)  // @todo implement search mode
{
  // Note: This method is only called for consuming flows and for the leading
  // flow of an alternate group. See SolverMRP::checkOperation

  SolverMRPdata* data = static_cast<SolverMRPdata*>(v);
  if (fl->hasAlternates())
  {
    // CASE I: It is an alternate flow.
    // We ask each alternate flow in order of priority till we find a flow
    // that has a non-zero reply.

    // 1) collect a list of alternates
    list<const Flow*> thealternates;
    const Flow *x = fl->hasAlternates() ? fl : fl->getAlternate();
    for (Operation::flowlist::const_iterator i = fl->getOperation()->getFlows().begin();
        i != fl->getOperation()->getFlows().end(); ++i)
      if ((i->getAlternate() == x || &*i == x)
          && i->getEffective().within(data->state->q_flowplan->getDate()))
        thealternates.push_front(&*i);

    // 2) Sort the list
    thealternates.sort(sortFlow);

    // 3) Control the planning mode
    bool originalPlanningMode = data->constrainedPlanning;
    data->constrainedPlanning = true;
    const Flow *firstAlternate = NULL;
    double firstQuantity = 0.0;

    // Remember the top constraint
    bool originalLogConstraints = data->logConstraints;
    //Problem* topConstraint = data->planningDemand->getConstraints().top();

    // 4) Loop through the alternates till we find a non-zero reply
    Date min_next_date(Date::infiniteFuture);
    double ask_qty;
    FlowPlan *flplan = data->state->q_flowplan;
    for (list<const Flow*>::const_iterator i = thealternates.begin();
        i != thealternates.end();)
    {
      const Flow *curflow = *i;
      data->state->q_flowplan = flplan; // because q_flowplan can change

      // 4a) Switch to this flow
      if (data->state->q_flowplan->getFlow() != curflow)
        data->state->q_flowplan->setFlow(curflow);

      // 4b) Call the Python user exit if there is one
      if (userexit_flow)
      {
        PythonObject result = userexit_flow.call(data->state->q_flowplan, PythonObject(data->constrainedPlanning));
        if (!result.getBool())
        {
          // Return value is false, alternate rejected
          if (data->getSolver()->getLogLevel()>1)
            logger << indent(curflow->getOperation()->getLevel())
                << "   User exit disallows consumption from '"
                << (*i)->getBuffer()->getName() << "'" << endl;
          // Move to the next alternate
          if (++i != thealternates.end() && data->getSolver()->getLogLevel()>1)
            logger << indent(curflow->getOperation()->getLevel()) << "   Alternate flow switches from '"
                << curflow->getBuffer()->getName() << "' to '"
                << (*i)->getBuffer()->getName() << "'" << endl;
          continue;
        }
      }

      // Remember the first alternate
      if (!firstAlternate)
      {
        firstAlternate = *i;
        firstQuantity = data->state->q_flowplan->getQuantity();
      }

      // Constraint tracking
      if (*i != firstAlternate)
        // Only enabled on first alternate
        data->logConstraints = false;
      else
        // Keep track of constraints, if enabled
        data->logConstraints = originalLogConstraints;

      // 4c) Ask the buffer
      data->state->q_qty = ask_qty = - data->state->q_flowplan->getQuantity();
      data->state->q_date = data->state->q_flowplan->getDate();
      CommandManager::Bookmark* topcommand = data->setBookmark();
      curflow->getBuffer()->solve(*this,data);

      // 4d) A positive reply: exit the loop
      if (data->state->a_qty > ROUNDING_ERROR)
      {
        // Update the opplan, which is required to (1) update the flowplans
        // and to (2) take care of lot sizing constraints of this operation.
        if (data->state->a_qty < ask_qty - ROUNDING_ERROR)
        {
          flplan->setQuantity(-data->state->a_qty, true);
          data->state->a_qty = -flplan->getQuantity();
        }
        if (data->state->a_qty > ROUNDING_ERROR)
        {
          data->constrainedPlanning = originalPlanningMode;
          data->logConstraints = originalLogConstraints;
          return;
        }
      }

      // 4e) Undo the plan on the alternate
      data->rollback(topcommand);

      // 4f) Prepare for the next alternate
      if (data->state->a_date < min_next_date)
        min_next_date = data->state->a_date;
      if (++i != thealternates.end() && data->getSolver()->getLogLevel()>1)
        logger << indent(curflow->getOperation()->getLevel()) << "   Alternate flow switches from '"
            << curflow->getBuffer()->getName() << "' to '"
            << (*i)->getBuffer()->getName() << "'" << endl;
    }

    // 5) No reply found, all alternates are infeasible
    if (!originalPlanningMode)
    {
      assert(firstAlternate);
      // Unconstrained plan: Plan on the primary alternate
      // Switch to this flow
      if (flplan->getFlow() != firstAlternate)
        flplan->setFlow(firstAlternate);
      // Message
      if (data->getSolver()->getLogLevel()>1)
        logger << indent(fl->getOperation()->getLevel())
            << "   Alternate flow plans unconstrained on alternate '"
            << firstAlternate->getBuffer()->getName() << "'" << endl;
      // Plan unconstrained
      data->constrainedPlanning = false;
      data->state->q_flowplan = flplan; // because q_flowplan can change
      flplan->setQuantity(firstQuantity, true);
      data->state->q_qty = ask_qty = - flplan->getQuantity();
      data->state->q_date = flplan->getDate();
      firstAlternate->getBuffer()->solve(*this,data);
      data->state->a_qty = -flplan->getQuantity();
      // Restore original planning mode
      data->constrainedPlanning = originalPlanningMode;
    }
    else
    {
      // Constrained plan: Return 0
      data->state->a_date = min_next_date;
      data->state->a_qty = 0;
      if (data->getSolver()->getLogLevel()>1)
        logger << indent(fl->getOperation()->getLevel()) <<
            "   Alternate flow doesn't find supply on any alternate : "
            << data->state->a_qty << "  " << data->state->a_date << endl;
    }
  }
  else
  {
    // CASE II: Not an alternate flow.
    // In this case, this method is passing control on to the buffer.
    data->state->q_qty = - data->state->q_flowplan->getQuantity();
    data->state->q_date = data->state->q_flowplan->getDate();
    if (data->state->q_qty != 0.0)
    {
      fl->getBuffer()->solve(*this,data);
      if (data->state->a_date > fl->getEffective().getEnd())
      {
        // The reply date must be less than the effectivity end date: after
        // that date the flow in question won't consume any material any more.
        if (data->getSolver()->getLogLevel()>1
            && data->state->a_qty < ROUNDING_ERROR)
          logger << indent(fl->getBuffer()->getLevel()) << "  Buffer '"
              << fl->getBuffer()->getName() << "' answer date is adjusted to "
              << fl->getEffective().getEnd()
              << " because of a date effective flow" << endl;
        data->state->a_date = fl->getEffective().getEnd();
      }
    }
    else
    {
      // It's a zero quantity flowplan.
      // E.g. because it is not effective.
      data->state->a_date = data->state->q_date;
      data->state->a_qty = 0.0;
    }
  }
}
Beispiel #3
0
DECLARE_EXPORT void SolverMRP::solve(const Demand* l, void* v)
{
  // Set a bookmark at the current command
  SolverMRPdata* data = static_cast<SolverMRPdata*>(v);
  CommandManager::Bookmark* topcommand = data->setBookmark();

  // Create a state stack
  State* mystate = data->state;
  data->push();

  try
  {
    // Call the user exit
    if (userexit_demand) userexit_demand.call(l, PythonData(data->constrainedPlanning));
    short loglevel = data->getSolver()->getLogLevel();

    // Note: This solver method does not push/pop states on the stack.
    // We continue to work on the top element of the stack.

    // Message
    if (loglevel>0)
    {
      logger << "Planning demand '" << l->getName() << "' (" << l->getPriority()
          << ", " << l->getDue() << ", " << l->getQuantity() << ")";
      if (!data->constrainedPlanning || !data->getSolver()->isConstrained())
        logger << " in unconstrained mode";
      logger << endl;
    }

    // Unattach previous delivery operationplans, if required.
    if (data->getSolver()->getErasePreviousFirst())
    {
      // Locked operationplans will NOT be deleted, and a part of the demand can
      // still remain planned.
      const_cast<Demand*>(l)->deleteOperationPlans(false, data);

      // Empty constraint list
      const_cast<Demand*>(l)->getConstraints().clear();
    }

    // Track constraints or not
    data->logConstraints = (getPlanType() == 1);

    // Determine the quantity to be planned and the date for the planning loop
    double plan_qty = l->getQuantity() - l->getPlannedQuantity();
    Date plan_date = l->getDue();
    if (plan_qty < ROUNDING_ERROR || plan_date == Date::infiniteFuture)
    {
      if (loglevel>0) logger << "  Nothing to be planned." << endl;
      data->pop();
      return;
    }
    if (plan_qty < l->getMinShipment()) plan_qty = l->getMinShipment();

    // Temporary values to store the 'best-reply' so far
    double best_q_qty = 0.0, best_a_qty = 0.0;
    Date best_q_date;

    // Select delivery operation
    Operation* deliveryoper = l->getDeliveryOperation();

    // Handle invalid or missing delivery operations
    {
    string problemtext = string("Demand '") + l->getName() + "' has no delivery operation";
    Problem::const_iterator j = Problem::begin(const_cast<Demand*>(l), false);
    while (j!=Problem::end())
    {
      if (&(j->getType()) == ProblemInvalidData::metadata
          && j->getDescription() == problemtext)
        break;
      ++j;
    }
    if (!deliveryoper)
    {
      // Create a problem
      if (j == Problem::end())
      new ProblemInvalidData(const_cast<Demand*>(l), problemtext, "demand",
          l->getDue(), l->getDue(), l->getQuantity());
      // Abort planning of this demand
      throw DataException("Demand '" + l->getName() + "' can't be planned");
    }
    else
      // Remove problem that may already exist
      delete &*j;
    }

    // Planning loop
    do
    {
      // Message
      if (loglevel>0)
        logger << "Demand '" << l << "' asks: "
            << plan_qty << "  " << plan_date << endl;

      // Store the last command in the list, in order to undo the following
      // commands if required.
      CommandManager::Bookmark* topcommand = data->setBookmark();

      // Plan the demand by asking the delivery operation to plan
      double q_qty = plan_qty;
      data->state->curBuffer = NULL;
      data->state->q_qty = plan_qty;
      data->state->q_date = plan_date;
      data->planningDemand = const_cast<Demand*>(l);
      data->state->curDemand = const_cast<Demand*>(l);
      data->state->curOwnerOpplan = NULL;
      deliveryoper->solve(*this,v);
      Date next_date = data->state->a_date;

      if (data->state->a_qty < ROUNDING_ERROR
          && plan_qty > l->getMinShipment() && l->getMinShipment() > 0)
      {
        bool originalLogConstraints = data->logConstraints;
        data->logConstraints = false;
        try
        {
          // The full asked quantity is not possible.
          // Try with the minimum shipment quantity.
          if (loglevel>1)
            logger << "Demand '" << l << "' tries planning minimum quantity " << l->getMinShipment() << endl;
          data->rollback(topcommand);
          data->state->curBuffer = NULL;
          data->state->q_qty = l->getMinShipment();
          data->state->q_date = plan_date;
          data->state->curDemand = const_cast<Demand*>(l);
          deliveryoper->solve(*this,v);
          if (data->state->a_date < next_date)
            next_date = data->state->a_date;
          if (data->state->a_qty > ROUNDING_ERROR)
          {
            // The minimum shipment quantity is feasible.
            // Now try iteratively different quantities to find the best we can do.
            double min_qty = l->getMinShipment();
            double max_qty = plan_qty;
            double delta = fabs(max_qty - min_qty);
            while (delta > data->getSolver()->getIterationAccuracy() * l->getQuantity()
                && delta > data->getSolver()->getIterationThreshold())
            {
              // Note: we're kind of assuming that the demand is an integer value here.
              double new_qty = floor((min_qty + max_qty) / 2);
              if (new_qty == min_qty)
              {
                // Required to avoid an infinite loop on the same value...
                new_qty += 1;
                if (new_qty > max_qty) break;
              }
              if (loglevel>0)
                logger << "Demand '" << l << "' tries planning a different quantity " << new_qty << endl;
              data->rollback(topcommand);
              data->state->curBuffer = NULL;
              data->state->q_qty = new_qty;
              data->state->q_date = plan_date;
              data->state->curDemand = const_cast<Demand*>(l);
              deliveryoper->solve(*this,v);
              if (data->state->a_date < next_date)
                next_date = data->state->a_date;
              if (data->state->a_qty > ROUNDING_ERROR)
                // Too small: new min
                min_qty = new_qty;
              else
                // Too big: new max
                max_qty = new_qty;
              delta = fabs(max_qty - min_qty);
            }
            q_qty = min_qty;  // q_qty is the biggest Q quantity giving a positive reply
            if (data->state->a_qty <= ROUNDING_ERROR)
            {
              if (loglevel>0)
                logger << "Demand '" << l << "' restores plan for quantity " << min_qty << endl;
              // Restore the last feasible plan
              data->rollback(topcommand);
              data->state->curBuffer = NULL;
              data->state->q_qty = min_qty;
              data->state->q_date = plan_date;
              data->state->curDemand = const_cast<Demand*>(l);
              deliveryoper->solve(*this,v);
            }
          }
        }
        catch (...)
        {
          data->logConstraints = originalLogConstraints;
          throw;
        }
        data->logConstraints = originalLogConstraints;
      }

      // Message
      if (loglevel>0)
        logger << "Demand '" << l << "' gets answer: "
            << data->state->a_qty << "  " << next_date << "  "
            << data->state->a_cost << "  " << data->state->a_penalty << endl;

      // Update the date to plan in the next loop
      Date copy_plan_date = plan_date;

      // Compare the planned quantity with the minimum allowed shipment quantity
      // We don't accept the answer in case:
      // 1) Nothing is planned
      // 2) The planned quantity is less than the minimum shipment quantity
      // 3) The remaining quantity after accepting this answer is less than
      //    the minimum quantity.
      if (data->state->a_qty < ROUNDING_ERROR
          || data->state->a_qty + ROUNDING_ERROR < l->getMinShipment()
          || (plan_qty - data->state->a_qty < l->getMinShipment()
              && fabs(plan_qty - data->state->a_qty) > ROUNDING_ERROR))
      {
        if (plan_qty - data->state->a_qty < l->getMinShipment()
            && data->state->a_qty + ROUNDING_ERROR >= l->getMinShipment()
            && data->state->a_qty > best_a_qty )
        {
          // The remaining quantity after accepting this answer is less than
          // the minimum quantity. Therefore, we delay accepting it now, but
          // still keep track of this best offer.
          best_a_qty = data->state->a_qty;
          best_q_qty = q_qty;
          best_q_date = plan_date;
        }

        // Delete operationplans - Undo all changes
        data->rollback(topcommand);

        // Set the ask date for the next pass through the loop
        if (next_date <= copy_plan_date
          || (!data->getSolver()->getAllowSplits() && data->state->a_qty > ROUNDING_ERROR)
          || (data->state->a_qty > ROUNDING_ERROR && plan_qty - data->state->a_qty < l->getMinShipment()
              && plan_qty - data->state->a_qty > ROUNDING_ERROR))
        {
          // Oops, we didn't get a proper answer we can use for the next loop.
          // Print a warning and simply try one day later.
          if (loglevel>0)
            logger << "Warning: Demand '" << l << "': Lazy retry" << endl;
          plan_date = copy_plan_date + data->getSolver()->getLazyDelay();
        }
        else
          // Use the next-date answer from the solver
          plan_date = next_date;
      }
      else
      {
        // Accepting this answer
        if (data->state->a_qty + ROUNDING_ERROR < q_qty)
        {
          // The demand was only partially planned. We need to do a new
          // 'coordinated' planning run.

          // Delete operationplans created in the 'testing round'
          data->rollback(topcommand);

          // Create the correct operationplans
          if (loglevel>=2)
            logger << "Demand '" << l << "' plans coordination." << endl;
          data->getSolver()->setLogLevel(0);
          double tmpresult = 0;
          try
          {
            for(double remainder = data->state->a_qty;
                remainder > ROUNDING_ERROR; remainder -= data->state->a_qty)
            {
              data->state->q_qty = remainder;
              data->state->q_date = copy_plan_date;
              data->state->curDemand = const_cast<Demand*>(l);
              data->state->curBuffer = NULL;
              deliveryoper->solve(*this,v);
              if (data->state->a_qty < ROUNDING_ERROR)
              {
                logger << "Warning: Demand '" << l << "': Failing coordination" << endl;
                break;
              }
              tmpresult += data->state->a_qty;
            }
          }
          catch (...)
          {
            data->getSolver()->setLogLevel(loglevel);
            throw;
          }
          data->getSolver()->setLogLevel(loglevel);
          data->state->a_qty = tmpresult;
          if (tmpresult == 0) break;
        }

        // Register the new operationplans. We need to make sure that the
        // correct execute method is called!
        if (data->getSolver()->getAutocommit())
        {
          data->getSolver()->scanExcess(data);
          data->CommandManager::commit();
        }

        // Update the quantity to plan in the next loop
        plan_qty -= data->state->a_qty;
        best_a_qty = 0.0;  // Reset 'best-answer' remember
      }

    }
    // Repeat while there is still a quantity left to plan and we aren't
    // exceeding the maximum delivery delay.
    while (plan_qty > ROUNDING_ERROR
        && ((data->getSolver()->getPlanType() != 2 && plan_date < l->getDue() + l->getMaxLateness())
            || (data->getSolver()->getPlanType() == 2 && !data->constrainedPlanning && plan_date < l->getDue() + l->getMaxLateness())
            || (data->getSolver()->getPlanType() == 2 && data->constrainedPlanning && plan_date == l->getDue())
           ));

    // Accept the best possible answer.
    // We may have skipped it in the previous loop, awaiting a still better answer
    if (best_a_qty > 0.0 && data->constrainedPlanning)
    {
      if (loglevel>=2) logger << "Demand '" << l << "' accepts a best answer." << endl;
      data->getSolver()->setLogLevel(0);
      try
      {
        for (double remainder = best_q_qty;
          remainder > ROUNDING_ERROR && remainder > l->getMinShipment();
          remainder -= data->state->a_qty)
        {
          data->state->q_qty = remainder;
          data->state->q_date = best_q_date;
          data->state->curDemand = const_cast<Demand*>(l);
          data->state->curBuffer = NULL;
          deliveryoper->solve(*this,v);
          if (data->state->a_qty < ROUNDING_ERROR)
          {
            logger << "Warning: Demand '" << l << "': Failing accepting best answer" << endl;
            break;
          }
        }
        if (data->getSolver()->getAutocommit())
        {
          data->getSolver()->scanExcess(data);
          data->CommandManager::commit();
        }
      }
      catch (...)
      {
        data->getSolver()->setLogLevel(loglevel);
        throw;
      }
      data->getSolver()->setLogLevel(loglevel);
    }

    // Reset the state stack to the position we found it at.
    while (data->state > mystate) data->pop();

  }
  catch (...)
  {
    // Clean up if any exception happened during the planning of the demand
    while (data->state > mystate) data->pop();
    data->rollback(topcommand);
    throw;
  }
}
Beispiel #4
0
DECLARE_EXPORT void SolverMRP::chooseResource(const Load* l, void* v)   // @todo handle unconstrained plan!!!!
{
  if (!l->getSkill() && !l->getResource()->isGroup())
  {
    // CASE 1: No skill involved, and no aggregate resource either
    l->getResource()->solve(*this, v);
    return;
  }

  // CASE 2: Skill involved, or aggregate resource
  SolverMRPdata* data = static_cast<SolverMRPdata*>(v);
  unsigned int loglevel = data->getSolver()->getLogLevel();

  // Control the planning mode
  bool originalPlanningMode = data->constrainedPlanning;
  data->constrainedPlanning = true;

  // Don't keep track of the constraints right now
  bool originalLogConstraints = data->logConstraints;
  data->logConstraints = false;

  // Initialize
  Date min_next_date(Date::infiniteFuture);
  LoadPlan *lplan = data->state->q_loadplan;
  Resource *bestAlternateSelection = NULL;
  double bestCost = DBL_MAX;
  bool qualified_resource_exists = false;
  double bestAlternateValue = DBL_MAX;
  double bestAlternateQuantity = DBL_MIN;
  double beforeCost = data->state->a_cost;
  double beforePenalty = data->state->a_penalty;
  OperationPlanState originalOpplan(lplan->getOperationPlan());
  double originalLoadplanQuantity = lplan->getQuantity();
  data->getSolver()->setLogLevel(0);  // Silence during this loop

  // Loop over all candidate resources
  stack<Resource*> res_stack;
  res_stack.push(l->getResource());
  while (!res_stack.empty())
  {
    // Pick next resource
    Resource* res = res_stack.top();
    res_stack.pop();

    // If it's an aggregate, push it's members on the stack
    if (res->isGroup())
    {
      for (Resource::memberIterator x = res->beginMember(); x != Resource::end(); ++x)
        res_stack.push(&*x);
      continue;
    }

    // Check if the resource has the right skill
    if (l->getSkill())
    {
      bool isqualified = false;
      for (Resource::skilllist::const_iterator i = res->getSkills().begin();
        i != res->getSkills().end() && !isqualified; ++i)
      {
        if (i->getSkill() == l->getSkill()
            && originalOpplan.start >= i->getEffective().getStart()
            && originalOpplan.end <= i->getEffective().getEnd())
          isqualified = true;
      }
      // Next resource in the loop if not qualified
      if (!isqualified) continue;   // TODO if there is a date effective skill, we need to consider it in the reply
    }
    qualified_resource_exists = true;

    // Switch to this resource
    data->state->q_loadplan = lplan; // because q_loadplan can change!
    lplan->setResource(res);
    lplan->getOperationPlan()->restore(originalOpplan);
    data->state->q_qty = lplan->getQuantity();
    data->state->q_date = lplan->getDate();

    // Plan the resource
    CommandManager::Bookmark* topcommand = data->setBookmark();
    try { res->solve(*this,data); }
    catch (...)
    {
      data->getSolver()->setLogLevel(loglevel);
      data->constrainedPlanning = originalPlanningMode;
      data->logConstraints = originalLogConstraints;
      data->rollback(topcommand);
      throw;
    }
    data->rollback(topcommand);

    // Evaluate the result
    if (data->state->a_qty > ROUNDING_ERROR
        && lplan->getOperationPlan()->getQuantity() > 0)
    {
      double deltaCost = data->state->a_cost - beforeCost;
      double deltaPenalty = data->state->a_penalty - beforePenalty;
      // Message
      if (loglevel>1)
        logger << indent(l->getOperation()->getLevel()) << "   Operation '"
            << l->getOperation()->getName() << "' evaluates alternate '"
            << res << "': cost " << deltaCost
            << ", penalty " << deltaPenalty << endl;
      data->state->a_cost = beforeCost;
      data->state->a_penalty = beforePenalty;
      double val = 0.0;
      switch (l->getSearch())
      {
      case PRIORITY:
          val = 1; // @todo skill-resource model doesn't have a priority field yet
          break;
        case MINCOST:
          val = deltaCost / lplan->getOperationPlan()->getQuantity();
          break;
        case MINPENALTY:
          val = deltaPenalty / lplan->getOperationPlan()->getQuantity();
          break;
        case MINCOSTPENALTY:
          val = (deltaCost + deltaPenalty) / lplan->getOperationPlan()->getQuantity();
          break;
        default:
          LogicException("Unsupported search mode for alternate load");
      }
      if (val + ROUNDING_ERROR < bestAlternateValue
          || (fabs(val - bestAlternateValue) < ROUNDING_ERROR
              && lplan->getOperationPlan()->getQuantity() > bestAlternateQuantity))
      {
        // Found a better alternate
        bestAlternateValue = val;
        bestAlternateSelection = res;
        bestAlternateQuantity = lplan->getOperationPlan()->getQuantity();
      }
    }
    else if (loglevel>1)
      logger << indent(l->getOperation()->getLevel()) << "   Operation '"
          << l->getOperation()->getName() << "' evaluates alternate '"
          << lplan->getResource() << "': not available before "
          << data->state->a_date << endl;

    // Keep track of best next date
    if (data->state->a_date < min_next_date)
      min_next_date = data->state->a_date;
  }
  data->getSolver()->setLogLevel(loglevel);

  // Not a single resource has the appropriate skills. You're joking?
  if (!qualified_resource_exists)
    throw DataException("No qualified resource exists for load");

  // Restore the best candidate we found in the loop above
  if (bestAlternateSelection)
  {
    // Message
    if (loglevel>1)
      logger << indent(l->getOperation()->getLevel()) << "   Operation '"
          << l->getOperation()->getName() << "' chooses alternate '"
          << bestAlternateSelection << "' " << l->getSearch() << endl;

    // Switch back
    data->state->q_loadplan = lplan; // because q_loadplan can change!
    data->state->a_cost = beforeCost;
    data->state->a_penalty = beforePenalty;
    if (lplan->getResource() != bestAlternateSelection)
      lplan->setResource(bestAlternateSelection);
    lplan->getOperationPlan()->restore(originalOpplan);
    data->state->q_qty = lplan->getQuantity();
    data->state->q_date = lplan->getDate();
    bestAlternateSelection->solve(*this,data);

    // Restore the planning mode
    data->constrainedPlanning = originalPlanningMode;
    data->logConstraints = originalLogConstraints;
    return;
  }

  // 7) No alternate gave a good result
  data->state->a_date = min_next_date;
  data->state->a_qty = 0;

  // Restore the planning mode
  data->constrainedPlanning = originalPlanningMode;

  // Maintain the constraint list
  if (originalLogConstraints)
  {
    data->planningDemand->getConstraints().push(
      ProblemCapacityOverload::metadata,
      l->getResource(), originalOpplan.start, originalOpplan.end,
      -originalLoadplanQuantity);
  }
  data->logConstraints = originalLogConstraints;

  if (loglevel>1)
    logger << indent(lplan->getOperationPlan()->getOperation()->getLevel()) <<
        "   Alternate load doesn't find supply on any alternate : "
        << data->state->a_qty << "  " << data->state->a_date << endl;
}
Beispiel #5
0
void SolverMRP::solve(const Load* l, void* v)
{
  // Note: This method is only called for decrease loadplans and for the leading
  // load of an alternate group. See SolverMRP::checkOperation
  SolverMRPdata* data = static_cast<SolverMRPdata*>(v);
  unsigned int loglevel = data->getSolver()->getLogLevel();

  if (data->state->q_qty >= 0.0)
  {
    // The loadplan is an increase in size, and the resource solver only needs
    // the decreases.
    data->state->a_qty = data->state->q_qty;
    data->state->a_date = data->state->q_date;
    return;
  }

  if (!l->hasAlternates() && !l->getAlternate())
  {
    // CASE I: It is not an alternate load.
    // Delegate the answer immediately to the resource
    chooseResource(l, data);
    return;
  }

  // CASE II: It is an alternate load.
  // We ask each alternate load in order of priority till we find a load
  // that has a non-zero reply.

  // 1) collect a list of alternates
  list<const Load*> thealternates;
  const Load *x = l->hasAlternates() ? l : l->getAlternate();
  SearchMode search = l->getSearch();
  for (Operation::loadlist::const_iterator i = l->getOperation()->getLoads().begin();
      i != l->getOperation()->getLoads().end(); ++i)
    if ((i->getAlternate() == x || &*i == x)
        && i->getEffective().within(data->state->q_loadplan->getDate()))
      thealternates.push_back(&*i);

  // 2) Sort the list
  thealternates.sort(sortLoad);  // @todo cpu-intensive - better is to maintain the list in the correct order

  // 3) Control the planning mode
  bool originalPlanningMode = data->constrainedPlanning;
  data->constrainedPlanning = true;

  // Don't keep track of the constraints right now
  bool originalLogConstraints = data->logConstraints;
  data->logConstraints = false;

  // 4) Loop through all alternates or till we find a non-zero
  // reply (priority search)
  Date min_next_date(Date::infiniteFuture);
  LoadPlan *lplan = data->state->q_loadplan;
  double bestAlternateValue = DBL_MAX;
  double bestAlternateQuantity = DBL_MIN;
  const Load* bestAlternateSelection = NULL;
  double beforeCost = data->state->a_cost;
  double beforePenalty = data->state->a_penalty;
  OperationPlanState originalOpplan(lplan->getOperationPlan());
  double originalLoadplanQuantity = data->state->q_loadplan->getQuantity();
  for (list<const Load*>::const_iterator i = thealternates.begin();
      i != thealternates.end();)
  {
    const Load *curload = *i;
    data->state->q_loadplan = lplan; // because q_loadplan can change!

    // 4a) Switch to this load
    if (lplan->getLoad() != curload) lplan->setLoad(curload);
    lplan->getOperationPlan()->restore(originalOpplan);
    data->state->q_qty = lplan->getQuantity();
    data->state->q_date = lplan->getDate();

    // 4b) Ask the resource
    // TODO XXX Need to insert another loop here! It goes over all resources qualified for the required skill.
    // The qualified resources need to be sorted based on their cost. If the cost is the same we should use a decent tie breaker, eg number of skills or number of loads.
    // The first resource with the qualified skill that is available will be used.
    CommandManager::Bookmark* topcommand = data->setBookmark();
    if (search == PRIORITY)
      curload->getResource()->solve(*this,data);
    else
    {
      data->getSolver()->setLogLevel(0);
      try {curload->getResource()->solve(*this,data);}
      catch (...)
      {
        data->getSolver()->setLogLevel(loglevel);
        // Restore the planning mode
        data->constrainedPlanning = originalPlanningMode;
        data->logConstraints = originalLogConstraints;
        throw;
      }
      data->getSolver()->setLogLevel(loglevel);
    }

    // 4c) Evaluate the result
    if (data->state->a_qty > ROUNDING_ERROR
        && lplan->getOperationPlan()->getQuantity() > 0)
    {
      if (search == PRIORITY)
      {
        // Priority search: accept any non-zero reply
        // Restore the planning mode
        data->constrainedPlanning = originalPlanningMode;
        data->logConstraints = originalLogConstraints;
        return;
      }
      else
      {
        // Other search modes: evaluate all
        double deltaCost = data->state->a_cost - beforeCost;
        double deltaPenalty = data->state->a_penalty - beforePenalty;
        // Message
        if (loglevel>1 && search != PRIORITY)
          logger << indent(l->getOperation()->getLevel()) << "   Operation '"
              << l->getOperation()->getName() << "' evaluates alternate '"
              << curload->getResource() << "': cost " << deltaCost
              << ", penalty " << deltaPenalty << endl;
        if (deltaCost < ROUNDING_ERROR && deltaPenalty < ROUNDING_ERROR)
        {
          // Zero cost and zero penalty on this alternate. It won't get any better
          // than this, so we accept this alternate.
          if (loglevel>1)
            logger << indent(l->getOperation()->getLevel()) << "   Operation '"
                << l->getOperation()->getName() << "' chooses alternate '"
                << curload->getResource() << "' " << search << endl;
          // Restore the planning mode
          data->constrainedPlanning = originalPlanningMode;
          data->logConstraints = originalLogConstraints;
          return;
        }
        data->state->a_cost = beforeCost;
        data->state->a_penalty = beforePenalty;
        double val = 0.0;
        switch (search)
        {
          case MINCOST:
            val = deltaCost / lplan->getOperationPlan()->getQuantity();
            break;
          case MINPENALTY:
            val = deltaPenalty / lplan->getOperationPlan()->getQuantity();
            break;
          case MINCOSTPENALTY:
            val = (deltaCost + deltaPenalty) / lplan->getOperationPlan()->getQuantity();
            break;
          default:
            LogicException("Unsupported search mode for alternate load");
        }
        if (val + ROUNDING_ERROR < bestAlternateValue
            || (fabs(val - bestAlternateValue) < ROUNDING_ERROR
                && lplan->getOperationPlan()->getQuantity() > bestAlternateQuantity))
        {
          // Found a better alternate
          bestAlternateValue = val;
          bestAlternateSelection = curload;
          bestAlternateQuantity = lplan->getOperationPlan()->getQuantity();
        }
      }
    }
    else if (loglevel>1 && search != PRIORITY)
      logger << indent(l->getOperation()->getLevel()) << "   Operation '"
          << l->getOperation()->getName() << "' evaluates alternate '"
          << curload->getResource() << "': not available before "
          << data->state->a_date << endl;

    // 4d) Undo the plan on the alternate
    data->rollback(topcommand);

    // 4e) Prepare for the next alternate
    if (data->state->a_date < min_next_date)
      min_next_date = data->state->a_date;
    if (++i != thealternates.end() && loglevel>1 && search == PRIORITY)
      logger << indent(curload->getOperation()->getLevel()) << "   Alternate load switches from '"
          << curload->getResource()->getName() << "' to '"
          << (*i)->getResource()->getName() << "'" << endl;
  }

  // 5) Unconstrained plan: plan on the first alternate
  if (!originalPlanningMode && !(search != PRIORITY && bestAlternateSelection))
  {
    // Switch to unconstrained planning
    data->constrainedPlanning = false;
    bestAlternateSelection = *(thealternates.begin());
  }

  // 6) Finally replan on the best alternate
  if (!originalPlanningMode || (search != PRIORITY && bestAlternateSelection))
  {
    // Message
    if (loglevel>1)
      logger << indent(l->getOperation()->getLevel()) << "   Operation '"
          << l->getOperation()->getName() << "' chooses alternate '"
          << bestAlternateSelection->getResource() << "' " << search << endl;

    // Switch back
    data->state->q_loadplan = lplan; // because q_loadplan can change!
    data->state->a_cost = beforeCost;
    data->state->a_penalty = beforePenalty;
    if (lplan->getLoad() != bestAlternateSelection)
      lplan->setLoad(bestAlternateSelection);
    lplan->getOperationPlan()->restore(originalOpplan);
    // TODO XXX need to restore also the selected resource with the right skill!
    data->state->q_qty = lplan->getQuantity();
    data->state->q_date = lplan->getDate();
    bestAlternateSelection->getResource()->solve(*this,data);

    // Restore the planning mode
    data->constrainedPlanning = originalPlanningMode;
    data->logConstraints = originalLogConstraints;
    return;
  }

  // 7) No alternate gave a good result
  data->state->a_date = min_next_date;
  data->state->a_qty = 0;

  // Restore the planning mode
  data->constrainedPlanning = originalPlanningMode;

  // Maintain the constraint list
  if (originalLogConstraints)
  {
    const Load *primary = *(thealternates.begin());
    data->planningDemand->getConstraints().push(
      ProblemCapacityOverload::metadata,
      primary->getResource(), originalOpplan.start, originalOpplan.end,
      -originalLoadplanQuantity);
  }
  data->logConstraints = originalLogConstraints;

  if (loglevel>1)
    logger << indent(lplan->getOperationPlan()->getOperation()->getLevel()) <<
        "   Alternate load doesn't find supply on any alternate : "
        << data->state->a_qty << "  " << data->state->a_date << endl;
}
Beispiel #6
0
void SolverMRP::solveSafetyStock(const Buffer* b, void* v)
{
  SolverMRPdata* data = static_cast<SolverMRPdata*>(v);

  // Message
  if (data->getSolver()->getLogLevel()>1)
    logger << indent(b->getLevel()) << "  Buffer '" << b->getName()
        << "' replenishes for safety stock" << endl;

  // Scan the complete horizon
  Date currentDate;
  const TimeLine<FlowPlan>::Event *prev = nullptr;
  double shortage(0.0);
  double current_minimum(0.0);
  Buffer::flowplanlist::const_iterator cur=b->getFlowPlans().begin();
  while (true)
  {
    // Iterator has now changed to a new date or we have arrived at the end.
    // If multiple flows are at the same moment in time, we are not interested
    // in the inventory changes. It gets interesting only when a certain
    // inventory level remains unchanged for a certain time.
    if ((cur == b->getFlowPlans().end() || cur->getDate()>currentDate) && prev)
    {
      // Some variables
      double theOnHand = prev->getOnhand();
      double theDelta = theOnHand - current_minimum + shortage;
      bool loop = true;

      // Evaluate the situation at the last flowplan before the date change.
      // Is there a shortage at that date?
      Date nextAskDate;
      while (theDelta < -ROUNDING_ERROR && b->getProducingOperation() && loop)
      {
        // Create supply
        data->state->curBuffer = const_cast<Buffer*>(b);
        data->state->q_qty = -theDelta;
        data->state->q_date = nextAskDate ? nextAskDate : prev->getDate();

        // Make sure the new operationplans don't inherit an owner.
        // When an operation calls the solve method of suboperations, this field is
        // used to pass the information about the owner operationplan down. When
        // solving for buffers we must make sure NOT to pass owner information.
        // At the end of solving for a buffer we need to restore the original
        // settings...
        data->state->curOwnerOpplan = nullptr;

        // Note that the supply created with the next line changes the
        // onhand value at all later dates!
        CommandManager::Bookmark* topcommand = data->setBookmark();
        b->getProducingOperation()->solve(*this,v);

        if (data->state->a_qty > ROUNDING_ERROR)
          // If we got some extra supply, we retry to get some more supply.
          // Only when no extra material is obtained, we give up.
          theDelta += data->state->a_qty;
        else
        {
          data->rollback(topcommand);
          if ( (cur != b->getFlowPlans().end() && data->state->a_date < cur->getDate())
            || (cur == b->getFlowPlans().end() && data->state->a_date < Date::infiniteFuture) )
              nextAskDate = data->state->a_date;
          else
              loop = false;
        }
      }
    }

    // We have reached the end of the flowplans. Breaking out of the loop
    // needs to be done here because in the next statements we are accessing
    // *cur, which isn't valid at the end of the list
    if (cur == b->getFlowPlans().end()) break;

    // The minimum or the maximum have changed
    // Note that these limits can be updated only after the processing of the
    // date change in the statement above. Otherwise the code above would
    // already use the new value before the intended date.
    if (cur->getEventType() == 3) current_minimum = cur->getMin();

    // Update the pointer to the previous flowplan.
    prev = &*cur;
    currentDate = cur->getDate();
    ++cur;
  }

  // Message
  if (data->getSolver()->getLogLevel()>1)
    logger << indent(b->getLevel()) << "  Buffer '" << b->getName()
        << "' solved for safety stock" << endl;
}
Beispiel #7
0
/** @todo The flow quantity is handled at the wrong place. It needs to be
  * handled by the operation, since flows can exist on multiple suboperations
  * with different quantities. The buffer solve can't handle this, because
  * it only calls the solve() for the producing operation...
  * Are there some situations where the operation solver doesn't know enough
  * on the buffer behavior???
  */
void SolverMRP::solve(const Buffer* b, void* v)
{
  // Call the user exit
  SolverMRPdata* data = static_cast<SolverMRPdata*>(v);
  if (userexit_buffer) userexit_buffer.call(b, PythonData(data->constrainedPlanning));

  // Verify the iteration limit isn't exceeded.
  if (data->getSolver()->getIterationMax()
    && ++data->iteration_count > data->getSolver()->getIterationMax())
  {
    ostringstream ch;
    ch << "Maximum iteration count " << data->getSolver()->getIterationMax() << " exceeded";
    throw RuntimeException(ch.str());
  }

  // Safety stock planning is refactored to a separate method
  double requested_qty(data->state->q_qty);
  if (requested_qty == -1.0)
  {
    solveSafetyStock(b,v);
    return;
  }
  Date requested_date(data->state->q_date);
  bool tried_requested_date(false);

  // Message
  if (data->getSolver()->getLogLevel()>1)
    logger << indent(b->getLevel()) << "  Buffer '" << b->getName()
      << "' is asked: " << data->state->q_qty << "  " << data->state->q_date << endl;

  // Store the last command in the list, in order to undo the following
  // commands if required.
  CommandManager::Bookmark* topcommand = data->setBookmark();
  OperationPlan *prev_owner_opplan = data->state->curOwnerOpplan;

  // Evaluate the buffer profile and solve shortages by asking more material.
  // The loop goes from the requested date till the very end. Whenever the
  // event date changes, we evaluate if a shortage exists.
  Date currentDate;
  const TimeLine<FlowPlan>::Event *prev = nullptr;
  double shortage(0.0);
  Date extraSupplyDate(Date::infiniteFuture);
  Date extraInventoryDate(Date::infiniteFuture);
  double cumproduced = (b->getFlowPlans().rbegin() == b->getFlowPlans().end())
    ? 0
    : b->getFlowPlans().rbegin()->getCumulativeProduced();
  double current_minimum(0.0);
  double unconfirmed_supply(0.0);
  for (Buffer::flowplanlist::const_iterator cur=b->getFlowPlans().begin();
      ; ++cur)
  {
    if(&*cur && cur->getEventType() == 1)
    {
      const FlowPlan* fplan = static_cast<const FlowPlan*>(&*cur);
      if (!fplan->getOperationPlan()->getRawIdentifier()
        && fplan->getQuantity()>0
        && fplan->getOperationPlan()->getOperation() != b->getProducingOperation())
          unconfirmed_supply += fplan->getQuantity();
    }

    // Iterator has now changed to a new date or we have arrived at the end.
    // If multiple flows are at the same moment in time, we are not interested
    // in the inventory changes. It gets interesting only when a certain
    // inventory level remains unchanged for a certain time.
    if ((cur == b->getFlowPlans().end() || cur->getDate()>currentDate) && prev)
    {
      // Some variables
      Date theDate = prev->getDate();
      double theOnHand = prev->getOnhand();
      double theDelta = theOnHand - current_minimum + shortage;

      // Evaluate the situation at the last flowplan before the date change.
      // Is there a shortage at that date?
      // We have 3 ways to resolve it:
      //  - Scan backward for a producer we can combine with to make a
      //    single batch.
      //  - Scan forward for producer we can replace in a single batch.
      //  - Create new supply for the shortage at that date.

      // Solution one: we scan backward in time for producers we can merge with.
      if (theDelta < -ROUNDING_ERROR
        && b->getMinimumInterval() >= 0L
        && prev
        && prev->getDate() >= theDate - b->getMinimumInterval())
      {
        Operation *prevOper = nullptr;
        DateRange prevDates;
        double prevQty = 0.0;
        Buffer::flowplanlist::const_iterator prevbatchiter = b->getFlowPlans().end();
        for (Buffer::flowplanlist::const_iterator batchiter = prev;
          batchiter != b->getFlowPlans().end() && batchiter->getDate() >= theDate - b->getMinimumInterval();
          prevbatchiter = batchiter--)
        {
          // Check if it is an unlocked producing operationplan
          if (batchiter->getQuantity() <= 0) continue;
          const FlowPlan* batchcandidate = nullptr;
          if (batchiter->getEventType() == 1)
            batchcandidate = static_cast<const FlowPlan*>(&*batchiter);
          if (!batchcandidate || batchcandidate->getOperationPlan()->getLocked())
            continue;

          // Store date and quantity of the candidate
          Date batchdate = batchcandidate->getDate();
          double batchqty = batchcandidate->getOperationPlan()->getTotalFlow(b) - theDelta;
          double consumed_in_window = b->getFlowPlans().getFlow(batchcandidate, b->getMinimumInterval(), true);
          if (batchqty > consumed_in_window)
            batchqty = consumed_in_window;
          Operation* candidate_operation = batchcandidate->getOperationPlan()->getOperation();
          DateRange candidate_dates = batchcandidate->getOperationPlan()->getDates();
          double candidate_qty = batchcandidate->getOperationPlan()->getQuantity();

          // Verify we haven't tried the same kind of candidate before
          if (candidate_operation == prevOper
            && candidate_dates == prevDates
            && fabs(candidate_qty - prevQty) < ROUNDING_ERROR)
              continue;
          prevOper = candidate_operation;
          prevDates = candidate_dates;
          prevQty = candidate_qty;

          // Delete existing producer, and propagate the deletion upstream
          CommandManager::Bookmark* batchbookmark = data->setBookmark();
          data->operator_delete->solve(batchcandidate->getOperationPlan());

          // Create new producer
          short loglevel = data->getSolver()->getLogLevel();
          try
          {
            data->getSolver()->setLogLevel(0);
            data->state->curBuffer = const_cast<Buffer*>(b);
            data->state->q_qty = batchqty;
            // We need to add the post-operation time, because the operation
            // solver will subtract it again. For merging operationaplans we
            // want to plan *exactly* at the date of the existing operationplan.
            data->state->q_date =
              batchdate + b->getProducingOperation()->getPostTime();
            data->state->curOwnerOpplan = nullptr;
            b->getProducingOperation()->solve(*this, v);
          }
          catch (...)
          {
            data->getSolver()->setLogLevel(loglevel);
            throw;
          }
          data->getSolver()->setLogLevel(loglevel);

          // Check results
          if (data->state->a_qty < batchqty - ROUNDING_ERROR)
          {
            // It didn't work.
            if (loglevel > 1)
              logger << indent(b->getLevel())
                << "  Rejected resized batch '" << candidate_operation
                << "' " << candidate_dates << " " << candidate_qty << endl;
            data->rollback(batchbookmark);
            // Assure batchiter remains valid
            batchiter = prevbatchiter;
            if (batchiter != b->getFlowPlans().end())
              --batchiter;
          }
          else
          {
            // It worked.
            if (loglevel > 1)
              logger << indent(b->getLevel())
                << "  Accepting resized batch '" << candidate_operation
                << "' " << candidate_dates << " " << candidate_qty << endl;

            theDelta = 0.0;
            break;
          }
          // Assure the prev pointer remains valid after this loop
          Buffer::flowplanlist::const_iterator c = cur;
          --c;
          if (c == b->getFlowPlans().end())
          {
            c = b->getFlowPlans().rbegin();
            if (c == b->getFlowPlans().end())
              prev = nullptr;
            else
              prev = &*c;
          }
          else
            prev = &*c;
        }
      }

      // Solution two: we scan forward in time for producers we can replace.
      if (theDelta < -ROUNDING_ERROR
        && b->getMinimumInterval() >= 0L
        && cur != b->getFlowPlans().end()
        && cur->getDate() <= theDate + b->getMinimumInterval())
      {
        Operation *prevOper = nullptr;
        DateRange prevDates;
        double prevQty = 0.0;
        Buffer::flowplanlist::const_iterator prevbatchiter = b->getFlowPlans().end();
        for (Buffer::flowplanlist::const_iterator batchiter = cur;
          batchiter != b->getFlowPlans().end() && batchiter->getDate() <= theDate + b->getMinimumInterval();
          prevbatchiter = batchiter++)
        {
          // Check if it is an unlocked producing operationplan
          if (batchiter->getQuantity() <= 0) continue;
          const FlowPlan* batchcandidate = nullptr;
          if (batchiter->getEventType() == 1)
            batchcandidate = static_cast<const FlowPlan*>(&*batchiter);
          if (!batchcandidate || batchcandidate->getOperationPlan()->getLocked())
            continue;

          // Store date and quantity of the candidate
          double batchqty = batchcandidate->getQuantity()- theDelta;
          double consumed_in_window = b->getFlowPlans().getFlow(prev, b->getMinimumInterval(), true);
          if (batchqty > consumed_in_window)
            batchqty = consumed_in_window;
          Operation* candidate_operation = batchcandidate->getOperationPlan()->getOperation();
          DateRange candidate_dates = batchcandidate->getOperationPlan()->getDates();
          double candidate_qty = batchcandidate->getOperationPlan()->getQuantity();

          // Verify we haven't tried the same kind of candidate before
          if (candidate_operation == prevOper
            && prevDates == candidate_dates
            && fabs(candidate_qty - prevQty) < ROUNDING_ERROR)
              continue;
          prevOper = candidate_operation;
          prevDates = candidate_dates;
          prevQty = candidate_qty;

          // Delete existing producer, and propagate the deletion upstream
          CommandManager::Bookmark* batchbookmark = data->setBookmark();
          data->operator_delete->solve(batchcandidate->getOperationPlan());

          // Create new producer
          short loglevel = data->getSolver()->getLogLevel();
          try
          {
            data->getSolver()->setLogLevel(0);
            data->state->curBuffer = const_cast<Buffer*>(b);
            data->state->q_qty = batchqty;
            data->state->q_date = theDate;
            data->state->curOwnerOpplan = nullptr;
            b->getProducingOperation()->solve(*this, v);
          }
          catch (...)
          {
            data->getSolver()->setLogLevel(loglevel);
            throw;
          }
          data->getSolver()->setLogLevel(loglevel);

          // Check results
          if (data->state->a_qty < batchqty - ROUNDING_ERROR)
          {
            // It didn't work.
            if (loglevel > 1)
              logger << indent(b->getLevel())
                << "  Rejected joining batch with '" << candidate_operation
                << "' " << candidate_dates << " " << candidate_qty << endl;
            data->rollback(batchbookmark);
            // Assure batchiter remains valid
            batchiter = prevbatchiter;
            if (batchiter != b->getFlowPlans().end())
              ++batchiter;
          }
          else
          {
            // It worked.
            if (loglevel > 1)
              logger << indent(b->getLevel())
                << "  Accepted joining batch with '" << candidate_operation
                << "' " << candidate_dates << " " << candidate_qty << endl;
            theDelta = 0.0;
            // Assure the cur iterator remains valid after this loop
            cur = prev;
            if (cur != b->getFlowPlans().end())
              ++cur;
            break;
          }
          // Assure the cur iterator remains valid after this loop
          cur = prev;
          if (cur != b->getFlowPlans().end())
            ++cur;
        }
      }

      // Solution three: create supply at the shortage date itself
      if (theDelta < -ROUNDING_ERROR)
      {
        // Can we get extra supply to solve the problem, or part of it?
        // If the shortage already starts before the requested date, it
        // was not created by the newly added flowplan, but existed before.
        // We don't consider this as a shortage for the current flowplan,
        // and we want our flowplan to try to repair the previous problems
        // if it can...
        bool loop = true;
        while (b->getProducingOperation() && theDate >= requested_date && loop)
        {
          // Create supply
          data->state->curBuffer = const_cast<Buffer*>(b);
          data->state->q_qty = -theDelta;
          data->state->q_date = theDate;

          // Check whether this date doesn't match with the requested date.
          // See a bit further why this is required.
          if (data->state->q_date == requested_date) tried_requested_date = true;

          // Make sure the new operationplans don't inherit an owner.
          // When an operation calls the solve method of suboperations, this field is
          // used to pass the information about the owner operationplan down. When
          // solving for buffers we must make sure NOT to pass owner information.
          // At the end of solving for a buffer we need to restore the original
          // settings...
          data->state->curOwnerOpplan = nullptr;

          // Note that the supply created with the next line changes the
          // onhand value at all later dates!
          b->getProducingOperation()->solve(*this,v);

          // Evaluate the reply date. The variable extraSupplyDate will store
          // the date when the producing operation tells us it can get extra
          // supply.
          if (data->state->a_date < extraSupplyDate
              && data->state->a_date > requested_date)
            extraSupplyDate = data->state->a_date;

          // If we got some extra supply, we retry to get some more supply.
          // Only when no extra material is obtained, we give up.
          // When solving for safety stock or when the parameter allowsplit is
          // set to false we need to get a single replenishing operationplan.
          if (data->state->a_qty > ROUNDING_ERROR
              && data->state->a_qty < -theDelta - ROUNDING_ERROR
              && ((data->getSolver()->getAllowSplits() && !data->safety_stock_planning)
               || data->state->a_qty == b->getProducingOperation()->getSizeMaximum())
             )
            theDelta += data->state->a_qty;
          else
            loop = false;
        }

        // Not enough supply was received to repair the complete problem
        if (prev && prev->getOnhand() + shortage < -ROUNDING_ERROR)
        {
          // Keep track of the shorted quantity.
          // Only consider shortages later than the requested date.
          if (theDate >= requested_date)
            shortage = -prev->getOnhand();

          // Reset the date from which excess material is in the buffer. This
          // excess material can be used to compute the date when the buffer
          // can be asked again for additional supply.
          extraInventoryDate = Date::infiniteFuture;
        }
      }
      else if (theDelta > unconfirmed_supply + ROUNDING_ERROR)
        // There is excess material at this date (coming from planned/frozen
        // material arrivals, surplus material created by lotsized operations,
        // etc...)
        // The unconfirmed_supply element is required to exclude any of the
        // excess inventory we may have caused ourselves. Such situations are
        // possible when there are loops in the supply chain.
        if (theDate > requested_date
            && extraInventoryDate == Date::infiniteFuture)
          extraInventoryDate = theDate;
    }

    // We have reached the end of the flowplans. Breaking out of the loop
    // needs to be done here because in the next statements we are accessing
    // *cur, which isn't valid at the end of the list
    if (cur == b->getFlowPlans().end()) break;

    // The minimum has changed.
    // Note that these limits can be updated only after the processing of the
    // date change in the statement above. Otherwise the code above would
    // already use the new value before the intended date.
    // If the flag getPlanSafetyStockFirst is set, then we need to replenish
    // up to the minimum quantity. If it is not set (which is the default) then
    // we only replenish up to 0.
    if (cur->getEventType() == 3 && (getPlanSafetyStockFirst() || data->safety_stock_planning))
      current_minimum = cur->getMin();

    // Update the pointer to the previous flowplan.
    prev = &*cur;
    currentDate = cur->getDate();
  }

  // Note: the variable extraInventoryDate now stores the date from which
  // excess material is available in the buffer. The excess
  // We don't need to care how much material is lying there.

  // Check for supply at the requested date
  // Isn't this included in the normal loop? In some cases it is indeed, but
  // sometimes it isn't because in the normal loop there may still have been
  // onhand available and the shortage only shows at a later date than the
  // requested date.
  // E.g. Initial situation:              After extra consumer at time y:
  //      -------+                                --+
  //             |                                  |
  //             +------                            +---+
  //                                                    |
  //    0 -------y------                        0 --y---x-----
  //                                                    |
  //                                                    +-----
  // The first loop only checks for supply at times x and later. If it is not
  // feasible, we now check for supply at time y. It will create some extra
  // inventory, but at least the demand is met.
  // @todo The buffer solver could move backward in time from x till time y,
  // and try multiple dates. This would minimize the excess inventory created.
  while (shortage > ROUNDING_ERROR
      && b->getProducingOperation() && !tried_requested_date)
  {
    // Create supply at the requested date
    data->state->curBuffer = const_cast<Buffer*>(b);
    data->state->q_qty = shortage;
    data->state->q_date = requested_date;
    // Make sure the new operationplans don't inherit an owner.
    // When an operation calls the solve method of suboperations, this field is
    // used to pass the information about the owner operationplan down. When
    // solving for buffers we must make sure NOT to pass owner information.
    // At the end of solving for a buffer we need to restore the original
    // settings...
    data->state->curOwnerOpplan = nullptr;
    // Note that the supply created with the next line changes the onhand value
    // at all later dates!
    // Note that asking at the requested date doesn't keep the material on
    // stock to a minimum.
    if (requested_qty - shortage < ROUNDING_ERROR)
      data->rollback(topcommand);
    b->getProducingOperation()->solve(*this,v);
    // Evaluate the reply
    if (data->state->a_date < extraSupplyDate
        && data->state->a_date > requested_date)
      extraSupplyDate = data->state->a_date;
    if (data->state->a_qty > ROUNDING_ERROR)
      shortage -= data->state->a_qty;
    else
      tried_requested_date = true;
  }

  // Final evaluation of the replenishment
  if (data->constrainedPlanning && data->getSolver()->isConstrained())
  {
    // Use the constrained planning result
    data->state->a_qty = requested_qty - shortage;
    if (data->state->a_qty < ROUNDING_ERROR)
    {
      data->rollback(topcommand);
      data->state->a_qty = 0.0;
    }
    data->state->a_date = (extraInventoryDate < extraSupplyDate) ?
        extraInventoryDate :
        extraSupplyDate;
    // Monitor as a constraint if there is no producing operation.
    // Note that if there is a producing operation the constraint is flagged
    // on the operation instead of on this buffer.
    if (!b->getProducingOperation() && data->logConstraints && shortage > ROUNDING_ERROR && data->planningDemand)
      data->planningDemand->getConstraints().push(ProblemMaterialShortage::metadata,
          b, requested_date, Date::infiniteFuture, shortage);
  }
  else
  {
    // Enough inventory or supply available, or not material constrained.
    // In case of a plan that is not material constrained, the buffer tries to
    // solve for shortages as good as possible. Only in the end we 'lie' about
    // the result to the calling function. Material shortages will then remain
    // in the buffer.
    data->state->a_qty = requested_qty;
    data->state->a_date = Date::infiniteFuture;
  }

  // Restore the owning operationplan.
  data->state->curOwnerOpplan = prev_owner_opplan;

  // Reply quantity must be greater than 0
  assert( data->state->a_qty >= 0 );

  // Increment the cost
  // Only the quantity consumed directly from the buffer is counted.
  // The cost of the material supply taken from producing operations is
  // computed seperately and not considered here.
  if (b->getItem() && data->state->a_qty > 0)
  {
    if (b->getFlowPlans().empty())
      cumproduced = 0.0;
    else
      cumproduced = b->getFlowPlans().rbegin()->getCumulativeProduced() - cumproduced;
    if (data->state->a_qty > cumproduced)
      data->state->a_cost += (data->state->a_qty - cumproduced) * b->getItem()->getPrice();
  }

  // Message
  if (data->getSolver()->getLogLevel()>1)
    logger << indent(b->getLevel()) << "  Buffer '" << b->getName()
        << "' answers: " << data->state->a_qty << "  " << data->state->a_date << "  "
        << data->state->a_cost << "  " << data->state->a_penalty << endl;
}