The Integrated Network
modelling Level 4

The Integrated Network

Crude oil, NGLs, natural gas, and refined products each have their own pipeline systems — but they share nodes, rights-of-way, and economic logic. Treating the full Alberta hydrocarbon network as a directed graph reveals where the real constraints lie, which nodes are most critical, and how the netback price surface varies across the continent.

Prerequisites: Graph theory (directed graphs, max flow min cut), netback price surface, network centrality

Updated 50 min read

1. The Question

When all four pipeline systems are treated as a single network, what does the geometry reveal that individual system analyses miss?

Essays P1 through P4 examined Alberta’s pipeline infrastructure one commodity at a time: crude oil, NGLs and condensate, natural gas, refined products. Each essay found a large, multi-directional system with significant export capacity and some specific constraints.

But the four systems are not independent. They share physical nodes — Hardisty is a hub for crude, condensate, and NGL gathering; Fort Saskatchewan is the fractionation hub for NGLs and the feedstock hub for petrochemicals; Edmonton is the origin of the refined products system and a major compression hub for the gas system. Constraints in one system propagate to others: a condensate shortage constrains dilbit production, which reduces crude throughput, which tightens Enbridge nominations.

Treating the full system as a directed graph — with nodes representing hubs and terminals, and edges representing pipeline segments with defined capacities — allows us to apply the mathematics of network flow theory. This reveals which nodes are structurally critical, what the maximum sustainable throughput is, and how the netback price surface changes across the continent as a function of transport costs and market conditions.


2. The Conceptual Model

Directed graphs for pipeline networks

A directed graph (digraph) $G = (V, E)$ consists of:

  • A set of vertices (nodes) $V$ — pipeline hubs, terminals, refineries, market price points
  • A set of directed edges $E$ — pipeline segments, each with a direction of flow and a capacity $c(u,v)$ in appropriate units

For a multi-commodity pipeline network, each edge carries one or more commodity flows. We define:

  • $f_k(u,v)$ — the flow of commodity $k$ on edge $(u,v)$
  • $c_k(u,v)$ — the capacity of edge $(u,v)$ for commodity $k$
  • The feasibility constraint: $f_k(u,v) \leq c_k(u,v)$
  • The conservation constraint at each node: flow in = flow out (except at source and sink nodes)

The max-flow min-cut theorem

For a single-commodity network with a defined source $s$ and sink $t$, the maximum flow from $s$ to $t$ equals the minimum cut — the minimum total capacity of any set of edges whose removal disconnects $s$ from $t$.

\[\max \text{ flow}(s,t) = \min \text{ cut}(s,t)\]

This theorem, due to Ford and Fulkerson (1956), is one of the most useful results in combinatorial optimization. For pipeline analysis it means: the binding constraint on total system throughput is always a cut — a set of pipeline segments whose combined capacity is smaller than any other disconnecting set.

Finding the minimum cut tells us which segments, if constrained or disrupted, most limit total system throughput. This is the network’s structural vulnerability.

Centrality measures

Betweenness centrality of a node $v$ measures how often it lies on the shortest path between other node pairs:

\[C_B(v) = \sum_{s \neq v \neq t} \frac{\sigma_{st}(v)}{\sigma_{st}}\]

where $\sigma_{st}$ is the total number of shortest paths from $s$ to $t$, and $\sigma_{st}(v)$ is the number that pass through $v$.

A node with high betweenness centrality is a structural bottleneck: much of the network’s flow passes through it. Its disruption degrades network performance disproportionately. In Alberta’s pipeline network, we expect Hardisty and Fort Saskatchewan to show high betweenness centrality.

The netback price surface

Across the pipeline network, every destination market has an associated netback price for a given commodity. Mapping these values spatially creates a price surface — a function over geographic space where the value at each market point is the producer netback after subtracting all transport costs along the cheapest path to that market.

Formally, if we define $w(e)$ as the transport cost on edge $e$, the netback at destination $d$ for commodity $k$ is:

\[P_{\text{netback}}(d, k) = P_{\text{market}}(d, k) - \min_{\text{path}} \sum_{e \in \text{path}} w_k(e)\]

The minimum-cost path from the Alberta production node to market $d$ determines the achievable netback. If multiple paths exist, the best netback uses the lowest-cost routing. If only one path exists, the operator of that path has pricing power — the basis differential cannot be arbitraged away.


3. The Mathematical Model

Max-flow: the Ford-Fulkerson framework

For a network with source $s$, sink $t$, and edge capacities $c(u,v)$, the Ford-Fulkerson algorithm finds maximum flow by iteratively finding augmenting paths — paths from $s$ to $t$ with available residual capacity — and pushing flow along them until no augmenting path exists.

The residual graph $G_f$ has residual capacity:

\[c_f(u,v) = c(u,v) - f(u,v)\]

An augmenting path exists if there is a path in $G_f$ from $s$ to $t$ with $c_f > 0$ on every edge. The Edmonds-Karp variant uses breadth-first search to find augmenting paths, guaranteeing $O(VE^2)$ runtime.

Network flow conservation

At every intermediate node $v$ (not source or sink), flow is conserved:

\[\sum_{u : (u,v) \in E} f(u,v) = \sum_{w : (v,w) \in E} f(v,w)\]

In a pipeline network, this means the total barrels entering a hub equals the total barrels leaving it — no storage accumulation in the steady-state model (storage tanks at hubs are represented separately as capacity buffers, not flow nodes).

Minimum cost flow for multi-commodity networks

The real Alberta network is a multi-commodity flow problem: crude, NGLs, gas, and products flow simultaneously on overlapping (but not identical) network topologies. The general multi-commodity flow problem minimizes total transport cost subject to capacity and conservation constraints:

\[\min \sum_k \sum_{(u,v) \in E} c_k(u,v) \cdot f_k(u,v)\]

subject to:

  • $0 \leq f_k(u,v) \leq \text{cap}_k(u,v)$ for all $k$, $(u,v)$
  • Flow conservation at each node for each commodity $k$
  • Shared capacity constraints where pipelines carry multiple products

This is a linear program solvable with standard LP solvers. For understanding, the single-commodity max-flow model on each product subsystem captures the essential structure.


4. Worked Example by Hand

Minimum cut on the Alberta crude oil export system

Consider a simplified version of the crude export network with four nodes and five edges:

Node A: Alberta production (Fort McMurray / Hardisty)
Node B: U.S. Midwest hub (Superior WI)
Node C: Pacific tidewater (Burnaby BC)
Node D: Gulf Coast hub (Cushing OK / Port Arthur TX)

Edges (capacities in 000 bbl/d):
A → B : 3,100  (Enbridge Mainline)
A → C :   890  (Trans Mountain)
A → D :   870  (Keystone + Express, simplified)
B → D :   800  (Capline, southbound, simplified)

Find the minimum cut separating A from all market sinks.

A cut is a partition of nodes into two sets $S$ (containing $A$) and $T$ (containing all market nodes). The cut capacity is the sum of capacities of edges from $S$ to $T$.

Cut 1: $S = {A}$, $T = {B, C, D}$

Cut edges: $A \to B$, $A \to C$, $A \to D$

\[\text{Capacity} = 3{,}100 + 890 + 870 = 4{,}860 \text{ thousand bbl/d}\]

Cut 2: $S = {A, B}$, $T = {C, D}$

Cut edges from $S$ to $T$: $A \to C$ (890), $A \to D$ (870), $B \to D$ (800)

\[\text{Capacity} = 890 + 870 + 800 = 2{,}560 \text{ thousand bbl/d}\]

Cut 2 is smaller. But let us check: is Cut 1 actually achievable as a flow? The max flow is bounded by the minimum cut, which appears to be Cut 2 at 2,560 thousand bbl/d — but Cut 1 gives 4,860. The minimum cut is the smaller of the two, which is Cut 2 at 2,560 thousand bbl/d.

This means that even if the Enbridge Mainline (A→B) is fully utilized, the onward connections from B to the Gulf Coast (B→D at 800 thousand bbl/d) constrain total Alberta-to-Gulf-Coast flow — the Mainline itself is not the binding constraint for Alberta-to-Gulf routing.

Alberta’s maximum crude flow to the Gulf Coast specifically is constrained by the combined capacity of (1) direct Alberta-to-Cushing routes and (2) the connecting pipeline from Superior to Cushing — not by the Enbridge Mainline itself, which has ample capacity to deliver to the Midwest but limited onward connections southward.

Step 2 — Netback comparison by path:

At WTI $75 USD/bbl, CAD/USD 0.73:

Destination Market price (CAD/bbl) Transport cost (CAD/bbl) Netback (CAD/bbl)
Superior WI (Enbridge) $93.2 $9.9 $83.3
Burnaby BC (TMX) $97.3 $15.1 $82.2
Port Arthur TX (Keystone) $95.9 $14.4 $81.5

The minimum-cost path from Alberta to each market determines the maximum achievable netback. Superior WI offers the best netback in this scenario — lower transport cost more than compensates for the slightly lower destination price.


5. Computational Implementation

# Foundation Implementation
# Pipeline network as directed graph: max-flow and netback surface

from collections import defaultdict, deque

class PipelineNetwork:
    """
    Directed graph representing a pipeline network.
    Supports max-flow (Edmonds-Karp BFS) and netback surface calculation.
    """

    def __init__(self):
        self.nodes = {}          # node_id -> {"name", "type", "lat", "lon"}
        self.edges = []          # list of (u, v, capacity, tariff, commodity)
        self.adj = defaultdict(dict)  # adjacency for flow algorithms

    def add_node(self, node_id: str, name: str, node_type: str,
                  lat: float = 0.0, lon: float = 0.0):
        self.nodes[node_id] = {"name": name, "type": node_type,
                                "lat": lat, "lon": lon}

    def add_edge(self, u: str, v: str, capacity: float,
                  tariff_cad_bbl: float = 0.0, commodity: str = "crude"):
        self.edges.append((u, v, capacity, tariff_cad_bbl, commodity))
        # Build residual graph structure
        if v not in self.adj[u]:
            self.adj[u][v] = {"cap": 0, "flow": 0, "tariff": tariff_cad_bbl}
        self.adj[u][v]["cap"] += capacity
        if u not in self.adj[v]:
            self.adj[v][u] = {"cap": 0, "flow": 0, "tariff": 0}  # reverse edge

    def _bfs_path(self, source: str, sink: str) -> list:
        """BFS to find augmenting path in residual graph."""
        visited = {source}
        queue = deque([[source]])
        while queue:
            path = queue.popleft()
            node = path[-1]
            if node == sink:
                return path
            for neighbour, data in self.adj[node].items():
                residual = data["cap"] - data["flow"]
                if neighbour not in visited and residual > 0:
                    visited.add(neighbour)
                    queue.append(path + [neighbour])
        return []

    def max_flow(self, source: str, sink: str) -> float:
        """Edmonds-Karp max-flow algorithm."""
        total_flow = 0.0
        while True:
            path = self._bfs_path(source, sink)
            if not path:
                break
            # Find bottleneck
            bottleneck = min(
                self.adj[path[i]][path[i+1]]["cap"]
                - self.adj[path[i]][path[i+1]]["flow"]
                for i in range(len(path) - 1)
            )
            # Update flows
            for i in range(len(path) - 1):
                self.adj[path[i]][path[i+1]]["flow"] += bottleneck
                self.adj[path[i+1]][path[i]]["flow"] -= bottleneck
            total_flow += bottleneck
        return total_flow

    def shortest_cost_path(self, source: str, target: str) -> tuple:
        """
        Dijkstra's algorithm on tariff weights.
        Returns (total_tariff, path).
        """
        import heapq
        dist = {n: float('inf') for n in self.nodes}
        dist[source] = 0.0
        prev = {}
        pq = [(0.0, source)]
        while pq:
            cost, u = heapq.heappop(pq)
            if cost > dist[u]:
                continue
            for v, data in self.adj[u].items():
                if data["cap"] > 0:  # only forward edges
                    new_cost = cost + data.get("tariff", 0.0)
                    if new_cost < dist[v]:
                        dist[v] = new_cost
                        prev[v] = u
                        heapq.heappush(pq, (new_cost, v))
        # Reconstruct path
        path = []
        node = target
        while node in prev:
            path.append(node)
            node = prev[node]
        path.append(source)
        return dist[target], list(reversed(path))

    def netback_surface(self,
                         source: str,
                         markets: dict,
                         quality_discount_cad: float = 19.0) -> dict:
        """
        Compute netback at each market destination.
        markets: {node_id: market_price_cad_bbl}
        """
        results = {}
        for market_id, price in markets.items():
            tariff, path = self.shortest_cost_path(source, market_id)
            netback = price - tariff - quality_discount_cad
            results[market_id] = {
                "market_price": price,
                "transport_tariff": tariff,
                "quality_discount": quality_discount_cad,
                "netback": netback,
                "path": path,
                "n_hops": len(path) - 1
            }
        return results


# --- Build Alberta crude network ---
net = PipelineNetwork()

# Nodes: Alberta production hubs
net.add_node("HARDISTY",  "Hardisty AB",            "hub",     52.67, -111.30)
net.add_node("EDMONTON",  "Edmonton AB",             "hub",     53.55, -113.49)
net.add_node("FSK",       "Fort Saskatchewan AB",    "hub",     53.71, -113.21)

# Nodes: Canadian intermediate
net.add_node("SUPERIOR",  "Superior WI",             "hub",     46.72,  -92.10)
net.add_node("BURNABY",   "Burnaby BC (Westridge)",  "terminal",49.25, -122.95)
net.add_node("SARNIA",    "Sarnia ON",               "terminal",42.97,  -82.40)

# Nodes: U.S. market hubs
net.add_node("PATOKA",    "Patoka IL",               "market",  38.75,  -88.63)
net.add_node("CUSHING",   "Cushing OK",              "market",  35.98,  -96.77)
net.add_node("PORT_ARTHUR","Port Arthur TX",         "market",  29.90,  -93.93)
net.add_node("GUERNSEY",  "Guernsey SD",             "market",  42.27, -104.73)

# Edges: Alberta export pipelines (capacity in 000 bbl/d, tariff in CAD/bbl)
net.add_edge("HARDISTY", "SUPERIOR",    3100, 8.2,  "crude")   # Enbridge Mainline
net.add_edge("HARDISTY", "BURNABY",      890, 11.0, "crude")   # Trans Mountain
net.add_edge("HARDISTY", "GUERNSEY",     280, 9.0,  "crude")   # Express Pipeline
net.add_edge("HARDISTY", "CUSHING",      590, 10.5, "crude")   # Keystone (direct segment)

# Edges: U.S. connecting pipelines
net.add_edge("SUPERIOR",  "PATOKA",      900, 2.5,  "crude")   # Enbridge Line 6B
net.add_edge("SUPERIOR",  "SARNIA",      500, 2.8,  "crude")   # Enbridge Line 5 / 78
net.add_edge("PATOKA",    "CUSHING",     400, 2.0,  "crude")   # Capline (reversed)
net.add_edge("CUSHING",   "PORT_ARTHUR", 600, 1.8,  "crude")   # Seaway / Longhorn
net.add_edge("GUERNSEY",  "CUSHING",     280, 3.5,  "crude")   # Platte Pipeline

# Market prices (CAD/bbl) — WTI &#36;75 USD, exchange 0.73, location differentials
MARKETS = {
    "PATOKA":     102.7,   # WTI Midwest
    "CUSHING":    102.7,   # WTI Cushing
    "PORT_ARTHUR": 104.1,  # LLS premium
    "BURNABY":    106.0,   # Asia-Pacific (Brent-linked, premium)
    "SARNIA":      100.5,  # Ontario refineries
    "GUERNSEY":    101.0,  # WTI Plains
}

# --- Max flow analysis ---
print("=== Alberta Crude Network: Max Flow Analysis ===\n")
for sink_id in ["PATOKA", "CUSHING", "PORT_ARTHUR", "BURNABY"]:
    # Reset flows
    for u in net.adj:
        for v in net.adj[u]:
            net.adj[u][v]["flow"] = 0
    mf = net.max_flow("HARDISTY", sink_id)
    sink_name = net.nodes[sink_id]["name"]
    print(f"  Max flow Hardisty → {sink_name:<25}: {mf:>8,.0f} 000 bbl/d")

# --- Netback surface ---
print(f"\n=== Netback Price Surface (CAD/bbl) ===\n")
print(f"  WTI: &#36;75 USD/bbl | Quality discount: &#36;19 CAD/bbl | Exchange: 0.73\n")

surface = net.netback_surface("HARDISTY", MARKETS, quality_discount_cad=19.0)

print(f"  {'Market':<28} {'Price':>8} {'Tariff':>8} {'Discount':>10} {'Netback':>10}  Path")
print(f"  {'-'*85}")
for market_id, r in sorted(surface.items(), key=lambda x: -x[1]["netback"]):
    name = net.nodes[market_id]["name"]
    path_str = "".join(net.nodes[n]["name"].split(" ")[0]
                           for n in r["path"])
    print(f"  {name:<28} ${r['market_price']:>6.1f}  ${r['transport_tariff']:>5.1f}  "
          f"  -${r['quality_discount']:>5.1f}  ${r['netback']:>8.1f}  {path_str}")

Professional Implementation

# Professional Implementation
# Multi-commodity network: centrality, vulnerability, and full netback surface

import heapq
from collections import defaultdict, deque
from dataclasses import dataclass, field
from typing import Optional
import math

@dataclass
class Node:
    node_id: str
    name: str
    node_type: str    # "production", "hub", "terminal", "market"
    lat: float
    lon: float
    commodity_types: list = field(default_factory=list)

@dataclass
class Edge:
    source: str
    target: str
    capacity: float        # 000 bbl/d or Bcf/d
    tariff: float          # CAD/bbl or CAD/GJ
    commodity: str
    pipeline_name: str
    length_km: float = 0.0
    utilization_pct: float = 85.0

    @property
    def actual_flow(self) -> float:
        return self.capacity * self.utilization_pct / 100

    @property
    def annual_value_cad(self) -> float:
        """Approximate annual transport revenue (crude, 000 bbl/d basis)."""
        return self.actual_flow * 1000 * 365 * self.tariff


class HydrocarbonNetwork:
    """
    Multi-commodity directed graph of Alberta's full hydrocarbon
    pipeline network.
    """

    def __init__(self):
        self.nodes: dict[str, Node] = {}
        self.edges: list[Edge] = []
        self._adj: dict[str, dict] = defaultdict(dict)

    def add_node(self, node: Node):
        self.nodes[node.node_id] = node

    def add_edge(self, edge: Edge):
        self.edges.append(edge)
        key = edge.commodity
        if edge.target not in self._adj[edge.source]:
            self._adj[edge.source][edge.target] = {}
        self._adj[edge.source][edge.target][key] = {
            "cap": edge.capacity,
            "flow": 0.0,
            "tariff": edge.tariff,
            "name": edge.pipeline_name
        }
        # Reverse edge for residual graph
        if edge.source not in self._adj[edge.target]:
            self._adj[edge.target][edge.source] = {}
        if key not in self._adj[edge.target][edge.source]:
            self._adj[edge.target][edge.source][key] = {
                "cap": 0.0, "flow": 0.0, "tariff": 0.0, "name": "reverse"
            }

    def betweenness_centrality(self, commodity: str = "crude") -> dict:
        """
        Approximate betweenness centrality via Brandes algorithm.
        Only considers edges carrying the specified commodity.
        """
        centrality = {n: 0.0 for n in self.nodes}
        relevant_nodes = list(self.nodes.keys())

        for s in relevant_nodes:
            # BFS from s
            stack = []
            pred = {n: [] for n in relevant_nodes}
            sigma = {n: 0.0 for n in relevant_nodes}
            sigma[s] = 1.0
            dist = {n: -1 for n in relevant_nodes}
            dist[s] = 0
            queue = deque([s])

            while queue:
                v = queue.popleft()
                stack.append(v)
                for w, commodities in self._adj[v].items():
                    if commodity not in commodities:
                        continue
                    if commodities[commodity]["cap"] <= 0:
                        continue
                    if dist[w] < 0:
                        queue.append(w)
                        dist[w] = dist[v] + 1
                    if dist[w] == dist[v] + 1:
                        sigma[w] += sigma[v]
                        pred[w].append(v)

            # Accumulation
            delta = {n: 0.0 for n in relevant_nodes}
            while stack:
                w = stack.pop()
                for v in pred[w]:
                    if sigma[w] > 0:
                        delta[v] += (sigma[v] / sigma[w]) * (1.0 + delta[w])
                if w != s:
                    centrality[w] += delta[w]

        # Normalize
        n = len(relevant_nodes)
        scale = 1.0 / ((n - 1) * (n - 2)) if n > 2 else 1.0
        return {k: v * scale for k, v in centrality.items()}

    def dijkstra_tariff(self, source: str, commodity: str) -> dict:
        """Minimum tariff cost from source to all reachable nodes."""
        dist = {n: float('inf') for n in self.nodes}
        dist[source] = 0.0
        prev = {}
        pq = [(0.0, source)]

        while pq:
            cost, u = heapq.heappop(pq)
            if cost > dist[u]:
                continue
            for v, commodities in self._adj[u].items():
                if commodity not in commodities:
                    continue
                data = commodities[commodity]
                if data["cap"] <= 0:
                    continue
                new_cost = cost + data["tariff"]
                if new_cost < dist[v]:
                    dist[v] = new_cost
                    prev[v] = u
                    heapq.heappush(pq, (new_cost, v))

        return dist

    def netback_surface(self,
                         source: str,
                         market_prices: dict,
                         commodity: str = "crude",
                         quality_discount: float = 19.0) -> dict:
        """Full netback surface from source to all market nodes."""
        tariffs = self.dijkstra_tariff(source, commodity)
        results = {}
        for node_id, price in market_prices.items():
            if node_id not in tariffs:
                continue
            t = tariffs[node_id]
            results[node_id] = {
                "name": self.nodes[node_id].name,
                "market_price_cad": price,
                "transport_tariff_cad": t,
                "quality_discount_cad": quality_discount,
                "netback_cad": price - t - quality_discount
            }
        return results

    def system_report(self, commodity: str = "crude"):
        edges_c = [e for e in self.edges if e.commodity == commodity]
        total_cap = sum(e.capacity for e in edges_c)
        total_actual = sum(e.actual_flow for e in edges_c)
        annual_rev = sum(e.annual_value_cad for e in edges_c)

        print(f"\n{'='*65}")
        print(f"  Alberta Hydrocarbon Network — {commodity.upper()} Subsystem")
        print(f"{'='*65}")
        print(f"  Segments      : {len(edges_c)}")
        print(f"  Total capacity: {total_cap:,.0f} 000 bbl/d")
        print(f"  Actual flow   : {total_actual:,.0f} 000 bbl/d")
        print(f"  Utilization   : {total_actual/total_cap*100:.1f}%")
        print(f"  Annual tariff revenue: CAD ${annual_rev/1e9:.1f}B/yr")

        print(f"\n  Betweenness Centrality (top 5 nodes):")
        bc = self.betweenness_centrality(commodity)
        for node_id, score in sorted(bc.items(), key=lambda x: -x[1])[:5]:
            name = self.nodes[node_id].name
            print(f"    {name:<30}: {score:.4f}")


# --- Build the integrated network ---
network = HydrocarbonNetwork()

# Production nodes
for nid, name, lat, lon in [
    ("ATHABASCA", "Athabasca Oil Sands", 57.0, -111.5),
    ("PEACE_RIVER", "Peace River Region", 56.2, -117.5),
    ("LLOYDMINSTER", "Lloydminster AB/SK", 53.3, -110.0),
    ("MONTNEY", "Montney Play (NE BC/AB)", 55.5, -120.0),
]:
    network.add_node(Node(nid, name, "production", lat, lon, ["crude", "gas", "ngl"]))

# Hub nodes
for nid, name, lat, lon, comms in [
    ("HARDISTY", "Hardisty AB",         52.67, -111.30, ["crude", "ngl"]),
    ("EDMONTON", "Edmonton AB",          53.55, -113.49, ["crude", "products", "gas"]),
    ("FSK",      "Fort Saskatchewan AB", 53.71, -113.21, ["ngl", "products"]),
    ("AECO",     "AECO Hub (Suffield)",  50.12, -110.74, ["gas"]),
    ("SUPERIOR", "Superior WI",          46.72,  -92.10, ["crude"]),
    ("SUMAS",    "Sumas/Huntingdon BC",  49.00, -122.27, ["gas"]),
    ("KANKAKEE", "Kankakee IL",          41.12,  -87.86, ["ngl"]),
]:
    network.add_node(Node(nid, name, "hub", lat, lon, comms))

# Terminal / market nodes
for nid, name, lat, lon, comms in [
    ("BURNABY",    "Burnaby BC",          49.25, -122.95, ["crude", "products"]),
    ("KAMLOOPS",   "Kamloops BC",         50.67, -120.33, ["products"]),
    ("PATOKA",     "Patoka IL",           38.75,  -88.63, ["crude"]),
    ("CUSHING",    "Cushing OK",          35.98,  -96.77, ["crude"]),
    ("PORT_ARTHUR","Port Arthur TX",      29.90,  -93.93, ["crude"]),
    ("CHICAGO",    "Chicago IL",          41.88,  -87.63, ["gas"]),
    ("TORONTO",    "Toronto ON",          43.70,  -79.42, ["gas", "products"]),
    ("KITCHENER",  "Kitchener ON (gas)",  43.45,  -80.49, ["gas"]),
    ("JOFFRE",     "Joffre AB (cracker)", 52.35, -113.52, ["ngl"]),
]:
    network.add_node(Node(nid, name, "market", lat, lon, comms))

# --- Crude edges ---
for src, tgt, cap, tariff, name in [
    ("ATHABASCA",  "HARDISTY",   3300, 2.5,  "Oil Sands gathering"),
    ("HARDISTY",   "SUPERIOR",   3100, 8.2,  "Enbridge Mainline"),
    ("HARDISTY",   "BURNABY",     890, 11.0, "Trans Mountain"),
    ("HARDISTY",   "CUSHING",     590, 10.5, "Keystone"),
    ("HARDISTY",   "PATOKA",      280, 9.0,  "Express + Platte"),
    ("SUPERIOR",   "PATOKA",      900, 2.5,  "Enbridge 6B"),
    ("SUPERIOR",   "TORONTO",     500, 3.5,  "Enbridge Line 5/78"),
    ("PATOKA",     "CUSHING",     400, 2.0,  "Capline"),
    ("CUSHING",    "PORT_ARTHUR", 600, 1.8,  "Seaway/Longhorn"),
]:
    network.add_edge(Edge(src, tgt, cap, tariff, "crude", name))

# --- Gas edges ---
for src, tgt, cap, tariff, name in [
    ("MONTNEY",  "AECO",     6.5, 0.30, "NGTL gathering"),
    ("HARDISTY", "AECO",     4.0, 0.25, "NGTL mainline"),
    ("AECO",     "CHICAGO",  1.6, 0.85, "Alliance Pipeline"),
    ("AECO",     "TORONTO",  4.2, 1.20, "TC Canadian Mainline"),
    ("AECO",     "SUMAS",    2.8, 0.70, "Westcoast/Spectra"),
    ("SUMAS",    "CHICAGO",  1.0, 0.60, "U.S. Pacific NW connects"),
]:
    network.add_edge(Edge(src, tgt, cap, tariff, "gas", name))

# --- NGL edges ---
for src, tgt, cap, tariff, name in [
    ("MONTNEY",   "FSK",      200, 1.5, "NGL gathering / NGTL"),
    ("AECO",      "FSK",      150, 1.2, "NGL extraction feeds"),
    ("KANKAKEE",  "HARDISTY",  95, 2.0, "Cochin (condensate, N)"),
    ("FSK",       "HARDISTY", 350, 0.8, "Condensate to blending"),
    ("FSK",       "JOFFRE",   180, 0.5, "Ethane to cracker"),
]:
    network.add_edge(Edge(src, tgt, cap, tariff, "ngl", name))

# --- Products edges ---
for src, tgt, cap, tariff, name in [
    ("EDMONTON", "BURNABY",  100, 3.5, "Trans Mountain (products)"),
    ("EDMONTON", "KAMLOOPS",  40, 1.8, "Trans Mountain (products, partial)"),
    ("EDMONTON", "TORONTO",   55, 5.2, "Enbridge products line"),
]:
    network.add_edge(Edge(src, tgt, cap, tariff, "products", name))

# --- System reports ---
for commodity in ["crude", "gas", "ngl"]:
    network.system_report(commodity)

# --- Netback surface ---
CRUDE_MARKETS = {
    "PATOKA":      102.7,
    "CUSHING":     102.7,
    "PORT_ARTHUR": 104.1,
    "BURNABY":     106.0,
    "TORONTO":     100.5,
}

print(f"\n{'='*65}")
print(f"  Netback Price Surface — Crude Oil from Hardisty")
print(f"{'='*65}")
surface = network.netback_surface("HARDISTY", CRUDE_MARKETS, "crude", 19.0)
print(f"\n  {'Market':<22} {'Price':>8} {'Tariff':>8} {'Discount':>10} {'Netback':>10}")
print(f"  {'-'*60}")
for nid, r in sorted(surface.items(), key=lambda x: -x[1]["netback_cad"]):
    print(f"  {r['name']:<22} ${r['market_price_cad']:>6.1f}  "
          f"${r['transport_tariff_cad']:>5.1f}  "
          f"  -${r['quality_discount_cad']:>4.1f}  "
          f"${r['netback_cad']:>8.1f}")

6. Visualization

Figure 1 — Network Node Betweenness Centrality

Figure 2 — Netback Price Surface by Market Destination

Figure 3 — Multi-Commodity Network Capacity Summary

Figure 4 — Max Flow vs Minimum Cut by Market Corridor

Figure 5 — Integrated Network Flow: Sankey


7. Interpretation

Hardisty is the network’s most critical node

The betweenness centrality analysis (Figure 1) confirms what the geography suggests: Hardisty, Alberta has the highest centrality score in the crude oil subsystem. Almost all oil sands crude passes through Hardisty — or through the Hardisty-Edmonton corridor — before entering an export trunk line. Its disruption would affect all three export directions simultaneously.

This is not a secret or a vulnerability that the analysis reveals for the first time — it is simply the mathematical expression of Hardisty’s geographic position as the junction of the gathering system from the north and the export trunks to the south, west, and east. Infrastructure investment to add redundancy around Hardisty — additional tankage, multiple pipeline connections, cross-connects between the Enbridge and Trans Mountain systems — reflects the industry’s own assessment of this centrality.

The minimum cut is not where most people look

The max-flow analysis produces a counterintuitive result: for Alberta crude moving to the Gulf Coast, the binding constraint is not the Enbridge Mainline (3.1 million bbl/d) but the combined capacity of onward connections from the Midwest to Cushing and Port Arthur. The Mainline is large enough to deliver more crude to Superior, Wisconsin than the connecting pipelines can carry south. This is the minimum cut — and it means that adding capacity to the Mainline itself would not increase Alberta’s Gulf Coast access without also adding connecting capacity south of Superior.

This is precisely the logic behind Keystone XL: it was not primarily a substitute for the Mainline but a parallel direct route from Hardisty to Cushing, bypassing the minimum-cut constraint entirely. Its cancellation left the minimum cut in place.

The netback surface is nearly flat — which matters

Figure 2 shows that netback prices across all six major market destinations range from approximately CAD $74.7 to $77.0 per barrel — a spread of only $2.3/bbl. This relative flatness reflects the fact that higher-priced markets (Burnaby, Port Arthur) are also more expensive to reach, while cheaper markets (Patoka, Cushing) have lower transport costs. The market has largely arbitraged away the geographic rent — transport costs eat most of the destination price premium.

The implication for producers: market access matters, but the marginal improvement from reaching a new market destination is often smaller than pipeline tariff debates suggest. What matters more is whether a market is accessible at all — the binary of having-a-path vs having-no-path is more economically significant than small differences in netback across available destinations.

The integrated system is structurally robust — with specific exceptions

Treating all four commodity systems together, the Alberta hydrocarbon network is large, multi-directional, and serves multiple commodity types through overlapping but independent infrastructure. It is not fragile in the aggregate. The specific vulnerabilities are:

  • Condensate supply (P2): a thin margin between diluent supply and demand means a Cochin outage or cold-weather gas plant constraint directly limits dilbit production
  • AECO basis events (P3): rapid Montney production growth has twice overwhelmed NGTL’s gathering capacity, collapsing AECO prices
  • Hardisty node concentration (P5): the gathering-to-trunk junction is a single point of geographic concentration

These are real and manageable constraints — the kind that the Canada Energy Regulator monitors continuously and that the industry builds around. They are not evidence of systemic inadequacy; they are the normal friction of a large continental infrastructure system.


8. What Could Go Wrong?

Graph simplification

The network model here uses approximately 20 nodes and 30 edges. The actual Alberta pipeline system has thousands of physical nodes and tens of thousands of kilometres of pipe. The simplified graph captures strategic structure but misses:

  • Parallel paths within the Enbridge Mainline system (five separate lines that can be operated semi-independently)
  • Storage interactions (Hardisty’s tank farm can buffer short-term mismatches between gathering and trunk throughput)
  • Bidirectional pipes (several segments can reverse direction under certain operating conditions)

Multi-commodity coupling is complex

The model treats crude, gas, NGL, and products as semi-independent commodity flows with shared nodes. The actual coupling is tighter: diluent supply directly constrains dilbit volume; gas plant output determines NGL yield; crack spreads determine whether refineries maximize gasoline or diesel production, affecting the batch sequence on the products lines. A fully coupled model is a large-scale mixed-integer program beyond the scope of this essay.

Netback calculations are sensitive to exchange rates

All CAD/bbl calculations here assume a fixed USD/CAD exchange rate of 0.73. In practice, the Canadian dollar co-moves with oil prices (a well-documented correlation) — when WTI falls, the CAD typically weakens, which partially cushions the revenue impact in CAD terms. This correlation means that the netback surface is not static; it shifts with currency movements independently of pipeline tariffs.


9. Summary

Treating Alberta’s full hydrocarbon pipeline network as a directed graph reveals structure that commodity-by-commodity analysis misses. The key results:

  • Hardisty AB has the highest betweenness centrality in the crude subsystem — it is the structural hub through which almost all oil sands export flow passes
  • The minimum cut for Alberta-to-Gulf-Coast crude flow lies not on the Enbridge Mainline but on the connecting pipelines south of Superior, Wisconsin — the Mainline itself is not the Gulf binding constraint
  • The netback price surface is nearly flat across available markets (CAD $74.7–$77.0/bbl range) because transport costs largely absorb destination price premiums — market access matters most as a binary, not as a marginal optimization
  • The integrated network is structurally robust with three specific manageable vulnerabilities: condensate supply margin, AECO basis events during rapid production growth, and Hardisty node concentration
  • The max-flow min-cut theorem provides a rigorous framework for identifying binding constraints — the same mathematical result that governs internet routing and logistics networks governs Alberta’s pipeline export capacity

Network mathematics does not determine pipeline policy. But it describes the system precisely enough to make policy arguments testable against physical reality.


Math Refresher

Graph theory basics

A graph is a set of nodes connected by edges. A directed graph (digraph) has edges with direction — flow goes one way. A path from $s$ to $t$ is a sequence of nodes connected by directed edges all pointing the right way. A cut is a partition of nodes into two sets that separates source from sink; its capacity is the sum of capacities of edges crossing from the source set to the sink set.

The max-flow min-cut theorem says: \(\text{Maximum flow from } s \text{ to } t = \text{Minimum cut capacity separating } s \text{ from } t\)

This is a deep result — it says that the physical bottleneck (the minimum cut) exactly determines the maximum achievable flow, no matter how complex the network is or how many paths exist.

Dijkstra’s algorithm in one paragraph

To find the cheapest path from node $A$ to all other nodes: start with cost 0 at $A$ and infinite cost everywhere else. Repeatedly pick the unvisited node with the lowest current cost, then update all its neighbours’ costs if going through this node is cheaper. Stop when all nodes are visited. The result is the minimum-cost path to every reachable destination — which is exactly the information needed to compute the netback price surface.


Sources and Data Notes

Source Used For
Canada Energy Regulator, Pipeline Profiles (2024) All pipeline capacity data
Ford, L.R. & Fulkerson, D.R., Flows in Networks (1962) Max-flow min-cut theory
Brandes, U. (2001), A Faster Algorithm for Betweenness Centrality Centrality algorithm
Dijkstra, E.W. (1959), A Note on Two Problems in Connexion with Graphs Shortest path algorithm
Alberta Energy Regulator, ST98 (2024) Production volumes
TC Energy, Enbridge, Trans Mountain: public system overviews Network topology

All network parameters are approximate and based on publicly available capacity information. The graph model is a strategic simplification; it is not suitable for operational pipeline scheduling or engineering design.


References

Alberta Energy Regulator. 2024. ST98: Alberta Energy Outlook — Crude Bitumen Production. Calgary: AER. https://www.aer.ca/data-and-performance-reports/statistical-reports/alberta-energy-outlook-st98/crude-bitumen/crude-bitumen-production

Brandes, Ulrik. 2001. “A Faster Algorithm for Betweenness Centrality.” Journal of Mathematical Sociology 25 (2): 163–177. https://doi.org/10.1080/0022250X.2001.9990249

Canada Energy Regulator. 2024. Pipeline Profiles. Calgary: CER. https://www.cer-rec.gc.ca/en/data-analysis/facilities-we-regulate/pipeline-profiles/

Canada Energy Regulator. 2025. Market Snapshot: Oil Pipeline Throughputs for 2024 and the First Half of 2025 Remain High. Calgary: CER. https://www.cer-rec.gc.ca/en/data-analysis/energy-markets/market-snapshots/2025/market-snapshot-oil-pipeline-throughputs-for-2024-and-the-first-half-of-2025-remain-high.html

Canada Energy Regulator. 2025. Market Snapshot: Trans Mountain Expansion Eases Pipeline Constraints and Increases Exports to Overseas Markets. Calgary: CER. https://www.cer-rec.gc.ca/en/data-analysis/energy-markets/market-snapshots/2025/market-snapshot-trans-mountain-expansion-eases-pipeline-constraints-and-increases-exports-to-overseas-markets.html

Dijkstra, Edsger W. 1959. “A Note on Two Problems in Connexion with Graphs.” Numerische Mathematik 1: 269–271. https://doi.org/10.1007/BF01386390

Enbridge Inc. 2025. Energy Infrastructure Assets. Calgary: Enbridge. https://www.enbridge.com/~/media/Enb/Documents/Factsheets/FS_EnergyInfrastructureAssets.pdf

Ford, Lester R., Jr., and D. Ray Fulkerson. 1962. Flows in Networks. Princeton: Princeton University Press. https://press.princeton.edu/books/hardcover/9780691651842/flows-in-networks

Statistics Canada. 2025. “Another Record Year for Canadian Crude Oil: Crude Oil Year in Review, 2024.” Ottawa: Statistics Canada. https://www.statcan.gc.ca/o1/en/plus/7940-another-record-year-canadian-crude-oil-crude-oil-year-review-2024

TC Energy. 2024. NGTL System. Calgary: TC Energy. https://www.tcenergy.com/operations/natural-gas/ngtl-system/

TC Energy. 2024. Keystone Pipeline System. Calgary: TC Energy. https://www.tcenergy.com/operations/oil-and-liquids/keystone-pipeline-system/

Trans Mountain Corporation. 2024. Trans Mountain Pipeline System. Calgary: Trans Mountain. https://www.transmountain.com/pipeline-system


References