Linear programming optimization for petroleum refinery operations: optimal crude slate selection, unit utilization, and product blending to maximize gross refining margin under capacity, quality, and demand constraints.
This is the downstream complement to a broader oil & gas portfolio:
- Upstream: decline-curve-analysis
- Midstream: midstream-pipeline-hydraulics
Refinery LP is the canonical optimization problem in downstream oil & gas. Industrial tools like Aspen PIMS and Haverly's GRTMPS are LPs (often with non-linear extensions) at their core. Refineries use them to answer:
- What crude slate should we buy this month?
- Given product prices and operational constraints, what's the optimal product mix?
- What's the shadow value of additional FCC capacity?
This project implements a simplified but faithful LP model that captures the essential structure: crudes with assay-driven yields, conversion units (FCC, reformer, hydrotreater), product blending with quality specs, and margin maximization.
- Crude assay handling: YAML-loaded assays for WTI, Brent, Arab Light, Maya — representative of light sweet through heavy sour crude grades
- Process unit models: CDU, FCC, reformer, hydrotreater (and optional VDU)
- Product blending: gasoline (with octane and RVP indices), diesel (with ULSD sulfur spec), jet fuel
- LP formulation: PuLP-based model with CBC solver (no external solver required); typical solve time < 1 second
- Three example refineries of increasing complexity:
- Hydroskimming (CDU + HTU only)
- Conversion (adds reformer)
- Complex (full conversion: CDU + reformer + FCC + HTU)
from refinery_lp.io.examples import complex_refinery
from refinery_lp.optimization.lp_model import RefineryLP
config = complex_refinery(cdu_capacity_bpd=100_000)
result = RefineryLP(config).solve()
print(f"Status: {result.status}")
print(f"Gross margin: ${result.objective_value:,.0f}/day")
print(f"\nOptimal crude slate:")
for crude, bpd in result.crude_purchases.items():
if bpd > 1:
print(f" {crude:>6}: {bpd:>8,.0f} bpd")
print(f"\nProduct slate:")
for product, bpd in result.product_sales.items():
if bpd > 1:
print(f" {product:>14}: {bpd:>8,.0f} bpd")git clone https://github.com/khb-git/downstream-refinery-lp.git
cd downstream-refinery-lp
pip install -e ".[dev]"
pytestdownstream-refinery-lp/
├── refinery_lp/
│ ├── crudes/
│ │ └── assay.py # CrudeAssay + YAML loaders
│ ├── units/
│ │ └── units.py # CDU, FCC, Reformer, Hydrotreater, VDU
│ ├── blending/
│ │ └── blending.py # RVP indices, octane blending, product specs
│ ├── optimization/
│ │ └── lp_model.py # The LP model
│ ├── viz/ # (planned) Sankey diagrams, dashboards
│ └── io/
│ └── examples.py # Pre-configured refinery scenarios
├── data/
│ └── assays/ # Crude assay YAML files
├── tests/
├── notebooks/ # Demo workflows
└── pyproject.toml
Decision variables (all in barrels per day):
crude_buy[c]: barrels of crudecpurchasedcut_to_fcc[c]: AGO from crudecsent to FCCcut_to_reformer[c]: heavy naphtha from crudecsent to reformercut_to_htu[s]: streams sent to hydrotreater<product>_blend[stream]: streams blended into final productsproduct_sale[p]: barrels of finished productpsold
Objective (maximize):
Revenue (product sales × prices)
− crude purchase costs
− variable opex (per-barrel processing costs)
Constraints:
- Crude availability limits
- Per-unit capacity constraints (CDU, FCC, reformer, hydrotreater)
- Mass balance at every node: cut flows out ≤ assay yield × crude in
- Product blend balance: barrels in = barrels of product sold
- Quality specs: ULSD-style sulfur limit on diesel
Each unit has a yield matrix mapping input streams to output streams. The CDU is unique — its yields come from crude-specific assays, so heavy crude produces more residue than light crude even on the same CDU. The FCC, reformer, and hydrotreater have crude-independent yields as a simplification (real refineries use crude-dependent yields).
Several physically non-linear properties are linearized to keep the problem LP-tractable:
- Octane blending: linear-by-volume (real practice uses interaction models like Ethyl RT-70; linear is an industry-standard simplification)
- Sulfur blending: linear on volume basis (slight inaccuracy vs density-weighted)
- RVP blending: uses Chevron blend index (RVP^1.25), which combines linearly on a volume basis
For more accurate quality blending, the next step beyond LP is an MILP or NLP with bilinear terms — the famous "pooling problem" in optimization literature.
For a 100,000 bpd complex refinery with typical mid-cycle prices (gasoline $95/bbl, diesel $100/bbl, heavy sour crude $58/bbl):
- Optimal slate typically favors Maya (heavy sour) given the discount
- Complex refinery margin ~$10-15/bbl crude processed
- Hydroskimming margin substantially lower (often <$5/bbl) due to inability to upgrade residue and low-octane naphtha
- FCC and reformer have positive shadow prices — they're capacity- limited and adding more would increase margin
- Sankey diagram visualization of material flows
- Pooling problem extension (bilinear quality blending)
- Multi-period planning (inventory, contract obligations)
- Demo notebook with sensitivity analysis
- Streamlit dashboard for what-if analysis
- Gary, J.H., Handwerk, G.E., & Kaiser, M.J. (2007). Petroleum Refining: Technology and Economics. CRC Press.
- Maples, R.E. (2000). Petroleum Refinery Process Economics. PennWell.
- Bechtel, B. (1981). "PIMS — Process Industry Modeling System."
- Misener, R. & Floudas, C.A. (2009). "Advances for the Pooling Problem: Modeling, Global Optimization, and Computational Studies." Applied and Computational Mathematics 8(1).
MIT