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