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

tutorial/sugarcane_light.png
tutorial/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 biosteam as bst
from warnings import filterwarnings
filterwarnings('ignore')
chemicals = bst.Chemicals(
    ['Water', # Define common chemicals by name
     'Ethanol',
     'Octane',
     'Propane',
     'Hexane',
     bst.Chemical('Glucose', phase='s'), # These will always remain as solids
     bst.Chemical('Sucrose', phase='s'), # Specify phase if chemicals not in vapor-liquid equilibrium
     bst.Chemical('H3PO4', phase='s'),
     bst.Chemical('P4O10',
                  rho=1540, # Density [kg/m3]
                  default=True,  # Default other chemicals properties like viscosity to that of water at 25 C
                  phase='s'),
     bst.Chemical('CO2', phase='g'), # Assume they will always remain a gas
     bst.Chemical('O2', phase='g'),
     bst.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]
     bst.Chemical('Hemicellulose',
                  Cp=1.364,
                  rho=1540,
                  default=True,
                  search_db=False,
                  phase='s',
                  formula="C5H8O5", # Xylose monomer minus water
                  Hf=-761906.4),
     bst.Chemical('Lignin',
                  Cp=1.364,
                  rho=1540,
                  default=True,
                  search_db=False,
                  phase='s',
                  formula='C8H8O3', # Vainillin formula
                  Hf=-452909.632),
     bst.Chemical('Flocculant',
                  Cp=4.184,
                  rho=1540,
                  default=True,
                  search_db=False,
                  phase='s',
                  MW=1.), # No formula, so molecular weight should be defined
     bst.Chemical('Solids',
                  Cp=1.100,
                  rho=1540,
                  default=True,
                  search_db=False,
                  phase='s',
                  MW=1.),
     bst.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
     bst.Chemical('CaO',
                  Cp=1.02388,
                  rho=1540,
                  default=True,
                  search_db=False,
                  phase='s',
                  formula='CaO'),
     bst.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()
chemicals.show()
CompiledChemicals([
    Water,         Ethanol, Octane,     Propane,
    Hexane,        Glucose, Sucrose,    H3PO4,
    P4O10,         CO2,     O2,         Cellulose,
    Hemicellulose, Lignin,  Flocculant, Solids,
    DryYeast,      CaO,     Ash,
])

17.2. Mixture objects#

Before creating a Thermo property package, we must define the mixing rules to calculate mixture properties through a Mixture object. BioSTEAM includes an ideal mixture model and several equations of state. Let’s define an ideal mixture model:

[2]:
# Note that the mixture defaults to ideal mixing rules (weighted by mol)
# and excess energies are ignored by default
ideal_mixture = bst.IdealMixture.from_chemicals(chemicals)
ideal_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]) # Material flow rate [kmol/hr]
H = ideal_mixture.H('l', mol, T=300, P=101325) # Ethalpy flow rate [kJ/hr]
round(H)
[3]:
695
[4]:
mol = array(['Water', 'Ethanol'], [2, 2]) # Material flow rate [kmol/hr]
z_mol = mol / mol.sum() # Normalize composition for result on a molar basis
round(ideal_mixture.Cn('l', z_mol, T=300)) # Heat capacity [J/kmol/K]
[4]:
94

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

[5]:
mol_liquid = array(['Water', 'Ethanol'], [2, 2])
mol_vapor = array(['Water', 'Ethanol'], [2, 2])
phase_data = [('l', mol_liquid), ('g', mol_vapor)] # Material flow rate by phase [kmol/hr]
H = ideal_mixture.xH(phase_data, T=300, P=101325) # Ethalpy flow rate [kJ/hr]
round(H)
[5]:
172627

Let’s say we have a hydrocarbon mixture at low temperatures. In such cases, we can use an equation of state such as Peng-Robinson to account for excess enthalpies due to chemical interactions. Let’s observe how the difference in enthalpies and heat capacities can be significant:

[6]:
PR_mixture = bst.mixture.PRMixture.from_chemicals(chemicals)
mol = array(['Propane', 'Hexane'], [2, 2])
T = 300
P = 101325 * 5
round(PR_mixture.H('g', mol, T, P) - ideal_mixture.H('g', mol, T, P))
[6]:
-4565
[7]:
round(PR_mixture.Cn('g', mol, T, P) - ideal_mixture.Cn('g', mol, T, P))
[7]:
35

Here is a list of all mixture classes implemented in BioSTEAM:

[8]:
bst.mixture.mixture_classes
[8]:
[thermosteam.mixture.IdealMixture,
 thermosteam.mixture.PRMixture,
 thermosteam.mixture.SRKMixture,
 thermosteam.mixture.PR78Mixture,
 thermosteam.mixture.VDWMixture,
 thermosteam.mixture.PRSVMixture,
 thermosteam.mixture.PRSV2Mixture,
 thermosteam.mixture.TWUPRMixture,
 thermosteam.mixture.TWUSRKMixture,
 thermosteam.mixture.APISRKMixture,
 thermosteam.mixture.IGMixture,
 thermosteam.mixture.RKMixture,
 thermosteam.mixture.PRTranslatedConsistentMixture,
 thermosteam.mixture.PRTranslatedPPJPMixture,
 thermosteam.mixture.PRTranslatedMixture,
 thermosteam.mixture.SRKTranslatedConsistentMixture,
 thermosteam.mixture.PSRKMixture,
 thermosteam.mixture.MSRKTranslatedMixture,
 thermosteam.mixture.SRKTranslatedMixture]

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:

[9]:
bst.settings.set_thermo(chemicals, mixture=ideal_mixture) # Set the default property package
bst.settings.thermo.show()
Thermo(
    chemicals=CompiledChemicals([
        Water,         Ethanol, Octane,     Propane,
        Hexane,        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):

[10]:
bst.settings.set_thermo(chemicals)
bst.settings.get_thermo()
Thermo(
    chemicals=CompiledChemicals([
        Water,         Ethanol, Octane,     Propane,
        Hexane,        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
)

Here is a list of all activity coefficients, fugacity coefficients, and poyinting correction factor classes implemented in BioSTEAM:

[11]:
bst.activity_coefficient_classes
[11]:
[thermosteam.equilibrium.activity_coefficients.UNIFACActivityCoefficients,
 thermosteam.equilibrium.activity_coefficients.DortmundActivityCoefficients,
 thermosteam.equilibrium.activity_coefficients.NISTActivityCoefficients,
 thermosteam.equilibrium.activity_coefficients.PRActivityCoefficients,
 thermosteam.equilibrium.activity_coefficients.SRKActivityCoefficients,
 thermosteam.equilibrium.activity_coefficients.PR78ActivityCoefficients,
 thermosteam.equilibrium.activity_coefficients.VDWActivityCoefficients,
 thermosteam.equilibrium.activity_coefficients.PRSVActivityCoefficients,
 thermosteam.equilibrium.activity_coefficients.PRSV2ActivityCoefficients,
 thermosteam.equilibrium.activity_coefficients.TWUPRActivityCoefficients,
 thermosteam.equilibrium.activity_coefficients.TWUSRKActivityCoefficients,
 thermosteam.equilibrium.activity_coefficients.APISRKActivityCoefficients,
 thermosteam.equilibrium.activity_coefficients.IGActivityCoefficients,
 thermosteam.equilibrium.activity_coefficients.RKActivityCoefficients,
 thermosteam.equilibrium.activity_coefficients.PRMIXTranslatedConsistActivityCoefficients,
 thermosteam.equilibrium.activity_coefficients.PRMIXTranslatedPActivityCoefficients,
 thermosteam.equilibrium.activity_coefficients.PRMIXTranslaActivityCoefficients,
 thermosteam.equilibrium.activity_coefficients.SRKMIXTranslatedConsistActivityCoefficients,
 thermosteam.equilibrium.activity_coefficients.PActivityCoefficients,
 thermosteam.equilibrium.activity_coefficients.MSRKMIXTranslaActivityCoefficients,
 thermosteam.equilibrium.activity_coefficients.SRKMIXTranslaActivityCoefficients]
[12]:
bst.fugacity_coefficient_classes
[12]:
[thermosteam.equilibrium.fugacity_coefficients.PRFugacityCoefficients,
 thermosteam.equilibrium.fugacity_coefficients.SRKFugacityCoefficients,
 thermosteam.equilibrium.fugacity_coefficients.PR78FugacityCoefficients,
 thermosteam.equilibrium.fugacity_coefficients.VDWFugacityCoefficients,
 thermosteam.equilibrium.fugacity_coefficients.PRSVFugacityCoefficients,
 thermosteam.equilibrium.fugacity_coefficients.PRSV2FugacityCoefficients,
 thermosteam.equilibrium.fugacity_coefficients.TWUPRFugacityCoefficients,
 thermosteam.equilibrium.fugacity_coefficients.TWUSRKFugacityCoefficients,
 thermosteam.equilibrium.fugacity_coefficients.APISRKFugacityCoefficients,
 thermosteam.equilibrium.fugacity_coefficients.IGFugacityCoefficients,
 thermosteam.equilibrium.fugacity_coefficients.RKFugacityCoefficients,
 thermosteam.equilibrium.fugacity_coefficients.PRTranslatedConsistentFugacityCoefficients,
 thermosteam.equilibrium.fugacity_coefficients.PRTranslatedPPJPFugacityCoefficients,
 thermosteam.equilibrium.fugacity_coefficients.PRTranslatedFugacityCoefficients,
 thermosteam.equilibrium.fugacity_coefficients.SRKTranslatedConsistentFugacityCoefficients,
 thermosteam.equilibrium.fugacity_coefficients.PSRKFugacityCoefficients,
 thermosteam.equilibrium.fugacity_coefficients.MSRKTranslatedFugacityCoefficients,
 thermosteam.equilibrium.fugacity_coefficients.SRKTranslatedFugacityCoefficients]
[13]:
bst.poyinting_correction_factor_classes
[13]:
[thermosteam.equilibrium.poyinting_correction_factors.MockPoyintingCorrectionFactors,
 thermosteam.equilibrium.poyinting_correction_factors.IdealGasPoyintingCorrectionFactors]

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:

[14]:
# Without Poyinting correction factors, the dew point is high
bst.settings.set_thermo(['Butanol', 'CO2'], cache=True)
eq = bst.equilibrium
s = bst.Stream(Butanol=1, CO2=10, P=1e7)
s.dew_point_at_P()
[14]:
DewPointValues(T=470.72, P=10000000, IDs=('Butanol', 'CO2'), z=[0.091 0.909], x=[0.997 0.003])
[15]:
# With Poyinting correction factors, the dew point is lower
bst.settings.set_thermo(['Butanol', 'CO2'], cache=True, PCF=eq.IdealGasPoyintingCorrectionFactors)
s = bst.Stream(Butanol=1, CO2=10, P=1e7)
s.dew_point_at_P()
[15]:
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:

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

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

H_ideal_gas: -3.07e+05
MultiStream: s_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: -3.18e+05
MultiStream: s_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.5e-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