6. Stoichiometric reactions

The thermosteam.reaction package contains array based objects that can model stoichiometric reactions given a conversion.

6.1. Single reaction

Create a Reaction object based on the transesterification reaction:

Reaction

Reactant

% Converted

Lipid + 3Methanol -> 3Biodiesel + Glycerol

Lipid

90

[1]:
import thermosteam as tmo
from thermosteam import reaction as rxn
from biorefineries.lipidcane.chemicals import lipidcane_chemicals

tmo.settings.set_thermo(lipidcane_chemicals)
transesterification = rxn.Reaction('Lipid + 3Methanol -> 3Biodiesel + Glycerol',
                                   reactant='Lipid',  X=0.9)
transesterification
Reaction (by mol):
 stoichiometry                                 reactant    X[%]
 3 Methanol + Lipid -> Glycerol + 3 Biodiesel  Lipid      90.00
[2]:
transesterification.chemicals
[2]:
CompiledChemicals([Water, Methanol, Ethanol, Glycerol, Glucose, Sucrose, H3PO4, P4O10, CO2, Octane, O2, Biodiesel, Ash, Cellulose, Hemicellulose, Flocculant, Lignin, Solids, DryYeast, CaO, HCl, NaOH, NaOCH3, Lipid])
[3]:
transesterification.stoichiometry
[3]:
array([ 0., -3.,  0.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  3.,  0.,
        0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0., -1.])
[4]:
transesterification.reactant
[4]:
'Lipid'
[5]:
transesterification.X
[5]:
0.9
[6]:
transesterification.dH # Heat of reaction J / mol-reacted
[6]:
51642.0

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

[7]:
feed = tmo.Stream(Lipid=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
                 Lipid     100
AFTER REACTION
Stream: s1
 phase: 'l', T: 298.15 K, P: 101325 Pa
 flow (kmol/hr): Methanol   330
                 Glycerol   90
                 Biodiesel  270
                 Lipid      10

Let’s change the basis of the reaction:

[8]:
transesterification.basis = 'wt'
transesterification
Reaction (by wt):
 stoichiometry                                           reactant    X[%]
 0.109 Methanol + Lipid -> 0.104 Glycerol + 1 Biodiesel  Lipid      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(Lipid=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
                 Lipid     100
AFTER REACTION
Stream: s2
 phase: 'l', T: 298.15 K, P: 101325 Pa
 flow (kmol/hr): Methanol   330
                 Glycerol   90
                 Biodiesel  270
                 Lipid      10

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

[10]:
transesterification.dH # Accounts for conversion too
[10]:
58.32406836499681

The only situation which you need to watch out for is when you pass an array (where each entry corresponds to a chemical), in which case the reaction object will assume the array is in the same basis. For example:

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

# React molar flow rate array on a mass basis.
# If this doesn't sound right, its because its not;
# as you can see in the result.
mol_array = feed.mol
transesterification(mol_array)

print('AFTER BAD REACTION')
feed.show(N=100)
BEFORE REACTION
Stream: s3
 phase: 'l', T: 298.15 K, P: 101325 Pa
 flow (kmol/hr): Methanol  600
                 Lipid     100
AFTER BAD REACTION
Stream: s3
 phase: 'l', T: 298.15 K, P: 101325 Pa
 flow (kmol/hr): Methanol   590
                 Glycerol   9.36
                 Biodiesel  90.4
                 Lipid      10

If you’re not sure your equation is correct, you could also correct the stoichiometry through an atomic balance:

[12]:
fermentation = rxn.Reaction('Glucose + O2 -> Ethanol + CO2',
                            reactant='Glucose',  X=0.9,
                            correct_atomic_balance=True)
fermentation
Reaction (by mol):
 stoichiometry                 reactant    X[%]
 Glucose -> 2 Ethanol + 2 CO2  Glucose    90.00

But you cannot solve the atomic balance if your equation is underdetermined:

[13]:
fermentation = rxn.Reaction('Glucose -> Ethanol + CO2',
                            reactant='Glucose',  X=0.9,
                            correct_atomic_balance=True)
---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
<ipython-input-13-19b51ae89dd7> in <module>
      1 fermentation = rxn.Reaction('Glucose -> Ethanol + CO2',
      2                             reactant='Glucose',  X=0.9,
----> 3                             correct_atomic_balance=True)

~\OneDrive\Code\thermosteam\thermosteam\reaction\_reaction.py in __init__(self, reaction, reactant, X, chemicals, basis, check_material_balance, check_atomic_balance, correct_atomic_balance)
    162             self._rescale()
    163             if correct_atomic_balance:
--> 164                 self.correct_atomic_balance()
    165             else:
    166                 if check_atomic_balance:

~\OneDrive\Code\thermosteam\thermosteam\reaction\_reaction.py in correct_atomic_balance(self, constants)
    371         if M_atoms != N_chemicals:
    372             raise RuntimeError(
--> 373                  'to solve atomic balance, number of atoms '
    374                 f'({M_atoms} available) must be equal to the number of '
    375                  'chemicals, not including the reactant '

RuntimeError: to solve atomic balance, number of atoms (3 available) must be equal to the number of chemicals, not including the reactant (2 available)

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

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

# We set the state to completely ignore other possible phases
NaOH.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.
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 = rxn.Reaction({'NaCl':-1, 'Na+':1, 'Cl-': 1},
                            reactant='NaCl',  X=1.,
                            chemicals=chemicals)
dissociation
Reaction (by mol):
 stoichiometry      reactant    X[%]
 NaCl -> Na+ + Cl-  NaCl      100.00

6.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:

[15]:
from biorefineries.cornstover.chemicals import cornstover_chemicals

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

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

pretreatment_parallel_rxn
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:

[16]:
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: s4
 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: s4
 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

6.3. Reactions in series

SeriesReaction objects work the same way, but in series:

[17]:
pretreatment_series_rxn = rxn.SeriesReaction(pretreatment_parallel_rxn)
pretreatment_series_rxn
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:

[18]:
pretreatment_parallel_rxn.X_net
ChemicalIndexer:
 Sucrose  0.003
 Acetate  1
 Lignin   0.005
 Glucan   0.105
 Xylan    0.9074

Net conversion in series:

[19]:
# Notice how the conversion is
# slightly lower for some reactants
pretreatment_series_rxn.X_net
ChemicalIndexer:
 Sucrose  0.003
 Acetate  1
 Lignin   0.005
 Glucan   0.1044
 Xylan    0.9007
[20]:
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: 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         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

6.4. Indexing reactions

Both SeriesReaction, and ParallelReaction objects are indexable:

[21]:
# Index a slice
pretreatment_parallel_rxn[0:2]
ParallelReaction (by mol):
index  stoichiometry                      reactant    X[%]
[0]    Water + Glucan -> Glucose          Glucan      9.90
[1]    Water + Glucan -> GlucoseOligomer  Glucan      0.30
[22]:
# Index an item
pretreatment_parallel_rxn[0]
ReactionItem (by mol):
 stoichiometry              reactant    X[%]
 Water + Glucan -> Glucose  Glucan      9.90
[23]:
# Change conversion through the item
pretreatment_parallel_rxn[0].X = 0.10
[24]:
pretreatment_parallel_rxn
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 ReationItem object changes the converion in the ParallelReaction object.

6.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.