Chapter6Models.xlsx, and why the periods run across| Sheet | What it does | Which part of the chapter |
|---|---|---|
Record |
the same requirements carried forward twice, without and with netting | the record, and the \(\max\) |
Explosion |
the structure in three columns, then one record per item in low-level-code order | the explosion of all five items |
Lumpiness |
what each rule has the deck assembly receive, with its order count and variability coefficient | what lot sizing does to the level below |
DRP |
three regions and a center, every quantity per equal to one | the same calculation on a network |
The periods run across the columns, the reverse of the dynamic lot sizing workbook. That is not this book’s choice: a planner reads one item’s six rows at a time and compares them with each other, and the layout follows the field.
Record: Computed TwiceThe upper block carries the balance forward without netting: it has no net requirement row at all, and its projected on hand runs to \(-1{,}840\).
The lower block nets, and its projected on hand never leaves zero.
Notice that the two blocks share a gross requirement row to the unit. The only thing that differs between them is one formula, and putting them one above the other is the point of the sheet.
Explosion: the Structure Is LiveThe structure sits in three columns at the top. The five blue cells in column C are the quantities per, and every component’s gross requirement formula reads them: the explosion equation written out, one term per parent.
Notice the deck belt’s gross requirement row against the deck assembly’s release row above it. They are the same numbers, because the quantity per is one.
Type a 3 over the 2 and every record below the spindle assembly moves.
Explosion: the Bearing and the TotalsThe spindle assembly, the bearing, the housing, and the totals.
The bearing’s gross requirements are the largest numbers on the sheet, reaching 1,980 in period 8. Five bearings per deck and two paths through the structure will do that.
The three cells at the foot make the first session’s cost computable: 57 orders, $14,500 of setup, and nothing carried.
LumpinessEach row is what one rule has the deck assembly receive over the twelve scheduled months. The variability coefficient is written as the mean of the squares less the square of the mean, over the square of the mean, because the more obvious SUMPRODUCT((range - mean)^2) is an array expression a reader cannot follow by clicking on it.
Notice the Largest receipt column: six of the seven rules never exceed 420, and part-period balancing’s single lot of 680 is why its coefficient is the highest on the sheet. The Total column reads 1,840 on every row, and is there to be checked.
DRP: the Explosion Sheet on Its SideEach region is planned exactly as an end item is planned on the Explosion sheet; the only word that changes is the one naming the offset, a transit time for a lead time. The center’s gross requirement row is the three release rows added down the column, and nothing else.
Notice period 3, where the center needs ten belts: the southern region alone is asking, because its transit time is a month longer than the other two.
Requirement 5 is the one to watch. It looks like a second feature, and the design argues that it is not one.
A planned item is what the item master holds: lead time, opening stock, what is on order, the costs, the rule. Not its requirements, which are computed.
A bill of material is the structure, and it owns the low-level codes: an item cannot answer what its own code is without seeing every path that reaches it.
An MRP record is one item’s six rows; a requirements plan is every item’s record, in the order computed; a receipt policy is how much to have arrive, given what must arrive.
Level is the instructive rejection. It reads like a class and the chapter uses the word constantly, but a level has no behavior: it is an integer attached to an item, and the only thing anyone does with it is sort by it.
The design turns on one sentence: the net requirement stream is a requirements schedule of the dynamic lot sizing chapter. Once that is said, no lot sizing rule needs an MRP version.
class PlanTheHorizon(private val rule: LotSizingRule) : ReceiptPolicy {
override fun receiptsFor(netRequirements: List<Double>, item: PlannedItem): List<Double> {
val first = netRequirements.indexOfFirst { it > 0.0 }
if (first < 0) return netRequirements.map { 0.0 }
val plan = rule.plan(item.scheduleFor(netRequirements.drop(first)))
return netRequirements.indices.map {
if (it < first) 0.0 else plan.orderIn(it - first + 1)
}
}
}Notice the drop. It is part of the contract, not a convenience: a rule handed a stream that opens with empty periods places a receipt in the first of them and carries it. The empty periods are the horizon’s lead-in, not part of the problem.
A fixed order quantity decides period by period and looks nowhere. Silver-Meal cannot decide anything until it has seen the whole horizon. They look as though they need two interfaces, and they do not:
The net requirement stream is computed before any rule runs, so the whole of it is available to a rule that needs the whole of it.
The fixed order quantity is a rule the dynamic lot sizing chapter had to exclude, because it cannot finish the horizon empty. Inside a record it is admissible: what is left over is on hand at the start of the next planning cycle, which is what the projected on hand row is for.
The record computes in two sweeps, and this is the only place in the package where a name means two things.
The first sweep asks what would have to arrive in each period if nothing were carried beyond the opening stock and the scheduled receipts. That is the stream a policy is asked to cover, and it does not depend on the policy.
The second sweep works out what the policy’s receipts actually leave on hand, and recomputes the row the record prints. For example, a fixed order quantity that covers three periods at once makes the printed net requirement zero in the two periods after it, where the stream it was given had positive numbers.
Thus, two names. One property meaning whichever the caller needed would put the package into a quiet disagreement with every worked example in the literature.
The claim that distribution requirements planning is the same calculation is made structural by not writing a second class:
That is the whole of the network’s definition. RequirementsPlan assigns the low-level codes, which puts the regions at 0 and the center at 1, plans them in that order, and computes the center’s gross requirements as the sum over its parents.
The temptation is a DistributionNetwork class with regions and center on it. It would duplicate the low-level coding, the planning order and the explosion, and the two could then disagree about a network that is also a structure: a warehouse supplying both dealers and an assembly plant.
Notice that the record computes its net requirements before the policy is consulted, and re-nets after. Nothing in the trace would change if the graph were a network instead of a structure.
val bom = BillOfMaterial(
listOf(
PlannedItem("DA", "Mower deck assembly", leadTime = 1, policy = LotForLotPolicy,
orderCost = 500.0, unitCost = 180.0, carryingCharge = 0.02),
PlannedItem("B", "Deck belt", leadTime = 2, policy = LotForLotPolicy,
orderCost = 300.0, unitCost = 50.0, carryingCharge = 0.02),
PlannedItem("S", "Spindle assembly", leadTime = 1, policy = LotForLotPolicy,
orderCost = 250.0, unitCost = 25.0, carryingCharge = 0.03),
PlannedItem("H", "Spindle housing", leadTime = 2, policy = LotForLotPolicy,
orderCost = 150.0, unitCost = 12.0, carryingCharge = 0.02),
PlannedItem("BR", "Bearing", leadTime = 3, policy = LotForLotPolicy,
orderCost = 100.0, unitCost = 4.0, carryingCharge = 0.02),
),
listOf(
BillOfMaterial.Usage("DA", "B", 1.0), BillOfMaterial.Usage("DA", "S", 2.0),
BillOfMaterial.Usage("DA", "BR", 1.0), BillOfMaterial.Usage("S", "H", 1.0),
BillOfMaterial.Usage("S", "BR", 2.0),
),
)
println(bom.lowLevelCodes.toSortedMap()) // {B=1, BR=2, DA=0, H=2, S=1}
println(bom.planningOrder()) // [DA, B, S, BR, H]Notice that no level was typed anywhere. lowLevelCodes is the low-level code table and planningOrder is the order the explosion plans in, both read off the usages.
val schedule = List(5) { 0.0 } +
listOf(40.0, 60.0, 120.0, 300.0, 420.0, 260.0, 120.0, 40.0, 0.0, 80.0, 180.0, 220.0)
val plan = RequirementsPlan(bom, mapOf("DA" to schedule))
val bearing = plan.recordFor("BR")
println(bearing.grossRequirements.map { it.toInt() })
println(bearing.plannedOrderReleases.map { it.toInt() })
println("%d orders, %.0f setup, %.0f carrying"
.format(bearing.orderCount, bearing.setupCost, bearing.carryingCost))
println("%.0f past due".format(plan.pastDueReleases))[0, 0, 0, 160, 280, 540, 1320, 1980, 1460, 740, 280, 40, 320, 800, 1060, 220, 0]
[160, 280, 540, 1320, 1980, 1460, 740, 280, 40, 320, 800, 1060, 220, 0, 0, 0, 0]
13 orders, 1300 setup, 0 carrying
0 past due
The five zeros in front of schedule are the five-month lead-in, and the schedule is a map because a structure may have more than one end item. The first output row is the bearing’s gross requirements from the hand explosion, and pastDueReleases checks the horizon without reading the record.
Changing a rule means changing one item’s policy. Putting the deck assembly on Silver-Meal, with every component still on lot-for-lot, is one row of the comparison table:
That is against $14,500 with lot-for-lot everywhere. Notice that relevantCost is available on the record and on the plan, which is what lets the comparison separate what a rule saved at the deck assembly from what it cost below: \(\$11{,}636 - \$4{,}936 = \$6{,}700\), the Silver-Meal row’s Cost below.
val regions = listOf("N" to 1, "S" to 2, "W" to 1)
val network = BillOfMaterial(
regions.map { (id, transit) ->
PlannedItem(id, "$id region", leadTime = transit, policy = LotForLotPolicy,
orderCost = 200.0, unitCost = 50.0, carryingCharge = 0.02)
} + PlannedItem("CW", "Central warehouse", leadTime = 2, policy = LotForLotPolicy,
orderCost = 400.0, unitCost = 50.0, carryingCharge = 0.02),
regions.map { (id, _) -> BillOfMaterial.Usage(parent = id, child = "CW", quantity = 1.0) },
)
val drp = RequirementsPlan(network, demand.mapValues { List(4) { 0.0 } + it.value })
println(network.lowLevelCodes.toSortedMap()) // {CW=1, N=0, S=0, W=0}
println(network.planningOrder()) // [N, S, W, CW]
println(drp.recordFor("CW").grossRequirements.map { it.toInt() })
// [0, 0, 10, 50, 80, 180, 360, 360, 210, 80, 30, 20, 120, 190, 150, 0]The last row is the center’s roll-up. Notice the lead-in is four here and five above, because the network is a shallower graph, and notice what is absent: no class is told that the graph is a network. The only thing that says so is the word region in a label.
The workbook’s periods run across, because a planner reads one item’s six rows at a time; the Record sheet differs from itself in one formula.
The Explosion sheet’s structure is live: the quantities per are the cells every gross requirement formula reads.
The net requirement stream is a requirements schedule, so every dynamic lot sizing rule runs inside a record through one adapter, PlanTheHorizon.
One interface serves every rule, because netting happens before any rule runs; the record keeps two named readings of net requirement.
A distribution network is a bill of material with every quantity per equal to one, and the code proves it by having no second class.
Read before next session: One Order, One Chance and Working the Decision by Hand, in the single-period chapter.
Today’s material was Building These Models in a Worksheet and Designing the Software.
Next session begins the second half of the course:
Everything so far has assumed demand is known. From here it is not, and the first model that admits it is also the simplest one in the book.