Integrated Carbon Capture Plant Design#
This notebook demonstrates a complete carbon capture system with:
Amine absorption/stripping loop
Performance analysis
Economic evaluation
Sensitivity to key parameters
Learning Objectives#
Design a complete amine capture loop
Calculate mass and energy balances
Evaluate capture cost ($/tonne CO2)
Optimize the integrated system
1. System Overview#
Treated Gas
↑
Flue Gas ┌─────────────┐
─────────► │ ABSORBER │
│ │
Lean ───────┤ ├──────► Rich
Solvent └─────────────┘ Solvent
↑ │
│ ┌─────────────┐ │
└────────┤ STRIPPER │◄──────────┘
│ │
└──────┬──────┘
│
↓
CO2 Product
Key Design Variables#
Solvent type and concentration
L/G ratio (liquid-to-gas molar ratio)
Lean loading (CO2 remaining after regeneration)
Operating temperatures and pressures
2. Setup#
import jax
import jax.numpy as jnp
from jax import grad, jit, vmap
jax.config.update("jax_enable_x64", True)
from difflow.streams import make_stream, get_flows, total_flow
from difflow_cc import (
# Database
get_solvent, list_solvents,
# Unit operations
AbsorberParams, AmineAbsorber,
StripperParams, AmineStripper,
)
print("Available solvents:", list_solvents())
Available solvents: ['MEA', 'DEA', 'MDEA', 'PZ', 'AMP', 'Glycine', 'Sarcosine']
3. Define Feed Conditions#
Typical coal-fired power plant flue gas:
~12-15% CO2
~5% O2
Balance N2
~40-50°C after cooling
# Define flue gas for a 500 MW power plant
# Typical: ~500 kg/s flue gas, ~13% CO2
F_flue_mol = 100.0 # mol/s total (scaled for example)
y_CO2 = 0.13 # 13% CO2
flue_gas = make_stream(
flows={
"CO2": F_flue_mol * y_CO2,
"N2": F_flue_mol * (1 - y_CO2),
},
T=313.15, # 40°C
P=101325.0, # 1 atm
)
print("Flue Gas Feed:")
print(f" Total flow: {F_flue_mol:.1f} mol/s")
print(f" CO2 flow: {F_flue_mol * y_CO2:.1f} mol/s")
print(f" CO2 mass flow: {F_flue_mol * y_CO2 * 44.0 / 1000:.2f} kg/s")
print(f" CO2 annual: {F_flue_mol * y_CO2 * 44.0 * 3600 * 8000 / 1e9:.1f} kt/yr")
Flue Gas Feed:
Total flow: 100.0 mol/s
CO2 flow: 13.0 mol/s
CO2 mass flow: 0.57 kg/s
CO2 annual: 16.5 kt/yr
4. Design the Capture Loop#
def capture_loop(params, flue_gas):
"""Complete amine capture loop.
Args:
params: dict with L_G_ratio, lean_loading, T_reboiler
flue_gas: inlet flue gas stream
Returns:
results: dict with all performance metrics
"""
# Unpack parameters
L_G_ratio = params['L_G_ratio']
lean_loading = params['lean_loading']
T_reboiler = params['T_reboiler']
solvent = params.get('solvent', 'MEA')
solvent_conc = params.get('solvent_conc', 30.0)
n_stages_abs = params.get('n_stages_abs', 15)
n_stages_strip = params.get('n_stages_strip', 10)
# Create absorber
abs_params = AbsorberParams(
solvent=solvent,
n_stages=n_stages_abs,
solvent_conc=solvent_conc,
L_G_ratio=L_G_ratio,
lean_loading=lean_loading,
T_gas_in=313.15,
T_liquid_in=313.15,
)
absorber = AmineAbsorber(abs_params)
# Run absorber
treated_gas, rich_solvent, abs_info = absorber(flue_gas)
# Create stripper
strip_params = StripperParams(
solvent=solvent,
n_stages=n_stages_strip,
T_reboiler=T_reboiler,
P_stripper=200000.0, # 2 bar
)
stripper = AmineStripper(strip_params)
# Run stripper
lean_out, co2_product, strip_info = stripper(rich_solvent)
# Compile results
CO2_in = get_flows(flue_gas).get('CO2', 0.0)
CO2_captured = abs_info['CO2_captured']
results = {
# Capture performance
'capture_efficiency': abs_info['capture_efficiency'],
'CO2_captured': CO2_captured, # mol/s
'rich_loading': abs_info['rich_loading'],
'lean_loading': lean_loading,
'delta_loading': abs_info['rich_loading'] - lean_loading,
# Energy
'reboiler_duty': strip_info['reboiler_duty'], # W
'specific_energy': strip_info['specific_energy'], # GJ/t CO2
# Product
'CO2_purity': strip_info['CO2_purity'],
# Operating conditions
'L_G_ratio': L_G_ratio,
'T_reboiler': T_reboiler,
'absorber_stages': n_stages_abs,
'stripper_stages': n_stages_strip,
}
return results
# Base case design
base_params = {
'solvent': 'MEA',
'solvent_conc': 30.0,
'L_G_ratio': 3.5,
'lean_loading': 0.25,
'T_reboiler': 393.15, # 120°C
'n_stages_abs': 15,
'n_stages_strip': 10,
}
results = capture_loop(base_params, flue_gas)
print("Base Case Results:")
print("="*50)
print(f"\nCapture Performance:")
print(f" Capture efficiency: {float(results['capture_efficiency']):.1%}")
print(f" CO2 captured: {float(results['CO2_captured']):.2f} mol/s")
print(f" CO2 mass: {float(results['CO2_captured']) * 44.0 / 1000:.3f} kg/s")
print(f"\nSolvent Loading:")
print(f" Lean loading: {float(results['lean_loading']):.3f} mol/mol")
print(f" Rich loading: {float(results['rich_loading']):.3f} mol/mol")
print(f" Working capacity: {float(results['delta_loading']):.3f} mol/mol")
print(f"\nEnergy:")
print(f" Reboiler duty: {float(results['reboiler_duty'])/1e6:.2f} MW")
print(f" Specific energy: {float(results['specific_energy']):.2f} GJ/t CO2")
print(f"\nProduct:")
print(f" CO2 purity: {float(results['CO2_purity']):.1%}")
Base Case Results:
==================================================
Capture Performance:
Capture efficiency: 75.5%
CO2 captured: 9.81 mol/s
CO2 mass: 0.432 kg/s
Solvent Loading:
Lean loading: 0.250 mol/mol
Rich loading: 0.500 mol/mol
Working capacity: 0.250 mol/mol
Energy:
Reboiler duty: 2.15 MW
Specific energy: 4.36 GJ/t CO2
Product:
CO2 purity: 41.7%
5. Economic Analysis#
def calculate_cost(results, economic_params=None):
"""Calculate capture cost in $/tonne CO2.
Includes:
- Capital cost (absorber, stripper, heat exchangers)
- Operating cost (steam, electricity, solvent makeup)
"""
if economic_params is None:
economic_params = {
'steam_cost': 15.0, # $/GJ
'electricity_cost': 0.06, # $/kWh
'solvent_cost': 2.0, # $/kg
'solvent_loss_rate': 1.5, # kg/t CO2
'capacity_factor': 0.85,
'capital_factor': 0.15, # $/yr per $ capital
}
ep = economic_params
# CO2 captured (tonnes/year)
CO2_captured_kg_s = float(results['CO2_captured']) * 44.0 / 1000
hours_per_year = 8760 * ep['capacity_factor']
CO2_tonnes_yr = CO2_captured_kg_s * 3600 * hours_per_year / 1000
# Operating costs
# Steam for reboiler
reboiler_GJ_yr = float(results['reboiler_duty']) / 1e9 * 3600 * hours_per_year
steam_cost_yr = reboiler_GJ_yr * ep['steam_cost']
# Electricity (pumps, blowers) - estimated as 10% of thermal
electricity_kWh_yr = float(results['reboiler_duty']) / 1000 * 0.10 * hours_per_year
electricity_cost_yr = electricity_kWh_yr * ep['electricity_cost']
# Solvent makeup
solvent_cost_yr = CO2_tonnes_yr * ep['solvent_loss_rate'] * ep['solvent_cost']
# Total operating cost
opex_yr = steam_cost_yr + electricity_cost_yr + solvent_cost_yr
opex_per_tonne = opex_yr / CO2_tonnes_yr
# Capital cost (simplified correlation)
# Based on: CAPEX ~ $1000-2000 per (tonne/year) capacity
capex_per_capacity = 1500 # $/tonne/yr capacity
capex_total = capex_per_capacity * CO2_tonnes_yr
capex_yr = capex_total * ep['capital_factor']
capex_per_tonne = capex_yr / CO2_tonnes_yr
# Total cost
total_cost = opex_per_tonne + capex_per_tonne
return {
'CO2_tonnes_yr': CO2_tonnes_yr,
'steam_cost': steam_cost_yr / CO2_tonnes_yr,
'electricity_cost': electricity_cost_yr / CO2_tonnes_yr,
'solvent_cost': solvent_cost_yr / CO2_tonnes_yr,
'opex': opex_per_tonne,
'capex': capex_per_tonne,
'total_cost': total_cost,
}
cost = calculate_cost(results)
print("Economic Analysis:")
print("="*50)
print(f"\nAnnual CO2 captured: {cost['CO2_tonnes_yr']/1000:.1f} kt/yr")
print(f"\nCost Breakdown ($/tonne CO2):")
print(f" Steam: ${cost['steam_cost']:.1f}")
print(f" Electricity: ${cost['electricity_cost']:.1f}")
print(f" Solvent: ${cost['solvent_cost']:.1f}")
print(f" ─────────────────────")
print(f" OPEX: ${cost['opex']:.1f}")
print(f" CAPEX: ${cost['capex']:.1f}")
print(f" ─────────────────────")
print(f" TOTAL: ${cost['total_cost']:.1f}/tonne CO2")
Economic Analysis:
==================================================
Annual CO2 captured: 11.6 kt/yr
Cost Breakdown ($/tonne CO2):
Steam: $74.6
Electricity: $8.3
Solvent: $3.0
─────────────────────
OPEX: $85.9
CAPEX: $225.0
─────────────────────
TOTAL: $310.9/tonne CO2
6. Sensitivity Analysis#
# Sensitivity to L/G ratio
print("Sensitivity to L/G Ratio:")
print(f"{'L/G':<8} {'Capture':<10} {'Energy':<12} {'Cost':<10}")
print("-" * 40)
for L_G in [2.5, 3.0, 3.5, 4.0, 4.5, 5.0]:
params = base_params.copy()
params['L_G_ratio'] = L_G
res = capture_loop(params, flue_gas)
cost_data = calculate_cost(res)
print(f"{L_G:<8.1f} {float(res['capture_efficiency']):<10.1%} "
f"{float(res['specific_energy']):<12.2f} ${cost_data['total_cost']:<10.1f}")
Sensitivity to L/G Ratio:
L/G Capture Energy Cost
----------------------------------------
2.5 53.9% 4.36 $310.9
3.0 64.7% 4.36 $310.9
3.5 75.5% 4.36 $310.9
4.0 86.3% 4.36 $310.9
4.5 97.0% 4.36 $310.9
5.0 99.9% 4.40 $312.8
# Sensitivity to lean loading
print("\nSensitivity to Lean Loading:")
print(f"{'Loading':<10} {'Capture':<10} {'Energy':<12} {'Cost':<10}")
print("-" * 42)
for loading in [0.15, 0.20, 0.25, 0.30, 0.35]:
params = base_params.copy()
params['lean_loading'] = loading
res = capture_loop(params, flue_gas)
cost_data = calculate_cost(res)
print(f"{loading:<10.2f} {float(res['capture_efficiency']):<10.1%} "
f"{float(res['specific_energy']):<12.2f} ${cost_data['total_cost']:<10.1f}")
Sensitivity to Lean Loading:
Loading Capture Energy Cost
------------------------------------------
0.15 99.9% 4.40 $287.2
0.20 90.6% 4.36 $297.1
0.25 75.5% 4.36 $310.9
0.30 60.4% 4.36 $331.6
0.35 45.3% 4.36 $366.1
# Sensitivity to reboiler temperature
print("\nSensitivity to Reboiler Temperature:")
print(f"{'T_reb (°C)':<12} {'Capture':<10} {'Energy':<12} {'Cost':<10}")
print("-" * 44)
for T_reb_C in [110, 115, 120, 125, 130]:
params = base_params.copy()
params['T_reboiler'] = T_reb_C + 273.15
res = capture_loop(params, flue_gas)
cost_data = calculate_cost(res)
print(f"{T_reb_C:<12} {float(res['capture_efficiency']):<10.1%} "
f"{float(res['specific_energy']):<12.2f} ${cost_data['total_cost']:<10.1f}")
Sensitivity to Reboiler Temperature:
T_reb (°C) Capture Energy Cost
--------------------------------------------
110 75.5% 4.73 $285.1
115 75.5% 4.52 $296.9
120 75.5% 4.36 $310.9
125 75.5% 4.36 $310.9
130 75.5% 4.36 $310.9
7. Solvent Comparison#
# Compare different solvents
solvents = ['MEA', 'DEA', 'MDEA', 'PZ']
print("Solvent Comparison:")
print(f"{'Solvent':<10} {'Capture':<10} {'Energy':<12} {'Cost':<12}")
print("-" * 44)
for solvent in solvents:
try:
params = base_params.copy()
params['solvent'] = solvent
# Adjust parameters for each solvent
s = get_solvent(solvent)
if hasattr(s, 'loading_capacity'):
params['lean_loading'] = min(0.25, s.loading_capacity * 0.5)
res = capture_loop(params, flue_gas)
cost_data = calculate_cost(res)
print(f"{solvent:<10} {float(res['capture_efficiency']):<10.1%} "
f"{float(res['specific_energy']):<12.2f} ${cost_data['total_cost']:<12.1f}")
except Exception as e:
print(f"{solvent:<10} Error: {e}")
Solvent Comparison:
Solvent Capture Energy Cost
--------------------------------------------
MEA 75.5% 4.36 $310.9
DEA 37.7% 4.80 $322.5
MDEA 0.1% 10.57 $10489.6
PZ 99.9% 4.10 $300.1
8. Gradient-Based Optimization#
# Define cost function for optimization
def total_cost_objective(x):
"""Objective: minimize $/tonne CO2.
x = [L_G_ratio, lean_loading]
"""
params = base_params.copy()
params['L_G_ratio'] = x[0]
params['lean_loading'] = x[1]
res = capture_loop(params, flue_gas)
# Penalty for low capture
capture_eff = res['capture_efficiency']
penalty = 1000.0 * jnp.maximum(0, 0.90 - capture_eff)**2
# Energy cost (proportional to specific energy)
energy_cost = res['specific_energy'] * 15.0 # $/tonne
# Solvent cost (proportional to L/G)
solvent_cost = x[0] * 5.0 # $/tonne
return energy_cost + solvent_cost + penalty
# Test
x0 = jnp.array([3.5, 0.25])
cost0 = total_cost_objective(x0)
print(f"Initial cost: ${float(cost0):.1f}/tonne")
Initial cost: $104.0/tonne
# Gradient computation
grad_cost = grad(total_cost_objective)
g = grad_cost(x0)
print(f"Gradients at initial point:")
print(f" d(cost)/d(L/G) = {float(g[0]):.2f}")
print(f" d(cost)/d(loading) = {float(g[1]):.2f}")
Gradients at initial point:
d(cost)/d(L/G) = -57.64
d(cost)/d(loading) = 876.98
# Simple gradient descent
def optimize_plant(n_iters=50):
x = jnp.array([4.0, 0.20]) # Start point
learning_rate = jnp.array([0.02, 0.002])
best_x = x
best_cost = total_cost_objective(x)
for i in range(n_iters):
cost = total_cost_objective(x)
g = grad_cost(x)
if cost < best_cost:
best_cost = cost
best_x = x
# Update
x = x - learning_rate * g
# Bounds
x = jnp.array([
jnp.clip(x[0], 2.0, 6.0),
jnp.clip(x[1], 0.10, 0.40),
])
if i % 10 == 0:
print(f"Iter {i:3d}: L/G={float(x[0]):.2f}, loading={float(x[1]):.3f}, "
f"cost=${float(cost):.1f}")
return best_x, best_cost
x_opt, cost_opt = optimize_plant()
print(f"\nOptimal: L/G={float(x_opt[0]):.2f}, loading={float(x_opt[1]):.3f}")
print(f"Cost: ${float(cost_opt):.1f}/tonne")
Iter 0: L/G=3.85, loading=0.270, cost=$85.8
Iter 10: L/G=5.68, loading=0.400, cost=$117.1
Iter 20: L/G=5.68, loading=0.400, cost=$117.1
Iter 30: L/G=5.68, loading=0.400, cost=$117.1
Iter 40: L/G=5.68, loading=0.400, cost=$117.1
Optimal: L/G=4.00, loading=0.200
Cost: $85.8/tonne
# Verify optimized design
opt_params = base_params.copy()
opt_params['L_G_ratio'] = float(x_opt[0])
opt_params['lean_loading'] = float(x_opt[1])
opt_results = capture_loop(opt_params, flue_gas)
opt_cost = calculate_cost(opt_results)
print("\nOptimized Design Performance:")
print("="*50)
print(f"L/G ratio: {float(x_opt[0]):.2f}")
print(f"Lean loading: {float(x_opt[1]):.3f} mol/mol")
print(f"Capture efficiency: {float(opt_results['capture_efficiency']):.1%}")
print(f"Specific energy: {float(opt_results['specific_energy']):.2f} GJ/t CO2")
print(f"Total cost: ${opt_cost['total_cost']:.1f}/tonne CO2")
Optimized Design Performance:
==================================================
L/G ratio: 4.00
Lean loading: 0.200 mol/mol
Capture efficiency: 99.9%
Specific energy: 4.38 GJ/t CO2
Total cost: $297.4/tonne CO2
9. Key Takeaways#
Integrated design considers absorber and stripper together
Key trade-offs:
Higher L/G → better capture but more energy for regeneration
Lower lean loading → more working capacity but higher reboiler duty
Higher reboiler T → better stripping but more energy
Typical costs for amine capture: $50-80/tonne CO2
Steam (reboiler) is dominant operating cost
Capital cost significant for large plants
Solvent selection affects:
Capture efficiency (capacity, kinetics)
Energy consumption (heat of reaction)
Degradation and makeup costs
Gradient-based optimization efficiently finds optimal operating point