Membrane-Based CO2 Separation#

This notebook explores membrane-based gas separation for CO2 capture. Membranes offer advantages over absorption:

  • No solvent regeneration needed

  • Modular and scalable

  • Lower energy for moderate purity requirements

Learning Objectives#

  1. Understand the solution-diffusion model for gas permeation

  2. Compare different membrane materials

  3. Design single and multi-stage membrane systems

  4. Analyze trade-offs between recovery, purity, and area

1. Background: Membrane Gas Separation#

The Solution-Diffusion Model#

Gas permeates through dense polymer membranes by:

  1. Dissolving into the membrane at the high-pressure side

  2. Diffusing through the membrane material

  3. Desorbing at the low-pressure side

The flux of component \(i\) is:

\[J_i = \frac{P_i}{\delta}(p_{i,feed} - p_{i,permeate})\]

where:

  • \(P_i\) = permeability (Barrer = 10⁻¹⁰ cm³(STP)·cm/(cm²·s·cmHg))

  • \(\delta\) = membrane thickness

  • \(p_i\) = partial pressure of component \(i\)

Selectivity#

The selectivity determines separation quality:

\[\alpha_{i/j} = \frac{P_i}{P_j}\]

For CO2/N2 separation:

  • Commercial polymers: α = 20-40

  • High-performance materials: α = 40-100+

Robeson Upper Bound#

There’s a fundamental trade-off: high permeability membranes tend to have low selectivity. The Robeson upper bound (2008) defines the empirical limit:

\[P_{CO2} = k \cdot \alpha^{-n}\]

2. Setup#

import jax
import jax.numpy as jnp

jax.config.update("jax_enable_x64", True)

from difflow.streams import make_stream, get_flows, total_flow

from difflow_cc import (
    get_membrane, list_membranes,
    MembraneParams, MembraneSeparator,
)
from difflow_cc.units.membrane import MultistageMembrane

print("Available membranes:", list_membranes())
Available membranes: ['Matrimid', 'PDMS', 'Cellulose_Acetate', 'PIM_1', 'ZIF8_Matrimid', 'MOF74_Polymer', 'PVAm_Carrier', 'IL_SIL_Membrane', 'CMS', 'Zeolite_DDR']

3. Exploring the Membrane Database#

# Get Matrimid properties (common commercial polyimide)
matrimid = get_membrane("Matrimid")

print(f"Membrane: {matrimid.full_name}")
print(f"Type: {matrimid.membrane_type}")
print()
print("Permeability (Barrer):")
for gas, perm in matrimid.permeability.items():
    print(f"  {gas}: {perm}")
print()
print("Selectivity:")
for pair, sel in matrimid.selectivity.items():
    print(f"  {pair}: {sel}")
print()
print(f"Typical thickness: {matrimid.typical_thickness} μm")
print(f"Max temperature: {matrimid.max_temperature - 273.15:.0f} °C")
print(f"Cost: ${matrimid.cost_usd_m2}/m²")
Membrane: Matrimid 5218 (polyimide)
Type: polymeric

Permeability (Barrer):
  CO2: 10.0
  N2: 0.32
  CH4: 0.28
  O2: 2.1
  H2: 28.0

Selectivity:
  CO2_N2: 31.0
  CO2_CH4: 36.0
  O2_N2: 6.5

Typical thickness: 0.1 μm
Max temperature: 250 °C
Cost: $50.0/m²
# Compare all membrane materials
print(f"{'Membrane':<18} {'Type':<12} {'P_CO2 (Barrer)':<16} {'α (CO2/N2)':<12}")
print("-" * 58)

for mem_name in list_membranes():
    mem = get_membrane(mem_name)
    p_co2 = mem.permeability.get('CO2', 0)
    alpha = mem.selectivity.get('CO2_N2', mem.selectivity.get('CO2_CH4', 0))
    print(f"{mem_name:<18} {mem.membrane_type:<12} {p_co2:<16.1f} {alpha:<12.1f}")
Membrane           Type         P_CO2 (Barrer)   α (CO2/N2)  
----------------------------------------------------------
Matrimid           polymeric    10.0             31.0        
PDMS               polymeric    3200.0           11.4        
Cellulose_Acetate  polymeric    6.0              29.0        
PIM_1              polymeric    2300.0           25.0        
ZIF8_Matrimid      mixed_matrix 25.0             38.5        
MOF74_Polymer      mixed_matrix 800.0            44.0        
PVAm_Carrier       facilitated_transport 50.0             50.0        
IL_SIL_Membrane    facilitated_transport 1000.0           40.0        
CMS                ceramic      180.0            51.0        
Zeolite_DDR        ceramic      100.0            67.0        

4. Single-Stage Membrane Separation#

Key design parameters:

  • Area: Larger area → higher recovery, but more cost

  • Pressure ratio: Higher ratio → better driving force

  • Stage cut: Fraction of feed that permeates (θ = F_permeate/F_feed)

# Define feed gas (flue gas at elevated pressure)
feed = make_stream(
    flows={"CO2": 1.5, "N2": 8.5},  # 15% CO2
    T=298.15,  # 25°C
    P=1000000.0,  # 10 bar
)

print("Feed Gas:")
print(f"  Total flow: {total_flow(feed):.1f} mol/s")
print(f"  CO2: {float(feed['F_CO2']):.1f} mol/s ({float(feed['F_CO2']/total_flow(feed)):.0%})")
print(f"  Pressure: {float(feed['P'])/1e5:.1f} bar")
Feed Gas:
  Total flow: 10.0 mol/s
  CO2: 1.5 mol/s (15%)
  Pressure: 10.0 bar
# Configure membrane separator
params = MembraneParams(
    membrane_type="Matrimid",
    area=1000.0,  # m²
    pressure_ratio=10.0,  # Feed/permeate pressure
    feed_pressure=1000000.0,  # 10 bar
    T_operation=298.15,
)

membrane = MembraneSeparator(params)

# Run separation
retentate, permeate, info = membrane(feed)

print("Membrane Separation Results:")
print(f"  Stage cut: {float(info['stage_cut']):.2%}")
print(f"  CO2 recovery: {float(info['CO2_recovery']):.1%}")
print(f"  CO2 purity in permeate: {float(info['CO2_purity']):.1%}")
print()
print("Permeate (CO2-enriched):")
perm_flows = get_flows(permeate)
print(f"  Flow: {float(info['permeate_flow']):.3f} mol/s")
print(f"  Pressure: {float(permeate['P'])/1e5:.1f} bar")
print()
print("Retentate (treated gas):")
ret_flows = get_flows(retentate)
print(f"  Flow: {float(info['retentate_flow']):.3f} mol/s")
print(f"  CO2 remaining: {float(ret_flows['CO2']):.3f} mol/s")
Membrane Separation Results:
  Stage cut: 18.23%
  CO2 recovery: 59.1%
  CO2 purity in permeate: 48.6%

Permeate (CO2-enriched):
  Flow: 1.823 mol/s
  Pressure: 1.0 bar

Retentate (treated gas):
  Flow: 8.177 mol/s
  CO2 remaining: 0.614 mol/s

5. Effect of Membrane Area#

Larger membrane area increases recovery but has diminishing returns.

areas = [100, 250, 500, 1000, 2000, 5000]

print(f"{'Area (m²)':<12} {'Stage Cut':<12} {'CO2 Recovery':<14} {'CO2 Purity':<12}")
print("-" * 50)

for area in areas:
    params = MembraneParams(
        membrane_type="Matrimid",
        area=float(area),
        pressure_ratio=10.0,
        feed_pressure=1000000.0,
    )
    membrane = MembraneSeparator(params)
    _, _, info = membrane(feed)
    
    print(f"{area:<12} {float(info['stage_cut']):.2%}        "
          f"{float(info['CO2_recovery']):.1%}          "
          f"{float(info['CO2_purity']):.1%}")
Area (m²)    Stage Cut    CO2 Recovery   CO2 Purity  
--------------------------------------------------
100          3.00%        14.0%          70.1%
250          6.50%        28.1%          64.9%
500          11.04%        42.8%          58.2%
1000         18.23%        59.1%          48.6%
2000         30.17%        74.9%          37.2%
5000         61.62%        92.1%          22.4%

6. Comparing Membrane Materials#

membranes = ["Matrimid", "PDMS", "CA", "PIM1", "ZIF8_Matrimid"]

print(f"{'Membrane':<18} {'Stage Cut':<12} {'CO2 Recovery':<14} {'CO2 Purity':<12}")
print("-" * 56)

for mem_name in membranes:
    try:
        params = MembraneParams(
            membrane_type=mem_name,
            area=1000.0,
            pressure_ratio=10.0,
            feed_pressure=1000000.0,
        )
        membrane = MembraneSeparator(params)
        _, _, info = membrane(feed)
        
        print(f"{mem_name:<18} {float(info['stage_cut']):.2%}        "
              f"{float(info['CO2_recovery']):.1%}          "
              f"{float(info['CO2_purity']):.1%}")
    except Exception as e:
        print(f"{mem_name:<18} Error: {e}")
Membrane           Stage Cut    CO2 Recovery   CO2 Purity  
--------------------------------------------------------
Matrimid           18.23%        59.1%          48.6%
PDMS               100.00%        100.0%          15.0%
CA                 Error: "Unknown membrane: CA. Available: ['Matrimid', 'PDMS', 'Cellulose_Acetate', 'PIM_1', 'ZIF8_Matrimid', 'MOF74_Polymer', 'PVAm_Carrier', 'IL_SIL_Membrane', 'CMS', 'Zeolite_DDR']"
PIM1               Error: "Unknown membrane: PIM1. Available: ['Matrimid', 'PDMS', 'Cellulose_Acetate', 'PIM_1', 'ZIF8_Matrimid', 'MOF74_Polymer', 'PVAm_Carrier', 'IL_SIL_Membrane', 'CMS', 'Zeolite_DDR']"
ZIF8_Matrimid      18.78%        61.5%          49.1%

7. Multi-Stage Membrane Cascades#

Single-stage membranes face a trade-off: high recovery means low purity.

Multi-stage configurations can achieve both:

  • Series: Each stage treats the retentate from the previous

  • Permeate recycle: Second stage enriches the permeate further

# Single stage baseline
single_params = MembraneParams(
    membrane_type="Matrimid",
    area=500.0,
    pressure_ratio=10.0,
    feed_pressure=1000000.0,
)
single = MembraneSeparator(single_params)
_, _, single_info = single(feed)

# Two-stage series
series_cascade = MultistageMembrane(
    single_params,
    n_stages=2,
    configuration="series"
)
_, _, series_info = series_cascade(feed)

# Two-stage permeate recycle
recycle_cascade = MultistageMembrane(
    single_params,
    n_stages=2,
    configuration="permeate_recycle"
)
_, _, recycle_info = recycle_cascade(feed)

print(f"{'Configuration':<25} {'CO2 Recovery':<15} {'CO2 Purity':<15}")
print("-" * 55)
print(f"{'Single stage (500 m²)':<25} {float(single_info['CO2_recovery']):.1%}          "
      f"{float(single_info['CO2_purity']):.1%}")
print(f"{'Two-stage series':<25} {float(series_info['overall_CO2_recovery']):.1%}          "
      f"{float(series_info['overall_CO2_purity']):.1%}")
print(f"{'Two-stage w/ recycle':<25} {float(recycle_info['overall_CO2_recovery']):.1%}          "
      f"{float(recycle_info['overall_CO2_purity']):.1%}")
Configuration             CO2 Recovery    CO2 Purity     
-------------------------------------------------------
Single stage (500 m²)     42.8%          58.2%
Two-stage series          66.0%          51.5%
Two-stage w/ recycle      42.8%          58.2%

8. Key Takeaways#

  1. Membrane separation is governed by permeability and selectivity

  2. Trade-offs exist:

    • Recovery vs purity (single stage)

    • Permeability vs selectivity (Robeson bound)

    • Area vs cost

  3. Material selection depends on application:

    • PDMS: High flux, moderate selectivity (natural gas)

    • Polyimides: Good selectivity (post-combustion)

    • Mixed-matrix: Approaching upper bound

  4. Multi-stage configurations overcome single-stage limitations