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; }
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; } } }
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; } }
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; }
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; }
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; }
/** @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; }