{ "cells": [ { "cell_type": "markdown", "id": "8fc5de98", "metadata": {}, "source": [ "# `Component` \n", "\n", "*Click the badge below to try this tutorial interactively in your browser:*\n", "\n", "[![Launch Binder](../images/custom_binder_logo.svg)](https://mybinder.org/v2/gh/QSD-Group/QSDsan-env/main?urlpath=git-pull%3Frepo%3Dhttps%253A%252F%252Fgithub.com%252FQSD-group%252FQSDsan%26urlpath%3Dlab%252Ftree%252FQSDsan%252Fdocs%252Fsource%252Ftutorials%26branch%3Dmain)\n", "\n", "*You can also run this tutorial in [Google Colab](https://colab.research.google.com). It takes a one-time setup per session: follow the [Colab instructions](https://qsdsan.readthedocs.io/en/latest/tutorials/index.html#run-in-colab).*\n", "\n", "- **Prepared by:**\n", "\n", " - [Yalin Li](https://github.com/yalinli2)\n", " - [Joy Zhang](https://github.com/joyxyz1994/)\n", "\n", "- **Learning objectives.** After this tutorial, you will be able to:\n", "\n", " - Define a `Component` and inspect its thermodynamic and biological properties\n", " - Build a `Component` registry for a system\n", " - Switch between mass, mole, and volumetric representations\n", "\n", "- **Covered topics:**\n", "\n", " - 1. Component\n", " - 2. Components\n", " - 3. CompiledComponents\n", "\n", "> **Companion video.** A walkthrough of this tutorial is available on [YouTube](https://youtu.be/1OlGsjbqUX8?si=3eIxJUjyu7PfiQor), presented by [Tori Morgan](https://github.com/vlmorgan93). Recorded against `QSDsan` v1.3.0. The concepts still apply, but if the code on screen differs from this notebook, follow the notebook.\n" ] }, { "cell_type": "markdown", "id": "462eadc571f0", "metadata": {}, "source": [ "\n", "\n", "## Setup\n", "\n", "Import `QSDsan` and confirm the installed version.\n" ] }, { "cell_type": "code", "execution_count": 1, "id": "3c75dd5b", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "This tutorial was made with qsdsan v1.5.3.\n" ] } ], "source": [ "import qsdsan as qs\n", "print(f'This tutorial was made with qsdsan v{qs.__version__}.')" ] }, { "cell_type": "markdown", "id": "21b7914e", "metadata": {}, "source": [ "## 1. `Component` \n", "`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)." ] }, { "cell_type": "code", "execution_count": 2, "id": "f9a8e5a2", "metadata": {}, "outputs": [], "source": [ "# It's always good to read through the documentation\n", "# commented out to avoid too much output\n", "# help(qs.Component)" ] }, { "cell_type": "markdown", "id": "56ffe14b", "metadata": {}, "source": [ "### 1.1. `Component` from scratch\n", "You can make a `Component` object from scratch by providing relevant information upon initialization (i.e., creation) of the object.\n", "\n", "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." ] }, { "cell_type": "code", "execution_count": 3, "id": "0ab53b31", "metadata": {}, "outputs": [], "source": [ "# We usually capitalize the ID of a Component\n", "XPAO = qs.Component('XPAO', formula = 'C5H7O2N', measured_as = 'COD', phase='l',\n", " particle_size = 'Particulate', degradability = 'Slowly',\n", " organic = True)" ] }, { "cell_type": "code", "execution_count": 4, "id": "f1745293", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Component: XPAO (phase_ref='l') at phase='l'\n", "[Names] CAS: XPAO\n", " InChI: None\n", " InChI_key: None\n", " common_name: None\n", " iupac_name: None\n", " pubchemid: None\n", " smiles: None\n", " formula: C5H7O2N\n", "[Groups] Dortmund: \n", " UNIFAC: \n", " PSRK: \n", " NIST: \n", "[Data] MW: 113.11 g/mol\n", " Tm: None\n", " Tb: None\n", " Tt: None\n", " Tc: None\n", " Pt: None\n", " Pc: None\n", " Vc: None\n", " Hf: None\n", " S0: 0 J/K/mol\n", " LHV: None\n", " HHV: None\n", " Hfus: None\n", " Sfus: None\n", " omega: None\n", " dipole: None\n", " similarity_variable: None\n", " iscyclic_aliphatic: None\n", " combustion: None\n", "Component-specific properties:\n", "[Others] measured_as: COD\n", " description: None\n", " particle_size: Particulate\n", " degradability: Slowly\n", " organic: True\n", " i_C: 0.37535 g C/g COD\n", " i_N: 0.087545 g N/g COD\n", " i_P: 0 g P/g COD\n", " i_K: 0 g K/g COD\n", " i_Mg: 0 g Mg/g COD\n", " i_Ca: 0 g Ca/g COD\n", " i_mass: 0.70699 g mass/g COD\n", " i_charge: 0 mol +/g COD\n", " i_COD: 1 g COD/g COD\n", " i_NOD: 0.4 g NOD/g COD\n", " f_BOD5_COD: 0\n", " f_uBOD_COD: 0\n", " f_Vmass_Totmass: 0\n", " chem_MW: 113.11\n" ] } ], "source": [ "# The `show` method is very helpful in getting an overview of the component\n", "# The `chemical_info` argument will show the chemical information of the component, such as formula and molecular weight\n", "XPAO.show(chemical_info=True)" ] }, { "cell_type": "code", "execution_count": 5, "id": "6634622b", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Component: XPAO (phase_ref='l') at phase='l'\n", "Component-specific properties:\n", "[Others] measured_as: COD\n", " description: None\n", " particle_size: Particulate\n", " degradability: Slowly\n", " organic: True\n", " i_C: 0.37535 g C/g COD\n", " i_N: 0.087545 g N/g COD\n", " i_P: 0 g P/g COD\n", " i_K: 0 g K/g COD\n", " i_Mg: 0 g Mg/g COD\n", " i_Ca: 0 g Ca/g COD\n", " i_mass: 0.70699 g mass/g COD\n", " i_charge: 0 mol +/g COD\n", " i_COD: 1 g COD/g COD\n", " i_NOD: 0.4 g NOD/g COD\n", " f_BOD5_COD: 0\n", " f_uBOD_COD: 0\n", " f_Vmass_Totmass: 0\n", " chem_MW: 113.11\n" ] } ], "source": [ "# As a comparison, here is the same component without the chemical information\n", "XPAO.show(False)" ] }, { "cell_type": "code", "execution_count": 6, "id": "e76a56be", "metadata": { "tags": [ "raises-exception" ] }, "outputs": [ { "ename": "ValueError", "evalue": "particle_size must be in ('Dissolved gas', 'Soluble', 'Colloidal', 'Particulate').", "output_type": "error", "traceback": [ "\u001b[31m---------------------------------------------------------------------------\u001b[39m", "\u001b[31mValueError\u001b[39m Traceback (most recent call last)", "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[6]\u001b[39m\u001b[32m, line 3\u001b[39m\n\u001b[32m 1\u001b[39m \u001b[38;5;66;03m# If you provide some inputs that are not legit, you will likely receive an error\u001b[39;00m\n\u001b[32m 2\u001b[39m \u001b[38;5;66;03m# For example, the particle size cannot be \"Dissolved liquid\" (should be \"Dissolved gas\")\u001b[39;00m\n\u001b[32m----> \u001b[39m\u001b[32m3\u001b[39m bad_CH4 = qs.Component(ID='bad_CH4', search_ID='CH4', particle_size='Dissolved liquid',\n\u001b[32m 4\u001b[39m degradability=\u001b[33m'Readily'\u001b[39m, organic=\u001b[38;5;28;01mFalse\u001b[39;00m)\n", "\u001b[36mFile \u001b[39m\u001b[32m~\\Documents\\Coding\\QSDsan-platform\\QSDsan\\qsdsan\\_component.py:261\u001b[39m, in \u001b[36mComponent.__new__\u001b[39m\u001b[34m(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)\u001b[39m\n\u001b[32m 257\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m phase: lock_phase(\u001b[38;5;28mself\u001b[39m, phase)\n\u001b[32m 259\u001b[39m \u001b[38;5;66;03m# Assign through the property setters so invalid values are caught at\u001b[39;00m\n\u001b[32m 260\u001b[39m \u001b[38;5;66;03m# creation (setters validate via `check_return_property`)\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m261\u001b[39m \u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mparticle_size\u001b[39;49m = particle_size\n\u001b[32m 262\u001b[39m \u001b[38;5;28mself\u001b[39m.degradability = degradability\n\u001b[32m 263\u001b[39m \u001b[38;5;28mself\u001b[39m.organic = organic\n", "\u001b[36mFile \u001b[39m\u001b[32m~\\Documents\\Coding\\QSDsan-platform\\QSDsan\\qsdsan\\_component.py:522\u001b[39m, in \u001b[36mComponent.particle_size\u001b[39m\u001b[34m(self, particle_size)\u001b[39m\n\u001b[32m 520\u001b[39m \u001b[38;5;129m@particle_size\u001b[39m.setter\n\u001b[32m 521\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mparticle_size\u001b[39m(\u001b[38;5;28mself\u001b[39m, particle_size):\n\u001b[32m--> \u001b[39m\u001b[32m522\u001b[39m \u001b[38;5;28mself\u001b[39m._particle_size = \u001b[30;43mcheck_return_property\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43m'\u001b[39;49m\u001b[30;43mparticle_size\u001b[39;49m\u001b[30;43m'\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43mparticle_size\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n", "\u001b[36mFile \u001b[39m\u001b[32m~\\Documents\\Coding\\QSDsan-platform\\QSDsan\\qsdsan\\_component.py:110\u001b[39m, in \u001b[36mcheck_return_property\u001b[39m\u001b[34m(name, value)\u001b[39m\n\u001b[32m 108\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m\n\u001b[32m 109\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m value \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;129;01min\u001b[39;00m allowed_values[name]:\n\u001b[32m--> \u001b[39m\u001b[32m110\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[33mf\u001b[39m\u001b[33m'\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mname\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m must be in \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mallowed_values[name]\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m.\u001b[39m\u001b[33m'\u001b[39m)\n\u001b[32m 111\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m value\n", "\u001b[31mValueError\u001b[39m: particle_size must be in ('Dissolved gas', 'Soluble', 'Colloidal', 'Particulate')." ] } ], "source": [ "# If you provide some inputs that are not legit, you will likely receive an error\n", "# For example, the particle size cannot be \"Dissolved liquid\" (should be \"Dissolved gas\")\n", "bad_CH4 = qs.Component(ID='bad_CH4', search_ID='CH4', particle_size='Dissolved liquid',\n", " degradability='Readily', organic=False)" ] }, { "cell_type": "markdown", "id": "eeef3467", "metadata": {}, "source": [ "### 1.2 `Component` from `Chemical`\n", "You can convert a `Chemical` object (native to `biosteam`) to `Component` by providing the extra information needed." ] }, { "cell_type": "code", "execution_count": 7, "id": "ddb3f7d5", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Chemical: H2O (phase_ref='l')\n", "[Names] CAS: 7732-18-5\n", " InChI: H2O/h1H2\n", " InChI_key: XLYOFNOQVPJJNP-U...\n", " common_name: water\n", " iupac_name: ('oxidane',)\n", " pubchemid: 962\n", " smiles: O\n", " formula: H2O\n", "[Groups] Dortmund: <1H2O>\n", " UNIFAC: <1H2O>\n", " PSRK: <1H2O>\n", " NIST: \n", "[Data] MW: 18.015 g/mol\n", " Tm: 273.15 K\n", " Tb: 373.12 K\n", " Tt: 273.16 K\n", " Tc: 647.1 K\n", " Pt: 611.65 Pa\n", " Pc: 2.2064e+07 Pa\n", " Vc: 5.5948e-05 m^3/mol\n", " Hf: -2.8582e+05 J/mol\n", " S0: 70 J/K/mol\n", " LHV: -44011 J/mol\n", " HHV: -0 J/mol\n", " Hfus: 6010 J/mol\n", " Sfus: None\n", " omega: 0.3443\n", " dipole: 1.85 Debye\n", " similarity_variable: 0.16653\n", " iscyclic_aliphatic: 0\n", " combustion: {'H2O': 1.0}\n" ] } ], "source": [ "H2O_chem = qs.Chemical('H2O')\n", "H2O_chem.show()" ] }, { "cell_type": "code", "execution_count": 8, "id": "bf48c688", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "thermosteam._chemical.Chemical" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "type(H2O_chem)" ] }, { "cell_type": "code", "execution_count": 9, "id": "2a45637d", "metadata": {}, "outputs": [], "source": [ "# 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\n", "H2O_cmp1 = qs.Component('H2O_cmp1', chemical=H2O_chem, particle_size='Soluble', degradability='Undegradable', organic=False)" ] }, { "cell_type": "code", "execution_count": 10, "id": "7c50613c", "metadata": {}, "outputs": [], "source": [ "# You can also use the `from_chemical` method\n", "H2O_cmp2 = qs.Component.from_chemical('H2O_cmp2', chemical=H2O_chem, particle_size='Soluble', degradability='Undegradable', organic=False)" ] }, { "cell_type": "code", "execution_count": 11, "id": "65887841", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Component: SNH4 (phase_ref='l') at phase='l'\n", "[Names] CAS: 14798-03-9\n", " InChI: H3N/h1H3/p+1\n", " InChI_key: QGZKDVFQNNGYKY-U...\n", " common_name: ammonium\n", " iupac_name: ('azanium',)\n", " pubchemid: 223\n", " smiles: [NH4+]\n", " formula: H4N+\n", "[Groups] Dortmund: \n", " UNIFAC: \n", " PSRK: \n", " NIST: \n", "[Data] MW: 18.038 g/mol\n", " Tm: None\n", " Tb: None\n", " Tt: None\n", " Tc: None\n", " Pt: None\n", " Pc: None\n", " Vc: 0.00011214 m^3/mol\n", " Hf: None\n", " S0: 0 J/K/mol\n", " LHV: None\n", " HHV: None\n", " Hfus: 0 J/mol\n", " Sfus: None\n", " omega: None\n", " dipole: None\n", " similarity_variable: 0.27719\n", " iscyclic_aliphatic: 0\n", " combustion: None\n", "Component-specific properties:\n", "[Others] measured_as: N\n", " description: None\n", " particle_size: Soluble\n", " degradability: Undegradable\n", " organic: False\n", " i_C: 0 g C/g N\n", " i_N: 1 g N/g N\n", " i_P: 0 g P/g N\n", " i_K: 0 g K/g N\n", " i_Mg: 0 g Mg/g N\n", " i_Ca: 0 g Ca/g N\n", " i_mass: 1.2878 g mass/g N\n", " i_charge: 0.071394 mol +/g N\n", " i_COD: 0 g COD/g N\n", " i_NOD: 4.5691 g NOD/g N\n", " f_BOD5_COD: 0\n", " f_uBOD_COD: 0\n", " f_Vmass_Totmass: 0\n", " chem_MW: 18.038\n" ] } ], "source": [ "# For components that are chemicals, you can pass a `search_ID` to look into the database\n", "SNH4 = qs.Component('SNH4', search_ID='Ammonium', measured_as='N', phase='l',\n", " particle_size='Soluble', degradability='Undegradable',\n", " organic=False)\n", "SNH4.show(True)" ] }, { "cell_type": "markdown", "id": "ad72fec9", "metadata": {}, "source": [ "
\n", "Python Aside: positional vs. keyword arguments (click to expand)\n", "\n", "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.\n", "\n", "```python\n", "def foo(param0='Husky', param1='is', param2='a', param3='kind', param4='of', param5='dog'):\n", " print(' '.join((param0, param1, param2, param3, param4, param5)))\n", "\n", "foo() # Husky is a kind of dog\n", "foo(param0='Fuji', param5='apple') # Fuji is a kind of apple\n", "foo('Fuji', 'apple') # Fuji apple a kind of dog ('apple' lands in param1!)\n", "foo('Fuji', param5='apple') # Fuji is a kind of apple (positional then keyword is OK)\n", "# foo(param0='Fuji', 'apple') # SyntaxError: positional arg can't follow keyword arg\n", "```\n", "\n", "
" ] }, { "cell_type": "code", "execution_count": 12, "id": "474313b6", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "0.0" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# If you create a `Component` with molecular formula,\n", "# attributes such as `i_C`, `i_N` will be automatically calcualted,\n", "H2O = qs.Component('H2O', formula='H2O', particle_size='Soluble', degradability='Undegradable', organic=False)\n", "H2O.i_C" ] }, { "cell_type": "code", "execution_count": 13, "id": "d708cae1", "metadata": { "tags": [ "raises-exception" ] }, "outputs": [ { "ename": "AttributeError", "evalue": "This component has formula, i_C is calculated based on formula, cannot be set.", "output_type": "error", "traceback": [ "\u001b[31m---------------------------------------------------------------------------\u001b[39m", "\u001b[31mAttributeError\u001b[39m Traceback (most recent call last)", "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[13]\u001b[39m\u001b[32m, line 2\u001b[39m\n\u001b[32m 1\u001b[39m \u001b[38;5;66;03m# `qsdsan` will raise an error if you want to change the value\u001b[39;00m\n\u001b[32m----> \u001b[39m\u001b[32m2\u001b[39m H2O.i_C = \u001b[32m1\u001b[39m\n", "\u001b[36mFile \u001b[39m\u001b[32m~\\Documents\\Coding\\QSDsan-platform\\QSDsan\\qsdsan\\_component.py:309\u001b[39m, in \u001b[36mComponent.i_C\u001b[39m\u001b[34m(self, i)\u001b[39m\n\u001b[32m 307\u001b[39m \u001b[38;5;129m@i_C\u001b[39m.setter\n\u001b[32m 308\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mi_C\u001b[39m(\u001b[38;5;28mself\u001b[39m, i):\n\u001b[32m--> \u001b[39m\u001b[32m309\u001b[39m \u001b[38;5;28mself\u001b[39m._i_C = \u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43m_atom_frac_setter\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43m'\u001b[39;49m\u001b[30;43mC\u001b[39;49m\u001b[30;43m'\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43mi\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n", "\u001b[36mFile \u001b[39m\u001b[32m~\\Documents\\Coding\\QSDsan-platform\\QSDsan\\qsdsan\\_component.py:291\u001b[39m, in \u001b[36mComponent._atom_frac_setter\u001b[39m\u001b[34m(self, atom, frac)\u001b[39m\n\u001b[32m 289\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m.formula:\n\u001b[32m 290\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m frac:\n\u001b[32m--> \u001b[39m\u001b[32m291\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mAttributeError\u001b[39;00m(\u001b[33m'\u001b[39m\u001b[33mThis component has formula, \u001b[39m\u001b[33m'\u001b[39m\n\u001b[32m 292\u001b[39m \u001b[33mf\u001b[39m\u001b[33m'\u001b[39m\u001b[33mi_\u001b[39m\u001b[38;5;132;01m{\u001b[39;00matom\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m is calculated based on formula, \u001b[39m\u001b[33m'\u001b[39m\n\u001b[32m 293\u001b[39m \u001b[33m'\u001b[39m\u001b[33mcannot be set.\u001b[39m\u001b[33m'\u001b[39m)\n\u001b[32m 294\u001b[39m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[32m 295\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m atom \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m.atoms.keys():\n", "\u001b[31mAttributeError\u001b[39m: This component has formula, i_C is calculated based on formula, cannot be set." ] } ], "source": [ "# `qsdsan` will raise an error if you want to change the value\n", "H2O.i_C = 1" ] }, { "cell_type": "code", "execution_count": 14, "id": "48ca0d35", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Component: SNH4 (phase_ref='l') at phase='l'\n", "Component-specific properties:\n", "[Others] measured_as: None\n", " description: None\n", " particle_size: Soluble\n", " degradability: Undegradable\n", " organic: False\n", " i_C: 0 g C/g \n", " i_N: 0.77649 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 g mass/g \n", " i_charge: 0.055437 mol +/g \n", " i_COD: 0 g COD/g \n", " i_NOD: 3.5478 g NOD/g \n", " f_BOD5_COD: 0\n", " f_uBOD_COD: 0\n", " f_Vmass_Totmass: 0\n", " chem_MW: 18.038\n" ] } ], "source": [ "# If you change the `measured_as` attribute of a `Component`, \n", "# the units and values of the `i_` attributes, such as `i_N`, \n", "# `i_COD` will also be updated automatically.\n", "SNH4.measured_as = None\n", "SNH4.show(False)" ] }, { "cell_type": "code", "execution_count": 15, "id": "ca4c43b6", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Component: SNH4 (phase_ref='l') at phase='l'\n", "Component-specific properties:\n", "[Others] measured_as: N\n", " description: None\n", " particle_size: Soluble\n", " degradability: Undegradable\n", " organic: False\n", " i_C: 0 g C/g N\n", " i_N: 1 g N/g N\n", " i_P: 0 g P/g N\n", " i_K: 0 g K/g N\n", " i_Mg: 0 g Mg/g N\n", " i_Ca: 0 g Ca/g N\n", " i_mass: 1.2878 g mass/g N\n", " i_charge: 0.071394 mol +/g N\n", " i_COD: 0 g COD/g N\n", " i_NOD: 4.5691 g NOD/g N\n", " f_BOD5_COD: 0\n", " f_uBOD_COD: 0\n", " f_Vmass_Totmass: 0\n", " chem_MW: 18.038\n" ] } ], "source": [ "# Note the difference in the `i_N` value and unit before and after changing the `measured_as` attribute\n", "SNH4.measured_as = 'N'\n", "SNH4.show()" ] }, { "cell_type": "code", "execution_count": 16, "id": "fd9ca130", "metadata": { "tags": [ "raises-exception" ] }, "outputs": [ { "ename": "ValueError", "evalue": "SNH4 cannot be measured as C", "output_type": "error", "traceback": [ "\u001b[31m---------------------------------------------------------------------------\u001b[39m", "\u001b[31mValueError\u001b[39m Traceback (most recent call last)", "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[16]\u001b[39m\u001b[32m, line 2\u001b[39m\n\u001b[32m 1\u001b[39m \u001b[38;5;66;03m# Apparently you cannot have a component measured as some element that it does not have\u001b[39;00m\n\u001b[32m----> \u001b[39m\u001b[32m2\u001b[39m SNH4.measured_as = \u001b[33m'C'\u001b[39m\n", "\u001b[36mFile \u001b[39m\u001b[32m~\\Documents\\Coding\\QSDsan-platform\\QSDsan\\qsdsan\\_component.py:463\u001b[39m, in \u001b[36mComponent.measured_as\u001b[39m\u001b[34m(self, measured_as)\u001b[39m\n\u001b[32m 461\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mhasattr\u001b[39m(\u001b[38;5;28mself\u001b[39m, \u001b[33m'\u001b[39m\u001b[33m_measured_as\u001b[39m\u001b[33m'\u001b[39m):\n\u001b[32m 462\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m._measured_as != measured_as:\n\u001b[32m--> \u001b[39m\u001b[32m463\u001b[39m \u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43m_convert_i_attr\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43mmeasured_as\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n\u001b[32m 464\u001b[39m \u001b[38;5;28mself\u001b[39m._measured_as = measured_as\n", "\u001b[36mFile \u001b[39m\u001b[32m~\\Documents\\Coding\\QSDsan-platform\\QSDsan\\qsdsan\\_component.py:506\u001b[39m, in \u001b[36mComponent._convert_i_attr\u001b[39m\u001b[34m(self, new)\u001b[39m\n\u001b[32m 501\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mAttributeError\u001b[39;00m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mComponent \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mself\u001b[39m.ID\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m must be measured as \u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 502\u001b[39m \u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33meither COD or one of its constituent atoms, \u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 503\u001b[39m \u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mif not as itself.\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m 505\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m denom == \u001b[32m0\u001b[39m:\n\u001b[32m--> \u001b[39m\u001b[32m506\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[33mf\u001b[39m\u001b[33m'\u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mself\u001b[39m.ID\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m cannot be measured as \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mnew\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m'\u001b[39m)\n\u001b[32m 508\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m field \u001b[38;5;129;01min\u001b[39;00m _num_component_properties:\n\u001b[32m 509\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m field.startswith(\u001b[33m'\u001b[39m\u001b[33mi_\u001b[39m\u001b[33m'\u001b[39m):\n", "\u001b[31mValueError\u001b[39m: SNH4 cannot be measured as C" ] } ], "source": [ "# Apparently you cannot have a component measured as some element that it does not have\n", "SNH4.measured_as = 'C'" ] }, { "cell_type": "markdown", "id": "d92fd9de", "metadata": {}, "source": [ "
\n", "\n", "**Tip:** Why do we want to set the phase (or \"lock the state\") of a `Component`?\n", "\n", "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.\n", "\n", "
" ] }, { "cell_type": "markdown", "id": "26df00a4", "metadata": {}, "source": [ "### 1.3. `measured_as`, `i_mass`, and the `i_` factors \n", "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.\n", "\n", "- `measured_as=None` (default) — measured as itself, i.e., in grams of its own mass.\n", "- `measured_as='N'` (a constituent element) — quantified by its nitrogen content: 1 g of the component means 1 g of N.\n", "- `measured_as='COD'` — quantified by its oxygen demand: 1 g means 1 g COD.\n", "\n", "Every `i_` attribute reads as **\"g of X per g of the measure unit\"**:\n", "\n", "| Attribute | Meaning |\n", "|---|---|\n", "| `i_mass` | g of actual component mass per g of the measure unit |\n", "| `i_N`, `i_C`, `i_P`, `i_K`, `i_Mg`, `i_Ca` | g of that element per g of the measure unit |\n", "| `i_COD`, `i_NOD` | g of carbonaceous / nitrogenous oxygen demand per g of the measure unit |\n", "| `i_charge` | mol of positive charge per g of the measure unit |\n", "\n", "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." ] }, { "cell_type": "code", "execution_count": 17, "id": "bf1f87a6", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "NH4_as_mass measured_as=None i_mass=1.0000 i_N=0.7765 i_NOD=3.5478\n", "NH4_as_N measured_as=N i_mass=1.2878 i_N=1.0000 i_NOD=4.5691\n" ] } ], "source": [ "# Ammonium quantified two ways: as its own mass vs. as nitrogen\n", "NH4_as_mass = qs.Component('NH4_as_mass', search_ID='Ammonium', measured_as=None,\n", " particle_size='Soluble', degradability='Undegradable', organic=False)\n", "NH4_as_N = qs.Component('NH4_as_N', search_ID='Ammonium', measured_as='N',\n", " particle_size='Soluble', degradability='Undegradable', organic=False)\n", "for cmp in (NH4_as_mass, NH4_as_N):\n", " print(f'{cmp.ID:12} measured_as={str(cmp.measured_as):4} '\n", " f'i_mass={cmp.i_mass:.4f} i_N={cmp.i_N:.4f} i_NOD={cmp.i_NOD:.4f}')\n", "# as mass -> i_mass=1.0000, i_N=0.7765 (N is ~78% of NH4+ by mass)\n", "# as N -> i_N=1.0000, i_mass=1.2878 (1 g N corresponds to ~1.29 g of ammonium)" ] }, { "cell_type": "markdown", "id": "fbf5532b", "metadata": {}, "source": [ "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`.\n", "\n", "
\n", "\n", "**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.\n", "\n", "
" ] }, { "cell_type": "markdown", "id": "02eb2af4", "metadata": {}, "source": [ "## 2. `Components` \n", "`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." ] }, { "cell_type": "markdown", "id": "c55b963e", "metadata": {}, "source": [ "### 2.1. `Components` from scratch\n", "You can create a `Components` object from scratch by specifying all the `Component` objects." ] }, { "cell_type": "code", "execution_count": 18, "id": "3f6a8f2f", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Components([XPAO, SNH4])\n" ] } ], "source": [ "# Note that you need to provide all components as one iterable,\n", "# hence the double parentheses\n", "# This is euivalent to\n", "# cmps1_list = (XPAO, SNH4)\n", "# cmps1 = qs.Components(cmps1_list)\n", "cmps1 = qs.Components((XPAO, SNH4))\n", "cmps1" ] }, { "cell_type": "code", "execution_count": 19, "id": "6019cd4e", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Component: XPAO (phase_ref='l') at phase='l'\n", "Component-specific properties:\n", "[Others] measured_as: COD\n", " description: None\n", " particle_size: Particulate\n", " degradability: Slowly\n", " organic: True\n", " i_C: 0.37535 g C/g COD\n", " i_N: 0.087545 g N/g COD\n", " i_P: 0 g P/g COD\n", " i_K: 0 g K/g COD\n", " i_Mg: 0 g Mg/g COD\n", " i_Ca: 0 g Ca/g COD\n", " i_mass: 0.70699 g mass/g COD\n", " i_charge: 0 mol +/g COD\n", " i_COD: 1 g COD/g COD\n", " i_NOD: 0.4 g NOD/g COD\n", " f_BOD5_COD: 0\n", " f_uBOD_COD: 0\n", " f_Vmass_Totmass: 0\n", " chem_MW: 113.11\n" ] } ], "source": [ "# All the `Component` objects are stored as attributes of the `Components` object\n", "cmps1.XPAO" ] }, { "cell_type": "code", "execution_count": 20, "id": "b9be3731", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Components([XPAO, SNH4, H2O])\n" ] } ], "source": [ "# You can add more `Component` objects by appending (adding one per time)\n", "cmps1.append(H2O)\n", "cmps1.show()" ] }, { "cell_type": "code", "execution_count": 21, "id": "dc135cd8", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Components([\n", " XPAO, SNH4, H2O,\n", " Methanol, Ethanol,\n", "])\n" ] } ], "source": [ "# or extending (adding several per time)\n", "Methanol = qs.Component('Methanol', search_ID='methanol', particle_size='Soluble', degradability='Readily',\n", " organic=True)\n", "Ethanol = qs.Component('Ethanol', search_ID='ethanol', particle_size='Soluble', degradability='Readily',\n", " organic=True)\n", "cmps1.extend((Methanol, Ethanol))\n", "cmps1.show() " ] }, { "cell_type": "code", "execution_count": 22, "id": "84d77e69", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Components([\n", " XPAO, SNH4, H2O,\n", " Methanol, Ethanol,\n", "])\n" ] } ], "source": [ "# Of course, you can make a copy\n", "cmps1_copy = cmps1.copy()\n", "cmps1_copy.show()" ] }, { "cell_type": "markdown", "id": "53a3de05", "metadata": {}, "source": [ "### 2.2. `Components` from `Chemicals`\n", "Similar to how you make `Component` from `Chemical`, you can also make `Components` from `Chemicals` using the `from_chemicals` method of `Components`." ] }, { "cell_type": "markdown", "id": "c15c9d57", "metadata": {}, "source": [ "
\n", "Python Aside: function vs. method (click to expand)\n", "\n", "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](https://docs.python.org/3/tutorial/classes.html).\n", "\n", "
" ] }, { "cell_type": "code", "execution_count": 23, "id": "0baf56ec", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Components([Water, Ethanol])\n" ] } ], "source": [ "import qsdsan as qs\n", "chems = qs.Chemicals((qs.Chemical('Water'), qs.Chemical('Ethanol')))\n", "data = {'Water': {'particle_size': 'Soluble',\n", " 'degradability': 'Undegradable',\n", " 'organic': False},\n", " 'Ethanol': {'particle_size': 'Soluble',\n", " 'degradability': 'Readily',\n", " 'organic': False}}\n", "cmps2 = qs.Components.from_chemicals(chems, **data)\n", "cmps2.show()" ] }, { "cell_type": "markdown", "id": "f82109fd", "metadata": {}, "source": [ "### 2.3. Loading default `Components`\n", "You can also load the default `Components`, which are the ones that commonly used in wastewater modeling." ] }, { "cell_type": "code", "execution_count": 24, "id": "c2cdae70", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Components([\n", " S_H2, S_CH4, S_CH3OH, S_Ac, \n", " S_Prop, S_F, S_U_Inf, S_U_E, \n", " C_B_Subst, C_B_BAP, C_B_UAP, C_U_Inf, \n", " X_B_Subst, X_OHO_PHA, X_GAO_PHA, X_PAO_PHA,\n", " X_GAO_Gly, X_PAO_Gly, X_OHO, X_AOO, \n", " X_NOO, X_AMO, X_PAO, X_MEOLO, \n", " X_FO, X_ACO, X_HMO, X_PRO, \n", " X_U_Inf, X_U_OHO_E, X_U_PAO_E, X_Ig_ISS, \n", " X_MgCO3, X_CaCO3, X_MAP, X_HAP, \n", " X_HDP, X_FePO4, X_AlPO4, X_AlOH, \n", " X_FeOH, X_PAO_PP_Lo, X_PAO_PP_Hi, S_NH4, \n", " S_NO2, S_NO3, S_PO4, S_K, \n", " S_Ca, S_Mg, S_CO3, S_N2, \n", " S_O2, S_CAT, S_AN, H2O, \n", "])\n" ] } ], "source": [ "cmps3 = qs.Components.load_default(default_compile=False)\n", "cmps3" ] }, { "cell_type": "markdown", "id": "f3f81520", "metadata": {}, "source": [ "### 2.4. Loading `Components` from file\n", "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." ] }, { "cell_type": "code", "execution_count": 25, "id": "10e28a7a", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
IDdescriptionformulaparticle_sizedegradability...f_BOD5_CODf_uBOD_CODf_Vmass_TotmassCASPubChem
0S_H2Dissolved dihydrogen gasH2Dissolved gasReadily...0011333-74-0783
1S_CH4Dissolved MethaneCH4Dissolved gasReadily...00174-82-8297
2S_CH3OHMethanolCH3OHSolubleReadily...0.7170.863167-56-1887
3S_AcAcetateCH3COO(-)SolubleReadily...0.7170.863171-50-1175
4S_PropPropionateC2H5COO-SolubleReadily...0.7170.863172-03-71.05e+05
\n", "

5 rows × 22 columns

\n", "
" ], "text/plain": [ " ID description formula particle_size degradability ... f_BOD5_COD f_uBOD_COD f_Vmass_Totmass CAS PubChem\n", "0 S_H2 Dissolved dihydrogen gas H2 Dissolved gas Readily ... 0 0 1 1333-74-0 783\n", "1 S_CH4 Dissolved Methane CH4 Dissolved gas Readily ... 0 0 1 74-82-8 297\n", "2 S_CH3OH Methanol CH3OH Soluble Readily ... 0.717 0.863 1 67-56-1 887\n", "3 S_Ac Acetate CH3COO(-) Soluble Readily ... 0.717 0.863 1 71-50-1 175\n", "4 S_Prop Propionate C2H5COO- Soluble Readily ... 0.717 0.863 1 72-03-7 1.05e+05\n", "\n", "[5 rows x 22 columns]" ] }, "execution_count": 25, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import pandas as pd\n", "from qsdsan.utils import data_path\n", "import os\n", "file_path = os.path.join(data_path, '_components.tsv')\n", "df = pd.read_csv(file_path, sep='\\t', header=0)\n", "df.head()" ] }, { "cell_type": "code", "execution_count": 26, "id": "daab4b3f", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Components([\n", " S_H2, S_CH4, S_CH3OH, S_Ac, \n", " S_Prop, S_F, S_U_Inf, S_U_E, \n", " C_B_Subst, C_B_BAP, C_B_UAP, C_U_Inf, \n", " X_B_Subst, X_OHO_PHA, X_GAO_PHA, X_PAO_PHA,\n", " X_GAO_Gly, X_PAO_Gly, X_OHO, X_AOO, \n", " X_NOO, X_AMO, X_PAO, X_MEOLO, \n", " X_FO, X_ACO, X_HMO, X_PRO, \n", " X_U_Inf, X_U_OHO_E, X_U_PAO_E, X_Ig_ISS, \n", " X_MgCO3, X_CaCO3, X_MAP, X_HAP, \n", " X_HDP, X_FePO4, X_AlPO4, X_AlOH, \n", " X_FeOH, X_PAO_PP_Lo, X_PAO_PP_Hi, S_NH4, \n", " S_NO2, S_NO3, S_PO4, S_K, \n", " S_Ca, S_Mg, S_CO3, S_N2, \n", " S_O2, S_CAT, S_AN, \n", "])\n" ] } ], "source": [ "cmps4 = qs.Components.load_from_file(df)\n", "cmps4.show()" ] }, { "cell_type": "markdown", "id": "ea298f6e", "metadata": {}, "source": [ "## 3. `CompiledComponents` \n", "Finally, we need to \"compile\" `Components` into `CompiledComponents`, which can be easily done using the `compile` method.\n", "\n", "When executing the `compile` method, the package will check if all `Component` objects have the properties needed for thermodynamic calculation." ] }, { "cell_type": "code", "execution_count": 27, "id": "14b9e6b4", "metadata": { "tags": [ "raises-exception" ] }, "outputs": [ { "ename": "RuntimeError", "evalue": "XPAO is missing key thermodynamic properties (V, S, H and Cn); use the `.get_missing_properties()` to check all missing properties", "output_type": "error", "traceback": [ "\u001b[31m---------------------------------------------------------------------------\u001b[39m", "\u001b[31mRuntimeError\u001b[39m Traceback (most recent call last)", "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[27]\u001b[39m\u001b[32m, line 3\u001b[39m\n\u001b[32m 1\u001b[39m \u001b[38;5;66;03m# For example, we will receive an error if we try to compile cmps1,\u001b[39;00m\n\u001b[32m 2\u001b[39m \u001b[38;5;66;03m# because there is not enough data\u001b[39;00m\n\u001b[32m----> \u001b[39m\u001b[32m3\u001b[39m cmps1.compile()\n", "\u001b[36mFile \u001b[39m\u001b[32m~\\Documents\\Coding\\QSDsan-platform\\QSDsan\\qsdsan\\_components.py:183\u001b[39m, in \u001b[36mComponents.compile\u001b[39m\u001b[34m(self, skip_checks, ignore_inaccurate_molar_weight, adjust_MW_to_measured_as)\u001b[39m\n\u001b[32m 159\u001b[39m \u001b[38;5;250m\u001b[39m\u001b[33;03m'''\u001b[39;00m\n\u001b[32m 160\u001b[39m \u001b[33;03mCast as a :class:`CompiledComponents` object.\u001b[39;00m\n\u001b[32m 161\u001b[39m \n\u001b[32m (...)\u001b[39m\u001b[32m 180\u001b[39m \u001b[33;03m:func:`Components.default_compile` for a fuller explanation and examples.\u001b[39;00m\n\u001b[32m 181\u001b[39m \u001b[33;03m'''\u001b[39;00m\n\u001b[32m 182\u001b[39m components = \u001b[38;5;28mtuple\u001b[39m(\u001b[38;5;28mself\u001b[39m)\n\u001b[32m--> \u001b[39m\u001b[32m183\u001b[39m \u001b[30;43mprepare_chemicals\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43mcomponents\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43mskip_checks\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n\u001b[32m 184\u001b[39m \u001b[38;5;28msetattr\u001b[39m(\u001b[38;5;28mself\u001b[39m, \u001b[33m'\u001b[39m\u001b[33m__class__\u001b[39m\u001b[33m'\u001b[39m, CompiledComponents)\n\u001b[32m 186\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m: \u001b[38;5;28mself\u001b[39m._compile(components, ignore_inaccurate_molar_weight, adjust_MW_to_measured_as)\n", "\u001b[36mFile \u001b[39m\u001b[32mc:\\Users\\Yalin\\Documents\\Coding\\QSDsan-platform\\.venv\\Lib\\site-packages\\thermosteam\\_chemicals.py:43\u001b[39m, in \u001b[36mprepare\u001b[39m\u001b[34m(chemicals, skip_checks)\u001b[39m\n\u001b[32m 41\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m missing_properties: \u001b[38;5;28;01mcontinue\u001b[39;00m\n\u001b[32m 42\u001b[39m missing = utils.repr_listed_values(missing_properties)\n\u001b[32m---> \u001b[39m\u001b[32m43\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mRuntimeError\u001b[39;00m(\n\u001b[32m 44\u001b[39m \u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mchemical\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m is missing key thermodynamic properties (\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mmissing\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m); \u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 45\u001b[39m \u001b[33m\"\u001b[39m\u001b[33muse the `.get_missing_properties()` to check \u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 46\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mall missing properties\u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 47\u001b[39m )\n", "\u001b[31mRuntimeError\u001b[39m: XPAO is missing key thermodynamic properties (V, S, H and Cn); use the `.get_missing_properties()` to check all missing properties" ] } ], "source": [ "# For example, we will receive an error if we try to compile cmps1,\n", "# because there is not enough data\n", "cmps1.compile()" ] }, { "cell_type": "code", "execution_count": 28, "id": "651669ed", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['S_excess',\n", " 'H_excess',\n", " 'mu',\n", " 'kappa',\n", " 'V',\n", " 'S',\n", " 'H',\n", " 'Cn',\n", " 'Psat',\n", " 'Hvap',\n", " 'sigma',\n", " 'epsilon',\n", " 'Dortmund',\n", " 'UNIFAC',\n", " 'PSRK',\n", " 'Hf',\n", " 'LHV',\n", " 'HHV',\n", " 'combustion',\n", " 'Tt',\n", " 'Hfus',\n", " 'Tb',\n", " 'Pc',\n", " 'Pt',\n", " 'Vc',\n", " 'Sfus',\n", " 'Tm',\n", " 'Tc',\n", " 'omega',\n", " 'iscyclic_aliphatic',\n", " 'dipole',\n", " 'similarity_variable']" ] }, "execution_count": 28, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Before compiling, you can see exactly what each component still lacks with\n", "# `get_missing_properties` (here XPAO has no thermodynamic models yet)\n", "XPAO.get_missing_properties()" ] }, { "cell_type": "markdown", "id": "4ec1fad9", "metadata": {}, "source": [ "
\n", "\n", "**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.\n", "\n", "
" ] }, { "cell_type": "code", "execution_count": 29, "id": "553a8e0c", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "CompiledComponents([\n", " XPAO, SNH4, H2O,\n", " Methanol, Ethanol,\n", "])\n" ] } ], "source": [ "# We can either provide the missing data as indicated in the error method,\n", "# or we can use the `default` method and copy some data from H2O (or other more relevant components)\n", "for i in cmps1:\n", " if i is H2O:\n", " continue # \"continue\" means skip the rest of the codes and continue with the next one in the loop\n", " i.default()\n", " i.copy_models_from(H2O, names=('sigma', 'epsilon', 'kappa', 'V', 'Cn', 'mu'))\n", " \n", "# Now we can compile\n", "cmps1.compile(ignore_inaccurate_molar_weight=True)\n", "cmps1.show()" ] }, { "cell_type": "markdown", "id": "bddf6669", "metadata": {}, "source": [ "
\n", "\n", "**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.\n", "\n", "
" ] }, { "cell_type": "code", "execution_count": 30, "id": "1f2a6cb8", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "CompiledComponents([\n", " S_H2, S_CH4, S_CH3OH, S_Ac, \n", " S_Prop, S_F, S_U_Inf, S_U_E, \n", " C_B_Subst, C_B_BAP, C_B_UAP, C_U_Inf, \n", " X_B_Subst, X_OHO_PHA, X_GAO_PHA, X_PAO_PHA,\n", " X_GAO_Gly, X_PAO_Gly, X_OHO, X_AOO, \n", " X_NOO, X_AMO, X_PAO, X_MEOLO, \n", " X_FO, X_ACO, X_HMO, X_PRO, \n", " X_U_Inf, X_U_OHO_E, X_U_PAO_E, X_Ig_ISS, \n", " X_MgCO3, X_CaCO3, X_MAP, X_HAP, \n", " X_HDP, X_FePO4, X_AlPO4, X_AlOH, \n", " X_FeOH, X_PAO_PP_Lo, X_PAO_PP_Hi, S_NH4, \n", " S_NO2, S_NO3, S_PO4, S_K, \n", " S_Ca, S_Mg, S_CO3, S_N2, \n", " S_O2, S_CAT, S_AN, H2O, \n", "])\n" ] } ], "source": [ "# For the default `Components` objects, the easy way is to let it compile during loading\n", "cmps3 = qs.Components.load_default()\n", "cmps3.show()" ] }, { "cell_type": "code", "execution_count": 31, "id": "11e9507d", "metadata": { "tags": [ "raises-exception" ] }, "outputs": [ { "ename": "TypeError", "evalue": "'CompiledComponents' object is read-only", "output_type": "error", "traceback": [ "\u001b[31m---------------------------------------------------------------------------\u001b[39m", "\u001b[31mTypeError\u001b[39m Traceback (most recent call last)", "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[31]\u001b[39m\u001b[32m, line 3\u001b[39m\n\u001b[32m 1\u001b[39m \u001b[38;5;66;03m# Once compiled, you can no longer add `Component` to it\u001b[39;00m\n\u001b[32m 2\u001b[39m \u001b[38;5;66;03m# so the following code will raise an error\u001b[39;00m\n\u001b[32m----> \u001b[39m\u001b[32m3\u001b[39m cmps1.append(cmps3.S_H2)\n", "\u001b[36mFile \u001b[39m\u001b[32mc:\\Users\\Yalin\\Documents\\Coding\\QSDsan-platform\\.venv\\Lib\\site-packages\\thermosteam\\utils\\decorators\\read_only.py:14\u001b[39m, in \u001b[36mdeny\u001b[39m\u001b[34m(self, *args, **kwargs)\u001b[39m\n\u001b[32m 13\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mdeny\u001b[39m(\u001b[38;5;28mself\u001b[39m, *args, **kwargs):\n\u001b[32m---> \u001b[39m\u001b[32m14\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mTypeError\u001b[39;00m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33m'\u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mtype\u001b[39m(\u001b[38;5;28mself\u001b[39m).\u001b[34m__name__\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m'\u001b[39m\u001b[33m object is read-only\u001b[39m\u001b[33m\"\u001b[39m)\n", "\u001b[31mTypeError\u001b[39m: 'CompiledComponents' object is read-only" ] } ], "source": [ "# Once compiled, you can no longer add `Component` to it\n", "# so the following code will raise an error\n", "cmps1.append(cmps3.S_H2)" ] }, { "cell_type": "code", "execution_count": 32, "id": "03989f52", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Components([\n", " XPAO, SNH4, H2O, Methanol,\n", " Ethanol, S_H2,\n", "])\n" ] } ], "source": [ "# If you really need to add something, the best way (other than adding it before compiling)\n", "# would be to make a new `Components` containing all the `Component` objects in the `CompiledComponent`\n", "cmps4 = qs.Components(cmps1)\n", "cmps4.append(cmps3.S_H2)\n", "cmps4.show()" ] }, { "cell_type": "code", "execution_count": 33, "id": "471a04ad", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "CompiledComponents([S_CH4, S_Ac, S_F, S_NH4])\n" ] } ], "source": [ "# If you only want to work with a subset of the `CompiledComponents`, \n", "# you can hand pick the `Component` IDs you want and input it to the \n", "# `.subgroup()` method.\n", "cmps5 = cmps3.subgroup(['S_CH4', 'S_Ac', 'S_F', 'S_NH4'])\n", "cmps5.show()" ] }, { "cell_type": "markdown", "id": "5139e289", "metadata": {}, "source": [ "
\n", "\n", "**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](https://www.nature.com/articles/s41586-020-2649-2) on scientific application of `numpy`.\n", "\n", "
" ] }, { "cell_type": "code", "execution_count": 34, "id": "c195c059", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "H2O\n", "H2O\n" ] } ], "source": [ "# Once compiled, you can set synonyms of `Component` so that you can find one component by any of its synonyms\n", "# note that its ID won't change\n", "cmps1.set_synonym('H2O', 'Water')\n", "print(cmps1.H2O.ID)\n", "print(cmps1.Water.ID)" ] }, { "cell_type": "code", "execution_count": 35, "id": "f919549b", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 35, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# In fact, all of the synonyms point to the same object\n", "cmps1.H2O is cmps1.Water" ] }, { "cell_type": "markdown", "id": "10207380", "metadata": {}, "source": [ "
\n", "Python Aside: identity vs. equality (click to expand)\n", "\n", "`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:\n", "\n", "```python\n", "lst1 = [1, 2]\n", "lst2 = lst1.copy()\n", "lst1 is lst2 # False - different objects\n", "lst1 == lst2 # True - same contents\n", "id(lst1), id(lst2) # different memory addresses\n", "```\n", "\n", "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.\n", "\n", "
" ] }, { "cell_type": "markdown", "id": "d1bbfa05", "metadata": {}, "source": [ "### 3.1. Selecting components by property \n", "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)." ] }, { "cell_type": "code", "execution_count": 36, "id": "377e5a5e", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n", " 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n", " 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0])" ] }, "execution_count": 36, "metadata": {}, "output_type": "execute_result" } ], "source": [ "cmps3 = qs.Components.load_default()\n", "cmps3.g # 1 for each dissolved gas, 0 otherwise" ] }, { "cell_type": "code", "execution_count": 37, "id": "55240aca", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "('S_H2', 'S_CH4', 'S_N2', 'S_O2')" ] }, "execution_count": 37, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Translate a 1/0 array into the component IDs it selects\n", "cmps3.get_IDs_from_array(cmps3.g)" ] }, { "cell_type": "code", "execution_count": 38, "id": "0f4329b6", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n", " 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n", " 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0])" ] }, "execution_count": 38, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# ...and the other way, from a set of IDs to an aligned 1/0 array\n", "cmps3.get_array_from_IDs(('S_H2', 'S_CH4', 'S_N2', 'S_O2'))" ] }, { "cell_type": "markdown", "id": "s32md", "metadata": {}, "source": [ "### 3.2. Defining component groups \n", "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](https://qsdsan.readthedocs.io/en/latest/tutorials/3_WasteStream.html)). Define your own with `define_group`, then use the name anywhere a subgroup is accepted." ] }, { "cell_type": "code", "execution_count": 39, "id": "s32code", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['S_Ac', 'S_Prop']" ] }, "execution_count": 39, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Group components under a name, then reuse it (here, the volatile fatty acids)\n", "cmps3.define_group('VFAs', IDs=('S_Ac', 'S_Prop'))\n", "[c.ID for c in cmps3.VFAs]" ] }, { "cell_type": "markdown", "id": "nav-footer-2_component", "metadata": {}, "source": [ "\n", "\n", "---\n", "\n", "↑ Back to top\n" ] } ], "metadata": { "kernelspec": { "display_name": ".venv", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.7" }, "varInspector": { "cols": { "lenName": 16, "lenType": 16, "lenVar": 40 }, "kernels_config": { "python": { "delete_cmd_postfix": "", "delete_cmd_prefix": "del ", "library": "var_list.py", "varRefreshCmd": "print(var_dic_list())" }, "r": { "delete_cmd_postfix": ") ", "delete_cmd_prefix": "rm(", "library": "var_list.r", "varRefreshCmd": "cat(var_dic_list()) " } }, "types_to_exclude": [ "module", "function", "builtin_function_or_method", "instance", "_Feature" ], "window_display": false } }, "nbformat": 4, "nbformat_minor": 5 }