{ "cells": [ { "cell_type": "markdown", "id": "a265decc", "metadata": {}, "source": [ "# `SanUnit` (basic) \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", "\n", "- **Learning objectives.** After this tutorial, you will be able to:\n", "\n", " - Instantiate existing `SanUnit` subclasses from `qsdsan.unit_operations`\n", " - Connect units via influent and effluent streams\n", " - Inspect a unit's design and cost outputs\n", "\n", "- **Prerequisites:** [2. Component](https://qsdsan.readthedocs.io/en/latest/tutorials/2_Component.html), [3. WasteStream](https://qsdsan.readthedocs.io/en/latest/tutorials/3_WasteStream.html)\n", "\n", "- **Covered topics:**\n", "\n", " - 1. Understanding SanUnit/unit_operations/Unit\n", " - 2. Using existing SanUnit subclasses\n", "\n", "> **Companion video.** A walkthrough of this tutorial is available on [YouTube](https://youtu.be/s9zr0rCX3UY), presented by [Tori Morgan](https://github.com/vlmorgan93). Recorded against `QSDsan` v1.2.0. The concepts still apply, but if the code on screen differs from this notebook, follow the notebook.\n" ] }, { "cell_type": "markdown", "id": "cf1f07a7a9bf", "metadata": {}, "source": [ "\n", "\n", "## Setup\n", "\n", "Import `QSDsan` and confirm the installed version.\n" ] }, { "cell_type": "code", "execution_count": 1, "id": "0396645c", "metadata": { "execution": { "iopub.execute_input": "2026-05-29T15:55:19.364270Z", "iopub.status.busy": "2026-05-29T15:55:19.364270Z", "iopub.status.idle": "2026-05-29T15:55:41.669026Z", "shell.execute_reply": "2026-05-29T15:55:41.669026Z" } }, "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": "55e141ed", "metadata": {}, "source": [ "## 1. Understanding `SanUnit`/`unit_operations`/`Unit` \n", "The `SanUnit` class is used to model the design and operation of a unit operation through default or user-implemented algorithms. Upon simulation, it will generate the influent/effluent mass/energy flows and construction/operation inventories. It is the most extensible class in `qsdsan` and most directly relevant to new technologies.\n", "\n", "In this tutorial, we will focus on how to use existing `SanUnit` classes, creation of new `SanUnit` subclasses will be covered in the [next tutorial](https://qsdsan.readthedocs.io/en/latest/tutorials/5_SanUnit_advanced.html)." ] }, { "cell_type": "markdown", "id": "fda0fe65", "metadata": {}, "source": [ "### 1.1. `SanUnit` and `unit_operations`" ] }, { "cell_type": "code", "execution_count": 2, "id": "2a7bd266", "metadata": { "execution": { "iopub.execute_input": "2026-05-29T15:55:41.669026Z", "iopub.status.busy": "2026-05-29T15:55:41.669026Z", "iopub.status.idle": "2026-05-29T15:55:41.680287Z", "shell.execute_reply": "2026-05-29T15:55:41.680287Z" } }, "outputs": [ { "data": { "text/plain": [ "qsdsan._sanunit.SanUnit" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# In qsdsan's top-level diretory, you will see two entries related to SanUnit:\n", "qs.SanUnit" ] }, { "cell_type": "markdown", "id": "b92ef0f5", "metadata": {}, "source": [ "`SanUnit` is the class itself. The unit operations already built with `qsdsan` (subclasses of `SanUnit`) live in the `unit_operations` package, organized into three sub-namespaces (`bst`, `static`, `dynamic`). You access them as `qs.` or `qs.unit_operations.`, e.g. `qs.PitLatrine` or `qs.unit_operations.PitLatrine`. Similarly, `qsdsan.equipments` contains the pre-built `Equipment` classes, and `qsdsan.process_models` contains `Process` classes." ] }, { "cell_type": "markdown", "id": "cb7f340e", "metadata": {}, "source": [ "
\n", "\n", "**Note:** `qsdsan.sanunits` is a legacy alias for `qsdsan.unit_operations` (just as `qsdsan.processes` aliases `qsdsan.process_models`). Both names point to the same package, but `unit_operations` is the current, preferred name.\n", "\n", "
" ] }, { "cell_type": "markdown", "id": "2e8a1f62", "metadata": {}, "source": [ "
\n", "Python Aside: modules, packages, and naming conventions (click to expand)\n", "\n", "A few Python conventions explain the names you see:\n", "\n", "- **Module vs. package.** `qs.SanUnit` lives in a *module* (a single file, `_sanunit.py`); `qs.unit_operations` is a *package* (a directory with an `__init__.py` that wires up its contents). A *directory* is the same thing as a folder — just the more common programmatic name. The file path tells you which is which:\n", "\n", "```python\n", "qs._sanunit # .../qsdsan/_sanunit.py (a file)\n", "qs.unit_operations # .../qsdsan/unit_operations/__init__.py (a directory)\n", "```\n", "\n", "- **Dunder attributes.** Names with leading/trailing double underscores (`__path__`, `__name__`, …) are Python built-ins; e.g. `qs.unit_operations.__path__` gives the directory location. `dir()` lists these alongside the public names.\n", "- **Leading underscore = private.** A single leading underscore (as in `_sanunit.py`) marks something developers consider internal — you don't use it directly, you just use `SanUnit`.\n", "\n", "
" ] }, { "cell_type": "code", "execution_count": 3, "id": "09e7d3f9", "metadata": { "execution": { "iopub.execute_input": "2026-05-29T15:55:41.683531Z", "iopub.status.busy": "2026-05-29T15:55:41.683531Z", "iopub.status.idle": "2026-05-29T15:55:41.687323Z", "shell.execute_reply": "2026-05-29T15:55:41.687323Z" } }, "outputs": [], "source": [ "# Hit Tab after `qs.unit_operations.` to autocomplete the available units,\n", "# or list them with `dir`\n", "# dir(qs.unit_operations)" ] }, { "cell_type": "markdown", "id": "cc72dde9", "metadata": {}, "source": [ "### 1.2. `SanUnit` and `Unit`" ] }, { "cell_type": "markdown", "id": "fb927de9", "metadata": {}, "source": [ "`SanUnit` is a subclass with the `Unit` class in `biosteam`, like we mentioned in the previous tutorials, this means it inherits the attributes of the `Unit` class while having some new features (e.g., enables dynamic simulation, supports comprehensive LCA)." ] }, { "cell_type": "markdown", "id": "e7309f73", "metadata": {}, "source": [ "## 2. Using existing `SanUnit` subclasses \n", "In Python terms, the \"use\" here means creating instances of a `SanUnit` subclass." ] }, { "cell_type": "markdown", "id": "cf993cba", "metadata": {}, "source": [ "### 2.1. Instance initialization (creation) and simulation" ] }, { "cell_type": "code", "execution_count": 4, "id": "fab56b51", "metadata": { "execution": { "iopub.execute_input": "2026-05-29T15:55:41.687323Z", "iopub.status.busy": "2026-05-29T15:55:41.687323Z", "iopub.status.idle": "2026-05-29T15:55:41.957522Z", "shell.execute_reply": "2026-05-29T15:55:41.957522Z" } }, "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": [ "# Like always, we need to firstly tell `qsdsan` what components we will be working with,\n", "# for demo purpose we will just use the default ones\n", "cmps = qs.Components.load_default()\n", "qs.set_thermo(cmps)\n", "cmps.show()" ] }, { "cell_type": "markdown", "id": "84f8dc5a", "metadata": {}, "source": [ "Let's firstly try using a `Mixer`, it is one of the most basic units, it takes N numbers of influents and the effluent is the mixture of all influents." ] }, { "cell_type": "code", "execution_count": 5, "id": "db4246a9", "metadata": { "execution": { "iopub.execute_input": "2026-05-29T15:55:41.957522Z", "iopub.status.busy": "2026-05-29T15:55:41.957522Z", "iopub.status.idle": "2026-05-29T15:55:42.009323Z", "shell.execute_reply": "2026-05-29T15:55:42.008177Z" } }, "outputs": [], "source": [ "# Make three random influents, I'm deliberately using different ways to make these streams\n", "# as a recap previous tutorials\n", "\n", "# Method 1: by directly providing the flow rates of select components\n", "ins1 = qs.WasteStream('ins1', H2O=100)\n", "\n", "# Method 2: using `copy` and adjust flow rates later\n", "ins2 = ins1.copy('ins2')\n", "ins2.imol['X_GAO_Gly'] = ins2.imol['X_GAO_PHA'] = 0.01\n", "\n", "# Method 3: using default models\n", "ins3 = qs.WasteStream.codstates_inf_model('ins3', flow_tot=50)" ] }, { "cell_type": "code", "execution_count": 6, "id": "710630b3", "metadata": { "execution": { "iopub.execute_input": "2026-05-29T15:55:42.009323Z", "iopub.status.busy": "2026-05-29T15:55:42.009323Z", "iopub.status.idle": "2026-05-29T15:55:42.092108Z", "shell.execute_reply": "2026-05-29T15:55:42.092108Z" } }, "outputs": [], "source": [ "# Use a shorthand to make our lives easier\n", "su = qs.unit_operations\n", "\n", "# This is the actual line used to initialize the instance,\n", "# and we can pass the influents through the `ins` argument\n", "M1 = su.Mixer(ins=(ins1, ins2, ins3))" ] }, { "cell_type": "code", "execution_count": 7, "id": "f897beee", "metadata": { "execution": { "iopub.execute_input": "2026-05-29T15:55:42.096657Z", "iopub.status.busy": "2026-05-29T15:55:42.092108Z", "iopub.status.idle": "2026-05-29T15:55:43.592620Z", "shell.execute_reply": "2026-05-29T15:55:43.591060Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Mixer: M1\n", "ins...\n", "[0] ins1\n", "phase: 'l', T: 298.15 K, P: 101325 Pa\n", "flow: 1e+05 g/hr H2O\n", " WasteStream-specific properties:\n", " pH : 7.0\n", " Alkalinity : 2.5 mmol/L\n", "[1] ins2\n", "phase: 'l', T: 298.15 K, P: 101325 Pa\n", "flow (g/hr): X_GAO_PHA 10\n", " X_GAO_Gly 10\n", " H2O 1e+05\n", " WasteStream-specific properties:\n", " pH : 7.0\n", " Alkalinity : 2.5 mmol/L\n", " COD : 199.4 mg/L\n", " BOD : 115.6 mg/L\n", " TC : 70.6 mg/L\n", " TOC : 70.6 mg/L\n", " TSS : 143.7 mg/L\n", "[2] ins3\n", "phase: 'l', T: 298.15 K, P: 101325 Pa\n", "flow (g/hr): S_F 4.3\n", " S_U_Inf 1.07\n", " C_B_Subst 2\n", " X_B_Subst 11.3\n", " X_U_Inf 2.79\n", " X_Ig_ISS 2.62\n", " S_NH4 1.25\n", " S_PO4 0.4\n", " S_K 1.4\n", " S_Ca 7\n", " S_Mg 2.5\n", " S_CO3 6\n", " S_N2 0.9\n", " S_CAT 0.15\n", " S_AN 0.6\n", " ... 4.98e+04\n", " WasteStream-specific properties:\n", " pH : 7.0\n", " Alkalinity : 10.0 mmol/L\n", " COD : 430.0 mg/L\n", " BOD : 221.8 mg/L\n", " TC : 265.0 mg/L\n", " TOC : 137.6 mg/L\n", " TN : 40.0 mg/L\n", " TP : 10.0 mg/L\n", " TK : 28.0 mg/L\n", " TSS : 209.3 mg/L\n", "outs...\n", "[0] ws1\n", "phase: 'l', T: 298.15 K, P: 101325 Pa\n", "flow: 0\n", " WasteStream-specific properties: None for empty waste streams\n" ] } ], "source": [ "# Like many other classes, there is a `show` method\n", "M1.show()" ] }, { "cell_type": "markdown", "id": "dad01856", "metadata": {}, "source": [ "**Hold on — why is the effluent empty?**" ] }, { "cell_type": "code", "execution_count": 8, "id": "74b85604", "metadata": { "execution": { "iopub.execute_input": "2026-05-29T15:55:43.595194Z", "iopub.status.busy": "2026-05-29T15:55:43.595194Z", "iopub.status.idle": "2026-05-29T15:55:45.532350Z", "shell.execute_reply": "2026-05-29T15:55:45.532350Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Mixer: M1\n", "ins...\n", "[0] ins1\n", "phase: 'l', T: 298.15 K, P: 101325 Pa\n", "flow: 1e+05 g/hr H2O\n", " WasteStream-specific properties:\n", " pH : 7.0\n", " Alkalinity : 2.5 mmol/L\n", "[1] ins2\n", "phase: 'l', T: 298.15 K, P: 101325 Pa\n", "flow (g/hr): X_GAO_PHA 10\n", " X_GAO_Gly 10\n", " H2O 1e+05\n", " WasteStream-specific properties:\n", " pH : 7.0\n", " Alkalinity : 2.5 mmol/L\n", " COD : 199.4 mg/L\n", " BOD : 115.6 mg/L\n", " TC : 70.6 mg/L\n", " TOC : 70.6 mg/L\n", " TSS : 143.7 mg/L\n", "[2] ins3\n", "phase: 'l', T: 298.15 K, P: 101325 Pa\n", "flow (g/hr): S_F 4.3\n", " S_U_Inf 1.07\n", " C_B_Subst 2\n", " X_B_Subst 11.3\n", " X_U_Inf 2.79\n", " X_Ig_ISS 2.62\n", " S_NH4 1.25\n", " S_PO4 0.4\n", " S_K 1.4\n", " S_Ca 7\n", " S_Mg 2.5\n", " S_CO3 6\n", " S_N2 0.9\n", " S_CAT 0.15\n", " S_AN 0.6\n", " ... 4.98e+04\n", " WasteStream-specific properties:\n", " pH : 7.0\n", " Alkalinity : 10.0 mmol/L\n", " COD : 430.0 mg/L\n", " BOD : 221.8 mg/L\n", " TC : 265.0 mg/L\n", " TOC : 137.6 mg/L\n", " TN : 40.0 mg/L\n", " TP : 10.0 mg/L\n", " TK : 28.0 mg/L\n", " TSS : 209.3 mg/L\n", "outs...\n", "[0] ws1\n", "phase: 'l', T: 298.15 K, P: 101325 Pa\n", "flow (g/hr): S_F 4.3\n", " S_U_Inf 1.07\n", " C_B_Subst 2\n", " X_B_Subst 11.3\n", " X_GAO_PHA 10\n", " X_GAO_Gly 10\n", " X_U_Inf 2.79\n", " X_Ig_ISS 2.62\n", " S_NH4 1.25\n", " S_PO4 0.4\n", " S_K 1.4\n", " S_Ca 7\n", " S_Mg 2.5\n", " S_CO3 6\n", " S_N2 0.9\n", " ... 2.5e+05\n", " WasteStream-specific properties:\n", " pH : 7.0\n", " Alkalinity : 4.0 mmol/L\n", " COD : 165.6 mg/L\n", " BOD : 90.5 mg/L\n", " TC : 81.1 mg/L\n", " TOC : 55.7 mg/L\n", " TN : 8.0 mg/L\n", " TP : 2.0 mg/L\n", " TK : 5.6 mg/L\n", " TSS : 99.3 mg/L\n" ] } ], "source": [ "# Well, we have to simulate the unit first\n", "M1.simulate()\n", "M1.show()" ] }, { "cell_type": "markdown", "id": "92285758", "metadata": {}, "source": [ "Aha! Now we are seeing the effluent being simulated." ] }, { "cell_type": "code", "execution_count": 9, "id": "47750bf1", "metadata": { "execution": { "iopub.execute_input": "2026-05-29T15:55:45.536262Z", "iopub.status.busy": "2026-05-29T15:55:45.536262Z", "iopub.status.idle": "2026-05-29T15:55:45.540991Z", "shell.execute_reply": "2026-05-29T15:55:45.540991Z" } }, "outputs": [], "source": [ "# If you recall the `mix_from` method of `WasteStream`,\n", "# effluent of the `Mixer` is actually generated by it\n", "ws4 = qs.WasteStream('ws4')\n", "ws4.mix_from(M1.ins)" ] }, { "cell_type": "code", "execution_count": 10, "id": "0f059b5c", "metadata": { "execution": { "iopub.execute_input": "2026-05-29T15:55:45.544101Z", "iopub.status.busy": "2026-05-29T15:55:45.543046Z", "iopub.status.idle": "2026-05-29T15:55:45.547631Z", "shell.execute_reply": "2026-05-29T15:55:45.547631Z" } }, "outputs": [ { "data": { "text/plain": [ "sparse([ True, True, True, True, True, True, True, True, True,\n", " True, True, True, True, True, True, True, True, True,\n", " True, True, True, True, True, True, True, True, True,\n", " True, True, True, True, True, True, True, True, True,\n", " True, True, True, True, True, True, True, True, True,\n", " True, True, True, True, True, True, True, True, True,\n", " True, True])" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# We can check if the flow rate of ws4 is the same as the effluent of the mixer M1\n", "ws4.mass == M1.outs[0].mass" ] }, { "cell_type": "code", "execution_count": 11, "id": "6a89de4e", "metadata": { "execution": { "iopub.execute_input": "2026-05-29T15:55:45.550728Z", "iopub.status.busy": "2026-05-29T15:55:45.550728Z", "iopub.status.idle": "2026-05-29T15:55:51.718804Z", "shell.execute_reply": "2026-05-29T15:55:51.718804Z" } }, "outputs": [ { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "\n", "111678877565:e->111671246056:w\n", "\n", "\n", " ws1\n", "\n", "\n", "\n", "\n", "\n", "111671245216:e->111678877565:c\n", "\n", "\n", " ins1\n", "\n", "\n", "\n", "\n", "\n", "111671245136:e->111678877565:c\n", "\n", "\n", " ins2\n", "\n", "\n", "\n", "\n", "\n", "111671245176:e->111678877565:c\n", "\n", "\n", " ins3\n", "\n", "\n", "\n", "\n", "\n", "111678877565\n", "\n", "\n", "M1\n", "Mixer\n", "\n", "\n", "\n", "\n", "\n", "111671245216\n", "\n", "\n", "\n", "\n", "111671246056\n", "\n", "\n", "\n", "\n", "111671245136\n", "\n", "\n", "\n", "\n", "111671245176\n", "\n", "\n", "\n", "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# As you can see in the above examples,\n", "# I use `ins` and `outs` to set/retrieve the influents and effluents of a unit\n", "# Alternatively, there is the `diagram` method for a more intuitive look\n", "M1.diagram()" ] }, { "cell_type": "markdown", "id": "su4md1", "metadata": {}, "source": [ "
\n", "\n", "**Tip:** `init_with` sets which stream class each port uses: `'WasteStream'` (default, or `'ws'`), `'SanStream'` (`'ss'`), or `'Stream'` (`'s'`). Pass a single class name to apply it to every port, or a `dict` to set ports individually, which is handy when, say, the influents are `WasteStream` but a gas stream should be a plain `Stream`. Dict keys are `ins`/`outs` plus the port number (e.g., `ins0`, `outs-1`), a range (e.g., `ins2:4`), or `else` for any ports you did not name. Only `WasteStream` ports carry the wastewater properties from [3. WasteStream](https://qsdsan.readthedocs.io/en/latest/tutorials/3_WasteStream.html), while `Stream` ports are plain process streams.\n", "\n", "
" ] }, { "cell_type": "code", "execution_count": 12, "id": "su4code1", "metadata": { "execution": { "iopub.execute_input": "2026-05-29T15:55:51.722252Z", "iopub.status.busy": "2026-05-29T15:55:51.722252Z", "iopub.status.idle": "2026-05-29T15:55:51.728107Z", "shell.execute_reply": "2026-05-29T15:55:51.728107Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "ins0: SanStream | outs0: SanStream\n" ] } ], "source": [ "# A single class name applies to every port\n", "M_ss = su.Mixer('M_ss', ins=('a', 'b'), outs='c', init_with='SanStream')\n", "print('ins0:', type(M_ss.ins[0]).__name__, '| outs0:', type(M_ss.outs[0]).__name__)" ] }, { "cell_type": "code", "execution_count": 13, "id": "su4code2", "metadata": { "execution": { "iopub.execute_input": "2026-05-29T15:55:51.729339Z", "iopub.status.busy": "2026-05-29T15:55:51.729339Z", "iopub.status.idle": "2026-05-29T15:55:51.767290Z", "shell.execute_reply": "2026-05-29T15:55:51.767290Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "ins0: Stream | ins1: WasteStream | outs0: WasteStream\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "c:\\Users\\Yalin\\Documents\\Coding\\QSDsan-platform\\.venv\\Lib\\site-packages\\thermosteam\\_stream.py:407: RuntimeWarning: has been replaced in registry\n", " self._register(ID)\n", "c:\\Users\\Yalin\\Documents\\Coding\\QSDsan-platform\\.venv\\Lib\\site-packages\\thermosteam\\_stream.py:407: RuntimeWarning: has been replaced in registry\n", " self._register(ID)\n", "c:\\Users\\Yalin\\Documents\\Coding\\QSDsan-platform\\.venv\\Lib\\site-packages\\thermosteam\\_stream.py:407: RuntimeWarning: has been replaced in registry\n", " self._register(ID)\n" ] } ], "source": [ "# A dict sets ports individually; 'else' covers the rest\n", "M_mix = su.Mixer('M_mix', ins=('a', 'b'), outs='c',\n", " init_with={'ins0': 'Stream', 'else': 'WasteStream'})\n", "print('ins0:', type(M_mix.ins[0]).__name__,\n", " '| ins1:', type(M_mix.ins[1]).__name__,\n", " '| outs0:', type(M_mix.outs[0]).__name__)" ] }, { "cell_type": "markdown", "id": "2f5bf2ad", "metadata": {}, "source": "
\n\n**Tip:** By default, `diagram()` renders an inline picture of the flowsheet. You can pass a different `format` (notably `format='html'` for an interactive diagram with hover-able stream and unit information) or save it to a file with the `file` kwarg (e.g., `M1.diagram(file='M1', format='png')`). If `diagram()` produces nothing, you likely don't have the `graphviz` package installed (it renders the diagrams); see QSDsan's [graphviz FAQ](https://qsdsan.readthedocs.io/en/latest/faq/errors.html#graphviz-installation).\n\n
" }, { "cell_type": "markdown", "id": "208de147", "metadata": {}, "source": [ "### 2.2. Retrieving design and cost" ] }, { "cell_type": "code", "execution_count": 14, "id": "cc3938f0", "metadata": { "execution": { "iopub.execute_input": "2026-05-29T15:55:51.767290Z", "iopub.status.busy": "2026-05-29T15:55:51.767290Z", "iopub.status.idle": "2026-05-29T15:55:53.322769Z", "shell.execute_reply": "2026-05-29T15:55:53.320378Z" } }, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "C:\\Users\\Yalin\\AppData\\Local\\Temp\\ipykernel_68680\\3311111030.py:4: RuntimeWarning: undocked inlet ins1 from M1; ins1 is now docked at M2\n", " M2 = su.MixTank('M2', ins=(ins1, ins2, ins3), outs='M2out', # init_with='WasteStream',\n", "C:\\Users\\Yalin\\AppData\\Local\\Temp\\ipykernel_68680\\3311111030.py:4: RuntimeWarning: undocked inlet ins2 from M1; ins2 is now docked at M2\n", " M2 = su.MixTank('M2', ins=(ins1, ins2, ins3), outs='M2out', # init_with='WasteStream',\n", "C:\\Users\\Yalin\\AppData\\Local\\Temp\\ipykernel_68680\\3311111030.py:4: RuntimeWarning: undocked inlet ins3 from M1; ins3 is now docked at M2\n", " M2 = su.MixTank('M2', ins=(ins1, ins2, ins3), outs='M2out', # init_with='WasteStream',\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "MixTank: M2\n", "ins...\n", "[0] ins1\n", "phase: 'l', T: 298.15 K, P: 101325 Pa\n", "flow: 1e+05 g/hr H2O\n", " WasteStream-specific properties:\n", " pH : 7.0\n", " Alkalinity : 2.5 mmol/L\n", "[1] ins2\n", "phase: 'l', T: 298.15 K, P: 101325 Pa\n", "flow (g/hr): X_GAO_PHA 10\n", " X_GAO_Gly 10\n", " H2O 1e+05\n", " WasteStream-specific properties:\n", " pH : 7.0\n", " Alkalinity : 2.5 mmol/L\n", " COD : 199.4 mg/L\n", " BOD : 115.6 mg/L\n", " TC : 70.6 mg/L\n", " TOC : 70.6 mg/L\n", " TSS : 143.7 mg/L\n", "[2] ins3\n", "phase: 'l', T: 298.15 K, P: 101325 Pa\n", "flow (g/hr): S_F 4.3\n", " S_U_Inf 1.07\n", " C_B_Subst 2\n", " X_B_Subst 11.3\n", " X_U_Inf 2.79\n", " X_Ig_ISS 2.62\n", " S_NH4 1.25\n", " S_PO4 0.4\n", " S_K 1.4\n", " S_Ca 7\n", " S_Mg 2.5\n", " S_CO3 6\n", " S_N2 0.9\n", " S_CAT 0.15\n", " S_AN 0.6\n", " ... 4.98e+04\n", " WasteStream-specific properties:\n", " pH : 7.0\n", " Alkalinity : 10.0 mmol/L\n", " COD : 430.0 mg/L\n", " BOD : 221.8 mg/L\n", " TC : 265.0 mg/L\n", " TOC : 137.6 mg/L\n", " TN : 40.0 mg/L\n", " TP : 10.0 mg/L\n", " TK : 28.0 mg/L\n", " TSS : 209.3 mg/L\n", "outs...\n", "[0] M2out\n", "phase: 'l', T: 298.15 K, P: 101325 Pa\n", "flow: 0\n", " WasteStream-specific properties: None for empty waste streams\n" ] } ], "source": [ "# Note that `Mixer` does nothing other than mix the influents,\n", "# let's using another example\n", "qs.set_thermo(cmps) # here we need to reset the `cmps` since I introduced the `biosteam` environment\n", "M2 = su.MixTank('M2', ins=(ins1, ins2, ins3), outs='M2out', # init_with='WasteStream',\n", " tau=1, kW_per_m3=1)\n", "M2.show()" ] }, { "cell_type": "markdown", "id": "23de76be", "metadata": {}, "source": [ "Two things to take note of in the example above:\n", "- By setting `outs='M2out'`, I set the ID of the effluent to be `M2out`\n", " - You can also make a stream ahead of time and set the effluent to that stream (e.g., `outs=qs.WasteStream('M2out')`\n", " - If the unit has multiple effluents, then you'll want to use an Iterable (e.g., tuple, list), e.g., `outs=['M2out1', qs.WasteStream('M2out2')]`\n", " - This is applicable to `ins` as well\n", "- You will see warnings about streams being undocked from the previous unit and docked at the new unit, this is because we set the `ins` of `M2` to be the same as the `ins` of `M1`. Since one stream can only go to one unit, these streams will be taken away from `M1` and connect to `M2`" ] }, { "cell_type": "code", "execution_count": 15, "id": "401cb26d", "metadata": { "execution": { "iopub.execute_input": "2026-05-29T15:55:53.322769Z", "iopub.status.busy": "2026-05-29T15:55:53.322769Z", "iopub.status.idle": "2026-05-29T15:55:53.335002Z", "shell.execute_reply": "2026-05-29T15:55:53.335002Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Mix tank Units M2\n", "Electricity Power kW 0.313\n", " Cost USD/hr 0.0245\n", "Design Residence time hr 1\n", " Total volume m^3 0.313\n", "Purchase cost Tank USD 7.09e+03\n", "Total purchase cost USD 7.09e+03\n", "Utility cost USD/hr 0.0245\n" ] } ], "source": [ "# Because `M2` is a `MixTank`, we can look at its design\n", "M2.simulate() # runs _run then _summary (_design + _cost); see tutorial 5 §1.1\n", "print(M2.results()) # you can see the design and capital/power cost" ] }, { "cell_type": "code", "execution_count": 16, "id": "59098df1", "metadata": { "execution": { "iopub.execute_input": "2026-05-29T15:55:53.339532Z", "iopub.status.busy": "2026-05-29T15:55:53.335002Z", "iopub.status.idle": "2026-05-29T15:55:53.645190Z", "shell.execute_reply": "2026-05-29T15:55:53.645190Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Heat exchanger Units H1\n", "Low pressure steam Duty kJ/hr 3.18e+03\n", " Flow kmol/hr 0.0822\n", " Cost USD/hr 0.0196\n", "Design Area ft^2 0.0505\n", " Overall heat transfer coefficient kW/m^2/K 0.5\n", " Log-mean temperature difference K 377\n", " Fouling correction factor 1\n", " Operating pressure psi 50\n", " Total tube length ft 0.116\n", " Inner pipe weight kg 0.12\n", " Outer pipe weight kg 0.193\n", " Total steel weight kg 0.313\n", "Purchase cost Double pipe USD 65.3\n", "Total purchase cost USD 65.3\n", "Utility cost USD/hr 0.0196\n" ] } ], "source": [ "# If there is utility usage, it will be shown in the results as well\n", "ws5 = qs.WasteStream('ws5', H2O=10, T=20)\n", "H1 = su.HXutility(ins=ws5, T=50)\n", "H1.simulate()\n", "print(H1.results())" ] }, { "cell_type": "code", "execution_count": 17, "id": "6c71ac75", "metadata": { "execution": { "iopub.execute_input": "2026-05-29T15:55:53.645190Z", "iopub.status.busy": "2026-05-29T15:55:53.645190Z", "iopub.status.idle": "2026-05-29T15:55:53.651627Z", "shell.execute_reply": "2026-05-29T15:55:53.651627Z" } }, "outputs": [ { "data": { "text/plain": [ "7093.933003892887" ] }, "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# You can also retrieve information such as\n", "M2.purchase_cost # this is the sum" ] }, { "cell_type": "code", "execution_count": 18, "id": "ea4f99a7", "metadata": { "execution": { "iopub.execute_input": "2026-05-29T15:55:53.655641Z", "iopub.status.busy": "2026-05-29T15:55:53.654639Z", "iopub.status.idle": "2026-05-29T15:55:53.659651Z", "shell.execute_reply": "2026-05-29T15:55:53.659651Z" } }, "outputs": [ { "data": { "text/plain": [ "{'Tank': 7093.933003892887}" ] }, "execution_count": 18, "metadata": {}, "output_type": "execute_result" } ], "source": [ "M2.purchase_costs # this is a `dict` contains all the entries" ] }, { "cell_type": "code", "execution_count": 19, "id": "91be16c7", "metadata": { "execution": { "iopub.execute_input": "2026-05-29T15:55:53.661941Z", "iopub.status.busy": "2026-05-29T15:55:53.661941Z", "iopub.status.idle": "2026-05-29T15:55:53.665427Z", "shell.execute_reply": "2026-05-29T15:55:53.665427Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "M2 contains the following cost items:\n", " Tank costs 11705\n", "The sum of all these cost items is 11705.\n" ] } ], "source": [ "# (this unit only has one cost item, but the pattern generalizes)\n", "print(f'{M2.ID} contains the following cost items:')\n", "for item_name, item_cost in M2.installed_costs.items():\n", " print(f' {item_name} costs {item_cost:.0f}')\n", "print(f'The sum of all these cost items is {M2.installed_cost:.0f}.')" ] }, { "cell_type": "markdown", "id": "3a33ca98", "metadata": {}, "source": [ "
\n", "\n", "**Baseline, purchase, and installed cost.** Following the bare-module method (Seider et al.), BioSTEAM derives both costs from a **baseline purchase cost** (`baseline_purchase_costs`) using four per-item factors, each defaulting to 1:\n", "\n", "- `F_D` (design), `F_P` (pressure), and `F_M` (material) give the **purchase cost**: `purchase = baseline × F_D × F_P × F_M`.\n", "- `F_BM` (bare-module) rolls in installation, piping, instrumentation, etc. to give the **installed cost**: `installed = baseline × (F_BM + F_D × F_P × F_M - 1)`.\n", "\n", "Each factor lives in its own dict (`F_BM`, `F_D`, `F_P`, `F_M`). For the `MixTank` above, only `F_M` (2.0) and `F_BM` (2.3) differ from 1, so its installed cost is `baseline × (2.3 + 2.0 - 1)`, not `purchase × F_BM`.\n", "\n", "
" ] }, { "cell_type": "code", "execution_count": 20, "id": "d5d0b2ee", "metadata": { "execution": { "iopub.execute_input": "2026-05-29T15:55:53.668442Z", "iopub.status.busy": "2026-05-29T15:55:53.667437Z", "iopub.status.idle": "2026-05-29T15:55:53.673502Z", "shell.execute_reply": "2026-05-29T15:55:53.672490Z" } }, "outputs": [ { "data": { "text/plain": [ "{'Tank': 2.3}" ] }, "execution_count": 20, "metadata": {}, "output_type": "execute_result" } ], "source": [ "M2.F_BM # bare-module factor for each cost item" ] }, { "cell_type": "markdown", "id": "c55a074d", "metadata": {}, "source": [ "
\n", "\n", "**Operating cost beyond utilities.** The `Additional OPEX` row in `results()` is `unit.add_OPEX` — costs such as chemicals or labor, given as a float or a `dict` of USD/hr. The fraction of time a unit actually runs is `unit.uptime_ratio` (0–1), which scales these operating costs in TEA.\n", "\n", "
" ] }, { "cell_type": "markdown", "id": "4891ee4a", "metadata": {}, "source": [ "
\n", "\n", "**Tip:** Some units may require specific components or certain number of influents, for example\n", "\n", "
" ] }, { "cell_type": "code", "execution_count": 21, "id": "a4a731ea", "metadata": { "execution": { "iopub.execute_input": "2026-05-29T15:55:53.675040Z", "iopub.status.busy": "2026-05-29T15:55:53.675040Z", "iopub.status.idle": "2026-05-29T15:55:55.459912Z", "shell.execute_reply": "2026-05-29T15:55:55.458684Z" }, "tags": [ "raises-exception" ] }, "outputs": [ { "ename": "UndefinedComponent", "evalue": "'NH3'", "output_type": "error", "traceback": [ "\u001b[31m---------------------------------------------------------------------------\u001b[39m", "\u001b[31mKeyError\u001b[39m Traceback (most recent call last)", "\u001b[36mFile \u001b[39m\u001b[32mc:\\Users\\Yalin\\Documents\\Coding\\QSDsan-platform\\.venv\\Lib\\site-packages\\thermosteam\\_chemicals.py:1283\u001b[39m, in \u001b[36mCompiledChemicals._get_index_and_kind\u001b[39m\u001b[34m(self, key)\u001b[39m\n\u001b[32m 1282\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m key.\u001b[34m__hash__\u001b[39m \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m: key = \u001b[38;5;28mtuple\u001b[39m(key)\n\u001b[32m-> \u001b[39m\u001b[32m1283\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[30;43mindex_cache\u001b[39;49m\u001b[30;43m[\u001b[39;49m\u001b[30;43mkey\u001b[39;49m\u001b[30;43m]\u001b[39;49m\n\u001b[32m 1284\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mKeyError\u001b[39;00m:\n", "\u001b[31mKeyError\u001b[39m: 'NH3'", "\nDuring handling of the above exception, another exception occurred:\n", "\u001b[31mKeyError\u001b[39m Traceback (most recent call last)", "\u001b[36mFile \u001b[39m\u001b[32m~\\Documents\\Coding\\QSDsan-platform\\QSDsan\\qsdsan\\_components.py:751\u001b[39m, in \u001b[36mCompiledComponents.index\u001b[39m\u001b[34m(self, ID)\u001b[39m\n\u001b[32m 750\u001b[39m \u001b[38;5;250m\u001b[39m\u001b[33;03m'''Return index of specified component.'''\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m751\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m: \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43m_index\u001b[39;49m\u001b[30;43m[\u001b[39;49m\u001b[30;43mID\u001b[39;49m\u001b[30;43m]\u001b[39;49m\n\u001b[32m 752\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mKeyError\u001b[39;00m:\n", "\u001b[31mKeyError\u001b[39m: 'NH3'", "\nDuring handling of the above exception, another exception occurred:\n", "\u001b[31mUndefinedComponent\u001b[39m Traceback (most recent call last)", "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[21]\u001b[39m\u001b[32m, line 3\u001b[39m\n\u001b[32m 1\u001b[39m \u001b[38;5;66;03m# You'll receive an `UndefinedComponent` error\u001b[39;00m\n\u001b[32m 2\u001b[39m bad_su = su.Excretion()\n\u001b[32m----> \u001b[39m\u001b[32m3\u001b[39m bad_su.simulate()\n", "\u001b[36mFile \u001b[39m\u001b[32m~\\Documents\\Coding\\QSDsan-platform\\QSDsan\\qsdsan\\_sanunit.py:436\u001b[39m, in \u001b[36mSanUnit.simulate\u001b[39m\u001b[34m(self, run, design_kwargs, cost_kwargs, **kwargs)\u001b[39m\n\u001b[32m 408\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34msimulate\u001b[39m(\u001b[38;5;28mself\u001b[39m, run=\u001b[38;5;28;01mTrue\u001b[39;00m, design_kwargs={}, cost_kwargs={}, **kwargs):\n\u001b[32m 409\u001b[39m \u001b[38;5;250m \u001b[39m\u001b[33;03m'''\u001b[39;00m\n\u001b[32m 410\u001b[39m \u001b[33;03m Converge mass and energy flows, design, and cost the unit.\u001b[39;00m\n\u001b[32m 411\u001b[39m \n\u001b[32m (...)\u001b[39m\u001b[32m 434\u001b[39m \u001b[33;03m `scipy.integrate.solve_ivp `_\u001b[39;00m\n\u001b[32m 435\u001b[39m \u001b[33;03m '''\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m436\u001b[39m \u001b[30;43msuper\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43m)\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43msimulate\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43mrun\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43mrun\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43mdesign_kwargs\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43mdesign_kwargs\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43mcost_kwargs\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43mcost_kwargs\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n\u001b[32m 437\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m.isdynamic:\n\u001b[32m 438\u001b[39m sys = \u001b[38;5;28mself\u001b[39m._mock_dyn_sys\n", "\u001b[36mFile \u001b[39m\u001b[32mc:\\Users\\Yalin\\Documents\\Coding\\QSDsan-platform\\.venv\\Lib\\site-packages\\biosteam\\_unit.py:1464\u001b[39m, in \u001b[36mUnit.simulate\u001b[39m\u001b[34m(self, run, design_kwargs, cost_kwargs)\u001b[39m\n\u001b[32m 1462\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m ps \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m._specifications: ps.compile_path(\u001b[38;5;28mself\u001b[39m)\n\u001b[32m 1463\u001b[39m \u001b[38;5;28mself\u001b[39m._load_stream_links()\n\u001b[32m-> \u001b[39m\u001b[32m1464\u001b[39m \u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mrun\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n\u001b[32m 1465\u001b[39m \u001b[38;5;28mself\u001b[39m._summary(design_kwargs, cost_kwargs)\n", "\u001b[36mFile \u001b[39m\u001b[32mc:\\Users\\Yalin\\Documents\\Coding\\QSDsan-platform\\.venv\\Lib\\site-packages\\biosteam\\_unit.py:48\u001b[39m, in \u001b[36mphenomena_based_run\u001b[39m\u001b[34m(self)\u001b[39m\n\u001b[32m 46\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mphenomena_based_run\u001b[39m(\u001b[38;5;28mself\u001b[39m):\n\u001b[32m 47\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m (\u001b[38;5;28mself\u001b[39m._recycle_system \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;28mself\u001b[39m._system.algorithm == \u001b[33m'\u001b[39m\u001b[33mPhenomena based\u001b[39m\u001b[33m'\u001b[39m):\n\u001b[32m---> \u001b[39m\u001b[32m48\u001b[39m \u001b[30;43mUnit\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mrun\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43mself\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n\u001b[32m 49\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m\n\u001b[32m 50\u001b[39m ins = \u001b[38;5;28mself\u001b[39m.ins\n", "\u001b[36mFile \u001b[39m\u001b[32mc:\\Users\\Yalin\\Documents\\Coding\\QSDsan-platform\\.venv\\Lib\\site-packages\\thermosteam\\network.py:1560\u001b[39m, in \u001b[36mAbstractUnit.run\u001b[39m\u001b[34m(self)\u001b[39m\n\u001b[32m 1558\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m i \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m._outs: i.empty()\n\u001b[32m 1559\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m\n\u001b[32m-> \u001b[39m\u001b[32m1560\u001b[39m \u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43m_run_with_specifications\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n", "\u001b[36mFile \u001b[39m\u001b[32mc:\\Users\\Yalin\\Documents\\Coding\\QSDsan-platform\\.venv\\Lib\\site-packages\\thermosteam\\network.py:1576\u001b[39m, in \u001b[36mAbstractUnit._run_with_specifications\u001b[39m\u001b[34m(self)\u001b[39m\n\u001b[32m 1574\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m.run_after_specifications: \u001b[38;5;28mself\u001b[39m._run()\n\u001b[32m 1575\u001b[39m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[32m-> \u001b[39m\u001b[32m1576\u001b[39m \u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43m_run\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n", "\u001b[36mFile \u001b[39m\u001b[32m~\\Documents\\Coding\\QSDsan-platform\\QSDsan\\qsdsan\\unit_operations\\static\\_excretion.py:85\u001b[39m, in \u001b[36mExcretion._run\u001b[39m\u001b[34m(self)\u001b[39m\n\u001b[32m 81\u001b[39m factor = \u001b[32m24\u001b[39m * \u001b[32m1e3\u001b[39m \u001b[38;5;66;03m# from g per person per day to kg per hour\u001b[39;00m\n\u001b[32m 83\u001b[39m ur_N = (\u001b[38;5;28mself\u001b[39m.p_veg+\u001b[38;5;28mself\u001b[39m.p_anim)/factor*\u001b[38;5;28mself\u001b[39m.N_prot \\\n\u001b[32m 84\u001b[39m * \u001b[38;5;28mself\u001b[39m.N_exc*\u001b[38;5;28mself\u001b[39m.N_ur*not_wasted\n\u001b[32m---> \u001b[39m\u001b[32m85\u001b[39m \u001b[30;43mur\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mimass\u001b[39;49m\u001b[30;43m[\u001b[39;49m\u001b[30;43m'\u001b[39;49m\u001b[30;43mNH3\u001b[39;49m\u001b[30;43m'\u001b[39;49m\u001b[30;43m]\u001b[39;49m = ur_N * \u001b[38;5;28mself\u001b[39m.N_ur_NH3\n\u001b[32m 86\u001b[39m ur.imass[\u001b[33m'\u001b[39m\u001b[33mNonNH3\u001b[39m\u001b[33m'\u001b[39m] = ur_N - ur.imass[\u001b[33m'\u001b[39m\u001b[33mNH3\u001b[39m\u001b[33m'\u001b[39m]\n\u001b[32m 88\u001b[39m ur.imass[\u001b[33m'\u001b[39m\u001b[33mP\u001b[39m\u001b[33m'\u001b[39m] = (\u001b[38;5;28mself\u001b[39m.p_veg*\u001b[38;5;28mself\u001b[39m.P_prot_v+\u001b[38;5;28mself\u001b[39m.p_anim*\u001b[38;5;28mself\u001b[39m.P_prot_a)/factor \\\n\u001b[32m 89\u001b[39m * \u001b[38;5;28mself\u001b[39m.P_exc*\u001b[38;5;28mself\u001b[39m.P_ur*not_wasted\n", "\u001b[36mFile \u001b[39m\u001b[32mc:\\Users\\Yalin\\Documents\\Coding\\QSDsan-platform\\.venv\\Lib\\site-packages\\thermosteam\\indexer.py:544\u001b[39m, in \u001b[36mChemicalIndexer.__setitem__\u001b[39m\u001b[34m(self, key, data)\u001b[39m\n\u001b[32m 543\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34m__setitem__\u001b[39m(\u001b[38;5;28mself\u001b[39m, key, data):\n\u001b[32m--> \u001b[39m\u001b[32m544\u001b[39m index, kind = \u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43m_chemicals\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43m_get_index_and_kind\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43mkey\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n\u001b[32m 545\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m kind \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[32m 546\u001b[39m reset_sparse_chemical_data(\u001b[38;5;28mself\u001b[39m.data, data)\n", "\u001b[36mFile \u001b[39m\u001b[32mc:\\Users\\Yalin\\Documents\\Coding\\QSDsan-platform\\.venv\\Lib\\site-packages\\thermosteam\\_chemicals.py:1293\u001b[39m, in \u001b[36mCompiledChemicals._get_index_and_kind\u001b[39m\u001b[34m(self, key)\u001b[39m\n\u001b[32m 1286\u001b[39m \u001b[38;5;66;03m# [int|None] Kind of index:\u001b[39;00m\n\u001b[32m 1287\u001b[39m \u001b[38;5;66;03m# None - all\u001b[39;00m\n\u001b[32m 1288\u001b[39m \u001b[38;5;66;03m# 0 - chemical\u001b[39;00m\n\u001b[32m 1289\u001b[39m \u001b[38;5;66;03m# 1 - chemical group\u001b[39;00m\n\u001b[32m 1290\u001b[39m \u001b[38;5;66;03m# 2 - nested chemical group\u001b[39;00m\n\u001b[32m 1291\u001b[39m \u001b[38;5;66;03m# 3 - array\u001b[39;00m\n\u001b[32m 1292\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m isa(key, \u001b[38;5;28mstr\u001b[39m):\n\u001b[32m-> \u001b[39m\u001b[32m1293\u001b[39m index = \u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mindex\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43mkey\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n\u001b[32m 1294\u001b[39m kind = \u001b[32m0\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m isa(index, \u001b[38;5;28mint\u001b[39m) \u001b[38;5;28;01melse\u001b[39;00m \u001b[32m1\u001b[39m\n\u001b[32m 1295\u001b[39m \u001b[38;5;28;01melif\u001b[39;00m isa(key, \u001b[38;5;28mtuple\u001b[39m):\n", "\u001b[36mFile \u001b[39m\u001b[32m~\\Documents\\Coding\\QSDsan-platform\\QSDsan\\qsdsan\\_components.py:753\u001b[39m, in \u001b[36mCompiledComponents.index\u001b[39m\u001b[34m(self, ID)\u001b[39m\n\u001b[32m 751\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m: \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m._index[ID]\n\u001b[32m 752\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mKeyError\u001b[39;00m:\n\u001b[32m--> \u001b[39m\u001b[32m753\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m UndefinedComponent(ID)\n", "\u001b[31mUndefinedComponent\u001b[39m: 'NH3'" ] } ], "source": [ "# You'll receive an `UndefinedComponent` error\n", "bad_su = su.Excretion()\n", "bad_su.simulate()" ] }, { "cell_type": "markdown", "id": "bc000b77", "metadata": {}, "source": [ "For those units, the best way is to look at the documentation/examples (e.g., for the above case, check the `bwaise` system referenced in the [Excretion documentation](https://qsdsan.readthedocs.io/en/latest/api/unit_operations/static/Excretion.html))." ] }, { "cell_type": "markdown", "id": "nav-footer-4_sanunit_basic", "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 }