{
"cells": [
{
"cell_type": "markdown",
"id": "4b625e5a",
"metadata": {},
"source": [
"# `SanUnit` (advanced) \n",
"\n",
"*Click the badge below to try this tutorial interactively in your browser:*\n",
"\n",
"[](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",
" - Subclass `SanUnit` to create a custom unit operation\n",
" - Implement `_run`, `_design`, and `_cost` methods\n",
" - Integrate the new unit into a `System`\n",
"\n",
"- **Prerequisites:** [4. SanUnit (basic)](https://qsdsan.readthedocs.io/en/latest/tutorials/4_SanUnit_basic.html)\n",
"\n",
"- **Covered topics:**\n",
"\n",
" - 1. Basic structure of SanUnit subclasses\n",
" - 2. Making a simple AerobicReactor\n",
" - 3. Other convenient features\n",
"\n",
"> **Companion video.** A walkthrough of this tutorial is available on [YouTube](https://youtu.be/G20J2U8g7Dg), presented by [Hannah Lohman](https://github.com/haclohman). 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": "bcd41d4755ad",
"metadata": {},
"source": [
"\n",
"\n",
"## Setup\n",
"\n",
"Import `QSDsan` and confirm the installed version.\n"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "940d8811",
"metadata": {
"execution": {
"iopub.execute_input": "2026-05-31T12:13:05.769241Z",
"iopub.status.busy": "2026-05-31T12:13:05.768240Z",
"iopub.status.idle": "2026-05-31T12:13:25.612902Z",
"shell.execute_reply": "2026-05-31T12:13:25.612902Z"
}
},
"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": "a271368b",
"metadata": {},
"source": [
"## 1. Basic structure of `SanUnit` subclasses \n",
"Building a custom unit means subclassing `SanUnit`. Assuming you are familiar with the topics covered in the [previous tutorial](https://qsdsan.readthedocs.io/en/latest/tutorials/4_SanUnit_basic.html) on `SanUnit`, we can now learn the specifics of creating subclasses."
]
},
{
"cell_type": "markdown",
"id": "0d509dc3",
"metadata": {},
"source": [
"New to Python classes (the building blocks for subclassing)? Expand the aside below for a refresher, or see the [resources in the tutorials index](https://qsdsan.readthedocs.io/en/latest/tutorials/index.html#new-to-python-or-jupyter)."
]
},
{
"cell_type": "markdown",
"id": "50232c5b",
"metadata": {},
"source": [
"Python Aside: classes, methods, attributes, and properties (click to expand)
\n",
"\n",
"**Classes and instances.** A class bundles data and functions; you create instances from it.\n",
"\n",
"```python\n",
"class Apple:\n",
" kind = 'fruit' # class attribute (shared by all instances)\n",
" def __init__(self, name, color):\n",
" self.name = name # instance attributes (per object)\n",
" self.color = color\n",
" def introduce(self): # instance method (takes self)\n",
" print(f'{self.name} is {self.color}')\n",
"\n",
"gala = Apple('Gala', 'red')\n",
"gala.introduce() # Gala is red\n",
"```\n",
"\n",
"**Method kinds.** Instance methods take `self`; a `@classmethod` takes `cls` (e.g. `Components.load_default`); a `@staticmethod` takes neither. The `@` is a *decorator* — a wrapper that adds behavior.\n",
"\n",
"**Properties.** A `property` looks like an attribute but is computed by getter/setter functions, so you can validate or protect a value (omit the setter to make it read-only):\n",
"\n",
"```python\n",
"class Reactor:\n",
" @property\n",
" def conversion(self): # getter\n",
" return self._conversion\n",
" @conversion.setter\n",
" def conversion(self, i): # setter\n",
" if not 0 <= i <= 1:\n",
" raise AttributeError('conversion must be in [0, 1]')\n",
" self._conversion = i\n",
"```\n",
"\n",
"**Subclassing** reuses a parent's behavior: `class Apple2(Apple): ...` inherits `introduce`. Subclassing `SanUnit` is exactly how you build a custom unit — which is what the rest of this tutorial does.\n",
"\n",
"