3. Transport 3: Periodic Truck Shipments¶

ISE 754, Fall 2024

Package Used: No new packages used.

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?

In [1]:
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?

In [2]:
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?

In [3]:
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?

In [4]:
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:

In [5]:
α = 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?

In [6]:
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?

In [7]:
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?

In [8]:
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?

In [9]:
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?

In [10]:
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.

In [11]:
using DataFrames, Optim

tr = (r = rTL, Kwt = 25, Kcu = 2750)
sh = DataFrame(f=f, s=s, α=α, v=v, h=h, d=d)
Out[11]:
1×6 DataFrame
Rowfsαvhd
Int64Float64Int64Int64Float64Int64
1204.444441850000.3532
In [12]:
# 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)
Out[12]:
(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?

In [13]:
# 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
Out[13]:
2×6 DataFrame
Rowfsαvhd
Int64Float64Int64Int64Float64Int64
1204.444441850000.3532
2204.444441850000.3532
In [14]:
# Change selected values
sh[2, [:f, :s, :v]] = [80, 32.16, 5000]
sh
Out[14]:
2×6 DataFrame
Rowfsαvhd
Int64Float64Int64Int64Float64Int64
1204.444441850000.3532
28032.16150000.3532
In [15]:
# Determin optimal size for second product
res = minTLC(last(sh), tr, ppiLTL)
Out[15]:
(qᵒ = 8.507865272955305, TLCᵒ = 25523.595818865913, isLTL = false)
In [16]:
# Add results to `sh`
sh.qmax = [maxpayld(sh, tr) for sh in eachrow(sh)]
transform!(sh, AsTable(:) => ByRow(row -> minTLC(row, tr, ppiLTL)) => AsTable)
Out[16]:
2×10 DataFrame
RowfsαvhdqmaxqᵒTLCᵒisLTL
Int64Float64Int64Int64Float64Int64Float64Float64Float64Bool
1204.444441850000.35326.111110.27354547801.0true
28032.16150000.353225.08.5078725523.6false

19. Aggregate shipment: What would have been the annual full-truckload transport cost if both shipments were always shipped together on the same truck?

In [17]:
ash = first(sh)
Out[17]:
DataFrameRow (10 columns)
RowfsαvhdqmaxqᵒTLCᵒisLTL
Int64Float64Int64Int64Float64Int64Float64Float64Float64Bool
1204.444441850000.35326.111110.27354547801.0true
In [18]:
fagg = sum(sh.f)
Out[18]:
100
In [19]:
sagg = fagg / sum(sh.f./sh.s)
Out[19]:
14.311142755428978
In [20]:
vagg = sum(sh.f.*sh.v)/fagg
Out[20]:
21000.0
In [21]:
# 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
Out[21]:
1×6 DataFrame
Rowfsαvhd
Int64Float64Float64Float64Float64Int64
110014.31111.021000.00.3532
In [22]:
# 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)
Out[22]:
1×10 DataFrame
RowfsαvhdqmaxqᵒTLCᵒisLTL
Int64Float64Float64Float64Float64Int64Float64Float64Float64Bool
110014.31111.021000.00.353219.67784.6414258481.9false
In [23]:
# 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