© 2022 (CC BY) Gerd Wagner, Brandenburg University of Technology, Germany.
The four stage (single-product) supply chain of the classical "Beer Game" consists of a retailer, a wholesaler, a distributor and a factory.
You can inspect the model's OESjs code on the OES GitHub repo.
A conceptual model, also called domain model, describes the real-world system under investigation by identifying the relevant types of objects and events, and describing their dynamics, allowing to understand what's going on in the real system. It does not describe any software/design artifact.
A conceptual information model describes the subject matter vocabulary used, e.g., in the system narrative, in a semi-formal way. Such a vocabulary essentially consists of names for
The main categories of types are object types and event types. A simple form of conceptual information model is obtained by providing a list of each of them, while a more elaborated model, preferably in the form of a UML class diagram, also defines properties and associations, including the participation of objects (of certain types) in events (of certain types).
The potentially relevant object types are:
Potentially relevant types of events are:
Both object types and event types, together with their participation associations, can be visually described in a conceptual information model in the form of a UML class diagram, as shown below.
For implementing the information design model, we have to code all object types and event types specified in the model in the form of classes.
The agent class AbstractSupplyChainNode
can be coded with OESjs-Core4 in the following way:
class AbstractSupplyChainNode extends aGENT { constructor({ id, name, stockQuantity, safetyStock=3, backorderQuantity=0}) { super({id, name}); if (stockQuantity !== undefined) this.stockQuantity = stockQuantity; // extra inventory beyond the expected lead time demand if (safetyStock !== undefined) this.safetyStock = safetyStock; // orders of previous cycles that aren't fulfilled yet if (backorderQuantity !== undefined) this.backorderQuantity = backorderQuantity; // the quantity ordered last time by the downstream customer from this node this.lastSalesOrderQuantity = 0; // the accumulated inventory costs of this node this.accumulatedInventoryCosts = 0; } onReceiveOrder( quantity) { // store order quantity for later processing this.lastSalesOrderQuantity = quantity; } // not used by TopSupplyChainNode onPerceive( percept) { switch (percept.type) { case "InDelivery": this.stockQuantity += percept.quantity; break; } } // overwritten by TopSupplyChainNode onTimeEvent( e) { switch (e.type) { case "EndOfWeek": ... } } }
All agent classes inherit an id
attribute and a name
attribute from the
pre-defined OES foundation class aGENT
.
The onTimeEvent
method of the class AbstractSupplyChainNode
contains most of the model logic:
onTimeEvent( e) { switch (e.type) { case "EndOfWeek": /******************************************************** *** ship items to downstream node or end customer ****** ********************************************************/ const stockQuantityAtStartOfWeek = this.stockQuantity; let deliveryQuantity = 0, stockoutCosts = 0; if (this.stockQuantity < this.lastSalesOrderQuantity + this.backorderQuantity) { // not enough stock quantity for fulfilling the sum of order and backorder quantity deliveryQuantity = this.stockQuantity; if (this.lastSalesOrderQuantity > this.stockQuantity) { const newBackorderQuantity = this.lastSalesOrderQuantity - this.stockQuantity; stockoutCosts = newBackorderQuantity * sim.model.p.stockoutCostsPerUnit; // increment backorder quantity this.backorderQuantity += newBackorderQuantity; } else if (this.stockQuantity > this.lastSalesOrderQuantity) { // decrement backorder quantity this.backorderQuantity -= this.stockQuantity - this.lastSalesOrderQuantity; } // stock quantity is reset to zero this.stockQuantity = 0; } else { // enough stock quantity for fulfilling the sum of order and backorder quantity deliveryQuantity = this.lastSalesOrderQuantity + this.backorderQuantity; // decrement stock quantity this.stockQuantity -= deliveryQuantity; // backorder quantity is reset to zero this.backorderQuantity = 0; } // only ship non-zero quantities if (deliveryQuantity > 0) { sim.schedule( new ShipItems({quantity: deliveryQuantity, performer: this})); } /*********************************************** *** Send purchase order to upstream node ****** ***********************************************/ let orderQuantity = 0; // Try to keep the inventory as big as the latest order received by this node (plus a bit extra quantity) if (this.stockQuantity > 0) { orderQuantity = Math.max( this.lastSalesOrderQuantity - this.stockQuantity + this.safetyStock, 0); } else { orderQuantity = this.lastSalesOrderQuantity + this.safetyStock; } //TODO: DELETE this.lastPuchaseOrderQuantity = orderQuantity; // only place orders with values greater than zero if (orderQuantity > 0) { sim.schedule( new PurchaseOrder({ quantity: orderQuantity, sender: this, receiver: this.upStreamNode})); } /*********************************************** *** Calculate inventory costs ***************** ***********************************************/ // the average inventory is the stock quantity at the beginning of the week plus the stock quantity // at the end of the week divided by two const averageStockQuantity = (stockQuantityAtStartOfWeek + this.stockQuantity) / 2, totalHoldingCostsPerWeek = averageStockQuantity * sim.model.p.holdingCostsPerUnitPerWeek; this.accumulatedInventoryCosts = totalHoldingCostsPerWeek + stockoutCosts; break; } }