17. Property packages#

A Thermo object defines a thermodynamic property package. To build a Thermo object, we must first define all the chemicals involed. In the following example, we create a property package that BioSTEAM can use to model a sugarcane biorefinery producing ethanol from fermenting the juice and electricity from the burning the bagasse [1-2].

../_images/sugarcane_light.png
../_images/sugarcane_dark.png

17.1. Creating chemicals#

Here is a demonstrative example of how to create chemicals from the databank and define new chemicals. The assumptions used here are reasonable but may not be accurate depending on the lignocellulosic feedstock:

[1]:
import thermosteam as tmo
from warnings import filterwarnings
filterwarnings('ignore')
chemicals = tmo.Chemicals(
    ['Water', # Define common chemicals by name
     'Ethanol',
     'Octane',
     tmo.Chemical('Glucose', phase='s'), # These will always remain as solids
     tmo.Chemical('Sucrose', phase='s'), # Specify phase if chemicals not in vapor-liquid equilibrium
     tmo.Chemical('H3PO4', phase='s'),
     tmo.Chemical('P4O10',
                  rho=1540, # Density [kg/m3]
                  default=True,  # Default other chemicals properties like viscosity to that of water at 25 C
                  phase='s'),
     tmo.Chemical('CO2', phase='g'), # Assume they will always remain a gas
     tmo.Chemical('O2', phase='g'),
     tmo.Chemical('Cellulose',
                  Cp=1.364, # Heat capacity [kJ/kg]
                  rho=1540, # Density [kg/m3]
                  default=True, # Default other chemicals properties like viscosity to that of water at 25 C
                  search_db=False, # Not in database, so do not search the database
                  phase='s',
                  formula="C6H10O5", # Glucose monomer minus water, molecular weight is computed based on formula
                  Hf=-975708.8), # Heat of formation [J/mol]
     tmo.Chemical('Hemicellulose',
                  Cp=1.364,
                  rho=1540,
                  default=True,
                  search_db=False,
                  phase='s',
                  formula="C5H8O5", # Xylose monomer minus water
                  Hf=-761906.4),
     tmo.Chemical('Lignin',
                  Cp=1.364,
                  rho=1540,
                  default=True,
                  search_db=False,
                  phase='s',
                  formula='C8H8O3', # Vainillin formula
                  Hf=-452909.632),
     tmo.Chemical('Flocculant',
                  Cp=4.184,
                  rho=1540,
                  default=True,
                  search_db=False,
                  phase='s',
                  MW=1.), # No formula, so molecular weight should be defined
     tmo.Chemical('Solids',
                  Cp=1.100,
                  rho=1540,
                  default=True,
                  search_db=False,
                  phase='s',
                  MW=1.),
     tmo.Chemical('DryYeast',
                  rho=1540,
                  default=True,
                  search_db=False,
                  phase='s',
                  MW=1.,
                  aliases={'Yeast'}), # We can also give aliases to refer to them later
     tmo.Chemical('CaO',
                  Cp=1.02388,
                  rho=1540,
                  default=True,
                  search_db=False,
                  phase='s',
                  formula='CaO'),
     tmo.Chemical('Ash',
                  rho=1540,
                  Cp=0.37656,
                  default=True,
                  search_db=False,
                  phase='s',
                  MW=1.)]
)

# Compile once you are done adding chemicals so that the
# order of chemicals becomes immutable
chemicals.compile()

17.2. Mixture objects#

Before creating a Thermo property package, we must define the mixing rules to calculate mixture properties through a Mixture object:

[2]:
# Note that the mixture defaults to ideal mixing rules (weighted by mol)
# and excess energies are ignored by default
mixture = tmo.IdealMixture.from_chemicals(chemicals)
mixture.show()
IdealMixture(...
    include_excess_energies=False
)

You can use the mixture for estimating mixture properties:

[3]:
array = chemicals.array
mol = array(['Water', 'Ethanol'], [2, 2])
H = mixture.H('l', mol, 300, 101325)
round(H)
[3]:
695
[4]:
mol = array(['Water', 'Ethanol'], [2, 2])
mixture.Cn('l', mol / mol.sum(), 300) # Normalize composition for result on a molar basis
[4]:
94.07098289709057

You can also estimate multi-phase mixture properties through methods that start with “x” (e.g. xCn):

[5]:
mol_liquid = array(['Water', 'Ethanol'], [2, 2])
mol_vapor = array(['Water', 'Ethanol'], [2, 2])
phase_data = [('l', mol_liquid), ('g', mol_vapor)]
Cn = mixture.xCn(phase_data, T=300) # Returns total capacity [J/K] because composition was not normalized
round(Cn)
[5]:
574

Note: To implement a your own Mixture object, you can request help through BioSTEAMDevelopmentGroup/thermosteam.

17.3. Thermo objects#

Once the chemicals and mixture objects are finalized, we can compile them into a Thermo object and set the default property package:

[6]:
tmo.settings.set_thermo(chemicals, mixture=mixture) # Set the default property package
tmo.settings.thermo.show()
Thermo(
    chemicals=CompiledChemicals([Water, Ethanol, Octane, Glucose, Sucrose, H3PO4, P4O10, CO2, O2, Cellulose, Hemicellulose, Lignin, Flocculant, Solids, DryYeast, CaO, Ash]),
    mixture=IdealMixture(...
        include_excess_energies=False
    ),
    Gamma=DortmundActivityCoefficients,
    Phi=IdealFugacityCoefficients,
    PCF=MockPoyintingCorrectionFactors
)

Note that a Thermo object contains ActivityCoefficients, FugacityCoefficients, and PoyintingCorrectionFactors subclasses to define fugacity estimation methods. By default, activities are estimated by Dortmund modified UNIFAC method, while vapor phase fugacity coefficients and Poyinting correction factors are assumed to be 1. If you plan on using all defaults, you can just use the chemicals to set the property package (and skip the creation of Thermo and Mixture objects):

[7]:
tmo.settings.set_thermo(chemicals)
tmo.settings.get_thermo()
Thermo(
    chemicals=CompiledChemicals([Water, Ethanol, Octane, Glucose, Sucrose, H3PO4, P4O10, CO2, O2, Cellulose, Hemicellulose, Lignin, Flocculant, Solids, DryYeast, CaO, Ash]),
    mixture=IdealMixture(...
        include_excess_energies=False
    ),
    Gamma=DortmundActivityCoefficients,
    Phi=IdealFugacityCoefficients,
    PCF=MockPoyintingCorrectionFactors
)

17.3.1. More rigorous thermodynamic calculations#

If you have mixtures with non-condensible gases, you may want to use IdealGasPoyintingCorrectionFactors to account for Poyinting effects:

[8]:
# Without Poyinting correction factors, the dew point is high
tmo.settings.set_thermo(['Butanol', 'CO2'], cache=True)
eq = tmo.equilibrium
s = tmo.Stream(None, Butanol=1, CO2=10, P=1e7)
s.dew_point_at_P()
[8]:
DewPointValues(T=470.72, P=10000000, IDs=('Butanol', 'CO2'), z=[0.091 0.909], x=[0.997 0.003])
[9]:
# With Poyinting correction factors, the dew point is lower
tmo.settings.set_thermo(['Butanol', 'CO2'], cache=True, PCF=eq.IdealGasPoyintingCorrectionFactors)
s = tmo.Stream(None, Butanol=1, CO2=10, P=1e7)
s.dew_point_at_P()
[9]:
DewPointValues(T=458.21, P=10000000, IDs=('Butanol', 'CO2'), z=[0.091 0.909], x=[0.995 0.005])

You may need more rigorous mixing rules and phase equilibrium for high-pressure processes. BioSTEAM features a wide selection of equations of state for estimating excess free energies and fugacity coefficients:

[10]:
chemicals = tmo.Chemicals(['H2', 'N2', 'CO2', 'H2O'])
tmo.settings.set_thermo(chemicals)
s_ideal_gas = tmo.Stream('ideal_gas', H2=10, N2=10, CO2=10, phase='g')
s_ideal_gas.vle(T=160, P=1e7)
print('H_ideal_gas:', round(s_ideal_gas.H))
s_ideal_gas.show('cwt')

mixture = tmo.SRKMixture.from_chemicals(chemicals) # Soave-Redlich-Kuang EOS
tmo.settings.set_thermo(chemicals, mixture=mixture, Phi=tmo.SRKFugacityCoefficients)
s_eos = tmo.Stream('eos', H2=10, N2=10, CO2=10, phase='g')
s_eos.vle(T=160, P=1e7)
print('H_eos:', round(s_eos.H))
s_eos.show('cwt')

H_ideal_gas: -306734
MultiStream: ideal_gas
phases: ('g', 'l'), T: 160 K, P: 1e+07 Pa
composition (%): (g) H2   8.55
                     N2   91
                     CO2  0.495
                     ---  236 kg/hr
                 (l) H2   2.39e-06
                     N2   13
                     CO2  87
                     ---  505 kg/hr
H_eos: -499936
MultiStream: eos
phases: ('g', 'l'), T: 160 K, P: 1e+07 Pa
composition (%): (g) H2   7.75
                     N2   90.3
                     CO2  1.94
                     ---  260 kg/hr
                 (l) H2   2.51e-06
                     N2   9.43
                     CO2  90.6
                     ---  480 kg/hr

17.3.2. References#

  1. Huang, H., Long, S., & Singh, V. (2016) “Techno-economic analysis of biodiesel and ethanol co-production from lipid-producing sugarcane” Biofuels, Bioproducts and Biorefining, 10(3), 299–315. https://doi.org/10.1002/bbb.1640

  2. Cortes-Peña, Y.; Kumar, D.; Singh, V.; Guest, J. S. BioSTEAM: A Fast and Flexible Platform for the Design, Simulation, and Techno-Economic Analysis of Biorefineries under Uncertainty. ACS Sustainable Chem. Eng. 2020. https://doi.org/10.1021/acssuschemeng.9b07040.

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

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