├── .gitignore ├── README.md ├── requirements.txt ├── solution-notebooks ├── day1_1_Introduction_to_py-pde_solution.ipynb ├── day1_2_source_degradation_solution.ipynb ├── day1_3_reaction-diffusion_solution.ipynb ├── day2_1_pole-to-pole_solution.ipynb ├── day2_2_physics-of-diffusion_solution.ipynb └── day2_3_non-ideal-reaction-diffusion_solution.ipynb ├── test_compatibility.sh └── tutorial-notebooks ├── day1_1_Introduction_to_py-pde.ipynb ├── day1_2_source_degradation.ipynb ├── day1_3_reaction-diffusion.ipynb ├── day2_1_pole-to-pole.ipynb ├── day2_2_physics-of-diffusion.ipynb └── day2_3_non-ideal-reaction-diffusion.ipynb /.gitignore: -------------------------------------------------------------------------------- 1 | *.hdf 2 | *.mp4 3 | 4 | .DS_Store 5 | 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | pip-wheel-metadata/ 29 | share/python-wheels/ 30 | *.egg-info/ 31 | .installed.cfg 32 | *.egg 33 | MANIFEST 34 | 35 | # PyInstaller 36 | # Usually these files are written by a python script from a template 37 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 38 | *.manifest 39 | *.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .nox/ 49 | .coverage 50 | .coverage.* 51 | .cache 52 | nosetests.xml 53 | coverage.xml 54 | *.cover 55 | *.py,cover 56 | .hypothesis/ 57 | .pytest_cache/ 58 | 59 | # Translations 60 | *.mo 61 | *.pot 62 | 63 | # Django stuff: 64 | *.log 65 | local_settings.py 66 | db.sqlite3 67 | db.sqlite3-journal 68 | 69 | # Flask stuff: 70 | instance/ 71 | .webassets-cache 72 | 73 | # Scrapy stuff: 74 | .scrapy 75 | 76 | # Sphinx documentation 77 | docs/_build/ 78 | 79 | # PyBuilder 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # IPython 86 | profile_default/ 87 | ipython_config.py 88 | 89 | # pyenv 90 | .python-version 91 | 92 | # pipenv 93 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 94 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 95 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 96 | # install all needed dependencies. 97 | #Pipfile.lock 98 | 99 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 100 | __pypackages__/ 101 | 102 | # Celery stuff 103 | celerybeat-schedule 104 | celerybeat.pid 105 | 106 | # SageMath parsed files 107 | *.sage.py 108 | 109 | # Environments 110 | .env 111 | .venv 112 | env/ 113 | venv/ 114 | ENV/ 115 | env.bak/ 116 | venv.bak/ 117 | 118 | # Spyder project settings 119 | .spyderproject 120 | .spyproject 121 | 122 | # Rope project settings 123 | .ropeproject 124 | 125 | # mkdocs documentation 126 | /site 127 | 128 | # mypy 129 | .mypy_cache/ 130 | .dmypy.json 131 | dmypy.json 132 | 133 | # Pyre type checker 134 | .pyre/ 135 | tutorial-notebooks/day2_3_non-ideal-reaction-diffusion.ipynb 136 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tutorial: Simulations of pattern formation in biological cells 2 | 3 | This repository contains material that we prepared for the 4 | [PSL-Qlife Winter school "Active Matter in Biology"](https://www.enseignement.biologie.ens.fr/spip.php?article245) that took place in Paris in February 2022. 5 | The course took place over two days, spanning about four hours each and we divided each day into three sections. 6 | The folder [`tutorial-notebooks`](https://github.com/zwicker-group/tutorial-pattern-formation-in-cells/tree/main/tutorial-notebooks) contains respective jupyter notebooks with explanations and tasks for the students. 7 | Solutions and further explanations are stored in the [`solution-notebooks`](https://github.com/zwicker-group/tutorial-pattern-formation-in-cells/tree/main/solution-notebooks) folder. 8 | 9 | ## Course description 10 | 11 | This workshop focuses on numerical simulations of patterns that 12 | form within biological cells. We focus on two relevant patterns that 13 | structure the intracellular environment: Min oscillations and 14 | biomolecular condensates. Both examples are modeled by partial 15 | differential equations, which will be discussed and analyzed in detail. 16 | Students can playfully explorer the behavior of these dynamical systems 17 | using the python package [`py-pde`](https://github.com/zwicker-group/py-pde), 18 | which we introduced in the beginning. 19 | On the first day, we start with simple cases 20 | with analytical solutions, then turn to classical reaction-diffusion 21 | systems, and finally analyze stable oscillations of the Min system. On 22 | the second day, we discuss diffusion in non-ideal solutions, which leads 23 | us to the physics of phase separation that 24 | models biomolecular condensates. By augmenting the classical 25 | Cahn-Hilliard equation we will learn how non-equilibrium chemical 26 | reactions can control phase separation in the cellular context. After 27 | this workshop, students will be able to simulate and analyze 28 | non-linear partial differential equations that often appear in biology. 29 | 30 | ## Further help and reading 31 | 32 | Feel free to use the [Discussions](https://github.com/zwicker-group/tutorial-pattern-formation-in-cells/discussions) 33 | to discuss this tutorial and ask questions. 34 | The [github page of the python package `py-pde`](https://github.com/zwicker-group/py-pde) that we use to run simulations also contains further information. 35 | If you are interested in the scientific theories discussed in the tutorials, you find more information on the 36 | webpages of 37 | [the group of Erwin Frey](https://www.theorie.physik.uni-muenchen.de/lsfrey/research/index.html) and 38 | [the group of David Zwicker](https://www.zwickergroup.org). 39 | Below, we also list a few scientific papers with further information. 40 | 41 | **Pattern formation and Min oscillations:** 42 | 43 | * E. Frey et al. [Self-organisation of Protein Patterns (2012)](https://arxiv.org/abs/2012.01797) 44 | * T. Burkart et al., [Control of protein-based pattern formation via guiding cues (2022)](https://doi.org/10.1101/2022.02.11.480095) 45 | * J. Halatek et al., [Highly Canalized MinD Transfer and MinE Sequestration Explain the Origin of Robust MinCDE-Protein Dynamic (2021)](https://doi.org/10.1016/j.celrep.2012.04.005) 46 | * K. Zieske et al., [Reconstitution of self-organizing protein gradients as spatial cues in cell-free systems, eLife (2014)](https://dx.doi.org/10.7554/elife.03949) 47 | 48 | **(Active) phase separation:** 49 | 50 | * M. Cates, [Complex Fluids: The Physics of Emulsions (2012)](https://arxiv.org/abs/1209.2290) 51 | * C. Weber et al., [Physics of Active Emulsions, Rep. Prog. Phys. 82 (2019)](https://iopscience.iop.org/article/10.1088/1361-6633/ab052b) 52 | * J. Kirschbaum et al., [Controlling biomolecular condensates via chemical reactions, J. R. Soc. Interface (2021)](https://royalsocietypublishing.org/doi/10.1098/rsif.2021.0255) 53 | * D. Zwicker et al., [Suppression of Ostwald ripening in Active Emulsions, PRE 92 (2015)](http://dx.doi.org/10.1103/PhysRevE.92.012317) 54 | 55 | ## Contributors 56 | 57 | The notebooks were created by Tom Burkart, Jan Kirschbaum, and [David Zwicker](https://www.zwickergroup.org/david-zwicker). 58 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | matplotlib>=3.1.0 2 | numba>=0.50.0 3 | numpy>=2 4 | scipy>=1.4.0 5 | sympy>=1.5.0 6 | py-pde>=0.40 7 | h5py>=2.10 8 | pandas>=1.2 9 | tqdm>=4.45 10 | ipywidgets>=7 11 | -------------------------------------------------------------------------------- /solution-notebooks/day1_2_source_degradation_solution.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "5800110e-994a-4668-9b90-a8c44e278ec1", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "# This notebook requires py-pde in version 0.17.1 or later\n", 11 | "# The package can be obtained from https://github.com/zwicker-group/py-pde\n", 12 | "# Alternatively, it can be installed via pip or conda\n", 13 | "\n", 14 | "import pde\n", 15 | "\n", 16 | "# functools and make_derivative2 are required for anisotropic laplacians\n", 17 | "import functools\n", 18 | "from pde.grids.operators.common import make_derivative2\n", 19 | "\n", 20 | "# plotting functions\n", 21 | "import matplotlib.pyplot as plt\n", 22 | "import numpy as np\n", 23 | "\n", 24 | "# fitting methods\n", 25 | "import scipy\n", 26 | "\n", 27 | "# a simple tracker to check parameter sweeps\n", 28 | "import datetime\n", 29 | "def ETA(step, maxStep, startTime):\n", 30 | " _ETA = None\n", 31 | " total_dt = 0\n", 32 | " dt = 0\n", 33 | " if step==0:\n", 34 | " _ETA = \"Indeterminate\"\n", 35 | " else:\n", 36 | " dt = datetime.datetime.now() - startTime\n", 37 | " dt = dt.seconds\n", 38 | " total_dt = dt/step * maxStep\n", 39 | " _ETA = (startTime + datetime.timedelta(seconds = total_dt))\n", 40 | " _ETA = str(_ETA.time())\n", 41 | " \n", 42 | " print(f\"{int(100 * step / maxStep):>3} % completed. ETA: {_ETA} ({int(total_dt - dt)} seconds remain).\" + '\\t' * 5,\n", 43 | " end='\\r')" 44 | ] 45 | }, 46 | { 47 | "cell_type": "markdown", 48 | "id": "62f71aa4-682e-48ba-803c-52f02edff5ec", 49 | "metadata": {}, 50 | "source": [ 51 | "# A simple source-degradation process\n", 52 | "In this task, you will explore how chemical gradients in biological systems can emerge from a local source with a global degradation process. Along the lines, you will learn some neat functionalities of the py-pde package.\n", 53 | "\n", 54 | "Consider the following situation: a cell's membrane, constantly produces some chemical $c$ at a rate $k_{prod}$. This could be a protein complex that is created on the membrane, or an external chemical that is constantly uptaken by the membrane. This chemical -- whose concentration is denoted by $c_m(x,t)$ -- can move on the membrane diffusively (diffusion constant $D_m$). At a rate $k$, these chemicals detach from the membrane into the cell's volume (cytosol). In the cytosol, this chemical $c_b(x,y,t)$ diffuses as well ($D_c$), and it is degraded at a rate $k_{degr}$, for example due to (de-)phosphorylation. We look at a small (flat) region of the cell's membrane and cytosol in 2D. Here, the cytosol is a 2D plane (parameterized by the coordinates $x$ and $y$), and the membrane is a 1D line located at the boundary of the cytosol, parameterized by the coordinate $x$. More precisely, the membrane is located at height $y=0$. The differential equations corresponding to this situation are\n", 55 | "\n", 56 | "$$\n", 57 | "\\partial_t c_m(x,t) = D_m \\nabla_x^2 c_m(x,t) + k_{prod} - k \\cdot c_m(x,t) \\, ,\\\\\n", 58 | "\\partial_t c_b(x,y,t) = D_c \\nabla^2 c_b(x,y,t) - k_{degr} \\cdot c_b(x,y,t) \\, .\n", 59 | "$$\n", 60 | "\n", 61 | "Note that the operators representing the diffusion on the membrane ($\\nabla_x$) and in the cytosol ($\\nabla$) are distinct, and require specific numerical treatments.\n", 62 | "\n", 63 | "Mathematically, the coupling of the membrane to the cytosol dynamics (i.e., the influx of particles into the cytosol from the membrane) is denoted as a Neumann boundary condition\n", 64 | "\n", 65 | "$$ \n", 66 | "-D_c \\, \\partial_y c_b(x,0,t) = k \\cdot c_m(x,t) \\, .\n", 67 | "$$\n", 68 | "\n", 69 | "In general, such boundary conditions are difficult to implement numerically (there is dedicated software that takes care of them), and are not very intuitive. Here, we use a much simpler and more intuitive approach to couple the membrane and cytosolic dynamics: we add to the cytosolic dynamics a space-dependent reaction rate, which allows the creation ('influx') of chemicals only at a restricted area in the cytosol. Naturally, this restricted area will be very close to the boundary. These modified equations now read\n", 70 | "\n", 71 | "$$\n", 72 | "\\partial_t c_m(x,t) = D_m \\nabla_x^2 c_m(x,t) + k_{prod} - k \\cdot c_m(x,t) \\, ,\\\\\n", 73 | "\\partial_t c_b(x,y,t) = D_c \\nabla^2 c_b(x,y,t) - k_{degr} \\cdot c_b(x,y,t) + m(y) \\cdot k \\cdot c_m(x,t) \\, ,\n", 74 | "$$\n", 75 | "\n", 76 | "where $m(y)$ represents a mask with $m(0) = 1$ (close to the membrane) and $m(y) = 0$ everywhere else." 77 | ] 78 | }, 79 | { 80 | "cell_type": "markdown", 81 | "id": "b5e643d9-2fac-4872-8162-a2fa1eebf8b8", 82 | "metadata": {}, 83 | "source": [ 84 | "## Part 1: Analysing the problem analytically\n", 85 | "In today's lecture, you learnt about characteristic profiles and length scales of such source degradation processes. Before starting to simulate the problem, let's think about what we expect.\n", 86 | "\n", 87 | "### Problem 1\n", 88 | "Make a sketch of the profile of the cytosolic concentration perpendicular to the membrane, $c_b(0,y,t)$, at the steady state of the system. What function represents this profile? What is the concentration far away from the membrane, $c_b(0, \\infty, t)$?\n", 89 | "\n", 90 | "(Optional) Determine the steady state profile analytically. And compare it with your sketch.\n", 91 | "\n", 92 | "_Hint: you obtain the steady state by solving $\\partial_t c_b = 0$._" 93 | ] 94 | }, 95 | { 96 | "cell_type": "markdown", 97 | "id": "c97ff45d", 98 | "metadata": {}, 99 | "source": [ 100 | "### Solution 1\n", 101 | "The steady state is an exponentially decaying function.\n", 102 | "Far away from the membrane, the concentration is $c_b(0, \\infty, t) = 0$." 103 | ] 104 | }, 105 | { 106 | "cell_type": "markdown", 107 | "id": "1d477631", 108 | "metadata": {}, 109 | "source": [ 110 | "### Problem 2\n", 111 | "The length scale $\\ell$ of the cytosolic concentration profile is (in this example) determined by a diffusion constant and a reaction rate. Which?\n", 112 | "\n", 113 | "Compare the units of the length scale $\\ell$, the diffusion constant, and the reaction rate, and use your results to obtain an equation for the length scale. " 114 | ] 115 | }, 116 | { 117 | "cell_type": "markdown", 118 | "id": "7d800e4a", 119 | "metadata": {}, 120 | "source": [ 121 | "### Solution 2\n", 122 | "The cytosolic profile is determined by the cytosolic diffusion constant $D_c$ and the degradation rate in the cytosol $k_{degr}$.\n", 123 | "\n", 124 | "The units/dimensions are:\n", 125 | "$$\n", 126 | "[\\ell] = m \\\\\n", 127 | "[D_c] = m^2/s \\\\\n", 128 | "[k_{degr}] = 1/s \\, .\n", 129 | "$$\n", 130 | "\n", 131 | "Thus, from comparing the units, the relation between the length scale and the system parameters is\n", 132 | "$$ \\ell = \\sqrt{\\frac{D_c}{k_{degr}}} \\, . $$" 133 | ] 134 | }, 135 | { 136 | "cell_type": "markdown", 137 | "id": "0f00e9ba", 138 | "metadata": {}, 139 | "source": [ 140 | "## Part 2: Testing the system numerically\n", 141 | "Now that we roughly know what to expect, we can solve this system by numerically solving the underlying PDEs. To this end, we will use the PDE solver provided by the ```py-pde``` solver. To give you a feeling how it works, we provide you with a working example with blanks that you need to fill in. In later exercises, you will learn how to set up the code by yourself. *Please take some time to understand the example.* Blanks that need to be filled in by you will be indicated by ```## BLANK ##```." 142 | ] 143 | }, 144 | { 145 | "cell_type": "markdown", 146 | "id": "6fb046cc", 147 | "metadata": {}, 148 | "source": [ 149 | "### Tutorial 1: How does a numerical PDE solver work?\n", 150 | " - Geometry\n", 151 | " - Mesh\n", 152 | " - Equations and Operators\n", 153 | " - Time stepping\n", 154 | " - Workarounds\n", 155 | " - Errors" 156 | ] 157 | }, 158 | { 159 | "cell_type": "markdown", 160 | "id": "9a4449ba", 161 | "metadata": {}, 162 | "source": [ 163 | "### Problem 3: Implementing the source-degradation process\n", 164 | "Fill in the ```## BLANKS ##``` to complete the code and run the simulations." 165 | ] 166 | }, 167 | { 168 | "cell_type": "code", 169 | "execution_count": null, 170 | "id": "0931cd86-7cec-424e-967e-f563ecef30ba", 171 | "metadata": {}, 172 | "outputs": [], 173 | "source": [ 174 | "# Define the geometry of the system.\n", 175 | "# We use a regular grid (rectangle) with a specified width (x-direction) and height (y-direction).\n", 176 | "\n", 177 | "width = 10 ## BLANK ##\n", 178 | "height = 5 ## BLANK ##\n", 179 | "resolution = (2**5)/10\n", 180 | "\n", 181 | "# pde.CartesianGrid() creates a grid object (mesh) specified by the rectangle corners and a sampling resolution for both axes.\n", 182 | "grid = pde.CartesianGrid(\n", 183 | " [[0, width], [0, height]],\n", 184 | " [int(resolution*i) for i in [width, height]]\n", 185 | ")\n", 186 | "grid.plot();" 187 | ] 188 | }, 189 | { 190 | "cell_type": "code", 191 | "execution_count": null, 192 | "id": "c398d35c-4fa6-4427-b9a9-0cdb343c7a45", 193 | "metadata": {}, 194 | "outputs": [], 195 | "source": [ 196 | "# Define a mask on the grid that represents the 1D membrane.\n", 197 | "# Here, we choose the bottom row (y=0) to be the membrane.\n", 198 | "# Note: in general, such masks are boolean arrays. For computation purposes, it is often convenient\n", 199 | "# to store them as float arrays, though, since some operations (e.g. arithmetic '-') are not well-\n", 200 | "# defined on boolean arrays\n", 201 | "\n", 202 | "membrane_mask = pde.ScalarField(grid, dtype=float)\n", 203 | "membrane_mask.data[:, 0] = 1.0\n", 204 | "membrane_mask.plot();" 205 | ] 206 | }, 207 | { 208 | "cell_type": "markdown", 209 | "id": "b43456a3", 210 | "metadata": {}, 211 | "source": [ 212 | "Any system with two different domains coupled to each other (in this case: cytosol and membrane) comes with certain numerical subtleties, in particular when the domains have different dimensions. One of these subtleties is the diffusion operator (second spatial derivative, laplacian), which looks fundamentally different on flat regions (e.g., a 2D cytosol) vs. curved regions (e.g., a membrane that is not perfectly flat). Here, we have a flat membrane, which saves us a lot of trouble, but nevertheless, we need to make sure that diffusion is restricted exclusively to the membrane for $c_m(x,t)$. To this end, we need to tinker a custom laplacian:" 213 | ] 214 | }, 215 | { 216 | "cell_type": "code", 217 | "execution_count": null, 218 | "id": "b7491eed-85ad-4caa-a2b4-b8a4fee39e99", 219 | "metadata": {}, 220 | "outputs": [], 221 | "source": [ 222 | "# Set up laplace operator in x direction only\n", 223 | "# (You do not need to understand the details here)\n", 224 | "make_laplace_x = functools.partial(make_derivative2, axis=0)\n", 225 | "pde.CartesianGrid.register_operator('laplace_x', make_laplace_x)\n", 226 | "\n", 227 | "# Now we test whether the custom laplacian works as intended:\n", 228 | "# We set up a random initial field on the membrane, and perform one time step.\n", 229 | "# If we did everything right, then there will be no diffusion in y-direction,\n", 230 | "# so that the field values at y>0 will remain 0.\n", 231 | "\n", 232 | "# Make a random initial field\n", 233 | "c_m = pde.ScalarField.random_uniform(grid)\n", 234 | "# Apply the mask to ensure that the field is non-zero only on the membrane\n", 235 | "c_m *= membrane_mask\n", 236 | "# Look at the field\n", 237 | "c_m.plot();\n", 238 | "# Perform a time step and look at the field again\n", 239 | "c_m.apply_operator('laplace_x', bc='auto_periodic_neumann').plot();" 240 | ] 241 | }, 242 | { 243 | "cell_type": "markdown", 244 | "id": "288a1fdd", 245 | "metadata": {}, 246 | "source": [ 247 | "Now that the numerical details are settled (geometry, mesh, and operators), we can feed the PDEs into the numerical PDE solver. Of course, we have to set values for all parameters first, since the numerical solver can not perform symbolic operations." 248 | ] 249 | }, 250 | { 251 | "cell_type": "code", 252 | "execution_count": null, 253 | "id": "3b0cfd18-6631-4b8e-a95b-335feb71eac8", 254 | "metadata": {}, 255 | "outputs": [], 256 | "source": [ 257 | "# Define parameters\n", 258 | "k_prod = 1. # production in membrane\n", 259 | "k = 0.1 # detachment rate\n", 260 | "k_degr = 1. # degradation in the cytosol\n", 261 | "\n", 262 | "D_c = 1. # diffusion in the cytosol\n", 263 | "D_m = 0.1 # diffusion on the membrane\n", 264 | "\n", 265 | "# Define the PDEs: l.h.s. specifies the field for which the equation holds,\n", 266 | "# r.h.s. equals the time derivative of the field.\n", 267 | "# In py-pde, the equations are defined as f-strings.\n", 268 | "# The reactions can be restricted to the membrane by multiplying with the\n", 269 | "# mask (which will be a field itself and therefore does not need to be escaped).\n", 270 | "expr = {'c_m': f'{D_m} * laplace_x(c_m) + mask * ({k_prod} - {k} * c_m)',\n", 271 | " 'c_b': f'{D_c} * laplace(c_b) + {k} * mask * c_m - {k_degr} * c_b'}\n", 272 | "\n", 273 | "# Set the initial values for both fields:\n", 274 | "c_m = pde.ScalarField.random_uniform(grid) * membrane_mask\n", 275 | "c_b = pde.ScalarField.random_uniform(grid)\n", 276 | "\n", 277 | "# Create the PDE object\n", 278 | "eq = pde.PDE(expr, consts={'mask': membrane_mask})\n", 279 | "\n", 280 | "# Set the initial values for all fields, and show it\n", 281 | "field = pde.FieldCollection([c_m, c_b])\n", 282 | "field.plot();\n", 283 | "\n", 284 | "# Simulate the system for 100 time steps, and show the result\n", 285 | "res = eq.solve(field, t_range=100, tracker=\"progress\")\n", 286 | "res.plot();" 287 | ] 288 | }, 289 | { 290 | "cell_type": "markdown", 291 | "id": "2e3681de", 292 | "metadata": {}, 293 | "source": [ 294 | "### Problem 4: Evaluation\n", 295 | "In general, the data you obtain from simulations needs to be post-processed into meaningful quantities. Here, we want to compare the results with the analytical results, i.e., we want to test whether the predicted concentration profile and length scale match the results.\n", 296 | "\n", 297 | "Do so by extracting a meaningful dataset from the solution data (which is stored in ```res.data```), and by plotting your result. Then, use ```numpy```'s ```polyfit``` to fit your predicted concentration profile to the data.\n", 298 | "_Hint: ```numpy.polyfit``` only fits polynomials. I your function contains exponentials, fit a polynomial to the log of your data, and then apply the exponential to the fitted result._" 299 | ] 300 | }, 301 | { 302 | "cell_type": "code", 303 | "execution_count": null, 304 | "id": "7214879c", 305 | "metadata": {}, 306 | "outputs": [], 307 | "source": [ 308 | "# Get plot data:\n", 309 | "coords = grid._axes_coords[1]\n", 310 | "concentration = res.data[1,0]\n", 311 | "\n", 312 | "# Plot:\n", 313 | "plt.plot(coords, concentration, 'o', label='data')\n", 314 | "plt.legend()\n", 315 | "plt.show();" 316 | ] 317 | }, 318 | { 319 | "cell_type": "code", 320 | "execution_count": null, 321 | "id": "1e99deb6", 322 | "metadata": {}, 323 | "outputs": [], 324 | "source": [ 325 | "# Guessed function for the concentration depending on the position\n", 326 | "def fit_func(x, a, b):\n", 327 | " return a * np.exp(- b * x)\n", 328 | "\n", 329 | "# Fit parameters and covariance for the fit data\n", 330 | "popt, pcov = scipy.optimize.curve_fit(fit_func, coords, concentration)\n", 331 | "\n", 332 | "# Visualization\n", 333 | "plt.plot(coords, concentration, 'o', label='data')\n", 334 | "plt.plot(coords, fit_func(coords, *popt), 'g--',\n", 335 | " label='fit')\n", 336 | "plt.title('Fit $c_b(y) = a \\cdot \\exp(b \\cdot y)$:\\n'+\n", 337 | " f'$a\\;=${popt[0]: .3},\\t$b\\;=${popt[1]: .4}')\n", 338 | "plt.xlabel('$y$')\n", 339 | "plt.ylabel('Concentration $c_b(y)$')\n", 340 | "plt.legend(loc=\"upper right\")\n", 341 | "plt.show()" 342 | ] 343 | }, 344 | { 345 | "cell_type": "markdown", 346 | "id": "4eb31502", 347 | "metadata": {}, 348 | "source": [ 349 | "### Problem 5: Sweeping\n", 350 | "A single dataset is of course rarely sufficient to be confident about your predictions. In general, we are mostly interested in the dependence of observable quantities on experimental control parameters. A good way to test whether we got the right _formula_ for the characteristic length scale $\\ell$ is to sweep over a few parameters and check whether the results match our formula across the entire sweep range.\n", 351 | "\n", 352 | "Set up the sweep by completing the definition of the method ```simulate_and_fit```. Use this method to obtain a reasonable dataset, and compare it to the function for $\\ell$. If you feel like your results take too long to calculate, tinker the mesh parameters (spatial and temporal) to get faster results." 353 | ] 354 | }, 355 | { 356 | "cell_type": "code", 357 | "execution_count": null, 358 | "id": "164d700c", 359 | "metadata": {}, 360 | "outputs": [], 361 | "source": [ 362 | "def simulate_and_fit(width=10, height=5, resolution=(2**5)/10,\n", 363 | " k_prod=1., k=0.1, k_degr=1., D_c=1., D_m=0.1):\n", 364 | " # Make a new grid\n", 365 | " grid = pde.CartesianGrid(\n", 366 | " [[0, width], [0, height]],\n", 367 | " [int(resolution*i) for i in [width, height]]\n", 368 | " )\n", 369 | " \n", 370 | " # Define the membrane mask\n", 371 | " membrane_mask = pde.ScalarField(grid, dtype=float)\n", 372 | " membrane_mask.data[:, 0] = 1.0\n", 373 | " \n", 374 | " # Create the PDE object\n", 375 | " expr = {'c_m': f'{D_m} * laplace_x(c_m) + mask * ({k_prod} - {k} * c_m)',\n", 376 | " 'c_b': f'{D_c} * laplace(c_b) + {k} * mask * c_m - {k_degr} * c_b'}\n", 377 | " eq = pde.PDE(expr, consts={'mask': membrane_mask})\n", 378 | " \n", 379 | " # Set initial concentrations\n", 380 | " c_m = pde.ScalarField.random_uniform(grid) * membrane_mask\n", 381 | " c_b = pde.ScalarField.random_uniform(grid)\n", 382 | " \n", 383 | " # Calculate the solution\n", 384 | " field = pde.FieldCollection([c_m, c_b])\n", 385 | " res = eq.solve(field, t_range=10, tracker=[])\n", 386 | " \n", 387 | " # Post-process the data\n", 388 | " coords = grid._axes_coords[1]\n", 389 | " concentration = res.data[1,0]\n", 390 | " popt, pcov = scipy.optimize.curve_fit(fit_func, coords, concentration)\n", 391 | " \n", 392 | " # Return the (relevant) fit parameter\n", 393 | " return popt[1]" 394 | ] 395 | }, 396 | { 397 | "cell_type": "code", 398 | "execution_count": null, 399 | "id": "241361c5-718e-49cd-8839-e4bc95adffef", 400 | "metadata": {}, 401 | "outputs": [], 402 | "source": [ 403 | "# Set the sweep parameters (note: sweep over ~10 values)\n", 404 | "sweepParameters = np.power(10, np.arange(-.2, 1.,.25))\n", 405 | "\n", 406 | "# Set up the sweep in a for loop\n", 407 | "sweepResults = []\n", 408 | "startTime = datetime.datetime.now()\n", 409 | "for _ in sweepParameters:\n", 410 | " ETA(list(sweepParameters).index(_), len(sweepParameters), startTime)\n", 411 | " fitResult = simulate_and_fit(D_c=_, width = 2.)\n", 412 | " sweepResults.append(fitResult)\n", 413 | "\n", 414 | "# For convenience, convert the results to a numpy array\n", 415 | "sweepResults=np.array(sweepResults)\n", 416 | "print('Finished.' + \"\\t\"*100)" 417 | ] 418 | }, 419 | { 420 | "cell_type": "code", 421 | "execution_count": null, 422 | "id": "3795f2d7", 423 | "metadata": {}, 424 | "outputs": [], 425 | "source": [ 426 | "# Define a high-resolution range for plotting the analytic function\n", 427 | "aRange = np.linspace(min(sweepParameters), max(sweepParameters), 100)\n", 428 | "\n", 429 | "# Plot the sweep results together with the prediction\n", 430 | "plt.plot(sweepParameters, 1/sweepResults, 'o', label='data')\n", 431 | "plt.plot(aRange, np.sqrt(aRange/1.), label='prediction')\n", 432 | "plt.title(\"Characteristic length scale vs. diffusion constant\")\n", 433 | "plt.xlabel('Diffusion constant')\n", 434 | "plt.ylabel('Characteristic length scale $\\ell$')\n", 435 | "plt.legend()\n", 436 | "plt.show()" 437 | ] 438 | }, 439 | { 440 | "cell_type": "markdown", 441 | "id": "18cd1285", 442 | "metadata": {}, 443 | "source": [ 444 | "### Problem 6: Interpretation\n", 445 | "Despite having done everything correctly, you will notice that the predicted (analytic) dependence of the length scale on the control parameters does not match the sweep for all tested parameters. Why? How could you solve this problem? What does this teach you about numerical simulations in general?\n", 446 | "\n", 447 | "_Hint: Plot one of the profiles where the fit does not match the prediction. What do you notice?_" 448 | ] 449 | }, 450 | { 451 | "cell_type": "markdown", 452 | "id": "22d689e0", 453 | "metadata": {}, 454 | "source": [ 455 | "### Solution 6\n", 456 | "Since the geometry is finite, there are boundary effects once the $\\ell$ gets close enough to the system size. This problem could be solved easily by increasing the system size. In general, this means that boundary effects as well as other numerical artifacts (such as aliasing, mesh defects etc) need to be taken into account. Also, one should always make sure that the obtained results are not due to one of these numerical effects." 457 | ] 458 | } 459 | ], 460 | "metadata": { 461 | "kernelspec": { 462 | "display_name": "Python 3 (ipykernel)", 463 | "language": "python", 464 | "name": "python3" 465 | }, 466 | "language_info": { 467 | "codemirror_mode": { 468 | "name": "ipython", 469 | "version": 3 470 | }, 471 | "file_extension": ".py", 472 | "mimetype": "text/x-python", 473 | "name": "python", 474 | "nbconvert_exporter": "python", 475 | "pygments_lexer": "ipython3", 476 | "version": "3.9.9" 477 | } 478 | }, 479 | "nbformat": 4, 480 | "nbformat_minor": 5 481 | } 482 | -------------------------------------------------------------------------------- /solution-notebooks/day1_3_reaction-diffusion_solution.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "0e838efd", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "# This notebook requires py-pde in version 0.17.1 or later\n", 11 | "# The package can be obtained from https://github.com/zwicker-group/py-pde\n", 12 | "# Alternatively, it can be installed via pip or conda\n", 13 | "\n", 14 | "import pde\n", 15 | "\n", 16 | "# functools and make_derivative2 are required for anisotropic laplacians\n", 17 | "import functools\n", 18 | "from pde.grids.operators.common import make_derivative2\n", 19 | "\n", 20 | "# plotting functions\n", 21 | "import matplotlib.pyplot as plt\n", 22 | "import numpy as np\n", 23 | "\n", 24 | "# fitting methods\n", 25 | "import scipy\n", 26 | "\n", 27 | "# a simple tracker to check parameter sweeps\n", 28 | "import datetime\n", 29 | "def ETA(step, maxStep, startTime):\n", 30 | " _ETA = None\n", 31 | " total_dt = 0\n", 32 | " dt = 0\n", 33 | " if step==0:\n", 34 | " _ETA = \"Indeterminate\"\n", 35 | " else:\n", 36 | " dt = datetime.datetime.now() - startTime\n", 37 | " dt = dt.seconds\n", 38 | " total_dt = dt/step * maxStep\n", 39 | " _ETA = (startTime + datetime.timedelta(seconds = total_dt))\n", 40 | " _ETA = str(_ETA.time())\n", 41 | " \n", 42 | " print(f\"{int(100 * step / maxStep):>3} % completed. ETA: {_ETA} ({int(total_dt - dt)} seconds remain).\" + '\\t' * 5,\n", 43 | " end='\\r')" 44 | ] 45 | }, 46 | { 47 | "cell_type": "markdown", 48 | "id": "963cef99", 49 | "metadata": {}, 50 | "source": [ 51 | "# Reaction-Diffusion processes\n", 52 | "Generic design features shared by the diverse biochemical interaction networks underlying protein pattern formation in cells include:\n", 53 | " - The dynamics (approximately) conserves the mass of each individual protein species: on the time scale of pattern formation neither protein production nor protein degradation are significant processes.\n", 54 | " - The biochemical reactions are characterised by (positive and negative) feedback mechanisms such that the chemical rate equations describing the dynamics of these reactions are generically nonlinear.\n", 55 | " - The proteins are typically transported by diffusive fluxes.\n", 56 | " \n", 57 | "Then, the spatiotemporal dynamics of protein patterns is described by _mass-conserving reaction-diffusion_ (MCRD) equations.\n", 58 | "\n", 59 | "The aspect of mass conservation is a constraint imposed by nature: in general, proteins do not appear out of nowhere, nor do they disappear into the void. For biological systems, the mass conservation plays a crucial role that will bother us later in the tutorial; for now, we will focus only on generic properties of pattern-forming reaction-diffusion systems. In the following, you will learn how to set up generic pattern-forming systems with a PDE solver, what individual parts of the differential equations mean, and how to interpret the results." 60 | ] 61 | }, 62 | { 63 | "cell_type": "markdown", 64 | "id": "588efa89", 65 | "metadata": {}, 66 | "source": [ 67 | "## The Fitzhugh-Nagumo model\n", 68 | "We will analyse and implement a very generic system that shows stationary patterns here, namely the Fitzhugh-Nagumo model. This model was originally proposed as a simplified description of neuron spiking. The model features two components, $u(x,y,t)$ and $v(x,y,t)$, that interact nonlinearly with each other. The general formulation reads:\n", 69 | "$$\n", 70 | "\\partial_t u(x,y,t) = D_u \\, \\nabla^2 u + f(u) - \\sigma \\cdot v \\, , \\\\\n", 71 | "\\partial_t v(x,y,t) = D_v \\, \\nabla^2 v + u - v \\, ,\n", 72 | "$$\n", 73 | "with $f(u)$ containing all the nonlinear interactions (see below).\n", 74 | "For simplicity, the spatial/temporal variables $x$, $y$, and $t$ where omitted on the r.h.s. of the equations.\n", 75 | "Note that these equations are a simplified version of the variants typically found in the literature, in particular regarding additional prefactors.\n", 76 | "\n", 77 | "The nonlinear interaction term is the key component that leads to the formation of patterns in this model. Here, we will use\n", 78 | "$$f(u) = - u^3 + \\alpha \\cdot u - \\kappa \\, .$$\n", 79 | "In this representation, all parameters ($D_u$, $D_v$, $\\alpha$, $\\kappa$, and $\\sigma$) are positive." 80 | ] 81 | }, 82 | { 83 | "cell_type": "markdown", 84 | "id": "2a26884b", 85 | "metadata": {}, 86 | "source": [ 87 | "## Part 1: Analysing the problem analytically" 88 | ] 89 | }, 90 | { 91 | "cell_type": "markdown", 92 | "id": "bce04c04", 93 | "metadata": {}, 94 | "source": [ 95 | "### Problem 1: Understanding the differential equations\n", 96 | "For each of the terms on the r.h.s. of the PDEs, state their effect on the dynamics of the system.\n", 97 | "For each of the parameters, state how varying this parameter will qualitatively change the dynamics of the system.\n", 98 | "\n", 99 | "_Hint: You should remain on a very broad level here, using phrases such as 'smoothens out rough profiles', 'leads to an increase/decrease of $v$ (or $u$)', etc._\n", 100 | "\n", 101 | "_Hint: For examining the parameters, it helps to consider a specific state of the system. For example, if $u>0$ then the term $\\alpha \\cdot u$ will lead to further increase of $u$, but it will lead to a decrease if $u<0$._" 102 | ] 103 | }, 104 | { 105 | "cell_type": "markdown", 106 | "id": "8a902964", 107 | "metadata": {}, 108 | "source": [ 109 | "### Solution 1\n", 110 | "Terms in the equations:\n", 111 | "- $D_u \\nabla^2 u$: diffusion of quantity $u$\n", 112 | "- $-u^3$: nonlinear decay of $u$ towards $u=0$\n", 113 | "- $+\\alpha \\cdot u$: linear growth of $u$ away from $u=0$\n", 114 | "- $-\\kappa$: constant bias of $u$ dynamics to decrease $u$\n", 115 | "- $-\\sigma \\cdot v$: decreases $u$ if $v$ is positive, and increases $u$ if $v$ is negative\n", 116 | "- $D_v \\nabla^2 v$: diffusion of quantity $v$\n", 117 | "- $+u$: increases $v$ if $u$ is positive, decreases $v$ if $u$ is negative\n", 118 | "- $-v$: linear decay of $u$ away from $u=0$\n", 119 | "\n", 120 | "Parameters:\n", 121 | "- $D_u$: diffusion constant of the activator species, smoothening out rough profiles in $u$\n", 122 | "- $D_v$: diffusion constant of the inhibitor species, smoothening out rough profiles in $v$\n", 123 | "- $\\alpha$: for larger values of $\\alpha$, the stable fixed points are further away from the unstable one, and a stronger perturbation is needed to overcome the diffusive smoothening\n", 124 | "- $\\kappa$: bias/skewness of the dynamics/fixed points. If $\\kappa = 0$, then the system is perfectly symmetric with respect to sign changes. For $\\kappa \\neq 0$, the unstable fixed point is at $u \\neq 0$. This decides whether the system shows a hole pattern or a spot pattern.\n", 125 | "- $\\sigma$ strength of the coupling to the quantity $v$: for $\\sigma = 0$, the $u$ dynamics are independent of $v$, and no patterns will emerge. For $\\sigma = \\alpha = 1$, the system happens to be mass-conserving." 126 | ] 127 | }, 128 | { 129 | "cell_type": "markdown", 130 | "id": "ed72a3a3", 131 | "metadata": {}, 132 | "source": [ 133 | "### Bonus Problem (Optional): Linear stability analysis and dispersion relation\n", 134 | "The following problem requires some background in theoretical physics or mathematics. If you are comfortable with the terms used in the problem, or have time to spare after completing all other problems, you can try this bonus problem. It will teach you how the values for the constants used in the code were derived (since, of course, you can't just guess any values). You can even solve the problem without the suggested simplifications to obtain more general results.\n", 135 | "\n", 136 | "Perform a linear stability analysis of the FitzHugh-Nagumo model for the case $\\kappa = 0$ and $\\sigma = 1$ (ignoring the diffusive component, i.e., assuming $D_u = D_v = 0$). What is the condition for the system to be linearly (un)stable?\n", 137 | "\n", 138 | "_Hint: You will need to calculate the fixed points first. Then, determine whether the fixed point at $u^* = v^* = 0$ is stable or unstable._\n", 139 | "\n", 140 | "Now calculate the dispersion relation, with the additional simplification that $\\alpha = 1$. What are the conditions for the diffusion constants $D_u$ and $D_v$ to obtain a band of unstable modes?\n", 141 | "\n", 142 | "_Hint: Use the following ansatz for calculating the dispersion relation to lowest order in $\\delta u$ around the fixed point $u^* = v^* = 0$: $u(x,t) = \\delta u \\cdot \\exp(\\eta t) \\sin(k \\, x)$, and similar for $v$ It also helps to express one diffusion constant as a fraction of the other, e.g., $D_u = z \\cdot D_v$._\n" 143 | ] 144 | }, 145 | { 146 | "cell_type": "markdown", 147 | "id": "225fa2ae", 148 | "metadata": {}, 149 | "source": [ 150 | "### Bonus Problem: Solution\n", 151 | "#### Fixed points:\n", 152 | "In the well-mixed case, the equation for $v$ yields $u^* = v^*$ immediately. From the equation for $u$ it then follows that $u^*_1 = 0$, and $u^*_{2,3} = \\pm \\sqrt{\\alpha - \\sigma}$. The expression becomes more entangled for $\\kappa \\neq 0$.\n", 153 | "\n", 154 | "#### Stability:\n", 155 | "Still in the well-mixed case, one can write the equations linearized around $u^* = v^* = 0$ in matrix representation:\n", 156 | "$$\\partial_t \\begin{pmatrix} \\delta u \\\\ \\delta v \\end{pmatrix} = \\begin{pmatrix} \\alpha & -\\sigma \\\\ 1 & -1 \\end{pmatrix} \\cdot \\begin{pmatrix} \\delta u \\\\ \\delta v \\end{pmatrix} \\, .$$\n", 157 | "The largest eigenvalue of this matrix determines the stability of the fixed point. The eigenvalues are\n", 158 | "$$ \\eta_\\pm = \\frac{\\alpha - 1}{2} \\bigl(1 \\pm \\sqrt{1 + 4 \\frac{\\alpha - \\sigma}{(\\alpha - 1)^2}}\\bigr) \\, .$$\n", 159 | "For $\\sigma = 1$, there is a positive eigenvalue (i.e., the fixed point is unstable) if $\\alpha > 1$.\n", 160 | "\n", 161 | "#### Dispersion relation:\n", 162 | "The suggested simplification implies that the fixed point is now linearly (meta)stable, so the dispersion relation is bound to be zero at the mode $k=0$. Using the suggested ansatz, one can again write the system in matrix representation,\n", 163 | "$$\\partial_t \\begin{pmatrix} \\delta u \\\\ \\delta v \\end{pmatrix} = \\begin{pmatrix} -D_u k^2 + \\alpha & -\\sigma \\\\ 1 & -D_v k^2 -1 \\end{pmatrix} \\cdot \\begin{pmatrix} \\delta u \\\\ \\delta v \\end{pmatrix} \\, .$$\n", 164 | "With $\\alpha = \\sigma = 1$, the dispersion relation then reads\n", 165 | "$$ \\eta(k) = \\frac{1}{2} \\biggl[-D_v k^2 (1+z) \\pm \\sqrt{D_v k^2 \\cdot (1-z) \\cdot \\bigl(4 + D_v k^2 (1-z)\\bigr)} \\biggr] \\, .$$\n", 166 | "To check whether there is a band of unstable modes, it is sufficient to calculate the derivative of the dispersion relation w.r.t. k at $k \\to 0$, since from the previous part we know that $\\eta(k=0) = 0$. Alternatively, one may also just plot the obtained expression for varying values of $z$ and $D_v$. In the end, one finds that there is a band of unstable modes if $z < 1$." 167 | ] 168 | }, 169 | { 170 | "cell_type": "markdown", 171 | "id": "aedd79d1", 172 | "metadata": {}, 173 | "source": [ 174 | "## Part 2: Testing the system numerically\n", 175 | "Different to the source-degradation, we don't know yet what to expect from the system. In particular, the size of the system and the grid resolution need to be determined without knowing the system dynamics. " 176 | ] 177 | }, 178 | { 179 | "cell_type": "markdown", 180 | "id": "c919232b", 181 | "metadata": {}, 182 | "source": [ 183 | "### Problem 2: Determining the numerical constraints\n", 184 | "Use arbitrarily chosen values for the diffusion constants (e.g., $D_u = 0.1 \\; [\\mu m^2/s]$ and $D_v = 1.0 \\; [\\mu m^2/s]$) and the reaction coefficients ($\\alpha = 1 \\; [1/s]$, $\\kappa = 0.1 \\; [1/ (s \\cdot \\mu m ^2)]$, and $\\sigma = 1 \\; [1/s]$) to make an educated guess about the system size and spatial resolution." 185 | ] 186 | }, 187 | { 188 | "cell_type": "markdown", 189 | "id": "5af23793", 190 | "metadata": {}, 191 | "source": [ 192 | "### Solution 2\n", 193 | "Using the same formula for the length scale as in the source-degradation problem ($\\ell \\sim \\sqrt{D/k}$), we find that the characteristic length scale of the patterns should be on the order of $\\sim \\mu m$. To see a noticable pattern (i.e., not just a single blob), the system should be several tens of $\\mu m$ large, while the spatial resolution should be below the $\\mu m $ scale. Possible values are $L = 30 [\\mu m]$ and `resolution` = 2, which means that individual grid cells are $0.5 [\\mu m]$ in size." 194 | ] 195 | }, 196 | { 197 | "cell_type": "code", 198 | "execution_count": null, 199 | "id": "c32acd79", 200 | "metadata": {}, 201 | "outputs": [], 202 | "source": [ 203 | "# Define the geometry of the system.\n", 204 | "# This time, we use a square grid of width L. The mesh size can be adapted by changing `resolution`.\n", 205 | "# Setting `resolution = 10`, for example, means that grid cells are 1/resolution = 0.1 (length units) in size.\n", 206 | "\n", 207 | "L = 30 ## BLANK ##\n", 208 | "resolution = 2 ## BLANK ##\n", 209 | "\n", 210 | "# pde.CartesianGrid() creates a grid object.\n", 211 | "# To avoid boundary effects, we use periodic boundary conditions here.\n", 212 | "\n", 213 | "grid = pde.CartesianGrid(\n", 214 | " [[0, L], [0, L]],\n", 215 | " [int(resolution*L), int(resolution*L)],\n", 216 | " periodic = True\n", 217 | ")\n", 218 | "grid.plot();" 219 | ] 220 | }, 221 | { 222 | "cell_type": "code", 223 | "execution_count": null, 224 | "id": "a283b4d6", 225 | "metadata": {}, 226 | "outputs": [], 227 | "source": [ 228 | "# Since we do not have any membrane dynamics, we do not need a special diffusion operator this time." 229 | ] 230 | }, 231 | { 232 | "cell_type": "markdown", 233 | "id": "901e667d", 234 | "metadata": {}, 235 | "source": [ 236 | "### Problem 3: Setting up the equations\n", 237 | "According to your choice of the system size and resolution, it is now time to define the system parameters. Translate the equations of the Fitzhugh-Nagumo model into the required syntax of `py-pde`. As a reminder, the expression for $v$ is already given." 238 | ] 239 | }, 240 | { 241 | "cell_type": "code", 242 | "execution_count": null, 243 | "id": "64ba193a", 244 | "metadata": {}, 245 | "outputs": [], 246 | "source": [ 247 | "D_u = 0.1\n", 248 | "D_v = 1.0\n", 249 | "\n", 250 | "alpha = 1.0\n", 251 | "kappa = 0.1\n", 252 | "sigma = 1.0\n", 253 | "\n", 254 | "expr = {'u' : f'laplace(u) * {D_u} + {alpha} * u - u * u * u - {kappa} - {sigma} * v', ## BLANK ##\n", 255 | " 'v' : f'laplace(v) * {D_v} + u - v'}\n" 256 | ] 257 | }, 258 | { 259 | "cell_type": "markdown", 260 | "id": "a34ab012", 261 | "metadata": {}, 262 | "source": [ 263 | "Now you are almost ready to execute the simulation: only the initial conditions are still missing. In general, for pattern-forming systems, it is a good start to use an unstable fixed point with a sufficiently strong perturbation as initial states. In the Fitzhugh-Nagumo model, for $\\kappa = 0$, one fixed point is at $u^* = v^* = 0$, and this fixed point is unstable _in a spatially extended system_ (in a 0D system, this need not be the case). Since we have a comparably small $\\kappa$, the actual fixed point will be close to this, so for simplicity, we perturb the system around $(u, v) = (0,0)$.\n", 264 | "\n", 265 | "`random_uniform(grid)` returns an array with values randomly chosen between 0 and 1. The code below transforms this to random values between -1 and 1, centered around the fixed point." 266 | ] 267 | }, 268 | { 269 | "cell_type": "code", 270 | "execution_count": null, 271 | "id": "26a97961", 272 | "metadata": {}, 273 | "outputs": [], 274 | "source": [ 275 | "# Construct the field objects\n", 276 | "u = pde.ScalarField.random_uniform(grid) * 2. - 1.\n", 277 | "v = pde.ScalarField.random_uniform(grid) * 2. - 1.\n", 278 | "\n", 279 | "\n", 280 | "# Create the PDE object\n", 281 | "eq = pde.PDE(expr)\n", 282 | "\n", 283 | "# Set the initial values for all fields, and show it\n", 284 | "field = pde.FieldCollection([u,v])\n", 285 | "field.plot();\n", 286 | "\n", 287 | "# Simulate the system and show the result\n", 288 | "storage = pde.MemoryStorage()\n", 289 | "\n", 290 | "res = eq.solve(field, t_range=200, tracker=[storage.tracker(1), \"progress\"])\n", 291 | "res.plot();" 292 | ] 293 | }, 294 | { 295 | "cell_type": "markdown", 296 | "id": "5f317639", 297 | "metadata": {}, 298 | "source": [ 299 | "## Part 3: Evaluation\n", 300 | "### Problem 4: Interpreting the results\n", 301 | "Look at the final state of the simulation. Do you observe a pattern? Do your results match the expactations? What is the order of magnitude of the pattern's characteristic length scale? Run the simulation again with different values for $\\kappa$ and note down your observations." 302 | ] 303 | }, 304 | { 305 | "cell_type": "markdown", 306 | "id": "ee107bdd", 307 | "metadata": {}, 308 | "source": [ 309 | "### Solution 4\n", 310 | "We can observe a spot pattern, where the distance between neighbouring spots is $\\sim 10 \\; [\\mu m ]$.\n", 311 | "\n", 312 | "Varying $\\kappa$ changes the pattern from spots to holes." 313 | ] 314 | }, 315 | { 316 | "cell_type": "markdown", 317 | "id": "50560ec6", 318 | "metadata": {}, 319 | "source": [ 320 | "### Problem 5: Quantifying the results\n", 321 | "We now want to quantify the characteristic length scale. To do so, we will try to find the center of each spot in the pattern, and calculate the distances to the other spots. This analysis is done by the code below, which shows you a histogram of the spot distances, and returns a proxy for the mean distance between neighboring spots.\n", 322 | "\n", 323 | "The function `get_length_scale` does not contain any blanks, however you are encouraged to walk through it and to understand the underlying idea. This will help you to properly interpret the results later on." 324 | ] 325 | }, 326 | { 327 | "cell_type": "code", 328 | "execution_count": null, 329 | "id": "03f60ed4", 330 | "metadata": {}, 331 | "outputs": [], 332 | "source": [ 333 | "def get_length_scale(data: np.array, show_stats: bool = True, show_centers: bool = False):\n", 334 | " # Make an empty array of the same size as the data array.\n", 335 | " # This is not necessary, but it helps to visualize things if needed, and is useful for debugging\n", 336 | " pattern_maxima = np.zeros_like(data)\n", 337 | " \n", 338 | " # Make a list to store all pattern spot maxima\n", 339 | " maxima_coordinates = []\n", 340 | " \n", 341 | " # Define some auxiliary quantities\n", 342 | " lx, ly = data.shape\n", 343 | " delta = 2 # size of the scanning window to find the maxima\n", 344 | " offsets = [(dx, dy) for dx in range(-delta, delta+1) for dy in range(-delta, delta+1)]\n", 345 | " offsets.remove((0,0)) # coordinate offsets that should be checked\n", 346 | " \n", 347 | " # Scan the entire array and check whether a pixel has the highes value of all neighbours\n", 348 | " for iy, ix in np.ndindex(data.shape):\n", 349 | " nbs = [data[(ix+dx)%lx, (iy+dy)%ly] for dx,dy in offsets]\n", 350 | " if data[ix,iy] > max(nbs):\n", 351 | " pattern_maxima[ix,iy] += 1.\n", 352 | " maxima_coordinates.append(np.array((ix,iy)))\n", 353 | " pattern_maxima[ix, iy] = 1.\n", 354 | " \n", 355 | " # Show the spot centers\n", 356 | " if show_centers:\n", 357 | " plt.imshow(data)\n", 358 | " plt.show();\n", 359 | "\n", 360 | " plt.imshow(pattern_maxima)\n", 361 | " plt.show();\n", 362 | " \n", 363 | " # Average area 'occupied' by a spot pattern: (total area)/(nr of spots)\n", 364 | " # Average distance between spots: sqrt((total area)/(nr of spots)):\n", 365 | " neighbour_distances = np.sqrt(L*L/len(maxima_coordinates))\n", 366 | " if show_stats:\n", 367 | " print(f\"Nr. of spots: {len(maxima_coordinates)}\")\n", 368 | " print(f\"Occupied area per spot: {L*L/len(maxima_coordinates)}\")\n", 369 | " print(f\"Estimated average distance between spots: {neighbour_distances}\")\n", 370 | " \n", 371 | " \n", 372 | " return np.mean(neighbour_distances)" 373 | ] 374 | }, 375 | { 376 | "cell_type": "code", 377 | "execution_count": null, 378 | "id": "cebe8f7f", 379 | "metadata": {}, 380 | "outputs": [], 381 | "source": [ 382 | "get_length_scale(storage.data[-1][0], show_centers = True)" 383 | ] 384 | }, 385 | { 386 | "cell_type": "markdown", 387 | "id": "ab15a32e", 388 | "metadata": {}, 389 | "source": [ 390 | "### Problem 6: Sweeping\n", 391 | "Again, we want to check what happens when we change one of the parameters. Here, we want to vary the diffusion constants. Sweep over an appropriate parameter regime and describe how the characteristic pattern length scale $\\ell$ changes.\n", 392 | "\n", 393 | "What (approximate) functional dependence of the length scale on the diffusion constant can you derive from your sweep? Is it meaningful to extrapolate your data? Where do you expect your results to fail?" 394 | ] 395 | }, 396 | { 397 | "cell_type": "code", 398 | "execution_count": null, 399 | "id": "42364f98", 400 | "metadata": {}, 401 | "outputs": [], 402 | "source": [ 403 | "# Define a function that yields the characteristic length scale for a given parameter set\n", 404 | "def simulate_and_get_length_scale(D_u = 0.1, D_v = 1.0, alpha = 1.0, kappa = 0.1, sigma = 1.0, tracker = \"progress\"):\n", 405 | " expr = {'u' : f'laplace(u) * {D_u} + {alpha} * u - u * u * u - {kappa} - {sigma} * v', \n", 406 | " 'v' : f'laplace(v) * {D_v} + u - v'} \n", 407 | " \n", 408 | " # Construct the field objects\n", 409 | " u = pde.ScalarField.random_uniform(grid) * 2. - 1.\n", 410 | " v = pde.ScalarField.random_uniform(grid) * 2. - 1.\n", 411 | "\n", 412 | " eq = pde.PDE(expr)\n", 413 | " field = pde.FieldCollection([u,v])\n", 414 | "\n", 415 | " res = eq.solve(field, t_range=200, tracker=tracker)\n", 416 | " \n", 417 | " return get_length_scale(res.data[0], show_stats = False) " 418 | ] 419 | }, 420 | { 421 | "cell_type": "code", 422 | "execution_count": null, 423 | "id": "d83dc576", 424 | "metadata": {}, 425 | "outputs": [], 426 | "source": [ 427 | "# Make a parameter sweep\n", 428 | "# Empty list to store the results in\n", 429 | "sweep_results = []\n", 430 | "\n", 431 | "# Parameters to sweep over\n", 432 | "sweep_parameters = np.power(10, np.arange(-.5, .5, 0.125))\n", 433 | "\n", 434 | "# Nr of iterations per parameter to get a statistical average\n", 435 | "sample_size = 4\n", 436 | "\n", 437 | "# Launch the sweep\n", 438 | "start_time = datetime.datetime.now()\n", 439 | "for _ in sweep_parameters:\n", 440 | " tmp = []\n", 441 | " for __ in range(sample_size):\n", 442 | " ETA(list(sweep_parameters).index(_) * sample_size + __, len(sweep_parameters) * sample_size, start_time)\n", 443 | " tmp.append(simulate_and_get_length_scale(D_u = 0.1 * _, D_v = 1.0 * _, tracker = []))\n", 444 | " length_scale = np.mean(tmp)\n", 445 | " sweep_results.append(length_scale)\n", 446 | "\n", 447 | "# Convert the results to a numpy array for better handling afterwards\n", 448 | "sweep_results = np.array(sweep_results)\n", 449 | "\n", 450 | "# Plot the data\n", 451 | "plt.plot(sweep_parameters, sweep_results, 'o', label='data')\n", 452 | "plt.title('Characteristic length scale vs. diffusion constant')\n", 453 | "plt.xlabel('$D/D_0$')\n", 454 | "plt.ylabel('Characteristic length $\\ell$')\n", 455 | "plt.legend(loc=\"upper left\")\n", 456 | "plt.show()" 457 | ] 458 | }, 459 | { 460 | "cell_type": "code", 461 | "execution_count": null, 462 | "id": "2390fe26", 463 | "metadata": {}, 464 | "outputs": [], 465 | "source": [ 466 | "# Guessed function for the length scale depending on the sweep parameter\n", 467 | "def fit_func(x, a, b):\n", 468 | " return a + np.sqrt(x * b)\n", 469 | "\n", 470 | "# Fit parameters and covariance for the fit data\n", 471 | "popt, pcov = scipy.optimize.curve_fit(fit_func, sweep_parameters, sweep_results)\n", 472 | "\n", 473 | "# Visualization\n", 474 | "\n", 475 | "plt.plot(sweep_parameters, sweep_results, 'o', label='data')\n", 476 | "plt.plot(sweep_parameters, fit_func(sweep_parameters, *popt), 'g--',\n", 477 | " label='fit')\n", 478 | "plt.title('Fit $\\ell(D) = a + \\sqrt{b \\cdot D/D_0}$\\nwith $D_0$ some reference diffusion constant:\\n'+\n", 479 | " f'$a\\;=${popt[0]: .3},\\t$b\\;=${popt[1]: .4}')\n", 480 | "plt.xlabel('$D/D_0$')\n", 481 | "plt.ylabel('Characteristic length $\\ell$')\n", 482 | "plt.legend(loc=\"upper left\")\n", 483 | "plt.show()" 484 | ] 485 | }, 486 | { 487 | "cell_type": "markdown", 488 | "id": "84f2429a", 489 | "metadata": {}, 490 | "source": [ 491 | "### Solution 6\n", 492 | "From comparing the units, we know that the characteristic length scales as a square root function with the diffusion constant. Indeed, we can fit such a function to the data sweep.\n", 493 | "\n", 494 | "The sweep has two obvious limitations: for too small diffusion constants (i.e., too small length scales), the spatial resolution of the numeric analysis is too low to properly identify the centers of spots, and thus no length scale can be extracted in these cases. On the other hand, once the characteristic length scale exceeds the system length, there will be only one spot in the system, so that again no characteristic length scale can be extracted.\n", 495 | "\n", 496 | "In addition, in the limit $D_i \\to 0$, the fit function fails, since no patterns can arise without diffusion. Extrapolation is therefore only meaningful in a limited parameter regime." 497 | ] 498 | }, 499 | { 500 | "cell_type": "markdown", 501 | "id": "265076d2", 502 | "metadata": {}, 503 | "source": [ 504 | "### Problem 7: (Non-)Linearities\n", 505 | "The PDEs of the Fitzhugh-Nagumo model include a nonlinear term, namely $-u^3$. This nonlinearity is essential for the formation of patterns here. Test this explicitly by implementing a variant of the Fitzhugh-Nagumo model where the nonlinear term is replaced by $-u^3 \\rightarrow -u^n$, with $n$ an integer exponent. In particular, test the cases $n \\in \\{1, 3, 5\\}$, and describe your results. Give an explanation why you observe patterns in some cases, and why not in others." 506 | ] 507 | }, 508 | { 509 | "cell_type": "code", 510 | "execution_count": null, 511 | "id": "3d167013", 512 | "metadata": { 513 | "scrolled": false 514 | }, 515 | "outputs": [], 516 | "source": [ 517 | "for n in [1, 3, 5]:\n", 518 | "\n", 519 | " D_u = 0.1 \n", 520 | " D_v = 1.0 \n", 521 | "\n", 522 | " alpha = 1.0\n", 523 | " kappa = 0.1\n", 524 | " sigma = 1.0 \n", 525 | "\n", 526 | " expr = {'u' : f'laplace(u) * {D_u} + {alpha} * u - u' + \" * u\" * (n-1) + f'- {kappa} - {sigma} * v',\n", 527 | " 'v' : f'laplace(v) * {D_v} + u - v'}\n", 528 | "\n", 529 | "\n", 530 | " # Construct the field objects\n", 531 | " u = pde.ScalarField.random_uniform(grid) * 2. - 1.\n", 532 | " v = pde.ScalarField.random_uniform(grid) * 2. - 1.\n", 533 | "\n", 534 | "\n", 535 | " # Create the PDE object\n", 536 | " eq = pde.PDE(expr)\n", 537 | "\n", 538 | " # Set the initial values for all fields, and show it\n", 539 | " field = pde.FieldCollection([u,v])\n", 540 | "\n", 541 | " res = eq.solve(field, t_range=200, tracker=[])\n", 542 | " res.plot(title=f\"n = {n}\");" 543 | ] 544 | }, 545 | { 546 | "cell_type": "markdown", 547 | "id": "bcdd0e00", 548 | "metadata": {}, 549 | "source": [ 550 | "### Solution 7\n", 551 | "For $n=1$, the system decays to an approximately homogeneous state. For $n=2$, the system is not bistable anymore, and apart from not making a pattern, it also diverges. For odd $n>1$, patterns are possible.\n", 552 | "\n", 553 | "As a rule of thumb, the number of fixed points in a system is determined by the highest power in the PDEs. For $n=1$, there is only one fixed point. If the fixed point is stable, then the system converges to the fixed point, and if it is unstable, then the system diverges to $\\pm \\infty$, so no (stable) pattern is observed. For even orders, the system is not symmetric with respect to sign flips $u \\to -u$, so that the system will diverge again. Only for odd orders larger than $1$, there are sufficiently many fixed points to prevent divergence while at the same time allowing for an unstable fixed point, so that in this case patterns are possible." 554 | ] 555 | }, 556 | { 557 | "cell_type": "markdown", 558 | "id": "75df1baa", 559 | "metadata": {}, 560 | "source": [ 561 | "## Part 4: A variation of the system (Optional)\n", 562 | "### Problem 8: Dynamic patterns\n", 563 | "The pattern you analysed above is a stationary pattern, i.e. it will remain the same if you continue simulating for much longer times. For a different parameter regime, the Fitzhugh-Nagumo model shows dynamic patterns that reflect the 'neuron spiking' idea of the model. While these dynamic patterns are particularly difficult to analyse, they are at the same time very nice to look at. The code below simulates such a dynamic pattern. Run and enjoy!" 564 | ] 565 | }, 566 | { 567 | "cell_type": "code", 568 | "execution_count": null, 569 | "id": "faf5b8a7", 570 | "metadata": {}, 571 | "outputs": [], 572 | "source": [ 573 | "D_u = 0.01\n", 574 | "D_v = 0.01\n", 575 | "\n", 576 | "alpha = 2.0\n", 577 | "kappa = 0.0\n", 578 | "sigma = 2.0\n", 579 | "\n", 580 | "expr = {'u' : f'laplace(u) * {D_u} + {alpha} * u - u * u * u - {kappa} - {sigma} * v',\n", 581 | " 'v' : f'laplace(v) * {D_v} + u - v'}\n", 582 | "\n", 583 | "# Construct the field objects\n", 584 | "u = pde.ScalarField.random_uniform(grid) * 2. - 1.\n", 585 | "v = pde.ScalarField.random_uniform(grid) * 2. - 1.\n", 586 | "\n", 587 | "\n", 588 | "# Create the PDE object\n", 589 | "eq = pde.PDE(expr)\n", 590 | "\n", 591 | "# Set the initial values for all fields, and show it\n", 592 | "field = pde.FieldCollection([u,v])\n", 593 | "field.plot();\n", 594 | "\n", 595 | "# Simulate the system for 100 time steps, and show the result\n", 596 | "storage = pde.MemoryStorage()\n", 597 | "\n", 598 | "res = eq.solve(field, t_range=100, dt = 0.01, tracker=[storage.tracker(0.2), \"progress\"])\n", 599 | "res.plot();\n" 600 | ] 601 | }, 602 | { 603 | "cell_type": "code", 604 | "execution_count": null, 605 | "id": "3a1d26a5", 606 | "metadata": {}, 607 | "outputs": [], 608 | "source": [ 609 | "# make a movie of the dynamic pattern\n", 610 | "pde.movie(storage, 'dynamic_pattern.mp4', progress=True)" 611 | ] 612 | } 613 | ], 614 | "metadata": { 615 | "kernelspec": { 616 | "display_name": "Python 3 (ipykernel)", 617 | "language": "python", 618 | "name": "python3" 619 | }, 620 | "language_info": { 621 | "codemirror_mode": { 622 | "name": "ipython", 623 | "version": 3 624 | }, 625 | "file_extension": ".py", 626 | "mimetype": "text/x-python", 627 | "name": "python", 628 | "nbconvert_exporter": "python", 629 | "pygments_lexer": "ipython3", 630 | "version": "3.9.9" 631 | } 632 | }, 633 | "nbformat": 4, 634 | "nbformat_minor": 5 635 | } 636 | -------------------------------------------------------------------------------- /solution-notebooks/day2_1_pole-to-pole_solution.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "6af6e5c4", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "# This notebook requires py-pde in version 0.17.1 or later\n", 11 | "# The package can be obtained from https://github.com/zwicker-group/py-pde\n", 12 | "# Alternatively, it can be installed via pip or conda\n", 13 | "\n", 14 | "import pde\n", 15 | "\n", 16 | "# functools and make_derivative2 are required for anisotropic laplacians\n", 17 | "import functools\n", 18 | "from pde.grids.operators.common import make_derivative2\n", 19 | "\n", 20 | "# plotting functions\n", 21 | "import matplotlib.pyplot as plt\n", 22 | "import numpy as np" 23 | ] 24 | }, 25 | { 26 | "cell_type": "markdown", 27 | "id": "7a3a6851", 28 | "metadata": {}, 29 | "source": [ 30 | "# Pole-to-pole oscillations\n", 31 | "In the previous two notebooks, you learnt about the relevance of membrane interactions and how this can lead to gradients in the bulk (volume, cytosol) of a cell, and about the importance of nonlinearities in the interaction terms. Now, we will combine these two effects to reproduce a prominent example of dynamic protein pattern in living cells: the pole-to-pole oscillations of the MinCDE system in _E. coli_.\n", 32 | "\n", 33 | "The MinCDE system regulates the placement of the division site in _E. coli_ bacteria. More precisely, the MinC protein inhibits the formation of the Z-ring. By oscillating between the poles, the MinC proteins is mainly located at the cell poles, and rarely at the cell center. Thus, the inhibitory effect is weakest at the cell center, such that the Z-ring forms just there, resulting in symmetric cell division.\n", 34 | "\n", 35 | "If you are unfamiliar with the system, have a look at https://static-movie-usa.glencoesoftware.com/mp4/10.7554/384/2cd69eb620315bb0a04c26d92e1300548d3bca6d/elife-03949-media1.mp4 (from Zieske and Schwille, eLife, 2014, elifesciences.org/articles/03949). The video shows protein concentrations in _E. coli_ cells of varying aspect ratio. In nature, _E. coli_ have an aspect ratio of about $5$ just before cell division, for which the proteins oscillate between the two cell poles (e.g., column 1, row 4 in the linked movie). In the following, we will aim to reproduce these oscillations in simulations.\n", 36 | "\n", 37 | "The Min protein oscillations require only the MinD and MinE proteins; the MinC protein is needed for downstream regulation of the Z-ring, but not to maintain the patterning process. Thus, we will only use the former two proteins in the following." 38 | ] 39 | }, 40 | { 41 | "cell_type": "markdown", 42 | "id": "1bade711", 43 | "metadata": {}, 44 | "source": [ 45 | "The Min proteins interact with each other as follows:\n", 46 | " - MinD exists in three states: bound to the membrane ($c_d$, `c_d`), as well as in the cytosol bound to ATP ($c_{DT}$, `c_DT`) and to ADP ($c_{DD}$, `c_DD`).\n", 47 | " - MinE is present in the cytosol ($c_E$, `c_E`).\n", 48 | " - On the membrane, MinD and MinE can form a complex ($c_{de}$, `c_de`).\n", 49 | "\n", 50 | "\n", 51 | " - MinD-ATP binds to the membrane at a rate $k_D$. In addition, it is recruited to the membrane by other membrane-bound MinD proteins at a rate $k_{dD}$.\n", 52 | " - MinE is recruited by MinD to the membrane at a rate $k_{dE}$. Whenever a MinE protein is recruited, it forms a complex together with the MinD protein ($c_{de}$).\n", 53 | " - MinDE complexes detach from the membrane at a rate $k_{de}$. Upon detachment, the complex breaks up, releasing a MinE protein and a MinD-ADP protein into the cytosol. Note that the MinD-ADP proteins can not bind to the membrane (see above).\n", 54 | " - Finally, in the cytosol, MinD-ADP is converted to MinD-ATP at a rate $k_{nucEx}$, corresponding to nucleotide exchange." 55 | ] 56 | }, 57 | { 58 | "cell_type": "markdown", 59 | "id": "2a49a7cd", 60 | "metadata": {}, 61 | "source": [ 62 | "The corresponding partial differential equations read:\n", 63 | "$$\n", 64 | "\\partial_t c_{DD} = D_{cytD} \\cdot \\nabla^2 c_{DD} - k_{nucEx} \\cdot c_{DD} \\\\\n", 65 | "\\partial_t c_{DT} = D_{cytD} \\cdot \\nabla^2 c_{DT} + k_{nucEx} \\cdot c_{DD} \\\\\n", 66 | "\\partial_t c_{E} = D_{cytE} \\cdot \\nabla^2 c_{E} \\\\\n", 67 | "\\partial_t c_{d} = D_{mem} \\cdot \\nabla_m^2 c_{d} + c_{DT} \\cdot ( k_{D} + k_{dD} \\cdot c_d) - k_{dE} \\cdot c_E \\cdot c_d \\\\\n", 68 | "\\partial_t c_{de} = D_{mem} \\cdot \\nabla_m^2 c_{de} + k_{dE} \\cdot c_E \\cdot c_d - k_{de} \\cdot c_{de} \\, ,\n", 69 | "$$\n", 70 | "with reactive boundary conditions for the cytosolic quantities\n", 71 | "$$\n", 72 | "D_{cytD} \\, \\nabla_n c_{DD} = k_{de} \\cdot c_{de} \\\\\n", 73 | "D_{cytD} \\, \\nabla_n c_{DT} = - c_{DT} \\cdot ( k_{D} + k_{dD} \\cdot c_d) \\\\\n", 74 | "D_{cytE} \\, \\nabla_n c_{E} = k_{de} \\cdot c_{de} - k_{dE} \\cdot c_E \\cdot c_d \\, .\n", 75 | "$$\n", 76 | "\n", 77 | "As before, $\\nabla_n$ denotes the gradient perpendicular to the membrane, whereas $\\nabla_m^2$ is the diffusion operator along the membrane." 78 | ] 79 | }, 80 | { 81 | "cell_type": "markdown", 82 | "id": "82fcd82e", 83 | "metadata": {}, 84 | "source": [ 85 | "## Part 1: Analysing the problem heuristically" 86 | ] 87 | }, 88 | { 89 | "cell_type": "markdown", 90 | "id": "caa75312", 91 | "metadata": {}, 92 | "source": [ 93 | "### Problem 1: Nonlinearities\n", 94 | "Note down all the nonlinear terms in the PDEs above." 95 | ] 96 | }, 97 | { 98 | "cell_type": "markdown", 99 | "id": "c5984082", 100 | "metadata": {}, 101 | "source": [ 102 | "### Solution 1\n", 103 | "Nonlinarities appear where two (or more) concentrations are multiplied with each other: $k_{dE} \\cdot c_E \\cdot c_d$, and $k_{dD} \\cdot c_{DT} \\cdot c_d$." 104 | ] 105 | }, 106 | { 107 | "cell_type": "markdown", 108 | "id": "2a8f6b4e", 109 | "metadata": {}, 110 | "source": [ 111 | "### Problem 2: Intuitive analysis\n", 112 | "Consider the states where MinD and MinE proteins are homogeneously distributed\n", 113 | " - both on the membrane (only $c_{de}$ is non-zero),\n", 114 | " - on the membrane (MinD, $c_d \\neq 0$) and in the cytosol (MinE, $c_E \\neq 0$, all other quantities are zero),\n", 115 | " - both in the cytosol (ATP-bound), i.e., $c_{DD} = c_d = c_{de} = 0$, and the other two quantities are non-zero.\n", 116 | " \n", 117 | "For each of these cases, describe (in words) the protein dynamics in this state.\n", 118 | "\n", 119 | "_Hint: use phrases like \"MinD proteins bind to the membrane\", \"additional MinD is recruited to the membrane\", \"MinDE complexes detach from the membrane\", etc._" 120 | ] 121 | }, 122 | { 123 | "cell_type": "markdown", 124 | "id": "52cae7b5", 125 | "metadata": {}, 126 | "source": [ 127 | "### Solution 2\n", 128 | " - MinDE complexes detach from the membrane, replenishing the cytosolic reservoir of MinE and MinD-ADP. MinD-ADP is then converted to MinD-ATP over time.\n", 129 | " - MinD on the membrane recruits MinE from the cytosol, forming MinDE complexes which then detach from the membrane and replenish the reservoir of MinD and MinE in the cytosol.\n", 130 | " - MinD in the cytosol will bind to the membrane. Membrane-bound MinD recruits more MinD from the cytosol, and also MinE." 131 | ] 132 | }, 133 | { 134 | "cell_type": "markdown", 135 | "id": "7ac9e420", 136 | "metadata": {}, 137 | "source": [ 138 | "## Part 2: Testing the system numerically\n", 139 | "### Problem 3\n", 140 | "The system involves reactive boundary conditions, diffusion on curved membranes, and is extremely sensitive to protein concentration numbers. This all makes a numeric implementation rather tedious, and requires some tinkering at several points. Walk through the following code carefully, and make a list of the steps and pitfalls required when translating a formal set of equations into real code. You are also required to complete (i.e., fill in blanks) the expressions representing the PDEs." 141 | ] 142 | }, 143 | { 144 | "cell_type": "markdown", 145 | "id": "6354b419", 146 | "metadata": {}, 147 | "source": [ 148 | "#### Geometry, grid, and concentrations:\n", 149 | "The Min system is extremely sensitive to variations in average protein concentrations. This means that pole-to-pole oscillations can be observed, for example, at a specific protein concentration $c_E^*$, but for slightly varied concentrations ($\\pm 10 \\%$) the oscillations break down. This poses a major problem for numerical implementation: since the grid is discrete and not continuous, it is not straightforward to define a concentration that respects the conservation of mass and at the same time does not alter the PDEs. In general, this issue can be resolved by carefully rescaling all parameters and quantities based on the mesh resolution. For simplicity, however, we cheat here and use a geometry that happens to have the correct scaling (i.e., one where the grid cells have resolution 1).\n", 150 | "\n", 151 | "In nature, _E. coli_ cells are $5 \\mu m$ long and $1 \\mu m$ wide. To observe a pattern, we need about five grid cells (preferably much more) in each direction. Instead of the \"natural\" geometry of the cells, we therefore blow up the geometry by a factor 5, so that we can have sufficiently high resolution of the grid cells while keeping the resolution parameter at 1." 152 | ] 153 | }, 154 | { 155 | "cell_type": "code", 156 | "execution_count": null, 157 | "id": "ba604df2", 158 | "metadata": {}, 159 | "outputs": [], 160 | "source": [ 161 | "# Define the geometry of the system.\n", 162 | "# We use a regular grid (rectangle) with a specified width (x-direction) and height (y-direction).\n", 163 | "\n", 164 | "width = 25\n", 165 | "height = 5\n", 166 | "resolution = 1\n", 167 | "\n", 168 | "# pde.CartesianGrid() creates a grid object (mesh) specified by the rectangle corners and a sampling resolution for both axes.\n", 169 | "grid = pde.CartesianGrid(\n", 170 | " [[0, width], [0, height]],\n", 171 | " [int(resolution*i) for i in [width, height]]\n", 172 | ")\n", 173 | "grid.plot();" 174 | ] 175 | }, 176 | { 177 | "cell_type": "markdown", 178 | "id": "fda1dfe6", 179 | "metadata": {}, 180 | "source": [ 181 | "#### Membrane masking:\n", 182 | "To simulate the membrane dynamics, `py-pde` requires a mask, like in the source-degradation exercise. Now, however, the situation is more complex, since the membrane now encloses the cell (instead of being just one horizontal line). On the horizontal cell membranes, there will be only diffuxion in x-direction, whereas on the vertical membrane there is diffusion in y-direction. To account for this, we define two separate masks, `mask_x` and `mask_y`." 183 | ] 184 | }, 185 | { 186 | "cell_type": "code", 187 | "execution_count": null, 188 | "id": "2fbf9c6c", 189 | "metadata": {}, 190 | "outputs": [], 191 | "source": [ 192 | "membrane_mask = pde.ScalarField(grid, dtype=float)\n", 193 | "membrane_mask.data[:, 0] = 1.0\n", 194 | "membrane_mask.data[:, -1] = 1.0\n", 195 | "membrane_mask.data[0,:] = 1.0\n", 196 | "membrane_mask.data[-1,:] = 1.0\n", 197 | "\n", 198 | "mask_x, mask_y = [pde.ScalarField(grid, dtype=float) for _ in range(2)]\n", 199 | "mask_x.data[:,0] = 1.0\n", 200 | "mask_x.data[:,-1] = 1.0\n", 201 | "mask_y.data[0,:] = 1.0\n", 202 | "mask_y.data[-1,:] = 1.0\n", 203 | "\n", 204 | "\n", 205 | "membrane_mask.plot();\n", 206 | "mask_x.plot();\n", 207 | "mask_y.plot();" 208 | ] 209 | }, 210 | { 211 | "cell_type": "code", 212 | "execution_count": null, 213 | "id": "248f3ea2", 214 | "metadata": {}, 215 | "outputs": [], 216 | "source": [ 217 | "# Set up laplace operator in x direction only\n", 218 | "make_laplace_x = functools.partial(make_derivative2, axis=0)\n", 219 | "pde.CartesianGrid.register_operator('laplace_x', make_laplace_x)\n", 220 | "\n", 221 | "# Set up laplace operator in y direction only\n", 222 | "make_laplace_y = functools.partial(make_derivative2, axis=1)\n", 223 | "pde.CartesianGrid.register_operator('laplace_y', make_laplace_y)\n" 224 | ] 225 | }, 226 | { 227 | "cell_type": "markdown", 228 | "id": "3d796a39", 229 | "metadata": {}, 230 | "source": [ 231 | "#### Defining the system:\n", 232 | "Now we can translate the PDEs into code. For the parameters, we use values obtained in Halatek and Frey, Cell, 2012 (cell.com/cell-reports/fulltext/S2211-1247(12)00118-0). These are in parts determined experimentally, and in parts via numeric parameter sweeps." 233 | ] 234 | }, 235 | { 236 | "cell_type": "code", 237 | "execution_count": null, 238 | "id": "83633fa3", 239 | "metadata": {}, 240 | "outputs": [], 241 | "source": [ 242 | "# Parameters\n", 243 | "D_cytD = 16\n", 244 | "D_cytE = 10\n", 245 | "D_mem = 0.01\n", 246 | "\n", 247 | "k_nucEx = 6\n", 248 | "\n", 249 | "k_D = 0.1\n", 250 | "k_dD = 0.1\n", 251 | "k_dE = 0.4\n", 252 | "k_de = 0.4\n", 253 | "\n", 254 | "nD = 2000\n", 255 | "nE = 700\n", 256 | "\n", 257 | "# Equations with boundary conditions\n", 258 | "expr = {'c_d' : f'(laplace_x(c_d ) * maskX + laplace_y(c_d ) * maskY) * {D_mem} + ' + # Diffusion\n", 259 | " f'mask * (c_DT * ({k_D} + {k_dD} * c_d) - {k_dE} * c_E * c_d)', # Reaction\n", 260 | " 'c_de': f'(laplace_x(c_de) * maskX + laplace_y(c_de) * maskY) * {D_mem} + ' + # Diffusion\n", 261 | " f'mask * ({k_dE} * c_E * c_d - {k_de} * c_de)', # Reaction\n", 262 | " \n", 263 | " 'c_E': f'laplace(c_E) * {D_cytE} + ' + # Diffusion\n", 264 | " f'mask * ({k_de} * c_de - {k_dE} * c_E * c_d)', # Boundary\n", 265 | " 'c_DD': f'laplace(c_DD) * {D_cytD} - {k_nucEx} * c_DD + ' + # Diffusion\n", 266 | " f'mask * ({k_de} * c_de)', # Boundary\n", 267 | " 'c_DT': f'laplace(c_DT) * {D_cytD} + {k_nucEx} * c_DD + ' + # Diffusion\n", 268 | " f'mask * (-c_DT * ({k_D} + {k_dD} * c_d))'} # Boundary\n", 269 | "\n", 270 | "# Set the initial values for all fields:\n", 271 | "# To keep deviations from the total mass minimal, we randomize only two fields.\n", 272 | "c_d = pde.ScalarField.random_uniform(grid) * 0.0\n", 273 | "c_de = pde.ScalarField.random_uniform(grid) * 0.0\n", 274 | "c_E = pde.ScalarField.random_uniform(grid) * 1.0 - 0.5\n", 275 | "c_DD = pde.ScalarField.random_uniform(grid) * 1.0 - 0.5\n", 276 | "c_DT = pde.ScalarField.random_uniform(grid) * 0.0\n", 277 | "\n", 278 | "# Introduce an initial asymmetry to speed up the simulations:\n", 279 | "## To ensure mass conservation later on, we need to introduce an auxilary variable.\n", 280 | "nodeCount = grid.shape[0] * grid.shape[1]\n", 281 | "\n", 282 | "## Now we place some MinD on the left side of the membrane\n", 283 | "c_d.data[:8,:] = 5\n", 284 | "c_d.data = c_d.data * membrane_mask\n", 285 | "\n", 286 | "## To ensure mass conservation, we need to remove the MinD proteins that we just placed\n", 287 | "## on the membrane from the cytosol, and evenly distribute the remaining MinD in the \n", 288 | "## MinD-ADP state.\n", 289 | "c_DD.data[:,:] += (nD - sum(sum(c_d.data))) / nodeCount\n", 290 | "c_E.data[:,:] += nE / nodeCount\n", 291 | "\n", 292 | "\n", 293 | "# Now solve the system as usual:\n", 294 | "eq = pde.PDE(expr, consts={'mask': membrane_mask, 'maskX': mask_x, 'maskY': mask_y})\n", 295 | "\n", 296 | "field = pde.FieldCollection([c_d, c_de, c_E, c_DD, c_DT])\n", 297 | "field.plot();\n", 298 | "\n", 299 | "storage = pde.MemoryStorage()\n", 300 | "\n", 301 | "res = eq.solve(field, t_range=2000, tracker=[storage.tracker(1), \"progress\", \"plot\"], backend='numpy')\n", 302 | "res.plot();" 303 | ] 304 | }, 305 | { 306 | "cell_type": "markdown", 307 | "id": "7d1a9553", 308 | "metadata": {}, 309 | "source": [ 310 | "## Part 3: Evaluation" 311 | ] 312 | }, 313 | { 314 | "cell_type": "markdown", 315 | "id": "b322a81c", 316 | "metadata": {}, 317 | "source": [ 318 | "### Problem 4: Basic consistency checks\n", 319 | "To make sure that there are no typos in our code, it is good practice to make consitency checks on the simulation results. These consist of calculating/plotting quantities from the data that we can also compute analytically: if the results don't match the expectations, then something went wrong.\n", 320 | "\n", 321 | "A simple consistency check for mass-conserving systems is the conservation of mass. Plot the _total_ MinD mass and MinE mass over time, and check whether the mass is conserved. Explain observed deviations from mass conservation.\n", 322 | "\n", 323 | "_Hint: the total mass is computed by adding the concentrations of all corresponding fields and summing over all vertices._\n", 324 | "\n", 325 | "_Hint: note that the concentration of MinDE complexes contributes to the MinD mass as well as to the MinE mass._" 326 | ] 327 | }, 328 | { 329 | "cell_type": "code", 330 | "execution_count": null, 331 | "id": "e96a855f", 332 | "metadata": {}, 333 | "outputs": [], 334 | "source": [ 335 | "times = np.arange(len(storage.data))\n", 336 | "\n", 337 | "# This is a check whether the total mass is conserved. The entries in each sublist should remain constant (within factor 0.001).\n", 338 | "# The first row is the total MinD mass (c_d + c_de + c_DD + c_DT = nD), the second row is the total MinE mass (c_de + c_E = nE).\n", 339 | "MinD_mass = np.array([sum([sum(sum(a)) for a in [storage.data[t][i] for i in [0,1,3,4]]]) for t in times])\n", 340 | "MinE_mass = np.array([sum([sum(sum(a)) for a in [storage.data[t][i] for i in [1,2]]]) for t in times])\n", 341 | "\n", 342 | "plt.plot(times, MinD_mass, label=\"MinD\")\n", 343 | "plt.plot(times, MinE_mass, label=\"MinE\")\n", 344 | "plt.legend()\n", 345 | "plt.title(\"Total MinD/MinE masses\")\n", 346 | "plt.show()\n", 347 | "\n", 348 | "plt.plot(times, (MinD_mass - MinD_mass[0])/nD, label=\"MinD\")\n", 349 | "plt.plot(times, (MinE_mass - MinE_mass[0])/nE, label=\"MinE\")\n", 350 | "plt.legend()\n", 351 | "plt.title(\"Relative deviation from\\nthe initial total masses\")\n", 352 | "plt.show()\n" 353 | ] 354 | }, 355 | { 356 | "cell_type": "markdown", 357 | "id": "550fd645", 358 | "metadata": {}, 359 | "source": [ 360 | "### Problem 4: Concentration plot\n", 361 | "To visualize our results and be able to interpret them, make a plot of the MinD concentration on the membrane (`c_d`) over time.\n", 362 | "\n", 363 | "_Hint: choose a specific pixel, and extract the concentration at this pixel over time._" 364 | ] 365 | }, 366 | { 367 | "cell_type": "code", 368 | "execution_count": null, 369 | "id": "9eb6fcba", 370 | "metadata": { 371 | "scrolled": true 372 | }, 373 | "outputs": [], 374 | "source": [ 375 | "times = np.arange(len(storage.data))\n", 376 | "MinD_concentration = np.array([[storage.data[t][0][0][2]] for t in times])\n", 377 | "plt.plot(times, MinD_concentration)\n", 378 | "plt.show()" 379 | ] 380 | }, 381 | { 382 | "cell_type": "markdown", 383 | "id": "bbf855ca", 384 | "metadata": {}, 385 | "source": [ 386 | "### Problem 5: Kymograph\n", 387 | "A very suitable representation of the simulation data is a kymograph. In a kymograph, one axis represents time, and the other axis represents a 1D slice through the system. Thus, the kymograph shows a concentration along this slice over time.\n", 388 | "\n", 389 | "Make a kymograph of the MinD-ADP concentration along the horizontal center axis of the system.\n", 390 | "\n", 391 | "_Hint: due to the specific choice of initial conditions, the first few time steps distort the color range. Consider excluding the first 5 time steps in the kymograph for better visibility._" 392 | ] 393 | }, 394 | { 395 | "cell_type": "code", 396 | "execution_count": null, 397 | "id": "2aa378f3", 398 | "metadata": {}, 399 | "outputs": [], 400 | "source": [ 401 | "kymo = []\n", 402 | "for i in range(len(storage.data)):\n", 403 | " if (i > 5):\n", 404 | " kymo.append(storage.data[i][0][:,0])\n", 405 | "kymo = np.array(kymo)\n", 406 | "\n", 407 | "\n", 408 | "\n", 409 | "plt.imshow(kymo.T, aspect=20);\n", 410 | "plt.title(\"MinD-ADP concentration\")\n", 411 | "plt.xlabel(\"time (s)\")\n", 412 | "plt.ylabel(\"x $(\\mu m)$\")\n", 413 | "plt.show();" 414 | ] 415 | }, 416 | { 417 | "cell_type": "markdown", 418 | "id": "dc750a14", 419 | "metadata": {}, 420 | "source": [ 421 | "Make kymographs for the MinD-ATP concentration, the MinE concentration, as well as the two membrane concentrations.\n", 422 | "\n", 423 | "_Hint: the membrane concentrations should be evaluated on the membrane, not at a central slize._" 424 | ] 425 | }, 426 | { 427 | "cell_type": "code", 428 | "execution_count": null, 429 | "id": "b17611f0", 430 | "metadata": {}, 431 | "outputs": [], 432 | "source": [ 433 | "def full_kymograph(data, aspect_ratio_rescaling = 1):\n", 434 | "\n", 435 | " lst = [(\"MinD-ADP\", 3, 2), (\"MinD-ATP\", 4, 2), (\"MinE\", 2, 2), (\"MinD membrane\", 0, 0), (\"MinDE membrane\", 1, 0)]\n", 436 | "\n", 437 | " for lbl, index, pos in lst:\n", 438 | " kymo = []\n", 439 | " for i in range(len(data)):\n", 440 | " if (i > 5):\n", 441 | " kymo.append(data[i][index][:,pos])\n", 442 | " kymo = np.array(kymo)\n", 443 | "\n", 444 | "\n", 445 | "\n", 446 | " plt.imshow(kymo.T, aspect=20/aspect_ratio_rescaling);\n", 447 | " plt.title(lbl + \" concentration\")\n", 448 | " plt.xlabel(\"time (s)\")\n", 449 | " plt.ylabel(\"x $(\\mu m)$\")\n", 450 | " plt.show();\n", 451 | "\n", 452 | "full_kymograph(storage.data)" 453 | ] 454 | }, 455 | { 456 | "cell_type": "markdown", 457 | "id": "ac6b2582", 458 | "metadata": {}, 459 | "source": [ 460 | "### Problem 6: Length variations\n", 461 | "As you can see in the video of _E. coli_ cells linked above, different oscillation \"modes\" can be observed for different cell aspect ratios: for short cells, the desired pole-to-pole oscillations are obtained, but for long cells, multiple oscillations are present simultaneously. In the following, we will try to reproduce these observations." 462 | ] 463 | }, 464 | { 465 | "cell_type": "code", 466 | "execution_count": null, 467 | "id": "8e04717a", 468 | "metadata": {}, 469 | "outputs": [], 470 | "source": [ 471 | "def simulate_with_different_length(rescale_factor):\n", 472 | "\n", 473 | " width = 25 * rescale_factor ## BLANK ##\n", 474 | " height = 5 ## BLANK ##\n", 475 | " resolution = 1\n", 476 | "\n", 477 | " \n", 478 | " grid = pde.CartesianGrid(\n", 479 | " [[0, width], [0, height]],\n", 480 | " [int(resolution*i) for i in [width, height]]\n", 481 | " )\n", 482 | " grid.plot();\n", 483 | "\n", 484 | "\n", 485 | " membrane_mask = pde.ScalarField(grid, dtype=float)\n", 486 | " membrane_mask.data[:, 0] = 1.0\n", 487 | " membrane_mask.data[:, -1] = 1.0\n", 488 | " membrane_mask.data[0,:] = 1.0\n", 489 | " membrane_mask.data[-1,:] = 1.0\n", 490 | "\n", 491 | " mask_x, mask_y = [pde.ScalarField(grid, dtype=float) for _ in range(2)]\n", 492 | " mask_x.data[:,0] = 1.0\n", 493 | " mask_x.data[:,-1] = 1.0\n", 494 | " mask_y.data[0,:] = 1.0\n", 495 | " mask_y.data[-1,:] = 1.0\n", 496 | "\n", 497 | " # Parameters\n", 498 | " D_cytD = 16\n", 499 | " D_cytE = 10\n", 500 | " D_mem = 0.01\n", 501 | "\n", 502 | " k_nucEx = 6\n", 503 | "\n", 504 | " k_D = 0.1\n", 505 | " k_dD = 0.1\n", 506 | " k_dE = 0.4\n", 507 | " k_de = 0.4\n", 508 | "\n", 509 | " nD = 2000 * rescale_factor\n", 510 | " nE = 700 * rescale_factor\n", 511 | "\n", 512 | " expr = {'c_d' : f'(laplace_x(c_d ) * maskX + laplace_y(c_d ) * maskY) * {D_mem} + ' + # Diffusion\n", 513 | " f'mask * (c_DT * ({k_D} + {k_dD} * c_d) - {k_dE} * c_E * c_d)', # Reaction\n", 514 | " 'c_de': f'(laplace_x(c_de) * maskX + laplace_y(c_de) * maskY) * {D_mem} + ' + # Diffusion\n", 515 | " f'mask * ({k_dE} * c_E * c_d - {k_de} * c_de)', # Reaction\n", 516 | "\n", 517 | " 'c_E': f'laplace(c_E) * {D_cytE} + ' + # Diffusion\n", 518 | " f'mask * ({k_de} * c_de - {k_dE} * c_E * c_d)', # Boundary\n", 519 | " 'c_DD': f'laplace(c_DD) * {D_cytD} - {k_nucEx} * c_DD + ' + # Diffusion\n", 520 | " f'mask * ({k_de} * c_de)', # Boundary\n", 521 | " 'c_DT': f'laplace(c_DT) * {D_cytD} + {k_nucEx} * c_DD + ' + # Diffusion\n", 522 | " f'mask * (-c_DT * ({k_D} + {k_dD} * c_d))'} # Boundary\n", 523 | "\n", 524 | " c_d = pde.ScalarField.random_uniform(grid) * 0.0\n", 525 | " c_de = pde.ScalarField.random_uniform(grid) * 0.0\n", 526 | " c_E = pde.ScalarField.random_uniform(grid) * 1.0 - 0.5\n", 527 | " c_DD = pde.ScalarField.random_uniform(grid) * 1.0 - 0.5\n", 528 | " c_DT = pde.ScalarField.random_uniform(grid) * 0.0\n", 529 | "\n", 530 | " nodeCount = grid.shape[0] * grid.shape[1]\n", 531 | " c_d.data[:8,:] = 5\n", 532 | " c_d.data = c_d.data * membrane_mask\n", 533 | " c_DD.data[:,:] += (nD - sum(sum(c_d.data))) / nodeCount\n", 534 | " c_E.data[:,:] += nE / nodeCount\n", 535 | "\n", 536 | " eq = pde.PDE(expr, consts={'mask': membrane_mask, 'maskX': mask_x, 'maskY': mask_y})\n", 537 | " field = pde.FieldCollection([c_d, c_de, c_E, c_DD, c_DT])\n", 538 | "\n", 539 | " storage = pde.MemoryStorage()\n", 540 | "\n", 541 | " res = eq.solve(field, t_range=2000, tracker=[storage.tracker(1)])\n", 542 | " res.plot();\n", 543 | " return storage" 544 | ] 545 | }, 546 | { 547 | "cell_type": "code", 548 | "execution_count": null, 549 | "id": "7dff9559", 550 | "metadata": {}, 551 | "outputs": [], 552 | "source": [ 553 | "s3 = simulate_with_different_length(3.)\n", 554 | "s6 = simulate_with_different_length(6.)" 555 | ] 556 | }, 557 | { 558 | "cell_type": "code", 559 | "execution_count": null, 560 | "id": "a1858e60", 561 | "metadata": {}, 562 | "outputs": [], 563 | "source": [ 564 | "for s, i in [(s3, 3), (s6, 6)]:\n", 565 | " print(f\"{i}-fold length\")\n", 566 | " full_kymograph(s.data, aspect_ratio_rescaling=i)" 567 | ] 568 | }, 569 | { 570 | "cell_type": "code", 571 | "execution_count": null, 572 | "id": "49d78304", 573 | "metadata": {}, 574 | "outputs": [], 575 | "source": [] 576 | } 577 | ], 578 | "metadata": { 579 | "kernelspec": { 580 | "display_name": "Python 3 (ipykernel)", 581 | "language": "python", 582 | "name": "python3" 583 | }, 584 | "language_info": { 585 | "codemirror_mode": { 586 | "name": "ipython", 587 | "version": 3 588 | }, 589 | "file_extension": ".py", 590 | "mimetype": "text/x-python", 591 | "name": "python", 592 | "nbconvert_exporter": "python", 593 | "pygments_lexer": "ipython3", 594 | "version": "3.9.9" 595 | } 596 | }, 597 | "nbformat": 4, 598 | "nbformat_minor": 5 599 | } 600 | -------------------------------------------------------------------------------- /test_compatibility.sh: -------------------------------------------------------------------------------- 1 | # This script checks whether the notebooks run with current versions of the modules. 2 | # It should typically be ignored by users and are only useful for developers. 3 | # 4 | # To run, you need to install the python packages `nbmake` and `pytest-xdist`. 5 | # Note that the tests may take several minutes to run. 6 | 7 | pytest --nbmake --nbmake-timeout=1000 -n=auto solution-notebooks/*.ipynb -------------------------------------------------------------------------------- /tutorial-notebooks/day1_2_source_degradation.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "5800110e-994a-4668-9b90-a8c44e278ec1", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "# This notebook requires py-pde in version 0.17.1 or later\n", 11 | "# The package can be obtained from https://github.com/zwicker-group/py-pde\n", 12 | "# Alternatively, it can be installed via pip or conda\n", 13 | "\n", 14 | "import pde\n", 15 | "\n", 16 | "# functools and make_derivative2 are required for anisotropic laplacians\n", 17 | "import functools\n", 18 | "from pde.grids.operators.common import make_derivative2\n", 19 | "\n", 20 | "# plotting functions\n", 21 | "import matplotlib.pyplot as plt\n", 22 | "import numpy as np\n", 23 | "\n", 24 | "# fitting methods\n", 25 | "import scipy\n", 26 | "\n", 27 | "# a simple tracker to check parameter sweeps\n", 28 | "import datetime\n", 29 | "def ETA(step, maxStep, startTime):\n", 30 | " _ETA = None\n", 31 | " total_dt = 0\n", 32 | " dt = 0\n", 33 | " if step==0:\n", 34 | " _ETA = \"Indeterminate\"\n", 35 | " else:\n", 36 | " dt = datetime.datetime.now() - startTime\n", 37 | " dt = dt.seconds\n", 38 | " total_dt = dt/step * maxStep\n", 39 | " _ETA = (startTime + datetime.timedelta(seconds = total_dt))\n", 40 | " _ETA = str(_ETA.time())\n", 41 | " \n", 42 | " print(f\"{int(100 * step / maxStep):>3} % completed. ETA: {_ETA} ({int(total_dt - dt)} seconds remain).\" + '\\t' * 5,\n", 43 | " end='\\r')" 44 | ] 45 | }, 46 | { 47 | "cell_type": "markdown", 48 | "id": "62f71aa4-682e-48ba-803c-52f02edff5ec", 49 | "metadata": {}, 50 | "source": [ 51 | "# A simple source-degradation process\n", 52 | "In this task, you will explore how chemical gradients in biological systems can emerge from a local source with a global degradation process. Along the lines, you will learn some neat functionalities of the py-pde package.\n", 53 | "\n", 54 | "Consider the following situation: a cell's membrane, constantly produces some chemical $c$ at a rate $k_{prod}$. This could be a protein complex that is created on the membrane, or an external chemical that is constantly uptaken by the membrane. This chemical -- whose concentration is denoted by $c_m(x,t)$ -- can move on the membrane diffusively (diffusion constant $D_m$). At a rate $k$, these chemicals detach from the membrane into the cell's volume (cytosol). In the cytosol, this chemical $c_b(x,y,t)$ diffuses as well ($D_c$), and it is degraded at a rate $k_{degr}$, for example due to (de-)phosphorylation. We look at a small (flat) region of the cell's membrane and cytosol in 2D. Here, the cytosol is a 2D plane (parameterized by the coordinates $x$ and $y$), and the membrane is a 1D line located at the boundary of the cytosol, parameterized by the coordinate $x$. More precisely, the membrane is located at height $y=0$. The differential equations corresponding to this situation are\n", 55 | "\n", 56 | "$$\n", 57 | "\\partial_t c_m(x,t) = D_m \\nabla_x^2 c_m(x,t) + k_{prod} - k \\cdot c_m(x,t) \\, ,\\\\\n", 58 | "\\partial_t c_b(x,y,t) = D_c \\nabla^2 c_b(x,y,t) - k_{degr} \\cdot c_b(x,y,t) \\, .\n", 59 | "$$\n", 60 | "\n", 61 | "Note that the operators representing the diffusion on the membrane ($\\nabla_x$) and in the cytosol ($\\nabla$) are distinct, and require specific numerical treatments.\n", 62 | "\n", 63 | "Mathematically, the coupling of the membrane to the cytosol dynamics (i.e., the influx of particles into the cytosol from the membrane) is denoted as a Neumann boundary condition\n", 64 | "\n", 65 | "$$ \n", 66 | "-D_c \\, \\partial_y c_b(x,0,t) = k \\cdot c_m(x,t) \\, .\n", 67 | "$$\n", 68 | "\n", 69 | "In general, such boundary conditions are difficult to implement numerically (there is dedicated software that takes care of them), and are not very intuitive. Here, we use a much simpler and more intuitive approach to couple the membrane and cytosolic dynamics: we add to the cytosolic dynamics a space-dependent reaction rate, which allows the creation ('influx') of chemicals only at a restricted area in the cytosol. Naturally, this restricted area will be very close to the boundary. These modified equations now read\n", 70 | "\n", 71 | "$$\n", 72 | "\\partial_t c_m(x,t) = D_m \\nabla_x^2 c_m(x,t) + k_{prod} - k \\cdot c_m(x,t) \\, ,\\\\\n", 73 | "\\partial_t c_b(x,y,t) = D_c \\nabla^2 c_b(x,y,t) - k_{degr} \\cdot c_b(x,y,t) + m(y) \\cdot k \\cdot c_m(x,t) \\, ,\n", 74 | "$$\n", 75 | "\n", 76 | "where $m(y)$ represents a mask with $m(0) = 1$ (close to the membrane) and $m(y) = 0$ everywhere else." 77 | ] 78 | }, 79 | { 80 | "cell_type": "markdown", 81 | "id": "b5e643d9-2fac-4872-8162-a2fa1eebf8b8", 82 | "metadata": {}, 83 | "source": [ 84 | "## Part 1: Analysing the problem analytically\n", 85 | "In today's lecture, you learnt about characteristic profiles and length scales of such source degradation processes. Before starting to simulate the problem, let's think about what we expect.\n", 86 | "\n", 87 | "### Problem 1\n", 88 | "Make a sketch of the profile of the cytosolic concentration perpendicular to the membrane, $c_b(0,y,t)$, at the steady state of the system. What function represents this profile? What is the concentration far away from the membrane, $c_b(0, \\infty, t)$?\n", 89 | "\n", 90 | "(Optional) Determine the steady state profile analytically. And compare it with your sketch.\n", 91 | "\n", 92 | "_Hint: you obtain the steady state by solving $\\partial_t c_b = 0$._" 93 | ] 94 | }, 95 | { 96 | "cell_type": "markdown", 97 | "id": "1d477631", 98 | "metadata": {}, 99 | "source": [ 100 | "### Problem 2\n", 101 | "The length scale $\\ell$ of the cytosolic concentration profile is (in this example) determined by a diffusion constant and a reaction rate. Which?\n", 102 | "\n", 103 | "Compare the units of the length scale $\\ell$, the diffusion constant, and the reaction rate, and use your results to obtain an equation for the length scale. " 104 | ] 105 | }, 106 | { 107 | "cell_type": "markdown", 108 | "id": "0f00e9ba", 109 | "metadata": {}, 110 | "source": [ 111 | "## Part 2: Testing the system numerically\n", 112 | "Now that we roughly know what to expect, we can solve this system by numerically solving the underlying PDEs. To this end, we will use the PDE solver provided by the ```py-pde``` solver. To give you a feeling how it works, we provide you with a working example with blanks that you need to fill in. In later exercises, you will learn how to set up the code by yourself. *Please take some time to understand the example.* Blanks that need to be filled in by you will be indicated by ```## BLANK ##```." 113 | ] 114 | }, 115 | { 116 | "cell_type": "markdown", 117 | "id": "6fb046cc", 118 | "metadata": {}, 119 | "source": [ 120 | "### Tutorial 1: How does a numerical PDE solver work?\n", 121 | " - Geometry\n", 122 | " - Mesh\n", 123 | " - Equations and Operators\n", 124 | " - Time stepping\n", 125 | " - Workarounds\n", 126 | " - Errors" 127 | ] 128 | }, 129 | { 130 | "cell_type": "markdown", 131 | "id": "9a4449ba", 132 | "metadata": {}, 133 | "source": [ 134 | "### Problem 3: Implementing the source-degradation process\n", 135 | "Fill in the ```## BLANKS ##``` to complete the code and run the simulations." 136 | ] 137 | }, 138 | { 139 | "cell_type": "code", 140 | "execution_count": null, 141 | "id": "0931cd86-7cec-424e-967e-f563ecef30ba", 142 | "metadata": {}, 143 | "outputs": [], 144 | "source": [ 145 | "# Define the geometry of the system.\n", 146 | "# We use a regular grid (rectangle) with a specified width (x-direction) and height (y-direction).\n", 147 | "\n", 148 | "width = ## BLANK ##\n", 149 | "height = ## BLANK ##\n", 150 | "resolution = (2**5)/10\n", 151 | "\n", 152 | "# pde.CartesianGrid() creates a grid object (mesh) specified by the rectangle corners and a sampling resolution for both axes.\n", 153 | "grid = pde.CartesianGrid(\n", 154 | " [[0, width], [0, height]],\n", 155 | " [int(resolution*i) for i in [width, height]]\n", 156 | ")\n", 157 | "grid.plot();" 158 | ] 159 | }, 160 | { 161 | "cell_type": "code", 162 | "execution_count": null, 163 | "id": "c398d35c-4fa6-4427-b9a9-0cdb343c7a45", 164 | "metadata": {}, 165 | "outputs": [], 166 | "source": [ 167 | "# Define a mask on the grid that represents the 1D membrane.\n", 168 | "# Here, we choose the bottom row (y=0) to be the membrane.\n", 169 | "# Note: in general, such masks are boolean arrays. For computation purposes, it is often convenient\n", 170 | "# to store them as float arrays, though, since some operations (e.g. arithmetic '-') are not well-\n", 171 | "# defined on boolean arrays\n", 172 | "\n", 173 | "membrane_mask = pde.ScalarField(grid, dtype=float)\n", 174 | "membrane_mask.data[:, ## BLANK ##] = 1.0\n", 175 | "membrane_mask.plot();" 176 | ] 177 | }, 178 | { 179 | "cell_type": "markdown", 180 | "id": "b43456a3", 181 | "metadata": {}, 182 | "source": [ 183 | "Any system with two different domains coupled to each other (in this case: cytosol and membrane) comes with certain numerical subtleties, in particular when the domains have different dimensions. One of these subtleties is the diffusion operator (second spatial derivative, laplacian), which looks fundamentally different on flat regions (e.g., a 2D cytosol) vs. curved regions (e.g., a membrane that is not perfectly flat). Here, we have a flat membrane, which saves us a lot of trouble, but nevertheless, we need to make sure that diffusion is restricted exclusively to the membrane for $c_m(x,t)$. To this end, we need to tinker a custom laplacian:" 184 | ] 185 | }, 186 | { 187 | "cell_type": "code", 188 | "execution_count": null, 189 | "id": "b7491eed-85ad-4caa-a2b4-b8a4fee39e99", 190 | "metadata": {}, 191 | "outputs": [], 192 | "source": [ 193 | "# Set up laplace operator in x direction only\n", 194 | "# (You do not need to understand the details here)\n", 195 | "make_laplace_x = functools.partial(make_derivative2, axis=0)\n", 196 | "pde.CartesianGrid.register_operator('laplace_x', make_laplace_x)\n", 197 | "\n", 198 | "# Now we test whether the custom laplacian works as intended:\n", 199 | "# We set up a random initial field on the membrane, and perform one time step.\n", 200 | "# If we did everything right, then there will be no diffusion in y-direction,\n", 201 | "# so that the field values at y>0 will remain 0.\n", 202 | "\n", 203 | "# Make a random initial field\n", 204 | "c_m = pde.ScalarField.random_uniform(grid)\n", 205 | "# Apply the mask to ensure that the field is non-zero only on the membrane\n", 206 | "c_m *= membrane_mask\n", 207 | "# Look at the field\n", 208 | "c_m.plot();\n", 209 | "# Perform a time step and look at the field again\n", 210 | "c_m.apply_operator('laplace_x', bc='auto_periodic_neumann').plot();" 211 | ] 212 | }, 213 | { 214 | "cell_type": "markdown", 215 | "id": "288a1fdd", 216 | "metadata": {}, 217 | "source": [ 218 | "Now that the numerical details are settled (geometry, mesh, and operators), we can feed the PDEs into the numerical PDE solver. Of course, we have to set values for all parameters first, since the numerical solver can not perform symbolic operations." 219 | ] 220 | }, 221 | { 222 | "cell_type": "code", 223 | "execution_count": null, 224 | "id": "3b0cfd18-6631-4b8e-a95b-335feb71eac8", 225 | "metadata": {}, 226 | "outputs": [], 227 | "source": [ 228 | "# Define parameters\n", 229 | "k_prod = 1. # production in membrane\n", 230 | "k = 0.1 # detachment rate\n", 231 | "k_degr = 1. # degradation in the cytosol\n", 232 | "\n", 233 | "D_c = 1. # diffusion in the cytosol\n", 234 | "D_m = 0.1 # diffusion on the membrane\n", 235 | "\n", 236 | "# Define the PDEs: l.h.s. specifies the field for which the equation holds,\n", 237 | "# r.h.s. equals the time derivative of the field.\n", 238 | "# In py-pde, the equations are defined as f-strings.\n", 239 | "# The reactions can be restricted to the membrane by multiplying with the\n", 240 | "# mask (which will be a field itself and therefore does not need to be escaped).\n", 241 | "expr = {'c_m': f'{D_m} * laplace_x(c_m) + mask * ({k_prod} - {k} * c_m)',\n", 242 | " 'c_b': f'{D_c} * laplace(c_b) + {k} * mask * c_m - {k_degr} * c_b'}\n", 243 | "\n", 244 | "# Set the initial values for both fields:\n", 245 | "c_m = pde.ScalarField.random_uniform(grid) * membrane_mask\n", 246 | "c_b = pde.ScalarField.random_uniform(grid)\n", 247 | "\n", 248 | "# Create the PDE object\n", 249 | "eq = pde.PDE(expr, consts={'mask': membrane_mask})\n", 250 | "\n", 251 | "# Set the initial values for all fields, and show it\n", 252 | "field = pde.FieldCollection([c_m, c_b])\n", 253 | "field.plot();\n", 254 | "\n", 255 | "# Simulate the system for 100 time steps, and show the result\n", 256 | "res = eq.solve(field, t_range=100, tracker=\"progress\")\n", 257 | "res.plot();" 258 | ] 259 | }, 260 | { 261 | "cell_type": "markdown", 262 | "id": "2e3681de", 263 | "metadata": {}, 264 | "source": [ 265 | "### Problem 4: Evaluation\n", 266 | "In general, the data you obtain from simulations needs to be post-processed into meaningful quantities. Here, we want to compare the results with the analytical results, i.e., we want to test whether the predicted concentration profile and length scale match the results.\n", 267 | "\n", 268 | "Do so by extracting a meaningful dataset from the solution data (which is stored in ```res.data```), and by plotting your result. Then, use ```numpy```'s ```polyfit``` to fit your predicted concentration profile to the data.\n", 269 | "_Hint: ```numpy.polyfit``` only fits polynomials. I your function contains exponentials, fit a polynomial to the log of your data, and then apply the exponential to the fitted result._" 270 | ] 271 | }, 272 | { 273 | "cell_type": "code", 274 | "execution_count": null, 275 | "id": "7214879c", 276 | "metadata": {}, 277 | "outputs": [], 278 | "source": [ 279 | "# Get plot data:\n", 280 | "coords = grid._axes_coords[1]\n", 281 | "concentration = ## BLANK ## # concentration at the specified coordinates\n", 282 | "\n", 283 | "# Plot:\n", 284 | "plt.plot(coords, concentration, 'o', label='data')\n", 285 | "plt.legend()\n", 286 | "plt.show();" 287 | ] 288 | }, 289 | { 290 | "cell_type": "code", 291 | "execution_count": null, 292 | "id": "59853a2a", 293 | "metadata": {}, 294 | "outputs": [], 295 | "source": [ 296 | "# Guessed function for the concentration depending on the position\n", 297 | "def fit_func(x, a, b):\n", 298 | " return ## BLANK ##\n", 299 | "\n", 300 | "# Fit parameters and covariance for the fit data\n", 301 | "popt, pcov = scipy.optimize.curve_fit(fit_func, coords, concentration)\n", 302 | "\n", 303 | "# Visualization\n", 304 | "plt.plot(coords, concentration, 'o', label='data')\n", 305 | "plt.plot(coords, fit_func(coords, *popt), 'g--',\n", 306 | " label='fit')\n", 307 | "plt.title('Fit $c_b(y) = a \\cdot \\exp(-b \\cdot y)$:\\n'+\n", 308 | " f'$a\\;=${popt[0]: .3},\\t$b\\;=${popt[1]: .4}')\n", 309 | "plt.xlabel('$y$')\n", 310 | "plt.ylabel('Concentration $c_b(y)$')\n", 311 | "plt.legend(loc=\"upper right\")\n", 312 | "plt.show()" 313 | ] 314 | }, 315 | { 316 | "cell_type": "markdown", 317 | "id": "4eb31502", 318 | "metadata": {}, 319 | "source": [ 320 | "### Problem 5: Sweeping\n", 321 | "A single dataset is of course rarely sufficient to be confident about your predictions. In general, we are mostly interested in the dependence of observable quantities on experimental control parameters. A good way to test whether we got the right _formula_ for the characteristic length scale $\\ell$ is to sweep over a few parameters and check whether the results match our formula across the entire sweep range.\n", 322 | "\n", 323 | "Set up the sweep by completing the definition of the method ```simulate_and_fit```. Use this method to obtain a reasonable dataset, and compare it to the function for $\\ell$. If you feel like your results take too long to calculate, tinker the mesh parameters (spatial and temporal) to get faster results." 324 | ] 325 | }, 326 | { 327 | "cell_type": "code", 328 | "execution_count": null, 329 | "id": "164d700c", 330 | "metadata": {}, 331 | "outputs": [], 332 | "source": [ 333 | "def simulate_and_fit(width=10, height=5, resolution=(2**5)/10,\n", 334 | " k_prod=1., k=0.1, k_degr=1., D_c=1., D_m=0.1):\n", 335 | " # Make a new grid\n", 336 | " grid = pde.CartesianGrid(\n", 337 | " [[0, width], [0, height]],\n", 338 | " [int(resolution*i) for i in [width, height]]\n", 339 | " )\n", 340 | " \n", 341 | " # Define the membrane mask\n", 342 | " membrane_mask = pde.ScalarField(grid, dtype=float)\n", 343 | " membrane_mask.data[:, 0] = 1.0\n", 344 | " \n", 345 | " # Create the PDE object\n", 346 | " expr = {'c_m': ## BLANK ##,\n", 347 | " 'c_b': ## BLANK ##}\n", 348 | " eq = pde.PDE(expr, consts={'mask': membrane_mask})\n", 349 | " \n", 350 | " # Set initial concentrations\n", 351 | " c_m = pde.ScalarField.random_uniform(grid) * ## BLANK ##\n", 352 | " c_b = ## BLANK ##\n", 353 | " \n", 354 | " # Calculate the solution\n", 355 | " field = pde.FieldCollection([c_m, c_b])\n", 356 | " res = eq.solve(field, t_range=10, tracker=[])\n", 357 | " \n", 358 | " # Post-process the data\n", 359 | " coords = grid._axes_coords[1]\n", 360 | " concentration = res.data[1,0]\n", 361 | " popt, pcov = scipy.optimize.curve_fit(fit_func, coords, concentration)\n", 362 | " \n", 363 | " # Return the (relevant) fit parameter: length scale\n", 364 | " return popt[1]" 365 | ] 366 | }, 367 | { 368 | "cell_type": "code", 369 | "execution_count": null, 370 | "id": "241361c5-718e-49cd-8839-e4bc95adffef", 371 | "metadata": {}, 372 | "outputs": [], 373 | "source": [ 374 | "# Set the sweep parameters (note: sweep over ~10 values)\n", 375 | "sweepParameters = np.power(10, ## BLANK ##)\n", 376 | "\n", 377 | "# Set up the sweep in a for loop\n", 378 | "sweepResults = []\n", 379 | "startTime = datetime.datetime.now()\n", 380 | "for _ in sweepParameters:\n", 381 | " ETA(list(sweepParameters).index(_), len(sweepParameters), startTime)\n", 382 | " fitResult = simulate_and_fit(## BLANK ## = _, width = 2.)\n", 383 | " sweepResults.append(fitResult)\n", 384 | "\n", 385 | "# For convenience, convert the results to a numpy array\n", 386 | "sweepResults=np.array(sweepResults)\n", 387 | "print('Finished.' + \"\\t\"*100)" 388 | ] 389 | }, 390 | { 391 | "cell_type": "code", 392 | "execution_count": null, 393 | "id": "3795f2d7", 394 | "metadata": {}, 395 | "outputs": [], 396 | "source": [ 397 | "# Define a high-resolution range for plotting the analytic function\n", 398 | "aRange = np.linspace(min(sweepParameters), max(sweepParameters), 100)\n", 399 | "\n", 400 | "# Plot the sweep results together with the prediction\n", 401 | "plt.plot(sweepParameters, 1/sweepResults, 'o', label='data')\n", 402 | "plt.plot(aRange, ## BLANK ##, label='prediction')\n", 403 | "plt.title(\"Characteristic length scale vs. diffusion constant\")\n", 404 | "plt.xlabel('Diffusion constant')\n", 405 | "plt.ylabel('Characteristic length scale $\\ell$')\n", 406 | "plt.legend()\n", 407 | "plt.show()" 408 | ] 409 | }, 410 | { 411 | "cell_type": "markdown", 412 | "id": "18cd1285", 413 | "metadata": {}, 414 | "source": [ 415 | "### Problem 6: Interpretation\n", 416 | "Despite having done everything correctly, you will notice that the predicted (analytic) dependence of the length scale on the control parameters does not match the sweep for all tested parameters. Why? How could you solve this problem? What does this teach you about numerical simulations in general?\n", 417 | "\n", 418 | "_Hint: Plot one of the profiles where the fit does not match the prediction. What do you notice?_" 419 | ] 420 | } 421 | ], 422 | "metadata": { 423 | "kernelspec": { 424 | "display_name": "Python 3 (ipykernel)", 425 | "language": "python", 426 | "name": "python3" 427 | }, 428 | "language_info": { 429 | "codemirror_mode": { 430 | "name": "ipython", 431 | "version": 3 432 | }, 433 | "file_extension": ".py", 434 | "mimetype": "text/x-python", 435 | "name": "python", 436 | "nbconvert_exporter": "python", 437 | "pygments_lexer": "ipython3", 438 | "version": "3.9.9" 439 | } 440 | }, 441 | "nbformat": 4, 442 | "nbformat_minor": 5 443 | } 444 | -------------------------------------------------------------------------------- /tutorial-notebooks/day1_3_reaction-diffusion.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "0e838efd", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "# This notebook requires py-pde in version 0.17.1 or later\n", 11 | "# The package can be obtained from https://github.com/zwicker-group/py-pde\n", 12 | "# Alternatively, it can be installed via pip or conda\n", 13 | "\n", 14 | "import pde\n", 15 | "\n", 16 | "# functools and make_derivative2 are required for anisotropic laplacians\n", 17 | "import functools\n", 18 | "from pde.grids.operators.common import make_derivative2\n", 19 | "\n", 20 | "# plotting functions\n", 21 | "import matplotlib.pyplot as plt\n", 22 | "import numpy as np\n", 23 | "\n", 24 | "# fitting methods\n", 25 | "import scipy\n", 26 | "\n", 27 | "# a simple tracker to check parameter sweeps\n", 28 | "import datetime\n", 29 | "def ETA(step, maxStep, startTime):\n", 30 | " _ETA = None\n", 31 | " total_dt = 0\n", 32 | " dt = 0\n", 33 | " if step==0:\n", 34 | " _ETA = \"Indeterminate\"\n", 35 | " else:\n", 36 | " dt = datetime.datetime.now() - startTime\n", 37 | " dt = dt.seconds\n", 38 | " total_dt = dt/step * maxStep\n", 39 | " _ETA = (startTime + datetime.timedelta(seconds = total_dt))\n", 40 | " _ETA = str(_ETA.time())\n", 41 | " \n", 42 | " print(f\"{int(100 * step / maxStep):>3} % completed. ETA: {_ETA} ({int(total_dt - dt)} seconds remain).\" + '\\t' * 5,\n", 43 | " end='\\r')" 44 | ] 45 | }, 46 | { 47 | "cell_type": "markdown", 48 | "id": "963cef99", 49 | "metadata": {}, 50 | "source": [ 51 | "# Reaction-Diffusion processes\n", 52 | "Generic design features shared by the diverse biochemical interaction networks underlying protein pattern formation in cells include:\n", 53 | " - The dynamics (approximately) conserves the mass of each individual protein species: on the time scale of pattern formation neither protein production nor protein degradation are significant processes.\n", 54 | " - The biochemical reactions are characterised by (positive and negative) feedback mechanisms such that the chemical rate equations describing the dynamics of these reactions are generically nonlinear.\n", 55 | " - The proteins are typically transported by diffusive fluxes.\n", 56 | " \n", 57 | "Then, the spatiotemporal dynamics of protein patterns is described by _mass-conserving reaction-diffusion_ (MCRD) equations.\n", 58 | "\n", 59 | "The aspect of mass conservation is a constraint imposed by nature: in general, proteins do not appear out of nowhere, nor do they disappear into the void. For biological systems, the mass conservation plays a crucial role that will bother us later in the tutorial; for now, we will focus only on generic properties of pattern-forming reaction-diffusion systems. In the following, you will learn how to set up generic pattern-forming systems with a PDE solver, what individual parts of the differential equations mean, and how to interpret the results." 60 | ] 61 | }, 62 | { 63 | "cell_type": "markdown", 64 | "id": "588efa89", 65 | "metadata": {}, 66 | "source": [ 67 | "## The Fitzhugh-Nagumo model\n", 68 | "We will analyse and implement a very generic system that shows stationary patterns here, namely the Fitzhugh-Nagumo model. This model was originally proposed as a simplified description of neuron spiking. The model features two components, $u(x,y,t)$ and $v(x,y,t)$, that interact nonlinearly with each other. The general formulation reads:\n", 69 | "$$\n", 70 | "\\partial_t u(x,y,t) = D_u \\, \\nabla^2 u + f(u) - \\sigma \\cdot v \\, , \\\\\n", 71 | "\\partial_t v(x,y,t) = D_v \\, \\nabla^2 v + u - v \\, ,\n", 72 | "$$\n", 73 | "with $f(u)$ containing all the nonlinear interactions (see below).\n", 74 | "For simplicity, the spatial/temporal variables $x$, $y$, and $t$ where omitted on the r.h.s. of the equations.\n", 75 | "Note that these equations are a simplified version of the variants typically found in the literature, in particular regarding additional prefactors.\n", 76 | "\n", 77 | "The nonlinear interaction term is the key component that leads to the formation of patterns in this model. Here, we will use\n", 78 | "$$f(u) = - u^3 + \\alpha \\cdot u - \\kappa \\, .$$\n", 79 | "In this representation, all parameters ($D_u$, $D_v$, $\\alpha$, $\\kappa$, and $\\sigma$) are positive." 80 | ] 81 | }, 82 | { 83 | "cell_type": "markdown", 84 | "id": "2a26884b", 85 | "metadata": {}, 86 | "source": [ 87 | "## Part 1: Analysing the problem analytically" 88 | ] 89 | }, 90 | { 91 | "cell_type": "markdown", 92 | "id": "bce04c04", 93 | "metadata": {}, 94 | "source": [ 95 | "### Problem 1: Understanding the differential equations\n", 96 | "For each of the terms on the r.h.s. of the PDEs, state their effect on the dynamics of the system.\n", 97 | "For each of the parameters, state how varying this parameter will qualitatively change the dynamics of the system.\n", 98 | "\n", 99 | "_Hint: You should remain on a very broad level here, using phrases such as 'smoothens out rough profiles', 'leads to an increase/decrease of $v$ (or $u$)', etc._\n", 100 | "\n", 101 | "_Hint: For examining the parameters, it helps to consider a specific state of the system. For example, if $u>0$ then the term $\\alpha \\cdot u$ will lead to further increase of $u$, but it will lead to a decrease if $u<0$._" 102 | ] 103 | }, 104 | { 105 | "cell_type": "markdown", 106 | "id": "bb125f56", 107 | "metadata": {}, 108 | "source": [ 109 | "### Bonus Problem (Optional): Linear stability analysis and dispersion relation\n", 110 | "The following problem requires some background in theoretical physics or mathematics. If you are comfortable with the terms used in the problem, or have time to spare after completing all other problems, you can try this bonus problem. It will teach you how the values for the constants used in the code were derived (since, of course, you can't just guess any values). You can even solve the problem without the suggested simplifications to obtain more general results.\n", 111 | "\n", 112 | "Perform a linear stability analysis of the FitzHugh-Nagumo model for the case $\\kappa = 0$ and $\\sigma = 1$ (ignoring the diffusive component, i.e., assuming $D_u = D_v = 0$). What is the condition for the system to be linearly (un)stable?\n", 113 | "\n", 114 | "_Hint: You will need to calculate the fixed points first. Then, determine whether the fixed point at $u^* = v^* = 0$ is stable or unstable._\n", 115 | "\n", 116 | "Now calculate the dispersion relation, with the additional simplification that $\\alpha = 1$. What are the conditions for the diffusion constants $D_u$ and $D_v$ to obtain a band of unstable modes?\n", 117 | "\n", 118 | "_Hint: Use the following ansatz for calculating the dispersion relation to lowest order in $\\delta u$ around the fixed point $u^* = v^* = 0$: $u(x,t) = \\delta u \\cdot \\exp(\\eta t) \\sin(k \\, x)$, and similar for $v$ It also helps to express one diffusion constant as a fraction of the other, e.g., $D_u = z \\cdot D_v$._\n" 119 | ] 120 | }, 121 | { 122 | "cell_type": "markdown", 123 | "id": "aedd79d1", 124 | "metadata": {}, 125 | "source": [ 126 | "## Part 2: Testing the system numerically\n", 127 | "Different to the source-degradation, we don't know yet what to expect from the system. In particular, the size of the system and the grid resolution need to be determined without knowing the system dynamics. " 128 | ] 129 | }, 130 | { 131 | "cell_type": "markdown", 132 | "id": "c919232b", 133 | "metadata": {}, 134 | "source": [ 135 | "### Problem 2: Determining the numerical constraints\n", 136 | "Use arbitrarily chosen values for the diffusion constants (e.g., $D_u = 0.1 \\; [\\mu m^2/s]$ and $D_v = 1.0 \\; [\\mu m^2/s]$) and the reaction coefficients ($\\alpha = 1 \\; [1/s]$, $\\kappa = 0.1 \\; [1/ (s \\cdot \\mu m ^2)]$, and $\\sigma = 1 \\; [1/s]$) to make an educated guess about the system size and spatial resolution." 137 | ] 138 | }, 139 | { 140 | "cell_type": "code", 141 | "execution_count": null, 142 | "id": "c32acd79", 143 | "metadata": {}, 144 | "outputs": [], 145 | "source": [ 146 | "# Define the geometry of the system.\n", 147 | "# This time, we use a square grid of width L. The mesh size can be adapted by changing `resolution`.\n", 148 | "# Setting `resolution = 10`, for example, means that grid cells are 1/resolution = 0.1 (length units) in size.\n", 149 | "\n", 150 | "L = ## BLANK ##\n", 151 | "resolution = ## BLANK ##\n", 152 | "\n", 153 | "# pde.CartesianGrid() creates a grid object.\n", 154 | "# To avoid boundary effects, we use periodic boundary conditions here.\n", 155 | "\n", 156 | "grid = pde.CartesianGrid(\n", 157 | " [[0, L], [0, L]],\n", 158 | " [int(resolution*L), int(resolution*L)],\n", 159 | " periodic = True\n", 160 | ")\n", 161 | "grid.plot();" 162 | ] 163 | }, 164 | { 165 | "cell_type": "code", 166 | "execution_count": null, 167 | "id": "a283b4d6", 168 | "metadata": {}, 169 | "outputs": [], 170 | "source": [ 171 | "# Since we do not have any membrane dynamics, we do not need a special diffusion operator this time." 172 | ] 173 | }, 174 | { 175 | "cell_type": "markdown", 176 | "id": "901e667d", 177 | "metadata": {}, 178 | "source": [ 179 | "### Problem 3: Setting up the equations\n", 180 | "According to your choice of the system size and resolution, it is now time to define the system parameters. Translate the equations of the Fitzhugh-Nagumo model into the required syntax of `py-pde`. As a reminder, the expression for $v$ is already given." 181 | ] 182 | }, 183 | { 184 | "cell_type": "code", 185 | "execution_count": null, 186 | "id": "64ba193a", 187 | "metadata": {}, 188 | "outputs": [], 189 | "source": [ 190 | "D_u = 0.1\n", 191 | "D_v = 1.0\n", 192 | "\n", 193 | "alpha = 1.0\n", 194 | "kappa = 0.1\n", 195 | "sigma = 1.0\n", 196 | "\n", 197 | "expr = {'u' : ## BLANK ##, \n", 198 | " 'v' : f'laplace(v) * {D_v} + u - v'}\n" 199 | ] 200 | }, 201 | { 202 | "cell_type": "markdown", 203 | "id": "a34ab012", 204 | "metadata": {}, 205 | "source": [ 206 | "Now you are almost ready to execute the simulation: only the initial conditions are still missing. In general, for pattern-forming systems, it is a good start to use an unstable fixed point with a sufficiently strong perturbation as initial states. In the Fitzhugh-Nagumo model, for $\\kappa = 0$, one fixed point is at $u^* = v^* = 0$, and this fixed point is unstable _in a spatially extended system_ (in a 0D system, this need not be the case). Since we have a comparably small $\\kappa$, the actual fixed point will be close to this, so for simplicity, we perturb the system around $(u, v) = (0,0)$.\n", 207 | "\n", 208 | "`random_uniform(grid)` returns an array with values randomly chosen between 0 and 1. The code below transforms this to random values between -1 and 1, centered around the fixed point." 209 | ] 210 | }, 211 | { 212 | "cell_type": "code", 213 | "execution_count": null, 214 | "id": "26a97961", 215 | "metadata": {}, 216 | "outputs": [], 217 | "source": [ 218 | "# Construct the field objects\n", 219 | "u = pde.ScalarField.random_uniform(grid) * 2. - 1.\n", 220 | "v = pde.ScalarField.random_uniform(grid) * 2. - 1.\n", 221 | "\n", 222 | "\n", 223 | "# Create the PDE object\n", 224 | "eq = pde.PDE(expr)\n", 225 | "\n", 226 | "# Set the initial values for all fields, and show it\n", 227 | "field = pde.FieldCollection([u,v])\n", 228 | "field.plot();\n", 229 | "\n", 230 | "# Simulate the system and show the result\n", 231 | "storage = pde.MemoryStorage()\n", 232 | "\n", 233 | "res = eq.solve(field, t_range=200, tracker=[storage.tracker(1), \"progress\"])\n", 234 | "res.plot();" 235 | ] 236 | }, 237 | { 238 | "cell_type": "markdown", 239 | "id": "5f317639", 240 | "metadata": {}, 241 | "source": [ 242 | "## Part 3: Evaluation\n", 243 | "### Problem 4: Interpreting the results\n", 244 | "Look at the final state of the simulation. Do you observe a pattern? Do your results match the expactations? What is the order of magnitude of the pattern's characteristic length scale? Run the simulation again with different values for $\\kappa$ and note down your observations." 245 | ] 246 | }, 247 | { 248 | "cell_type": "markdown", 249 | "id": "50560ec6", 250 | "metadata": {}, 251 | "source": [ 252 | "### Problem 5: Quantifying the results\n", 253 | "We now want to quantify the characteristic length scale. To do so, we will try to find the center of each spot in the pattern, and calculate the distances to the other spots. This analysis is done by the code below, which shows you a histogram of the spot distances, and returns a proxy for the mean distance between neighboring spots.\n", 254 | "\n", 255 | "The function `get_length_scale` does not contain any blanks, however you are encouraged to walk through it and to understand the underlying idea. This will help you to properly interpret the results later on." 256 | ] 257 | }, 258 | { 259 | "cell_type": "code", 260 | "execution_count": null, 261 | "id": "03f60ed4", 262 | "metadata": {}, 263 | "outputs": [], 264 | "source": [ 265 | "def get_length_scale(data: np.array, show_stats: bool = True, show_centers: bool = False):\n", 266 | " # Make an empty array of the same size as the data array.\n", 267 | " # This is not necessary, but it helps to visualize things if needed, and is useful for debugging\n", 268 | " pattern_maxima = np.zeros_like(data)\n", 269 | " \n", 270 | " # Make a list to store all pattern spot maxima\n", 271 | " maxima_coordinates = []\n", 272 | " \n", 273 | " # Define some auxiliary quantities\n", 274 | " lx, ly = data.shape\n", 275 | " delta = 2 # size of the scanning window to find the maxima\n", 276 | " offsets = [(dx, dy) for dx in range(-delta, delta+1) for dy in range(-delta, delta+1)]\n", 277 | " offsets.remove((0,0)) # coordinate offsets that should be checked\n", 278 | " \n", 279 | " # Scan the entire array and check whether a pixel has the highes value of all neighbours\n", 280 | " for iy, ix in np.ndindex(data.shape):\n", 281 | " nbs = [data[(ix+dx)%lx, (iy+dy)%ly] for dx,dy in offsets]\n", 282 | " if data[ix,iy] > max(nbs):\n", 283 | " pattern_maxima[ix,iy] += 1.\n", 284 | " maxima_coordinates.append(np.array((ix,iy)))\n", 285 | " pattern_maxima[ix, iy] = 1.\n", 286 | " \n", 287 | " # Show the spot centers\n", 288 | " if show_centers:\n", 289 | " plt.imshow(data)\n", 290 | " plt.show();\n", 291 | "\n", 292 | " plt.imshow(pattern_maxima)\n", 293 | " plt.show();\n", 294 | " \n", 295 | " # Average area 'occupied' by a spot pattern: (total area)/(nr of spots)\n", 296 | " # Average distance between spots: sqrt((total area)/(nr of spots)):\n", 297 | " neighbour_distances = np.sqrt(L*L/len(maxima_coordinates))\n", 298 | " if show_stats:\n", 299 | " print(f\"Nr. of spots: {len(maxima_coordinates)}\")\n", 300 | " print(f\"Occupied area per spot: {L*L/len(maxima_coordinates)}\")\n", 301 | " print(f\"Estimated average distance between spots: {neighbour_distances}\")\n", 302 | " \n", 303 | " \n", 304 | " return np.mean(neighbour_distances)" 305 | ] 306 | }, 307 | { 308 | "cell_type": "code", 309 | "execution_count": null, 310 | "id": "cebe8f7f", 311 | "metadata": {}, 312 | "outputs": [], 313 | "source": [ 314 | "get_length_scale(storage.data[-1][0], show_centers = True)" 315 | ] 316 | }, 317 | { 318 | "cell_type": "markdown", 319 | "id": "ab15a32e", 320 | "metadata": {}, 321 | "source": [ 322 | "### Problem 6: Sweeping\n", 323 | "Again, we want to check what happens when we change one of the parameters. Here, we want to vary the diffusion constants. Sweep over an appropriate parameter regime and describe how the characteristic pattern length scale $\\ell$ changes.\n", 324 | "\n", 325 | "What (approximate) functional dependence of the length scale on the diffusion constant can you derive from your sweep? Is it meaningful to extrapolate your data? Where do you expect your results to fail?" 326 | ] 327 | }, 328 | { 329 | "cell_type": "code", 330 | "execution_count": null, 331 | "id": "42364f98", 332 | "metadata": {}, 333 | "outputs": [], 334 | "source": [ 335 | "# Define a function that yields the characteristic length scale for a given parameter set\n", 336 | "def simulate_and_get_length_scale(D_u = 0.1, D_v = 1.0, alpha = 1.0, kappa = 0.1, sigma = 1.0, tracker = \"progress\"):\n", 337 | " expr = {'u' : f'laplace(u) * {D_u} + {alpha} * u - u * u * u - {kappa} - {sigma} * v', \n", 338 | " 'v' : f'laplace(v) * {D_v} + u - v'} \n", 339 | " \n", 340 | " # Construct the field objects\n", 341 | " u = pde.ScalarField.random_uniform(grid) * 2. - 1.\n", 342 | " v = pde.ScalarField.random_uniform(grid) * 2. - 1.\n", 343 | "\n", 344 | " eq = pde.PDE(expr)\n", 345 | " field = pde.FieldCollection([u,v])\n", 346 | "\n", 347 | " res = eq.solve(field, t_range=200, tracker=tracker)\n", 348 | " \n", 349 | " return get_length_scale(res.data[0], show_stats = False) " 350 | ] 351 | }, 352 | { 353 | "cell_type": "code", 354 | "execution_count": null, 355 | "id": "d83dc576", 356 | "metadata": {}, 357 | "outputs": [], 358 | "source": [ 359 | "# Make a parameter sweep\n", 360 | "# Empty list to store the results in\n", 361 | "sweep_results = []\n", 362 | "\n", 363 | "# Parameters to sweep over\n", 364 | "sweep_parameters = np.power(10, np.arange(-.5, .5, ## BLANK ##))\n", 365 | "\n", 366 | "# Nr of iterations per parameter to get a statistical average\n", 367 | "sample_size = ## BLANK ##\n", 368 | "\n", 369 | "# Launch the sweep\n", 370 | "start_time = datetime.datetime.now()\n", 371 | "for _ in sweep_parameters:\n", 372 | " tmp = []\n", 373 | " for __ in range(sample_size):\n", 374 | " ETA(list(sweep_parameters).index(_) * sample_size + __, len(sweep_parameters) * sample_size, start_time)\n", 375 | " tmp.append(simulate_and_get_length_scale(D_u = 0.1 * _, D_v = 1.0 * _, tracker = []))\n", 376 | " length_scale = np.mean(tmp)\n", 377 | " sweep_results.append(length_scale)\n", 378 | "\n", 379 | "# Convert the results to a numpy array for better handling afterwards\n", 380 | "sweep_results = np.array(sweep_results)\n", 381 | "\n", 382 | "# Plot the data\n", 383 | "plt.plot(## BLANK ##, ## BLANK ##, 'o', label='data')\n", 384 | "plt.title('Characteristic length scale vs. diffusion constant')\n", 385 | "plt.xlabel('$D/D_0$')\n", 386 | "plt.ylabel('Characteristic length $\\ell$')\n", 387 | "plt.legend(loc=\"upper left\")\n", 388 | "plt.show()" 389 | ] 390 | }, 391 | { 392 | "cell_type": "code", 393 | "execution_count": null, 394 | "id": "2390fe26", 395 | "metadata": {}, 396 | "outputs": [], 397 | "source": [ 398 | "# Guessed function for the length scale depending on the sweep parameter\n", 399 | "def fit_func(x, a, b):\n", 400 | " return ## BLANK ##\n", 401 | "\n", 402 | "# Fit parameters and covariance for the fit data\n", 403 | "popt, pcov = scipy.optimize.curve_fit(fit_func, sweep_parameters, sweep_results)\n", 404 | "\n", 405 | "# Visualization\n", 406 | "\n", 407 | "plt.plot(sweep_parameters, sweep_results, 'o', label='data')\n", 408 | "plt.plot(sweep_parameters, fit_func(sweep_parameters, *popt), 'g--',\n", 409 | " label='fit')\n", 410 | "plt.title('Fit $\\ell(D) = a + \\sqrt{b \\cdot D/D_0}$\\nwith $D_0$ some reference diffusion constant:\\n'+\n", 411 | " f'$a\\;=${popt[0]: .3},\\t$b\\;=${popt[1]: .4}')\n", 412 | "plt.xlabel('$D/D_0$')\n", 413 | "plt.ylabel('Characteristic length $\\ell$')\n", 414 | "plt.legend(loc=\"upper left\")\n", 415 | "plt.show()" 416 | ] 417 | }, 418 | { 419 | "cell_type": "markdown", 420 | "id": "265076d2", 421 | "metadata": {}, 422 | "source": [ 423 | "### Problem 7: (Non-)Linearities\n", 424 | "The PDEs of the Fitzhugh-Nagumo model include a nonlinear term, namely $-u^3$. This nonlinearity is essential for the formation of patterns here. Test this explicitly by implementing a variant of the Fitzhugh-Nagumo model where the nonlinear term is replaced by $-u^3 \\rightarrow -u^n$, with $n$ an integer exponent. In particular, test the cases $n \\in \\{1, 3, 5\\}$, and describe your results. Give an explanation why you observe patterns in some cases, and why not in others." 425 | ] 426 | }, 427 | { 428 | "cell_type": "code", 429 | "execution_count": null, 430 | "id": "3d167013", 431 | "metadata": { 432 | "scrolled": false 433 | }, 434 | "outputs": [], 435 | "source": [ 436 | "for n in [1, 3, 5]:\n", 437 | "\n", 438 | " D_u = 0.1 \n", 439 | " D_v = 1.0 \n", 440 | "\n", 441 | " alpha = 1.0\n", 442 | " kappa = 0.1\n", 443 | " sigma = 1.0 \n", 444 | "\n", 445 | " expr = {'u' : ## BLANK ##,\n", 446 | " 'v' : ## BLANK ##}\n", 447 | "\n", 448 | "\n", 449 | " # Construct the field objects\n", 450 | " u = pde.ScalarField.random_uniform(grid) * 2. - 1.\n", 451 | " v = pde.ScalarField.random_uniform(grid) * 2. - 1.\n", 452 | "\n", 453 | "\n", 454 | " # Create the PDE object\n", 455 | " eq = pde.PDE(expr)\n", 456 | "\n", 457 | " # Set the initial values for all fields, and show it\n", 458 | " field = pde.FieldCollection([u,v])\n", 459 | "\n", 460 | " res = eq.solve(field, t_range=200, tracker=[])\n", 461 | " res.plot(title=f\"n = {n}\");" 462 | ] 463 | }, 464 | { 465 | "cell_type": "markdown", 466 | "id": "75df1baa", 467 | "metadata": {}, 468 | "source": [ 469 | "## Part 4: A variation of the system (Optional)\n", 470 | "### Problem 8: Dynamic patterns\n", 471 | "The pattern you analysed above is a stationary pattern, i.e. it will remain the same if you continue simulating for much longer times. For a different parameter regime, the Fitzhugh-Nagumo model shows dynamic patterns that reflect the 'neuron spiking' idea of the model. While these dynamic patterns are particularly difficult to analyse, they are at the same time very nice to look at. The code below simulates such a dynamic pattern. Run and enjoy!" 472 | ] 473 | }, 474 | { 475 | "cell_type": "code", 476 | "execution_count": null, 477 | "id": "faf5b8a7", 478 | "metadata": {}, 479 | "outputs": [], 480 | "source": [ 481 | "D_u = 0.01\n", 482 | "D_v = 0.01\n", 483 | "\n", 484 | "alpha = 2.0\n", 485 | "kappa = 0.0\n", 486 | "sigma = 2.0\n", 487 | "\n", 488 | "expr = {'u' : f'laplace(u) * {D_u} + {alpha} * u - u * u * u - {kappa} - {sigma} * v',\n", 489 | " 'v' : f'laplace(v) * {D_v} + u - v'}\n", 490 | "\n", 491 | "# Construct the field objects\n", 492 | "u = pde.ScalarField.random_uniform(grid) * 2. - 1.\n", 493 | "v = pde.ScalarField.random_uniform(grid) * 2. - 1.\n", 494 | "\n", 495 | "\n", 496 | "# Create the PDE object\n", 497 | "eq = pde.PDE(expr)\n", 498 | "\n", 499 | "# Set the initial values for all fields, and show it\n", 500 | "field = pde.FieldCollection([u,v])\n", 501 | "field.plot();\n", 502 | "\n", 503 | "# Simulate the system for 100 time steps, and show the result\n", 504 | "storage = pde.MemoryStorage()\n", 505 | "\n", 506 | "res = eq.solve(field, t_range=100, dt = 0.01, tracker=[storage.tracker(0.2), \"progress\"])\n", 507 | "res.plot();\n" 508 | ] 509 | }, 510 | { 511 | "cell_type": "code", 512 | "execution_count": null, 513 | "id": "3a1d26a5", 514 | "metadata": {}, 515 | "outputs": [], 516 | "source": [ 517 | "# make a movie of the dynamic pattern\n", 518 | "pde.movie(storage, 'dynamic_pattern.mp4', progress=True)" 519 | ] 520 | } 521 | ], 522 | "metadata": { 523 | "kernelspec": { 524 | "display_name": "Python 3 (ipykernel)", 525 | "language": "python", 526 | "name": "python3" 527 | }, 528 | "language_info": { 529 | "codemirror_mode": { 530 | "name": "ipython", 531 | "version": 3 532 | }, 533 | "file_extension": ".py", 534 | "mimetype": "text/x-python", 535 | "name": "python", 536 | "nbconvert_exporter": "python", 537 | "pygments_lexer": "ipython3", 538 | "version": "3.9.9" 539 | } 540 | }, 541 | "nbformat": 4, 542 | "nbformat_minor": 5 543 | } 544 | -------------------------------------------------------------------------------- /tutorial-notebooks/day2_1_pole-to-pole.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "6af6e5c4", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "# This notebook requires py-pde in version 0.17.1 or later\n", 11 | "# The package can be obtained from https://github.com/zwicker-group/py-pde\n", 12 | "# Alternatively, it can be installed via pip or conda\n", 13 | "\n", 14 | "import pde\n", 15 | "\n", 16 | "# functools and make_derivative2 are required for anisotropic laplacians\n", 17 | "import functools\n", 18 | "from pde.grids.operators.common import make_derivative2\n", 19 | "\n", 20 | "# plotting functions\n", 21 | "import matplotlib.pyplot as plt\n", 22 | "import numpy as np" 23 | ] 24 | }, 25 | { 26 | "cell_type": "markdown", 27 | "id": "7a3a6851", 28 | "metadata": {}, 29 | "source": [ 30 | "# Pole-to-pole oscillations\n", 31 | "In the previous two notebooks, you learnt about the relevance of membrane interactions and how this can lead to gradients in the bulk (volume, cytosol) of a cell, and about the importance of nonlinearities in the interaction terms. Now, we will combine these two effects to reproduce a prominent example of dynamic protein pattern in living cells: the pole-to-pole oscillations of the MinCDE system in _E. coli_.\n", 32 | "\n", 33 | "The MinCDE system regulates the placement of the division site in _E. coli_ bacteria. More precisely, the MinC protein inhibits the formation of the Z-ring. By oscillating between the poles, the MinC proteins is mainly located at the cell poles, and rarely at the cell center. Thus, the inhibitory effect is weakest at the cell center, such that the Z-ring forms just there, resulting in symmetric cell division.\n", 34 | "\n", 35 | "If you are unfamiliar with the system, have a look at https://static-movie-usa.glencoesoftware.com/mp4/10.7554/384/2cd69eb620315bb0a04c26d92e1300548d3bca6d/elife-03949-media1.mp4 (from Zieske and Schwille, eLife, 2014, elifesciences.org/articles/03949). The video shows protein concentrations in _E. coli_ cells of varying aspect ratio. In nature, _E. coli_ have an aspect ratio of about $5$ just before cell division, for which the proteins oscillate between the two cell poles (e.g., column 1, row 4 in the linked movie). In the following, we will aim to reproduce these oscillations in simulations.\n", 36 | "\n", 37 | "The Min protein oscillations require only the MinD and MinE proteins; the MinC protein is needed for downstream regulation of the Z-ring, but not to maintain the patterning process. Thus, we will only use the former two proteins in the following." 38 | ] 39 | }, 40 | { 41 | "cell_type": "markdown", 42 | "id": "1bade711", 43 | "metadata": {}, 44 | "source": [ 45 | "The Min proteins interact with each other as follows:\n", 46 | " - MinD exists in three states: bound to the membrane ($c_d$, `c_d`), as well as in the cytosol bound to ATP ($c_{DT}$, `c_DT`) and to ADP ($c_{DD}$, `c_DD`).\n", 47 | " - MinE is present in the cytosol ($c_E$, `c_E`).\n", 48 | " - On the membrane, MinD and MinE can form a complex ($c_{de}$, `c_de`).\n", 49 | "\n", 50 | "\n", 51 | " - MinD-ATP binds to the membrane at a rate $k_D$. In addition, it is recruited to the membrane by other membrane-bound MinD proteins at a rate $k_{dD}$.\n", 52 | " - MinE is recruited by MinD to the membrane at a rate $k_{dE}$. Whenever a MinE protein is recruited, it forms a complex together with the MinD protein ($c_{de}$).\n", 53 | " - MinDE complexes detach from the membrane at a rate $k_{de}$. Upon detachment, the complex breaks up, releasing a MinE protein and a MinD-ADP protein into the cytosol. Note that the MinD-ADP proteins can not bind to the membrane (see above).\n", 54 | " - Finally, in the cytosol, MinD-ADP is converted to MinD-ATP at a rate $k_{nucEx}$, corresponding to nucleotide exchange." 55 | ] 56 | }, 57 | { 58 | "cell_type": "markdown", 59 | "id": "2a49a7cd", 60 | "metadata": {}, 61 | "source": [ 62 | "The corresponding partial differential equations read:\n", 63 | "$$\n", 64 | "\\partial_t c_{DD} = D_{cytD} \\cdot \\nabla^2 c_{DD} - k_{nucEx} \\cdot c_{DD} \\\\\n", 65 | "\\partial_t c_{DT} = D_{cytD} \\cdot \\nabla^2 c_{DT} + k_{nucEx} \\cdot c_{DD} \\\\\n", 66 | "\\partial_t c_{E} = D_{cytE} \\cdot \\nabla^2 c_{E} \\\\\n", 67 | "\\partial_t c_{d} = D_{mem} \\cdot \\nabla_m^2 c_{d} + c_{DT} \\cdot ( k_{D} + k_{dD} \\cdot c_d) - k_{dE} \\cdot c_E \\cdot c_d \\\\\n", 68 | "\\partial_t c_{de} = D_{mem} \\cdot \\nabla_m^2 c_{de} + k_{dE} \\cdot c_E \\cdot c_d - k_{de} \\cdot c_{de} \\, ,\n", 69 | "$$\n", 70 | "with reactive boundary conditions for the cytosolic quantities\n", 71 | "$$\n", 72 | "D_{cytD} \\, \\nabla_n c_{DD} = k_{de} \\cdot c_{de} \\\\\n", 73 | "D_{cytD} \\, \\nabla_n c_{DT} = - c_{DT} \\cdot ( k_{D} + k_{dD} \\cdot c_d) \\\\\n", 74 | "D_{cytE} \\, \\nabla_n c_{E} = k_{de} \\cdot c_{de} - k_{dE} \\cdot c_E \\cdot c_d \\, .\n", 75 | "$$\n", 76 | "\n", 77 | "As before, $\\nabla_n$ denotes the gradient perpendicular to the membrane, whereas $\\nabla_m^2$ is the diffusion operator along the membrane." 78 | ] 79 | }, 80 | { 81 | "cell_type": "markdown", 82 | "id": "82fcd82e", 83 | "metadata": {}, 84 | "source": [ 85 | "## Part 1: Analysing the problem heuristically" 86 | ] 87 | }, 88 | { 89 | "cell_type": "markdown", 90 | "id": "caa75312", 91 | "metadata": {}, 92 | "source": [ 93 | "### Problem 1: Nonlinearities\n", 94 | "Note down all the nonlinear terms in the PDEs above." 95 | ] 96 | }, 97 | { 98 | "cell_type": "markdown", 99 | "id": "2a8f6b4e", 100 | "metadata": {}, 101 | "source": [ 102 | "### Problem 2: Intuitive analysis\n", 103 | "Consider the states where MinD and MinE proteins are homogeneously distributed\n", 104 | " - both on the membrane (only $c_{de}$ is non-zero),\n", 105 | " - on the membrane (MinD, $c_d \\neq 0$) and in the cytosol (MinE, $c_E \\neq 0$, all other quantities are zero),\n", 106 | " - both in the cytosol (ATP-bound), i.e., $c_{DD} = c_d = c_{de} = 0$, and the other two quantities are non-zero.\n", 107 | " \n", 108 | "For each of these cases, describe (in words) the protein dynamics in this state.\n", 109 | "\n", 110 | "_Hint: use phrases like \"MinD proteins bind to the membrane\", \"additional MinD is recruited to the membrane\", \"MinDE complexes detach from the membrane\", etc._" 111 | ] 112 | }, 113 | { 114 | "cell_type": "markdown", 115 | "id": "7ac9e420", 116 | "metadata": {}, 117 | "source": [ 118 | "## Part 2: Testing the system numerically\n", 119 | "### Problem 3\n", 120 | "The system involves reactive boundary conditions, diffusion on curved membranes, and is extremely sensitive to protein concentration numbers. This all makes a numeric implementation rather tedious, and requires some tinkering at several points. Walk through the following code carefully, and make a list of the steps and pitfalls required when translating a formal set of equations into real code. You are also required to complete (i.e., fill in blanks) the expressions representing the PDEs." 121 | ] 122 | }, 123 | { 124 | "cell_type": "markdown", 125 | "id": "6354b419", 126 | "metadata": {}, 127 | "source": [ 128 | "#### Geometry, grid, and concentrations:\n", 129 | "The Min system is extremely sensitive to variations in average protein concentrations. This means that pole-to-pole oscillations can be observed, for example, at a specific protein concentration $c_E^*$, but for slightly varied concentrations ($\\pm 10 \\%$) the oscillations break down. This poses a major problem for numerical implementation: since the grid is discrete and not continuous, it is not straightforward to define a concentration that respects the conservation of mass and at the same time does not alter the PDEs. In general, this issue can be resolved by carefully rescaling all parameters and quantities based on the mesh resolution. For simplicity, however, we cheat here and use a geometry that happens to have the correct scaling (i.e., one where the grid cells have resolution 1).\n", 130 | "\n", 131 | "In nature, _E. coli_ cells are $5 \\mu m$ long and $1 \\mu m$ wide. To observe a pattern, we need about five grid cells (preferably much more) in each direction. Instead of the \"natural\" geometry of the cells, we therefore blow up the geometry by a factor 5, so that we can have sufficiently high resolution of the grid cells while keeping the resolution parameter at 1." 132 | ] 133 | }, 134 | { 135 | "cell_type": "code", 136 | "execution_count": null, 137 | "id": "ba604df2", 138 | "metadata": {}, 139 | "outputs": [], 140 | "source": [ 141 | "# Define the geometry of the system.\n", 142 | "# We use a regular grid (rectangle) with a specified width (x-direction) and height (y-direction).\n", 143 | "\n", 144 | "width = 25\n", 145 | "height = 5\n", 146 | "resolution = 1\n", 147 | "\n", 148 | "# pde.CartesianGrid() creates a grid object (mesh) specified by the rectangle corners and a sampling resolution for both axes.\n", 149 | "grid = pde.CartesianGrid(\n", 150 | " [[0, width], [0, height]],\n", 151 | " [int(resolution*i) for i in [width, height]]\n", 152 | ")\n", 153 | "grid.plot();" 154 | ] 155 | }, 156 | { 157 | "cell_type": "markdown", 158 | "id": "fda1dfe6", 159 | "metadata": {}, 160 | "source": [ 161 | "#### Membrane masking:\n", 162 | "To simulate the membrane dynamics, `py-pde` requires a mask, like in the source-degradation exercise. Now, however, the situation is more complex, since the membrane now encloses the cell (instead of being just one horizontal line). On the horizontal cell membranes, there will be only diffuxion in x-direction, whereas on the vertical membrane there is diffusion in y-direction. To account for this, we define two separate masks, `mask_x` and `mask_y`." 163 | ] 164 | }, 165 | { 166 | "cell_type": "code", 167 | "execution_count": null, 168 | "id": "2fbf9c6c", 169 | "metadata": {}, 170 | "outputs": [], 171 | "source": [ 172 | "membrane_mask = pde.ScalarField(grid, dtype=float)\n", 173 | "membrane_mask.data[:, 0] = 1.0\n", 174 | "membrane_mask.data[:, -1] = 1.0\n", 175 | "membrane_mask.data[0,:] = 1.0\n", 176 | "membrane_mask.data[-1,:] = 1.0\n", 177 | "\n", 178 | "mask_x, mask_y = [pde.ScalarField(grid, dtype=float) for _ in range(2)]\n", 179 | "mask_x.data[:,0] = 1.0\n", 180 | "mask_x.data[:,-1] = 1.0\n", 181 | "mask_y.data[0,:] = 1.0\n", 182 | "mask_y.data[-1,:] = 1.0\n", 183 | "\n", 184 | "\n", 185 | "membrane_mask.plot();\n", 186 | "mask_x.plot();\n", 187 | "mask_y.plot();" 188 | ] 189 | }, 190 | { 191 | "cell_type": "code", 192 | "execution_count": null, 193 | "id": "248f3ea2", 194 | "metadata": {}, 195 | "outputs": [], 196 | "source": [ 197 | "# Set up laplace operator in x direction only\n", 198 | "make_laplace_x = functools.partial(make_derivative2, axis=0)\n", 199 | "pde.CartesianGrid.register_operator('laplace_x', make_laplace_x)\n", 200 | "\n", 201 | "# Set up laplace operator in y direction only\n", 202 | "make_laplace_y = functools.partial(make_derivative2, axis=1)\n", 203 | "pde.CartesianGrid.register_operator('laplace_y', make_laplace_y)\n" 204 | ] 205 | }, 206 | { 207 | "cell_type": "markdown", 208 | "id": "3d796a39", 209 | "metadata": {}, 210 | "source": [ 211 | "#### Defining the system:\n", 212 | "Now we can translate the PDEs into code. For the parameters, we use values obtained in Halatek and Frey, Cell, 2012 (cell.com/cell-reports/fulltext/S2211-1247(12)00118-0). These are in parts determined experimentally, and in parts via numeric parameter sweeps." 213 | ] 214 | }, 215 | { 216 | "cell_type": "code", 217 | "execution_count": null, 218 | "id": "83633fa3", 219 | "metadata": {}, 220 | "outputs": [], 221 | "source": [ 222 | "# Parameters\n", 223 | "D_cytD = 16\n", 224 | "D_cytE = 10\n", 225 | "D_mem = 0.01\n", 226 | "\n", 227 | "k_nucEx = 6\n", 228 | "\n", 229 | "k_D = 0.1\n", 230 | "k_dD = 0.1\n", 231 | "k_dE = 0.4\n", 232 | "k_de = 0.4\n", 233 | "\n", 234 | "nD = 2000\n", 235 | "nE = 700\n", 236 | "\n", 237 | "# Equations with boundary conditions\n", 238 | "expr = {'c_d' : f'(laplace_x(c_d ) * maskX + laplace_y(c_d ) * maskY) * {D_mem} + ' + # Diffusion\n", 239 | " f'mask * (c_DT * ({k_D} + {k_dD} * c_d) - {k_dE} * c_E * c_d)', # Reaction\n", 240 | " \n", 241 | " 'c_de': f' ## BLANK ## * {D_mem} + ' + # Diffusion\n", 242 | " f'mask * ({k_dE} * c_E * c_d - {k_de} * c_de)', # Reaction\n", 243 | "\n", 244 | " \n", 245 | " 'c_E': f'laplace(c_E) * {D_cytE} + ' + # Diffusion\n", 246 | " f'mask * ({k_de} * c_de - {k_dE} * c_E * c_d)', # Boundary\n", 247 | " \n", 248 | " 'c_DD': f'laplace(c_DD) * {D_cytD} ' + # Diffusion\n", 249 | " f'- {k_nucEx} * c_DD + ' + # Reactions\n", 250 | " f'mask * ({k_de} * c_de)', # Boundary\n", 251 | " \n", 252 | " 'c_DT': f'## BLANK ## ' + # Diffusion\n", 253 | " f'+ ## BLANK ## + ' + # Reactions\n", 254 | " f'mask * (-c_DT * ({k_D} + {k_dD} * c_d))'} # Boundary\n", 255 | "\n", 256 | "# Set the initial values for all fields:\n", 257 | "# To keep deviations from the total mass minimal, we randomize only two fields.\n", 258 | "c_d = pde.ScalarField.random_uniform(grid) * 0.0\n", 259 | "c_de = pde.ScalarField.random_uniform(grid) * 0.0\n", 260 | "c_E = pde.ScalarField.random_uniform(grid) * 1.0 - 0.5\n", 261 | "c_DD = pde.ScalarField.random_uniform(grid) * 1.0 - 0.5\n", 262 | "c_DT = pde.ScalarField.random_uniform(grid) * 0.0\n", 263 | "\n", 264 | "# Introduce an initial asymmetry to speed up the simulations:\n", 265 | "## To ensure mass conservation later on, we need to introduce an auxilary variable.\n", 266 | "nodeCount = grid.shape[0] * grid.shape[1]\n", 267 | "\n", 268 | "## Now we place some MinD on the left side of the membrane\n", 269 | "c_d.data[:8,:] = 5\n", 270 | "c_d.data = c_d.data * membrane_mask\n", 271 | "\n", 272 | "## To ensure mass conservation, we need to remove the MinD proteins that we just placed\n", 273 | "## on the membrane from the cytosol, and evenly distribute the remaining MinD in the \n", 274 | "## MinD-ADP state.\n", 275 | "c_DD.data[:,:] += (nD - sum(sum(c_d.data))) / nodeCount\n", 276 | "c_E.data[:,:] += nE / nodeCount\n", 277 | "\n", 278 | "\n", 279 | "# Now solve the system as usual:\n", 280 | "eq = pde.PDE(expr, consts={'mask': membrane_mask, 'maskX': mask_x, 'maskY': mask_y})\n", 281 | "\n", 282 | "field = pde.FieldCollection([c_d, c_de, c_E, c_DD, c_DT])\n", 283 | "field.plot();\n", 284 | "\n", 285 | "storage = pde.MemoryStorage()\n", 286 | "\n", 287 | "res = eq.solve(field, t_range=2000, tracker=[storage.tracker(1), \"progress\"])\n", 288 | "res.plot();" 289 | ] 290 | }, 291 | { 292 | "cell_type": "markdown", 293 | "id": "7d1a9553", 294 | "metadata": {}, 295 | "source": [ 296 | "## Part 3: Evaluation" 297 | ] 298 | }, 299 | { 300 | "cell_type": "markdown", 301 | "id": "b322a81c", 302 | "metadata": {}, 303 | "source": [ 304 | "### Problem 4: Basic consistency checks\n", 305 | "To make sure that there are no typos in our code, it is good practice to make consitency checks on the simulation results. These consist of calculating/plotting quantities from the data that we can also compute analytically: if the results don't match the expectations, then something went wrong.\n", 306 | "\n", 307 | "A simple consistency check for mass-conserving systems is the conservation of mass. Plot the _total_ MinD mass and MinE mass over time, and check whether the mass is conserved. Explain observed deviations from mass conservation.\n", 308 | "\n", 309 | "_Hint: the total mass is computed by adding the concentrations of all corresponding fields and summing over all vertices._\n", 310 | "\n", 311 | "_Hint: note that the concentration of MinDE complexes contributes to the MinD mass as well as to the MinE mass._" 312 | ] 313 | }, 314 | { 315 | "cell_type": "code", 316 | "execution_count": null, 317 | "id": "e96a855f", 318 | "metadata": {}, 319 | "outputs": [], 320 | "source": [ 321 | "times = np.arange(len(storage.data))\n", 322 | "\n", 323 | "# This is a check whether the total mass is conserved. The entries in each sublist should remain constant (within factor 0.001).\n", 324 | "# The first row is the total MinD mass (c_d + c_de + c_DD + c_DT = nD), the second row is the total MinE mass (c_de + c_E = nE).\n", 325 | "MinD_mass = np.array([sum([sum(sum(_)) for _ in [storage.data[t][i] for i in [0,1,3,4]]]) for t in times])\n", 326 | "MinE_mass = np.array([sum([## BLANK ## for _ in [## BLANK ##]]) for t in times])\n", 327 | "\n", 328 | "plt.plot(times, MinD_mass, label=\"MinD\")\n", 329 | "plt.plot(times, MinE_mass, label=\"MinE\")\n", 330 | "plt.legend()\n", 331 | "plt.title(\"Total MinD/MinE masses\")\n", 332 | "plt.show()\n", 333 | "\n", 334 | "plt.plot(times, (MinD_mass - MinD_mass[0])/nD, label=\"MinD\")\n", 335 | "plt.plot(times, (MinE_mass - MinE_mass[0])/nE, label=\"MinE\")\n", 336 | "plt.legend()\n", 337 | "plt.title(\"Relative deviation from\\nthe initial total masses\")\n", 338 | "plt.show()\n" 339 | ] 340 | }, 341 | { 342 | "cell_type": "markdown", 343 | "id": "550fd645", 344 | "metadata": {}, 345 | "source": [ 346 | "### Problem 4: Concentration plot\n", 347 | "To visualize our results and be able to interpret them, make a plot of the MinD concentration on the membrane (`c_d`) over time.\n", 348 | "\n", 349 | "_Hint: choose a specific pixel, and extract the concentration at this pixel over time._" 350 | ] 351 | }, 352 | { 353 | "cell_type": "code", 354 | "execution_count": null, 355 | "id": "9eb6fcba", 356 | "metadata": { 357 | "scrolled": true 358 | }, 359 | "outputs": [], 360 | "source": [ 361 | "times = ## BLANK ##\n", 362 | "MinD_concentration = ## BLANK ##\n", 363 | "plt.plot(times, MinD_concentration)\n", 364 | "plt.show()" 365 | ] 366 | }, 367 | { 368 | "cell_type": "markdown", 369 | "id": "bbf855ca", 370 | "metadata": {}, 371 | "source": [ 372 | "### Problem 5: Kymograph\n", 373 | "A very suitable representation of the simulation data is a kymograph. In a kymograph, one axis represents time, and the other axis represents a 1D slice through the system. Thus, the kymograph shows a concentration along this slice over time.\n", 374 | "\n", 375 | "Make a kymograph of the MinD-ADP concentration along the horizontal center axis of the system.\n", 376 | "\n", 377 | "_Hint: due to the specific choice of initial conditions, the first few time steps distort the color range. Consider excluding the first 5 time steps in the kymograph for better visibility._" 378 | ] 379 | }, 380 | { 381 | "cell_type": "code", 382 | "execution_count": null, 383 | "id": "2aa378f3", 384 | "metadata": {}, 385 | "outputs": [], 386 | "source": [ 387 | "kymo = []\n", 388 | "for i in range(len(storage.data)):\n", 389 | " kymo.append(## BLANK ##)\n", 390 | "kymo = np.array(kymo)\n", 391 | "\n", 392 | "\n", 393 | "\n", 394 | "plt.imshow(kymo.T, aspect=20);\n", 395 | "plt.title(\"MinD-ADP concentration\")\n", 396 | "plt.xlabel(\"time (s)\")\n", 397 | "plt.ylabel(\"x $(\\mu m)$\")\n", 398 | "plt.show();" 399 | ] 400 | }, 401 | { 402 | "cell_type": "markdown", 403 | "id": "dc750a14", 404 | "metadata": {}, 405 | "source": [ 406 | "Make kymographs for the MinD-ATP concentration, the MinE concentration, as well as the two membrane concentrations.\n", 407 | "\n", 408 | "_Hint: the membrane concentrations should be evaluated on the membrane, not at a central slize._" 409 | ] 410 | }, 411 | { 412 | "cell_type": "code", 413 | "execution_count": null, 414 | "id": "b17611f0", 415 | "metadata": {}, 416 | "outputs": [], 417 | "source": [ 418 | "def full_kymograph(data, aspect_ratio_rescaling = 1):\n", 419 | "\n", 420 | " lst = [(\"MinD-ADP\", 3, 2), (\"MinD-ATP\", 4, 2), (\"MinE\", 2, 2), (\"MinD membrane\", 0, 0), (\"MinDE membrane\", 1, 0)]\n", 421 | "\n", 422 | " for lbl, index, pos in lst:\n", 423 | " kymo = []\n", 424 | " for i in range(len(data)):\n", 425 | " ## BLANK ##\n", 426 | " kymo = np.array(kymo)\n", 427 | "\n", 428 | "\n", 429 | "\n", 430 | " plt.imshow(kymo.T, aspect=20/aspect_ratio_rescaling);\n", 431 | " plt.title(lbl + \" concentration\")\n", 432 | " plt.xlabel(\"time (s)\")\n", 433 | " plt.ylabel(\"x $(\\mu m)$\")\n", 434 | " plt.show();\n", 435 | "\n", 436 | "full_kymograph(storage.data)" 437 | ] 438 | }, 439 | { 440 | "cell_type": "markdown", 441 | "id": "ac6b2582", 442 | "metadata": {}, 443 | "source": [ 444 | "### Problem 6: Length variations\n", 445 | "As you can see in the video of _E. coli_ cells linked above, different oscillation \"modes\" can be observed for different cell aspect ratios: for short cells, the desired pole-to-pole oscillations are obtained, but for long cells, multiple oscillations are present simultaneously. In the following, we will reproduce these observations.\n", 446 | "\n", 447 | "Discuss your observations." 448 | ] 449 | }, 450 | { 451 | "cell_type": "code", 452 | "execution_count": null, 453 | "id": "8e04717a", 454 | "metadata": {}, 455 | "outputs": [], 456 | "source": [ 457 | "def simulate_with_different_length(rescale_factor):\n", 458 | "\n", 459 | " width = 25 * rescale_factor\n", 460 | " height = 5\n", 461 | " resolution = 1\n", 462 | "\n", 463 | " \n", 464 | " grid = pde.CartesianGrid(\n", 465 | " [[0, width], [0, height]],\n", 466 | " [int(resolution*i) for i in [width, height]]\n", 467 | " )\n", 468 | " grid.plot();\n", 469 | "\n", 470 | "\n", 471 | " membrane_mask = pde.ScalarField(grid, dtype=float)\n", 472 | " membrane_mask.data[:, 0] = 1.0\n", 473 | " membrane_mask.data[:, -1] = 1.0\n", 474 | " membrane_mask.data[0,:] = 1.0\n", 475 | " membrane_mask.data[-1,:] = 1.0\n", 476 | "\n", 477 | " mask_x, mask_y = [pde.ScalarField(grid, dtype=float) for _ in range(2)]\n", 478 | " mask_x.data[:,0] = 1.0\n", 479 | " mask_x.data[:,-1] = 1.0\n", 480 | " mask_y.data[0,:] = 1.0\n", 481 | " mask_y.data[-1,:] = 1.0\n", 482 | "\n", 483 | " # Parameters\n", 484 | " D_cytD = 16\n", 485 | " D_cytE = 10\n", 486 | " D_mem = 0.01\n", 487 | "\n", 488 | " k_nucEx = 6\n", 489 | "\n", 490 | " k_D = 0.1\n", 491 | " k_dD = 0.1\n", 492 | " k_dE = 0.4\n", 493 | " k_de = 0.4\n", 494 | "\n", 495 | " nD = 2000 * rescale_factor\n", 496 | " nE = 700 * rescale_factor\n", 497 | "\n", 498 | " expr = {'c_d' : f'(laplace_x(c_d ) * maskX + laplace_y(c_d ) * maskY) * {D_mem} + ' + # Diffusion\n", 499 | " f'mask * (c_DT * ({k_D} + {k_dD} * c_d) - {k_dE} * c_E * c_d)', # Reaction\n", 500 | " 'c_de': f'(laplace_x(c_de) * maskX + laplace_y(c_de) * maskY) * {D_mem} + ' + # Diffusion\n", 501 | " f'mask * ({k_dE} * c_E * c_d - {k_de} * c_de)', # Reaction\n", 502 | "\n", 503 | " 'c_E': f'laplace(c_E) * {D_cytE} + ' + # Diffusion\n", 504 | " f'mask * ({k_de} * c_de - {k_dE} * c_E * c_d)', # Boundary\n", 505 | " 'c_DD': f'laplace(c_DD) * {D_cytD} - {k_nucEx} * c_DD + ' + # Diffusion\n", 506 | " f'mask * ({k_de} * c_de)', # Boundary\n", 507 | " 'c_DT': f'laplace(c_DT) * {D_cytD} + {k_nucEx} * c_DD + ' + # Diffusion\n", 508 | " f'mask * (-c_DT * ({k_D} + {k_dD} * c_d))'} # Boundary\n", 509 | "\n", 510 | " c_d = pde.ScalarField.random_uniform(grid) * 0.0\n", 511 | " c_de = pde.ScalarField.random_uniform(grid) * 0.0\n", 512 | " c_E = pde.ScalarField.random_uniform(grid) * 1.0 - 0.5\n", 513 | " c_DD = pde.ScalarField.random_uniform(grid) * 1.0 - 0.5\n", 514 | " c_DT = pde.ScalarField.random_uniform(grid) * 0.0\n", 515 | "\n", 516 | " nodeCount = grid.shape[0] * grid.shape[1]\n", 517 | " c_d.data[:8,:] = 5\n", 518 | " c_d.data = c_d.data * membrane_mask\n", 519 | " c_DD.data[:,:] += (nD - sum(sum(c_d.data))) / nodeCount\n", 520 | " c_E.data[:,:] += nE / nodeCount\n", 521 | "\n", 522 | " eq = pde.PDE(expr, consts={'mask': membrane_mask, 'maskX': mask_x, 'maskY': mask_y})\n", 523 | " field = pde.FieldCollection([c_d, c_de, c_E, c_DD, c_DT])\n", 524 | "\n", 525 | " storage = pde.MemoryStorage()\n", 526 | "\n", 527 | " res = eq.solve(field, t_range=2000, tracker=[storage.tracker(1), \"progress\"])\n", 528 | " res.plot();\n", 529 | " return storage" 530 | ] 531 | }, 532 | { 533 | "cell_type": "code", 534 | "execution_count": null, 535 | "id": "7dff9559", 536 | "metadata": {}, 537 | "outputs": [], 538 | "source": [ 539 | "s3 = simulate_with_different_length(3.)\n", 540 | "s6 = simulate_with_different_length(6.)" 541 | ] 542 | }, 543 | { 544 | "cell_type": "code", 545 | "execution_count": null, 546 | "id": "a1858e60", 547 | "metadata": {}, 548 | "outputs": [], 549 | "source": [ 550 | "for s, i in [(s3, 3), (s6, 6)]:\n", 551 | " print(f\"{i}-fold length\")\n", 552 | " full_kymograph(s.data, aspect_ratio_rescaling=i)" 553 | ] 554 | }, 555 | { 556 | "cell_type": "code", 557 | "execution_count": null, 558 | "id": "49d78304", 559 | "metadata": {}, 560 | "outputs": [], 561 | "source": [] 562 | } 563 | ], 564 | "metadata": { 565 | "kernelspec": { 566 | "display_name": "Python 3", 567 | "language": "python", 568 | "name": "python3" 569 | }, 570 | "language_info": { 571 | "codemirror_mode": { 572 | "name": "ipython", 573 | "version": 3 574 | }, 575 | "file_extension": ".py", 576 | "mimetype": "text/x-python", 577 | "name": "python", 578 | "nbconvert_exporter": "python", 579 | "pygments_lexer": "ipython3", 580 | "version": "3.8.12" 581 | }, 582 | "toc": { 583 | "base_numbering": 1, 584 | "nav_menu": {}, 585 | "number_sections": true, 586 | "sideBar": true, 587 | "skip_h1_title": false, 588 | "title_cell": "Table of Contents", 589 | "title_sidebar": "Contents", 590 | "toc_cell": false, 591 | "toc_position": {}, 592 | "toc_section_display": true, 593 | "toc_window_display": false 594 | } 595 | }, 596 | "nbformat": 4, 597 | "nbformat_minor": 5 598 | } 599 | -------------------------------------------------------------------------------- /tutorial-notebooks/day2_2_physics-of-diffusion.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "6af6e5c4", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "# This notebook requires py-pde in version 0.17.1 or later\n", 11 | "# The package can be obtained from https://github.com/zwicker-group/py-pde\n", 12 | "# Alternatively, it can be installed via pip or conda\n", 13 | "\n", 14 | "import pde\n", 15 | "\n", 16 | "# plotting functions\n", 17 | "import matplotlib.pyplot as plt\n", 18 | "import numpy as np" 19 | ] 20 | }, 21 | { 22 | "cell_type": "markdown", 23 | "id": "9f82fe2c", 24 | "metadata": {}, 25 | "source": [ 26 | "So far, we have looked at simple diffusion ($\\nabla^2 c$), where patterns arose from non-linear reactions.\n", 27 | "\n", 28 | "Next, we want to go beyond simple diffusion. We will see that patterns can then arise even from linear reactions." 29 | ] 30 | }, 31 | { 32 | "cell_type": "markdown", 33 | "id": "7a3a6851", 34 | "metadata": {}, 35 | "source": [ 36 | "# Physics of simple diffusion\n", 37 | "\n", 38 | "To understand more complex cases, let us first investigate the physics of simple diffusion.\n", 39 | "Simple diffusion is a good description of dilute solutions or ideal gases." 40 | ] 41 | }, 42 | { 43 | "cell_type": "markdown", 44 | "id": "1bade711", 45 | "metadata": {}, 46 | "source": [ 47 | "## Thermodynamics of ideal solutions\n", 48 | "\n", 49 | "Let us consider $N$ particles in a volume $V$ at fixed temperature $T$.\n", 50 | "We then know (from Statistical Mechanics):\n", 51 | "\n", 52 | "- The free energy $F=k_\\mathrm{B}T N\\left[\\ln\\frac{N}{V} + a \\right]$\n", 53 | "- The chemical potential $\\mu = \\left(\\frac{\\partial F}{\\partial N}\\right)_{V,T} = k_\\mathrm{B}T \\ln\\frac{N}{V} + \\mu_0 $\n", 54 | "\n", 55 | "Defining the concentration $c=\\frac{N}{V}$, we find the simpler expression\n", 56 | "\\begin{align}\n", 57 | "\\mu = k_\\mathrm{B}T \\ln c + \\mu_0\n", 58 | "\\end{align}" 59 | ] 60 | }, 61 | { 62 | "cell_type": "markdown", 63 | "id": "685d86f7", 64 | "metadata": {}, 65 | "source": [ 66 | "### Problem 1: Chemical potential of ideal solutions\n", 67 | "Plot the chemical potential $\\mu$ as a function of $c$. Can you interpret the qualitative functional form?" 68 | ] 69 | }, 70 | { 71 | "cell_type": "code", 72 | "execution_count": 2, 73 | "id": "0e6b7583", 74 | "metadata": {}, 75 | "outputs": [ 76 | { 77 | "ename": "SyntaxError", 78 | "evalue": "invalid syntax (2156466757.py, line 2)", 79 | "output_type": "error", 80 | "traceback": [ 81 | "\u001b[0;36m File \u001b[0;32m\"/var/folders/dy/9_2jrg4s4zj7m6c8z14d5ntw0000gn/T/ipykernel_61299/2156466757.py\"\u001b[0;36m, line \u001b[0;32m2\u001b[0m\n\u001b[0;31m plt.plot(c, %% BLANK %%)\u001b[0m\n\u001b[0m ^\u001b[0m\n\u001b[0;31mSyntaxError\u001b[0m\u001b[0;31m:\u001b[0m invalid syntax\n" 82 | ] 83 | } 84 | ], 85 | "source": [ 86 | "c = np.linspace(1, 10)\n", 87 | "plt.plot(c, %% BLANK %%)\n", 88 | "plt.xlabel('Concentration $c$')\n", 89 | "plt.ylabel(r'Chemical potential $\\mu$');" 90 | ] 91 | }, 92 | { 93 | "cell_type": "markdown", 94 | "id": "bd9715cc", 95 | "metadata": {}, 96 | "source": [ 97 | "## Dynamics of an inhomogeneous system\n", 98 | "An inhomogeneous system can be described by a concentration field $c(x, y, t)$. The dynamics of such a system are given by\n", 99 | "\n", 100 | "\\begin{align}\n", 101 | " \\partial_t c(x, y, t) = \\nabla\\bigl[M(c) \\nabla \\mu(c) \\bigr]\n", 102 | "\\end{align}" 103 | ] 104 | }, 105 | { 106 | "cell_type": "markdown", 107 | "id": "97a4b3c9", 108 | "metadata": {}, 109 | "source": [ 110 | "### Problem 2: Dynamics of ideal solutions\n", 111 | "Use the chemical potential $\\mu = k_\\mathrm{B}T \\ln c + \\mu_0$ and the mobility function $M(c) = M_0 c$ to derive the dynamics of ideal solutions analytically." 112 | ] 113 | }, 114 | { 115 | "cell_type": "markdown", 116 | "id": "fa93579d", 117 | "metadata": {}, 118 | "source": [ 119 | "# Physics of non-ideal fluids\n", 120 | "We saw that the physics of ideal solutions, which consist of non-interacting particles, leads to simple diffusion.\n", 121 | "We next consider a simple model of non-ideal fluids, where particles will interact." 122 | ] 123 | }, 124 | { 125 | "cell_type": "markdown", 126 | "id": "4980fa2f", 127 | "metadata": {}, 128 | "source": [ 129 | "## Thermodynamics of non-ideal fluids\n", 130 | "We consider a fluid comprised of two components, $A$ and $B$, which have the same molecular volume $\\nu$.\n", 131 | "The composition can then be described by the volume fraction $\\phi_B = \\nu c_B$, while $\\phi_A = \\nu c_A = 1- \\phi_B$ because the fluid fills the entire space.\n", 132 | "We start by deriving the free energy of such solutions, which comprises entropic and enthalpic contributions." 133 | ] 134 | }, 135 | { 136 | "cell_type": "markdown", 137 | "id": "23d102c7", 138 | "metadata": {}, 139 | "source": [ 140 | "### Problem 3: Entropic contributions\n", 141 | "Visualize the entropic contributions\n", 142 | "\n", 143 | "\\begin{align}\n", 144 | " F_\\mathrm{S} = k_\\mathrm{B} N \\left[\\phi\\ln\\phi + (1 - \\phi)\\ln(1-\\phi)\\right]\n", 145 | "\\end{align}\n", 146 | "\n", 147 | "for $\\phi \\in (0, 1)$.\n", 148 | "What can you conclude from the qualitative shape?" 149 | ] 150 | }, 151 | { 152 | "cell_type": "code", 153 | "execution_count": null, 154 | "id": "f8c6962a", 155 | "metadata": {}, 156 | "outputs": [], 157 | "source": [ 158 | "c = np.linspace(0, 1)[1:-1]\n", 159 | "plt.plot(c, %% BLANK %%)\n", 160 | "plt.xlabel('Fraction $\\phi$')\n", 161 | "plt.ylabel(r'Entropic contributions $F_\\mathrm{S}$');" 162 | ] 163 | }, 164 | { 165 | "cell_type": "markdown", 166 | "id": "bd484fdd", 167 | "metadata": {}, 168 | "source": [ 169 | "### Problem 4: Enthalpic contributions\n", 170 | "Visualize the enthalpic contributions\n", 171 | "\n", 172 | "\n", 173 | "\\begin{align}\n", 174 | " F_\\mathrm{I} = k_\\mathrm{B} N \\chi \\phi (1 - \\phi)\n", 175 | "\\end{align}\n", 176 | "\n", 177 | "for $\\phi \\in (0, 1)$. What can you conclude from the qualitative shape?" 178 | ] 179 | }, 180 | { 181 | "cell_type": "code", 182 | "execution_count": null, 183 | "id": "05e4b104", 184 | "metadata": {}, 185 | "outputs": [], 186 | "source": [ 187 | "c = np.linspace(0, 1)[1:-1]\n", 188 | "plt.plot(c, %% BLANK %%)\n", 189 | "plt.xlabel('Fraction $\\phi$')\n", 190 | "plt.ylabel(r'Enthalpic contributions $F_\\mathrm{S}$');" 191 | ] 192 | }, 193 | { 194 | "cell_type": "markdown", 195 | "id": "ce3b31c3", 196 | "metadata": {}, 197 | "source": [ 198 | "### Problem 5: Total free energy\n", 199 | "We now combine the entropic and enthalpic part to obtain the Flory-Huggins free energy \n", 200 | "\n", 201 | "\\begin{align}\n", 202 | " F = k_\\mathrm{B} N \\left[\\phi\\ln\\phi + (1 - \\phi)\\ln(1-\\phi) + \\chi\\phi(1 - \\phi)\\right]\n", 203 | "\\end{align}\n", 204 | "\n", 205 | "Visualize $F$ as a function of $\\phi \\in(0,1)$ for $\\chi = 1.5$ and $\\chi = 2.5$. What do you notice?" 206 | ] 207 | }, 208 | { 209 | "cell_type": "code", 210 | "execution_count": null, 211 | "id": "e863f6f2", 212 | "metadata": {}, 213 | "outputs": [], 214 | "source": [ 215 | "c = np.linspace(0, 1)[1:-1]\n", 216 | "plt.plot(c, %% BLANK %%, label='$\\chi=1.5$')\n", 217 | "plt.plot(c, %% BLANK %%, label='$\\chi=2.5$')\n", 218 | "plt.xlabel('Fraction $\\phi$')\n", 219 | "plt.ylabel(r'Free energy $F$')\n", 220 | "plt.legend();" 221 | ] 222 | }, 223 | { 224 | "cell_type": "markdown", 225 | "id": "4e301b40", 226 | "metadata": {}, 227 | "source": [ 228 | "## Simplified free energy of non-ideal fluids\n", 229 | "Instead of using the Flory-Huggins free energy, we will use the simpler polynomial form\n", 230 | "\n", 231 | "\\begin{align}\n", 232 | " F = a V \\phi^2 ( 1- \\phi)^2\n", 233 | "\\end{align}" 234 | ] 235 | }, 236 | { 237 | "cell_type": "markdown", 238 | "id": "4b1d8a49", 239 | "metadata": {}, 240 | "source": [ 241 | "### Problem 6: Visualize the free energy\n", 242 | "Visualize the free energy for $\\phi\\in[-0.1,1.1]$. What are qualitatively important features of this free energy?" 243 | ] 244 | }, 245 | { 246 | "cell_type": "code", 247 | "execution_count": null, 248 | "id": "ef243ee5", 249 | "metadata": {}, 250 | "outputs": [], 251 | "source": [ 252 | "c = np.linspace(-0.1, 1.1)\n", 253 | "plt.plot(c, %% BLANK %%)\n", 254 | "plt.xlabel('Fraction $\\phi$')\n", 255 | "plt.ylabel(r'Free energy $F$');" 256 | ] 257 | }, 258 | { 259 | "cell_type": "markdown", 260 | "id": "ca8c6bbe", 261 | "metadata": {}, 262 | "source": [ 263 | "### Problem 7: Derive the chemical potential of this free energy" 264 | ] 265 | }, 266 | { 267 | "cell_type": "markdown", 268 | "id": "2e7ea845", 269 | "metadata": {}, 270 | "source": [ 271 | "Derive the chemical potential, which is given by $\\mu \\propto \\partial F/\\partial \\phi$.\n", 272 | "\n", 273 | "Plot the resulting function. What do you notice?" 274 | ] 275 | }, 276 | { 277 | "cell_type": "code", 278 | "execution_count": null, 279 | "id": "71ead3c9", 280 | "metadata": {}, 281 | "outputs": [], 282 | "source": [ 283 | "plt.plot(c, %% BLANK %%)\n", 284 | "plt.xlabel('Fraction $\\phi$')\n", 285 | "plt.ylabel(r'Chemical potential $\\mu$');" 286 | ] 287 | }, 288 | { 289 | "cell_type": "markdown", 290 | "id": "1765f480", 291 | "metadata": {}, 292 | "source": [ 293 | "## Naive dynamics of non-ideal fluids\n", 294 | "To obtain the dynamics of non-ideal solutions, we next combine the generalized diffusion equation with the chemical potential of a non-ideal solution." 295 | ] 296 | }, 297 | { 298 | "cell_type": "markdown", 299 | "id": "c57de142", 300 | "metadata": {}, 301 | "source": [ 302 | "### Problem 8: Naive dynamics of non-ideal solutions\n", 303 | "Simulate the dynamics of the non-ideal solution using `py-pde`.\n", 304 | "Start with a random initial condition and observe the behavior over time.\n", 305 | "\n", 306 | "What do you observe in the dynamics and the final state?" 307 | ] 308 | }, 309 | { 310 | "cell_type": "code", 311 | "execution_count": null, 312 | "id": "d85b544a", 313 | "metadata": {}, 314 | "outputs": [], 315 | "source": [ 316 | "# prepare a random initial state\n", 317 | "grid = pde.UnitGrid([32, 32], periodic=True)\n", 318 | "initial_state = pde.ScalarField.random_uniform(grid, vmin=0, vmax=1)\n", 319 | "initial_state.plot();" 320 | ] 321 | }, 322 | { 323 | "cell_type": "code", 324 | "execution_count": null, 325 | "id": "599be4b2", 326 | "metadata": {}, 327 | "outputs": [], 328 | "source": [ 329 | "# define the partial differential equation\n", 330 | "eq = pde.PDE({'c': '%% BLANK %%'})\n", 331 | "eq.expressions" 332 | ] 333 | }, 334 | { 335 | "cell_type": "code", 336 | "execution_count": null, 337 | "id": "97983133", 338 | "metadata": {}, 339 | "outputs": [], 340 | "source": [ 341 | "# simulate the dynamics\n", 342 | "final_state = eq.solve(initial_state, t_range=1000, tracker=['progress', 'plot'])\n", 343 | "final_state.plot();" 344 | ] 345 | }, 346 | { 347 | "cell_type": "markdown", 348 | "id": "bbe4ea4f", 349 | "metadata": {}, 350 | "source": [ 351 | "## The gradient term\n", 352 | "It turns out that our naive approach above neglects an important physics process, namely that it is energetically costly to have two regions of very different composition next to each other. Formally, this can be included in our description by adding a term $\\frac{\\kappa}{2} |\\nabla c|^2$ to the free energy, which needs to be integrated over the entire volume. However, since analyzing this term would require functional analysis, we here simply use its consequence, which is to modify the chemical potential like so,\n", 353 | "\n", 354 | "\\begin{align}\n", 355 | " \\mu \\propto \\phi (1 - \\phi) (1 - 2 \\phi) - \\kappa \\nabla^2 c\n", 356 | "\\end{align}" 357 | ] 358 | }, 359 | { 360 | "cell_type": "markdown", 361 | "id": "1074bd05", 362 | "metadata": {}, 363 | "source": [ 364 | "### Problem 9: Improved dynamics of non-ideal fluids\n", 365 | "Simulate the dynamics of the non-ideal solution using the improved chemical potential with $\\kappa=1$.\n", 366 | "Start with a random initial condition and observe the behavior over time.\n", 367 | "\n", 368 | "What do you observe in the dynamics and the final state?" 369 | ] 370 | }, 371 | { 372 | "cell_type": "code", 373 | "execution_count": null, 374 | "id": "dc85ea06", 375 | "metadata": {}, 376 | "outputs": [], 377 | "source": [ 378 | "# prepare a random initial state\n", 379 | "grid = pde.UnitGrid([32, 32], periodic=True)\n", 380 | "initial_state = pde.ScalarField.random_uniform(grid, vmin=0, vmax=1)\n", 381 | "initial_state.plot();" 382 | ] 383 | }, 384 | { 385 | "cell_type": "code", 386 | "execution_count": null, 387 | "id": "d0587ef4", 388 | "metadata": {}, 389 | "outputs": [], 390 | "source": [ 391 | "# define the partial differential equation\n", 392 | "eq = pde.PDE({'c': '%% BLANK %%'})\n", 393 | "eq.expressions" 394 | ] 395 | }, 396 | { 397 | "cell_type": "code", 398 | "execution_count": null, 399 | "id": "9d8f4cd2", 400 | "metadata": {}, 401 | "outputs": [], 402 | "source": [ 403 | "# simulate the dynamics\n", 404 | "final_state = eq.solve(initial_state, t_range=1000, tracker=['progress', 'plot']);\n", 405 | "final_state.plot();" 406 | ] 407 | }, 408 | { 409 | "cell_type": "markdown", 410 | "id": "b302b829", 411 | "metadata": {}, 412 | "source": [ 413 | "### Problem 10: Droplets\n", 414 | "Re-run the simulation with a initial condition where particles $A$ occupy $25\\%$ and particles B occupy $75\\%$ of the system." 415 | ] 416 | }, 417 | { 418 | "cell_type": "code", 419 | "execution_count": null, 420 | "id": "4d1b1548", 421 | "metadata": {}, 422 | "outputs": [], 423 | "source": [ 424 | "# prepare a random initial state\n", 425 | "initial_state = %% BLANK %%\n", 426 | "initial_state.plot();" 427 | ] 428 | }, 429 | { 430 | "cell_type": "code", 431 | "execution_count": null, 432 | "id": "b035764a", 433 | "metadata": {}, 434 | "outputs": [], 435 | "source": [ 436 | "# simulate the dynamics\n", 437 | "final_state = eq.solve(initial_state, t_range=1000, tracker=['progress', 'plot']);\n", 438 | "final_state.plot();" 439 | ] 440 | }, 441 | { 442 | "cell_type": "markdown", 443 | "id": "05687313", 444 | "metadata": {}, 445 | "source": [ 446 | "## Ostwald ripening\n", 447 | "Phase separating systems have a very stereo-typical dynamics, which is known as Ostwald ripening. To demonstrate this, we next consider a slightly larger system." 448 | ] 449 | }, 450 | { 451 | "cell_type": "markdown", 452 | "id": "8c9a2157", 453 | "metadata": {}, 454 | "source": [ 455 | "### Problem 11: Dynamics of many droplets\n", 456 | "Run the simulation shown below (using the same equation as the last problem). The code below produces a plot of the magnitude as a function of time and a movie of the time evolution.\n", 457 | "\n", 458 | "- What do you expect the first plot to look like?\n", 459 | "- What do you observe in the dynamics of the droplets?\n", 460 | "- What could be a physical reason for the observed dynamics?" 461 | ] 462 | }, 463 | { 464 | "cell_type": "code", 465 | "execution_count": null, 466 | "id": "c222c584", 467 | "metadata": {}, 468 | "outputs": [], 469 | "source": [ 470 | "grid = pde.UnitGrid([64, 64], periodic=True)\n", 471 | "\n", 472 | "# prepare a random initial state\n", 473 | "initial_state = pde.ScalarField.random_uniform(grid, vmin=0, vmax=0.5)\n", 474 | "initial_state.plot();" 475 | ] 476 | }, 477 | { 478 | "cell_type": "code", 479 | "execution_count": null, 480 | "id": "27a974c1", 481 | "metadata": { 482 | "scrolled": true 483 | }, 484 | "outputs": [], 485 | "source": [ 486 | "# simulate the dynamics once and store it, so we don't have to run the long simulation twice.\n", 487 | "storage = pde.MemoryStorage() # intialize a storage to save intermediate data\n", 488 | "eq.solve(initial_state, t_range=1e4, tracker=['progress', 'plot', storage.tracker(10)]);" 489 | ] 490 | }, 491 | { 492 | "cell_type": "code", 493 | "execution_count": null, 494 | "id": "4fd8e698", 495 | "metadata": {}, 496 | "outputs": [], 497 | "source": [ 498 | "# Plot the average fraction as a function of time\n", 499 | "pde.plot_magnitudes(storage)" 500 | ] 501 | }, 502 | { 503 | "cell_type": "code", 504 | "execution_count": null, 505 | "id": "9752db5a", 506 | "metadata": {}, 507 | "outputs": [], 508 | "source": [ 509 | "# make a movie of the evolution\n", 510 | "pde.movie(storage, 'ostwald_ripening.mp4', progress=True)" 511 | ] 512 | }, 513 | { 514 | "cell_type": "code", 515 | "execution_count": null, 516 | "id": "0b637a16", 517 | "metadata": {}, 518 | "outputs": [], 519 | "source": [] 520 | } 521 | ], 522 | "metadata": { 523 | "kernelspec": { 524 | "display_name": "Python 3 (ipykernel)", 525 | "language": "python", 526 | "name": "python3" 527 | }, 528 | "language_info": { 529 | "codemirror_mode": { 530 | "name": "ipython", 531 | "version": 3 532 | }, 533 | "file_extension": ".py", 534 | "mimetype": "text/x-python", 535 | "name": "python", 536 | "nbconvert_exporter": "python", 537 | "pygments_lexer": "ipython3", 538 | "version": "3.8.10" 539 | }, 540 | "toc": { 541 | "base_numbering": 1, 542 | "nav_menu": {}, 543 | "number_sections": true, 544 | "sideBar": true, 545 | "skip_h1_title": false, 546 | "title_cell": "Table of Contents", 547 | "title_sidebar": "Contents", 548 | "toc_cell": false, 549 | "toc_position": { 550 | "height": "calc(100% - 180px)", 551 | "left": "10px", 552 | "top": "150px", 553 | "width": "336px" 554 | }, 555 | "toc_section_display": true, 556 | "toc_window_display": true 557 | } 558 | }, 559 | "nbformat": 4, 560 | "nbformat_minor": 5 561 | } 562 | --------------------------------------------------------------------------------