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