Ex: Truck Shipment Example (cont.)¶
- Product shipped in cartons from Raleigh, NC (27606) to Gainesville, FL (32606)
- Each identical carton weighs 40 lb and occupies 9 ft3 (its cube)
- Don’t know the linear dimensions of each unit for TL and LTL
- Cartons can be stacked on top of each other in a trailer
- Additional info/data is presented only when it is needed to determine the answer
9. Continuing with the example: assuming a constant annual demand for the product of 20 tons, what is the number of full truckloads per year?
using Printf
function prt(value, label, units="", digits=2)
fmtval = split(Printf.format(Printf.Format("%."*string(digits)*"f"),value),".")
fmtval[1] = reverse(join(Iterators.partition(reverse(fmtval[1]), 3), ","))
println("$label\t= $(join(fmtval, ".")) $units")
return value
end
# Previous:
uwt = 40 # lb
ucu = 9 # ft3
s = prt( uwt/ucu, "s", "(lb/ft3)", 4)
Kwt = 25 # ton
Kcu = 2750 # ft3
qmax = prt( min(Kwt, s*Kcu/2000), "qmax", "(ton)")
# New:
f = 20 # ton/yr
q = qmax # ton (FTL => q = qmax)
n = prt( f/q, "n", "(TL/yr)");
s = 4.4444 (lb/ft3) qmax = 6.11 (ton) n = 3.27 (TL/yr)
10. What is the shipment interval?
t = prt( q/f, "t\t", "(yr/TL)")
prt( 365.25/n, "Days b/w TLs", "(day/TL)");
t = 0.31 (yr/TL) Days b/w TLs = 111.60 (day/TL)
11. What is the annual full-truckload transport cost?
d = 532 # Google Maps road distance
ppiTL = 131.0 # TL PPI for Jan 2018
rTL = prt( 2.00ppiTL/102.7, "rTL", "(\$/mi)")
rFTL = prt( rTL/qmax, "rFTL", "(\$/ton-mi)")
TC_FTL = prt( n * rTL * d, "TC_FTL", "(\$/yr)");
rTL = 2.55 ($/mi) rFTL = 0.42 ($/ton-mi) TC_FTL = 4,441.73 ($/yr)
What would be the cost if the shipments were to be made at least every three months?
tmax = prt( 3/12, "tmax", "(yr/TL)")
nmin = prt( 1/tmax, "nmin", "(TL/yr)")
q = prt( f/max(n, nmin), "q", "(ton)")
TC′_FTL = prt( max(n, nmin) * rTL * d, "TC′_FTL", "(\$/yr)");
tmax = 0.25 (yr/TL) nmin = 4.00 (TL/yr) q = 5.00 (ton) TC′_FTL = 5,428.78 ($/yr)
12. “Reasonable estimate” for the total annual cost for the cycle inventory:
α = 1
v = 25000 # $/ton
h = 0.3 # 1/yr
IC_FTL = prt( α*v*h*qmax, "IC_FTL", "(\$/yr)");
IC_FTL = 45,833.33 ($/yr)
13. What is the annual total logistics cost (TLC) for these full-truckload TL shipments?
prt( TC_FTL, "TC_FTL", "(\$/yr)")
prt( IC_FTL, "IC_FTL", "(\$/yr)")
TLC_FTL = prt( TC_FTL + IC_FTL, "TLC_FTL", "(\$/yr)");
TC_FTL = 4,441.73 ($/yr) IC_FTL = 45,833.33 ($/yr) TLC_FTL = 50,275.06 ($/yr)
14. What is minimum possible annual total logistics cost for TL shipments, where the shipment size can now be less than a full truckload?
qᵒTL = prt( sqrt((f*rTL*d)/(α*v*h)), "qᵒTL", "(ton)", 4)
TCᵒ_TL = prt( (f/qᵒTL)*rTL*d, "TCᵒ_TL", "(\$/yr)")
ICᵒ_TL = prt( α*v*h*qᵒTL, "ICᵒ_TL", "(\$/yr)")
TLCᵒ_TL = prt( TCᵒ_TL + ICᵒ_TL, "TLCᵒ_TL", "(\$/yr)");
qᵒTL = 1.9024 (ton) TCᵒ_TL = 14,268.12 ($/yr) ICᵒ_TL = 14,268.12 ($/yr) TLCᵒ_TL = 28,536.25 ($/yr)
15. What is the optimal LTL shipment size?
using Optim
ppiLTL = 177.4
rateLTL(q,s,d,ppi) = ppi*(s^2/8 + 14)/((q^(1/7) * d^(15/29) - 7/2) * (s^2 + 2*s + 14))
TLC_LTLh(q) = (f/q)*rateLTL(q,s,d,ppiLTL)*q*d + α*v*h*q
LB = prt( 150/2000, "LB", "(ton)", 4)
UB = prt( min(5, 650s/2000), "UB", "(ton)", 4)
qᵒLTL = prt( optimize(TLC_LTLh, LB, UB).minimizer, "qᵒLTL", "(ton)", 4);
LB = 0.0750 (ton) UB = 1.4444 (ton) qᵒLTL = 0.7622 (ton)
16. Should the product be shipped TL or LTL?
rLTL = prt( rateLTL(qᵒLTL,s,d,ppiLTL), "rLTL\t", "(\$/ton-mi)")
cLTL = prt( rLTL * qᵒLTL * d, "cLTL\t", "(\$)")
TCᵒ_LTL = prt( (f/qᵒLTL) * cLTL, "TCᵒ_LTL\t", "(\$/yr)")
ICᵒ_LTL = prt( α*v*h*qᵒLTL, "ICᵒ_LTL\t", "(\$/yr)")
TLCᵒ_LTL = prt( TCᵒ_LTL + ICᵒ_LTL, "TLCᵒ_LTL", "(\$/yr)")
prt( TLCᵒ_TL, "\nTLCᵒ_TL\t", "(\$/yr)")
TLCᵒ_TL > TLCᵒ_LTL ? println("\nShip LTL") : println("\nShip TL")
rLTL = 3.23 ($/ton-mi) cLTL = 1,309.00 ($) TCᵒ_LTL = 34,349.30 ($/yr) ICᵒ_LTL = 5,716.28 ($/yr) TLCᵒ_LTL = 40,065.59 ($/yr) TLCᵒ_TL = 28,536.25 ($/yr) Ship TL
17. If the value of the product increased to $85,000 per ton, should the product be shipped TL or LTL?
v = 85000 # $/ton
qᵒTL = prt( sqrt((f*rTL*d)/(α*v*h)), "qᵒTL", "(ton)", 4)
TCᵒ_TL = prt( (f/qᵒTL)*rTL*d, "TCᵒ_TL", "(\$/yr)")
ICᵒ_TL = prt( α*v*h*qᵒTL, "ICᵒ_TL", "(\$/yr)")
TLCᵒ_TL = prt( TCᵒ_TL + ICᵒ_TL, "TLCᵒ_TL", "(\$/yr)")
qᵒLTL = prt( optimize(TLC_LTLh, LB, UB).minimizer, "\nqᵒLTL\t", "(ton)", 4)
rLTL = prt( rateLTL(qᵒLTL,s,d,ppiLTL), "rLTL\t", "(\$/ton-mi)")
cLTL = prt( rLTL * qᵒLTL * d, "cLTL\t", "(\$)")
TCᵒ_LTL = prt( (f/qᵒLTL) * cLTL, "TCᵒ_LTL\t", "(\$/yr)")
ICᵒ_LTL = prt( α*v*h*qᵒLTL, "ICᵒ_LTL\t", "(\$/yr)")
TLCᵒ_LTL = prt( TCᵒ_LTL + ICᵒ_LTL, "TLCᵒ_LTL", "(\$/yr)")
TLCᵒ_TL > TLCᵒ_LTL ? println("\nShip LTL") : println("\nShip TL")
qᵒTL = 1.0317 (ton) TCᵒ_TL = 26,309.12 ($/yr) ICᵒ_TL = 26,309.12 ($/yr) TLCᵒ_TL = 52,618.24 ($/yr) qᵒLTL = 0.2735 (ton) rLTL = 3.84 ($/ton-mi) cLTL = 558.38 ($) TCᵒ_LTL = 40,825.62 ($/yr) ICᵒ_LTL = 6,975.39 ($/yr) TLCᵒ_LTL = 47,801.01 ($/yr) Ship LTL
Using sh and tr
To make it easier to work with shipment data, a name tuple tr can be used to store rate and capacity limits for TL shipments, and shipment-related information can be stored in a DataFrame sh. These can then be used as inputs into a series of functions.
using DataFrames, Optim
tr = (r = rTL, Kwt = 25, Kcu = 2750)
sh = DataFrame(f=f, s=s, α=α, v=v, h=h, d=d)
| Row | f | s | α | v | h | d |
|---|---|---|---|---|---|---|
| Int64 | Float64 | Int64 | Int64 | Float64 | Int64 | |
| 1 | 20 | 4.44444 | 1 | 85000 | 0.3 | 532 |
# Maximum payload
maxpayld(sh, tr) = min(tr.Kwt, sh.s*tr.Kcu/2000)
# TL transport charge
tranchgTL(q, sh, tr) = max(ceil(q/maxpayld(sh, tr))*sh.d*tr.r, 45tr.r/2)
# LTL transport rate
rateLTL(q,s,d,ppi) = ppi*(s^2/8 + 14)/((q^(1/7) * d^(15/29) - 7/2) * (s^2 + 2*s + 14))
# LTL transport charge
tranchgLTL(q, sh, ppi) = max(rateLTL(q, sh.s, sh.d, ppi) * q * sh.d,
(ppi/104.2)*(45 + sh.d^(28/19)/1625))
# Total logistics cost for size `q` shipment with transport charge `c`
totlogcost(q, c, sh) = sh.f*c/q + sh.α*sh.v*sh.h*q
# Determine independent shipment size that minimizes TLC
function minTLC(sh, tr = nothing, ppi = nothing)
qᵒ, TLCᵒ, isLTL = nothing, Inf, false
if tr !== nothing # Check TL option
qTL = min(sqrt((sh.f * max(tr.r*sh.d, 45tr.r/2))/(sh.α*sh.v*sh.h)),
maxpayld(sh, tr))
TLCtl = totlogcost(qTL, tranchgTL(qTL, sh, tr), sh)
qᵒ, TLCᵒ = qTL, TLCtl
end
if ppi !== nothing # Check LTL option
qLTL = optimize(q -> totlogcost(q, tranchgLTL(q,sh,ppi), sh),
150/2000, min(5,650sh.s/2000)).minimizer
TLCltl = totlogcost(qLTL, tranchgLTL(qLTL, sh, ppi), sh)
if TLCltl < TLCᵒ
qᵒ, TLCᵒ, isLTL = qLTL, TLCltl, true
end
end
return (qᵒ = qᵒ, TLCᵒ = TLCᵒ, isLTL = isLTL)
end
# Redo calculations using function
res = minTLC(first(sh), tr, ppiLTL)
(qᵒ = 0.27354473025205917, TLCᵒ = 47801.00891038934, isLTL = true)
18. Second product: On Jan 10, 2018, what was the optimal independent shipment size to ship 80 tons per year of a second, Class 60 product valued at $5000 per ton between Raleigh and Gainesville?
# First, duplicate the data for the first product since much of it will stay the same
push!(sh, first(sh)) # `first` used to get a row of DataFrame
| Row | f | s | α | v | h | d |
|---|---|---|---|---|---|---|
| Int64 | Float64 | Int64 | Int64 | Float64 | Int64 | |
| 1 | 20 | 4.44444 | 1 | 85000 | 0.3 | 532 |
| 2 | 20 | 4.44444 | 1 | 85000 | 0.3 | 532 |
# Change selected values
sh[2, [:f, :s, :v]] = [80, 32.16, 5000]
sh
| Row | f | s | α | v | h | d |
|---|---|---|---|---|---|---|
| Int64 | Float64 | Int64 | Int64 | Float64 | Int64 | |
| 1 | 20 | 4.44444 | 1 | 85000 | 0.3 | 532 |
| 2 | 80 | 32.16 | 1 | 5000 | 0.3 | 532 |
# Determin optimal size for second product
res = minTLC(last(sh), tr, ppiLTL)
(qᵒ = 8.507865272955305, TLCᵒ = 25523.595818865913, isLTL = false)
# Add results to `sh`
sh.qmax = [maxpayld(sh, tr) for sh in eachrow(sh)]
transform!(sh, AsTable(:) => ByRow(row -> minTLC(row, tr, ppiLTL)) => AsTable)
| Row | f | s | α | v | h | d | qmax | qᵒ | TLCᵒ | isLTL |
|---|---|---|---|---|---|---|---|---|---|---|
| Int64 | Float64 | Int64 | Int64 | Float64 | Int64 | Float64 | Float64 | Float64 | Bool | |
| 1 | 20 | 4.44444 | 1 | 85000 | 0.3 | 532 | 6.11111 | 0.273545 | 47801.0 | true |
| 2 | 80 | 32.16 | 1 | 5000 | 0.3 | 532 | 25.0 | 8.50787 | 25523.6 | false |
19. Aggregate shipment: What would have been the annual full-truckload transport cost if both shipments were always shipped together on the same truck?
ash = first(sh)
| Row | f | s | α | v | h | d | qmax | qᵒ | TLCᵒ | isLTL |
|---|---|---|---|---|---|---|---|---|---|---|
| Int64 | Float64 | Int64 | Int64 | Float64 | Int64 | Float64 | Float64 | Float64 | Bool | |
| 1 | 20 | 4.44444 | 1 | 85000 | 0.3 | 532 | 6.11111 | 0.273545 | 47801.0 | true |
fagg = sum(sh.f)
100
sagg = fagg / sum(sh.f./sh.s)
14.311142755428978
vagg = sum(sh.f.*sh.v)/fagg
21000.0
# Use `combine` to create aggregate shipment
ash = combine(sh,
:f => sum => :f,
[:f, :s] => ((f, s) -> sum(f)/sum(f./s)) => :s,
[:f, :α] => ((f, α) -> sum(f.*α)/sum(f)) => :α,
[:f, :v] => ((f, v) -> sum(f.*v)/sum(f)) => :v,
[:f, :h] => ((f, h) -> sum(f.*h)/sum(f)) => :h,
:d => first => :d) # Distance the same for all shipments, so use first
| Row | f | s | α | v | h | d |
|---|---|---|---|---|---|---|
| Int64 | Float64 | Float64 | Float64 | Float64 | Int64 | |
| 1 | 100 | 14.3111 | 1.0 | 21000.0 | 0.3 | 532 |
# Determine optimal aggregate shipment size (only use TL)
ash.qmax = [maxpayld(sh, tr) for sh in eachrow(ash)]
transform!(ash, AsTable(:) => ByRow(row -> minTLC(row, tr)) => AsTable)
| Row | f | s | α | v | h | d | qmax | qᵒ | TLCᵒ | isLTL |
|---|---|---|---|---|---|---|---|---|---|---|
| Int64 | Float64 | Float64 | Float64 | Float64 | Int64 | Float64 | Float64 | Float64 | Bool | |
| 1 | 100 | 14.3111 | 1.0 | 21000.0 | 0.3 | 532 | 19.6778 | 4.64142 | 58481.9 | false |
# Compare with total TLC for separate shipments
prt( sum(sh.TLCᵒ), "Shmt 1 + 2 TLC")
prt( ash.TLCᵒ[1], "Agg shmt TLC");
Shmt 1 + 2 TLC = 73,324.60 Agg shmt TLC = 58,481.90