{ "cells": [ { "cell_type": "markdown", "id": "1efd058e", "metadata": {}, "source": [ "# `System` \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", " - Compose `SanUnit` instances into a `System`\n", " - Simulate a system and read its results\n", " - View the flowsheet diagram and summary tables\n", " - Attach a process specification to steer a system toward a target\n", " - Compile one annualized result across operating modes with `AgileSystem`\n", " - Diagnose and control recycle convergence\n", "\n", "- **Prerequisites:** [4. SanUnit (basic)](https://qsdsan.readthedocs.io/en/latest/tutorials/4_SanUnit_basic.html)\n", "\n", "* **Covered topics:**\n", "\n", " - 1. Creating a simple System\n", " - 2. Retrieving useful information\n", " - 3. Process specifications\n", " - 4. Operational flexibility\n", " - 5. Controlling recycle convergence\n", "\n", "> **Companion video.** A walkthrough of this tutorial is available on [YouTube](https://youtu.be/iIx28JkNjQ8), 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." ] }, { "cell_type": "markdown", "id": "6ef69ec5595b", "metadata": {}, "source": [ "\n", "\n", "## Setup\n", "\n", "Import `QSDsan` and confirm the installed version.\n" ] }, { "cell_type": "code", "execution_count": 1, "id": "bd0a32f7", "metadata": { "execution": { "iopub.execute_input": "2026-05-31T11:07:27.850487Z", "iopub.status.busy": "2026-05-31T11:07:27.850487Z", "iopub.status.idle": "2026-05-31T11:08:00.283960Z", "shell.execute_reply": "2026-05-31T11:08:00.282951Z" } }, "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": "201a6f52", "metadata": {}, "source": [ "## 1. Creating a simple System " ] }, { "cell_type": "markdown", "id": "3fc4fe1d", "metadata": {}, "source": [ "`System` objects are used to organize one or more unit operations in a certain order and facilitate mass and energy convergence, techno-economic analysis (TEA), and life cycle assessment (LCA)." ] }, { "cell_type": "markdown", "id": "f9ac93a2", "metadata": {}, "source": [ "The system mimics the **Benchmark Simulation Model No. 1 (BSM1)**, a standard activated-sludge layout: two anoxic tanks, three aerated tanks, and a clarifier, with two recycles returning to the first tank.\n", "\n", "\"BSM1-style\n", "\"BSM1-style" ] }, { "cell_type": "markdown", "id": "39b0d9b5", "metadata": {}, "source": [ "At this stage we won't include any of the process models (covered later in [10. Process](https://qsdsan.readthedocs.io/en/latest/tutorials/10_Process.html) and [11. Dynamic Simulation](https://qsdsan.readthedocs.io/en/latest/tutorials/11_Dynamic_Simulation.html)); we will just use some surrogate units in their place.\n", "\n", "So we'll want to firstly have two anoxic reactors (mix tanks without O2), followed by three aerated reactors (mix tanks with O2), and a clarifier (splitter)\n", "\n", "Note that we will also need two recycles, one from the last aerated reactor to the first anoxic reactor, and another one from the clarifier to the anoxic reactor" ] }, { "cell_type": "code", "execution_count": 2, "id": "b3fabab6", "metadata": { "execution": { "iopub.execute_input": "2026-05-31T11:08:00.285967Z", "iopub.status.busy": "2026-05-31T11:08:00.285967Z", "iopub.status.idle": "2026-05-31T11:08:00.895803Z", "shell.execute_reply": "2026-05-31T11:08:00.894791Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "WasteStream: ww\n", "phase: 'l', T: 298.15 K, P: 101325 Pa\n", "flow (g/hr): S_F 75\n", " S_U_Inf 32.5\n", " C_B_Subst 40\n", " X_B_Subst 227\n", " X_U_Inf 55.8\n", " X_Ig_ISS 52.3\n", " S_NH4 25\n", " S_PO4 8\n", " S_K 28\n", " S_Ca 140\n", " S_Mg 50\n", " S_CO3 120\n", " S_N2 18\n", " S_CAT 3\n", " S_AN 12\n", " ... 9.96e+05\n", " WasteStream-specific properties:\n", " pH : 7.0\n", " Alkalinity : 10.0 mmol/L\n", " COD : 430.0 mg/L\n", " BOD : 249.4 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", " Component concentrations (mg/L):\n", " S_F 75.0\n", " S_U_Inf 32.5\n", " C_B_Subst 40.0\n", " X_B_Subst 226.7\n", " X_U_Inf 55.8\n", " X_Ig_ISS 52.3\n", " S_NH4 25.0\n", " S_PO4 8.0\n", " S_K 28.0\n", " S_Ca 140.0\n", " S_Mg 50.0\n", " S_CO3 120.0\n", " S_N2 18.0\n", " S_CAT 3.0\n", " S_AN 12.0\n", " ...\n" ] } ], "source": [ "# As always, firstly we need to set the components we want to work with,\n", "# here we will just load the default components\n", "cmps = qs.Components.load_default()\n", "# Set the components and make an influent\n", "qs.set_thermo(cmps)\n", "ww = qs.WasteStream.codbased_inf_model('ww', flow_tot=1000, units=('L/hr', 'mg/L'))\n", "ww.show()" ] }, { "cell_type": "code", "execution_count": 3, "id": "5a2258cb", "metadata": { "execution": { "iopub.execute_input": "2026-05-31T11:08:00.897803Z", "iopub.status.busy": "2026-05-31T11:08:00.897803Z", "iopub.status.idle": "2026-05-31T11:08:00.975668Z", "shell.execute_reply": "2026-05-31T11:08:00.975668Z" } }, "outputs": [], "source": [ "# Make the first two anoxic reactors\n", "A1 = qs.unit_operations.MixTank('A1', ins=(ww, 'recycle1', 'recycle2'),\n", " tau=1, V_wf=0.8, init_with='WasteStream')\n", "A2 = qs.unit_operations.MixTank('A2', ins=A1-0, tau=1, V_wf=0.8, init_with='WasteStream')" ] }, { "cell_type": "markdown", "id": "ac49b567", "metadata": {}, "source": [ "Note that for `A1`, we saved two spots for recycles by just giving them the IDs of \"`recycle1`\" and \"`recycle2`\", we will connect them to the corresponding units after we create them later." ] }, { "cell_type": "markdown", "id": "7c787411", "metadata": {}, "source": [ "Additionally, when creating `A2`, we indicated that the `ins=A1-0`, the expression with hyphen `-` is called \"[-pipe-notation](https://biosteam.readthedocs.io/en/latest/tutorial/-pipe-_notation.html)\", briefly (`U1`, `U2`, and `U3` are just units):\n", "\n", "- `U1-0` is equivalent to `U1.outs[0]`\n", "- `0-U1` is equivalent to `U1.ins[0]`\n", "- `U1-0-1-U2` is equivalent to `U2.ins[1] = U1.outs[0]`\n", "\n", " - Note that `U1-0-1-U2` is not equivalent to `U2-1-0-U1`, which means `U1.outs[0] = U2.ins[1]` (like `a = b`, which gives the value of `b` to `a`, is not the same as `b = a`, which gives the value of `a` to `b`)\n", " - If `U1` just has one effluent and `U2` just have one influent, we can use `U1-U2`\n", "\n", "- This is applicable for multiple influents/effluents as well, e.g., `(U1-0, U3-0)-U2` is equivalent to `U2.ins[0] = U1.outs[0]` and `U2.ins[1] = U3.outs[0]`" ] }, { "cell_type": "code", "execution_count": 4, "id": "73dffc0d", "metadata": { "execution": { "iopub.execute_input": "2026-05-31T11:08:00.978767Z", "iopub.status.busy": "2026-05-31T11:08:00.977774Z", "iopub.status.idle": "2026-05-31T11:08:00.981769Z", "shell.execute_reply": "2026-05-31T11:08:00.981769Z" } }, "outputs": [], "source": [ "# Then have three O2 streams, assuming we are just pumping air\n", "# (the numbers here are illustrative)\n", "oxy1 = qs.WasteStream('oxy1', S_O2=ww.F_mass*0.01)\n", "oxy1.imol['S_N2'] = oxy1.imol['S_O2']/0.21*0.79\n", "oxy2 = oxy1.copy('oxy2')\n", "oxy3 = oxy1.copy('oxy3')" ] }, { "cell_type": "code", "execution_count": 5, "id": "1b2fc42e", "metadata": { "execution": { "iopub.execute_input": "2026-05-31T11:08:00.984487Z", "iopub.status.busy": "2026-05-31T11:08:00.984487Z", "iopub.status.idle": "2026-05-31T11:08:00.989870Z", "shell.execute_reply": "2026-05-31T11:08:00.988852Z" } }, "outputs": [], "source": [ "# Setting up the three aerated tanks\n", "O1 = qs.unit_operations.MixTank('O1', ins=(A2-0, oxy1), tau=1, V_wf=0.8, init_with='WasteStream')\n", "O2 = qs.unit_operations.MixTank('O2', ins=(O1-0, oxy2), tau=1, V_wf=0.8, init_with='WasteStream')\n", "O3 = qs.unit_operations.MixTank('O3', ins=(O2-0, oxy3), tau=1, V_wf=0.8, init_with='WasteStream')" ] }, { "cell_type": "code", "execution_count": 6, "id": "8d0e6d2e", "metadata": { "execution": { "iopub.execute_input": "2026-05-31T11:08:00.990270Z", "iopub.status.busy": "2026-05-31T11:08:00.990270Z", "iopub.status.idle": "2026-05-31T11:08:00.995034Z", "shell.execute_reply": "2026-05-31T11:08:00.995034Z" } }, "outputs": [], "source": [ "# Now note that we need to set up a splitter after the last aerated tank,\n", "# to create the recycle stream\n", "S1 = qs.unit_operations.Splitter('S1', ins=O3-0, outs=('', 1-A1), # `''` means we use default ID\n", " split=0.9, init_with='WasteStream')" ] }, { "cell_type": "code", "execution_count": 7, "id": "0b56485f", "metadata": { "execution": { "iopub.execute_input": "2026-05-31T11:08:00.995034Z", "iopub.status.busy": "2026-05-31T11:08:00.995034Z", "iopub.status.idle": "2026-05-31T11:08:01.000295Z", "shell.execute_reply": "2026-05-31T11:08:01.000295Z" } }, "outputs": [], "source": [ "# Add in the clarifier, which is actually also a splitter\n", "# (if we ignore the part that splitter does not have costs),\n", "# since it's a clarifier, let's assume that 90% of the solubles\n", "# (including dissolved gas and colloidal) will\n", "# go to the liquid stream while 10% go to the sludge,\n", "# and all solids go to the sludge\n", "split_dct = {i.ID: 0.9 if i.particle_size != 'Particulate' else 0 for i in cmps }" ] }, { "cell_type": "markdown", "id": "29ad58f3", "metadata": {}, "source": [ "
\n", "Python Aside: dict/list comprehensions (click to expand)\n", "\n", "`split_dct` above is built with a *dict comprehension*, a compact one-line way to build a dict from a loop. It is equivalent to:\n", "\n", "```python\n", "split_dct = {}\n", "for i in cmps:\n", " if i.particle_size != 'Particulate':\n", " split_dct[i.ID] = 0.9\n", " else:\n", " split_dct[i.ID] = 0\n", "```\n", "\n", "
" ] }, { "cell_type": "code", "execution_count": 8, "id": "6c40eb94", "metadata": { "execution": { "iopub.execute_input": "2026-05-31T11:08:01.002324Z", "iopub.status.busy": "2026-05-31T11:08:01.002324Z", "iopub.status.idle": "2026-05-31T11:08:01.006896Z", "shell.execute_reply": "2026-05-31T11:08:01.006896Z" } }, "outputs": [], "source": [ "S2 = qs.unit_operations.Splitter(\n", " 'S2', ins=S1-0, outs=('liquid_eff', 2-A1),\n", " split=split_dct, init_with='WasteStream')" ] }, { "cell_type": "code", "execution_count": 9, "id": "eafb8dd4", "metadata": { "execution": { "iopub.execute_input": "2026-05-31T11:08:01.008910Z", "iopub.status.busy": "2026-05-31T11:08:01.008910Z", "iopub.status.idle": "2026-05-31T11:08:01.012014Z", "shell.execute_reply": "2026-05-31T11:08:01.012014Z" } }, "outputs": [], "source": [ "# It's time to create the system!\n", "# Since one system can only handle one recycle,\n", "# we need to make two systems\n", "internal_sys = qs.System('internal_sys',\n", " path=(A1, A2, O1, O2, O3, S1), # all units within this internal sys\n", " recycle=S1-1 # the recycle stream\n", " )" ] }, { "cell_type": "code", "execution_count": 10, "id": "8d2f31e0", "metadata": { "execution": { "iopub.execute_input": "2026-05-31T11:08:01.014020Z", "iopub.status.busy": "2026-05-31T11:08:01.014020Z", "iopub.status.idle": "2026-05-31T11:08:01.016839Z", "shell.execute_reply": "2026-05-31T11:08:01.016839Z" } }, "outputs": [], "source": [ "# When creating the second system,\n", "# we can include the first one in the `path`\n", "external_sys = qs.System('external_sys',\n", " path=(internal_sys, S2),\n", " recycle=S2-1\n", " )" ] }, { "cell_type": "code", "execution_count": 11, "id": "2dce3dcb", "metadata": { "execution": { "iopub.execute_input": "2026-05-31T11:08:01.019164Z", "iopub.status.busy": "2026-05-31T11:08:01.019164Z", "iopub.status.idle": "2026-05-31T11:08:03.107863Z", "shell.execute_reply": "2026-05-31T11:08:03.107863Z" }, "scrolled": true }, "outputs": [ { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "\n", "147783321932:c->147783384026:c\n", "\n", "\n", "\n", " ws1\n", "\n", "\n", "\n", "\n", "\n", "147783384026:c->147783388369:c\n", "\n", "\n", "\n", " ws2\n", "\n", "\n", "\n", "\n", "\n", "147783388369:c->147783381780:c\n", "\n", "\n", "\n", " ws3\n", "\n", "\n", "\n", "\n", "\n", "147783381780:c->147783479886:c\n", "\n", "\n", "\n", " ws4\n", "\n", "\n", "\n", "\n", "\n", "147783479886:c->147783322130:w\n", "\n", "\n", "\n", " ws5\n", "\n", "\n", "\n", "\n", "\n", "147783322130:c->147783321932:c\n", "\n", "\n", "\n", " recycle1\n", "\n", "\n", "\n", "\n", "\n", "147783322130:c->147783381840:w\n", "\n", "\n", "\n", " ws6\n", "\n", "\n", "\n", "\n", "\n", "147783381840:c->147783321932:c\n", "\n", "\n", "\n", " recycle2\n", "\n", "\n", "\n", "\n", "\n", "147783381840:c->147777624608:w\n", "\n", "\n", " liquid eff\n", "\n", "\n", "\n", "\n", "\n", "147777624008:e->147783321932:c\n", "\n", "\n", " ww\n", "\n", "\n", "\n", "\n", "\n", "147777623888:e->147783388369:c\n", "\n", "\n", " oxy1\n", "\n", "\n", "\n", "\n", "\n", "147777623288:e->147783381780:c\n", "\n", "\n", " oxy2\n", "\n", "\n", "\n", "\n", "\n", "147777623968:e->147783479886:c\n", "\n", "\n", " oxy3\n", "\n", "\n", "\n", "\n", "\n", "147783321932\n", "\n", "\n", "A1\n", "Mix tank\n", "\n", "\n", "\n", "\n", "\n", "147783384026\n", "\n", "\n", "A2\n", "Mix tank\n", "\n", "\n", "\n", "\n", "\n", "147783388369\n", "\n", "\n", "O1\n", "Mix tank\n", "\n", "\n", "\n", "\n", "\n", "147783381780\n", "\n", "\n", "O2\n", "Mix tank\n", "\n", "\n", "\n", "\n", "\n", "147783479886\n", "\n", "\n", "O3\n", "Mix tank\n", "\n", "\n", "\n", "\n", "\n", "147783322130\n", "\n", "\n", "S1\n", "Splitter\n", "\n", "\n", "\n", "\n", "\n", "147783381840\n", "\n", "\n", "S2\n", "Splitter\n", "\n", "\n", "\n", "\n", "\n", "147777624008\n", "\n", "\n", "\n", "\n", "147777624608\n", "\n", "\n", "\n", "\n", "147777623888\n", "\n", "\n", "\n", "\n", "147777623288\n", "\n", "\n", "\n", "\n", "147777623968\n", "\n", "\n", "\n", "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# Tada~! Let's take a look at the system\n", "external_sys.diagram()" ] }, { "cell_type": "code", "execution_count": 12, "id": "sys_fromunits", "metadata": { "execution": { "iopub.execute_input": "2026-05-31T11:08:03.107863Z", "iopub.status.busy": "2026-05-31T11:08:03.107863Z", "iopub.status.idle": "2026-05-31T11:08:03.650270Z", "shell.execute_reply": "2026-05-31T11:08:03.649264Z" } }, "outputs": [ { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "\n", "147783321932:c->147783384026:c\n", "\n", "\n", "\n", " ws1\n", "\n", "\n", "\n", "\n", "\n", "147783384026:c->147783388369:c\n", "\n", "\n", "\n", " ws2\n", "\n", "\n", "\n", "\n", "\n", "147783388369:c->147783381780:c\n", "\n", "\n", "\n", " ws3\n", "\n", "\n", "\n", "\n", "\n", "147783381780:c->147783479886:c\n", "\n", "\n", "\n", " ws4\n", "\n", "\n", "\n", "\n", "\n", "147783479886:c->147783322130:w\n", "\n", "\n", "\n", " ws5\n", "\n", "\n", "\n", "\n", "\n", "147783322130:c->147783321932:c\n", "\n", "\n", "\n", " recycle1\n", "\n", "\n", "\n", "\n", "\n", "147783322130:c->147783381840:w\n", "\n", "\n", "\n", " ws6\n", "\n", "\n", "\n", "\n", "\n", "147783381840:c->147783321932:c\n", "\n", "\n", "\n", " recycle2\n", "\n", "\n", "\n", "\n", "\n", "147783381840:c->147777624608:w\n", "\n", "\n", " liquid eff\n", "\n", "\n", "\n", "\n", "\n", "147777624008:e->147783321932:c\n", "\n", "\n", " ww\n", "\n", "\n", "\n", "\n", "\n", "147777623888:e->147783388369:c\n", "\n", "\n", " oxy1\n", "\n", "\n", "\n", "\n", "\n", "147777623288:e->147783381780:c\n", "\n", "\n", " oxy2\n", "\n", "\n", "\n", "\n", "\n", "147777623968:e->147783479886:c\n", "\n", "\n", " oxy3\n", "\n", "\n", "\n", "\n", "\n", "147783321932\n", "\n", "\n", "A1\n", "Mix tank\n", "\n", "\n", "\n", "\n", "\n", "147783384026\n", "\n", "\n", "A2\n", "Mix tank\n", "\n", "\n", "\n", "\n", "\n", "147783388369\n", "\n", "\n", "O1\n", "Mix tank\n", "\n", "\n", "\n", "\n", "\n", "147783381780\n", "\n", "\n", "O2\n", "Mix tank\n", "\n", "\n", "\n", "\n", "\n", "147783479886\n", "\n", "\n", "O3\n", "Mix tank\n", "\n", "\n", "\n", "\n", "\n", "147783322130\n", "\n", "\n", "S1\n", "Splitter\n", "\n", "\n", "\n", "\n", "\n", "147783381840\n", "\n", "\n", "S2\n", "Splitter\n", "\n", "\n", "\n", "\n", "\n", "147777624008\n", "\n", "\n", "\n", "\n", "147777624608\n", "\n", "\n", "\n", "\n", "147777623888\n", "\n", "\n", "\n", "\n", "147777623288\n", "\n", "\n", "\n", "\n", "147777623968\n", "\n", "\n", "\n", "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# Building the system by hand (above) meant nesting one recycle per `System`.\n", "# `from_units` instead infers the whole network (including both recycle loops) from a\n", "# list of units, so you do not have to nest them yourself:\n", "auto_sys = qs.System.from_units('auto_sys', units=external_sys.units)\n", "auto_sys.diagram()" ] }, { "cell_type": "markdown", "id": "sys_converge", "metadata": {}, "source": [ "
\n", "\n", "**Recycle convergence.** A system with recycle streams is solved iteratively until the recycle streams stop changing. The default solver usually converges on its own. When a system will not, the `molar_tolerance`, `temperature_tolerance`, `maxiter`, and `method` attributes (or `sys.set_tolerance(...)`) control the solve. [Section 5](#5.-Controlling-recycle-convergence) collects the full set, shows how to read a non-convergence error, and lists what to adjust.\n", "\n", "
" ] }, { "cell_type": "markdown", "id": "a149b216", "metadata": {}, "source": [ "## 2. Retrieving useful information \n", "Now that we have the system, we can retrieve information using the many attributes `System` has." ] }, { "cell_type": "code", "execution_count": 13, "id": "ccce38a7", "metadata": { "execution": { "iopub.execute_input": "2026-05-31T11:08:03.652275Z", "iopub.status.busy": "2026-05-31T11:08:03.651274Z", "iopub.status.idle": "2026-05-31T11:08:03.656161Z", "shell.execute_reply": "2026-05-31T11:08:03.656161Z" } }, "outputs": [ { "data": { "text/plain": [ "[,\n", " ,\n", " ,\n", " ,\n", " ,\n", " ,\n", " ]" ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Firstly, if you want to look at what units are within the system\n", "sys = external_sys # Give it a shorthand\n", "sys.units" ] }, { "cell_type": "code", "execution_count": 14, "id": "bb5eaf2a", "metadata": { "execution": { "iopub.execute_input": "2026-05-31T11:08:03.658183Z", "iopub.status.busy": "2026-05-31T11:08:03.658183Z", "iopub.status.idle": "2026-05-31T11:08:03.663712Z", "shell.execute_reply": "2026-05-31T11:08:03.663712Z" } }, "outputs": [ { "data": { "text/plain": [ "(, )" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Which is different from\n", "sys.path" ] }, { "cell_type": "markdown", "id": "sys_fsmd", "metadata": {}, "source": [ "Every unit and stream is registered in the active **flowsheet** (`qs.F`, i.e., `qs.main_flowsheet`), so you can fetch one by its ID instead of keeping a variable around. This is also why reusing an ID triggers a \"replaced in registry\" warning. Switch or isolate namespaces with `qs.main_flowsheet.set_flowsheet('name')`." ] }, { "cell_type": "code", "execution_count": 15, "id": "sys_fscode", "metadata": { "execution": { "iopub.execute_input": "2026-05-31T11:08:03.666000Z", "iopub.status.busy": "2026-05-31T11:08:03.666000Z", "iopub.status.idle": "2026-05-31T11:08:04.066771Z", "shell.execute_reply": "2026-05-31T11:08:04.066771Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "A1 | True\n", "WasteStream: ww to \n", "phase: 'l', T: 298.15 K, P: 101325 Pa\n", "flow (g/hr): S_F 75\n", " S_U_Inf 32.5\n", " C_B_Subst 40\n", " X_B_Subst 227\n", " X_U_Inf 55.8\n", " X_Ig_ISS 52.3\n", " S_NH4 25\n", " S_PO4 8\n", " S_K 28\n", " S_Ca 140\n", " S_Mg 50\n", " S_CO3 120\n", " S_N2 18\n", " S_CAT 3\n", " S_AN 12\n", " ... 9.96e+05\n", " WasteStream-specific properties:\n", " pH : 7.0\n", " Alkalinity : 10.0 mmol/L\n", " COD : 430.0 mg/L\n", " BOD : 249.4 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", " Component concentrations (mg/L):\n", " S_F 75.0\n", " S_U_Inf 32.5\n", " C_B_Subst 40.0\n", " X_B_Subst 226.7\n", " X_U_Inf 55.8\n", " X_Ig_ISS 52.3\n", " S_NH4 25.0\n", " S_PO4 8.0\n", " S_K 28.0\n", " S_Ca 140.0\n", " S_Mg 50.0\n", " S_CO3 120.0\n", " S_N2 18.0\n", " S_CAT 3.0\n", " S_AN 12.0\n", " ...\n" ] } ], "source": [ "# Fetch a unit or stream by ID from the flowsheet\n", "print(qs.F.unit.A1, '|', qs.F.unit.A1 is A1) # same object as the `A1` variable\n", "qs.F.stream.ww # a stream by ID" ] }, { "cell_type": "code", "execution_count": 16, "id": "83edfdd0", "metadata": { "execution": { "iopub.execute_input": "2026-05-31T11:08:04.066771Z", "iopub.status.busy": "2026-05-31T11:08:04.066771Z", "iopub.status.idle": "2026-05-31T11:08:04.693666Z", "shell.execute_reply": "2026-05-31T11:08:04.693666Z" } }, "outputs": [ { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "\n", "147770736801:c->147783531149:c\n", "\n", "\n", "\n", " \n", "\n", "\n", "\n", "\n", "\n", "147783531146:c->147770736801:c\n", "\n", "\n", "\n", " \n", "\n", "\n", "\n", "\n", "\n", "147770736801\n", "\n", "\n", "external_sys\n", "System\n", "\n", "\n", "\n", "\n", "\n", "147783531146\n", "\n", "\n", "ww\n", "oxy1\n", "oxy2\n", "oxy3\n", "\n", "\n", "\n", "\n", "\n", "147783531149\n", "\n", "\n", "liquid_eff\n", "\n", "\n", "\n", "\n", "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "System: external_sys\n", "Highest convergence error among components in recycle\n", "stream S2-1 after 2 loops:\n", "- flow rate 6.03e-01 kmol/hr (48%)\n", "- temperature 0.00e+00 K (0%)\n", "ins...\n", "[0] ww \n", " phase: 'l', T: 298.15 K, P: 101325 Pa\n", " flow (kmol/hr): S_F 0.075\n", " S_U_Inf 0.0325\n", " C_B_Subst 0.04\n", " X_B_Subst 0.227\n", " X_U_Inf 0.0558\n", " X_Ig_ISS 0.0523\n", " S_NH4 0.00139\n", " ... 55.3\n", "[1] oxy1 \n", " phase: 'l', T: 298.15 K, P: 101325 Pa\n", " flow (kmol/hr): S_N2 1.17\n", " S_O2 0.312\n", "[2] oxy2 \n", " phase: 'l', T: 298.15 K, P: 101325 Pa\n", " flow (kmol/hr): S_N2 1.17\n", " S_O2 0.312\n", "[3] oxy3 \n", " phase: 'l', T: 298.15 K, P: 101325 Pa\n", " flow (kmol/hr): S_N2 1.17\n", " S_O2 0.312\n", "outs...\n", "[0] liquid_eff \n", " phase: 'l', T: 298.15 K, P: 101325 Pa\n", " flow (kmol/hr): S_F 0.0734\n", " S_U_Inf 0.0319\n", " C_B_Subst 0.0392\n", " S_NH4 0.00136\n", " S_PO4 4.06e-05\n", " S_K 0.000701\n", " S_Ca 0.00342\n", " ... 58.5\n" ] } ], "source": [ "# Similar to units, you need to first simulate the system\n", "# before you check out attributes that are not set up\n", "# upon initialization (e.g., units, recycles)\n", "sys.simulate()\n", "sys" ] }, { "cell_type": "markdown", "id": "2737f7e8", "metadata": {}, "source": [ "In the above printout, you'll are actually looking at the following attributes:\n", "- `sys.diagram('minimal')`\n", "- `sys._error_info()`\n", "- `sys.feeds`\n", "- `sys.products`" ] }, { "cell_type": "code", "execution_count": 17, "id": "1301ca6f", "metadata": { "execution": { "iopub.execute_input": "2026-05-31T11:08:04.693666Z", "iopub.status.busy": "2026-05-31T11:08:04.693666Z", "iopub.status.idle": "2026-05-31T11:08:04.699809Z", "shell.execute_reply": "2026-05-31T11:08:04.699809Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Purchase cost of the system equipment is 85249 USD.\n", "Installed cost of the system equipment is 140661 USD.\n" ] } ], "source": [ "# Ones related to costs\n", "print(f'Purchase cost of the system equipment is {sys.purchase_cost:.0f} USD.')\n", "print(f'Installed cost of the system equipment is {sys.installed_equipment_cost:.0f} USD.')" ] }, { "cell_type": "code", "execution_count": 18, "id": "86be91ee", "metadata": { "execution": { "iopub.execute_input": "2026-05-31T11:08:04.699809Z", "iopub.status.busy": "2026-05-31T11:08:04.699809Z", "iopub.status.idle": "2026-05-31T11:08:04.705906Z", "shell.execute_reply": "2026-05-31T11:08:04.704822Z" } }, "outputs": [], "source": [ "# If you set the operating hour of the system,\n", "# you can also see costs related to operation\n", "sys.operating_hours = 365*0.8" ] }, { "cell_type": "code", "execution_count": 19, "id": "44c7cb32", "metadata": { "execution": { "iopub.execute_input": "2026-05-31T11:08:04.708418Z", "iopub.status.busy": "2026-05-31T11:08:04.708418Z", "iopub.status.idle": "2026-05-31T11:08:04.711805Z", "shell.execute_reply": "2026-05-31T11:08:04.711805Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "This system uses 239.57 kWh electricity per year.\n", "This system produces 0.00 kWh electricity per year.\n" ] } ], "source": [ "# Electricity\n", "print(f'This system uses {sys.get_electricity_consumption():.2f} kWh electricity per year.')\n", "print(f'This system produces {sys.get_electricity_production():.2f} kWh electricity per year.')" ] }, { "cell_type": "code", "execution_count": 20, "id": "3b6d328c", "metadata": { "execution": { "iopub.execute_input": "2026-05-31T11:08:04.713813Z", "iopub.status.busy": "2026-05-31T11:08:04.713813Z", "iopub.status.idle": "2026-05-31T11:08:05.612866Z", "shell.execute_reply": "2026-05-31T11:08:05.611855Z" } }, "outputs": [ { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "\n", "147783321932:c->147783384026:c\n", "\n", "\n", "\n", " ws1\n", "\n", "\n", "\n", "\n", "\n", "147783384026:c->147783388369:c\n", "\n", "\n", "\n", " ws2\n", "\n", "\n", "\n", "\n", "\n", "147783388369:c->147783381780:c\n", "\n", "\n", "\n", " ws3\n", "\n", "\n", "\n", "\n", "\n", "147783381780:c->147783479886:c\n", "\n", "\n", "\n", " ws4\n", "\n", "\n", "\n", "\n", "\n", "147783479886:c->147783322130:w\n", "\n", "\n", "\n", " ws5\n", "\n", "\n", "\n", "\n", "\n", "147783322130:c->147783321932:c\n", "\n", "\n", "\n", " recycle1\n", "\n", "\n", "\n", "\n", "\n", "147783322130:c->147783381840:w\n", "\n", "\n", "\n", " ws6\n", "\n", "\n", "\n", "\n", "\n", "147783381840:c->147783321932:c\n", "\n", "\n", "\n", " recycle2\n", "\n", "\n", "\n", "\n", "\n", "147783381840:c->147783376888:c\n", "\n", "\n", "\n", " liquid eff\n", "\n", "\n", "\n", "\n", "\n", "147783376888:c->147783400618:c\n", "\n", "\n", "\n", " s7\n", "\n", "\n", "\n", "\n", "\n", "147783400618:c->147783465348:w\n", "\n", "\n", " s8\n", "\n", "\n", "\n", "\n", "\n", "147777624008:e->147783321932:c\n", "\n", "\n", " ww\n", "\n", "\n", "\n", "\n", "\n", "147777623888:e->147783388369:c\n", "\n", "\n", " oxy1\n", "\n", "\n", "\n", "\n", "\n", "147777623288:e->147783381780:c\n", "\n", "\n", " oxy2\n", "\n", "\n", "\n", "\n", "\n", "147777623968:e->147783479886:c\n", "\n", "\n", " oxy3\n", "\n", "\n", "\n", "\n", "\n", "147783321932\n", "\n", "\n", "A1\n", "Mix tank\n", "\n", "\n", "\n", "\n", "\n", "147783384026\n", "\n", "\n", "A2\n", "Mix tank\n", "\n", "\n", "\n", "\n", "\n", "147783388369\n", "\n", "\n", "O1\n", "Mix tank\n", "\n", "\n", "\n", "\n", "\n", "147783381780\n", "\n", "\n", "O2\n", "Mix tank\n", "\n", "\n", "\n", "\n", "\n", "147783479886\n", "\n", "\n", "O3\n", "Mix tank\n", "\n", "\n", "\n", "\n", "\n", "147783322130\n", "\n", "\n", "S1\n", "Splitter\n", "\n", "\n", "\n", "\n", "\n", "147783381840\n", "\n", "\n", "S2\n", "Splitter\n", "\n", "\n", "\n", "\n", "\n", "147783376888\n", "\n", "\n", "H1\n", "Heating\n", "\n", "\n", "\n", "\n", "\n", "147783400618\n", "\n", "\n", "H2\n", "Cooling\n", "\n", "\n", "\n", "\n", "\n", "147777624008\n", "\n", "\n", "\n", "\n", "147783465348\n", "\n", "\n", "\n", "\n", "147777623888\n", "\n", "\n", "\n", "\n", "147777623288\n", "\n", "\n", "\n", "\n", "147777623968\n", "\n", "\n", "\n", "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "Cooling duty of the second system is 62173345.96 kJ per year.\n", "Heating duty of the second system is 54793314.40 kJ per year.\n" ] } ], "source": [ "# And many others related to heating and cooling utilities,\n", "# since there is no use of heating/cooling duties in our system,\n", "# let's just add two new units to demonstrate it\n", "H1 = qs.unit_operations.HXutility('H1', ins=sys.outs[0], T=50+273.15)\n", "H2 = qs.unit_operations.HXutility('H2', ins=H1-0, T=20+273.15)\n", "sys2 = qs.System('sys2', path=(sys, H1, H2))\n", "sys2.operating_hours = sys.operating_hours\n", "sys2.simulate()\n", "sys2.diagram()\n", "print(f'Cooling duty of the second system is {sys2.get_cooling_duty():.2f} kJ per year.')\n", "print(f'Heating duty of the second system is {sys2.get_heating_duty():.2f} kJ per year.')" ] }, { "cell_type": "markdown", "id": "dc22ade4", "metadata": {}, "source": [ "### 2.3. Inspecting and exporting results\n", "Beyond the attributes above, a `System` offers a few high-value methods:\n", "\n", "- `sys.results()` returns a `DataFrame` summarizing every unit's design and cost.\n", "- `sys.save_report('report.xlsx')` writes a full Excel report (stream tables, designs, costs, utilities) — the usual way to share results.\n", "- `sys.diagram(kind='thorough', file='sys', format='png')` saves the flowsheet to a file; `kind` can be `'thorough'`, `'cluster'`, `'minimal'`, or `'surface'`." ] }, { "cell_type": "code", "execution_count": 21, "id": "3f43d9fe", "metadata": { "execution": { "iopub.execute_input": "2026-05-31T11:08:05.615878Z", "iopub.status.busy": "2026-05-31T11:08:05.615878Z", "iopub.status.idle": "2026-05-31T11:08:05.628710Z", "shell.execute_reply": "2026-05-31T11:08:05.628710Z" } }, "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", "
SystemUnitsexternal_sys
ElectricityPowerkW0.836
CostUSD/hr0.0654
Total purchase costUSD8.61e+04
Installed equipment costUSD1.42e+05
Utility costUSD/hr0.0654
Material costUSD/hr0
SalesUSD/hr0
\n", "
" ], "text/plain": [ "System Units external_sys\n", "Electricity Power kW 0.836\n", " Cost USD/hr 0.0654\n", "Total purchase cost USD 8.61e+04\n", "Installed equipment cost USD 1.42e+05\n", "Utility cost USD/hr 0.0654\n", "Material cost USD/hr 0\n", "Sales USD/hr 0" ] }, "execution_count": 21, "metadata": {}, "output_type": "execute_result" } ], "source": [ "sys.results()" ] }, { "cell_type": "code", "execution_count": 22, "id": "sys_export", "metadata": { "execution": { "iopub.execute_input": "2026-05-31T11:08:05.631852Z", "iopub.status.busy": "2026-05-31T11:08:05.630764Z", "iopub.status.idle": "2026-05-31T11:08:05.635749Z", "shell.execute_reply": "2026-05-31T11:08:05.634735Z" } }, "outputs": [], "source": [ "# To export everything to a single Excel workbook (stream tables, unit designs,\n", "# costs, and utilities), use `save_report`. It saves next to this notebook unless\n", "# you pass a path via `file=`. The `results()` DataFrame can also be saved directly\n", "# with pandas (e.g., `.to_csv(...)`). Commented out so no files are written here.\n", "# sys.save_report('system_report.xlsx')\n", "# sys.results().to_csv('system_results.csv')" ] }, { "cell_type": "markdown", "id": "fbc02ff7", "metadata": {}, "source": [ "
\n", "\n", "**Heads up: dynamic simulation.** `QSDsan` also extends `System` to integrate unit states over time. With dynamic units you call `sys.simulate(t_span=(0, 10), ...)` instead of a steady-state solve. That is covered in [11. Dynamic Simulation](https://qsdsan.readthedocs.io/en/latest/tutorials/11_Dynamic_Simulation.html).\n", "\n", "
" ] }, { "cell_type": "markdown", "id": "ed3f5fa1", "metadata": {}, "source": [ "## 3. Process specifications \n", "\n", "Just like a unit (see the [SanUnit advanced tutorial section 3.6](5_SanUnit_advanced.ipynb#3.7.-Specifications)), a whole `System` can carry *specifications*: functions that run during `sys.simulate()`. Add one with `sys.add_specification`, often as a decorator, and pass `simulate=True` so the system re-converges after the spec has run. System-level specifications are the right altitude when the quantity you want to control depends on more than one unit, or on the converged recycle streams, rather than on a single unit in isolation." ] }, { "cell_type": "code", "execution_count": 23, "id": "2e7cdc77", "metadata": { "execution": { "iopub.execute_input": "2026-05-31T11:08:05.637750Z", "iopub.status.busy": "2026-05-31T11:08:05.637750Z", "iopub.status.idle": "2026-05-31T11:08:05.717206Z", "shell.execute_reply": "2026-05-31T11:08:05.716697Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Without a spec: product COD = 143.1 mg/L\n", "With spec (target 200): product COD = 199.7 mg/L, dilution = 1150 kg/hr\n" ] } ], "source": [ "# Build a small blend-and-split train from the same kinds of units used above:\n", "# a high-strength wastewater diluted with process water, then split to a product line.\n", "conc = qs.WasteStream.codbased_inf_model('conc', flow_tot=1000, units=('L/hr', 'mg/L'))\n", "dil = qs.WasteStream('dil', H2O=2000, units='kg/hr') # process water, negligible COD\n", "M1 = qs.unit_operations.MixTank('M1', ins=(conc, dil), tau=1, V_wf=0.8, init_with='WasteStream')\n", "S3 = qs.unit_operations.Splitter('S3', ins=M1-0, outs=('product', 'sidestream'),\n", " split=0.7, init_with='WasteStream')\n", "blend_sys = qs.System.from_units('blend_sys', units=[M1, S3])\n", "blend_sys.simulate()\n", "print(f'Without a spec: product COD = {qs.F.stream.product.COD:.1f} mg/L')\n", "\n", "# Hold the product COD at a target by adjusting the dilution-water flow.\n", "# `simulate=True` re-converges the system after the spec sets the knob.\n", "target_COD = 200.0 # mg/L\n", "@blend_sys.add_specification(simulate=True)\n", "def hold_product_COD():\n", " needed_vol = conc.COD * conc.F_vol / target_COD # total volume to reach the target conc\n", " dil.imass['H2O'] = max(needed_vol - conc.F_vol, 0.) * 1000 # ~1000 kg/m3 of added water\n", "blend_sys.simulate()\n", "print(f'With spec (target {target_COD:.0f}): product COD = {qs.F.stream.product.COD:.1f} mg/L, '\n", " f'dilution = {dil.imass[\"H2O\"]:.0f} kg/hr')" ] }, { "cell_type": "markdown", "id": "f3fd9e74", "metadata": {}, "source": [ "Here one parameter (the dilution-water flow) is set to meet one target (the product COD). That balance is the whole story of a specification: **each spec consumes one degree of freedom.** To hit N independent targets you must free N parameters; asking for more targets than free parameter over-constrains the system and has no consistent solution. (This is the system-level version of the degrees-of-freedom point from the [SanUnit advanced tutorial section 3.6](5_SanUnit_advanced.ipynb#3.7.-Specifications).)\n", "\n", "We could compute the dilution analytically above because mixing is simple. In general an outlet depends on the converged system in ways you cannot invert by hand, and you instead drive a parameter to the target numerically. In this case, you can use `add_bounded_numerical_specification`, which adjusts one parameter until your objective function returns zero, using a bracketed root-finder." ] }, { "cell_type": "code", "execution_count": 24, "id": "b3626b1d", "metadata": { "execution": { "iopub.execute_input": "2026-05-31T11:08:05.719220Z", "iopub.status.busy": "2026-05-31T11:08:05.719220Z", "iopub.status.idle": "2026-05-31T11:08:06.094128Z", "shell.execute_reply": "2026-05-31T11:08:06.093110Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Solved dilution = 1147 kg/hr -> product COD = 200.0 mg/L\n" ] } ], "source": [ "# Drive the same target by root-finding instead of a formula. First clear the analytic\n", "# spec so the two do not both fight over the dilution knob (one knob, one spec).\n", "blend_sys.specifications = []\n", "\n", "@blend_sys.add_bounded_numerical_specification(x0=0., x1=2e4, xtol=1.0)\n", "def hit_product_COD(h2o):\n", " dil.imass['H2O'] = h2o # the one knob: dilution-water flow (kg/hr)\n", " blend_sys.simulate()\n", " return qs.F.stream.product.COD - target_COD # objective: zero at the target\n", "blend_sys.simulate()\n", "print(f'Solved dilution = {dil.imass[\"H2O\"]:.0f} kg/hr -> product COD = {qs.F.stream.product.COD:.1f} mg/L')" ] }, { "cell_type": "markdown", "id": "s4-intro", "metadata": {}, "source": [ "## 4. Operational flexibility \n", "\n", "The systems built so far are each evaluated at a single steady state. Many real systems instead operate under conditions that vary over the year, such as seasonal loading, day and night cycles, or periodic feedstock switching. Evaluating a single representative condition can misstate the annual totals.\n", "\n", "An `AgileSystem` represents a system that runs in several operation modes over a year, and compiles one annualized result across all of them, with each mode weighted by its operating hours. It is re-exported from `biosteam` at the top level as `qs.AgileSystem`. This is particularly useful for techno-economic analysis (TEA) and life cycle assessment (LCA), where a single annualized result across operating modes is more representative than a single steady-state snapshot. Each operation mode is solved at steady state, so `AgileSystem` is not a dynamic (time-resolved) simulation; for transient behavior over time see [11. Dynamic Simulation](https://qsdsan.readthedocs.io/en/latest/tutorials/11_Dynamic_Simulation.html)." ] }, { "cell_type": "markdown", "id": "s4-modes-md", "metadata": {}, "source": [ "An agile system is assembled from two ingredients:\n", "\n", "- operation parameters: functions that set a quantity that varies between modes (for example, the influent strength), registered with `agile_sys.operation_parameter`; and\n", "- operation modes: each a system paired with its operating hours and the parameter values that hold during that mode, registered with `agile_sys.operation_mode`.\n", "\n", "The individual mode objects are created through the `operation_mode` method rather than instantiated directly." ] }, { "cell_type": "code", "execution_count": 25, "id": "s4-load", "metadata": { "execution": { "iopub.execute_input": "2026-05-31T11:08:06.096146Z", "iopub.status.busy": "2026-05-31T11:08:06.096146Z", "iopub.status.idle": "2026-05-31T11:08:06.123190Z", "shell.execute_reply": "2026-05-31T11:08:06.122182Z" } }, "outputs": [], "source": [ "from qsdsan.utils import create_example_treatment_systems\n", "\n", "# Reuse the aerobic treatment plant from the TEA and LCA tutorials\n", "# (municipal wastewater, 4,000 m3/d). A dedicated flowsheet isolates registries.\n", "qs.main_flowsheet.set_flowsheet('agile_demo')\n", "aer_sys, ana_sys = create_example_treatment_systems()\n", "aer = aer_sys.units[0]\n", "influent = aer.ins[0]\n", "Substrate = influent.components.Substrate\n", "design_flow = 4000/24 # m3/hr" ] }, { "cell_type": "code", "execution_count": 26, "id": "s4-build", "metadata": { "execution": { "iopub.execute_input": "2026-05-31T11:08:06.125197Z", "iopub.status.busy": "2026-05-31T11:08:06.125197Z", "iopub.status.idle": "2026-05-31T11:08:06.132476Z", "shell.execute_reply": "2026-05-31T11:08:06.131456Z" } }, "outputs": [ { "data": { "text/plain": [ "[OperationMode(system=aer_sys, operating_hours=4380.0, COD=800),\n", " OperationMode(system=aer_sys, operating_hours=4380.0, COD=300)]" ] }, "execution_count": 26, "metadata": {}, "output_type": "execute_result" } ], "source": [ "agile_sys = qs.AgileSystem()\n", "\n", "@agile_sys.operation_parameter\n", "def set_influent_COD(COD):\n", " # influent strength in mg/L, applied as the substrate mass flow\n", " influent.imass['Substrate'] = (COD/1000 * design_flow)/Substrate.i_COD\n", "\n", "half_year = 0.5 * 365 * 24 # hours\n", "agile_sys.operation_mode(aer_sys, operating_hours=half_year, COD=800) # high-load season\n", "agile_sys.operation_mode(aer_sys, operating_hours=half_year, COD=300) # low-load season\n", "\n", "agile_sys.simulate()\n", "agile_sys.operation_modes" ] }, { "cell_type": "markdown", "id": "s4-results-md", "metadata": {}, "source": [ "After simulation, the agile system reports results aggregated across all modes. The aerobic plant spends electricity on aeration in proportion to the organic load it oxidizes, so the higher-load season draws more power. The annual electricity consumption is the sum over modes of each mode's power multiplied by its operating hours, which we confirm below." ] }, { "cell_type": "code", "execution_count": 27, "id": "s4-results", "metadata": { "execution": { "iopub.execute_input": "2026-05-31T11:08:06.134481Z", "iopub.status.busy": "2026-05-31T11:08:06.134481Z", "iopub.status.idle": "2026-05-31T11:08:06.140474Z", "shell.execute_reply": "2026-05-31T11:08:06.139351Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Total operating hours: 8760 hr\n", "Annual electricity consumption: 381425 kWh\n", " COD 800 mg/L: 63.3 kW over 4380 hr\n", " COD 300 mg/L: 23.7 kW over 4380 hr\n" ] } ], "source": [ "print(f'Total operating hours: {agile_sys.operating_hours:.0f} hr')\n", "print(f'Annual electricity consumption: {agile_sys.get_electricity_consumption():.0f} kWh')\n", "\n", "# The annualized value is the operating-hour-weighted sum over the modes\n", "for mode in agile_sys.operation_modes:\n", " set_influent_COD(mode.COD); aer_sys.simulate()\n", " print(f' COD {mode.COD:>4} mg/L: {aer.power_utility.rate:5.1f} kW '\n", " f'over {mode.operating_hours:.0f} hr')\n", "agile_sys.simulate() # restore the aggregated state" ] }, { "cell_type": "markdown", "id": "s4-tea-md", "metadata": {}, "source": [ "The TEA and LCA classes accept an agile system in place of a single system, so one analysis spans all modes. Operating costs that depend on the mode, such as electricity, reflect the operating-hour-weighted result, while the installed capital is shared across modes." ] }, { "cell_type": "code", "execution_count": 28, "id": "s4-tea", "metadata": { "execution": { "iopub.execute_input": "2026-05-31T11:08:06.143482Z", "iopub.status.busy": "2026-05-31T11:08:06.142481Z", "iopub.status.idle": "2026-05-31T11:08:06.331444Z", "shell.execute_reply": "2026-05-31T11:08:06.330422Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Installed equipment cost: 5,000,000 USD\n", "Annual operating cost: 48,145 USD/yr\n", "Net present value: -10,599,997 USD\n" ] } ], "source": [ "tea = qs.TEA(agile_sys, discount_rate=0.05, lifetime=20, simulate_system=False)\n", "print(f'Installed equipment cost: {agile_sys.installed_equipment_cost:,.0f} USD')\n", "print(f'Annual operating cost: {tea.AOC:,.0f} USD/yr')\n", "print(f'Net present value: {tea.NPV:,.0f} USD')" ] }, { "cell_type": "markdown", "id": "s4-lca-md", "metadata": {}, "source": [ "An LCA spans all modes in the same way. Construction impacts are one-time and independent of the operating mode, whereas operational impacts such as electricity use the annualized consumption reported by the agile system. Below we attach construction materials to the plant and supply the annualized electricity, each characterized for global warming potential." ] }, { "cell_type": "code", "execution_count": 29, "id": "s4-lca", "metadata": { "execution": { "iopub.execute_input": "2026-05-31T11:08:06.333443Z", "iopub.status.busy": "2026-05-31T11:08:06.333443Z", "iopub.status.idle": "2026-05-31T11:08:06.341108Z", "shell.execute_reply": "2026-05-31T11:08:06.339910Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Total GWP over 20 yr: 3,964,250 kg CO2-eq\n", " construction: 150,000\n", " operation: 3,814,250\n", "Annualized GWP: 198,212 kg CO2-eq/yr\n" ] } ], "source": [ "GWP = qs.ImpactIndicator('GlobalWarming', alias='GWP', unit='kg CO2-eq')\n", "Concrete = qs.ImpactItem('Concrete', functional_unit='m3', GWP=300.)\n", "Steel = qs.ImpactItem('Steel', functional_unit='kg', GWP=2.)\n", "electricity = qs.ImpactItem('electricity', functional_unit='kWh', GWP=0.5)\n", "\n", "aer.construction = [\n", " qs.Construction(linked_unit=aer, item=Concrete, quantity=300, quantity_unit='m3', lifetime=30),\n", " qs.Construction(linked_unit=aer, item=Steel, quantity=30000, quantity_unit='kg', lifetime=30),\n", "]\n", "\n", "lifetime = 20\n", "annual_electricity = agile_sys.get_electricity_consumption() # kWh/yr, across all modes\n", "lca = qs.LCA(system=agile_sys, lifetime=lifetime,\n", " electricity=annual_electricity*lifetime, simulate_system=False)\n", "print(f'Total GWP over {lifetime} yr: {lca.get_total_impacts()[\"GlobalWarming\"]:,.0f} kg CO2-eq')\n", "print(f' construction: {lca.total_construction_impacts[\"GlobalWarming\"]:,.0f}')\n", "print(f' operation: {lca.get_total_impacts(operation_only=True)[\"GlobalWarming\"]:,.0f}')\n", "print(f'Annualized GWP: {lca.get_total_impacts(annual=True)[\"GlobalWarming\"]:,.0f} kg CO2-eq/yr')" ] }, { "cell_type": "markdown", "id": "s4-pointers", "metadata": {}, "source": [ "
\n", "\n", "**Mode-dependent parameters and further analysis.** An operation parameter can also depend on the mode itself (pass `mode_dependent=True` to `operation_parameter`), and metrics can be summed across modes with `annual_operation_metrics`. See `?qs.AgileSystem` for the full interface. For how the annualized TEA and LCA results are read, interpreted, and exported, see [Tutorial 7 on techno-economic analysis](7_TEA.ipynb) and [Tutorial 8 on life cycle assessment](8_LCA.ipynb).\n", "\n", "
" ] }, { "cell_type": "markdown", "id": "conv-s5", "metadata": {}, "source": [ "## 5. Controlling recycle convergence \n", "\n", "A system with recycle streams is solved iteratively: each unit runs in order, the recycle streams are updated, and the loop repeats until the recycle streams stop changing (within a tolerance). The systems above converged on their own, which is the common case. This section gives some tips on what to do when one does not: how to read the convergence report, the settings that control the solve and their defaults, and what to change first. It is the convergence-control surface that the recycle-convergence note in [14. Modeling Notes & Pitfalls](14_Modeling_Notes_and_Pitfalls.ipynb) points to.\n", "\n", "
\n", "\n", "**Two different kinds of convergence.** This section is about steady-state recycle convergence, the iteration that resolves recycle loops in `simulate()`. Dynamic systems integrate unit states over time instead, where convergence means the ODE solver advancing through the time span. Those settings (the integration method, the time-step tolerances) are separate and covered in [11. Dynamic Simulation](https://qsdsan.readthedocs.io/en/latest/tutorials/11_Dynamic_Simulation.html).\n", "\n", "
" ] }, { "cell_type": "markdown", "id": "conv-5_1-md", "metadata": {}, "source": [ "### 5.1. Reading the convergence report\n", "\n", "After each `simulate()`, the system compares the recycle streams against the tolerances and reports the largest remaining error. This is the block shown under the diagram in Section 2: the first line is the worst component flow-rate error (in kmol/hr, with its value as a percentage of the stream flow in parentheses), the second is the temperature error (in K, with its percentage), and \"after N loops\" is the number of iterations taken.\n", "\n", "When the solver reaches `maxiter` without meeting the tolerances, `simulate()` raises a `RuntimeError` carrying that same report, prefixed with \"could not converge\". The cell below forces that error on purpose with far too strict tolerances and too few iterations, so the traceback below is the error. The cell after it restores the defaults and converges normally again." ] }, { "cell_type": "code", "execution_count": 30, "id": "conv-5_1-code", "metadata": { "execution": { "iopub.execute_input": "2026-05-31T11:08:06.343118Z", "iopub.status.busy": "2026-05-31T11:08:06.343118Z", "iopub.status.idle": "2026-05-31T11:08:07.516727Z", "shell.execute_reply": "2026-05-31T11:08:07.516727Z" }, "tags": [ "raises-exception" ] }, "outputs": [ { "ename": "RuntimeError", "evalue": " could not converge\nHighest convergence error among components in recycle\nstream S2-1 after 2 loops:\n- flow rate 2.04e-01 kmol/hr (20%)\n- temperature 0.00e+00 K (0%)", "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[30]\u001b[39m\u001b[32m, line 10\u001b[39m\n\u001b[32m 6\u001b[39m \n\u001b[32m 7\u001b[39m sys.molar_tolerance = sys.relative_molar_tolerance = \u001b[32m1e-9\u001b[39m \u001b[38;5;66;03m# far too strict\u001b[39;00m\n\u001b[32m 8\u001b[39m sys.temperature_tolerance = sys.relative_temperature_tolerance = \u001b[32m1e-9\u001b[39m\n\u001b[32m 9\u001b[39m sys.maxiter = \u001b[32m2\u001b[39m \u001b[38;5;66;03m# far too few iterations\u001b[39;00m\n\u001b[32m---> \u001b[39m\u001b[32m10\u001b[39m sys.simulate()\n", "\u001b[36mFile \u001b[39m\u001b[32mc:\\Users\\Yalin\\Documents\\Coding\\QSDsan-platform\\.venv\\Lib\\site-packages\\biosteam\\_system.py:3374\u001b[39m, in \u001b[36mSystem.simulate\u001b[39m\u001b[34m(self, update_configuration, units, design_and_cost, **kwargs)\u001b[39m\n\u001b[32m 3354\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, update_configuration: Optional[\u001b[38;5;28mbool\u001b[39m]=\u001b[38;5;28;01mNone\u001b[39;00m, units=\u001b[38;5;28;01mNone\u001b[39;00m, \n\u001b[32m 3355\u001b[39m design_and_cost=\u001b[38;5;28;01mNone\u001b[39;00m, **kwargs):\n\u001b[32m 3356\u001b[39m \u001b[38;5;250m \u001b[39m\u001b[33;03m\"\"\"\u001b[39;00m\n\u001b[32m 3357\u001b[39m \u001b[33;03m If system is dynamic, run the system dynamically. Otherwise, converge \u001b[39;00m\n\u001b[32m 3358\u001b[39m \u001b[33;03m the path of unit operations to steady state. After running/converging \u001b[39;00m\n\u001b[32m (...)\u001b[39m\u001b[32m 3372\u001b[39m \u001b[33;03m \u001b[39;00m\n\u001b[32m 3373\u001b[39m \u001b[33;03m \"\"\"\u001b[39;00m\n\u001b[32m-> \u001b[39m\u001b[32m3374\u001b[39m \u001b[38;5;28;01mwith\u001b[39;00m \u001b[38;5;28mself\u001b[39m.flowsheet:\n\u001b[32m 3375\u001b[39m specifications = \u001b[38;5;28mself\u001b[39m._specifications\n\u001b[32m 3376\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m specifications \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28mself\u001b[39m._running_specifications:\n", "\u001b[36mFile \u001b[39m\u001b[32mc:\\Users\\Yalin\\Documents\\Coding\\QSDsan-platform\\.venv\\Lib\\site-packages\\biosteam\\_flowsheet.py:120\u001b[39m, in \u001b[36mFlowsheet.__exit__\u001b[39m\u001b[34m(self, type, exception, traceback)\u001b[39m\n\u001b[32m 118\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34m__exit__\u001b[39m(\u001b[38;5;28mself\u001b[39m, \u001b[38;5;28mtype\u001b[39m, exception, traceback):\n\u001b[32m 119\u001b[39m main_flowsheet.set_flowsheet(\u001b[38;5;28mself\u001b[39m._temporary_stack.pop())\n\u001b[32m--> \u001b[39m\u001b[32m120\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m exception: \u001b[38;5;28;01mraise\u001b[39;00m exception\n", "\u001b[36mFile \u001b[39m\u001b[32mc:\\Users\\Yalin\\Documents\\Coding\\QSDsan-platform\\.venv\\Lib\\site-packages\\biosteam\\_system.py:3435\u001b[39m, in \u001b[36mSystem.simulate\u001b[39m\u001b[34m(self, update_configuration, units, design_and_cost, **kwargs)\u001b[39m\n\u001b[32m 3429\u001b[39m outputs = \u001b[38;5;28mself\u001b[39m.simulate(\n\u001b[32m 3430\u001b[39m update_configuration=\u001b[38;5;28;01mTrue\u001b[39;00m, \n\u001b[32m 3431\u001b[39m design_and_cost=design_and_cost,\n\u001b[32m 3432\u001b[39m **kwargs\n\u001b[32m 3433\u001b[39m )\n\u001b[32m 3434\u001b[39m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[32m-> \u001b[39m\u001b[32m3435\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m error\n\u001b[32m 3436\u001b[39m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[32m 3437\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m (\u001b[38;5;129;01mnot\u001b[39;00m update_configuration \u001b[38;5;66;03m# Avoid infinite loop\u001b[39;00m\n\u001b[32m 3438\u001b[39m \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;28mself\u001b[39m._connections != \u001b[38;5;28mself\u001b[39m._get_connections()):\n\u001b[32m 3439\u001b[39m \u001b[38;5;66;03m# Connections has been updated within simulation.\u001b[39;00m\n\u001b[32m 3440\u001b[39m \u001b[38;5;66;03m# Must reset path.\u001b[39;00m\n", "\u001b[36mFile \u001b[39m\u001b[32mc:\\Users\\Yalin\\Documents\\Coding\\QSDsan-platform\\.venv\\Lib\\site-packages\\biosteam\\_system.py:3422\u001b[39m, in \u001b[36mSystem.simulate\u001b[39m\u001b[34m(self, update_configuration, units, design_and_cost, **kwargs)\u001b[39m\n\u001b[32m 3420\u001b[39m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[32m 3421\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m-> \u001b[39m\u001b[32m3422\u001b[39m outputs = \u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mconverge\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43m*\u001b[39;49m\u001b[30;43m*\u001b[39;49m\u001b[30;43mkwargs\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n\u001b[32m 3423\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m design_and_cost: \u001b[38;5;28mself\u001b[39m._summary()\n\u001b[32m 3424\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mException\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m error:\n", "\u001b[36mFile \u001b[39m\u001b[32mc:\\Users\\Yalin\\Documents\\Coding\\QSDsan-platform\\.venv\\Lib\\site-packages\\biosteam\\_system.py:3046\u001b[39m, in \u001b[36mSystem.converge\u001b[39m\u001b[34m(self, recycle_data, update_recycle_data)\u001b[39m\n\u001b[32m 3044\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m i \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mrange\u001b[39m(\u001b[38;5;28mself\u001b[39m._N_runs): method()\n\u001b[32m 3045\u001b[39m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[32m-> \u001b[39m\u001b[32m3046\u001b[39m \u001b[30;43mmethod\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n\u001b[32m 3047\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m update_recycle_data:\n\u001b[32m 3048\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m: recycle_data.update()\n", "\u001b[36mFile \u001b[39m\u001b[32mc:\\Users\\Yalin\\Documents\\Coding\\QSDsan-platform\\.venv\\Lib\\site-packages\\biosteam\\_system.py:3002\u001b[39m, in \u001b[36mSystem._solve\u001b[39m\u001b[34m(self)\u001b[39m\n\u001b[32m 3000\u001b[39m data = \u001b[38;5;28mself\u001b[39m._get_recycle_data()\n\u001b[32m 3001\u001b[39m f = \u001b[38;5;28mself\u001b[39m._iter_run_conditional \u001b[38;5;28;01mif\u001b[39;00m conditional \u001b[38;5;28;01melse\u001b[39;00m \u001b[38;5;28mself\u001b[39m._iter_run\n\u001b[32m-> \u001b[39m\u001b[32m3002\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m: \u001b[30;43msolver\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43mf\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43mdata\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43m*\u001b[39;49m\u001b[30;43m*\u001b[39;49m\u001b[30;43mkwargs\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n\u001b[32m 3003\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m (\u001b[38;5;167;01mIndexError\u001b[39;00m, \u001b[38;5;167;01mValueError\u001b[39;00m) \u001b[38;5;28;01mas\u001b[39;00m error:\n\u001b[32m 3004\u001b[39m data = \u001b[38;5;28mself\u001b[39m._get_recycle_data()\n", "\u001b[36mFile \u001b[39m\u001b[32mc:\\Users\\Yalin\\Documents\\Coding\\QSDsan-platform\\.venv\\Lib\\site-packages\\flexsolve\\fixed_point_solvers.py:188\u001b[39m, in \u001b[36mconditional_aitken\u001b[39m\u001b[34m(f, x)\u001b[39m\n\u001b[32m 186\u001b[39m g, condition = f(x)\n\u001b[32m 187\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m condition: \u001b[38;5;28;01mreturn\u001b[39;00m g\n\u001b[32m--> \u001b[39m\u001b[32m188\u001b[39m gg, condition = \u001b[30;43mf\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43mg\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n\u001b[32m 189\u001b[39m x = aitken_iter(x, gg, x - g, gg - g)\n\u001b[32m 190\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m x\n", "\u001b[36mFile \u001b[39m\u001b[32mc:\\Users\\Yalin\\Documents\\Coding\\QSDsan-platform\\.venv\\Lib\\site-packages\\biosteam\\_system.py:2403\u001b[39m, in \u001b[36mSystem._iter_run_conditional\u001b[39m\u001b[34m(self, data)\u001b[39m\n\u001b[32m 2400\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m (\u001b[38;5;28mself\u001b[39m._iter >= \u001b[38;5;28mself\u001b[39m.maxiter\n\u001b[32m 2401\u001b[39m \u001b[38;5;129;01mor\u001b[39;00m (\u001b[38;5;28mself\u001b[39m.maxtime \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;28mself\u001b[39m.timer.elapsed_time > \u001b[38;5;28mself\u001b[39m.maxtime)): \n\u001b[32m 2402\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m.strict_convergence: \n\u001b[32m-> \u001b[39m\u001b[32m2403\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mRuntimeError\u001b[39;00m(\u001b[33mf\u001b[39m\u001b[33m'\u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mrepr\u001b[39m(\u001b[38;5;28mself\u001b[39m)\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m could not converge\u001b[39m\u001b[33m'\u001b[39m + \u001b[38;5;28mself\u001b[39m._error_info())\n\u001b[32m 2404\u001b[39m \u001b[38;5;28;01melse\u001b[39;00m: \n\u001b[32m 2405\u001b[39m not_converged = \u001b[38;5;28;01mFalse\u001b[39;00m\n", "\u001b[31mRuntimeError\u001b[39m: could not converge\nHighest convergence error among components in recycle\nstream S2-1 after 2 loops:\n- flow rate 2.04e-01 kmol/hr (20%)\n- temperature 0.00e+00 K (0%)" ] } ], "source": [ "# Reuse the system built above (sys = external_sys). Save the defaults so the next\n", "# cell can restore them, then force a non-convergence error on purpose: with far too\n", "# strict tolerances and too few iterations, simulate() cannot converge and raises.\n", "saved = (sys.molar_tolerance, sys.relative_molar_tolerance,\n", " sys.temperature_tolerance, sys.relative_temperature_tolerance, sys.maxiter)\n", "\n", "sys.molar_tolerance = sys.relative_molar_tolerance = 1e-9 # far too strict\n", "sys.temperature_tolerance = sys.relative_temperature_tolerance = 1e-9\n", "sys.maxiter = 2 # far too few iterations\n", "sys.simulate()" ] }, { "cell_type": "code", "execution_count": 31, "id": "conv-5_1-restore", "metadata": { "execution": { "iopub.execute_input": "2026-05-31T11:08:07.516727Z", "iopub.status.busy": "2026-05-31T11:08:07.516727Z", "iopub.status.idle": "2026-05-31T11:08:07.525928Z", "shell.execute_reply": "2026-05-31T11:08:07.525928Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Re-converged with the default settings.\n" ] } ], "source": [ "# Restore the saved settings and converge normally again.\n", "(sys.molar_tolerance, sys.relative_molar_tolerance,\n", " sys.temperature_tolerance, sys.relative_temperature_tolerance, sys.maxiter) = saved\n", "sys.simulate()\n", "print('Re-converged with the default settings.')" ] }, { "cell_type": "markdown", "id": "conv-5_2-md", "metadata": {}, "source": [ "### 5.2. The settings that control the solve\n", "\n", "Recycle convergence is governed by a handful of `System` attributes:\n", "\n", "- `molar_tolerance` (kmol/hr) and `relative_molar_tolerance` (a fraction): the absolute and relative flow-rate tolerances.\n", "- `temperature_tolerance` (K) and `relative_temperature_tolerance` (a fraction): the absolute and relative temperature tolerances.\n", "- `maxiter`: the maximum number of iterations before `simulate()` gives up and raises exceptions.\n", "- `method` (also exposed as `converge_method`): the acceleration method used to update the recycle guess between iterations.\n", "\n", "The solve stops once a recycle is within the molar tolerance (absolute or relative) and within the temperature tolerance (absolute or relative). Set any of these attributes directly, or set the common ones together with `set_tolerance(mol=, rmol=, T=, maxiter=)`. The live defaults and the available methods are printed below." ] }, { "cell_type": "code", "execution_count": 32, "id": "conv-5_2-code", "metadata": { "execution": { "iopub.execute_input": "2026-05-31T11:08:07.528256Z", "iopub.status.busy": "2026-05-31T11:08:07.528256Z", "iopub.status.idle": "2026-05-31T11:08:07.534828Z", "shell.execute_reply": "2026-05-31T11:08:07.534828Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "molar_tolerance = 1.0 kmol/hr\n", "relative_molar_tolerance = 0.01\n", "temperature_tolerance = 0.1 K\n", "relative_temperature_tolerance = 0.001\n", "maxiter = 200\n", "method = 'aitken'\n", "available methods = ['aitken', 'wegstein', 'fixedpoint', 'anderson', 'diagbroyden', 'excitingmixing', 'linearmixing', 'broyden1', 'broyden2']\n" ] } ], "source": [ "print(f'molar_tolerance = {sys.molar_tolerance} kmol/hr')\n", "print(f'relative_molar_tolerance = {sys.relative_molar_tolerance}')\n", "print(f'temperature_tolerance = {sys.temperature_tolerance} K')\n", "print(f'relative_temperature_tolerance = {sys.relative_temperature_tolerance}')\n", "print(f'maxiter = {sys.maxiter}')\n", "print(f'method = {sys.method!r}')\n", "print(f'available methods = {list(sys.available_methods)}')" ] }, { "cell_type": "markdown", "id": "conv-5_3-md", "metadata": {}, "source": [ "### 5.3. What to adjust when a system will not converge\n", "\n", "When a system stalls or raises a convergence error, change things roughly in this order:\n", "\n", "1. **Loosen the tolerances** if the defaults are tighter than the analysis needs. The default `molar_tolerance` of 1.0 kmol/hr and `temperature_tolerance` of 0.1 K may be stricter than many wastewater analyses require; a larger value is looser and lets the solve stop sooner.\n", "2. **Improve the recycle initial guess.** The solver starts from whatever is already on the recycle streams, so writing realistic flows onto `sys.recycle` before `simulate()` gives it a closer starting point. This is often the single most effective change for a stiff biokinetic loop.\n", "3. **Raise `maxiter`** if the loop is converging but has not finished in time.\n", "4. **Switch the acceleration method.** The default `'aitken'` is fastest near the solution but can over-correct when the guess is far off; `'wegstein'` is often steadier for an oscillating loop, and `'fixedpoint'` is plain successive substitution (no acceleration), the most robust but slowest." ] }, { "cell_type": "code", "execution_count": 33, "id": "conv-5_3-code", "metadata": { "execution": { "iopub.execute_input": "2026-05-31T11:08:07.534828Z", "iopub.status.busy": "2026-05-31T11:08:07.534828Z", "iopub.status.idle": "2026-05-31T11:08:07.611919Z", "shell.execute_reply": "2026-05-31T11:08:07.611919Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "Highest convergence error among components in recycle\n", "stream S2-1 after 2 loops:\n", "- flow rate 4.82e+00 kmol/hr (39%)\n", "- temperature 0.00e+00 K (0%)\n" ] } ], "source": [ "# 1. Loosen the tolerances (a larger number is looser than the default).\n", "sys.set_tolerance(mol=5.0, T=0.5) # defaults are 1.0 kmol/hr and 0.1 K\n", "\n", "# 2. Seed the recycle stream with a realistic guess before solving.\n", "sys.recycle.imass['H2O'] = ww.imass['H2O'] # the clarifier recycle (S2-1)\n", "\n", "# 3. Allow more iterations if needed.\n", "sys.maxiter = 400\n", "\n", "# 4. Try a steadier acceleration method.\n", "sys.method = 'wegstein'\n", "\n", "sys.simulate()\n", "print(sys._error_info())" ] }, { "cell_type": "markdown", "id": "nav-footer-6_system", "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 }