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#
Understand adsorption isotherms and working capacity
Compare different regeneration strategies
Select appropriate adsorbents for different applications
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:
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#
Working capacity is the key metric for cyclic processes
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
Adsorbent selection depends on:
Feed CO2 concentration (isotherm shape matters)
Required purity (selectivity)
Energy cost (heat of adsorption)
Capital cost (material price, stability)
DAC is challenging due to very low CO2 concentration (400 ppm)