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#

  1. Design a complete amine capture loop

  2. Calculate mass and energy balances

  3. Evaluate capture cost ($/tonne CO2)

  4. 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#

  1. Integrated design considers absorber and stripper together

  2. 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

  3. Typical costs for amine capture: $50-80/tonne CO2

    • Steam (reboiler) is dominant operating cost

    • Capital cost significant for large plants

  4. Solvent selection affects:

    • Capture efficiency (capacity, kinetics)

    • Energy consumption (heat of reaction)

    • Degradation and makeup costs

  5. Gradient-based optimization efficiently finds optimal operating point