Batch scheduling, transmix arithmetic, and the Edmonton-to-Burnaby products line
2026-03-10
Every litre of gasoline sold in Vancouver left Edmonton as part of a numbered batch in a 24-inch pipe — separated from the next batch of diesel only by the turbulence that inevitably mixes them at the boundary.
That boundary is the operational challenge this essay is about. The crude oil and gas pipelines in P1–P3 each carry a single commodity. The Trans Mountain refined products line carries four or more simultaneously, sequenced one behind another, and the physical fact of turbulent mixing at each interface creates a contaminated zone — transmix — that cannot be delivered as either product without downgrading or reprocessing. Managing transmix is the discipline of multi-product pipeline operation, and the mathematics that describes it is the Cola equation.
A multi-product pipeline carries different petroleum products sequentially as discrete batches. The Trans Mountain refined products system moves premium gasoline, regular gasoline, ultra-low sulphur diesel (ULSD), and jet fuel through the same 24-inch pipe from Edmonton’s refinery cluster to the Burnaby Marine Terminal — a distance of approximately 1,150 km.
Each product has tight quality specifications:
| Product | Key specification |
|---|---|
| Premium gasoline (91 octane) | RON ≥ 91, Reid vapour pressure limits |
| Regular gasoline (87 octane) | RON ≥ 87, vapour pressure limits |
| ULSD (diesel) | Sulphur ≤ 15 ppm, cetane index ≥ 40 |
| Jet fuel (Jet A) | Flash point ≥ 38°C, freeze point ≤ −47°C |
At every interface between adjacent batches, turbulent mixing creates a contaminated slug — transmix — that exceeds the specification limits of both products. Transmix is segregated at the destination terminal by detecting the interface (using density meters, chromatographs, or fluorescence sensors) and diverting the off-spec slug to a separate tank. It is subsequently downgraded — typically blended into heavy fuel oil at a discounted price — or returned to a refinery for reprocessing.
Batch size matters because the transmix-to-product ratio determines specification compliance. Too small a batch and the transmix represents an unacceptable fraction of the delivery; too large a batch and cycle times grow, destination terminals require more storage, and responsiveness to demand shifts suffers. The minimum acceptable batch size is typically set at three to five times the predicted transmix volume.
The batch cycle model:
t\_{\text{cycle}} = \frac{V\_{\text{total}}}{Q} \qquad V\_{\text{batch}} \geq V\_{\text{min}}
| Symbol | Meaning | Notes |
|---|---|---|
| tcycle | Time to complete one full product cycle | days |
| Vtotal | Total volume of all batches in one cycle | bbl |
| Q | Pipeline flow rate | bbl/day |
| Vbatch | Volume of individual product batch | bbl |
| Vmin | Minimum acceptable batch size | bbl — rule of thumb: 3–5× transmix volume |
The Cola equation is an empirical relationship between pipe geometry, route length, and the volume of transmix generated at each batch interface:
Vmix = C ⋅ (D1.9 ⋅ L)0.5
| Symbol | Meaning | Units / value |
|---|---|---|
| Vmix | Transmix volume at the batch interface | barrels |
| C | Empirical constant | 11.75 (field-calibrated; range: 10–14) |
| D | Internal pipe diameter | inches |
| L | Pipe segment length | miles |
The exponent on D is 1.9 — nearly quadratic. This has a counterintuitive implication: a larger diameter pipe generates more transmix per interface, not less. Doubling the diameter increases transmix by approximately 21.9/2 ≈ 1.87 times. A 36-inch pipe doesn’t just carry more product; it generates proportionally more contamination at each batch boundary than a 24-inch pipe. Larger minimum batch sizes are required to compensate.
For the Trans Mountain refined products line (24-inch diameter, 1,150 km ≈ 714 miles):
Vmix = 11.75 × (241.9 × 714)0.5 ≈ 7, 100 bbl
With four products and a safety factor of 4, minimum batch size is 4 × 7, 100 = 28, 400 bbl per product grade. The pipeline cycle time at 150,000 bbl/day throughput is:
t\_{\text{cycle}} = \frac{4 \times 28{,}400}{150{,}000} \approx 0.76 \text{ days}
But actual batch sizes are much larger than the minimum — set by demand volumes and the transit time from Edmonton to Burnaby (~7 days). A product dispatched today arrives in Burnaby in a week; the scheduling must balance in-transit inventory against Burnaby terminal storage capacity.
Edmonton refinery cluster (2024):
| Refinery | Operator | Capacity (bbl/day) |
|---|---|---|
| Strathcona | Imperial Oil (Esso) | ~187,000 |
| Edmonton | Suncor Energy | ~142,000 |
| Redwater | Federated Co-op | ~55,000 |
| Scotford | Shell / CNOOC-Shell JV | ~100,000 |
Total: ~484,000 bbl/day nameplate capacity. Edmonton is the largest refining centre in Canada. Its geographic position — landlocked, served by a single products export pipeline — makes the Trans Mountain refined products line a critical infrastructure link. When that line has a disruption, BC’s fuel supply depends on terminal inventory and alternative supply by marine barge or rail.
The Burnaby Marine Terminal must hold enough product to supply British Columbia’s demand continuously despite the 7-day pipeline transit time from Edmonton. Storage capacity sets the response time to demand shifts and the buffer against supply disruptions.
Vstorage = Ddaily ⋅ Tlead + Vsafety
= Ddaily ⋅ (Ttransit + Tsafety)
| Symbol | Meaning | Typical value |
|---|---|---|
| Ddaily | Average daily demand at terminal | bbl/day |
| Ttransit | Pipeline transit time from Edmonton | 7 days |
| Tsafety | Safety stock duration | 7 days (minimum; often 10–14 days) |
| Vstorage | Required terminal storage tank capacity | bbl |
At approximately 110,000 bbl/day total refined products demand for the BC market (distributed across the four product grades), with 7-day transit and 7-day safety stock:
Vstorage = 110, 000 × (7 + 7) = 1, 540, 000 bbl
That is 36+ standard 42,000-barrel storage tanks — a significant infrastructure commitment. Tank capacity at Burnaby is a binding constraint on how quickly the product mix can respond to demand shifts. When jet fuel demand spikes (airline capacity additions, seasonal peaks), terminal storage limits how quickly supply can respond — the pipeline is already full of last week’s batch schedule, and seven days of transit stand between an Edmonton dispatch decision and a Burnaby delivery.
The storage constraint is also the reason why BC fuel prices respond slowly to world oil price changes: the terminal’s product buffer means retailers are drawing down inventory priced at last week’s cost, not today’s refinery margin.
import numpy as np
MILES_PER_KM = 0.621371
def cola_transmix(diameter_in: float, length_miles: float, C: float = 11.75) -> float:
"""
Cola equation: estimated transmix volume at a batch interface.
Parameters
----------
diameter_in : internal pipe diameter (inches)
length_miles : pipe segment length (miles)
C : empirical constant — default 11.75; range 10–14
Returns
-------
float : transmix volume (barrels)
"""
return C * (diameter_in**1.9 * length_miles)**0.5
def batch_schedule(products: dict, flow_rate_bbl_day: float,
transmix_bbl: float, safety_factor: float = 4.0) -> dict:
"""
Simple batch schedule for a multi-product pipeline.
Parameters
----------
products : dict mapping product name → daily demand (bbl/day)
flow_rate_bbl_day: pipeline throughput (bbl/day)
transmix_bbl : Cola transmix volume at each interface (bbl)
safety_factor : minimum batch = safety_factor × transmix_bbl
Returns
-------
dict with cycle time, batch sizes, and pipeline utilisation
"""
n_products = len(products)
min_batch_bbl = safety_factor * transmix_bbl
total_min_volume = min_batch_bbl * n_products
cycle_time_days = total_min_volume / flow_rate_bbl_day
total_demand = sum(products.values())
batch_sizes = {}
for name, demand in products.items():
proportional = demand * cycle_time_days
batch_sizes[name] = max(proportional, min_batch_bbl)
total_cycle_vol = sum(batch_sizes.values())
actual_cycle_time = total_cycle_vol / flow_rate_bbl_day
return {
"min_batch_bbl": min_batch_bbl,
"transmix_bbl": transmix_bbl,
"n_interfaces": n_products,
"transmix_total_bbl": transmix_bbl * n_products,
"cycle_time_days": actual_cycle_time,
"batch_sizes": batch_sizes,
"total_cycle_vol": total_cycle_vol,
"utilisation": total_demand / flow_rate_bbl_day,
}
def terminal_inventory(daily_demand_bbl: float, transit_days: float,
safety_days: float = 7.0) -> float:
"""Required terminal storage capacity (bbl)."""
return daily_demand_bbl * (transit_days + safety_days)
# ── Reference values — Trans Mountain refined products line ────────────────
# Edmonton → Burnaby, BC: ~1,150 km; D = 24 inches
diameter_in = 24.0
length_km = 1_150.0
length_miles = length_km * MILES_PER_KM
v_mix = cola_transmix(diameter_in, length_miles)
print(f"Trans Mountain refined products line")
print(f" Diameter : {diameter_in:.0f} inches")
print(f" Length : {length_km:.0f} km ({length_miles:.0f} miles)")
print(f" Transmix : {v_mix:,.0f} bbl per interface")
print()
# Product mix — illustrative daily demand into Burnaby terminal
products = {
"Premium gasoline": 12_000, # bbl/day
"Regular gasoline": 45_000,
"Ultra-low sulphur diesel": 38_000,
"Jet fuel": 15_000,
}
flow_rate = 150_000 # bbl/day — Trans Mountain products capacity (approximate)
sched = batch_schedule(products, flow_rate, v_mix, safety_factor=4.0)
print(f"Batch schedule (safety factor = 4× transmix):")
print(f" Min batch size : {sched['min_batch_bbl']:,.0f} bbl")
print(f" Cycle time : {sched['cycle_time_days']:.1f} days")
print(f" Transmix total : {sched['transmix_total_bbl']:,.0f} bbl/cycle ({sched['n_interfaces']} interfaces)")
print(f" Utilisation : {sched['utilisation']*100:.1f}%")
print()
for name, vol in sched["batch_sizes"].items():
print(f" {name:<30}: {vol:>8,.0f} bbl/batch")
print()
# Burnaby terminal storage requirement
total_demand = sum(products.values())
transit_days = 7.0
storage = terminal_inventory(total_demand, transit_days, safety_days=7.0)
print(f"Burnaby terminal storage requirement:")
print(f" Daily demand : {total_demand:,.0f} bbl/day")
print(f" Transit time : {transit_days:.0f} days")
print(f" Safety stock : 7 days")
print(f" Storage needed : {storage:,.0f} bbl ({storage/42_000:.0f} × 42,000-bbl tanks)")Output:
Trans Mountain refined products line
Diameter : 24 inches
Length : 1,150 km (714 miles)
Transmix : 7,049 bbl per interface
Batch schedule (safety factor = 4× transmix):
Min batch size : 28,196 bbl
Cycle time : 0.8 days
Transmix total : 28,196 bbl/cycle (4 interfaces)
Utilisation : 73.3%
Premium gasoline : 28,196 bbl/batch
Regular gasoline : 28,196 bbl/batch
Ultra-low sulphur diesel : 28,196 bbl/batch
Jet fuel : 28,196 bbl/batch
Burnaby terminal storage requirement:
Daily demand : 110,000 bbl/day
Transit time : 7 days
Safety stock : 7 days
Storage needed : 1,540,000 bbl (37 × 42,000-bbl tanks)
The cell below lets you explore transmix sensitivity to pipe geometry and the effect of product count on cycle time. The most counterintuitive result is the diameter scaling: increase diameter_in from 24 to 30 and watch transmix grow despite the larger pipe — that is the D1.9 exponent at work. Increasing the number of products from four to six adds two interfaces, grows minimum cycle time by 50%, and requires larger terminal storage to buffer the longer batch cycle.
import numpy as np
MILES_PER_KM = 0.621371
# ── Parameters — change these ──────────────────────────────────────────────
diameter_in = 24.0 # pipe diameter (inches) — try: 16–36
length_km = 1_150.0 # route length (km) — Edmonton→Burnaby: 1150; try: 500–2000
flow_rate_bblday = 150_000 # pipeline capacity (bbl/day) — try: 80_000–200_000
safety_factor = 4.0 # minimum batch = N × transmix — try: 3–6
n_products = 4 # number of product grades in the schedule — try: 3–6
transit_days = 7.0 # pipeline transit time (days) — try: 3–12
daily_demand = 110_000 # total daily demand at destination terminal (bbl/day)
# ── Transmix calculation ───────────────────────────────────────────────────
length_miles = length_km * MILES_PER_KM
v_mix = 11.75 * (diameter_in**1.9 * length_miles)**0.5
# ── Batch schedule ─────────────────────────────────────────────────────────
min_batch = safety_factor * v_mix
total_min_vol = min_batch * n_products
cycle_days = total_min_vol / flow_rate_bblday
transmix_total = v_mix * n_products
utilisation = daily_demand / flow_rate_bblday
# ── Terminal storage ───────────────────────────────────────────────────────
storage_bbl = daily_demand * (transit_days + 7.0) # transit + 7-day safety stock
# ── Output ─────────────────────────────────────────────────────────────────
print(f"Pipeline geometry:")
print(f" {diameter_in:.0f}-inch pipe, {length_km:.0f} km ({length_miles:.0f} miles)")
print()
print(f"Transmix per interface : {v_mix:,.0f} bbl")
print(f"Total transmix/cycle : {transmix_total:,.0f} bbl ({n_products} interfaces)")
print(f"Min batch size : {min_batch:,.0f} bbl ({safety_factor:.0f}× transmix)")
print(f"Cycle time : {cycle_days:.1f} days ({n_products} products)")
print()
print(f"Pipeline utilisation : {utilisation*100:.1f}%")
print(f"Terminal storage needed : {storage_bbl:,.0f} bbl")
print(f" ({storage_bbl/42_000:.0f} × 42,000-bbl tanks)")
print()
print("Transmix sensitivity to pipe diameter:")
for d in [16, 20, 24, 30, 36]:
vm = 11.75 * (d**1.9 * length_miles)**0.5
print(f" D={d:2d} in: transmix = {vm:>6,.0f} bbl")
Each of the four commodity systems — crude oil, NGL, natural gas, refined products — can be modelled in isolation using the equations developed in P1 through P4. But Alberta’s energy economy operates all four simultaneously, and the systems interact: NGL volumes depend on gas production, diluent volumes depend on bitumen output, refined products volumes depend on refinery utilisation. P5 takes all four systems as a single directed graph and asks the overarching question: what is the total export capacity of Alberta’s energy network, and where is the binding constraint?
Cluster P — Pipeline Connectivity · Essay 4 of 5 · Difficulty: 2