Where every ton of board goes
64 US containerboard mills, 441 box markets, and the min-cost flow between them. Hover anything; pick a scenario on the right.
Board moved by distance band, October 2026 (Mt)
Integration: model vs disclosed
Planned outages: capacity offline (kt per month, mills down)
Network cost of that month's outages ($M, monthly rate)
Knock out one mill: extra network cost per year
Network cost is freight plus outside purchases valued at $150/t plus unmet demand at $400/t. Click a bar to see the re-routed map.
One corrugator, in numbers
Width classes at these settings
Markets that import sheets
One day of board, end to end (short tons; hover a band)
Loads on the move, per day
Conversions used
What a unilateral $70/t increase does to open-market volume
Integrated tonnage does not move: a company's own box plants keep buying from its own mills. The open-market tons are the ones that can walk, and in a market where the next mill is $5/t away, they all do. Real increases hold because the producers announce them together and the index follows.
Distribution of the gap to the second-cheapest mill ($/t)
Open-market tons by owner, October 2026
Published price points
Mullen to ECT, in board weight
The 1991 change to Rule 41 / Item 222 let boxes be specified by edge crush instead of burst. Lighter liners carry the same stacking load, and the industry's tons per thousand square feet fell. The calculator prices both specs so the gap is visible by market.
Diesel and the freight bill
The loop in numbers
Named plants from the EPA Facility Registry
Proxy vs measured: demand share by state
Board demand by Census division and end use (Mt, allocated)
Model vs record
Error bars
Did freight position predict the 2025 closures?
The model
Each mill is a source with a capacity and a set of grades. Each demand point wants a fixed number of tons of liner and of medium. Moving a ton costs freight, which rises with distance and switches from truck to rail near 450 road miles. Minimize freight subject to meeting demand without exceeding any mill: a transportation problem, the special case of min-cost flow where every arc runs from a source to a sink. Two things about this industry break the plain version, and both are fixed by adding nodes rather than changing the algorithm.
First, the big companies are integrated: a ton of IP board goes to the nearest IP plant. Each integrated owner gets its own copy of every demand point holding its share of the local market (IP 31%, Smurfit Westrock 20%, PCA 15.5%, Georgia-Pacific 9.5%, Pratt 6%, Green Bay 3%, Cascades 2%; 13% independents), served only by its own mills; an arc from the open market into each copy at $150/t lets an owner buy outside when short. Second, mills make specific grades: splitting each mill into a liner node and a medium node behind one capacity arc keeps kraft mills out of the medium market without a second commodity. With those expansions it is one min-cost flow, solved as an LP by HiGHS.
min Σ f_ij (x_ijg + y_ijg) + Σ f_ip z_ip + 150 Σ v_kjg + 400 Σ u_jg
s.t. Σ_{j,g} (x_ijg + y_ijg) + Σ_p z_ip ≤ c_i a_i # mill capacity, a_i = availability this month
Σ_{i owned by k} x_ijg + v_kjg = s_k φ_g D_j # owner k's box plants at j
Σ_i y_ijg − Σ_k v_kjg + u_jg = s_open φ_g D_j # open market at j
Σ_i z_ip = E_p # exports through gateway p (kraft mills only)
f(d) = min(8 + 0.125 d·ft, 35 + 0.065 d·fr) $/ton, d = 1.2 × great-circle miles, ft/fr = diesel factors (22% / 12% fuel share at $3.75)
# October 2026 instance: 95,211 variables, 7,141 constraints, 1.2 s with HiGHS. 55 scenarios on this page.
Duals. The multiplier on a mill's capacity row is what one more ton of that mill saves the network (the scarcity layer). The multiplier on a demand row is the delivered cost of the marginal ton in that county (the delivered-cost layer and the calculator's freight). Prospects rank, for every market, the active mills by delivered freight; the gap between the first two is the switching threshold. Price shocks add $70/t to one owner's open-market arcs only. The fiber loop is a second transportation LP: counties supply recovered OCC (92% of consumption, plus 3% for imported goods), recycled mills demand 1.15 t per ton of recycled board they ship, export docks take the AF&PA/USITC export tonnage, and freight decides. The maintenance calendar is an assignment problem: for each mill and month the cost of an outage is estimated as capacity dual × tons lost, from twelve no-outage solves, then assigned with at most 10 mills per month and two per owner (a small MILP), and re-solved month by month to measure the true penalty. Knockouts remove one mill and re-solve. The backtest puts 2026 demand on the 2024 mill set and asks which mills the LP leaves idle.
Assumptions that matter
- Demand geography is population tilted by state manufacturing share, rolled up to 441 points; two thirds liner, one third medium; 34.5 Mt domestic + 4.6 Mt exports in 2024, 32.2 + 3.6 in 2026. Canadian mills sell 35% of capacity south; Mexican mills are listed, not modeled.
- Owner shares of the box market are national; a state-level plant count by owner (in the 10-K property lists) would make them regional.
- Mill capacities are public figures of mixed vintage; owner totals reconcile to the 10-Ks, single IP or PCA legacy mills could be off by 10%.
- Freight is two straight lines with a diesel factor. No lanes, backhaul, car supply, transload economics or contracts; no swaps beyond the $150/t outside-purchase arc.
- The outage calendar (9 days kraft, 5 recycled, 3% unplanned) and the monthly demand index are the author's; companies' actual outage months are in their earnings guidance.
The core of the LP, in Python
owners = [k for k in owner_shares if (M.owner==k).any()]
s_open = 1.0 - sum(owner_shares[k] for k in owners)
grades = [("L", grade_split[0], liner_ok), ("M", grade_split[1], medium_ok)]
# ---- variable bookkeeping
cols = [] # (kind, k, m, j, g) ; kind in x,y,v,z,u
c_obj = []
rows_cap, rows_own, rows_open, rows_exp = [], {}, {}, []
A_r, A_c, A_v = [], [], []
def addvar(kind, k, m, j, g, c):
cols.append((kind, k, m, j, g)); c_obj.append(c); return len(cols)-1
# constraint indices
cap_row = {m: m for m in range(nM)}
nrow = nM
own_row = {}
for k in owners:
for j in range(J):
for gi,(g,_,_) in enumerate(grades):
own_row[(k,j,g)] = nrow; nrow += 1
open_row = {}
for j in range(J):
for g,_,_ in grades:
open_row[(j,g)] = nrow; nrow += 1
exp_row = {p: nrow+p for p in range(len(PORTS))}; nrow += len(PORTS)
def coef(r, cidx, v): A_r.append(r); A_c.append(cidx); A_v.append(v)
owner_of = M.owner.values
for m in range(nM):
if cap[m] <= 0: continue
for g, phi, ok in grades:
if not ok[m]: continue
for j in range(J):
# open-market arc
ci = addvar("y", None, m, j, g, cost[m,j] + surcharge[m]); coef(cap_row[m], ci, 1); coef(open_row[(j,g)], ci, 1)
k = owner_of[m]
if k in owner_shares and k in owners:
ci = addvar("x", k, m, j, g, cost[m,j]); coef(cap_row[m], ci, 1); coef(own_row[(k,j,g)], ci, 1)
if kraft[m]:
for p in range(len(PORTS)):
ci = addvar("z", None, m, None, p, pcost[m,p]); coef(cap_row[m], ci, 1); coef(exp_row[p], ci, 1)
for k in owners:
for j in range(J):
for g,_,_ in grades:
ci = addvar("v", k, None, j, g, premium); coef(own_row[(k,j,g)], ci, 1); coef(open_row[(j,g)], ci, -1)
for j in range(J):
for g,_,_ in grades:
ci = addvar("u", None, None, j, g, unmet_penalty); coef(open_row[(j,g)], ci, 1)
nvar = len(cols)
A = coo_matrix((A_v, (A_r, A_c)), shape=(nrow, nvar)).tocsr()
# split into inequality (capacity rows) and equality rows
A_ub = A[:nM]; b_ub = cap
A_eq = A[nM:]
b_eq = np.zeros(nrow-nM)
for k in owners:
for j in range(J):
for g, phi, _ in grades:
b_eq[own_row[(k,j,g)]-nM] = owner_shares[k]*phi*D[j]
for j in range(J):
for g, phi, _ in grades:
b_eq[open_row[(j,g)]-nM] = s_open*phi*D[j]
for p in range(len(PORTS)):
b_eq[exp_row[p]-nM] = export_tons*PORTS.share.values[p]
res = linprog(np.array(c_obj), A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq=b_eq, bounds=(0,None), method="highs")Sources
IP 10-K FY2024 · Smurfit Westrock 10-K FY2024 · PCA 10-K FY2025 · AF&PA 2025 capacity survey (Packaging Dive) · Fastmarkets on 2025–26 capacity cuts · IP Savannah and Riceboro closures · GP Cedar Springs · PCA buys Greif containerboard · Fibre Box Association · AICC on box plants · EIA diesel · AF&PA recycling statistics · us-atlas · JHU county population table · Every mill row and every price point carries its own citation in data/ and notes/.
What the next version needs
- EPA Facility Registry Service bulk CSV, NAICS 322211: the free list of every US box plant with coordinates. Replaces the demand proxy. No key.
- Census County Business Patterns key (free): plants and employment by county, to weight demand and check the list.
- Google Places key (~$50–100 one-time) or Overpass (free): plant discovery and contact details for the call sheet.
- EIA key (free): weekly diesel by region, live into the freight factor.
- Fastmarkets RISI (paid): the actual price series, East and West; AF&PA and FBA reports (members): mill-level grades, regional and end-use shipments.
- AICC/FBA directories and a job-posting feed: ground truth for independents and a cheap corrugator-vs-sheet-plant tag.
Details, endpoints and the scripts that consume each one: NEEDS.md and ingest/.