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