Base stock optimisation

The base stock policy

This page offers an oversight of the request parameters for the OnStock implementation of the Base stock 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 base stock optimisation.

Request parameters

Solver

Property Type Value
solver String BASE_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).

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

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.

First OnStock request

Figure 1: simple assembly networkFigure 1: simple assembly network

Figure 1: simple assembly network

Figure 1 translates in the ‘networkNodes’ object of the request below, which contains all node names and the names of their respective successors. The request is enriched with a few other node characteristics and options. We clarify the meaning of nominalTime, cost, holdingRate, distribution and serviceLevel. (This guide will build further on the first request, but of course, the reader is free to experiment with parameters/values to gather insights.)

curl -X POST https://api.solvice.io/solve \
   -H "Content-Type: application/json" \
   -H "Authorization: " \
   -d \
'{
 "solver": "BASE_STOCK",
 "networkNodes": [
   {
     "name": "Final assembly",
     "nominalTime": 2,
     "cost": 5,
     "successors": [],
     "avgCustomerDemand": 52000,
     "sdCustomerDemand": 3000,
     "serviceLevel": 0.9
   },
   {
     "name": "Sub assembly",
     "nominalTime": 1,
     "cost": 4,
     "successors": [
       "Final assembly"
     ],
     "serviceLevel": 0.9
   },
   {
     "name": "Part A",
     "nominalTime": 2,
     "cost": 1,
     "successors": [
       "Sub assembly"
     ],
     "serviceLevel": 0.9
   },
   {
     "name": "Part B",
     "nominalTime": 2,
     "cost": 1,
     "successors": [
       "Sub assembly"
     ],
     "serviceLevel": 0.9
   },
   {
     "name": "Part C",
     "nominalTime": 3,
     "cost": 1,
     "successors": [
       "Final assembly"
     ],
     "serviceLevel": 0.9
   }
 ],
 "options": {
   "holdingRate": 0.25,
   "distribution": "NORMAL"
 }
}'

The OnStock API solver decides where to keep stock and how much. We can think of three critical time indicators per node:
-how long it takes for a good to arrive from the upstream part of the network (incomingServiceTime);
-how long it takes for the node to complete its task regarding the good (assemble, package…) (nominalTime);
-and the time a node quotes to all of his downstream nodes (outgoingServiceTime).

Of these three time indicators, the nominalTime is a fixed value we feed to the API, as we can see in the request above. The other two are related, we will discuss their values based on the response of the API.

To calculate the total safety inventory holding cost, we first need to determine the inventory cost per unit, for every node. We associate a cost with every stage in the network. For the total cost of an (intermediary) product, we take into account all the previous processing steps that the product has undergone (cumulativeCost). We multiply this with the holdingRate in a node. This rate expresses how much it would cost to keep a unit of safety stock at a node by aggregating inventory storing costs such as cooling and renting, opportunity cost because of the money that is tied up in the goods and possible losses if the good does not end up being sold (Springer reference). Let us now multiply the aggregated costs (for 'Sub assembly this would be 4 + 1 + 1) by the holdingRate. This yields a unit holding cost that the OnStock solver will later multiply with the actual amount of an SKU that will be kept as safetyStock.

The GSM assumes that the demand profile is known. The API accepts both the normal distribution (NORMAL) and the poisson distribution (POISSON). Serving all possible demand 100% of the time is infeasible - a network would need an infinite amount of stock for that. To make sure we're working on a bounded problem, we define a serviceLevel per node. A default value for the serviceLevel is set at 95%, meaning all demand will be fulfilled 95% of the time. However, it’s possible to override this value for each node, as shown in the request. Based on these inputs, the OnStock solver calculates the corresponding safety stock values to make sure that the asked demand is met.

{
    "id": "...",
    "score": {
        "initScore": 0,
        "hardScore": 0,
        "mediumScore": 0,
        "softScore": -24054,
        "feasible": true,
        "solutionInitialized": true
    },
    "unresolved": [
        {
            "name": "Inventory Holding Cost related to the safety stock",
            "value": -24054,
            "level": "SOFT",
            "notZero": true
        }
    ],
    "status": "SOLVED",
    "username": "anonymousUser",
    "solver": "BASE_STOCK",
    "networkNodes": [
        {
            "name": "Final assembly",
            "nominalTime": 2,
            "cost": 5.0,
            "serviceLevel": 0.9,
            "avgCustomerDemand": 52000.0,
            "sdCustomerDemand": 3000.0,
            "fixedOrderCost": 0.0,
            "orderSizeStep": 1,
            "successors": [],
            "cumulativeCost": 12.0
        },
        {
            "name": "Sub assembly",
            "nominalTime": 1,
            "cost": 4.0,
            "serviceLevel": 0.9,
            "fixedOrderCost": 0.0,
            "orderSizeStep": 1,
            "successors": [
                "Final assembly"
            ],
            "cumulativeCost": 6.0
        },
        {
            "name": "Part A",
            "nominalTime": 2,
            "cost": 1.0,
            "serviceLevel": 0.9,
            "fixedOrderCost": 0.0,
            "orderSizeStep": 1,
            "successors": [
                "Sub assembly"
            ],
            "cumulativeCost": 1.0
        },
        {
            "name": "Part B",
            "nominalTime": 2,
            "cost": 1.0,
            "serviceLevel": 0.9,
            "fixedOrderCost": 0.0,
            "orderSizeStep": 1,
            "successors": [
                "Sub assembly"
            ],
            "cumulativeCost": 1.0
        },
        {
            "name": "Part C",
            "nominalTime": 3,
            "cost": 1.0,
            "serviceLevel": 0.9,
            "fixedOrderCost": 0.0,
            "orderSizeStep": 1,
            "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": {
        "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
        },
        {
            "node": "Sub assembly",
            "safetyStock": 0,
            "netLeadTime": 0,
            "holdingCostSS": 0
        },
        {
            "node": "Part A",
            "safetyStock": 5437,
            "netLeadTime": 2,
            "holdingCostSS": 1359
        },
        {
            "node": "Part B",
            "safetyStock": 5437,
            "netLeadTime": 2,
            "holdingCostSS": 1359
        },
        {
            "node": "Part C",
            "safetyStock": 5437,
            "netLeadTime": 2,
            "holdingCostSS": 1359
        }
    ],
    "solve_duration": 0
}

Let’s focus on the score of ‘First OnStock response’ first. The hardScore and mediumScore are equal to zero, which means that no constraints were broken. The softScore corresponds to the total yearly cost for keeping stock in your network.The other parameters we’ll discuss are the incomingServiceTime and outgoingServiceTime. Both are part of the serviceTimeAssignments.

For every node, the incoming service time is equal to the highest outgoing service time of the predecessors of a node: i.e. a node can only start processing as soon as all necessary inputs are available. Now, based on the service time promises that are made, we can derive how many time units we need to keep stock.

Say a demand node wants its supplier to deliver a good. The promise made by the supplier is to deliver at its outgoing service time; that supplier itself will only receive input at its incoming service time. If for instance the outgoing service time is 1 and the incoming service time is 0, but the nominal time is 3, the node needs 2 time units to replenish stock. In the solution object, we refer to this period as netLeadTime. Based on the netLeadTime, the demand profile and the service level, safety stock is calculated.

In the solution object of the response, we notice that all ‘Part’ nodes have a netLeadTime of 2. The time units example in the last paragraph is a description of how this is calculated for ‘Part C’. We note that the values for safetyStock and holdingCostTau are also identical for ‘Part A’, ‘Part B’ and ‘Part C’. This makes sense because these nodes have the same cumulativeCost (which is equal to their cost because they have no predecessors) and they have the same serviceLevel value.

Summing up all holdingCostTau values from the solution object, 19977 + 3*1359, we retrieve the total cost of €24054.

Conclusion

We used the OnStock API to perform MEIO on an assembly network. We introduced core concepts regarding the GSM and MEIO, whilst explaining parameters of the API requests we constructed and their corresponding responses. The reader was presented with request examples of which the values can be changed and experimented with.