Component
¶
Prepared by:
Covered topics:
0. Before getting started
1. Component
2. Components
3. CompiledComponents
Video demo:
To run tutorials in your browser, go to this Binder page.
You can also watch a video demo on YouTube (subscriptions & likes appreciated!).
0. Before getting started¶
Check out this video here about qsdsan
, make sure you understand the UML (unified modeling language) diagram.
[1]:
from IPython.display import Image
Image(url='https://lucid.app/publicSegments/view/c8de361f-7292-47e3-8870-d6f668691728/image.png', width=800)
[1]:
[2]:
import qsdsan as qs
print(f'This tutorial was made with qsdsan v{qs.__version__}.')
This tutorial was made with qsdsan v1.3.0.
Back to top
1. Component
¶
As shown in the UML diagram above, Component
is the most basic class of qsdsan
, it can represent a pure chemical (e.g., Water) or a group of chemicals that are of similiar properties (e.g., X_PAO, phosphorus accumulating organisms).
[3]:
# It's always good to read through the documentation
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.
[4]:
# We usually capitalize the ID of a Component
XPAO = qs.Component('XPAO', formula = 'C5H7O2N', measured_as = 'COD', phase='l',
particle_size = 'Particulate', degradability = 'Biological',
organic = True)
[5]:
# The `show` method is very helpful in getting an overview of the component
XPAO.show?
[6]:
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: Biological
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 g NOD/g COD
f_BOD5_COD: 0
f_uBOD_COD: 0
f_Vmass_Totmass: 0
chem_MW: 113.11
[7]:
XPAO.show(False)
Component: XPAO (phase_ref='l') at phase='l'
Component-specific properties:
[Others] measured_as: COD
description: None
particle_size: Particulate
degradability: Biological
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 g NOD/g COD
f_BOD5_COD: 0
f_uBOD_COD: 0
f_Vmass_Totmass: 0
chem_MW: 113.11
[8]:
# If you want to know what a specific attribute means, you can check the documentation
XPAO.MW?
[9]:
# If you provide some inputs that are not legit, you will likely receive an error
# bad_CH4 = qs.Component.from_chemical(ID=None, search_ID='CH4', particle_size='Dissolved gas',
# degradability='Readily', organic=False)
1.2 Component
from Chemical
¶
You can convert a Chemical
object (native to biosteam
) to Component
by providing the extra information needed.
[10]:
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}
[11]:
type(H2O_chem)
[11]:
thermosteam._chemical.Chemical
[12]:
H2O = qs.Component.from_chemical(ID='H2O', chemical=H2O_chem,
particle_size='Soluble', degradability='Undegradable', organic=False)
[13]:
# Or you 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: InChI=1S/H3N/h1H3/p+...
InChI_key: QGZKDVFQNNGYKY-U...
common_name: Ammonium
iupac_name: ('azane;hydron'...
pubchemid: 1.6741e+07
smiles: [NH4+]
formula: H4N+
[Groups] Dortmund: <Empty>
UNIFAC: <Empty>
PSRK: <Empty>
NIST: <Empty>
[Data] MW: 18.039 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.27718
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
Tip¶
You may notice that sometimes I just put the values of positional arguments (often shortened to args), e.g.,
“SNH4”
sometimes I put both the keyword arguments (kwargs) and their values e.g.,
measured_as=’N’
If you don’t give the keyword, all the values will be assumed to be in the same order as in the __init__
method of the object, it is also really convenient for functions without too many arguments, but can be problematic if there are many kwargs with some default values, and you only want to update several values.
[14]:
# Let's make a small function to compare,
# "foo" is often used as a name for a dummy function,
# note that Python is 0-indexed (e.g., counting from 0)
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
[15]:
# This can still make sense if we do
foo(param0='Fuji', param5='apple')
Fuji is a kind of apple
[16]:
# But it won't make any sense if we take out the keyward part,
# since 'apple' will be passed to `param1`
foo('Fuji', 'apple')
Fuji apple a kind of dog
[17]:
# Also note that you can do this:
foo('Fuji', param5='apple')
Fuji is a kind of apple
[18]:
# But you can't do this because Python doesn't know which argument you want pass "apple" to
# foo(param0='Fuji', 'apple')
[19]:
# If you create a `Component` with molecular formula,
# attributes such as `i_C`, `i_N` will be automatically calcualted,
# and `qsdsan` will raise an error if you want to change the value
H2O.i_C
[19]:
0.0
[20]:
# H2O.i_C = 1
[21]:
# 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
[22]:
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
[23]:
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
[24]:
# Apparently you cannot have a component measured as some element that it does not have
# SNH4.measured_as = 'C'
# SNH4.show()
[25]:
# If you have two components of similiar properties, you can use the `copy` method
CH4_g = qs.Component(ID='CH4_g', search_ID='CH4', phase='g',
particle_size='Dissolved gas', degradability='Readily',
organic=False, description='gas phase CH4')
[26]:
CH4_g.show()
Component: CH4_g (phase_ref='g') at phase='g'
Component-specific properties:
[Others] measured_as: None
description: gas phase CH4
particle_size: Dissolved gas
degradability: Readily
organic: False
i_C: 0.74868 g C/g
i_N: 0 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 mol +/g
i_COD: 0 g COD/g
i_NOD: 0 g NOD/g
f_BOD5_COD: 0
f_uBOD_COD: 0
f_Vmass_Totmass: 0
chem_MW: 16.042
[27]:
CH4_l = CH4_g.copy('CH4_l', phase='l', particle_size='Soluble', description='liquid phase CH4')
[28]:
CH4_l.show() # the only difference is that CH4_g is locked at gas phase while CH4_l at liquid phase
Component: CH4_l (phase_ref='g') at phase='l'
Component-specific properties:
[Others] measured_as: None
description: liquid phase CH4
particle_size: Soluble
degradability: Readily
organic: False
i_C: 0.74868 g C/g
i_N: 0 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 mol +/g
i_COD: 0 g COD/g
i_NOD: 0 g NOD/g
f_BOD5_COD: 0
f_uBOD_COD: 0
f_Vmass_Totmass: 0
chem_MW: 16.042
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, and thermosteam
can ignore phase equilibrium.
Back to top
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.
[29]:
cmps1 = qs.Components((XPAO, SNH4))
cmps1
Components([XPAO, SNH4])
[30]:
# 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: Biological
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 g NOD/g COD
f_BOD5_COD: 0
f_uBOD_COD: 0
f_Vmass_Totmass: 0
chem_MW: 113.11
[31]:
# You can add more `Component` objects by appending (adding one per time)
# or extending (adding several per time)
cmps1.append(H2O)
cmps1
Components([XPAO, SNH4, H2O])
[32]:
# Of course, you can make a copy
cmps1_copy = cmps1.copy()
cmps1_copy
Components([XPAO, SNH4, H2O])
[33]:
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)
[34]:
cmps1.extend((Methanol, Ethanol))
[35]:
# Note that `extend` only takes one argument, this means you cannot do (because there are two arguments)
# cmps1.extend(Methanol, Ethanol)
[36]:
# By using the "()" to wrap around all components, you are essentially doing
collection = (Methanol, Ethanol)
cmps1_copy.extend(collection)
[37]:
# Note that we use `cmps1_copy` above instead of `cmps1` because we cannot add already defined components
# in the same `Components` object
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
.
Tip¶
You may be wondering about the difference between “function” and “method”, in Python, a function becomes a “method” if it belongs to a certain class. For example, from_chemicals
is a “method” because it belongs to the Components
class, but foo
(remember Husky?) is just a function because there is no class associated with it.
Methods can be further divide into instance method, class method, and staticmethod, you can search for those if you are interested.
[38]:
# A good example can be found at the documentation of the `from_chemicals` method
cmps1.from_chemicals?
You can just copy the codes and run it, I just changed cmps
to cmps2
for clarity (and pseudo-OCD).
[39]:
>>> 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}}
>>> cmps = qs.Components.from_chemicals(chems, **data)
>>> cmps
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.
[40]:
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.
[41]:
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()
[41]:
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
[42]:
df.columns
[42]:
Index(['ID', 'description', 'formula', 'particle_size', 'degradability', 'organic', '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', 'CAS', 'PubChem'],
dtype='object')
[43]:
cmps2 = qs.Components.load_from_file(df)
cmps2.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])
Back to top
3. CompiledComponents
¶
Finally, we need to “compile” Components
into CompiledComponents
, which can be easily done using the compile
method.
When executing the compile
method, thermosteam
will check if all Component
objects have the properties needed for thermodynamic calculation.
[44]:
# For example, we will receive an error if we try to compile cmps1,
# because there is not enough data
# cmps1.compile()
[45]:
# 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()
cmps1.show()
CompiledComponents([XPAO, SNH4, H2O, Methanol, Ethanol])
[46]:
# 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])
[47]:
# Once compiled, you can no longer add `Component` to it
# cmps1.append(cmps3.S_H2)
[48]:
# 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])
[49]:
# 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.
Maybe check out this paper on scientific application of numpy
.
[50]:
# 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')
[51]:
cmps1.H2O.ID
[51]:
'H2O'
[52]:
cmps1.Water.ID
[52]:
'H2O'
[53]:
# In fact, all of the synonyms point to the same object
cmps1.H2O is cmps1.Water
[53]:
True
Tip¶
Note that in Python, being the same object is much stricter, it compares the memory address of two objects.
[54]:
# We can make two identical lists, but they are not the same
lst1 = [1, 2]
lst2 = lst1.copy()
[55]:
lst1 is lst2
[55]:
False
[56]:
lst1 == lst2 # this only compares the contents within these two lists
[56]:
True
[57]:
id(lst1)
[57]:
2332755558272
[58]:
id(lst2)
[58]:
2332718576768
Back to top