solveroperation.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  * *
3  * Copyright (C) 2007-2012 by Johan De Taeye, frePPLe bvba *
4  * *
5  * This library is free software; you can redistribute it and/or modify it *
6  * under the terms of the GNU Affero General Public License as published *
7  * by the Free Software Foundation; either version 3 of the License, or *
8  * (at your option) any later version. *
9  * *
10  * This library is distributed in the hope that it will be useful, *
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of *
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
13  * GNU Affero General Public License for more details. *
14  * *
15  * You should have received a copy of the GNU Affero General Public *
16  * License along with this program. *
17  * If not, see <http://www.gnu.org/licenses/>. *
18  * *
19  ***************************************************************************/
20 
21 #define FREPPLE_CORE
22 #include "frepple/solver.h"
23 namespace frepple
24 {
25 
26 
29 {
30  unsigned short constrainedLoads = 0;
32  h!=opplan->endLoadPlans(); ++h)
33  if (h->getResource()->getType() != *(ResourceInfinite::metadata)
34  && h->isStart() && h->getLoad()->getQuantity() != 0.0)
35  {
36  if (++constrainedLoads > 1) break;
37  }
38  DateRange orig;
39  Date minimumEndDate = opplan->getDates().getEnd();
40  bool backuplogconstraints = data.logConstraints;
41  bool backupForceLate = data.state->forceLate;
42  bool recheck, first;
43  double loadqty = 1.0;
44 
45  // Loop through all loadplans, and solve for the resource.
46  // This may move an operationplan early or late.
47  do
48  {
49  orig = opplan->getDates();
50  recheck = false;
51  first = true;
53  h!=opplan->endLoadPlans() && opplan->getDates()==orig; ++h)
54  {
55  if (h->getLoad()->getQuantity() == 0.0 || h->getQuantity() == 0.0)
56  // Empty load or loadplan (eg when load is not effective)
57  continue;
58  // Call the load solver - which will call the resource solver.
59  data.state->q_operationplan = opplan;
60  data.state->q_loadplan = &*h;
61  data.state->q_qty = h->getQuantity();
62  loadqty = h->getQuantity();
63  data.state->q_date = h->getDate();
64  h->getLoad()->solve(*this,&data);
65  if (opplan->getDates()!=orig)
66  {
67  if (data.state->a_qty==0)
68  // One of the resources is late. We want to prevent that other resources
69  // are trying to pull in the operationplan again. It can only be delayed
70  // from now on in this loop.
71  data.state->forceLate = true;
72  if (!first) recheck = true;
73  }
74  first = false;
75  }
76  data.logConstraints = false; // Only first loop collects constraint info
77  }
78  // Imagine there are multiple loads. As soon as one of them is moved, we
79  // need to redo the capacity check for the ones we already checked.
80  // Repeat until no load has touched the opplan, or till proven infeasible.
81  // No need to reloop if there is only a single load (= 2 loadplans)
82  while (constrainedLoads>1 && opplan->getDates()!=orig
83  && ((data.state->a_qty==0.0 && data.state->a_date > minimumEndDate)
84  || recheck));
85  // TODO doesn't this loop increment a_penalty incorrectly???
86 
87  // Restore original flags
88  data.logConstraints = backuplogconstraints; // restore the original value
89  data.state->forceLate = backupForceLate;
90 
91  // In case of a zero reply, we resize the operationplan to 0 right away.
92  // This is required to make sure that the buffer inventory profile also
93  // respects this answer.
94  if (data.state->a_qty==0.0 && opplan->getQuantity() > 0.0)
95  opplan->setQuantity(0.0);
96 }
97 
98 
101 {
102  // The default answer...
103  data.state->a_date = Date::infiniteFuture;
104  data.state->a_qty = data.state->q_qty;
105 
106  // Handle unavailable time.
107  // Note that this unavailable time is checked also in an unconstrained plan.
108  // This means that also an unconstrained plan can plan demand late!
109  if (opplan->getQuantity() == 0.0)
110  {
111  // It is possible that the operation could not be created properly.
112  // This happens when the operation is not available for enough time.
113  // Eg. A fixed time operation needs 10 days on jan 20 on an operation
114  // that is only available only 2 days since the start of the horizon.
115  // Resize to the minimum quantity
116  opplan->setQuantity(0.0001,false);
117  // Move to the earliest start date
118  opplan->setStart(Plan::instance().getCurrent());
119  // Pick up the earliest date we can reply back
120  data.state->a_date = opplan->getDates().getEnd();
121  data.state->a_qty = 0.0;
122  return false;
123  }
124 
125  // Check the leadtime constraints
126  if (data.constrainedPlanning && !checkOperationLeadtime(opplan,data,true))
127  // This operationplan is a wreck. It is impossible to make it meet the
128  // leadtime constraints
129  return false;
130 
131  // Set a bookmark in the command list.
132  CommandManager::Bookmark* topcommand = data.setBookmark();
133 
134  // Temporary variables
135  DateRange orig_dates = opplan->getDates();
136  bool okay = true;
137  Date a_date;
138  double a_qty;
139  Date orig_q_date = data.state->q_date;
140  double orig_opplan_qty = data.state->q_qty;
141  double q_qty_Flow;
142  Date q_date_Flow;
143  bool incomplete;
144  bool tmp_forceLate = data.state->forceLate;
145  bool isPlannedEarly;
146  DateRange matnext;
147 
148  // Loop till everything is okay. During this loop the quanity and date of the
149  // operationplan can be updated, but it cannot be split or deleted.
150  data.state->forceLate = false;
151  do
152  {
153  if (isCapacityConstrained())
154  {
155  // Verify the capacity. This can move the operationplan early or late.
156  checkOperationCapacity(opplan,data);
157  // Return false if no capacity is available
158  if (data.state->a_qty==0.0) return false;
159  }
160 
161  // Check material
162  data.state->q_qty = opplan->getQuantity();
163  data.state->q_date = opplan->getDates().getEnd();
164  a_qty = opplan->getQuantity();
165  a_date = data.state->q_date;
166  incomplete = false;
167  matnext.setStart(Date::infinitePast);
168  matnext.setEnd(Date::infiniteFuture);
169 
170  // Loop through all flowplans // @todo need some kind of coordination run here!!! see test alternate_flow_1
172  g!=opplan->endFlowPlans(); ++g)
173  if (g->getFlow()->isConsumer())
174  {
175  // Switch back to the main alternate if this flowplan was already // @todo is this really required? If yes, in this place?
176  // planned on an alternate
177  if (g->getFlow()->getAlternate())
178  g->setFlow(g->getFlow()->getAlternate());
179 
180  // Trigger the flow solver, which will call the buffer solver
181  data.state->q_flowplan = &*g;
182  q_qty_Flow = - data.state->q_flowplan->getQuantity(); // @todo flow quantity can change when using alternate flows -> move to flow solver!
183  q_date_Flow = data.state->q_flowplan->getDate();
184  g->getFlow()->solve(*this,&data);
185 
186  // Validate the answered quantity
187  if (data.state->a_qty < q_qty_Flow)
188  {
189  // Update the opplan, which is required to (1) update the flowplans
190  // and to (2) take care of lot sizing constraints of this operation.
191  g->setQuantity(-data.state->a_qty, true);
192  a_qty = opplan->getQuantity();
193  incomplete = true;
194 
195  // Validate the answered date of the most limiting flowplan.
196  // Note that the delay variable only reflects the delay due to
197  // material constraints. If the operationplan is moved early or late
198  // for capacity constraints, this is not included.
199  if (data.state->a_date < Date::infiniteFuture)
200  {
202  opplan, 0.01, data.state->a_date, Date::infinitePast, false, false
203  );
204  if (at.end < matnext.getEnd()) matnext = DateRange(at.start, at.end);
205  //xxxif (matnext.getEnd() <= orig_q_date) logger << "STRANGE" << matnext << " " << orig_q_date << " " << at.second << " " << opplan->getQuantity() << endl;
206  }
207 
208  // Jump out of the loop if the answered quantity is 0.
209  if (a_qty <= ROUNDING_ERROR)
210  {
211  // @TODO disabled To speed up the planning the constraining flow is moved up a
212  // position in the list of flows. It'll thus be checked earlier
213  // when this operation is asked again
214  //const_cast<Operation::flowlist&>(g->getFlow()->getOperation()->getFlows()).promote(g->getFlow());
215  // There is absolutely no need to check other flowplans if the
216  // operationplan quantity is already at 0.
217  break;
218  }
219  }
220  else if (data.state->a_qty >+ q_qty_Flow + ROUNDING_ERROR)
221  // Never answer more than asked.
222  // The actual operationplan could be bigger because of lot sizing.
223  a_qty = - q_qty_Flow / g->getFlow()->getQuantity();
224  }
225 
226  isPlannedEarly = opplan->getDates().getEnd() < orig_dates.getEnd();
227 
228  if (matnext.getEnd() != Date::infiniteFuture && a_qty <= ROUNDING_ERROR
229  && matnext.getEnd() <= data.state->q_date_max && matnext.getEnd() > orig_q_date)
230  {
231  // The reply is 0, but the next-date is still less than the maximum
232  // ask date. In this case we will violate the post-operation -soft-
233  // constraint.
234  data.state->q_date = matnext.getEnd();
235  orig_q_date = data.state->q_date;
236  data.state->q_qty = orig_opplan_qty;
237  data.state->a_date = Date::infiniteFuture;
238  data.state->a_qty = data.state->q_qty;
240  opplan, orig_opplan_qty, Date::infinitePast, matnext.getEnd()
241  );
242  okay = false;
243  // Pop actions from the command "stack" in the command list
244  data.rollback(topcommand);
245  // Echo a message
246  if (data.getSolver()->getLogLevel()>1)
247  logger << indent(opplan->getOperation()->getLevel())
248  << " Retrying new date." << endl;
249  }
250  else if (matnext.getEnd() != Date::infiniteFuture && a_qty <= ROUNDING_ERROR
251  && matnext.getStart() < a_date)
252  {
253  // The reply is 0, but the next-date is not too far out.
254  // If the operationplan would fit in a smaller timeframe we can potentially
255  // create a non-zero reply...
256  // Resize the operationplan
258  opplan, orig_opplan_qty, matnext.getStart(),
259  a_date
260  );
261  if (opplan->getDates().getStart() >= matnext.getStart()
262  && opplan->getDates().getEnd() <= a_date
263  && opplan->getQuantity() > ROUNDING_ERROR)
264  {
265  // It worked
266  orig_dates = opplan->getDates();
267  data.state->q_date = orig_dates.getEnd();
268  data.state->q_qty = opplan->getQuantity();
269  data.state->a_date = Date::infiniteFuture;
270  data.state->a_qty = data.state->q_qty;
271  okay = false;
272  // Pop actions from the command stack in the command list
273  data.rollback(topcommand);
274  // Echo a message
275  if (data.getSolver()->getLogLevel()>1)
276  logger << indent(opplan->getOperation()->getLevel())
277  << " Retrying with a smaller quantity: "
278  << opplan->getQuantity() << endl;
279  }
280  else
281  {
282  // It didn't work
283  opplan->setQuantity(0);
284  okay = true;
285  }
286  }
287  else
288  okay = true;
289  }
290  while (!okay); // Repeat the loop if the operation was moved and the
291  // feasibility needs to be rechecked.
292 
293  if (a_qty <= ROUNDING_ERROR && !data.state->forceLate
294  && isPlannedEarly
295  && matnext.getStart() != Date::infiniteFuture
296  && matnext.getStart() != Date::infinitePast
297  && (data.constrainedPlanning && isCapacityConstrained()))
298  {
299  // The operationplan was moved early (because of a resource constraint)
300  // and we can't properly trust the reply date in such cases...
301  // We want to enforce rechecking the next date.
302  if (data.getSolver()->getLogLevel()>1)
303  logger << indent(opplan->getOperation()->getLevel())
304  << " Recheck capacity" << endl;
305 
306  // Move the operationplan to the next date where the material is feasible
308  (opplan, orig_opplan_qty,
309  matnext.getStart()>orig_dates.getStart() ? matnext.getStart() : orig_dates.getStart(),
310  Date::infinitePast);
311 
312  // Move the operationplan to a later date where it is feasible.
313  data.state->forceLate = true;
314  checkOperationCapacity(opplan,data);
315 
316  // Reply of this function
317  a_qty = 0.0;
318  matnext.setEnd(opplan->getDates().getEnd());
319  }
320 
321  // Compute the final reply
322  data.state->a_date = incomplete ? matnext.getEnd() : Date::infiniteFuture;
323  data.state->a_qty = a_qty;
324  data.state->forceLate = tmp_forceLate;
325  if (a_qty > ROUNDING_ERROR)
326  return true;
327  else
328  {
329  // Undo the plan
330  data.rollback(topcommand);
331  return false;
332  }
333 }
334 
335 
337 (OperationPlan* opplan, SolverMRP::SolverMRPdata& data, bool extra)
338 {
339  // No lead time constraints
340  if (!data.constrainedPlanning || (!isFenceConstrained() && !isLeadtimeConstrained()))
341  return true;
342 
343  // Compute offset from the current date: A fence problem uses the release
344  // fence window, while a leadtimeconstrained constraint has an offset of 0.
345  // If both constraints apply, we need the bigger of the two (since it is the
346  // most constraining date.
347  Date threshold = Plan::instance().getCurrent();
348  if (isFenceConstrained()
349  && !(isLeadtimeConstrained() && opplan->getOperation()->getFence()<0L))
350  threshold += opplan->getOperation()->getFence();
351 
352  // Check the setup operationplan
353  OperationPlanState original(opplan);
354  bool ok = true;
355  bool checkSetup = true;
356 
357  // If there are alternate loads we take the best case and assume that
358  // at least one of those can give us a zero-time setup.
359  // When evaluating the leadtime when solving for capacity we don't use
360  // this assumption. The resource solver takes care of the constraints.
361  if (extra && isCapacityConstrained())
362  for (Operation::loadlist::const_iterator j = opplan->getOperation()->getLoads().begin();
363  j != opplan->getOperation()->getLoads().end(); ++j)
364  if (j->hasAlternates())
365  {
366  checkSetup = false;
367  break;
368  }
369  if (checkSetup)
370  {
371  OperationPlan::iterator i(opplan);
372  if (i != opplan->end()
374  && i->getDates().getStart() < threshold)
375  {
376  // The setup operationplan is violating the lead time and/or fence
377  // constraint. We move it to start on the earliest allowed date,
378  // which automatically also moves the owner operationplan.
379  i->setStart(threshold);
380  threshold = i->getDates().getEnd();
381  ok = false;
382  }
383  }
384 
385  // Compare the operation plan start with the threshold date
386  if (ok && opplan->getDates().getStart() >= threshold)
387  // There is no problem
388  return true;
389 
390  // Compute how much we can supply in the current timeframe.
391  // In other words, we try to resize the operation quantity to fit the
392  // available timeframe: used for e.g. time-per operations
393  // Note that we allow the complete post-operation time to be eaten
394  if (extra)
395  // Leadtime check during operation resolver
397  opplan, opplan->getQuantity(),
398  threshold,
399  original.end + opplan->getOperation()->getPostTime(),
400  false
401  );
402  else
403  // Leadtime check during capacity resolver
405  opplan, opplan->getQuantity(),
406  threshold,
407  original.end,
408  true
409  );
410 
411  // Check the result of the resize
412  if (opplan->getDates().getStart() >= threshold
413  && (!extra || opplan->getDates().getEnd() <= data.state->q_date_max)
414  && opplan->getQuantity() > ROUNDING_ERROR)
415  {
416  // Resizing did work! The operation now fits within constrained limits
417  data.state->a_qty = opplan->getQuantity();
418  data.state->a_date = opplan->getDates().getEnd();
419  // Acknowledge creation of operationplan
420  return true;
421  }
422  else
423  {
424  // This operation doesn't fit at all within the constrained window.
425  data.state->a_qty = 0.0;
426  // Resize to the minimum quantity
427  if (opplan->getQuantity() + ROUNDING_ERROR < opplan->getOperation()->getSizeMinimum())
428  opplan->setQuantity(0.0001,false);
429  // Move to the earliest start date
430  opplan->setStart(threshold);
431  // Pick up the earliest date we can reply back
432  data.state->a_date = opplan->getDates().getEnd();
433  // Set the quantity to 0 (to make sure the buffer doesn't see the supply).
434  opplan->setQuantity(0.0);
435 
436  // Log the constraint
437  if (data.logConstraints)
438  data.planningDemand->getConstraints().push(
439  (threshold == Plan::instance().getCurrent()) ?
442  opplan->getOperation(), original.start, original.end,
443  original.quantity
444  );
445 
446  // Deny creation of the operationplan
447  return false;
448  }
449 }
450 
451 
452 DECLARE_EXPORT void SolverMRP::solve(const Operation* oper, void* v)
453 {
454  // Make sure we have a valid operation
455  assert(oper);
456 
457  SolverMRPdata* data = static_cast<SolverMRPdata*>(v);
458  OperationPlan *z;
459 
460  // Call the user exit
461  if (userexit_operation) userexit_operation.call(oper, PythonObject(data->constrainedPlanning));
462 
463  // Find the flow for the quantity-per. This can throw an exception if no
464  // valid flow can be found.
465  double flow_qty_per = 1.0;
466  if (data->state->curBuffer)
467  {
468  Flow* f = oper->findFlow(data->state->curBuffer, data->state->q_date);
469  if (f && f->getQuantity()>0.0)
470  flow_qty_per = f->getQuantity();
471  else
472  // The producing operation doesn't have a valid flow into the current
473  // buffer. Either it is missing or it is producing a negative quantity.
474  throw DataException("Invalid producing operation '" + oper->getName()
475  + "' for buffer '" + data->state->curBuffer->getName() + "'");
476  }
477 
478  // Message
479  if (data->getSolver()->getLogLevel()>1)
480  logger << indent(oper->getLevel()) << " Operation '" << oper->getName()
481  << "' is asked: " << data->state->q_qty << " " << data->state->q_date << endl;
482 
483  // Find the current list of constraints
484  Problem* topConstraint = data->planningDemand->getConstraints().top();
485  double originalqty = data->state->q_qty;
486 
487  // Subtract the post-operation time
488  Date prev_q_date_max = data->state->q_date_max;
489  data->state->q_date_max = data->state->q_date;
490  data->state->q_date -= oper->getPostTime();
491 
492  // Create the operation plan.
493  if (data->state->curOwnerOpplan)
494  {
495  // There is already an owner and thus also an owner command
496  assert(!data->state->curDemand);
497  z = oper->createOperationPlan(
498  data->state->q_qty / flow_qty_per,
499  Date::infinitePast, data->state->q_date, data->state->curDemand,
500  data->state->curOwnerOpplan, 0
501  );
502  }
503  else
504  {
505  // There is no owner operationplan yet. We need a new command.
508  oper, data->state->q_qty / flow_qty_per,
509  Date::infinitePast, data->state->q_date, data->state->curDemand,
510  data->state->curOwnerOpplan
511  );
512  data->state->curDemand = NULL;
513  a->getOperationPlan()->setMotive(data->state->motive);
514  z = a->getOperationPlan();
515  data->add(a);
516  }
517  assert(z);
518 
519  // Check the constraints
520  data->getSolver()->checkOperation(z,*data);
521  data->state->q_date_max = prev_q_date_max;
522 
523  // Multiply the operation reqply with the flow quantity to get a final reply
524  if (data->state->curBuffer) data->state->a_qty *= flow_qty_per;
525 
526  // Ignore any constraints if we get a complete reply.
527  // Sometimes constraints are flagged due to a pre- or post-operation time.
528  // Such constraints ultimately don't result in lateness and can be ignored.
529  if (data->state->a_qty >= originalqty - ROUNDING_ERROR)
530  data->planningDemand->getConstraints().pop(topConstraint);
531 
532  // Check positive reply quantity
533  assert(data->state->a_qty >= 0);
534 
535  // Increment the cost
536  if (data->state->a_qty > 0.0)
537  data->state->a_cost += z->getQuantity() * oper->getCost();
538 
539  // Message
540  if (data->getSolver()->getLogLevel()>1)
541  logger << indent(oper->getLevel()) << " Operation '" << oper->getName()
542  << "' answers: " << data->state->a_qty << " " << data->state->a_date
543  << " " << data->state->a_cost << " " << data->state->a_penalty << endl;
544 }
545 
546 
547 // No need to take post- and pre-operation times into account
549 {
550  SolverMRPdata* data = static_cast<SolverMRPdata*>(v);
551 
552  // Call the user exit
553  if (userexit_operation) userexit_operation.call(oper, PythonObject(data->constrainedPlanning));
554 
555  // Message
556  if (data->getSolver()->getLogLevel()>1)
557  logger << indent(oper->getLevel()) << " Routing operation '" << oper->getName()
558  << "' is asked: " << data->state->q_qty << " " << data->state->q_date << endl;
559 
560  // Find the total quantity to flow into the buffer.
561  // Multiple suboperations can all produce into the buffer.
562  double flow_qty = 1.0;
563  if (data->state->curBuffer)
564  {
565  flow_qty = 0.0;
566  Flow *f = oper->findFlow(data->state->curBuffer, data->state->q_date);
567  if (f) flow_qty += f->getQuantity();
568  for (Operation::Operationlist::const_iterator
569  e = oper->getSubOperations().begin();
570  e != oper->getSubOperations().end();
571  ++e)
572  {
573  f = (*e)->findFlow(data->state->curBuffer, data->state->q_date);
574  if (f) flow_qty += f->getQuantity();
575  }
576  if (flow_qty <= 0.0)
577  throw DataException("Invalid producing operation '" + oper->getName()
578  + "' for buffer '" + data->state->curBuffer->getName() + "'");
579  }
580  // Because we already took care of it... @todo not correct if the suboperation is again a owning operation
581  data->state->curBuffer = NULL;
582  double a_qty(data->state->q_qty / flow_qty);
583 
584  // Create the top operationplan
586  oper, a_qty, Date::infinitePast,
587  data->state->q_date, data->state->curDemand, data->state->curOwnerOpplan, false
588  );
589  data->state->curDemand = NULL;
590  a->getOperationPlan()->setMotive(data->state->motive);
591 
592  // Make sure the subopplans know their owner & store the previous value
593  OperationPlan *prev_owner_opplan = data->state->curOwnerOpplan;
594  data->state->curOwnerOpplan = a->getOperationPlan();
595 
596  // Loop through the steps
597  Date max_Date;
598  TimePeriod delay;
599  Date top_q_date(data->state->q_date);
600  Date q_date;
601  for (Operation::Operationlist::const_reverse_iterator
602  e = oper->getSubOperations().rbegin();
603  e != oper->getSubOperations().rend() && a_qty > 0.0;
604  ++e)
605  {
606  // Plan the next step
607  data->state->q_qty = a_qty;
608  data->state->q_date = data->state->curOwnerOpplan->getDates().getStart();
609  Buffer *tmpBuf = data->state->curBuffer;
610  q_date = data->state->q_date;
611  (*e)->solve(*this,v); // @todo if the step itself has child operations, the curOwnerOpplan field is changed here!!!
612  a_qty = data->state->a_qty;
613  data->state->curBuffer = tmpBuf;
614 
615  // Update the top operationplan
616  data->state->curOwnerOpplan->setQuantity(a_qty,true);
617 
618  // Maximum for the next date
619  if (data->state->a_date != Date::infiniteFuture)
620  {
621  if (delay < data->state->a_date - q_date)
622  delay = data->state->a_date - q_date;
624  data->state->curOwnerOpplan, 0.01, //data->state->curOwnerOpplan->getQuantity(),
625  data->state->a_date, Date::infinitePast, false, false
626  );
627  if (at.end > max_Date) max_Date = at.end;
628  }
629  }
630 
631  // Check the flows and loads on the top operationplan.
632  // This can happen only after the suboperations have been dealt with
633  // because only now we know how long the operation lasts in total.
634  // Solving for the top operationplan can resize and move the steps that are
635  // in the routing!
636  /** @todo moving routing opplan doesn't recheck for feasibility of steps... */
638  if (data->state->curOwnerOpplan->getQuantity() > 0.0)
639  {
640  data->state->q_qty = a_qty;
641  data->state->q_date = data->state->curOwnerOpplan->getDates().getEnd();
642  data->getSolver()->checkOperation(data->state->curOwnerOpplan,*data);
643  a_qty = data->state->a_qty;
644  // The reply date is the combination of the reply date of all steps and the
645  // reply date of the top operationplan.
646  if (data->state->a_date > max_Date && data->state->a_date != Date::infiniteFuture)
647  max_Date = data->state->a_date;
648  }
649  data->state->a_date = (max_Date ? max_Date : Date::infiniteFuture);
650  if (data->state->a_date < data->state->q_date)
651  data->state->a_date = data->state->q_date;
652 
653  // Multiply the operationplan quantity with the flow quantity to get the
654  // final reply quantity
655  data->state->a_qty = a_qty * flow_qty;
656 
657  // Add to the list (even if zero-quantity!)
658  if (!prev_owner_opplan) data->add(a);
659 
660  // Increment the cost
661  if (data->state->a_qty > 0.0)
662  data->state->a_cost += data->state->curOwnerOpplan->getQuantity() * oper->getCost();
663 
664  // Make other operationplans don't take this one as owner any more.
665  // We restore the previous owner, which could be NULL.
666  data->state->curOwnerOpplan = prev_owner_opplan;
667 
668  // Check positive reply quantity
669  assert(data->state->a_qty >= 0);
670 
671  if (data->state->a_date <= top_q_date && delay > TimePeriod(0L))
672  // At least one of the steps is late, but the reply date at the overall routing level is not late.
673  // This causes trouble, so we enforce a lateness of at least one hour. @todo not very cool/performant/generic...
674  data->state->a_date = top_q_date + delay; // TimePeriod(3600L);
675 
676  // Check reply date is later than requested date
677  assert(data->state->a_date >= data->state->q_date);
678 
679  // Message
680  if (data->getSolver()->getLogLevel()>1)
681  logger << indent(oper->getLevel()) << " Routing operation '" << oper->getName()
682  << "' answers: " << data->state->a_qty << " " << data->state->a_date << " "
683  << data->state->a_cost << " " << data->state->a_penalty << endl;
684 }
685 
686 
687 // No need to take post- and pre-operation times into account
688 // @todo This method should only be allowed to create 1 operationplan
690 {
691  SolverMRPdata *data = static_cast<SolverMRPdata*>(v);
692  Date origQDate = data->state->q_date;
693  double origQqty = data->state->q_qty;
694  Buffer *buf = data->state->curBuffer;
695  Demand *d = data->state->curDemand;
696 
697  // Call the user exit
698  if (userexit_operation) userexit_operation.call(oper, PythonObject(data->constrainedPlanning));
699 
700  unsigned int loglevel = data->getSolver()->getLogLevel();
701  SearchMode search = oper->getSearch();
702 
703  // Message
704  if (loglevel>1)
705  logger << indent(oper->getLevel()) << " Alternate operation '" << oper->getName()
706  << "' is asked: " << data->state->q_qty << " " << data->state->q_date << endl;
707 
708  // Make sure sub-operationplans know their owner & store the previous value
709  OperationPlan *prev_owner_opplan = data->state->curOwnerOpplan;
710 
711  // Find the flow into the requesting buffer for the quantity-per
712  double top_flow_qty_per = 0.0;
713  bool top_flow_exists = false;
714  if (buf)
715  {
716  Flow* f = oper->findFlow(buf, data->state->q_date);
717  if (f && f->getQuantity() > 0.0)
718  {
719  top_flow_qty_per = f->getQuantity();
720  top_flow_exists = true;
721  }
722  }
723 
724  // Control the planning mode
725  bool originalPlanningMode = data->constrainedPlanning;
726  data->constrainedPlanning = true;
727 
728  // Remember the top constraint
729  bool originalLogConstraints = data->logConstraints;
730  Problem* topConstraint = data->planningDemand->getConstraints().top();
731 
732  // Try all alternates:
733  // - First, all alternates that are fully effective in the order of priority.
734  // - Next, the alternates beyond their effective end date.
735  // We loop through these since they can help in meeting a demand on time,
736  // but using them will also create extra inventory or delays.
737  double a_qty = data->state->q_qty;
738  bool effectiveOnly = true;
739  Date a_date = Date::infiniteFuture;
740  Date ask_date;
741  Operation *firstAlternate = NULL;
742  double firstFlowPer;
743  while (a_qty > 0)
744  {
745  // Evaluate all alternates
746  bool plannedAlternate = false;
747  double bestAlternateValue = DBL_MAX;
748  double bestAlternateQuantity = 0;
749  Operation* bestAlternateSelection = NULL;
750  double bestFlowPer;
751  Date bestQDate;
752  for (Operation::Operationlist::const_iterator altIter
753  = oper->getSubOperations().begin();
754  altIter != oper->getSubOperations().end(); )
755  {
756  // Set a bookmark in the command list.
757  CommandManager::Bookmark* topcommand = data->setBookmark();
758  bool nextalternate = true;
759 
760  // Operations with 0 priority are considered unavailable
762  = oper->getProperties(*altIter);
763 
764  // Filter out alternates that are not suitable
765  if (props.first == 0.0
766  || (effectiveOnly && !props.second.within(data->state->q_date))
767  || (!effectiveOnly && props.second.getEnd() > data->state->q_date)
768  )
769  {
770  ++altIter;
771  if (altIter == oper->getSubOperations().end() && effectiveOnly)
772  {
773  // Prepare for a second iteration over all alternates
774  effectiveOnly = false;
775  altIter = oper->getSubOperations().begin();
776  }
777  continue;
778  }
779 
780  // Establish the ask date
781  ask_date = effectiveOnly ? origQDate : props.second.getEnd();
782 
783  // Find the flow into the requesting buffer. It may or may not exist, since
784  // the flow could already exist on the top operationplan
785  double sub_flow_qty_per = 0.0;
786  if (buf)
787  {
788  Flow* f = (*altIter)->findFlow(buf, ask_date);
789  if (f && f->getQuantity() > 0.0)
790  sub_flow_qty_per = f->getQuantity();
791  else if (!top_flow_exists)
792  {
793  // Neither the top nor the sub operation have a flow in the buffer,
794  // we're in trouble...
795  // Restore the planning mode
796  data->constrainedPlanning = originalPlanningMode;
797  throw DataException("Invalid producing operation '" + oper->getName()
798  + "' for buffer '" + buf->getName() + "'");
799  }
800  }
801  else
802  // Default value is 1.0, if no matching flow is required
803  sub_flow_qty_per = 1.0;
804 
805  // Remember the first alternate
806  if (!firstAlternate)
807  {
808  firstAlternate = *altIter;
809  firstFlowPer = sub_flow_qty_per + top_flow_qty_per;
810  }
811 
812  // Constraint tracking
813  if (*altIter != firstAlternate)
814  // Only enabled on first alternate
815  data->logConstraints = false;
816  else
817  {
818  // Forget previous constraints if we are replanning the first alternate
819  // multiple times
820  data->planningDemand->getConstraints().pop(topConstraint);
821  // Potentially keep track of constraints
822  data->logConstraints = originalLogConstraints;
823  }
824 
825  // Create the top operationplan.
826  // Note that both the top- and the sub-operation can have a flow in the
827  // requested buffer
829  oper, a_qty, Date::infinitePast, ask_date,
830  d, prev_owner_opplan, false
831  );
832  a->getOperationPlan()->setMotive(data->state->motive);
833  if (!prev_owner_opplan) data->add(a);
834 
835  // Create a sub operationplan
836  data->state->q_date = ask_date;
837  data->state->curDemand = NULL;
838  data->state->curOwnerOpplan = a->getOperationPlan();
839  data->state->curBuffer = NULL; // Because we already took care of it... @todo not correct if the suboperation is again a owning operation
840  data->state->q_qty = a_qty / (sub_flow_qty_per + top_flow_qty_per);
841 
842  // Solve constraints on the sub operationplan
843  double beforeCost = data->state->a_cost;
844  double beforePenalty = data->state->a_penalty;
845  if (search == PRIORITY)
846  {
847  // Message
848  if (loglevel)
849  logger << indent(oper->getLevel()) << " Alternate operation '" << oper->getName()
850  << "' tries alternate '" << *altIter << "' " << endl;
851  (*altIter)->solve(*this,v);
852  }
853  else
854  {
855  data->getSolver()->setLogLevel(0);
856  try {(*altIter)->solve(*this,v);}
857  catch (...)
858  {
859  data->getSolver()->setLogLevel(loglevel);
860  // Restore the planning mode
861  data->constrainedPlanning = originalPlanningMode;
862  data->logConstraints = originalLogConstraints;
863  throw;
864  }
865  data->getSolver()->setLogLevel(loglevel);
866  }
867  double deltaCost = data->state->a_cost - beforeCost;
868  double deltaPenalty = data->state->a_penalty - beforePenalty;
869  data->state->a_cost = beforeCost;
870  data->state->a_penalty = beforePenalty;
871 
872  // Keep the lowest of all next-date answers on the effective alternates
873  if (effectiveOnly && data->state->a_date < a_date && data->state->a_date > ask_date)
874  a_date = data->state->a_date;
875 
876  // Now solve for loads and flows of the top operationplan.
877  // Only now we know how long that top-operation lasts in total.
878  if (data->state->a_qty > ROUNDING_ERROR)
879  {
880  // Multiply the operation reply with the flow quantity to obtain the
881  // reply to return
882  data->state->q_qty = data->state->a_qty;
883  data->state->q_date = origQDate;
885  data->getSolver()->checkOperation(data->state->curOwnerOpplan,*data);
886  data->state->a_qty *= (sub_flow_qty_per + top_flow_qty_per);
887 
888  // Combine the reply date of the top-opplan with the alternate check: we
889  // need to return the minimum next-date.
890  if (data->state->a_date < a_date && data->state->a_date > ask_date)
891  a_date = data->state->a_date;
892  }
893 
894  // Message
895  if (loglevel && search != PRIORITY)
896  logger << indent(oper->getLevel()) << " Alternate operation '" << oper->getName()
897  << "' evaluates alternate '" << *altIter << "': quantity " << data->state->a_qty
898  << ", cost " << deltaCost << ", penalty " << deltaPenalty << endl;
899 
900  // Process the result
901  if (search == PRIORITY)
902  {
903  // Undo the operationplans of this alternate
904  if (data->state->a_qty < ROUNDING_ERROR) data->rollback(topcommand);
905 
906  // Prepare for the next loop
907  a_qty -= data->state->a_qty;
908  plannedAlternate = true;
909 
910  // As long as we get a positive reply we replan on this alternate
911  if (data->state->a_qty > 0) nextalternate = false;
912 
913  // Are we at the end already?
914  if (a_qty < ROUNDING_ERROR)
915  {
916  a_qty = 0.0;
917  break;
918  }
919  }
920  else
921  {
922  double val = 0.0;
923  switch (search)
924  {
925  case MINCOST:
926  val = deltaCost / data->state->a_qty;
927  break;
928  case MINPENALTY:
929  val = deltaPenalty / data->state->a_qty;
930  break;
931  case MINCOSTPENALTY:
932  val = (deltaCost + deltaPenalty) / data->state->a_qty;
933  break;
934  default:
935  LogicException("Unsupported search mode for alternate operation '"
936  + oper->getName() + "'");
937  }
938  if (data->state->a_qty > ROUNDING_ERROR && (
939  val + ROUNDING_ERROR < bestAlternateValue
940  || (fabs(val - bestAlternateValue) < ROUNDING_ERROR
941  && data->state->a_qty > bestAlternateQuantity)
942  ))
943  {
944  // Found a better alternate
945  bestAlternateValue = val;
946  bestAlternateSelection = *altIter;
947  bestAlternateQuantity = data->state->a_qty;
948  bestFlowPer = sub_flow_qty_per + top_flow_qty_per;
949  bestQDate = ask_date;
950  }
951  // This was only an evaluation
952  data->rollback(topcommand);
953  }
954 
955  // Select the next alternate
956  if (nextalternate)
957  {
958  ++altIter;
959  if (altIter == oper->getSubOperations().end() && effectiveOnly)
960  {
961  // Prepare for a second iteration over all alternates
962  effectiveOnly = false;
963  altIter = oper->getSubOperations().begin();
964  }
965  }
966  } // End loop over all alternates
967 
968  // Replan on the best alternate
969  if (bestAlternateQuantity > ROUNDING_ERROR && search != PRIORITY)
970  {
971  // Message
972  if (loglevel)
973  logger << indent(oper->getLevel()) << " Alternate operation '" << oper->getName()
974  << "' chooses alternate '" << bestAlternateSelection << "' " << search << endl;
975 
976  // Create the top operationplan.
977  // Note that both the top- and the sub-operation can have a flow in the
978  // requested buffer
980  oper, a_qty, Date::infinitePast, bestQDate,
981  d, prev_owner_opplan, false
982  );
983  a->getOperationPlan()->setMotive(data->state->motive);
984  if (!prev_owner_opplan) data->add(a);
985 
986  // Recreate the ask
987  data->state->q_qty = a_qty / bestFlowPer;
988  data->state->q_date = bestQDate;
989  data->state->curDemand = NULL;
990  data->state->curOwnerOpplan = a->getOperationPlan();
991  data->state->curBuffer = NULL; // Because we already took care of it... @todo not correct if the suboperation is again a owning operation
992 
993  // Create a sub operationplan and solve constraints
994  bestAlternateSelection->solve(*this,v);
995 
996  // Now solve for loads and flows of the top operationplan.
997  // Only now we know how long that top-operation lasts in total.
998  data->state->q_qty = data->state->a_qty;
999  data->state->q_date = origQDate;
1001  data->getSolver()->checkOperation(data->state->curOwnerOpplan,*data);
1002 
1003  // Multiply the operation reply with the flow quantity to obtain the
1004  // reply to return
1005  data->state->a_qty *= bestFlowPer;
1006 
1007  // Combine the reply date of the top-opplan with the alternate check: we
1008  // need to return the minimum next-date.
1009  if (data->state->a_date < a_date && data->state->a_date > ask_date)
1010  a_date = data->state->a_date;
1011 
1012  // Prepare for the next loop
1013  a_qty -= data->state->a_qty;
1014 
1015  // Are we at the end already?
1016  if (a_qty < ROUNDING_ERROR)
1017  {
1018  a_qty = 0.0;
1019  break;
1020  }
1021  }
1022  else
1023  // No alternate can plan anything any more
1024  break;
1025 
1026  } // End while loop until the a_qty > 0
1027 
1028  // Forget any constraints if we are not short or are planning unconstrained
1029  if (a_qty < ROUNDING_ERROR || !originalLogConstraints)
1030  data->planningDemand->getConstraints().pop(topConstraint);
1031 
1032  // Unconstrained plan: If some unplanned quantity remains, switch to
1033  // unconstrained planning on the first alternate.
1034  // If something could be planned, we expect the caller to re-ask this
1035  // operation.
1036  if (!originalPlanningMode && fabs(origQqty - a_qty) < ROUNDING_ERROR && firstAlternate)
1037  {
1038  // Switch to unconstrained planning
1039  data->constrainedPlanning = false;
1040  data->logConstraints = false;
1041 
1042  // Message
1043  if (loglevel)
1044  logger << indent(oper->getLevel()) << " Alternate operation '" << oper->getName()
1045  << "' plans unconstrained on alternate '" << firstAlternate << "' " << search << endl;
1046 
1047  // Create the top operationplan.
1048  // Note that both the top- and the sub-operation can have a flow in the
1049  // requested buffer
1051  oper, a_qty, Date::infinitePast, origQDate,
1052  d, prev_owner_opplan, false
1053  );
1054  a->getOperationPlan()->setMotive(data->state->motive);
1055  if (!prev_owner_opplan) data->add(a);
1056 
1057  // Recreate the ask
1058  data->state->q_qty = a_qty / firstFlowPer;
1059  data->state->q_date = origQDate;
1060  data->state->curDemand = NULL;
1061  data->state->curOwnerOpplan = a->getOperationPlan();
1062  data->state->curBuffer = NULL; // Because we already took care of it... @todo not correct if the suboperation is again a owning operation
1063 
1064  // Create a sub operationplan and solve constraints
1065  firstAlternate->solve(*this,v);
1066 
1067  // Expand flows of the top operationplan.
1068  data->state->q_qty = data->state->a_qty;
1069  data->state->q_date = origQDate;
1071  data->getSolver()->checkOperation(data->state->curOwnerOpplan,*data);
1072 
1073  // Fully planned
1074  a_qty = 0.0;
1075  data->state->a_date = origQDate;
1076  }
1077 
1078  // Set up the reply
1079  data->state->a_qty = origQqty - a_qty; // a_qty is the unplanned quantity
1080  data->state->a_date = a_date;
1081  assert(data->state->a_qty >= 0);
1082  assert(data->state->a_date >= data->state->q_date);
1083 
1084  // Restore the planning mode
1085  data->constrainedPlanning = originalPlanningMode;
1086  data->logConstraints = originalLogConstraints;
1087 
1088  // Increment the cost
1089  if (data->state->a_qty > 0.0)
1090  data->state->a_cost += data->state->curOwnerOpplan->getQuantity() * oper->getCost();
1091 
1092  // Make sure other operationplans don't take this one as owner any more.
1093  // We restore the previous owner, which could be NULL.
1094  data->state->curOwnerOpplan = prev_owner_opplan;
1095 
1096  // Message
1097  if (loglevel>1)
1098  logger << indent(oper->getLevel()) << " Alternate operation '" << oper->getName()
1099  << "' answers: " << data->state->a_qty << " " << data->state->a_date
1100  << " " << data->state->a_cost << " " << data->state->a_penalty << endl;
1101 }
1102 
1103 
1104 }