+
+
+
+
+
Documentation
+
+
+
What is PySDM?
+
+
+ PySDM is a package for simulating the
dynamics of population of particles undergoing diffusional and collisional growth (and breakage).
+ The package features a Pythonic high-performance (multi-threaded CPU & CUDA GPU) implementation of the
Super-Droplet Method (SDM) Monte-Carlo algorithm
+ for representing collisional growth (
Shima et al. 2009), hence the name.
+ It is intended to serve as a building block for simulation systems modelling
fluid flows involving a dispersed phase,
+ with PySDM being responsible for representation of the dispersed phase.
+ Currently, the development is focused on atmospheric
cloud physics applications, in particular on modelling the dynamics of
+ particles immersed in moist air using the particle-based (a.k.a. super-droplet) approach to represent aerosol/cloud/rain microphysics.
+ The key goal of PySDM is to enable rapid development and independent reproducibility of simulations in cloud microphysics
+ while being free from the two-language barrier commonly separating prototype and high-performance research code.
+ PySDM ships with a set of examples reproducing results from literature and serving as tutorials.
+ The animation shown here depicts a flow-coupled simulation in which the flow is resolved using PySDM's sibling project:
PyMPDATA.
+ The examples include also single-column setups (with PyMPDATA used for advection) as well as adiabatic cloud parcel model setups
+ (with
PySDM alone sufficient to constitute a microphysics-resolving cloud parcel model in Python).
+
+
+
+
What is the difference between PySDM and PySDM-examples?
+
+ PySDM is a Python package that provides the implementation of SDM that can be used in your own projects.
+
+
+ PySDM-examples is a Python package that provides examples of how to use PySDM.
+ The package contains common code used in PySDM examples Jupyter notebooks, as well as in PySDM test suite.
+
+
The two projects exist separately on PyPI, but their development and issue tracking is hosted at the same GitHub repository.
+
+
+
Important links
+
+
+ | PySDM |
+ PySDM-examples |
+
+
+ |
+
+ |
+
+
+ |
+
+
+
+
+ 
+ 
+
+ |
+
+
+
+
+
Installation
+
+ PySDM is available on PyPI and can be installed using pip:
+
+
pip install PySDM
+
Note: the way above will not install test-time dependencies, to install them and run the tests, likely the most convenient way is:
+
git clone https://github.com/open-atmos/PySDM.git
+pip install -e PySDM[tests] -e PySDM/examples[tests]
+pytest PySDM
+
(the above should be a viable way to set up development environment for PySDM, see also our Python dev hints Wiki
+ and PySDM HOWTOs for further information)
+
+ PySDM-examples is also available on PyPI and can be installed using pip:
+
+
pip install PySDM-examples
+
Note: this will also install PySDM if needed, but the examples package wheels do not include the Jupyter notebooks - only common code used from the notebooks.
+ All PySDM example notebooks can be viewed on GitHub and feature header cells with badges enabling single-click execution on either
+ Google Colab or mybinder.org platforms.
+ To try the notebooks out locally, use:
+
+
git clone https://github.com/open-atmos/PySDM.git
+pip install -e PySDM -e PySDM/examples
+jupyter-notebook PySDM/examples
+
+
+
+
Contributing, reporting issues, seeking support
+
+ Submitting new code to both packages is done through the same GitHub repository via
+ Pull requests.
+
+
+ Issues regarding any incorrect, unintuitive or undocumented behaviour of PySDM or PySDM-examples
+ are best to be reported on the
+ GitHub issue tracker.
+
+
+ We encourage to use the GitHub Discussions
+ feature (rather than the issue tracker) for seeking support in understanding,
+ using and extending PySDM code.
+
+
+
+
+
Bibliography with code cross-references
+
+ The list below summarises all literature references included in PySDM codebase and includes links to both the referenced papers, as well as to the referring PySDM source files.
+
+ {% include "bibliography.html" %}
+
EOF
+
+
+
+{# {% include "search.html.jinja2" %}#}
+{% endblock %}
+
diff --git a/PySDM/source/docs/templates/syntax-highlighting.css b/PySDM/source/docs/templates/syntax-highlighting.css
new file mode 100644
index 0000000000000000000000000000000000000000..b0a7fe3746cdf607a1b31a6fb97a691eab9a2286
--- /dev/null
+++ b/PySDM/source/docs/templates/syntax-highlighting.css
@@ -0,0 +1,80 @@
+/* monokai color scheme, see pdoc/template/README.md */
+pre { line-height: 125%; }
+span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 20px; }
+.pdoc-code .hll { background-color: #49483e }
+.pdoc-code { background: #272822; color: #f8f8f2 }
+.pdoc-code .c { color: #75715e } /* Comment */
+.pdoc-code .err { color: #960050; background-color: #1e0010 } /* Error */
+.pdoc-code .esc { color: #f8f8f2 } /* Escape */
+.pdoc-code .g { color: #f8f8f2 } /* Generic */
+.pdoc-code .k { color: #66d9ef } /* Keyword */
+.pdoc-code .l { color: #ae81ff } /* Literal */
+.pdoc-code .n { color: #f8f8f2 } /* Name */
+.pdoc-code .o { color: #f92672 } /* Operator */
+.pdoc-code .x { color: #f8f8f2 } /* Other */
+.pdoc-code .p { color: #f8f8f2 } /* Punctuation */
+.pdoc-code .ch { color: #75715e } /* Comment.Hashbang */
+.pdoc-code .cm { color: #75715e } /* Comment.Multiline */
+.pdoc-code .cp { color: #75715e } /* Comment.Preproc */
+.pdoc-code .cpf { color: #75715e } /* Comment.PreprocFile */
+.pdoc-code .c1 { color: #75715e } /* Comment.Single */
+.pdoc-code .cs { color: #75715e } /* Comment.Special */
+.pdoc-code .gd { color: #f92672 } /* Generic.Deleted */
+.pdoc-code .ge { color: #f8f8f2; font-style: italic } /* Generic.Emph */
+.pdoc-code .gr { color: #f8f8f2 } /* Generic.Error */
+.pdoc-code .gh { color: #f8f8f2 } /* Generic.Heading */
+.pdoc-code .gi { color: #a6e22e } /* Generic.Inserted */
+.pdoc-code .go { color: #66d9ef } /* Generic.Output */
+.pdoc-code .gp { color: #f92672; font-weight: bold } /* Generic.Prompt */
+.pdoc-code .gs { color: #f8f8f2; font-weight: bold } /* Generic.Strong */
+.pdoc-code .gu { color: #75715e } /* Generic.Subheading */
+.pdoc-code .gt { color: #f8f8f2 } /* Generic.Traceback */
+.pdoc-code .kc { color: #66d9ef } /* Keyword.Constant */
+.pdoc-code .kd { color: #66d9ef } /* Keyword.Declaration */
+.pdoc-code .kn { color: #f92672 } /* Keyword.Namespace */
+.pdoc-code .kp { color: #66d9ef } /* Keyword.Pseudo */
+.pdoc-code .kr { color: #66d9ef } /* Keyword.Reserved */
+.pdoc-code .kt { color: #66d9ef } /* Keyword.Type */
+.pdoc-code .ld { color: #e6db74 } /* Literal.Date */
+.pdoc-code .m { color: #ae81ff } /* Literal.Number */
+.pdoc-code .s { color: #e6db74 } /* Literal.String */
+.pdoc-code .na { color: #a6e22e } /* Name.Attribute */
+.pdoc-code .nb { color: #f8f8f2 } /* Name.Builtin */
+.pdoc-code .nc { color: #a6e22e } /* Name.Class */
+.pdoc-code .no { color: #66d9ef } /* Name.Constant */
+.pdoc-code .nd { color: #a6e22e } /* Name.Decorator */
+.pdoc-code .ni { color: #f8f8f2 } /* Name.Entity */
+.pdoc-code .ne { color: #a6e22e } /* Name.Exception */
+.pdoc-code .nf { color: #a6e22e } /* Name.Function */
+.pdoc-code .nl { color: #f8f8f2 } /* Name.Label */
+.pdoc-code .nn { color: #f8f8f2 } /* Name.Namespace */
+.pdoc-code .nx { color: #a6e22e } /* Name.Other */
+.pdoc-code .py { color: #f8f8f2 } /* Name.Property */
+.pdoc-code .nt { color: #f92672 } /* Name.Tag */
+.pdoc-code .nv { color: #f8f8f2 } /* Name.Variable */
+.pdoc-code .ow { color: #f92672 } /* Operator.Word */
+.pdoc-code .w { color: #f8f8f2 } /* Text.Whitespace */
+.pdoc-code .mb { color: #ae81ff } /* Literal.Number.Bin */
+.pdoc-code .mf { color: #ae81ff } /* Literal.Number.Float */
+.pdoc-code .mh { color: #ae81ff } /* Literal.Number.Hex */
+.pdoc-code .mi { color: #ae81ff } /* Literal.Number.Integer */
+.pdoc-code .mo { color: #ae81ff } /* Literal.Number.Oct */
+.pdoc-code .sa { color: #e6db74 } /* Literal.String.Affix */
+.pdoc-code .sb { color: #e6db74 } /* Literal.String.Backtick */
+.pdoc-code .sc { color: #e6db74 } /* Literal.String.Char */
+.pdoc-code .dl { color: #e6db74 } /* Literal.String.Delimiter */
+.pdoc-code .sd { color: #e6db74 } /* Literal.String.Doc */
+.pdoc-code .s2 { color: #e6db74 } /* Literal.String.Double */
+.pdoc-code .se { color: #ae81ff } /* Literal.String.Escape */
+.pdoc-code .sh { color: #e6db74 } /* Literal.String.Heredoc */
+.pdoc-code .si { color: #e6db74 } /* Literal.String.Interpol */
+.pdoc-code .sx { color: #e6db74 } /* Literal.String.Other */
+.pdoc-code .sr { color: #e6db74 } /* Literal.String.Regex */
+.pdoc-code .s1 { color: #e6db74 } /* Literal.String.Single */
+.pdoc-code .ss { color: #e6db74 } /* Literal.String.Symbol */
+.pdoc-code .bp { color: #f8f8f2 } /* Name.Builtin.Pseudo */
+.pdoc-code .fm { color: #a6e22e } /* Name.Function.Magic */
+.pdoc-code .vc { color: #f8f8f2 } /* Name.Variable.Class */
+.pdoc-code .vg { color: #f8f8f2 } /* Name.Variable.Global */
+.pdoc-code .vi { color: #f8f8f2 } /* Name.Variable.Instance */
+.pdoc-code .vm { color: #f8f8f2 } /* Name.Variable.Magic */
diff --git a/PySDM/source/docs/templates/theme.css b/PySDM/source/docs/templates/theme.css
new file mode 100644
index 0000000000000000000000000000000000000000..1dfe7c4a1c300ecced5a18c5030c7efcd18779e4
--- /dev/null
+++ b/PySDM/source/docs/templates/theme.css
@@ -0,0 +1,20 @@
+:root {
+ --pdoc-background: #212529;
+}
+
+.pdoc {
+ --text: #f7f7f7;
+ --muted: #9d9d9d;
+ --link: #58a6ff;
+ --link-hover: #3989ff;
+ --code: #333;
+ --active: #555;
+
+ --accent: #343434;
+ --accent2: #555;
+
+ --nav-hover: rgba(0, 0, 0, 0.1);
+ --name: #77C1FF;
+ --def: #0cdd0c;
+ --annotation: #00c037;
+}
diff --git a/PySDM/source/examples/MANIFEST.in b/PySDM/source/examples/MANIFEST.in
new file mode 100644
index 0000000000000000000000000000000000000000..b2f953c2d0812ca2e9796a8f3799523c06527337
--- /dev/null
+++ b/PySDM/source/examples/MANIFEST.in
@@ -0,0 +1,3 @@
+global-exclude *.ipynb
+global-exclude *.csv
+include docs/*.md
\ No newline at end of file
diff --git a/PySDM/source/examples/PySDM_examples/Abade_and_Albuquerque_2024/__init__.py b/PySDM/source/examples/PySDM_examples/Abade_and_Albuquerque_2024/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..f74f0c2df5f7f7d2f40cadfcdd933a8b630af756
--- /dev/null
+++ b/PySDM/source/examples/PySDM_examples/Abade_and_Albuquerque_2024/__init__.py
@@ -0,0 +1,11 @@
+# pylint: disable=invalid-name
+"""
+mixed-phase example using parcel environment based on
+[Abade & Albuquerque 2024 (QJRMS)](https://doi.org/10.1002/qj.4775)
+
+fig_2.ipynb:
+.. include:: ./fig_2.ipynb.badges.md
+"""
+
+from .simulation import Simulation
+from .settings import Settings
diff --git a/PySDM/source/examples/PySDM_examples/Abade_and_Albuquerque_2024/fig_2.ipynb b/PySDM/source/examples/PySDM_examples/Abade_and_Albuquerque_2024/fig_2.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..97fbb7d6273a0cd111391a8c988d52138b529dec
--- /dev/null
+++ b/PySDM/source/examples/PySDM_examples/Abade_and_Albuquerque_2024/fig_2.ipynb
@@ -0,0 +1,19465 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "a3af9f8e-a138-47c8-af08-62451db09352",
+ "metadata": {},
+ "source": [
+ "[](https://github.com/open-atmos/PySDM/blob/main/examples/PySDM_examples/Abade_and_Albuquerque_2024/fig_2.ipynb)\n",
+ "[](https://mybinder.org/v2/gh/open-atmos/PySDM.git/main?urlpath=lab/tree/examples/PySDM_examples/Abade_and_Albuquerque_2024/fig_2.ipynb)\n",
+ "[](https://colab.research.google.com/github/open-atmos/PySDM/blob/main/examples/PySDM_examples/Abade_and_Albuquerque_2024/fig_2.ipynb)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "52caca20-41df-4878-b883-2dc2609591c6",
+ "metadata": {},
+ "source": [
+ "#### based on Fig. 2 from [Abade & Albuquerque 2024 (QJRMS)](https://doi.org/10.1002/qj.4775) \"_Persistent mixed‐phase states in adiabatic cloud parcels under idealised conditions_\"\n",
+ "\n",
+ "compared to the paper, the analysis below differs by:\n",
+ "- including only the \"Homogeneous\" and \"Bulk\" methods (no \"stochastic\" yet)\n",
+ "- extending the analysis to cover both singular (INAS, as used in the paper) as well as time-dependent (ABIFM) immersion freezing models\n",
+ "- extending the analysis to depict multiple realisations + mean\n",
+ "- extending the analysis to illustrate the dependence of realisation spread on the number of super droplets used\n",
+ "- extending the analysis to depict how the results differ depending on the vertical velocity (cooling rate)\n",
+ "\n",
+ "TODO #1656:\n",
+ "- extend to show how the monodisperse vs. polydisperse INP size spectrum assumption changes the results\n",
+ "- extend to cover the stochastic model"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "aa876f2db21bb522",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2024-12-06T10:37:52.672366Z",
+ "start_time": "2024-12-06T10:37:52.668120Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "import os, sys\n",
+ "os.environ['NUMBA_THREADING_LAYER'] = 'workqueue' # PySDM & PyMPDATA don't work with TBB; OpenMP has extra dependencies on macOS\n",
+ "if 'google.colab' in sys.modules:\n",
+ " !pip --quiet install open-atmos-jupyter-utils\n",
+ " from open_atmos_jupyter_utils import pip_install_on_colab\n",
+ " pip_install_on_colab('PySDM-examples', 'PySDM')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "id": "9176c292-0d71-4608-ac4b-030e33de42e5",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import numpy as np\n",
+ "from matplotlib import pyplot\n",
+ "from scipy.interpolate import interp1d\n",
+ "from open_atmos_jupyter_utils import show_plot\n",
+ "from PySDM import Formulae\n",
+ "from PySDM.physics import si, in_unit\n",
+ "from PySDM.backends import CPU\n",
+ "from PySDM_examples.utils.widgets import display, FloatProgress\n",
+ "from PySDM_examples.Arabas_et_al_2025.commons import FREEZING_CONSTANTS\n",
+ "from PySDM_examples.Abade_and_Albuquerque_2024 import Simulation, Settings"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "c51620c1-e00a-4acc-ad55-1b6d477e90ac",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "backend = CPU(\n",
+ " formulae = Formulae(\n",
+ " constants={\n",
+ " \"bulk_phase_partitioning_exponent\": 0.1,\n",
+ " **FREEZING_CONSTANTS[\"dust\"],\n",
+ " },\n",
+ " bulk_phase_partitioning=\"KaulEtAl2015\",\n",
+ " particle_shape_and_density=\"MixedPhaseSpheres\",\n",
+ " diffusion_coordinate=\"WaterMassLogarithm\",\n",
+ " freezing_temperature_spectrum=\"Niemand_et_al_2012\",\n",
+ " heterogeneous_ice_nucleation_rate=\"ABIFM\",\n",
+ " ),\n",
+ " override_jit_flags={\"parallel\": False}\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "id": "478730ad-0c93-4adf-82a1-c606fde3c0b9",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "711644582d8e41cb9de11b68db665218",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "FloatProgress(value=1.0, max=39.0)"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "CI = 'CI' in os.environ\n",
+ "n_realisations = 3 if not CI else 1\n",
+ "n_sds = (64, 512) if not CI else (64,) # PAPER: \"on the order of 1e6\"\n",
+ "updrafts = (3.6, 1.2, .4) if not CI else (3.6,.4) # PAPER: 0.5 m/s\n",
+ "\n",
+ "dz_out = 100 * si.s\n",
+ "timestep = 1 * si.s\n",
+ "z_max = 3 * si.km\n",
+ "display(progbar := FloatProgress(value=1, max=(1 + 2 * n_realisations * len(n_sds)) * len(updrafts)))\n",
+ "\n",
+ "\n",
+ "settings_commons_part = {\n",
+ " 'enable_immersion_freezing': True,\n",
+ " 'enable_vapour_deposition_on_ice': True,\n",
+ "}\n",
+ "\n",
+ "datasets = {}\n",
+ "for updraft in updrafts:\n",
+ " t_max = z_max / updraft\n",
+ " settings_commons = {\n",
+ " 'updraft': updraft,\n",
+ " 'timestep': timestep,\n",
+ " 'backend': backend,\n",
+ " 'inp_frac': .5, # PAPER: .1\n",
+ " }\n",
+ " run_args = {\n",
+ " 'nt': int(t_max / timestep),\n",
+ " 'steps_per_output_interval': int(dz_out / updraft / timestep),\n",
+ " }\n",
+ " progbar.description = f'Bulk-{updraft}'\n",
+ " datasets[f'Bulk-{updraft}'] = {'realisations': [Simulation(Settings(\n",
+ " **settings_commons,\n",
+ " n_sd=1,\n",
+ " enable_immersion_freezing=False,\n",
+ " enable_vapour_deposition_on_ice=False,\n",
+ " )).run(**run_args)]}\n",
+ " progbar.value += 1\n",
+ " for singular, label in {True: 'INAS', False: 'ABIFM'}.items():\n",
+ " for n_sd in n_sds:\n",
+ " datasets[(key := f'Homogeneous-{label}-{n_sd}-{updraft}')] = {'realisations': []}\n",
+ " backend.formulae.seed = 0\n",
+ " for i in range(n_realisations):\n",
+ " progbar.description = '...' + key[-3:] + f'-{i}-{updraft}'\n",
+ " datasets[key]['realisations'].append(\n",
+ " Simulation(Settings(\n",
+ " **settings_commons,\n",
+ " **settings_commons_part,\n",
+ " n_sd=n_sd,\n",
+ " singular=singular\n",
+ " )).run(**run_args)\n",
+ " )\n",
+ " backend.formulae.seed += 1\n",
+ " progbar.value += 1"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "id": "b7abadbe-aa26-433d-9a9a-1984e3360db5",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "colors = {\n",
+ " 'ice+water': 'black',\n",
+ " 'ice': 'cyan',\n",
+ " 'water': 'blue',\n",
+ " 'vapour': 'gray',\n",
+ " 'total': 'orange',\n",
+ "}\n",
+ "\n",
+ "def plot_setup(ax):\n",
+ " ax.set_xlabel(r\"r (g$\\cdot$kg$^{-1}$)\")\n",
+ " ax.set_ylabel('Height (km)')\n",
+ " ax.set_ylim(.75, 3)\n",
+ " ax.set_xlim(-.05, 1.75)\n",
+ " ax.grid()\n",
+ " \n",
+ "def plot_part(ax, data): \n",
+ " for realisation in data['realisations']:\n",
+ " realisation['ice+water'] = np.asarray(realisation['water']) + np.asarray(realisation['ice'])\n",
+ " realisation['vapour'] = np.asarray(realisation['vapour'])\n",
+ " realisation['total'] = np.asarray(realisation['ice+water']) + np.asarray(realisation['vapour'])\n",
+ "\n",
+ " data['mean'] = {}\n",
+ " for name in ('ice', 'water', 'ice+water', 'height', 'vapour', 'total'):\n",
+ " data['mean'][name] = [\n",
+ " np.mean([realisation[name][level] for realisation in data['realisations']]) \n",
+ " for level in range(len(data['realisations'][0][name]))\n",
+ " ]\n",
+ " \n",
+ " for name in ('ice', 'water', 'ice+water', 'vapour', 'total'):\n",
+ " for realisation in data['realisations']:\n",
+ " ax.plot(\n",
+ " in_unit(np.asarray(realisation[name]), si.g / si.kg),\n",
+ " in_unit(np.asarray(realisation['height']), si.km),\n",
+ " linestyle='--' if name == 'ice+water' else '-',\n",
+ " color=colors[name],\n",
+ " linewidth=.75,\n",
+ " )\n",
+ " mean = data['mean']\n",
+ " ax.plot(\n",
+ " in_unit(np.asarray(mean[name]), si.g / si.kg),\n",
+ " in_unit(np.asarray(mean['height']), si.km),\n",
+ " label=name,\n",
+ " marker='.',\n",
+ " color=colors[name],\n",
+ " )\n",
+ "\n",
+ "def plot_bulk(ax, data):\n",
+ " liquid_fraction = backend.formulae.bulk_phase_partitioning.liquid_fraction(np.asarray(data['T']))\n",
+ " total_condensed_mixing_ratio = np.asarray(data['water'])\n",
+ " vapour_mixing_ratio = np.asarray(data['vapour'])\n",
+ " for name in ('ice', 'water', 'ice+water', 'vapour', 'total'):\n",
+ " values = {\n",
+ " 'ice+water': total_condensed_mixing_ratio,\n",
+ " 'ice': (1 - liquid_fraction) * total_condensed_mixing_ratio,\n",
+ " 'water': liquid_fraction * total_condensed_mixing_ratio,\n",
+ " 'vapour': vapour_mixing_ratio,\n",
+ " 'total': vapour_mixing_ratio + total_condensed_mixing_ratio,\n",
+ " }[name]\n",
+ " ax.plot(\n",
+ " in_unit(values, si.g / si.kg),\n",
+ " in_unit(np.asarray(data['height']), si.km),\n",
+ " label=name,\n",
+ " marker='.',\n",
+ " color=colors[name],\n",
+ " linestyle='--' if name == 'ice+water' else '-'\n",
+ " )\n",
+ " interp_temp = interp1d(\n",
+ " in_unit(np.asarray(data['height']), si.km),\n",
+ " backend.formulae.trivia.K2C(np.asarray(data['T'])),\n",
+ " bounds_error=True\n",
+ " ) \n",
+ " ax2 = ax.twinx()\n",
+ " ax2.set_yticks(ax.get_yticks(), [f\"{t:.1f}\" for t in interp_temp(ax.get_yticks())])\n",
+ " ax2.set_ylim(ax.get_ylim()) \n",
+ " ax2.set_ylabel('Temperature [°C]')\n",
+ " ax.legend(\n",
+ " loc='lower center',\n",
+ " bbox_to_anchor=(0.5, -.65),\n",
+ " fancybox=True,\n",
+ " shadow=True,\n",
+ " ncol=1\n",
+ " )"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "id": "721e5606-9e19-44b9-87f3-cba86b54574e",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/svg+xml": [
+ "\n",
+ "\n",
+ "