18. Stoichiometric reactions#

Thermosteam provides array based objects that can model stoichiometric reactions given a conversion.

18.1. Single reaction#

Create a Reaction object based on the transesterification reaction:

Reaction

Reactant

% Converted

Lipid + 3 Methanol -> 3 Biodiesel + Glycerol

Lipid

90

[1]:
import thermosteam as tmo
from biorefineries import cane
from warnings import filterwarnings; filterwarnings('ignore')
chemicals = cane.create_oilcane_chemicals()
tmo.settings.set_thermo(chemicals)
transesterification = tmo.Reaction(
    'TAG + Methanol -> Biodiesel + Glycerol', # Reaction
    correct_atomic_balance=True, # Corrects stoichiometric coefficients by atomic balance
    reactant='TAG', # Limiting reactant
    X=0.9, # Conversion
)
transesterification.show()
Reaction (by mol):
stoichiometry                                    reactant    X[%]
3 Methanol + TriOlein -> 3 Biodiesel + Glycerol  TriOlein   90.00
[2]:
transesterification.chemicals
CompiledChemicals([Water, Ethanol, Glucose, Sucrose, H3PO4, P4O10, CO2, Octane, O2, N2, CH4, Ash, Cellulose, Hemicellulose, Flocculant, Lignin, Solids, Yeast, CaO, Biodiesel, Methanol, Glycerol, HCl, NaOH, NaOCH3, Phosphatidylinositol, OleicAcid, MonoOlein, DiOlein, TriOlein, Acetone])
[3]:
transesterification.stoichiometry
[3]:
sparse([ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
         0.,  0.,  0.,  0.,  0.,  0.,  3., -3.,  1.,  0.,  0.,  0.,  0.,
         0.,  0.,  0., -1.,  0.])
[4]:
transesterification.reactant
[4]:
'TriOlein'
[5]:
transesterification.X
[5]:
0.9
[6]:
# Heat of reaction J / mol-reactant (accounts for conversion)
# Latent heats are not included here because the reaction is agnostic to phases
transesterification.dH
[6]:
-324288.0

When a Reaction object is called with a stream, it updates the material data to reflect the reaction:

[7]:
feed = tmo.Stream(TAG=100, Methanol=600)
print('BEFORE REACTION')
feed.show(N=100)

# React feed molar flow rate
transesterification(feed)

print('AFTER REACTION')
feed.show(N=100)
BEFORE REACTION
Stream: s1
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kmol/hr): Methanol  600
                TriOlein  100
AFTER REACTION
Stream: s1
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kmol/hr): Biodiesel  270
                Methanol   330
                Glycerol   90
                TriOlein   10

Let’s change the basis of the reaction:

[8]:
transesterification.basis = 'wt'
transesterification.show()
Reaction (by wt):
stoichiometry                                            reactant    X[%]
0.109 Methanol + TriOlein -> Biodiesel + 0.104 Glycerol  TriOlein   90.00

Notice that the stoichiometry also adjusted. If we react a stream, we should see the same result, regardless of basis:

[9]:
feed = tmo.Stream(TAG=100, Methanol=600)
print('BEFORE REACTION')
feed.show(N=100)

# React feed molar flow rate
transesterification(feed)

print('AFTER REACTION')
feed.show(N=100)
BEFORE REACTION
Stream: s2
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kmol/hr): Methanol  600
                TriOlein  100
AFTER REACTION
Stream: s2
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kmol/hr): Biodiesel  270
                Methanol   330
                Glycerol   90
                TriOlein   10

The net product yield accounting for both conversion and stoichiometry can be computed:

[10]:
round(transesterification.product_yield('Biodiesel', basis='mol'), 3)
[10]:
2.7
[11]:
round(transesterification.product_yield('Biodiesel', basis='wt'), 3)
[11]:
0.904

The heat of reaction is now in units of J/kg-reactant:

[12]:
transesterification.dH # Accounts for conversion too
[12]:
-366.2483149751772

Note that these reactions are carried out isothermally, but it is also possible to do so adiabatically:

[13]:
feed = tmo.Stream(Glucose=10, H2O=100, units='kg/hr')
print('BEFORE REACTION')
feed.show(N=100)

# React feed adiabatically (temperature should increase)
fermentation = tmo.Reaction('Glucose + O2 -> Ethanol + CO2',
                            reactant='Glucose',  X=0.9,
                            correct_atomic_balance=True)
fermentation.adiabatic_reaction(feed)

print('AFTER REACTION')
feed.show(N=100)
BEFORE REACTION
Stream: s3
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kmol/hr): Water    5.55
                Glucose  0.0555
AFTER REACTION
Stream: s3
phase: 'l', T: 308.47 K, P: 101325 Pa
flow (kmol/hr): Water    5.55
                Ethanol  0.0999
                Glucose  0.00555
                CO2      0.0999

Phases changes can be included in the reaction:

[14]:
fermentation = tmo.Reaction('Glucose,l -> Ethanol,l + CO2,g',
                            reactant='Glucose',  X=0.9,
                            correct_atomic_balance=True)
fermentation.show()
Reaction (by mol):
stoichiometry                       reactant     X[%]
Glucose,l -> 2 CO2,g + 2 Ethanol,l  Glucose,l   90.00

Note that the heat of reaction accounts for latent heats, the extent of reaction, and the basis:

[15]:
fermentation.X = 0.80
print(f"Heat of reaction at 80% conversion: {fermentation.dH:.3g} J/mol-reactant, {fermentation.copy(basis='wt').dH:.3g} J/g-reactant")
fermentation.X = 0.90
print(f"Heat of reaction at 90% conversion: {fermentation.dH:.3g} J/mol-reactant, {fermentation.copy(basis='wt').dH:.3g} J/g-reactant")
Heat of reaction at 80% conversion: -7.19e+04 J/mol-reactant, -399 J/g-reactant
Heat of reaction at 90% conversion: -8.09e+04 J/mol-reactant, -449 J/g-reactant
[16]:
feed = tmo.Stream(Glucose=10, H2O=100, units='kg/hr')
feed.phases = 'gl'
print('BEFORE ADIABATIC REACTION')
feed.show(N=100)

# React feed adiabatically (temperature should increase)
fermentation.adiabatic_reaction(feed)

print('AFTER ADIABATIC REACTION')
feed.show(N=100)
BEFORE ADIABATIC REACTION
MultiStream: s4
phases: ('g', 'l'), T: 298.15 K, P: 101325 Pa
flow (kmol/hr): (l) Water    5.55
                    Glucose  0.0555
AFTER ADIABATIC REACTION
MultiStream: s4
phases: ('g', 'l'), T: 308.47 K, P: 101325 Pa
flow (kmol/hr): (g) CO2      0.0999
                (l) Water    5.55
                    Ethanol  0.0999
                    Glucose  0.00555

Lastly, when working with positive ions, simply pass a dictionary of stoichiometric coefficients instead of the equation:

[17]:
# First let's define a new set of chemicals
chemicals = NaCl, SodiumIon, ChlorideIon = tmo.Chemicals(['NaCl', 'Na+', 'Cl-'])

# We set the state to completely ignore other possible phases
NaCl.at_state('s')
SodiumIon.at_state('l')
ChlorideIon.at_state('l')

# Molar volume doesn't matter in this scenario, but its
# required to compile the chemicals. We can assume
# a very low volume since its in solution.
NaCl.V.add_model(1e-6)
SodiumIon.V.add_model(1e-6)
ChlorideIon.V.add_model(1e-6)

# We can pass a Chemicals object to not have to override
# the lipidcane chemicals we set earlier.
dissociation = tmo.Reaction({'NaCl':-1, 'Na+':1, 'Cl-': 1},
                            reactant='NaCl',  X=1.,
                            chemicals=chemicals)
dissociation.show()
Reaction (by mol):
stoichiometry      reactant    X[%]
NaCl -> Na+ + Cl-  NaCl      100.00

18.2. Parallel reactions#

Model the pretreatment hydrolysis reactions and assumed conversions from Humbird et. al. as shown in the follwing table [1]:

Reaction

Reactant

% Converted

(Glucan)n + n H2O→ n Glucose

Glucan

9.9

(Glucan)n + n H2O → n Glucose Oligomer

Glucan

0.3

(Glucan)n → n HMF + 2n H2O

Glucan

0.3

Sucrose → HMF + Glucose + 2 H2O

Sucrose

100.0

(Xylan)n + n H2O→ n Xylose

Xylan

90.0

(Xylan)n + m H2O → m Xylose Oligomer

Xylan

2.4

(Xylan)n → n Furfural + 2n H2O

Xylan

5.0

Acetate → Acetic Acid

Acetate

100.0

(Lignin)n → n Soluble Lignin

Lignin

5.0

Create a ParallelReaction from Reaction objects:

[18]:
from biorefineries import cellulosic

# Set chemicals as defined in [1-4]
chemicals = cellulosic.create_cellulosic_ethanol_chemicals()
tmo.settings.set_thermo(chemicals)

# Create reactions
pretreatment_parallel_rxn = tmo.ParallelReaction([
    #            Reaction definition                 Reactant    Conversion
    tmo.Reaction('Glucan + H2O -> Glucose',          'Glucan',   0.0990),
    tmo.Reaction('Glucan + H2O -> GlucoseOligomer',  'Glucan',   0.0030),
    tmo.Reaction('Glucan -> HMF + 2 H2O',            'Glucan',   0.0030),
    tmo.Reaction('Sucrose -> HMF + Glucose + 2H2O',  'Sucrose',  0.0030),
    tmo.Reaction('Xylan + H2O -> Xylose',            'Xylan',    0.9000),
    tmo.Reaction('Xylan + H2O -> XyloseOligomer',    'Xylan',    0.0024),
    tmo.Reaction('Xylan -> Furfural + 2 H2O',        'Xylan',    0.0050),
    tmo.Reaction('Acetate -> AceticAcid',            'Acetate',  1.0000),
    tmo.Reaction('Lignin -> SolubleLignin',          'Lignin',   0.0050)])

pretreatment_parallel_rxn.show()
ParallelReaction (by mol):
index  stoichiometry                       reactant    X[%]
[0]    Water + Glucan -> Glucose           Glucan      9.90
[1]    Water + Glucan -> GlucoseOligomer   Glucan      0.30
[2]    Glucan -> 2 Water + HMF             Glucan      0.30
[3]    Sucrose -> 2 Water + HMF + Glucose  Sucrose     0.30
[4]    Water + Xylan -> Xylose             Xylan      90.00
[5]    Water + Xylan -> XyloseOligomer     Xylan       0.24
[6]    Xylan -> 2 Water + Furfural         Xylan       0.50
[7]    Acetate -> AceticAcid               Acetate   100.00
[8]    Lignin -> SolubleLignin             Lignin      0.50

Model the reaction:

[19]:
feed = tmo.Stream(H2O=2.07e+05,
                  Ethanol=18,
                  H2SO4=1.84e+03,
                  Sucrose=1.87,
                  Extract=67.8,
                  Acetate=25.1,
                  Ash=4.11e+03,
                  Lignin=1.31e+04,
                  Protein=108,
                  Glucan=180,
                  Xylan=123,
                  Arabinan=9.02,
                  Mannan=3.08,
                  Furfural=172)
print('BEFORE REACTION')
feed.show(N=100)

# React feed molar flow rate
pretreatment_parallel_rxn(feed)

print('AFTER REACTION')
feed.show(N=100)
BEFORE REACTION
Stream: s5
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kmol/hr): Water     2.07e+05
                Ethanol   18
                Furfural  172
                H2SO4     1.84e+03
                Sucrose   1.87
                Extract   67.8
                Acetate   25.1
                Ash       4.11e+03
                Lignin    1.31e+04
                Protein   108
                Glucan    180
                Xylan     123
                Arabinan  9.02
                Mannan    3.08
AFTER REACTION
Stream: s5
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kmol/hr): Water            2.07e+05
                Ethanol          18
                AceticAcid       25.1
                Furfural         173
                H2SO4            1.84e+03
                HMF              0.546
                Glucose          17.8
                Xylose           111
                Sucrose          1.86
                Extract          67.8
                Ash              4.11e+03
                Lignin           1.3e+04
                SolubleLignin    65.5
                GlucoseOligomer  0.54
                XyloseOligomer   0.295
                Protein          108
                Glucan           161
                Xylan            11.4
                Arabinan         9.02
                Mannan           3.08

18.3. Reactions in series#

SeriesReaction objects work the same way, but in series:

[20]:
pretreatment_series_rxn = tmo.SeriesReaction(pretreatment_parallel_rxn)
pretreatment_series_rxn.show()
SeriesReaction (by mol):
index  stoichiometry                       reactant    X[%]
[0]    Water + Glucan -> Glucose           Glucan      9.90
[1]    Water + Glucan -> GlucoseOligomer   Glucan      0.30
[2]    Glucan -> 2 Water + HMF             Glucan      0.30
[3]    Sucrose -> 2 Water + HMF + Glucose  Sucrose     0.30
[4]    Water + Xylan -> Xylose             Xylan      90.00
[5]    Water + Xylan -> XyloseOligomer     Xylan       0.24
[6]    Xylan -> 2 Water + Furfural         Xylan       0.50
[7]    Acetate -> AceticAcid               Acetate   100.00
[8]    Lignin -> SolubleLignin             Lignin      0.50

Net conversion in parallel:

[21]:
pretreatment_parallel_rxn.X_net()
[21]:
{'Glucan': 0.10500000000000001,
 'Sucrose': 0.003,
 'Xylan': 0.9074,
 'Acetate': 1.0,
 'Lignin': 0.005}

Net conversion in series:

[22]:
# Notice how the conversion is
# slightly lower for some reactants
pretreatment_series_rxn.X_net()
[22]:
{'Glucan': 0.104397891,
 'Sucrose': 0.003,
 'Xylan': 0.9007388000000001,
 'Acetate': 1.0,
 'Lignin': 0.005}
[23]:
feed = tmo.Stream(H2O=2.07e+05,
                  Ethanol=18,
                  H2SO4=1.84e+03,
                  Sucrose=1.87,
                  Extract=67.8,
                  Acetate=25.1,
                  Ash=4.11e+03,
                  Lignin=1.31e+04,
                  Protein=108,
                  Glucan=180,
                  Xylan=123,
                  Arabinan=9.02,
                  Mannan=3.08,
                  Furfural=172)
print('BEFORE REACTION')
feed.show(N=100)

# React feed molar flow rate
pretreatment_series_rxn(feed)

print('AFTER REACTION')
feed.show(N=100)
BEFORE REACTION
Stream: s6
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kmol/hr): Water     2.07e+05
                Ethanol   18
                Furfural  172
                H2SO4     1.84e+03
                Sucrose   1.87
                Extract   67.8
                Acetate   25.1
                Ash       4.11e+03
                Lignin    1.31e+04
                Protein   108
                Glucan    180
                Xylan     123
                Arabinan  9.02
                Mannan    3.08
AFTER REACTION
Stream: s6
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kmol/hr): Water            2.07e+05
                Ethanol          18
                AceticAcid       25.1
                Furfural         172
                H2SO4            1.84e+03
                HMF              0.491
                Glucose          17.8
                Xylose           111
                Sucrose          1.86
                Extract          67.8
                Ash              4.11e+03
                Lignin           1.3e+04
                SolubleLignin    65.5
                GlucoseOligomer  0.487
                XyloseOligomer   0.0295
                Protein          108
                Glucan           161
                Xylan            12.2
                Arabinan         9.02
                Mannan           3.08

18.4. Indexing reactions#

Both SeriesReaction, and ParallelReaction objects are indexable:

[24]:
# Index a slice
pretreatment_parallel_rxn[0:2].show()
ParallelReaction (by mol):
index  stoichiometry                      reactant    X[%]
[0]    Water + Glucan -> Glucose          Glucan      9.90
[1]    Water + Glucan -> GlucoseOligomer  Glucan      0.30
[25]:
# Index an item
pretreatment_parallel_rxn[0].show()
ReactionItem (by mol):
stoichiometry              reactant    X[%]
Water + Glucan -> Glucose  Glucan      9.90
[26]:
# Change conversion through the item
pretreatment_parallel_rxn[0].X = 0.10
[27]:
pretreatment_parallel_rxn.show()
ParallelReaction (by mol):
index  stoichiometry                       reactant    X[%]
[0]    Water + Glucan -> Glucose           Glucan     10.00
[1]    Water + Glucan -> GlucoseOligomer   Glucan      0.30
[2]    Glucan -> 2 Water + HMF             Glucan      0.30
[3]    Sucrose -> 2 Water + HMF + Glucose  Sucrose     0.30
[4]    Water + Xylan -> Xylose             Xylan      90.00
[5]    Water + Xylan -> XyloseOligomer     Xylan       0.24
[6]    Xylan -> 2 Water + Furfural         Xylan       0.50
[7]    Acetate -> AceticAcid               Acetate   100.00
[8]    Lignin -> SolubleLignin             Lignin      0.50

Notice how changing conversion of a ReactionItem object changes the conversion in the ParallelReaction object.

18.5. References#

  1. Humbird, D., Davis, R., Tao, L., Kinchin, C., Hsu, D., Aden, A., Dudgeon, D. (2011). Process Design and Economics for Biochemical Conversion of Lignocellulosic Biomass to Ethanol: Dilute-Acid Pretreatment and Enzymatic Hydrolysis of Corn Stover (No. NREL/TP-5100-47764, 1013269). https://doi.org/10.2172/1013269

  2. Hatakeyama, T., Nakamura, K., & Hatakeyama, H. (1982). Studies on heat capacity of cellulose and lignin by differential scanning calorimetry. Polymer, 23(12), 1801–1804. https://doi.org/10.1016/0032-3861(82)90125-2

  3. Thybring, E. E. (2014). Explaining the heat capacity of wood constituents by molecular vibrations. Journal of Materials Science, 49(3), 1317–1327. https://doi.org/10.1007/s10853-013-7815-6

  4. Murphy W. K., and K. R. Masters. (1978). Gross heat of combustion of northern red oak (Quercus rubra) chemical components. Wood Sci. 10:139-141.