Component¶
Click the badge below to try this tutorial interactively in your browser:
You can also run this tutorial inGoogle Colab. It takes a one-time setup per session: follow theColab instructions.
Prepared by:
Learning objectives. After this tutorial, you will be able to:
Define a
Componentand inspect its thermodynamic and biological propertiesBuild a
Componentregistry for a systemSwitch between mass, mole, and volumetric representations
Covered topics:
Component
Components
CompiledComponents
Companion video. A walkthrough of this tutorial is available on YouTube, presented by Tori Morgan. Recorded against
QSDsanv1.3.0. The concepts still apply, but if the code on screen differs from this notebook, follow the notebook.
Setup¶
Import QSDsan and confirm the installed version.
[1]:
import qsdsan as qs
print(f'This tutorial was made with qsdsan v{qs.__version__}.')
This tutorial was made with qsdsan v1.5.3.
1. Component¶
Component is the most basic class of qsdsan. It can represent a pure chemical (e.g., water) or a group of chemicals that share similar properties (e.g., X_PAO, phosphorus-accumulating organisms).
[2]:
# It's always good to read through the documentation
# commented out to avoid too much output
# help(qs.Component)
1.1. Component from scratch¶
You can make a Component object from scratch by providing relevant information upon initialization (i.e., creation) of the object.
Four attributes, ID, particle_size, degradability, and organic are required (i.e., must be provided). Specification of measured_as will affect the units and values of the i_ attributes.
[3]:
# We usually capitalize the ID of a Component
XPAO = qs.Component('XPAO', formula = 'C5H7O2N', measured_as = 'COD', phase='l',
particle_size = 'Particulate', degradability = 'Slowly',
organic = True)
[4]:
# The `show` method is very helpful in getting an overview of the component
# The `chemical_info` argument will show the chemical information of the component, such as formula and molecular weight
XPAO.show(chemical_info=True)
Component: XPAO (phase_ref='l') at phase='l'
[Names] CAS: XPAO
InChI: None
InChI_key: None
common_name: None
iupac_name: None
pubchemid: None
smiles: None
formula: C5H7O2N
[Groups] Dortmund: <Empty>
UNIFAC: <Empty>
PSRK: <Empty>
NIST: <Empty>
[Data] MW: 113.11 g/mol
Tm: None
Tb: None
Tt: None
Tc: None
Pt: None
Pc: None
Vc: None
Hf: None
S0: 0 J/K/mol
LHV: None
HHV: None
Hfus: None
Sfus: None
omega: None
dipole: None
similarity_variable: None
iscyclic_aliphatic: None
combustion: None
Component-specific properties:
[Others] measured_as: COD
description: None
particle_size: Particulate
degradability: Slowly
organic: True
i_C: 0.37535 g C/g COD
i_N: 0.087545 g N/g COD
i_P: 0 g P/g COD
i_K: 0 g K/g COD
i_Mg: 0 g Mg/g COD
i_Ca: 0 g Ca/g COD
i_mass: 0.70699 g mass/g COD
i_charge: 0 mol +/g COD
i_COD: 1 g COD/g COD
i_NOD: 0.4 g NOD/g COD
f_BOD5_COD: 0
f_uBOD_COD: 0
f_Vmass_Totmass: 0
chem_MW: 113.11
[5]:
# As a comparison, here is the same component without the chemical information
XPAO.show(False)
Component: XPAO (phase_ref='l') at phase='l'
Component-specific properties:
[Others] measured_as: COD
description: None
particle_size: Particulate
degradability: Slowly
organic: True
i_C: 0.37535 g C/g COD
i_N: 0.087545 g N/g COD
i_P: 0 g P/g COD
i_K: 0 g K/g COD
i_Mg: 0 g Mg/g COD
i_Ca: 0 g Ca/g COD
i_mass: 0.70699 g mass/g COD
i_charge: 0 mol +/g COD
i_COD: 1 g COD/g COD
i_NOD: 0.4 g NOD/g COD
f_BOD5_COD: 0
f_uBOD_COD: 0
f_Vmass_Totmass: 0
chem_MW: 113.11
[6]:
# If you provide some inputs that are not legit, you will likely receive an error
# For example, the particle size cannot be "Dissolved liquid" (should be "Dissolved gas")
bad_CH4 = qs.Component(ID='bad_CH4', search_ID='CH4', particle_size='Dissolved liquid',
degradability='Readily', organic=False)
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[6], line 3
1 # If you provide some inputs that are not legit, you will likely receive an error
2 # For example, the particle size cannot be "Dissolved liquid" (should be "Dissolved gas")
----> 3 bad_CH4 = qs.Component(ID='bad_CH4', search_ID='CH4', particle_size='Dissolved liquid',
4 degradability='Readily', organic=False)
File ~\Documents\Coding\QSDsan-platform\QSDsan\qsdsan\_component.py:261, in Component.__new__(cls, ID, cache, search_ID, chemical, formula, phase, measured_as, i_C, i_N, i_P, i_K, i_Mg, i_Ca, i_mass, i_charge, i_COD, i_NOD, f_BOD5_COD, f_uBOD_COD, f_Vmass_Totmass, description, particle_size, degradability, organic, **chemical_properties)
257 if phase: lock_phase(self, phase)
259 # Assign through the property setters so invalid values are caught at
260 # creation (setters validate via `check_return_property`)
--> 261 self.particle_size = particle_size
262 self.degradability = degradability
263 self.organic = organic
File ~\Documents\Coding\QSDsan-platform\QSDsan\qsdsan\_component.py:522, in Component.particle_size(self, particle_size)
520 @particle_size.setter
521 def particle_size(self, particle_size):
--> 522 self._particle_size = check_return_property('particle_size', particle_size)
File ~\Documents\Coding\QSDsan-platform\QSDsan\qsdsan\_component.py:110, in check_return_property(name, value)
108 return None
109 if value not in allowed_values[name]:
--> 110 raise ValueError(f'{name} must be in {allowed_values[name]}.')
111 return value
ValueError: particle_size must be in ('Dissolved gas', 'Soluble', 'Colloidal', 'Particulate').
1.2 Component from Chemical¶
You can convert a Chemical object (native to biosteam) to Component by providing the extra information needed.
[7]:
H2O_chem = qs.Chemical('H2O')
H2O_chem.show()
Chemical: H2O (phase_ref='l')
[Names] CAS: 7732-18-5
InChI: H2O/h1H2
InChI_key: XLYOFNOQVPJJNP-U...
common_name: water
iupac_name: ('oxidane',)
pubchemid: 962
smiles: O
formula: H2O
[Groups] Dortmund: <1H2O>
UNIFAC: <1H2O>
PSRK: <1H2O>
NIST: <Empty>
[Data] MW: 18.015 g/mol
Tm: 273.15 K
Tb: 373.12 K
Tt: 273.16 K
Tc: 647.1 K
Pt: 611.65 Pa
Pc: 2.2064e+07 Pa
Vc: 5.5948e-05 m^3/mol
Hf: -2.8582e+05 J/mol
S0: 70 J/K/mol
LHV: -44011 J/mol
HHV: -0 J/mol
Hfus: 6010 J/mol
Sfus: None
omega: 0.3443
dipole: 1.85 Debye
similarity_variable: 0.16653
iscyclic_aliphatic: 0
combustion: {'H2O': 1.0}
[8]:
type(H2O_chem)
[8]:
thermosteam._chemical.Chemical
[9]:
# You can do so by passing the `chemical` kwarg to the Component constructor, and then you can skip the `search_ID` since the chemical information is already provided
H2O_cmp1 = qs.Component('H2O_cmp1', chemical=H2O_chem, particle_size='Soluble', degradability='Undegradable', organic=False)
[10]:
# You can also use the `from_chemical` method
H2O_cmp2 = qs.Component.from_chemical('H2O_cmp2', chemical=H2O_chem, particle_size='Soluble', degradability='Undegradable', organic=False)
[11]:
# For components that are chemicals, you can pass a `search_ID` to look into the database
SNH4 = qs.Component('SNH4', search_ID='Ammonium', measured_as='N', phase='l',
particle_size='Soluble', degradability='Undegradable',
organic=False)
SNH4.show(True)
Component: SNH4 (phase_ref='l') at phase='l'
[Names] CAS: 14798-03-9
InChI: H3N/h1H3/p+1
InChI_key: QGZKDVFQNNGYKY-U...
common_name: ammonium
iupac_name: ('azanium',)
pubchemid: 223
smiles: [NH4+]
formula: H4N+
[Groups] Dortmund: <Empty>
UNIFAC: <Empty>
PSRK: <Empty>
NIST: <Empty>
[Data] MW: 18.038 g/mol
Tm: None
Tb: None
Tt: None
Tc: None
Pt: None
Pc: None
Vc: 0.00011214 m^3/mol
Hf: None
S0: 0 J/K/mol
LHV: None
HHV: None
Hfus: 0 J/mol
Sfus: None
omega: None
dipole: None
similarity_variable: 0.27719
iscyclic_aliphatic: 0
combustion: None
Component-specific properties:
[Others] measured_as: N
description: None
particle_size: Soluble
degradability: Undegradable
organic: False
i_C: 0 g C/g N
i_N: 1 g N/g N
i_P: 0 g P/g N
i_K: 0 g K/g N
i_Mg: 0 g Mg/g N
i_Ca: 0 g Ca/g N
i_mass: 1.2878 g mass/g N
i_charge: 0.071394 mol +/g N
i_COD: 0 g COD/g N
i_NOD: 4.5691 g NOD/g N
f_BOD5_COD: 0
f_uBOD_COD: 0
f_Vmass_Totmass: 0
chem_MW: 18.038
Python Aside: positional vs. keyword arguments (click to expand)
When calling a function you can pass values positionally (in definition order) or as keyword arguments (name=value). QSDsan objects often take many arguments with defaults, so we usually pass them by keyword to avoid ordering mistakes.
def foo(param0='Husky', param1='is', param2='a', param3='kind', param4='of', param5='dog'):
print(' '.join((param0, param1, param2, param3, param4, param5)))
foo() # Husky is a kind of dog
foo(param0='Fuji', param5='apple') # Fuji is a kind of apple
foo('Fuji', 'apple') # Fuji apple a kind of dog ('apple' lands in param1!)
foo('Fuji', param5='apple') # Fuji is a kind of apple (positional then keyword is OK)
# foo(param0='Fuji', 'apple') # SyntaxError: positional arg can't follow keyword arg
[12]:
# If you create a `Component` with molecular formula,
# attributes such as `i_C`, `i_N` will be automatically calcualted,
H2O = qs.Component('H2O', formula='H2O', particle_size='Soluble', degradability='Undegradable', organic=False)
H2O.i_C
[12]:
0.0
[13]:
# `qsdsan` will raise an error if you want to change the value
H2O.i_C = 1
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
Cell In[13], line 2
1 # `qsdsan` will raise an error if you want to change the value
----> 2 H2O.i_C = 1
File ~\Documents\Coding\QSDsan-platform\QSDsan\qsdsan\_component.py:309, in Component.i_C(self, i)
307 @i_C.setter
308 def i_C(self, i):
--> 309 self._i_C = self._atom_frac_setter('C', i)
File ~\Documents\Coding\QSDsan-platform\QSDsan\qsdsan\_component.py:291, in Component._atom_frac_setter(self, atom, frac)
289 if self.formula:
290 if frac:
--> 291 raise AttributeError('This component has formula, '
292 f'i_{atom} is calculated based on formula, '
293 'cannot be set.')
294 else:
295 if atom in self.atoms.keys():
AttributeError: This component has formula, i_C is calculated based on formula, cannot be set.
[14]:
# If you change the `measured_as` attribute of a `Component`,
# the units and values of the `i_` attributes, such as `i_N`,
# `i_COD` will also be updated automatically.
SNH4.measured_as = None
SNH4.show(False)
Component: SNH4 (phase_ref='l') at phase='l'
Component-specific properties:
[Others] measured_as: None
description: None
particle_size: Soluble
degradability: Undegradable
organic: False
i_C: 0 g C/g
i_N: 0.77649 g N/g
i_P: 0 g P/g
i_K: 0 g K/g
i_Mg: 0 g Mg/g
i_Ca: 0 g Ca/g
i_mass: 1 g mass/g
i_charge: 0.055437 mol +/g
i_COD: 0 g COD/g
i_NOD: 3.5478 g NOD/g
f_BOD5_COD: 0
f_uBOD_COD: 0
f_Vmass_Totmass: 0
chem_MW: 18.038
[15]:
# Note the difference in the `i_N` value and unit before and after changing the `measured_as` attribute
SNH4.measured_as = 'N'
SNH4.show()
Component: SNH4 (phase_ref='l') at phase='l'
Component-specific properties:
[Others] measured_as: N
description: None
particle_size: Soluble
degradability: Undegradable
organic: False
i_C: 0 g C/g N
i_N: 1 g N/g N
i_P: 0 g P/g N
i_K: 0 g K/g N
i_Mg: 0 g Mg/g N
i_Ca: 0 g Ca/g N
i_mass: 1.2878 g mass/g N
i_charge: 0.071394 mol +/g N
i_COD: 0 g COD/g N
i_NOD: 4.5691 g NOD/g N
f_BOD5_COD: 0
f_uBOD_COD: 0
f_Vmass_Totmass: 0
chem_MW: 18.038
[16]:
# Apparently you cannot have a component measured as some element that it does not have
SNH4.measured_as = 'C'
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[16], line 2
1 # Apparently you cannot have a component measured as some element that it does not have
----> 2 SNH4.measured_as = 'C'
File ~\Documents\Coding\QSDsan-platform\QSDsan\qsdsan\_component.py:463, in Component.measured_as(self, measured_as)
461 if hasattr(self, '_measured_as'):
462 if self._measured_as != measured_as:
--> 463 self._convert_i_attr(measured_as)
464 self._measured_as = measured_as
File ~\Documents\Coding\QSDsan-platform\QSDsan\qsdsan\_component.py:506, in Component._convert_i_attr(self, new)
501 raise AttributeError(f"Component {self.ID} must be measured as "
502 f"either COD or one of its constituent atoms, "
503 f"if not as itself.")
505 if denom == 0:
--> 506 raise ValueError(f'{self.ID} cannot be measured as {new}')
508 for field in _num_component_properties:
509 if field.startswith('i_'):
ValueError: SNH4 cannot be measured as C
Tip: Why do we want to set the phase (or “lock the state”) of a Component?
This is to reduce the computation burden in thermodynamic simulation. If we are sure that a particular Component will predominantly stay in a particular phase, then setting it to that phase will erase its properties related to other phases, reducing the needs for phase equilibrium.
1.3. measured_as, i_mass, and the i_ factors¶
A Component’s flow can be tracked on different bases: as its own mass (the default), as one of its constituent elements (e.g., nitrogen, 'N'), or as chemical oxygen demand ('COD'). The measured_as attribute records which currency you are using, and the i_ factors convert between that basis and mass / element / oxygen demand.
measured_as=None(default) — measured as itself, i.e., in grams of its own mass.measured_as='N'(a constituent element) — quantified by its nitrogen content: 1 g of the component means 1 g of N.measured_as='COD'— quantified by its oxygen demand: 1 g means 1 g COD.
Every i_ attribute reads as “g of X per g of the measure unit”:
Attribute |
Meaning |
|---|---|
|
g of actual component mass per g of the measure unit |
|
g of that element per g of the measure unit |
|
g of carbonaceous / nitrogenous oxygen demand per g of the measure unit |
|
mol of positive charge per g of the measure unit |
When the component has a chemical formula, qsdsan computes all of these from the formula and measured_as, so they cannot be set by hand.
[17]:
# Ammonium quantified two ways: as its own mass vs. as nitrogen
NH4_as_mass = qs.Component('NH4_as_mass', search_ID='Ammonium', measured_as=None,
particle_size='Soluble', degradability='Undegradable', organic=False)
NH4_as_N = qs.Component('NH4_as_N', search_ID='Ammonium', measured_as='N',
particle_size='Soluble', degradability='Undegradable', organic=False)
for cmp in (NH4_as_mass, NH4_as_N):
print(f'{cmp.ID:12} measured_as={str(cmp.measured_as):4} '
f'i_mass={cmp.i_mass:.4f} i_N={cmp.i_N:.4f} i_NOD={cmp.i_NOD:.4f}')
# as mass -> i_mass=1.0000, i_N=0.7765 (N is ~78% of NH4+ by mass)
# as N -> i_N=1.0000, i_mass=1.2878 (1 g N corresponds to ~1.29 g of ammonium)
NH4_as_mass measured_as=None i_mass=1.0000 i_N=0.7765 i_NOD=3.5478
NH4_as_N measured_as=N i_mass=1.2878 i_N=1.0000 i_NOD=4.5691
Switching measured_as rescales every i_ factor: measuring ammonium as mass gives i_mass=1 and i_N=0.78, while measuring it as N gives i_N=1 and i_mass=1.29.
How this connects to ``ignore_inaccurate_molar_weight``. When a component is measured as an element or COD, “1 unit” of it is 1 g of N (or COD) — not 1 gram of the molecule — so its molecular weight is no longer well defined on that basis. That is why compiling components that have a measured_as raises an error unless you pass ignore_inaccurate_molar_weight=True (keep mass-based results only) or adjust_MW_to_measured_as=True (rescale the MW for components that have a formula,
so molar and volumetric flows stay correct). The default of these two attributes are delibrately set to False, in order to alert users of this discrepancy.
2. Components¶
Components (note the s) objects are like a list (i.e., collection) of Component, we will want to use them to tell qsdsan what components we want to work with in the system.
2.1. Components from scratch¶
You can create a Components object from scratch by specifying all the Component objects.
[18]:
# Note that you need to provide all components as one iterable,
# hence the double parentheses
# This is euivalent to
# cmps1_list = (XPAO, SNH4)
# cmps1 = qs.Components(cmps1_list)
cmps1 = qs.Components((XPAO, SNH4))
cmps1
Components([XPAO, SNH4])
[19]:
# All the `Component` objects are stored as attributes of the `Components` object
cmps1.XPAO
Component: XPAO (phase_ref='l') at phase='l'
Component-specific properties:
[Others] measured_as: COD
description: None
particle_size: Particulate
degradability: Slowly
organic: True
i_C: 0.37535 g C/g COD
i_N: 0.087545 g N/g COD
i_P: 0 g P/g COD
i_K: 0 g K/g COD
i_Mg: 0 g Mg/g COD
i_Ca: 0 g Ca/g COD
i_mass: 0.70699 g mass/g COD
i_charge: 0 mol +/g COD
i_COD: 1 g COD/g COD
i_NOD: 0.4 g NOD/g COD
f_BOD5_COD: 0
f_uBOD_COD: 0
f_Vmass_Totmass: 0
chem_MW: 113.11
[20]:
# You can add more `Component` objects by appending (adding one per time)
cmps1.append(H2O)
cmps1.show()
Components([XPAO, SNH4, H2O])
[21]:
# or extending (adding several per time)
Methanol = qs.Component('Methanol', search_ID='methanol', particle_size='Soluble', degradability='Readily',
organic=True)
Ethanol = qs.Component('Ethanol', search_ID='ethanol', particle_size='Soluble', degradability='Readily',
organic=True)
cmps1.extend((Methanol, Ethanol))
cmps1.show()
Components([
XPAO, SNH4, H2O,
Methanol, Ethanol,
])
[22]:
# Of course, you can make a copy
cmps1_copy = cmps1.copy()
cmps1_copy.show()
Components([
XPAO, SNH4, H2O,
Methanol, Ethanol,
])
2.2. Components from Chemicals¶
Similar to how you make Component from Chemical, you can also make Components from Chemicals using the from_chemicals method of Components.
Python Aside: function vs. method (click to expand)
In Python a function becomes a method when it belongs to a class. from_chemicals is a method because it belongs to the Components class. Methods come in three flavors — instance methods, class methods, and static methods — which you can read about in the Python docs.
[23]:
import qsdsan as qs
chems = qs.Chemicals((qs.Chemical('Water'), qs.Chemical('Ethanol')))
data = {'Water': {'particle_size': 'Soluble',
'degradability': 'Undegradable',
'organic': False},
'Ethanol': {'particle_size': 'Soluble',
'degradability': 'Readily',
'organic': False}}
cmps2 = qs.Components.from_chemicals(chems, **data)
cmps2.show()
Components([Water, Ethanol])
2.3. Loading default Components¶
You can also load the default Components, which are the ones that commonly used in wastewater modeling.
[24]:
cmps3 = qs.Components.load_default(default_compile=False)
cmps3
Components([
S_H2, S_CH4, S_CH3OH, S_Ac,
S_Prop, S_F, S_U_Inf, S_U_E,
C_B_Subst, C_B_BAP, C_B_UAP, C_U_Inf,
X_B_Subst, X_OHO_PHA, X_GAO_PHA, X_PAO_PHA,
X_GAO_Gly, X_PAO_Gly, X_OHO, X_AOO,
X_NOO, X_AMO, X_PAO, X_MEOLO,
X_FO, X_ACO, X_HMO, X_PRO,
X_U_Inf, X_U_OHO_E, X_U_PAO_E, X_Ig_ISS,
X_MgCO3, X_CaCO3, X_MAP, X_HAP,
X_HDP, X_FePO4, X_AlPO4, X_AlOH,
X_FeOH, X_PAO_PP_Lo, X_PAO_PP_Hi, S_NH4,
S_NO2, S_NO3, S_PO4, S_K,
S_Ca, S_Mg, S_CO3, S_N2,
S_O2, S_CAT, S_AN, H2O,
])
2.4. Loading Components from file¶
You can also load a set of Components by providing a datasheet of component attributes. Here is an example datasheet containing information of 55 components.
[25]:
import pandas as pd
from qsdsan.utils import data_path
import os
file_path = os.path.join(data_path, '_components.tsv')
df = pd.read_csv(file_path, sep='\t', header=0)
df.head()
[25]:
| ID | description | formula | particle_size | degradability | ... | f_BOD5_COD | f_uBOD_COD | f_Vmass_Totmass | CAS | PubChem | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | S_H2 | Dissolved dihydrogen gas | H2 | Dissolved gas | Readily | ... | 0 | 0 | 1 | 1333-74-0 | 783 |
| 1 | S_CH4 | Dissolved Methane | CH4 | Dissolved gas | Readily | ... | 0 | 0 | 1 | 74-82-8 | 297 |
| 2 | S_CH3OH | Methanol | CH3OH | Soluble | Readily | ... | 0.717 | 0.863 | 1 | 67-56-1 | 887 |
| 3 | S_Ac | Acetate | CH3COO(-) | Soluble | Readily | ... | 0.717 | 0.863 | 1 | 71-50-1 | 175 |
| 4 | S_Prop | Propionate | C2H5COO- | Soluble | Readily | ... | 0.717 | 0.863 | 1 | 72-03-7 | 1.05e+05 |
5 rows × 22 columns
[26]:
cmps4 = qs.Components.load_from_file(df)
cmps4.show()
Components([
S_H2, S_CH4, S_CH3OH, S_Ac,
S_Prop, S_F, S_U_Inf, S_U_E,
C_B_Subst, C_B_BAP, C_B_UAP, C_U_Inf,
X_B_Subst, X_OHO_PHA, X_GAO_PHA, X_PAO_PHA,
X_GAO_Gly, X_PAO_Gly, X_OHO, X_AOO,
X_NOO, X_AMO, X_PAO, X_MEOLO,
X_FO, X_ACO, X_HMO, X_PRO,
X_U_Inf, X_U_OHO_E, X_U_PAO_E, X_Ig_ISS,
X_MgCO3, X_CaCO3, X_MAP, X_HAP,
X_HDP, X_FePO4, X_AlPO4, X_AlOH,
X_FeOH, X_PAO_PP_Lo, X_PAO_PP_Hi, S_NH4,
S_NO2, S_NO3, S_PO4, S_K,
S_Ca, S_Mg, S_CO3, S_N2,
S_O2, S_CAT, S_AN,
])
3. CompiledComponents¶
Finally, we need to “compile” Components into CompiledComponents, which can be easily done using the compile method.
When executing the compile method, the package will check if all Component objects have the properties needed for thermodynamic calculation.
[27]:
# For example, we will receive an error if we try to compile cmps1,
# because there is not enough data
cmps1.compile()
---------------------------------------------------------------------------
RuntimeError Traceback (most recent call last)
Cell In[27], line 3
1 # For example, we will receive an error if we try to compile cmps1,
2 # because there is not enough data
----> 3 cmps1.compile()
File ~\Documents\Coding\QSDsan-platform\QSDsan\qsdsan\_components.py:183, in Components.compile(self, skip_checks, ignore_inaccurate_molar_weight, adjust_MW_to_measured_as)
159 '''
160 Cast as a :class:`CompiledComponents` object.
161
(...) 180 :func:`Components.default_compile` for a fuller explanation and examples.
181 '''
182 components = tuple(self)
--> 183 prepare_chemicals(components, skip_checks)
184 setattr(self, '__class__', CompiledComponents)
186 try: self._compile(components, ignore_inaccurate_molar_weight, adjust_MW_to_measured_as)
File c:\Users\Yalin\Documents\Coding\QSDsan-platform\.venv\Lib\site-packages\thermosteam\_chemicals.py:43, in prepare(chemicals, skip_checks)
41 if not missing_properties: continue
42 missing = utils.repr_listed_values(missing_properties)
---> 43 raise RuntimeError(
44 f"{chemical} is missing key thermodynamic properties ({missing}); "
45 "use the `<Chemical>.get_missing_properties()` to check "
46 "all missing properties"
47 )
RuntimeError: XPAO is missing key thermodynamic properties (V, S, H and Cn); use the `<Chemical>.get_missing_properties()` to check all missing properties
[28]:
# Before compiling, you can see exactly what each component still lacks with
# `get_missing_properties` (here XPAO has no thermodynamic models yet)
XPAO.get_missing_properties()
[28]:
['S_excess',
'H_excess',
'mu',
'kappa',
'V',
'S',
'H',
'Cn',
'Psat',
'Hvap',
'sigma',
'epsilon',
'Dortmund',
'UNIFAC',
'PSRK',
'Hf',
'LHV',
'HHV',
'combustion',
'Tt',
'Hfus',
'Tb',
'Pc',
'Pt',
'Vc',
'Sfus',
'Tm',
'Tc',
'omega',
'iscyclic_aliphatic',
'dipole',
'similarity_variable']
Note: Some of these components (e.g., XPAO, measured as COD) are defined with a measured_as basis, so their molecular weight is not consistent with that basis and qsdsan cannot compute accurate molar/volumetric flows for them. compile() raises a RuntimeError to flag this. Pass ignore_inaccurate_molar_weight=True to proceed using mass-based quantities only, or adjust_MW_to_measured_as=True to fix the MW for components that have a chemical formula.
[29]:
# We can either provide the missing data as indicated in the error method,
# or we can use the `default` method and copy some data from H2O (or other more relevant components)
for i in cmps1:
if i is H2O:
continue # "continue" means skip the rest of the codes and continue with the next one in the loop
i.default()
i.copy_models_from(H2O, names=('sigma', 'epsilon', 'kappa', 'V', 'Cn', 'mu'))
# Now we can compile
cmps1.compile(ignore_inaccurate_molar_weight=True)
cmps1.show()
CompiledComponents([
XPAO, SNH4, H2O,
Methanol, Ethanol,
])
Shortcut: Instead of filling in models component by component, Components.default_compile() auto-fills the missing properties (boiling point and molar volume from a phase-appropriate reference, the rest from water) and compiles in one call. This is what load_default() uses under the hood.
[30]:
# For the default `Components` objects, the easy way is to let it compile during loading
cmps3 = qs.Components.load_default()
cmps3.show()
CompiledComponents([
S_H2, S_CH4, S_CH3OH, S_Ac,
S_Prop, S_F, S_U_Inf, S_U_E,
C_B_Subst, C_B_BAP, C_B_UAP, C_U_Inf,
X_B_Subst, X_OHO_PHA, X_GAO_PHA, X_PAO_PHA,
X_GAO_Gly, X_PAO_Gly, X_OHO, X_AOO,
X_NOO, X_AMO, X_PAO, X_MEOLO,
X_FO, X_ACO, X_HMO, X_PRO,
X_U_Inf, X_U_OHO_E, X_U_PAO_E, X_Ig_ISS,
X_MgCO3, X_CaCO3, X_MAP, X_HAP,
X_HDP, X_FePO4, X_AlPO4, X_AlOH,
X_FeOH, X_PAO_PP_Lo, X_PAO_PP_Hi, S_NH4,
S_NO2, S_NO3, S_PO4, S_K,
S_Ca, S_Mg, S_CO3, S_N2,
S_O2, S_CAT, S_AN, H2O,
])
[31]:
# Once compiled, you can no longer add `Component` to it
# so the following code will raise an error
cmps1.append(cmps3.S_H2)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[31], line 3
1 # Once compiled, you can no longer add `Component` to it
2 # so the following code will raise an error
----> 3 cmps1.append(cmps3.S_H2)
File c:\Users\Yalin\Documents\Coding\QSDsan-platform\.venv\Lib\site-packages\thermosteam\utils\decorators\read_only.py:14, in deny(self, *args, **kwargs)
13 def deny(self, *args, **kwargs):
---> 14 raise TypeError(f"'{type(self).__name__}' object is read-only")
TypeError: 'CompiledComponents' object is read-only
[32]:
# If you really need to add something, the best way (other than adding it before compiling)
# would be to make a new `Components` containing all the `Component` objects in the `CompiledComponent`
cmps4 = qs.Components(cmps1)
cmps4.append(cmps3.S_H2)
cmps4.show()
Components([
XPAO, SNH4, H2O, Methanol,
Ethanol, S_H2,
])
[33]:
# If you only want to work with a subset of the `CompiledComponents`,
# you can hand pick the `Component` IDs you want and input it to the
# `.subgroup()` method.
cmps5 = cmps3.subgroup(['S_CH4', 'S_Ac', 'S_F', 'S_NH4'])
cmps5.show()
CompiledComponents([S_CH4, S_Ac, S_F, S_NH4])
Tip: Why do we go through all the trouble to do the compilation? This is because compilation allows us to “freezes” the order of the properties (e.g., MW) and work with numbers only. It also allows us to use libraries like numpy and numba to speed things up. To learn more, check out this paper on scientific application of numpy.
[34]:
# Once compiled, you can set synonyms of `Component` so that you can find one component by any of its synonyms
# note that its ID won't change
cmps1.set_synonym('H2O', 'Water')
print(cmps1.H2O.ID)
print(cmps1.Water.ID)
H2O
H2O
[35]:
# In fact, all of the synonyms point to the same object
cmps1.H2O is cmps1.Water
[35]:
True
Python Aside: identity vs. equality (click to expand)
is checks whether two names point to the same object in memory, which is stricter than == (equal contents). Two lists can be equal but not identical:
lst1 = [1, 2]
lst2 = lst1.copy()
lst1 is lst2 # False - different objects
lst1 == lst2 # True - same contents
id(lst1), id(lst2) # different memory addresses
That is why cmps1.H2O is cmps1.Water above returns True: the synonym does not create a new object, it points to the existing one.
3.1. Selecting components by property¶
Compiling does more than freeze the property order — it also builds boolean (1/0) arrays, aligned with CompiledComponents.IDs, that flag each component by phase, biodegradability, and origin. These let you pick out groups of components without writing loops. Common ones are .g (dissolved gas), .s (soluble), .c (colloidal), .x (particulate), .b (biodegradable), .rb (readily biodegradable), .org (organic), and .inorg (inorganic).
[36]:
cmps3 = qs.Components.load_default()
cmps3.g # 1 for each dissolved gas, 0 otherwise
[36]:
array([1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0])
[37]:
# Translate a 1/0 array into the component IDs it selects
cmps3.get_IDs_from_array(cmps3.g)
[37]:
('S_H2', 'S_CH4', 'S_N2', 'S_O2')
[38]:
# ...and the other way, from a set of IDs to an aligned 1/0 array
cmps3.get_array_from_IDs(('S_H2', 'S_CH4', 'S_N2', 'S_O2'))
[38]:
array([1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0])
3.2. Defining component groups¶
A group is a named subset of components. QSDsan uses groups to compute composite variables: the built-in TKN group, for instance, powers WasteStream.TKN and composite(subgroup=cmps.TKN) (see 3. WasteStream). Define your own with define_group, then use the name anywhere a subgroup is accepted.
[39]:
# Group components under a name, then reuse it (here, the volatile fatty acids)
cmps3.define_group('VFAs', IDs=('S_Ac', 'S_Prop'))
[c.ID for c in cmps3.VFAs]
[39]:
['S_Ac', 'S_Prop']
↑ Back to top