From 5706471c7cf20725db9422a030add3adeb415f81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Schm=C3=B6lder?= Date: Tue, 21 May 2024 15:10:42 -0400 Subject: [PATCH] Update solution --- .../00_Introduction_Python.ipynb | 105 ++ .../00_Introduction_Python.md | 86 -- 00 Introduction Python/01_markdown.ipynb | 104 ++ 00 Introduction Python/01_markdown.md | 71 -- .../02_getting_started.ipynb | 773 ++++++++++++ 00 Introduction Python/02_getting_started.md | 337 ----- .../03_numpy_scipy_matplotlib.ipynb | 148 +++ .../03_numpy_scipy_matplotlib.md | 75 -- 01 Introduction/01_CADET_overview.ipynb | 307 +++++ 01 Introduction/01_CADET_overview.md | 169 --- .../01 inlet profiles/01_inlet_profiles.ipynb | 548 +++++++++ .../01 inlet profiles/01_inlet_profiles.md | 273 ----- .../02_inlet_profiles_exercises.ipynb | 315 +++++ .../02_inlet_profiles_exercises.md | 231 ---- .../01_rtd.ipynb | 653 ++++++++++ .../02 residence time distributions/01_rtd.md | 388 ------ .../02_rtd_exercise.ipynb | 260 ++++ .../02_rtd_exercise.md | 179 --- .../03 chemical reactions/01_reactions.ipynb | 292 +++++ .../03 chemical reactions/01_reactions.md | 159 --- .../02_reaction_exercises.ipynb | 307 +++++ .../02_reaction_exercises.md | 196 --- .../04 adsorption/01_adsorption.ipynb | 825 +++++++++++++ .../04 adsorption/01_adsorption.md | 448 ------- .../02_adsorption_exercise.ipynb | 287 +++++ .../04 adsorption/02_adsorption_exercise.md | 225 ---- 02 CADET Fundamentals/Overview.ipynb | 28 + 02 CADET Fundamentals/Overview.md | 19 - .../01_chromatography.ipynb | 843 +++++++++++++ .../01_chromatography.md | 502 -------- .../02_chromatography_exercise.ipynb | 317 +++++ .../02_chromatography_exercise.md | 260 ---- .../01_advanced_chromatography.ipynb | 588 +++++++++ .../01_advanced_chromatography.md | 347 ------ .../02_advanced_chromatography_exercise.ipynb | 277 +++++ .../02_advanced_chromatography_exercise.md | 202 --- .../01_optimization_intro.ipynb | 1084 +++++++++++++++++ .../01_introduction/01_optimization_intro.md | 560 --------- .../02_optimization_intro_exercise.ipynb | 132 ++ .../02_optimization_intro_exercise.md | 85 -- .../01_advanced_optimization.ipynb | 542 +++++++++ .../01_advanced_optimization.md | 301 ----- 05 Parameter Estimation/01_comparison.ipynb | 616 ++++++++++ 05 Parameter Estimation/01_comparison.md | 334 ----- .../01_comparison_exercise.ipynb | 408 +++++++ .../01_comparison_exercise.md | 309 ----- .../experimental_data/generate_data.ipynb | 41 +- .../experimental_data/generate_data.md | 128 -- .../01_fractionation.ipynb | 646 ++++++++++ 06 Process Optimization/01_fractionation.md | 313 ----- .../01_fractionation_exercise.ipynb | 158 +++ .../01_fractionation_exercise.md | 108 -- .../02_process_optimization.ipynb | 338 +++++ .../02_process_optimization.md | 187 --- 99 Misc/Yamamoto's Method/yamamoto.ipynb | 301 +++++ 99 Misc/Yamamoto's Method/yamamoto.md | 181 --- .../Yamamoto's Method/yamamoto_exercise.ipynb | 439 +++++++ .../Yamamoto's Method/yamamoto_exercise.md | 254 ---- 58 files changed, 11650 insertions(+), 6959 deletions(-) create mode 100644 00 Introduction Python/00_Introduction_Python.ipynb delete mode 100644 00 Introduction Python/00_Introduction_Python.md create mode 100644 00 Introduction Python/01_markdown.ipynb delete mode 100644 00 Introduction Python/01_markdown.md create mode 100644 00 Introduction Python/02_getting_started.ipynb delete mode 100644 00 Introduction Python/02_getting_started.md create mode 100644 00 Introduction Python/03_numpy_scipy_matplotlib.ipynb delete mode 100644 00 Introduction Python/03_numpy_scipy_matplotlib.md create mode 100644 01 Introduction/01_CADET_overview.ipynb delete mode 100644 01 Introduction/01_CADET_overview.md create mode 100644 02 CADET Fundamentals/01 inlet profiles/01_inlet_profiles.ipynb delete mode 100644 02 CADET Fundamentals/01 inlet profiles/01_inlet_profiles.md create mode 100644 02 CADET Fundamentals/01 inlet profiles/02_inlet_profiles_exercises.ipynb delete mode 100644 02 CADET Fundamentals/01 inlet profiles/02_inlet_profiles_exercises.md create mode 100644 02 CADET Fundamentals/02 residence time distributions/01_rtd.ipynb delete mode 100644 02 CADET Fundamentals/02 residence time distributions/01_rtd.md create mode 100644 02 CADET Fundamentals/02 residence time distributions/02_rtd_exercise.ipynb delete mode 100644 02 CADET Fundamentals/02 residence time distributions/02_rtd_exercise.md create mode 100644 02 CADET Fundamentals/03 chemical reactions/01_reactions.ipynb delete mode 100644 02 CADET Fundamentals/03 chemical reactions/01_reactions.md create mode 100644 02 CADET Fundamentals/03 chemical reactions/02_reaction_exercises.ipynb delete mode 100644 02 CADET Fundamentals/03 chemical reactions/02_reaction_exercises.md create mode 100644 02 CADET Fundamentals/04 adsorption/01_adsorption.ipynb delete mode 100644 02 CADET Fundamentals/04 adsorption/01_adsorption.md create mode 100644 02 CADET Fundamentals/04 adsorption/02_adsorption_exercise.ipynb delete mode 100644 02 CADET Fundamentals/04 adsorption/02_adsorption_exercise.md create mode 100644 02 CADET Fundamentals/Overview.ipynb delete mode 100644 02 CADET Fundamentals/Overview.md create mode 100644 03 Process Simulation/01 Simple Chromatographic Processes/01_chromatography.ipynb delete mode 100644 03 Process Simulation/01 Simple Chromatographic Processes/01_chromatography.md create mode 100644 03 Process Simulation/01 Simple Chromatographic Processes/02_chromatography_exercise.ipynb delete mode 100644 03 Process Simulation/01 Simple Chromatographic Processes/02_chromatography_exercise.md create mode 100644 03 Process Simulation/02 Advanced Operating Modes/01_advanced_chromatography.ipynb delete mode 100644 03 Process Simulation/02 Advanced Operating Modes/01_advanced_chromatography.md create mode 100644 03 Process Simulation/02 Advanced Operating Modes/02_advanced_chromatography_exercise.ipynb delete mode 100644 03 Process Simulation/02 Advanced Operating Modes/02_advanced_chromatography_exercise.md create mode 100644 04 Optimization Fundamentals/01_introduction/01_optimization_intro.ipynb delete mode 100644 04 Optimization Fundamentals/01_introduction/01_optimization_intro.md create mode 100644 04 Optimization Fundamentals/01_introduction/02_optimization_intro_exercise.ipynb delete mode 100644 04 Optimization Fundamentals/01_introduction/02_optimization_intro_exercise.md create mode 100644 04 Optimization Fundamentals/02_advanced_techniques/01_advanced_optimization.ipynb delete mode 100644 04 Optimization Fundamentals/02_advanced_techniques/01_advanced_optimization.md create mode 100644 05 Parameter Estimation/01_comparison.ipynb delete mode 100644 05 Parameter Estimation/01_comparison.md create mode 100644 05 Parameter Estimation/01_comparison_exercise.ipynb delete mode 100644 05 Parameter Estimation/01_comparison_exercise.md delete mode 100644 05 Parameter Estimation/experimental_data/generate_data.md create mode 100644 06 Process Optimization/01_fractionation.ipynb delete mode 100644 06 Process Optimization/01_fractionation.md create mode 100644 06 Process Optimization/01_fractionation_exercise.ipynb delete mode 100644 06 Process Optimization/01_fractionation_exercise.md create mode 100644 06 Process Optimization/02_process_optimization.ipynb delete mode 100644 06 Process Optimization/02_process_optimization.md create mode 100644 99 Misc/Yamamoto's Method/yamamoto.ipynb delete mode 100644 99 Misc/Yamamoto's Method/yamamoto.md create mode 100644 99 Misc/Yamamoto's Method/yamamoto_exercise.ipynb delete mode 100644 99 Misc/Yamamoto's Method/yamamoto_exercise.md diff --git a/00 Introduction Python/00_Introduction_Python.ipynb b/00 Introduction Python/00_Introduction_Python.ipynb new file mode 100644 index 0000000..9eb56de --- /dev/null +++ b/00 Introduction Python/00_Introduction_Python.ipynb @@ -0,0 +1,105 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "8e86c514", + "metadata": {}, + "source": [ + "# Introduction to Python\n", + "\n", + "## Why Python?\n", + "- Simplicity: Easy to code, easy to read.\n", + "- Availability: Free and open source and compatible with all major operating systems.\n", + "- \"Batteries included\": Robust standard library provides wide range of modules to add functionality to the Python application.\n", + "- Excellent third party libraries:\n", + "\n", + "***The Python ecosystem:***\n", + "\n", + "```{figure} ./images/python_ecosystem.png\n", + ":alt: Python Ecosystem\n", + ":width: 500px\n", + ":align: center\n", + "```\n", + "\n", + "- In this course we will be using:\n", + " - [NumPy](https://www.numpy.org/) for array-computing\n", + " - [SciPy](https://www.scipy.org/) for numerical routines\n", + " - [Matplotlib](https://www.matplotlib.org/) for data visualization\n", + "\n", + "- Widespread Adoption in Science and Education:\n", + "\n", + "\n", + "```{figure} ./images/stackoverflow-growth.png\n", + ":alt: Python Growth\n", + ":width: 500px\n", + ":align: center\n", + "```\n", + "\n", + "### Tutorial objectives\n", + "This tutorial covers the following topics:\n", + "- Installation of Python\n", + "- `help`, `print`, `type`\n", + "- Variables\n", + "- Operators\n", + "- Basic Functions\n", + "- Import of packages\n", + "- Introduction to numpy\n", + "- Introduction to matplotlib" + ] + }, + { + "cell_type": "markdown", + "id": "a158bb04", + "metadata": {}, + "source": [ + "___" + ] + }, + { + "cell_type": "markdown", + "id": "7f6a7f60", + "metadata": {}, + "source": [ + "## Python Installation\n", + "- In this course, we will be using the Conda Python distribution. Conda is an open source package management system and environment management system that runs on Windows, macOS and Linux. Conda quickly installs, runs and updates packages and their dependencies completely seprate from any system Python installations and withoud needing administrative rights.\n", + "- Download Miniconda from [here](https://docs.conda.io/en/latest/miniconda.html)\n", + "- Installation remarks:\n", + " - Accept License Agreement\n", + " - Select *Install Just Me*\n", + " - Select Destination Folder: use recommended\n", + " - Do *not* add Anaconda to your PATH\n", + " - Click install\n", + " - To install additional packages, open the anaconda prompt from the windows menu and enter:\n", + " ```\n", + " conda install numpy scipy matplotlib jupyterlab\n", + " ```\n", + "- The tutorial and exercises will be presented using jupyter notebooks. Jupyterlab is an open-source web application that allows you to create and share documents that contain live code, equations, visualizations and narrative text.\n", + " - Markdown cells are used for formatted text\n", + " - Code cells contain executable code\n", + "\n", + "***Task:*** Start Jupyterlab Server by entering ``` jupyter-lab ``` in the anaconda prompt, create a new notebook and a new cell and convert the type to 'Markdown'." + ] + }, + { + "cell_type": "markdown", + "id": "4afc4cfc", + "metadata": {}, + "source": [ + "## Further Watching\n", + "You can find plenty of great resources for (Python) programming online.\n", + "- [Corey Schafer](https://www.youtube.com/user/schafer5) is focused on general programming aspectes, mainly in Python, but also has good introductions to *Bash*, *Git*, *SQL*, and many more.\n", + "- [APMonitor](https://www.youtube.com/user/APMonitorCom) has a lot of engineering examples in *Python*, *Matlab* or on *paper*. In fact, some of this course's exercises are based heavily on his work.\n", + "- [3Blue1Brown](https://www.youtube.com/channel/UCYO_jab_esuFRV4b17AJtAw) makes great videos series on a broad range of (physical) math topics such as *linear algebra*, *calculus*, and *differential equations*." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/00 Introduction Python/00_Introduction_Python.md b/00 Introduction Python/00_Introduction_Python.md deleted file mode 100644 index 51ec957..0000000 --- a/00 Introduction Python/00_Introduction_Python.md +++ /dev/null @@ -1,86 +0,0 @@ ---- -jupytext: - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.15.2 -kernelspec: - display_name: Python 3 (ipykernel) - language: python - name: python3 ---- - -# Introduction to Python - -## Why Python? -- Simplicity: Easy to code, easy to read. -- Availability: Free and open source and compatible with all major operating systems. -- "Batteries included": Robust standard library provides wide range of modules to add functionality to the Python application. -- Excellent third party libraries: - -***The Python ecosystem:*** - -```{figure} ./images/python_ecosystem.png -:alt: Python Ecosystem -:width: 500px -:align: center -``` - -- In this course we will be using: - - [NumPy](https://www.numpy.org/) for array-computing - - [SciPy](https://www.scipy.org/) for numerical routines - - [Matplotlib](https://www.matplotlib.org/) for data visualization - -- Widespread Adoption in Science and Education: - - -```{figure} ./images/stackoverflow-growth.png -:alt: Python Growth -:width: 500px -:align: center -``` - -### Tutorial objectives -This tutorial covers the following topics: -- Installation of Python -- `help`, `print`, `type` -- Variables -- Operators -- Basic Functions -- Import of packages -- Introduction to numpy -- Introduction to matplotlib - -+++ - -___ - -+++ - -## Python Installation -- In this course, we will be using the Conda Python distribution. Conda is an open source package management system and environment management system that runs on Windows, macOS and Linux. Conda quickly installs, runs and updates packages and their dependencies completely seprate from any system Python installations and withoud needing administrative rights. -- Download Miniconda from [here](https://docs.conda.io/en/latest/miniconda.html) -- Installation remarks: - - Accept License Agreement - - Select *Install Just Me* - - Select Destination Folder: use recommended - - Do *not* add Anaconda to your PATH - - Click install - - To install additional packages, open the anaconda prompt from the windows menu and enter: - ``` - conda install numpy scipy matplotlib jupyterlab - ``` -- The tutorial and exercises will be presented using jupyter notebooks. Jupyterlab is an open-source web application that allows you to create and share documents that contain live code, equations, visualizations and narrative text. - - Markdown cells are used for formatted text - - Code cells contain executable code - -***Task:*** Start Jupyterlab Server by entering ``` jupyter-lab ``` in the anaconda prompt, create a new notebook and a new cell and convert the type to 'Markdown'. - -+++ - -## Further Watching -You can find plenty of great resources for (Python) programming online. -- [Corey Schafer](https://www.youtube.com/user/schafer5) is focused on general programming aspectes, mainly in Python, but also has good introductions to *Bash*, *Git*, *SQL*, and many more. -- [APMonitor](https://www.youtube.com/user/APMonitorCom) has a lot of engineering examples in *Python*, *Matlab* or on *paper*. In fact, some of this course's exercises are based heavily on his work. -- [3Blue1Brown](https://www.youtube.com/channel/UCYO_jab_esuFRV4b17AJtAw) makes great videos series on a broad range of (physical) math topics such as *linear algebra*, *calculus*, and *differential equations*. diff --git a/00 Introduction Python/01_markdown.ipynb b/00 Introduction Python/01_markdown.ipynb new file mode 100644 index 0000000..cceb01d --- /dev/null +++ b/00 Introduction Python/01_markdown.ipynb @@ -0,0 +1,104 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "4de5a21d", + "metadata": {}, + "source": [ + "## Markdown\n", + "Markdown is a lightweight markup language with plain text formatting syntax. You can find more information and examples [here](https://en.wikipedia.org/wiki/Markdown).\n", + "___\n", + "\n", + "### Example: Raw input" + ] + }, + { + "cell_type": "raw", + "id": "da8dadae", + "metadata": {}, + "source": [ + "It's very easy to make some words **bold** and other words *italic* with Markdown.\n", + "\n", + "You can insert [links](https://www.example.com),\n", + "\n", + "create lists\n", + "- item 1\n", + "- item 2\n", + "\n", + "and Headlines.\n", + "# Heading\n", + "## Sub-heading\n", + "\n", + "Also, mathematical equations are supported using KaTeX notation:\n", + "$\n", + "\\frac{\\partial c(z,t)}{\\partial t} + F \\frac{\\partial q(z,t)}{\\partial t} + u \\frac{\\partial c(z,t)}{\\partial z} = 0\n", + "$" + ] + }, + { + "cell_type": "markdown", + "id": "0a4f08a5", + "metadata": {}, + "source": [ + "### Example: Output:\n", + "It's very easy to make some words **bold** and other words *italic* with Markdown.\n", + "\n", + "You can insert [links](https://www.example.com),\n", + "\n", + "create lists\n", + "- item 1\n", + "- item 2\n", + "\n", + "and Headlines.\n", + "# Heading\n", + "## Sub-heading\n", + "\n", + "Also, mathematical equations are supported using KaTeX notation:\n", + "$\n", + "\\frac{\\partial c(z,t)}{\\partial t} + F \\frac{\\partial q(z,t)}{\\partial t} + u \\frac{\\partial c(z,t)}{\\partial z} = 0\n", + "$" + ] + }, + { + "cell_type": "markdown", + "id": "45545c69", + "metadata": {}, + "source": [ + "___" + ] + }, + { + "cell_type": "markdown", + "id": "72265331", + "metadata": {}, + "source": [ + "***Task:*** Write a sentence using **bold**, *italic*, as well as ***bold and italic*** highlighting." + ] + }, + { + "cell_type": "markdown", + "id": "7bd73023", + "metadata": {}, + "source": [ + "***Task:*** Download an image and insert a link with the alternative text 'Figure 1'." + ] + }, + { + "cell_type": "markdown", + "id": "0ff58696", + "metadata": {}, + "source": [ + "***Task:*** Write down the equation for your favorite adsorption isotherm." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/00 Introduction Python/01_markdown.md b/00 Introduction Python/01_markdown.md deleted file mode 100644 index 6a0c11f..0000000 --- a/00 Introduction Python/01_markdown.md +++ /dev/null @@ -1,71 +0,0 @@ ---- -jupytext: - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.15.2 -kernelspec: - display_name: Python 3 - language: python - name: python3 ---- - -## Markdown -Markdown is a lightweight markup language with plain text formatting syntax. You can find more information and examples [here](https://en.wikipedia.org/wiki/Markdown). -___ - -### Example: Raw input - -```{raw-cell} -It's very easy to make some words **bold** and other words *italic* with Markdown. - -You can insert [links](https://www.example.com), - -create lists -- item 1 -- item 2 - -and Headlines. -# Heading -## Sub-heading - -Also, mathematical equations are supported using KaTeX notation: -$ -\frac{\partial c(z,t)}{\partial t} + F \frac{\partial q(z,t)}{\partial t} + u \frac{\partial c(z,t)}{\partial z} = 0 -$ -``` - -### Example: Output: -It's very easy to make some words **bold** and other words *italic* with Markdown. - -You can insert [links](https://www.example.com), - -create lists -- item 1 -- item 2 - -and Headlines. -# Heading -## Sub-heading - -Also, mathematical equations are supported using KaTeX notation: -$ -\frac{\partial c(z,t)}{\partial t} + F \frac{\partial q(z,t)}{\partial t} + u \frac{\partial c(z,t)}{\partial z} = 0 -$ - -+++ - -___ - -+++ - -***Task:*** Write a sentence using **bold**, *italic*, as well as ***bold and italic*** highlighting. - -+++ - -***Task:*** Download an image and insert a link with the alternative text 'Figure 1'. - -+++ - -***Task:*** Write down the equation for your favorite adsorption isotherm. diff --git a/00 Introduction Python/02_getting_started.ipynb b/00 Introduction Python/02_getting_started.ipynb new file mode 100644 index 0000000..895d640 --- /dev/null +++ b/00 Introduction Python/02_getting_started.ipynb @@ -0,0 +1,773 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "6818276e", + "metadata": {}, + "source": [ + "## Getting started with Python\n", + "\n", + "### Hello World!\n", + "The simplest program in Python consists of a line that tells the computer a command. Traditionally, the first program of every programmer in every new language prints \"Hello World!\"" + ] + }, + { + "cell_type": "markdown", + "id": "b1f81cb5", + "metadata": {}, + "source": [ + "***Task:*** Change ```'Hello world!'``` to a different text message." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e4c65a2a", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "print('Hello world!')" + ] + }, + { + "cell_type": "markdown", + "id": "66e53657", + "metadata": {}, + "source": [ + "### Help me!\n", + "The most important thing to learn in programming is finding help. The easiest way is to to use the ```help()``` function. If you get stuck, search for answers online in the online documentation of [Python](https://docs.python.org/3/) or in forums such as [Stackoverflow](https://stackoverflow.com/questions/tagged/python). Chances are, other programmers will have had the same problem as you have!" + ] + }, + { + "cell_type": "markdown", + "id": "a96c968f", + "metadata": {}, + "source": [ + "***Task:*** Show the help documentation for the ```max``` function instead of the ```print``` function. Based on the help, use the ```max``` function to find the highest value of two numbers: ```5``` and ```2```." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b2f1b65b", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "help(print)" + ] + }, + { + "cell_type": "markdown", + "id": "25b3ae13", + "metadata": {}, + "source": [ + "### Python as a calculator\n", + "\n", + "Python can be used just like a calculator. Enter an expression and the interpreter returns the result. The syntax is simple: the operators ```+```, ```-```, ```*```, and ```/``` act as you would expect. Round brackets ```( )``` can be used to group expressions." + ] + }, + { + "cell_type": "markdown", + "id": "75c247a2", + "metadata": {}, + "source": [ + "***Task:*** Play around with the interpreter and enter some equations!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bc3799df", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "1 + 1" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ba0363ee", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "(50 * 5 - 6) / 3" + ] + }, + { + "cell_type": "markdown", + "id": "b0bc7462", + "metadata": {}, + "source": [ + "### Operators\n", + "\n", + "We've already seen some operators. Operators are used to transform, compare, join, substract, etc. Below is a list of operators in descending order of precedence. When there are no parenthesis to guide the precedence, this order will be assumed.\n", + "\n", + "| Operator | Description |\n", + "| --- | --- |\n", + "| ** | Exponent |\n", + "| *, /, //, % | Multiplication, Division, Floor division, Modulus |\n", + "| +, - | Addition, Subtraction |\n", + "| <=, <, >, >= | Comparison |\n", + "| =, %=, /=, //=, -=, +=, *=, **= | Assignment |\n", + "| is, is not | Identity |\n", + "| in, not in | Membership |\n", + "| not, or, and | Logical |" + ] + }, + { + "cell_type": "markdown", + "id": "ee90b456", + "metadata": {}, + "source": [ + "***Example:***" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2efe3986", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "# Area of unit circle. <-- Note the '#', it makes everything after that a comment, i.e. it will NOT be executed\n", + "3.1415926/4 * 2**2" + ] + }, + { + "cell_type": "markdown", + "id": "6b57c307", + "metadata": {}, + "source": [ + "***Example:***" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7642f66c", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "1 == 0" + ] + }, + { + "cell_type": "markdown", + "id": "77685ebf", + "metadata": {}, + "source": [ + "***Task:*** Calculate the volume of the unit sphere." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ffe62408", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "id": "7c6f2302", + "metadata": {}, + "source": [ + "***Task:*** Determine whether $35^2$ is greater than $2^{10}$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bfc8cfa0", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "id": "29d931c5", + "metadata": {}, + "source": [ + "### Variables\n", + "\n", + "Variables are reserved memory locations to store values. By assigning different data types to variables, you can store integers, decimals or characters in these variables. Python variables do not need explicit declaration to reserve memory space. The declaration happens automatically when you assign a value to a variable using the equal sign (```=```).\n", + "\n", + "The operand to the left of the ```=``` operator is the name of the variable and the operand to the right of the ```=``` operator is the value stored in the variable. A variable name must begin with a letter or underscore but not with a number or other special characters. A variable name must not have a space and lowercase or uppercase are permitted." + ] + }, + { + "cell_type": "markdown", + "id": "8a51d122", + "metadata": {}, + "source": [ + "***Example:***" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b369738c", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "answer = 42\n", + "print(answer)" + ] + }, + { + "cell_type": "markdown", + "id": "a9c8a8c0", + "metadata": {}, + "source": [ + "***Task:*** Correct the following errors in the variable names and print their values." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3c4174ac", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "1diameter = 1\n", + "length! = 10\n", + "circle_area = 3.1415926/4* diameter**2\n", + "#bar = 4" + ] + }, + { + "cell_type": "markdown", + "id": "92c01cc7", + "metadata": {}, + "source": [ + "***Task:*** Write an equation for the volume of a cylinder using predefined variables." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4350633b", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "id": "a76f58e4", + "metadata": {}, + "source": [ + "### Object Orientation/Types\n", + "Object-oriented Programming (OOP), is a programming paradigm which provides means of structuring programs so that properties and behaviors are bundled into individual objects. For instance, an object could represent a person with a name property, age, address, etc., with behaviors like walking, talking, breathing, and running. Or an email with properties like recipient list, subject, body, etc., and behaviors like adding attachments and sending.\n", + "\n", + "A deeper introduction to OOP is out of scope for this course. However, it is important to know that in Python *everything* is an object. This means, it is of a certain *type* and every type brings with it certain behaviour. Python has five standard data types and we've already met some (subclasses) of them:\n", + "- Numbers\n", + "- String\n", + "- List\n", + "- Tuple\n", + "- Dictionary\n", + "\n", + "The type can be determined using the ```type``` function." + ] + }, + { + "cell_type": "markdown", + "id": "e1e8d557", + "metadata": {}, + "source": [ + "***Example:***" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "022621da", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "foo = 2.0\n", + "print(type(foo))" + ] + }, + { + "cell_type": "markdown", + "id": "9c5b0163", + "metadata": {}, + "source": [ + "***Task:*** Print the variable value and type for ```answer```, and ```file_name```." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8d3f248d", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "answer = 42\n", + "file_name = \"readme.md\"" + ] + }, + { + "cell_type": "markdown", + "id": "13102acf", + "metadata": {}, + "source": [ + "Almost everything in Python has *attributes* and *methods*." + ] + }, + { + "cell_type": "markdown", + "id": "7a0bb307", + "metadata": {}, + "source": [ + "***Example:*** Complex numbers are an extension of the familiar real number system in which all numbers are expressed as a sum of a real part and an imaginary part. Imaginary numbers are real multiples of the imaginary unit (the square root of -1), written with $j$ in Python. You can access the real and imaginary parts of complex numbers using ```real``` and ```imganiary``` parts." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "83c70b5c", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "c = complex(3,2)\n", + "print(type(c))\n", + "print(c.real)\n", + "print(c.imag)" + ] + }, + { + "cell_type": "markdown", + "id": "a1ce2aa6", + "metadata": {}, + "source": [ + "***Task:*** The method ```upper()``` returns a copy of the string in which all case-based characters have been uppercased. Use this method to capitalize a string variable." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bd41553b", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "id": "89861667", + "metadata": {}, + "source": [ + "### Containers\n", + "Lists are the most versatile compound data type for grouping together values in Python. A list contains items separated by commas and enclosed within square brackets (```[]```). The values stored in a list can be accessed using the slice operator (```[index]```, ```[start:end]```) with indexes starting at ***0*** in the beginning of the list and working their way to end ***-1***." + ] + }, + { + "cell_type": "markdown", + "id": "84ee0693", + "metadata": {}, + "source": [ + "***Example:***" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "da1bfb1f", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "my_list = ['abcd', 786 , 2.23, 'john', 70.2]\n", + "print(my_list)\n", + "print(my_list[0])" + ] + }, + { + "cell_type": "markdown", + "id": "74f89d00", + "metadata": {}, + "source": [ + "***Task:*** Print elements starting from 3rd element." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c0f86eae", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "id": "8f29d0c7", + "metadata": {}, + "source": [ + "***Task:*** Append ```99``` to the list using the ```append()``` method." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8008e4b3", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "id": "1fec61c1", + "metadata": {}, + "source": [ + "### Dictionaries\n", + "\n", + "Dictionaries in Python are unordered collections of key-value pairs enclosed within curly brackets (`{}`). Unlike lists, which are indexed by numbers, dictionaries are indexed by\n", + "keys, which can be either numbers or strings." + ] + }, + { + "cell_type": "markdown", + "id": "c9881901", + "metadata": {}, + "source": [ + "***Example:***" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6c03cfa8", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "my_dict = {'name': 'Cadet', 'version': '4.1.0', 'nUsers': 500}\n", + "print(my_dict)\n", + "print(my_dict['name'])\n", + "my_dict['nDownloads'] = 2000\n", + "print(my_dict['nDownloads'])" + ] + }, + { + "cell_type": "markdown", + "id": "f25fbe87", + "metadata": {}, + "source": [ + "Dictionaries, like lists, can be nested." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fec5bdec", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "my_dict = {'name': 'Cadet', 'version': '4.1.0', 'nUsers': 500}\n", + "my_dict['stats'] = { 'github_stars': 1000, 'downloads': 2000, 'issues': 3}\n", + "print(my_dict)" + ] + }, + { + "cell_type": "markdown", + "id": "d45b61cd", + "metadata": {}, + "source": [ + "#### Addict\n", + "\n", + "Since the syntax to traverse deeply nested dictionaries can be quite tedious, we can use the package `addict` to simplify it to the dot notation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ab5506c6", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from addict import Dict\n", + "\n", + "my_new_dict = Dict(my_dict)\n", + "print(my_new_dict.stats)\n", + "my_new_dict.stats.pull_requests = 10\n", + "print(my_new_dict.stats)" + ] + }, + { + "cell_type": "markdown", + "id": "386ac540", + "metadata": {}, + "source": [ + "### Python syntax\n", + "\n", + "In Python code blocks are structured by indentation level. It is is a language requirement not a matter of style. This principle makes it easier to read because it eliminates most of braces ```{ }``` and ```end``` statements which makes Python code much more readable. All statements with the same distance from left belong to the same block of code, i.e. the statements within a block line up vertically. If a block has to be more deeply nested, it is simply indented further to the right.\n", + "\n", + "Loops and Conditional statements end with a colon ```:```. The same is true for functions and other structures introducing blocks." + ] + }, + { + "cell_type": "markdown", + "id": "ed8cc61c", + "metadata": {}, + "source": [ + "### Conditions\n", + "\n", + "Conditional statements are used to direct the flow of the program to different commands based on whether a statement is ```True``` or ```False```. A boolean (```True``` or ```False```) is used to direct the flow with an ```if```, ```elif``` (else if), or ```else``` parts to the statement." + ] + }, + { + "cell_type": "markdown", + "id": "635fb340", + "metadata": {}, + "source": [ + "***Example:***" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "00dc3d3b", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "x = 4\n", + "if x < 3:\n", + " print('less than 3')\n", + "elif x<4:\n", + " print('between 3 and 4')\n", + "else:\n", + " print('greater than 4')" + ] + }, + { + "cell_type": "markdown", + "id": "a5821a7f", + "metadata": {}, + "source": [ + "***Task:*** Write a conditional statement that checks whether the string ```'spam'``` is in ```menu```.\n", + "**Hint:** Check the operators list for membership statements." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f277ace0", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "menu = ['eggs','bacon']" + ] + }, + { + "cell_type": "markdown", + "id": "ab81515e", + "metadata": {}, + "source": [ + "### Loops\n", + "Often, we don't know (or care) how many elements are in a container (e.g. list). We just want to perform an operation on every element of the container. The ```for``` statement in Python differs a bit from what you may be used to in C or Pascal. Python’s ```for``` statement iterates over the items of any sequence (e.g. a list or a string), in the order that they appear in the sequence." + ] + }, + { + "cell_type": "markdown", + "id": "d0f0f8cb", + "metadata": {}, + "source": [ + "***Example:***" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "acdf0985", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "list_of_primes = [2, 3, 5, 7]\n", + "for element in list_of_primes:\n", + " print(element)" + ] + }, + { + "cell_type": "markdown", + "id": "a177e3e6", + "metadata": {}, + "source": [ + "***Task:*** Create a list with strings, iterate over all elements and print the string and the length of the string using the ```len``` function." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3e12ad53", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "id": "89a42576", + "metadata": {}, + "source": [ + "## Functions\n", + "A function is a block of organized, reusable code that is used to perform a single, related action. Functions provide better modularity for your application and a high degree of code reusing. There are built-in functions like ```print()```, ```help()```, etc. but it is possible to create your own functions. These functions are called user-defined functions.\n", + "A function definition describes what is to be calculated once the function is called. The keyword ```def``` introduces a function definition. It must be followed by the function name and the parenthesized list of formal parameters. The statements that form the body of the function start at the next line, and must be indented.\n", + "\n", + "***Note***, the function is not evaluated when defined!" + ] + }, + { + "cell_type": "markdown", + "id": "111f9730", + "metadata": {}, + "source": [ + "***Example:***" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "518a56eb", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "def circle_area(diameter):\n", + " return 3.1415926/4 * diameter**2\n", + "\n", + "area = circle_area(3)\n", + "print(area)" + ] + }, + { + "cell_type": "markdown", + "id": "9cc6003d", + "metadata": {}, + "source": [ + "***Task:*** Define a function that returns the volume of a cylinder as a function of diameter and length." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5f6b82f9", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/00 Introduction Python/02_getting_started.md b/00 Introduction Python/02_getting_started.md deleted file mode 100644 index 3c97b5e..0000000 --- a/00 Introduction Python/02_getting_started.md +++ /dev/null @@ -1,337 +0,0 @@ ---- -jupytext: - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.15.2 -kernelspec: - display_name: Python 3 (ipykernel) - language: python - name: python3 ---- - -## Getting started with Python - -### Hello World! -The simplest program in Python consists of a line that tells the computer a command. Traditionally, the first program of every programmer in every new language prints "Hello World!" - -+++ - -***Task:*** Change ```'Hello world!'``` to a different text message. - -```{code-cell} ipython3 -:tags: [solution] -print('Hello world!') -``` - -### Help me! -The most important thing to learn in programming is finding help. The easiest way is to to use the ```help()``` function. If you get stuck, search for answers online in the online documentation of [Python](https://docs.python.org/3/) or in forums such as [Stackoverflow](https://stackoverflow.com/questions/tagged/python). Chances are, other programmers will have had the same problem as you have! - -+++ - -***Task:*** Show the help documentation for the ```max``` function instead of the ```print``` function. Based on the help, use the ```max``` function to find the highest value of two numbers: ```5``` and ```2```. - -```{code-cell} ipython3 -:tags: [solution] -help(print) -``` - -### Python as a calculator - -Python can be used just like a calculator. Enter an expression and the interpreter returns the result. The syntax is simple: the operators ```+```, ```-```, ```*```, and ```/``` act as you would expect. Round brackets ```( )``` can be used to group expressions. - -+++ - -***Task:*** Play around with the interpreter and enter some equations! - -```{code-cell} ipython3 -:tags: [solution] -1 + 1 -``` - -```{code-cell} ipython3 -:tags: [solution] -(50 * 5 - 6) / 3 -``` - -### Operators - -We've already seen some operators. Operators are used to transform, compare, join, substract, etc. Below is a list of operators in descending order of precedence. When there are no parenthesis to guide the precedence, this order will be assumed. - -| Operator | Description | -| --- | --- | -| ** | Exponent | -| *, /, //, % | Multiplication, Division, Floor division, Modulus | -| +, - | Addition, Subtraction | -| <=, <, >, >= | Comparison | -| =, %=, /=, //=, -=, +=, *=, **= | Assignment | -| is, is not | Identity | -| in, not in | Membership | -| not, or, and | Logical | - -+++ - -***Example:*** - -```{code-cell} ipython3 -:tags: [solution] -# Area of unit circle. <-- Note the '#', it makes everything after that a comment, i.e. it will NOT be executed -3.1415926/4 * 2**2 -``` - -***Example:*** - -```{code-cell} ipython3 -:tags: [solution] -1 == 0 -``` - -***Task:*** Calculate the volume of the unit sphere. - -```{code-cell} ipython3 -:tags: [solution] - -``` - -***Task:*** Determine whether $35^2$ is greater than $2^{10}$. - -```{code-cell} ipython3 -:tags: [solution] - -``` - -### Variables - -Variables are reserved memory locations to store values. By assigning different data types to variables, you can store integers, decimals or characters in these variables. Python variables do not need explicit declaration to reserve memory space. The declaration happens automatically when you assign a value to a variable using the equal sign (```=```). - -The operand to the left of the ```=``` operator is the name of the variable and the operand to the right of the ```=``` operator is the value stored in the variable. A variable name must begin with a letter or underscore but not with a number or other special characters. A variable name must not have a space and lowercase or uppercase are permitted. - -+++ - -***Example:*** - -```{code-cell} ipython3 -:tags: [solution] -answer = 42 -print(answer) -``` - -***Task:*** Correct the following errors in the variable names and print their values. - -```{code-cell} ipython3 -:tags: [solution] -1diameter = 1 -length! = 10 -circle_area = 3.1415926/4* diameter**2 -#bar = 4 -``` - -***Task:*** Write an equation for the volume of a cylinder using predefined variables. - -```{code-cell} ipython3 -:tags: [solution] - -``` - -### Object Orientation/Types -Object-oriented Programming (OOP), is a programming paradigm which provides means of structuring programs so that properties and behaviors are bundled into individual objects. For instance, an object could represent a person with a name property, age, address, etc., with behaviors like walking, talking, breathing, and running. Or an email with properties like recipient list, subject, body, etc., and behaviors like adding attachments and sending. - -A deeper introduction to OOP is out of scope for this course. However, it is important to know that in Python *everything* is an object. This means, it is of a certain *type* and every type brings with it certain behaviour. Python has five standard data types and we've already met some (subclasses) of them: -- Numbers -- String -- List -- Tuple -- Dictionary - -The type can be determined using the ```type``` function. - -+++ - -***Example:*** - -```{code-cell} ipython3 -:tags: [solution] -foo = 2.0 -print(type(foo)) -``` - -***Task:*** Print the variable value and type for ```answer```, and ```file_name```. - -```{code-cell} ipython3 -:tags: [solution] -answer = 42 -file_name = "readme.md" -``` - -Almost everything in Python has *attributes* and *methods*. - -+++ - -***Example:*** Complex numbers are an extension of the familiar real number system in which all numbers are expressed as a sum of a real part and an imaginary part. Imaginary numbers are real multiples of the imaginary unit (the square root of -1), written with $j$ in Python. You can access the real and imaginary parts of complex numbers using ```real``` and ```imganiary``` parts. - -```{code-cell} ipython3 -:tags: [solution] -c = complex(3,2) -print(type(c)) -print(c.real) -print(c.imag) -``` - -***Task:*** The method ```upper()``` returns a copy of the string in which all case-based characters have been uppercased. Use this method to capitalize a string variable. - -```{code-cell} ipython3 -:tags: [solution] - -``` - -### Containers -Lists are the most versatile compound data type for grouping together values in Python. A list contains items separated by commas and enclosed within square brackets (```[]```). The values stored in a list can be accessed using the slice operator (```[index]```, ```[start:end]```) with indexes starting at ***0*** in the beginning of the list and working their way to end ***-1***. - -+++ - -***Example:*** - -```{code-cell} ipython3 -:tags: [solution] -my_list = ['abcd', 786 , 2.23, 'john', 70.2] -print(my_list) -print(my_list[0]) -``` - -***Task:*** Print elements starting from 3rd element. - -```{code-cell} ipython3 -:tags: [solution] - -``` - -***Task:*** Append ```99``` to the list using the ```append()``` method. - -```{code-cell} ipython3 -:tags: [solution] - -``` - -### Dictionaries - -Dictionaries in Python are unordered collections of key-value pairs enclosed within curly brackets (`{}`). Unlike lists, which are indexed by numbers, dictionaries are indexed by -keys, which can be either numbers or strings. - -+++ - -***Example:*** - -```{code-cell} ipython3 -:tags: [solution] -my_dict = {'name': 'Cadet', 'version': '4.1.0', 'nUsers': 500} -print(my_dict) -print(my_dict['name']) -my_dict['nDownloads'] = 2000 -print(my_dict['nDownloads']) -``` - -Dictionaries, like lists, can be nested. - -```{code-cell} ipython3 -:tags: [solution] -my_dict = {'name': 'Cadet', 'version': '4.1.0', 'nUsers': 500} -my_dict['stats'] = { 'github_stars': 1000, 'downloads': 2000, 'issues': 3} -print(my_dict) -``` - -#### Addict - -Since the syntax to traverse deeply nested dictionaries can be quite tedious, we can use the package `addict` to simplify it to the dot notation. - -```{code-cell} ipython3 -:tags: [solution] -from addict import Dict - -my_new_dict = Dict(my_dict) -print(my_new_dict.stats) -my_new_dict.stats.pull_requests = 10 -print(my_new_dict.stats) -``` - -### Python syntax - -In Python code blocks are structured by indentation level. It is is a language requirement not a matter of style. This principle makes it easier to read because it eliminates most of braces ```{ }``` and ```end``` statements which makes Python code much more readable. All statements with the same distance from left belong to the same block of code, i.e. the statements within a block line up vertically. If a block has to be more deeply nested, it is simply indented further to the right. - -Loops and Conditional statements end with a colon ```:```. The same is true for functions and other structures introducing blocks. - -+++ - -### Conditions - -Conditional statements are used to direct the flow of the program to different commands based on whether a statement is ```True``` or ```False```. A boolean (```True``` or ```False```) is used to direct the flow with an ```if```, ```elif``` (else if), or ```else``` parts to the statement. - -+++ - -***Example:*** - -```{code-cell} ipython3 -:tags: [solution] -x = 4 -if x < 3: - print('less than 3') -elif x<4: - print('between 3 and 4') -else: - print('greater than 4') -``` - -***Task:*** Write a conditional statement that checks whether the string ```'spam'``` is in ```menu```. -**Hint:** Check the operators list for membership statements. - -```{code-cell} ipython3 -:tags: [solution] -menu = ['eggs','bacon'] -``` - -### Loops -Often, we don't know (or care) how many elements are in a container (e.g. list). We just want to perform an operation on every element of the container. The ```for``` statement in Python differs a bit from what you may be used to in C or Pascal. Python’s ```for``` statement iterates over the items of any sequence (e.g. a list or a string), in the order that they appear in the sequence. - -+++ - -***Example:*** - -```{code-cell} ipython3 -:tags: [solution] -list_of_primes = [2, 3, 5, 7] -for element in list_of_primes: - print(element) -``` - -***Task:*** Create a list with strings, iterate over all elements and print the string and the length of the string using the ```len``` function. - -```{code-cell} ipython3 -:tags: [solution] - -``` - -## Functions -A function is a block of organized, reusable code that is used to perform a single, related action. Functions provide better modularity for your application and a high degree of code reusing. There are built-in functions like ```print()```, ```help()```, etc. but it is possible to create your own functions. These functions are called user-defined functions. -A function definition describes what is to be calculated once the function is called. The keyword ```def``` introduces a function definition. It must be followed by the function name and the parenthesized list of formal parameters. The statements that form the body of the function start at the next line, and must be indented. - -***Note***, the function is not evaluated when defined! - -+++ - -***Example:*** - -```{code-cell} ipython3 -:tags: [solution] -def circle_area(diameter): - return 3.1415926/4 * diameter**2 - -area = circle_area(3) -print(area) -``` - -***Task:*** Define a function that returns the volume of a cylinder as a function of diameter and length. - -```{code-cell} ipython3 -:tags: [solution] - -``` diff --git a/00 Introduction Python/03_numpy_scipy_matplotlib.ipynb b/00 Introduction Python/03_numpy_scipy_matplotlib.ipynb new file mode 100644 index 0000000..c65b9d8 --- /dev/null +++ b/00 Introduction Python/03_numpy_scipy_matplotlib.ipynb @@ -0,0 +1,148 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "854e4c48", + "metadata": {}, + "source": [ + "# Importing packages and introduction to Numpy/Scipy/Matplotlib\n", + "Python capabilities are extended with many useful packages. A few of the packages that we'll use in this class are:\n", + "\n", + "- [NumPy](https://www.numpy.org/) for array-computing\n", + "- [SciPy](https://www.scipy.org/) for numerical routines\n", + "- [Matplotlib](https://www.matplotlib.org/) for data visualization\n", + "\n", + "You can import a package with ```import package as pk``` where ```package``` is the package name and ```pk``` is the abbreviated name.\n", + "\n", + "\n", + "***Task:*** Import the ```numpy``` package as ```np``` (shortened name) and get help on the ```np.linspace``` function." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a20c0aba", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "import numpy as np\n", + "help(np.linspace)" + ] + }, + { + "cell_type": "markdown", + "id": "9ad86949", + "metadata": {}, + "source": [ + "***Task:*** Use ```np.linspace``` to define 20 equally spaced values between $0$ and $2\\,\\pi$. Name the variable ```x``` and use the built-in ```np.pi``` constant for $\\pi$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c4fb9004", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "x = np.linspace(0,2*np.pi,20)\n", + "print(x)" + ] + }, + { + "cell_type": "markdown", + "id": "95872b0c", + "metadata": {}, + "source": [ + "***Task:*** Use ```np.sin``` to calculate a new variable ```y``` as $y=2\\,\\sin{\\left(\\frac{x}{2}\\right)}$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f053e288", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "y = 2*np.sin(x/2)\n", + "print(y)" + ] + }, + { + "cell_type": "markdown", + "id": "d05b76ed", + "metadata": {}, + "source": [ + "***Task:*** Import the ```matplotlib.pyplot``` package as ```plt``` (shortened name) and show the help on the ```plt.plot``` function." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0e52352a", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "help(plt.plot)" + ] + }, + { + "cell_type": "markdown", + "id": "4be15c2a", + "metadata": {}, + "source": [ + "***Task:*** Create a plot of ```x``` and ```y``` with the ```plt.plot``` function. Add an x-label with ```plt.xlabel``` and a y-label with ```plt.ylabel```." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "83592734", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "plt.plot(x,y,label='y=2*sin(x/2)')\n", + "plt.xlabel('x')\n", + "plt.ylabel('y')\n", + "\n", + "z = np.cos(x)\n", + "plt.plot(x,z,label='cos(x)')\n", + "\n", + "plt.title('Trigonometry Functions')\n", + "plt.legend()" + ] + } + ], + "metadata": { + "jupytext": { + "formats": "ipynb,md:myst" + }, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/00 Introduction Python/03_numpy_scipy_matplotlib.md b/00 Introduction Python/03_numpy_scipy_matplotlib.md deleted file mode 100644 index c72dc16..0000000 --- a/00 Introduction Python/03_numpy_scipy_matplotlib.md +++ /dev/null @@ -1,75 +0,0 @@ ---- -jupytext: - formats: ipynb,md:myst - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.15.2 -kernelspec: - display_name: Python 3 (ipykernel) - language: python - name: python3 ---- - -# Importing packages and introduction to Numpy/Scipy/Matplotlib -Python capabilities are extended with many useful packages. A few of the packages that we'll use in this class are: - -- [NumPy](https://www.numpy.org/) for array-computing -- [SciPy](https://www.scipy.org/) for numerical routines -- [Matplotlib](https://www.matplotlib.org/) for data visualization - -You can import a package with ```import package as pk``` where ```package``` is the package name and ```pk``` is the abbreviated name. - - -***Task:*** Import the ```numpy``` package as ```np``` (shortened name) and get help on the ```np.linspace``` function. - -```{code-cell} ipython3 -:tags: [solution] - -import numpy as np -help(np.linspace) -``` - -***Task:*** Use ```np.linspace``` to define 20 equally spaced values between $0$ and $2\,\pi$. Name the variable ```x``` and use the built-in ```np.pi``` constant for $\pi$. - -```{code-cell} ipython3 -:tags: [solution] - -x = np.linspace(0,2*np.pi,20) -print(x) -``` - -***Task:*** Use ```np.sin``` to calculate a new variable ```y``` as $y=2\,\sin{\left(\frac{x}{2}\right)}$. - -```{code-cell} ipython3 -:tags: [solution] - -y = 2*np.sin(x/2) -print(y) -``` - -***Task:*** Import the ```matplotlib.pyplot``` package as ```plt``` (shortened name) and show the help on the ```plt.plot``` function. - -```{code-cell} ipython3 -:tags: [solution] - -import matplotlib.pyplot as plt -help(plt.plot) -``` - -***Task:*** Create a plot of ```x``` and ```y``` with the ```plt.plot``` function. Add an x-label with ```plt.xlabel``` and a y-label with ```plt.ylabel```. - -```{code-cell} ipython3 -:tags: [solution] - -plt.plot(x,y,label='y=2*sin(x/2)') -plt.xlabel('x') -plt.ylabel('y') - -z = np.cos(x) -plt.plot(x,z,label='cos(x)') - -plt.title('Trigonometry Functions') -plt.legend() -``` diff --git a/01 Introduction/01_CADET_overview.ipynb b/01 Introduction/01_CADET_overview.ipynb new file mode 100644 index 0000000..93e0c60 --- /dev/null +++ b/01 Introduction/01_CADET_overview.ipynb @@ -0,0 +1,307 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "1bf35c9f", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# CADET Overview\n", + "\n", + "```{figure} ./resources/cadet_overview.png\n", + ":width: 75%\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "f5de9239", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Unit Operation Model\n", + "\n", + "```{figure} ./resources/unit_operation.png\n", + ":width: 30%\n", + ":align: center\n", + "\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "028a9d83", + "metadata": { + "editable": true, + "jp-MarkdownHeadingCollapsed": true, + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Flow Sheet Model\n", + "\n", + "```{figure} ./resources/batch_elution.svg\n", + ":width: 50%\n", + ":align: center\n", + "\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "d18dfd8a", + "metadata": { + "editable": true, + "jp-MarkdownHeadingCollapsed": true, + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Process Model\n", + "\n", + "```{figure} ./resources/batch_elution_process_model.svg\n", + ":width: 50%\n", + ":align: center\n", + "\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "7148132c", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Simulation\n", + "\n", + "```{figure} ./resources/simulation.png\n", + ":align: center\n", + ":width: 50%\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "7dc0d5b1", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "```{figure} ./resources/simulation_results.png\n", + ":align: center\n", + ":width: 50%\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "8da70c83", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Simulation: Cyclic Stationarity\n", + "\n", + "```{figure} ./resources/stationarity.png\n", + ":align: center\n", + ":width: 50%\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "995890cc", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "```{figure} ./resources/simulation_results_cyclic.png\n", + ":align: center\n", + ":width: 33%\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "8d4c1370", + "metadata": { + "jp-MarkdownHeadingCollapsed": true, + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Post Processing\n", + "\n", + "```{figure} ./resources/post_processing.png\n", + ":align: center\n", + ":width: 750\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "c6a765ba", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Post Processing: Fractionation\n", + "\n", + "```{figure} ./resources/fractionator.png\n", + ":align: center\n", + ":width: 750\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "263e7860", + "metadata": {}, + "source": [ + "```{figure} ./resources/fractionation.png\n", + ":align: center\n", + ":width: 50%\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "85087f6b", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Post Processing - Comparison with experimental data\n", + "\n", + "```{figure} ./resources/comparator.png\n", + ":align: center\n", + ":width: 750\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "8300b732", + "metadata": {}, + "source": [ + "```{figure} ./resources/comparison.png\n", + ":align: center\n", + ":width: 50%\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "7241e902", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Optimization\n", + "\n", + "```{figure} ./resources/optimization.png\n", + ":align: center\n", + ":width: 50%\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "0b5fc221", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Optimization - Parameter Estimation\n", + "\n", + "```{figure} ./resources/parameter_estimation.png\n", + ":align: center\n", + ":width: 90%\n", + "``````" + ] + }, + { + "cell_type": "markdown", + "id": "1360c463", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Optimization - Process Optimization\n", + "\n", + "```{figure} ./resources/process_optimization.png\n", + ":align: center\n", + ":width: 90%\n", + "``````" + ] + }, + { + "cell_type": "markdown", + "id": "fa73bb4b", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Further Resources\n", + "\n", + "## General Information\n", + "- **Website**: https://cadet-web.de\n", + "- **User Forum**: https://forum.cadet-web.de\n", + "- **JupyterHub**: https://jupyter.cadet-web.de\n", + "\n", + "## CADET-Core\n", + "- **Source Code**: https://github.com/modsim/cadet\n", + "- **Documentation**: https://cadet.github.io\n", + "- **Tutorials**: https://github.com/modsim/cadet-tutorial\n", + "\n", + "## CADET-Process\n", + "- **Source Code**: https://github.com/fau-advanced-separations/cadet-process\n", + "- **Documentation**: https://cadet-process.readthedocs.io/en/latest/\n", + "- **Tutorials**: https://github.com/modsim/cadet-workshop" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/01 Introduction/01_CADET_overview.md b/01 Introduction/01_CADET_overview.md deleted file mode 100644 index a351705..0000000 --- a/01 Introduction/01_CADET_overview.md +++ /dev/null @@ -1,169 +0,0 @@ ---- -jupytext: - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.16.2 -kernelspec: - display_name: Python 3 (ipykernel) - language: python - name: python3 ---- - -+++ {"slideshow": {"slide_type": "slide"}, "editable": true} - -# CADET Overview - -```{figure} ./resources/cadet_overview.png -:width: 75% -``` - -+++ {"slideshow": {"slide_type": "slide"}, "editable": true} - -## Unit Operation Model - -```{figure} ./resources/unit_operation.png -:width: 30% -:align: center - -``` - -+++ {"slideshow": {"slide_type": "slide"}, "jp-MarkdownHeadingCollapsed": true, "editable": true} - -## Flow Sheet Model - -```{figure} ./resources/batch_elution.svg -:width: 50% -:align: center - -``` - -+++ {"slideshow": {"slide_type": "slide"}, "jp-MarkdownHeadingCollapsed": true, "editable": true} - -## Process Model - -```{figure} ./resources/batch_elution_process_model.svg -:width: 50% -:align: center - -``` - -+++ {"slideshow": {"slide_type": "slide"}, "editable": true} - -## Simulation - -```{figure} ./resources/simulation.png -:align: center -:width: 50% -``` - -+++ {"slideshow": {"slide_type": "fragment"}, "editable": true} - -```{figure} ./resources/simulation_results.png -:align: center -:width: 50% -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -## Simulation: Cyclic Stationarity - -```{figure} ./resources/stationarity.png -:align: center -:width: 50% -``` - -+++ {"slideshow": {"slide_type": "fragment"}} - -```{figure} ./resources/simulation_results_cyclic.png -:align: center -:width: 33% -``` - -+++ {"slideshow": {"slide_type": "slide"}, "jp-MarkdownHeadingCollapsed": true} - -## Post Processing - -```{figure} ./resources/post_processing.png -:align: center -:width: 750 -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -## Post Processing: Fractionation - -```{figure} ./resources/fractionator.png -:align: center -:width: 750 -``` - -+++ - -```{figure} ./resources/fractionation.png -:align: center -:width: 50% -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -## Post Processing - Comparison with experimental data - -```{figure} ./resources/comparator.png -:align: center -:width: 750 -``` - -+++ - -```{figure} ./resources/comparison.png -:align: center -:width: 50% -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -## Optimization - -```{figure} ./resources/optimization.png -:align: center -:width: 50% -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -## Optimization - Parameter Estimation - -```{figure} ./resources/parameter_estimation.png -:align: center -:width: 90% -`````` - -+++ {"slideshow": {"slide_type": "slide"}} - -## Optimization - Process Optimization - -```{figure} ./resources/process_optimization.png -:align: center -:width: 90% -`````` - -+++ {"slideshow": {"slide_type": "slide"}} - -# Further Resources - -## General Information -- **Website**: https://cadet-web.de -- **User Forum**: https://forum.cadet-web.de -- **JupyterHub**: https://jupyter.cadet-web.de - -## CADET-Core -- **Source Code**: https://github.com/modsim/cadet -- **Documentation**: https://cadet.github.io -- **Tutorials**: https://github.com/modsim/cadet-tutorial - -## CADET-Process -- **Source Code**: https://github.com/fau-advanced-separations/cadet-process -- **Documentation**: https://cadet-process.readthedocs.io/en/latest/ -- **Tutorials**: https://github.com/modsim/cadet-workshop diff --git a/02 CADET Fundamentals/01 inlet profiles/01_inlet_profiles.ipynb b/02 CADET Fundamentals/01 inlet profiles/01_inlet_profiles.ipynb new file mode 100644 index 0000000..a17a0a8 --- /dev/null +++ b/02 CADET Fundamentals/01 inlet profiles/01_inlet_profiles.ipynb @@ -0,0 +1,548 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "114b6f32", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Inlet Profiles\n", + "\n", + "Chromatographic systems always require some kind of convective flow through the column." + ] + }, + { + "cell_type": "markdown", + "id": "6758a9c9", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "In this lesson, we will:\n", + "- Create and connect our first systems of unit operations.\n", + "- Define inlet profiles using piecewise cubic polynomials.\n", + "- Run CADET and analyze the results." + ] + }, + { + "cell_type": "markdown", + "id": "cc85d19e", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Example 1: Flow from `Inlet` to `Outlet`\n", + "\n", + "In a first example, we will look at a simple system with just two unit operations, an [Inlet](https://cadet-process.readthedocs.io/en/latest/reference/generated/CADETProcess.processModel.Inlet.html), and an [Outlet](https://cadet-process.readthedocs.io/en/latest/reference/generated/CADETProcess.processModel.Outlet.html).\n", + "\n", + "```{figure} ./resources/IO.png\n", + ":width: 30%\n", + "```\n", + "\n", + "We will introduce flow from the `Inlet` to the `Outlet` with a constant flow rate of $Q = 1~mL \\cdot s^{-1}$.\n", + "In the first section, the concentration is $1.0~mM$, and after $1~min$, it is changed to $0.0~mM$.\n", + "\n", + "```{figure} ./resources/step.png\n", + ":width: 30%\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "77a27bdb", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## 1. Setting up the model\n", + "\n", + "Before we start with specifying the system, we define some local auxiliary variables.\n", + "Note that we have to convert all units to SI units.\n", + "\n", + "\n", + "```{note}\n", + "Generally, CADET can be used with any consistent system of units.\n", + "However, we strongly recommend converting everything to the SI system.\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "432a3f9a", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "#### Component System\n", + "\n", + "- `ComponentSystem` ensure that all parts of the process have the same number of components.\n", + "- Components can be named which automatically adds legends to the plot methods.\n", + "\n", + "For advanced use, see [here](https://cadet-process.readthedocs.io/en/latest/reference/process_model/component_system.html)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a0f660e5", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import ComponentSystem\n", + "\n", + "component_system = ComponentSystem(1)" + ] + }, + { + "cell_type": "markdown", + "id": "bdd925e4", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Unit Operations\n", + "\n", + "For an overview of all models in CADET-Process, see [here](https://cadet-process.readthedocs.io/en/latest/reference/process_model/unit_operation_models.html).\n", + "\n", + "Unit operations require:\n", + "- the `ComponentSystem`\n", + "- as a unique name.\n", + "\n", + "Note that the name string passed in the constructor is later used to reference the unit in the flow sheet for setting `Events` and `OptimizationVariables`." + ] + }, + { + "cell_type": "markdown", + "id": "cabda1a6", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "## Inlet\n", + "\n", + "In CADET, the `Inlet` pseudo unit operation serves as source for the system and is used to create arbitary concentration profiles as boundary conditions (see also [here](https://cadet-process.readthedocs.io/en/latest/reference/generated/CADETProcess.processModel.Inlet.html)).\n", + "\n", + "- Concentration profiles are described using a third degree piecewise polynomial for each component.\n", + "- Flow rate can be expressed as a third degree piecewise polynomial.\n", + "\n", + "Here, the flow rate is constant, we can directly set the parameter on the object." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8dc1b267", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import Inlet\n", + "\n", + "inlet = Inlet(component_system, 'inlet')\n", + "inlet.flow_rate = 1e-6" + ] + }, + { + "cell_type": "markdown", + "id": "76c12520", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "Note that every unit operation model has different model parameters.\n", + "To display all parameters, simply print the `parameters` attribute." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "55acd0c9", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "print(inlet.parameters)" + ] + }, + { + "cell_type": "markdown", + "id": "eebe4e5c", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "## Outlet\n", + "The `Outlet` is another pseudo unit operation that serves as sink for the system (see also [here](https://cadet.github.io/master/modelling/unit_operations/outlet))." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9949abca", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import Outlet\n", + "\n", + "outlet = Outlet(component_system, 'outlet')" + ] + }, + { + "cell_type": "markdown", + "id": "93cfbd12", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "Note that the `Outlet` unit does not have any model parameters." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9aa34ef8", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "print(outlet.parameters)" + ] + }, + { + "cell_type": "markdown", + "id": "98c3a524", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Flow Sheet Connectivity\n", + "\n", + "The `FlowSheet` stores the connectivity between different unit operations.\n", + "\n", + "For more information, see also [here](https://cadet-process.readthedocs.io/en/latest/reference/process_model/flow_sheet.html)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ae5ff6c8", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import FlowSheet\n", + "\n", + "flow_sheet = FlowSheet(component_system)\n", + "\n", + "flow_sheet.add_unit(inlet)\n", + "flow_sheet.add_unit(outlet)\n", + "\n", + "flow_sheet.add_connection(inlet, outlet)" + ] + }, + { + "cell_type": "markdown", + "id": "9c7b9518", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Dynamic Events in Process\n", + "\n", + "Dynamic changes of model parameters or flow sheet connections are configure in `Process` class.\n", + "\n", + "For more information, see also [here](https://cadet-process.readthedocs.io/en/latest/user_guide/process_model/process.html)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0c61556b", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import Process\n", + "\n", + "process = Process(flow_sheet, 'process')\n", + "process.cycle_time = 120" + ] + }, + { + "cell_type": "markdown", + "id": "22419ee5", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "To add an event that changes the value of a parameter, use the `add_event` method which requires the following arguments:\n", + "- `name`: Name of the event.\n", + "- `parameter_path`: Path of the parameter that is changed in dot notation. E.g. the flow rate of the eluent unit is the parameter `flow_rate` of the `eluent` unit in the `flow_sheet`. Hence, the path is `flow_sheet.eluent.flow_rate`. As previously mentioned, the name of the unit operation is used to reference it, not the variable.\n", + "- `state`: Value of the attribute that is changed at Event execution.\n", + "- `time`: Time at which the event is executed.\n", + "\n", + "To display all time dependent parameters of an object, use the `section_dependent_parameters` attribute." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a0d607dd", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "print(inlet.section_dependent_parameters)" + ] + }, + { + "cell_type": "markdown", + "id": "83a2230a", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "Note that also flow sheet connectivity can be added as events. More on that later." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7ca11314", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "print(process.flow_sheet.section_dependent_parameters)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "57ffb3b5", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "_ = process.add_event('start load', 'flow_sheet.inlet.c', [1], 0)\n", + "_ = process.add_event('start wash', 'flow_sheet.inlet.c', [0], 60)" + ] + }, + { + "cell_type": "markdown", + "id": "424aee71", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "All events can are stored in the events attribute. To visualize the trajectory of the parameter state over the entire cycle, the Process provides a `plot_events()` method." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2b139e81", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "_ = process.plot_events()" + ] + }, + { + "cell_type": "markdown", + "id": "adb16c3e", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## 3. Setting up the simulator and running the simulation\n", + "\n", + "To simulate the process, a process simulator needs to be configured.\n", + "If no path is specified, CADET-Process will try to autodetect CADET." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18ff3a0c", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.simulator import Cadet\n", + "process_simulator = Cadet()" + ] + }, + { + "cell_type": "markdown", + "id": "1e2e3e89", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "If a specific version of CADET is to be used, add the install path to the constructor:\n", + "\n", + "```\n", + "process_simulator = Cadet(install_path='/path/to/cadet/executable')\n", + "```\n", + "\n", + "To check that everything works correctly, you can call the check_cadet method:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9098e427", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "process_simulator.check_cadet()" + ] + }, + { + "cell_type": "markdown", + "id": "e4d8f993", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "Now, run the simulation:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8153dc42", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "simulation_results = process_simulator.simulate(process)" + ] + }, + { + "cell_type": "markdown", + "id": "436d82db", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## 4. Plotting the results\n", + "\n", + "The simulation_results object contains the solution for the inlet and outlet of every unit operation also provide plot methods." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9d9fd1c6", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "_ = simulation_results.solution.inlet.outlet.plot()\n", + "_ = simulation_results.solution.outlet.inlet.plot()" + ] + } + ], + "metadata": { + "jupytext": { + "formats": "ipynb,md:myst" + }, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/02 CADET Fundamentals/01 inlet profiles/01_inlet_profiles.md b/02 CADET Fundamentals/01 inlet profiles/01_inlet_profiles.md deleted file mode 100644 index 6eeef39..0000000 --- a/02 CADET Fundamentals/01 inlet profiles/01_inlet_profiles.md +++ /dev/null @@ -1,273 +0,0 @@ ---- -jupytext: - formats: ipynb,md:myst - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.15.2 -kernelspec: - display_name: Python 3 (ipykernel) - language: python - name: python3 ---- - -+++ {"slideshow": {"slide_type": "slide"}} - -# Inlet Profiles - -Chromatographic systems always require some kind of convective flow through the column. - -+++ {"slideshow": {"slide_type": "fragment"}} - -In this lesson, we will: -- Create and connect our first systems of unit operations. -- Define inlet profiles using piecewise cubic polynomials. -- Run CADET and analyze the results. - -+++ {"slideshow": {"slide_type": "slide"}} - -## Example 1: Flow from `Inlet` to `Outlet` - -In a first example, we will look at a simple system with just two unit operations, an [Inlet](https://cadet-process.readthedocs.io/en/latest/reference/generated/CADETProcess.processModel.Inlet.html), and an [Outlet](https://cadet-process.readthedocs.io/en/latest/reference/generated/CADETProcess.processModel.Outlet.html). - -```{figure} ./resources/IO.png -:width: 30% -``` - -We will introduce flow from the `Inlet` to the `Outlet` with a constant flow rate of $Q = 1~mL \cdot s^{-1}$. -In the first section, the concentration is $1.0~mM$, and after $1~min$, it is changed to $0.0~mM$. - -```{figure} ./resources/step.png -:width: 30% -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -## 1. Setting up the model - -Before we start with specifying the system, we define some local auxiliary variables. -Note that we have to convert all units to SI units. - - -```{note} -Generally, CADET can be used with any consistent system of units. -However, we strongly recommend converting everything to the SI system. -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -#### Component System - -- `ComponentSystem` ensure that all parts of the process have the same number of components. -- Components can be named which automatically adds legends to the plot methods. - -For advanced use, see [here](https://cadet-process.readthedocs.io/en/latest/reference/process_model/component_system.html). - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.processModel import ComponentSystem - -component_system = ComponentSystem(1) -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -## Unit Operations - -For an overview of all models in CADET-Process, see [here](https://cadet-process.readthedocs.io/en/latest/reference/process_model/unit_operation_models.html). - -Unit operations require: -- the `ComponentSystem` -- as a unique name. - -Note that the name string passed in the constructor is later used to reference the unit in the flow sheet for setting `Events` and `OptimizationVariables`. - -+++ {"slideshow": {"slide_type": "fragment"}} - -## Inlet - -In CADET, the `Inlet` pseudo unit operation serves as source for the system and is used to create arbitary concentration profiles as boundary conditions (see also [here](https://cadet-process.readthedocs.io/en/latest/reference/generated/CADETProcess.processModel.Inlet.html)). - -- Concentration profiles are described using a third degree piecewise polynomial for each component. -- Flow rate can be expressed as a third degree piecewise polynomial. - -Here, the flow rate is constant, we can directly set the parameter on the object. - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.processModel import Inlet - -inlet = Inlet(component_system, 'inlet') -inlet.flow_rate = 1e-6 -``` - -+++ {"slideshow": {"slide_type": "fragment"}} - -Note that every unit operation model has different model parameters. -To display all parameters, simply print the `parameters` attribute. - -```{code-cell} ipython3 -:tags: [solution] - -print(inlet.parameters) -``` - -+++ {"slideshow": {"slide_type": "fragment"}} - -## Outlet -The `Outlet` is another pseudo unit operation that serves as sink for the system (see also [here](https://cadet.github.io/master/modelling/unit_operations/outlet)). - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.processModel import Outlet - -outlet = Outlet(component_system, 'outlet') -``` - -+++ {"slideshow": {"slide_type": "fragment"}} - -Note that the `Outlet` unit does not have any model parameters. - -```{code-cell} ipython3 -:tags: [solution] - -print(outlet.parameters) -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -## Flow Sheet Connectivity - -The `FlowSheet` stores the connectivity between different unit operations. - -For more information, see also [here](https://cadet-process.readthedocs.io/en/latest/reference/process_model/flow_sheet.html). - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.processModel import FlowSheet - -flow_sheet = FlowSheet(component_system) - -flow_sheet.add_unit(inlet) -flow_sheet.add_unit(outlet) - -flow_sheet.add_connection(inlet, outlet) -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -## Dynamic Events in Process - -Dynamic changes of model parameters or flow sheet connections are configure in `Process` class. - -For more information, see also [here](https://cadet-process.readthedocs.io/en/latest/user_guide/process_model/process.html). - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.processModel import Process - -process = Process(flow_sheet, 'process') -process.cycle_time = 120 -``` - -+++ {"slideshow": {"slide_type": "skip"}} - -To add an event that changes the value of a parameter, use the `add_event` method which requires the following arguments: -- `name`: Name of the event. -- `parameter_path`: Path of the parameter that is changed in dot notation. E.g. the flow rate of the eluent unit is the parameter `flow_rate` of the `eluent` unit in the `flow_sheet`. Hence, the path is `flow_sheet.eluent.flow_rate`. As previously mentioned, the name of the unit operation is used to reference it, not the variable. -- `state`: Value of the attribute that is changed at Event execution. -- `time`: Time at which the event is executed. - -To display all time dependent parameters of an object, use the `section_dependent_parameters` attribute. - -```{code-cell} ipython3 -:tags: [solution] - -print(inlet.section_dependent_parameters) -``` - -+++ {"slideshow": {"slide_type": "fragment"}} - -Note that also flow sheet connectivity can be added as events. More on that later. - -```{code-cell} ipython3 -:tags: [solution] - -print(process.flow_sheet.section_dependent_parameters) -``` - -```{code-cell} ipython3 -:tags: [solution] - -_ = process.add_event('start load', 'flow_sheet.inlet.c', [1], 0) -_ = process.add_event('start wash', 'flow_sheet.inlet.c', [0], 60) -``` - -+++ {"slideshow": {"slide_type": "fragment"}} - -All events can are stored in the events attribute. To visualize the trajectory of the parameter state over the entire cycle, the Process provides a `plot_events()` method. - -```{code-cell} ipython3 -:tags: [solution] - -_ = process.plot_events() -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -## 3. Setting up the simulator and running the simulation - -To simulate the process, a process simulator needs to be configured. -If no path is specified, CADET-Process will try to autodetect CADET. - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.simulator import Cadet -process_simulator = Cadet() -``` - -+++ {"slideshow": {"slide_type": "fragment"}} - -If a specific version of CADET is to be used, add the install path to the constructor: - -``` -process_simulator = Cadet(install_path='/path/to/cadet/executable') -``` - -To check that everything works correctly, you can call the check_cadet method: - -```{code-cell} ipython3 -:tags: [solution] - -process_simulator.check_cadet() -``` - -+++ {"slideshow": {"slide_type": "fragment"}} - -Now, run the simulation: - -```{code-cell} ipython3 -:tags: [solution] - -simulation_results = process_simulator.simulate(process) -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -## 4. Plotting the results - -The simulation_results object contains the solution for the inlet and outlet of every unit operation also provide plot methods. - -```{code-cell} ipython3 -:tags: [solution] - -_ = simulation_results.solution.inlet.outlet.plot() -_ = simulation_results.solution.outlet.inlet.plot() -``` diff --git a/02 CADET Fundamentals/01 inlet profiles/02_inlet_profiles_exercises.ipynb b/02 CADET Fundamentals/01 inlet profiles/02_inlet_profiles_exercises.ipynb new file mode 100644 index 0000000..75ab703 --- /dev/null +++ b/02 CADET Fundamentals/01 inlet profiles/02_inlet_profiles_exercises.ipynb @@ -0,0 +1,315 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "c0af470c", + "metadata": { + "slideshow": { + "slide_type": "slide" + }, + "user_expressions": [] + }, + "source": [ + "# Inlet Profiles - Exercise" + ] + }, + { + "cell_type": "markdown", + "id": "021ffb24", + "metadata": { + "user_expressions": [] + }, + "source": [ + "## Exercise 1\n", + "\n", + "Create the following flow profile:\n", + "\n", + "```{figure} ./resources/2_comp.png\n", + ":width: 50%\n", + ":align: center\n", + "```\n", + "Assume a flow rate of $Q = 1\\cdot mL\\cdot min^{-1}$\n", + "\n", + "***Hint:*** We need to specify a second component for the template function and then pass a list of to the concentration parameter." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b57086e5", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import ComponentSystem\n", + "\n", + "component_system = ComponentSystem(2)\n", + "\n", + "from CADETProcess.processModel import Inlet\n", + "inlet = Inlet(component_system, 'inlet')\n", + "inlet.flow_rate = 1e-6/60\n", + "\n", + "from CADETProcess.processModel import Outlet\n", + "outlet = Outlet(component_system, 'outlet')\n", + "\n", + "from CADETProcess.processModel import FlowSheet\n", + "flow_sheet = FlowSheet(component_system)\n", + "\n", + "flow_sheet.add_unit(inlet)\n", + "flow_sheet.add_unit(outlet)\n", + "\n", + "flow_sheet.add_connection(inlet, outlet)\n", + "\n", + "from CADETProcess.processModel import Process\n", + "process = Process(flow_sheet, 'flow_exercise_1')\n", + "process.cycle_time = 120\n", + "\n", + "process.add_event('load', 'flow_sheet.inlet.c', [0.5, 0])\n", + "process.add_event('wash', 'flow_sheet.inlet.c', [1, 2], 60)\n", + "\n", + "from CADETProcess.simulator import Cadet\n", + "simulator = Cadet()\n", + "\n", + "simulation_results = simulator.simulate(process)\n", + "\n", + "_ = simulation_results.solution.outlet.inlet.plot()" + ] + }, + { + "cell_type": "markdown", + "id": "cbaae972", + "metadata": { + "slideshow": { + "slide_type": "slide" + }, + "user_expressions": [] + }, + "source": [ + "## Exercise 2\n", + "\n", + "Create the following inlet profile:\n", + "\n", + "```{figure} ./resources/gradient.png\n", + ":width: 50%\n", + ":align: center\n", + "```\n", + "\n", + "Assume a flow rate of $Q = 1\\cdot mL\\cdot min^{-1}$\n", + "\n", + "***Hint:*** We need three `Events` and for linear gradients, the concentration for each component needs to be passed as a list of cubic polynomial coefficients." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5bf398f4", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import ComponentSystem\n", + "\n", + "component_system = ComponentSystem(1)\n", + "\n", + "from CADETProcess.processModel import Inlet\n", + "inlet = Inlet(component_system, 'inlet')\n", + "inlet.flow_rate = 1e-6/60\n", + "\n", + "from CADETProcess.processModel import Outlet\n", + "outlet = Outlet(component_system, 'outlet')\n", + "\n", + "from CADETProcess.processModel import FlowSheet\n", + "flow_sheet = FlowSheet(component_system)\n", + "\n", + "flow_sheet.add_unit(inlet)\n", + "flow_sheet.add_unit(outlet)\n", + "\n", + "flow_sheet.add_connection(inlet, outlet)\n", + "\n", + "from CADETProcess.processModel import Process\n", + "process = Process(flow_sheet, 'flow_exercise_2')\n", + "process.cycle_time = 180\n", + "\n", + "process.add_event('load', 'flow_sheet.inlet.c', [[0,1/60]], 0)\n", + "process.add_event('wash', 'flow_sheet.inlet.c', [[1]], 60)\n", + "process.add_event('elute', 'flow_sheet.inlet.c', [[1,-1/60]], 120)\n", + "\n", + "from CADETProcess.simulator import Cadet\n", + "simulator = Cadet()\n", + "\n", + "simulation_results = simulator.simulate(process)\n", + "\n", + "_ = simulation_results.solution.outlet.inlet.plot()" + ] + }, + { + "cell_type": "markdown", + "id": "48ea7729", + "metadata": { + "slideshow": { + "slide_type": "slide" + }, + "user_expressions": [] + }, + "source": [ + "## Bonus Exercise: Changing the system connectivity\n", + "\n", + "Instead of creating inlet profiles by modifying the concentration profile of a single `Inlet` unit operation, the same effect can also be achieved by assuming multiple `Inlets`, each with a constant concentration and changing the flow rates of the system.\n", + "\n", + "***Task:*** Creating a system with a second `Inlet` and recreate the previous examples." + ] + }, + { + "cell_type": "markdown", + "id": "96860b88", + "metadata": { + "user_expressions": [] + }, + "source": [ + "### Example 1" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "46b1e1c9", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import ComponentSystem\n", + "\n", + "component_system = ComponentSystem(2)\n", + "\n", + "from CADETProcess.processModel import Inlet\n", + "\n", + "inlet_1 = Inlet(component_system, 'inlet_1')\n", + "inlet_1.c = [0.5, 0]\n", + "\n", + "inlet_2 = Inlet(component_system, 'inlet_2')\n", + "inlet_2.c = [1, 2]\n", + "\n", + "from CADETProcess.processModel import Outlet\n", + "outlet = Outlet(component_system, 'outlet')\n", + "\n", + "from CADETProcess.processModel import FlowSheet\n", + "flow_sheet = FlowSheet(component_system)\n", + "\n", + "flow_sheet.add_unit(inlet_1)\n", + "flow_sheet.add_unit(inlet_2)\n", + "flow_sheet.add_unit(outlet)\n", + "\n", + "flow_sheet.add_connection(inlet_1, outlet)\n", + "flow_sheet.add_connection(inlet_2, outlet)\n", + "\n", + "from CADETProcess.processModel import Process\n", + "process = Process(flow_sheet, 'flow_exercise_1_dyn_flow')\n", + "process.cycle_time = 180\n", + "\n", + "process.add_event('inlet_1_on', 'flow_sheet.inlet_1.flow_rate', 1e-6/60, 0)\n", + "process.add_event('inlet_2_off', 'flow_sheet.inlet_2.flow_rate', 0, 0)\n", + "process.add_event('inlet_1_off', 'flow_sheet.inlet_1.flow_rate', 0, 60)\n", + "process.add_event('inlet_2_on', 'flow_sheet.inlet_2.flow_rate', 1e-6/60, 60)\n", + "\n", + "from CADETProcess.simulator import Cadet\n", + "simulator = Cadet()\n", + "\n", + "simulation_results = simulator.simulate(process)\n", + "\n", + "# _ = simulation_results.solution.inlet_1.outlet.plot()\n", + "# _ = simulation_results.solution.inlet_2.outlet.plot()\n", + "_ = simulation_results.solution.outlet.inlet.plot()" + ] + }, + { + "cell_type": "markdown", + "id": "597e176a", + "metadata": { + "slideshow": { + "slide_type": "slide" + }, + "user_expressions": [] + }, + "source": [ + "### Example 2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a733214d", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import ComponentSystem\n", + "\n", + "component_system = ComponentSystem(1)\n", + "\n", + "from CADETProcess.processModel import Inlet\n", + "\n", + "inlet_1 = Inlet(component_system, 'inlet_1')\n", + "inlet_1.c = [1]\n", + "\n", + "inlet_2 = Inlet(component_system, 'inlet_2')\n", + "inlet_2.c = [0]\n", + "\n", + "from CADETProcess.processModel import Outlet\n", + "outlet = Outlet(component_system, 'outlet')\n", + "\n", + "from CADETProcess.processModel import FlowSheet\n", + "flow_sheet = FlowSheet(component_system)\n", + "\n", + "flow_sheet.add_unit(inlet_1)\n", + "flow_sheet.add_unit(inlet_2)\n", + "flow_sheet.add_unit(outlet)\n", + "\n", + "flow_sheet.add_connection(inlet_1, outlet)\n", + "flow_sheet.add_connection(inlet_2, outlet)\n", + "\n", + "from CADETProcess.processModel import Process\n", + "process = Process(flow_sheet, 'flow_exercise_2_dyn_flow')\n", + "process.cycle_time = 180\n", + "\n", + "Q = 1e-6/60\n", + "process.add_event('first_grad_1', 'flow_sheet.inlet_1.flow_rate', [0, Q/60], 0)\n", + "process.add_event('first_grad_2', 'flow_sheet.inlet_2.flow_rate', [Q, -Q/60], 0)\n", + "\n", + "process.add_event('start_plateau_1', 'flow_sheet.inlet_1.flow_rate', Q, 60)\n", + "process.add_event('start_plateau_2', 'flow_sheet.inlet_2.flow_rate', 0, 60)\n", + "\n", + "process.add_event('second_grad_1', 'flow_sheet.inlet_1.flow_rate', [Q, -Q/60], 120)\n", + "process.add_event('second_grad_2', 'flow_sheet.inlet_2.flow_rate', [0, Q/60], 120)\n", + "\n", + "\n", + "from CADETProcess.simulator import Cadet\n", + "simulator = Cadet()\n", + "\n", + "simulation_results = simulator.simulate(process)\n", + "\n", + "_ = simulation_results.solution.outlet.inlet.plot()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/02 CADET Fundamentals/01 inlet profiles/02_inlet_profiles_exercises.md b/02 CADET Fundamentals/01 inlet profiles/02_inlet_profiles_exercises.md deleted file mode 100644 index 14d3390..0000000 --- a/02 CADET Fundamentals/01 inlet profiles/02_inlet_profiles_exercises.md +++ /dev/null @@ -1,231 +0,0 @@ ---- -jupytext: - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.15.2 -kernelspec: - display_name: Python 3 (ipykernel) - language: python - name: python3 ---- - -+++ {"user_expressions": [], "slideshow": {"slide_type": "slide"}} - -# Inlet Profiles - Exercise - -+++ {"user_expressions": []} - -## Exercise 1 - -Create the following flow profile: - -```{figure} ./resources/2_comp.png -:width: 50% -:align: center -``` -Assume a flow rate of $Q = 1\cdot mL\cdot min^{-1}$ - -***Hint:*** We need to specify a second component for the template function and then pass a list of to the concentration parameter. - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.processModel import ComponentSystem - -component_system = ComponentSystem(2) - -from CADETProcess.processModel import Inlet -inlet = Inlet(component_system, 'inlet') -inlet.flow_rate = 1e-6/60 - -from CADETProcess.processModel import Outlet -outlet = Outlet(component_system, 'outlet') - -from CADETProcess.processModel import FlowSheet -flow_sheet = FlowSheet(component_system) - -flow_sheet.add_unit(inlet) -flow_sheet.add_unit(outlet) - -flow_sheet.add_connection(inlet, outlet) - -from CADETProcess.processModel import Process -process = Process(flow_sheet, 'flow_exercise_1') -process.cycle_time = 120 - -process.add_event('load', 'flow_sheet.inlet.c', [0.5, 0]) -process.add_event('wash', 'flow_sheet.inlet.c', [1, 2], 60) - -from CADETProcess.simulator import Cadet -simulator = Cadet() - -simulation_results = simulator.simulate(process) - -_ = simulation_results.solution.outlet.inlet.plot() -``` - -+++ {"user_expressions": [], "slideshow": {"slide_type": "slide"}} - -## Exercise 2 - -Create the following inlet profile: - -```{figure} ./resources/gradient.png -:width: 50% -:align: center -``` - -Assume a flow rate of $Q = 1\cdot mL\cdot min^{-1}$ - -***Hint:*** We need three `Events` and for linear gradients, the concentration for each component needs to be passed as a list of cubic polynomial coefficients. - -```{code-cell} ipython3 -:tags: [solution] -from CADETProcess.processModel import ComponentSystem - -component_system = ComponentSystem(1) - -from CADETProcess.processModel import Inlet -inlet = Inlet(component_system, 'inlet') -inlet.flow_rate = 1e-6/60 - -from CADETProcess.processModel import Outlet -outlet = Outlet(component_system, 'outlet') - -from CADETProcess.processModel import FlowSheet -flow_sheet = FlowSheet(component_system) - -flow_sheet.add_unit(inlet) -flow_sheet.add_unit(outlet) - -flow_sheet.add_connection(inlet, outlet) - -from CADETProcess.processModel import Process -process = Process(flow_sheet, 'flow_exercise_2') -process.cycle_time = 180 - -process.add_event('load', 'flow_sheet.inlet.c', [[0,1/60]], 0) -process.add_event('wash', 'flow_sheet.inlet.c', [[1]], 60) -process.add_event('elute', 'flow_sheet.inlet.c', [[1,-1/60]], 120) - -from CADETProcess.simulator import Cadet -simulator = Cadet() - -simulation_results = simulator.simulate(process) - -_ = simulation_results.solution.outlet.inlet.plot() -``` - -+++ {"user_expressions": [], "slideshow": {"slide_type": "slide"}} - -## Bonus Exercise: Changing the system connectivity - -Instead of creating inlet profiles by modifying the concentration profile of a single `Inlet` unit operation, the same effect can also be achieved by assuming multiple `Inlets`, each with a constant concentration and changing the flow rates of the system. - -***Task:*** Creating a system with a second `Inlet` and recreate the previous examples. - -+++ {"user_expressions": []} - -### Example 1 - -```{code-cell} ipython3 -:tags: [solution] -from CADETProcess.processModel import ComponentSystem - -component_system = ComponentSystem(2) - -from CADETProcess.processModel import Inlet - -inlet_1 = Inlet(component_system, 'inlet_1') -inlet_1.c = [0.5, 0] - -inlet_2 = Inlet(component_system, 'inlet_2') -inlet_2.c = [1, 2] - -from CADETProcess.processModel import Outlet -outlet = Outlet(component_system, 'outlet') - -from CADETProcess.processModel import FlowSheet -flow_sheet = FlowSheet(component_system) - -flow_sheet.add_unit(inlet_1) -flow_sheet.add_unit(inlet_2) -flow_sheet.add_unit(outlet) - -flow_sheet.add_connection(inlet_1, outlet) -flow_sheet.add_connection(inlet_2, outlet) - -from CADETProcess.processModel import Process -process = Process(flow_sheet, 'flow_exercise_1_dyn_flow') -process.cycle_time = 180 - -process.add_event('inlet_1_on', 'flow_sheet.inlet_1.flow_rate', 1e-6/60, 0) -process.add_event('inlet_2_off', 'flow_sheet.inlet_2.flow_rate', 0, 0) -process.add_event('inlet_1_off', 'flow_sheet.inlet_1.flow_rate', 0, 60) -process.add_event('inlet_2_on', 'flow_sheet.inlet_2.flow_rate', 1e-6/60, 60) - -from CADETProcess.simulator import Cadet -simulator = Cadet() - -simulation_results = simulator.simulate(process) - -# _ = simulation_results.solution.inlet_1.outlet.plot() -# _ = simulation_results.solution.inlet_2.outlet.plot() -_ = simulation_results.solution.outlet.inlet.plot() -``` - -+++ {"user_expressions": [], "slideshow": {"slide_type": "slide"}} - -### Example 2 - -```{code-cell} ipython3 -:tags: [solution] -from CADETProcess.processModel import ComponentSystem - -component_system = ComponentSystem(1) - -from CADETProcess.processModel import Inlet - -inlet_1 = Inlet(component_system, 'inlet_1') -inlet_1.c = [1] - -inlet_2 = Inlet(component_system, 'inlet_2') -inlet_2.c = [0] - -from CADETProcess.processModel import Outlet -outlet = Outlet(component_system, 'outlet') - -from CADETProcess.processModel import FlowSheet -flow_sheet = FlowSheet(component_system) - -flow_sheet.add_unit(inlet_1) -flow_sheet.add_unit(inlet_2) -flow_sheet.add_unit(outlet) - -flow_sheet.add_connection(inlet_1, outlet) -flow_sheet.add_connection(inlet_2, outlet) - -from CADETProcess.processModel import Process -process = Process(flow_sheet, 'flow_exercise_2_dyn_flow') -process.cycle_time = 180 - -Q = 1e-6/60 -process.add_event('first_grad_1', 'flow_sheet.inlet_1.flow_rate', [0, Q/60], 0) -process.add_event('first_grad_2', 'flow_sheet.inlet_2.flow_rate', [Q, -Q/60], 0) - -process.add_event('start_plateau_1', 'flow_sheet.inlet_1.flow_rate', Q, 60) -process.add_event('start_plateau_2', 'flow_sheet.inlet_2.flow_rate', 0, 60) - -process.add_event('second_grad_1', 'flow_sheet.inlet_1.flow_rate', [Q, -Q/60], 120) -process.add_event('second_grad_2', 'flow_sheet.inlet_2.flow_rate', [0, Q/60], 120) - - -from CADETProcess.simulator import Cadet -simulator = Cadet() - -simulation_results = simulator.simulate(process) - -_ = simulation_results.solution.outlet.inlet.plot() -``` diff --git a/02 CADET Fundamentals/02 residence time distributions/01_rtd.ipynb b/02 CADET Fundamentals/02 residence time distributions/01_rtd.ipynb new file mode 100644 index 0000000..ab03ac4 --- /dev/null +++ b/02 CADET Fundamentals/02 residence time distributions/01_rtd.ipynb @@ -0,0 +1,653 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "c051d24d", + "metadata": {}, + "source": [ + "# Residence time distributions\n", + "\n", + "Basic residence time theory treats a system with an input and an output. The residence time of a small particle is the time between entering and leaving the system.\n", + "\n", + "```{figure} ./resources/system.png\n", + ":width: 50%\n", + ":align: center\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "511733eb", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "The residence time distribution can be described with the function $E(t)$ which has the properties of a probability distribution:\n", + "$$E(t) \\ge 0~\\text{and}~\\int_0^\\infty E(t)~dt = 1$$" + ] + }, + { + "cell_type": "markdown", + "id": "8b298635", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "Residence time distributions are measured by introducing a non-reactive tracer into the system at the inlet:\n", + "1. Change input concentration according to a known function (e.g. Dirac $\\delta$-function or step function)\n", + "2. Measure output concentration\n", + "3. Transform output concentration in residence time distribution curve $E(t)$" + ] + }, + { + "cell_type": "markdown", + "id": "1c85cf75", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "For some simple systems (e.g. CSTR and Plug flow reactor model) analytic solutions exist and we can compare them with the simulations of CADET." + ] + }, + { + "cell_type": "markdown", + "id": "d35a6142", + "metadata": {}, + "source": [ + "**In this lesson, we will:**\n", + "- Learn about system responses.\n", + "- Setup our first 'real' unit operation models.\n", + "- Analyze the solution and take a look 'inside' a column." + ] + }, + { + "cell_type": "markdown", + "id": "83fe8cfa", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Example 1: Continuous stirred-tank reactor\n", + "\n", + "In an ideal continuous stirred-tank reactor (CSTR), the flow at the inlet is completely and instantly mixed into the bulk of the reactor. The reactor and the outlet fluid have identical, homogeneous compositions at all times. The residence time distribution is exponential:\n", + "\n", + "$$E(t) = \\frac{1}{\\tau} \\cdot e^{-\\frac{t}{\\tau}}$$\n", + "\n", + "with\n", + "\n", + "$$\\tau = \\frac{V}{Q}$$" + ] + }, + { + "cell_type": "markdown", + "id": "23df8825", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "```{note}\n", + "In reality, it is impossible to obtain such rapid mixing, especially on industrial scales where reactor vessels may range between 1 and thousands of cubic meters, and hence the RTD of a real reactor will deviate from the ideal exponential decay.\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "1b96e296", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "To model this system, the following flow sheet is assumed:\n", + "\n", + "```{figure} ./resources/RTD_CSTR.png\n", + ":width: 50%\n", + ":align: center\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "6fbb1413", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### CSTR model\n", + "\n", + "The `CSTR` [model](https://cadet.github.io/master/modelling/unit_operations/cstr.html#cstr-model) in CADET requires the following [parameters](https://cadet.github.io/master/interface/unit_operations/cstr.html#cstr-config):\n", + "- Initial volume\n", + "- Initial concentration\n", + "\n", + "In later examples, we will also associate [adsorption models](https://cadet.github.io/master/modelling/binding/index.html) and [chemical reactions](https://cadet.github.io/master/modelling/reactions.html) with the `CSTR` unit operation.\n", + "For this example, however, we will only consider convective flow.\n", + "Also, we assume that the flow rate is constant over time." + ] + }, + { + "cell_type": "markdown", + "id": "57815ab6", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "Assume the following parameters:\n", + "\n", + "\n", + "| Parameter | Value | Unit | Attribute |\n", + "| ------------------- | ----- | ---- | --------- |\n", + "| Volume | 1 | L | `V` |\n", + "| Mean Residence Time | 1 | min | - |\n", + "\n", + "\n", + "**Note:** Since the `CSTR` has a variable volume, the flow rate also needs to be set." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7ca1f4d5", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import ComponentSystem\n", + "\n", + "component_system = ComponentSystem(1)\n", + "\n", + "tau = 60\n", + "V = 1e-3\n", + "Q = V/tau\n", + "\n", + "# Unit Operations\n", + "from CADETProcess.processModel import Inlet, Cstr, Outlet\n", + "\n", + "## Inlet\n", + "# We assume constant flow. Concentrations will later be modified using events.\n", + "inlet = Inlet(component_system, 'inlet')\n", + "inlet.flow_rate = Q\n", + "\n", + "cstr = Cstr(component_system, 'cstr')\n", + "cstr.c = [0]\n", + "cstr.V = V\n", + "cstr.flow_rate = Q\n", + "\n", + "outlet = Outlet(component_system, 'outlet')\n", + "\n", + "# Flow Sheet\n", + "from CADETProcess.processModel import FlowSheet\n", + "\n", + "flow_sheet = FlowSheet(component_system)\n", + "\n", + "flow_sheet.add_unit(inlet)\n", + "flow_sheet.add_unit(cstr)\n", + "flow_sheet.add_unit(outlet)\n", + "\n", + "flow_sheet.add_connection(inlet, cstr)\n", + "flow_sheet.add_connection(cstr, outlet)" + ] + }, + { + "cell_type": "markdown", + "id": "01d8a544", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Pulse experiment CSTR\n", + "\n", + "This method requires the introduction of a very small volume of concentrated tracer at the inlet of a CSTR, such that it approaches the Dirac $\\delta$-function.\n", + "By definition, the integral of this function is equal to one.\n", + "Although an infinitely short injection cannot be produced, it can be made much smaller than the mean residence time of the vessel.\n", + "\n", + "```{figure} ./resources/system_dirac.png\n", + ":width: 50%\n", + ":align: center\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0a9eed51", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "step_size = 1e-3\n", + "\n", + "from CADETProcess.processModel import Process\n", + "process = Process(flow_sheet, 'rtd_cstr')\n", + "process.cycle_time = 10 * tau\n", + "\n", + "process.add_event('start peak', 'flow_sheet.inlet.c', 1/step_size, 0)\n", + "process.add_event('end peak', 'flow_sheet.inlet.c', 0, step_size)" + ] + }, + { + "cell_type": "markdown", + "id": "4e6a9960", + "metadata": { + "slideshow": { + "slide_type": "slide" + }, + "user_expressions": [] + }, + "source": [ + "### Simulate Process" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6582333a", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.simulator import Cadet\n", + "\n", + "simulator = Cadet()\n", + "\n", + "simulation_results = simulator.simulate(process)\n", + "simulation_results.solution.cstr.inlet.plot()\n", + "simulation_results.solution.cstr.outlet.plot()" + ] + }, + { + "cell_type": "markdown", + "id": "3b446a88", + "metadata": { + "slideshow": { + "slide_type": "slide" + }, + "user_expressions": [] + }, + "source": [ + "## Example 2: Plug flow reactor\n", + "\n", + "In an ideal plug flow reactor the fluid elements that enter at the same time continue to move at the same rate and leave together.\n", + "Therefore, fluid entering at time $t$ will exit at time $t + \\tau$, where $\\tau$ is the residence time.\n", + "The fraction leaving is a step function, going from $0$ to $1$ at time $\\tau$.\n", + "The distribution function is therefore a Dirac delta function at $\\tau$.\n", + "\n", + "```{figure} ./resources/RTD_PFR.png\n", + ":width: 50%\n", + ":align: center\n", + "\n", + "```\n", + "\n", + "$$E(t) = \\delta (t - \\tau)$$\n", + "\n", + "\n", + "The RTD of a real reactor deviates from that of an ideal reactor, depending on the hydrodynamics (e.g. the axial dispersion) within the vessel." + ] + }, + { + "cell_type": "markdown", + "id": "90958632", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### PFR model\n", + "\n", + "Although in CADET there is no explicit implementation of the PFR model, we can still model this reactor if we use any of the column models and set the porosity to 1 and the axial dispersion to 0.\n", + "\n", + "In this example, we will use the `LumpedRateModelWithoutPores`. For the model equations see [here](https://cadet.github.io/master/modelling/unit_operations/lumped_rate_model_without_pores.html) and the parameters [here](https://cadet.github.io/master/interface/unit_operations/lumped_rate_model_without_pores.html)." + ] + }, + { + "cell_type": "markdown", + "id": "3a0e063c", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "Assume the following parameters:\n", + "\n", + "\n", + "| Parameter | Value | Unit | Attribute |\n", + "| -------------------------- | ----- | ---- | -------------------- |\n", + "| Reactor Length | 1 | L | `V` |\n", + "| Reactor Cross Section Area | 0.1 | m² | `cross_section_area` |\n", + "| Mean Residence Time | 1 | min | - |" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "246e2973", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import ComponentSystem\n", + "\n", + "component_system = ComponentSystem(1)\n", + "\n", + "tau = 60\n", + "length = 1\n", + "cross_section_area = 0.1\n", + "V = cross_section_area * length\n", + "Q = V/tau\n", + "\n", + "# Unit Operations\n", + "from CADETProcess.processModel import Inlet, TubularReactor, Outlet\n", + "\n", + "## Inlet\n", + "# We assume constant flow. Concentrations will later be modified using events.\n", + "inlet = Inlet(component_system, 'inlet')\n", + "inlet.flow_rate = V/tau\n", + "\n", + "pfr = TubularReactor(component_system, 'pfr')\n", + "pfr.length = 1\n", + "pfr.cross_section_area = cross_section_area\n", + "\n", + "pfr.axial_dispersion = 0\n", + "\n", + "outlet = Outlet(component_system, 'outlet')\n", + "\n", + "# Flow Sheet\n", + "from CADETProcess.processModel import FlowSheet\n", + "\n", + "flow_sheet = FlowSheet(component_system)\n", + "\n", + "flow_sheet.add_unit(inlet)\n", + "flow_sheet.add_unit(pfr)\n", + "flow_sheet.add_unit(outlet)\n", + "\n", + "flow_sheet.add_connection(inlet, pfr)\n", + "flow_sheet.add_connection(pfr, outlet)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ab0de274", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "step_size = 1e-3\n", + "\n", + "from CADETProcess.processModel import Process\n", + "process = Process(flow_sheet, 'rtd_pfr')\n", + "process.cycle_time = 2 * tau\n", + "\n", + "process.add_event('start peak', 'flow_sheet.inlet.c', 1/step_size, 0)\n", + "process.add_event('end peak', 'flow_sheet.inlet.c', 0, step_size)" + ] + }, + { + "cell_type": "markdown", + "id": "8f18b48a", + "metadata": { + "slideshow": { + "slide_type": "slide" + }, + "user_expressions": [] + }, + "source": [ + "### Simulate Process" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "577cca0e", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.simulator import Cadet\n", + "\n", + "simulator = Cadet()\n", + "\n", + "simulation_results = simulator.simulate(process)\n", + "simulation_results.solution.pfr.inlet.plot()\n", + "simulation_results.solution.pfr.outlet.plot()" + ] + }, + { + "cell_type": "markdown", + "id": "4ee79baa", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "```{warning}\n", + "Because of numerical dispersion, solvers like CADET are not suited to simulate stiff systems like the one presented.\n", + "To get a more acurate solution, the number of axial cells needs to be increased (a lot) which also increases simulation time (a lot).\n", + "Since usually there is some (physical) dispersion in real systems anyway, mostly this is not a problem because it will smoothen the profiles anyway.\n", + "The example just serves to show the limitations of CADET and that while it may not be very accurate in this case, the value of the mean residence time is still where we would expect it.\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "02689665", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Discretization\n", + "\n", + "In case of the column, there are several options for adapting the spatial discretization of the PDE model.\n", + "However, the two most important ones are the number of grid cells in the column (axial direction) and the particles.\n", + "Since the lumped rate model without pores does not have particles, we only need to specify axial cells `n_col`.\n", + "The default is $100$ which should work for most scenarios.\n", + "\n", + "\n", + "```{note}\n", + "CADET currently uses a finite volume scheme for the spatial discretization.\n", + "However, we are in the process of implementing a new method, the *Discontinuous Galerkin* method which will increase speed substantially.\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0e51b6cb", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "pfr.discretization" + ] + }, + { + "cell_type": "markdown", + "id": "67b70d73", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### High discretization" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "165a6fab", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "pfr.discretization.ncol = 2000\n", + "simulation_results = simulator.simulate(process)\n", + "simulation_results.solution.pfr.outlet.plot()" + ] + }, + { + "cell_type": "markdown", + "id": "ebd6b959", + "metadata": { + "slideshow": { + "slide_type": "slide" + }, + "user_expressions": [] + }, + "source": [ + "### Low discretization" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f1862042", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "pfr.discretization.ncol = 20\n", + "simulation_results = simulator.simulate(process)\n", + "simulation_results.solution.pfr.outlet.plot()" + ] + }, + { + "cell_type": "markdown", + "id": "c43c36dd", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Visualization\n", + "\n", + "Additionally to the solution at the inlet and outlet of a unit operation, we can also take a look inside the column to see the peak move.\n", + "\n", + "For this purpose, set the flag in the unit's `SolutionRecorder`.\n", + "Then, the `SimulationResults` will also contain an entry for the bulk.\n", + "\n", + "**Note:** Since this solution is two-dimensinal (space and time), the solution can be plotted at a given position (`plot_at_location`) or a given time (`plot_at_time`)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ab42c1ae", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "pfr.solution_recorder.write_solution_bulk = True\n", + "\n", + "simulation_results = simulator.simulate(process)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "29a773d1", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "simulation_results.solution.pfr.bulk.plot_at_position(0.5)\n", + "simulation_results.solution.pfr.bulk.plot_at_time(0.01)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "76e4ad3e", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from ipywidgets import interact, interactive\n", + "import ipywidgets as widgets\n", + "\n", + "Visualization\n", + "def graph_column(time=0):\n", + " fig, ax = simulation_results.solution.pfr.bulk.plot_at_time(time)\n", + " ax.set_ylim(0,0.1)\n", + "\n", + "style = {'description_width': 'initial'}\n", + "interact(\n", + " graph_column,\n", + " time=widgets.IntSlider(\n", + " min=0, max=process.cycle_time, step=10, layout={'width': '800px'}, style=style, description='Time'\n", + " )\n", + ")" + ] + } + ], + "metadata": { + "jupytext": { + "formats": "ipynb,md:myst" + }, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/02 CADET Fundamentals/02 residence time distributions/01_rtd.md b/02 CADET Fundamentals/02 residence time distributions/01_rtd.md deleted file mode 100644 index 7a05145..0000000 --- a/02 CADET Fundamentals/02 residence time distributions/01_rtd.md +++ /dev/null @@ -1,388 +0,0 @@ ---- -jupytext: - formats: ipynb,md:myst - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.15.2 -kernelspec: - display_name: Python 3 (ipykernel) - language: python - name: python3 ---- - -# Residence time distributions - -Basic residence time theory treats a system with an input and an output. The residence time of a small particle is the time between entering and leaving the system. - -```{figure} ./resources/system.png -:width: 50% -:align: center -``` - -+++ {"slideshow": {"slide_type": "fragment"}} - -The residence time distribution can be described with the function $E(t)$ which has the properties of a probability distribution: -$$E(t) \ge 0~\text{and}~\int_0^\infty E(t)~dt = 1$$ - -+++ {"slideshow": {"slide_type": "slide"}} - -Residence time distributions are measured by introducing a non-reactive tracer into the system at the inlet: -1. Change input concentration according to a known function (e.g. Dirac $\delta$-function or step function) -2. Measure output concentration -3. Transform output concentration in residence time distribution curve $E(t)$ - -+++ {"slideshow": {"slide_type": "fragment"}} - -For some simple systems (e.g. CSTR and Plug flow reactor model) analytic solutions exist and we can compare them with the simulations of CADET. - -+++ - -**In this lesson, we will:** -- Learn about system responses. -- Setup our first 'real' unit operation models. -- Analyze the solution and take a look 'inside' a column. - -+++ {"slideshow": {"slide_type": "slide"}} - -## Example 1: Continuous stirred-tank reactor - -In an ideal continuous stirred-tank reactor (CSTR), the flow at the inlet is completely and instantly mixed into the bulk of the reactor. The reactor and the outlet fluid have identical, homogeneous compositions at all times. The residence time distribution is exponential: - -$$E(t) = \frac{1}{\tau} \cdot e^{-\frac{t}{\tau}}$$ - -with - -$$\tau = \frac{V}{Q}$$ - -+++ {"slideshow": {"slide_type": "fragment"}} - -```{note} -In reality, it is impossible to obtain such rapid mixing, especially on industrial scales where reactor vessels may range between 1 and thousands of cubic meters, and hence the RTD of a real reactor will deviate from the ideal exponential decay. -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -To model this system, the following flow sheet is assumed: - -```{figure} ./resources/RTD_CSTR.png -:width: 50% -:align: center -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -### CSTR model - -The `CSTR` [model](https://cadet.github.io/master/modelling/unit_operations/cstr.html#cstr-model) in CADET requires the following [parameters](https://cadet.github.io/master/interface/unit_operations/cstr.html#cstr-config): -- Initial volume -- Initial concentration - -In later examples, we will also associate [adsorption models](https://cadet.github.io/master/modelling/binding/index.html) and [chemical reactions](https://cadet.github.io/master/modelling/reactions.html) with the `CSTR` unit operation. -For this example, however, we will only consider convective flow. -Also, we assume that the flow rate is constant over time. - -+++ {"slideshow": {"slide_type": "slide"}} - -Assume the following parameters: - - -| Parameter | Value | Unit | Attribute | -| ------------------- | ----- | ---- | --------- | -| Volume | 1 | L | `V` | -| Mean Residence Time | 1 | min | - | - - -**Note:** Since the `CSTR` has a variable volume, the flow rate also needs to be set. - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.processModel import ComponentSystem - -component_system = ComponentSystem(1) - -tau = 60 -V = 1e-3 -Q = V/tau - -# Unit Operations -from CADETProcess.processModel import Inlet, Cstr, Outlet - -## Inlet -# We assume constant flow. Concentrations will later be modified using events. -inlet = Inlet(component_system, 'inlet') -inlet.flow_rate = Q - -cstr = Cstr(component_system, 'cstr') -cstr.c = [0] -cstr.V = V -cstr.flow_rate = Q - -outlet = Outlet(component_system, 'outlet') - -# Flow Sheet -from CADETProcess.processModel import FlowSheet - -flow_sheet = FlowSheet(component_system) - -flow_sheet.add_unit(inlet) -flow_sheet.add_unit(cstr) -flow_sheet.add_unit(outlet) - -flow_sheet.add_connection(inlet, cstr) -flow_sheet.add_connection(cstr, outlet) -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -### Pulse experiment CSTR - -This method requires the introduction of a very small volume of concentrated tracer at the inlet of a CSTR, such that it approaches the Dirac $\delta$-function. -By definition, the integral of this function is equal to one. -Although an infinitely short injection cannot be produced, it can be made much smaller than the mean residence time of the vessel. - -```{figure} ./resources/system_dirac.png -:width: 50% -:align: center -``` - -```{code-cell} ipython3 -:tags: [solution] - -step_size = 1e-3 - -from CADETProcess.processModel import Process -process = Process(flow_sheet, 'rtd_cstr') -process.cycle_time = 10 * tau - -process.add_event('start peak', 'flow_sheet.inlet.c', 1/step_size, 0) -process.add_event('end peak', 'flow_sheet.inlet.c', 0, step_size) -``` - -+++ {"user_expressions": [], "slideshow": {"slide_type": "slide"}} - -### Simulate Process - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.simulator import Cadet - -simulator = Cadet() - -simulation_results = simulator.simulate(process) -simulation_results.solution.cstr.inlet.plot() -simulation_results.solution.cstr.outlet.plot() -``` - -+++ {"user_expressions": [], "slideshow": {"slide_type": "slide"}} - -## Example 2: Plug flow reactor - -In an ideal plug flow reactor the fluid elements that enter at the same time continue to move at the same rate and leave together. -Therefore, fluid entering at time $t$ will exit at time $t + \tau$, where $\tau$ is the residence time. -The fraction leaving is a step function, going from $0$ to $1$ at time $\tau$. -The distribution function is therefore a Dirac delta function at $\tau$. - -```{figure} ./resources/RTD_PFR.png -:width: 50% -:align: center - -``` - -$$E(t) = \delta (t - \tau)$$ - - -The RTD of a real reactor deviates from that of an ideal reactor, depending on the hydrodynamics (e.g. the axial dispersion) within the vessel. - -+++ {"slideshow": {"slide_type": "slide"}} - -### PFR model - -Although in CADET there is no explicit implementation of the PFR model, we can still model this reactor if we use any of the column models and set the porosity to 1 and the axial dispersion to 0. - -In this example, we will use the `LumpedRateModelWithoutPores`. For the model equations see [here](https://cadet.github.io/master/modelling/unit_operations/lumped_rate_model_without_pores.html) and the parameters [here](https://cadet.github.io/master/interface/unit_operations/lumped_rate_model_without_pores.html). - -+++ {"slideshow": {"slide_type": "fragment"}} - -Assume the following parameters: - - -| Parameter | Value | Unit | Attribute | -| -------------------------- | ----- | ---- | -------------------- | -| Reactor Length | 1 | L | `V` | -| Reactor Cross Section Area | 0.1 | m² | `cross_section_area` | -| Mean Residence Time | 1 | min | - | - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.processModel import ComponentSystem - -component_system = ComponentSystem(1) - -tau = 60 -length = 1 -cross_section_area = 0.1 -V = cross_section_area * length -Q = V/tau - -# Unit Operations -from CADETProcess.processModel import Inlet, TubularReactor, Outlet - -## Inlet -# We assume constant flow. Concentrations will later be modified using events. -inlet = Inlet(component_system, 'inlet') -inlet.flow_rate = V/tau - -pfr = TubularReactor(component_system, 'pfr') -pfr.length = 1 -pfr.cross_section_area = cross_section_area - -pfr.axial_dispersion = 0 - -outlet = Outlet(component_system, 'outlet') - -# Flow Sheet -from CADETProcess.processModel import FlowSheet - -flow_sheet = FlowSheet(component_system) - -flow_sheet.add_unit(inlet) -flow_sheet.add_unit(pfr) -flow_sheet.add_unit(outlet) - -flow_sheet.add_connection(inlet, pfr) -flow_sheet.add_connection(pfr, outlet) -``` - -```{code-cell} ipython3 -:tags: [solution] - -step_size = 1e-3 - -from CADETProcess.processModel import Process -process = Process(flow_sheet, 'rtd_pfr') -process.cycle_time = 2 * tau - -process.add_event('start peak', 'flow_sheet.inlet.c', 1/step_size, 0) -process.add_event('end peak', 'flow_sheet.inlet.c', 0, step_size) -``` - -+++ {"user_expressions": [], "slideshow": {"slide_type": "slide"}} - -### Simulate Process - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.simulator import Cadet - -simulator = Cadet() - -simulation_results = simulator.simulate(process) -simulation_results.solution.pfr.inlet.plot() -simulation_results.solution.pfr.outlet.plot() -``` - -+++ {"slideshow": {"slide_type": "fragment"}} - -```{warning} -Because of numerical dispersion, solvers like CADET are not suited to simulate stiff systems like the one presented. -To get a more acurate solution, the number of axial cells needs to be increased (a lot) which also increases simulation time (a lot). -Since usually there is some (physical) dispersion in real systems anyway, mostly this is not a problem because it will smoothen the profiles anyway. -The example just serves to show the limitations of CADET and that while it may not be very accurate in this case, the value of the mean residence time is still where we would expect it. -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -### Discretization - -In case of the column, there are several options for adapting the spatial discretization of the PDE model. -However, the two most important ones are the number of grid cells in the column (axial direction) and the particles. -Since the lumped rate model without pores does not have particles, we only need to specify axial cells `n_col`. -The default is $100$ which should work for most scenarios. - - -```{note} -CADET currently uses a finite volume scheme for the spatial discretization. -However, we are in the process of implementing a new method, the *Discontinuous Galerkin* method which will increase speed substantially. -``` - -```{code-cell} ipython3 -:tags: [solution] - -pfr.discretization -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -### High discretization - -```{code-cell} ipython3 -:tags: [solution] - -pfr.discretization.ncol = 2000 -simulation_results = simulator.simulate(process) -simulation_results.solution.pfr.outlet.plot() -``` - -+++ {"user_expressions": [], "slideshow": {"slide_type": "slide"}} - -### Low discretization - -```{code-cell} ipython3 -:tags: [solution] - -pfr.discretization.ncol = 20 -simulation_results = simulator.simulate(process) -simulation_results.solution.pfr.outlet.plot() -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -### Visualization - -Additionally to the solution at the inlet and outlet of a unit operation, we can also take a look inside the column to see the peak move. - -For this purpose, set the flag in the unit's `SolutionRecorder`. -Then, the `SimulationResults` will also contain an entry for the bulk. - -**Note:** Since this solution is two-dimensinal (space and time), the solution can be plotted at a given position (`plot_at_location`) or a given time (`plot_at_time`). - -```{code-cell} ipython3 -:tags: [solution] - -pfr.solution_recorder.write_solution_bulk = True - -simulation_results = simulator.simulate(process) -``` - -```{code-cell} ipython3 -:tags: [solution] - -simulation_results.solution.pfr.bulk.plot_at_position(0.5) -simulation_results.solution.pfr.bulk.plot_at_time(0.01) -``` - -```{code-cell} ipython3 -:tags: [solution] - -from ipywidgets import interact, interactive -import ipywidgets as widgets - -Visualization -def graph_column(time=0): - fig, ax = simulation_results.solution.pfr.bulk.plot_at_time(time) - ax.set_ylim(0,0.1) - -style = {'description_width': 'initial'} -interact( - graph_column, - time=widgets.IntSlider( - min=0, max=process.cycle_time, step=10, layout={'width': '800px'}, style=style, description='Time' - ) -) -``` diff --git a/02 CADET Fundamentals/02 residence time distributions/02_rtd_exercise.ipynb b/02 CADET Fundamentals/02 residence time distributions/02_rtd_exercise.ipynb new file mode 100644 index 0000000..3b42ab2 --- /dev/null +++ b/02 CADET Fundamentals/02 residence time distributions/02_rtd_exercise.ipynb @@ -0,0 +1,260 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "7f38f40c", + "metadata": {}, + "source": [ + "# Residence Time Distribution - Exercises" + ] + }, + { + "cell_type": "markdown", + "id": "143210be", + "metadata": {}, + "source": [ + "## Exercise 1: Step function in CSTR\n", + "\n", + "Analyze how the concentration profile of a `CSTR` reacts to a step function:\n", + "```{figure} ./resources/step.png\n", + ":width: 50%\n", + ":align: center\n", + "```\n", + "\n", + "```{figure} ./resources/RTD_CSTR.png\n", + ":width: 50%\n", + ":align: center\n", + "```\n", + "\n", + "\n", + "\n", + "***Hint:*** Always check the input arguments of our model template functions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3734f51f", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import ComponentSystem\n", + "\n", + "component_system = ComponentSystem(1)\n", + "\n", + "tau = 60\n", + "V = 1e-3\n", + "Q = V/tau\n", + "\n", + "# Unit Operations\n", + "from CADETProcess.processModel import Inlet, Cstr, Outlet\n", + "\n", + "## Inlet\n", + "# We assume constant flow and constant inlet concentrations.\n", + "inlet = Inlet(component_system, 'inlet')\n", + "inlet.c = [1]\n", + "inlet.flow_rate = Q\n", + "\n", + "cstr = Cstr(component_system, 'cstr')\n", + "cstr.c = [0]\n", + "cstr.V = V\n", + "cstr.flow_rate = Q\n", + "\n", + "outlet = Outlet(component_system, 'outlet')\n", + "\n", + "# Flow Sheet\n", + "from CADETProcess.processModel import FlowSheet\n", + "\n", + "flow_sheet = FlowSheet(component_system)\n", + "\n", + "flow_sheet.add_unit(inlet)\n", + "flow_sheet.add_unit(cstr)\n", + "flow_sheet.add_unit(outlet)\n", + "\n", + "flow_sheet.add_connection(inlet, cstr)\n", + "flow_sheet.add_connection(cstr, outlet)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0718a26e", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import Process\n", + "process = Process(flow_sheet, 'rtd_cstr')\n", + "process.cycle_time = 10 * tau" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9adcfbf5", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.simulator import Cadet\n", + "\n", + "simulator = Cadet()\n", + "\n", + "simulation_results = simulator.simulate(process)\n", + "simulation_results.solution.cstr.inlet.plot()\n", + "simulation_results.solution.cstr.outlet.plot()" + ] + }, + { + "cell_type": "markdown", + "id": "c5a47200", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Exercise 2: Step function in Tubular reactor\n", + "\n", + "**Task:** Also analyze the system behaviour of a Tubular reactor for different input profiles (see Exercise 1)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "be72db48", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import ComponentSystem\n", + "\n", + "component_system = ComponentSystem(1)\n", + "\n", + "tau = 60\n", + "V = 1e-3\n", + "Q = V/tau\n", + "\n", + "# Unit Operations\n", + "from CADETProcess.processModel import Inlet, TubularReactor, Outlet\n", + "\n", + "## Inlet\n", + "# We assume constant flow and constant inlet concentrations.\n", + "inlet = Inlet(component_system, 'inlet')\n", + "inlet.c = [1]\n", + "inlet.flow_rate = Q\n", + "\n", + "pfr = TubularReactor(component_system, 'pfr')\n", + "pfr.length = 1\n", + "pfr.diameter = 0.1\n", + "pfr.axial_dispersion = 0\n", + "\n", + "outlet = Outlet(component_system, 'outlet')\n", + "\n", + "# Flow Sheet\n", + "from CADETProcess.processModel import FlowSheet\n", + "\n", + "flow_sheet = FlowSheet(component_system)\n", + "\n", + "flow_sheet.add_unit(inlet)\n", + "flow_sheet.add_unit(pfr)\n", + "flow_sheet.add_unit(outlet)\n", + "\n", + "flow_sheet.add_connection(inlet, pfr)\n", + "flow_sheet.add_connection(pfr, outlet)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3f894fbc", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import Process\n", + "process = Process(flow_sheet, 'rtd_pfr')\n", + "process.cycle_time = 10 * tau" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "92f22192", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.simulator import Cadet\n", + "\n", + "simulator = Cadet()\n", + "\n", + "simulation_results = simulator.simulate(process)\n", + "simulation_results.solution.pfr.inlet.plot()\n", + "simulation_results.solution.pfr.outlet.plot()" + ] + }, + { + "cell_type": "markdown", + "id": "609a8998", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Bonus Exercise\n", + "Many systems can be modelled by a chain of unit operations.\n", + "\n", + "```{figure} ./resources/system_chain.png\n", + ":width: 50%\n", + ":align: center\n", + "```\n", + "Try connecting combining both the CSTR with a Tubular reactor and analyze the behavior." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "be836c76", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "jupytext": { + "formats": "ipynb,md:myst" + }, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/02 CADET Fundamentals/02 residence time distributions/02_rtd_exercise.md b/02 CADET Fundamentals/02 residence time distributions/02_rtd_exercise.md deleted file mode 100644 index 0568a38..0000000 --- a/02 CADET Fundamentals/02 residence time distributions/02_rtd_exercise.md +++ /dev/null @@ -1,179 +0,0 @@ ---- -jupytext: - formats: ipynb,md:myst - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.15.2 -kernelspec: - display_name: Python 3 (ipykernel) - language: python - name: python3 ---- - -+++ -# Residence Time Distribution - Exercises - - -+++ -## Exercise 1: Step function in CSTR - -Analyze how the concentration profile of a `CSTR` reacts to a step function: -```{figure} ./resources/step.png -:width: 50% -:align: center -``` - -```{figure} ./resources/RTD_CSTR.png -:width: 50% -:align: center -``` - - - -***Hint:*** Always check the input arguments of our model template functions. - - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.processModel import ComponentSystem - -component_system = ComponentSystem(1) - -tau = 60 -V = 1e-3 -Q = V/tau - -# Unit Operations -from CADETProcess.processModel import Inlet, Cstr, Outlet - -## Inlet -# We assume constant flow and constant inlet concentrations. -inlet = Inlet(component_system, 'inlet') -inlet.c = [1] -inlet.flow_rate = Q - -cstr = Cstr(component_system, 'cstr') -cstr.c = [0] -cstr.V = V -cstr.flow_rate = Q - -outlet = Outlet(component_system, 'outlet') - -# Flow Sheet -from CADETProcess.processModel import FlowSheet - -flow_sheet = FlowSheet(component_system) - -flow_sheet.add_unit(inlet) -flow_sheet.add_unit(cstr) -flow_sheet.add_unit(outlet) - -flow_sheet.add_connection(inlet, cstr) -flow_sheet.add_connection(cstr, outlet) -``` - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.processModel import Process -process = Process(flow_sheet, 'rtd_cstr') -process.cycle_time = 10 * tau -``` - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.simulator import Cadet - -simulator = Cadet() - -simulation_results = simulator.simulate(process) -simulation_results.solution.cstr.inlet.plot() -simulation_results.solution.cstr.outlet.plot() -``` - -+++ {"slideshow": {"slide_type": "slide"}} -## Exercise 2: Step function in Tubular reactor - -**Task:** Also analyze the system behaviour of a Tubular reactor for different input profiles (see Exercise 1). - - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.processModel import ComponentSystem - -component_system = ComponentSystem(1) - -tau = 60 -V = 1e-3 -Q = V/tau - -# Unit Operations -from CADETProcess.processModel import Inlet, TubularReactor, Outlet - -## Inlet -# We assume constant flow and constant inlet concentrations. -inlet = Inlet(component_system, 'inlet') -inlet.c = [1] -inlet.flow_rate = Q - -pfr = TubularReactor(component_system, 'pfr') -pfr.length = 1 -pfr.diameter = 0.1 -pfr.axial_dispersion = 0 - -outlet = Outlet(component_system, 'outlet') - -# Flow Sheet -from CADETProcess.processModel import FlowSheet - -flow_sheet = FlowSheet(component_system) - -flow_sheet.add_unit(inlet) -flow_sheet.add_unit(pfr) -flow_sheet.add_unit(outlet) - -flow_sheet.add_connection(inlet, pfr) -flow_sheet.add_connection(pfr, outlet) -``` - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.processModel import Process -process = Process(flow_sheet, 'rtd_pfr') -process.cycle_time = 10 * tau -``` - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.simulator import Cadet - -simulator = Cadet() - -simulation_results = simulator.simulate(process) -simulation_results.solution.pfr.inlet.plot() -simulation_results.solution.pfr.outlet.plot() -``` - -+++ {"slideshow": {"slide_type": "slide"}} -## Bonus Exercise -Many systems can be modelled by a chain of unit operations. - -```{figure} ./resources/system_chain.png -:width: 50% -:align: center -``` -Try connecting combining both the CSTR with a Tubular reactor and analyze the behavior. - - -```{code-cell} ipython3 -:tags: [solution] - - -``` diff --git a/02 CADET Fundamentals/03 chemical reactions/01_reactions.ipynb b/02 CADET Fundamentals/03 chemical reactions/01_reactions.ipynb new file mode 100644 index 0000000..695b3eb --- /dev/null +++ b/02 CADET Fundamentals/03 chemical reactions/01_reactions.ipynb @@ -0,0 +1,292 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "afe8a2ba", + "metadata": {}, + "source": [ + "# Chemical Reactions\n", + "\n", + "$$\n", + "\\require{mhchem}\n", + "$$\n", + "\n", + "Since version 4, it is possible to model chemical reactions with CADET using mass action law type reactions (see [Reaction models](https://cadet.github.io/master/modelling/reactions.html#reaction-models)).\n", + "The mass action law states that the speed of a reaction is proportional to the product of the concentrations of their reactants.\n", + "\n", + "In CADET-Process, a reaction module was implemented to facilitate the setup of these reactions.\n", + "There are two different classes: the `MassActionLaw` which is used for bulk phase reactions, as well as `MassActionLawParticle` which is specifically designed to model reactions in particle pore phase.\n", + "\n", + "In this tutorial, we're going to learn how to setup:\n", + "- Forward Reactions\n", + "- Equilibrium Reactions" + ] + }, + { + "cell_type": "markdown", + "id": "1b513df3", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Forward Reactions\n", + "As a simple example, consider the following system:\n", + "\n", + "$$\n", + "\\ce{1 A ->[k_{AB}] 1 B}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "id": "d3cec3b2", + "metadata": {}, + "source": [ + "First, initiate a `ComponentSystem` with components `A` and `B`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c5e770ca", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import ComponentSystem\n", + "component_system = ComponentSystem(['A', 'B'])" + ] + }, + { + "cell_type": "markdown", + "id": "3e4e77c4", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "Then, configure the `MassActionLaw` reaction model.\n", + "To instantiate it, pass the `ComponentSystem`.\n", + "Then, add the reaction using the `add_reaction` method.\n", + "The following arguments are expected:\n", + "- indices: The indices of the components that take part in the reaction (useful for bigger systems)\n", + "- stoichiometric coefficients in the order of the indices\n", + "- forward reaction rate\n", + "- backward reaction rate" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f4a141fd", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import MassActionLaw\n", + "reaction_system = MassActionLaw(component_system)\n", + "reaction_system.add_reaction(\n", + " indices=[0,1],\n", + " coefficients=[-1, 1],\n", + " k_fwd=0.1,\n", + " k_bwd=0\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "ed777e33", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "To demonstrate this reaction, a `Cstr` is instantiated and the reaction is added to the tank.\n", + "Moreover, the initial conditions are set.\n", + "In principle, the `Cstr` supports reactions in bulk and particle pore phase.\n", + "Since the porosity is $1$ by default, only the bulk phase is considered." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0319fb21", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import Cstr\n", + "\n", + "reactor = Cstr(component_system, 'reactor')\n", + "reactor.bulk_reaction_model = reaction_system\n", + "reactor.V = 1e-6\n", + "reactor.c = [1.0, 0.0]" + ] + }, + { + "cell_type": "markdown", + "id": "f3d642a6", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "Now, the reactor is added to a `FlowSheet` and a `Process` is set up.\n", + "Here, the `FlowSheet` only consists of a single `Cstr`, and there are no `Events` in the process." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bbd4a5cb", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import FlowSheet\n", + "flow_sheet = FlowSheet(component_system)\n", + "flow_sheet.add_unit(reactor)\n", + "\n", + "from CADETProcess.processModel import Process\n", + "process = Process(flow_sheet, 'reaction_demo')\n", + "process.cycle_time = 100" + ] + }, + { + "cell_type": "markdown", + "id": "b1a96c73", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "After simulation, the results can be plotted:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7c5748db", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.simulator import Cadet\n", + "simulator = Cadet()\n", + "sim_results = simulator.run(process)\n", + "_ = sim_results.solution.reactor.outlet.plot()" + ] + }, + { + "cell_type": "markdown", + "id": "764c8996", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Equilibrium Reactions\n", + "It is also possible to consider equilibrium reactions where the product can react back to the educts.\n", + "\n", + "$$\n", + "\\ce{ 2 A <=>[k_{AB}][k_{BA}] B}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "id": "bbcaac0b", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "Here, the same units, flow sheet, and process are reused which were defined above." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "80e79a2a", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "reaction_system = MassActionLaw(component_system)\n", + "reaction_system.add_reaction(\n", + " indices=[0,1],\n", + " coefficients=[-2, 1],\n", + " k_fwd=0.2,\n", + " k_bwd=0.1\n", + ")\n", + "\n", + "reactor.bulk_reaction_model = reaction_system" + ] + }, + { + "cell_type": "markdown", + "id": "0a86f118", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "After simulation, the results can be plotted:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0ca62fb9", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "sim_results = simulator.run(process)\n", + "_ = sim_results.solution.reactor.outlet.plot()" + ] + } + ], + "metadata": { + "jupytext": { + "formats": "ipynb,md:myst" + }, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/02 CADET Fundamentals/03 chemical reactions/01_reactions.md b/02 CADET Fundamentals/03 chemical reactions/01_reactions.md deleted file mode 100644 index e9d5389..0000000 --- a/02 CADET Fundamentals/03 chemical reactions/01_reactions.md +++ /dev/null @@ -1,159 +0,0 @@ ---- -jupytext: - formats: ipynb,md:myst - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.15.2 -kernelspec: - display_name: Python 3 (ipykernel) - language: python - name: python3 ---- - -# Chemical Reactions - -$$ -\require{mhchem} -$$ - -Since version 4, it is possible to model chemical reactions with CADET using mass action law type reactions (see [Reaction models](https://cadet.github.io/master/modelling/reactions.html#reaction-models)). -The mass action law states that the speed of a reaction is proportional to the product of the concentrations of their reactants. - -In CADET-Process, a reaction module was implemented to facilitate the setup of these reactions. -There are two different classes: the `MassActionLaw` which is used for bulk phase reactions, as well as `MassActionLawParticle` which is specifically designed to model reactions in particle pore phase. - -In this tutorial, we're going to learn how to setup: -- Forward Reactions -- Equilibrium Reactions - -+++ {"slideshow": {"slide_type": "slide"}} - -## Forward Reactions -As a simple example, consider the following system: - -$$ -\ce{1 A ->[k_{AB}] 1 B} -$$ - -+++ - -First, initiate a `ComponentSystem` with components `A` and `B`. - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.processModel import ComponentSystem -component_system = ComponentSystem(['A', 'B']) -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -Then, configure the `MassActionLaw` reaction model. -To instantiate it, pass the `ComponentSystem`. -Then, add the reaction using the `add_reaction` method. -The following arguments are expected: -- indices: The indices of the components that take part in the reaction (useful for bigger systems) -- stoichiometric coefficients in the order of the indices -- forward reaction rate -- backward reaction rate - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.processModel import MassActionLaw -reaction_system = MassActionLaw(component_system) -reaction_system.add_reaction( - indices=[0,1], - coefficients=[-1, 1], - k_fwd=0.1, - k_bwd=0 -) -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -To demonstrate this reaction, a `Cstr` is instantiated and the reaction is added to the tank. -Moreover, the initial conditions are set. -In principle, the `Cstr` supports reactions in bulk and particle pore phase. -Since the porosity is $1$ by default, only the bulk phase is considered. - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.processModel import Cstr - -reactor = Cstr(component_system, 'reactor') -reactor.bulk_reaction_model = reaction_system -reactor.V = 1e-6 -reactor.c = [1.0, 0.0] -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -Now, the reactor is added to a `FlowSheet` and a `Process` is set up. -Here, the `FlowSheet` only consists of a single `Cstr`, and there are no `Events` in the process. - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.processModel import FlowSheet -flow_sheet = FlowSheet(component_system) -flow_sheet.add_unit(reactor) - -from CADETProcess.processModel import Process -process = Process(flow_sheet, 'reaction_demo') -process.cycle_time = 100 -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -After simulation, the results can be plotted: - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.simulator import Cadet -simulator = Cadet() -sim_results = simulator.run(process) -_ = sim_results.solution.reactor.outlet.plot() -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -## Equilibrium Reactions -It is also possible to consider equilibrium reactions where the product can react back to the educts. - -$$ -\ce{ 2 A <=>[k_{AB}][k_{BA}] B} -$$ - -+++ {"slideshow": {"slide_type": "slide"}} - -Here, the same units, flow sheet, and process are reused which were defined above. - -```{code-cell} ipython3 -:tags: [solution] - -reaction_system = MassActionLaw(component_system) -reaction_system.add_reaction( - indices=[0,1], - coefficients=[-2, 1], - k_fwd=0.2, - k_bwd=0.1 -) - -reactor.bulk_reaction_model = reaction_system -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -After simulation, the results can be plotted: - -```{code-cell} ipython3 -:tags: [solution] - -sim_results = simulator.run(process) -_ = sim_results.solution.reactor.outlet.plot() -``` diff --git a/02 CADET Fundamentals/03 chemical reactions/02_reaction_exercises.ipynb b/02 CADET Fundamentals/03 chemical reactions/02_reaction_exercises.ipynb new file mode 100644 index 0000000..abd9060 --- /dev/null +++ b/02 CADET Fundamentals/03 chemical reactions/02_reaction_exercises.ipynb @@ -0,0 +1,307 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "e6aae2c9", + "metadata": {}, + "source": [ + "# Lesson 3: Exercises\n", + "\n", + "\n", + "## Exercise 1: Equilibrium reaction with intermediate state\n", + "\n", + "We will consider again consider a batch reaction in a `CSTR` but this time, we will also account for an intermediate state:\n", + "\n", + "$\\require{mhchem}$\n", + "$$\\ce{A <=>[k_{AB}][k_{BA}] B <=>[k_{BC}][k_{CB}] C}$$\n", + "\n", + "***Task:*** Implement the reaction and plot the results. Assume the following values for the rate constants:\n", + "- $k_{AB} = 0.080~s^{-1}$\n", + "- $k_{BA} = 0.0~s^{-1}$\n", + "- $k_{BC} = 0.060~s^{-1}$\n", + "- $k_{CB} = 0.0~s^{-1}$" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7345dc0f", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import ComponentSystem\n", + "component_system = ComponentSystem(['A', 'B', 'C'])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "51759793", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import MassActionLaw\n", + "reaction_system = MassActionLaw(component_system)\n", + "reaction_system.add_reaction(\n", + " indices=[0,1],\n", + " coefficients=[-1, 1],\n", + " k_fwd=0.080,\n", + " k_bwd=0.0\n", + ")\n", + "reaction_system.add_reaction(\n", + " indices=[1,2],\n", + " coefficients=[-1, 1],\n", + " k_fwd=0.06,\n", + " k_bwd=0.0\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "becc6fb7", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import Cstr\n", + "reactor = Cstr(component_system, 'reactor')\n", + "reactor.V = 1e-6\n", + "reactor.bulk_reaction_model = reaction_system\n", + "reactor.c = [1.0, 0.0, 0.0]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "487c74ed", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import FlowSheet\n", + "flow_sheet = FlowSheet(component_system)\n", + "flow_sheet.add_unit(reactor)\n", + "\n", + "from CADETProcess.processModel import Process\n", + "process = Process(flow_sheet, 'reaction_demo')\n", + "process.cycle_time = 100" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2f2f1875", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.simulator import Cadet\n", + "simulator = Cadet()\n", + "sim_results = simulator.run(process)\n", + "_ = sim_results.solution.reactor.outlet.plot()" + ] + }, + { + "cell_type": "markdown", + "id": "96b49652", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Exercise 2: Equilibrium reaction with intermediate state in tubular reactor\n", + "***Task:*** Implement the reaction in a tubular reactor and plot the results at the outlet, as well as over the length of the column for the last timestep.\n", + "\n", + "Consider again the reaction parameters from Exercise 1.\n", + "For the `TubularReactor` use the following parameters:\n", + "- length: 1 m\n", + "- diameter: 10 cm\n", + "- flow rate: 0.1 m³/s\n", + "\n", + "\n", + "\n", + "\n", + "***Hint:*** To plot the bulk solution, make sure that you set the `write_solution_bulk` flag in the `TubularReactor`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9f89440e", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import ComponentSystem\n", + "\n", + "component_system = ComponentSystem(['A', 'B','C'])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e51d4d89", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import MassActionLaw\n", + "\n", + "reaction_system = MassActionLaw(component_system)\n", + "reaction_system.add_reaction(\n", + " indices=[0, 1],\n", + " coefficients=[-1, 1],\n", + " k_fwd=0.08,\n", + " k_bwd=0.0\n", + ")\n", + "reaction_system.add_reaction(\n", + " indices=[1, 2],\n", + " coefficients=[-1, 1],\n", + " k_fwd=0.060,\n", + " k_bwd=0.0\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fca8b8f8", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import Inlet, TubularReactor, Outlet\n", + "\n", + "inlet = Inlet(component_system, name='inlet')\n", + "inlet.c = [10, 0, 0]\n", + "inlet.flow_rate = 1e-4\n", + "\n", + "outlet = Outlet(component_system, name= 'outlet')\n", + "\n", + "reactor = TubularReactor(component_system, 'reactor')\n", + "reactor.length = 1\n", + "reactor.diameter = 0.1\n", + "reactor.bulk_reaction_model = reaction_system\n", + "reactor.c = [0.0, 0.0, 0.0]\n", + "reactor.axial_dispersion = 1e-7\n", + "reactor.solution_recorder.write_solution_bulk = True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "07bdac5b", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import FlowSheet\n", + "\n", + "flow_sheet = FlowSheet(component_system)\n", + "flow_sheet.add_unit(reactor)\n", + "flow_sheet.add_unit(inlet)\n", + "flow_sheet.add_unit(outlet)\n", + "flow_sheet.add_connection(inlet, reactor)\n", + "flow_sheet.add_connection(reactor, outlet)\n", + "\n", + "from CADETProcess.processModel import Process\n", + "\n", + "process = Process(flow_sheet, 'reaction_demo')\n", + "process.cycle_time = 100" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "de3770d9", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.simulator import Cadet\n", + "\n", + "simulator = Cadet()\n", + "sim_results = simulator.simulate(process)\n", + "_ = sim_results.solution.reactor.bulk.plot_at_time(100)" + ] + }, + { + "cell_type": "markdown", + "id": "b877e497", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Bonus Exercise\n", + "\n", + "Try implementing other reaction systems such as:\n", + "\n", + "$\\require{mhchem}$\n", + "$$\\ce{A + B ->[k_{1}] C}$$\n", + "$$\\ce{2 A + B ->[k_{1}] C}$$\n", + "$$\\ce{A + B <=>[k_{1}][k_{-1}] C ->[k_{2}] D}$$\n", + "$$\\ce{A + B ->[k_{1}] C} \\quad \\text{and as a parallel reaction} \\quad \\ce{A + C ->[k_{2}] D}$$" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "38dd83ab", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "jupytext": { + "formats": "ipynb,md:myst" + }, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/02 CADET Fundamentals/03 chemical reactions/02_reaction_exercises.md b/02 CADET Fundamentals/03 chemical reactions/02_reaction_exercises.md deleted file mode 100644 index 72b9ec8..0000000 --- a/02 CADET Fundamentals/03 chemical reactions/02_reaction_exercises.md +++ /dev/null @@ -1,196 +0,0 @@ ---- -jupytext: - formats: ipynb,md:myst - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.15.2 -kernelspec: - display_name: Python 3 (ipykernel) - language: python - name: python3 ---- - -# Lesson 3: Exercises - - -## Exercise 1: Equilibrium reaction with intermediate state - -We will consider again consider a batch reaction in a `CSTR` but this time, we will also account for an intermediate state: - -$\require{mhchem}$ -$$\ce{A <=>[k_{AB}][k_{BA}] B <=>[k_{BC}][k_{CB}] C}$$ - -***Task:*** Implement the reaction and plot the results. Assume the following values for the rate constants: -- $k_{AB} = 0.080~s^{-1}$ -- $k_{BA} = 0.0~s^{-1}$ -- $k_{BC} = 0.060~s^{-1}$ -- $k_{CB} = 0.0~s^{-1}$ - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.processModel import ComponentSystem -component_system = ComponentSystem(['A', 'B', 'C']) -``` - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.processModel import MassActionLaw -reaction_system = MassActionLaw(component_system) -reaction_system.add_reaction( - indices=[0,1], - coefficients=[-1, 1], - k_fwd=0.080, - k_bwd=0.0 -) -reaction_system.add_reaction( - indices=[1,2], - coefficients=[-1, 1], - k_fwd=0.06, - k_bwd=0.0 -) -``` - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.processModel import Cstr -reactor = Cstr(component_system, 'reactor') -reactor.V = 1e-6 -reactor.bulk_reaction_model = reaction_system -reactor.c = [1.0, 0.0, 0.0] -``` - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.processModel import FlowSheet -flow_sheet = FlowSheet(component_system) -flow_sheet.add_unit(reactor) - -from CADETProcess.processModel import Process -process = Process(flow_sheet, 'reaction_demo') -process.cycle_time = 100 -``` - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.simulator import Cadet -simulator = Cadet() -sim_results = simulator.run(process) -_ = sim_results.solution.reactor.outlet.plot() -``` - -+++ {"slideshow": {"slide_type": "slide"}} -## Exercise 2: Equilibrium reaction with intermediate state in tubular reactor -***Task:*** Implement the reaction in a tubular reactor and plot the results at the outlet, as well as over the length of the column for the last timestep. - -Consider again the reaction parameters from Exercise 1. -For the `TubularReactor` use the following parameters: -- length: 1 m -- diameter: 10 cm -- flow rate: 0.1 m³/s - - - - -***Hint:*** To plot the bulk solution, make sure that you set the `write_solution_bulk` flag in the `TubularReactor`. - - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.processModel import ComponentSystem - -component_system = ComponentSystem(['A', 'B','C']) -``` - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.processModel import MassActionLaw - -reaction_system = MassActionLaw(component_system) -reaction_system.add_reaction( - indices=[0, 1], - coefficients=[-1, 1], - k_fwd=0.08, - k_bwd=0.0 -) -reaction_system.add_reaction( - indices=[1, 2], - coefficients=[-1, 1], - k_fwd=0.060, - k_bwd=0.0 -) -``` - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.processModel import Inlet, TubularReactor, Outlet - -inlet = Inlet(component_system, name='inlet') -inlet.c = [10, 0, 0] -inlet.flow_rate = 1e-4 - -outlet = Outlet(component_system, name= 'outlet') - -reactor = TubularReactor(component_system, 'reactor') -reactor.length = 1 -reactor.diameter = 0.1 -reactor.bulk_reaction_model = reaction_system -reactor.c = [0.0, 0.0, 0.0] -reactor.axial_dispersion = 1e-7 -reactor.solution_recorder.write_solution_bulk = True -``` - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.processModel import FlowSheet - -flow_sheet = FlowSheet(component_system) -flow_sheet.add_unit(reactor) -flow_sheet.add_unit(inlet) -flow_sheet.add_unit(outlet) -flow_sheet.add_connection(inlet, reactor) -flow_sheet.add_connection(reactor, outlet) - -from CADETProcess.processModel import Process - -process = Process(flow_sheet, 'reaction_demo') -process.cycle_time = 100 -``` - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.simulator import Cadet - -simulator = Cadet() -sim_results = simulator.simulate(process) -_ = sim_results.solution.reactor.bulk.plot_at_time(100) -``` - -+++ {"slideshow": {"slide_type": "slide"}} -## Bonus Exercise - -Try implementing other reaction systems such as: - -$\require{mhchem}$ -$$\ce{A + B ->[k_{1}] C}$$ -$$\ce{2 A + B ->[k_{1}] C}$$ -$$\ce{A + B <=>[k_{1}][k_{-1}] C ->[k_{2}] D}$$ -$$\ce{A + B ->[k_{1}] C} \quad \text{and as a parallel reaction} \quad \ce{A + C ->[k_{2}] D}$$ - - -```{code-cell} ipython3 -:tags: [solution] - - -``` diff --git a/02 CADET Fundamentals/04 adsorption/01_adsorption.ipynb b/02 CADET Fundamentals/04 adsorption/01_adsorption.ipynb new file mode 100644 index 0000000..4164ed3 --- /dev/null +++ b/02 CADET Fundamentals/04 adsorption/01_adsorption.ipynb @@ -0,0 +1,825 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "427d4f08", + "metadata": {}, + "source": [ + "# Adsorption models\n", + "\n", + "At the core of chromatographic processes are interactions between the components we want to separate and a stationary\n", + "phase.\n", + "\n", + "These components can be: atoms, ions or molecules of a gas, liquid or dissolved solids." + ] + }, + { + "cell_type": "markdown", + "id": "a9579f18", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Isotherms\n", + "\n", + "These interactions can often be described in terms of an isotherm:\n", + "\n", + "```{note}\n", + "\n", + "**Isotherm:**\n", + "An equation that describes how much of a component is __bound__ to the stationary phase or __solved__ in the mobile phase.\n", + "\n", + "Valid for a constant _temperature_ (iso - _therm_).\n", + "\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "c2eab5fe", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "```{figure} ./resources/isotherm.png\n", + ":width: 50%\n", + ":align: center\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "b79225a4", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "In CADET, many different models for adsorption are implemented.\n", + "All of the models can be modelled kinetically or in rapid equilibrium.\n", + "Moreover, many of them include features such as competitive effects, multi state binding, or a mobile phase modifier." + ] + }, + { + "cell_type": "markdown", + "id": "5d2eedc2", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "```{figure} ./resources/isotherm_models.png\n", + ":width: 100%\n", + ":align: center\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "6be4debc", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "**In this lesson,** we will:\n", + "\n", + "- Learn about different adsorption models.\n", + "- Associate adsorption models with different unit operations." + ] + }, + { + "cell_type": "markdown", + "id": "fd052472", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Example 1: Linear model\n", + "\n", + "The simplest model for adsorption is the [linear model](https://cadet.github.io/master/modelling/binding/linear.html).\n", + "\n", + "Analogously to Henry's law, it describes a linear correlation between the solved concentration and the bound\n", + "concentration of the component." + ] + }, + { + "cell_type": "markdown", + "id": "37ad716b", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "Let us consider a shaking flask experiment in a `CSTR` (without ingoing or outgoing streams).\n", + "In it, we add some porous material s.t.\n", + "\n", + "- the overal porosity is $0.5$.\n", + "- the volume is $1~L$\n", + "\n", + "Then, we add a solution of a component with a concentration of $1~mol \\cdot L^{-1}$.\n", + "\n", + "Let us first create a `ComponentSystem` and a `Linear` `BindingModel`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "349f8146", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import ComponentSystem\n", + "\n", + "component_system = ComponentSystem(1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f944fc0b", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import Linear\n", + "\n", + "binding_model = Linear(component_system, name=\"linear\")\n", + "binding_model.parameters\n", + "binding_model.is_kinetic = True\n", + "binding_model.adsorption_rate = [2]\n", + "binding_model.desorption_rate = [1]" + ] + }, + { + "cell_type": "markdown", + "id": "c07c9473", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "Now create the UnitOperation `Cstr` with a porosity of 0.5 and a volume of 1 L." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f70a8f6a", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "import numpy as np\n", + "from CADETProcess.processModel import Cstr\n", + "\n", + "reactor = Cstr(component_system, name='reactor')\n", + "reactor.binding_model = binding_model\n", + "\n", + "reactor.porosity = 0.5\n", + "reactor.V = 1e-3" + ] + }, + { + "cell_type": "markdown", + "id": "a8df9a23", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "Let's also initialize our `Cstr` with a component concentration of $1~mol \\cdot L^{-1}$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "29b2155a", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "reactor.c = [1]" + ] + }, + { + "cell_type": "markdown", + "id": "60d58e32", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "We care about the concentration of our component in the solid and bulk liquid phase, so let's tell the reactor to write\n", + "down those concentrations." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "522a0e11", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "reactor.solution_recorder.write_solution_bulk = True\n", + "reactor.solution_recorder.write_solution_solid = True" + ] + }, + { + "cell_type": "markdown", + "id": "366438fe", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "Now, create a `FlowSheet` and a `Process` and a `simulator`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cd812a23", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import FlowSheet\n", + "\n", + "flow_sheet = FlowSheet(component_system)\n", + "\n", + "flow_sheet.add_unit(reactor)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "974e9c45", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import Process\n", + "\n", + "process = Process(flow_sheet, 'process')\n", + "process.cycle_time = 10" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "156386ed", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.simulator import Cadet\n", + "simulator = Cadet()\n", + "sim_results = simulator.run(process)\n", + "\n", + "# _ = sim_results.solution.reactor.bulk.plot()\n", + "_ = sim_results.solution.reactor.outlet.plot()\n", + "_ = sim_results.solution.reactor.solid.plot()" + ] + }, + { + "cell_type": "markdown", + "id": "a59ab19a", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### A note on resolution\n", + "\n", + "As can be seen in the figure abore, the time resolution is not sufficiently high.\n", + "By default, CADET-Process stores 1 sample per second.\n", + "To increase the resolution, set the `time_resolution` parameter of the `Simulator`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1031a744", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "simulator.time_resolution = 0.1" + ] + }, + { + "cell_type": "markdown", + "id": "d67bfdba", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "Now, the solution looks much smoother." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b2f70439", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "sim_results = simulator.run(process)\n", + "\n", + "# _ = sim_results.solution.reactor.bulk.plot()\n", + "_ = sim_results.solution.reactor.outlet.plot()\n", + "_ = sim_results.solution.reactor.solid.plot()" + ] + }, + { + "cell_type": "markdown", + "id": "ec3b5ac0", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Example 2: Linear adsorption model with linear concentration gradient\n", + "\n", + "To plot the solid phase concentration as a function of the bulk concentration, we can introduce a linear concentration\n", + "gradient to the `CSTR` that has an initial concentration of $0~mM$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c097c9c4", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import ComponentSystem\n", + "\n", + "component_system = ComponentSystem(2)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d7d9a780", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import Linear\n", + "\n", + "binding_model = Linear(component_system, name='linear')\n", + "binding_model.is_kinetic = False\n", + "binding_model.adsorption_rate = [3, 2]\n", + "binding_model.desorption_rate = [1, 1]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2087c6b0", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import Inlet, Cstr\n", + "\n", + "inlet = Inlet(component_system, name='inlet')\n", + "inlet.c = [[0, 1, 0, 0], [0, 1, 0, 0]]\n", + "inlet.flow_rate = 1e-3\n", + "\n", + "reactor = Cstr(component_system, name='reactor')\n", + "reactor.binding_model = binding_model\n", + "\n", + "reactor.porosity = 0.5\n", + "reactor.V = 1e-3\n", + "reactor.c = [0.0, 0.0]\n", + "reactor.q = [0.0, 0.0] # optional\n", + "\n", + "reactor.solution_recorder.write_solution_bulk = True\n", + "reactor.solution_recorder.write_solution_solid = True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4faeefb9", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import FlowSheet\n", + "\n", + "flow_sheet = FlowSheet(component_system)\n", + "\n", + "flow_sheet.add_unit(reactor)\n", + "flow_sheet.add_unit(inlet)\n", + "\n", + "flow_sheet.add_connection(inlet, reactor)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b0ea392a", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import Process\n", + "\n", + "process = Process(flow_sheet, 'process')\n", + "process.cycle_time = 10" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "50d57f7c", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.simulator import Cadet\n", + "simulator = Cadet()\n", + "sim_results = simulator.run(process)\n", + "\n", + "# _ = sim_results.solution.reactor.bulk.plot()\n", + "_ = sim_results.solution.reactor.outlet.plot()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e89f0afd", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "# solution_bulk = sim_results.solution.reactor.bulk.solution\n", + "solution_bulk = sim_results.solution.reactor.outlet.solution\n", + "solution_solid = sim_results.solution.reactor.solid.solution\n", + "\n", + "import matplotlib.pyplot as plt\n", + "fig, ax = plt.subplots()\n", + "ax.plot(solution_bulk, solution_solid)\n", + "ax.set_title('Isotherm')\n", + "ax.set_xlabel('$c_{bulk}$')\n", + "ax.set_ylabel('$c_{solid}$')" + ] + }, + { + "cell_type": "markdown", + "id": "08d240ff", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Example 3: Multi component Langmuir model\n", + "\n", + "Usually, the linear isotherm can only be assumed for very low solute concentrations.\n", + "At higher, higher concentrations the limited number of available binding sites on the surface of the adsorbent also\n", + "needs to be considered which\n", + "the [Langmuir equation](https://cadet.github.io/master/modelling/binding/multi_component_langmuir.html) takes into\n", + "account.\n", + "\n", + "$$q = q_{sat} \\cdot \\frac{b \\cdot c}{1 + b \\cdot c} = \\frac{a \\cdot c}{1 + b \\cdot c}$$\n", + "\n", + "***with:***\n", + "\n", + "- $q_{Sat}$ = saturation loading\n", + "- $b$ = equilibrium factor" + ] + }, + { + "cell_type": "markdown", + "id": "4af6252d", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "***Assumptions:***\n", + "\n", + "- All of the adsorption sites are equivalent, and each site can only accommodate one molecule\n", + "- The surface is energetically homogeneous\n", + "- Adsorbed molecules do not interact\n", + "- There are no phase transitions\n", + "- At the maximum adsorption, only a monolayer is formed\n", + "\n", + "For this example, we will introduce a concentration step to the `CSTR`.\n", + "We consider two components, both with an inital concentration of $0~mM$, but with different binding strengths." + ] + }, + { + "cell_type": "markdown", + "id": "47cc10c3", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "To start: create a `ComponentSystem`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ad51ba3a", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import ComponentSystem\n", + "\n", + "component_system = ComponentSystem(2)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7cce7758", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import Langmuir\n", + "\n", + "binding_model = Langmuir(component_system, name='langmuir')\n", + "binding_model.is_kinetic = False\n", + "binding_model.adsorption_rate = [3,1]\n", + "binding_model.desorption_rate = [1,1]\n", + "binding_model.capacity = [1,1]" + ] + }, + { + "cell_type": "markdown", + "id": "196dfc7d", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "Set up the `processModel`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "662d8467", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import Inlet, Outlet, Cstr\n", + "\n", + "inlet = Inlet(component_system, name='inlet')\n", + "inlet.c = [1,1]\n", + "inlet.flow_rate = 1e-3\n", + "\n", + "reactor = Cstr(component_system, name='reactor')\n", + "reactor.binding_model = binding_model\n", + "\n", + "reactor.V = 1e-3\n", + "reactor.porosity = 0.5\n", + "reactor.c = [0.0, 0.0]\n", + "reactor.q = [0.0, 0.0] # optional\n", + "\n", + "reactor.solution_recorder.write_solution_bulk = True\n", + "reactor.solution_recorder.write_solution_solid = True" + ] + }, + { + "cell_type": "markdown", + "id": "1a27a3f8", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "Set up the `FlowSheet`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "32ce2bd1", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import FlowSheet\n", + "\n", + "flow_sheet = FlowSheet(component_system)\n", + "\n", + "flow_sheet.add_unit(reactor)\n", + "flow_sheet.add_unit(inlet)\n", + "\n", + "flow_sheet.add_connection(inlet,reactor)" + ] + }, + { + "cell_type": "markdown", + "id": "36c85cd7", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "Create a `Process`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "33d1cdd7", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import Process\n", + "\n", + "process = Process(flow_sheet, 'process')\n", + "process.cycle_time = 10" + ] + }, + { + "cell_type": "markdown", + "id": "f6a4cb50", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "Create a `Simulator` and simulate" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "52953d19", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.simulator import Cadet\n", + "\n", + "simulator = Cadet()\n", + "simulator.time_resolution = 0.01\n", + "\n", + "sim_results = simulator.run(process)\n", + "\n", + "# _ = sim_results.solution.reactor.bulk.plot()\n", + "_ = sim_results.solution.reactor.outlet.plot()" + ] + }, + { + "cell_type": "markdown", + "id": "8e9184f8", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "Plot the solutions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cb714d4b", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "# solution_bulk = sim_results.solution.reactor.bulk.solution\n", + "solution_bulk = sim_results.solution.reactor.outlet.solution\n", + "solution_solid = sim_results.solution.reactor.solid.solution\n", + "\n", + "import matplotlib.pyplot as plt\n", + "fig, ax = plt.subplots()\n", + "ax.plot(solution_bulk, solution_solid)\n", + "ax.legend([\"Component A\", \"Component B\"])\n", + "ax.set_title('Isotherm')\n", + "ax.set_xlabel('$c_{liquid}$')\n", + "ax.set_ylabel('$c_{solid}$')\n", + "plt.tight_layout()" + ] + } + ], + "metadata": { + "jupytext": { + "formats": "ipynb,md:myst" + }, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/02 CADET Fundamentals/04 adsorption/01_adsorption.md b/02 CADET Fundamentals/04 adsorption/01_adsorption.md deleted file mode 100644 index 1a30c2b..0000000 --- a/02 CADET Fundamentals/04 adsorption/01_adsorption.md +++ /dev/null @@ -1,448 +0,0 @@ ---- -jupytext: - formats: ipynb,md:myst - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.15.2 -kernelspec: - display_name: Python 3 (ipykernel) - language: python - name: python3 ---- - -# Adsorption models - -At the core of chromatographic processes are interactions between the components we want to separate and a stationary -phase. - -These components can be: atoms, ions or molecules of a gas, liquid or dissolved solids. - -+++ {"slideshow": {"slide_type": "slide"}} - -## Isotherms - -These interactions can often be described in terms of an isotherm: - -```{note} - -**Isotherm:** -An equation that describes how much of a component is __bound__ to the stationary phase or __solved__ in the mobile phase. - -Valid for a constant _temperature_ (iso - _therm_). - -``` - -+++ {"slideshow": {"slide_type": "fragment"}} - -```{figure} ./resources/isotherm.png -:width: 50% -:align: center -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -In CADET, many different models for adsorption are implemented. -All of the models can be modelled kinetically or in rapid equilibrium. -Moreover, many of them include features such as competitive effects, multi state binding, or a mobile phase modifier. - -+++ {"slideshow": {"slide_type": "fragment"}} - -```{figure} ./resources/isotherm_models.png -:width: 100% -:align: center -``` - -+++ {"slideshow": {"slide_type": "fragment"}} - -**In this lesson,** we will: - -- Learn about different adsorption models. -- Associate adsorption models with different unit operations. - -+++ {"slideshow": {"slide_type": "slide"}} - -## Example 1: Linear model - -The simplest model for adsorption is the [linear model](https://cadet.github.io/master/modelling/binding/linear.html). - -Analogously to Henry's law, it describes a linear correlation between the solved concentration and the bound -concentration of the component. - -+++ {"slideshow": {"slide_type": "fragment"}} - -Let us consider a shaking flask experiment in a `CSTR` (without ingoing or outgoing streams). -In it, we add some porous material s.t. - -- the overal porosity is $0.5$. -- the volume is $1~L$ - -Then, we add a solution of a component with a concentration of $1~mol \cdot L^{-1}$. - -Let us first create a `ComponentSystem` and a `Linear` `BindingModel`. - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.processModel import ComponentSystem - -component_system = ComponentSystem(1) -``` - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.processModel import Linear - -binding_model = Linear(component_system, name="linear") -binding_model.parameters -binding_model.is_kinetic = True -binding_model.adsorption_rate = [2] -binding_model.desorption_rate = [1] -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -Now create the UnitOperation `Cstr` with a porosity of 0.5 and a volume of 1 L. - -```{code-cell} ipython3 -:tags: [solution] - -import numpy as np -from CADETProcess.processModel import Cstr - -reactor = Cstr(component_system, name='reactor') -reactor.binding_model = binding_model - -reactor.porosity = 0.5 -reactor.V = 1e-3 -``` - -+++ {"slideshow": {"slide_type": "fragment"}} - -Let's also initialize our `Cstr` with a component concentration of $1~mol \cdot L^{-1}$. - -```{code-cell} ipython3 -:tags: [solution] - -reactor.c = [1] -``` - -+++ {"slideshow": {"slide_type": "fragment"}} - -We care about the concentration of our component in the solid and bulk liquid phase, so let's tell the reactor to write -down those concentrations. - -```{code-cell} ipython3 -:tags: [solution] - -reactor.solution_recorder.write_solution_bulk = True -reactor.solution_recorder.write_solution_solid = True -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -Now, create a `FlowSheet` and a `Process` and a `simulator` - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.processModel import FlowSheet - -flow_sheet = FlowSheet(component_system) - -flow_sheet.add_unit(reactor) -``` - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.processModel import Process - -process = Process(flow_sheet, 'process') -process.cycle_time = 10 -``` - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.simulator import Cadet -simulator = Cadet() -sim_results = simulator.run(process) - -# _ = sim_results.solution.reactor.bulk.plot() -_ = sim_results.solution.reactor.outlet.plot() -_ = sim_results.solution.reactor.solid.plot() -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -### A note on resolution - -As can be seen in the figure abore, the time resolution is not sufficiently high. -By default, CADET-Process stores 1 sample per second. -To increase the resolution, set the `time_resolution` parameter of the `Simulator`. - -```{code-cell} ipython3 -:tags: [solution] - -simulator.time_resolution = 0.1 -``` - -+++ {"slideshow": {"slide_type": "fragment"}} - -Now, the solution looks much smoother. - -```{code-cell} ipython3 -:tags: [solution] - -sim_results = simulator.run(process) - -# _ = sim_results.solution.reactor.bulk.plot() -_ = sim_results.solution.reactor.outlet.plot() -_ = sim_results.solution.reactor.solid.plot() -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -## Example 2: Linear adsorption model with linear concentration gradient - -To plot the solid phase concentration as a function of the bulk concentration, we can introduce a linear concentration -gradient to the `CSTR` that has an initial concentration of $0~mM$. - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.processModel import ComponentSystem - -component_system = ComponentSystem(2) -``` - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.processModel import Linear - -binding_model = Linear(component_system, name='linear') -binding_model.is_kinetic = False -binding_model.adsorption_rate = [3, 2] -binding_model.desorption_rate = [1, 1] -``` - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.processModel import Inlet, Cstr - -inlet = Inlet(component_system, name='inlet') -inlet.c = [[0, 1, 0, 0], [0, 1, 0, 0]] -inlet.flow_rate = 1e-3 - -reactor = Cstr(component_system, name='reactor') -reactor.binding_model = binding_model - -reactor.porosity = 0.5 -reactor.V = 1e-3 -reactor.c = [0.0, 0.0] -reactor.q = [0.0, 0.0] # optional - -reactor.solution_recorder.write_solution_bulk = True -reactor.solution_recorder.write_solution_solid = True -``` - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.processModel import FlowSheet - -flow_sheet = FlowSheet(component_system) - -flow_sheet.add_unit(reactor) -flow_sheet.add_unit(inlet) - -flow_sheet.add_connection(inlet, reactor) -``` - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.processModel import Process - -process = Process(flow_sheet, 'process') -process.cycle_time = 10 -``` - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.simulator import Cadet -simulator = Cadet() -sim_results = simulator.run(process) - -# _ = sim_results.solution.reactor.bulk.plot() -_ = sim_results.solution.reactor.outlet.plot() -``` - -```{code-cell} ipython3 -:tags: [solution] - -# solution_bulk = sim_results.solution.reactor.bulk.solution -solution_bulk = sim_results.solution.reactor.outlet.solution -solution_solid = sim_results.solution.reactor.solid.solution - -import matplotlib.pyplot as plt -fig, ax = plt.subplots() -ax.plot(solution_bulk, solution_solid) -ax.set_title('Isotherm') -ax.set_xlabel('$c_{bulk}$') -ax.set_ylabel('$c_{solid}$') -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -## Example 3: Multi component Langmuir model - -Usually, the linear isotherm can only be assumed for very low solute concentrations. -At higher, higher concentrations the limited number of available binding sites on the surface of the adsorbent also -needs to be considered which -the [Langmuir equation](https://cadet.github.io/master/modelling/binding/multi_component_langmuir.html) takes into -account. - -$$q = q_{sat} \cdot \frac{b \cdot c}{1 + b \cdot c} = \frac{a \cdot c}{1 + b \cdot c}$$ - -***with:*** - -- $q_{Sat}$ = saturation loading -- $b$ = equilibrium factor - -+++ {"slideshow": {"slide_type": "fragment"}} - -***Assumptions:*** - -- All of the adsorption sites are equivalent, and each site can only accommodate one molecule -- The surface is energetically homogeneous -- Adsorbed molecules do not interact -- There are no phase transitions -- At the maximum adsorption, only a monolayer is formed - -For this example, we will introduce a concentration step to the `CSTR`. -We consider two components, both with an inital concentration of $0~mM$, but with different binding strengths. - -+++ {"slideshow": {"slide_type": "slide"}} - -To start: create a `ComponentSystem` - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.processModel import ComponentSystem - -component_system = ComponentSystem(2) -``` - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.processModel import Langmuir - -binding_model = Langmuir(component_system, name='langmuir') -binding_model.is_kinetic = False -binding_model.adsorption_rate = [3,1] -binding_model.desorption_rate = [1,1] -binding_model.capacity = [1,1] -``` - -+++ {"slideshow": {"slide_type": "fragment"}} - -Set up the `processModel` - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.processModel import Inlet, Outlet, Cstr - -inlet = Inlet(component_system, name='inlet') -inlet.c = [1,1] -inlet.flow_rate = 1e-3 - -reactor = Cstr(component_system, name='reactor') -reactor.binding_model = binding_model - -reactor.V = 1e-3 -reactor.porosity = 0.5 -reactor.c = [0.0, 0.0] -reactor.q = [0.0, 0.0] # optional - -reactor.solution_recorder.write_solution_bulk = True -reactor.solution_recorder.write_solution_solid = True -``` - -+++ {"slideshow": {"slide_type": "fragment"}} - -Set up the `FlowSheet` - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.processModel import FlowSheet - -flow_sheet = FlowSheet(component_system) - -flow_sheet.add_unit(reactor) -flow_sheet.add_unit(inlet) - -flow_sheet.add_connection(inlet,reactor) -``` - -+++ {"slideshow": {"slide_type": "fragment"}} - -Create a `Process` - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.processModel import Process - -process = Process(flow_sheet, 'process') -process.cycle_time = 10 -``` - -+++ {"slideshow": {"slide_type": "fragment"}} - -Create a `Simulator` and simulate - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.simulator import Cadet - -simulator = Cadet() -simulator.time_resolution = 0.01 - -sim_results = simulator.run(process) - -# _ = sim_results.solution.reactor.bulk.plot() -_ = sim_results.solution.reactor.outlet.plot() -``` - -+++ {"slideshow": {"slide_type": "fragment"}} - -Plot the solutions - -```{code-cell} ipython3 -:tags: [solution] - -# solution_bulk = sim_results.solution.reactor.bulk.solution -solution_bulk = sim_results.solution.reactor.outlet.solution -solution_solid = sim_results.solution.reactor.solid.solution - -import matplotlib.pyplot as plt -fig, ax = plt.subplots() -ax.plot(solution_bulk, solution_solid) -ax.legend(["Component A", "Component B"]) -ax.set_title('Isotherm') -ax.set_xlabel('$c_{liquid}$') -ax.set_ylabel('$c_{solid}$') -plt.tight_layout() -``` diff --git a/02 CADET Fundamentals/04 adsorption/02_adsorption_exercise.ipynb b/02 CADET Fundamentals/04 adsorption/02_adsorption_exercise.ipynb new file mode 100644 index 0000000..ae71b0b --- /dev/null +++ b/02 CADET Fundamentals/04 adsorption/02_adsorption_exercise.ipynb @@ -0,0 +1,287 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "b8fc8ceb", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Adsorption Models - Exercise" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "90c0941c", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import ComponentSystem\n", + "\n", + "component_system = ComponentSystem(['A'])\n", + "\n", + "from CADETProcess.processModel import Langmuir\n", + "\n", + "binding_model = Langmuir(component_system, name='langmuir')\n", + "binding_model.is_kinetic = True\n", + "binding_model.adsorption_rate = [3]\n", + "binding_model.desorption_rate = [1]\n", + "binding_model.capacity = [1]\n", + "\n", + "from CADETProcess.processModel import Inlet, Outlet, Cstr\n", + "\n", + "reactor = Cstr(component_system, name='reactor')\n", + "reactor.porosity = 0.5\n", + "reactor.binding_model = binding_model\n", + "reactor.V = 1e-3\n", + "\n", + "inlet = Inlet(component_system, name='inlet')\n", + "inlet.c = [[0,1,0,0]]\n", + "inlet.flow_rate = 1e-3\n", + "\n", + "outlet = Outlet(component_system, name= 'outlet')\n", + "\n", + "\n", + "from CADETProcess.processModel import FlowSheet\n", + "\n", + "flow_sheet = FlowSheet(component_system)\n", + "\n", + "flow_sheet.add_unit(reactor)\n", + "flow_sheet.add_unit(inlet)\n", + "flow_sheet.add_unit(outlet)\n", + "\n", + "flow_sheet.add_connection(inlet,reactor)\n", + "flow_sheet.add_connection(reactor,outlet)\n", + "\n", + "from CADETProcess.processModel import Process\n", + "\n", + "process = Process(flow_sheet, 'process')\n", + "process.cycle_time = 10\n", + "\n", + "from CADETProcess.simulator import Cadet\n", + "simulator = Cadet()\n", + "sim_results = simulator.run(process)\n", + "_ = sim_results.solution.reactor.outlet.plot()" + ] + }, + { + "cell_type": "markdown", + "id": "454dddcf", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Exercise 1: Breakthrough curve in a Column\n", + "\n", + "In this example, consider a column with a `Linear` isotherm with $k_{eq, 1} = 2$, and $k_{eq, 2} = 1$.\n", + "We want to record the breakthrough curve and for this purpose, a concentration step with $c_{feed} = [1.0, 1.0]~g \\cdot L^{-1} $is introduced at $t = 0$.\n", + "\n", + "***Task:*** Implement the configuration for the adsorption isotherm and pass it to the `create_column_template()` function. You also need to provide the number of can also provide\n", + "\n", + "Try experimenting with:\n", + "- The binding strength.\n", + "- The number of components.\n", + "- Modify how many components bind.\n", + "- Compare kinetic binding with rapid equilibrium." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8f60260f", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import ComponentSystem\n", + "\n", + "component_system = ComponentSystem(['A', 'B'])\n", + "\n", + "from CADETProcess.processModel import Linear\n", + "\n", + "binding_model = Linear(component_system, name='linear')\n", + "binding_model.is_kinetic = False\n", + "binding_model.adsorption_rate = [2, 1]\n", + "binding_model.desorption_rate = [1, 1]\n", + "\n", + "\n", + "from CADETProcess.processModel import Inlet, Outlet, Cstr\n", + "\n", + "reactor = Cstr(component_system, name='reactor')\n", + "reactor.porosity = 0.5\n", + "reactor.binding_model = binding_model\n", + "reactor.V = 1e-3\n", + "\n", + "inlet = Inlet(component_system, name='inlet')\n", + "inlet.c = [1.0, 1.0]\n", + "inlet.flow_rate = 1e-3\n", + "\n", + "outlet = Outlet(component_system, name= 'outlet')\n", + "\n", + "\n", + "from CADETProcess.processModel import FlowSheet\n", + "\n", + "flow_sheet = FlowSheet(component_system)\n", + "\n", + "flow_sheet.add_unit(reactor)\n", + "flow_sheet.add_unit(inlet)\n", + "flow_sheet.add_unit(outlet)\n", + "\n", + "flow_sheet.add_connection(inlet,reactor)\n", + "flow_sheet.add_connection(reactor,outlet)\n", + "\n", + "from CADETProcess.processModel import Process\n", + "\n", + "process = Process(flow_sheet, 'process')\n", + "process.cycle_time = 10\n", + "\n", + "from CADETProcess.simulator import Cadet\n", + "simulator = Cadet()\n", + "simulator.time_resolution = 0.1\n", + "\n", + "sim_results = simulator.run(process)\n", + "_ = sim_results.solution.reactor.outlet.plot()" + ] + }, + { + "cell_type": "markdown", + "id": "0375c3e4", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Exercise 2: Langmuir Model in Column\n", + "\n", + "Let us consider the same experiment as in the first example, but this time with `Langmuir` adsorption model.\n", + "\n", + "with $k_{eq, i} = [2, 1]$, and $q_{max, i} = [10, 10]$.\n", + "We again introduce a step with $c_{feed} = [1.0, 1.0]~g \\cdot L^{-1} $is introduced at $t = 0$ and the breakthrough curve is recorded.\n", + "\n", + "Try experimenting with:\n", + "- The binding strenth.\n", + "- The number of components.\n", + "- Modify how many components bind.\n", + "- Compare kinetic binding with rapid equilibrium." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e057938b", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import ComponentSystem\n", + "\n", + "component_system = ComponentSystem(['A', 'B'])\n", + "\n", + "from CADETProcess.processModel import Langmuir\n", + "\n", + "binding_model = Langmuir(component_system, name='langmuir')\n", + "binding_model.is_kinetic = True\n", + "binding_model.adsorption_rate = [2,1]\n", + "binding_model.desorption_rate = [1,1]\n", + "binding_model.capacity = [10,10]\n", + "\n", + "\n", + "from CADETProcess.processModel import Inlet, Outlet, LumpedRateModelWithoutPores\n", + "\n", + "reactor = LumpedRateModelWithoutPores(component_system, name='reactor')\n", + "reactor.length = 0.5\n", + "reactor.diameter = 0.1784\n", + "reactor.total_porosity = 0.5\n", + "reactor.binding_model = binding_model\n", + "reactor.axial_dispersion = 1e-7\n", + "\n", + "inlet = Inlet(component_system, name='inlet')\n", + "inlet.c = [1.0, 1.0]\n", + "inlet.flow_rate = 1e-3\n", + "\n", + "outlet = Outlet(component_system, name= 'outlet')\n", + "\n", + "\n", + "from CADETProcess.processModel import FlowSheet\n", + "\n", + "flow_sheet = FlowSheet(component_system)\n", + "\n", + "flow_sheet.add_unit(reactor)\n", + "flow_sheet.add_unit(inlet)\n", + "flow_sheet.add_unit(outlet)\n", + "\n", + "flow_sheet.add_connection(inlet,reactor)\n", + "flow_sheet.add_connection(reactor,outlet)\n", + "\n", + "from CADETProcess.processModel import Process\n", + "\n", + "process = Process(flow_sheet, 'process')\n", + "process.cycle_time = 100\n", + "\n", + "from CADETProcess.simulator import Cadet\n", + "simulator = Cadet()\n", + "sim_results = simulator.run(process)\n", + "_ = sim_results.solution.reactor.outlet.plot()" + ] + }, + { + "cell_type": "markdown", + "id": "252ead17", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Bonus Exercise\n", + "\n", + "There are many other models implemented in `CADET`.\n", + "Some binding models can account for multiple binding states, others allow for mobile phase modulators.\n", + "Moreover, binding models can use external functions for modifying the values of the parameters during the simulation.\n", + "This could be used to model other effects like temperature that have an influence on the binding strength.\n", + "\n", + "Try implementing some of the more advanced isotherms and features." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d9883817", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "jupytext": { + "formats": "ipynb,md:myst" + }, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/02 CADET Fundamentals/04 adsorption/02_adsorption_exercise.md b/02 CADET Fundamentals/04 adsorption/02_adsorption_exercise.md deleted file mode 100644 index 0e08794..0000000 --- a/02 CADET Fundamentals/04 adsorption/02_adsorption_exercise.md +++ /dev/null @@ -1,225 +0,0 @@ ---- -jupytext: - formats: ipynb,md:myst - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.15.2 -kernelspec: - display_name: Python 3 (ipykernel) - language: python - name: python3 ---- - -+++ {"slideshow": {"slide_type": "slide"}} -# Adsorption Models - Exercise - - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.processModel import ComponentSystem - -component_system = ComponentSystem(['A']) - -from CADETProcess.processModel import Langmuir - -binding_model = Langmuir(component_system, name='langmuir') -binding_model.is_kinetic = True -binding_model.adsorption_rate = [3] -binding_model.desorption_rate = [1] -binding_model.capacity = [1] - -from CADETProcess.processModel import Inlet, Outlet, Cstr - -reactor = Cstr(component_system, name='reactor') -reactor.porosity = 0.5 -reactor.binding_model = binding_model -reactor.V = 1e-3 - -inlet = Inlet(component_system, name='inlet') -inlet.c = [[0,1,0,0]] -inlet.flow_rate = 1e-3 - -outlet = Outlet(component_system, name= 'outlet') - - -from CADETProcess.processModel import FlowSheet - -flow_sheet = FlowSheet(component_system) - -flow_sheet.add_unit(reactor) -flow_sheet.add_unit(inlet) -flow_sheet.add_unit(outlet) - -flow_sheet.add_connection(inlet,reactor) -flow_sheet.add_connection(reactor,outlet) - -from CADETProcess.processModel import Process - -process = Process(flow_sheet, 'process') -process.cycle_time = 10 - -from CADETProcess.simulator import Cadet -simulator = Cadet() -sim_results = simulator.run(process) -_ = sim_results.solution.reactor.outlet.plot() - -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -## Exercise 1: Breakthrough curve in a Column - -In this example, consider a column with a `Linear` isotherm with $k_{eq, 1} = 2$, and $k_{eq, 2} = 1$. -We want to record the breakthrough curve and for this purpose, a concentration step with $c_{feed} = [1.0, 1.0]~g \cdot L^{-1} $is introduced at $t = 0$. - -***Task:*** Implement the configuration for the adsorption isotherm and pass it to the `create_column_template()` function. You also need to provide the number of can also provide - -Try experimenting with: -- The binding strength. -- The number of components. -- Modify how many components bind. -- Compare kinetic binding with rapid equilibrium. - - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.processModel import ComponentSystem - -component_system = ComponentSystem(['A', 'B']) - -from CADETProcess.processModel import Linear - -binding_model = Linear(component_system, name='linear') -binding_model.is_kinetic = False -binding_model.adsorption_rate = [2, 1] -binding_model.desorption_rate = [1, 1] - - -from CADETProcess.processModel import Inlet, Outlet, Cstr - -reactor = Cstr(component_system, name='reactor') -reactor.porosity = 0.5 -reactor.binding_model = binding_model -reactor.V = 1e-3 - -inlet = Inlet(component_system, name='inlet') -inlet.c = [1.0, 1.0] -inlet.flow_rate = 1e-3 - -outlet = Outlet(component_system, name= 'outlet') - - -from CADETProcess.processModel import FlowSheet - -flow_sheet = FlowSheet(component_system) - -flow_sheet.add_unit(reactor) -flow_sheet.add_unit(inlet) -flow_sheet.add_unit(outlet) - -flow_sheet.add_connection(inlet,reactor) -flow_sheet.add_connection(reactor,outlet) - -from CADETProcess.processModel import Process - -process = Process(flow_sheet, 'process') -process.cycle_time = 10 - -from CADETProcess.simulator import Cadet -simulator = Cadet() -simulator.time_resolution = 0.1 - -sim_results = simulator.run(process) -_ = sim_results.solution.reactor.outlet.plot() -``` - -+++ {"slideshow": {"slide_type": "slide"}} -## Exercise 2: Langmuir Model in Column - -Let us consider the same experiment as in the first example, but this time with `Langmuir` adsorption model. - -with $k_{eq, i} = [2, 1]$, and $q_{max, i} = [10, 10]$. -We again introduce a step with $c_{feed} = [1.0, 1.0]~g \cdot L^{-1} $is introduced at $t = 0$ and the breakthrough curve is recorded. - -Try experimenting with: -- The binding strenth. -- The number of components. -- Modify how many components bind. -- Compare kinetic binding with rapid equilibrium. - - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.processModel import ComponentSystem - -component_system = ComponentSystem(['A', 'B']) - -from CADETProcess.processModel import Langmuir - -binding_model = Langmuir(component_system, name='langmuir') -binding_model.is_kinetic = True -binding_model.adsorption_rate = [2,1] -binding_model.desorption_rate = [1,1] -binding_model.capacity = [10,10] - - -from CADETProcess.processModel import Inlet, Outlet, LumpedRateModelWithoutPores - -reactor = LumpedRateModelWithoutPores(component_system, name='reactor') -reactor.length = 0.5 -reactor.diameter = 0.1784 -reactor.total_porosity = 0.5 -reactor.binding_model = binding_model -reactor.axial_dispersion = 1e-7 - -inlet = Inlet(component_system, name='inlet') -inlet.c = [1.0, 1.0] -inlet.flow_rate = 1e-3 - -outlet = Outlet(component_system, name= 'outlet') - - -from CADETProcess.processModel import FlowSheet - -flow_sheet = FlowSheet(component_system) - -flow_sheet.add_unit(reactor) -flow_sheet.add_unit(inlet) -flow_sheet.add_unit(outlet) - -flow_sheet.add_connection(inlet,reactor) -flow_sheet.add_connection(reactor,outlet) - -from CADETProcess.processModel import Process - -process = Process(flow_sheet, 'process') -process.cycle_time = 100 - -from CADETProcess.simulator import Cadet -simulator = Cadet() -sim_results = simulator.run(process) -_ = sim_results.solution.reactor.outlet.plot() - -``` - -+++ {"slideshow": {"slide_type": "slide"}} -## Bonus Exercise - -There are many other models implemented in `CADET`. -Some binding models can account for multiple binding states, others allow for mobile phase modulators. -Moreover, binding models can use external functions for modifying the values of the parameters during the simulation. -This could be used to model other effects like temperature that have an influence on the binding strength. - -Try implementing some of the more advanced isotherms and features. - - -```{code-cell} ipython3 -:tags: [solution] - - -``` diff --git a/02 CADET Fundamentals/Overview.ipynb b/02 CADET Fundamentals/Overview.ipynb new file mode 100644 index 0000000..1046a7b --- /dev/null +++ b/02 CADET Fundamentals/Overview.ipynb @@ -0,0 +1,28 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "c06e6389", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "jupytext": { + "formats": "ipynb,md:myst" + }, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/02 CADET Fundamentals/Overview.md b/02 CADET Fundamentals/Overview.md deleted file mode 100644 index 3f4059d..0000000 --- a/02 CADET Fundamentals/Overview.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -jupytext: - formats: ipynb,md:myst - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.15.2 -kernelspec: - display_name: Python 3 (ipykernel) - language: python - name: python3 ---- - -```{code-cell} ipython3 -:tags: [solution] - - -``` diff --git a/03 Process Simulation/01 Simple Chromatographic Processes/01_chromatography.ipynb b/03 Process Simulation/01 Simple Chromatographic Processes/01_chromatography.ipynb new file mode 100644 index 0000000..68d42dd --- /dev/null +++ b/03 Process Simulation/01 Simple Chromatographic Processes/01_chromatography.ipynb @@ -0,0 +1,843 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d037a740", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Simple Chromatographic Processes" + ] + }, + { + "cell_type": "markdown", + "id": "6ee52000", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "Liquid chromatography is a technique for the separation of mixtures dissolved in a fluid. That fluid is called the mobile phase, which carries the mixture through a structure holding another material, called the stationary phase." + ] + }, + { + "cell_type": "markdown", + "id": "db742938", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "The various components of the mixture interact with different strengths with the stationary phase. Therefore they travel at different speeds, causing them to separate.\n", + "\n", + "Different mechanisms can be used for the separation:\n", + " - adsorption,\n", + " - ion exchange,\n", + " - hydrophobic interactions,\n", + " - size exclusion,\n", + " - etc.\n", + "\n", + "For each mechanism, various stationary phases are available." + ] + }, + { + "cell_type": "markdown", + "id": "ce2ba0ce", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "There are broadly speaking, _two_ types of chromatography:\n", + "\n", + "__Preparative__ and __analytical__\n", + "\n", + "_Preparative_ targets isolation and purification of \"large\" quantities of a substance.\n", + "\n", + "_Analytical_ serves to identify or quantify analytes in a mixture.\n", + "\n", + "Generally, chromatographic models are used for optimization of _preparative_ processes." + ] + }, + { + "cell_type": "markdown", + "id": "9d70ecd0", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "For modelling these processes, we have to combine all of the techniques we learnt in the previous lessons:\n", + "- Configure unit operations models.\n", + "- Associate adsorption models with unit operations.\n", + "- Generate dynamic inlet profiles.\n", + "- Chemical reactions (if required)" + ] + }, + { + "cell_type": "markdown", + "id": "58757bc2", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Example 1: Dextran pulse\n", + "\n", + "In this exercise, we will consider the following system:\n", + "\n", + "```{figure} ./resources/system.png\n", + ":width: 50%\n", + ":align: center\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "1ebcdd83", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "Simplify the model by using a large, inert molecule:\n", + "- exclude pore access\n", + " - no pore porosity\n", + " - no diffusion parameters\n", + "- exclude binding\n", + " - no binding parameters\n", + "\n", + "Only:\n", + "the __bed porosity__ and __axial dispersion__ affect the Dextran pulse." + ] + }, + { + "cell_type": "markdown", + "id": "237dde23", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "For the column, assume the following parameters which are usually provided by the manufacturer (or can be measured):\n", + "- length: $0.1~m$\n", + "- diameter: $0.01~m$\n", + "- particle radius: $4.5 \\cdot 10^{-5}~m$\n", + "- flow rate: $10^{-6}/60~m^3 s^{-1}$" + ] + }, + { + "cell_type": "markdown", + "id": "22fa5b04", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "Moreover, since Dextran does not penetrate pores, the film diffusion coefficient can be set to $0~m \\cdot s^{-1}$." + ] + }, + { + "cell_type": "markdown", + "id": "2c60b0e1", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "Finally, bed porosity and axial dispersion need to be specified.\n", + "Usually, these parameters will be estimated using an inverse method (see later tutorials).\n", + "For now, assume the following values:\n", + "- bed porosity: $0.37$\n", + "- axial dispersion: $2.0 \\cdot 10^{-7}~m^2 \\cdot s^{-1}$" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1b979ab8", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import ComponentSystem\n", + "from CADETProcess.processModel import Inlet, LumpedRateModelWithPores, Outlet\n", + "from CADETProcess.processModel import FlowSheet\n", + "from CADETProcess.processModel import Process\n", + "\n", + "component_system = ComponentSystem(['Dextran'])\n", + "\n", + "inlet = Inlet(component_system, 'inlet')\n", + "inlet.flow_rate = 1e-6/60\n", + "inlet.c = [0]\n", + "\n", + "column = LumpedRateModelWithPores(component_system, 'column')\n", + "column.length = 0.1\n", + "column.diameter = 0.01\n", + "column.bed_porosity = 0.37\n", + "column.particle_radius = 4.5e-5\n", + "column.particle_porosity = 0.33\n", + "\n", + "column.axial_dispersion = 2.0e-7\n", + "column.film_diffusion = [0.0]\n", + "\n", + "outlet = Outlet(component_system, 'outlet')\n", + "\n", + "flow_sheet = FlowSheet(component_system)\n", + "flow_sheet.add_unit(inlet)\n", + "flow_sheet.add_unit(column)\n", + "flow_sheet.add_unit(outlet)\n", + "\n", + "flow_sheet.add_connection(inlet, column)\n", + "flow_sheet.add_connection(column, outlet)" + ] + }, + { + "cell_type": "markdown", + "id": "a30914fb", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "For the injection, we need to Introduce two sections.\n", + "In the first section, which lasts $50~s$, the concentration of Dextran at the `INLET` is $1.0~mM$, afterwards it is $0.0~mM$.\n", + "The flow rate is a constant $1~mL \\cdot min^{-1}$.\n", + "\n", + "```{figure} ./resources/dextran_inlet.png\n", + ":width: 50%\n", + ":align: center\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c2455ecb", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "process = Process(flow_sheet, 'dextran')\n", + "process.cycle_time = 600\n", + "\n", + "process.add_event('inject_on', 'flow_sheet.inlet.c', [1.0], 0)\n", + "process.add_event('inject_off', 'flow_sheet.inlet.c', [0.0], 50.0)\n", + "\n", + "from CADETProcess.simulator import Cadet\n", + "simulator = Cadet()\n", + "\n", + "simulation_results = simulator.simulate(process)\n", + "\n", + "_ = simulation_results.solution.column.outlet.plot()" + ] + }, + { + "cell_type": "markdown", + "id": "0ad8e17a", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Example 2: Multi Component Langmuir Separation\n", + "\n", + "Now, we will use the same system, but add `Langmuir` model to the column with two components using batch elution chromatography.\n", + "This process is often used for the purification of small molecules like amino acids or sugars." + ] + }, + { + "cell_type": "markdown", + "id": "16397c72", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "For the Langmuir isotherm, use the following parameters:\n", + "- adsorption rate: $[0.02, 0.03]~m^3 mol^{-1} s^{-1}$\n", + "- desorption rate: $[1, 1]~s^{-1}$\n", + "- binding capacity: $[100, 100]~mM$\n", + "\n", + "additionally, use these transport parameters:\n", + "\n", + "- particle porosity: $0.33$\n", + "- film diffusion: $10^{-4}$\n", + "\n", + "This time, use two `Inlet UnitOperations`, a feed `Inlet` with a concentration of $10 ~mM$ for both components and an eluent `Inlet` with a concentration of $0~mM$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8ba55f87", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import ComponentSystem\n", + "from CADETProcess.processModel import Langmuir\n", + "from CADETProcess.processModel import (\n", + " Inlet, LumpedRateModelWithPores, Outlet\n", + ")\n", + "from CADETProcess.processModel import FlowSheet\n", + "from CADETProcess.processModel import Process\n", + "\n", + "# Component System\n", + "component_system = ComponentSystem()\n", + "component_system.add_component('A')\n", + "component_system.add_component('B')\n", + "\n", + "# Binding Model\n", + "binding_model = Langmuir(component_system, name='langmuir')\n", + "binding_model.is_kinetic = False\n", + "binding_model.adsorption_rate = [0.02, 0.03]\n", + "binding_model.desorption_rate = [1, 1]\n", + "binding_model.capacity = [100, 100]\n", + "\n", + "# Unit Operations\n", + "feed = Inlet(component_system, name='feed')\n", + "feed.c = [10, 10]\n", + "\n", + "eluent = Inlet(component_system, name='eluent')\n", + "eluent.c = [0, 0]\n", + "\n", + "column = LumpedRateModelWithPores(component_system, 'column')\n", + "column.binding_model = binding_model\n", + "column.length = 0.1\n", + "column.diameter = 0.01\n", + "column.bed_porosity = 0.37\n", + "column.particle_radius = 4.5e-5\n", + "column.particle_porosity = 0.33\n", + "column.axial_dispersion = 2.0e-7\n", + "column.film_diffusion = [1e-4, 1e-4]\n", + "\n", + "outlet = Outlet(component_system, name='outlet')\n", + "\n", + "# flow sheet\n", + "flow_sheet = FlowSheet(component_system)\n", + "\n", + "flow_sheet.add_unit(feed)\n", + "flow_sheet.add_unit(eluent)\n", + "flow_sheet.add_unit(column)\n", + "flow_sheet.add_unit(outlet)\n", + "\n", + "flow_sheet.add_connection(feed, column)\n", + "flow_sheet.add_connection(eluent, column)\n", + "flow_sheet.add_connection(column, outlet)" + ] + }, + { + "cell_type": "markdown", + "id": "f462ed1a", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "Again, we create two sections to model the injections. This time by turning off the flow rate of the feed `Inlet` and turning on the flow rate of the eluent `Inlet`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e1c2c7f8", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "# Process\n", + "process = Process(flow_sheet, 'batch elution')\n", + "\n", + "## Create Events and Durations\n", + "Q = 1e-6/60\n", + "process.add_event('feed_on', 'flow_sheet.feed.flow_rate', Q, 0)\n", + "process.add_event('feed_off', 'flow_sheet.feed.flow_rate', 0.0, 60)\n", + "\n", + "process.add_event('eluent_off', 'flow_sheet.eluent.flow_rate', 0.0, 0)\n", + "process.add_event('eluent_on', 'flow_sheet.eluent.flow_rate', Q, 60)\n", + "\n", + "## Set process times\n", + "process.cycle_time = 1200" + ] + }, + { + "cell_type": "markdown", + "id": "5063c249", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### A Note on Event Dependencies\n", + "\n", + "Often, multiple `Events` happen simulateneously.\n", + "Here, for example, when the feed is turned off, the eluent also needs to be switched on.\n", + "To eliminate the need to manually change all event times, dependencies can be specified using `add_event_dependency`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a9df4c07", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "# Process\n", + "process = Process(flow_sheet, 'batch elution')\n", + "\n", + "## Create Events and Durations\n", + "Q = 1e-6/60\n", + "process.add_event(name='feed_on', parameter_path='flow_sheet.feed.flow_rate', state=Q, time=0)\n", + "process.add_event('feed_off', 'flow_sheet.feed.flow_rate', 0.0, 60)\n", + "\n", + "process.add_event(name='eluent_on', parameter_path='flow_sheet.eluent.flow_rate', state=Q)\n", + "process.add_event_dependency(\"eluent_on\", [\"feed_off\"])\n", + "\n", + "process.add_event('eluent_off', 'flow_sheet.eluent.flow_rate', 0.0)\n", + "process.add_event_dependency('eluent_off', ['feed_on'])\n", + "\n", + "## Set process times\n", + "process.cycle_time = 1200" + ] + }, + { + "cell_type": "markdown", + "id": "70b4e224", + "metadata": {}, + "source": [ + "You can see all the dependent events with the `.dependent_events` method" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5049c90f", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "process.dependent_events" + ] + }, + { + "cell_type": "markdown", + "id": "aa3da926", + "metadata": {}, + "source": [ + "Now simulate and plot the results." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "580213d6", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.simulator import Cadet\n", + "process_simulator = Cadet()\n", + "\n", + "simulation_results = process_simulator.simulate(process)\n", + "_ = simulation_results.solution.column.outlet.plot()" + ] + }, + { + "cell_type": "markdown", + "id": "19889b0d", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Example 3: Load wash elute with Steric Mass Action law binding\n", + "\n", + "The [Steric Mass Action model](https://cadet.github.io/master/modelling/binding/steric_mass_action.html#steric-mass-action-model) takes charges of the molecules into account and is, thus, often used in ion-exchange chromatography.\n", + "Each component has a characteristic charge $\\nu$ that determines the number of available binding sites $\\Lambda$ (ionic capacity) used up by a molecule.\n", + "Due to the molecule’s shape, some additional binding sites (steric shielding factor $\\sigma$) may be shielded from other molecules and are not available for binding." + ] + }, + { + "cell_type": "markdown", + "id": "85096842", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "The model is given by this eqution:\n", + "\n", + "$$\\frac{\\mathrm{d} q_i}{\\mathrm{d} t} = k_{a,i} c_{p,i}\\bar{q}_0^{\\nu_i} - k_{d,i} q_i c_{p,0}^{\\nu_i}$$\n", + "\n", + "where $c_{p,0}$ denotes the mobile phase salt concentration, and" + ] + }, + { + "cell_type": "markdown", + "id": "98f8eaf4", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "$$\\bar{q}_0 = \\Lambda - \\sum_{j=1}^{N_{\\text{comp}} - 1} \\left( \\nu_j + \\sigma_j \\right) q_j$$\n", + "\n", + "is the number of available binding sites." + ] + }, + { + "cell_type": "markdown", + "id": "2106502d", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "Using the parameter transformation\n", + "\n", + "$$k_{a,i} = \\tilde{k}_{a,i} q_{\\text{ref}}^{-\\nu_i}$$\n", + "\n", + "$$k_{d,i} = \\tilde{k}_{d,i} c_{\\text{ref}}^{-\\nu_i}$$" + ] + }, + { + "cell_type": "markdown", + "id": "400b0281", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "we obtain the modified model equation:\n", + "\n", + "$$\\frac{\\mathrm{d} q_i}{\\mathrm{d} t} = \\tilde{k}_{a,i} c_{p,i} \\left(\\frac{\\bar{q}_0}{q_{\\text{ref}}}\\right)^{\\nu_i} - \\tilde{k}_{d,i} q_i \\left(\\frac{c_{p,0}}{c_{\\text{ref}}}\\right)^{\\nu_i}$$" + ] + }, + { + "cell_type": "markdown", + "id": "aa40b572", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "This transformation serves as a (partial) nondimensionalization of the adsorption and desorption rates." + ] + }, + { + "cell_type": "markdown", + "id": "9877ec75", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "The basic goal is to have $\\left(\\frac{\\bar{q}_0}{q_{\\text{ref}}}\\right) \\leq 1$ and $\\left(\\frac{c_{p,0}}{c_{\\text{ref}}}\\right) \\leq 1$\n", + "\n", + "Recommended choices for $c_{\\text{ref}}$ are the average or maximum inlet concentration of the mobile phase modifier $c_0$, and for $q_{\\text{ref}}$ the ionic capacity $\\Lambda$.\n", + "Note that setting the reference concentrations to ${1.0}$ each results in the original binding model." + ] + }, + { + "cell_type": "markdown", + "id": "93a40ffd", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "```{note}\n", + "From a practical perspective, modern resins have a very high capacity and large proteins can can have a very high charactistic charge.\n", + "If the concentration is not normalized, the system is often numerically unstable.\n", + "It may run slowly or not at all.\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "d9770199", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "In this example, we will look at a typical process for protein purification.\n", + "\n", + "1. First, a protein salt mixture is loaded on the column and binds to the resin.\n", + "2. Then, the column is washed with a lower concentrated salt solution.\n", + "3. Finally, the protein is eluted by adding a linear salt gradient.\n", + "\n", + "```{figure} ./resources/lwe_inlet.png\n", + ":width: 80%\n", + ":align: center\n", + "\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "40932e66", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "First, define the `ComponentSystem` and the parameters for the `StericMassAction` model.\n", + "As mentioned earlier, consider a reference concentration in the pore for numeric purposes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cc74c5e2", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "import numpy as np\n", + "\n", + "from CADETProcess.processModel import ComponentSystem\n", + "from CADETProcess.processModel import StericMassAction\n", + "from CADETProcess.processModel import Inlet, GeneralRateModel, Outlet\n", + "from CADETProcess.processModel import FlowSheet\n", + "from CADETProcess.processModel import Process\n", + "\n", + "# Component System\n", + "component_system = ComponentSystem()\n", + "component_system.add_component('Salt')\n", + "component_system.add_component('Protein')\n", + "\n", + "# Binding Model\n", + "binding_model = StericMassAction(component_system, name='SMA')\n", + "binding_model.is_kinetic = True\n", + "binding_model.adsorption_rate = [0.0, 0.3]\n", + "binding_model.desorption_rate = [0.0, 1.5]\n", + "binding_model.characteristic_charge = [0.0, 7.0]\n", + "binding_model.steric_factor = [0.0, 50.0]\n", + "binding_model.capacity = 225.0" + ] + }, + { + "cell_type": "markdown", + "id": "c90d2260", + "metadata": {}, + "source": [ + "Then, we define the system of unit operations." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6bca0bf8", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "# Unit Operations\n", + "inlet = Inlet(component_system, name='inlet')\n", + "inlet.flow_rate = 2.88e-8\n", + "\n", + "column = GeneralRateModel(component_system, name='column')\n", + "column.binding_model = binding_model\n", + "\n", + "column.length = 0.25\n", + "column.diameter = 0.0115\n", + "column.bed_porosity = 0.37\n", + "column.particle_radius = 4.5e-5\n", + "column.particle_porosity = 0.33\n", + "column.axial_dispersion = 2.0e-7\n", + "column.film_diffusion = [2.0e-5, 2.0e-7]\n", + "column.pore_diffusion = [7e-5, 1e-9]\n", + "column.surface_diffusion = [0.0, 0.0]\n", + "\n", + "outlet = Outlet(component_system, name='outlet')\n", + "\n", + "# Flow Sheet\n", + "flow_sheet = FlowSheet(component_system)\n", + "\n", + "flow_sheet.add_unit(inlet)\n", + "flow_sheet.add_unit(column)\n", + "flow_sheet.add_unit(outlet)\n", + "\n", + "flow_sheet.add_connection(inlet, column)\n", + "flow_sheet.add_connection(column, outlet)" + ] + }, + { + "cell_type": "markdown", + "id": "bfe0ba2a", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "The protein is loaded for $7500 s$, then there is a wash step, which takes $2000 s$, and the gradient takes another $5500 s$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2859b0d1", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "# Process\n", + "process = Process(flow_sheet, 'lwe')\n", + "process.cycle_time = 15000.0\n", + "\n", + "## Create Events and Durations\n", + "wash_start = 7500.0\n", + "gradient_start = 9500.0\n", + "concentration_difference = np.array([500.0, 0.0]) - np.array([70.0, 0.0])\n", + "gradient_duration = process.cycle_time - gradient_start\n", + "gradient_slope = concentration_difference/gradient_duration\n", + "\n", + "_ = process.add_event('load', 'flow_sheet.inlet.c', [180.0, 0.1])\n", + "_ = process.add_event('wash', 'flow_sheet.inlet.c', [70.0, 0.0], wash_start)\n", + "_ = process.add_event(\n", + " 'grad_start',\n", + " 'flow_sheet.inlet.c',\n", + " [[70.0, gradient_slope[0]], [0, gradient_slope[1]]],\n", + " gradient_start\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "a62c52ac", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "Finally, we set the initial conditions of the column.\n", + "We assume, that in the beginning of the process, the stationary phase is fully loaded with salt." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c9d911b1", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "column.c = [180, 0]\n", + "column.q = [binding_model.capacity, 0]" + ] + }, + { + "cell_type": "markdown", + "id": "36fb91c9", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "Now, we run the simulation and plot the results. Because the concentration ranges are very different, we use different scales for both components." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "43350e86", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.simulator import Cadet\n", + "process_simulator = Cadet()\n", + "\n", + "simulation_results = process_simulator.simulate(process)\n", + "\n", + "from CADETProcess.plotting import SecondaryAxis\n", + "sec = SecondaryAxis()\n", + "sec.components = [\"Salt\"]\n", + "sec.y_label = '$c_{salt}$'\n", + "\n", + "_ = simulation_results.solution.column.outlet.plot(secondary_axis=sec)" + ] + } + ], + "metadata": { + "jupytext": { + "formats": "ipynb,md:myst" + }, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/03 Process Simulation/01 Simple Chromatographic Processes/01_chromatography.md b/03 Process Simulation/01 Simple Chromatographic Processes/01_chromatography.md deleted file mode 100644 index 37aed2f..0000000 --- a/03 Process Simulation/01 Simple Chromatographic Processes/01_chromatography.md +++ /dev/null @@ -1,502 +0,0 @@ ---- -jupytext: - formats: ipynb,md:myst - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.15.2 -kernelspec: - display_name: Python 3 (ipykernel) - language: python - name: python3 ---- - -+++ {"slideshow": {"slide_type": "slide"}} - -# Simple Chromatographic Processes - -+++ {"slideshow": {"slide_type": "fragment"}} - -Liquid chromatography is a technique for the separation of mixtures dissolved in a fluid. That fluid is called the mobile phase, which carries the mixture through a structure holding another material, called the stationary phase. - -+++ {"slideshow": {"slide_type": "fragment"}} - -The various components of the mixture interact with different strengths with the stationary phase. Therefore they travel at different speeds, causing them to separate. - -Different mechanisms can be used for the separation: - - adsorption, - - ion exchange, - - hydrophobic interactions, - - size exclusion, - - etc. - -For each mechanism, various stationary phases are available. - -+++ {"slideshow": {"slide_type": "slide"}} - -There are broadly speaking, _two_ types of chromatography: - -__Preparative__ and __analytical__ - -_Preparative_ targets isolation and purification of "large" quantities of a substance. - -_Analytical_ serves to identify or quantify analytes in a mixture. - -Generally, chromatographic models are used for optimization of _preparative_ processes. - -+++ {"slideshow": {"slide_type": "fragment"}} - -For modelling these processes, we have to combine all of the techniques we learnt in the previous lessons: -- Configure unit operations models. -- Associate adsorption models with unit operations. -- Generate dynamic inlet profiles. -- Chemical reactions (if required) - -+++ {"slideshow": {"slide_type": "slide"}} - -## Example 1: Dextran pulse - -In this exercise, we will consider the following system: - -```{figure} ./resources/system.png -:width: 50% -:align: center -``` - -+++ {"slideshow": {"slide_type": "fragment"}} - -Simplify the model by using a large, inert molecule: -- exclude pore access - - no pore porosity - - no diffusion parameters -- exclude binding - - no binding parameters - -Only: -the __bed porosity__ and __axial dispersion__ affect the Dextran pulse. - -+++ {"slideshow": {"slide_type": "slide"}} - -For the column, assume the following parameters which are usually provided by the manufacturer (or can be measured): -- length: $0.1~m$ -- diameter: $0.01~m$ -- particle radius: $4.5 \cdot 10^{-5}~m$ -- flow rate: $10^{-6}/60~m^3 s^{-1}$ - -+++ {"slideshow": {"slide_type": "fragment"}} - -Moreover, since Dextran does not penetrate pores, the film diffusion coefficient can be set to $0~m \cdot s^{-1}$. - -+++ {"slideshow": {"slide_type": "fragment"}} - -Finally, bed porosity and axial dispersion need to be specified. -Usually, these parameters will be estimated using an inverse method (see later tutorials). -For now, assume the following values: -- bed porosity: $0.37$ -- axial dispersion: $2.0 \cdot 10^{-7}~m^2 \cdot s^{-1}$ - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.processModel import ComponentSystem -from CADETProcess.processModel import Inlet, LumpedRateModelWithPores, Outlet -from CADETProcess.processModel import FlowSheet -from CADETProcess.processModel import Process - -component_system = ComponentSystem(['Dextran']) - -inlet = Inlet(component_system, 'inlet') -inlet.flow_rate = 1e-6/60 -inlet.c = [0] - -column = LumpedRateModelWithPores(component_system, 'column') -column.length = 0.1 -column.diameter = 0.01 -column.bed_porosity = 0.37 -column.particle_radius = 4.5e-5 -column.particle_porosity = 0.33 - -column.axial_dispersion = 2.0e-7 -column.film_diffusion = [0.0] - -outlet = Outlet(component_system, 'outlet') - -flow_sheet = FlowSheet(component_system) -flow_sheet.add_unit(inlet) -flow_sheet.add_unit(column) -flow_sheet.add_unit(outlet) - -flow_sheet.add_connection(inlet, column) -flow_sheet.add_connection(column, outlet) -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -For the injection, we need to Introduce two sections. -In the first section, which lasts $50~s$, the concentration of Dextran at the `INLET` is $1.0~mM$, afterwards it is $0.0~mM$. -The flow rate is a constant $1~mL \cdot min^{-1}$. - -```{figure} ./resources/dextran_inlet.png -:width: 50% -:align: center -``` - -```{code-cell} ipython3 -:tags: [solution] - -process = Process(flow_sheet, 'dextran') -process.cycle_time = 600 - -process.add_event('inject_on', 'flow_sheet.inlet.c', [1.0], 0) -process.add_event('inject_off', 'flow_sheet.inlet.c', [0.0], 50.0) - -from CADETProcess.simulator import Cadet -simulator = Cadet() - -simulation_results = simulator.simulate(process) - -_ = simulation_results.solution.column.outlet.plot() -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -## Example 2: Multi Component Langmuir Separation - -Now, we will use the same system, but add `Langmuir` model to the column with two components using batch elution chromatography. -This process is often used for the purification of small molecules like amino acids or sugars. - -+++ {"slideshow": {"slide_type": "fragment"}} - -For the Langmuir isotherm, use the following parameters: -- adsorption rate: $[0.02, 0.03]~m^3 mol^{-1} s^{-1}$ -- desorption rate: $[1, 1]~s^{-1}$ -- binding capacity: $[100, 100]~mM$ - -additionally, use these transport parameters: - -- particle porosity: $0.33$ -- film diffusion: $10^{-4}$ - -This time, use two `Inlet UnitOperations`, a feed `Inlet` with a concentration of $10 ~mM$ for both components and an eluent `Inlet` with a concentration of $0~mM$. - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.processModel import ComponentSystem -from CADETProcess.processModel import Langmuir -from CADETProcess.processModel import ( - Inlet, LumpedRateModelWithPores, Outlet -) -from CADETProcess.processModel import FlowSheet -from CADETProcess.processModel import Process - -# Component System -component_system = ComponentSystem() -component_system.add_component('A') -component_system.add_component('B') - -# Binding Model -binding_model = Langmuir(component_system, name='langmuir') -binding_model.is_kinetic = False -binding_model.adsorption_rate = [0.02, 0.03] -binding_model.desorption_rate = [1, 1] -binding_model.capacity = [100, 100] - -# Unit Operations -feed = Inlet(component_system, name='feed') -feed.c = [10, 10] - -eluent = Inlet(component_system, name='eluent') -eluent.c = [0, 0] - -column = LumpedRateModelWithPores(component_system, 'column') -column.binding_model = binding_model -column.length = 0.1 -column.diameter = 0.01 -column.bed_porosity = 0.37 -column.particle_radius = 4.5e-5 -column.particle_porosity = 0.33 -column.axial_dispersion = 2.0e-7 -column.film_diffusion = [1e-4, 1e-4] - -outlet = Outlet(component_system, name='outlet') - -# flow sheet -flow_sheet = FlowSheet(component_system) - -flow_sheet.add_unit(feed) -flow_sheet.add_unit(eluent) -flow_sheet.add_unit(column) -flow_sheet.add_unit(outlet) - -flow_sheet.add_connection(feed, column) -flow_sheet.add_connection(eluent, column) -flow_sheet.add_connection(column, outlet) -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -Again, we create two sections to model the injections. This time by turning off the flow rate of the feed `Inlet` and turning on the flow rate of the eluent `Inlet`. - -```{code-cell} ipython3 -:tags: [solution] - -# Process -process = Process(flow_sheet, 'batch elution') - -## Create Events and Durations -Q = 1e-6/60 -process.add_event('feed_on', 'flow_sheet.feed.flow_rate', Q, 0) -process.add_event('feed_off', 'flow_sheet.feed.flow_rate', 0.0, 60) - -process.add_event('eluent_off', 'flow_sheet.eluent.flow_rate', 0.0, 0) -process.add_event('eluent_on', 'flow_sheet.eluent.flow_rate', Q, 60) - -## Set process times -process.cycle_time = 1200 -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -### A Note on Event Dependencies - -Often, multiple `Events` happen simulateneously. -Here, for example, when the feed is turned off, the eluent also needs to be switched on. -To eliminate the need to manually change all event times, dependencies can be specified using `add_event_dependency`. - -```{code-cell} ipython3 -:tags: [solution] - -# Process -process = Process(flow_sheet, 'batch elution') - -## Create Events and Durations -Q = 1e-6/60 -process.add_event(name='feed_on', parameter_path='flow_sheet.feed.flow_rate', state=Q, time=0) -process.add_event('feed_off', 'flow_sheet.feed.flow_rate', 0.0, 60) - -process.add_event(name='eluent_on', parameter_path='flow_sheet.eluent.flow_rate', state=Q) -process.add_event_dependency("eluent_on", ["feed_off"]) - -process.add_event('eluent_off', 'flow_sheet.eluent.flow_rate', 0.0) -process.add_event_dependency('eluent_off', ['feed_on']) - -## Set process times -process.cycle_time = 1200 -``` - -You can see all the dependent events with the `.dependent_events` method - -```{code-cell} ipython3 -:tags: [solution] - -process.dependent_events -``` - -Now simulate and plot the results. - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.simulator import Cadet -process_simulator = Cadet() - -simulation_results = process_simulator.simulate(process) -_ = simulation_results.solution.column.outlet.plot() -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -## Example 3: Load wash elute with Steric Mass Action law binding - -The [Steric Mass Action model](https://cadet.github.io/master/modelling/binding/steric_mass_action.html#steric-mass-action-model) takes charges of the molecules into account and is, thus, often used in ion-exchange chromatography. -Each component has a characteristic charge $\nu$ that determines the number of available binding sites $\Lambda$ (ionic capacity) used up by a molecule. -Due to the molecule’s shape, some additional binding sites (steric shielding factor $\sigma$) may be shielded from other molecules and are not available for binding. - -+++ {"slideshow": {"slide_type": "fragment"}} - -The model is given by this eqution: - -$$\frac{\mathrm{d} q_i}{\mathrm{d} t} = k_{a,i} c_{p,i}\bar{q}_0^{\nu_i} - k_{d,i} q_i c_{p,0}^{\nu_i}$$ - -where $c_{p,0}$ denotes the mobile phase salt concentration, and - -+++ {"slideshow": {"slide_type": "fragment"}} - -$$\bar{q}_0 = \Lambda - \sum_{j=1}^{N_{\text{comp}} - 1} \left( \nu_j + \sigma_j \right) q_j$$ - -is the number of available binding sites. - -+++ {"slideshow": {"slide_type": "slide"}} - -Using the parameter transformation - -$$k_{a,i} = \tilde{k}_{a,i} q_{\text{ref}}^{-\nu_i}$$ - -$$k_{d,i} = \tilde{k}_{d,i} c_{\text{ref}}^{-\nu_i}$$ - -+++ {"slideshow": {"slide_type": "fragment"}} - -we obtain the modified model equation: - -$$\frac{\mathrm{d} q_i}{\mathrm{d} t} = \tilde{k}_{a,i} c_{p,i} \left(\frac{\bar{q}_0}{q_{\text{ref}}}\right)^{\nu_i} - \tilde{k}_{d,i} q_i \left(\frac{c_{p,0}}{c_{\text{ref}}}\right)^{\nu_i}$$ - -+++ {"slideshow": {"slide_type": "fragment"}} - -This transformation serves as a (partial) nondimensionalization of the adsorption and desorption rates. - -+++ {"slideshow": {"slide_type": "fragment"}} - -The basic goal is to have $\left(\frac{\bar{q}_0}{q_{\text{ref}}}\right) \leq 1$ and $\left(\frac{c_{p,0}}{c_{\text{ref}}}\right) \leq 1$ - -Recommended choices for $c_{\text{ref}}$ are the average or maximum inlet concentration of the mobile phase modifier $c_0$, and for $q_{\text{ref}}$ the ionic capacity $\Lambda$. -Note that setting the reference concentrations to ${1.0}$ each results in the original binding model. - -+++ {"slideshow": {"slide_type": "fragment"}} - -```{note} -From a practical perspective, modern resins have a very high capacity and large proteins can can have a very high charactistic charge. -If the concentration is not normalized, the system is often numerically unstable. -It may run slowly or not at all. -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -In this example, we will look at a typical process for protein purification. - -1. First, a protein salt mixture is loaded on the column and binds to the resin. -2. Then, the column is washed with a lower concentrated salt solution. -3. Finally, the protein is eluted by adding a linear salt gradient. - -```{figure} ./resources/lwe_inlet.png -:width: 80% -:align: center - -``` - -+++ {"slideshow": {"slide_type": "fragment"}} - -First, define the `ComponentSystem` and the parameters for the `StericMassAction` model. -As mentioned earlier, consider a reference concentration in the pore for numeric purposes. - -```{code-cell} ipython3 -:tags: [solution] - -import numpy as np - -from CADETProcess.processModel import ComponentSystem -from CADETProcess.processModel import StericMassAction -from CADETProcess.processModel import Inlet, GeneralRateModel, Outlet -from CADETProcess.processModel import FlowSheet -from CADETProcess.processModel import Process - -# Component System -component_system = ComponentSystem() -component_system.add_component('Salt') -component_system.add_component('Protein') - -# Binding Model -binding_model = StericMassAction(component_system, name='SMA') -binding_model.is_kinetic = True -binding_model.adsorption_rate = [0.0, 0.3] -binding_model.desorption_rate = [0.0, 1.5] -binding_model.characteristic_charge = [0.0, 7.0] -binding_model.steric_factor = [0.0, 50.0] -binding_model.capacity = 225.0 -``` - -Then, we define the system of unit operations. - -```{code-cell} ipython3 -:tags: [solution] - -# Unit Operations -inlet = Inlet(component_system, name='inlet') -inlet.flow_rate = 2.88e-8 - -column = GeneralRateModel(component_system, name='column') -column.binding_model = binding_model - -column.length = 0.25 -column.diameter = 0.0115 -column.bed_porosity = 0.37 -column.particle_radius = 4.5e-5 -column.particle_porosity = 0.33 -column.axial_dispersion = 2.0e-7 -column.film_diffusion = [2.0e-5, 2.0e-7] -column.pore_diffusion = [7e-5, 1e-9] -column.surface_diffusion = [0.0, 0.0] - -outlet = Outlet(component_system, name='outlet') - -# Flow Sheet -flow_sheet = FlowSheet(component_system) - -flow_sheet.add_unit(inlet) -flow_sheet.add_unit(column) -flow_sheet.add_unit(outlet) - -flow_sheet.add_connection(inlet, column) -flow_sheet.add_connection(column, outlet) -``` - -+++ {"slideshow": {"slide_type": "fragment"}} - -The protein is loaded for $7500 s$, then there is a wash step, which takes $2000 s$, and the gradient takes another $5500 s$. - -```{code-cell} ipython3 -:tags: [solution] - -# Process -process = Process(flow_sheet, 'lwe') -process.cycle_time = 15000.0 - -## Create Events and Durations -wash_start = 7500.0 -gradient_start = 9500.0 -concentration_difference = np.array([500.0, 0.0]) - np.array([70.0, 0.0]) -gradient_duration = process.cycle_time - gradient_start -gradient_slope = concentration_difference/gradient_duration - -_ = process.add_event('load', 'flow_sheet.inlet.c', [180.0, 0.1]) -_ = process.add_event('wash', 'flow_sheet.inlet.c', [70.0, 0.0], wash_start) -_ = process.add_event( - 'grad_start', - 'flow_sheet.inlet.c', - [[70.0, gradient_slope[0]], [0, gradient_slope[1]]], - gradient_start -) -``` - -+++ {"slideshow": {"slide_type": "fragment"}} - -Finally, we set the initial conditions of the column. -We assume, that in the beginning of the process, the stationary phase is fully loaded with salt. - -```{code-cell} ipython3 -:tags: [solution] - -column.c = [180, 0] -column.q = [binding_model.capacity, 0] -``` - -+++ {"slideshow": {"slide_type": "fragment"}} - -Now, we run the simulation and plot the results. Because the concentration ranges are very different, we use different scales for both components. - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.simulator import Cadet -process_simulator = Cadet() - -simulation_results = process_simulator.simulate(process) - -from CADETProcess.plotting import SecondaryAxis -sec = SecondaryAxis() -sec.components = ["Salt"] -sec.y_label = '$c_{salt}$' - -_ = simulation_results.solution.column.outlet.plot(secondary_axis=sec) -``` diff --git a/03 Process Simulation/01 Simple Chromatographic Processes/02_chromatography_exercise.ipynb b/03 Process Simulation/01 Simple Chromatographic Processes/02_chromatography_exercise.ipynb new file mode 100644 index 0000000..7f5b277 --- /dev/null +++ b/03 Process Simulation/01 Simple Chromatographic Processes/02_chromatography_exercise.ipynb @@ -0,0 +1,317 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "5be28a19", + "metadata": {}, + "source": [ + "# Chromatographic Processes - Exercise" + ] + }, + { + "cell_type": "markdown", + "id": "d9803309", + "metadata": {}, + "source": [ + "## Exercise 1: Combine nonbinding tracer with binding components\n", + "\n", + "From the tutorial, combine the dextran pulse with the langmuir experiment.\n", + "Then, start modifying:\n", + "- Feed concentrations\n", + "- Adsorption parameters\n", + "- Transport parameters\n", + "- Flow Rates" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "77c199d3", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import ComponentSystem\n", + "from CADETProcess.processModel import Langmuir\n", + "from CADETProcess.processModel import Inlet, LumpedRateModelWithPores, Outlet\n", + "from CADETProcess.processModel import FlowSheet\n", + "from CADETProcess.processModel import Process\n", + "\n", + "# Component System\n", + "component_system = ComponentSystem()\n", + "component_system.add_component('Dextran')\n", + "component_system.add_component('A')\n", + "component_system.add_component('B')\n", + "\n", + "# Binding Model\n", + "binding_model = Langmuir(component_system, name='langmuir')\n", + "binding_model.is_kinetic = False\n", + "binding_model.adsorption_rate = [0, 0.02, 0.03]\n", + "binding_model.desorption_rate = [1, 1, 1]\n", + "binding_model.capacity = [100, 100, 100]\n", + "\n", + "inlet = Inlet(component_system, 'inlet')\n", + "inlet.flow_rate = 2.88e-8\n", + "\n", + "column = LumpedRateModelWithPores(component_system, 'column')\n", + "column.binding_model = binding_model\n", + "column.length = 0.1\n", + "column.diameter = 0.01\n", + "column.bed_porosity = 0.37\n", + "column.particle_radius = 4.5e-5\n", + "column.particle_porosity = 0.33\n", + "column.axial_dispersion = 2.0e-7\n", + "column.film_diffusion = [0, 1e-4, 1e-4]\n", + "\n", + "outlet = Outlet(component_system, 'outlet')\n", + "\n", + "flow_sheet = FlowSheet(component_system)\n", + "flow_sheet.add_unit(inlet)\n", + "flow_sheet.add_unit(column)\n", + "flow_sheet.add_unit(outlet)\n", + "\n", + "flow_sheet.add_connection(inlet, column)\n", + "flow_sheet.add_connection(column, outlet)\n", + "\n", + "process = Process(flow_sheet, 'dextran')\n", + "process.cycle_time = 1200\n", + "\n", + "process.add_event('inject_on', 'flow_sheet.inlet.c', [1.0, 1.0, 1.0], 0)\n", + "process.add_event('inject_off', 'flow_sheet.inlet.c', [0.0, 0.0, 0.0], 50.0)\n", + "\n", + "from CADETProcess.simulator import Cadet\n", + "simulator = Cadet()\n", + "\n", + "simulation_results = simulator.simulate(process)\n", + "\n", + "_ = simulation_results.solution.column.outlet.plot()" + ] + }, + { + "cell_type": "markdown", + "id": "02b7f076", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Exercise 2: Multiple injections\n", + "\n", + "For some processes, multiple injections onto a column in sequence.\n", + "Take the previous example and create an inlet profile with three injections.\n", + "For this purpose, the `Simulator` can automatically run multiple simulations by setting `n_cycles`.\n", + "\n", + "***Task:*** Try finding the best interval (cycle time) s.t. the column is used most efficiently." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0683df92", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import ComponentSystem\n", + "from CADETProcess.processModel import Langmuir\n", + "from CADETProcess.processModel import Inlet, LumpedRateModelWithPores, Outlet\n", + "from CADETProcess.processModel import FlowSheet\n", + "from CADETProcess.processModel import Process\n", + "\n", + "# Component System\n", + "component_system = ComponentSystem()\n", + "component_system.add_component('A')\n", + "component_system.add_component('B')\n", + "\n", + "# Binding Model\n", + "binding_model = Langmuir(component_system, name='langmuir')\n", + "binding_model.is_kinetic = False\n", + "binding_model.adsorption_rate = [0.02, 0.03]\n", + "binding_model.desorption_rate = [1, 1]\n", + "binding_model.capacity = [100, 100]\n", + "\n", + "inlet = Inlet(component_system, 'inlet')\n", + "inlet.flow_rate = 2.88e-8\n", + "\n", + "column = LumpedRateModelWithPores(component_system, 'column')\n", + "column.binding_model = binding_model\n", + "column.length = 0.1\n", + "column.diameter = 0.01\n", + "column.bed_porosity = 0.37\n", + "column.particle_radius = 4.5e-5\n", + "column.particle_porosity = 0.33\n", + "column.axial_dispersion = 2.0e-7\n", + "column.film_diffusion = [1e-4, 1e-4]\n", + "\n", + "outlet = Outlet(component_system, 'outlet')\n", + "\n", + "flow_sheet = FlowSheet(component_system)\n", + "flow_sheet.add_unit(inlet)\n", + "flow_sheet.add_unit(column)\n", + "flow_sheet.add_unit(outlet)\n", + "\n", + "flow_sheet.add_connection(inlet, column)\n", + "flow_sheet.add_connection(column, outlet)\n", + "\n", + "process = Process(flow_sheet, 'dextran')\n", + "process.cycle_time = 5*60\n", + "\n", + "process.add_event('inject_on', 'flow_sheet.inlet.c', [1.0, 1.0], 0)\n", + "process.add_event('inject_off', 'flow_sheet.inlet.c', [0.0, 0.0], 50.0)\n", + "\n", + "from CADETProcess.simulator import Cadet\n", + "simulator = Cadet()\n", + "simulator.n_cycles = 3\n", + "\n", + "simulation_results = simulator.simulate(process)\n", + "\n", + "_ = simulation_results.solution.column.outlet.plot()" + ] + }, + { + "cell_type": "markdown", + "id": "3d042dce", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Example 3: Load wash elute with three components\n", + "\n", + "Add a second protein component to the LWE example from the tutorial lesson.\n", + "Assume that all parameters are the same as the first protein, only change:\n", + "- adsorption rate: $0.3~m^{3}_{MP}m^{-3}_{SP}s^{-1}$\n", + "- characteristic charge: $5.0$" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6eb75029", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "import numpy as np\n", + "\n", + "from CADETProcess.processModel import ComponentSystem\n", + "from CADETProcess.processModel import StericMassAction\n", + "from CADETProcess.processModel import Inlet, GeneralRateModel, Outlet\n", + "from CADETProcess.processModel import FlowSheet\n", + "from CADETProcess.processModel import Process\n", + "\n", + "# Component System\n", + "component_system = ComponentSystem()\n", + "component_system.add_component('Salt')\n", + "component_system.add_component('Protein A')\n", + "component_system.add_component('Protein B')\n", + "\n", + "# Binding Model\n", + "binding_model = StericMassAction(component_system, name='SMA')\n", + "binding_model.is_kinetic = True\n", + "binding_model.adsorption_rate = [0.0, 0.3, 0.2]\n", + "binding_model.desorption_rate = [0.0, 1.5, 1.5]\n", + "binding_model.characteristic_charge = [0.0, 7.0, 5.0]\n", + "binding_model.steric_factor = [0.0, 50.0, 50.0]\n", + "binding_model.capacity = 225.0\n", + "binding_model.reference_liquid_phase_conc = 300\n", + "binding_model.reference_solid_phase_conc = 225.0\n", + "\n", + "# Unit Operations\n", + "inlet = Inlet(component_system, name='inlet')\n", + "inlet.flow_rate = 2.88e-8\n", + "\n", + "column = GeneralRateModel(component_system, name='column')\n", + "column.binding_model = binding_model\n", + "\n", + "column.length = 0.25\n", + "column.diameter = 0.0115\n", + "column.bed_porosity = 0.37\n", + "column.particle_radius = 4.5e-5\n", + "column.particle_porosity = 0.33\n", + "column.axial_dispersion = 2.0e-7\n", + "column.film_diffusion = [2.0e-5, 2.0e-7, 2.0e-7]\n", + "column.pore_diffusion = [7e-5, 1e-9, 1e-9]\n", + "column.surface_diffusion = [0.0, 0.0, 0.0]\n", + "\n", + "outlet = Outlet(component_system, name='outlet')\n", + "\n", + "# Flow Sheet\n", + "flow_sheet = FlowSheet(component_system)\n", + "\n", + "flow_sheet.add_unit(inlet)\n", + "flow_sheet.add_unit(column)\n", + "flow_sheet.add_unit(outlet, product_outlet=True)\n", + "\n", + "flow_sheet.add_connection(inlet, column)\n", + "flow_sheet.add_connection(column, outlet)\n", + "\n", + "# Process\n", + "process = Process(flow_sheet, 'lwe')\n", + "process.cycle_time = 15000.0\n", + "\n", + "## Create Events and Durations\n", + "wash_start = 7500.0\n", + "gradient_start = 9500.0\n", + "concentration_difference = np.array([500.0, 0.0]) - np.array([70.0, 0.0])\n", + "gradient_duration = process.cycle_time - gradient_start\n", + "gradient_slope = concentration_difference/gradient_duration\n", + "\n", + "_ = process.add_event('load', 'flow_sheet.inlet.c', [180.0, 0.1, 0.1])\n", + "_ = process.add_event('wash', 'flow_sheet.inlet.c', [70.0, 0.0, 0.0], wash_start)\n", + "_ = process.add_event(\n", + " 'grad_start',\n", + " 'flow_sheet.inlet.c',\n", + " [[70.0, gradient_slope[0]], [0, gradient_slope[1]], [0, gradient_slope[1]]],\n", + " gradient_start\n", + ")\n", + "\n", + "column.c = [180, 0, 0]\n", + "column.q = [binding_model.capacity, 0, 0]\n", + "\n", + "from CADETProcess.simulator import Cadet\n", + "process_simulator = Cadet()\n", + "\n", + "simulation_results = process_simulator.simulate(process)\n", + "\n", + "from CADETProcess.plotting import SecondaryAxis\n", + "sec = SecondaryAxis()\n", + "sec.components = [\"Salt\"]\n", + "sec.y_label = '$c_{salt}$'\n", + "\n", + "_ = simulation_results.solution.column.outlet.plot(secondary_axis=sec)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "eb2d3f70", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "jupytext": { + "formats": "ipynb,md:myst" + }, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/03 Process Simulation/01 Simple Chromatographic Processes/02_chromatography_exercise.md b/03 Process Simulation/01 Simple Chromatographic Processes/02_chromatography_exercise.md deleted file mode 100644 index b44f4e0..0000000 --- a/03 Process Simulation/01 Simple Chromatographic Processes/02_chromatography_exercise.md +++ /dev/null @@ -1,260 +0,0 @@ ---- -jupytext: - formats: ipynb,md:myst - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.15.2 -kernelspec: - display_name: Python 3 (ipykernel) - language: python - name: python3 ---- - -# Chromatographic Processes - Exercise - -+++ - -## Exercise 1: Combine nonbinding tracer with binding components - -From the tutorial, combine the dextran pulse with the langmuir experiment. -Then, start modifying: -- Feed concentrations -- Adsorption parameters -- Transport parameters -- Flow Rates - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.processModel import ComponentSystem -from CADETProcess.processModel import Langmuir -from CADETProcess.processModel import Inlet, LumpedRateModelWithPores, Outlet -from CADETProcess.processModel import FlowSheet -from CADETProcess.processModel import Process - -# Component System -component_system = ComponentSystem() -component_system.add_component('Dextran') -component_system.add_component('A') -component_system.add_component('B') - -# Binding Model -binding_model = Langmuir(component_system, name='langmuir') -binding_model.is_kinetic = False -binding_model.adsorption_rate = [0, 0.02, 0.03] -binding_model.desorption_rate = [1, 1, 1] -binding_model.capacity = [100, 100, 100] - -inlet = Inlet(component_system, 'inlet') -inlet.flow_rate = 2.88e-8 - -column = LumpedRateModelWithPores(component_system, 'column') -column.binding_model = binding_model -column.length = 0.1 -column.diameter = 0.01 -column.bed_porosity = 0.37 -column.particle_radius = 4.5e-5 -column.particle_porosity = 0.33 -column.axial_dispersion = 2.0e-7 -column.film_diffusion = [0, 1e-4, 1e-4] - -outlet = Outlet(component_system, 'outlet') - -flow_sheet = FlowSheet(component_system) -flow_sheet.add_unit(inlet) -flow_sheet.add_unit(column) -flow_sheet.add_unit(outlet) - -flow_sheet.add_connection(inlet, column) -flow_sheet.add_connection(column, outlet) - -process = Process(flow_sheet, 'dextran') -process.cycle_time = 1200 - -process.add_event('inject_on', 'flow_sheet.inlet.c', [1.0, 1.0, 1.0], 0) -process.add_event('inject_off', 'flow_sheet.inlet.c', [0.0, 0.0, 0.0], 50.0) - -from CADETProcess.simulator import Cadet -simulator = Cadet() - -simulation_results = simulator.simulate(process) - -_ = simulation_results.solution.column.outlet.plot() -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -## Exercise 2: Multiple injections - -For some processes, multiple injections onto a column in sequence. -Take the previous example and create an inlet profile with three injections. -For this purpose, the `Simulator` can automatically run multiple simulations by setting `n_cycles`. - -***Task:*** Try finding the best interval (cycle time) s.t. the column is used most efficiently. - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.processModel import ComponentSystem -from CADETProcess.processModel import Langmuir -from CADETProcess.processModel import Inlet, LumpedRateModelWithPores, Outlet -from CADETProcess.processModel import FlowSheet -from CADETProcess.processModel import Process - -# Component System -component_system = ComponentSystem() -component_system.add_component('A') -component_system.add_component('B') - -# Binding Model -binding_model = Langmuir(component_system, name='langmuir') -binding_model.is_kinetic = False -binding_model.adsorption_rate = [0.02, 0.03] -binding_model.desorption_rate = [1, 1] -binding_model.capacity = [100, 100] - -inlet = Inlet(component_system, 'inlet') -inlet.flow_rate = 2.88e-8 - -column = LumpedRateModelWithPores(component_system, 'column') -column.binding_model = binding_model -column.length = 0.1 -column.diameter = 0.01 -column.bed_porosity = 0.37 -column.particle_radius = 4.5e-5 -column.particle_porosity = 0.33 -column.axial_dispersion = 2.0e-7 -column.film_diffusion = [1e-4, 1e-4] - -outlet = Outlet(component_system, 'outlet') - -flow_sheet = FlowSheet(component_system) -flow_sheet.add_unit(inlet) -flow_sheet.add_unit(column) -flow_sheet.add_unit(outlet) - -flow_sheet.add_connection(inlet, column) -flow_sheet.add_connection(column, outlet) - -process = Process(flow_sheet, 'dextran') -process.cycle_time = 5*60 - -process.add_event('inject_on', 'flow_sheet.inlet.c', [1.0, 1.0], 0) -process.add_event('inject_off', 'flow_sheet.inlet.c', [0.0, 0.0], 50.0) - -from CADETProcess.simulator import Cadet -simulator = Cadet() -simulator.n_cycles = 3 - -simulation_results = simulator.simulate(process) - -_ = simulation_results.solution.column.outlet.plot() -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -## Example 3: Load wash elute with three components - -Add a second protein component to the LWE example from the tutorial lesson. -Assume that all parameters are the same as the first protein, only change: -- adsorption rate: $0.3~m^{3}_{MP}m^{-3}_{SP}s^{-1}$ -- characteristic charge: $5.0$ - -```{code-cell} ipython3 -:tags: [solution] - -import numpy as np - -from CADETProcess.processModel import ComponentSystem -from CADETProcess.processModel import StericMassAction -from CADETProcess.processModel import Inlet, GeneralRateModel, Outlet -from CADETProcess.processModel import FlowSheet -from CADETProcess.processModel import Process - -# Component System -component_system = ComponentSystem() -component_system.add_component('Salt') -component_system.add_component('Protein A') -component_system.add_component('Protein B') - -# Binding Model -binding_model = StericMassAction(component_system, name='SMA') -binding_model.is_kinetic = True -binding_model.adsorption_rate = [0.0, 0.3, 0.2] -binding_model.desorption_rate = [0.0, 1.5, 1.5] -binding_model.characteristic_charge = [0.0, 7.0, 5.0] -binding_model.steric_factor = [0.0, 50.0, 50.0] -binding_model.capacity = 225.0 -binding_model.reference_liquid_phase_conc = 300 -binding_model.reference_solid_phase_conc = 225.0 - -# Unit Operations -inlet = Inlet(component_system, name='inlet') -inlet.flow_rate = 2.88e-8 - -column = GeneralRateModel(component_system, name='column') -column.binding_model = binding_model - -column.length = 0.25 -column.diameter = 0.0115 -column.bed_porosity = 0.37 -column.particle_radius = 4.5e-5 -column.particle_porosity = 0.33 -column.axial_dispersion = 2.0e-7 -column.film_diffusion = [2.0e-5, 2.0e-7, 2.0e-7] -column.pore_diffusion = [7e-5, 1e-9, 1e-9] -column.surface_diffusion = [0.0, 0.0, 0.0] - -outlet = Outlet(component_system, name='outlet') - -# Flow Sheet -flow_sheet = FlowSheet(component_system) - -flow_sheet.add_unit(inlet) -flow_sheet.add_unit(column) -flow_sheet.add_unit(outlet, product_outlet=True) - -flow_sheet.add_connection(inlet, column) -flow_sheet.add_connection(column, outlet) - -# Process -process = Process(flow_sheet, 'lwe') -process.cycle_time = 15000.0 - -## Create Events and Durations -wash_start = 7500.0 -gradient_start = 9500.0 -concentration_difference = np.array([500.0, 0.0]) - np.array([70.0, 0.0]) -gradient_duration = process.cycle_time - gradient_start -gradient_slope = concentration_difference/gradient_duration - -_ = process.add_event('load', 'flow_sheet.inlet.c', [180.0, 0.1, 0.1]) -_ = process.add_event('wash', 'flow_sheet.inlet.c', [70.0, 0.0, 0.0], wash_start) -_ = process.add_event( - 'grad_start', - 'flow_sheet.inlet.c', - [[70.0, gradient_slope[0]], [0, gradient_slope[1]], [0, gradient_slope[1]]], - gradient_start -) - -column.c = [180, 0, 0] -column.q = [binding_model.capacity, 0, 0] - -from CADETProcess.simulator import Cadet -process_simulator = Cadet() - -simulation_results = process_simulator.simulate(process) - -from CADETProcess.plotting import SecondaryAxis -sec = SecondaryAxis() -sec.components = ["Salt"] -sec.y_label = '$c_{salt}$' - -_ = simulation_results.solution.column.outlet.plot(secondary_axis=sec) -``` - -```{code-cell} ipython3 - -``` diff --git a/03 Process Simulation/02 Advanced Operating Modes/01_advanced_chromatography.ipynb b/03 Process Simulation/02 Advanced Operating Modes/01_advanced_chromatography.ipynb new file mode 100644 index 0000000..b623bbc --- /dev/null +++ b/03 Process Simulation/02 Advanced Operating Modes/01_advanced_chromatography.ipynb @@ -0,0 +1,588 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "723793af", + "metadata": {}, + "source": [ + "# Advanced Chromatographic Processes\n", + "\n", + "In this lesson we will learn how to create more complex models with binding, multiple components, and multiple unit operations" + ] + }, + { + "cell_type": "markdown", + "id": "dba86f3f", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Example 1: Modelling dispersion of valves and tubing\n", + "\n", + "A real system does not have an inlet connected directly to the column which connects directly to the outlet.\n", + "Real systems have tubes and mixing valves.\n", + "If they are not accounted for, the pulse that arrives at the column inlet will be far too sharp.\n", + "Moreover, the measured output signal from your column will be appear more diffuse than the real column outlet, because it will have spent time in tubing between the outlet and the detector." + ] + }, + { + "cell_type": "markdown", + "id": "dac8e52c", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "We can model this, by introducing additional unit operations that can account for the time shift and dispersion that is introduced by the periphery of the column.\n", + "For this example, we will model the mixer at the inlet using a `Cstr`." + ] + }, + { + "cell_type": "markdown", + "id": "1e0cea03", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "Moreover, in this example we will use two `Inlets` and modify `flow_rate` to create the inlet profile.\n", + "\n", + "```{figure} ./resources/valve_mixer.png\n", + ":width: 50%\n", + ":align: center\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "7617b2a4", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "For the column, assume the following parameters which are usually provided by the manufacturer (or can be measured):\n", + "- length: $0.1~m$\n", + "- diameter: $0.01~m$\n", + "- bed porosity: $0.37$\n", + "- particle radius: $4.5 \\cdot 10^{-5}~m$\n", + "- particle porosity: $0.33$\n", + "- axial dispersion: $2.0 \\cdot 10^{-7}~m^2 \\cdot s^{-1}$" + ] + }, + { + "cell_type": "markdown", + "id": "499aac37", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "For the Langmuir isotherm, use the following parameters:\n", + "- adsorption rate: $[0.02, 0.03]~m^3 mol^{-1} s^{-1}$\n", + "- desorption rate: $[1, 1]~s^{-1}$\n", + "- binding capacity: $[100, 100]~mM$" + ] + }, + { + "cell_type": "markdown", + "id": "1635c423", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "For the tank, assume a volume of $1 mL$.\n", + "\n", + "Moreover, consider a flow rate of $1~mL/min$, a feed concentration of $10~mM$, and a feed duration of $60~s$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "91fdfff7", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "Q = 1e-6/60\n", + "\n", + "from CADETProcess.processModel import ComponentSystem\n", + "from CADETProcess.processModel import Langmuir\n", + "from CADETProcess.processModel import (\n", + " Inlet, Cstr, LumpedRateModelWithPores, Outlet\n", + ")\n", + "from CADETProcess.processModel import FlowSheet\n", + "from CADETProcess.processModel import Process\n", + "\n", + "# Component System\n", + "component_system = ComponentSystem(['A', 'B'])\n", + "\n", + "# Binding Model\n", + "binding_model = Langmuir(component_system, name='langmuir')\n", + "binding_model.is_kinetic = False\n", + "binding_model.adsorption_rate = [0.02, 0.03]\n", + "binding_model.desorption_rate = [1, 1]\n", + "binding_model.capacity = [100, 100]\n", + "\n", + "# Unit Operations\n", + "feed = Inlet(component_system, name='feed')\n", + "feed.c = [10, 10]\n", + "\n", + "eluent = Inlet(component_system, name='eluent')\n", + "eluent.c = [0, 0]\n", + "\n", + "# Mixer\n", + "valve = Cstr(component_system, 'valve')\n", + "valve.V = 1e-6\n", + "valve.flow_rate = Q\n", + "\n", + "# Column\n", + "column = LumpedRateModelWithPores(component_system, 'column')\n", + "column.binding_model = binding_model\n", + "column.length = 0.1\n", + "column.diameter = 0.01\n", + "column.bed_porosity = 0.37\n", + "column.particle_radius = 4.5e-5\n", + "column.particle_porosity = 0.33\n", + "column.axial_dispersion = 2.0e-7\n", + "column.film_diffusion = [1e-4, 1e-4]\n", + "\n", + "outlet = Outlet(component_system, name='outlet')\n", + "\n", + "# Flow Sheet\n", + "flow_sheet = FlowSheet(component_system)\n", + "\n", + "flow_sheet.add_unit(feed)\n", + "flow_sheet.add_unit(eluent)\n", + "flow_sheet.add_unit(valve)\n", + "flow_sheet.add_unit(column)\n", + "flow_sheet.add_unit(outlet)\n", + "\n", + "flow_sheet.add_connection(feed, valve)\n", + "flow_sheet.add_connection(eluent, valve)\n", + "flow_sheet.add_connection(valve, column)\n", + "flow_sheet.add_connection(column, outlet)\n", + "\n", + "# Process\n", + "process = Process(flow_sheet, 'batch elution')\n", + "\n", + "## Create Events and Durations\n", + "process.add_event('feed_on', 'flow_sheet.feed.flow_rate', Q, 0)\n", + "process.add_event('feed_off', 'flow_sheet.feed.flow_rate', 0.0, 60)\n", + "\n", + "process.add_event('eluent_off', 'flow_sheet.eluent.flow_rate', 0.0, 0)\n", + "process.add_event('eluent_on', 'flow_sheet.eluent.flow_rate', Q, 60)\n", + "\n", + "## Set Process Times\n", + "process.cycle_time = 1200" + ] + }, + { + "cell_type": "markdown", + "id": "34c1239b", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "Now simulate and plot the results." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b3ba622d", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.simulator import Cadet\n", + "process_simulator = Cadet()\n", + "\n", + "simulation_results = process_simulator.simulate(process)\n", + "_ = simulation_results.solution.column.inlet.plot()\n", + "_ = simulation_results.solution.column.outlet.plot()" + ] + }, + { + "cell_type": "markdown", + "id": "b7f99ce8", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Simulated Moving Bed (Cyclic Systems)\n", + "\n", + "For many applications, the use of multiple columns can improve process performance when compared with conventional batch elution processes.\n", + "Next to the well known simulated moving bed (SMB) many other operating modes exist which extend the use of multiple columns, e.g. Varicol, or PowerFeed processes and gradient operations." + ] + }, + { + "cell_type": "markdown", + "id": "ace4c7d8", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "In all of the aforementioned processes, multiple chromatographic columns are mounted to a rotating column carousel and a central multiport switching valve distributes in- and outgoing streams to and from the columns. After a given time, the column positions are moved to the next position in the carousel. In this process, the columns pass through different zones which serve different purposes." + ] + }, + { + "cell_type": "markdown", + "id": "9d964934", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "For example, in a classical SMB, four zones are present (see Figure below)\n", + "\n", + "- Zone I: Elution of the strongly adsorbing component\n", + "- Zone II: Elution of the weakly adsorbing component\n", + "- Zone III: Adsorption of the strongly adsorbing component\n", + "- Zone IV : Adsorption of the weakly adsorbing component\n", + "\n", + "```{figure} ./resources/smb.png\n", + ":width: 50%\n", + ":align: center\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "70865590", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "Moreover, four in- and outlets are connected to the zones:\n", + "- Feed: Inlet containing the components to be separated\n", + "- Eluent: Inlet with elution buffer\n", + "- Extract: Outlet containing the strongly adsorbing component\n", + "- Raffinate: Outlet containing the weakly adsorbing component\n", + "\n", + "To facilitate the configuration of complex SMB, carousel, or other multi column systems systems, a CarouselBuilder was implemented in CADET-Process.\n", + "It allows a straight-forward configuration of the zones and returns a fully configured Process object including all internal connections, as well as switching events." + ] + }, + { + "cell_type": "markdown", + "id": "7869c567", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "Before configuring the zones, the binding and column models are configured.\n", + "The column is later used as a template for all columns in the system." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fe2bd51f", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import ComponentSystem\n", + "component_system = ComponentSystem(2)\n", + "\n", + "# Binding Model\n", + "from CADETProcess.processModel import Linear\n", + "binding_model = Linear(component_system)\n", + "binding_model.adsorption_rate = [6, 8]\n", + "binding_model.desorption_rate = [1, 1]\n", + "\n", + "from CADETProcess.processModel import LumpedRateModelWithoutPores\n", + "column = LumpedRateModelWithoutPores(component_system, name='master_column')\n", + "column.length = 0.6\n", + "column.diameter = 0.024\n", + "column.axial_dispersion = 4.7e-7\n", + "column.total_porosity = 0.7\n", + "\n", + "column.binding_model = binding_model" + ] + }, + { + "cell_type": "markdown", + "id": "3c056efd", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "Now, the inlets and outlets of the system are configured:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "561e1270", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import Inlet, Outlet\n", + "feed = Inlet(component_system, name='feed')\n", + "feed.c = [10, 10]\n", + "feed.flow_rate = 2e-7\n", + "eluent = Inlet(component_system, name='eluent')\n", + "eluent.c = [0, 0]\n", + "eluent.flow_rate = 6e-7\n", + "\n", + "raffinate = Outlet(component_system, name='raffinate')\n", + "extract = Outlet(component_system, name='extract')" + ] + }, + { + "cell_type": "markdown", + "id": "4e25ed80", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "To allow more complicated systems, CADET-Process provides two options for configuring zones, a SerialZone and a ParallelZone.\n", + "For both, the number of columns in the zone needs to be specified.\n", + "Since here all the zones only consist of one column, either can be used." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b7375700", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.modelBuilder import SerialZone, ParallelZone\n", + "\n", + "zone_I = SerialZone(component_system, 'zone_I', 1)\n", + "zone_II = SerialZone(component_system, 'zone_II', 1)\n", + "zone_III = SerialZone(component_system, 'zone_III', 1)\n", + "zone_IV = SerialZone(component_system, 'zone_IV', 1)" + ] + }, + { + "cell_type": "markdown", + "id": "fa23e35d", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "The CarouselBuilder can now be used like a regular FlowSheet where the zones are conceptually used like other UnitOperations.\n", + "After initializing the CarouselBuilder, the column template is assigned and all units and zones are added." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "edf08206", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.modelBuilder import CarouselBuilder\n", + "\n", + "builder = CarouselBuilder(component_system, 'smb')\n", + "builder.column = column\n", + "builder.add_unit(feed)\n", + "builder.add_unit(eluent)\n", + "\n", + "builder.add_unit(raffinate)\n", + "builder.add_unit(extract)\n", + "\n", + "builder.add_unit(zone_I)\n", + "builder.add_unit(zone_II)\n", + "builder.add_unit(zone_III)\n", + "builder.add_unit(zone_IV)" + ] + }, + { + "cell_type": "markdown", + "id": "1619b556", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "Now, the connections are added to the builder.\n", + "To define split streams, the output_state is used which sets the ratio between outgoing streams of a unit operation in the flow sheet." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c0c45ff7", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "builder.add_connection(eluent, zone_I)\n", + "\n", + "builder.add_connection(zone_I, extract)\n", + "builder.add_connection(zone_I, zone_II)\n", + "w_e = 0.15\n", + "builder.set_output_state(zone_I, [w_e, 1-w_e])\n", + "\n", + "builder.add_connection(zone_II, zone_III)\n", + "\n", + "builder.add_connection(feed, zone_III)\n", + "\n", + "builder.add_connection(zone_III, raffinate)\n", + "builder.add_connection(zone_III, zone_IV)\n", + "w_r = 0.15\n", + "builder.set_output_state(zone_III, [w_r, 1-w_r])\n", + "\n", + "builder.add_connection(zone_IV, zone_I)" + ] + }, + { + "cell_type": "markdown", + "id": "af51376c", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "Now, the switch time is assigned to the builder which determines after how much time a column is switched to the next position.\n", + "By calling the build_process() method, a regular Process object is constructed which can be simulated just as usual using CADET.\n", + "It contains the assembled flow sheet with all columns, as well as the events required for simulation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8f8ed931", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "builder.switch_time = 300\n", + "process = builder.build_process()" + ] + }, + { + "cell_type": "markdown", + "id": "3df0a587", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "Since multi column systems often exhibit a transient startup behavior, it might be useful to simulate multiple cycles until cyclic stationarity is reached (see Cyclic Stationarity).\n", + "Because this simulation is computationally expensive, only a few simulations are run here.\n", + "Please run this simulation locally to see the full results." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7882a127", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.simulator import Cadet\n", + "\n", + "process_simulator = Cadet()\n", + "# process_simulator.evaluate_stationarity = True\n", + "process_simulator.n_cycles = 3\n", + "\n", + "simulation_results = process_simulator.simulate(process)" + ] + }, + { + "cell_type": "markdown", + "id": "60f76855", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "The results can now be plotted.\n", + "For example, this is how the concentration profiles of the raffinate and extract outlets are plotted:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "62408e8e", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "_ = simulation_results.solution.raffinate.inlet.plot()\n", + "_ = simulation_results.solution.extract.inlet.plot()" + ] + } + ], + "metadata": { + "jupytext": { + "formats": "ipynb,md:myst" + }, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/03 Process Simulation/02 Advanced Operating Modes/01_advanced_chromatography.md b/03 Process Simulation/02 Advanced Operating Modes/01_advanced_chromatography.md deleted file mode 100644 index 66d21ba..0000000 --- a/03 Process Simulation/02 Advanced Operating Modes/01_advanced_chromatography.md +++ /dev/null @@ -1,347 +0,0 @@ ---- -jupytext: - formats: ipynb,md:myst - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.15.2 -kernelspec: - display_name: Python 3 (ipykernel) - language: python - name: python3 ---- - -# Advanced Chromatographic Processes - -In this lesson we will learn how to create more complex models with binding, multiple components, and multiple unit operations - -+++ {"slideshow": {"slide_type": "slide"}} - -## Example 1: Modelling dispersion of valves and tubing - -A real system does not have an inlet connected directly to the column which connects directly to the outlet. -Real systems have tubes and mixing valves. -If they are not accounted for, the pulse that arrives at the column inlet will be far too sharp. -Moreover, the measured output signal from your column will be appear more diffuse than the real column outlet, because it will have spent time in tubing between the outlet and the detector. - -+++ {"slideshow": {"slide_type": "fragment"}} - -We can model this, by introducing additional unit operations that can account for the time shift and dispersion that is introduced by the periphery of the column. -For this example, we will model the mixer at the inlet using a `Cstr`. - -+++ {"slideshow": {"slide_type": "fragment"}} - -Moreover, in this example we will use two `Inlets` and modify `flow_rate` to create the inlet profile. - -```{figure} ./resources/valve_mixer.png -:width: 50% -:align: center -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -For the column, assume the following parameters which are usually provided by the manufacturer (or can be measured): -- length: $0.1~m$ -- diameter: $0.01~m$ -- bed porosity: $0.37$ -- particle radius: $4.5 \cdot 10^{-5}~m$ -- particle porosity: $0.33$ -- axial dispersion: $2.0 \cdot 10^{-7}~m^2 \cdot s^{-1}$ - -+++ {"slideshow": {"slide_type": "fragment"}} - -For the Langmuir isotherm, use the following parameters: -- adsorption rate: $[0.02, 0.03]~m^3 mol^{-1} s^{-1}$ -- desorption rate: $[1, 1]~s^{-1}$ -- binding capacity: $[100, 100]~mM$ - -+++ {"slideshow": {"slide_type": "fragment"}} - -For the tank, assume a volume of $1 mL$. - -Moreover, consider a flow rate of $1~mL/min$, a feed concentration of $10~mM$, and a feed duration of $60~s$. - -```{code-cell} ipython3 -:tags: [solution] - -Q = 1e-6/60 - -from CADETProcess.processModel import ComponentSystem -from CADETProcess.processModel import Langmuir -from CADETProcess.processModel import ( - Inlet, Cstr, LumpedRateModelWithPores, Outlet -) -from CADETProcess.processModel import FlowSheet -from CADETProcess.processModel import Process - -# Component System -component_system = ComponentSystem(['A', 'B']) - -# Binding Model -binding_model = Langmuir(component_system, name='langmuir') -binding_model.is_kinetic = False -binding_model.adsorption_rate = [0.02, 0.03] -binding_model.desorption_rate = [1, 1] -binding_model.capacity = [100, 100] - -# Unit Operations -feed = Inlet(component_system, name='feed') -feed.c = [10, 10] - -eluent = Inlet(component_system, name='eluent') -eluent.c = [0, 0] - -# Mixer -valve = Cstr(component_system, 'valve') -valve.V = 1e-6 -valve.flow_rate = Q - -# Column -column = LumpedRateModelWithPores(component_system, 'column') -column.binding_model = binding_model -column.length = 0.1 -column.diameter = 0.01 -column.bed_porosity = 0.37 -column.particle_radius = 4.5e-5 -column.particle_porosity = 0.33 -column.axial_dispersion = 2.0e-7 -column.film_diffusion = [1e-4, 1e-4] - -outlet = Outlet(component_system, name='outlet') - -# Flow Sheet -flow_sheet = FlowSheet(component_system) - -flow_sheet.add_unit(feed) -flow_sheet.add_unit(eluent) -flow_sheet.add_unit(valve) -flow_sheet.add_unit(column) -flow_sheet.add_unit(outlet) - -flow_sheet.add_connection(feed, valve) -flow_sheet.add_connection(eluent, valve) -flow_sheet.add_connection(valve, column) -flow_sheet.add_connection(column, outlet) - -# Process -process = Process(flow_sheet, 'batch elution') - -## Create Events and Durations -process.add_event('feed_on', 'flow_sheet.feed.flow_rate', Q, 0) -process.add_event('feed_off', 'flow_sheet.feed.flow_rate', 0.0, 60) - -process.add_event('eluent_off', 'flow_sheet.eluent.flow_rate', 0.0, 0) -process.add_event('eluent_on', 'flow_sheet.eluent.flow_rate', Q, 60) - -## Set Process Times -process.cycle_time = 1200 -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -Now simulate and plot the results. - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.simulator import Cadet -process_simulator = Cadet() - -simulation_results = process_simulator.simulate(process) -_ = simulation_results.solution.column.inlet.plot() -_ = simulation_results.solution.column.outlet.plot() -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -## Simulated Moving Bed (Cyclic Systems) - -For many applications, the use of multiple columns can improve process performance when compared with conventional batch elution processes. -Next to the well known simulated moving bed (SMB) many other operating modes exist which extend the use of multiple columns, e.g. Varicol, or PowerFeed processes and gradient operations. - -+++ {"slideshow": {"slide_type": "fragment"}} - -In all of the aforementioned processes, multiple chromatographic columns are mounted to a rotating column carousel and a central multiport switching valve distributes in- and outgoing streams to and from the columns. After a given time, the column positions are moved to the next position in the carousel. In this process, the columns pass through different zones which serve different purposes. - -+++ {"slideshow": {"slide_type": "slide"}} - -For example, in a classical SMB, four zones are present (see Figure below) - -- Zone I: Elution of the strongly adsorbing component -- Zone II: Elution of the weakly adsorbing component -- Zone III: Adsorption of the strongly adsorbing component -- Zone IV : Adsorption of the weakly adsorbing component - -```{figure} ./resources/smb.png -:width: 50% -:align: center -``` - -+++ {"slideshow": {"slide_type": "fragment"}} - -Moreover, four in- and outlets are connected to the zones: -- Feed: Inlet containing the components to be separated -- Eluent: Inlet with elution buffer -- Extract: Outlet containing the strongly adsorbing component -- Raffinate: Outlet containing the weakly adsorbing component - -To facilitate the configuration of complex SMB, carousel, or other multi column systems systems, a CarouselBuilder was implemented in CADET-Process. -It allows a straight-forward configuration of the zones and returns a fully configured Process object including all internal connections, as well as switching events. - -+++ {"slideshow": {"slide_type": "slide"}} - -Before configuring the zones, the binding and column models are configured. -The column is later used as a template for all columns in the system. - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.processModel import ComponentSystem -component_system = ComponentSystem(2) - -# Binding Model -from CADETProcess.processModel import Linear -binding_model = Linear(component_system) -binding_model.adsorption_rate = [6, 8] -binding_model.desorption_rate = [1, 1] - -from CADETProcess.processModel import LumpedRateModelWithoutPores -column = LumpedRateModelWithoutPores(component_system, name='master_column') -column.length = 0.6 -column.diameter = 0.024 -column.axial_dispersion = 4.7e-7 -column.total_porosity = 0.7 - -column.binding_model = binding_model -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -Now, the inlets and outlets of the system are configured: - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.processModel import Inlet, Outlet -feed = Inlet(component_system, name='feed') -feed.c = [10, 10] -feed.flow_rate = 2e-7 -eluent = Inlet(component_system, name='eluent') -eluent.c = [0, 0] -eluent.flow_rate = 6e-7 - -raffinate = Outlet(component_system, name='raffinate') -extract = Outlet(component_system, name='extract') -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -To allow more complicated systems, CADET-Process provides two options for configuring zones, a SerialZone and a ParallelZone. -For both, the number of columns in the zone needs to be specified. -Since here all the zones only consist of one column, either can be used. - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.modelBuilder import SerialZone, ParallelZone - -zone_I = SerialZone(component_system, 'zone_I', 1) -zone_II = SerialZone(component_system, 'zone_II', 1) -zone_III = SerialZone(component_system, 'zone_III', 1) -zone_IV = SerialZone(component_system, 'zone_IV', 1) -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -The CarouselBuilder can now be used like a regular FlowSheet where the zones are conceptually used like other UnitOperations. -After initializing the CarouselBuilder, the column template is assigned and all units and zones are added. - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.modelBuilder import CarouselBuilder - -builder = CarouselBuilder(component_system, 'smb') -builder.column = column -builder.add_unit(feed) -builder.add_unit(eluent) - -builder.add_unit(raffinate) -builder.add_unit(extract) - -builder.add_unit(zone_I) -builder.add_unit(zone_II) -builder.add_unit(zone_III) -builder.add_unit(zone_IV) -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -Now, the connections are added to the builder. -To define split streams, the output_state is used which sets the ratio between outgoing streams of a unit operation in the flow sheet. - -```{code-cell} ipython3 -:tags: [solution] - -builder.add_connection(eluent, zone_I) - -builder.add_connection(zone_I, extract) -builder.add_connection(zone_I, zone_II) -w_e = 0.15 -builder.set_output_state(zone_I, [w_e, 1-w_e]) - -builder.add_connection(zone_II, zone_III) - -builder.add_connection(feed, zone_III) - -builder.add_connection(zone_III, raffinate) -builder.add_connection(zone_III, zone_IV) -w_r = 0.15 -builder.set_output_state(zone_III, [w_r, 1-w_r]) - -builder.add_connection(zone_IV, zone_I) -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -Now, the switch time is assigned to the builder which determines after how much time a column is switched to the next position. -By calling the build_process() method, a regular Process object is constructed which can be simulated just as usual using CADET. -It contains the assembled flow sheet with all columns, as well as the events required for simulation. - -```{code-cell} ipython3 -:tags: [solution] - -builder.switch_time = 300 -process = builder.build_process() -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -Since multi column systems often exhibit a transient startup behavior, it might be useful to simulate multiple cycles until cyclic stationarity is reached (see Cyclic Stationarity). -Because this simulation is computationally expensive, only a few simulations are run here. -Please run this simulation locally to see the full results. - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.simulator import Cadet - -process_simulator = Cadet() -# process_simulator.evaluate_stationarity = True -process_simulator.n_cycles = 3 - -simulation_results = process_simulator.simulate(process) -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -The results can now be plotted. -For example, this is how the concentration profiles of the raffinate and extract outlets are plotted: - -```{code-cell} ipython3 -:tags: [solution] - -_ = simulation_results.solution.raffinate.inlet.plot() -_ = simulation_results.solution.extract.inlet.plot() -``` diff --git a/03 Process Simulation/02 Advanced Operating Modes/02_advanced_chromatography_exercise.ipynb b/03 Process Simulation/02 Advanced Operating Modes/02_advanced_chromatography_exercise.ipynb new file mode 100644 index 0000000..2b918d2 --- /dev/null +++ b/03 Process Simulation/02 Advanced Operating Modes/02_advanced_chromatography_exercise.ipynb @@ -0,0 +1,277 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d5834ba0", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Advanced Chromatographic Processes - Exercises\n", + "\n", + "\n", + "## Exercise 1: Modelling dispersion of valves and tubing\n", + "\n", + "Take the example from the lesson and add tubing using a `TubularReactor` with $L_c = 0.5~m$, $A_c = 1 \\cdot 10^{-5}~m^2$, and $D_{ax} = 1 \\cdot 10^{-5}~m^2 \\cdot s^{-1}$.\n", + "\n", + "***Task:*** Plot the inlet and outlet of every unit operation and compare the results to a system without any considerations for valving and tubing." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "97d63314", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "Q = 1e-6/60\n", + "\n", + "from CADETProcess.processModel import ComponentSystem\n", + "from CADETProcess.processModel import Langmuir\n", + "from CADETProcess.processModel import (\n", + " Inlet, Cstr, TubularReactor, LumpedRateModelWithPores, Outlet\n", + ")\n", + "from CADETProcess.processModel import FlowSheet\n", + "from CADETProcess.processModel import Process\n", + "\n", + "# Component System\n", + "component_system = ComponentSystem(['A', 'B'])\n", + "\n", + "# Binding Model\n", + "binding_model = Langmuir(component_system, name='langmuir')\n", + "binding_model.is_kinetic = False\n", + "binding_model.adsorption_rate = [0.02, 0.03]\n", + "binding_model.desorption_rate = [1, 1]\n", + "binding_model.capacity = [100, 100]\n", + "\n", + "# Unit Operations\n", + "feed = Inlet(component_system, name='feed')\n", + "feed.c = [10, 10]\n", + "\n", + "eluent = Inlet(component_system, name='eluent')\n", + "eluent.c = [0, 0]\n", + "\n", + "# Mixer\n", + "valve = Cstr(component_system, 'valve')\n", + "valve.V = 1e-6\n", + "valve.flow_rate = Q\n", + "\n", + "# Tubing\n", + "tubing = TubularReactor(component_system, 'tubing')\n", + "tubing.length = 0.5\n", + "tubing.cross_section_area = 1e-5\n", + "tubing.axial_dispersion = 1e-5\n", + "\n", + "# Column\n", + "column = LumpedRateModelWithPores(component_system, 'column')\n", + "column.binding_model = binding_model\n", + "column.length = 0.1\n", + "column.diameter = 0.01\n", + "column.bed_porosity = 0.37\n", + "column.particle_radius = 4.5e-5\n", + "column.particle_porosity = 0.33\n", + "column.axial_dispersion = 2.0e-7\n", + "column.film_diffusion = [1e-4, 1e-4]\n", + "\n", + "outlet = Outlet(component_system, name='outlet')\n", + "\n", + "# Flow Sheet\n", + "flow_sheet = FlowSheet(component_system)\n", + "\n", + "flow_sheet.add_unit(feed)\n", + "flow_sheet.add_unit(eluent)\n", + "flow_sheet.add_unit(valve)\n", + "flow_sheet.add_unit(tubing)\n", + "flow_sheet.add_unit(column)\n", + "flow_sheet.add_unit(outlet)\n", + "\n", + "flow_sheet.add_connection(feed, valve)\n", + "flow_sheet.add_connection(eluent, valve)\n", + "flow_sheet.add_connection(valve, tubing)\n", + "flow_sheet.add_connection(tubing, column)\n", + "flow_sheet.add_connection(column, outlet)\n", + "\n", + "# Process\n", + "process = Process(flow_sheet, 'batch elution')\n", + "\n", + "## Create Events and Durations\n", + "process.add_event('feed_on', 'flow_sheet.feed.flow_rate', Q, 0)\n", + "process.add_event('feed_off', 'flow_sheet.feed.flow_rate', 0.0, 60)\n", + "\n", + "process.add_event('eluent_off', 'flow_sheet.eluent.flow_rate', 0.0, 0)\n", + "process.add_event('eluent_on', 'flow_sheet.eluent.flow_rate', Q, 60)\n", + "\n", + "## Set Process Times\n", + "process.cycle_time = 1200" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "375a0573", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.simulator import Cadet\n", + "process_simulator = Cadet()\n", + "\n", + "simulation_results = process_simulator.simulate(process)\n", + "_ = simulation_results.solution.column.inlet.plot()\n", + "_ = simulation_results.solution.column.outlet.plot()" + ] + }, + { + "cell_type": "markdown", + "id": "2014d951", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Example 2: Carousel System\n", + "\n", + "Consider the following multi column system:\n", + "\n", + "```{figure} ./resources/carousel.png\n", + ":width: 50%\n", + ":align: center\n", + "```\n", + "There exist four zones in this system:\n", + "- Wash: 3 columns in series\n", + "- Feed: 3 columns in parallel\n", + "- Dilute: 2 columns in series; reverse flow\n", + "- Elute: 2 Columns in series" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1aa98f62", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import Inlet, Outlet\n", + "i_wash = Inlet(component_system, name='i_wash')\n", + "i_wash.c = [0, 0]\n", + "i_wash.flow_rate = 60/60/1e6\n", + "i_feed = Inlet(component_system, name='i_feed')\n", + "i_feed.c = [10, 10]\n", + "i_feed.flow_rate = 30/60/1e6\n", + "i_elute = Inlet(component_system, name='i_elute')\n", + "i_elute.c = [0, 0]\n", + "i_elute.flow_rate = 60/60/1e6\n", + "\n", + "o_waste = Outlet(component_system, name='o_waste')\n", + "o_product = Outlet(component_system, name='o_product')" + ] + }, + { + "cell_type": "markdown", + "id": "c58ef9b6", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "Now the zones are set up and the reverse flow is set in the dilution zone." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "02a5d1e7", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.modelBuilder import SerialZone, ParallelZone\n", + "\n", + "z_wash = SerialZone(component_system, 'z_wash', 3)\n", + "z_feed = ParallelZone(component_system, 'z_feed', 3)\n", + "z_dilute = SerialZone(component_system, 'z_dilute', 2, flow_direction=-1)\n", + "z_elute = SerialZone(component_system, 'z_elute', 2)" + ] + }, + { + "cell_type": "markdown", + "id": "64b47469", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "As in the previous example, the units and zones are added and connected in the CarouselBuilder" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8685811f", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.modelBuilder import CarouselBuilder\n", + "\n", + "builder = CarouselBuilder(component_system, 'multi_zone')\n", + "builder.column = column\n", + "builder.add_unit(i_wash)\n", + "builder.add_unit(i_feed)\n", + "builder.add_unit(i_elute)\n", + "\n", + "builder.add_unit(o_waste)\n", + "builder.add_unit(o_product)\n", + "\n", + "builder.add_unit(z_wash)\n", + "builder.add_unit(z_feed)\n", + "builder.add_unit(z_dilute)\n", + "builder.add_unit(z_elute)\n", + "\n", + "builder.add_connection(i_wash, z_wash)\n", + "builder.add_connection(z_wash, z_dilute)\n", + "builder.add_connection(i_feed, z_feed)\n", + "builder.add_connection(z_feed, z_dilute)\n", + "builder.add_connection(z_dilute, o_waste)\n", + "builder.add_connection(z_dilute, z_elute)\n", + "builder.set_output_state(z_dilute, [0.5, 0.5])\n", + "builder.add_connection(i_elute, z_elute)\n", + "builder.add_connection(z_elute, o_product)" + ] + } + ], + "metadata": { + "jupytext": { + "formats": "ipynb,md:myst" + }, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/03 Process Simulation/02 Advanced Operating Modes/02_advanced_chromatography_exercise.md b/03 Process Simulation/02 Advanced Operating Modes/02_advanced_chromatography_exercise.md deleted file mode 100644 index 5bb6e30..0000000 --- a/03 Process Simulation/02 Advanced Operating Modes/02_advanced_chromatography_exercise.md +++ /dev/null @@ -1,202 +0,0 @@ ---- -jupytext: - formats: ipynb,md:myst - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.15.2 -kernelspec: - display_name: Python 3 (ipykernel) - language: python - name: python3 ---- - -+++ {"slideshow": {"slide_type": "slide"}} -# Advanced Chromatographic Processes - Exercises - - -## Exercise 1: Modelling dispersion of valves and tubing - -Take the example from the lesson and add tubing using a `TubularReactor` with $L_c = 0.5~m$, $A_c = 1 \cdot 10^{-5}~m^2$, and $D_{ax} = 1 \cdot 10^{-5}~m^2 \cdot s^{-1}$. - -***Task:*** Plot the inlet and outlet of every unit operation and compare the results to a system without any considerations for valving and tubing. - -```{code-cell} ipython3 -:tags: [solution] - -Q = 1e-6/60 - -from CADETProcess.processModel import ComponentSystem -from CADETProcess.processModel import Langmuir -from CADETProcess.processModel import ( - Inlet, Cstr, TubularReactor, LumpedRateModelWithPores, Outlet -) -from CADETProcess.processModel import FlowSheet -from CADETProcess.processModel import Process - -# Component System -component_system = ComponentSystem(['A', 'B']) - -# Binding Model -binding_model = Langmuir(component_system, name='langmuir') -binding_model.is_kinetic = False -binding_model.adsorption_rate = [0.02, 0.03] -binding_model.desorption_rate = [1, 1] -binding_model.capacity = [100, 100] - -# Unit Operations -feed = Inlet(component_system, name='feed') -feed.c = [10, 10] - -eluent = Inlet(component_system, name='eluent') -eluent.c = [0, 0] - -# Mixer -valve = Cstr(component_system, 'valve') -valve.V = 1e-6 -valve.flow_rate = Q - -# Tubing -tubing = TubularReactor(component_system, 'tubing') -tubing.length = 0.5 -tubing.cross_section_area = 1e-5 -tubing.axial_dispersion = 1e-5 - -# Column -column = LumpedRateModelWithPores(component_system, 'column') -column.binding_model = binding_model -column.length = 0.1 -column.diameter = 0.01 -column.bed_porosity = 0.37 -column.particle_radius = 4.5e-5 -column.particle_porosity = 0.33 -column.axial_dispersion = 2.0e-7 -column.film_diffusion = [1e-4, 1e-4] - -outlet = Outlet(component_system, name='outlet') - -# Flow Sheet -flow_sheet = FlowSheet(component_system) - -flow_sheet.add_unit(feed) -flow_sheet.add_unit(eluent) -flow_sheet.add_unit(valve) -flow_sheet.add_unit(tubing) -flow_sheet.add_unit(column) -flow_sheet.add_unit(outlet) - -flow_sheet.add_connection(feed, valve) -flow_sheet.add_connection(eluent, valve) -flow_sheet.add_connection(valve, tubing) -flow_sheet.add_connection(tubing, column) -flow_sheet.add_connection(column, outlet) - -# Process -process = Process(flow_sheet, 'batch elution') - -## Create Events and Durations -process.add_event('feed_on', 'flow_sheet.feed.flow_rate', Q, 0) -process.add_event('feed_off', 'flow_sheet.feed.flow_rate', 0.0, 60) - -process.add_event('eluent_off', 'flow_sheet.eluent.flow_rate', 0.0, 0) -process.add_event('eluent_on', 'flow_sheet.eluent.flow_rate', Q, 60) - -## Set Process Times -process.cycle_time = 1200 -``` - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.simulator import Cadet -process_simulator = Cadet() - -simulation_results = process_simulator.simulate(process) -_ = simulation_results.solution.column.inlet.plot() -_ = simulation_results.solution.column.outlet.plot() -``` - -+++ {"slideshow": {"slide_type": "slide"}} -## Example 2: Carousel System - -Consider the following multi column system: - -```{figure} ./resources/carousel.png -:width: 50% -:align: center -``` -There exist four zones in this system: -- Wash: 3 columns in series -- Feed: 3 columns in parallel -- Dilute: 2 columns in series; reverse flow -- Elute: 2 Columns in series - - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.processModel import Inlet, Outlet -i_wash = Inlet(component_system, name='i_wash') -i_wash.c = [0, 0] -i_wash.flow_rate = 60/60/1e6 -i_feed = Inlet(component_system, name='i_feed') -i_feed.c = [10, 10] -i_feed.flow_rate = 30/60/1e6 -i_elute = Inlet(component_system, name='i_elute') -i_elute.c = [0, 0] -i_elute.flow_rate = 60/60/1e6 - -o_waste = Outlet(component_system, name='o_waste') -o_product = Outlet(component_system, name='o_product') -``` - -+++ {"slideshow": {"slide_type": "slide"}} -Now the zones are set up and the reverse flow is set in the dilution zone. - - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.modelBuilder import SerialZone, ParallelZone - -z_wash = SerialZone(component_system, 'z_wash', 3) -z_feed = ParallelZone(component_system, 'z_feed', 3) -z_dilute = SerialZone(component_system, 'z_dilute', 2, flow_direction=-1) -z_elute = SerialZone(component_system, 'z_elute', 2) - -``` - -+++ {"slideshow": {"slide_type": "slide"}} -As in the previous example, the units and zones are added and connected in the CarouselBuilder - - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.modelBuilder import CarouselBuilder - -builder = CarouselBuilder(component_system, 'multi_zone') -builder.column = column -builder.add_unit(i_wash) -builder.add_unit(i_feed) -builder.add_unit(i_elute) - -builder.add_unit(o_waste) -builder.add_unit(o_product) - -builder.add_unit(z_wash) -builder.add_unit(z_feed) -builder.add_unit(z_dilute) -builder.add_unit(z_elute) - -builder.add_connection(i_wash, z_wash) -builder.add_connection(z_wash, z_dilute) -builder.add_connection(i_feed, z_feed) -builder.add_connection(z_feed, z_dilute) -builder.add_connection(z_dilute, o_waste) -builder.add_connection(z_dilute, z_elute) -builder.set_output_state(z_dilute, [0.5, 0.5]) -builder.add_connection(i_elute, z_elute) -builder.add_connection(z_elute, o_product) -``` diff --git a/04 Optimization Fundamentals/01_introduction/01_optimization_intro.ipynb b/04 Optimization Fundamentals/01_introduction/01_optimization_intro.ipynb new file mode 100644 index 0000000..19548ae --- /dev/null +++ b/04 Optimization Fundamentals/01_introduction/01_optimization_intro.ipynb @@ -0,0 +1,1084 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "52e41cd4", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Optimization\n", + "\n", + "One of the main applications of **CADET-Process** is performing optimization studies.\n", + "Optimization refers to the selection of a solution with regard to some criterion.\n", + "In the simplest case, an optimization problem consists of minimizing some function $f(x)$ by systematically varying the input values $x$ and computing the value of that function.\n", + "\n", + "$$\n", + "\\min_x f(x)\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "id": "8439e345", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "In the context of physico-chemical processes, examples for the application of optimization studies include scenarios such as process optimization and parameter estimation.\n", + "Here, often many variables are subject to optimization, multiple criteria have to be balanced, and additional linear and nonlinear constraints need to be considered.\n", + "\n", + "$$\n", + "\\min_x f(x) \\\\\n", + "s.t. \\\\\n", + " g(x) \\le 0, \\\\\n", + " h(x) = 0, \\\\\n", + " x \\in \\mathbb{R}^n \\\\\n", + "$$\n", + "\n", + "\n", + "where $g$ summarizes all inequality constraint functions, and $h$ equality constraints." + ] + }, + { + "cell_type": "markdown", + "id": "924e3f95", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "In the following, the `optimization` module of **CADET-Process** is introduced. To decouple the problem formulation from the problem solution, two classes are provided:\n", + "- An `OptimizationProblem` class to specify optimization variables, objectives and constraints, and\n", + "- an abstract `Optimizer` interface which allows using different external optimizers to solve the problem." + ] + }, + { + "cell_type": "markdown", + "id": "7cfe6756", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Example 1: Minimization of a quadratic function\n", + "\n", + "Usually, the objective function is not known; it can only be evaluated at certain points.\n", + "For demonstration purpouses, consider a quadratic function to be minimized.\n", + "\n", + "$$\n", + "f(x) = x^2\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "id": "a16e1263", + "metadata": {}, + "source": [ + "Since we already know a lot about this function, it can help to introduce some of the Optimization concepts of CADET-Process.\n", + "For example, the results should yield:\n", + "- $x_{opt} = 0$\n", + "- $f_{opt} = 0$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e5603593", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "%matplotlib inline\n", + "\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "\n", + "x = np.linspace(-10, 10, 101)\n", + "y = x**2\n", + "\n", + "fig, ax = plt.subplots(figsize=(3, 3))\n", + "ax.plot(x, y)" + ] + }, + { + "cell_type": "markdown", + "id": "072c8a7c", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### OptimizationProblem\n", + "\n", + "The `OptimizationProblem` class is is used to specify optimization variables, objectives and constraints.\n", + "After import, the `OptimizationProblem` initialized with a name." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c5b85478", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.optimization import OptimizationProblem\n", + "\n", + "optimization_problem = OptimizationProblem('quadratic')" + ] + }, + { + "cell_type": "markdown", + "id": "360c89e5", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "#### Optimization Variables\n", + "Any number of variables can be added to the `OptimizationProblem`.\n", + "To add a variable, use the `add_variable` method.\n", + "In this case, there is only a single variable.\n", + "The first argument is the name of the variable.\n", + "Moreover, lower and upper bounds can be specified." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d2b6b5ad", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "optimization_problem.add_variable(name='x', lb=-10, ub=10)" + ] + }, + { + "cell_type": "markdown", + "id": "f04035ec", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "The total number of variables is stored in `n_variables` and the names in `variable_names`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cd14ff98", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "print(optimization_problem.n_variables)\n", + "print(optimization_problem.variable_names)" + ] + }, + { + "cell_type": "markdown", + "id": "b58814eb", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Objectives\n", + "Any `callable` (i.e. an object that can be called using the `( )` operator) can be added as an objective as long as it takes x as the first argument.\n", + "Multi-objective optimization is also possible with CADET-Python (more on that later).\n", + "For now, the objective must return a single, scalar value.\n", + "\n", + "```{note}\n", + "Usually, there are multiple variables involved. Hence, the function is expected to accept a list.\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "595395e2", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "def objective_fun(x):\n", + " return x[0] ** 2\n", + "\n", + "optimization_problem.add_objective(objective_fun)" + ] + }, + { + "cell_type": "markdown", + "id": "5c7a939c", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "To evaluate the this function, the `evaluate_objective` method can be used.\n", + "This is useful to test whether everything works as expected." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "86b24eeb", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "print(optimization_problem.evaluate_objectives([2]))" + ] + }, + { + "cell_type": "markdown", + "id": "bd504d53", + "metadata": { + "slideshow": { + "slide_type": "notes" + } + }, + "source": [ + "If the value is out of bounds, an error will be thrown." + ] + }, + { + "cell_type": "markdown", + "id": "9ae8d322", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Optimizer\n", + "The `OptimizerAdapter` provides a unified interface for using external optimization libraries.\n", + "It is responsible for converting the `OptimizationProblem` to the specific API of the external `Optimizer`.\n", + "Currently, adapters to **Pymoo** and **Scipy's** optimization suite are implemented, all of which are published under open source licenses that allow for academic as well as commercial use.\n", + "\n", + "Before the optimization can be run, the `Optimizer` needs to be initialized and configured.\n", + "For this example, `U_NSGA3` is used, a genetic algorithm." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e3a4bb0b", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.optimization import U_NSGA3\n", + "\n", + "optimizer = U_NSGA3()" + ] + }, + { + "cell_type": "markdown", + "id": "c82399ec", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "All options can be displayed the following way:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8b18db31", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "print(optimizer.options)" + ] + }, + { + "cell_type": "markdown", + "id": "b9b8fda6", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "To reduce the calculation time, let's limit the maximum number of generations that the `Optimizer` evaluates:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "713bd225", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "optimizer.n_max_gen = 10" + ] + }, + { + "cell_type": "markdown", + "id": "15283024", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "To optimize the `OptimizationProblem`, call the `optimize()` method.\n", + "By default, CADET-Process tries to autogenerate initial values.\n", + "However, it's also possible to pass them as an additional `x0` argument.\n", + "More on generating initial values later." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "29755b05", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "optimization_results = optimizer.optimize(optimization_problem, x0=1)" + ] + }, + { + "cell_type": "markdown", + "id": "e88b307b", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Optimization Progress and Results\n", + "\n", + "The `OptimizationResults` which are returned contain information about the progress of the optimization.\n", + "For example, the attributes `x` and `f` contain the final value(s) of parameters and the objective function." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3fdd9dd3", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "print(optimization_results.x)\n", + "print(optimization_results.f)" + ] + }, + { + "cell_type": "markdown", + "id": "8a73a27e", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "After optimization, several figures can be plotted to vizualize the results.\n", + "For example, the convergence plot shows how the function value changes with the number of evaluations." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "79c3a8fc", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "optimization_results.plot_convergence('objectives')" + ] + }, + { + "cell_type": "markdown", + "id": "e9e9a660", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "The `plot_objectives` method shows the objective function values of all evaluated individuals.\n", + "Here, lighter color represent later evaluations.\n", + "Note that by default the values are plotted on a log scale if they span many orders of magnitude.\n", + "To disable this, set `autoscale=False`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7bd881aa", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "optimization_results.plot_objectives(autoscale=True)" + ] + }, + { + "cell_type": "markdown", + "id": "931fc93b", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "Note that more figures are created for constrained optimization, as well as multi-objective optimization.\n", + "All figures are also saved automatically in the `working_directory`.\n", + "Moreover, results are stored in a `.csv` file.\n", + "- The `results_all.csv` file contains information about all evaluated individuals.\n", + "- The `results_last.csv` file contains information about the last generation of evaluated individuals.\n", + "- The `results_pareto.csv` file contains only the best individual(s)." + ] + }, + { + "cell_type": "markdown", + "id": "214efd8e", + "metadata": {}, + "source": [ + "$$\n", + "\\text{subject to: } \\\\\n", + "x_0 + 2 x_1 \\leq 1 \\\\\n", + "x_0^2 + x_1 \\leq 1 \\\\\n", + " x_0^2 - x_1 \\leq 1 \\\\\n", + " 2 x_0 + x_1 = 1 \\\\\n", + " 0 \\leq x_0 \\leq 1 \\\\\n", + " -0.5 \\leq x_1 \\leq 2.0.\\\\\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "id": "ff7a7e56", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Example 2: Constrained Optimization\n", + "\n", + "Example taken from [SciPy Documentation](https://docs.scipy.org/doc/scipy/tutorial/optimize.html#id34)\n", + "\n", + "As an example let us consider the constrained minimization of the Rosenbrock function:\n", + "\n", + "$$\n", + "\\min_{x_0, x_1} & ~~100\\left(x_{1}-x_{0}^{2}\\right)^{2}+\\left(1-x_{0}\\right)^{2} &\\\\\n", + " \\text{subject to: } & x_0 + 2 x_1 \\leq 1 & \\\\\n", + " & x_0^2 + x_1 \\leq 1 & \\\\\n", + " & x_0^2 - x_1 \\leq 1 & \\\\\n", + " & 2 x_0 + x_1 = 1 & \\\\\n", + " & 0 \\leq x_0 \\leq 1 & \\\\\n", + " & -0.5 \\leq x_1 \\leq 2.0. &\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "id": "d2c886fe", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "This optimization problem has the unique solution $[x_0, x_1] = [0.4149,~ 0.1701]$.\n", + "\n", + "```{figure} ./figures/rosenbrock.png\n", + ":align: center\n", + ":width: 50%\n", + "\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "94683e7b", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "To setup this problem, first a new `OptimizationProblem` is created and the variables are added, including bounds.\n", + "It is important to note, that `x0` cannot be used as variable name since it is reserved for the initial values." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cc042fb0", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.optimization import OptimizationProblem\n", + "\n", + "rosenbrock_problem = OptimizationProblem('rosenbrock')\n", + "\n", + "rosenbrock_problem.add_variable('x_0', lb=0, ub=1)\n", + "rosenbrock_problem.add_variable('x_1', lb=-0.5, ub=2.0)" + ] + }, + { + "cell_type": "markdown", + "id": "9ddf025d", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "Then, then objective function is defined and added." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8b0c5aae", + "metadata": {}, + "outputs": [], + "source": [ + "def rosenbrock_objective(x):\n", + " x_0 = x[0]\n", + " x_1 = x[1]\n", + "\n", + " return 100 * (x_1 - x_0 ** 2) ** 2 + (1 - x_0) ** 2\n", + "\n", + "\n", + "rosenbrock_problem.add_objective(rosenbrock_objective)" + ] + }, + { + "cell_type": "markdown", + "id": "3dfe5ddb", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Linear inequality constraints\n", + "Linear constraints are usually defined in the following way\n", + "\n", + "$$\n", + "A \\cdot x \\leq b\n", + "$$\n", + "\n", + "In **CADET-Process**, add each row $a$ of the constraint matrix needs to be added individually.\n", + "The `add_linear_constraint` function takes the variables subject to the constraint as first argument.\n", + "The left-hand side $a$ and the bound $b_a$ are passed as second and third argument.\n", + "It is important to note that the column order in $a$ is inferred from the order in which the optimization variables are passed.\n", + "\n", + "To add the constraints of the Rosenbrock function to the optimization problem, add the following:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "67a66105", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "rosenbrock_problem.add_linear_constraint(['x_0', 'x_1'], [1, 2], 1)" + ] + }, + { + "cell_type": "markdown", + "id": "f1901682", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "To wheck if a point fulfils the linear inequality constraints, use the `check_linear_constraints` method.\n", + "It returns `True` if the point is within bounds and `False` otherwise." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2c7f1be3", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "rosenbrock_x0 = [0.5, 0]\n", + "rosenbrock_problem.check_linear_constraints(rosenbrock_x0)" + ] + }, + { + "cell_type": "markdown", + "id": "c26b906f", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Linear equality constraints\n", + "Linear equality constraints are usually defined in the following way\n", + "\n", + "$$\n", + "A_{eq} \\cdot x = b_{eq}\n", + "$$\n", + "\n", + "In **CADET-Process**, add each row $a_{eq}$ of the constraint matrix needs to be added individually.\n", + "The `add_linear_equality_constraint` function takes the variables subject to the constraint as first argument.\n", + "The left-hand side $a_{eq}$ and the bound $b_{eq, a}$ are passed as second and third argument.\n", + "It is important to note that the column order in $a$ is inferred from the order in which the optimization variables are passed.\n", + "\n", + "To add this constraint of the Rosenbrock function\n", + "\n", + "$$\n", + "2 x_0 + x_1 = 1\n", + "$$\n", + "\n", + "to the optimization problem, add the following:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "325c436c", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "rosenbrock_problem.add_linear_equality_constraint(['x_0', 'x_1'], lhs=[2, 1], beq=1, eps=1e-8)" + ] + }, + { + "cell_type": "markdown", + "id": "42e46b20", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "To wheck if a point fulfils the linear equality constraints, use the `check_linear_equality_constraints` method.\n", + "It returns `True` if the point is within bounds and `False` otherwise." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f27f704b", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "rosenbrock_problem.check_linear_equality_constraints(rosenbrock_x0)" + ] + }, + { + "cell_type": "markdown", + "id": "dddf925f", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Nonlinear constraints\n", + "It is also possible to add nonlinear constraints to the `OptimizationProblem`.\n", + "\n", + "$$\n", + "g(x) \\le 0 \\\\\n", + "$$\n", + "\n", + "In contrast to linear constraints, and analogous to objective functions, nonlinear constraints need to be added as a callable functions.\n", + "Note that multiple nonlinear constraints can be added.\n", + "In addition to the function, lower or upper bounds can be added." + ] + }, + { + "cell_type": "markdown", + "id": "7c807caa", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "To add the constraints of the Rosenbrock function to the optimization problem, add the following." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "51f81191", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "def nonlincon_1(x):\n", + " x_0 = x[0]\n", + " x_1 = x[1]\n", + "\n", + " return x_0 ** 2 + x_1\n", + "\n", + "\n", + "rosenbrock_problem.add_nonlinear_constraint(nonlincon_1, bounds=[1])\n", + "\n", + "\n", + "def nonlincon_2(x):\n", + " x_0 = x[0]\n", + " x_1 = x[1]\n", + "\n", + " return x_0 ** 2 - x_1\n", + "\n", + "\n", + "rosenbrock_problem.add_nonlinear_constraint(nonlincon_2, bounds=[1])" + ] + }, + { + "cell_type": "markdown", + "id": "9f0f510d", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "Again, the function can be evaluated manually." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b457c955", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "rosenbrock_problem.check_nonlinear_constraints(rosenbrock_x0)" + ] + }, + { + "cell_type": "markdown", + "id": "224836f6", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Optimizer\n", + "To solve this problem, a **trust region method** is used, here:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e323e7fb", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.optimization import TrustConstr\n", + "\n", + "trust_const_optimizer = TrustConstr()\n", + "\n", + "rosenbrock_optimization_results = trust_const_optimizer.optimize(\n", + " rosenbrock_problem,\n", + " x0=rosenbrock_x0\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "115447ed", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Optimization Progress and Results\n", + "\n", + "Since in this problem, nonlinear constraints are involved, their convergence can also be plotted" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6108bb69", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "rosenbrock_optimization_results.x" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "121db50d", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "rosenbrock_optimization_results.plot_convergence('objectives')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "214f7fe3", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "rosenbrock_optimization_results.plot_convergence('nonlinear_constraints')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b871e5af", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "rosenbrock_optimization_results.plot_objectives()" + ] + }, + { + "cell_type": "markdown", + "id": "04355f89", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Initial Values\n", + "\n", + "To start solving any optimization problem, initial values are required.\n", + "To facilitate the definition of starting points, the `OptimizationProblem` provides a `create_initial_values` method." + ] + }, + { + "cell_type": "markdown", + "id": "14dee995", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "```{note}\n", + "This method only works if all optimization variables have defined lower and upper bounds.\n", + "\n", + "Moreover, this method only guarantees that linear constraints are fulfilled.\n", + "Any nonlinear constraints may not be satisfied by the generated samples, and nonlinear parameter dependencies can be challenging to incorporate.\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "2da00a96", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "By default, the method returns a random point from the feasible region of the parameter space.\n", + "For this purpose, [hopsy](https://modsim.github.io/hopsy/) is used to efficiently (uniformly) sample the parameter space.\n", + "To create initial values, call `create_initial_values` and specify the number of individuals that should be returned." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6827e5b8", + "metadata": {}, + "outputs": [], + "source": [ + "x0 = rosenbrock_problem.create_initial_values(10)\n", + "print(x0)" + ] + }, + { + "cell_type": "markdown", + "id": "81384956", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "Alternatively, the Chebyshev center of the polytope can be computed, which is the center of the largest Euclidean ball that is fully contained within that polytope." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9c4c5d09", + "metadata": {}, + "outputs": [], + "source": [ + "x0 = rosenbrock_problem.get_chebyshev_center()\n", + "print(x0)" + ] + }, + { + "cell_type": "markdown", + "id": "966abcc2", + "metadata": {}, + "source": [ + "Let's create a method to visualize these points in the parameter space." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21e70c44", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + } + }, + "outputs": [], + "source": [ + "def plot_initial_values(x0):\n", + " import matplotlib.pyplot as plt\n", + " import numpy as np\n", + " fig, ax = plt.subplots()\n", + " try:\n", + " ax.scatter(x0[:, 0], x0[:, 1])\n", + " ax.set_xlabel(r'$x_0$')\n", + " ax.set_ylabel(r'$x_1$')\n", + " except IndexError:\n", + " ax.scatter(x0, np.ones_like((x0)))\n", + " ax.set_xlabel(r'$x_0$')\n", + " fig.tight_layout()\n", + "\n", + "x0 = rosenbrock_problem.create_initial_values(500)\n", + "plot_initial_values(x0)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/04 Optimization Fundamentals/01_introduction/01_optimization_intro.md b/04 Optimization Fundamentals/01_introduction/01_optimization_intro.md deleted file mode 100644 index e6d6ee4..0000000 --- a/04 Optimization Fundamentals/01_introduction/01_optimization_intro.md +++ /dev/null @@ -1,560 +0,0 @@ ---- -jupytext: - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.16.1 -kernelspec: - display_name: Python 3 (ipykernel) - language: python - name: python3 ---- - -+++ {"slideshow": {"slide_type": "slide"}} - -# Optimization - -One of the main applications of **CADET-Process** is performing optimization studies. -Optimization refers to the selection of a solution with regard to some criterion. -In the simplest case, an optimization problem consists of minimizing some function $f(x)$ by systematically varying the input values $x$ and computing the value of that function. - -$$ -\min_x f(x) -$$ - -+++ {"slideshow": {"slide_type": "fragment"}} - -In the context of physico-chemical processes, examples for the application of optimization studies include scenarios such as process optimization and parameter estimation. -Here, often many variables are subject to optimization, multiple criteria have to be balanced, and additional linear and nonlinear constraints need to be considered. - -$$ -\min_x f(x) \\ -s.t. \\ - g(x) \le 0, \\ - h(x) = 0, \\ - x \in \mathbb{R}^n \\ -$$ - - -where $g$ summarizes all inequality constraint functions, and $h$ equality constraints. - -+++ {"slideshow": {"slide_type": "slide"}} - -In the following, the `optimization` module of **CADET-Process** is introduced. To decouple the problem formulation from the problem solution, two classes are provided: -- An `OptimizationProblem` class to specify optimization variables, objectives and constraints, and -- an abstract `Optimizer` interface which allows using different external optimizers to solve the problem. - -+++ {"slideshow": {"slide_type": "slide"}, "editable": true} - -## Example 1: Minimization of a quadratic function - -Usually, the objective function is not known; it can only be evaluated at certain points. -For demonstration purpouses, consider a quadratic function to be minimized. - -$$ -f(x) = x^2 -$$ - -+++ - -Since we already know a lot about this function, it can help to introduce some of the Optimization concepts of CADET-Process. -For example, the results should yield: -- $x_{opt} = 0$ -- $f_{opt} = 0$. - -```{code-cell} ipython3 -:tags: [solution] - -%matplotlib inline - -import numpy as np -import matplotlib.pyplot as plt - -x = np.linspace(-10, 10, 101) -y = x**2 - -fig, ax = plt.subplots(figsize=(3, 3)) -ax.plot(x, y) -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -### OptimizationProblem - -The `OptimizationProblem` class is is used to specify optimization variables, objectives and constraints. -After import, the `OptimizationProblem` initialized with a name. - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.optimization import OptimizationProblem - -optimization_problem = OptimizationProblem('quadratic') -``` - -+++ {"slideshow": {"slide_type": "fragment"}} - -#### Optimization Variables -Any number of variables can be added to the `OptimizationProblem`. -To add a variable, use the `add_variable` method. -In this case, there is only a single variable. -The first argument is the name of the variable. -Moreover, lower and upper bounds can be specified. - -```{code-cell} ipython3 -:tags: [solution] - -optimization_problem.add_variable(name='x', lb=-10, ub=10) -``` - -+++ {"slideshow": {"slide_type": "fragment"}} - -The total number of variables is stored in `n_variables` and the names in `variable_names` - -```{code-cell} ipython3 -:tags: [solution] - -print(optimization_problem.n_variables) -print(optimization_problem.variable_names) -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -### Objectives -Any `callable` (i.e. an object that can be called using the `( )` operator) can be added as an objective as long as it takes x as the first argument. -Multi-objective optimization is also possible with CADET-Python (more on that later). -For now, the objective must return a single, scalar value. - -```{note} -Usually, there are multiple variables involved. Hence, the function is expected to accept a list. -``` - -```{code-cell} ipython3 -:tags: [solution] - -def objective_fun(x): - return x[0] ** 2 - -optimization_problem.add_objective(objective_fun) -``` - -+++ {"slideshow": {"slide_type": "fragment"}} - -To evaluate the this function, the `evaluate_objective` method can be used. -This is useful to test whether everything works as expected. - -```{code-cell} ipython3 -:tags: [solution] - -print(optimization_problem.evaluate_objectives([2])) -``` - -+++ {"slideshow": {"slide_type": "notes"}} - -If the value is out of bounds, an error will be thrown. - -+++ {"slideshow": {"slide_type": "slide"}} - -### Optimizer -The `OptimizerAdapter` provides a unified interface for using external optimization libraries. -It is responsible for converting the `OptimizationProblem` to the specific API of the external `Optimizer`. -Currently, adapters to **Pymoo** and **Scipy's** optimization suite are implemented, all of which are published under open source licenses that allow for academic as well as commercial use. - -Before the optimization can be run, the `Optimizer` needs to be initialized and configured. -For this example, `U_NSGA3` is used, a genetic algorithm. - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.optimization import U_NSGA3 - -optimizer = U_NSGA3() -``` - -+++ {"slideshow": {"slide_type": "fragment"}} - -All options can be displayed the following way: - -```{code-cell} ipython3 -:tags: [solution] - -print(optimizer.options) -``` - -+++ {"slideshow": {"slide_type": "fragment"}} - -To reduce the calculation time, let's limit the maximum number of generations that the `Optimizer` evaluates: - -```{code-cell} ipython3 -:tags: [solution] - -optimizer.n_max_gen = 10 -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -To optimize the `OptimizationProblem`, call the `optimize()` method. -By default, CADET-Process tries to autogenerate initial values. -However, it's also possible to pass them as an additional `x0` argument. -More on generating initial values later. - -```{code-cell} ipython3 -:tags: [solution] - -optimization_results = optimizer.optimize(optimization_problem, x0=1) -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -### Optimization Progress and Results - -The `OptimizationResults` which are returned contain information about the progress of the optimization. -For example, the attributes `x` and `f` contain the final value(s) of parameters and the objective function. - -```{code-cell} ipython3 -:tags: [solution] - -print(optimization_results.x) -print(optimization_results.f) -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -After optimization, several figures can be plotted to vizualize the results. -For example, the convergence plot shows how the function value changes with the number of evaluations. - -```{code-cell} ipython3 -:tags: [solution] - -optimization_results.plot_convergence('objectives') -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -The `plot_objectives` method shows the objective function values of all evaluated individuals. -Here, lighter color represent later evaluations. -Note that by default the values are plotted on a log scale if they span many orders of magnitude. -To disable this, set `autoscale=False`. - -```{code-cell} ipython3 -:tags: [solution] - -optimization_results.plot_objectives(autoscale=True) -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -Note that more figures are created for constrained optimization, as well as multi-objective optimization. -All figures are also saved automatically in the `working_directory`. -Moreover, results are stored in a `.csv` file. -- The `results_all.csv` file contains information about all evaluated individuals. -- The `results_last.csv` file contains information about the last generation of evaluated individuals. -- The `results_pareto.csv` file contains only the best individual(s). - -+++ - -$$ -\text{subject to: } \\ -x_0 + 2 x_1 \leq 1 \\ -x_0^2 + x_1 \leq 1 \\ - x_0^2 - x_1 \leq 1 \\ - 2 x_0 + x_1 = 1 \\ - 0 \leq x_0 \leq 1 \\ - -0.5 \leq x_1 \leq 2.0.\\ -$$ - -+++ {"slideshow": {"slide_type": "slide"}} - -## Example 2: Constrained Optimization - -Example taken from [SciPy Documentation](https://docs.scipy.org/doc/scipy/tutorial/optimize.html#id34) - -As an example let us consider the constrained minimization of the Rosenbrock function: - -$$ -\min_{x_0, x_1} & ~~100\left(x_{1}-x_{0}^{2}\right)^{2}+\left(1-x_{0}\right)^{2} &\\ - \text{subject to: } & x_0 + 2 x_1 \leq 1 & \\ - & x_0^2 + x_1 \leq 1 & \\ - & x_0^2 - x_1 \leq 1 & \\ - & 2 x_0 + x_1 = 1 & \\ - & 0 \leq x_0 \leq 1 & \\ - & -0.5 \leq x_1 \leq 2.0. & -$$ - -+++ {"slideshow": {"slide_type": "slide"}} - -This optimization problem has the unique solution $[x_0, x_1] = [0.4149,~ 0.1701]$. - -```{figure} ./figures/rosenbrock.png -:align: center -:width: 50% - -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -To setup this problem, first a new `OptimizationProblem` is created and the variables are added, including bounds. -It is important to note, that `x0` cannot be used as variable name since it is reserved for the initial values. - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.optimization import OptimizationProblem - -rosenbrock_problem = OptimizationProblem('rosenbrock') - -rosenbrock_problem.add_variable('x_0', lb=0, ub=1) -rosenbrock_problem.add_variable('x_1', lb=-0.5, ub=2.0) -``` - -+++ {"slideshow": {"slide_type": "fragment"}} - -Then, then objective function is defined and added. - -```{code-cell} ipython3 -def rosenbrock_objective(x): - x_0 = x[0] - x_1 = x[1] - - return 100 * (x_1 - x_0 ** 2) ** 2 + (1 - x_0) ** 2 - - -rosenbrock_problem.add_objective(rosenbrock_objective) -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -### Linear inequality constraints -Linear constraints are usually defined in the following way - -$$ -A \cdot x \leq b -$$ - -In **CADET-Process**, add each row $a$ of the constraint matrix needs to be added individually. -The `add_linear_constraint` function takes the variables subject to the constraint as first argument. -The left-hand side $a$ and the bound $b_a$ are passed as second and third argument. -It is important to note that the column order in $a$ is inferred from the order in which the optimization variables are passed. - -To add the constraints of the Rosenbrock function to the optimization problem, add the following: - -```{code-cell} ipython3 -:tags: [solution] - -rosenbrock_problem.add_linear_constraint(['x_0', 'x_1'], [1, 2], 1) -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -To wheck if a point fulfils the linear inequality constraints, use the `check_linear_constraints` method. -It returns `True` if the point is within bounds and `False` otherwise. - -```{code-cell} ipython3 -:tags: [solution] - -rosenbrock_x0 = [0.5, 0] -rosenbrock_problem.check_linear_constraints(rosenbrock_x0) -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -### Linear equality constraints -Linear equality constraints are usually defined in the following way - -$$ -A_{eq} \cdot x = b_{eq} -$$ - -In **CADET-Process**, add each row $a_{eq}$ of the constraint matrix needs to be added individually. -The `add_linear_equality_constraint` function takes the variables subject to the constraint as first argument. -The left-hand side $a_{eq}$ and the bound $b_{eq, a}$ are passed as second and third argument. -It is important to note that the column order in $a$ is inferred from the order in which the optimization variables are passed. - -To add this constraint of the Rosenbrock function - -$$ -2 x_0 + x_1 = 1 -$$ - -to the optimization problem, add the following: - -```{code-cell} ipython3 -:tags: [solution] - -rosenbrock_problem.add_linear_equality_constraint(['x_0', 'x_1'], lhs=[2, 1], beq=1, eps=1e-8) -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -To wheck if a point fulfils the linear equality constraints, use the `check_linear_equality_constraints` method. -It returns `True` if the point is within bounds and `False` otherwise. - -```{code-cell} ipython3 -:tags: [solution] - -rosenbrock_problem.check_linear_equality_constraints(rosenbrock_x0) -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -### Nonlinear constraints -It is also possible to add nonlinear constraints to the `OptimizationProblem`. - -$$ -g(x) \le 0 \\ -$$ - -In contrast to linear constraints, and analogous to objective functions, nonlinear constraints need to be added as a callable functions. -Note that multiple nonlinear constraints can be added. -In addition to the function, lower or upper bounds can be added. - -+++ {"slideshow": {"slide_type": "slide"}} - -To add the constraints of the Rosenbrock function to the optimization problem, add the following. - -```{code-cell} ipython3 -:tags: [solution] - -def nonlincon_1(x): - x_0 = x[0] - x_1 = x[1] - - return x_0 ** 2 + x_1 - - -rosenbrock_problem.add_nonlinear_constraint(nonlincon_1, bounds=[1]) - - -def nonlincon_2(x): - x_0 = x[0] - x_1 = x[1] - - return x_0 ** 2 - x_1 - - -rosenbrock_problem.add_nonlinear_constraint(nonlincon_2, bounds=[1]) -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -Again, the function can be evaluated manually. - -```{code-cell} ipython3 -:tags: [solution] - -rosenbrock_problem.check_nonlinear_constraints(rosenbrock_x0) -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -### Optimizer -To solve this problem, a **trust region method** is used, here: - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.optimization import TrustConstr - -trust_const_optimizer = TrustConstr() - -rosenbrock_optimization_results = trust_const_optimizer.optimize( - rosenbrock_problem, - x0=rosenbrock_x0 -) -``` - -+++ {"slideshow": {"slide_type": "slide"}, "editable": true} - -### Optimization Progress and Results - -Since in this problem, nonlinear constraints are involved, their convergence can also be plotted - -```{code-cell} ipython3 -:tags: [solution] - -rosenbrock_optimization_results.x -``` - -```{code-cell} ipython3 -:tags: [solution] - -rosenbrock_optimization_results.plot_convergence('objectives') -``` - -```{code-cell} ipython3 -:tags: [solution] - -rosenbrock_optimization_results.plot_convergence('nonlinear_constraints') -``` - -```{code-cell} ipython3 ---- -editable: true -slideshow: - slide_type: '' -tags: [solution] ---- -rosenbrock_optimization_results.plot_objectives() -``` - -+++ {"slideshow": {"slide_type": "slide"}, "editable": true} - -### Initial Values - -To start solving any optimization problem, initial values are required. -To facilitate the definition of starting points, the `OptimizationProblem` provides a `create_initial_values` method. - -+++ {"editable": true, "slideshow": {"slide_type": "fragment"}} - -```{note} -This method only works if all optimization variables have defined lower and upper bounds. - -Moreover, this method only guarantees that linear constraints are fulfilled. -Any nonlinear constraints may not be satisfied by the generated samples, and nonlinear parameter dependencies can be challenging to incorporate. -``` - -+++ {"slideshow": {"slide_type": "fragment"}, "editable": true} - -By default, the method returns a random point from the feasible region of the parameter space. -For this purpose, [hopsy](https://modsim.github.io/hopsy/) is used to efficiently (uniformly) sample the parameter space. -To create initial values, call `create_initial_values` and specify the number of individuals that should be returned. - -```{code-cell} ipython3 -x0 = rosenbrock_problem.create_initial_values(10) -print(x0) -``` - -+++ {"slideshow": {"slide_type": "fragment"}} - -Alternatively, the Chebyshev center of the polytope can be computed, which is the center of the largest Euclidean ball that is fully contained within that polytope. - -```{code-cell} ipython3 -x0 = rosenbrock_problem.get_chebyshev_center() -print(x0) -``` - -Let's create a method to visualize these points in the parameter space. - -```{code-cell} ipython3 ---- -editable: true -slideshow: - slide_type: '' ---- -def plot_initial_values(x0): - import matplotlib.pyplot as plt - import numpy as np - fig, ax = plt.subplots() - try: - ax.scatter(x0[:, 0], x0[:, 1]) - ax.set_xlabel(r'$x_0$') - ax.set_ylabel(r'$x_1$') - except IndexError: - ax.scatter(x0, np.ones_like((x0))) - ax.set_xlabel(r'$x_0$') - fig.tight_layout() - -x0 = rosenbrock_problem.create_initial_values(500) -plot_initial_values(x0) -``` diff --git a/04 Optimization Fundamentals/01_introduction/02_optimization_intro_exercise.ipynb b/04 Optimization Fundamentals/01_introduction/02_optimization_intro_exercise.ipynb new file mode 100644 index 0000000..447a6e2 --- /dev/null +++ b/04 Optimization Fundamentals/01_introduction/02_optimization_intro_exercise.ipynb @@ -0,0 +1,132 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d7df0dca", + "metadata": {}, + "source": [ + "# Optimization - Exercise" + ] + }, + { + "cell_type": "markdown", + "id": "593af8b9", + "metadata": {}, + "source": [ + "## Exercise 1: Setup simple optimization problem\n", + "\n", + "Solve the following optimization problem.\n", + "\n", + "$$\n", + "f(x) = \\left(x - 2 \\right)^3 - 3 \\cdot \\left( x - 2 \\right) + 1 \\\\\n", + "s.t. \\\\\n", + "0.2 \\le x \\le 4.5\n", + "$$\n", + "\n", + "Explore different optimizers, modify their options and start from different initial values." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c4a9956c", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.optimization import OptimizationProblem\n", + "\n", + "optimization_problem = OptimizationProblem('constrained_cubic')\n", + "optimization_problem.add_variable('x', lb=0.2, ub=4.5)\n", + "\n", + "def constrained_cubic(x):\n", + " \"\"\"\n", + " Compute a cubic objective function with a linear constraint.\n", + "\n", + " Parameters\n", + " ----------\n", + " x : float\n", + " The variable for the objective function.\n", + "\n", + " Returns\n", + " -------\n", + " float\n", + " The value of the objective function at.\n", + " \"\"\"\n", + " obj_val = (x - 2)**3 - 3 * (x - 2) + 1\n", + "\n", + " return obj_val\n", + "\n", + "optimization_problem.add_objective(constrained_cubic)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d7662302", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.optimization import U_NSGA3, TrustConstr\n", + "\n", + "optimizer = U_NSGA3()\n", + "optimizer.n_max_gen = 10\n", + "\n", + "optimization_results = optimizer.optimize(optimization_problem)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c0e01214", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "optimizer.results.plot_objectives()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f02b2ff8", + "metadata": {}, + "outputs": [], + "source": [ + "optimizer = TrustConstr()\n", + "optimizer.n_max_gen = 10\n", + "\n", + "optimization_results = optimizer.optimize(optimization_problem, x0=0.5)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7dfa73f3", + "metadata": {}, + "outputs": [], + "source": [ + "optimizer.results.plot_objectives()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/04 Optimization Fundamentals/01_introduction/02_optimization_intro_exercise.md b/04 Optimization Fundamentals/01_introduction/02_optimization_intro_exercise.md deleted file mode 100644 index 08789cb..0000000 --- a/04 Optimization Fundamentals/01_introduction/02_optimization_intro_exercise.md +++ /dev/null @@ -1,85 +0,0 @@ ---- -jupytext: - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.15.2 -kernelspec: - display_name: Python 3 (ipykernel) - language: python - name: python3 ---- - -# Optimization - Exercise - -+++ - -## Exercise 1: Setup simple optimization problem - -Solve the following optimization problem. - -$$ -f(x) = \left(x - 2 \right)^3 - 3 \cdot \left( x - 2 \right) + 1 \\ -s.t. \\ -0.2 \le x \le 4.5 -$$ - -Explore different optimizers, modify their options and start from different initial values. - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.optimization import OptimizationProblem - -optimization_problem = OptimizationProblem('constrained_cubic') -optimization_problem.add_variable('x', lb=0.2, ub=4.5) - -def constrained_cubic(x): - """ - Compute a cubic objective function with a linear constraint. - - Parameters - ---------- - x : float - The variable for the objective function. - - Returns - ------- - float - The value of the objective function at. - """ - obj_val = (x - 2)**3 - 3 * (x - 2) + 1 - - return obj_val - -optimization_problem.add_objective(constrained_cubic) -``` - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.optimization import U_NSGA3, TrustConstr - -optimizer = U_NSGA3() -optimizer.n_max_gen = 10 - -optimization_results = optimizer.optimize(optimization_problem) -``` - -```{code-cell} ipython3 -:tags: [solution] - -optimizer.results.plot_objectives() -``` - -```{code-cell} ipython3 -optimizer = TrustConstr() -optimizer.n_max_gen = 10 - -optimization_results = optimizer.optimize(optimization_problem, x0=0.5) -``` - -```{code-cell} ipython3 -optimizer.results.plot_objectives() -``` diff --git a/04 Optimization Fundamentals/02_advanced_techniques/01_advanced_optimization.ipynb b/04 Optimization Fundamentals/02_advanced_techniques/01_advanced_optimization.ipynb new file mode 100644 index 0000000..df9c021 --- /dev/null +++ b/04 Optimization Fundamentals/02_advanced_techniques/01_advanced_optimization.ipynb @@ -0,0 +1,542 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "373e6364", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Advanced Optimization Techniques\n", + "\n", + "In this tutorial, we will look at some advanced techniques that can\n", + "- improve convergence,\n", + "- facilitate setting up complex optimization problems\n", + "- provide usefull feedback during (long) optimization runs." + ] + }, + { + "cell_type": "markdown", + "id": "ec44f58f", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Parameter Normalization\n", + "Most optimization algorithms struggle when optimization variables spread over multiple orders of magnitude.\n", + "**CADET-Process** provides several transformation methods which can help to soften these challenges." + ] + }, + { + "cell_type": "markdown", + "id": "fd969e5d", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "```{figure} ./figures/transform.png\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "fa4f40b8", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Linear Normalization\n", + "The linear normalization maps the variable space from the lower and upper bound to a range between $0$ and $1$ by applying the following transformation:\n", + "\n", + "$$\n", + "x^\\prime = \\frac{x - x_{lb}}{x_{ub} - x_{lb}}\n", + "$$" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3f6c58f6", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.optimization import OptimizationProblem\n", + "\n", + "optimization_problem = OptimizationProblem('transform_demo')\n", + "optimization_problem.add_variable('var_lin', lb=-100, ub=100, transform='linear')" + ] + }, + { + "cell_type": "markdown", + "id": "4647aac6", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Log Normalization\n", + "The log normalization maps the variable space from the lower and upper bound to a range between $0$ and $1$ by applying the following transformation:\n", + "\n", + "$$\n", + "x^\\prime = \\frac{log \\left( \\frac{x}{x_{lb}} \\right) }{log \\left( \\frac{x_{ub} }{x_{lb}} \\right) }\n", + "$$" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c6c81281", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "optimization_problem.add_variable('var_log', lb=-100, ub=100, transform='log')" + ] + }, + { + "cell_type": "markdown", + "id": "bd3a8996", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Auto Transform\n", + "This transform will automatically switch between a linear and a log transform if the ratio of upper and lower bounds is larger than some value ($1000$ by default)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a5e3b617", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "optimization_problem.add_variable('var_auto', lb=-100, ub=100, transform='auto')" + ] + }, + { + "cell_type": "markdown", + "id": "0f2aca9b", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Evaluation Toolchains\n", + "\n", + "In many situations, some pre- and postprocessing steps are required before the objective function can be evaluated." + ] + }, + { + "cell_type": "markdown", + "id": "43cff284", + "metadata": {}, + "source": [ + "```{figure} ./figures/evaluation_example.png\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "e63696dd", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Evaluation Objects\n", + "\n", + "```{figure} ./figures/evaluation_steps.png\n", + "```\n", + "\n", + "- `OptimizationVariables` usually refers to attributes of a `Process` model (e.g. model parameters / event times.\n", + "- `EvaluationObject` objects manage the value of that optimization variable\n", + "- `Evaluators` execute (intermediate) steps required for calculating the objective (e.g. simulation)" + ] + }, + { + "cell_type": "markdown", + "id": "342c3f04", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "```{figure} ./figures/evaluation_single_variable.png\n", + ":width: 30%\n", + "```\n", + "\n", + "\n", + "To associate an `OptimizationVariable` with an `EvaluationObject`, it first needs to be added to the `OptimizationProblem`.\n", + "For this purpose, consider a simple `Process` object from the [examples collection](https://cadet-process.readthedocs.io/en/stable/examples/batch_elution/process.html)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7e928b4e", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from examples.batch_elution.process import process\n", + "\n", + "optimization_problem = OptimizationProblem('evaluator')\n", + "\n", + "optimization_problem.add_evaluation_object(process)" + ] + }, + { + "cell_type": "markdown", + "id": "d751f6bc", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "Then add the variable. In addition, specify:\n", + "\n", + "- `parameter_path`: Path to the variable in the evaluation object\n", + "- `evaluation_objects`: The evaluation object(s) for which the variable should be set." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a32eafa8", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "optimization_problem.add_variable('length', evaluation_objects=[process], parameter_path='flow_sheet.column.length', lb=0, ub=1)" + ] + }, + { + "cell_type": "markdown", + "id": "8a002d32", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Multiple Evaluation Objects\n", + "\n", + "```{figure} ./figures/evaluation_multiple_variables.png\n", + ":width: 30%\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b7bea0d0", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "optimization_problem = OptimizationProblem('two_eval_obj')\n", + "\n", + "import copy\n", + "process_2 = copy.deepcopy(process)\n", + "process_2.name = 'foo'\n", + "\n", + "optimization_problem.add_evaluation_object(process)\n", + "optimization_problem.add_evaluation_object(process_2)\n", + "\n", + "optimization_problem.add_variable('flow_sheet.column.length', lb=0, ub=1)\n", + "optimization_problem.add_variable('flow_sheet.column.diameter', lb=0, ub=1, evaluation_objects=process_2)" + ] + }, + { + "cell_type": "markdown", + "id": "489380bd", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Evaluators\n", + "Any callable function can be added as `Evaluator`, assuming the first argument is the result of the previous step and it returns a single result object which is then processed by the next step.\n", + "\n", + "```{figure} ./figures/evaluation_steps.png\n", + "```\n", + "\n", + "- Any callable function can be added as `Evaluator`.\n", + "- Each `Evaluator` takes the previous result as input and returns a new (intermediate) result.\n", + "- Intermediate results are automatically cached." + ] + }, + { + "cell_type": "markdown", + "id": "111318b0", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Evaluator Example\n", + "\n", + "In this example, two steps are required:\n", + "- Process Simulation\n", + "- Fractionation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7dff48fe", + "metadata": { + "slideshow": { + "slide_type": "slide" + }, + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.simulator import Cadet\n", + "simulator = Cadet()\n", + "\n", + "optimization_problem.add_evaluator(simulator)\n", + "\n", + "from CADETProcess.fractionation import FractionationOptimizer\n", + "frac_opt = FractionationOptimizer()\n", + "\n", + "optimization_problem.add_evaluator(\n", + " frac_opt,\n", + " kwargs={\n", + " 'purity_required': [0.95, 0.95],\n", + " 'ignore_failed': False,\n", + " 'allow_empty_fractions': False,\n", + " }\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "6c70c8e1", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Adding Objectives\n", + "\n", + "Now, when adding objectives, specify which steps are required for each objective" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3d2e95fa", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.performance import Productivity, Recovery, Purity\n", + "\n", + "productivity = Productivity()\n", + "optimization_problem.add_objective(\n", + " productivity,\n", + " n_objectives=2,\n", + " requires=[simulator, frac_opt]\n", + ")\n", + "\n", + "recovery = Recovery()\n", + "optimization_problem.add_objective(\n", + " recovery,\n", + " n_objectives=2,\n", + " requires=[simulator, frac_opt]\n", + ")\n", + "\n", + "purity = Purity()\n", + "optimization_problem.add_nonlinear_constraint(\n", + " purity,\n", + " n_nonlinear_constraints=2,\n", + " requires=[simulator, frac_opt],\n", + " bounds=[0.95, 0.95]\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "a608a549", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Evaluate Toolchain\n", + "\n", + "To check the toolchain, simply call `evaluate_objectives`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5f19bd2d", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "optimization_problem.evaluate_objectives([0.5, 0.01])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1c612155", + "metadata": {}, + "outputs": [], + "source": [ + "optimization_problem.objective_labels" + ] + }, + { + "cell_type": "markdown", + "id": "40d9b19b", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Callbacks\n", + "A `callback` function is a user function that is called periodically by the optimizer in order to allow the user to query the state of the optimization.\n", + "For example, a simple user callback function might be used to plot results.\n", + "The function is called after each iteration for all best individuals at that state.\n", + "\n", + "```{figure} ./figures/callbacks_evaluation.png\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "13a41547", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "The callback signature may include any of the following arguments:\n", + "- `results`: obj\n", + "\n", + " x or final result of evaluation toolchain.\n", + "- `individual`: {class}`Individual`, optional\n", + "\n", + " Information about current step of optimzer.\n", + "- `evaluation_object`: obj, optional\n", + "\n", + " Current evaluation object.\n", + "- `callbacks_dir`: Path, optional\n", + "\n", + " Path to store results." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ba26a1df", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + } + }, + "outputs": [], + "source": [ + "def callback(fractionation, individual, evaluation_object, callbacks_dir):\n", + " fractionation.plot_fraction_signal(\n", + " file_name=f'{callbacks_dir}/{individual.id}_{evaluation_object}_fractionation.png',\n", + " show=False\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "4e54fea7", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "To add the function to the `OptimizationProblem`, use the `add_callback` method.\n", + "Analogous to objectives," + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5aeb1d44", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "optimization_problem.add_callback(\n", + " callback, requires=[simulator, frac_opt]\n", + ")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/04 Optimization Fundamentals/02_advanced_techniques/01_advanced_optimization.md b/04 Optimization Fundamentals/02_advanced_techniques/01_advanced_optimization.md deleted file mode 100644 index 2e3318a..0000000 --- a/04 Optimization Fundamentals/02_advanced_techniques/01_advanced_optimization.md +++ /dev/null @@ -1,301 +0,0 @@ ---- -jupytext: - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.16.1 -kernelspec: - display_name: Python 3 (ipykernel) - language: python - name: python3 ---- - -+++ {"slideshow": {"slide_type": "slide"}} - -# Advanced Optimization Techniques - -In this tutorial, we will look at some advanced techniques that can -- improve convergence, -- facilitate setting up complex optimization problems -- provide usefull feedback during (long) optimization runs. - -+++ {"slideshow": {"slide_type": "slide"}} - -## Parameter Normalization -Most optimization algorithms struggle when optimization variables spread over multiple orders of magnitude. -**CADET-Process** provides several transformation methods which can help to soften these challenges. - -+++ {"slideshow": {"slide_type": "fragment"}} - -```{figure} ./figures/transform.png -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -### Linear Normalization -The linear normalization maps the variable space from the lower and upper bound to a range between $0$ and $1$ by applying the following transformation: - -$$ -x^\prime = \frac{x - x_{lb}}{x_{ub} - x_{lb}} -$$ - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.optimization import OptimizationProblem - -optimization_problem = OptimizationProblem('transform_demo') -optimization_problem.add_variable('var_lin', lb=-100, ub=100, transform='linear') -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -### Log Normalization -The log normalization maps the variable space from the lower and upper bound to a range between $0$ and $1$ by applying the following transformation: - -$$ -x^\prime = \frac{log \left( \frac{x}{x_{lb}} \right) }{log \left( \frac{x_{ub} }{x_{lb}} \right) } -$$ - -```{code-cell} ipython3 -:tags: [solution] - -optimization_problem.add_variable('var_log', lb=-100, ub=100, transform='log') -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -### Auto Transform -This transform will automatically switch between a linear and a log transform if the ratio of upper and lower bounds is larger than some value ($1000$ by default). - -```{code-cell} ipython3 -:tags: [solution] - -optimization_problem.add_variable('var_auto', lb=-100, ub=100, transform='auto') -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -# Evaluation Toolchains - -In many situations, some pre- and postprocessing steps are required before the objective function can be evaluated. - -+++ - -```{figure} ./figures/evaluation_example.png -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -## Evaluation Objects - -```{figure} ./figures/evaluation_steps.png -``` - -- `OptimizationVariables` usually refers to attributes of a `Process` model (e.g. model parameters / event times. -- `EvaluationObject` objects manage the value of that optimization variable -- `Evaluators` execute (intermediate) steps required for calculating the objective (e.g. simulation) - -+++ {"slideshow": {"slide_type": "slide"}} - -```{figure} ./figures/evaluation_single_variable.png -:width: 30% -``` - - -To associate an `OptimizationVariable` with an `EvaluationObject`, it first needs to be added to the `OptimizationProblem`. -For this purpose, consider a simple `Process` object from the [examples collection](https://cadet-process.readthedocs.io/en/stable/examples/batch_elution/process.html). - -```{code-cell} ipython3 -:tags: [solution] - -from examples.batch_elution.process import process - -optimization_problem = OptimizationProblem('evaluator') - -optimization_problem.add_evaluation_object(process) -``` - -+++ {"slideshow": {"slide_type": "fragment"}} - -Then add the variable. In addition, specify: - -- `parameter_path`: Path to the variable in the evaluation object -- `evaluation_objects`: The evaluation object(s) for which the variable should be set. - -```{code-cell} ipython3 -:tags: [solution] - -optimization_problem.add_variable('length', evaluation_objects=[process], parameter_path='flow_sheet.column.length', lb=0, ub=1) -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -## Multiple Evaluation Objects - -```{figure} ./figures/evaluation_multiple_variables.png -:width: 30% -``` - -```{code-cell} ipython3 -:tags: [solution] - -optimization_problem = OptimizationProblem('two_eval_obj') - -import copy -process_2 = copy.deepcopy(process) -process_2.name = 'foo' - -optimization_problem.add_evaluation_object(process) -optimization_problem.add_evaluation_object(process_2) - -optimization_problem.add_variable('flow_sheet.column.length', lb=0, ub=1) -optimization_problem.add_variable('flow_sheet.column.diameter', lb=0, ub=1, evaluation_objects=process_2) -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -### Evaluators -Any callable function can be added as `Evaluator`, assuming the first argument is the result of the previous step and it returns a single result object which is then processed by the next step. - -```{figure} ./figures/evaluation_steps.png -``` - -- Any callable function can be added as `Evaluator`. -- Each `Evaluator` takes the previous result as input and returns a new (intermediate) result. -- Intermediate results are automatically cached. - -+++ {"slideshow": {"slide_type": "slide"}} - -## Evaluator Example - -In this example, two steps are required: -- Process Simulation -- Fractionation - -```{code-cell} ipython3 ---- -slideshow: - slide_type: slide -tags: [solution] ---- -from CADETProcess.simulator import Cadet -simulator = Cadet() - -optimization_problem.add_evaluator(simulator) - -from CADETProcess.fractionation import FractionationOptimizer -frac_opt = FractionationOptimizer() - -optimization_problem.add_evaluator( - frac_opt, - kwargs={ - 'purity_required': [0.95, 0.95], - 'ignore_failed': False, - 'allow_empty_fractions': False, - } -) -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -## Adding Objectives - -Now, when adding objectives, specify which steps are required for each objective - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.performance import Productivity, Recovery, Purity - -productivity = Productivity() -optimization_problem.add_objective( - productivity, - n_objectives=2, - requires=[simulator, frac_opt] -) - -recovery = Recovery() -optimization_problem.add_objective( - recovery, - n_objectives=2, - requires=[simulator, frac_opt] -) - -purity = Purity() -optimization_problem.add_nonlinear_constraint( - purity, - n_nonlinear_constraints=2, - requires=[simulator, frac_opt], - bounds=[0.95, 0.95] -) -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -## Evaluate Toolchain - -To check the toolchain, simply call `evaluate_objectives` - -```{code-cell} ipython3 -:tags: [solution] - -optimization_problem.evaluate_objectives([0.5, 0.01]) -``` - -```{code-cell} ipython3 -optimization_problem.objective_labels -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -## Callbacks -A `callback` function is a user function that is called periodically by the optimizer in order to allow the user to query the state of the optimization. -For example, a simple user callback function might be used to plot results. -The function is called after each iteration for all best individuals at that state. - -```{figure} ./figures/callbacks_evaluation.png -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -The callback signature may include any of the following arguments: -- `results`: obj - - x or final result of evaluation toolchain. -- `individual`: {class}`Individual`, optional - - Information about current step of optimzer. -- `evaluation_object`: obj, optional - - Current evaluation object. -- `callbacks_dir`: Path, optional - - Path to store results. - -```{code-cell} ipython3 ---- -editable: true -slideshow: - slide_type: '' ---- -def callback(fractionation, individual, evaluation_object, callbacks_dir): - fractionation.plot_fraction_signal( - file_name=f'{callbacks_dir}/{individual.id}_{evaluation_object}_fractionation.png', - show=False - ) -``` - -+++ {"slideshow": {"slide_type": "fragment"}} - -To add the function to the `OptimizationProblem`, use the `add_callback` method. -Analogous to objectives, - -```{code-cell} ipython3 -:tags: [solution] - -optimization_problem.add_callback( - callback, requires=[simulator, frac_opt] -) -``` diff --git a/05 Parameter Estimation/01_comparison.ipynb b/05 Parameter Estimation/01_comparison.ipynb new file mode 100644 index 0000000..b73bfb4 --- /dev/null +++ b/05 Parameter Estimation/01_comparison.ipynb @@ -0,0 +1,616 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "e7503463", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + } + }, + "source": [ + "# Parameter Estimation\n", + "One important aspect in modelling is parameter estimation.\n", + "For this purpose, model parameters are varied until the simulated output matches some reference (usually experimental data).\n", + "To quantify the difference between simulation and reference, **CADET-Process** provides a `comparison` module." + ] + }, + { + "cell_type": "markdown", + "id": "331b7eea", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "Consider a simple tracer pulse injection onto a chromatographic column.\n", + "The following (experimental) concentration profile is measured at the column outlet." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "534c09e8", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + } + }, + "outputs": [], + "source": [ + "import numpy as np\n", + "data = np.loadtxt('./experimental_data/non_pore_penetrating_tracer.csv', delimiter=',')\n", + "\n", + "time_experiment = data[:, 0]\n", + "dextran_experiment = data[:, 1]\n", + "\n", + "import matplotlib.pyplot as plt\n", + "_ = plt.plot(time_experiment, dextran_experiment)" + ] + }, + { + "cell_type": "markdown", + "id": "c84e4618", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "The goal is to determine the bed porosity of the column, as well the axial dispersion.\n", + "Other process parameters like the column geometry and particle sizes are assumed to be known." + ] + }, + { + "cell_type": "markdown", + "id": "19cb5cd0", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## References\n", + "To properly work with **CADET-Process**, the experimental data needs to be converted to an internal standard.\n", + "The `reference` module provides different classes for different types of experiments.\n", + "For in- and outgoing streams of unit operations, the `ReferenceIO` class must be used." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "22a354bb", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + } + }, + "outputs": [], + "source": [ + "from CADETProcess.reference import ReferenceIO\n", + "\n", + "reference = ReferenceIO('dextran experiment', time_experiment, dextran_experiment)" + ] + }, + { + "cell_type": "markdown", + "id": "9b8a4547", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "Similarly to the `SolutionIO` class, the `ReferenceIO` class also provides a plot method:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8bbab416", + "metadata": {}, + "outputs": [], + "source": [ + "_ = reference.plot()" + ] + }, + { + "cell_type": "markdown", + "id": "ceba8391", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Comparator\n", + "\n", + "The `Comparator` class comparing the simulation output with experimental data. It provides several methods for visualizing and analyzing the differences between the data sets. Users can choose from a range of metrics to quantify the differences between the two data sets, such as sum squared errors or shape comparison." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "121666bf", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.comparison import Comparator\n", + "comparator = Comparator()\n", + "\n", + "comparator.add_reference(reference)" + ] + }, + { + "cell_type": "markdown", + "id": "559e9b48", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "```{note}\n", + "It's also possible to add multiple references, e.g. for triplicate experiments or for different sensors.\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "ef905d3d", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Difference Metrics\n", + "There are many metrics which can be used to quantify the difference between the simulation and the reference.\n", + "Most commonly, the sum squared error (SSE) is used." + ] + }, + { + "cell_type": "markdown", + "id": "33725781", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "However, SSE is often not an ideal measurement for chromatography.\n", + "Because of experimental non-idealities like pump delays and fluctuations in flow rate there is a tendency for the peaks to shift in time.\n", + "This causes the optimizer to favour peak position over peak shape and can lead for example to an overestimation of axial dispersion.\n", + "\n", + "In contrast, the peak shape is dictated by the physics of the physico-chemical interactions while the position can shift slightly due to systematic errors like pump delays.\n", + "Hence, a metric which prioritizes the shape of the peaks being accurate over the peak eluting exactly at the correct time is preferable.\n", + "For this purpose, **CADET-Process** offers a `Shape` metric." + ] + }, + { + "cell_type": "markdown", + "id": "d7ab2eda", + "metadata": { + "jupyterlab-deck": { + "layer": "fragment" + } + }, + "source": [ + "To add a difference metric, the following arguments need to be passed to the `add_difference_metric` method:\n", + "- `difference_metric`: The type of the metric.\n", + "- `reference`: The reference which should be used for the metric.\n", + "- `solution_path`: The path to the corresponding solution in the simulation results." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ac5dfd76", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "comparator.add_difference_metric('SSE', reference, 'column.outlet')" + ] + }, + { + "cell_type": "markdown", + "id": "96951248", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "Optionally, a start and end time can be specified to only evaluate the difference metric at that slice.\n", + "This is particularly useful if system noise (e.g. injection peaks) should be ignored or if certain peaks correspond to certain components." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b147aa9e", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "comparator = Comparator()\n", + "comparator.add_reference(reference)\n", + "comparator.add_difference_metric(\n", + " 'SSE', reference, 'column.outlet', start=3*60, end=6*60\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "8307c966", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Reference Model\n", + "\n", + "Next to the experimental data, a reference model needs to be configured.\n", + "It must include relevant details s.t. it is capable of accurately predicting the experimental system (e.g. tubing, valves etc.).\n", + "For this example, the full process configuration can be found {ref}`here `.\n", + "\n", + "As an initial guess, the bed porosity is set to $0.5$, and the axial dispersion to $1.0 \\cdot 10^{-7}$.\n", + "After process simulation, the `evaluate` method is called with the simulation results." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7338dd25", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.simulator import Cadet\n", + "simulator = Cadet()\n", + "\n", + "from examples.characterize_chromatographic_system.column_transport_parameters import process\n", + "process.flow_sheet.column.bed_porosity = 0.5\n", + "process.flow_sheet.column.axial_dispersion = 1e-7\n", + "\n", + "simulation_results = simulator.simulate(process)\n", + "\n", + "metrics = comparator.evaluate(simulation_results)\n", + "print(metrics)" + ] + }, + { + "cell_type": "markdown", + "id": "47f06f7b", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "The difference can also be visualized:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cd95f365", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "_ = comparator.plot_comparison(simulation_results)" + ] + }, + { + "cell_type": "markdown", + "id": "784ab20d", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "The comparison shows that there is still a large discrepancy between simulation and experiment." + ] + }, + { + "cell_type": "markdown", + "id": "107b8a8b", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Optimization\n", + "\n", + "Instead of manually adjusting these parameters, an `OptimizationProblem` can be set up which automatically determines the parameter values.\n", + "For this purpose, an `OptimimizationProblem` is defined and the process is added as an evaluation object." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f6f9a3e2", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.optimization import OptimizationProblem\n", + "optimization_problem = OptimizationProblem('bed_porosity_axial_dispersion')\n", + "\n", + "optimization_problem.add_evaluation_object(process)" + ] + }, + { + "cell_type": "markdown", + "id": "45ce819f", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "Then, the optimization variables are added.\n", + "Note, the parameter path associates the variable with the parameter of the corresponding column unit operation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "89257558", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "optimization_problem.add_variable(\n", + " name='bed_porosity', parameter_path='flow_sheet.column.bed_porosity',\n", + " lb=0.1, ub=0.5,\n", + " transform='auto'\n", + ")\n", + "\n", + "optimization_problem.add_variable(\n", + " name='axial_dispersion', parameter_path='flow_sheet.column.axial_dispersion',\n", + " lb=1e-10, ub=1e-6,\n", + " transform='auto'\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "606ab817", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "Before the difference metrics, which we want to minimize, the `Process` needs to be simulated.\n", + "For this purpose, register the `Cadet` simulator instance as an evaluator." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7d98d45f", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "optimization_problem.add_evaluator(simulator)" + ] + }, + { + "cell_type": "markdown", + "id": "00b424a9", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "Now, when adding the `Comparator` (which determines the difference metrics) as objective function, the simulator can be added to the `required` list.\n", + "Note that the number of metrics needs to be passed as `n_objectives`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d5473d46", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "optimization_problem.add_objective(\n", + " comparator,\n", + " n_objectives=comparator.n_metrics,\n", + " requires=[simulator]\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "2d634a8a", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "To get some feedback during optimization, a callback function is added which plots the comparison with the experimental data and stores it in a separate directory." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "366319a9", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "def callback(simulation_results, individual, evaluation_object, callbacks_dir='./'):\n", + " comparator.plot_comparison(\n", + " simulation_results,\n", + " file_name=f'{callbacks_dir}/{individual.id}_{evaluation_object}_comparison.png',\n", + " show=False\n", + " )\n", + "\n", + "optimization_problem.add_callback(callback, requires=[simulator])" + ] + }, + { + "cell_type": "markdown", + "id": "59d7f69f", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Optimizer\n", + "\n", + "A couple of optimizers are available in **CADET-Process**.\n", + "Depending on the problem at hand, some optimizers might outperform others.\n", + "Generally, `U_NSGA3`, a genetic algorithm, is a robust choice.\n", + "While not necessarily the most efficient, it usually manages to handle complex problems with multiple dimensions, constraints, and objectives.\n", + "Here, we limit the number of cores, the population size, as well as the maximum number of generations." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fc91b4f1", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.optimization import U_NSGA3\n", + "optimizer = U_NSGA3()\n", + "optimizer.n_cores = 8\n", + "optimizer.pop_size = 32\n", + "optimizer.n_max_gen = 8" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9707f47d", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "optimization_results = optimizer.optimize(\n", + " optimization_problem,\n", + " use_checkpoint=False\n", + ")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/05 Parameter Estimation/01_comparison.md b/05 Parameter Estimation/01_comparison.md deleted file mode 100644 index dc08381..0000000 --- a/05 Parameter Estimation/01_comparison.md +++ /dev/null @@ -1,334 +0,0 @@ ---- -jupytext: - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.16.2 -kernelspec: - display_name: Python 3 (ipykernel) - language: python - name: python3 ---- - -+++ {"editable": true, "slideshow": {"slide_type": ""}} - -# Parameter Estimation -One important aspect in modelling is parameter estimation. -For this purpose, model parameters are varied until the simulated output matches some reference (usually experimental data). -To quantify the difference between simulation and reference, **CADET-Process** provides a `comparison` module. - -+++ {"slideshow": {"slide_type": "fragment"}} - -Consider a simple tracer pulse injection onto a chromatographic column. -The following (experimental) concentration profile is measured at the column outlet. - -```{code-cell} ipython3 ---- -editable: true -slideshow: - slide_type: '' ---- -import numpy as np -data = np.loadtxt('./experimental_data/non_pore_penetrating_tracer.csv', delimiter=',') - -time_experiment = data[:, 0] -dextran_experiment = data[:, 1] - -import matplotlib.pyplot as plt -_ = plt.plot(time_experiment, dextran_experiment) -``` - -+++ {"slideshow": {"slide_type": "fragment"}} - -The goal is to determine the bed porosity of the column, as well the axial dispersion. -Other process parameters like the column geometry and particle sizes are assumed to be known. - -+++ {"slideshow": {"slide_type": "slide"}} - -## References -To properly work with **CADET-Process**, the experimental data needs to be converted to an internal standard. -The `reference` module provides different classes for different types of experiments. -For in- and outgoing streams of unit operations, the `ReferenceIO` class must be used. - -```{code-cell} ipython3 ---- -editable: true -slideshow: - slide_type: '' ---- -from CADETProcess.reference import ReferenceIO - -reference = ReferenceIO('dextran experiment', time_experiment, dextran_experiment) -``` - -+++ {"slideshow": {"slide_type": "fragment"}} - -Similarly to the `SolutionIO` class, the `ReferenceIO` class also provides a plot method: - -```{code-cell} ipython3 -_ = reference.plot() -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -## Comparator - -The `Comparator` class comparing the simulation output with experimental data. It provides several methods for visualizing and analyzing the differences between the data sets. Users can choose from a range of metrics to quantify the differences between the two data sets, such as sum squared errors or shape comparison. - -```{code-cell} ipython3 ---- -editable: true -slideshow: - slide_type: '' -tags: [solution] ---- -from CADETProcess.comparison import Comparator -comparator = Comparator() - -comparator.add_reference(reference) -``` - -+++ {"slideshow": {"slide_type": "fragment"}} - -```{note} -It's also possible to add multiple references, e.g. for triplicate experiments or for different sensors. -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -## Difference Metrics -There are many metrics which can be used to quantify the difference between the simulation and the reference. -Most commonly, the sum squared error (SSE) is used. - -+++ {"slideshow": {"slide_type": "fragment"}} - -However, SSE is often not an ideal measurement for chromatography. -Because of experimental non-idealities like pump delays and fluctuations in flow rate there is a tendency for the peaks to shift in time. -This causes the optimizer to favour peak position over peak shape and can lead for example to an overestimation of axial dispersion. - -In contrast, the peak shape is dictated by the physics of the physico-chemical interactions while the position can shift slightly due to systematic errors like pump delays. -Hence, a metric which prioritizes the shape of the peaks being accurate over the peak eluting exactly at the correct time is preferable. -For this purpose, **CADET-Process** offers a `Shape` metric. - -+++ {"jupyterlab-deck": {"layer": "fragment"}} - -To add a difference metric, the following arguments need to be passed to the `add_difference_metric` method: -- `difference_metric`: The type of the metric. -- `reference`: The reference which should be used for the metric. -- `solution_path`: The path to the corresponding solution in the simulation results. - -```{code-cell} ipython3 ---- -editable: true -slideshow: - slide_type: '' -tags: [solution] ---- -comparator.add_difference_metric('SSE', reference, 'column.outlet') -``` - -+++ {"slideshow": {"slide_type": "fragment"}} - -Optionally, a start and end time can be specified to only evaluate the difference metric at that slice. -This is particularly useful if system noise (e.g. injection peaks) should be ignored or if certain peaks correspond to certain components. - -```{code-cell} ipython3 ---- -editable: true -slideshow: - slide_type: '' -tags: [solution] ---- -comparator = Comparator() -comparator.add_reference(reference) -comparator.add_difference_metric( - 'SSE', reference, 'column.outlet', start=3*60, end=6*60 -) -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -## Reference Model - -Next to the experimental data, a reference model needs to be configured. -It must include relevant details s.t. it is capable of accurately predicting the experimental system (e.g. tubing, valves etc.). -For this example, the full process configuration can be found {ref}`here `. - -As an initial guess, the bed porosity is set to $0.5$, and the axial dispersion to $1.0 \cdot 10^{-7}$. -After process simulation, the `evaluate` method is called with the simulation results. - -```{code-cell} ipython3 ---- -editable: true -slideshow: - slide_type: '' -tags: [solution] ---- -from CADETProcess.simulator import Cadet -simulator = Cadet() - -from examples.characterize_chromatographic_system.column_transport_parameters import process -process.flow_sheet.column.bed_porosity = 0.5 -process.flow_sheet.column.axial_dispersion = 1e-7 - -simulation_results = simulator.simulate(process) - -metrics = comparator.evaluate(simulation_results) -print(metrics) -``` - -+++ {"slideshow": {"slide_type": "fragment"}} - -The difference can also be visualized: - -```{code-cell} ipython3 ---- -editable: true -slideshow: - slide_type: '' -tags: [solution] ---- -_ = comparator.plot_comparison(simulation_results) -``` - -+++ {"slideshow": {"slide_type": "fragment"}} - -The comparison shows that there is still a large discrepancy between simulation and experiment. - -+++ {"editable": true, "slideshow": {"slide_type": "slide"}} - -## Optimization - -Instead of manually adjusting these parameters, an `OptimizationProblem` can be set up which automatically determines the parameter values. -For this purpose, an `OptimimizationProblem` is defined and the process is added as an evaluation object. - -```{code-cell} ipython3 ---- -editable: true -slideshow: - slide_type: '' -tags: [solution] ---- -from CADETProcess.optimization import OptimizationProblem -optimization_problem = OptimizationProblem('bed_porosity_axial_dispersion') - -optimization_problem.add_evaluation_object(process) -``` - -+++ {"editable": true, "slideshow": {"slide_type": "fragment"}} - -Then, the optimization variables are added. -Note, the parameter path associates the variable with the parameter of the corresponding column unit operation. - -```{code-cell} ipython3 ---- -editable: true -slideshow: - slide_type: '' -tags: [solution] ---- -optimization_problem.add_variable( - name='bed_porosity', parameter_path='flow_sheet.column.bed_porosity', - lb=0.1, ub=0.5, - transform='auto' -) - -optimization_problem.add_variable( - name='axial_dispersion', parameter_path='flow_sheet.column.axial_dispersion', - lb=1e-10, ub=1e-6, - transform='auto' -) -``` - -+++ {"editable": true, "slideshow": {"slide_type": "slide"}} - -Before the difference metrics, which we want to minimize, the `Process` needs to be simulated. -For this purpose, register the `Cadet` simulator instance as an evaluator. - -```{code-cell} ipython3 ---- -editable: true -slideshow: - slide_type: '' -tags: [solution] ---- -optimization_problem.add_evaluator(simulator) -``` - -+++ {"editable": true, "slideshow": {"slide_type": "fragment"}} - -Now, when adding the `Comparator` (which determines the difference metrics) as objective function, the simulator can be added to the `required` list. -Note that the number of metrics needs to be passed as `n_objectives`. - -```{code-cell} ipython3 ---- -editable: true -slideshow: - slide_type: '' -tags: [solution] ---- -optimization_problem.add_objective( - comparator, - n_objectives=comparator.n_metrics, - requires=[simulator] -) -``` - -+++ {"editable": true, "slideshow": {"slide_type": "fragment"}} - -To get some feedback during optimization, a callback function is added which plots the comparison with the experimental data and stores it in a separate directory. - -```{code-cell} ipython3 ---- -editable: true -slideshow: - slide_type: '' -tags: [solution] ---- -def callback(simulation_results, individual, evaluation_object, callbacks_dir='./'): - comparator.plot_comparison( - simulation_results, - file_name=f'{callbacks_dir}/{individual.id}_{evaluation_object}_comparison.png', - show=False - ) - -optimization_problem.add_callback(callback, requires=[simulator]) -``` - -+++ {"editable": true, "slideshow": {"slide_type": "slide"}} - -## Optimizer - -A couple of optimizers are available in **CADET-Process**. -Depending on the problem at hand, some optimizers might outperform others. -Generally, `U_NSGA3`, a genetic algorithm, is a robust choice. -While not necessarily the most efficient, it usually manages to handle complex problems with multiple dimensions, constraints, and objectives. -Here, we limit the number of cores, the population size, as well as the maximum number of generations. - -```{code-cell} ipython3 ---- -editable: true -slideshow: - slide_type: '' -tags: [solution] ---- -from CADETProcess.optimization import U_NSGA3 -optimizer = U_NSGA3() -optimizer.n_cores = 8 -optimizer.pop_size = 32 -optimizer.n_max_gen = 8 -``` - -```{code-cell} ipython3 ---- -editable: true -slideshow: - slide_type: '' -tags: [solution] ---- -optimization_results = optimizer.optimize( - optimization_problem, - use_checkpoint=False -) -``` diff --git a/05 Parameter Estimation/01_comparison_exercise.ipynb b/05 Parameter Estimation/01_comparison_exercise.ipynb new file mode 100644 index 0000000..31b982c --- /dev/null +++ b/05 Parameter Estimation/01_comparison_exercise.ipynb @@ -0,0 +1,408 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "138bb38c", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + } + }, + "source": [ + "# Comparison - Exercise\n", + "\n", + "For this exercise, consider the following flow sheet:\n", + "\n", + "```{figure} ./figures/comparator_uv_cond.png\n", + "```\n", + "\n", + "To characterize the system periphery, a pore-penetrating tracer (Acetone) pulse was injected into the system without a column and UV and conductivity were measured.\n", + "\n", + "```{note}\n", + "To model the additional dispersion of the system, two `Cstr`s were introduced\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "09d47a50", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + } + }, + "source": [ + "## Exercise 1: Comparator\n", + "\n", + "The (synthetic) experiment was repeated to account for system variability. The data was converted to concentrations in $mM$ and can be found in `./experimental_data`.\n", + "\n", + "**Task:**\n", + "- Import and plot the experimental data using the `ReferenceIO` class.\n", + "- Add the references to the `Comparator`.\n", + "- Add the `SSE` difference metric and compare with simulation results.\n", + "- Compare with other metrics." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b094e269", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + } + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import ComponentSystem\n", + "from CADETProcess.processModel import Inlet, Cstr, TubularReactor, Outlet\n", + "from CADETProcess.processModel import FlowSheet\n", + "from CADETProcess.processModel import Process\n", + "from CADETProcess.simulator import Cadet\n", + "\n", + "# Some Variables\n", + "Q_ml_min = 0.5 # ml/min\n", + "Q_m3_s = Q_ml_min/(60*1e6)\n", + "V_tracer = 50e-9 # m3\n", + "\n", + "# Component System\n", + "component_system = ComponentSystem(['Acetone'])\n", + "\n", + "# Unit Operations\n", + "acetone = Inlet(component_system, name='acetone')\n", + "acetone.c = [131.75]\n", + "\n", + "water = Inlet(component_system, name='water')\n", + "water.c = [0]\n", + "\n", + "inlet_valve = Cstr(component_system, name='inlet_valve')\n", + "inlet_valve.V = 0.3e-6\n", + "inlet_valve.c = [0]\n", + "\n", + "tubing = TubularReactor(component_system, name='tubing')\n", + "tubing.length = 0.85\n", + "tubing.diameter = 0.75e-3\n", + "tubing.axial_dispersion = 1e-7\n", + "tubing.c = [0]\n", + "\n", + "uv_detector = Cstr(component_system, name='uv_detector')\n", + "uv_detector.V = 0.1e-6\n", + "uv_detector.c = [0]\n", + "\n", + "cond_detector = Cstr(component_system, name='cond_detector')\n", + "cond_detector.V = 0.2e-6\n", + "cond_detector.c = [0]\n", + "\n", + "outlet = Outlet(component_system, name='outlet')\n", + "\n", + "# Flow Sheet\n", + "fs = FlowSheet(component_system)\n", + "fs.add_unit(acetone)\n", + "fs.add_unit(water)\n", + "fs.add_unit(inlet_valve)\n", + "fs.add_unit(tubing)\n", + "fs.add_unit(uv_detector)\n", + "fs.add_unit(cond_detector)\n", + "fs.add_unit(outlet)\n", + "\n", + "fs.add_connection(acetone, inlet_valve)\n", + "fs.add_connection(water, inlet_valve)\n", + "fs.add_connection(inlet_valve, tubing)\n", + "fs.add_connection(tubing, uv_detector)\n", + "fs.add_connection(uv_detector, cond_detector)\n", + "fs.add_connection(cond_detector, outlet)\n", + "\n", + "# Process\n", + "process = Process(fs, 'Acetone_Pulse_no_column')\n", + "process.cycle_time = 500\n", + "\n", + "process.add_event('pulse_acetone_on', 'flow_sheet.acetone.flow_rate', Q_m3_s, 0)\n", + "process.add_event('pulse_acetone_off', 'flow_sheet.acetone.flow_rate', 0, V_tracer/Q_m3_s)\n", + "\n", + "process.add_event('feed_water_on', 'flow_sheet.water.flow_rate', Q_m3_s, V_tracer/Q_m3_s)\n", + "process.add_event('feed_water_off', 'flow_sheet.water.flow_rate', 0, process.cycle_time)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a30eaab5", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + } + }, + "outputs": [], + "source": [ + "simulator = Cadet()\n", + "\n", + "simulation_results = simulator.simulate(process)\n", + "\n", + "fig, ax = None, None\n", + "y_max = 30\n", + "fig, ax = simulation_results.solution.inlet_valve.outlet.plot(fig=fig, ax=ax, y_max=y_max)\n", + "fig, ax = simulation_results.solution.tubing.outlet.plot(fig=fig, ax=ax, y_max=y_max)\n", + "fig, ax = simulation_results.solution.uv_detector.outlet.plot(fig=fig, ax=ax, y_max=y_max)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6eafff93", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.reference import ReferenceIO\n", + "import numpy as np\n", + "\n", + "data_uv_1 = np.loadtxt('./experimental_data/uv_detector_1.csv', delimiter=',')\n", + "time_uv_1 = data_uv_1[:, 0]\n", + "uv_1 = data_uv_1[:, 1]\n", + "reference_uv_1 = ReferenceIO('uv_1', time_uv_1, uv_1)\n", + "_ = reference_uv_1.plot()\n", + "\n", + "data_uv_2 = np.loadtxt('./experimental_data/uv_detector_2.csv', delimiter=',')\n", + "time_uv_2 = data_uv_2[:, 0]\n", + "uv_2 = data_uv_2[:, 1]\n", + "reference_uv_2 = ReferenceIO('uv_2', time_uv_2, uv_2)\n", + "_ = reference_uv_2.plot()\n", + "\n", + "data_cond_1 = np.loadtxt('./experimental_data/cond_detector_1.csv', delimiter=',')\n", + "time_cond_1 = data_cond_1[:, 0]\n", + "cond_1 = data_cond_1[:, 1]\n", + "reference_cond_1 = ReferenceIO('cond_1', time_cond_1, cond_1)\n", + "_ = reference_cond_1.plot()\n", + "\n", + "data_cond_2 = np.loadtxt('./experimental_data/cond_detector_2.csv', delimiter=',')\n", + "time_cond_2 = data_cond_2[:, 0]\n", + "cond_2 = data_cond_2[:, 1]\n", + "reference_cond_2 = ReferenceIO('cond_2', time_cond_2, cond_2)\n", + "_ = reference_cond_2.plot()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6f06ae16", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.comparison import Comparator\n", + "\n", + "comparator = Comparator()\n", + "\n", + "comparator.add_reference(reference_uv_1)\n", + "comparator.add_reference(reference_uv_2)\n", + "comparator.add_reference(reference_cond_1)\n", + "comparator.add_reference(reference_cond_2)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5e6353c8", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "comparator.add_difference_metric('Shape', reference_uv_1, 'uv_detector.outlet')\n", + "comparator.add_difference_metric('Shape', reference_uv_2, 'uv_detector.outlet')\n", + "\n", + "comparator.add_difference_metric('Shape', reference_cond_1, 'cond_detector.outlet')\n", + "comparator.add_difference_metric('Shape', reference_cond_2, 'cond_detector.outlet')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5a102f77", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "comparator.evaluate(simulation_results)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "056898f0", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + } + }, + "outputs": [], + "source": [ + "_ = comparator.plot_comparison(simulation_results)" + ] + }, + { + "cell_type": "markdown", + "id": "4df446d9", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "```{note}\n", + "It's also possible to add multiple references, e.g. for triplicate experiments or for different sensors.\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "1e25f4c5", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + } + }, + "source": [ + "## Exercise 2: Optimization\n", + "\n", + "**Task:**\n", + "- Setup optimization problem\n", + "- Add the process to the optimization problem and define the following optimization variables.\n", + " - `inlet_valve.V`: [1e-7, 1e-6] m³\n", + " - `tubing.axial_dispersion` = [1e-8, 1e-6] m²/s\n", + " - `uv_detector.V`: [1e-7, 1e-6] m³\n", + " - `cond_detector.V`: [1e-7, 1e-6] m³\n", + "- Add the `Comparator` as objective, using the simulator as evaluator\n", + "- Define a callback function that compares simulation output with experimental data.\n", + "- Setup an optimizer, e.g. `U_NSGA3` and run the optimization" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "aeff1924", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + } + }, + "outputs": [], + "source": [ + "from CADETProcess.optimization import OptimizationProblem\n", + "optimization_problem = OptimizationProblem('System periphery')\n", + "\n", + "optimization_problem.add_evaluation_object(process)\n", + "\n", + "optimization_problem.add_variable(\n", + " name='Inlet valve volume', parameter_path='flow_sheet.inlet_valve.V',\n", + " lb=1e-7, ub=1e-6,\n", + " transform='auto'\n", + ")\n", + "\n", + "optimization_problem.add_variable(\n", + " name='Tubing axial dispersion', parameter_path='flow_sheet.tubing.axial_dispersion',\n", + " lb=1e-8, ub=1e-6,\n", + " transform='auto'\n", + ")\n", + "\n", + "optimization_problem.add_variable(\n", + " name='UV Detector volume', parameter_path='flow_sheet.uv_detector.V',\n", + " lb=1e-7, ub=1e-6,\n", + " transform='auto'\n", + ")\n", + "\n", + "optimization_problem.add_variable(\n", + " name='Conductivity detector volume', parameter_path='flow_sheet.cond_detector.V',\n", + " lb=1e-7, ub=1e-6,\n", + " transform='auto'\n", + ")\n", + "\n", + "optimization_problem.add_evaluator(simulator)\n", + "\n", + "optimization_problem.add_objective(\n", + " comparator,\n", + " n_objectives=comparator.n_metrics,\n", + " requires=[simulator]\n", + ")\n", + "\n", + "def callback(simulation_results, individual, evaluation_object, callbacks_dir='./'):\n", + " comparator.plot_comparison(\n", + " simulation_results,\n", + " file_name=f'{callbacks_dir}/{individual.id}_{evaluation_object}_comparison.png',\n", + " show=False\n", + " )\n", + "\n", + "\n", + "optimization_problem.add_callback(callback, requires=[simulator])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8f3fe14e", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + } + }, + "outputs": [], + "source": [ + "from CADETProcess.optimization import U_NSGA3\n", + "optimizer = U_NSGA3()\n", + "optimizer.n_cores = 8\n", + "optimizer.pop_size = 64\n", + "optimizer.n_max_gen = 16\n", + "\n", + "optimization_results = optimizer.optimize(\n", + " optimization_problem,\n", + " use_checkpoint=False\n", + ")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/05 Parameter Estimation/01_comparison_exercise.md b/05 Parameter Estimation/01_comparison_exercise.md deleted file mode 100644 index 585616b..0000000 --- a/05 Parameter Estimation/01_comparison_exercise.md +++ /dev/null @@ -1,309 +0,0 @@ ---- -jupytext: - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.16.2 -kernelspec: - display_name: Python 3 (ipykernel) - language: python - name: python3 ---- - -+++ {"editable": true, "slideshow": {"slide_type": ""}} - -# Comparison - Exercise - -For this exercise, consider the following flow sheet: - -```{figure} ./figures/comparator_uv_cond.png -``` - -To characterize the system periphery, a pore-penetrating tracer (Acetone) pulse was injected into the system without a column and UV and conductivity were measured. - -```{note} -To model the additional dispersion of the system, two `Cstr`s were introduced -``` - -+++ {"editable": true, "slideshow": {"slide_type": ""}} - -## Exercise 1: Comparator - -The (synthetic) experiment was repeated to account for system variability. The data was converted to concentrations in $mM$ and can be found in `./experimental_data`. - -**Task:** -- Import and plot the experimental data using the `ReferenceIO` class. -- Add the references to the `Comparator`. -- Add the `SSE` difference metric and compare with simulation results. -- Compare with other metrics. - -```{code-cell} ipython3 ---- -editable: true -slideshow: - slide_type: '' ---- -from CADETProcess.processModel import ComponentSystem -from CADETProcess.processModel import Inlet, Cstr, TubularReactor, Outlet -from CADETProcess.processModel import FlowSheet -from CADETProcess.processModel import Process -from CADETProcess.simulator import Cadet - -# Some Variables -Q_ml_min = 0.5 # ml/min -Q_m3_s = Q_ml_min/(60*1e6) -V_tracer = 50e-9 # m3 - -# Component System -component_system = ComponentSystem(['Acetone']) - -# Unit Operations -acetone = Inlet(component_system, name='acetone') -acetone.c = [131.75] - -water = Inlet(component_system, name='water') -water.c = [0] - -inlet_valve = Cstr(component_system, name='inlet_valve') -inlet_valve.V = 0.3e-6 -inlet_valve.c = [0] - -tubing = TubularReactor(component_system, name='tubing') -tubing.length = 0.85 -tubing.diameter = 0.75e-3 -tubing.axial_dispersion = 1e-7 -tubing.c = [0] - -uv_detector = Cstr(component_system, name='uv_detector') -uv_detector.V = 0.1e-6 -uv_detector.c = [0] - -cond_detector = Cstr(component_system, name='cond_detector') -cond_detector.V = 0.2e-6 -cond_detector.c = [0] - -outlet = Outlet(component_system, name='outlet') - -# Flow Sheet -fs = FlowSheet(component_system) -fs.add_unit(acetone) -fs.add_unit(water) -fs.add_unit(inlet_valve) -fs.add_unit(tubing) -fs.add_unit(uv_detector) -fs.add_unit(cond_detector) -fs.add_unit(outlet) - -fs.add_connection(acetone, inlet_valve) -fs.add_connection(water, inlet_valve) -fs.add_connection(inlet_valve, tubing) -fs.add_connection(tubing, uv_detector) -fs.add_connection(uv_detector, cond_detector) -fs.add_connection(cond_detector, outlet) - -# Process -process = Process(fs, 'Acetone_Pulse_no_column') -process.cycle_time = 500 - -process.add_event('pulse_acetone_on', 'flow_sheet.acetone.flow_rate', Q_m3_s, 0) -process.add_event('pulse_acetone_off', 'flow_sheet.acetone.flow_rate', 0, V_tracer/Q_m3_s) - -process.add_event('feed_water_on', 'flow_sheet.water.flow_rate', Q_m3_s, V_tracer/Q_m3_s) -process.add_event('feed_water_off', 'flow_sheet.water.flow_rate', 0, process.cycle_time) -``` - -```{code-cell} ipython3 ---- -editable: true -slideshow: - slide_type: '' ---- -simulator = Cadet() - -simulation_results = simulator.simulate(process) - -fig, ax = None, None -y_max = 30 -fig, ax = simulation_results.solution.inlet_valve.outlet.plot(fig=fig, ax=ax, y_max=y_max) -fig, ax = simulation_results.solution.tubing.outlet.plot(fig=fig, ax=ax, y_max=y_max) -fig, ax = simulation_results.solution.uv_detector.outlet.plot(fig=fig, ax=ax, y_max=y_max) -``` - -```{code-cell} ipython3 ---- -editable: true -slideshow: - slide_type: '' -tags: [solution] ---- -from CADETProcess.reference import ReferenceIO -import numpy as np - -data_uv_1 = np.loadtxt('./experimental_data/uv_detector_1.csv', delimiter=',') -time_uv_1 = data_uv_1[:, 0] -uv_1 = data_uv_1[:, 1] -reference_uv_1 = ReferenceIO('uv_1', time_uv_1, uv_1) -_ = reference_uv_1.plot() - -data_uv_2 = np.loadtxt('./experimental_data/uv_detector_2.csv', delimiter=',') -time_uv_2 = data_uv_2[:, 0] -uv_2 = data_uv_2[:, 1] -reference_uv_2 = ReferenceIO('uv_2', time_uv_2, uv_2) -_ = reference_uv_2.plot() - -data_cond_1 = np.loadtxt('./experimental_data/cond_detector_1.csv', delimiter=',') -time_cond_1 = data_cond_1[:, 0] -cond_1 = data_cond_1[:, 1] -reference_cond_1 = ReferenceIO('cond_1', time_cond_1, cond_1) -_ = reference_cond_1.plot() - -data_cond_2 = np.loadtxt('./experimental_data/cond_detector_2.csv', delimiter=',') -time_cond_2 = data_cond_2[:, 0] -cond_2 = data_cond_2[:, 1] -reference_cond_2 = ReferenceIO('cond_2', time_cond_2, cond_2) -_ = reference_cond_2.plot() -``` - -```{code-cell} ipython3 ---- -editable: true -slideshow: - slide_type: '' -tags: [solution] ---- -from CADETProcess.comparison import Comparator - -comparator = Comparator() - -comparator.add_reference(reference_uv_1) -comparator.add_reference(reference_uv_2) -comparator.add_reference(reference_cond_1) -comparator.add_reference(reference_cond_2) -``` - -```{code-cell} ipython3 ---- -editable: true -slideshow: - slide_type: '' -tags: [solution] ---- -comparator.add_difference_metric('Shape', reference_uv_1, 'uv_detector.outlet') -comparator.add_difference_metric('Shape', reference_uv_2, 'uv_detector.outlet') - -comparator.add_difference_metric('Shape', reference_cond_1, 'cond_detector.outlet') -comparator.add_difference_metric('Shape', reference_cond_2, 'cond_detector.outlet') -``` - -```{code-cell} ipython3 ---- -editable: true -slideshow: - slide_type: '' -tags: [solution] ---- -comparator.evaluate(simulation_results) -``` - -```{code-cell} ipython3 ---- -editable: true -slideshow: - slide_type: '' ---- -_ = comparator.plot_comparison(simulation_results) -``` - -+++ {"slideshow": {"slide_type": "fragment"}, "editable": true} - -```{note} -It's also possible to add multiple references, e.g. for triplicate experiments or for different sensors. -``` - -+++ {"editable": true, "slideshow": {"slide_type": ""}} - -## Exercise 2: Optimization - -**Task:** -- Setup optimization problem -- Add the process to the optimization problem and define the following optimization variables. - - `inlet_valve.V`: [1e-7, 1e-6] m³ - - `tubing.axial_dispersion` = [1e-8, 1e-6] m²/s - - `uv_detector.V`: [1e-7, 1e-6] m³ - - `cond_detector.V`: [1e-7, 1e-6] m³ -- Add the `Comparator` as objective, using the simulator as evaluator -- Define a callback function that compares simulation output with experimental data. -- Setup an optimizer, e.g. `U_NSGA3` and run the optimization - -```{code-cell} ipython3 ---- -editable: true -slideshow: - slide_type: '' ---- -from CADETProcess.optimization import OptimizationProblem -optimization_problem = OptimizationProblem('System periphery') - -optimization_problem.add_evaluation_object(process) - -optimization_problem.add_variable( - name='Inlet valve volume', parameter_path='flow_sheet.inlet_valve.V', - lb=1e-7, ub=1e-6, - transform='auto' -) - -optimization_problem.add_variable( - name='Tubing axial dispersion', parameter_path='flow_sheet.tubing.axial_dispersion', - lb=1e-8, ub=1e-6, - transform='auto' -) - -optimization_problem.add_variable( - name='UV Detector volume', parameter_path='flow_sheet.uv_detector.V', - lb=1e-7, ub=1e-6, - transform='auto' -) - -optimization_problem.add_variable( - name='Conductivity detector volume', parameter_path='flow_sheet.cond_detector.V', - lb=1e-7, ub=1e-6, - transform='auto' -) - -optimization_problem.add_evaluator(simulator) - -optimization_problem.add_objective( - comparator, - n_objectives=comparator.n_metrics, - requires=[simulator] -) - -def callback(simulation_results, individual, evaluation_object, callbacks_dir='./'): - comparator.plot_comparison( - simulation_results, - file_name=f'{callbacks_dir}/{individual.id}_{evaluation_object}_comparison.png', - show=False - ) - - -optimization_problem.add_callback(callback, requires=[simulator]) -``` - -```{code-cell} ipython3 ---- -editable: true -slideshow: - slide_type: '' ---- -from CADETProcess.optimization import U_NSGA3 -optimizer = U_NSGA3() -optimizer.n_cores = 8 -optimizer.pop_size = 64 -optimizer.n_max_gen = 16 - -optimization_results = optimizer.optimize( - optimization_problem, - use_checkpoint=False -) -``` diff --git a/05 Parameter Estimation/experimental_data/generate_data.ipynb b/05 Parameter Estimation/experimental_data/generate_data.ipynb index b1a0b2f..4cd8abd 100644 --- a/05 Parameter Estimation/experimental_data/generate_data.ipynb +++ b/05 Parameter Estimation/experimental_data/generate_data.ipynb @@ -2,26 +2,15 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, - "id": "b20b021f", + "execution_count": null, + "id": "ff9c4a54", "metadata": { "editable": true, "slideshow": { "slide_type": "" } }, - "outputs": [ - { - "data": { - "text/plain": [ - "Event(name=feed_water_off, parameter_path=flow_sheet.water.flow_rate, state=0, time=0.0, indices=[(slice(None, None, None),)])" - ] - }, - "execution_count": 1, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "from CADETProcess.processModel import ComponentSystem\n", "from CADETProcess.processModel import Inlet, Cstr, TubularReactor, Outlet\n", @@ -93,8 +82,8 @@ }, { "cell_type": "code", - "execution_count": 2, - "id": "fc3f288c", + "execution_count": null, + "id": "ff06f8a2", "metadata": {}, "outputs": [], "source": [ @@ -106,8 +95,8 @@ }, { "cell_type": "code", - "execution_count": 3, - "id": "da174b29", + "execution_count": null, + "id": "81449696", "metadata": {}, "outputs": [], "source": [ @@ -120,8 +109,8 @@ }, { "cell_type": "code", - "execution_count": 4, - "id": "e2cc7d30", + "execution_count": null, + "id": "f8c5ab0e", "metadata": {}, "outputs": [], "source": [ @@ -159,18 +148,6 @@ "display_name": "Python 3 (ipykernel)", "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.11.9" } }, "nbformat": 4, diff --git a/05 Parameter Estimation/experimental_data/generate_data.md b/05 Parameter Estimation/experimental_data/generate_data.md deleted file mode 100644 index db09874..0000000 --- a/05 Parameter Estimation/experimental_data/generate_data.md +++ /dev/null @@ -1,128 +0,0 @@ ---- -jupytext: - formats: ipynb,md:myst - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.16.2 -kernelspec: - display_name: Python 3 (ipykernel) - language: python - name: python3 ---- - -```{code-cell} ipython3 ---- -editable: true -slideshow: - slide_type: '' ---- -from CADETProcess.processModel import ComponentSystem -from CADETProcess.processModel import Inlet, Cstr, TubularReactor, Outlet -from CADETProcess.processModel import FlowSheet -from CADETProcess.processModel import Process - -# Some Variables -Q_ml_min = 0.5 # ml/min -Q_m3_s = Q_ml_min/(60*1e6) -V_tracer = 50e-9 # m3 - -# Component System -component_system = ComponentSystem(['Acetone']) - -# Unit Operations -acetone = Inlet(component_system, name='acetone') -acetone.c = [131.75] - -water = Inlet(component_system, name='water') -water.c = [0] - -inlet_valve = Cstr(component_system, name='inlet_valve') -inlet_valve.V = 0.5e-6 -inlet_valve.c = [0] - -tubing = TubularReactor(component_system, name='tubing') -tubing.length = 0.85 -tubing.diameter = 0.75e-3 -tubing.axial_dispersion = 2.5e-7 -tubing.c = [0] - -uv_detector = Cstr(component_system, name='uv_detector') -uv_detector.V = 0.1e-6 -uv_detector.c = [0] - -cond_detector = Cstr(component_system, name='cond_detector') -cond_detector.V = 0.2e-6 -cond_detector.c = [0] - -outlet = Outlet(component_system, name='outlet') - -# Flow Sheet -fs = FlowSheet(component_system) -fs.add_unit(acetone) -fs.add_unit(water) -fs.add_unit(inlet_valve) -fs.add_unit(tubing) -fs.add_unit(uv_detector) -fs.add_unit(cond_detector) -fs.add_unit(outlet) - -fs.add_connection(acetone, inlet_valve) -fs.add_connection(water, inlet_valve) -fs.add_connection(inlet_valve, tubing) -fs.add_connection(tubing, uv_detector) -fs.add_connection(uv_detector, cond_detector) -fs.add_connection(cond_detector, outlet) - -# Process -process = Process(fs, 'Acetone_Pulse_no_column') -process.cycle_time = 500 - -process.add_event('pulse_acetone_on', 'flow_sheet.acetone.flow_rate', Q_m3_s, 0) -process.add_event('pulse_acetone_off', 'flow_sheet.acetone.flow_rate', 0, V_tracer/Q_m3_s) - -process.add_event('feed_water_on', 'flow_sheet.water.flow_rate', Q_m3_s, V_tracer/Q_m3_s) -process.add_event('feed_water_off', 'flow_sheet.water.flow_rate', 0, process.cycle_time) -``` - -```{code-cell} ipython3 -from CADETProcess.simulator import Cadet -simulator = Cadet() - -simulation_results = simulator.simulate(process) -``` - -```{code-cell} ipython3 -import numpy as np - -def add_noise(solution, noise_percentage=2.5): - solution = (solution + (np.random.random(solution.shape) - 0.5) * noise_percentage / 100 * solution.max(axis=0)) - return solution -``` - -```{code-cell} ipython3 -target = 'uv_detector' -time = simulation_results.solution[target].outlet.time -solution = simulation_results.solution[target].outlet.solution - -solution_noisy_1 = add_noise(solution) -together_1 = np.hstack((time.reshape(-1, 1), solution_noisy_1)) -np.savetxt(f"{target}_1.csv", together_1, delimiter=",") - -solution_noisy_2 = add_noise(solution) -together_2 = np.hstack((time.reshape(-1, 1), solution_noisy_2)) -np.savetxt(f"{target}_2.csv", together_2, delimiter=",") - -target = 'cond_detector' -time = simulation_results.solution[target].outlet.time -solution = simulation_results.solution[target].outlet.solution - -solution_noisy_1 = add_noise(solution) -together_1 = np.hstack((time.reshape(-1, 1), solution_noisy_1)) -np.savetxt(f"{target}_1.csv", together_1, delimiter=",") - -solution_noisy_2 = add_noise(solution) -together_2 = np.hstack((time.reshape(-1, 1), solution_noisy_2)) -np.savetxt(f"{target}_2.csv", together_2, delimiter=",") -``` diff --git a/06 Process Optimization/01_fractionation.ipynb b/06 Process Optimization/01_fractionation.ipynb new file mode 100644 index 0000000..93a3492 --- /dev/null +++ b/06 Process Optimization/01_fractionation.ipynb @@ -0,0 +1,646 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "a0b132d9", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Product Fractionation\n", + "\n", + "- Key information for evaluating the separation performance of a chromatographic process: Amounts of the target components in the collected product fractions.\n", + "- Evaluation of chromatograms $c_{i,k}\\left(t\\right)$ at the outlet(s) of the process must be evaluated.\n", + "\n", + "$$\n", + "m_{i} = \\sum_{k=1}^{n_{chrom}} \\sum_{j=1}^{n_{frac, k}^{i}}\\int_{t_{start, j}}^{t_{end, j}} Q_k(t) \\cdot c_{i,k}(t) dt,\\\\\n", + "$$\n", + "\n", + "where $n_{frac, k}^{i}$ is the number of fractions considered for component $i$ in chromatogram $k$, and $n_{chrom}$ is the number of chromatograms that is evaluated." + ] + }, + { + "cell_type": "markdown", + "id": "7cb7f85d", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Key Performance Indicators (KPI)\n", + "\n", + "### Productivity\n", + "$$\n", + "PR_{i} = \\frac{m_i}{V_{solid} \\cdot \\Delta t_{cycle}},\\\\\n", + "$$\n", + "with $V_{solid}$: volume of stationary phase." + ] + }, + { + "cell_type": "markdown", + "id": "1bfb5d9d", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "### Recovery Yield\n", + "$$\n", + "Y_{i} = \\frac{m_i}{m_{feed, i}},\\\\\n", + "$$\n", + "with $m_{feed}$: injected amount of mixture." + ] + }, + { + "cell_type": "markdown", + "id": "b46e0886", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "### Eluent Consumption\n", + "$$\n", + "EC_{i} = \\frac{V_{solvent}}{m_i},\\\\\n", + "$$\n", + "with $V_{solvent}$: solvent used during a cycle." + ] + }, + { + "cell_type": "markdown", + "id": "d079335a", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "### Purity\n", + "\n", + "$$\n", + "PU_{i} = \\frac{m_{i}^{i}}{\\sum_{l=1}^{n_{comp}} m_{l}^{i}},\\\\\n", + "$$\n", + "where $n_{comp}$ is the number of mixture components and $m_{l}^{i}$ is the mass of component $l$ in target fraction $i$." + ] + }, + { + "cell_type": "markdown", + "id": "12eb18d3", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Fractionator\n", + "\n", + "In **CADET-Process**, the `fractionation` module provides methods to calculate these performance indicators." + ] + }, + { + "cell_type": "markdown", + "id": "71502ec4", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "The `Fractionator` allows slicing the solution and pool fractions for the individual components.\n", + "It enables evaluating multiple chromatograms at once and multiple fractions per component per chromatogram." + ] + }, + { + "cell_type": "markdown", + "id": "baef9a4d", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "The most basic strategy is to manually set all fractionation times manually.\n", + "To demonstrate the strategy, a process from the [examples collection](https://cadet-process.readthedocs.io/en/latest/examples/batch_elution/process.html) is used." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "53e0b599", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from examples.batch_elution.process import process" + ] + }, + { + "cell_type": "markdown", + "id": "31fbf559", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "To enable the calculation of the process parameters, it is necessary to specify which of the inlets should be considered for the feed and eluent consumption.\n", + "Moreover, the outlet(s) which are used for evaluation need to be defined.\n", + "\n", + "```\n", + "process.flow_sheet.add_feed_inlet('feed')\n", + "process.flow_sheet.add_eluent_inlet('eluent')\n", + "process.flow_sheet.add_chromatogram_outlet('outlet')\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "eadce805", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.simulator import Cadet\n", + "process_simulator = Cadet()\n", + "simulation_results = process_simulator.simulate(process)" + ] + }, + { + "cell_type": "markdown", + "id": "a67b7e6e", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "For reference, this is the chromatogram at the outlet that needs to be fractionated:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4126465b", + "metadata": { + "editable": true, + "render": { + "figure": { + "caption": "Concentration profile at column outlet.\n", + "name": "column_outlet" + } + }, + "slideshow": { + "slide_type": "" + }, + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "_ = simulation_results.solution.outlet.outlet.plot()" + ] + }, + { + "cell_type": "markdown", + "id": "30b4d272", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "After import, the `Fractionator` is instantiated with the simulation results." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "79e6eb7d", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.fractionation import Fractionator\n", + "fractionator = Fractionator(simulation_results)" + ] + }, + { + "cell_type": "markdown", + "id": "c2d4129b", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "To add a fractionation event, the following arguments need to be provided:\n", + "- `event_name`: Name of the event.\n", + "- `target`: Pool to which fraction is added. `-1` indicates waste.\n", + "- `time`: Time of the event\n", + "- `chromatogram`: Name of the chromatogram. Optional if only one outlet is set as `chromatogram_sink`.\n", + "\n", + "Here, component $A$ seems to have sufficient purity between $5 \\colon 00~min$ and $5 \\colon 45~min$ and component $B$ between $6 \\colon 30~min$ and $9 \\colon 00~min$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "579ff8cc", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "fractionator.add_fractionation_event('start_A', 0, 5*60, 'outlet')\n", + "fractionator.add_fractionation_event('end_A', -1, 5.75*60)\n", + "fractionator.add_fractionation_event('start_B', 1, 6.5*60)\n", + "fractionator.add_fractionation_event('end_B', -1, 9*60)" + ] + }, + { + "cell_type": "markdown", + "id": "51cee0ef", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "The `performance` object of the `Fractionator` contains the parameters:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3d9c241f", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "print(fractionator.performance)" + ] + }, + { + "cell_type": "markdown", + "id": "3f902891", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "With these fractionation times, the both component fractions reach a purity of $99.7~\\%$, and $97.2~\\%$ respectively.\n", + "The recovery yields are $65.2~\\%$ and $63.4~\\%$." + ] + }, + { + "cell_type": "markdown", + "id": "d71e3af0", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "The chromatogram can be plotted with the fraction times overlaid:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "aa173cda", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "_ = fractionator.plot_fraction_signal()" + ] + }, + { + "cell_type": "markdown", + "id": "1e008d01", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Optimization of Fractionation Times\n", + "- The `fractionation` module provides tools to automatically determines optimal cut times.\n", + "- By default, the mass of the components is maximized under purity constraints.\n", + "- Different purity requirements can be specified for each component" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a8d9e45d", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.fractionation import FractionationOptimizer\n", + "fractionation_optimizer = FractionationOptimizer()\n", + "fractionation_optimizer.optimizer.rhobeg = 1e-3 # more on that later!" + ] + }, + { + "cell_type": "markdown", + "id": "f147effb", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "To automatically optimize the fractionation times, pass the simulation results to the `optimize_fractionation` function." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "014be8f8", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "fractionator = fractionation_optimizer.optimize_fractionation(simulation_results, purity_required=[0.95, 0])" + ] + }, + { + "cell_type": "markdown", + "id": "878b2ede", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "The results are stored in a `Performance` object." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4ec9b0c2", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "print(fractionator.performance)" + ] + }, + { + "cell_type": "markdown", + "id": "27b3b90c", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "The chromatogram can also be plotted with the fraction times overlaid:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "80dfc312", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "_ = fractionator.plot_fraction_signal()" + ] + }, + { + "cell_type": "markdown", + "id": "c76a4a89", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "For comparison, this is the results if only the second component is relevant:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "78234b48", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "fractionator = fractionation_optimizer.optimize_fractionation(simulation_results, purity_required=[0, 0.95])\n", + "\n", + "print(fractionator.performance)\n", + "_ = fractionator.plot_fraction_signal()" + ] + }, + { + "cell_type": "markdown", + "id": "0636cd0b", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "But of course, also both components can be valuable.\n", + "Here, the required purity is also reduced to demonstrate that overlapping fractions are automatically avoided by internally introducing linear constraints." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5bf8f898", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "fractionator = fractionation_optimizer.optimize_fractionation(simulation_results, purity_required=[0.8, 0.8])\n", + "\n", + "print(fractionator.performance)\n", + "_ = fractionator.plot_fraction_signal()" + ] + }, + { + "cell_type": "markdown", + "id": "8abe414f", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Alternative Objectives\n", + "\n", + "- define function that that takes a `Performance` as an input.\n", + "- Here, also consider concentration of the fraction." + ] + }, + { + "cell_type": "markdown", + "id": "ca0f4574", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "```{note}\n", + "As previously mentioned, `COBYLA` only handles single objectives.\n", + "Hence, a `RankedPerformance` is used which transforms the `Performance` object by adding a weight $w_i$ to each component.\n", + "```\n", + "\n", + "$$\n", + "p = \\frac{\\sum_i^{n_{comp}}w_i \\cdot p_i}{\\sum_i^{n_{comp}}(w_i)}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "id": "b2fbee21", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "It is also important to remember that by convention, objectives are minimized.\n", + "Since in this example, the product of mass and concentration should be maximized, the value of the objective function is multiplied by $-1$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1fc08fce", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.performance import RankedPerformance\n", + "ranking = [1, 1]\n", + "\n", + "def alternative_objective(performance):\n", + " performance = RankedPerformance(performance, ranking)\n", + " return - performance.mass * performance.concentration\n", + "\n", + "fractionator = fractionation_optimizer.optimize_fractionation(\n", + " simulation_results, purity_required=[0.95, 0.95],\n", + " obj_fun=alternative_objective,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0d2a7c2f", + "metadata": { + "slideshow": { + "slide_type": "slide" + }, + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "print(fractionator.performance)\n", + "_ = fractionator.plot_fraction_signal()" + ] + }, + { + "cell_type": "markdown", + "id": "3d808ddd", + "metadata": { + "slideshow": { + "slide_type": "notes" + } + }, + "source": [ + "The resulting fractionation times show that in this case, it is advantageous to discard some slices of the peak in order not to dilute the overall product fraction." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/06 Process Optimization/01_fractionation.md b/06 Process Optimization/01_fractionation.md deleted file mode 100644 index 5b03423..0000000 --- a/06 Process Optimization/01_fractionation.md +++ /dev/null @@ -1,313 +0,0 @@ ---- -jupytext: - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.16.2 -kernelspec: - display_name: Python 3 (ipykernel) - language: python - name: python3 ---- - -+++ {"slideshow": {"slide_type": "slide"}, "editable": true} - -# Product Fractionation - -- Key information for evaluating the separation performance of a chromatographic process: Amounts of the target components in the collected product fractions. -- Evaluation of chromatograms $c_{i,k}\left(t\right)$ at the outlet(s) of the process must be evaluated. - -$$ -m_{i} = \sum_{k=1}^{n_{chrom}} \sum_{j=1}^{n_{frac, k}^{i}}\int_{t_{start, j}}^{t_{end, j}} Q_k(t) \cdot c_{i,k}(t) dt,\\ -$$ - -where $n_{frac, k}^{i}$ is the number of fractions considered for component $i$ in chromatogram $k$, and $n_{chrom}$ is the number of chromatograms that is evaluated. - -+++ {"slideshow": {"slide_type": "slide"}, "editable": true} - -## Key Performance Indicators (KPI) - -### Productivity -$$ -PR_{i} = \frac{m_i}{V_{solid} \cdot \Delta t_{cycle}},\\ -$$ -with $V_{solid}$: volume of stationary phase. - -+++ {"slideshow": {"slide_type": "fragment"}, "editable": true} - -### Recovery Yield -$$ -Y_{i} = \frac{m_i}{m_{feed, i}},\\ -$$ -with $m_{feed}$: injected amount of mixture. - -+++ {"slideshow": {"slide_type": "fragment"}, "editable": true} - -### Eluent Consumption -$$ -EC_{i} = \frac{V_{solvent}}{m_i},\\ -$$ -with $V_{solvent}$: solvent used during a cycle. - -+++ {"slideshow": {"slide_type": "fragment"}, "editable": true} - -### Purity - -$$ -PU_{i} = \frac{m_{i}^{i}}{\sum_{l=1}^{n_{comp}} m_{l}^{i}},\\ -$$ -where $n_{comp}$ is the number of mixture components and $m_{l}^{i}$ is the mass of component $l$ in target fraction $i$. - -+++ {"editable": true, "slideshow": {"slide_type": "slide"}} - -## Fractionator - -In **CADET-Process**, the `fractionation` module provides methods to calculate these performance indicators. - -+++ {"slideshow": {"slide_type": "fragment"}, "editable": true} - -The `Fractionator` allows slicing the solution and pool fractions for the individual components. -It enables evaluating multiple chromatograms at once and multiple fractions per component per chromatogram. - -+++ {"slideshow": {"slide_type": "fragment"}, "editable": true} - -The most basic strategy is to manually set all fractionation times manually. -To demonstrate the strategy, a process from the [examples collection](https://cadet-process.readthedocs.io/en/latest/examples/batch_elution/process.html) is used. - -```{code-cell} ipython3 ---- -editable: true -slideshow: - slide_type: '' -tags: [solution] ---- -from examples.batch_elution.process import process -``` - -+++ {"slideshow": {"slide_type": "fragment"}, "editable": true} - -To enable the calculation of the process parameters, it is necessary to specify which of the inlets should be considered for the feed and eluent consumption. -Moreover, the outlet(s) which are used for evaluation need to be defined. - -``` -process.flow_sheet.add_feed_inlet('feed') -process.flow_sheet.add_eluent_inlet('eluent') -process.flow_sheet.add_chromatogram_outlet('outlet') -``` - -```{code-cell} ipython3 ---- -editable: true -slideshow: - slide_type: '' -tags: [solution] ---- -from CADETProcess.simulator import Cadet -process_simulator = Cadet() -simulation_results = process_simulator.simulate(process) -``` - -+++ {"slideshow": {"slide_type": "slide"}, "editable": true} - -For reference, this is the chromatogram at the outlet that needs to be fractionated: - -```{code-cell} ipython3 ---- -editable: true -render: - figure: - caption: 'Concentration profile at column outlet. - - ' - name: column_outlet -slideshow: - slide_type: '' -tags: [solution] ---- -_ = simulation_results.solution.outlet.outlet.plot() -``` - -+++ {"slideshow": {"slide_type": "slide"}, "editable": true} - -After import, the `Fractionator` is instantiated with the simulation results. - -```{code-cell} ipython3 ---- -editable: true -slideshow: - slide_type: '' -tags: [solution] ---- -from CADETProcess.fractionation import Fractionator -fractionator = Fractionator(simulation_results) -``` - -+++ {"slideshow": {"slide_type": "fragment"}, "editable": true} - -To add a fractionation event, the following arguments need to be provided: -- `event_name`: Name of the event. -- `target`: Pool to which fraction is added. `-1` indicates waste. -- `time`: Time of the event -- `chromatogram`: Name of the chromatogram. Optional if only one outlet is set as `chromatogram_sink`. - -Here, component $A$ seems to have sufficient purity between $5 \colon 00~min$ and $5 \colon 45~min$ and component $B$ between $6 \colon 30~min$ and $9 \colon 00~min$. - -```{code-cell} ipython3 -:tags: [solution] - -fractionator.add_fractionation_event('start_A', 0, 5*60, 'outlet') -fractionator.add_fractionation_event('end_A', -1, 5.75*60) -fractionator.add_fractionation_event('start_B', 1, 6.5*60) -fractionator.add_fractionation_event('end_B', -1, 9*60) -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -The `performance` object of the `Fractionator` contains the parameters: - -```{code-cell} ipython3 -:tags: [solution] - -print(fractionator.performance) -``` - -+++ {"slideshow": {"slide_type": "fragment"}} - -With these fractionation times, the both component fractions reach a purity of $99.7~\%$, and $97.2~\%$ respectively. -The recovery yields are $65.2~\%$ and $63.4~\%$. - -+++ {"slideshow": {"slide_type": "slide"}} - -The chromatogram can be plotted with the fraction times overlaid: - -```{code-cell} ipython3 -:tags: [solution] - -_ = fractionator.plot_fraction_signal() -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -## Optimization of Fractionation Times -- The `fractionation` module provides tools to automatically determines optimal cut times. -- By default, the mass of the components is maximized under purity constraints. -- Different purity requirements can be specified for each component - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.fractionation import FractionationOptimizer -fractionation_optimizer = FractionationOptimizer() -fractionation_optimizer.optimizer.rhobeg = 1e-3 # more on that later! -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -To automatically optimize the fractionation times, pass the simulation results to the `optimize_fractionation` function. - -```{code-cell} ipython3 -:tags: [solution] - -fractionator = fractionation_optimizer.optimize_fractionation(simulation_results, purity_required=[0.95, 0]) -``` - -+++ {"slideshow": {"slide_type": "fragment"}} - -The results are stored in a `Performance` object. - -```{code-cell} ipython3 -:tags: [solution] - -print(fractionator.performance) -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -The chromatogram can also be plotted with the fraction times overlaid: - -```{code-cell} ipython3 -:tags: [solution] - -_ = fractionator.plot_fraction_signal() -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -For comparison, this is the results if only the second component is relevant: - -```{code-cell} ipython3 -:tags: [solution] - -fractionator = fractionation_optimizer.optimize_fractionation(simulation_results, purity_required=[0, 0.95]) - -print(fractionator.performance) -_ = fractionator.plot_fraction_signal() -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -But of course, also both components can be valuable. -Here, the required purity is also reduced to demonstrate that overlapping fractions are automatically avoided by internally introducing linear constraints. - -```{code-cell} ipython3 -:tags: [solution] - -fractionator = fractionation_optimizer.optimize_fractionation(simulation_results, purity_required=[0.8, 0.8]) - -print(fractionator.performance) -_ = fractionator.plot_fraction_signal() -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -## Alternative Objectives - -- define function that that takes a `Performance` as an input. -- Here, also consider concentration of the fraction. - -+++ {"slideshow": {"slide_type": "fragment"}} - -```{note} -As previously mentioned, `COBYLA` only handles single objectives. -Hence, a `RankedPerformance` is used which transforms the `Performance` object by adding a weight $w_i$ to each component. -``` - -$$ -p = \frac{\sum_i^{n_{comp}}w_i \cdot p_i}{\sum_i^{n_{comp}}(w_i)} -$$ - -+++ {"slideshow": {"slide_type": "fragment"}} - -It is also important to remember that by convention, objectives are minimized. -Since in this example, the product of mass and concentration should be maximized, the value of the objective function is multiplied by $-1$. - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.performance import RankedPerformance -ranking = [1, 1] - -def alternative_objective(performance): - performance = RankedPerformance(performance, ranking) - return - performance.mass * performance.concentration - -fractionator = fractionation_optimizer.optimize_fractionation( - simulation_results, purity_required=[0.95, 0.95], - obj_fun=alternative_objective, -) -``` - -```{code-cell} ipython3 ---- -slideshow: - slide_type: slide -tags: [solution] ---- -print(fractionator.performance) -_ = fractionator.plot_fraction_signal() -``` - -+++ {"slideshow": {"slide_type": "notes"}} - -The resulting fractionation times show that in this case, it is advantageous to discard some slices of the peak in order not to dilute the overall product fraction. diff --git a/06 Process Optimization/01_fractionation_exercise.ipynb b/06 Process Optimization/01_fractionation_exercise.ipynb new file mode 100644 index 0000000..788f868 --- /dev/null +++ b/06 Process Optimization/01_fractionation_exercise.ipynb @@ -0,0 +1,158 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "c28809a7", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + } + }, + "source": [ + "# Fractionation - Exercise\n", + "\n", + "Take the following process from [this example](https://cadet-process.readthedocs.io/en/stable/examples/load_wash_elute/lwe_flow_rate.html).\n", + "\n", + "- Simulate the process.\n", + "- Plot the outlet concentration. Use a secondary axis for the `Salt` component.\n", + "- Instantiate a `Fractionator` and manually set fractionation times to purify component `C`.\n", + "- Plot the results and analyse the `performance`, especially the purity.\n", + "- Exclude the `Salt` component from the fractionation and analyse the `performance` again.\n", + "- Use a `FractionationOptimizer` to automatically determine adequate cut times. Play around with different purity requirements and objecives." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9ba2a437", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + } + }, + "outputs": [], + "source": [ + "from examples.load_wash_elute.lwe_flow_rate import process" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a507ecf5", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.simulator import Cadet\n", + "\n", + "simulator = Cadet()\n", + "\n", + "simulation_results = simulator.simulate(process)\n", + "\n", + "from CADETProcess.plotting import SecondaryAxis\n", + "sec = SecondaryAxis()\n", + "sec.components = ['Salt']\n", + "sec.y_label = '$c_{salt}$'\n", + "\n", + "simulation_results.solution.outlet.inlet.plot(secondary_axis=sec)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6ca22e62", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.fractionation import Fractionator\n", + "fractionator = Fractionator(simulation_results)\n", + "\n", + "fractionator.add_fractionation_event('start_C', 2, 5*60)\n", + "fractionator.add_fractionation_event('end_C', -1, 8*60)\n", + "\n", + "fractionator.plot_fraction_signal(secondary_axis=sec)\n", + "fractionator.performance" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4180be80", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "fractionator = Fractionator(simulation_results, components=['A', 'B', 'C'])\n", + "\n", + "fractionator.add_fractionation_event('start_C', 0, 5*60)\n", + "fractionator.add_fractionation_event('end_C', -1, 8*60)\n", + "\n", + "fractionator.plot_fraction_signal()\n", + "fractionator.performance" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d1239b26", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.fractionation import FractionationOptimizer\n", + "fractionation_optimizer = FractionationOptimizer()\n", + "\n", + "fractionator = fractionation_optimizer.optimize_fractionation(\n", + " simulation_results,\n", + " components=['A', 'B', 'C'],\n", + " purity_required=[0, 0, 0.95]\n", + ")\n", + "print(fractionator.performance)\n", + "_ = fractionator.plot_fraction_signal()" + ] + } + ], + "metadata": { + "jupytext": { + "formats": "ipynb,md:myst" + }, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/06 Process Optimization/01_fractionation_exercise.md b/06 Process Optimization/01_fractionation_exercise.md deleted file mode 100644 index 9dbc2e3..0000000 --- a/06 Process Optimization/01_fractionation_exercise.md +++ /dev/null @@ -1,108 +0,0 @@ ---- -jupytext: - formats: ipynb,md:myst - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.16.2 -kernelspec: - display_name: Python 3 (ipykernel) - language: python - name: python3 ---- - -+++ {"editable": true, "slideshow": {"slide_type": ""}} - -# Fractionation - Exercise - -Take the following process from [this example](https://cadet-process.readthedocs.io/en/stable/examples/load_wash_elute/lwe_flow_rate.html). - -- Simulate the process. -- Plot the outlet concentration. Use a secondary axis for the `Salt` component. -- Instantiate a `Fractionator` and manually set fractionation times to purify component `C`. -- Plot the results and analyse the `performance`, especially the purity. -- Exclude the `Salt` component from the fractionation and analyse the `performance` again. -- Use a `FractionationOptimizer` to automatically determine adequate cut times. Play around with different purity requirements and objecives. - -```{code-cell} ipython3 ---- -editable: true -slideshow: - slide_type: '' ---- -from examples.load_wash_elute.lwe_flow_rate import process -``` - -```{code-cell} ipython3 ---- -editable: true -slideshow: - slide_type: '' -tags: [solution] ---- -from CADETProcess.simulator import Cadet - -simulator = Cadet() - -simulation_results = simulator.simulate(process) - -from CADETProcess.plotting import SecondaryAxis -sec = SecondaryAxis() -sec.components = ['Salt'] -sec.y_label = '$c_{salt}$' - -simulation_results.solution.outlet.inlet.plot(secondary_axis=sec) -``` - -```{code-cell} ipython3 ---- -editable: true -slideshow: - slide_type: '' -tags: [solution] ---- -from CADETProcess.fractionation import Fractionator -fractionator = Fractionator(simulation_results) - -fractionator.add_fractionation_event('start_C', 2, 5*60) -fractionator.add_fractionation_event('end_C', -1, 8*60) - -fractionator.plot_fraction_signal(secondary_axis=sec) -fractionator.performance -``` - -```{code-cell} ipython3 ---- -editable: true -slideshow: - slide_type: '' -tags: [solution] ---- -fractionator = Fractionator(simulation_results, components=['A', 'B', 'C']) - -fractionator.add_fractionation_event('start_C', 0, 5*60) -fractionator.add_fractionation_event('end_C', -1, 8*60) - -fractionator.plot_fraction_signal() -fractionator.performance -``` - -```{code-cell} ipython3 ---- -editable: true -slideshow: - slide_type: '' -tags: [solution] ---- -from CADETProcess.fractionation import FractionationOptimizer -fractionation_optimizer = FractionationOptimizer() - -fractionator = fractionation_optimizer.optimize_fractionation( - simulation_results, - components=['A', 'B', 'C'], - purity_required=[0, 0, 0.95] -) -print(fractionator.performance) -_ = fractionator.plot_fraction_signal() -``` diff --git a/06 Process Optimization/02_process_optimization.ipynb b/06 Process Optimization/02_process_optimization.ipynb new file mode 100644 index 0000000..dd6a51c --- /dev/null +++ b/06 Process Optimization/02_process_optimization.ipynb @@ -0,0 +1,338 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "e8abe93a", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + } + }, + "source": [ + "# Process Optimization" + ] + }, + { + "cell_type": "markdown", + "id": "1bfa10bd", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "First, an `OptimizationProblem` is instantiated." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e2b0b5f2", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + } + }, + "outputs": [], + "source": [ + "from CADETProcess.optimization import OptimizationProblem\n", + "optimization_problem = OptimizationProblem('batch_elution')" + ] + }, + { + "cell_type": "markdown", + "id": "7dbbcb90", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "The fully configured `Process` is imported from the examples and added to the `OptimizationProblem`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7f4599c9", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + } + }, + "outputs": [], + "source": [ + "from examples.batch_elution.process import process\n", + "optimization_problem.add_evaluation_object(process)" + ] + }, + { + "cell_type": "markdown", + "id": "01bc9192", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "In this case, the cycle time of the process, as well as the feed duration are to be optimized." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4f2e2ea2", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + } + }, + "outputs": [], + "source": [ + "optimization_problem.add_variable('cycle_time', lb=10, ub=600)\n", + "optimization_problem.add_variable('feed_duration.time', lb=10, ub=300)" + ] + }, + { + "cell_type": "markdown", + "id": "fa213dc6", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "To ensure that the feed duration is always shorter than the cycle time, a linear constraint is added." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "da2a851c", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + } + }, + "outputs": [], + "source": [ + "optimization_problem.add_linear_constraint(\n", + " ['feed_duration.time', 'cycle_time'], [1, -1]\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "baf1bf26", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "Now, the a simulator is configured and registered as evaluator.\n", + "We want to ensure that the simulator repeats the simulatation until cyclic stationarity is reached." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d528a4fc", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + } + }, + "outputs": [], + "source": [ + "from CADETProcess.simulator import Cadet\n", + "process_simulator = Cadet()\n", + "process_simulator.evaluate_stationarity = True\n", + "\n", + "optimization_problem.add_evaluator(process_simulator)" + ] + }, + { + "cell_type": "markdown", + "id": "886ed874", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "Here, the fractionation optimizer is configured and registered as another evaluator." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8f4e9369", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + } + }, + "outputs": [], + "source": [ + "from CADETProcess.fractionation import FractionationOptimizer\n", + "frac_opt = FractionationOptimizer()\n", + "\n", + "optimization_problem.add_evaluator(\n", + " frac_opt,\n", + " kwargs={\n", + " 'purity_required': [0.95, 0.95],\n", + " 'ignore_failed': False,\n", + " 'allow_empty_fractions': False,\n", + " }\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "5ba0efc9", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "Now, the objectives are defined." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a439396a", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + } + }, + "outputs": [], + "source": [ + "from CADETProcess.performance import PerformanceProduct\n", + "ranking = [1, 1]\n", + "performance = PerformanceProduct(ranking=ranking)\n", + "\n", + "optimization_problem.add_objective(\n", + " performance,\n", + " requires=[process_simulator, frac_opt],\n", + " minimize=False,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "d6425a6c", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "Add callback for post-processing" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2d5f2afc", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + } + }, + "outputs": [], + "source": [ + "def callback(fractionation, individual, evaluation_object, callbacks_dir):\n", + " fractionation.plot_fraction_signal(\n", + " file_name=f'{callbacks_dir}/{individual.id}_{evaluation_object}_fractionation.png',\n", + " show=False\n", + " )\n", + "\n", + "optimization_problem.add_callback(\n", + " callback, requires=[process_simulator, frac_opt]\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "91e9d9ba", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "Finally, the an optimizer is configured.\n", + "Again, we use `U_NSGA3`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e5a2679d", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + } + }, + "outputs": [], + "source": [ + "from CADETProcess.optimization import U_NSGA3\n", + "optimizer = U_NSGA3()\n", + "\n", + "optimizer.n_cores = 8\n", + "optimizer.pop_size = 32\n", + "optimizer.n_max_gen = 16" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ccc3f3e1", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + } + }, + "outputs": [], + "source": [ + "results = optimizer.optimize(\n", + " optimization_problem,\n", + " use_checkpoint=False,\n", + ")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/06 Process Optimization/02_process_optimization.md b/06 Process Optimization/02_process_optimization.md deleted file mode 100644 index 326283d..0000000 --- a/06 Process Optimization/02_process_optimization.md +++ /dev/null @@ -1,187 +0,0 @@ ---- -jupytext: - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.16.2 -kernelspec: - display_name: Python 3 (ipykernel) - language: python - name: python3 ---- - -+++ {"slideshow": {"slide_type": ""}, "editable": true} - -# Process Optimization - -+++ {"editable": true, "slideshow": {"slide_type": "fragment"}} - -First, an `OptimizationProblem` is instantiated. - -```{code-cell} ipython3 ---- -editable: true -slideshow: - slide_type: '' ---- -from CADETProcess.optimization import OptimizationProblem -optimization_problem = OptimizationProblem('batch_elution') -``` - -+++ {"editable": true, "slideshow": {"slide_type": "fragment"}} - -The fully configured `Process` is imported from the examples and added to the `OptimizationProblem`. - -```{code-cell} ipython3 ---- -editable: true -slideshow: - slide_type: '' ---- -from examples.batch_elution.process import process -optimization_problem.add_evaluation_object(process) -``` - -+++ {"editable": true, "slideshow": {"slide_type": "fragment"}} - -In this case, the cycle time of the process, as well as the feed duration are to be optimized. - -```{code-cell} ipython3 ---- -editable: true -slideshow: - slide_type: '' ---- -optimization_problem.add_variable('cycle_time', lb=10, ub=600) -optimization_problem.add_variable('feed_duration.time', lb=10, ub=300) -``` - -+++ {"editable": true, "slideshow": {"slide_type": "fragment"}} - -To ensure that the feed duration is always shorter than the cycle time, a linear constraint is added. - -```{code-cell} ipython3 ---- -editable: true -slideshow: - slide_type: '' ---- -optimization_problem.add_linear_constraint( - ['feed_duration.time', 'cycle_time'], [1, -1] -) -``` - -+++ {"editable": true, "slideshow": {"slide_type": "fragment"}} - -Now, the a simulator is configured and registered as evaluator. -We want to ensure that the simulator repeats the simulatation until cyclic stationarity is reached. - -```{code-cell} ipython3 ---- -editable: true -slideshow: - slide_type: '' ---- -from CADETProcess.simulator import Cadet -process_simulator = Cadet() -process_simulator.evaluate_stationarity = True - -optimization_problem.add_evaluator(process_simulator) -``` - -+++ {"editable": true, "slideshow": {"slide_type": "fragment"}} - -Here, the fractionation optimizer is configured and registered as another evaluator. - -```{code-cell} ipython3 ---- -editable: true -slideshow: - slide_type: '' ---- -from CADETProcess.fractionation import FractionationOptimizer -frac_opt = FractionationOptimizer() - -optimization_problem.add_evaluator( - frac_opt, - kwargs={ - 'purity_required': [0.95, 0.95], - 'ignore_failed': False, - 'allow_empty_fractions': False, - } -) -``` - -+++ {"editable": true, "slideshow": {"slide_type": "fragment"}} - -Now, the objectives are defined. - -```{code-cell} ipython3 ---- -editable: true -slideshow: - slide_type: '' ---- -from CADETProcess.performance import PerformanceProduct -ranking = [1, 1] -performance = PerformanceProduct(ranking=ranking) - -optimization_problem.add_objective( - performance, - requires=[process_simulator, frac_opt], - minimize=False, -) -``` - -+++ {"editable": true, "slideshow": {"slide_type": "fragment"}} - -Add callback for post-processing - -```{code-cell} ipython3 ---- -editable: true -slideshow: - slide_type: '' ---- -def callback(fractionation, individual, evaluation_object, callbacks_dir): - fractionation.plot_fraction_signal( - file_name=f'{callbacks_dir}/{individual.id}_{evaluation_object}_fractionation.png', - show=False - ) - -optimization_problem.add_callback( - callback, requires=[process_simulator, frac_opt] -) -``` - -+++ {"editable": true, "slideshow": {"slide_type": "fragment"}} - -Finally, the an optimizer is configured. -Again, we use `U_NSGA3`. - -```{code-cell} ipython3 ---- -editable: true -slideshow: - slide_type: '' ---- -from CADETProcess.optimization import U_NSGA3 -optimizer = U_NSGA3() - -optimizer.n_cores = 8 -optimizer.pop_size = 32 -optimizer.n_max_gen = 16 -``` - -```{code-cell} ipython3 ---- -editable: true -slideshow: - slide_type: '' ---- -results = optimizer.optimize( - optimization_problem, - use_checkpoint=False, -) -``` diff --git a/99 Misc/Yamamoto's Method/yamamoto.ipynb b/99 Misc/Yamamoto's Method/yamamoto.ipynb new file mode 100644 index 0000000..ba92501 --- /dev/null +++ b/99 Misc/Yamamoto's Method/yamamoto.ipynb @@ -0,0 +1,301 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "566fd008", + "metadata": { + "slideshow": { + "slide_type": "slide" + }, + "user_expressions": [] + }, + "source": [ + "# Yamamoto's Method\n", + "\n", + "Yamamoto's method is a semi-empirical method used for estimating the characteristic charge and equilibrium constant in the stoichiometric displacement model or in the linear region of the steric mass-action model.\n", + "\n", + "To determine the parameters, the salt concentration at which a peak elutes $I_R$ for various gradient slopes is plotted over the normalized gradient slope.\n", + "\n", + "$log(GH) = (\\nu+1) \\cdot log(I_R) - log(K_{eq} \\cdot \\lambda^\\nu \\cdot (\\nu+1))$\n", + "\n", + "The values for $GH$ and $c_{s,peak}$ are retrieved from the linear gradient experiments and the values for $\\nu$ and $K_{eq}$ are fitted to the data.\n", + "The value of the characteristic charge and the equilibration constant can be used further to predict retention times." + ] + }, + { + "cell_type": "markdown", + "id": "959a5466", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Load Experimental Data\n", + "First, the experimental data needs to be read.\n", + "For this purpose, a `GradientExperiment` class is provided which takes the following arguments:\n", + "- time\n", + "- salt concentration\n", + "- protein concentration (multiple proteins are supported)\n", + "- gradient volume\n", + "\n", + "For the Yamamoto method, at least two experiments are needed for the parameter estiamtion (linear regression).\n", + "However a third experiment is adviced for validation purposes.\n", + "\n", + "For this tutorial, four experiments are used with two different protein sample.\n", + "The data can be found in `./experiments/single_protein`." + ] + }, + { + "cell_type": "markdown", + "id": "c445f0e4", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Note on Experiments\n", + "\n", + "It is important to consider the dead volume between the UV sensor and the conductivity sensor in the experimental system.\n", + "If this is not accounted for, the time offset will lead to inacurate results.\n", + "\n", + "In practice, two tracer experiments are required.\n", + "One tracer experiment will produce a signal at the UV sensor, followed by another tracer detected by the conductivity sensor.\n", + "For example, Acetone could be used for the UV and NaCl for the conductivity measurement.\n", + "By subtracting the retention time of the system from the time of salt pulse, the time offset between the two sensors can be determined.\n", + "\n", + "```{figure} ./resources/acetone_no_column.png\n", + "```\n", + "\n", + "```{figure} ./resources/salt_no_column.png\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dee0961e", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "import numpy as np\n", + "from CADETProcess.tools.yamamoto import GradientExperiment\n", + "def create_experiment(file_name, gradient_volume):\n", + " \"\"\"CSV should have format of [time, salt, protein].\"\"\"\n", + "\n", + " data = np.loadtxt(file_name, delimiter=',')\n", + "\n", + " time = data[:, 0]\n", + " c_salt = data[:, 1]\n", + " c_protein = data[:, 2:]\n", + "\n", + " return GradientExperiment(time, c_salt, c_protein, gradient_volume)\n", + "\n", + "\n", + "experiment_1 = create_experiment('./experiments/single_protein/18.8mL.csv', 18.8e-6)\n", + "experiment_2 = create_experiment('./experiments/single_protein/37.6mL.csv', 37.6e-6)\n", + "experiment_3 = create_experiment('./experiments/single_protein/56.4mL.csv', 56.4e-6)\n", + "experiment_4 = create_experiment('./experiments/single_protein/75.2mL.csv', 75.2e-6)\n", + "\n", + "experiments = [experiment_1, experiment_2, experiment_3, experiment_4]" + ] + }, + { + "cell_type": "markdown", + "id": "3a5e7228", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "To visualize the experiments, a plot method is provided." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e3961060", + "metadata": {}, + "outputs": [], + "source": [ + "experiment_1.plot()" + ] + }, + { + "cell_type": "markdown", + "id": "db609fd8", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "The experiments can also be plotted together." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7f80cab1", + "metadata": {}, + "outputs": [], + "source": [ + "from CADETProcess.tools.yamamoto import plot_experiments\n", + "\n", + "plot_experiments(experiments)" + ] + }, + { + "cell_type": "markdown", + "id": "224320c2", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Configure System\n", + "\n", + "To run Yamamoto's method, initialize a `ComponentSystem`, a column (any model will work), and a `StericMassAction` binding model.\n", + "\n", + "For the binding model, only the `capacity` is required.\n", + "\n", + "Consider the following parameters:\n", + "- length: $0.1~m$\n", + "- diameter: $7.7~mm$\n", + "- bed porosity: $0.36$\n", + "- particle radius: $34 \\cdot 10^{-6}~m$\n", + "- particle porosity: $0.85$" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ad6e760d", + "metadata": {}, + "outputs": [], + "source": [ + "from CADETProcess.processModel import ComponentSystem\n", + "component_system = ComponentSystem(['Salt', 'A'])\n", + "\n", + "from CADETProcess.processModel import StericMassAction\n", + "binding_model = StericMassAction(component_system)\n", + "binding_model.adsorption_rate = [1, 1]\n", + "binding_model.desorption_rate = [1, 1]\n", + "binding_model.capacity = 4.7 * 175" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "27595c7b", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import LumpedRateModelWithPores\n", + "column = LumpedRateModelWithPores(component_system, 'column')\n", + "column.binding_model = binding_model\n", + "column.length = 0.1\n", + "column.diameter = 0.0077\n", + "column.bed_porosity = 0.36\n", + "column.particle_radius = 34e-6\n", + "column.particle_porosity = 0.85\n", + "\n", + "column.axial_dispersion = 1.5e-6\n", + "column.film_diffusion = [2e-6, 5e-7]" + ] + }, + { + "cell_type": "markdown", + "id": "3677b395", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Fit Parameters\n", + "\n", + "To fit the parameters, pass the experiments and the column to the `fit_parameters` method which returns a `YamamotoResults` object.\n", + "This contains the results and provides a plot method to visualize the results." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b35f0a48", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.tools.yamamoto import fit_parameters\n", + "yamamoto_results = fit_parameters(experiments, column)\n", + "\n", + "print(yamamoto_results.characteristic_charge)\n", + "print(yamamoto_results.k_eq)\n", + "\n", + "yamamoto_results.plot()" + ] + }, + { + "cell_type": "markdown", + "id": "9d2e2db0", + "metadata": { + "user_expressions": [] + }, + "source": [ + "## Bonus: Compare with simulation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a8ba8ac3", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "id": "704b38af", + "metadata": { + "user_expressions": [] + }, + "source": [ + "Besides the visualization of the fitted parameter from the experiments, there is also the option to use the characteristic charge $\\nu$ and equilibrium constant $K_{eq}$ in a load wash elution simulation with the steric mass action isotherm. The structure of the model is quite simple as depicted in the figure below. The inlet will be used to modify the concentration in the column. The column is modelled with a general rate model.\n", + "\n", + "![image.png](attachment:329c1a6b-6642-4726-bd59-592ff48c1831.png)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7012e784", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/99 Misc/Yamamoto's Method/yamamoto.md b/99 Misc/Yamamoto's Method/yamamoto.md deleted file mode 100644 index 9d83513..0000000 --- a/99 Misc/Yamamoto's Method/yamamoto.md +++ /dev/null @@ -1,181 +0,0 @@ ---- -jupytext: - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.15.2 -kernelspec: - display_name: Python 3 (ipykernel) - language: python - name: python3 ---- - -+++ {"user_expressions": [], "slideshow": {"slide_type": "slide"}} - -# Yamamoto's Method - -Yamamoto's method is a semi-empirical method used for estimating the characteristic charge and equilibrium constant in the stoichiometric displacement model or in the linear region of the steric mass-action model. - -To determine the parameters, the salt concentration at which a peak elutes $I_R$ for various gradient slopes is plotted over the normalized gradient slope. - -$log(GH) = (\nu+1) \cdot log(I_R) - log(K_{eq} \cdot \lambda^\nu \cdot (\nu+1))$ - -The values for $GH$ and $c_{s,peak}$ are retrieved from the linear gradient experiments and the values for $\nu$ and $K_{eq}$ are fitted to the data. -The value of the characteristic charge and the equilibration constant can be used further to predict retention times. - -+++ {"slideshow": {"slide_type": "slide"}} - -## Load Experimental Data -First, the experimental data needs to be read. -For this purpose, a `GradientExperiment` class is provided which takes the following arguments: -- time -- salt concentration -- protein concentration (multiple proteins are supported) -- gradient volume - -For the Yamamoto method, at least two experiments are needed for the parameter estiamtion (linear regression). -However a third experiment is adviced for validation purposes. - -For this tutorial, four experiments are used with two different protein sample. -The data can be found in `./experiments/single_protein`. - -+++ {"slideshow": {"slide_type": "slide"}} - -### Note on Experiments - -It is important to consider the dead volume between the UV sensor and the conductivity sensor in the experimental system. -If this is not accounted for, the time offset will lead to inacurate results. - -In practice, two tracer experiments are required. -One tracer experiment will produce a signal at the UV sensor, followed by another tracer detected by the conductivity sensor. -For example, Acetone could be used for the UV and NaCl for the conductivity measurement. -By subtracting the retention time of the system from the time of salt pulse, the time offset between the two sensors can be determined. - -```{figure} ./resources/acetone_no_column.png -``` - -```{figure} ./resources/salt_no_column.png -``` - -```{code-cell} ipython3 -:tags: [solution] - -import numpy as np -from CADETProcess.tools.yamamoto import GradientExperiment -def create_experiment(file_name, gradient_volume): - """CSV should have format of [time, salt, protein].""" - - data = np.loadtxt(file_name, delimiter=',') - - time = data[:, 0] - c_salt = data[:, 1] - c_protein = data[:, 2:] - - return GradientExperiment(time, c_salt, c_protein, gradient_volume) - - -experiment_1 = create_experiment('./experiments/single_protein/18.8mL.csv', 18.8e-6) -experiment_2 = create_experiment('./experiments/single_protein/37.6mL.csv', 37.6e-6) -experiment_3 = create_experiment('./experiments/single_protein/56.4mL.csv', 56.4e-6) -experiment_4 = create_experiment('./experiments/single_protein/75.2mL.csv', 75.2e-6) - -experiments = [experiment_1, experiment_2, experiment_3, experiment_4] -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -To visualize the experiments, a plot method is provided. - -```{code-cell} ipython3 -experiment_1.plot() -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -The experiments can also be plotted together. - -```{code-cell} ipython3 -from CADETProcess.tools.yamamoto import plot_experiments - -plot_experiments(experiments) -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -## Configure System - -To run Yamamoto's method, initialize a `ComponentSystem`, a column (any model will work), and a `StericMassAction` binding model. - -For the binding model, only the `capacity` is required. - -Consider the following parameters: -- length: $0.1~m$ -- diameter: $7.7~mm$ -- bed porosity: $0.36$ -- particle radius: $34 \cdot 10^{-6}~m$ -- particle porosity: $0.85$ - -```{code-cell} ipython3 -from CADETProcess.processModel import ComponentSystem -component_system = ComponentSystem(['Salt', 'A']) - -from CADETProcess.processModel import StericMassAction -binding_model = StericMassAction(component_system) -binding_model.adsorption_rate = [1, 1] -binding_model.desorption_rate = [1, 1] -binding_model.capacity = 4.7 * 175 -``` - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.processModel import LumpedRateModelWithPores -column = LumpedRateModelWithPores(component_system, 'column') -column.binding_model = binding_model -column.length = 0.1 -column.diameter = 0.0077 -column.bed_porosity = 0.36 -column.particle_radius = 34e-6 -column.particle_porosity = 0.85 - -column.axial_dispersion = 1.5e-6 -column.film_diffusion = [2e-6, 5e-7] -``` - -+++ {"slideshow": {"slide_type": "slide"}} - -## Fit Parameters - -To fit the parameters, pass the experiments and the column to the `fit_parameters` method which returns a `YamamotoResults` object. -This contains the results and provides a plot method to visualize the results. - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.tools.yamamoto import fit_parameters -yamamoto_results = fit_parameters(experiments, column) - -print(yamamoto_results.characteristic_charge) -print(yamamoto_results.k_eq) - -yamamoto_results.plot() -``` - -+++ {"user_expressions": []} - -## Bonus: Compare with simulation - -```{code-cell} ipython3 - -``` - -+++ {"user_expressions": []} - -Besides the visualization of the fitted parameter from the experiments, there is also the option to use the characteristic charge $\nu$ and equilibrium constant $K_{eq}$ in a load wash elution simulation with the steric mass action isotherm. The structure of the model is quite simple as depicted in the figure below. The inlet will be used to modify the concentration in the column. The column is modelled with a general rate model. - -![image.png](attachment:329c1a6b-6642-4726-bd59-592ff48c1831.png) - -```{code-cell} ipython3 - -``` diff --git a/99 Misc/Yamamoto's Method/yamamoto_exercise.ipynb b/99 Misc/Yamamoto's Method/yamamoto_exercise.ipynb new file mode 100644 index 0000000..6f58c09 --- /dev/null +++ b/99 Misc/Yamamoto's Method/yamamoto_exercise.ipynb @@ -0,0 +1,439 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "81417241", + "metadata": {}, + "source": [ + "## Exercise Yamamoto Method\n", + "\n", + "In this exercise the Yamamoto method will be applied and used as model parameter for the linear region of the steric mass action isotherm." + ] + }, + { + "cell_type": "markdown", + "id": "e538e1c0", + "metadata": {}, + "source": [ + "Import the data given in `./experiments/three_proteins` using the following helper function.\n", + "Then, visualize the data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7d391f60", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "import numpy as np\n", + "from CADETProcess.tools.yamamoto import GradientExperiment\n", + "def create_experiment(file_name, gradient_volume):\n", + " \"\"\"CSV should have format of [time, salt, protein].\"\"\"\n", + "\n", + " data = np.loadtxt(file_name, delimiter=',')\n", + "\n", + " time = data[:, 0]\n", + " c_salt = data[:, 1]\n", + " c_protein = data[:, 2:]\n", + "\n", + " return GradientExperiment(time, c_salt, c_protein, gradient_volume)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ffe06898", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "experiment_1 = create_experiment('./experiments/three_proteins/18.8mL.csv', 18.8e-6)\n", + "experiment_2 = create_experiment('./experiments/three_proteins/37.6mL.csv', 37.6e-6)\n", + "experiment_3 = create_experiment('./experiments/three_proteins/56.4mL.csv', 56.4e-6)\n", + "experiment_4 = create_experiment('./experiments/three_proteins/75.2mL.csv', 75.2e-6)\n", + "\n", + "experiments = [experiment_1, experiment_2, experiment_3, experiment_4]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2b574440", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.tools.yamamoto import plot_experiments\n", + "\n", + "for experiment in experiments:\n", + " experiment.plot()" + ] + }, + { + "cell_type": "markdown", + "id": "756b54b8", + "metadata": {}, + "source": [ + "Define the parameters given in the table below as variables.\n", + "\n", + "Consider the following parameters:\n", + "- length: $0.1~m$\n", + "- diameter: $7.7~mm$\n", + "- bed porosity: $0.36$\n", + "- particle radius: $34 \\cdot 10^{-6}~m$\n", + "- particle porosity: $0.85$" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b0e83fbf", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import ComponentSystem\n", + "component_system = ComponentSystem(['Salt', 'A', 'B', 'C'])\n", + "\n", + "from CADETProcess.processModel import StericMassAction\n", + "binding_model = StericMassAction(component_system)\n", + "binding_model.adsorption_rate = [1, 1, 1, 1]\n", + "binding_model.desorption_rate = [1, 1, 1, 1]\n", + "binding_model.capacity = 822.5\n", + "binding_model.steric_factor = [0, 0, 0, 0]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "575f847b", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.processModel import LumpedRateModelWithPores\n", + "column = LumpedRateModelWithPores(component_system, 'column')\n", + "column.binding_model = binding_model\n", + "column.length = 0.1\n", + "column.diameter = 0.0077\n", + "column.bed_porosity = 0.36\n", + "column.particle_radius = 34e-6\n", + "column.particle_porosity = 0.85\n", + "\n", + "column.axial_dispersion = 1.5e-6\n", + "column.film_diffusion = [2e-6, 5e-7, 5e-7, 5e-7]" + ] + }, + { + "cell_type": "markdown", + "id": "62921050", + "metadata": {}, + "source": [ + "Apply the Yamamoto method and save the return of the method in a variable. Print the characteristic charge and the equilibrium constant for all proteins in the system." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f0c13dea", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "from CADETProcess.tools.yamamoto import fit_parameters\n", + "yamamoto_results = fit_parameters(experiments, column)\n", + "\n", + "print(yamamoto_results.characteristic_charge)\n", + "print(yamamoto_results.k_eq)" + ] + }, + { + "cell_type": "markdown", + "id": "987f386f", + "metadata": {}, + "source": [ + "Visualize the results of the parameter estimation and check if the results are a good estimate of the experiments." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d22b48b0", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "yamamoto_results.plot()" + ] + }, + { + "cell_type": "markdown", + "id": "caf6ab54", + "metadata": {}, + "source": [ + "It can be recognized that at least one experiment is not in a good alignment with the found parameter.\n", + "Check the output of the parameter estimation and decide which experiment to neglect in the estimation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "09a7913e", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "print(f'logarithm of peak salt concentration for Protein 1: {yamamoto_results.log_c_salt_at_max_M[:, 0]}')\n", + "print(f'logarithm of peak salt concentration for Protein 2: {yamamoto_results.log_c_salt_at_max_M[:, 1]}')\n", + "print(f'logarithm of peak salt concentration for Protein 3: {yamamoto_results.log_c_salt_at_max_M[:, 2]}')\n", + "print(f'equilibrium constant: {yamamoto_results.k_eq}')" + ] + }, + { + "cell_type": "markdown", + "id": "ae92b154", + "metadata": {}, + "source": [ + "Viewing the data, the third entry related to the third experiment does not fit into the ascending sequence for the peak salt concentration. Remove the third experiment from the parameter estimation and redo the estimation. Also, visualize the results." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a37103dd", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "experiments_adapted = [experiment_1, experiment_2, experiment_4]\n", + "yamamoto_results_adapted = fit_parameters(experiments_adapted, column)\n", + "\n", + "print(yamamoto_results_adapted.characteristic_charge)\n", + "print(yamamoto_results_adapted.k_eq)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0a4d61f3", + "metadata": { + "tags": [ + "solution" + ] + }, + "outputs": [], + "source": [ + "yamamoto_results_adapted.plot()" + ] + }, + { + "cell_type": "markdown", + "id": "b128c6c6", + "metadata": {}, + "source": [ + "# Bonus Task\n", + "\n", + "Try to build a simple model which reproduces the experimental results.\n", + "The model should consist of an `Inlet`, the column model and an `Outlet`.\n", + "Use the values specified in the tables above.\n", + "\n", + "Keep in mind the following steps for creating a model:\n", + "1. Specify missing UnitOperations\n", + "1. Construct the flow sheet\n", + "1. Construct the process\n", + "1. Simulation and plotting\n", + "1. Comparison to experiment" + ] + }, + { + "cell_type": "markdown", + "id": "5101ad3a", + "metadata": {}, + "source": [ + "### 1. Specify missing UnitOperations" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b3b0a85f", + "metadata": {}, + "outputs": [], + "source": [ + "from CADETProcess.processModel import Inlet, Outlet\n", + "\n", + "inlet = Inlet(component_system, name='inlet')\n", + "inlet.flow_rate = 8.33e-09\n", + "\n", + "outlet = Outlet(component_system, name='outlet')" + ] + }, + { + "cell_type": "markdown", + "id": "7d7a69cd", + "metadata": {}, + "source": [ + "### 2. Construct the flow sheet" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f947664a", + "metadata": {}, + "outputs": [], + "source": [ + "from CADETProcess.processModel import FlowSheet\n", + "\n", + "flow_sheet = FlowSheet(component_system)\n", + "flow_sheet.add_unit(inlet)\n", + "flow_sheet.add_unit(column)\n", + "flow_sheet.add_unit(outlet)\n", + "\n", + "flow_sheet.add_connection(inlet, column)\n", + "flow_sheet.add_connection(column, outlet)" + ] + }, + { + "cell_type": "markdown", + "id": "d2b0b86a", + "metadata": {}, + "source": [ + "### 3. Construct the process" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "03dad42b", + "metadata": {}, + "outputs": [], + "source": [ + "from CADETProcess.processModel import Process\n", + "\n", + "flow_rate = 8.33e-09\n", + "sample_volume = 50e-9\n", + "wash_volume = 15e-6\n", + "gradient_volume =18.8e-6\n", + "strip_volume = 10e-6\n", + "\n", + "wash_start = sample_volume/flow_rate\n", + "gradient_start = sample_volume/flow_rate + wash_volume/flow_rate\n", + "strip_start = sample_volume/flow_rate + wash_volume/flow_rate + gradient_volume/flow_rate\n", + "t_cycle = sample_volume/flow_rate + wash_volume/flow_rate + gradient_volume/flow_rate + strip_volume/flow_rate\n", + "\n", + "slope = ((experiment_1.c_salt_end-experiment_1.c_salt_start)/(gradient_volume/flow_rate))\n", + "\n", + "process = Process(flow_sheet, 'LWE_Lysozyme_18_8mL')\n", + "process.cycle_time = t_cycle\n", + "\n", + "process.add_event('load', 'flow_sheet.inlet.c', [experiment_1.c_salt_start, 0.2, 0.24, 0.16], 0)\n", + "process.add_event('wash', 'flow_sheet.inlet.c', [experiment_1.c_salt_start, 0, 0, 0], wash_start)\n", + "process.add_event(\n", + " 'grad_start',\n", + " 'flow_sheet.inlet.c',\n", + " [[experiment_1.c_salt_start, slope], [0, 0], [0, 0], [0, 0]],\n", + " gradient_start\n", + ")\n", + "process.add_event('strip','flow_sheet.inlet.c', [experiment_1.c_salt_end, 0, 0, 0], strip_start)" + ] + }, + { + "cell_type": "markdown", + "id": "48f775ad", + "metadata": {}, + "source": [ + "### 7. Simulation and plotting" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5635058b", + "metadata": {}, + "outputs": [], + "source": [ + "from CADETProcess.simulator import Cadet\n", + "\n", + "simulator = Cadet()\n", + "sim_res = simulator.simulate (process)\n", + "\n", + "from CADETProcess.plotting import SecondaryAxis\n", + "sec = SecondaryAxis()\n", + "sec.components = [\"Salt\"]\n", + "sec.y_label = '$c_{Salt}$'\n", + "_ = sim_res.solution.column.outlet.plot(secondary_axis=sec)" + ] + }, + { + "cell_type": "markdown", + "id": "6682bac2", + "metadata": {}, + "source": [ + "### 8. Comparison to experiment" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5c1af6c2", + "metadata": {}, + "outputs": [], + "source": [ + "from CADETProcess.comparison import Comparator, ReferenceIO\n", + "\n", + "comparator = Comparator()\n", + "for i_p in range(experiment_1.n_proteins):\n", + " Ref_name = 'Protein_'+str(i_p)\n", + " comparator.add_reference(ReferenceIO(\n", + " Ref_name, experiment_1.time, experiment_1.c_protein[:,i_p]\n", + " )\n", + " )\n", + " comparator.add_difference_metric(\n", + " 'SSE', ReferenceIO(Ref_name, experiment_1.time, experiment_1.c_protein[:, i_p]),\n", + " 'outlet.outlet', components=component_system.labels[i_p+1]\n", + " )\n", + "\n", + "comparator.plot_comparison(sim_res, plot_individual=True)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/99 Misc/Yamamoto's Method/yamamoto_exercise.md b/99 Misc/Yamamoto's Method/yamamoto_exercise.md deleted file mode 100644 index dafedd0..0000000 --- a/99 Misc/Yamamoto's Method/yamamoto_exercise.md +++ /dev/null @@ -1,254 +0,0 @@ ---- -jupytext: - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.15.2 -kernelspec: - display_name: Python 3 (ipykernel) - language: python - name: python3 ---- - -## Exercise Yamamoto Method - -In this exercise the Yamamoto method will be applied and used as model parameter for the linear region of the steric mass action isotherm. - -+++ - -Import the data given in `./experiments/three_proteins` using the following helper function. -Then, visualize the data. - -```{code-cell} ipython3 -:tags: [solution] - -import numpy as np -from CADETProcess.tools.yamamoto import GradientExperiment -def create_experiment(file_name, gradient_volume): - """CSV should have format of [time, salt, protein].""" - - data = np.loadtxt(file_name, delimiter=',') - - time = data[:, 0] - c_salt = data[:, 1] - c_protein = data[:, 2:] - - return GradientExperiment(time, c_salt, c_protein, gradient_volume) -``` - -```{code-cell} ipython3 -:tags: [solution] - -experiment_1 = create_experiment('./experiments/three_proteins/18.8mL.csv', 18.8e-6) -experiment_2 = create_experiment('./experiments/three_proteins/37.6mL.csv', 37.6e-6) -experiment_3 = create_experiment('./experiments/three_proteins/56.4mL.csv', 56.4e-6) -experiment_4 = create_experiment('./experiments/three_proteins/75.2mL.csv', 75.2e-6) - -experiments = [experiment_1, experiment_2, experiment_3, experiment_4] -``` - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.tools.yamamoto import plot_experiments - -for experiment in experiments: - experiment.plot() -``` - -Define the parameters given in the table below as variables. - -Consider the following parameters: -- length: $0.1~m$ -- diameter: $7.7~mm$ -- bed porosity: $0.36$ -- particle radius: $34 \cdot 10^{-6}~m$ -- particle porosity: $0.85$ - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.processModel import ComponentSystem -component_system = ComponentSystem(['Salt', 'A', 'B', 'C']) - -from CADETProcess.processModel import StericMassAction -binding_model = StericMassAction(component_system) -binding_model.adsorption_rate = [1, 1, 1, 1] -binding_model.desorption_rate = [1, 1, 1, 1] -binding_model.capacity = 822.5 -binding_model.steric_factor = [0, 0, 0, 0] -``` - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.processModel import LumpedRateModelWithPores -column = LumpedRateModelWithPores(component_system, 'column') -column.binding_model = binding_model -column.length = 0.1 -column.diameter = 0.0077 -column.bed_porosity = 0.36 -column.particle_radius = 34e-6 -column.particle_porosity = 0.85 - -column.axial_dispersion = 1.5e-6 -column.film_diffusion = [2e-6, 5e-7, 5e-7, 5e-7] -``` - -Apply the Yamamoto method and save the return of the method in a variable. Print the characteristic charge and the equilibrium constant for all proteins in the system. - -```{code-cell} ipython3 -:tags: [solution] - -from CADETProcess.tools.yamamoto import fit_parameters -yamamoto_results = fit_parameters(experiments, column) - -print(yamamoto_results.characteristic_charge) -print(yamamoto_results.k_eq) -``` - -Visualize the results of the parameter estimation and check if the results are a good estimate of the experiments. - -```{code-cell} ipython3 -:tags: [solution] - -yamamoto_results.plot() -``` - -It can be recognized that at least one experiment is not in a good alignment with the found parameter. -Check the output of the parameter estimation and decide which experiment to neglect in the estimation. - -```{code-cell} ipython3 -:tags: [solution] - -print(f'logarithm of peak salt concentration for Protein 1: {yamamoto_results.log_c_salt_at_max_M[:, 0]}') -print(f'logarithm of peak salt concentration for Protein 2: {yamamoto_results.log_c_salt_at_max_M[:, 1]}') -print(f'logarithm of peak salt concentration for Protein 3: {yamamoto_results.log_c_salt_at_max_M[:, 2]}') -print(f'equilibrium constant: {yamamoto_results.k_eq}') -``` - -Viewing the data, the third entry related to the third experiment does not fit into the ascending sequence for the peak salt concentration. Remove the third experiment from the parameter estimation and redo the estimation. Also, visualize the results. - -```{code-cell} ipython3 -:tags: [solution] - -experiments_adapted = [experiment_1, experiment_2, experiment_4] -yamamoto_results_adapted = fit_parameters(experiments_adapted, column) - -print(yamamoto_results_adapted.characteristic_charge) -print(yamamoto_results_adapted.k_eq) -``` - -```{code-cell} ipython3 -:tags: [solution] - -yamamoto_results_adapted.plot() -``` - -# Bonus Task - -Try to build a simple model which reproduces the experimental results. -The model should consist of an `Inlet`, the column model and an `Outlet`. -Use the values specified in the tables above. - -Keep in mind the following steps for creating a model: -1. Specify missing UnitOperations -1. Construct the flow sheet -1. Construct the process -1. Simulation and plotting -1. Comparison to experiment - -+++ - -### 1. Specify missing UnitOperations - -```{code-cell} ipython3 -from CADETProcess.processModel import Inlet, Outlet - -inlet = Inlet(component_system, name='inlet') -inlet.flow_rate = 8.33e-09 - -outlet = Outlet(component_system, name='outlet') -``` - -### 2. Construct the flow sheet - -```{code-cell} ipython3 -from CADETProcess.processModel import FlowSheet - -flow_sheet = FlowSheet(component_system) -flow_sheet.add_unit(inlet) -flow_sheet.add_unit(column) -flow_sheet.add_unit(outlet) - -flow_sheet.add_connection(inlet, column) -flow_sheet.add_connection(column, outlet) -``` - -### 3. Construct the process - -```{code-cell} ipython3 -from CADETProcess.processModel import Process - -flow_rate = 8.33e-09 -sample_volume = 50e-9 -wash_volume = 15e-6 -gradient_volume =18.8e-6 -strip_volume = 10e-6 - -wash_start = sample_volume/flow_rate -gradient_start = sample_volume/flow_rate + wash_volume/flow_rate -strip_start = sample_volume/flow_rate + wash_volume/flow_rate + gradient_volume/flow_rate -t_cycle = sample_volume/flow_rate + wash_volume/flow_rate + gradient_volume/flow_rate + strip_volume/flow_rate - -slope = ((experiment_1.c_salt_end-experiment_1.c_salt_start)/(gradient_volume/flow_rate)) - -process = Process(flow_sheet, 'LWE_Lysozyme_18_8mL') -process.cycle_time = t_cycle - -process.add_event('load', 'flow_sheet.inlet.c', [experiment_1.c_salt_start, 0.2, 0.24, 0.16], 0) -process.add_event('wash', 'flow_sheet.inlet.c', [experiment_1.c_salt_start, 0, 0, 0], wash_start) -process.add_event( - 'grad_start', - 'flow_sheet.inlet.c', - [[experiment_1.c_salt_start, slope], [0, 0], [0, 0], [0, 0]], - gradient_start -) -process.add_event('strip','flow_sheet.inlet.c', [experiment_1.c_salt_end, 0, 0, 0], strip_start) -``` - -### 7. Simulation and plotting - -```{code-cell} ipython3 -from CADETProcess.simulator import Cadet - -simulator = Cadet() -sim_res = simulator.simulate (process) - -from CADETProcess.plotting import SecondaryAxis -sec = SecondaryAxis() -sec.components = ["Salt"] -sec.y_label = '$c_{Salt}$' -_ = sim_res.solution.column.outlet.plot(secondary_axis=sec) -``` - -### 8. Comparison to experiment - -```{code-cell} ipython3 -from CADETProcess.comparison import Comparator, ReferenceIO - -comparator = Comparator() -for i_p in range(experiment_1.n_proteins): - Ref_name = 'Protein_'+str(i_p) - comparator.add_reference(ReferenceIO( - Ref_name, experiment_1.time, experiment_1.c_protein[:,i_p] - ) - ) - comparator.add_difference_metric( - 'SSE', ReferenceIO(Ref_name, experiment_1.time, experiment_1.c_protein[:, i_p]), - 'outlet.outlet', components=component_system.labels[i_p+1] - ) - -comparator.plot_comparison(sim_res, plot_individual=True) -```