From 1882abce261d22de1f3192e4dc9f4f841121c490 Mon Sep 17 00:00:00 2001 From: Nick Murphy Date: Thu, 25 Jul 2024 21:33:35 -0400 Subject: [PATCH] Copy particles-formulary notebook --- notebooks/particles-formulary.ipynb | 2376 +++++++++++++++++++++++++++ 1 file changed, 2376 insertions(+) create mode 100644 notebooks/particles-formulary.ipynb diff --git a/notebooks/particles-formulary.ipynb b/notebooks/particles-formulary.ipynb new file mode 100644 index 0000000..a5c6e68 --- /dev/null +++ b/notebooks/particles-formulary.ipynb @@ -0,0 +1,2376 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "5b8f9968", + "metadata": {}, + "source": [ + "# Particles and Formulary" + ] + }, + { + "cell_type": "markdown", + "id": "c8c364e5", + "metadata": {}, + "source": [ + "[`astropy.units`]: https://docs.astropy.org/en/stable/units/index.html\n", + "[`plasmapy.particles`]: https://docs.plasmapy.org/en/stable/particles/index.html\n", + "[`plasmapy.formulary`]: https://docs.plasmapy.org/en/stable/formulary/index.html\n", + "\n", + "Thank you for coming to this interactive tutorial. 🌱 We are excited for you to be here! 😺\n", + "\n", + "In this notebook, we will be going through [`plasmapy.particles`] ⚛️ and [`plasmapy.formulary`] 🧮, before talking about how to create a formulary function.\n", + "\n", + "Let's start with some preliminary imports & settings. To execute a cell in a Jupyter notebook, press `Shift + Enter`. If you have to restart the notebook, please re-execute the following cell.\n", + "\n", + "If using Google Colab, click **Run anyway** when prompted. (If prompted again, select **Restart runtime** when the installation finishes.)" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "fa326226", + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if 'google.colab' in str(get_ipython()):\n", + " if 'plasmapy' not in sys.modules:\n", + " !pip install plasmapy==2024.7.0\n", + "\n", + "import astropy.units as u\n", + "from plasmapy.particles import *\n", + "from plasmapy.formulary import *\n", + "from plasmapy.utils.decorators import validate_quantities" + ] + }, + { + "cell_type": "markdown", + "id": "6e8f0d92", + "metadata": {}, + "source": [ + "## Quick refresher on `astropy.units`" + ] + }, + { + "cell_type": "markdown", + "id": "93658d4f", + "metadata": {}, + "source": [ + "[`astropy.units`]: https://docs.astropy.org/en/stable/units/index.html\n", + "\n", + "PlasmaPy makes heavy use of [`astropy.units`], which is my favorite part of the scientific pythoniverse! We typically import this subpackage as `u`." + ] + }, + { + "cell_type": "markdown", + "id": "e9ccbb06", + "metadata": {}, + "source": [ + "We can create a physical quantity by multiplying or dividing a number or array with a unit." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "8437650a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/latex": [ + "$60 \\; \\mathrm{km}$" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "60 * u.km" + ] + }, + { + "cell_type": "markdown", + "id": "ccc6659f", + "metadata": {}, + "source": [ + "[`Quantity`]: https://docs.astropy.org/en/stable/api/astropy.units.Quantity.html#astropy.units.Quantity\n", + "\n", + "This operation creates a [`Quantity`] object: a number, sequence, or array that has been assigned a physical unit. We can create [`Quantity`] objects with compound units." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4932159b", + "metadata": {}, + "outputs": [], + "source": [ + "V = 88 * u.imperial.mile / u.hour\n", + "print(V)" + ] + }, + { + "cell_type": "markdown", + "id": "f9dd4743-ffab-40d7-9407-62840ea1c7ba", + "metadata": {}, + "source": [ + "We can use `.to()` to convert a `Quantity` to different units." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f8b5a277-de41-4039-8442-2a392d1a553c", + "metadata": {}, + "outputs": [], + "source": [ + "V.to(\"m/s\")" + ] + }, + { + "cell_type": "markdown", + "id": "97a8d49a", + "metadata": {}, + "source": [ + "## Particles" + ] + }, + { + "cell_type": "markdown", + "id": "0c7eeced", + "metadata": {}, + "source": [ + "[`plasmapy.particles`]: https://docs.plasmapy.org/en/stable/particles/index.html\n", + "\n", + "The [`plasmapy.particles`] subpackage contains functions to access basic particle data and classes to represent particles." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24125310", + "metadata": {}, + "outputs": [], + "source": [ + "from plasmapy.particles import *" + ] + }, + { + "cell_type": "markdown", + "id": "e224284a", + "metadata": {}, + "source": [ + "### Particle properties" + ] + }, + { + "cell_type": "markdown", + "id": "7696deba", + "metadata": {}, + "source": [ + "[representation of a particle]: https://docs.plasmapy.org/en/stable/api/plasmapy.particles.ParticleLike.html#particlelike\n", + "\n", + "There are several functions that provide information about different particles that might be present in a plasma. The input of these functions is a [representation of a particle], such as a string for the atomic symbol or the element name." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "006b8d3e", + "metadata": {}, + "outputs": [], + "source": [ + "atomic_number(\"Fe\")" + ] + }, + { + "cell_type": "markdown", + "id": "2aa5af34", + "metadata": {}, + "source": [ + "[atomic number]: https://en.wikipedia.org/wiki/Atomic_number\n", + "\n", + "We can provide a number that represents the [atomic number]." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "69958326", + "metadata": {}, + "outputs": [], + "source": [ + "element_name(26)" + ] + }, + { + "cell_type": "markdown", + "id": "cca89cef", + "metadata": {}, + "source": [ + "We can provide standard symbols or the names of particles." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16ca5b6f", + "metadata": {}, + "outputs": [], + "source": [ + "is_stable(\"e-\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "35d8e49e", + "metadata": {}, + "outputs": [], + "source": [ + "charge_number(\"proton\")" + ] + }, + { + "cell_type": "markdown", + "id": "8f2e5b30", + "metadata": {}, + "source": [ + "[mass number]: https://en.wikipedia.org/wiki/Mass_number\n", + "[`Quantity`]: https://docs.astropy.org/en/stable/units/quantity.html#quantity\n", + "[`astropy.units`]: https://docs.astropy.org/en/stable/units/index.html\n", + "[`half_life`]: https://docs.plasmapy.org/en/stable/api/plasmapy.particles.atomic.half_life.html#half-life\n", + "\n", + "We can represent isotopes with the atomic symbol followed by a hyphen and the [mass number]. Let's use [`half_life`] to return the half-life of a radioactive particle in seconds as a [`Quantity`]." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "94fcf452", + "metadata": {}, + "outputs": [], + "source": [ + "half_life(\"C-14\")" + ] + }, + { + "cell_type": "markdown", + "id": "58ab7782", + "metadata": {}, + "source": [ + "We typically represent an ion in a string by putting together the atomic symbol or isotope symbol, a space, the charge number, and the sign of the charge." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a65e15da", + "metadata": {}, + "outputs": [], + "source": [ + "charge_number(\"Fe-56 13+\")" + ] + }, + { + "cell_type": "markdown", + "id": "ce4886cf", + "metadata": {}, + "source": [ + "[particle-like]: https://docs.plasmapy.org/en/latest/glossary.html#term-particle-like\n", + "[`plasmapy.particles`]: https://docs.plasmapy.org/en/stable/particles/index.html\n", + "\n", + "Functions in [`plasmapy.particles`] are quite flexible in terms of string inputs representing particles. An input is [particle-like] if it can be used to represent a physical particle. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "05e03a8b", + "metadata": {}, + "outputs": [], + "source": [ + "particle_mass(\"iron-56 +13\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0dd9541b", + "metadata": {}, + "outputs": [], + "source": [ + "particle_mass(\"iron-56+++++++++++++\")" + ] + }, + { + "cell_type": "markdown", + "id": "d3adf3a6", + "metadata": {}, + "source": [ + "Most of these functions take additional arguments, with `Z` representing the charge number of an ion and `mass_numb` representing the mass number of an isotope. These arguments are often [keyword-only](https://docs.plasmapy.org/en/latest/glossary.html#term-keyword-only) to avoid ambiguity." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "61e0f960", + "metadata": {}, + "outputs": [], + "source": [ + "particle_mass(\"Fe\", Z=13, mass_numb=56)" + ] + }, + { + "cell_type": "markdown", + "id": "53a1d993", + "metadata": {}, + "source": [ + "### Particle objects" + ] + }, + { + "cell_type": "markdown", + "id": "3c2c3835", + "metadata": {}, + "source": [ + "[`Particle`]: https://docs.plasmapy.org/en/stable/api/plasmapy.particles.particle_class.Particle.html#plasmapy.particles.particle_class.Particle\n", + "\n", + "Up until now, we have been using functions that accept representations of particles and then return particle properties. With the [`Particle`] class, we can create objects that represent physical particles." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fe9f824a", + "metadata": {}, + "outputs": [], + "source": [ + "proton = Particle(\"p+\")\n", + "electron = Particle(\"electron\")\n", + "iron56_nucleus = Particle(\"Fe\", Z=26, mass_numb=56)" + ] + }, + { + "cell_type": "markdown", + "id": "160df55b", + "metadata": {}, + "source": [ + "[`Particle`]: https://docs.plasmapy.org/en/stable/api/plasmapy.particles.particle_class.Particle.html#plasmapy.particles.particle_class.Particle\n", + "\n", + "Particle properties can be accessed via attributes of the [`Particle`] class." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bebcc477", + "metadata": {}, + "outputs": [], + "source": [ + "proton.mass" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "df0f2eed", + "metadata": {}, + "outputs": [], + "source": [ + "electron.charge" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "116cd6ad", + "metadata": {}, + "outputs": [], + "source": [ + "electron.charge_number" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "99d22bd3", + "metadata": {}, + "outputs": [], + "source": [ + "iron56_nucleus.binding_energy" + ] + }, + { + "cell_type": "markdown", + "id": "8986cef8", + "metadata": {}, + "source": [ + "#### Antiparticles\n", + "\n", + "[`antiparticle`]: https://docs.plasmapy.org/en/stable/api/plasmapy.particles.particle_class.Particle.html#plasmapy.particles.particle_class.Particle.antiparticle\n", + "[`Particle`]: https://docs.plasmapy.org/en/stable/api/plasmapy.particles.particle_class.Particle.html#plasmapy.particles.particle_class.Particle\n", + "\n", + "We can get antiparticles of fundamental particles by using the [`antiparticle`] attribute of a [`Particle`]." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f6663952", + "metadata": {}, + "outputs": [], + "source": [ + "electron.antiparticle" + ] + }, + { + "cell_type": "markdown", + "id": "bf40e592", + "metadata": {}, + "source": [ + "[`Particle`]: https://docs.plasmapy.org/en/stable/api/plasmapy.particles.particle_class.Particle.html#plasmapy.particles.particle_class.Particle\n", + "\n", + "\n", + "We can also use the tilde (`~`) operator on a [`Particle`] to get its antiparticle." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "180a1edf", + "metadata": {}, + "outputs": [], + "source": [ + "~proton" + ] + }, + { + "cell_type": "markdown", + "id": "8500bc48", + "metadata": {}, + "source": [ + "#### Ionization and recombination\n", + "\n", + "[`Particle`]: https://docs.plasmapy.org/en/stable/api/plasmapy.particles.particle_class.Particle.html#plasmapy.particles.particle_class.Particle\n", + "[`recombine()`]: https://docs.plasmapy.org/en/stable/api/plasmapy.particles.particle_class.Particle.html#plasmapy.particles.particle_class.Particle.recombine\n", + "[`ionize()`]: https://docs.plasmapy.org/en/stable/api/plasmapy.particles.particle_class.Particle.html#plasmapy.particles.particle_class.Particle.ionize\n", + "\n", + "The [`recombine()`] and [`ionize()`] methods of a [`Particle`] representing an ion or neutral atom will return a different [`Particle`] with fewer or more electrons." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "186b1dfa", + "metadata": {}, + "outputs": [], + "source": [ + "deuterium = Particle(\"D 0+\")\n", + "deuterium.ionize()" + ] + }, + { + "cell_type": "markdown", + "id": "4ee95241", + "metadata": {}, + "source": [ + "When provided with a number, these methods tell how many bound electrons to add or remove." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "242b53bd", + "metadata": {}, + "outputs": [], + "source": [ + "alpha = Particle(\"alpha\")\n", + "alpha.recombine(2)" + ] + }, + { + "cell_type": "markdown", + "id": "e5302bef", + "metadata": {}, + "source": [ + "### Custom particles" + ] + }, + { + "cell_type": "markdown", + "id": "4c8c2a63", + "metadata": {}, + "source": [ + "[`CustomParticle`]: https://docs.plasmapy.org/en/stable/api/plasmapy.particles.particle_class.CustomParticle.html\n", + "\n", + "Sometimes we want to use a particle with custom properties. For example, we might want to represent an average ion in a multi-species plasma or a dust particle. For that we can use [`CustomParticle`]." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "35b94505", + "metadata": {}, + "outputs": [], + "source": [ + "cp = CustomParticle(9e-26 * u.kg, 2.18e-18 * u.C, symbol=\"Fe 13.6+\")" + ] + }, + { + "cell_type": "markdown", + "id": "dfc19404", + "metadata": {}, + "source": [ + "[`CustomParticle`]: https://docs.plasmapy.org/en/stable/api/plasmapy.particles.particle_class.CustomParticle.html\n", + "[`Particle`]: https://docs.plasmapy.org/en/stable/api/plasmapy.particles.particle_class.Particle.html\n", + "\n", + "Many of the attributes of [`CustomParticle`] are the same as in [`Particle`]." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8a4b412b", + "metadata": {}, + "outputs": [], + "source": [ + "cp.mass" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a2da2b3b", + "metadata": {}, + "outputs": [], + "source": [ + "cp.charge" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ecb450f6", + "metadata": {}, + "outputs": [], + "source": [ + "cp.symbol" + ] + }, + { + "cell_type": "markdown", + "id": "21a992a9", + "metadata": {}, + "source": [ + "[`numpy.nan`]: https://numpy.org/doc/stable/reference/constants.html#numpy.nan\n", + "\n", + "If we do not include one of the physical quantities, it gets set to [`numpy.nan`] (not a number) in the appropriate units." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a31c421a", + "metadata": {}, + "outputs": [], + "source": [ + "CustomParticle(9.27e-26 * u.kg).charge" + ] + }, + { + "cell_type": "markdown", + "id": "6292a1f3", + "metadata": {}, + "source": [ + "[`CustomParticle`]: https://docs.plasmapy.org/en/stable/api/plasmapy.particles.particle_class.CustomParticle.html\n", + "[`plasmapy.formulary`]: https://docs.plasmapy.org/en/stable/formulary/index.html\n", + "\n", + "[`CustomParticle`] objects can provided to most of the commonly used functions in [`plasmapy.formulary`], and we're planning to improve interoperability in future releases of PlasmaPy." + ] + }, + { + "cell_type": "markdown", + "id": "0f2b0634", + "metadata": {}, + "source": [ + "### Particle lists" + ] + }, + { + "cell_type": "markdown", + "id": "7939cfad", + "metadata": {}, + "source": [ + "[`ParticleList`]: https://docs.plasmapy.org/en/stable/api/plasmapy.particles.particle_collections.ParticleList.html\n", + "[`CustomParticle`]: https://docs.plasmapy.org/en/stable/api/plasmapy.particles.particle_class.CustomParticle.html\n", + "[`Particle`]: https://docs.plasmapy.org/en/stable/api/plasmapy.particles.particle_class.Particle.html\n", + "\n", + "The [`ParticleList`] class is a container for [`Particle`] and [`CustomParticle`] objects." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "30cc7d36", + "metadata": {}, + "outputs": [], + "source": [ + "iron_ions = ParticleList([\"Fe 12+\", \"Fe 13+\", \"Fe 14+\"])" + ] + }, + { + "cell_type": "markdown", + "id": "d56ddb98", + "metadata": {}, + "source": [ + "[`ParticleList`]: https://docs.plasmapy.org/en/stable/api/plasmapy.particles.particle_collections.ParticleList.html\n", + "\n", + "By using a [`ParticleList`], we can access the properties of multiple particles at once." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2434f71b", + "metadata": {}, + "outputs": [], + "source": [ + "iron_ions.mass" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "14a431dd", + "metadata": {}, + "outputs": [], + "source": [ + "iron_ions.charge" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "48cd1c7a", + "metadata": {}, + "outputs": [], + "source": [ + "iron_ions.symbols" + ] + }, + { + "cell_type": "markdown", + "id": "cccc6c60", + "metadata": {}, + "source": [ + "[`ParticleList`]: https://docs.plasmapy.org/en/stable/api/plasmapy.particles.particle_collections.ParticleList.html\n", + "[`CustomParticle`]: https://docs.plasmapy.org/en/stable/api/plasmapy.particles.particle_class.CustomParticle.html\n", + "[`Particle`]: https://docs.plasmapy.org/en/stable/api/plasmapy.particles.particle_class.Particle.html\n", + "\n", + "We can also create a [`ParticleList`] by adding [`Particle`] and/or [`CustomParticle`] objects together." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3b953065", + "metadata": {}, + "outputs": [], + "source": [ + "proton + electron" + ] + }, + { + "cell_type": "markdown", + "id": "b78ee7d2", + "metadata": {}, + "source": [ + "We can also get an average particle." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bf01f619", + "metadata": {}, + "outputs": [], + "source": [ + "iron_ions.average_particle()" + ] + }, + { + "cell_type": "markdown", + "id": "e9038618", + "metadata": {}, + "source": [ + "[`ParticleList`]: https://docs.plasmapy.org/en/stable/api/plasmapy.particles.particle_collections.ParticleList.html\n", + "[`plasmapy.formulary`]: https://docs.plasmapy.org/en/stable/formulary/index.html\n", + "\n", + "[`ParticleList`] objects can also be provided to the most commonly used functions in [`plasmapy.formulary`], with more complete interoperability expected in the future." + ] + }, + { + "cell_type": "markdown", + "id": "8167cf2d-a13e-4847-ac6b-ca169c1ab96d", + "metadata": {}, + "source": [ + "### Particle Categorization" + ] + }, + { + "cell_type": "markdown", + "id": "1fa46bf5-200d-4297-aa13-fcdbd9ca286f", + "metadata": {}, + "source": [ + "[`categories`]: https://docs.plasmapy.org/en/stable/api/plasmapy.particles.particle_class.Particle.html#plasmapy.particles.particle_class.Particle.categories\n", + "[`Particle`]: https://docs.plasmapy.org/en/stable/api/plasmapy.particles.particle_class.Particle.html#particle\n", + "[`set`]: https://docs.python.org/3/library/stdtypes.html#set\n", + "\n", + "The [`categories`] attribute of a [`Particle`] provides a set of the categories that the particle belongs to as a [`set`]." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1833eff2-2fe1-4923-bb2f-94277504dff8", + "metadata": {}, + "outputs": [], + "source": [ + "muon = Particle(\"muon\")\n", + "muon.categories" + ] + }, + { + "cell_type": "markdown", + "id": "38d96c41-2e03-48fb-86ab-effae5c2bd87", + "metadata": {}, + "source": [ + "[`is_category()`]: https://docs.plasmapy.org/en/stable/api/plasmapy.particles.particle_class.Particle.html#plasmapy.particles.particle_class.Particle.is_category\n", + "[`Particle`]: https://docs.plasmapy.org/en/stable/api/plasmapy.particles.particle_class.Particle.html#particle\n", + "\n", + "The [`is_category()`] method of a [`Particle`] lets us determine if the particle belongs to one or more categories." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5d73f130-fa56-472a-a04c-23e364d8fdeb", + "metadata": {}, + "outputs": [], + "source": [ + "muon.is_category(\"lepton\")" + ] + }, + { + "cell_type": "markdown", + "id": "1f427bc9-83cf-4b88-916d-aa3d3d17dbb5", + "metadata": {}, + "source": [ + "[`Particle`]: https://docs.plasmapy.org/en/stable/api/plasmapy.particles.particle_class.Particle.html#particle\n", + "\n", + "If we need to be more specific with `is_category()`, we can use its keyword arguments: \n", + "\n", + " - `require`: for categories that a [`Particle`] _must_ belong to\n", + " - `exclude`: for categories that the [`Particle`] _cannot_ belong to\n", + " - `any_of`: for categories of which a [`Particle`] must belong to _at least one of_" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6c06149e-7ac3-4531-b5ab-7aa4eab21f67", + "metadata": {}, + "outputs": [], + "source": [ + "electron.is_category(require=\"lepton\", exclude=\"baryon\", any_of={\"boson\", \"fermion\"})" + ] + }, + { + "cell_type": "markdown", + "id": "77874c36-3b01-43f2-86f0-a4d28978fe12", + "metadata": {}, + "source": [ + "[`valid_categories`]: https://docs.plasmapy.org/en/latest/api/plasmapy.particles.particle_class.valid_categories.html\n", + "\n", + "All valid particle categories are included in [`valid_categories`]." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e0818ec0-2593-47ca-b58a-28b40a774f25", + "metadata": {}, + "outputs": [], + "source": [ + "print(valid_categories)" + ] + }, + { + "cell_type": "markdown", + "id": "aceafe85-aa39-43fd-89aa-637f017d2574", + "metadata": {}, + "source": [ + "[`is_category()`]: ../../api/plasmapy.particles.particle_class.Particle.rst#plasmapy.particles.particle_class.Particle.is_category\n", + "[`ParticleList`]: https://docs.plasmapy.org/en/stable/api/plasmapy.particles.particle_collections.ParticleList.html#plasmapy.particles.particle_collections.ParticleList\n", + "[`list`]: https://docs.python.org/3/library/stdtypes.html#list\n", + "\n", + "The [`is_category()`] method of [`ParticleList`] returns `True` if all particles match the criteria, and `False` otherwise." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "eafd9671-41d3-4062-9ec8-75e822ef950c", + "metadata": {}, + "outputs": [], + "source": [ + "particles = ParticleList([\"e-\", \"p+\", \"n\"])\n", + "particles.is_category(require=\"lepton\")" + ] + }, + { + "cell_type": "markdown", + "id": "368173b7-6af5-4be0-8f3f-0c683345e9d9", + "metadata": {}, + "source": [ + "If we want to do this operation for all of the particles, we can set the `particlewise` argument to `True`. We will then get a `list` of booleans." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ea9a8d0a-ad09-4558-8b1f-383fbdce47f4", + "metadata": {}, + "outputs": [], + "source": [ + "particles.is_category(require=\"lepton\", particlewise=True)" + ] + }, + { + "cell_type": "markdown", + "id": "efe199c8", + "metadata": {}, + "source": [ + "### Nuclear reactions" + ] + }, + { + "cell_type": "markdown", + "id": "58fb006b", + "metadata": {}, + "source": [ + "[`plasmapy.particles`]: https://docs.plasmapy.org/en/stable/particles/index.html\n", + "\n", + "We can use [`plasmapy.particles`] to calculate the energy of a nuclear reaction using the `>` operator. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d8a9ab9e", + "metadata": {}, + "outputs": [], + "source": [ + "deuteron = Particle(\"D+\")\n", + "triton = Particle(\"T+\")\n", + "alpha = Particle(\"α\")\n", + "neutron = Particle(\"n\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cb95c644", + "metadata": {}, + "outputs": [], + "source": [ + "energy = deuteron + triton > alpha + neutron" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e7a4c236", + "metadata": {}, + "outputs": [], + "source": [ + "energy.to(\"MeV\")" + ] + }, + { + "cell_type": "markdown", + "id": "8bf7e515", + "metadata": {}, + "source": [ + "If the nuclear reaction is invalid, then an exception is raised that states the reason why." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "67b32a3a", + "metadata": { + "tags": [ + "raises-exception" + ] + }, + "outputs": [], + "source": [ + "deuteron + triton > alpha + 56 * neutron" + ] + }, + { + "cell_type": "markdown", + "id": "f065a288", + "metadata": {}, + "source": [ + "## PlasmaPy formulary\n", + "\n", + "[`plasmapy.formulary`]: https://docs.plasmapy.org/en/stable/formulary/index.html\n", + "\n", + "The [`plasmapy.formulary`] subpackage contains a broad variety of formulas needed by plasma scientists across disciplines, in particular to calculate plasma parameters." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20c3b9b1", + "metadata": {}, + "outputs": [], + "source": [ + "from plasmapy.formulary import *" + ] + }, + { + "cell_type": "markdown", + "id": "955ce3dd", + "metadata": {}, + "source": [ + "### Plasma beta in the solar corona" + ] + }, + { + "cell_type": "markdown", + "id": "1bb15c25", + "metadata": {}, + "source": [ + "[Plasma beta]: https://en.wikipedia.org/wiki/Beta_(plasma_physics)\n", + "\n", + "[Plasma beta] ($β$) is one of the most fundamental plasma parameters. $β$ is the ratio of the plasma (gas) pressure to the magnetic pressure. How a plasma behaves depends strongly on $β$. When $β ≫ 1$, the magnetic field is not strong enough to exert much of a force on the plasma, so its motions start to resemble a gas. When $β ≪ 1$, magnetic tension and pressure are the dominant macroscopic forces. \n", + "\n", + "Let's use [`plasmapy.formulary`](https://docs.plasmapy.org/en/stable/formulary/index.html) to calculate plasma β in different regions of the solar atmosphere and see what we can learn." + ] + }, + { + "cell_type": "markdown", + "id": "24162ca9", + "metadata": {}, + "source": [ + "#### Solar corona" + ] + }, + { + "cell_type": "markdown", + "id": "60aa77b5", + "metadata": {}, + "source": [ + "Let's start by defining some plasma parameters for an active region in the solar corona." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c4b98f58", + "metadata": {}, + "outputs": [], + "source": [ + "B_corona = 50 * u.G\n", + "n_corona = 1e9 * u.cm ** -3\n", + "T_corona = 1 * u.MK" + ] + }, + { + "cell_type": "markdown", + "id": "cdc0c334", + "metadata": {}, + "source": [ + "When we use these parameters in [`beta`](https://docs.plasmapy.org/en/stable/api/plasmapy.formulary.dimensionless.beta.html#plasmapy.formulary.dimensionless.beta), we find that $β$ is quite small so that the corona is magnetically dominated." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "98b23229", + "metadata": {}, + "outputs": [], + "source": [ + "beta(T=T_corona, n=n_corona, B=B_corona)" + ] + }, + { + "cell_type": "markdown", + "id": "deb41bfa-bad5-4bdc-a0f2-75b4c0a16054", + "metadata": {}, + "source": [ + "#### Solar photosphere" + ] + }, + { + "cell_type": "markdown", + "id": "2442b569-a98d-4528-ab1a-374867be06f3", + "metadata": {}, + "source": [ + "Let's specify some characteristic plasma parameters for the solar photosphere, away from any sunspots." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1654acbb-056b-46e5-9fcf-900ab0fbd2b7", + "metadata": {}, + "outputs": [], + "source": [ + "T_photosphere = 5800 * u.K\n", + "B_photosphere = 400 * u.G\n", + "n_photosphere = 1e17 * u.cm ** -3" + ] + }, + { + "cell_type": "markdown", + "id": "be434e24-080a-449e-a3f2-6f18149ff362", + "metadata": {}, + "source": [ + "When we calculate for the photosphere, we find that it is an order of magnitude larger than 1, so plasma pressure forces are more important than magnetic tension and pressure." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7cf5b298-f68b-4bcb-bbf4-2fb93d8cdc6a", + "metadata": {}, + "outputs": [], + "source": [ + "beta(T_photosphere, n_photosphere, B_photosphere)" + ] + }, + { + "cell_type": "markdown", + "id": "dee08605", + "metadata": {}, + "source": [ + "### Plasma parameters in Earth's magnetosphere" + ] + }, + { + "cell_type": "markdown", + "id": "9587734f", + "metadata": {}, + "source": [ + "[magnetic reconnection]: https://en.wikipedia.org/wiki/Magnetic_reconnection\n", + "[`plasmapy.formulary`]: https://docs.plasmapy.org/en/stable/formulary/index.html\n", + "\n", + "The [*Magnetospheric Multiscale Mission*](https://www.nasa.gov/mission_pages/mms/overview/index.html) (*MMS*) is a constellation of four identical spacecraft. The goal of *MMS* is to investigate the small-scale physics of [magnetic reconnection] in Earth's magnetosphere. In order to do this, the spacecraft need to orbit in a tight configuration. But how tight does the tetrahedron have to be? Let's use [`plasmapy.formulary`] to find out." + ] + }, + { + "cell_type": "markdown", + "id": "706818eb", + "metadata": {}, + "source": [ + "#### Physics background" + ] + }, + { + "cell_type": "markdown", + "id": "e7f1ddf9", + "metadata": {}, + "source": [ + "[Magnetic reconnection]: https://en.wikipedia.org/wiki/Magnetic_reconnection\n", + "\n", + "[Magnetic reconnection] is the fundamental plasma process that converts stored magnetic energy into kinetic energy, thermal energy, and particle acceleration. Reconnection powers solar flares and is a key component of geomagnetic storms in Earth's magnetosphere. Reconnection can also degrade confinement in fusion devices such as tokamaks.\n", + "\n", + "The **inertial length** is the characteristic length scale for a particle to get accelerated or decelerated by electromagnetic forces in a plasma.\n", + "\n", + "When the reconnection layer thickness is shorter than the **ion inertial length**, $d_i ≡ c/ω_{pi}$, collisionless effects and the Hall effect enable reconnection to be fast (Zweibel & Yamada 2009). The inner electron diffusion region has a thickness of about the **electron inertial length**, $d_e ≡ c/ω_{pi}$. (Here, $ω_{pi}$ and $ω_{pe}$ are the ion and electron plasma frequencies.)\n", + "\n", + "**Our goal: calculate $d_i$ and $d_e$ to get an idea of how far the MMS spacecraft should be separated from each other to investigate reconnection.**" + ] + }, + { + "cell_type": "markdown", + "id": "749fba51", + "metadata": {}, + "source": [ + "### Length scales" + ] + }, + { + "cell_type": "markdown", + "id": "9db83228", + "metadata": {}, + "source": [ + "Let's choose some characteristic plasma parameters for the magnetosphere." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "1f317f40", + "metadata": {}, + "outputs": [], + "source": [ + "n = 1 * u.cm ** -3\n", + "B = 5 * u.nT\n", + "T = 30000 * u.K" + ] + }, + { + "cell_type": "markdown", + "id": "591bef20", + "metadata": {}, + "source": [ + "Let's calculate the ion inertial length, $d_i$. On length scales shorter than $d_i$, the Hall effect becomes important as the ions and electrons decouple from each other." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a717c75f", + "metadata": {}, + "outputs": [], + "source": [ + "inertial_length(n=n, particle=\"p+\").to(\"km\")" + ] + }, + { + "cell_type": "markdown", + "id": "5c56977f", + "metadata": {}, + "source": [ + "The ion diffusion regions should therefore be a few hundred kilometers thick. Let's calculate the electron inertial length next." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4e5a4dd9", + "metadata": {}, + "outputs": [], + "source": [ + "inertial_length(n=n, particle=\"e-\").to(\"km\")" + ] + }, + { + "cell_type": "markdown", + "id": "bb4b8d8d", + "metadata": {}, + "source": [ + "The electron diffusion region should therefore have a characteristic length scale of a few kilometers, which is significantly smaller than the ion diffusion region." + ] + }, + { + "cell_type": "markdown", + "id": "2b2875fc", + "metadata": {}, + "source": [ + "We can also calculate the gyroradii for different particles " + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "96fee1ef", + "metadata": {}, + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'B' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[15], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m gyroradius(B\u001b[38;5;241m=\u001b[39m\u001b[43mB\u001b[49m, particle\u001b[38;5;241m=\u001b[39m[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mp+\u001b[39m\u001b[38;5;124m\"\u001b[39m, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124me-\u001b[39m\u001b[38;5;124m\"\u001b[39m], T\u001b[38;5;241m=\u001b[39mT)\u001b[38;5;241m.\u001b[39mto(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mkm\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n", + "\u001b[0;31mNameError\u001b[0m: name 'B' is not defined" + ] + } + ], + "source": [ + "gyroradius(B=B, particle=[\"p+\", \"e-\"], T=T).to(\"km\")" + ] + }, + { + "cell_type": "markdown", + "id": "1b2b32b3", + "metadata": {}, + "source": [ + "The four *MMS* spacecraft have separations of ten to hundreds of kilometers, and thus are well-positioned to investigate Hall physics during reconnection in the magnetosphere." + ] + }, + { + "cell_type": "markdown", + "id": "38e1aff3", + "metadata": {}, + "source": [ + "#### Frequencies" + ] + }, + { + "cell_type": "markdown", + "id": "fc1a183f", + "metadata": {}, + "source": [ + "We can calculate some of the fundamental frequencies associated with magnetospheric plasma. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4b9f9403", + "metadata": {}, + "outputs": [], + "source": [ + "plasma_frequency(n=n, particle=[\"p+\", \"e-\"])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5cc9c0ca", + "metadata": {}, + "outputs": [], + "source": [ + "gyrofrequency(B=B, particle=[\"p+\", \"e-\"])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "88534868", + "metadata": {}, + "outputs": [], + "source": [ + "lower_hybrid_frequency(B=B, n_i=n, ion=\"p+\")" + ] + }, + { + "cell_type": "markdown", + "id": "8242486e-dcb4-4815-aa6f-04e6d6406546", + "metadata": {}, + "source": [ + "[`lower_hybrid_frequency`]: https://docs.plasmapy.org/en/stable/api/plasmapy.formulary.frequencies.lower_hybrid_frequency.html#lower-hybrid-frequency\n", + "[`plasmapy.formulary`]: https://docs.plasmapy.org/en/stable/formulary/index.html\n", + "\n", + "Most of the functions in [`plasmapy.formulary`] have descriptions of the plasma parameters that include the formula and physical interpretation. If we ever forget what the [`lower_hybrid_frequency`] represents, we can check out its documentation page! " + ] + }, + { + "cell_type": "markdown", + "id": "04bf2ba1-c580-4ca5-b410-530ad3b4e31a", + "metadata": {}, + "source": [ + "## Python interlude" + ] + }, + { + "cell_type": "markdown", + "id": "aac532e4-5249-458b-bf4b-aad05f36fc74", + "metadata": {}, + "source": [ + "[`plasmapy.formulary`]: https://docs.plasmapy.org/en/stable/formulary/index.html\n", + "\n", + "Now that we've gone through how to use [`plasmapy.particles`] and [`plasmapy.formulary`], we'll move on to the process for adding a function to [`plasmapy.formulary`]. Before that, we need to introduce two capabilities for the Python language:\n", + "\n", + " - Type hint annotations\n", + " - Decorators" + ] + }, + { + "cell_type": "markdown", + "id": "44e501d8-3065-4dea-a673-7b953725902d", + "metadata": {}, + "source": [ + "### Type hint annotations" + ] + }, + { + "cell_type": "markdown", + "id": "41d0e999-a6d5-474c-a5b2-cbff44e00f44", + "metadata": {}, + "source": [ + "[_dynamically typed language_]: https://en.wikipedia.org/wiki/Dynamic_programming_language\n", + "[Type hint annotations]: https://mypy.readthedocs.io/en/stable/cheat_sheet_py3.html\n", + "\n", + "In a _statically typed language_, variable types are explicitly declared. If `s` is declared to be a string, then `s` will always be a string. Type errors are found when code is _compiled_.\n", + "\n", + "Python is a [_dynamically typed language_]. Variable types are determined at runtime rather than at compile time, and it's possible for variables to even change types! \n", + "\n", + "There are tradeoffs! ⚖️ Dynamically are _more flexible_, but at the cost of _reduced type safety_.\n", + "\n", + "[Type hint annotations] provide a middle ground between statically vs. dynamically typed languages. \n", + "\n", + "🏷 Let's define a variable called `name` and provide it with a type hint annotation that says the variable should be a a `str`." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "cd171e0c-7233-479d-9e3d-18d5b71e7c3e", + "metadata": {}, + "outputs": [], + "source": [ + "name: str = \"Darth Fortran\"" + ] + }, + { + "cell_type": "markdown", + "id": "681d8f87-abdf-4a78-b4a8-619f0ed95d9b", + "metadata": {}, + "source": [ + "The type hint of `str` in the above cell didn't actually do anything when we ran it. But when we read the code, it does tell us what type to expect `name` to be.\n", + "\n", + "How about a `list` that starts empty but will eventually contain names? " + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "8733b1c7-1d85-463a-9d63-718379032fb0", + "metadata": {}, + "outputs": [], + "source": [ + "names: list[str] = []" + ] + }, + { + "cell_type": "markdown", + "id": "d8597c81-8e6e-49b3-9b6e-bbfe86f88c8a", + "metadata": {}, + "source": [ + "This type hint still doesn't do anything at runtime, but it helps us read the code, and lets us use code quality tools that can help us find errors.\n", + "\n", + "We can specify that a variable should be one of multiple types with `|`:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "24b7a377-0370-48ba-a3f6-a64181508a11", + "metadata": {}, + "outputs": [], + "source": [ + "identifier: str | int" + ] + }, + { + "cell_type": "markdown", + "id": "d5c8ac47-fcd1-4968-a14a-df7459a5538f", + "metadata": {}, + "source": [ + "Type hints are particularly helpful when defining functions:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "1c74d5de-3a75-4ea8-bac9-ac5dcb94f9e6", + "metadata": {}, + "outputs": [], + "source": [ + "def stringify(number: int) -> str:\n", + " return str(num)" + ] + }, + { + "cell_type": "markdown", + "id": "830261bc-0cb7-405f-91fa-9125f8cc7bec", + "metadata": {}, + "source": [ + "[static type checking]: https://mypy.readthedocs.io/en/stable/getting_started.html#dynamic-vs-static-typing\n", + "[mypy]: https://mypy.readthedocs.io/en/stable/\n", + "\n", + "Many Python packages, including PlasmaPy, make use of [static type checking] tools like [mypy]. These tools can help us find type errors such as in the following function before we even run the code:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "9190e68f-88d0-4746-93e0-5527f7ea014f", + "metadata": {}, + "outputs": [], + "source": [ + "def return_argument(x: int) -> str:\n", + " return x" + ] + }, + { + "cell_type": "markdown", + "id": "ac74f48b-a3cd-4f1d-9909-9c71faf5804a", + "metadata": {}, + "source": [ + "What if we want to specify that a function accepts a length and returns a volume?" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "3607d300-7ede-4a6c-a4bd-464877c2ea6a", + "metadata": {}, + "outputs": [], + "source": [ + "def volume(d: u.Quantity[u.m]) -> u.Quantity[u.m**3]:\n", + " return d**3" + ] + }, + { + "cell_type": "markdown", + "id": "28fa6bed-52ad-4a2c-b51c-32e2ba7e5c49", + "metadata": {}, + "source": [ + "### Decorators" + ] + }, + { + "cell_type": "markdown", + "id": "ce91e852-6f12-434d-943b-5ca64225b550", + "metadata": {}, + "source": [ + "In Python, functions are objects. This means that we can write a function that returns another function." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "e8d7e33f-9dd8-4351-a498-cd4ced4ce68c", + "metadata": {}, + "outputs": [], + "source": [ + "def return_function():\n", + "\n", + " print(\"Defining inner_function...\")\n", + " \n", + " def inner_function(): \n", + " print(\"Calling inner_function!\")\n", + " \n", + " return inner_function" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "ef09dad1-3d99-4d36-ae81-795433d25711", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Defining inner_function...\n" + ] + } + ], + "source": [ + "function = return_function()" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "ac56c46b-06d5-4296-b9c6-40697f25be11", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Calling inner_function!\n" + ] + } + ], + "source": [ + "function()" + ] + }, + { + "cell_type": "markdown", + "id": "03d3bae8-e510-4bcf-a01d-4d998e0b24a5", + "metadata": {}, + "source": [ + "Or we can pass a function as an argument to another function!" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "db188ae4-5ff7-48ff-b335-6d0bbfbe8b2f", + "metadata": {}, + "outputs": [], + "source": [ + "def apply_function(function, array):\n", + " return function(array)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3cc6dc5c-37f8-45a4-813c-5576b2e326f7", + "metadata": {}, + "outputs": [], + "source": [ + "array = [1, 2, 3, 4, 5]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2137ef7e-f0ac-4c7a-8a8b-596c98e2402d", + "metadata": {}, + "outputs": [], + "source": [ + "apply_function(max, array)" + ] + }, + { + "cell_type": "markdown", + "id": "bf4b7d59-1f13-42e6-87df-b3a31d635b4f", + "metadata": {}, + "source": [ + "[**decorator**]: https://www.geeksforgeeks.org/decorators-in-python/\n", + "\n", + "A function that _modifies another function_ is a [**decorator**]. \n", + "\n", + "Decorators in Python are a way to modify or enhance functions without changing their actual code. They wrap another function, potentially adding extra functionality before and after the original function runs." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "5095d1d9-a6de-401d-994c-83ffd16271c1", + "metadata": {}, + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'Callable' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[18], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mdecorator\u001b[39m(function: \u001b[43mCallable\u001b[49m):\n\u001b[1;32m 3\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mdecorated_function\u001b[39m() \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[1;32m 5\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mBefore calling the function.\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n", + "\u001b[0;31mNameError\u001b[0m: name 'Callable' is not defined" + ] + } + ], + "source": [ + "def decorator(function: Callable):\n", + " \n", + " def decorated_function() -> None:\n", + "\n", + " print(\"Before calling the function.\")\n", + " result = function()\n", + " print(\"After calling the function.\")\n", + "\n", + " return result\n", + "\n", + " return decorated_function" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "537d6027-fefc-4117-8c37-44555d38c49c", + "metadata": {}, + "outputs": [], + "source": [ + "def example_function():\n", + " print(\"Inside original example_function!\")" + ] + }, + { + "cell_type": "markdown", + "id": "d2aa4361-997e-49e7-bddc-87705ba858b6", + "metadata": {}, + "source": [ + "Let's try it out!" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "a5c89a55-260b-46de-bb30-6023e3f90c2c", + "metadata": {}, + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'decorator' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[20], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m modified_function \u001b[38;5;241m=\u001b[39m \u001b[43mdecorator\u001b[49m(example_function)\n", + "\u001b[0;31mNameError\u001b[0m: name 'decorator' is not defined" + ] + } + ], + "source": [ + "modified_function = decorator(example_function)" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "d15b8474-7cc1-4e0f-83cc-2f56d2c387ec", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Inside original example_function!\n" + ] + } + ], + "source": [ + "example_function()" + ] + }, + { + "cell_type": "markdown", + "id": "41320c46-19d7-4cbc-9ad7-dda5933208de", + "metadata": {}, + "source": [ + "In Python, we have _syntactic sugar_ for decorators, using `@`:" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "2f08b76f-d26b-4124-b453-a04d210366ba", + "metadata": {}, + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'decorator' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[22], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[38;5;129m@decorator\u001b[39m\n\u001b[1;32m 2\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21manother_function\u001b[39m():\n\u001b[1;32m 3\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mInside function2!\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n", + "\u001b[0;31mNameError\u001b[0m: name 'decorator' is not defined" + ] + } + ], + "source": [ + "@decorator\n", + "def another_function():\n", + " print(\"Inside function2!\")" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "b8f5eec3-136c-4ca8-876f-2a02362a32d1", + "metadata": {}, + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'function2' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[23], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mfunction2\u001b[49m()\n", + "\u001b[0;31mNameError\u001b[0m: name 'function2' is not defined" + ] + } + ], + "source": [ + "function2()" + ] + }, + { + "cell_type": "markdown", + "id": "4508ad5d-88a5-41e9-aba1-51d2bbe2907d", + "metadata": {}, + "source": [ + "#### Fibonacci sequence" + ] + }, + { + "cell_type": "markdown", + "id": "4bc96fbc-4b0c-4190-a1ca-9313bb149342", + "metadata": {}, + "source": [ + "Let's look at a _recursive_ function that computes the $n$th Fibonacci number. If we define $F_0 ≡ 0$ and $F_1 ≡ 1$, then $ F_n = F_{n-1} + F_{n-2}$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "27b42156-f06e-484d-ae8c-767d8267fb4d", + "metadata": {}, + "outputs": [], + "source": [ + "def fibonacci(n: int) -> int:\n", + " print(\"Calculating Fibonacci number for n =\", n)\n", + " return n if n < 2 else fibonacci(n - 1) + fibonacci(n - 2)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e7ddee57-66fb-4c28-9cd9-30fddd7d0b0b", + "metadata": {}, + "outputs": [], + "source": [ + "fibonacci(2)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fca36cc4-9349-4433-b13c-e2e2a1e66c23", + "metadata": {}, + "outputs": [], + "source": [ + "fibonacci(4)" + ] + }, + { + "cell_type": "markdown", + "id": "bb81be84-72a8-428d-8324-c9690f350719", + "metadata": {}, + "source": [ + "[`@functools.cache`]: https://docs.python.org/3/library/functools.html#functools.cache\n", + "\n", + "We'll use [`@functools.cache`] to store the output of a function for a particular argument." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bb8147dc-165d-48e7-a5ae-a325410b2d22", + "metadata": {}, + "outputs": [], + "source": [ + "from functools import cache\n", + "\n", + "@cache\n", + "def fibonacci_cached(n: int) -> int:\n", + " print(\"Calculating Fibonacci number for n =\", n)\n", + " return n if n < 2 else fibonacci_cached(n - 1) + fibonacci_cached(n - 2)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c861945a-c8e9-43d9-9dfe-00bde320129c", + "metadata": {}, + "outputs": [], + "source": [ + "fibonacci_decorated(2)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "01f5996f-5218-45c8-9fa1-c97755600a3e", + "metadata": {}, + "outputs": [], + "source": [ + "fibonacci_decorated(5)" + ] + }, + { + "cell_type": "markdown", + "id": "8fb186d4-0a5c-462c-ba67-117906be5771", + "metadata": {}, + "source": [ + "Let's try calling it again!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fee0a698-e73e-4f69-a690-68daa39a043c", + "metadata": {}, + "outputs": [], + "source": [ + "fibonacci_decorated(5)" + ] + }, + { + "cell_type": "markdown", + "id": "f7377f28-0f01-40f9-9125-e9f5ae78132f", + "metadata": {}, + "source": [ + "## Writing a formulary function" + ] + }, + { + "cell_type": "markdown", + "id": "227f37ab-5f1b-4a98-a34f-88ca38afce7c", + "metadata": {}, + "source": [ + "[`@particle_input`]: https://docs.plasmapy.org/en/stable/api/plasmapy.particles.decorators.particle_input.html\n", + "[`@validate_quantities`]: https://docs.plasmapy.org/en/stable/api/plasmapy.utils.decorators.validators.validate_quantities.html#validate-quantities\n", + "[`@check_relativistic`]: https://docs.plasmapy.org/en/stable/api/plasmapy.utils.decorators.checks.check_relativistic.html#check-relativistic\n", + "[`astropy.units`]: https://docs.astropy.org/en/stable/units/index.html\n", + "[`plasmapy.particles`]: https://docs.plasmapy.org/en/stable/particles/index.html\n", + "[`plasmapy.formulary`]: https://docs.plasmapy.org/en/stable/formulary/index.html\n", + "\n", + "PlasmaPy has three decorators that help us with writing formulary functions.\n", + "\n", + " - [`@check_relativistic`]\n", + " - [`@validate_quantities`]\n", + " - [`@particle_input`]\n", + "\n", + "Let's get some imports out of the way." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f2080cc3-2567-4ee1-87c4-a93aea165c47", + "metadata": {}, + "outputs": [], + "source": [ + "import astropy.units as u\n", + "from astropy import constants\n", + "from plasmapy.utils.decorators import check_relativistic, validate_quantities\n", + "from plasmapy.particles import particle_input" + ] + }, + { + "cell_type": "markdown", + "id": "5ce64829-a508-488d-a571-133c3b5b57af", + "metadata": {}, + "source": [ + "### Checking relativistic velocities" + ] + }, + { + "cell_type": "markdown", + "id": "9fab64f6-a591-456c-b482-7753c31e35da", + "metadata": {}, + "source": [ + "[`@check_relativistic`]: https://docs.plasmapy.org/en/stable/api/plasmapy.utils.decorators.checks.check_relativistic.html#check-relativistic\n", + "[`plasmapy.formulary`]: https://docs.plasmapy.org/en/stable/formulary/index.html\n", + "\n", + "Many functions that return velocities in [`plasmapy.formulary`] assume that relativistic velocities are unimportant. To check for this, we can use the [`@check_relativistic`] decorator. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "09dcca0c-4f88-4788-a901-146238c556c4", + "metadata": {}, + "outputs": [], + "source": [ + "@check_relativistic\n", + "def speed(distance, time):\n", + " return distance / time" + ] + }, + { + "cell_type": "markdown", + "id": "d380c763-f2f8-4944-845b-4256a191e3ae", + "metadata": {}, + "source": [ + "Let's try this with a velocity _approaching_ the speed of light." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ed16449c-39e3-4422-9a76-7bbceae478ce", + "metadata": {}, + "outputs": [], + "source": [ + "speed(299792457 * u.m, 1 * u.s)" + ] + }, + { + "cell_type": "markdown", + "id": "ac094c66-2990-48f8-958f-eb30baf46e08", + "metadata": {}, + "source": [ + "Now let's try this with a velocity _exceeding_ the speed of light." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a24084ea-f588-44c9-b945-401717acd2ec", + "metadata": {}, + "outputs": [], + "source": [ + "speed(299792459 * u.m, 1 * u.s)" + ] + }, + { + "cell_type": "markdown", + "id": "03f106da-02aa-4ad0-a35a-28d9d9ea3d4e", + "metadata": {}, + "source": [ + "### Validating quantities" + ] + }, + { + "cell_type": "markdown", + "id": "36d89f38-c4cf-4def-83c0-fe041082f26f", + "metadata": {}, + "source": [ + "[`Quantity`]: https://docs.astropy.org/en/stable/units/quantity.html\n", + "[`astropy.units`]: https://docs.astropy.org/en/stable/units/\n", + "[`plasmapy.formulary`]: https://docs.plasmapy.org/en/stable/formulary/index.html\n", + "\n", + "Most [`plasmapy.formulary`] functions accept and return [`Quantity`] objects from [`astropy.units`]. For example, we might write a function to calculate the magnetic pressure:\n", + "\n", + "$$ p_B ≡ \\frac{B^2}{2μ_o} $$" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "87b09a20-3ea4-4581-a095-f32b67437bb9", + "metadata": {}, + "outputs": [], + "source": [ + "def magnetic_pressure(B):\n", + " return B**2 / (2 * constants.mu0)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fa8103a8-a0ac-475c-bb75-ad4b0a7c3066", + "metadata": {}, + "outputs": [], + "source": [ + "magnetic_pressure(1 * u.T)" + ] + }, + { + "cell_type": "markdown", + "id": "659b63ad-9575-4676-bcbe-f9593c485c46", + "metadata": {}, + "source": [ + "But the above function doesn't prevent us from making mistakes simple mistakes..." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "eb4fd6db-71d6-4427-9c21-0cef9706d699", + "metadata": {}, + "outputs": [], + "source": [ + "magnetic_pressure(1 * u.B)" + ] + }, + { + "cell_type": "markdown", + "id": "4c1fa3a1-128d-4c09-bde5-341ba0a3add6", + "metadata": {}, + "source": [ + "[`@validate_quantities`]: https://docs.plasmapy.org/en/latest/api/plasmapy.utils.decorators.validators.validate_quantities.html#validate-quantities\n", + "\n", + "The [`@validate_quantities`] decorator makes sure that " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bc96b8eb-20e6-4812-921d-cfe4f3935a7a", + "metadata": {}, + "outputs": [], + "source": [ + "@validate_quantities\n", + "def magnetic_pressure(B: u.Quantity[u.T]) -> u.Quantity[u.Pa]:\n", + " return B**2 / (2 * constants.mu0)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "450c05cc-41bd-4d41-bb54-ddc67ac8e421", + "metadata": {}, + "outputs": [], + "source": [ + "magnetic_pressure(1 * u.T)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "aace6447-013b-4030-90bd-ec725a8ba9a4", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "magnetic_pressure(1 * u.B)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1f64a511-5882-4f6d-8ac6-22dadf422071", + "metadata": {}, + "outputs": [], + "source": [ + "magnetic_pressure(1)" + ] + }, + { + "cell_type": "markdown", + "id": "1ea028ab-730e-4a83-bd49-a9e8d5799af3", + "metadata": {}, + "source": [ + "[`@validate_quantities`]: https://docs.plasmapy.org/en/latest/api/plasmapy.utils.decorators.validators.validate_quantities.html#validate-quantities\n", + "[equivalency]: https://docs.astropy.org/en/stable/units/equivalencies.html\n", + "[`astropy.units`]: https://docs.astropy.org/en/stable/units/\n", + "\n", + "\n", + "We can also provide arguments directly to [`@validate_quantities`], for example if we want to do a validation on the return value whilst using an [equivalency] from [`astropy.units`]." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b47f2360-1037-4ded-a71e-ea94cd54f757", + "metadata": {}, + "outputs": [], + "source": [ + "@validate_quantities(\n", + " validations_on_return={\"units\": u.K, \"equivalencies\": u.temperature_energy()}\n", + ")\n", + "def get_temperature(T):\n", + " return T" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e3f27c37-01b1-4a36-847a-124d752e1342", + "metadata": {}, + "outputs": [], + "source": [ + "get_temperature(1 * u.eV)" + ] + }, + { + "cell_type": "markdown", + "id": "bd2fe7dc-6304-4e4d-a259-9fd730be272c", + "metadata": {}, + "source": [ + "The docstring for [`@validate_quantities`](https://docs.plasmapy.org/en/latest/api/plasmapy.utils.decorators.validators.validate_quantities.html#validate-quantities) has examples for several more validations that we can do, such as:\n", + "\n", + " - Allowing only positive values\n", + " - Restricting/allowing `complex` values\n", + " - Restricting/allowing `numpy.nan` values \n", + " - Allowing `None` instead of a `Quantity`" + ] + }, + { + "cell_type": "markdown", + "id": "c64c1d1f-31d0-4b85-ae18-ebc29add503c", + "metadata": {}, + "source": [ + "## Particle inputs" + ] + }, + { + "cell_type": "markdown", + "id": "dd6998ba-38ac-4d91-bea8-c7a9e22ed241", + "metadata": {}, + "source": [ + "[particle-like]: https://docs.plasmapy.org/en/latest/glossary.html#term-particle-like\n", + "\n", + "Plasma parameters often depend on particle properties. If we want to calculate a gyroradius, then we need a particle's mass and charge. \n", + "\n", + "Early on, we wrote functions like this:" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "b002c4f9-bbdf-4a26-8cae-0d2690d25749", + "metadata": {}, + "outputs": [], + "source": [ + "def get_particle_mass(particle_string):\n", + " particle_object = Particle(particle_string)\n", + " return particle_object.mass" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "10b511e8-9691-4a11-a6d7-c0bf44dc2d1e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/latex": [ + "$1.6726219 \\times 10^{-27} \\; \\mathrm{kg}$" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "get_particle_mass(\"p+\")" + ] + }, + { + "cell_type": "markdown", + "id": "123e5696-8cf5-44a5-a8e1-abe340b6856d", + "metadata": {}, + "source": [ + "[`ParticleList`]: https://docs.plasmapy.org/en/stable/api/plasmapy.particles.particle_collections.ParticleList.html\n", + "\n", + "That wasn't that bad. But if we wanted to make a function compatible with [`ParticleList`] objects too, then it started to become a mess." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dead2c8f-d7af-450e-acea-f7eaf2462da1", + "metadata": {}, + "outputs": [], + "source": [ + "from collections.abc import Iterable\n", + "\n", + "def get_particle_mass(particle):\n", + " if isinstance(particle, str):\n", + " particle_object = Particle(particle)\n", + " elif isinstance(particle, Iterable):\n", + " particle_object = ParticleList(particle)\n", + " return particle_object.mass" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bced4c71-7a29-470b-a54e-d34f6097102a", + "metadata": {}, + "outputs": [], + "source": [ + "get_particle_mass([\"p+\", \"e-\"])" + ] + }, + { + "cell_type": "markdown", + "id": "6db27d70-3f74-4cf3-976b-06294295eede", + "metadata": {}, + "source": [ + "[**`@particle_input`**]: https://docs.plasmapy.org/en/stable/api/plasmapy.particles.decorators.particle_input.html\n", + "[`ParticleLike`]: https://docs.plasmapy.org/en/stable/api/plasmapy.particles.particle_class.ParticleLike.html#particlelike\n", + "[`ParticleList`]: https://docs.plasmapy.org/en/stable/api/plasmapy.particles.particle_collections.ParticleList.html\n", + "[`CustomParticle`]: https://docs.plasmapy.org/en/stable/api/plasmapy.particles.particle_class.CustomParticle.html\n", + "[`Particle`]: https://docs.plasmapy.org/en/stable/api/plasmapy.particles.particle_class.Particle.html\n", + "\n", + "\n", + "The [**`@particle_input`**] decorator transforms arguments annotated with [`ParticleLike`] into a [`Particle`], [`CustomParticle`], or [`ParticleList`]. This transformation occurs _before_ the argument gets passed into the decorated function, so we end up with cleaner code!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f1955b73-9714-4280-8aeb-2e6cb3b855b3", + "metadata": {}, + "outputs": [], + "source": [ + "@particle_input\n", + "def make_particle(particle: ParticleLike):\n", + " return particle" + ] + }, + { + "cell_type": "markdown", + "id": "a285a095-544b-42cb-aed8-942a04c339aa", + "metadata": {}, + "source": [ + "Let's try passing it a string representing a particle." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1fa4d5f6-0670-4c38-9f84-2af55f1f5a01", + "metadata": {}, + "outputs": [], + "source": [ + "make_particle(\"p+\")" + ] + }, + { + "cell_type": "markdown", + "id": "eb07e7af-52db-4630-afdf-55dd38fa3f67", + "metadata": {}, + "source": [ + "[`Particle`]: https://docs.plasmapy.org/en/stable/api/plasmapy.particles.particle_class.Particle.html\n", + "[`@particle_input`]: https://docs.plasmapy.org/en/stable/api/plasmapy.particles.decorators.particle_input.html\n", + "\n", + "\n", + "The argument got converted into a [`Particle`] object by [`@particle_input`].\n", + "\n", + "If we add `Z` and `mass_numb` as parameters, they'll get incorporated into the particle." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b8dc7186-2f24-40f1-af6f-0aea0f6266d5", + "metadata": {}, + "outputs": [], + "source": [ + "@particle_input\n", + "def make_particle(particle: ParticleLike, Z=None, mass_numb=None):\n", + " return particle" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "79d4951b-bd64-465f-ab8d-2204a1dba965", + "metadata": {}, + "outputs": [], + "source": [ + "make_particle(\"He\", Z=2, mass_numb=4)" + ] + }, + { + "cell_type": "markdown", + "id": "081c1b64-ebc4-4fe0-b76f-a0d36c5faa33", + "metadata": {}, + "source": [ + "[`@particle_input`]: https://docs.plasmapy.org/en/stable/api/plasmapy.particles.decorators.particle_input.html\n", + "\n", + "If we name the parameter `element`, `ion`, or `isotope`, then [`@particle_input`] will make sure that the particle matches that identity. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bba411ee-8825-4056-8fcb-4860171c1cb2", + "metadata": {}, + "outputs": [], + "source": [ + "@particle_input\n", + "def mass_number(isotope: ParticleLike):\n", + " return isotope.mass_number" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4438edbb-ae2e-4733-a7c9-45e59aa4c3e9", + "metadata": {}, + "outputs": [], + "source": [ + "mass_number(\"Fe-56\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6b7cb398-74d8-487b-900a-d9cbe8b3dcf6", + "metadata": {}, + "outputs": [], + "source": [ + "mass_number(\"e-\")" + ] + }, + { + "cell_type": "markdown", + "id": "b00a6e00-3baf-4e1c-aa8e-271470a3fb5e", + "metadata": {}, + "source": [ + "### Putting it all together" + ] + }, + { + "cell_type": "markdown", + "id": "9994fef5-6c29-4639-a3be-f186e271302d", + "metadata": {}, + "source": [ + "[`@validate_quantities`]: https://docs.plasmapy.org/en/latest/api/plasmapy.utils.decorators.validators.validate_quantities.html#validate-quantities\n", + "[`@particle_input`]: https://docs.plasmapy.org/en/stable/api/plasmapy.particles.decorators.particle_input.html\n", + "\n", + "Let's use both [`@particle_input`] and [`@validate_quantities`] to write an Alfvén speed function. The formula is $$ V_A ≡ \\frac{B}{\\sqrt{μ_0 ρ}} $$ where we approximate the mass density as $ρ ≈ m_i n_i$. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "827f4dff-cb23-4c06-b45f-0771ddd511a4", + "metadata": {}, + "outputs": [], + "source": [ + "@particle_input\n", + "@check_relativistic\n", + "@validate_quantities\n", + "def alfven_speed(\n", + " B: u.Quantity[u.T],\n", + " n: u.Quantity[u.m**-3], \n", + " ion: ParticleLike,\n", + ") -> u.Quantity[u.m / u.s]:\n", + " \"\"\"Return the Alfvén speed.\"\"\"\n", + " return B / np.sqrt(constants.mu0 * n * ion.mass)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "73efd087-fd5d-482f-b131-e420e8fd2dfb", + "metadata": {}, + "outputs": [], + "source": [ + "alfven_speed(B = 0.02 * u.T, n = 1e15 * u.m**-3, ion=\"p+\")" + ] + } + ], + "metadata": { + "celltoolbar": "Tags", + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.1" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "state": {}, + "version_major": 2, + "version_minor": 0 + } + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}