diff --git a/.github/workflows/README.md b/.github/workflows/README.md new file mode 100644 index 0000000..c748fe1 --- /dev/null +++ b/.github/workflows/README.md @@ -0,0 +1,16 @@ +# GitHub workflows + +[GitHub Actions]: https://docs.github.com/en/actions +[Nox]: https://nox.thea.codes +[YAML]: https://en.wikipedia.org/wiki/YAML + +The [`.github/workflows`](.) directory contains [YAML] files that +describe [GitHub Actions] workflows such as those used to perform +continuous integration tests. + +Several of the workflows invoke [Nox] sessions that are defined in the +top-level [`noxfile.py`](../../noxfile.py). + +Continuous integration (CI) workflows include: + + - [`ci.yml`](./ci.yml) — perform CI checks during pull requests (PRs) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bfbc5fa..c08c1e3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,17 +31,17 @@ jobs: matrix: include: - - name: Tests, Python 3.12, Ubuntu + - name: Tests os: ubuntu-latest python: '3.12' nox_session: tests - - name: Documentation, Python 3.12, Ubuntu + - name: Documentation os: ubuntu-latest python: '3.12' nox_session: docs - - name: Packaging, Python 3.12, Ubuntu + - name: Packaging os: ubuntu-latest python: '3.12' nox_session: build diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d770454..caf5493 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,16 +8,8 @@ repos: hooks: - id: check-ast name: validate Python code - - id: check-merge-conflict - name: check for git merge conflicts exclude: .*\.rst - id: check-case-conflict - name: check for filename case conflicts - - id: trailing-whitespace - - id: end-of-file-fixer - - id: check-json - - id: check-toml - - id: check-yaml - repo: https://github.com/python-jsonschema/check-jsonschema rev: 0.28.6 @@ -35,16 +27,6 @@ repos: hooks: - id: rst-directive-colons - id: rst-inline-touching-normal - - id: text-unicode-replacement-char - -- repo: https://github.com/codespell-project/codespell - rev: v2.3.0 - hooks: - - id: codespell - name: codespell (add false positives to pyproject.toml) - args: [--write-changes] - additional_dependencies: - - tomli - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.5.0 diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 665ba33..725c604 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -15,4 +15,6 @@ sphinx: python: install: + - method: pip + path: . - requirements: docs/requirements.txt diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..2c4a3fb --- /dev/null +++ b/docs/README.md @@ -0,0 +1,49 @@ +# Documentation directory + +[**documentation guide**]: https://docs.plasmapy.org/en/latest/contributing/doc_guide.html +[Sphinx]: https://www.sphinx-doc.org +[Nox]: https://nox.thea.codes +[Read the Docs]: https://about.readthedocs.com +[reStructuredText]: https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html#rst-primer +[`docs`]: . +[`docs/source/conf.py`]: conf.py +[install graphviz]: https://graphviz.org/download +[install pandoc]: https://pandoc.org/installing.html + +> [!TIP] +> To learn more about writing documentation, please check out the +> [**documentation guide**]. + +The [`docs`] directory contains the source files for the narrative +documentation. The configuration file is [`docs/source/conf.py`]. + +The documentation is written in [reStructuredText], built using +[Sphinx] via a [Nox] session, and hosted by [Read the Docs]. + +## Building documentation + +> [!TIP] +> When making a pull request, the documentation can be previewed by +> clicking on *Details* next to **docs/readthedocs.org** in the +> list of checks. + +Prior to building documentation locally, please install [Nox] and its +dependencies with: + +```shell +python -m pip install nox uv +``` + +> [!NOTE] +> It may also be necessary to [install pandoc] (and possibly [install graphviz]) +> if you want to build the documentation locally. + +The documentation can be built by going to the top-level directory of +your clone of PlasmaPy and running: + +```shell +nox -s docs +``` + +The documentation preview will be built in `docs/build/html` in your +local clone of this repository. diff --git a/docs/source/conf.py b/docs/source/conf.py index 1935b4f..771a224 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -9,7 +9,7 @@ project = "hack" copyright = "2024, PlasmaPy Community" author = "PlasmaPy Community" -release = "0.1.0" +release = "2024.7.0" # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration @@ -21,6 +21,7 @@ "sphinx.ext.intersphinx", "sphinx.ext.viewcode", "sphinx_automodapi.automodapi", + "nbsphinx", ] exclude_patterns = [] diff --git a/docs/source/examples.rst b/docs/source/examples.rst new file mode 100644 index 0000000..017a920 --- /dev/null +++ b/docs/source/examples.rst @@ -0,0 +1,19 @@ +.. _examples: + +Examples +======== + +This page provides a gallery of example notebooks. + +.. contents:: + :local: + + +Tutorials +--------- + +.. nbgallery:: + :glob: + + notebooks/astropy-units-completed + notebooks/particles-formulary-completed diff --git a/docs/source/index.rst b/docs/source/index.rst index 58c6e67..bf37bc6 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -2,14 +2,14 @@ PlasmaPy Summer School 2024 Documentation ========================================= .. toctree:: - :maxdepth: 2 + :maxdepth: 1 :caption: Contents: densities frequencies lengths speeds - + examples Indices and tables ================== diff --git a/docs/source/notebooks/astropy-units-completed.ipynb b/docs/source/notebooks/astropy-units-completed.ipynb new file mode 100644 index 0000000..672ce8e --- /dev/null +++ b/docs/source/notebooks/astropy-units-completed.ipynb @@ -0,0 +1,871 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "6e8f0d92", + "metadata": {}, + "source": [ + "# Using Astropy Units" + ] + }, + { + "cell_type": "markdown", + "id": "fbdc1fd8-6542-45a6-9375-c8016a15ac67", + "metadata": { + "nbsphinx": "hidden" + }, + "source": [ + "[astropy.units]: https://docs.astropy.org/en/stable/units/index.html\n", + "\n", + "This tutorial introduces us to [astropy.units], which is heavily used in PlasmaPy." + ] + }, + { + "cell_type": "markdown", + "id": "6d4214ce-e9a3-4e4a-a51c-6dcf3f00357b", + "metadata": {}, + "source": [ + "Let's start with some preliminary imports. To execute a cell in a Jupyter notebook, press **Shift + Enter**. If you need to restart the notebook, please execute this cell again.\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": null, + "id": "33fe81d8-c1e6-4fe2-97a4-d62ce59b94b3", + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib inline\n", + "\n", + "import sys\n", + "\n", + "if 'google.colab' in str(get_ipython()):\n", + " if 'plasmapy' not in sys.modules:\n", + " !pip install astropy matplotlib numpy\n", + "\n", + "import astropy.units as u\n", + "from astropy.visualization import quantity_support\n", + "from astropy import constants\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np" + ] + }, + { + "cell_type": "markdown", + "id": "7c7ff8c4-5846-4cfd-9634-8b2aa8d871fa", + "metadata": {}, + "source": [ + "## Motivation" + ] + }, + { + "cell_type": "markdown", + "id": "c566fe1c", + "metadata": {}, + "source": [ + "In scientific computing, we often represent physical quantities as numbers." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ee456b33", + "metadata": {}, + "outputs": [], + "source": [ + "distance_in_miles = 50\n", + "time_in_hours = 2\n", + "velocity_in_mph = distance_in_miles / time_in_hours\n", + "print(velocity_in_mph)" + ] + }, + { + "cell_type": "markdown", + "id": "93658d4f", + "metadata": {}, + "source": [ + "[astropy.units]: https://docs.astropy.org/en/stable/units/index.html\n", + "[plasmapy.particles]: https://docs.plasmapy.org/en/latest/particles/index.html\n", + "[plasmapy.formulary]: https://docs.plasmapy.org/en/latest/formulary/index.html\n", + "\n", + "Representing a physical quantity as a number has risks. We might unknowingly perform operations with different units, like `time_in_seconds + time_in_hours`. We might even accidentally perform operations with physically incompatible units, like `length + time`, without catching our mistake. Unit conversion errors can be costly mistakes, such as the loss of spacecraft like the [Mars Climate Orbiter](https://science.nasa.gov/mission/mars-climate-orbiter).\n", + "\n", + "We can avoid these problems by using a units package. This notebook introduces [astropy.units] with an emphasis on the functionality needed to work with [plasmapy.particles] and [plasmapy.formulary]. We typically import [astropy.units] subpackage as `u`." + ] + }, + { + "cell_type": "markdown", + "id": "a9fad673", + "metadata": {}, + "source": [ + "## Unit essentials" + ] + }, + { + "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": null, + "id": "8437650a", + "metadata": {}, + "outputs": [], + "source": [ + "distance = 60 * u.km\n", + "print(distance)" + ] + }, + { + "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." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a6354b93", + "metadata": {}, + "outputs": [], + "source": [ + "type(distance)" + ] + }, + { + "cell_type": "markdown", + "id": "744a9b01", + "metadata": {}, + "source": [ + "[Quantity]: https://docs.astropy.org/en/stable/api/astropy.units.Quantity.html#astropy.units.Quantity\n", + "\n", + "We can create an object by using the [Quantity] class itself." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8d2bb681", + "metadata": {}, + "outputs": [], + "source": [ + "time = u.Quantity(120, u.min)" + ] + }, + { + "cell_type": "markdown", + "id": "1bb28951", + "metadata": {}, + "source": [ + "[Quantity]: https://docs.astropy.org/en/stable/api/astropy.units.Quantity.html#astropy.units.Quantity\n", + "\n", + "We can create [Quantity] objects with compound units." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4932159b", + "metadata": {}, + "outputs": [], + "source": [ + "88 * u.imperial.mile / u.hour" + ] + }, + { + "cell_type": "markdown", + "id": "9cc9adba", + "metadata": {}, + "source": [ + "[Quantity]: https://docs.astropy.org/en/stable/api/astropy.units.Quantity.html#astropy.units.Quantity\n", + "\n", + "We can even create [Quantity] objects that are explicitly dimensionless. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "09e9752c", + "metadata": {}, + "outputs": [], + "source": [ + "3 * u.dimensionless_unscaled" + ] + }, + { + "cell_type": "markdown", + "id": "4cffc8a0", + "metadata": {}, + "source": [ + "[Quantity]: https://docs.astropy.org/en/stable/api/astropy.units.Quantity.html#astropy.units.Quantity\n", + "\n", + "We can also create a [Quantity] based off of a NumPy array or a list." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e3235d80", + "metadata": {}, + "outputs": [], + "source": [ + "np.array([2.5, 3.2, 1.1]) * u.kg" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a686fd93", + "metadata": {}, + "outputs": [], + "source": [ + "[2, 3, 4] * u.m / u.s" + ] + }, + { + "cell_type": "markdown", + "id": "a60e9ea9", + "metadata": {}, + "source": [ + "## Unit operations\n", + "\n", + "[Quantity]: https://docs.astropy.org/en/stable/api/astropy.units.Quantity.html#astropy.units.Quantity\n", + "\n", + "Operations between [Quantity] objects handle unit conversions automatically. We can add [Quantity] objects together as long as their units have the same physical type." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "65e08284", + "metadata": {}, + "outputs": [], + "source": [ + "1 * u.m + 25 * u.cm" + ] + }, + { + "cell_type": "markdown", + "id": "edb43067", + "metadata": {}, + "source": [ + "Units get handled automatically during operations like multiplication, division, and exponentiation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c36788db", + "metadata": {}, + "outputs": [], + "source": [ + "velocity = distance / time\n", + "print(velocity)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "04b48a57", + "metadata": {}, + "outputs": [], + "source": [ + "area = distance**2\n", + "print(area)" + ] + }, + { + "cell_type": "markdown", + "id": "98331629", + "metadata": {}, + "source": [ + "Attempting an operation between physically incompatible units gives us an error, which we can use to find bugs in our code." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0877feb6", + "metadata": { + "tags": [ + "raises-exception" + ] + }, + "outputs": [], + "source": [ + "3 * u.m + 3 * u.s" + ] + }, + { + "cell_type": "markdown", + "id": "4f0c461e", + "metadata": {}, + "source": [ + "[Quantity]: https://docs.astropy.org/en/stable/api/astropy.units.Quantity.html#astropy.units.Quantity\n", + "[numpy.ndarray]: https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html\n", + "\n", + "[Quantity] arrays behave very similarly to NumPy arrays. In fact, [Quantity] is a subclass of [numpy.ndarray]." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "59389429", + "metadata": {}, + "outputs": [], + "source": [ + "balmer_series = [656, 486, 434, 410] * u.nm\n", + "Hα = balmer_series[0]\n", + "print(Hα)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a6545132", + "metadata": {}, + "outputs": [], + "source": [ + "np.max(balmer_series)" + ] + }, + { + "cell_type": "markdown", + "id": "e1bb9434", + "metadata": {}, + "source": [ + "[NumPy]: https://numpy.org/\n", + "[SciPy]: https://scipy.org/\n", + "\n", + "[Quantity]: https://docs.astropy.org/en/stable/api/astropy.units.Quantity.html#astropy.units.Quantity\n", + "[lose their units]: https://docs.astropy.org/en/stable/known_issues.html#quantities-lose-their-units-with-some-operations\n", + "\n", + "⚠️ Most frequently encountered [NumPy] and [SciPy] functions can be used with [Quantity] objects. However, there are some functions that cause [Quantity] objects to [lose their units]!" + ] + }, + { + "cell_type": "markdown", + "id": "b910a84b", + "metadata": {}, + "source": [ + "## Unit conversions\n", + "\n", + "[Quantity]: https://docs.astropy.org/en/stable/api/astropy.units.Quantity.html#astropy.units.Quantity\n", + "[.to]: https://docs.astropy.org/en/stable/api/astropy.units.Quantity.html#astropy.units.Quantity.to\n", + "\n", + "The [.to] method allows us to convert a [Quantity] to different units of the same physical type. This method accepts strings that represent a unit (including compound units) or a unit object." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "80f4f133", + "metadata": {}, + "outputs": [], + "source": [ + "velocity.to(\"m/s\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21c46ef1", + "metadata": {}, + "outputs": [], + "source": [ + "velocity.to(u.m / u.s)" + ] + }, + { + "cell_type": "markdown", + "id": "c1e742ca", + "metadata": {}, + "source": [ + "[Quantity]: https://docs.astropy.org/en/stable/api/astropy.units.Quantity.html#astropy.units.Quantity\n", + "[si]: https://docs.astropy.org/en/stable/api/astropy.units.Quantity.html#astropy.units.Quantity.si\n", + "[cgs]: https://docs.astropy.org/en/stable/api/astropy.units.Quantity.html#astropy.units.Quantity.cgs\n", + "\n", + "The [si] and [cgs] attributes convert the [Quantity] to SI or CGS units, respectively. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "02bda4d2", + "metadata": {}, + "outputs": [], + "source": [ + "velocity.si" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "560a0cfe", + "metadata": {}, + "outputs": [], + "source": [ + "velocity.cgs" + ] + }, + { + "cell_type": "markdown", + "id": "1429d5a2", + "metadata": {}, + "source": [ + "## Detaching units and values\n", + "\n", + "[value]: https://docs.astropy.org/en/stable/api/astropy.units.Quantity.html#astropy.units.Quantity.value \n", + "[Quantity]: https://docs.astropy.org/en/stable/api/astropy.units.Quantity.html#astropy.units.Quantity\n", + "\n", + "The [value] attribute of a [Quantity] provides the number or array *without* the unit." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24e8403c", + "metadata": {}, + "outputs": [], + "source": [ + "time.value" + ] + }, + { + "cell_type": "markdown", + "id": "4c6c027b", + "metadata": {}, + "source": [ + "[unit]: https://docs.astropy.org/en/stable/api/astropy.units.Quantity.html#astropy.units.Quantity.unit\n", + "[Quantity]: https://docs.astropy.org/en/stable/api/astropy.units.Quantity.html#astropy.units.Quantity\n", + "\n", + "The [unit] attribute of a [Quantity] provides the unit without the value." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2c41a2fa", + "metadata": {}, + "outputs": [], + "source": [ + "time.unit" + ] + }, + { + "cell_type": "markdown", + "id": "be0c5349", + "metadata": {}, + "source": [ + "## Equivalencies" + ] + }, + { + "cell_type": "markdown", + "id": "07c3b9c1", + "metadata": {}, + "source": [ + "[electron-volt]: https://en.wikipedia.org/wiki/Electronvolt\n", + "[Boltzmann constant]: https://en.wikipedia.org/wiki/Boltzmann_constant\n", + "\n", + "Plasma scientists often use the [electron-volt] (eV) as a unit of temperature. This is a shortcut for describing the thermal energy per particle, or more accurately the temperature multiplied by the [Boltzmann constant], $k_B$. Because an electron-volt is a unit of energy rather than temperature, we cannot directly convert electron-volts to kelvin." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6b6fafd6", + "metadata": { + "tags": [ + "raises-exception" + ] + }, + "outputs": [], + "source": [ + "u.eV.to(\"K\")" + ] + }, + { + "cell_type": "markdown", + "id": "9299c8a1", + "metadata": {}, + "source": [ + "[astropy.units]: https://docs.astropy.org/en/stable/units/index.html\n", + "[equivalencies]: https://docs.astropy.org/en/stable/units/equivalencies.html\n", + "[temperature_energy()]: https://docs.astropy.org/en/stable/units/equivalencies.html#temperature-energy-equivalency\n", + "\n", + "To handle non-standard unit conversions, [astropy.units] allows the use of [equivalencies]. The conversion from eV to K can be done by using the [temperature_energy()] equivalency." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "afac5b4a", + "metadata": {}, + "outputs": [], + "source": [ + "(1 * u.eV).to(\"K\", equivalencies=u.temperature_energy())" + ] + }, + { + "cell_type": "markdown", + "id": "b4c492cc", + "metadata": {}, + "source": [ + "[dimensionless_angles()]: https://docs.astropy.org/en/stable/api/astropy.units.equivalencies.dimensionless_angles.html#dimensionless-angles\n", + "[frequency]: https://en.wikipedia.org/wiki/Frequency\n", + "[angular frequency]: https://en.wikipedia.org/wiki/Angular_frequency\n", + "\n", + "Radians are treated dimensionlessly when the [dimensionless_angles()] equivalency is in effect. Note that this equivalency does not account for the multiplicative factor of $2π$ that is used when converting between [frequency] and [angular frequency]." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9735710b", + "metadata": {}, + "outputs": [], + "source": [ + "(3.2 * u.rad / u.s).to(\"1 / s\", equivalencies=u.dimensionless_angles())" + ] + }, + { + "cell_type": "markdown", + "id": "7ec6d857", + "metadata": {}, + "source": [ + "## Physical constants" + ] + }, + { + "cell_type": "markdown", + "id": "b714de6a", + "metadata": {}, + "source": [ + "[astropy.constants]: https://docs.astropy.org/en/stable/constants/index.html\n", + "\n", + "few can use [astropy.constants] to access the most commonly needed physical constants." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "746a79a7", + "metadata": {}, + "outputs": [], + "source": [ + "print(constants.c)" + ] + }, + { + "cell_type": "markdown", + "id": "c3ee9feb", + "metadata": {}, + "source": [ + "[Constant]: https://docs.astropy.org/en/stable/api/astropy.constants.Constant.html#astropy.constants.Constant\n", + "[Quantity]: https://docs.astropy.org/en/stable/api/astropy.units.Quantity.html#astropy.units.Quantity\n", + "[u.temperature_energy()]: https://docs.astropy.org/en/stable/units/equivalencies.html#temperature-energy-equivalency\n", + "\n", + "A [Constant] behaves very similarly to a [Quantity]. For example, we can use the Boltzmann constant to mimic the behavior of [u.temperature_energy()]." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d2d59d08", + "metadata": {}, + "outputs": [], + "source": [ + "thermal_energy_per_particle = 0.6 * u.keV\n", + "temperature = thermal_energy_per_particle / constants.k_B\n", + "print(temperature.to(\"MK\"))" + ] + }, + { + "cell_type": "markdown", + "id": "7c145497", + "metadata": {}, + "source": [ + "Electromagnetic constants often need the unit system to be specified, or will result in an exception." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "015de7fc", + "metadata": { + "tags": [ + "raises-exception" + ] + }, + "outputs": [], + "source": [ + "2 * constants.e" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1a90c979", + "metadata": {}, + "outputs": [], + "source": [ + "2 * constants.e.si" + ] + }, + { + "cell_type": "markdown", + "id": "41d93ce4-d0d5-4146-8e2e-5461c6ba12ce", + "metadata": {}, + "source": [ + "Code within PlasmaPy generally uses SI units." + ] + }, + { + "cell_type": "markdown", + "id": "cd539f42-ce70-4ecf-9bdc-e1f56a86dd07", + "metadata": {}, + "source": [ + "\n", + "## Plotting quantities" + ] + }, + { + "cell_type": "markdown", + "id": "90ced3d3-aa0d-47b7-b526-359e466e5cb5", + "metadata": {}, + "source": [ + "[Quantity]: https://docs.astropy.org/en/stable/api/astropy.units.Quantity.html#astropy.units.Quantity\n", + "\n", + "Astropy has built-in support for plotting [Quantity] objects. Let's plot the number density of electrons in the solar wind using an empirical formula given by [Kruparova et al. (2023)](https://iopscience.iop.org/article/10.3847/1538-4357/acf572), which has a range of validity from 13 to 50 solar radii." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2fce5587-16cf-40ea-baa5-dbd9023281c3", + "metadata": {}, + "outputs": [], + "source": [ + "radii = np.linspace(13, 50, num=50) * constants.R_sun" + ] + }, + { + "cell_type": "markdown", + "id": "d73d6343-d563-4f3d-ae41-a1eefaed4618", + "metadata": {}, + "source": [ + "Next we can apply the formula to get the electron density:\n", + "\n", + "$$ n_e(R) = \\left( 343466\\ \\mbox{cm}^{-3} \\right) × \\left( \\frac{R}{R_☉} \\right)^{-1.87} $$" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7a734940-a970-4991-9ada-f0844ae80567", + "metadata": {}, + "outputs": [], + "source": [ + "n_e = 343_466 * u.cm**-3 * (radii / constants.R_sun) ** -1.87" + ] + }, + { + "cell_type": "markdown", + "id": "07547260-da42-40b7-98de-693b60dbb788", + "metadata": {}, + "source": [ + "Will make use make use of [astropy.visualization.quantity_support](https://docs.astropy.org/en/stable/api/astropy.visualization.quantity_support.html). This is a [_context manager_](https://realpython.com/python-with-statement/), which means that we use the `with` statement." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a4563556-64d3-4b7e-80f6-7df22ccf4488", + "metadata": {}, + "outputs": [], + "source": [ + "with quantity_support():\n", + " plt.figure()\n", + " plt.plot(radii.to(u.R_sun), n_e)\n", + " plt.draw()" + ] + }, + { + "cell_type": "markdown", + "id": "f4da2ab0", + "metadata": {}, + "source": [ + "## Optimizing unit operations (if time)\n", + "\n", + "[performance tips]: https://docs.astropy.org/en/stable/units/index.html#performance-tips\n", + "[astropy.units]: https://docs.astropy.org/en/stable/units/index.html\n", + "[%timeit]: https://ipython.readthedocs.io/en/stable/interactive/magics.html#magic-timeit\n", + "\n", + "Astropy's documentation includes [performance tips] for using [astropy.units] in computationally intensive situations. We can test it with [%timeit], which runs a command repeatedly to see how long it takes.\n", + "\n", + "Putting compound units in parentheses speeds things up by reducing the number of copies made by the operation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e3bc2348", + "metadata": {}, + "outputs": [], + "source": [ + "%timeit 1.6 * u.barn * u.Mpc\n", + "%timeit 1.6 * (u.barn * u.Mpc)" + ] + }, + { + "cell_type": "markdown", + "id": "4ff6e798-91dd-460d-bf29-172a95bf34b7", + "metadata": {}, + "source": [ + "[Quantity]: https://docs.astropy.org/en/stable/api/astropy.units.Quantity.html#astropy.units.Quantity\n", + "\n", + "We can assign a unit to a value using the [Quantity] class directly." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "868b008e-1560-4edd-96ee-7ec1c00aca16", + "metadata": {}, + "outputs": [], + "source": [ + "%timeit u.Quantity(1.6, u.barn * u.Mpc)" + ] + }, + { + "cell_type": "markdown", + "id": "0f6bcf83-70d4-42e3-9392-9eaa73d3f3f9", + "metadata": {}, + "source": [ + "What else can we do to save time from the above operation?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3ecb16da-9dc3-4a62-a03a-e3afb312ae84", + "metadata": {}, + "outputs": [], + "source": [ + "volume_unit = u.barn * u.Mpc # pre-define the unit\n", + "%timeit u.Quantity(1.6, volume_unit)" + ] + }, + { + "cell_type": "markdown", + "id": "55b8dcd1", + "metadata": {}, + "source": [ + "## Physical types (if time)" + ] + }, + { + "cell_type": "markdown", + "id": "0421ee56", + "metadata": {}, + "source": [ + "[physical type]: https://docs.astropy.org/en/stable/units/physical_types.html\n", + "[physical_type]: https://docs.astropy.org/en/stable/api/astropy.units.UnitBase.html#astropy.units.UnitBase.physical_type\n", + "[get_physical_type()]: https://docs.astropy.org/en/stable/api/astropy.units.get_physical_type.html#astropy.units.get_physical_type\n", + "\n", + "A [physical type] corresponds to physical quantities with dimensionally compatible units. Astropy has functionality that represents different physical types. These physical type objects can be accessed using either the [physical_type] attribute of a unit or [get_physical_type()]." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "da6c9c7d", + "metadata": {}, + "outputs": [], + "source": [ + "(u.m**2 / u.s).physical_type" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f0d49b03", + "metadata": {}, + "outputs": [], + "source": [ + "u.get_physical_type(\"number density\")" + ] + }, + { + "cell_type": "markdown", + "id": "8cd7e09a", + "metadata": {}, + "source": [ + "These physical type objects can be used for dimensional analysis." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d03b8eb0", + "metadata": {}, + "outputs": [], + "source": [ + "energy_density = (u.J * u.m**-3).physical_type\n", + "velocity = u.get_physical_type(\"velocity\")\n", + "print(energy_density * velocity)" + ] + }, + { + "cell_type": "markdown", + "id": "ff6c5c8d-2a21-4314-8b03-5b1096e94232", + "metadata": {}, + "source": [ + "There are some pretty obscure physical types too!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2371647f-f29d-41cb-b89d-226d4743ce26", + "metadata": {}, + "outputs": [], + "source": [ + "u.get_physical_type(u.m * u.s)" + ] + } + ], + "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.4" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/source/notebooks/particles-formulary-completed.ipynb b/docs/source/notebooks/particles-formulary-completed.ipynb new file mode 100644 index 0000000..c2f5f3a --- /dev/null +++ b/docs/source/notebooks/particles-formulary-completed.ipynb @@ -0,0 +1,2329 @@ +{ + "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", + "[decorators]: https://www.geeksforgeeks.org/decorators-in-python/\n", + "[Type hint annotations]: https://mypy.readthedocs.io/en/stable/cheat_sheet_py3.html\n", + "\n", + "We'll start this notebook with a quick refresher on [astropy.units], before going through examples from [plasmapy.particles] ⚛️ and [plasmapy.formulary] 🧮. After an interlude about [type hint annotations] and [decorators], we'll discuss 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": null, + "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", + "import numpy as np\n", + "from plasmapy.particles import *\n", + "from plasmapy.particles.particle_class import valid_categories\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", + "We'll be using [astropy.units] throughout the rest of this notebook, so we'll begin with a brief reminder o fhow to use it. 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": null, + "id": "8437650a", + "metadata": {}, + "outputs": [], + "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": [ + "[Quantity]: https://docs.astropy.org/en/stable/api/astropy.units.Quantity.html#astropy.units.Quantity\n", + "[Quantity.to()]: https://docs.astropy.org/en/stable/api/astropy.units.Quantity.html#astropy.units.Quantity.to\n", + "\n", + "We can use [Quantity.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.nuclear_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": [ + "[plasmapy.particles.particle_class.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 [plasmapy.particles.particle_class.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": [ + "[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": null, + "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": null, + "id": "96fee1ef", + "metadata": {}, + "outputs": [], + "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", + "[plasmapy.particles]: https://docs.plasmapy.org/en/stable/particles/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": null, + "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": null, + "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": null, + "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": null, + "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": null, + "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": null, + "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": null, + "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": null, + "id": "ef09dad1-3d99-4d36-ae81-795433d25711", + "metadata": {}, + "outputs": [], + "source": [ + "function = return_function()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ac56c46b-06d5-4296-b9c6-40697f25be11", + "metadata": {}, + "outputs": [], + "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! We can use `typing.Callable` as the corresponding type hint annotation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "27d99306-584e-4d1d-a913-0ead6c9b982f", + "metadata": {}, + "outputs": [], + "source": [ + "from typing import Callable" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "db188ae4-5ff7-48ff-b335-6d0bbfbe8b2f", + "metadata": {}, + "outputs": [], + "source": [ + "def apply_function(function: Callable, 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": null, + "id": "5095d1d9-a6de-401d-994c-83ffd16271c1", + "metadata": {}, + "outputs": [], + "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": null, + "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": null, + "id": "a5c89a55-260b-46de-bb30-6023e3f90c2c", + "metadata": {}, + "outputs": [], + "source": [ + "modified_function = decorator(example_function)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d15b8474-7cc1-4e0f-83cc-2f56d2c387ec", + "metadata": {}, + "outputs": [], + "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": null, + "id": "2f08b76f-d26b-4124-b453-a04d210366ba", + "metadata": {}, + "outputs": [], + "source": [ + "@decorator\n", + "def function2():\n", + " print(\"Inside function2!\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b8f5eec3-136c-4ca8-876f-2a02362a32d1", + "metadata": {}, + "outputs": [], + "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", + "\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_cached(2)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "01f5996f-5218-45c8-9fa1-c97755600a3e", + "metadata": {}, + "outputs": [], + "source": [ + "fibonacci_cached(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_cached(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\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": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [ + "raises-exception" + ] + }, + "outputs": [], + "source": [ + "speed(299792459 * u.m, 1 * u.s)" + ] + }, + { + "cell_type": "markdown", + "id": "03f106da-02aa-4ad0-a35a-28d9d9ea3d4e", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "### Validating quantities" + ] + }, + { + "cell_type": "markdown", + "id": "36d89f38-c4cf-4def-83c0-fe041082f26f", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "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": { + "editable": true, + "scrolled": true, + "slideshow": { + "slide_type": "" + }, + "tags": [ + "raises-exception" + ] + }, + "outputs": [], + "source": [ + "magnetic_pressure(1 * u.B)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1f64a511-5882-4f6d-8ac6-22dadf422071", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "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) contains examples for several more validations that it can do, such as:\n", + "\n", + " - Allowing only non-negative 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, especially the particle's charge and mass. \n", + "\n", + "Early on, we wrote formulary functions like this:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "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": null, + "id": "10b511e8-9691-4a11-a6d7-c0bf44dc2d1e", + "metadata": {}, + "outputs": [], + "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", + "The above example isn't particularly 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", + "\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": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [ + "raises-exception" + ] + }, + "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", + "[@check_relativistic]: https://docs.plasmapy.org/en/stable/api/plasmapy.utils.decorators.checks.check_relativistic.html#check-relativistic\n", + "\n", + "Let's use both [@particle_input], [@validate_quantities], and [@check_relativistic] 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": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "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.4" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "state": {}, + "version_major": 2, + "version_minor": 0 + } + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..131db9f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +plasmapy[docs,tests] +nox +uv diff --git a/src/hack/README.md b/src/hack/README.md new file mode 100644 index 0000000..f3f051f --- /dev/null +++ b/src/hack/README.md @@ -0,0 +1,17 @@ +[`src/hack`]: . +[**coding guide**]: https://docs.plasmapy.org/en/latest/contributing/coding_guide.html +[contributor guide]: https://docs.plasmapy.org/en/latest/contributing +[`tests`]: ../../tests +[`docs`]: ../../docs + +# Source code + +The [`src/hack`] directory contains the source code for the `hack` +Python package hosted in this repository. + +The corresponding tests are in the top-level [`tests`] directory, with +documentation in the top-level [`docs`] directory. + +> [!TIP] +> For more coding tips, please see the [**coding guide**] in PlasmaPy's +> [contributor guide]. diff --git a/tests/README.md b/tests/README.md index e69de29..71b1ead 100644 --- a/tests/README.md +++ b/tests/README.md @@ -0,0 +1,44 @@ +# Tests + +[**testing guide**]: https://docs.plasmapy.org/en/latest/contributing/testing_guide.html +[`src/hack/formulary/frequencies.py`]: ../src/hack/formulary/frequencies.py +[`tests/formulary/test_frequencies.py`]: formulary/test_frequencies.py +[`tests`]: . +[`src/hack`]: ../src/hack +[Nox]: https://nox.thea.codes +[pytest]: https://docs.pytest.org +[running tests]: https://docs.plasmapy.org/en/latest/contributing/testing_guide.html#running-tests + +> [!TIP] +> To learn more about software testing, please check out PlasmaPy's +> [**testing guide**]. + +The tests in this directory are written using the [pytest] framework, +with [Nox] as the test runner. + +## Locating tests + +The [`tests`] directory contains the tests for the `hack` Python package +in this repository. The directory structure and organization of +[`tests`] largely mirrors that of [`src/hack`], which contains the +source code. For example, the source code of `hack.formulary.frequencies` +is located at [`src/hack/formulary/frequencies.py`], while its tests are +located at [`tests/formulary/test_frequencies.py`] + +## Running tests + +To run tests locally, first install [Nox] and its dependencies with: + +```shell +python -m pip install nox uv +``` + +To run all but the slowest tests, enter the top-level directory of your +clone of this repository and run: + +```shell +nox +``` + +For more information, please see the section in the testing guide on +[running tests]. diff --git a/tests/test_example.py b/tests/test_example.py index 41b0cad..0157aaa 100644 --- a/tests/test_example.py +++ b/tests/test_example.py @@ -1,3 +1,6 @@ -def test_that_intentionally_fails(): - """An example that is designed to fail.""" - assert 6 * 9 == 42 +"""Example tests.""" + + +def test_multiplication(): + """An sample test.""" + assert 4 * 5 == 20