Adsorption-Based CO2 Capture#

This notebook covers cyclic adsorption processes for CO2 capture:

  • PSA: Pressure Swing Adsorption

  • VSA: Vacuum Swing Adsorption

  • TSA: Temperature Swing Adsorption

  • TVSA: Temperature-Vacuum Swing Adsorption

Learning Objectives#

  1. Understand adsorption isotherms and working capacity

  2. Compare different regeneration strategies

  3. Select appropriate adsorbents for different applications

  4. Analyze energy consumption and productivity

1. Background: Adsorption Fundamentals#

Adsorption Isotherms#

Isotherms describe equilibrium between gas and adsorbed phase:

Langmuir (single site): $\(q = q_{sat} \frac{bP}{1 + bP}\)$

Sips (heterogeneous surfaces): $\(q = q_{sat} \frac{(bP)^n}{1 + (bP)^n}\)$

Toth (asymmetric energy distribution): $\(q = \frac{q_{sat} bP}{(1 + (bP)^t)^{1/t}}\)$

Working Capacity#

The key metric for cyclic processes is the working capacity:

\[\Delta q = q_{ads} - q_{des}\]

This represents how much CO2 is captured and released per cycle.

Regeneration Strategies#

Cycle

Regeneration Method

Typical Application

PSA

Pressure reduction

High-P feeds (H2 purification)

VSA

Vacuum

Atmospheric feeds (post-combustion)

TSA

Temperature increase

Dilute feeds (DAC)

TVSA

Combined T + vacuum

DAC, high purity

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 (
    # Database
    get_adsorbent, list_adsorbents, get_isotherm,
    # Isotherms
    langmuir, langmuir_T, sips, toth,
    working_capacity_PSA, working_capacity_TSA,
    # Unit operations
    AdsorptionParams, PSAUnit, VSAUnit, TSAUnit, TVSAUnit,
)

print("Available adsorbents:", list_adsorbents())
Available adsorbents: ['Zeolite_13X', 'Zeolite_5A', 'Mg_MOF_74', 'CALF_20', 'ZIF_8', 'AC_Coconut', 'AC_Nitrogen_Doped', 'PEI_Silica', 'TEPA_Alumina']

3. Exploring the Adsorbent Database#

# Examine Zeolite 13X (benchmark material)
zeolite = get_adsorbent("Zeolite_13X")

print(f"Adsorbent: {zeolite.full_name}")
print(f"Type: {zeolite.material_type}")
print()
print("Physical Properties:")
print(f"  BET surface area: {zeolite.surface_area} m²/g")
print(f"  Pore volume: {zeolite.pore_volume} cm³/g")
print(f"  Pore diameter: {zeolite.pore_diameter} Å")
print()
print("Adsorption Properties:")
print(f"  CO2 capacity (1 bar, 25°C): {zeolite.CO2_capacity} mol/kg")
print(f"  CO2/N2 selectivity: {zeolite.CO2_selectivity}")
print(f"  Heat of adsorption: {zeolite.heat_of_adsorption} kJ/mol")
print()
print(f"Isotherm model: {zeolite.isotherms['CO2'].model}")
print(f"  Parameters: {zeolite.isotherms['CO2'].params}")
Adsorbent: Zeolite 13X (NaX)
Type: zeolite

Physical Properties:
  BET surface area: 650.0 m²/g
  Pore volume: 0.3 cm³/g
  Pore diameter: 7.4 Å

Adsorption Properties:
  CO2 capacity (1 bar, 25°C): 5.5 mol/kg
  CO2/N2 selectivity: 62.0
  Heat of adsorption: 34.65 kJ/mol

Isotherm model: toth
  Parameters: {'q_sat': 6.06, 'b0': 1.73e-09, 'Q': 36000.0, 't0': 0.39, 'alpha': 0.0, 'T_ref': 298.15}
# Compare all adsorbents
print(f"{'Material':<16} {'Type':<20} {'CO2 Cap.':<10} {'ΔH_ads':<10} {'Cost':<8}")
print(f"{'':16} {'':20} {'(mol/kg)':10} {'(kJ/mol)':10} {'($/kg)':8}")
print("-" * 64)

for name in list_adsorbents():
    ads = get_adsorbent(name)
    print(f"{name:<16} {ads.material_type:<20} {ads.CO2_capacity:<10.2f} "
          f"{ads.heat_of_adsorption:<10.1f} {ads.cost_usd_kg:<8.0f}")
Material         Type                 CO2 Cap.   ΔH_ads     Cost    
                                      (mol/kg)   (kJ/mol)   ($/kg)  
----------------------------------------------------------------
Zeolite_13X      zeolite              5.50       34.6       3       
Zeolite_5A       zeolite              4.20       38.0       4       
Mg_MOF_74        MOF                  8.61       42.0       50      
CALF_20          MOF                  3.80       37.0       80      
ZIF_8            MOF                  2.50       25.0       100     
AC_Coconut       activated_carbon     2.50       22.0       2       
AC_Nitrogen_Doped activated_carbon     4.20       28.0       5       
PEI_Silica       amine_functionalized 3.00       70.0       15      
TEPA_Alumina     amine_functionalized 2.50       68.0       12      

4. Understanding Isotherms#

# Get CO2 isotherm for Zeolite 13X
isotherm = get_isotherm("Zeolite_13X", "CO2")

# Calculate loading at different pressures
pressures = [1000, 5000, 10000, 20000, 50000, 100000]  # Pa
T = 298.15  # K

print(f"Zeolite 13X CO2 isotherm at {T-273.15:.0f}°C:")
print()
print(f"{'Pressure (Pa)':<15} {'Pressure (bar)':<15} {'Loading (mol/kg)':<18}")
print("-" * 48)

for P in pressures:
    q = isotherm(P, T)
    print(f"{P:<15} {P/1e5:<15.3f} {float(q):<18.3f}")
Zeolite 13X CO2 isotherm at 25°C:

Pressure (Pa)   Pressure (bar)  Loading (mol/kg)  
------------------------------------------------
1000            0.010           1.779             
5000            0.050           2.933             
10000           0.100           3.422             
20000           0.200           3.875             
50000           0.500           4.397             
100000          1.000           4.727             
# Temperature effect on isotherm
P = 15000.0  # 15% CO2 at 1 bar
temperatures = [273.15, 298.15, 323.15, 348.15, 373.15]

print(f"Loading at P = {P/1e5:.3f} bar CO2:")
print()
print(f"{'Temperature (°C)':<18} {'Loading (mol/kg)':<18}")
print("-" * 36)

for T in temperatures:
    q = isotherm(P, T)
    print(f"{T-273.15:<18.0f} {float(q):<18.3f}")
Loading at P = 0.150 bar CO2:

Temperature (°C)   Loading (mol/kg)  
------------------------------------
0                  4.461             
25                 3.693             
50                 2.915             
75                 2.215             
100                1.639             

5. Working Capacity Analysis#

# PSA working capacity: vary desorption pressure
P_ads = 500000.0  # 5 bar adsorption
T = 298.15

print("PSA Working Capacity (Zeolite 13X):")
print(f"Adsorption: P = {P_ads/1e5:.1f} bar, T = {T-273.15:.0f}°C")
print()
print(f"{'P_des (bar)':<12} {'q_ads':<10} {'q_des':<10} {'Δq (mol/kg)':<12}")
print("-" * 44)

for P_des in [200000, 150000, 100000, 50000]:
    wc = working_capacity_PSA(isotherm, P_ads, P_des, T)
    q_ads = isotherm(P_ads, T)
    q_des = isotherm(P_des, T)
    print(f"{P_des/1e5:<12.1f} {float(q_ads):<10.3f} {float(q_des):<10.3f} {float(wc):<12.3f}")
PSA Working Capacity (Zeolite 13X):
Adsorption: P = 5.0 bar, T = 25°C

P_des (bar)  q_ads      q_des      Δq (mol/kg) 
--------------------------------------------
2.0          5.292      5.003      0.289       
1.5          5.292      4.895      0.397       
1.0          5.292      4.727      0.564       
0.5          5.292      4.397      0.895       
# TSA working capacity: vary desorption temperature
P_CO2 = 15000.0  # 15% CO2 at 1 bar
T_ads = 298.15

print("TSA Working Capacity (Zeolite 13X):")
print(f"P_CO2 = {P_CO2/1e5:.3f} bar, T_ads = {T_ads-273.15:.0f}°C")
print()
print(f"{'T_des (°C)':<12} {'q_ads':<10} {'q_des':<10} {'Δq (mol/kg)':<12}")
print("-" * 44)

for T_des in [353.15, 373.15, 393.15, 423.15]:
    wc = working_capacity_TSA(isotherm, P_CO2, T_ads, T_des)
    q_ads = isotherm(P_CO2, T_ads)
    q_des = isotherm(P_CO2, T_des)
    print(f"{T_des-273.15:<12.0f} {float(q_ads):<10.3f} {float(q_des):<10.3f} {float(wc):<12.3f}")
TSA Working Capacity (Zeolite 13X):
P_CO2 = 0.150 bar, T_ads = 25°C

T_des (°C)   q_ads      q_des      Δq (mol/kg) 
--------------------------------------------
80           3.693      2.089      1.604       
100          3.693      1.639      2.054       
120          3.693      1.273      2.419       
150          3.693      0.866      2.827       

6. Comparing Cycle Types#

# Define feed gases
feed_atm = make_stream(
    flows={"CO2": 1.5, "N2": 8.5},  # 15% CO2
    T=298.15,
    P=101325.0,  # 1 atm
)

feed_5bar = make_stream(
    flows={"CO2": 1.5, "N2": 8.5},
    T=298.15,
    P=500000.0,  # 5 bar
)

# PSA
psa_params = AdsorptionParams(
    adsorbent="Zeolite_13X",
    cycle_type="PSA",
    P_adsorption=500000.0,
    P_desorption=100000.0,
    bed_mass=100.0,
    n_beds=2,
)
psa = PSAUnit(psa_params)
_, _, psa_info = psa(feed_5bar)

# VSA
vsa_params = AdsorptionParams(
    adsorbent="Zeolite_13X",
    cycle_type="VSA",
    P_adsorption=101325.0,
    P_desorption=10000.0,
    bed_mass=100.0,
    n_beds=2,
)
vsa = VSAUnit(vsa_params)
_, _, vsa_info = vsa(feed_atm)

# TSA
tsa_params = AdsorptionParams(
    adsorbent="Zeolite_13X",
    cycle_type="TSA",
    T_adsorption=298.15,
    T_desorption=423.15,
    bed_mass=100.0,
    n_beds=2,
)
tsa = TSAUnit(tsa_params)
_, _, tsa_info = tsa(feed_atm)

# TVSA
tvsa_params = AdsorptionParams(
    adsorbent="Zeolite_13X",
    cycle_type="TVSA",
    T_adsorption=298.15,
    T_desorption=373.15,
    P_adsorption=101325.0,
    P_desorption=20000.0,
    bed_mass=100.0,
    n_beds=2,
)
tvsa = TVSAUnit(tvsa_params)
_, _, tvsa_info = tvsa(feed_atm)

# Compare
print(f"{'Cycle':<8} {'Work. Cap.':<12} {'Recovery':<10} {'Productivity':<14} {'Energy':<10}")
print(f"{'':8} {'(mol/kg)':12} {'(%)':10} {'(mol/kg/hr)':14} {'(GJ/t)':10}")
print("-" * 54)

for name, info in [("PSA", psa_info), ("VSA", vsa_info), ("TSA", tsa_info), ("TVSA", tvsa_info)]:
    wc = float(info['working_capacity'])
    rec = float(info['recovery'])
    prod = float(info['productivity'])
    energy = float(info['specific_energy'])
    print(f"{name:<8} {wc:<12.3f} {rec:<10.1%} {prod:<14.2f} {energy:<10.2f}")
Cycle    Work. Cap.   Recovery   Productivity   Energy    
         (mol/kg)     (%)        (mol/kg/hr)    (GJ/t)    
------------------------------------------------------
PSA      0.246        5.9%       1.64           0.13      
VSA      0.831        18.5%      5.54           0.22      
TSA      2.829        49.5%      18.86          2.09      
TVSA     2.371        43.7%      15.80          2.25      

7. Adsorbent Selection for Different Applications#

# Compare adsorbents for VSA application
adsorbents = ["Zeolite_13X", "Zeolite_5A", "Mg_MOF_74", "AC_Coconut", "PEI_Silica"]

print("VSA Performance Comparison (1 atm → 0.1 atm):")
print()
print(f"{'Adsorbent':<16} {'Work. Cap.':<12} {'Recovery':<10} {'Productivity':<14}")
print("-" * 52)

for ads_name in adsorbents:
    try:
        params = AdsorptionParams(
            adsorbent=ads_name,
            cycle_type="VSA",
            P_adsorption=101325.0,
            P_desorption=10000.0,
            bed_mass=100.0,
        )
        vsa = VSAUnit(params)
        _, _, info = vsa(feed_atm)
        
        wc = float(info['working_capacity'])
        rec = float(info['recovery'])
        prod = float(info['productivity'])
        
        print(f"{ads_name:<16} {wc:<12.3f} {rec:<10.1%} {prod:<14.2f}")
    except Exception as e:
        print(f"{ads_name:<16} Error: {e}")
VSA Performance Comparison (1 atm → 0.1 atm):

Adsorbent        Work. Cap.   Recovery   Productivity  
----------------------------------------------------
Zeolite_13X      0.831        18.5%      5.54          
Zeolite_5A       0.685        15.5%      4.56          
Mg_MOF_74        0.789        17.6%      5.26          
AC_Coconut       0.918        20.2%      6.12          
PEI_Silica       0.283        6.7%       1.89          

8. Direct Air Capture (DAC) Application#

DAC requires:

  • High capacity at very low CO2 concentrations (~400 ppm)

  • Strong affinity for CO2

  • Reasonable regeneration energy

Amine-functionalized adsorbents are preferred.

# DAC feed: 400 ppm CO2
dac_feed = make_stream(
    flows={"CO2": 0.04, "N2": 99.96},  # 400 ppm
    T=298.15,
    P=101325.0,
)

# TSA with amine-functionalized silica
dac_params = AdsorptionParams(
    adsorbent="PEI_Silica",
    cycle_type="TSA",
    T_adsorption=298.15,
    T_desorption=373.15,  # Lower than zeolites
    bed_mass=1000.0,  # Larger bed for dilute feed
    n_beds=4,
    t_adsorption=1800.0,  # 30 min adsorption
)

dac_tsa = TSAUnit(dac_params)
_, _, dac_info = dac_tsa(dac_feed)

print("DAC with PEI-Silica (TSA):")
print(f"  Working capacity: {float(dac_info['working_capacity']):.4f} mol/kg")
print(f"  Recovery: {float(dac_info['recovery']):.1%}")
print(f"  Heating power: {float(dac_info['heating_power'])/1000:.1f} kW")
print(f"  Specific energy: {float(dac_info['specific_energy']):.1f} GJ/tonne CO2")
DAC with PEI-Silica (TSA):
  Working capacity: 0.0042 mol/kg
  Recovery: 18.5%
  Heating power: 177.0 kW
  Specific energy: 543.5 GJ/tonne CO2

9. Key Takeaways#

  1. Working capacity is the key metric for cyclic processes

  2. Cycle selection depends on feed conditions:

    • PSA: High-pressure feeds (>5 bar)

    • VSA: Atmospheric pressure, moderate concentrations

    • TSA: Dilute feeds, where T-swing gives large Δq

    • TVSA: Best of both worlds, but more complex

  3. Adsorbent selection depends on:

    • Feed CO2 concentration (isotherm shape matters)

    • Required purity (selectivity)

    • Energy cost (heat of adsorption)

    • Capital cost (material price, stability)

  4. DAC is challenging due to very low CO2 concentration (400 ppm)