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