Tutorial: TSA regeneration (illustrative)¶
Illustrative deck, not a validated reproduction. No temperature swing experiment is bundled with Skarstrom yet, so this tutorial uses a deck built for the documentation. The adsorbent is a cited library entry and the column is that of the Perez (2019) experiments, but the operating conditions (feed velocity, step times, heater and cooler duties) are round numbers we chose, and the heating step takes the isotherm above its 298–348 K fit range. Use it to learn the workflow, not as design data.
Goal. Capture CO₂ from a 15 % CO₂ / 85 % N₂ flue gas with a four-step, indirectly heated temperature swing adsorption (TSA) cycle: adsorb cold, heat the bed to release CO₂, cool it down again, re-pressurise. You will set up per-step energy sources (an electric heater and a cooler), run to cyclic steady state, and read purity, recovery and the thermal energy and cooling duty per kilogram of CO₂.
1. Load the deck¶
import skarstrom
sim = skarstrom.examples.Illustrative_TSA_13X.load()
print(sim.steps.names) # ['Adsorption', 'Heating', 'Cooling', 'Pressurisation']
The deck is generated by the IllustrativeTSA13X test in core/test/integration/test_tsa_example.cpp, which builds it in code (the clearest example of setting up energy sources in C++), re-saves it to examples/decks/ and checks that it still runs. examples/tsa_illustrative.py runs it and plots the bed temperatures.
2. Walk through the setup¶
Fluid. CarbonDioxide and Nitrogen.
Adsorbents. One material, Zeolite 13X (Perez 2019), added unchanged from the curated library (AddLibraryAdsorbent in C++; skarstrom.adsorbent_path("zeolite_13x_perez2019") gives the file). It carries the competitive dual-site Langmuir isotherm (concentration form) from Table 2 of Estupiñan Perez et al. (2019), a macropore-controlled linear driving force with tortuosity 3, and heats of adsorption of 30.85 kJ/mol (CO₂) and 15.93 kJ/mol (N₂). The library entry states the isotherm was fitted at 298–348 K and 0–1 bar.
Column.
- Geometry: a cylinder of 0.032 m diameter, ambient temperature 298 K. One layer, 13X: 0.30 m long, 30 cells (with the default WENO advection the recovery is grid-converged on about 20), bed voidage 0.41, particle diameter 1.5 mm, momentum model Ergun (the default for a new layer; the Perez2019 decks use Darcy (Kozeny–Carman), see What to try next).
- Energy Balance: non-isothermal, local thermal equilibrium, adsorbed-phase heat capacity 30.7 J mol⁻¹ K⁻¹. The layer uses constant internal and external heat transfer coefficients of 8.6 and 10 W m⁻² K⁻¹ and an axial thermal conductivity of 0.0903 W m⁻¹ K⁻¹, as in the Perez2019 decks.
- Wall: material SS316L (density, heat capacity and conductivity come from the wall material library), 3.1 mm thick.
Cycle. Four steps, 2430 s per cycle:
| Step | Time | Bottom (feed end) | Top | Energy source |
|---|---|---|---|---|
| Adsorption | 600 s | Flow inlet Flue gas: 0.05 m/s superficial, 15 % CO₂, 298 K | Pressure outlet, 1 bar | none |
| Heating | 900 s | Pressure outlet, 1 bar: CO₂ product | Closed | +30 W |
| Cooling | 900 s | Closed | Closed | −12 W |
| Pressurisation | 30 s | Pressure inlet, linear ramp to 1 bar, feed gas at 298 K | Closed | none |
The Heating step has an energy source on the 13X layer with a power of 30 W. A positive power heats the layer and a negative one cools it. The duty is spread uniformly over the layer volume and added to the energy balance for the whole step (see the energy balance). It is a fixed duty, not a thermostat: the bed keeps heating for as long as the step lasts, whatever its temperature.
Heating vents the bed counter-currently, through the feed end, so the CO₂ comes out where it is most concentrated and the top of the bed stays clean for the next adsorption step. Cooling with both ends closed lets the pressure fall as the gas cools and CO₂ readsorbs; the pressurisation step then refills the bed with feed.
Cyclic steady state. Mass Balance (Per-Component), tolerance 0.005, minimum 5 cycles, at most 60 cycles.
KPI configuration. CO₂ is tracked. Feed: the Flue gas stream of Adsorption and the pressure inlet of Pressurisation. Product: the Heating pressure outlet.
3. Run¶
result = sim.run()
print(result.status, result.css_cycle + 1, f"{result.wall_time_s:.1f} s")
# reached_css 16 2.9 s
CSS is reached at cycle 16, with no step retries, in about 3 s natively.
4. Interpret the results¶
KPIs. At cycle 15 (the sixteenth cycle, counted from 0):
k = next(k for k in sim.kpis(cycle=-1) if k.component == "CarbonDioxide")
| KPI | Value |
|---|---|
| CO₂ purity | 83.51 % |
| CO₂ recovery | 66.59 % |
| Production rate | 4.16 × 10⁻⁵ mol/s |
| Productivity (mass) | 1.13 × 10⁻⁵ kg CO₂ per kg adsorbent per second |
| Electrical energy | 0 |
| Thermal energy | 6.07 MJ per kg CO₂ |
| Cooling duty | 2.43 MJ per kg CO₂ |
print(k.electrical_energy, k.thermal_energy, k.cooling_duty) # J/kg
print(k.step_thermal_energy, k.step_cooling_duty) # each step's share
The electrical energy counts compressor and vacuum-pump work, so it is zero here. The thermal energy is the heat the energy sources supply: the heater's 30 W × 900 s = 27 kJ per cycle over the 4.16 × 10⁻⁵ mol/s × 2430 s = 0.101 mol (4.45 g) of CO₂ produced, 6.07 MJ per kg of CO₂ (6.07 GJ/t; see energy units). The cooling duty, 12 W × 900 s = 10.8 kJ per cycle or 2.43 MJ/kg, is the heat the cooler removes; it is reported separately, not subtracted from the thermal energy, and in a real plant it would come on top of it. step_thermal_energy puts all the heat on Heating and step_cooling_duty all the cooling on Cooling. The thermal energy counts energy sources such as this heater; if you heat with a heating medium instead, its duty is not in the KPI yet.
Temperatures. The axial temperature profile at the end of each step of the last cycle:
for i, name in enumerate(sim.steps):
T = sim.profile("temperature", cycle=-1, step=i)
print(f"{name:15s} {T.values[-1].min():6.1f} - {T.values[-1].max():6.1f} K")
- at the end of Adsorption the bed runs from 301 K at the feed end to 320 K at the top: the heat of adsorption travels ahead of the CO₂ front;
- at the end of Heating it is at 383–395 K;
- at the end of Cooling it is back to 297–303 K.
sim.series("temperature", z=0.15) gives the temperature at one position over the last cycle; with cycle=0, 1, … you can watch it settle into the same loop every cycle.
Product quality. The CO₂ mole fraction leaving the bottom during Heating (step index 1):
y = sim.stream_series(1, "Heating Pressure Boundary Bottom", "mole_fraction", "CarbonDioxide")
F = sim.stream_series(1, "Heating Pressure Boundary Bottom", "molar_flow_rate")
The first gas out of the bed is the feed left in the voids, 15 % CO₂. The outlet then enriches as the bed heats: 41 % CO₂ after 150 s, 68 % after 300 s, 88 % after 450 s, 97 % after 600 s and 99.97 % at the end of the step. The diluted gas from the start of the step is why the overall purity is 84 % even though most of the CO₂ leaves at above 95 %. The molar flow on the same stream grows as desorption speeds up.
5. What to try next¶
- Change the cooling duty. The energy-source powers are registry parameters:
sim["step[Cooling].energy[13X].power"] = -20in Python, or--set "step[Cooling].energy[13X].power=-20"on the command line; the cooling duty KPI follows. While building the deck we ran the same cycle (native build, Ergun, upwind advection and the CSS rule of the time, so the cycle counts are lower than today's) with three cooler powers:
| Cooler | CSS at cycle | Purity | Recovery | Lowest bed temperature in the last cycle |
|---|---|---|---|---|
| −25 W | 11 | 87.3 % | 90.4 % | 249 K |
| −17 W | 10 | 85.0 % | 76.3 % | 277 K |
| −12 W (deck) | 9 | 83.6 % | 67.1 % | 293 K |
More cooling leaves the bed colder and able to hold more CO₂, so recovery rises, but a fixed duty overshoots far below ambient. The deck uses −12 W because it brings the bed back to about ambient temperature.
- Discard the diluted start of the heating step. Split Heating into two steps, say 150 s and 750 s, each with the 30 W source, and mark only the second step's outlet as Product in the KPI configuration so the first is vented. Purity should rise and recovery fall. Splitting steps and editing energy sources are deck edits (see the C++ test that builds this deck); from Python you can estimate the effect from the stream logs, integrating the Heating outlet flows from 150 s onwards.
- Heat with a hot purge instead of the heater. Remove the Heating energy source and give the step a top Flow inlet of N₂ whose Temperature Profile is Linear from 298 K to 423 K (see inlet temperature profiles). The purge sweeps CO₂ out faster but dilutes the product.
- Add a vacuum. Lower the Heating outlet pressure (a temperature–vacuum swing; step[Heating].bottom.pressure) and give that boundary a vacuum pump with power calculation so the electrical energy KPI picks up the pump work.
- Use a direct air capture sorbent. The library's APDES-NFC amine sorbent has a temperature-dependent Toth isotherm for CO₂ from Stampi-Bombelli et al. (2020). Its rate constant is 2 × 10⁻⁴ s⁻¹, so adsorption steps last hours rather than minutes.
- Compare the momentum models. Switch the layer's momentum model (layer[13X].momentum_model) to Darcy (Kozeny–Carman), as in the Perez2019 decks. At 0.05 m/s through 1.5 mm particles the inertial (Burke–Plummer) term of Ergun is small, so the results barely move: in our native run the cycle again reached CSS at cycle 9, with a CO₂ purity of 83.55 % and a recovery of 66.94 %. Both models cross this cycle's abrupt step switches without a solver retry.
- Sweep the heating time (step[Heating].time) in a parametric study and plot recovery against the thermal energy (k.thermal_energy).