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

Figure 1: simple assembly networkFigure 1: simple assembly network

Figure 1: simple assembly network

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.