Skip to content

Building a process from its parts ​

This example builds the four-step vacuum swing adsorption (VSA) process used in Getting started from its physical parameters, following Haghpanah et al. (2013). A process is described in layers, each of which can be edited, saved to a file and reused on its own:

  1. the gas and the feed;

  2. the isotherm, given component by component, and the adsorbent;

  3. the column;

  4. the flowsheet: beds, and the devices connected to them;

  5. the cycle: a list of steps;

  6. the process, which puts them together with the time stepping.

Everything is in SI units. Mocca.Units has conversions, so that 0.1bar is 1e4 (Pa) and -36.64kJ_per_mol is -36640.0 (J/mol).

julia
import Mocca
using Mocca.Units

1. The gas and the feed ​

Components are known by name. Their molar masses, heat capacities and viscosities default to standard values; here the molar mass of N₂ and the heat capacities follow Haghpanah et al. (2013). The mixture viscosity and density default to those of the feed. The first component is the one to capture.

julia
gas = Mocca.Gas(["CO2", "N2"];
    molar_masses = (N2 = 28e-3,),
    heat_capacities = (CO2 = 697.5687, N2 = 1096.4),
    diffusivity = 1.6e-5)

feed = Mocca.Feed(gas; composition = (CO2 = 0.15, N2 = 0.85), temperature = celsius(25), pressure = 1bar)
Feed([0.15, 0.85], 298.15, 100000.0, [1.0e-10, 0.9999999999000001], false)

2. The isotherm and the adsorbent ​

A dual-site Langmuir isotherm. Each component lists its own parameters, so adding or removing a component does not mean editing parallel arrays. Capacities are per m³ of adsorbent here (basis = :mass takes mol/kg), and affinities are on a concentration basis [m³/mol]. Mocca.langmuir gives a single-site isotherm.

julia
isotherm = Mocca.dual_site_langmuir(
    "CO2" => (qsb = 3489.44, b0 = 8.65e-7, ΔUb = -36.64121kJ_per_mol,
              qsd = 2872.35, d0 = 2.63e-8, ΔUd = -35.69066kJ_per_mol),
    "N2"  => (qsb = 6613.551, b0 = 2.5e-6, ΔUb = -15.8kJ_per_mol))
Mocca.Interface.DualSiteLangmuirData(["CO2", "N2"], [3489.44, 6613.551], [8.65e-7, 2.5e-6], [-36641.21, -15800.0], [2872.35, 0.0], [2.63e-8, 0.0], [-35690.66, 0.0], 298.15, :volume)

The adsorbent has a density and heat capacity, and the particle properties that set the macropore diffusion of the linear driving force for mass transfer.

julia
adsorbent = Mocca.Adsorbent("my zeolite 13X";
    isotherm = isotherm,
    density = 1130.0,              # kg/m³
    heat_capacity = 1070.0,        # J/(kg·K)
    particle_diameter = 2mm,
    particle_porosity = 0.35,
    tortuosity = 3.0)
Adsorbent my zeolite 13X
  dual-site Langmuir isotherm for CO2, N2
  density 1130.0 kg/m³, heat capacity 1070.0 J/(kg·K)
  particles 2.0 mm, porosity 0.35, tortuosity 3.0

Check the isotherm before simulating: the loading of each component in equilibrium with the feed, in mol/kg, and the CO₂ isotherm at two temperatures.

julia
q = Mocca.equilibrium_loading(adsorbent, gas; pressure = 1bar, temperature = 298.15, composition = (CO2 = 0.15, N2 = 0.85))
println("loading in equilibrium with the feed: CO₂ $(round(q[1], digits = 2)), N₂ $(round(q[2], digits = 3)) mol/kg")
pressures = [0.01, 0.05, 0.1, 0.2, 0.5, 1.0] .* bar
table = Mocca.isotherm_table(adsorbent, gas, "CO2"; pressures = pressures, temperatures = [298.15, 348.15])
println(rpad("CO₂ at", 10), rpad("25 °C", 14), "75 °C")
for (P, row) in zip(pressures, eachrow(table))
    println(rpad("$(P / bar) bar", 10), join([rpad("$(round(x, digits = 2)) mol/kg", 14) for x in row]))
end
loading in equilibrium with the feed: CO₂ 3.43, N₂ 0.02 mol/kg
CO₂ at    25 °C         75 °C
0.01 bar  1.52 mol/kg   0.27 mol/kg
0.05 bar  2.76 mol/kg   1.01 mol/kg
0.1 bar   3.19 mol/kg   1.55 mol/kg
0.2 bar   3.63 mol/kg   2.12 mol/kg
0.5 bar   4.26 mol/kg   2.78 mol/kg
1.0 bar   4.72 mol/kg   3.22 mol/kg

The adsorbent can be saved and shared. This one is the zeolite 13X of Haghpanah et al. (2013), which Mocca's library also holds, as Mocca.load_adsorbent("zeolite_13x_haghpanah_2013").

julia
path = Mocca.save_adsorbent(joinpath(mktempdir(), "my_13x.json"), adsorbent)
Mocca.load_adsorbent(path)
Adsorbent my zeolite 13X
  dual-site Langmuir isotherm for CO2, N2
  density 1130.0 kg/m³, heat capacity 1070.0 J/(kg·K)
  particles 2.0 mm, porosity 0.35, tortuosity 3.0

3. The column ​

The bed exchanges heat with the column wall, and the wall with the surroundings.

julia
column = Mocca.Column(
    length = 1.0,                  # m
    radius = 14.45cm,
    outer_radius = 16.2cm,         # of the wall
    porosity = 0.37,               # bed void fraction
    axial_conductivity = 0.0903,   # W/(m·K)
    wall = Mocca.Wall(density = 7800.0, heat_capacity = 502.0, conductivity = 16.0),
    inner_heat_transfer = 8.6,     # W/(m²·K), bed to wall
    outer_heat_transfer = 2.5,     # W/(m²·K), wall to surroundings
    ambient_temperature = 298.15)
Column 1.0 m long, radius 0.1445 m (wall to 0.162 m), bed porosity 0.37

4. The flowsheet ​

A bed of 50 cells and three devices. Each device has a role, which sets its default boundary (the feed, the raffinate header, the vacuum line), tells the steps which device to open, and tells performance which streams are feed and product.

feed ──V_feed──┐                 ┌──V_product── raffinate
               bottom  Bed  top
extract ──V_vacuum──┘

The bed has an energy balance for the wall by default (thermal = Mocca.WithWall()); Mocca.Adiabatic() and Mocca.Isothermal() are the alternatives (see the breakthrough example).

julia
fs = Mocca.Flowsheet(gas, feed)
Mocca.add_bed!(fs, :Bed, adsorbent, column; ncells = 50)
Mocca.add_device!(fs, :V_feed, :feed; at = (:Bed, :bottom))
Mocca.add_device!(fs, :V_product, :product; at = (:Bed, :top))
Mocca.add_device!(fs, :V_vacuum, :extract; at = (:Bed, :bottom))
fs
Flowsheet with 1 unit and 3 devices for CO2, N2
  Bed: fixed bed of my zeolite 13X, 50 cells
    bottom V_feed (feed), from boundary at 1.0 bar; V_vacuum (extract), to boundary at 1.0 bar
    top    V_product (product), to boundary at 1.0 bar

5. The cycle ​

Steps are written in process terms. Each opens the devices it needs, found by their roles, and keeps the others closed. Pressures carry over from step to step: the pressurisation starts where the evacuation left the bed, and the blowdown where the adsorption did. The feed velocity is interstitial. The other steps are Purge, Equalise and Idle (see the two-bed VSA example), and new kinds of step can be added (see Extending Mocca).

julia
cycle = Mocca.Cycle([
    Mocca.Pressurise(15.0),
    Mocca.Adsorb(15.0; velocity = 1.0),
    Mocca.Blowdown(30.0; to = 0.2bar),
    Mocca.Evacuate(40.0; to = 0.1bar),
])
Cycle of 4 steps
  pressurisation      15.0 s
  adsorption          15.0 s
  blowdown            30.0 s
  evacuation          40.0 s

6. The process, and a check of the description ​

By default a bed starts at the lowest pressure of its cycle; here it starts full of nitrogen at 1 bar. validate lists likely mistakes in the description that would not stop a simulation, such as a bed whose axial dispersion was set for a velocity far from that of its adsorption step.

julia
process = Mocca.Process(fs, cycle; initial_state = Dict(:Bed => (pressure = 1bar,)))
issues = Mocca.validate(process)
println(isempty(issues) ? "no problems found" : join(sprint.(showerror, issues), "\n"))
process
Process: CO2/N2 feed at 1.0 bar, 298.15 K, capturing CO2
  Bed: fixed bed of my zeolite 13X, 50 cells
    bottom V_feed (feed), from boundary at 1.0 bar; V_vacuum (extract), to boundary at 0.1 bar
    top    V_product (product), to boundary at 1.0 bar
  cycle of 4 stages, 100.0 s:
    pressurisation                        15.0 s   V_feed
    adsorption                            15.0 s   V_feed, V_product
    blowdown                              30.0 s   V_product
    evacuation                            40.0 s   V_vacuum

7. Simulate ​

simulate runs the cycle a given number of times from the initial state. The outlet at the top of the bed over the first five cycles. The pressure swings between the feed pressure and the evacuation pressure in every cycle, and the top of the bed cools by about 5 K each time it is depressurised. The gas there stays pure nitrogen: the CO₂ front never reaches the top of this bed.

julia
result = Mocca.simulate(process; cycles = 5)
bed_states = [s[:Bed] for s in result.states]
f_outlet = Mocca.plot_cell(bed_states, fs[:Bed], result.timesteps, 50)

and the state along the bed at the end:

julia
f_column = Mocca.plot_state(bed_states[end], fs[:Bed])

The bed is still far from steady state after five cycles. The performance over the last of them, and at cyclic steady state:

julia
perf_5 = Mocca.performance(result)
perf = Mocca.performance(Mocca.cyclic_steady_state(process))
for (label, p) in ("after 5 cycles" => perf_5, "at steady state" => perf)
    println(rpad(label, 18), "purity $(round(100p.purity, digits = 1))%, recovery $(round(100p.recovery, digits = 1))%")
end
after 5 cycles    purity 47.0%, recovery 28.7%
at steady state   purity 79.7%, recovery 37.1%

8. Save the whole process ​

The process file holds every layer, and reads back as the same process.

julia
file = Mocca.write_process(joinpath(mktempdir(), "my_vsa.json"), process)
same = Mocca.read_process(file)
Process: CO2/N2 feed at 1.0 bar, 298.15 K, capturing CO2
  Bed: fixed bed of my zeolite 13X, 50 cells
    bottom V_feed (feed), from boundary at 1.0 bar; V_vacuum (extract), to boundary at 0.1 bar
    top    V_product (product), to boundary at 1.0 bar
  cycle of 4 stages, 100.0 s:
    pressurisation                        15.0 s   V_feed
    adsorption                            15.0 s   V_feed, V_product
    blowdown                              30.0 s   V_product
    evacuation                            40.0 s   V_vacuum

Example on GitHub ​

If you would like to run this example yourself, it can be downloaded from the Mocca.jl GitHub repository.


This page was generated using Literate.jl.