DECLARE_EXPORT void Load::validate(Action action) { // Catch null operation and resource pointers Operation *oper = getOperation(); Resource *res = getResource(); if (!oper || !res) { // Invalid load model if (!oper && !res) throw DataException("Missing operation and resource on a load"); else if (!oper) throw DataException("Missing operation on a load on resource '" + res->getName() + "'"); else if (!res) throw DataException("Missing resource on a load on operation '" + oper->getName() + "'"); } // Check if a load with 1) identical resource, 2) identical operation and // 3) overlapping effectivity dates already exists Operation::loadlist::const_iterator i = oper->getLoads().begin(); for (; i != oper->getLoads().end(); ++i) if (i->getResource() == res && i->getEffective().overlap(getEffective()) && &*i != this) break; // Apply the appropriate action switch (action) { case ADD: if (i != oper->getLoads().end()) { throw DataException("Load of '" + oper->getName() + "' and '" + res->getName() + "' already exists"); } break; case CHANGE: throw DataException("Can't update a load"); case ADD_CHANGE: // ADD is handled in the code after the switch statement if (i == oper->getLoads().end()) break; throw DataException("Can't update a load"); case REMOVE: // This load was only used temporarily during the reading process delete this; if (i == oper->getLoads().end()) // Nothing to delete throw DataException("Can't remove nonexistent load of '" + oper->getName() + "' and '" + res->getName() + "'"); delete &*i; // Set a flag to make sure the level computation is triggered again HasLevel::triggerLazyRecomputation(); return; } // The statements below should be executed only when a new load is created. // Set a flag to make sure the level computation is triggered again HasLevel::triggerLazyRecomputation(); }
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; }