solverprocure.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  * *
3  * Copyright (C) 2007-2013 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 
24 namespace frepple
25 {
26 
27 
29 {
30  SolverMRPdata* data = static_cast<SolverMRPdata*>(v);
31 
32  // TODO create a more performant procurement solver. Instead of creating a list of operationplans
33  // moves and creations, we can create a custom command "updateProcurements". The commit of
34  // this command will update the operationplans.
35  // The solve method is only worried about getting a Yes/No reply. The reply is almost always yes,
36  // except a) when the request is inside max(current + the lead time, latest procurement + min time
37  // after locked procurement), or b) when the min time > 0 and max qty > 0
38 
39  // Call the user exit
40  if (userexit_buffer) userexit_buffer.call(b, PythonObject(data->constrainedPlanning));
41 
42  // Message
43  if (data->getSolver()->getLogLevel()>1)
44  logger << indent(b->getLevel()) << " Procurement buffer '" << b->getName()
45  << "' is asked: " << data->state->q_qty << " " << data->state->q_date << endl;
46 
47  // Standard reply date
48  data->state->a_date = Date::infiniteFuture;
49 
50  // Collect all reusable existing procurements in a vector data structure.
51  // Also find the latest locked procurement operation. It is used to know what
52  // the earliest date is for a new procurement.
53  int countProcurements = 0;
54  int indexProcurements = -1;
55  Date earliest_next;
56  Date latest_next = Date::infiniteFuture;
57  vector<OperationPlan*> procurements;
59  {
60  if (i->getLocked())
61  earliest_next = i->getDates().getEnd();
62  else
63  {
64  procurements.push_back(&*i);
65  ++countProcurements;
66  }
67  }
68 
69  // Find constraints on earliest and latest date for the next procurement
70  if (earliest_next && b->getMaximumInterval())
71  latest_next = earliest_next + b->getMaximumInterval();
72  if (earliest_next && b->getMinimumInterval())
73  earliest_next += b->getMinimumInterval();
74  if (data->constrainedPlanning)
75  {
76  if (data->getSolver()->isLeadtimeConstrained()
77  && earliest_next < Plan::instance().getCurrent() + b->getLeadtime())
78  earliest_next = Plan::instance().getCurrent() + b->getLeadtime();
79  if (data->getSolver()->isFenceConstrained()
80  && earliest_next < Plan::instance().getCurrent() + b->getFence())
81  earliest_next = Plan::instance().getCurrent() + b->getFence();
82  }
83 
84  // Loop through all flowplans
85  Date current_date;
86  double produced = 0.0;
87  double consumed = 0.0;
88  double current_inventory = 0.0;
89  const FlowPlan* current_flowplan = NULL;
90  for (Buffer::flowplanlist::const_iterator cur=b->getFlowPlans().begin();
91  latest_next != Date::infiniteFuture || cur != b->getFlowPlans().end(); )
92  {
93  if (cur==b->getFlowPlans().end() || latest_next < cur->getDate())
94  {
95  // Latest procument time is reached
96  current_date = latest_next;
97  current_flowplan = NULL;
98  }
99  else if (earliest_next && earliest_next < cur->getDate())
100  {
101  // Earliest procument time was reached
102  current_date = earliest_next;
103  current_flowplan = NULL;
104  }
105  else
106  {
107  // Date with flowplans found
108  if (current_date && current_date >= cur->getDate())
109  {
110  // When procurements are being moved, it happens that we revisit the
111  // same consuming flowplans twice. This check catches this case.
112  cur++;
113  continue;
114  }
115  current_date = cur->getDate();
116  bool noConsumers = true;
117  do
118  {
119  if (cur->getType() != 1)
120  {
121  cur++;
122  continue;
123  }
124  current_flowplan = static_cast<const FlowPlan*>(&*(cur++));
125  if (current_flowplan->getQuantity() < 0)
126  {
127  consumed -= current_flowplan->getQuantity();
128  noConsumers = false;
129  }
130  else if (current_flowplan->getOperationPlan()->getLocked())
131  produced += current_flowplan->getQuantity();
132  }
133  // Loop to pick up the last consuming flowplan on the given date
134  while (cur != b->getFlowPlans().end() && cur->getDate() == current_date);
135  // No further interest in dates with only producing flowplans.
136  if (noConsumers) continue;
137  }
138 
139  // Compute current inventory. The actual onhand in the buffer may be
140  // different since we count only consumers and *locked* producers.
141  current_inventory = produced - consumed;
142 
143  // Hard limit: respect minimum interval
144  if (current_date < earliest_next)
145  {
146  if (current_inventory < -ROUNDING_ERROR
147  && current_date >= data->state->q_date
148  && b->getMinimumInterval()
149  && data->state->a_date > earliest_next
150  && data->getSolver()->isMaterialConstrained()
151  && data->constrainedPlanning)
152  // The inventory goes negative here and we can't procure more
153  // material because of the minimum interval...
154  data->state->a_date = earliest_next;
155  continue;
156  }
157 
158  // Now the normal reorder check
159  if (current_inventory >= b->getMinimumInventory()
160  && current_date < latest_next)
161  {
162  if (current_date == earliest_next) earliest_next = Date::infinitePast;
163  continue;
164  }
165 
166  // When we are within the minimum interval, we may need to increase the
167  // size of the previous procurements.
168  if (current_date == earliest_next
169  && current_inventory < b->getMinimumInventory() - ROUNDING_ERROR)
170  {
171  for (int cnt=indexProcurements;
172  cnt>=0 && current_inventory < b->getMinimumInventory() - ROUNDING_ERROR;
173  cnt--)
174  {
175  double origqty = procurements[cnt]->getQuantity();
176  procurements[cnt]->setQuantity(
177  procurements[cnt]->getQuantity()
178  + b->getMinimumInventory() - current_inventory);
179  produced += procurements[cnt]->getQuantity() - origqty;
180  current_inventory = produced - consumed;
181  }
182  if (current_inventory < -ROUNDING_ERROR
183  && data->state->a_date > earliest_next
184  && earliest_next > data->state->q_date
185  && data->getSolver()->isMaterialConstrained()
186  && data->constrainedPlanning)
187  // Resizing didn't work, and we still have shortage (not only compared
188  // to the minimum, but also to 0.
189  data->state->a_date = earliest_next;
190  }
191 
192  // At this point, we know we need to reorder...
193  earliest_next = Date::infinitePast;
194  double order_qty = b->getMaximumInventory() - current_inventory;
195  do
196  {
197  if (order_qty <= 0)
198  {
199  if (latest_next == current_date && b->getSizeMinimum())
200  // Forced to buy the minumum quantity
201  order_qty = b->getSizeMinimum();
202  else
203  break;
204  }
205  // Create a procurement or update an existing one
206  indexProcurements++;
207  if (indexProcurements >= countProcurements)
208  {
209  // No existing procurement can be reused. Create a new one.
211  new CommandCreateOperationPlan(b->getOperation(), order_qty,
212  Date::infinitePast, current_date, data->state->curDemand);
213  a->getOperationPlan()->setMotive(data->state->motive);
214  a->getOperationPlan()->insertInOperationplanList(); // TODO Not very nice: unregistered opplan in the list!
215  produced += a->getOperationPlan()->getQuantity();
216  order_qty -= a->getOperationPlan()->getQuantity();
217  data->add(a);
218  procurements.push_back(a->getOperationPlan());
219  ++countProcurements;
220  }
221  else if (procurements[indexProcurements]->getDates().getEnd() == current_date
222  && procurements[indexProcurements]->getQuantity() == order_qty)
223  {
224  // Reuse existing procurement unchanged.
225  produced += order_qty;
226  order_qty = 0;
227  }
228  else
229  {
230  // Update an existing procurement to meet current needs
232  new CommandMoveOperationPlan(procurements[indexProcurements], Date::infinitePast, current_date, order_qty);
233  produced += procurements[indexProcurements]->getQuantity();
234  order_qty -= procurements[indexProcurements]->getQuantity();
235  data->add(a);
236  }
237  if (b->getMinimumInterval())
238  {
239  earliest_next = current_date + b->getMinimumInterval();
240  break; // Only 1 procurement allowed at this time...
241  }
242  }
243  while (order_qty > 0 && order_qty >= b->getSizeMinimum());
244  if (b->getMaximumInterval())
245  {
246  current_inventory = produced - consumed;
247  if (current_inventory >= b->getMaximumInventory()
248  && cur == b->getFlowPlans().end())
249  // Nothing happens any more further in the future.
250  // Abort procuring based on the max inteval
251  latest_next = Date::infiniteFuture;
252  else
253  latest_next = current_date + b->getMaximumInterval();
254  }
255  }
256 
257  // Get rid of extra procurements that have become redundant
258  indexProcurements++;
259  while (indexProcurements < countProcurements)
260  data->add(new CommandDeleteOperationPlan(procurements[indexProcurements++]));
261 
262  // Create the answer
263  if (data->constrainedPlanning && (data->getSolver()->isFenceConstrained()
264  || data->getSolver()->isLeadtimeConstrained()
265  || data->getSolver()->isMaterialConstrained()))
266  {
267  // Check if the inventory drops below zero somewhere
268  double shortage = 0;
269  Date startdate;
270  for (Buffer::flowplanlist::const_iterator cur = b->getFlowPlans().begin();
271  cur != b->getFlowPlans().end(); ++cur)
272  if (cur->getDate() >= data->state->q_date
273  && cur->getOnhand() < -ROUNDING_ERROR
274  && cur->getOnhand() < shortage)
275  {
276  shortage = cur->getOnhand();
277  if (-shortage >= data->state->q_qty) break;
278  if (startdate == Date::infinitePast) startdate = cur->getDate();
279  }
280  if (shortage < 0)
281  {
282  // Answer a shorted quantity
283  data->state->a_qty = data->state->q_qty + shortage;
284  // Log a constraint
285  if (data->logConstraints)
286  data->planningDemand->getConstraints().push(
287  ProblemMaterialShortage::metadata, b, startdate, Date::infiniteFuture, // @todo calculate a better end date
288  -shortage);
289  // Nothing to promise...
290  if (data->state->a_qty < 0) data->state->a_qty = 0;
291  // Check the reply date
292  if (data->constrainedPlanning)
293  {
294  if (data->getSolver()->isFenceConstrained()
295  && data->state->q_date < Plan::instance().getCurrent() + b->getFence()
296  && data->state->a_date > Plan::instance().getCurrent() + b->getFence())
297  data->state->a_date = Plan::instance().getCurrent() + b->getFence();
298  if (data->getSolver()->isLeadtimeConstrained()
299  && data->state->q_date < Plan::instance().getCurrent() + b->getLeadtime()
300  && data->state->a_date > Plan::instance().getCurrent() + b->getLeadtime())
301  data->state->a_date = Plan::instance().getCurrent() + b->getLeadtime();
302  }
303  }
304  else
305  // Answer the full quantity
306  data->state->a_qty = data->state->q_qty;
307  }
308  else
309  // Answer the full quantity
310  data->state->a_qty = data->state->q_qty;
311 
312  // Increment the cost
313  if (b->getItem() && data->state->a_qty > 0.0)
314  data->state->a_cost += data->state->a_qty * b->getItem()->getPrice();
315 
316  // Message
317  if (data->getSolver()->getLogLevel()>1)
318  logger << indent(b->getLevel()) << " Procurement buffer '" << b
319  << "' answers: " << data->state->a_qty << " " << data->state->a_date
320  << " " << data->state->a_cost << " " << data->state->a_penalty << endl;
321 }
322 
323 
324 }