import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.patches import FancyArrowPatch
import warnings
warnings.filterwarnings('ignore')
fig, ax = plt.subplots(figsize=(12, 8))
ax.set_xlim(-0.05, 1.15)
ax.set_ylim(-0.05, 1.10)
ax.set_aspect('equal')
ax.axis('off')
# ── node positions (normalised, approximate Alberta geography orientation) ─────
# left = west (Pacific), right = east (Chicago), top = north (Fort McMurray)
nodes = {
'Fort\nMcMurray': (0.62, 0.92),
'Peace\nRiver': (0.22, 0.88),
'WCSB Gas\nProduction':(0.42, 0.97),
'Fort\nSaskatchewan': (0.50, 0.75),
'Edmonton /\nRedwater': (0.44, 0.68),
'Hardisty': (0.66, 0.68),
'AECO /\nSuffield': (0.65, 0.56),
'Empress': (0.88, 0.60),
'Cromer /\nGretna': (0.95, 0.72),
'Burnaby': (0.08, 0.52),
'Kitimat': (0.06, 0.80),
'Chicago /\nPatoka': (1.08, 0.55),
'Dawn /\nEastern CA': (1.08, 0.68),
'Gulf\nCoast': (1.00, 0.30),
}
# betweenness centrality (computed from worked example, approximate for full network)
betweenness = {
'Fort\nMcMurray': 0.15,
'Peace\nRiver': 0.05,
'WCSB Gas\nProduction':0.20,
'Fort\nSaskatchewan': 0.35,
'Edmonton /\nRedwater': 0.65,
'Hardisty': 1.00, # highest: all crude export paths
'AECO /\nSuffield': 0.30,
'Empress': 0.25,
'Cromer /\nGretna': 0.55,
'Burnaby': 0.20,
'Kitimat': 0.10,
'Chicago /\nPatoka': 0.10,
'Dawn /\nEastern CA': 0.10,
'Gulf\nCoast': 0.08,
}
# node types for colouring
source_nodes = {'Fort\nMcMurray', 'Peace\nRiver', 'WCSB Gas\nProduction'}
proc_nodes = {'Fort\nSaskatchewan', 'Edmonton /\nRedwater', 'AECO /\nSuffield'}
junction_nodes = {'Hardisty', 'Empress', 'Cromer /\nGretna'}
market_nodes = {'Burnaby', 'Kitimat', 'Chicago /\nPatoka', 'Dawn /\nEastern CA', 'Gulf\nCoast'}
col_source = '#2d6a4f'
col_proc = '#e76f51'
col_junction = '#264653'
col_market = '#457b9d'
def node_color(n):
if n in source_nodes: return col_source
if n in proc_nodes: return col_proc
if n in junction_nodes: return col_junction
return col_market
# draw nodes
base_size = 0.048
for name, (x, y) in nodes.items():
r = base_size * (0.5 + 0.9 * betweenness[name])
circle = plt.Circle((x, y), r, color=node_color(name), zorder=3, alpha=0.92)
ax.add_patch(circle)
ax.text(x, y - r - 0.022, name, ha='center', va='top', fontsize=7.2,
fontfamily='monospace', color='#1c1c1a', zorder=4)
# ── edges ──────────────────────────────────────────────────────────────────────
# (from, to, commodity, capacity_relative, is_coupling_cycle)
edges = [
# crude feeders (black)
('Fort\nMcMurray', 'Hardisty', 'crude', 2.0, False),
('Peace\nRiver', 'Hardisty', 'crude', 0.8, False),
# crude export corridors
('Hardisty', 'Cromer /\nGretna', 'crude', 3.0, False),
('Hardisty', 'Burnaby', 'crude', 0.9, False),
('Cromer /\nGretna', 'Chicago /\nPatoka', 'crude', 3.0, False),
('Cromer /\nGretna', 'Gulf\nCoast', 'crude', 0.8, False),
# NGL / condensate (orange)
('WCSB Gas\nProduction','Fort\nSaskatchewan', 'ngl', 1.2, False),
('Fort\nSaskatchewan', 'Hardisty', 'ngl', 0.6, True), # diluent return — cycle
('Fort\nSaskatchewan', 'Edmonton /\nRedwater','ngl', 0.6, False),
# gas system (blue)
('WCSB Gas\nProduction','AECO /\nSuffield', 'gas', 3.0, False),
('AECO /\nSuffield', 'Empress', 'gas', 2.5, False),
('Empress', 'Dawn /\nEastern CA', 'gas', 2.5, False),
('AECO /\nSuffield', 'Chicago /\nPatoka', 'gas', 1.6, False), # Alliance
('AECO /\nSuffield', 'Kitimat', 'gas', 1.0, False), # Coastal GasLink
# refined products (green)
('Edmonton /\nRedwater','Burnaby', 'products', 0.3, False),
]
col_commodity = {
'crude': '#1c1c1a',
'ngl': '#e07b39',
'gas': '#2176ae',
'products': '#4caf50',
}
for (u, v, commodity, cap, is_cycle) in edges:
xu, yu = nodes[u]
xv, yv = nodes[v]
# shorten arrow so it doesn't overlap node circles
ru = base_size * (0.5 + 0.9 * betweenness[u])
rv = base_size * (0.5 + 0.9 * betweenness[v])
dx, dy = xv - xu, yv - yu
dist = np.hypot(dx, dy)
if dist < 1e-6:
continue
x0 = xu + dx / dist * ru
y0 = yu + dy / dist * ru
x1 = xv - dx / dist * (rv + 0.01)
y1 = yv - dy / dist * (rv + 0.01)
lw = 1.0 + cap * 1.5
ls = (0, (4, 2)) if is_cycle else 'solid'
alpha = 0.85
ax.annotate('', xy=(x1, y1), xytext=(x0, y0),
arrowprops=dict(arrowstyle='->', color=col_commodity[commodity],
lw=lw, linestyle=ls, alpha=alpha,
connectionstyle='arc3,rad=0.0'))
# ── legend ─────────────────────────────────────────────────────────────────────
legend_elements = [
mpatches.Patch(color=col_source, label='Source (production)'),
mpatches.Patch(color=col_proc, label='Processing hub'),
mpatches.Patch(color=col_junction, label='Junction node'),
mpatches.Patch(color=col_market, label='Market destination'),
mpatches.Patch(color=col_commodity['crude'], label='Crude pipeline'),
mpatches.Patch(color=col_commodity['ngl'], label='NGL / condensate'),
mpatches.Patch(color=col_commodity['gas'], label='Natural gas'),
mpatches.Patch(color=col_commodity['products'], label='Refined products'),
]
ax.legend(handles=legend_elements, loc='lower left', fontsize=8,
framealpha=0.95, edgecolor='#d0d0c8', frameon=True)
# coupling cycle annotation
ax.annotate('diluent return\n(directed cycle)', xy=(0.58, 0.715), fontsize=7.5,
color=col_commodity['ngl'], ha='center', style='italic')
ax.set_title('Alberta Integrated Energy Network — Directed Graph', fontsize=12,
fontfamily='monospace', color='#1c1c1a', pad=10)
plt.tight_layout()
plt.savefig('fig-network.png', dpi=150, bbox_inches='tight',
facecolor='#f8f8f4', edgecolor='none')
plt.show()