RQ stock optimisation
The RQ stock policy
This page offers an oversight of the request parameters for the OnStock implementation of the RQ model and applies them to a simple assembly network. On top of that, based on the requests to and responses from the Solvice API, beginning insights are offered for RQ stock optimisation.
Request parameters
Solver
Property | Type | Value |
---|---|---|
solver |
String | RQ_STOCK |
Network nodes
An array of supply chain node descriptions. All information a single node can contain is in the table below.
Property | Type | Description |
---|---|---|
name |
String | Unique name for the network node |
nominalTime |
Integer | The processing time for this node |
cost |
Number | The incremental cost of the processing in this node |
serviceLevel |
Number | The serviceLevel that is taken into account in the calculation of the demand bound for this node |
successors |
array | The network nodes that succeed this node directly (names) |
avgCustomerDemand |
Number | The average demand that needs to be sourced to a customer. This should only be filled in for a customer node (i.e. which has no successors in the network). |
sdCustomerDemand |
Number | The standard deviation of the demand that needs to be sourced to a customer. This should only be filled in for a customer node (i.e. which has no successors in the network). |
orderSizeStep |
Integer | The minimal batch order size increment |
orderSizeMax |
Integer | The maximal batch order size |
fixedOrderCost |
Integer | The cost of ordering in this node |
Assignments
An array of value assignments for the supply chain nodes. When solving, assignments are optional. When evaluating, the solution should be complete to get a full view of the score.
Service Time Assignments
Property | Type | Description |
---|---|---|
id |
String | Unique id |
outGoingServiceTime |
Integer | Value assignment for the outgoing service time |
node |
String | Name of the node the value gets assigned to |
Order Size Assignments
Property | Type | Description |
---|---|---|
id |
String | Unique id |
orderSize |
Integer | Value assignment for the order size |
node |
String | Name of the node the value gets assigned to |
RQ options
Property | Type | Description |
---|---|---|
holdingRate |
Number | Expression of how expensive it is to hold a good relative to its cost |
distribution |
String | Demand profile (NORMAL or POISSON) |
maxMovingRateCalculation |
Integer | For Poisson profiles, the maximal rate for which the solver performs exact calculations. Higher rates are approximated. |
maxOutGoingServiceTimeOffset |
Integer | The highest possible outgoing service time of a node is the sum of its processing time and this value. |
maxOrderSizeWeekFactor |
Integer | The order size of a node is capped by the product of its average demand per week and this factor. |
Extended OnStock request
We obtained the first results using the Base Stock policy. We remember that MEIO is about minimizing total cost - but have the first calculations yet considered every cost? A Base Stock policy assumes that inventory is refilled one at a time; which, in its turn, defines a specific policy to order goods.
In reality, we see that another important driver is the cost that stems from ordering goods. When we don't define a fixed order batch size, which we will call Q from this point on, the costs associated with ordering can increase dramatically in the base stock policy. When only taking into account a base-stock level, which should be replenished to the same reordering point R again every time, the amount ordered is very variable. This can get very expensive because of overhead administration and repacking. Sometimes it might not even be possible to order a certain non-round number Q in reality. Then, the Base Stock model definitely falls short, because the only way to reach the base level is by overshooting it. This implies keeping more stock than is accounted for in the total holding cost.
For a demonstration of the RQ policy, we will use the same network as before. For every node, we introduce an ordering cost (fixedOrderCost), a step by which the ordering batch size Q can increase (orderSizeStep) and a maximum value for Q (orderSizeMax). The orderSizeStep parameter together with the orderSizeMax defines the values of Q that can be chosen.
It is important to note that the chosen value for Q is not always how much a node will order when it falls below its reordering point. An actual order in reality will consist of a whole multiple of batches of size Q, more specifically the least multiple that compensates the stock level enough so that it is larger than the reordering point. Example: if R is 40, Q is 10 and safety stock falls down to 25, the order will consist of 20 pieces (25 + 2 batches * 10/batch > 40, 1 batch is not enough and 3 is too many since 2 already suffices).
We extend the first OnStockRequest. Note that the solver parameter changes from BASE_STOCK to RQ_STOCK.
curl -X POST https://api.solvice.io/solve?seconds=10 \
-H "Content-Type: application/json" \
-H "Authorization: " \
-d \
'{
"solver": "RQ_STOCK",
"networkNodes": [
{
"name": "Final assembly",
"nominalTime": 2,
"cost": 5,
"successors": [],
"avgCustomerDemand": 52000,
"sdCustomerDemand": 3000,
"serviceLevel": 0.9,
"orderSizeStep": 200,
"orderSizeMax": 5000,
"fixedOrderCost": 0.5
},
{
"name": "Sub assembly",
"nominalTime": 1,
"cost": 4,
"successors": [
"Final assembly"
],
"serviceLevel": 0.9,
"orderSizeStep": 200,
"orderSizeMax": 5000,
"fixedOrderCost": 0.3
},
{
"name": "Part A",
"nominalTime": 2,
"cost": 1,
"successors": [
"Sub assembly"
],
"serviceLevel": 0.9,
"orderSizeStep": 400,
"orderSizeMax": 5000,
"fixedOrderCost": 0.2
},
{
"name": "Part B",
"nominalTime": 2,
"cost": 1,
"successors": [
"Sub assembly"
],
"serviceLevel": 0.9,
"orderSizeStep": 600,
"orderSizeMax": 5000,
"fixedOrderCost": 0.1
},
{
"name": "Part C",
"nominalTime": 3,
"cost": 1,
"successors": [
"Final assembly"
],
"serviceLevel": 0.9,
"orderSizeStep": 600,
"orderSizeMax": 5000,
"fixedOrderCost": 0.1
}
],
"options": {
"holdingRate": 0.25,
"distribution": "NORMAL"
}
}'
{
"id": "...",
"score": {
"initScore": 0,
"hardScore": 0,
"mediumScore": 0,
"softScore": -29738,
"feasible": true,
"solutionInitialized": true
},
"unresolved": [
{
"name": "Node Ordering Cost",
"value": -2733,
"level": "SOFT",
"notZero": true
},
{
"name": "Inventory Holding Cost related to the safety stock",
"value": -24054,
"level": "SOFT",
"notZero": true
},
{
"name": "Inventory Holding Cost related to the order quantity",
"value": -2951,
"level": "SOFT",
"notZero": true
}
],
"status": "SOLVED",
"username": "anonymousUser",
"solver": "RQ_STOCK",
"networkNodes": [
{
"name": "Final assembly",
"nominalTime": 2,
"cost": 5.0,
"serviceLevel": 0.9,
"avgCustomerDemand": 52000.0,
"sdCustomerDemand": 3000.0,
"fixedOrderCost": 0.5,
"orderSizeStep": 200,
"orderSizeMax": 5000,
"successors": [],
"cumulativeCost": 12.0
},
{
"name": "Sub assembly",
"nominalTime": 1,
"cost": 4.0,
"serviceLevel": 0.9,
"fixedOrderCost": 0.3,
"orderSizeStep": 200,
"orderSizeMax": 5000,
"successors": [
"Final assembly"
],
"cumulativeCost": 6.0
},
{
"name": "Part A",
"nominalTime": 2,
"cost": 1.0,
"serviceLevel": 0.9,
"fixedOrderCost": 0.2,
"orderSizeStep": 400,
"orderSizeMax": 5000,
"successors": [
"Sub assembly"
],
"cumulativeCost": 1.0
},
{
"name": "Part B",
"nominalTime": 2,
"cost": 1.0,
"serviceLevel": 0.9,
"fixedOrderCost": 0.1,
"orderSizeStep": 600,
"orderSizeMax": 5000,
"successors": [
"Sub assembly"
],
"cumulativeCost": 1.0
},
{
"name": "Part C",
"nominalTime": 3,
"cost": 1.0,
"serviceLevel": 0.9,
"fixedOrderCost": 0.1,
"orderSizeStep": 600,
"orderSizeMax": 5000,
"successors": [
"Final assembly"
],
"cumulativeCost": 1.0
}
],
"serviceTimeAssignments": [
{
"id": 0,
"node": "Final assembly",
"incomingServiceTime": 1,
"outgoingServiceTime": 0
},
{
"id": 1,
"node": "Sub assembly",
"incomingServiceTime": 0,
"outgoingServiceTime": 1
},
{
"id": 2,
"node": "Part A",
"incomingServiceTime": 0,
"outgoingServiceTime": 0
},
{
"id": 3,
"node": "Part B",
"incomingServiceTime": 0,
"outgoingServiceTime": 0
},
{
"id": 4,
"node": "Part C",
"incomingServiceTime": 0,
"outgoingServiceTime": 1
}
],
"options": {
"maxOrderSizeWeekFactor": 26,
"holdingRate": 0.25,
"distribution": "NORMAL",
"maxMovingRateCalculation": 125,
"maxOutGoingServiceTimeOffset": 26
},
"weightsDto": {
"serviceTime": 1,
"netLeadTime": 1,
"holdingCost": 1,
"holdingCostQ": 1,
"orderingCost": 1
},
"solution": [
{
"node": "Final assembly",
"safetyStock": 6659,
"netLeadTime": 3,
"holdingCostSS": 19977,
"orderSize": 1000,
"orderCost": 1352,
"holdingCostQ": 1501
},
{
"node": "Sub assembly",
"safetyStock": 0,
"netLeadTime": 0,
"holdingCostSS": 0,
"orderSize": 1000,
"orderCost": 811,
"holdingCostQ": 750
},
{
"node": "Part A",
"safetyStock": 5437,
"netLeadTime": 2,
"holdingCostSS": 1359,
"orderSize": 2000,
"orderCost": 270,
"holdingCostQ": 250
},
{
"node": "Part B",
"safetyStock": 5437,
"netLeadTime": 2,
"holdingCostSS": 1359,
"orderSize": 1800,
"orderCost": 150,
"holdingCostQ": 225
},
{
"node": "Part C",
"safetyStock": 5437,
"netLeadTime": 2,
"holdingCostSS": 1359,
"orderSize": 1800,
"orderCost": 150,
"holdingCostQ": 225
}
],
"orderSizeAssignments": [
{
"id": 0,
"node": "Final assembly",
"orderSize": 1000
},
{
"id": 1,
"node": "Sub assembly",
"orderSize": 1000
},
{
"id": 2,
"node": "Part A",
"orderSize": 2000
},
{
"id": 3,
"node": "Part B",
"orderSize": 1800
},
{
"id": 4,
"node": "Part C",
"orderSize": 1800
}
],
"rqOptions": {
"maxOrderSizeWeekFactor": 26,
"holdingRate": 0.25,
"distribution": "NORMAL",
"maxMovingRateCalculation": 125,
"maxOutGoingServiceTimeOffset": 26
},
"solve_duration": 10
}
The score of 29738 (this is again a cost that is counted in €) is higher than our first result of €24054, but on further inspection of the score object, we see that Inventory Holding Cost is exactly €24054. This is not surprising, since the RQ-problem can be decomposed in the base-stock problem and the Q-problem. The other two scores are specific to Q: the ordering cost and the extra incurred holding cost because of order size batching (in the example where we calculate that an inventory has R = 40 and gets to inventory level 45, that extra 5 needs to be taken into account). The RQ-solution body also contains orderSizeAssignments, which are the choices made for Q per node, as well as an extended solution object.
curl -X POST https://api.solvice.io/evaluate \
-H "Content-Type: application/json" \
-H "Authorization: " \
-d \
'{
"solver": "RQ_STOCK",
"networkNodes": [
{
"name": "Final Assembly",
"nominalTime": 2,
"cost": 5,
"successors": [],
"serviceLevel": 0.9,
"orderSizeStep": 200,
"orderSizeMax": 5000,
"fixedOrderCost": 0.5,
"avgCustomerDemand": 52000,
"sdCustomerDemand": 3000
},
{
"name": "Sub Assembly",
"nominalTime": 1,
"cost": 4,
"successors": [
"Final Assembly"
],
"serviceLevel": 0.9,
"orderSizeStep": 200,
"orderSizeMax": 5000,
"fixedOrderCost": 0.3
},
{
"name": "Part A",
"nominalTime": 2,
"cost": 1,
"successors": [
"Sub Assembly"
],
"serviceLevel": 0.9,
"orderSizeStep": 600,
"orderSizeMax": 5000,
"fixedOrderCost": 0.2
},
{
"name": "Part B",
"nominalTime": 2,
"cost": 1,
"successors": [
"Sub Assembly"
],
"serviceLevel": 0.9,
"orderSizeStep": 600,
"orderSizeMax": 5000,
"fixedOrderCost": 0.1
},
{
"name": "Part C",
"nominalTime": 3,
"cost": 1,
"successors": [
"Final Assembly"
],
"serviceLevel": 0.9,
"orderSizeStep": 400,
"orderSizeMax": 5000,
"fixedOrderCost": 0.1
}
],
"serviceTimeAssignments": [
{
"id": 0,
"node": "Final Assembly",
"incomingServiceTime": 1,
"outgoingServiceTime": 0
},
{
"id": 1,
"node": "Sub Assembly",
"incomingServiceTime": 0,
"outgoingServiceTime": 1
},
{
"id": 2,
"node": "Part A",
"incomingServiceTime": 0,
"outgoingServiceTime": 0
},
{
"id": 3,
"node": "Part B",
"incomingServiceTime": 0,
"outgoingServiceTime": 0
},
{
"id": 4,
"node": "Part C",
"incomingServiceTime": 0,
"outgoingServiceTime": 1
}
],
"options": {
"holdingRate": 0.25,
"distribution": "NORMAL",
"maxMovingRateCalculation": 125,
"maxOutGoingServiceTimeOffset": 26
},
"weightsDto": {
"serviceTime": 1,
"netLeadTime": 1,
"holdingCost": 1,
"holdingCostQ": 1,
"orderingCost": 1
},
"solution": [],
"solve_duration": 0,
"orderSizeAssignments": [
{
"id": 0,
"node": "Final Assembly",
"orderSize": 200
},
{
"id": 1,
"node": "Sub Assembly",
"orderSize": 200
},
{
"id": 2,
"node": "Part A",
"orderSize": 600
},
{
"id": 3,
"node": "Part B",
"orderSize": 600
},
{
"id": 4,
"node": "Part C",
"orderSize": 400
}
]
}'
{
"id": "...",
"score": {
"initScore": 0,
"hardScore": 0,
"mediumScore": 0,
"softScore": -37548,
"feasible": true,
"solutionInitialized": true
},
"unresolved": [
{
"name": "Node Ordering Cost",
"value": -12843,
"level": "SOFT",
"notZero": true
},
{
"name": "Inventory Holding Cost related to the safety stock",
"value": -24054,
"level": "SOFT",
"notZero": true
},
{
"name": "Inventory Holding Cost related to the order quantity",
"value": -651,
"level": "SOFT",
"notZero": true
}
],
"status": "SOLVED",
"username": "anonymousUser",
"solver": "RQ_STOCK",
"networkNodes": [
{
"name": "Final Assembly",
"nominalTime": 2,
"cost": 5.0,
"serviceLevel": 0.9,
"avgCustomerDemand": 52000.0,
"sdCustomerDemand": 3000.0,
"fixedOrderCost": 0.5,
"orderSizeStep": 200,
"orderSizeMax": 5000,
"successors": [],
"cumulativeCost": 12.0
},
{
"name": "Sub Assembly",
"nominalTime": 1,
"cost": 4.0,
"serviceLevel": 0.9,
"fixedOrderCost": 0.3,
"orderSizeStep": 200,
"orderSizeMax": 5000,
"successors": [
"Final Assembly"
],
"cumulativeCost": 6.0
},
{
"name": "Part A",
"nominalTime": 2,
"cost": 1.0,
"serviceLevel": 0.9,
"fixedOrderCost": 0.2,
"orderSizeStep": 600,
"orderSizeMax": 5000,
"successors": [
"Sub Assembly"
],
"cumulativeCost": 1.0
},
{
"name": "Part B",
"nominalTime": 2,
"cost": 1.0,
"serviceLevel": 0.9,
"fixedOrderCost": 0.1,
"orderSizeStep": 600,
"orderSizeMax": 5000,
"successors": [
"Sub Assembly"
],
"cumulativeCost": 1.0
},
{
"name": "Part C",
"nominalTime": 3,
"cost": 1.0,
"serviceLevel": 0.9,
"fixedOrderCost": 0.1,
"orderSizeStep": 400,
"orderSizeMax": 5000,
"successors": [
"Final Assembly"
],
"cumulativeCost": 1.0
}
],
"serviceTimeAssignments": [
{
"id": 0,
"node": "Final Assembly",
"incomingServiceTime": 1,
"outgoingServiceTime": 0
},
{
"id": 1,
"node": "Sub Assembly",
"incomingServiceTime": 0,
"outgoingServiceTime": 1
},
{
"id": 2,
"node": "Part A",
"incomingServiceTime": 0,
"outgoingServiceTime": 0
},
{
"id": 3,
"node": "Part B",
"incomingServiceTime": 0,
"outgoingServiceTime": 0
},
{
"id": 4,
"node": "Part C",
"incomingServiceTime": 0,
"outgoingServiceTime": 1
}
],
"options": {
"maxOrderSizeWeekFactor": 26,
"holdingRate": 0.25,
"distribution": "NORMAL",
"maxMovingRateCalculation": 125,
"maxOutGoingServiceTimeOffset": 26
},
"weightsDto": {
"serviceTime": 1,
"netLeadTime": 1,
"holdingCost": 1,
"holdingCostQ": 1,
"orderingCost": 1
},
"solution": [
{
"node": "Final Assembly",
"safetyStock": 6659,
"netLeadTime": 3,
"holdingCostSS": 19977,
"orderSize": 200,
"orderCost": 6760,
"holdingCostQ": 301
},
{
"node": "Sub Assembly",
"safetyStock": 0,
"netLeadTime": 0,
"holdingCostSS": 0,
"orderSize": 200,
"orderCost": 4056,
"holdingCostQ": 150
},
{
"node": "Part A",
"safetyStock": 5437,
"netLeadTime": 2,
"holdingCostSS": 1359,
"orderSize": 600,
"orderCost": 901,
"holdingCostQ": 75
},
{
"node": "Part B",
"safetyStock": 5437,
"netLeadTime": 2,
"holdingCostSS": 1359,
"orderSize": 600,
"orderCost": 450,
"holdingCostQ": 75
},
{
"node": "Part C",
"safetyStock": 5437,
"netLeadTime": 2,
"holdingCostSS": 1359,
"orderSize": 400,
"orderCost": 676,
"holdingCostQ": 50
}
],
"orderSizeAssignments": [
{
"id": 0,
"node": "Final Assembly",
"orderSize": 200
},
{
"id": 1,
"node": "Sub Assembly",
"orderSize": 200
},
{
"id": 2,
"node": "Part A",
"orderSize": 600
},
{
"id": 3,
"node": "Part B",
"orderSize": 600
},
{
"id": 4,
"node": "Part C",
"orderSize": 400
}
],
"rqOptions": {
"maxOrderSizeWeekFactor": 26,
"holdingRate": 0.25,
"distribution": "NORMAL",
"maxMovingRateCalculation": 125,
"maxOutGoingServiceTimeOffset": 26
},
"solve_duration": 0
}
To illustrate the importance of choosing good values for Q, we will use the same evaluation method, but force the order batch size to be equal to its minimal step. This is the precise assumption in the base-stock policy if the orderSizeStep is equal to 1, so that the base stock level can always be replenished exactly. Here, the steps are larger, so it’s equal to a base stock policy in which Q related holding costs are accounted for.
RQ stock policy | Base stock policy | |
---|---|---|
Node Ordering Cost | 2883 | 13068 |
Inventory Holding Cost (SS) | 24054 | 24054 |
Inventory Holding Cost (Q) | 2801 | 651 |
Total Cost | 29738 | 37773 |
Table: composition of the total cost, RQ policy versus base stock policy
As a lower Q means that R gets overshot less, it makes sense that the Q related Inventory Holding Cost is lowest for the base stock policy. For this instance, Inventory Holding Cost (Q) cannot be lower than €651: it is impossible to choose a lower value for Q than was picked for the base stock policy, and the higher Q is, the more this cost gets driven up. We clearly notice the trade-off between Node Ordering Cost and Q related Inventory Holding Cost, which makes the RQ policy a far superior theory when ordering costs are not negligible.
Conclusion
Choosing smaller batch sizes drives down the inventory cost related to Q, but it drives up the ordering cost. The solver will balance these out when RQ_STOCK is selected to reach the smallest total cost.
Since we know that picking the smallest batch size is an assumption of the base stock policy, we conclude that the RQ policy offers potential large cost cuts.
Updated almost 3 years ago