PlasmaPy logo

PlasmaPy Documentation

PlasmaPy is an open source community-developed Python 3.10+ package for plasma research and education. PlasmaPy is a platform by which the plasma community can share code and collaboratively develop new software tools for plasma research.

If you are new to PlasmaPy, please check out our getting started notebooks and our example gallery. We invite you to share ideas and ask questions in our Matrix chat room or during our weekly virtual office hours.

PlasmaPy is developed openly on GitHub, where you can request a new feature or report a bug.

Important

If you use PlasmaPy for work presented in a publication or talk, please help the project by following these instructions to cite or acknowledge PlasmaPy.

Installing PlasmaPy

Note

If you would like to contribute to PlasmaPy, please check out the Contributor Guide.

Installing Python

PlasmaPy requires a version of Python between 3.10 and 3.12. If you do not have Python installed already, here are the instructions to download Python and install it.

Tip

New versions of Python are released annually in October, and it can take a few months for the scientific Python ecosystem to catch up. If you have trouble installing plasmapy on the most recent Python version between October and March, then try installing it on the second most recent version.

Installing PlasmaPy with pip

To install the most recent release of plasmapy on PyPI with pip into an existing Python 3.10+ environment on macOS or Linux, open a terminal and run:

python -m pip install plasmapy

On some systems, it might be necessary to specify the Python version number by using python3, python3.8, python3.9, python3.10, or python3.11 instead of python.

To install PlasmaPy on Windows, run:

py -3.11 -m pip install plasmapy

The version of Python may be changed from 3.11 to another supported Python 3.10+ release that has been installed on your computer.

For more detailed information, please refer to this tutorial on installing packages.

Installing PlasmaPy with Conda

Conda is a package management system and environment manager that is commonly used in the scientific Python ecosystem. Conda lets us create and switch between Python environments that are isolated from each other and the system installation. Conda can also be used for packages written in languages other than Python.

After installing Conda or miniconda, plasmapy can be installed into an activated Conda environment by opening a terminal and running:

conda install -c conda-forge plasmapy

Here -c conda-forge indicates that plasmapy should be installed from the conda-forge channel.

To install plasmapy into another existing Conda environment, append -n env_name to the previous command, where env_name is replaced with the name of the environment.

To create a new environment with plasmapy installed in it, run:

conda create -n env_name -c conda-forge plasmapy

where env_name is replaced by the name of the environment. This step may take several minutes. To activate this environment, run:

conda activate env_name

To update plasmapy to the most recent version within a currently activated Conda environment, run:

conda update plasmapy

Tip

Creating a Conda environment can sometimes take a few minutes. If it takes longer than that, try updating to the newest version of Conda with conda update conda or checking out these tips for improving Conda performance.

Installing PlasmaPy with Anaconda Navigator

Note

This section contains instructions on how to install PlasmaPy with Anaconda Navigator at the time of writing. For the most up-to-date information, please go to the official documentation on installing Anaconda Navigator and getting started with Anaconda Navigator.

Anaconda Navigator is a graphical user interface (GUI) for Conda that can be used to install Python packages. It is installed automatically with newer versions of Conda. If you are using Miniconda or a different Conda environment, you can install it with conda install anaconda-navigator. After that it can be opened by entering anaconda-navigator in the terminal.

First, go to the Environments tab and select Channels. If conda-forge is not listed, then go to Add, enter https://conda.anaconda.org/conda-forge, and click on Update channels and then Update index.

Next, while on the Environments tab, select the environment that you would like to install plasmapy in. The default is generally base (root). Optionally, you may select Create to start a new environment. In the search bar, enter plasmapy. Click on the checkbox next to plasmapy, and select Apply to begin the installation process. It may take several minutes for Anaconda Navigator to solve package specifications.

To test the installation, click on the icon that should be present next to the activated environment, and select Open terminal. Enter python in the terminal, and then import plasmapy to make sure it works.

Installing PlasmaPy from source code

Obtaining official releases

A ZIP file containing the source code for official releases of plasmapy can be obtained from PyPI or from Zenodo.

Alternatively, official releases since 0.7.0 can be downloaded from the releases page on PlasmaPy’s GitHub repository.

Obtaining source code from GitHub

If you have git installed on your computer, you may clone PlasmaPy’s GitHub repository and access the source code from the most recent development version by running:

git clone https://github.com/PlasmaPy/PlasmaPy.git

The repository will be cloned inside a new subdirectory called PlasmaPy.

If you do not have git installed on your computer, then you may download the most recent source code from PlasmaPy’s GitHub repository by going to Code and selecting Download ZIP. Unzipping the file will create a subdirectory called PlasmaPy that contains the source code.

Building and installing

To install the downloaded version of plasmapy, enter the PlasmaPy directory and run:

pip install .

If you expect to occasionally edit the source code, instead run:

pip install -e ".[tests,docs]"

The -e flag makes the installation editable and [tests,docs] specifies that all of the additional dependencies used while testing the package should also be installed.

Note

If you noticed any places where the installation instructions could be improved or have become out of date, please create an issue on PlasmaPy’s GitHub repository. It would really help!

Tip

The Contributor Guide has instructions on how to fork a repository and create branches so that you may make contributions via pull requests.

Getting Started

This page includes example notebooks intended for new users of PlasmaPy, including notebooks that introduce astropy.units and plasmapy.particles.

This page was generated by nbsphinx from docs/notebooks/getting_started/units.ipynb.
Interactive online version: Binder badge.

Using Astropy Units

In scientific computing, we often represent physical quantities as numbers.

[2]:
distance_in_miles = 50
time_in_hours = 2
velocity_in_mph = distance_in_miles / time_in_hours
print(velocity_in_mph)
25.0

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. 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 this subpackage as u.

[3]:
import astropy.units as u

Contents

  1. Unit basics

  2. Unit operations

  3. Unit conversations

  4. Detaching units and values

  5. Equivalencies

  6. Physical constants

  7. Units in PlasmaPy

  8. Optimizing unit operations

  9. Physical Types

Unit basics

We can create a physical quantity by multiplying or dividing a number or array with a unit.

[4]:
distance = 60 * u.km
print(distance)
60.0 km

This operation creates a Quantity: a number, sequence, or array that has been assigned a physical unit.

[5]:
type(distance)
[5]:
astropy.units.quantity.Quantity

We can also create an object by using the Quantity class itself.

[6]:
time = u.Quantity(120, u.min)

We can create Quantity objects with compound units.

[7]:
88 * u.imperial.mile / u.hour
[7]:
$88 \; \mathrm{\frac{mi}{h}}$

We can even create Quantity objects that are explicitly dimensionless.

[8]:
3 * u.dimensionless_unscaled
[8]:
$3 \; \mathrm{}$

We can also create a Quantity based off of a NumPy array or a list.

[9]:
import numpy as np

np.array([2.5, 3.2, 1.1]) * u.kg
[9]:
$[2.5,~3.2,~1.1] \; \mathrm{kg}$
[10]:
[2, 3, 4] * u.m / u.s
[10]:
$[2,~3,~4] \; \mathrm{\frac{m}{s}}$

Unit operations

Operations between Quantity objects handle unit conversions automatically. We can add Quantity objects together as long as their units have the same physical type.

[11]:
1 * u.m + 25 * u.cm
[11]:
$1.25 \; \mathrm{m}$

Units get handled automatically during operations like multiplication, division, and exponentiation.

[12]:
velocity = distance / time
print(velocity)
0.5 km / min
[13]:
area = distance**2
print(area)
3600.0 km2

Attempting an operation between physically incompatible units gives us an error, which we can use to find bugs in our code.

[14]:
3 * u.m + 3 * u.s
UnitConversionError: 's' (time) and 'm' (length) are not convertible


During handling of the above exception, another exception occurred:

UnitConversionError: Can only apply 'add' function to quantities with compatible dimensions

Quantity objects behave very similarly to NumPy arrays because Quantity is a subclass of numpy.ndarray.

[15]:
balmer_series = [656.279, 486.135, 434.0472, 410.1734] * u.nm
 = balmer_series[0]
print()
656.279 nm
[16]:
np.max(balmer_series)
[16]:
$656.279 \; \mathrm{nm}$

Most frequently encountered NumPy and SciPy functions can be used with Quantity objects. However, Quantity objects lose their units with some operations.

Unit conversions

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.

[17]:
velocity.to("m/s")
[17]:
$8.3333333 \; \mathrm{\frac{m}{s}}$
[18]:
velocity.to(u.m / u.s)
[18]:
$8.3333333 \; \mathrm{\frac{m}{s}}$

The si and cgs attributes convert the Quantity to SI or CGS units, respectively.

[19]:
velocity.si
[19]:
$8.3333333 \; \mathrm{\frac{m}{s}}$
[20]:
velocity.cgs
[20]:
$833.33333 \; \mathrm{\frac{cm}{s}}$

Detaching units and values

The value attribute of a Quantity provides the number (as a NumPy scalar) or NumPy array without the unit.

[21]:
time.value
[21]:
120.0

The unit attribute of a Quantity provides the unit without the value.

[22]:
time.unit
[22]:
$\mathrm{min}$

Equivalencies

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.

[23]:
u.eV.to("K")
UnitConversionError: 'eV' (energy/torque/work) and 'K' (temperature) are not convertible

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.

[24]:
(1 * u.eV).to("K", equivalencies=u.temperature_energy())
[24]:
$11604.518 \; \mathrm{K}$

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.

[25]:
(3.2 * u.rad / u.s).to("1 / s", equivalencies=u.dimensionless_angles())
[25]:
$3.2 \; \mathrm{\frac{1}{s}}$

Physical constants

We can use astropy.constants to access the most commonly needed physical constants.

[26]:
from astropy.constants import c, e, k_B

print(c)
  Name   = Speed of light in vacuum
  Value  = 299792458.0
  Uncertainty  = 0.0
  Unit  = m / s
  Reference = CODATA 2018

A Constant behaves very similarly to a Quantity. For example, we can use the Boltzmann constant to mimic the behavior of u.temperature_energy().

[27]:
thermal_energy_per_particle = 0.6 * u.keV
temperature = thermal_energy_per_particle / k_B
print(temperature.to("MK"))
6.962710872930049 MK

Electromagnetic constants often need the unit system to be specified. Code within PlasmaPy uses SI units.

[28]:
2 * e
TypeError: Constant 'e' does not have physically compatible units across all systems of units and cannot be combined with other values without specifying a system (eg. e.emu)

[29]:
2 * e.si
[29]:
$3.2043533 \times 10^{-19} \; \mathrm{C}$

Units in PlasmaPy

Now we can show some uses of astropy.units in PlasmaPy, starting with plasmapy.particles. Many of the attributes of Particle and ParticleList provide Quantity objects.

[30]:
from plasmapy.particles import Particle, ParticleList

alpha = Particle("He-4 2+")
[31]:
alpha.charge
[31]:
$3.2043533 \times 10^{-19} \; \mathrm{C}$
[32]:
ions = ParticleList(["O 1+", "O 2+", "O 3+"])
ions.mass
[32]:
$[2.6566054 \times 10^{-26},~2.6565143 \times 10^{-26},~2.6564232 \times 10^{-26}] \; \mathrm{kg}$

Similarly, Quantity objects are the expected inputs and outputs of most functions in plasmapy.formulary. We can use them to calculate some plasma parameters for a typical region of the solar corona.

[33]:
from plasmapy.formulary import Alfven_speed, Debye_length, gyrofrequency
[34]:
B = 0.01 * u.T
n = 1e15 * u.m**-3
proton = Particle("p+")
[35]:
Alfven_speed(B=B, density=n, ion=proton).to("km /s")
[35]:
$6895.6934 \; \mathrm{\frac{km}{s}}$
[36]:
gyrofrequency(B=B, particle="e-")
[36]:
$1.75882 \times 10^{9} \; \mathrm{\frac{rad}{s}}$

The to_hz keyword provides the frequency in hertz rather than radians per second, and accounts for the factor of \(2π\).

[37]:
gyrofrequency(B=B, particle="e-", to_hz=True)
[37]:
$2.799249 \times 10^{8} \; \mathrm{Hz}$

Formulary functions perform calculations based on SI units, but accept input arguments in other units. Temperature can be given in units of temperature (e.g., kelvin) or energy (e.g., electron-volts).

[38]:
Debye_length(T_e=1e6 * u.K, n_e=1e9 * u.m**-3)
[38]:
$2.1822556 \; \mathrm{m}$
[39]:
Debye_length(T_e=86.17 * u.eV, n_e=1e3 * u.cm**-3)
[39]:
$2.1822134 \; \mathrm{m}$

Optimizing unit operations

Astropy’s documentation includes performance tips for using astropy.units in computationally intensive situations. For example, putting compound units in parentheses reduces the need to make multiple copies of the data.

[40]:
volume = 0.62 * (u.barn * u.Mpc)

Physical types

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().

[41]:
(u.m**2 / u.s).physical_type
[41]:
PhysicalType({'diffusivity', 'kinematic viscosity'})
[42]:
u.get_physical_type("number density")
[42]:
PhysicalType('number density')

These physical type objects can be used for dimensional analysis.

[43]:
energy_density = (u.J * u.m**-3).physical_type
velocity = u.get_physical_type("velocity")
print(energy_density * velocity)
energy flux/irradiance

This page was generated by nbsphinx from docs/notebooks/getting_started/particles.ipynb.
Interactive online version: Binder badge.

Using PlasmaPy Particles

The plasmapy.particles subpackage contains functions to access basic particle data and classes to represent particles.

[1]:
from plasmapy.particles import (
    CustomParticle,
    DimensionlessParticle,
    Particle,
    ParticleList,
    atomic_number,
    charge_number,
    element_name,
    half_life,
    is_stable,
    molecule,
    particle_mass,
)
from plasmapy.particles.particle_class import valid_categories

Contents

  1. Particle properties

  2. Particle objects

  3. Custom particles

  4. Molecules

  5. Particle lists

  6. Dimensionless particles

  7. Nuclear reactions

Particle properties

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.

[2]:
atomic_number("Fe")
[2]:
26

We can provide a number that represents the atomic number.

[3]:
element_name(26)
[3]:
'iron'

We can also provide standard symbols or the names of particles.

[4]:
is_stable("e-")
[4]:
True
[5]:
charge_number("proton")
[5]:
1

The symbols for many particles can even be used directly, such as for an alpha particle. To create an “α” in a Jupyter notebook, type \alpha and press tab.

[6]:
particle_mass("α")
[6]:
$6.6446572 \times 10^{-27} \; \mathrm{kg}$

We can represent isotopes with the atomic symbol followed by a hyphen and the mass number. In this example, half_life returns the time in seconds as a Quantity from astropy.units.

[7]:
half_life("C-14")
[7]:
$1.8082505 \times 10^{11} \; \mathrm{s}$

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.

[8]:
charge_number("Fe-56 13+")
[8]:
13

Functions in plasmapy.particles are quite flexible in terms of string inputs representing particles. An input is particle-like if it can be transformed into a Particle.

[9]:
particle_mass("iron-56 +13")
[9]:
$9.2870305 \times 10^{-26} \; \mathrm{kg}$
[10]:
particle_mass("iron-56+++++++++++++")
[10]:
$9.2870305 \times 10^{-26} \; \mathrm{kg}$

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 to avoid ambiguity.

[11]:
particle_mass("Fe", Z=13, mass_numb=56)
[11]:
$9.2870305 \times 10^{-26} \; \mathrm{kg}$

Particle objects

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.

[12]:
proton = Particle("p+")
electron = Particle("electron")
iron56_nuclide = Particle("Fe", Z=26, mass_numb=56)

Particle properties can be accessed via attributes of the Particle class.

[13]:
proton.mass
[13]:
$1.6726219 \times 10^{-27} \; \mathrm{kg}$
[14]:
electron.charge
[14]:
$-1.6021766 \times 10^{-19} \; \mathrm{C}$
[15]:
electron.charge_number
[15]:
-1
[16]:
iron56_nuclide.binding_energy
[16]:
$7.8868678 \times 10^{-11} \; \mathrm{J}$
Antiparticles

We can get antiparticles of fundamental particles by using the antiparticle attribute of a Particle.

[17]:
electron.antiparticle
[17]:
Particle("e+")

We can also use the tilde operator on a Particle to get its antiparticle.

[18]:
~proton
[18]:
Particle("p-")
Ionization and recombination

The recombine and ionize methods of a Particle representing an ion or neutral atom will return a different Particle with fewer or more electrons.

[19]:
deuterium = Particle("D 0+")
deuterium.ionize()
[19]:
Particle("D 1+")

When provided with a number, these methods tell how many bound electrons to add or remove.

[20]:
alpha = Particle("alpha")
alpha.recombine(2)
[20]:
Particle("He-4 0+")

If the inplace keyword is set to True, then the Particle will be replaced with the new particle.

[21]:
argon = Particle("Ar 0+")
argon = argon.ionize()
print(argon)
Ar 1+

Custom particles

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. For that we can use CustomParticle.

[22]:
import astropy.constants as const
import astropy.units as u

custom_particle = CustomParticle(9.27e-26 * u.kg, 13.6 * const.e.si, symbol="Fe 13.6+")

Many of the attributes of CustomParticle are the same as in Particle.

[23]:
custom_particle.mass
[23]:
$9.27 \times 10^{-26} \; \mathrm{kg}$
[24]:
custom_particle.charge
[24]:
$2.1789602 \times 10^{-18} \; \mathrm{C}$
[25]:
custom_particle.symbol
[25]:
'Fe 13.6+'

If we do not include one of the physical quantities, it gets set to nan (not a number) in the appropriate units.

[26]:
CustomParticle(9.27e-26 * u.kg).charge
[26]:
${\rm NaN} \; \mathrm{C}$

CustomParticle objects are not yet able to be used by many of the functions in plasmapy.formulary, but are expected to become compatible with them in a future release of PlasmaPy. Similarly, CustomParticle objects are not able to be used by the functions in plasmapy.particles that require that the particle be real.

Particle lists

The ParticleList class is a container for Particle and CustomParticle objects.

[27]:
iron_ions = ParticleList(["Fe 12+", "Fe 13+", "Fe 14+"])

By using a ParticleList, we can access the properties of multiple particles at once.

[28]:
iron_ions.mass
[28]:
$[9.2721873 \times 10^{-26},~9.2720962 \times 10^{-26},~9.2720051 \times 10^{-26}] \; \mathrm{kg}$
[29]:
iron_ions.charge
[29]:
$[1.922612 \times 10^{-18},~2.0828296 \times 10^{-18},~2.2430473 \times 10^{-18}] \; \mathrm{C}$
[30]:
iron_ions.symbols
[30]:
['Fe 12+', 'Fe 13+', 'Fe 14+']

We can also create a ParticleList by adding Particle and/or CustomParticle objects together.

[31]:
proton + electron + custom_particle
[31]:
ParticleList(['p+', 'e-', 'Fe 13.6+'])

Molecules

We can use molecule to create a CustomParticle based on a chemical formula. The first argument to molecule is a string that represents a chemical formula, except that the subscript numbers are not given as subscripts. For example, water is "H2O".

[32]:
water = molecule("H2O")
water.symbol
[32]:
'H2O'

The properties of the molecule are found automatically.

[33]:
water.mass
[33]:
$2.9914611 \times 10^{-26} \; \mathrm{kg}$
[34]:
acetic_acid_anion = molecule("CH3COOH 1-")
acetic_acid_anion.charge
[34]:
$-1.6021766 \times 10^{-19} \; \mathrm{C}$

Particle categorization

The categories attribute of a Particle provides a set of the categories that the Particle belongs to.

[35]:
muon = Particle("muon")
muon.categories
[35]:
{'charged', 'fermion', 'lepton', 'matter', 'unstable'}

The is_category() method lets us determine if a Particle belongs to one or more categories.

[36]:
muon.is_category("lepton")
[36]:
True

If we need to be more specific, we can use the require keyword for categories that a Particle must belong to, the exclude keyword for categories that the Particle cannot belong to, and the any_of keyword for categories of which a Particle needs to belong to at least one.

[37]:
electron.is_category(require="lepton", exclude="baryon", any_of={"boson", "fermion"})
[37]:
True

All valid particle categories are included in valid_categories.

[38]:
print(valid_categories)
{'noble gas', 'custom', 'antimatter', 'neutrino', 'metal', 'uncharged', 'halogen', 'charged', 'neutron', 'positron', 'boson', 'stable', 'nonmetal', 'actinide', 'metalloid', 'lanthanide', 'alkali metal', 'proton', 'electron', 'transition metal', 'antineutrino', 'post-transition metal', 'ion', 'antibaryon', 'alkaline earth metal', 'element', 'antilepton', 'unstable', 'isotope', 'lepton', 'baryon', 'fermion', 'matter'}

The is_category() method of ParticleList returns a list of boolean values which correspond to whether or not each particle in the list meets the categorization criteria.

[39]:
particles = ParticleList(["e-", "p+", "n"])
particles.is_category(require="lepton")
[39]:
[True, False, False]

Dimensionless particles

When we need a dimensionless representation of a particle, we can use the DimensionlessParticle class.

[40]:
dimensionless_particle = DimensionlessParticle(mass=0.000545, charge=-1)

The properties of dimensionless particles may be accessed by its attributes.

[41]:
dimensionless_particle.mass
[41]:
0.000545
[42]:
dimensionless_particle.charge
[42]:
-1.0

Because a DimensionlessParticle does not uniquely describe a physical particle, it cannot be contained in a ParticleList.

Nuclear reactions

We can use plasmapy.particles to calculate the energy of a nuclear reaction using the > operator.

[43]:
deuteron = Particle("D+")
triton = Particle("T+")
alpha = Particle("α")
neutron = Particle("n")
[44]:
energy = deuteron + triton > alpha + neutron
[45]:
energy.to("MeV")
[45]:
$17.589253 \; \mathrm{MeV}$

If the nuclear reaction is invalid, then an exception is raised that states the reason why.

[47]:
deuteron + triton > alpha + 3 * neutron
ParticleError: The baryon number is not conserved for reactants = [Particle("D 1+"), Particle("T 1+")] and products = [Particle("He-4 2+"), Particle("n"), Particle("n"), Particle("n")].

Examples

Here we catalog all the example Jupyter notebooks that have been created for the various functionality contained in plasmapy.

Getting started

Analyses & Diagnostics

This page was generated by nbsphinx from docs/notebooks/analysis/fit_functions.ipynb.
Interactive online version: Binder badge.

Fit Functions

Fit functions are a set of callable classes designed to aid in fitting analytical functions to data. A fit function class combines the following functionality:

  1. An analytical function that is callable with given parameters or fitted parameters.

  2. Curve fitting functionality (usually SciPy’s curve_fit() or linregress()), which stores the fit statistics and parameters into the class. This makes the function easily callable with the fitted parameters.

  3. Error propagation calculations.

  4. A root solver that returns either the known analytical solutions or uses SciPy’s fsolve() to calculate the roots.

[1]:
%matplotlib inline

import matplotlib.pyplot as plt
import numpy as np

from plasmapy.analysis import fit_functions as ffuncs

plt.rcParams["figure.figsize"] = [10.5, 0.56 * 10.5]
Contents:
  1. Fit function basics

  2. Fitting to data

    1. Getting fit results

    2. Fit function is callable

    3. Plotting results

    4. Root solving

Fit function basics

There is an ever expanding collection of fit functions, but this notebook will use ExponentialPlusLinear as an example.

A fit function class has no required arguments at time of instantiation.

[2]:
# basic instantiation
explin = ffuncs.ExponentialPlusLinear()

# fit parameters are not set yet
(explin.params, explin.param_errors)
[2]:
(None, None)

Each fit parameter is given a name.

[3]:
explin.param_names
[3]:
('a', 'alpha', 'm', 'b')

These names are used throughout the fit function’s documentation, as well as in its __repr__, __str__, and latex_str methods.

[4]:
(explin, explin.__str__(), explin.latex_str)
[4]:
(f(x) = a exp(alpha x) + m x + b <class 'plasmapy.analysis.fit_functions.ExponentialPlusLinear'>,
 'f(x) = a exp(alpha x) + m x + b',
 'a \\, \\exp(\\alpha x) + m x + b')
Fitting to data

Fit functions provide the curve_fit() method to fit the analytical function to a set of \((x, y)\) data. This is typically done with SciPy’s curve_fit() function, but fitting is done with SciPy’s linregress() for the Linear fit function.

Let’s generate some noisy data to fit to…

[5]:
params = (5.0, 0.1, -0.5, -8.0)  # (a, alpha, m, b)
xdata = np.linspace(-20, 15, num=100)
ydata = explin.func(xdata, *params) + np.random.normal(0.0, 0.6, xdata.size)

plt.plot(xdata, ydata)
plt.xlabel("X", fontsize=14)
plt.ylabel("Y", fontsize=14)
[5]:
Text(0, 0.5, 'Y')
_images/notebooks_analysis_fit_functions_11_1.png

The fit function curve_fit() shares the same signature as SciPy’s curve_fit(), so any **kwargs will be passed on. By default, only the \((x, y)\) values are needed.

[6]:
explin.curve_fit(xdata, ydata)
Getting fit results

After fitting, the fitted parameters, uncertainties, and coefficient of determination, or \(r^2\), values can be retrieved through their respective properties, params, parame_errors, and rsq.

[7]:
(explin.params, explin.params.a, explin.params.alpha)
[7]:
(FitParamTuple(a=6.20801799471848, alpha=0.09027784967715434, m=-0.5468969241234746, b=-9.238021854208515),
 6.20801799471848,
 0.09027784967715434)
[8]:
(explin.param_errors, explin.param_errors.a, explin.param_errors.alpha)
[8]:
(FitParamTuple(a=1.3103530567155512, alpha=0.008659435937846585, m=0.05336031148796925, b=1.3285417225052036),
 1.3103530567155512,
 0.008659435937846585)
[9]:
explin.rsq
[9]:
0.951060540380906
Fit function is callable

Now that parameters are set, the fit function is callable.

[10]:
explin(0)
[10]:
-3.0300038594900354

Associated errors can also be generated.

[11]:
y, y_err = explin(np.linspace(-1, 1, num=10), reterr=True)
(y, y_err)
[11]:
(array([-3.0189999 , -3.02559072, -3.02985231, -3.03173749, -3.03119809,
        -3.02818498, -3.02264804, -3.01453611, -3.00379702, -2.99037754]),
 array([1.78987881, 1.80563829, 1.82204768, 1.83912362, 1.85688316,
        1.87534375, 1.89452323, 1.9144399 , 1.93511245, 1.95656003]))

Known uncertainties in \(x\) can be specified too.

[12]:
y, y_err = explin(np.linspace(-1, 1, num=10), reterr=True, x_err=0.1)
(y, y_err)
[12]:
(array([-3.0189999 , -3.02559072, -3.02985231, -3.03173749, -3.03119809,
        -3.02818498, -3.02264804, -3.01453611, -3.00379702, -2.99037754]),
 array([1.7898822 , 1.80563995, 1.82204821, 1.83912365, 1.85688333,
        1.87534473, 1.89452572, 1.91444459, 1.93512008, 1.95657133]))
Plotting results
[13]:
# plot original data
plt.plot(xdata, ydata, marker="o", linestyle=" ", label="Data")
ax = plt.gca()
ax.set_xlabel("X", fontsize=14)
ax.set_ylabel("Y", fontsize=14)

ax.axhline(0.0, color="r", linestyle="--")

# plot fitted curve + error
yfit, yfit_err = explin(xdata, reterr=True)
ax.plot(xdata, yfit, color="orange", label="Fit")
ax.fill_between(
    xdata,
    yfit + yfit_err,
    yfit - yfit_err,
    color="orange",
    alpha=0.12,
    zorder=0,
    label="Fit Error",
)

# plot annotations
plt.legend(fontsize=14, loc="upper left")

txt = f"$f(x) = {explin.latex_str}$\n$r^2 = {explin.rsq:.3f}$\n"
for name, param, err in zip(explin.param_names, explin.params, explin.param_errors, strict=False):
    txt += f"{name} = {param:.3f} $\\pm$ {err:.3f}\n"
txt_loc = [-13.0, ax.get_ylim()[1]]
txt_loc = ax.transAxes.inverted().transform(ax.transData.transform(txt_loc))
txt_loc[0] -= 0.02
txt_loc[1] -= 0.05
ax.text(
    txt_loc[0],
    txt_loc[1],
    txt,
    fontsize="large",
    transform=ax.transAxes,
    va="top",
    linespacing=1.5,
)
[13]:
Text(0.20727272727272733, 0.95, '$f(x) = a \\, \\exp(\\alpha x) + m x + b$\n$r^2 = 0.951$\na = 6.208 $\\pm$ 1.310\nalpha = 0.090 $\\pm$ 0.009\nm = -0.547 $\\pm$ 0.053\nb = -9.238 $\\pm$ 1.329\n')
_images/notebooks_analysis_fit_functions_25_1.png
Root solving

An exponential plus a linear offset has no analytical solutions for its roots, except for a few specific cases. To get around this, ExponentialPlusLinear().root_solve() uses SciPy’s fsolve() to calculate it’s roots. If a fit function has analytical solutions to its roots (e.g. Linear().root_solve()), then the method is overridden with the known solution.

[14]:
root, err = explin.root_solve(-15.0)
(root, err)
[14]:
(-13.551959234183208, nan)

Let’s use Linear().root_solve() as an example for a known solution.

[15]:
lin = ffuncs.Linear(params=(1.0, -5.0), param_errors=(0.1, 0.1))
root, err = lin.root_solve()
(root, err)
[15]:
(5.0, 0.5099019513592785)

This page was generated by nbsphinx from docs/notebooks/analysis/nullpoint.ipynb.
Interactive online version: Binder badge.

Null Point Finder

The null point finder is functionality that is designed to find and analyze 3D magnetic null point locations and structures using a trilinear interpolation method as described in Haynes et al. (2007).

This notebook covers how the null point finder utilizes trilinear interpolation in order to locate and classify the structures of magnetic null points.

[1]:
%matplotlib inline

import matplotlib.pyplot as plt
import numpy as np

from plasmapy.analysis import nullpoint

plt.rcParams["figure.figsize"] = [10.5, 0.56 * 10.5]
Contents:
  1. How the null point finder works

    1. Locating a null point

    2. Classifying a null point

  2. Running through examples

    1. Uniform regular grid with a model function

    2. Arbitrary regular grid

How the null point finder works

Null point finder provides two functions that can locate and classify null points in a magnetic field. The first function is uniform_null_point_find() and the second one is null_point_find(). As the names suggest, uniform_null_point_find() is used to locate and classify the magnetic null points of magnetic field located within a regular grid with uniform spacing in each dimension. It requires the user to provide the spacing between grid points in each of the three dimensions in addition to the minimum and maximum coordinate in each dimension. Moreover, it requires the user to provide a function which generates the vector values at a given point. uniform_null_point_find() is useful for when the user knows of such a function that accurately models the magnetic field vector at each point in space. On the other hand, null_point_find() is used when the user does not have an adequate modeling function. It also does not require the grid to have uniform spacing in each dimension. Instead, it will ask the user for three arrays (one for each dimension) of coordinates that determines the desired custom spacing in each of the three dimensions, and then constructs the resulting grid on its own. Furthermore, it requires the user to input all of the three components of magnetic field values, each as a 3D array with the same size as the grid, with each entry representing the strength of that component of the magnetic field for the corresponding grid coordinate. Finally, both functions take in as arguments two convergence thresholds that we will discuss later, for locating the null points.

Locating a null point

Locating a null point is done via the trilinear analysis method discussed in the paper by Haynes et al. (2007). There are three steps that goes into locating the null points of a given regular grid.

  1. Reduction: First, every grid cell is checked for a simple condition so that we can rule out cells that cannot contain a null point.

  2. Trilinear Analysis: Assuming a trilinear field, the cells that have passed the reduction check are then analyzed, so that that we can be sure if they do contain a null point.

  3. Locating the null point: The cell that contains a null point is isolated, and the location of the null point is estimated using the iterative Newton-Raphson method with an initial random guess.

Classifying a null point

Classification is done by analyzing the Jacobian matrix calculated at the location of null point. The full method is explained in the paper by Parnell et al. (1996).

Running through examples

We will now run through an example for each of the two null point finding functions to see how to properly utilize the null point finder.

Uniform regular grid with a model function

First, let’s define our modeling function for the magnetic field.

[2]:
def magnetic_field(x, y, z):
    return [(y - 1.5) * (z - 1.5), (x - 1.5) * (z - 1.5), (x - 1.5) * (y - 1.5)]

The vector field defined above has a total of eight null points, located at \((\pm 1.5, \pm 1.5, \pm 1.5)\). Now we will use uniform_null_point_find() to locate the null point with all positive components.

[3]:
nullpoint_args = {
    "x_range": [1, 2],
    "y_range": [1, 2],
    "z_range": [1, 2],
    "precision": [0.03, 0.03, 0.03],
    "func": magnetic_field,
}
npoints = nullpoint.uniform_null_point_find(**nullpoint_args)
print(npoints[0].loc)
print(npoints[0].classification)
/home/docs/checkouts/readthedocs.org/user_builds/plasmapy/envs/latest/lib/python3.12/site-packages/plasmapy/analysis/nullpoint.py:1309: MultipleNullPointWarning: Multiple null points suspected. Trilinear method may not work as intended.
  warnings.warn(
/home/docs/checkouts/readthedocs.org/user_builds/plasmapy/envs/latest/lib/python3.12/site-packages/plasmapy/analysis/nullpoint.py:1329: UserWarning: Max Iterations Reached without Convergence
  warnings.warn("Max Iterations Reached without Convergence")
[[1.01515152]
 [1.5       ]
 [1.5       ]]
Continuous potential X-points

As we can see uniform_null_point_find() correctly identifies the location of the null point in addition to its type, which is a proper radial null.

Arbitrary regular grid

Now we will run through an example where the field components have to be directly provided by the user since an adequate modeling function is not given.

[4]:
nullpoint2_args = {
    "x_arr": [5, 6],
    "y_arr": [5, 6],
    "z_arr": [5, 6],
    "u_arr": np.array([[[-0.5, -0.5], [0.5, 0.5]], [[-0.5, -0.5], [0.5, 0.5]]]),
    "v_arr": np.array([[[-0.5, 0.5], [-0.5, 0.5]], [[-0.5, 0.5], [-0.5, 0.5]]]),
    "w_arr": np.array([[[-0.5, -0.5], [-0.5, -0.5]], [[0.5, 0.5], [0.5, 0.5]]]),
}
npoints2 = nullpoint.null_point_find(**nullpoint2_args)
print(npoints2[0].loc)
print(npoints2[0].classification)
[[5.5]
 [5.5]
 [5.5]]
Spiral null

As we can see the magnetic field provided above has a spiral null point located at \((5.5,5.5,5.5)\).

This page was generated by nbsphinx from docs/notebooks/analysis/swept_langmuir/find_floating_potential.ipynb.
Interactive online version: Binder badge.

Swept Langmuir Analysis: Floating Potential

This notebook covers the use of the find_floating_potential() function and how it is used to determine the floating potential from a swept Langmuir trace.

The floating potential, \(V_f\), is defined as the probe bias voltage at which there is no net collected current, \(I=0\). This occurs because the floating potential slows the collected electrons and accelerates the collected ions to a point where the electron- and ion-currents balance each other out.

[1]:
%matplotlib inline

from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np

from plasmapy.analysis import swept_langmuir as sla

plt.rcParams["figure.figsize"] = [10.5, 0.56 * 10.5]

np.set_printoptions(precision=4, threshold=16)
Contents:
  1. How find_floating_potential() works

    1. Notes about usage

    2. Knobs to turn

  2. Calculate the Floating Potential

    1. Interpreting results

    2. Plotting results

How find_floating_potential() works
  1. The passed current array is scanned for points that equal zero and point-pairs that straddle where the current, \(I\), equals zero. This forms an collection of “crossing-points.”

  2. The crossing-points are then grouped into “crossing-islands” based on the threshold keyword.

    • A new island is formed when a successive crossing-point is more (index) steps away from the previous crossing-point than defined by threshold. For example, if threshold=4 then a new island is formed if a crossing-point candidate is more than 4 steps away from the previous candidate.

    • If multiple crossing-islands are identified, then the function will compare the total span of all crossing-islands to min_points. If the span is greater than min_points, then the function is incapable of identifying \(V_f\) and will return numpy.nan values; otherwise, the span will form one larger crossing-island.

  3. To calculate the floating potential…

    • If the number of points that make up the crossing-island is less than min_points, then each side of the “crossing-island” is equally padded with the nearest neighbor points until min_points is satisfied.

    • If fit_type="linear", then a scipy.stats.linregress fit is applied to the points that make up the crossing-island.

    • If fit_type="exponential", then a scipy.optimize.curve_fit fit is applied to the points that make up the crossing-island.

Notes about usage
  • The function provides no signal processing. If needed, the user must smooth, sort, crop, or process the arrays before passing them to the function.

  • The function requires the voltage array to be monotonically increasing.

  • If the total range spanned by all crossing-islands is less than or equal to min_points, then threshold is ignored and all crossing-islands are grouped into one island.

Knobs to turn
  • fit_type

    There are two types of curves that can be fitted to the identified crossing point data: "linear" and "exponential". The former will fit a line to the data, whereas, the latter will fit an exponential curve with an offset. The default curve is "exponential" since swept Langmuir data is not typically linear as it passes through \(I=0\).

  • min_points

    This variable specifies the minimum number of points that will be used in the curve fitting. As mentioned above, the crossing-islands are identified and then padded until min_points is satisfied. Usage:

    • min_points = None (DEFAULT): min_points is chosen to be the larger of 5 or factor * array_size, where factor = 0.1 for "linear" and 0.2 for "exponential".

    • min_points = numpy.inf: The entire array is fitted.

    • min_points is an integer >= 1: min_points is the minimum number of points to be used.

    • 0 < min_points < 1: The minimum number of points is taken as min_points * array_size.

  • threshold

    The max allowed index distance between crossing-points before a new crossing-island is formed.

Calculate the Floating Potential

Below we’ll compute the floating potential using the default fitting behavior (fit_type="exponential") and a linear fit (fit_type="linear").

[2]:
# load data
filename = "Beckers2017_noisy.npy"
filepath = (Path.cwd() / ".." / ".." / "langmuir_samples" / filename).resolve()
voltage, current = np.load(filepath)

# voltage array needs to be monotonically increasing/decreasing
isort = np.argsort(voltage)
voltage = voltage[isort]
current = current[isort]

# get default fit results (exponential fit)
results = sla.find_floating_potential(voltage, current, min_points=0.3)

# get linear fit results
results_lin = sla.find_floating_potential(voltage, current, fit_type="linear")
Interpreting results

The find_floating_potential() function returns a 2 element tuple, where the first element is the calculated floating potential \(V_f\) and the second element is a named tuple VFExtras containing additional parameters resulting from the calculation.

  • results[0] is the determined floating potential (same units as the pass voltage array)

[3]:
vf = results[0]
vf
[3]:
-5.665730698520963
  • results[1] is an instance of VFExtras and contains additional information from the calculation

[4]:
extras = results[1]
extras
[4]:
VFExtras(vf_err=0.45174119664601947, rsq=0.9564766142727993, fitted_func=f(x) = a exp(alpha x) + b <class 'plasmapy.analysis.fit_functions.ExponentialPlusOffset'>, islands=[slice(192, 195, None), slice(201, 210, None)], fitted_indices=slice(155, 247, None))
  • extras[0] = extras.vf_err = the associated uncertainty in the \(V_f\) calculation (same units as vf)

[5]:
(extras[0], extras.vf_err)
[5]:
(0.45174119664601947, 0.45174119664601947)
  • extras[1] = extras.rsq = the coefficient of determination (r-squared) value of the fit

[6]:
(extras[1], extras.rsq)
[6]:
(0.9564766142727993, 0.9564766142727993)
  • extras[2] = extras.fitted_func = the resulting fitted function

    • extras.fitted_func is a callable representation of the fitted function I = extras.fitted_func(V).

    • extras.fitted_func is an instance of a sub-class of AbstractFitFunction. (FitFunction classes)

    • Since extras.fitted_func is a class instance, there are many other attributes available. For example,

      • extras.fitted_func.params is a named tuple of the fitted parameters

      • extras.fitted_func.param_errors is a named tuple of the fitted parameter errors

      • extras.fitted_func.root_solve() finds the roots of the fitted function. This is how \(V_f\) is calculated.

[7]:
(
    extras[2],
    extras.fitted_func,
    extras.fitted_func.params,
    extras.fitted_func.params.a,
    extras.fitted_func.param_errors,
    extras.fitted_func.param_errors.a,
    extras.fitted_func(vf),
)
[7]:
(f(x) = a exp(alpha x) + b <class 'plasmapy.analysis.fit_functions.ExponentialPlusOffset'>,
 f(x) = a exp(alpha x) + b <class 'plasmapy.analysis.fit_functions.ExponentialPlusOffset'>,
 FitParamTuple(a=0.010275375458525872, alpha=0.338277953104458, b=-0.0015115846190350466),
 0.010275375458525872,
 FitParamTuple(a=0.00031439926674768734, alpha=0.02163110937712501, b=0.00012999596409070833),
 0.00031439926674768734,
 0.0)
  • extras[3] = extras.islands = a list of slice objects representing all the identified crossing-islands

[8]:
(
    extras[3],
    extras.islands,
    voltage[extras.islands[0]],
)
[8]:
([slice(192, 195, None), slice(201, 210, None)],
 [slice(192, 195, None), slice(201, 210, None)],
 array([-7.496 , -7.2238, -7.1512]))
  • extras[4] = extras.fitted_indices = a slice object representing the indices used in the fit

[9]:
(
    extras[4],
    extras.fitted_indices,
    voltage[extras.fitted_indices],
)
[9]:
(slice(155, 247, None),
 slice(155, 247, None),
 array([-11.6594, -11.6488, -11.5358, ...,  -1.251 ,  -0.9812,  -0.8504]))
Plotting results
[10]:
figwidth, figheight = plt.rcParams["figure.figsize"]
figheight = 2.0 * figheight
fig, axs = plt.subplots(3, 1, figsize=[figwidth, figheight])

# plot original data
axs[0].set_xlabel("Bias Voltage (V)", fontsize=12)
axs[0].set_ylabel("Current (A)", fontsize=12)

axs[0].plot(voltage, current, zorder=10, label="Sweep Data")
axs[0].axhline(0.0, color="r", linestyle="--", label="I = 0")
axs[0].legend(fontsize=12)

# zoom on fit
for ii, label, rtn in zip([1, 2], ["Exponential", "Linear"], [results, results_lin], strict=False):
    vf = rtn[0]
    extras = rtn[1]

    # calc island points
    isl_pts = np.array([], dtype=np.int64)
    for isl in extras.islands:
        isl_pts = np.concatenate((isl_pts, np.r_[isl]))

    # calc xrange for plot
    xlim = [voltage[extras.fitted_indices].min(), voltage[extras.fitted_indices].max()]
    vpad = 0.25 * (xlim[1] - xlim[0])
    xlim = [xlim[0] - vpad, xlim[1] + vpad]

    # calc data points for fit curve
    mask1 = np.where(voltage >= xlim[0], True, False)
    mask2 = np.where(voltage <= xlim[1], True, False)
    mask = np.logical_and(mask1, mask2)
    vfit = np.linspace(xlim[0], xlim[1], 201, endpoint=True)
    ifit, ifit_err = extras.fitted_func(vfit, reterr=True)

    axs[ii].set_xlabel("Bias Voltage (V)", fontsize=12)
    axs[ii].set_ylabel("Current (A)", fontsize=12)
    axs[ii].set_xlim(xlim)

    axs[ii].plot(
        voltage[mask],
        current[mask],
        marker="o",
        zorder=10,
        label="Sweep Data",
    )
    axs[ii].scatter(
        voltage[extras.fitted_indices],
        current[extras.fitted_indices],
        linewidth=2,
        s=6**2,
        facecolors="deepskyblue",
        edgecolors="deepskyblue",
        zorder=11,
        label="Points for Fit",
    )
    axs[ii].scatter(
        voltage[isl_pts],
        current[isl_pts],
        linewidth=2,
        s=8**2,
        facecolors="deepskyblue",
        edgecolors="black",
        zorder=12,
        label="Island Points",
    )
    axs[ii].autoscale(False)
    axs[ii].plot(vfit, ifit, color="orange", zorder=13, label=label + " Fit")
    axs[ii].fill_between(
        vfit,
        ifit + ifit_err,
        ifit - ifit_err,
        color="orange",
        alpha=0.12,
        zorder=0,
        label="Fit Error",
    )
    axs[ii].axhline(0.0, color="r", linestyle="--")
    axs[ii].fill_between(
        [vf - extras.vf_err, vf + extras.vf_err],
        axs[1].get_ylim()[0],
        axs[1].get_ylim()[1],
        color="grey",
        alpha=0.1,
    )
    axs[ii].axvline(vf, color="grey")
    axs[ii].legend(fontsize=12)

    # add text
    rsq = extras.rsq
    txt = f"$V_f = {vf:.2f} \\pm {extras.vf_err:.2f}$ V\n"
    txt += f"$r^2 = {rsq:.3f}$"
    txt_loc = [vf, axs[ii].get_ylim()[1]]
    txt_loc = axs[ii].transData.transform(txt_loc)
    txt_loc = axs[ii].transAxes.inverted().transform(txt_loc)
    txt_loc[0] -= 0.02
    txt_loc[1] -= 0.26
    axs[ii].text(
        txt_loc[0],
        txt_loc[1],
        txt,
        fontsize="large",
        transform=axs[ii].transAxes,
        ha="right",
    )
_images/notebooks_analysis_swept_langmuir_find_floating_potential_23_0.png

This page was generated by nbsphinx from docs/notebooks/diagnostics/charged_particle_radiography_film_stacks.ipynb.
Interactive online version: Binder badge.

Synthetic Charged Particle Radiographs with Multiple Detector Layers

Charged particle radiographs are often recorded on detector stacks with multiple layers (often either radiochromic film or CR39 plastic). Since charged particles deposit most of their energy at a depth related to their energy (the Bragg peak), each detector layer records a different energy band of particles. The energy bands recorded on each layer are further discriminated by including layers of filter material (eg. thin strips of aluminum) between the active layers. In order to analyze this data, it is necessary to calculate what range of particle energies are stopped in each detector layer.

The charged_particle_radiography module includes a tool for calculating the energy bands for a given stack of detectors. This calculation is a simple approximation (more accurate calculations can be made using Monte Carlo codes), but is sufficient in many cases.

[1]:
from tempfile import gettempdir

import astropy.units as u
import matplotlib.pyplot as plt
import numpy as np

from plasmapy.diagnostics.charged_particle_radiography.detector_stacks import (
    Layer,
    Stack,
)
from plasmapy.utils.data.downloader import Downloader

The charged_particle_radiography module represents a detector stack with a Stack object, which contains an ordered list of Layer objects. In this example, we will create a list of Layer objects, use them to initialize a Stack, then compute the energy bands.

Each Layer is defined by the following required properties: - thickness: an astropy Quantity defining the thickness of the layer. - active: a boolean flag which indicates whether the layer is active (detector) or inactive (eg. a filter). - energy_axis: An astropy Quantity array of energies. - stopping_power: An astropy Quantity array containing the product of stopping power and material density at each energy in energy_axis.

The stopping power in various materials for protons and electrons is tabulated by NIST in the PSTAR and ESTAR databases. The stopping powers are tabulated in units of MeV cm\(^2\) / g, so the product of stopping power and density has units of MeV/cm.

For this demonstration, we will load two stopping power tables, downloaded from PSTAR, for aluminum and a human tissue equivalent that is a good approximation for many radiochromic films.

[2]:
temp_dir = gettempdir()  # Get a temporary directory to save the files
dl = Downloader(directory=temp_dir, validate=False)
tissue_path = dl.get_file("NIST_PSTAR_tissue_equivalent.txt")
aluminum_path = dl.get_file("NIST_PSTAR_aluminum.txt")

These files can be directly downloaded from PSTAR, and the contents look like this

[3]:
with open(tissue_path) as f:
    for i in range(15):
        print(f.readline(), end="")
print("...")
PSTAR: Stopping Powers and Range Tables for Protons

A-150 TISSUE-EQUIVALENT PLASTIC

Kinetic   Total
Energy    Stp. Pow.
MeV       MeV cm2/g

1.000E-03 2.215E+02
1.500E-03 2.523E+02
2.000E-03 2.803E+02
2.500E-03 3.059E+02
3.000E-03 3.297E+02
4.000E-03 3.705E+02
5.000E-03 4.075E+02
...

Now we will load the energy and stopping power arrays from the files.

[4]:
arr = np.loadtxt(tissue_path, skiprows=8)
energy_axis = arr[:, 0] * u.MeV
tissue_density = 1.04 * u.g / u.cm**3
tissue_stopping_power = arr[:, 1] * u.MeV * u.cm**2 / u.g * tissue_density

arr = np.loadtxt(aluminum_path, skiprows=8)
aluminum_density = 2.7 * u.g / u.cm**3
aluminum_stopping_power = arr[:, 1] * u.MeV * u.cm**2 / u.g * aluminum_density

If we plot the stopping powers as a function of energy on a log-log scale, we see that they are non-linear, with much higher values at lower energies. This is why particles deposit most of their energy at a characteristic depth. As particles propagate into material, they lose energy slowly at first. But, as they loose energy, the effective stopping power increases expontentially.

[5]:
fig, ax = plt.subplots()
ax.plot(energy_axis.value, tissue_stopping_power, label="Tissue equivalent")
ax.plot(energy_axis.value, aluminum_stopping_power, label="Aluminum")
ax.set_xscale("log")
ax.set_yscale("log")
ax.set_xlabel("Energy (MeV)")
ax.set_ylabel("Stopping power (MeV/cm)")
ax.legend();
_images/notebooks_diagnostics_charged_particle_radiography_film_stacks_10_0.png

Before creating a Stack, we will start by creating a list that represents a common type of radiochromic film, HD-V2. HD-V2 consists of two layers: a 12 \(\mu\)m active layer deposited on a 97 \(\mu\)m substrate. The stopping powers of both layers are similar to human tissue (by design, HD-V2 is commonly used for medical research).

[6]:
HDV2 = [
    Layer(12 * u.um, energy_axis, tissue_stopping_power, active=True),
    Layer(97 * u.um, energy_axis, tissue_stopping_power, active=False),
]

We will now create a list of layers of HDV2 separated by aluminum filter layers of various thickness, then use this list to instantiate a Stack object

[7]:
layers = [
    Layer(100 * u.um, energy_axis, aluminum_stopping_power, active=False),
    *HDV2,
    Layer(100 * u.um, energy_axis, aluminum_stopping_power, active=False),
    *HDV2,
    Layer(100 * u.um, energy_axis, aluminum_stopping_power, active=False),
    *HDV2,
    Layer(100 * u.um, energy_axis, aluminum_stopping_power, active=False),
    *HDV2,
    Layer(500 * u.um, energy_axis, aluminum_stopping_power, active=False),
    *HDV2,
    Layer(500 * u.um, energy_axis, aluminum_stopping_power, active=False),
    *HDV2,
    Layer(1000 * u.um, energy_axis, aluminum_stopping_power, active=False),
    *HDV2,
    Layer(1000 * u.um, energy_axis, aluminum_stopping_power, active=False),
    *HDV2,
    Layer(2000 * u.um, energy_axis, aluminum_stopping_power, active=False),
    *HDV2,
    Layer(2000 * u.um, energy_axis, aluminum_stopping_power, active=False),
    *HDV2,
    Layer(2000 * u.um, energy_axis, aluminum_stopping_power, active=False),
]

stack = Stack(layers)

The number of layers, active layers, and the total thickness of the film stack are available as properties.

[8]:
print(f"Number of layers: {stack.num_layers}")
print(f"Number of active layers: {stack.num_active}")
print(f"Total stack thickness: {stack.thickness:.2f}")
Number of layers: 31
Number of active layers: 10
Total stack thickness: 0.01 m

The curves of deposited energy per layer for a given array of energies can then be calculated using the deposition_curves() method. The stopping power for a particle can change significantly within a layer, so the stopping power is numerically integrated within each layer. The spatial resolution of this integration can be set using the dx keyword. Setting the return_only_active keyword means that deposition curves will only be returned for the active layers (inactive layers are still included in the calculation).

[9]:
energies = np.arange(1, 60, 0.1) * u.MeV
deposition_curves = stack.deposition_curves(
    energies, dx=1 * u.um, return_only_active=True
);
[10]:
fig, ax = plt.subplots(figsize=(6, 4))
ax.set_title("Energy deposition curves")
ax.set_xlabel("Energy (MeV)")
ax.set_ylabel("Normalized energy deposition curve")
for layer in range(stack.num_active):
    ax.plot(energies, deposition_curves[layer, :], label=f"Layer {layer+1}")
ax.legend();
_images/notebooks_diagnostics_charged_particle_radiography_film_stacks_19_0.png

We can look at the deposition curves including all layers (active and inactive) to see where particles of a given energy are deposited.

[11]:
deposition_curves_all = stack.deposition_curves(energies, return_only_active=False)

fig, ax = plt.subplots(figsize=(6, 4))
ax.set_title("10 MeV Particles per layer")
ax.set_xlabel("Layer #")
ax.set_ylabel("Fraction of particles")
E0 = np.argmin(np.abs(10 * u.MeV - energies))
ax.plot(np.arange(stack.num_layers) + 1, deposition_curves_all[:, E0]);
_images/notebooks_diagnostics_charged_particle_radiography_film_stacks_21_0.png

The sharp peaks correspond to the aluminum filter layers, which stop more particles than the film layers.

The deposition curve is normalized such that each value represents the fraction of particles with a given energy that will be stopped in that layer. Since all particles are stopped before the last layer, the sum over all layers (including the inactive layers) for a given energy is always unity.

[12]:
integral = np.sum(deposition_curves_all, axis=0)
print(np.allclose(integral, 1))
True

We can quantify the range of initial particle energies primarily recorded on each film layer by calculating the full with at half maximum (FWHM) of the deposition curve for that layer. This calculation is done by the energy_bands() method, which returns an array of the \(\pm\) FWHM values for each film layer.

[13]:
ebands = stack.energy_bands([0.1, 60] * u.MeV, 0.1 * u.MeV, return_only_active=True)
for layer in range(stack.num_active):
    print(
        f"Layer {layer+1}: {ebands[layer,0].value:.1f}-{ebands[layer,1].value:.1f} MeV"
    )
Layer 1: 0.0-0.0 MeV
Layer 2: 0.0-0.0 MeV
Layer 3: 0.0-0.0 MeV
Layer 4: 0.0-0.0 MeV
Layer 5: 0.0-0.0 MeV
Layer 6: 0.0-0.0 MeV
Layer 7: 0.0-0.0 MeV
Layer 8: 0.0-0.0 MeV
Layer 9: 0.0-0.0 MeV
Layer 10: 0.0-0.0 MeV

From this information, we can conclude see that the radiograph recorded on Layer 5 is primarily created by protons bwetween 12.5 and 12.8 MeV. This energy information can then be used to inform analysis of the images.

This page was generated by nbsphinx from docs/notebooks/diagnostics/charged_particle_radiography_particle_tracing.ipynb.
Interactive online version: Binder badge.

Creating Synthetic Charged Particle Radiographs by Particle Tracing

Charged particle radiography is a diagnostic technique often used to interrogate the electric and magnetic fields inside high energy density plasmas. The area of interest is positioned between a bright source of charged particles (usually protons) and a detector plane. Electric and magnetic fields in the plasma deflect the particles, producing patterns on the detector. Since this represents a non-linear and line-integrated measurement of the fields, the interpretation of these “charged particle radiographs” is complicated.

The Tracker class within the charged_particle_radiography module creates a synthetic charged particle radiographs given a grid of electric and magnetic field (produced either by simulations or analytical models). After the geometry of the problem has been set up, a particle tracing algorithm is run, pushing the particles through the field region. After all of the particles have reached the detector plane, a synthetic radiograph is created by making a 2D histogram in that plane using the synthetic_radiograph function.

[1]:
import astropy.units as u
import matplotlib.pyplot as plt
import numpy as np

from plasmapy.diagnostics.charged_particle_radiography import (
    synthetic_radiography as cpr,
)
from plasmapy.plasma.grids import CartesianGrid

To illustrate the use of this package, we’ll first create an example CartesianGrid object and fill it with the analytical electric field produced by a sphere of Gaussian potential.

[2]:
# Create a Cartesian grid
L = 1 * u.mm
grid = CartesianGrid(-L, L, num=100)

# Create a spherical potential with a Gaussian radial distribution
radius = np.linalg.norm(grid.grid, axis=3)
arg = (radius / (L / 3)).to(u.dimensionless_unscaled)
potential = 2e5 * np.exp(-(arg**2)) * u.V

# Calculate E from the potential
Ex, Ey, Ez = np.gradient(potential, grid.dax0, grid.dax1, grid.dax2)
Ex = -np.where(radius < L / 2, Ex, 0)
Ey = -np.where(radius < L / 2, Ey, 0)
Ez = -np.where(radius < L / 2, Ez, 0)

# Add those quantities to the grid
grid.add_quantities(E_x=Ex, E_y=Ey, E_z=Ez, phi=potential)


# Plot the E-field
fig = plt.figure(figsize=(8, 8))
ax = fig.add_subplot(111, projection="3d")
ax.view_init(30, 30)

# skip some points to make the vector plot intelligible
s = tuple([slice(None, None, 6)] * 3)

ax.quiver(
    grid.pts0[s].to(u.mm).value,
    grid.pts1[s].to(u.mm).value,
    grid.pts2[s].to(u.mm).value,
    grid["E_x"][s],
    grid["E_y"][s],
    grid["E_z"][s],
    length=1e-6,
)

ax.set_xlabel("X (mm)")
ax.set_ylabel("Y (mm)")
ax.set_zlabel("Z (mm)")
ax.set_xlim(-1, 1)
ax.set_ylim(-1, 1)
ax.set_zlim(-1, 1)
ax.set_title("Gaussian Potential Electric Field");
_images/notebooks_diagnostics_charged_particle_radiography_particle_tracing_4_0.png

Prior to running the particle tracing algorithm, the simulation instance must be instantiated by providing some information about the setup, including the locations of the source and detector relative to the origin of the grid.

ecf74a5893024fb1a79dfeed31b1d35c

The source and detector coordinates are entered as a 3-tuple in one of three coordinate systems: Cartesian (\(x\), \(y\), \(z\)), spherical (\(r\), \(\theta\), \(\phi\)) or cylindrical (\(r\), \(\theta\), \(z\)). All values should be astropy.units.Quantity instances with units of either length or angle. The vector from the source to the detector should pass through the origin to maximize the number of particles that pass through the simulated fields.

[3]:
source = (0 * u.mm, -10 * u.mm, 0 * u.mm)
detector = (0 * u.mm, 100 * u.mm, 0 * u.mm)

sim = cpr.Tracker(grid, source, detector, verbose=True)
Source: [ 0.   -0.01  0.  ] m
Detector: [0.  0.1 0. ] m
Magnification: 11.0
/home/dominik/Projects/PlasmaPy/plasmapy/plasma/grids.py:192: RuntimeWarning: B_x is not specified for the provided grid.This quantity will be assumed to be zero.
  warnings.warn(
/home/dominik/Projects/PlasmaPy/plasmapy/plasma/grids.py:192: RuntimeWarning: B_y is not specified for the provided grid.This quantity will be assumed to be zero.
  warnings.warn(
/home/dominik/Projects/PlasmaPy/plasmapy/plasma/grids.py:192: RuntimeWarning: B_z is not specified for the provided grid.This quantity will be assumed to be zero.
  warnings.warn(

Note that, since the example grid did not include a B-field, the B-field is assumed to be zero and a warning is printed.

Next, a distribution of nparticles simulated particles of energy particle_energy is created using the create_particles() method. Setting the max_theta parameter eliminates particles with large angles (relative to the source-detector axis) which otherwise would likely not hit the detector. Particles with angles less than \(\theta_{max}\) but greater than \(\theta_{track}\) in the setup figure above will not cross the grid. These particles are retained, but are coasted directly to the detector plane instead of being pushed through the grid.

The particle keyword sets the type of the particles being tracked. The default particle is protons, which is set here explicitly to demonstrate the use of the keyword.

By default, the particle velocities are initialized with random angles (a Monte-Carlo approach) with a uniform flux per unit solid angle. However, particles can also be initialized in other ways by setting the distribution keyword.

[4]:
sim.create_particles(1e5, 3 * u.MeV, max_theta=np.pi / 15 * u.rad, particle="p")
Creating Particles

The particle tracking simulation is now ready to run. In brief, the steps of the simulation cycle are as follows:

  1. Particles that will never hit the field grid are ignored (until a later step, when they will be automatically advanced to the detector plane).

  2. Particles are advanced to the time when the first particle enters the simulation volume. This is done in one step to save computation time.

  3. While particles are on the grid, the particle pusher advances them each timestep by executing the following steps:

    1. The fields at each particle’s location are interpolated using the interpolators defined in the AbstractGrid subclasses.

    2. The simulation timestep is automatically (and adaptively) calculated based on the proton energy, grid resolution, and field amplitudes. This timestep can be clamped or overridden by setting the dt keyword in the run() function.

    3. An implementation of the Boris particle push algorithm is used to advance the velocities and positions of the particles in the interpolated fields.

  4. After all of the particles have left the grid, all particles are advanced to the detector plane (again saving time). Particles that are headed away from the detector plane at this point are deleted, as those particles will never be detected.

When the simulation runs, a progress meter will show the number of particles currently on the grid. This bar will start at zero, increase as particles enter the grid, then decrease as they leave it. When almost all particles have left the grid, the simulation ends.

[5]:
sim.run();
Particles on grid:   7%|██████▊                                                                                        4.0e+03/5.5e+04 particles
Run completed
Fraction of particles tracked: 55.4%
Fraction of tracked particles that entered the grid: 64.1%
Fraction of tracked particles deflected away from the detector plane: 0.0%

The following plot illustrates that, after the simulation has ended, all particles have been advanced to the detector plane.

[6]:
fig = plt.figure(figsize=(8, 8))
ax = fig.add_subplot(111, projection="3d")
ax.view_init(30, 150)
ax.set_xlabel("X (cm)")
ax.set_ylabel("Y (cm)")
ax.set_zlabel("Z (cm)")

# Plot the source-to-detector axis
ax.quiver(
    sim.source[0] * 100,
    sim.source[1] * 100,
    sim.source[2] * 100,
    sim.detector[0] * 100,
    sim.detector[1] * 100,
    sim.detector[2] * 100,
    color="black",
)

# Plot the simulation field grid volume
ax.scatter(0, 0, 0, color="green", marker="s", linewidth=5, label="Simulated Fields")

# Plot the proton source and detector plane locations
ax.scatter(
    sim.source[0] * 100,
    sim.source[1] * 100,
    sim.source[2] * 100,
    color="red",
    marker="*",
    linewidth=5,
    label="Source",
)

ax.scatter(
    sim.detector[0] * 100,
    sim.detector[1] * 100,
    sim.detector[2] * 100,
    color="blue",
    marker="*",
    linewidth=10,
    label="Detector",
)


# Plot the final proton positions of some (not all) of the protons
ind = slice(None, None, 200)
ax.scatter(
    sim.x[ind, 0] * 100,
    sim.x[ind, 1] * 100,
    sim.x[ind, 2] * 100,
    label="Protons",
)

ax.legend();
_images/notebooks_diagnostics_charged_particle_radiography_particle_tracing_12_0.png

A ‘synthetic proton radiograph’ can now be constructed by creating a 2D histogram of proton positions in the image plane. The synthetic_radiograph function takes one argument and two keywords:

  • ‘sim’ is the Tracker instance or an output dictionary created by the save_results() method that contains the final particle positions in the detector plane.

  • ‘size’ gives the locations of the lower left and upper right corners of the detector grid in image plane coordinates.

  • ‘bins’ is the number of histogram bins to be used in the horizontal and vertical directions. Using more bins creates a higher resolution image, but at the cost of more noise.

[7]:
# A function to reduce repetitive plotting


def plot_radiograph(hax, vax, intensity):
    fig, ax = plt.subplots(figsize=(8, 8))
    plot = ax.pcolormesh(
        hax.to(u.cm).value,
        vax.to(u.cm).value,
        intensity.T,
        cmap="Blues_r",
        shading="auto",
    )
    cb = fig.colorbar(plot)
    cb.ax.set_ylabel("Intensity")
    ax.set_aspect("equal")
    ax.set_xlabel("X (cm), Image plane")
    ax.set_ylabel("Z (cm), Image plane")
    ax.set_title("Synthetic Proton Radiograph")


size = np.array([[-1, 1], [-1, 1]]) * 1.5 * u.cm
bins = [200, 200]
hax, vax, intensity = cpr.synthetic_radiograph(sim, size=size, bins=bins)
plot_radiograph(hax, vax, intensity)
_images/notebooks_diagnostics_charged_particle_radiography_particle_tracing_14_0.png

As expected, the outward-pointing electric field in the sphere has deflected the protons out of the central region, leaving a dark shadow.

Kugland et al. 2012 and Bott et al. 2017 define the dimensionless “contrast parameter” that separates different regimes of proton radiography:

\begin{equation} \mu = \frac{l \alpha}{a} \end{equation} Where \(l\) is the distance from the source to the grid, \(a\) is the spatial scale of the scattering electromagnetic fields, and \(\alpha\) is the particle deflection angle. The value of \(\mu\) can fall in one of three regimes:

\begin{align} \mu &\ll 1 \rightarrow \text{ linear}\\ \mu &< \mu_c \rightarrow \text{ nonlinear injective}\\ \mu &> \mu_c \rightarrow \text{ caustic}\\ \end{align}

where \(\mu_c \sim 1\) is a characteristic value at which particle paths cross, leading to the formation of bright caustics. Correctly placing a radiograph in the correct regime is necessary to determine which analysis techniques can be applied to it.

The maximum deflection angle can be calculated after the simulation has run by comparing the initial and final velocity vectors of each particle

[8]:
max_deflection = sim.max_deflection
print(f"Maximum deflection α = {np.rad2deg(max_deflection):.2f}")
Maximum deflection α = 2.77 deg

The spatial scale of the field constructed in this example is \(\sim\) 1 mm, and \(l\) is approximately the distance from the source to the grid origin. Therefore, we can calculate the value of \(\mu\)

[9]:
a = 1 * u.mm
l = np.linalg.norm(sim.source * u.m).to(u.mm)
mu = l * max_deflection.value / a
print(f"a = {a}")
print(f"l = {l:.1f}")
print(f"μ = {mu:.2f}")
a = 1.0 mm
l = 10.0 mm
μ = 0.48

which places this example in the non-linear injective regime.

Options

For sake of comparison, here is the result achieved by setting distribution = 'uniform' in the create_particles() function.

[10]:
sim = cpr.Tracker(grid, source, detector, verbose=True)
sim.create_particles(
    1e5, 3 * u.MeV, max_theta=np.pi / 15 * u.rad, distribution="uniform"
)
sim.run()
size = np.array([[-1, 1], [-1, 1]]) * 1.5 * u.cm
bins = [200, 200]
hax, vax, intensity = cpr.synthetic_radiograph(sim, size=size, bins=bins)
plot_radiograph(hax, vax, intensity)
Source: [ 0.   -0.01  0.  ] m
Detector: [0.  0.1 0. ] m
Magnification: 11.0
Creating Particles
Particles on grid:   7%|██████▊                                                                                        6.2e+03/8.6e+04 particles
Run completed
Fraction of particles tracked: 85.9%
Fraction of tracked particles that entered the grid: 66.0%
Fraction of tracked particles deflected away from the detector plane: 0.0%
_images/notebooks_diagnostics_charged_particle_radiography_particle_tracing_22_1.png

Synthetic Radiographs with Custom Source Profiles

In real charged particle radiography experiments, the finite size and distribution of the particle source limits the resolution of the radiograph. Some realistic sources produce particles with a non-uniform angular distribution that then superimposes a large scale “source profile” on the radiograph. For these reasons, the Tracker particle tracing class allows users to specify their own initial particle positions and velocities. This example will demonstrate how to use this functionality to create a more realistic synthetic radiograph that includes the effects from a non-uniform, finite source profile.

[1]:
import warnings

import astropy.units as u
import matplotlib.pyplot as plt
import numpy as np

from plasmapy.diagnostics.charged_particle_radiography import (
    synthetic_radiography as cpr,
)
from plasmapy.formulary.mathematics import rot_a_to_b
from plasmapy.particles import Particle
from plasmapy.plasma.grids import CartesianGrid
Contents
  1. Creating Particles

    1. Creating the Initial Particle Velocities

    2. Creating the Initial Particle Positions

  2. Creating a Synthetic Radiograph

Creating Particles

In this example we will create a source of 1e5 protons with a 5% variance in energy, a non-uniform angular velocity distribution, and a finite size.

[2]:
nparticles = 1e5
particle = Particle("p+")

We will choose a setup in which the source-detector axis is parallel to the \(y\)-axis.

[3]:
# define location of source and detector plane
source = (0 * u.mm, -10 * u.mm, 0 * u.mm)
detector = (0 * u.mm, 100 * u.mm, 0 * u.mm)
Creating the Initial Particle Velocities

We will create the source distribution by utilizing the method of separation of variables,

\[f(v, \theta, \phi)=u(v)g(\theta)h(\phi)\]

and separately define the distribution component for each independent variable, \(u(v)\), \(g(\theta)\), and \(h(\phi)\). For geometric convenience, we will generate the velocity vector distribution around the \(z\)-axis and then rotate the final velocities to be parallel to the source-detector axis (in this case the \(y\)-axis).

50d5717d763b4abfa5a09a7c8b50cb01

First we will create the orientation angles polar (\(\theta\)) and azimuthal (\(\phi\)) for each particle. Generating \(\phi\) is simple: we will choose the azimuthal angles to just be uniformly distributed

[4]:
phi = np.random.uniform(high=2 * np.pi, size=int(nparticles))

However, choosing \(\theta\) is more complicated. Since the solid angle \(d\Omega = sin \theta d\theta d\phi\), if we draw a uniform distribution of \(\theta\) we will create a non-uniform distribution of particles in solid angle. This will create a sharp central peak on the detector plane.

[5]:
theta = np.random.uniform(high=np.pi / 2, size=int(nparticles))

fig, ax = plt.subplots(figsize=(6, 6))
theta_per_sa, bins = np.histogram(theta, bins=100, weights=1 / np.sin(theta))
ax.set_xlabel("$\\theta$ (rad)", fontsize=14)
ax.set_ylabel("N/N$_0$ per  d$\\Omega$", fontsize=14)
ax.plot(bins[1:], theta_per_sa / np.sum(theta_per_sa))
ax.set_title(f"N$_0$ = {nparticles:.0e}", fontsize=14)
ax.set_yscale("log")
ax.set_xlim(0, np.pi / 2)
ax.set_ylim(None, 1);
_images/notebooks_diagnostics_charged_particle_radiography_particle_tracing_custom_source_13_0.png

To create a uniform distribution in solid angle, we need to draw values of \(\theta\) with a probability distribution weighted by \(\sin \theta\). This can be done using the np.random.choice() function, which draws size elements from a distribution arg with a probability distribution prob. Setting the replace keyword allows the same arguments to be drawn multiple times.

[6]:
arg = np.linspace(0, np.pi / 2, num=int(1e5))
prob = np.sin(arg)
prob *= 1 / np.sum(prob)
theta = np.random.choice(arg, size=int(nparticles), replace=True, p=prob)

fig, ax = plt.subplots(figsize=(6, 6))
theta_per_sa, bins = np.histogram(theta, bins=100, weights=1 / np.sin(theta))
ax.plot(bins[1:], theta_per_sa / np.sum(theta_per_sa))
ax.set_xlabel("$\\theta$ (rad)", fontsize=14)
ax.set_ylabel("N/N$_0$ per  d$\\Omega$", fontsize=14)
ax.set_title(f"N$_0$ = {nparticles:.0e}", fontsize=14)
ax.set_yscale("log")
ax.set_xlim(0, np.pi / 2)
ax.set_ylim(None, 0.1);
_images/notebooks_diagnostics_charged_particle_radiography_particle_tracing_custom_source_15_0.png

Now that we have a \(\theta\) distribution that is uniform in solid angle, we can perturb it by adding additional factors to the probability distribution used in np.random.choice(). For this case, let’s create a Gaussian distribution in solid angle.

Since particles moving at large angles will not be seen in the synthetic radiograph, we will set an upper bound \(\theta_{max}\) on the argument here. This is equivalent to setting the max_theta keyword in create_particles()

[7]:
arg = np.linspace(0, np.pi / 8, num=int(1e5))
prob = np.sin(arg) * np.exp(-(arg**2) / 0.1**2)
prob *= 1 / np.sum(prob)
theta = np.random.choice(arg, size=int(nparticles), replace=True, p=prob)

fig, ax = plt.subplots(figsize=(6, 6))
theta_per_sa, bins = np.histogram(theta, bins=100, weights=1 / np.sin(theta))
ax.plot(bins[1:], theta_per_sa / np.sum(theta_per_sa))
ax.set_title(f"N$_0$ = {nparticles:.0e}", fontsize=14)
ax.set_xlabel("$\\theta$ (rad)", fontsize=14)
ax.set_ylabel("N/N$_0$ per  d$\\Omega$", fontsize=14)
ax.set_yscale("log")
ax.set_xlim(0, np.pi / 2)
ax.set_ylim(None, 1);
_images/notebooks_diagnostics_charged_particle_radiography_particle_tracing_custom_source_17_0.png

Now that the angular distributions are done, we will determine the energy (speed) for each particle. For this example, we will assume that the particle energy distribution is not a function of angle. We will create a Gaussian distribution of speeds with ~5% variance centered on a particle energy of 15 MeV.

[8]:
v_cent = np.sqrt(2 * 15 * u.MeV / particle.mass).to(u.m / u.s).value
v0 = np.random.normal(loc=v_cent, scale=1e6, size=int(nparticles))
v0 *= u.m / u.s

fig, ax = plt.subplots(figsize=(6, 6))
v_per_bin, bins = np.histogram(v0.si.value, bins=100)
ax.plot(bins[1:], v_per_bin / np.sum(v_per_bin))
ax.set_title(f"N$_0$ = {nparticles:.0e}", fontsize=14)
ax.set_xlabel("v0 (m/s)", fontsize=14)
ax.set_ylabel("N/N$_0$", fontsize=14)
ax.axvline(x=1.05 * v_cent, label="+5%", color="C1")
ax.axvline(x=0.95 * v_cent, label="-5%", color="C2")
ax.legend(fontsize=14, loc="upper right");
_images/notebooks_diagnostics_charged_particle_radiography_particle_tracing_custom_source_19_0.png

Next, we will construct velocity vectors centered around the z-axis for each particle.

[9]:
vel = np.zeros([int(nparticles), 3]) * u.m / u.s
vel[:, 0] = v0 * np.sin(theta) * np.cos(phi)
vel[:, 1] = v0 * np.sin(theta) * np.sin(phi)
vel[:, 2] = v0 * np.cos(theta)

Finally, we will use the function rot_a_to_b() to create a rotation matrix that will rotate the vel distribution so the distribution is centered about the \(y\) axis instead of the \(z\) axis.

[10]:
a = np.array([0, 0, 1])
b = np.array([0, 1, 0])
R = rot_a_to_b(a, b)
vel = np.matmul(vel, R)

Since the velocity vector distribution should be symmetric about the \(y\) axis, we can confirm this by checking that the normalized average velocity vector is close to the \(y\) unit vector.

[11]:
avg_v = np.mean(vel, axis=0)
print(avg_v / np.linalg.norm(avg_v))
[-1.56950008e-04  9.99999983e-01  9.68603269e-05]
Creating the Initial Particle Positions

For this example, we will create an initial position distribution representing a laser spot centered on the source location defined above as source. The distribution will be cylindrical (oriented along the \(y\)-axis) with a uniform distribution in y and a Gaussian distribution in radius (in the xz plane). We therefore need to create distributions in \(y\), \(\theta\), and \(r\), and then transform those into Cartesian positions.

Just as we previously weighted the \(\theta\) distribution with a \(sin \theta\) probability distribution to generate a uniform distribution in solid angle, we need to weight the \(r\) distribution with a \(r\) probability distribution so that the particles are uniformly distributed over the area of the disk.

[12]:
dy = 300 * u.um
y = np.random.uniform(
    low=(source[1] - dy).to(u.m).value,
    high=(source[1] + dy).to(u.m).value,
    size=int(nparticles),
)

arg = np.linspace(1e-9, 1e-3, num=int(1e5))
prob = arg * np.exp(-((arg / 3e-4) ** 2))
prob *= 1 / np.sum(prob)
r = np.random.choice(arg, size=int(nparticles), replace=True, p=prob)


theta = np.random.uniform(low=0, high=2 * np.pi, size=int(nparticles))

x = r * np.cos(theta)
z = r * np.sin(theta)

hist, xpos, zpos = np.histogram2d(
    x * 1e6, z * 1e6, bins=[100, 100], range=np.array([[-5e2, 5e2], [-5e2, 5e2]])
)

hist2, xpos2, ypos = np.histogram2d(
    x * 1e6,
    (y - source[1].to(u.m).value) * 1e6,
    bins=[100, 100],
    range=np.array([[-5e2, 5e2], [-5e2, 5e2]]),
)

fig, ax = plt.subplots(ncols=2, figsize=(12, 6))
fig.subplots_adjust(wspace=0.3, right=0.8)
fig.suptitle("Initial Particle Position Distribution", fontsize=14)
vmax = np.max([np.max(hist), np.max(hist2)])

p1 = ax[0].pcolormesh(xpos, zpos, hist.T, vmax=vmax)
ax[0].set_xlabel("x ($\\mu m$)", fontsize=14)
ax[0].set_ylabel("z ($\\mu m$)", fontsize=14)
ax[0].set_aspect("equal")

p2 = ax[1].pcolormesh(xpos2, ypos, hist2.T, vmax=vmax)
ax[1].set_xlabel("x ($\\mu m$)", fontsize=14)
ax[1].set_ylabel("y - $y_0$ ($\\mu m$)", fontsize=14)
ax[1].set_aspect("equal")

cbar_ax = fig.add_axes([0.85, 0.2, 0.03, 0.6])
cbar_ax.set_title("# Particles")
fig.colorbar(p2, cax=cbar_ax);
_images/notebooks_diagnostics_charged_particle_radiography_particle_tracing_custom_source_28_0.png

Finally we will combine these position arrays into an array with units.

[13]:
pos = np.zeros([int(nparticles), 3]) * u.m
pos[:, 0] = x * u.m
pos[:, 1] = y * u.m
pos[:, 2] = z * u.m
Creating a Synthetic Radiograph

To create an example synthetic radiograph, we will first create a field grid representing the analytical electric field produced by a sphere of Gaussian potential.

[14]:
# Create a Cartesian grid
L = 1 * u.mm
grid = CartesianGrid(-L, L, num=100)

# Create a spherical potential with a Gaussian radial distribution
radius = np.linalg.norm(grid.grid, axis=3)
arg = (radius / (L / 3)).to(u.dimensionless_unscaled)
potential = 6e5 * np.exp(-(arg**2)) * u.V

# Calculate E from the potential
Ex, Ey, Ez = np.gradient(potential, grid.dax0, grid.dax1, grid.dax2)
mask = radius < L / 2
Ex = -np.where(mask, Ex, 0)
Ey = -np.where(mask, Ey, 0)
Ez = -np.where(mask, Ez, 0)

# Add those quantities to the grid
grid.add_quantities(E_x=Ex, E_y=Ey, E_z=Ez, phi=potential)


# Plot the E-field
fig = plt.figure(figsize=(8, 8))
ax = fig.add_subplot(111, projection="3d")
ax.view_init(30, 30)

# skip some points to make the vector plot intelligible
s = tuple([slice(None, None, 6)] * 3)

ax.quiver(
    grid.pts0[s].to(u.mm).value,
    grid.pts1[s].to(u.mm).value,
    grid.pts2[s].to(u.mm).value,
    grid["E_x"][s],
    grid["E_y"][s],
    grid["E_z"][s],
    length=5e-7,
)

ax.set_xlabel("X (mm)", fontsize=14)
ax.set_ylabel("Y (mm)", fontsize=14)
ax.set_zlabel("Z (mm)", fontsize=14)
ax.set_xlim(-1, 1)
ax.set_ylim(-1, 1)
ax.set_zlim(-1, 1)
ax.set_title("Gaussian Potential Electric Field", fontsize=14);
_images/notebooks_diagnostics_charged_particle_radiography_particle_tracing_custom_source_33_0.png

We will then create the synthetic radiograph object. The warning filter ignores a warning that arises because \(B_x\), \(B_y\), \(B_z\) are not provided in the grid (they will be assumed to be zero).

[15]:
with warnings.catch_warnings():
    warnings.simplefilter("ignore")
    sim = cpr.Tracker(grid, source, detector, verbose=False)

Now, instead of using create_particles() to create the particle distribution, we will use the load_particles() function to use the particles we have created above.

[16]:
sim.load_particles(pos, vel, particle=particle)

Now the particle radiograph simulation can be run as usual.

[17]:
sim.run();
[18]:
size = np.array([[-1, 1], [-1, 1]]) * 1.5 * u.cm
bins = [200, 200]
hax, vax, intensity = cpr.synthetic_radiograph(sim, size=size, bins=bins)

fig, ax = plt.subplots(figsize=(8, 8))
plot = ax.pcolormesh(
    hax.to(u.cm).value,
    vax.to(u.cm).value,
    intensity.T,
    cmap="Blues_r",
    shading="auto",
)
cb = fig.colorbar(plot)
cb.ax.set_ylabel("Intensity", fontsize=14)
ax.set_aspect("equal")
ax.set_xlabel("X (cm), Image plane", fontsize=14)
ax.set_ylabel("Z (cm), Image plane", fontsize=14)
ax.set_title("Synthetic Proton Radiograph", fontsize=14);
_images/notebooks_diagnostics_charged_particle_radiography_particle_tracing_custom_source_40_0.png

Calling the synthetic_radiograph() function with the ignore_grid keyword will produce the synthetic radiograph corresponding to the source profile propagated freely through space (i.e. in the absence of any grid fields).

[19]:
hax, vax, intensity = cpr.synthetic_radiograph(
    sim, size=size, bins=bins, ignore_grid=True
)

fig, ax = plt.subplots(figsize=(8, 8))
plot = ax.pcolormesh(
    hax.to(u.cm).value,
    vax.to(u.cm).value,
    intensity.T,
    cmap="Blues_r",
    shading="auto",
)
cb = fig.colorbar(plot)
cb.ax.set_ylabel("Intensity", fontsize=14)
ax.set_aspect("equal")
ax.set_xlabel("X (cm), Image plane", fontsize=14)
ax.set_ylabel("Z (cm), Image plane", fontsize=14)
ax.set_title("Source Profile", fontsize=14);
_images/notebooks_diagnostics_charged_particle_radiography_particle_tracing_custom_source_42_0.png
[20]:
fig, ax = plt.subplots(figsize=(6, 6))
ax.plot(hax.to(u.cm).value, np.mean(intensity, axis=0))
ax.set_xlabel("X (cm), Image plane", fontsize=14)
ax.set_ylabel("Mean intensity", fontsize=14)
ax.set_title("Mean source profile", fontsize=14);
_images/notebooks_diagnostics_charged_particle_radiography_particle_tracing_custom_source_43_0.png

This page was generated by nbsphinx from docs/notebooks/diagnostics/charged_particle_radiography_particle_tracing_wire_mesh.ipynb.
Interactive online version: Binder badge.

Synthetic Charged Particle Radiographs with a Wire Mesh

In charged particle radiography experiments a wire mesh grid is often placed between the particle source and the object of interest, leaving a shadow of the grid in the particle fluence. The displacement of these shadow grid lines can then be used to quantitatively extract the line-integrated force experienced at each grid vertex.

The Tracker class includes a method (add_wire_mesh()) that can be used to create synthetic radiographs with a mesh in place. In this example notebook we will illustrate the options available for creating and placing the mesh(s), the demonstrate the use of a mesh grid in a practical example.

[1]:
import warnings

import astropy.units as u
import matplotlib.pyplot as plt
import numpy as np

from plasmapy.diagnostics.charged_particle_radiography import (
    synthetic_radiography as cpr,
)
from plasmapy.plasma.grids import CartesianGrid
Creating and Placing a Wire Mesh

We will begin by creating an empty CartesianGrid object in which the electric and magnetic fields are zero. Particle tracing through this grid will allow us to image just the mesh once we add one in place.

[2]:
empty_grid = CartesianGrid(-1 * u.mm, 1 * u.mm, num=50)

The charged particle radiography Tracker will warn us every time we use this grid that the fields are not specified (before assuming that they are zero). The following line will silence this warning.

[3]:
warnings.simplefilter("ignore")

We’ll also define a fixed source and detector that we won’t change for the rest of the example.

[4]:
source = (0 * u.mm, -10 * u.mm, 0 * u.mm)
detector = (0 * u.mm, 200 * u.mm, 0 * u.mm)

Finally, we’ll create an instance of Tracker.

[5]:
sim = cpr.Tracker(empty_grid, source, detector, verbose=False)

Now it’s time to create the mesh. add_wire_mesh() takes four required parameters:

  • location : A vector from the grid origin to the center of the mesh.

  • extent : The size of the mesh. If two values are given the mesh is assumed to be rectangular (extent is the width, height), but if only one is provided the mesh is assumed to be circular (extent is the diameter).

  • nwires : The number of wires in each direction. If only one value is given, it’s assumed to be the same for both directions.

  • wire_diameter : The diameter of each wire.

add_wire_mesh() works by extrapolating the positions of the particles in the mesh plane (based on their initial velocities) and removing those particles that will hit the wires. When add_wire_mesh() is called, the description of the mesh is stored inside the Tracker object. Multiple meshes can be added. The particles are then removed when the run() method is called.

[6]:
location = np.array([0, -2, 0]) * u.mm
extent = (1 * u.mm, 1 * u.mm)
nwires = (9, 12)
wire_diameter = 20 * u.um
sim.add_wire_mesh(location, extent, nwires, wire_diameter)

Now that the mesh has been created, we will run the particle tracing simulation and create a synthetic radiograph to visualize the result. We’ll wrap this in a function so we can use it again later.

[7]:
def run_radiograph(sim, vmax=None):
    sim.create_particles(1e5, 15 * u.MeV, max_theta=8 * u.deg)
    sim.run(field_weighting="nearest neighbor")
    h, v, i = cpr.synthetic_radiograph(
        sim, size=np.array([[-1, 1], [-1, 1]]) * 1.8 * u.cm, bins=[200, 200]
    )

    if vmax is None:
        vmax = np.max(i)

    fig, ax = plt.subplots(figsize=(8, 8))
    ax.pcolormesh(h.to(u.mm).value, v.to(u.mm).value, i.T, cmap="Blues_r", vmax=vmax)
    ax.set_aspect("equal")
    ax.set_xlabel("X (mm)")
    ax.set_ylabel("Y (mm)")
[8]:
run_radiograph(sim)
_images/notebooks_diagnostics_charged_particle_radiography_particle_tracing_wire_mesh_16_0.png

Notice that the distance from the source to the mesh is \(10 - 2 = 8\) mm, while the distance from the mesh to the detector is \(200 + 2 = 202\) mm. The magnification is therefore \(M = 1 + 202/8 = 26.25\), so the \(1\) mm wide mesh is \(26.25\) mm wide in the image.

Changing the location keyword can change both the magnification and shift the mesh center.

[9]:
sim = cpr.Tracker(empty_grid, source, detector, verbose=False)
sim.add_wire_mesh(np.array([0.5, -4, 0]) * u.mm, extent, nwires, wire_diameter)
run_radiograph(sim)
_images/notebooks_diagnostics_charged_particle_radiography_particle_tracing_wire_mesh_18_0.png

Setting the extent keyword to a single value will create a circular mesh (with a rectangular grid of wires).

[10]:
sim = cpr.Tracker(empty_grid, source, detector, verbose=False)
sim.add_wire_mesh(location, (1 * u.mm), nwires, wire_diameter)
run_radiograph(sim)
_images/notebooks_diagnostics_charged_particle_radiography_particle_tracing_wire_mesh_20_0.png

add_wire_mesh() has two optional keywords that can be used to change the orientation of the mesh. The first, mesh_hdir is a unit vector that sets the horizontal direction of the mesh plane. This can be used to effectively rotate the mesh. For example the following example will rotate the mesh by \(45^\circ\) (note that these unit vector inputs are automatically normalized).

[11]:
sim = cpr.Tracker(empty_grid, source, detector, verbose=False)
nremoved = sim.add_wire_mesh(
    location, extent, nwires, wire_diameter, mesh_hdir=np.array([0.5, 0, 0.5])
)
run_radiograph(sim)
_images/notebooks_diagnostics_charged_particle_radiography_particle_tracing_wire_mesh_22_0.png

The second keyword argument, mesh_vdir, overrides the unit vector that defines the vertical direction of the mesh plane. By default this vector is set to be mutually orthogonal to mesh_hdir and the detector plane normal so that the mesh is parallel to the detector plane. Changing this keyword (alone or in combination with mesh_hdir) can be used to create a tilted mesh.

[12]:
sim = cpr.Tracker(empty_grid, source, detector, verbose=False)
nremoved = sim.add_wire_mesh(
    location, extent, nwires, wire_diameter, mesh_vdir=np.array([0, 0.7, 1])
)
run_radiograph(sim)
_images/notebooks_diagnostics_charged_particle_radiography_particle_tracing_wire_mesh_24_0.png
Using a Wire Mesh in an Example Radiograph

To illustrate the use of a mesh in an actual example, we’ll first create an example CartesianGrid object and fill it with the analytical electric field produced by a sphere of Gaussian potential.

[13]:
# Create a Cartesian grid
L = 1 * u.mm
grid = CartesianGrid(-L, L, num=150)

# Create a spherical potential with a Gaussian radial distribution
radius = np.linalg.norm(grid.grid, axis=3)
arg = (radius / (L / 3)).to(u.dimensionless_unscaled)
potential = 2e5 * np.exp(-(arg**2)) * u.V

# Calculate E from the potential
Ex, Ey, Ez = np.gradient(potential, grid.dax0, grid.dax1, grid.dax2)
Ex = -np.where(radius < L / 2, Ex, 0)
Ey = -np.where(radius < L / 2, Ey, 0)
Ez = -np.where(radius < L / 2, Ez, 0)

# Add those quantities to the grid
grid.add_quantities(E_x=Ex, E_y=Ey, E_z=Ez, phi=potential)


# Plot the E-field
fig = plt.figure(figsize=(8, 8))
ax = fig.add_subplot(111, projection="3d")
ax.view_init(30, 30)

# skip some points to make the vector plot intelligible
s = tuple([slice(None, None, 10)] * 3)

ax.quiver(
    grid.pts0[s].to(u.mm).value,
    grid.pts1[s].to(u.mm).value,
    grid.pts2[s].to(u.mm).value,
    grid["E_x"][s],
    grid["E_y"][s],
    grid["E_z"][s],
    length=1e-6,
)

ax.set_xlabel("X (mm)")
ax.set_ylabel("Y (mm)")
ax.set_zlabel("Z (mm)")
ax.set_xlim(-1, 1)
ax.set_ylim(-1, 1)
ax.set_zlim(-1, 1)
ax.set_title("Gaussian Potential Electric Field");
_images/notebooks_diagnostics_charged_particle_radiography_particle_tracing_wire_mesh_27_0.png

Now we will create a mesh and run the particle tracing simulation

[14]:
sim = cpr.Tracker(grid, source, detector, verbose=False)
sim.add_wire_mesh(location, extent, 11, wire_diameter)
sim.create_particles(3e5, 15 * u.MeV, max_theta=8 * u.deg)
run_radiograph(sim, vmax=10)
_images/notebooks_diagnostics_charged_particle_radiography_particle_tracing_wire_mesh_29_0.png

Notice how the vertices of the grid are displaced by the fields.

This page was generated by nbsphinx from docs/notebooks/diagnostics/langmuir_analysis.ipynb.
Interactive online version: Binder badge.

Langmuir probe data analysis

Let’s analyze a few Langmuir probe characteristics using the diagnostics.langmuir subpackage. First we need to import the module and some basics.

[1]:
%matplotlib inline

from pathlib import Path
from pprint import pprint

import astropy.units as u
import numpy as np

from plasmapy.diagnostics.langmuir import Characteristic, swept_probe_analysis

The first characteristic we analyze is a simple single-probe measurement in a low (ion) temperature, low density plasma with a cylindrical probe. This allows us to utilize OML theory implemented in swept_probe_analysis(). The data has been preprocessed with some smoothing, which allows us to obtain a Electron Energy Distribution Function (EEDF) as well.

[2]:
# Load the bias and current values stored in the .p pickle file.
path = (Path.cwd() / ".." / "langmuir_samples" / "Beckers2017.npy").resolve()
bias, current = np.load(path)

# Create the Characteristic object, taking into account the correct units
characteristic = Characteristic(u.Quantity(bias, u.V), u.Quantity(current, u.A))

# Calculate the cylindrical probe surface area
probe_length = 1.145 * u.mm
probe_diameter = 1.57 * u.mm
probe_area = probe_length * np.pi * probe_diameter + np.pi * 0.25 * probe_diameter**2
/home/docs/checkouts/readthedocs.org/user_builds/plasmapy/envs/latest/lib/python3.12/site-packages/plasmapy/diagnostics/langmuir.py:36: FutureWarning: The plasmapy.diagnostics.langmuir module will be deprecated in favor of the plasmapy.analysis.swept_langmuir sub-package and phased out over 2021.  The plasmapy.analysis package was released in v0.5.0.
  warnings.warn(

Now we can actually perform the analysis. Since the plasma is in Helium an ion mass number of 4 is entered. The results are visualized and the obtained EEDF is also shown.

[3]:
pprint(
    swept_probe_analysis(
        characteristic, probe_area, "He-4+", visualize=True, plot_EEDF=True
    )
)
{'I_es': <Quantity 0.01873572 A>,
 'I_is': <Quantity -0.00231536 A>,
 'T_e': <Quantity 2.41021849 eV>,
 'V_F': <Quantity -5.75541887 V>,
 'V_P': <Quantity 2.15098227 V>,
 'n_e': <Quantity 5.93671704e+16 1 / m3>,
 'n_i': <Quantity 4.1665378e+17 1 / m3>,
 'n_i_OML': <Quantity 1.69390014e+17 1 / m3>}
_images/notebooks_diagnostics_langmuir_analysis_5_1.png
_images/notebooks_diagnostics_langmuir_analysis_5_2.png

The cyan and yellow lines indicate the fitted electron and ion currents, respectively. The green line is the sum of these and agrees nicely with the data. This indicates a successful analysis.

The next sample probe data is provided by David Pace. It is also obtained from a low relatively ion temperature and density plasma, in Argon.

[4]:
# Load the data from a file and create the Characteristic object
path = (Path.cwd() / ".." / "langmuir_samples" / "Pace2015.npy").resolve()
bias, current = np.load(path)
characteristic = Characteristic(u.Quantity(bias, u.V), u.Quantity(current, u.A))

Initially the electrons are assumed to be Maxwellian. To check this the fit of the electron growth region will be plotted.

[5]:
swept_probe_analysis(
    characteristic,
    0.738 * u.cm**2,
    "Ar-40 1+",
    bimaxwellian=False,
    plot_electron_fit=True,
)
[5]:
{'V_P': <Quantity -16.4 V>,
 'V_F': <Quantity -35.6 V>,
 'I_es': <Quantity 0.00282382 A>,
 'I_is': <Quantity -0.000129 A>,
 'n_e': <Quantity 7.60824393e+14 1 / m3>,
 'n_i': <Quantity 6.23732194e+15 1 / m3>,
 'T_e': <Quantity 3.51990716 eV>,
 'n_i_OML': <Quantity 3.07522416e+15 1 / m3>}
_images/notebooks_diagnostics_langmuir_analysis_10_1.png

It can be seen that this plasma is slightly bi-Maxwellian, as there are two distinct slopes in the exponential section. The analysis is now performed with bimaxwellian set to True, which yields improved results.

[6]:
pprint(
    swept_probe_analysis(
        characteristic,
        0.738 * u.cm**2,
        "Ar-40 1+",
        bimaxwellian=True,
        visualize=True,
        plot_electron_fit=True,
    )
)
/home/docs/checkouts/readthedocs.org/user_builds/plasmapy/envs/latest/lib/python3.12/site-packages/plasmapy/diagnostics/langmuir.py:1075: RuntimeWarning: overflow encountered in exp
  np.exp(fit_func(probe_characteristic.bias.to(u.V).value, *fit)) * u.A
{'I_es': <Quantity 0.00282382 A>,
 'I_is': <Quantity -0.000129 A>,
 'T_e': <Quantity [1.77947044e-07, 3.09914301e+00] eV>,
 'V_F': <Quantity -35.6 V>,
 'V_P': <Quantity -16.4 V>,
 'hot_fraction': 1.0,
 'n_e': <Quantity 8.10828907e+14 1 / m3>,
 'n_i': <Quantity 6.64726444e+15 1 / m3>,
 'n_i_OML': <Quantity 3.07522416e+15 1 / m3>}
_images/notebooks_diagnostics_langmuir_analysis_12_2.png
_images/notebooks_diagnostics_langmuir_analysis_12_3.png

The probe current resolution of the raw data is relatively poor, but the analysis still performs well in the ion current region. The bi-Maxwellian properties are not significant but do make a difference. Check this analysis without setting bimaxwellian to True! This is reflected in the results, which indicate that the temperatures of the cold and hot electron population are indeed different, but relatively close.

This Helium plasma is fully bi-Maxwellian.

[7]:
# Import probe data and calculate probe surface area.
path = (Path.cwd() / ".." / "langmuir_samples" / "Beckers2017b.npy").resolve()
bias, current = np.load(path)
characteristic = Characteristic(u.Quantity(bias, u.V), u.Quantity(current, u.A))
probe_length = 1.145 * u.mm
probe_diameter = 1.57 * u.mm
probe_area = probe_length * np.pi * probe_diameter + np.pi * 0.25 * probe_diameter**2

plot_electron_fit is set to True to check the bi-Maxwellian properties. The fit converges nicely to the two slopes of the electron growth region.

[8]:
pprint(
    swept_probe_analysis(
        characteristic,
        probe_area,
        "He-4+",
        bimaxwellian=True,
        plot_electron_fit=True,
        visualize=True,
    )
)
{'I_es': <Quantity 0.02655063 A>,
 'I_is': <Quantity -0.00080287 A>,
 'T_e': <Quantity [1.33644199, 6.45311087] eV>,
 'V_F': <Quantity -21.29773863 V>,
 'V_P': <Quantity 2.42370446 V>,
 'hot_fraction': 0.188060045293475,
 'n_e': <Quantity 8.6146857e+16 1 / m3>,
 'n_i': <Quantity 1.47942378e+17 1 / m3>,
 'n_i_OML': <Quantity 6.08613986e+16 1 / m3>}
_images/notebooks_diagnostics_langmuir_analysis_17_1.png
_images/notebooks_diagnostics_langmuir_analysis_17_2.png

This page was generated by nbsphinx from docs/notebooks/diagnostics/thomson.ipynb.
Interactive online version: Binder badge.

Thomson Scattering: Spectral Density

The thomson.spectral_density function calculates the spectral density function S(k,w), which is one of several terms that determine the scattered power spectrum for the Thomson scattering of a probe laser beam by a plasma. In particular, this function calculates \(S(k,w)\) for the case of a plasma consisting of one or more ion species and electron populations under the assumption that all of the ion species and the electron fluid have Maxwellian velocity distribution functions and that the combined plasma is quasi-neutral. In this regime, the spectral density is given by the equation:

\begin{equation} S(k,\omega) = \sum_e \frac{2\pi}{k} \bigg |1 - \frac{\chi_e}{\epsilon} \bigg |^2 f_{e0,e}\bigg ( \frac{\omega}{k} \bigg ) + \sum_i \frac{2\pi Z_i}{k} \bigg | \frac{\chi_e}{\epsilon} \bigg |^2 f_{i0, i} \bigg ( \frac{\omega}{k} \bigg ) \end{equation}

where \(\chi_e\) is the electron component susceptibility of the plasma and \(\epsilon = 1 + \sum_e \chi_e + \sum_i \chi_i\) is the total plasma dielectric function (with \(\chi_i\) being the ion component of the susceptibility), \(Z_i\) is the charge of each ion, \(k\) is the scattering wavenumber, \(\omega\) is the scattering frequency, and the functions \(f_{e0,e}\) and \(f_{i0,i}\) are the Maxwellian velocity distributions for the electrons and ion species respectively.

Thomson scattering can be either non-collective (the scattered spectrum is a linear sum of the light scattered by individual particles) or collective (the scattered spectrum is dominated by scattering off of collective plasma waves). The thomson.spectral_density function can be used in both cases. These regimes are delineated by the dimensionless constant \(\alpha\):

\begin{equation} \alpha = \frac{1}{k \lambda_{De}} \end{equation}

where \(\lambda_{De}\) is the Debye length. \(\alpha > 1\) corresponds to collective scattering, while \(\alpha < 1\) corresponds to non-collective scattering. Depending on which of these regimes applies, fitting the scattered spectrum can provide the electron (and sometimes ion) density and temperature. Doppler shifting of the spectrum can also provide a measurement of the drift velocity of each plasma species.

For a detailed explanation of the underlying physics (and derivations of these expressions), see “Plasma Scattering of Electromagnetic Radiation” by Sheffield et al.

[1]:
%matplotlib inline

import astropy.units as u
import matplotlib.pyplot as plt
import numpy as np

from plasmapy.diagnostics import thomson

Construct parameters that define the Thomson diagnostic setup, the probing beam and scattering collection. These parameters will be used for all examples.

[2]:
# The probe wavelength can in theory be anything, but in practice integer frequency multiples of the Nd:YAG wavelength
# 1064 nm are used (532 corresponds to a frequency-doubled probe beam from such a laser).
probe_wavelength = 532 * u.nm

# Array of wavelengths over which to calculate the spectral distribution
wavelengths = (
    np.arange(probe_wavelength.value - 60, probe_wavelength.value + 60, 0.01) * u.nm
)

# The scattering geometry is defined by unit vectors for the orientation of the probe laser beam (probe_n) and
# the path from the scattering volume (where the measurement is made) to the detector (scatter_n).
# These can be setup for any experimental geometry.
probe_vec = np.array([1, 0, 0])
scattering_angle = np.deg2rad(63)
scatter_vec = np.array([np.cos(scattering_angle), np.sin(scattering_angle), 0])

In order to calculate the scattered spectrum, we must also include some information about the plasma. For this plot we’ll allow the fract, ion_species, fluid_vel, and ion_vel keywords to keep their default values, describing a single-species H+ plasma at rest in the laboratory frame.

[3]:
ne = 2e17 * u.cm**-3
T_e = 12 * u.eV
T_i = 10 * u.eV

alpha, Skw = thomson.spectral_density(
    wavelengths,
    probe_wavelength,
    ne,
    T_e=T_e,
    T_i=T_i,
    probe_vec=probe_vec,
    scatter_vec=scatter_vec,
)

fig, ax = plt.subplots()
ax.plot(wavelengths, Skw, lw=2)
ax.set_xlim(probe_wavelength.value - 10, probe_wavelength.value + 10)
ax.set_ylim(0, 1e-13)
ax.set_xlabel(r"$\lambda$ (nm)")
ax.set_ylabel("S(k,w)")
ax.set_title("Thomson Scattering Spectral Density");
_images/notebooks_diagnostics_thomson_6_0.png
Example Cases in Different Scattering Regimes

We will now consider several example cases in different scattering regimes. In order to facilitate this, we’ll set up each example as a dictionary of plasma parameters:

A single-species, stationary hydrogen plasma with a density and temperature that results in a scattering spectrum dominated by scattering off of single electrons.

[4]:
non_collective = {
    "name": "Non-Collective Regime",
    "n": 5e15 * u.cm**-3,
    "T_e": 40 * u.eV,
    "T_i": np.array([10]) * u.eV,
    "ions": ["H+"],
    "electron_vel": np.array([[0, 0, 0]]) * u.km / u.s,
    "ion_vel": np.array([[0, 0, 0]]) * u.km / u.s,
}

A single-species, stationary hydrogen plasma with a density and temperature that result in weakly collective scattering (scattering parameter \(\alpha\) approaching 1)

[5]:
weakly_collective = {
    "name": "Weakly Collective Regime",
    "n": 2e17 * u.cm**-3,
    "T_e": 20 * u.eV,
    "T_i": 10 * u.eV,
    "ions": ["H+"],
    "electron_vel": np.array([[0, 0, 0]]) * u.km / u.s,
    "ion_vel": np.array([[0, 0, 0]]) * u.km / u.s,
}

A single-species, stationary hydrogen plasma with a density and temperature that result in a spectrum dominated by multi-particle scattering, including scattering off of ions.

[6]:
collective = {
    "name": "Collective Regime",
    "n": 5e17 * u.cm**-3,
    "T_e": 10 * u.eV,
    "T_i": 4 * u.eV,
    "ions": ["H+"],
    "electron_vel": np.array([[0, 0, 0]]) * u.km / u.s,
    "ion_vel": np.array([[0, 0, 0]]) * u.km / u.s,
}

A case identical to the collective example above, except that now the electron fluid has a substantial drift velocity parallel to the probe laser and the ions have a drift (relative to the electrons) at an angle.

[7]:
drifts = {
    "name": "Drift Velocities",
    "n": 5e17 * u.cm**-3,
    "T_e": 10 * u.eV,
    "T_i": 10 * u.eV,
    "ions": ["H+"],
    "electron_vel": np.array([[700, 0, 0]]) * u.km / u.s,
    "ion_vel": np.array([[-600, -100, 0]]) * u.km / u.s,
}

A case identical to the collective example, except that now the plasma consists 25% He+1 and 75% C+5, and two electron populations exist with different temperatures.

[8]:
two_species = {
    "name": "Two Ion and Electron Components",
    "n": 5e17 * u.cm**-3,
    "T_e": np.array([50, 10]) * u.eV,
    "T_i": np.array([10, 50]) * u.eV,
    "efract": np.array([0.5, 0.5]),
    "ifract": np.array([0.25, 0.75]),
    "ions": ["He-4 1+", "C-12 5+"],
    "electron_vel": np.array([[0, 0, 0], [0, 0, 0]]) * u.km / u.s,
    "ion_vel": np.array([[0, 0, 0], [0, 0, 0]]) * u.km / u.s,
}
[9]:
examples = [non_collective, weakly_collective, collective, drifts, two_species]

For each example, plot the spectral distribution function over a large range to show the broad electron scattering feature (top row) and a narrow range around the probe wavelength to show the ion scattering feature (bottom row)

[10]:
fig, ax = plt.subplots(ncols=len(examples), nrows=2, figsize=[25, 10])
fig.subplots_adjust(wspace=0.4, hspace=0.4)

lbls = "abcdefg"

for i, x in enumerate(examples):
    alpha, Skw = thomson.spectral_density(
        wavelengths,
        probe_wavelength,
        x["n"],
        T_e=x["T_e"],
        T_i=x["T_i"],
        ifract=x.get("ifract"),
        efract=x.get("efract"),
        ions=x["ions"],
        electron_vel=x["electron_vel"],
        ion_vel=x["ion_vel"],
        probe_vec=probe_vec,
        scatter_vec=scatter_vec,
    )

    ax[0][i].axvline(x=probe_wavelength.value, color="red")  # Mark the probe wavelength
    ax[0][i].plot(wavelengths, Skw)
    ax[0][i].set_xlim(probe_wavelength.value - 15, probe_wavelength.value + 15)
    ax[0][i].set_ylim(0, 1e-13)
    ax[0][i].set_xlabel(r"$\lambda$ (nm)")

    ax[0][i].set_title(lbls[i] + ") " + x["name"] + f"\n$\\alpha$={alpha:.4f}")

    ax[1][i].axvline(x=probe_wavelength.value, color="red")  # Mark the probe wavelength
    ax[1][i].plot(wavelengths, Skw)
    ax[1][i].set_xlim(probe_wavelength.value - 1, probe_wavelength.value + 1)
    ax[1][i].set_ylim(0, 1.1 * np.max(Skw.value))
    ax[1][i].set_xlabel(r"$\lambda$ (nm)")
_images/notebooks_diagnostics_thomson_20_0.png

Plots of the spectral density function (Skw) which determines the amount of light scattered into different wavelengths.

  1. In the non-collective regime only the electron feature is visible.

  2. In the weakly collective regime (alpha approaches 1) an ion feature starts to appear and the electron feature is distorted

  3. In the collective regime both features split into two peaks, corresponding to scattering off of forward and backwards propagating plasma oscillations.

  4. The introduction of drift velocities introduces several Doppler shifts in the calculations, resulting in a shifted spectrum.

  5. Including multiple ion and electron populations modifies the ion and electron features respectively.

This page was generated by nbsphinx from docs/notebooks/diagnostics/thomson_fitting.ipynb.
Interactive online version: Binder badge.

Fitting Thomson Scattering Spectra

Thomson scattering diagnostics record a scattered power spectrum that encodes information about the electron and ion density, temperatures, and flow velocities. This information can be retrieved by fitting the measured spectrum with the theoretical spectral density (spectral_density). This notebook demonstrates how to use the lmfit package (along with some helpful PlasmaPy functions) to fit 1D Thomson scattering spectra.

4e69a71d40144206ab081c96fa8b889e

Thomson scattering can be either non-collective (dominated by single electron scattering) or collective (dominated by scattering off of electron plasma waves (EPW) and ion acoustic waves (IAW). In the non-collective regime, the scattering spectrum contains a single peak. However, in the collective regime the spectrum contains separate features caused by the electron and ion populations (corresponding to separate scattering off of EPW and IAW). These features exist on different scales: the EPW feature is dim but covers a wide wavelength range, while the IAW feature is bright but narrow. They also encode partially-degenerate information (e.g., the flow velocities of the electrons and ions respectively). The two features are therefore often recorded on separate spectrometers and are fit separately.

[1]:
%matplotlib inline

import astropy.units as u
import matplotlib.pyplot as plt
import numpy as np
from lmfit import Parameters

from plasmapy.diagnostics import thomson
Contents
  1. Fitting Collective Thomson Scattering

    1. Fitting the EPW Feature

    2. Fitting the IAW Feature

  2. Fitting Non-Collective Thomson Scattering

Fitting Collective Thomson Scattering

To demonstrate the fitting capabilities, we’ll first generate some synthetic Thomson data using the spectral_density function. This data will be in the collective regime, so we will generate two datasets (using the same plasma parameters and probe geometry) that correspond to the EPW and IAW features. For more details on the spectral density function, see the spectral density function example notebook.

[2]:
# Generate theoretical spectrum
probe_wavelength = 532 * u.nm
epw_wavelengths = (
    np.linspace(probe_wavelength.value - 30, probe_wavelength.value + 30, num=500)
    * u.nm
)
iaw_wavelengths = (
    np.linspace(probe_wavelength.value - 3, probe_wavelength.value + 3, num=500) * u.nm
)

probe_vec = np.array([1, 0, 0])
scattering_angle = np.deg2rad(63)
scatter_vec = np.array([np.cos(scattering_angle), np.sin(scattering_angle), 0])

n = 2e17 * u.cm**-3
ions = ["H+", "C-12 5+"]
T_e = 10 * u.eV
T_i = np.array([20, 50]) * u.eV
electron_vel = np.array([[0, 0, 0]]) * u.km / u.s
ion_vel = np.array([[0, 0, 0], [200, 0, 0]]) * u.km / u.s
ifract = [0.3, 0.7]

alpha, epw_skw = thomson.spectral_density(
    epw_wavelengths,
    probe_wavelength,
    n,
    T_e=T_e,
    T_i=T_i,
    ions=ions,
    ifract=ifract,
    electron_vel=electron_vel,
    ion_vel=ion_vel,
    probe_vec=probe_vec,
    scatter_vec=scatter_vec,
)

alpha, iaw_skw = thomson.spectral_density(
    iaw_wavelengths,
    probe_wavelength,
    n,
    T_e=T_e,
    T_i=T_i,
    ions=ions,
    ifract=ifract,
    electron_vel=electron_vel,
    ion_vel=ion_vel,
    probe_vec=probe_vec,
    scatter_vec=scatter_vec,
)

# PLOTTING
fig, ax = plt.subplots(ncols=2, figsize=(12, 4))
fig.subplots_adjust(wspace=0.2)

for a in ax:
    a.set_xlabel("Wavelength (nm)")
    a.set_ylabel("Skw")
    a.axvline(x=probe_wavelength.value, color="red", label="Probe wavelength")

ax[0].set_xlim(520, 545)
ax[0].set_ylim(0, 3e-13)
ax[0].set_title("Electron Plasma Wave")
ax[0].plot(epw_wavelengths.value, epw_skw)
ax[0].legend()

ax[1].set_xlim(530, 534)
ax[1].set_title("Ion Acoustic Wave")
ax[1].plot(iaw_wavelengths.value, iaw_skw)
ax[1].legend();
_images/notebooks_diagnostics_thomson_fitting_7_0.png

Note that these plots are showing the same spectral distribution, just over different wavelength ranges: the large peak in the center of the EPW spectrum is the IAW spectrum. Next we’ll add some noise to the spectra to simulate an experimental measurement.

[3]:
epw_skw *= 1 + np.random.normal(loc=0, scale=0.1, size=epw_wavelengths.size)

iaw_skw *= 1 + np.random.normal(loc=0, scale=0.1, size=iaw_wavelengths.size)

During experiments, the IAW feature is typically blocked on the EPW detector using a notch filter to prevent it from saturating the measurement. We’ll mimic this by setting the center of the EPW spectrum to np.nan. The fitting algorithm applied later will recognize these values and not include them in the fit.

The more of the EPW spectrum we exclude, the less sensitive the fit will become. So, we want to block as little as possible while still obscuring the IAW feature.

[4]:
notch_range = (531, 533)
x0 = np.argmin(np.abs(epw_wavelengths.value - notch_range[0]))
x1 = np.argmin(np.abs(epw_wavelengths.value - notch_range[1]))
epw_skw[x0:x1] = np.nan

Another option is to delete the missing data from both the data vector and the corresponding wavelengths vector.

epw_skw = np.delete(epw_skw, slice(x0,x1,None))
epw_wavelengths = np.delete(epw_wavelengths, slice(x0,x1,None))

This does not look as nice on plots (since it leaves a line between datapoints at x0 and x1) but is necessary in some cases (e.g. when also using an instrument function).

Finally, we need to get rid of the units and normalize the data on each detector to its maximum value (this is a requirement of the fitting algorithm). We’re using np.nanmax() here to ignore the NaN values in epw_skw.

[5]:
epw_skw = epw_skw.value
epw_skw *= 1 / np.nanmax(epw_skw)
iaw_skw = iaw_skw.value
iaw_skw *= 1 / np.nanmax(iaw_skw)

# Plot again
fig, ax = plt.subplots(ncols=2, figsize=(12, 4))
fig.subplots_adjust(wspace=0.2)

for a in ax:
    a.set_xlabel("Wavelength (nm)")
    a.set_ylabel("Skw")
    a.axvline(x=probe_wavelength.value, color="red", label="Probe wavelength")

ax[0].set_xlim(520, 545)
ax[0].set_title("Electron Plasma Wave")
ax[0].plot(epw_wavelengths.value, epw_skw)
ax[0].legend()

ax[1].set_xlim(531, 533)
ax[1].set_title("Ion Acoustic Wave")
ax[1].plot(iaw_wavelengths.value, iaw_skw)
ax[1].legend();
_images/notebooks_diagnostics_thomson_fitting_13_0.png

We’ll start by fitting the EPW feature, then move on to the IAW feature. This is typically the process followed when analyzing experimental data, since the EPW feature depends on fewer parameters.

Fitting the EPW Feature

In order to fit this data in lmfit we need to create an lmfit.Model object for this problem. PlasmaPy includes such a model function for fitting Thomson scattering spectra, spectral_density_model. This model function is initialized by providing the parameters of the fit in the form of an lmfit.Parameters object.

A lmfit.Parameters object is an ordered dictionary of lmfit.Parameter objects. Each lmfit.Parameter has a number of elements, the most important of which for our purposes are - “name” (str) -> The name of the parameter (and the key in Parameters dictionary) - “value” (float) -> The initial value of the parameter. - “vary” (boolean) -> Whether or not the parameter will be varied during fitting. - “min”, “max” (float) -> The minimum and maximum bounds for the parameter during fitting.

Since lmfit.Parameter objects can only be scalars, arrays of multiple quantities must be broken apart into separate Parameter objects. To do so, this fitting routine adopts the following convention:

T_e = [1,2] -> "T_e_0"=1, "T_e_1" = 2

Specifying large arrays (like velocity vectors for multiple ion species) is clearly tedious in this format, and specifying non-numeric values (such as ions) is impossible. Therefore, this routine also takes a settings dictionary as a way to pass non-varying input to the spectral_density_model function.

A list of required and optional parameters and settings is provided in the docstring for the spectral_density_model function. For example:

[6]:
help(thomson.spectral_density_model)
Help on function spectral_density_model in module plasmapy.diagnostics.thomson:

spectral_density_model(wavelengths, settings, params)
    Returns a `lmfit.model.Model` function for Thomson spectral density
    function.

    Parameters
    ----------
    wavelengths : numpy.ndarray
        Wavelength array, in meters.

    settings : dict
        A dictionary of non-variable inputs to the spectral density
        function which must include the following keys:

        - ``"probe_wavelength"``: Probe wavelength in meters
        - ``"probe_vec"`` : (3,) unit vector in the probe direction
        - ``"scatter_vec"``: (3,) unit vector in the scattering
          direction
        - ``"ions"`` : list of particle strings,
          `~plasmapy.particles.particle_class.Particle` objects, or a
          `~plasmapy.particles.particle_collections.ParticleList`
          describing each ion species. All ions must be positive. Ion mass
          and charge number from this list will be automatically
          added as fixed parameters, overridden by any ``ion_mass`` or ``ion_z``
          parameters explicitly created.

        and may contain the following optional variables:

        - ``"electron_vdir"`` : (e#, 3) array of electron velocity unit
          vectors
        - ``"ion_vdir"`` : (e#, 3) array of ion velocity unit vectors
        - ``"instr_func"`` : A function that takes a wavelength
          |Quantity| array and returns a spectrometer instrument
          function as an `~numpy.ndarray`.
        - ``"notch"`` : A wavelength range or array of multiple wavelength
          ranges over which the spectral density is set to 0.

        These quantities cannot be varied during the fit.

    params : `~lmfit.parameter.Parameters` object
        A `~lmfit.parameter.Parameters` object that must contain the
        following variables:

        - n: Total combined density of the electron populations in
          m\ :sup:`-3`
        - :samp:`T_e_{e#}` : Temperature in eV
        - :samp:`T_i_{i#}` : Temperature in eV

        where where :samp:`{i#}` and where :samp:`{e#}` are replaced by
        the number of electron and ion populations, zero-indexed,
        respectively (e.g., 0, 1, 2, ...). The
        `~lmfit.parameter.Parameters` object may also contain the
        following optional variables:

        - :samp:`"efract_{e#}"` : Fraction of each electron population
          (must sum to 1)
        - :samp:`"ifract_{i#}"` : Fraction of each ion population (must
          sum to 1)
        - :samp:`"electron_speed_{e#}"` : Electron speed in m/s
        - :samp:`"ion_speed_{ei}"` : Ion speed in m/s
        - :samp:`"ion_mu_{i#}"` : Ion mass number, :math:`\mu = m_i/m_p`
        - :samp:`"ion_z_{i#}"` : Ion charge number
        - :samp:`"background"` : Background level, as fraction of max signal

        These quantities can be either fixed or varying.

    Returns
    -------
    model : `lmfit.model.Model`
        An `lmfit.model.Model` of the spectral density function for the
        provided settings and parameters that can be used to fit Thomson
        scattering data.

    Notes
    -----
    If an instrument function is included, the data should not include
    any `numpy.nan` values — instead regions with no data should be
    removed from both the data and wavelength arrays using
    `numpy.delete`.

We will now create the lmfit.Parameters object and settings dictionary using the values defined earlier when creating the sample data. We will choose to vary both the density and electron temperature, and will intentionally set incorrect initial values for both parameters. Note that, even though only one electron population is included, we must still name the temperature variable Te_0 in accordance with the convention defined above.

Note that the EPW spectrum is effectively independent of the ion parameters: only n, Te, and electron_vel will affect this fit. However, since the ion parameters are required arguments for spectral_density, we still need to provide values for them. We will therefore set these parameters as fixed (vary=False) with approximate values (in this case, intentionally poor estimates have been chosen to emphasize that they do not affect the EPW fit).

[7]:
params = Parameters()
params.add(
    "n", value=4e17 * 1e6, vary=True, min=5e16 * 1e6, max=1e18 * 1e6
)  # Converting cm^-3 to m^-3
params.add("T_e_0", value=5, vary=True, min=0.5, max=25)
params.add("T_i_0", value=5, vary=False)
params.add("T_i_1", value=10, vary=False)
params.add("ifract_0", value=0.8, vary=False)
params.add("ifract_1", value=0.2, vary=False)
params.add("ion_speed_0", value=0, vary=False)
params.add("ion_speed_1", value=0, vary=False)

settings = {}
settings["probe_wavelength"] = probe_wavelength.to(u.m).value
settings["probe_vec"] = probe_vec
settings["scatter_vec"] = scatter_vec
settings["ions"] = ions
settings["ion_vdir"] = np.array([[1, 0, 0], [1, 0, 0]])

lmfit allows the value of a parameter to be fixed using a constraint using the expr keyword in lmfit.Parameter as shown in the definition of ifract_1 above. In this case, ifract_1 is not actually a free parameter, since its value is fixed by the fact that ifract_0 + ifract_1 = 1.0. This constraint is made explicit here, but the spectral_density_model function will automatically enforce this constraint for efract and ifract variables.

Just as in the spectral_density function, some parameters are required while others are optional. For example, density (n) is required but ion_velocity is optional (and will be assumed to be zero if not provided). The list of required and optional parameters is identical to the required and optional arguments of spectral_density.

We can now use these objects to initialize an lmfit.Model object based on the spectral_density function using the spectral_density_model function.

[8]:
epw_model = thomson.spectral_density_model(
    epw_wavelengths.to(u.m).value, settings, params
)

With the model created, the fit can be easily performed using the model.fit() method. This method takes several keyword options that are worth mentioning:

  • “method” -> A string that defines the fitting method, from the list of minimizer options. A good choice for fitting Thomson spectra is differential_evolution.

  • “max_nfev” -> The maximum number of iterations allowed.

In addition, of course we also need to include the data to be fit (epw_skw), the independent variable (wavelengths) and the parameter object. It is important to note that the data to be fit should be a np.ndarray (unit-less) and normalized.

[9]:
fit_kws = {}
epw_result = epw_model.fit(
    epw_skw,
    params=params,
    wavelengths=epw_wavelengths.to(u.m).value,
    method="differential_evolution",
    fit_kws=fit_kws,
)

In the model.fit(), the fit_kws keyword can be set to a dictionary of keyword arguments that will be passed to the underlying scipy.optimize optimization function, and can be used to refine the fit. For details, see the SciPy documentation for the differential_evolution algorithm.

The return from this function is a lmfit.ModelResult object, which is a convenient container holding lots of information! To start with, we can see the best-fit parameters, number of iterations, the chiSquared goodness-of-fit metric, and plot the best-fit curve.

[10]:
# Print some of the results compared to the true values
answers = {"n": 2e17, "T_e_0": 10}
for key, ans in answers.items():
    print(f"{key}: {epw_result.best_values[key]:.1e} (true value {ans:.1e})")

print(f"Number of fit iterations:{epw_result.nfev}")
print(f"Reduced Chisquared:{epw_result.redchi:.4f}")

# Extract the best fit curve by evaluating the model at the final parameters
n_fit = epw_result.values["n"]
Te_0_fit = epw_result.values["T_e_0"]
n: 2.1e+23 (true value 2.0e+17)
T_e_0: 9.1e+00 (true value 1.0e+01)
Number of fit iterations:501
Reduced Chisquared:0.0012

Note that the best_fit curve skips the NaN values in the data, so the array is shorter than epw_skw. In order to plot them against the same wavelengths, we need to create an array of indices where epw_skw is not NaN.

[11]:
# Extract the best fit curve
best_fit_skw = epw_result.best_fit

# Get all the non-nan indices (the best_fit_skw just omits these values)
not_nan = np.argwhere(np.logical_not(np.isnan(epw_skw)))

# Plot
fig, ax = plt.subplots(ncols=1, figsize=(8, 8))
ax.set_xlabel("Wavelength (nm)")
ax.set_ylabel("Skw")
ax.axvline(x=probe_wavelength.value, color="red", label="Probe wavelength")

ax.set_xlim(520, 545)

ax.plot(epw_wavelengths.value, epw_skw, label="Data")
ax.plot(epw_wavelengths.value[not_nan], best_fit_skw, label="Best-fit")
ax.legend(loc="upper right");
_images/notebooks_diagnostics_thomson_fitting_27_0.png

The resulting fit is very good, even though many of the ion parameters are still pretty far from their actual values!

Fitting the IAW Feature

We will now follow the same steps to fit the IAW feature. We’ll start by setting up a new lmfit.Parameters object, this time using the best-fit values from the EPW fit as fixed values for those parameters.

[12]:
settings = {}
settings["probe_wavelength"] = probe_wavelength.to(u.m).value
settings["probe_vec"] = probe_vec
settings["scatter_vec"] = scatter_vec
settings["ions"] = ions
settings["ion_vdir"] = np.array([[1, 0, 0], [1, 0, 0]])

params = Parameters()
params.add("n", value=n_fit, vary=False)
params.add("T_e_0", value=Te_0_fit, vary=False)
params.add("T_i_0", value=10, vary=True, min=5, max=60)
params.add("T_i_1", value=10, vary=True, min=5, max=60)
params.add("ifract_0", value=0.5, vary=True, min=0.2, max=0.8)
params.add("ifract_1", value=0.5, vary=True, min=0.2, max=0.8, expr="1.0 - ifract_0")
params.add("ion_speed_0", value=0, vary=False)
params.add("ion_speed_1", value=0, vary=True, min=0, max=1e6)

Now we will run the fit

[13]:
iaw_model = thomson.spectral_density_model(
    iaw_wavelengths.to(u.m).value, settings, params
)

iaw_result = iaw_model.fit(
    iaw_skw,
    params=params,
    wavelengths=iaw_wavelengths.to(u.m).value,
    method="differential_evolution",
)

# Print some of the results compared to the true values
answers = {
    "T_i_0": 20,
    "T_i_1": 50,
    "ifract_0": 0.3,
    "ifract_1": 0.7,
    "ion_speed_1": 2e5,
}
for key, ans in answers.items():
    print(f"{key}: {iaw_result.best_values[key]:.1f} (true value {ans:.1f})")

print(f"Number of fit iterations:{iaw_result.nfev:.1f}")
print(f"Reduced Chisquared:{iaw_result.redchi:.4f}")
T_i_0: 23.7 (true value 20.0)
T_i_1: 54.7 (true value 50.0)
ifract_0: 0.3 (true value 0.3)
ifract_1: 0.7 (true value 0.7)
ion_speed_1: 201872.2 (true value 200000.0)
Number of fit iterations:1270.0
Reduced Chisquared:0.0003

And plot the results

[14]:
# Extract the best fit curve
best_fit_skw = iaw_result.best_fit

# Plot
fig, ax = plt.subplots(ncols=1, figsize=(8, 8))
ax.set_xlabel("Wavelength (nm)")
ax.set_ylabel("Skw")
ax.axvline(x=probe_wavelength.value, color="red", label="Probe wavelength")

ax.set_xlim(531, 533)

ax.plot(iaw_wavelengths.value, iaw_skw, label="Data")
ax.plot(iaw_wavelengths.value, best_fit_skw, label="Best-fit")
ax.legend(loc="upper right");
_images/notebooks_diagnostics_thomson_fitting_35_0.png

At this point, the best-fit parameters from the IAW fit could be used to further refine the EPW fit, and so on iteratively until both fits become stable.

Fitting Non-Collective Thomson Scattering

The non-collective Thomson scattering spectrum depends only on the electron density and temperature. In this regime the spectrum is less featured and, consequently, fits will produce larger errors. Otherwise, the fitting procedure is the same.

To illustrate fitting in this regime, we’ll start by generating some test data

[15]:
nc_wavelengths = (
    np.linspace(probe_wavelength.value - 50, probe_wavelength.value + 50, num=1000)
    * u.nm
)

n = 5e15 * u.cm**-3
T_e = 5 * u.eV
T_i = np.array([1]) * u.eV
ions = ["H+"]

alpha, Skw = thomson.spectral_density(
    nc_wavelengths,
    probe_wavelength,
    n,
    T_e=T_e,
    T_i=T_i,
    ions=ions,
    probe_vec=probe_vec,
    scatter_vec=scatter_vec,
)

# Normalize and add noise
nc_skw = Skw.value
nc_skw *= 1 / np.nanmax(nc_skw)
nc_theory = np.copy(nc_skw)
nc_skw *= 1 + np.random.normal(loc=0, scale=0.1, size=nc_wavelengths.size)


# Plot again
fig, ax = plt.subplots(ncols=2, figsize=(12, 4))
fig.subplots_adjust(wspace=0.2)

for a in ax:
    a.set_xlabel("Wavelength (nm)")
    a.set_ylabel("Skw")
    a.axvline(x=probe_wavelength.value, color="red", label="Probe wavelength")

ax[0].set_xlim(520, 545)
ax[0].set_title("Theoretical signal")
ax[0].plot(nc_wavelengths.value, nc_theory)
ax[0].legend()

ax[1].set_xlim(520, 545)
ax[1].set_title("With noise")
ax[1].plot(nc_wavelengths.value, nc_skw)
ax[1].legend();
_images/notebooks_diagnostics_thomson_fitting_39_0.png

Then we will setup the settings dictionary and lmfit.Parameters object and run the fit.

Note that, in the non-collective regime, the spectral_density function is effectively independent of density. Just as in the collective-regime EPW fitting section, we still need to provide a value for n because it is a required argument for spectral_density, but this parameter should be fixed and its value can be a rough approximation.

[16]:
settings = {}
settings["probe_wavelength"] = probe_wavelength.to(u.m).value
settings["probe_vec"] = probe_vec
settings["scatter_vec"] = scatter_vec
settings["ions"] = ions

params = Parameters()
params.add("n", value=1e15 * 1e6, vary=False)  # Converting cm^-3 to m^-3
params.add("T_e_0", value=1, vary=True, min=0.1, max=20)
params.add("T_i_0", value=1, vary=False)

nc_model = thomson.spectral_density_model(
    nc_wavelengths.to(u.m).value, settings, params
)

nc_result = nc_model.fit(
    nc_skw,
    params,
    wavelengths=nc_wavelengths.to(u.m).value,
    method="differential_evolution",
)
[17]:
best_fit_skw = nc_result.best_fit

print(f"T_e_0: {nc_result.best_values['T_e_0']:.1f} (true value 5)")
print(f"Number of fit iterations:{nc_result.nfev}")
print(f"Reduced Chisquared:{nc_result.redchi:.4f}")

# Plot
fig, ax = plt.subplots(ncols=1, figsize=(8, 8))
ax.set_xlabel("Wavelength (nm)")
ax.set_ylabel("Skw")
ax.axvline(x=probe_wavelength.value, color="red", label="Probe wavelength")

ax.set_xlim(520, 545)

ax.plot(nc_wavelengths.value, nc_skw, label="Data")
ax.plot(nc_wavelengths.value, best_fit_skw, label="Best-fit")
ax.legend(loc="upper right");
T_e_0: 6.0 (true value 5)
Number of fit iterations:126
Reduced Chisquared:0.0003
_images/notebooks_diagnostics_thomson_fitting_42_1.png

Dispersion

This page was generated by nbsphinx from docs/notebooks/dispersion/dispersion_function.ipynb.
Interactive online version: Binder badge.

[1]:
%matplotlib inline

The plasma dispersion function

Let’s import some basics (and plasmapy!)

[2]:
import matplotlib.pyplot as plt
import numpy as np
[3]:
from plasmapy.dispersion import plasma_dispersion_func, plasma_dispersion_func_deriv

Take a look at the docs to plasma_dispersion_func() for more information on this.

We’ll now make some sample data to visualize the dispersion function:

[4]:
x = np.linspace(-1, 1, 200)
X, Y = np.meshgrid(x, x)
Z = X + 1j * Y
print(Z.shape)
(200, 200)

Before we start plotting, let’s make a visualization function first:

[5]:
def plot_complex(X, Y, Z, N=50):
    fig, (real_axis, imag_axis) = plt.subplots(1, 2)
    real_axis.contourf(X, Y, Z.real, N)
    imag_axis.contourf(X, Y, Z.imag, N)
    real_axis.set_title("Real values")
    imag_axis.set_title("Imaginary values")
    for ax in [real_axis, imag_axis]:
        ax.set_xlabel("Real values")
        ax.set_ylabel("Imaginary values")
    fig.tight_layout()


plot_complex(X, Y, Z)
_images/notebooks_dispersion_dispersion_function_8_0.png

We can now apply our visualization function to our simple dispersion relation

[6]:
# sphinx_gallery_thumbnail_number = 2
F = plasma_dispersion_func(Z)
plot_complex(X, Y, F)
_images/notebooks_dispersion_dispersion_function_10_0.png

Let’s find the area where the dispersion function has a lesser than zero real part:

[7]:
plot_complex(X, Y, F.real < 0)
_images/notebooks_dispersion_dispersion_function_12_0.png

We can visualize the derivative:

[8]:
F = plasma_dispersion_func_deriv(Z)
plot_complex(X, Y, F)
_images/notebooks_dispersion_dispersion_function_14_0.png

Plotting the same function on a larger area:

[9]:
x = np.linspace(-2, 2, 400)
X, Y = np.meshgrid(x, x)
Z = X + 1j * Y
print(Z.shape)
(400, 400)
[10]:
F = plasma_dispersion_func(Z)
plot_complex(X, Y, F, 100)
_images/notebooks_dispersion_dispersion_function_17_0.png

Now we examine the derivative of the dispersion function as a function of the phase velocity of an electromagnetic wave propagating through the plasma. This is recreating figure 5.1 in J. Sheffield, D. Froula, S. H. Glenzer, and N. C. Luhmann Jr, Plasma scattering of electromagnetic radiation: theory and measurement techniques, ch. 5, p. 106 (Academic press, 2010).

[11]:
xs = np.linspace(0, 4, 100)
ws = (-1 / 2) * plasma_dispersion_func_deriv(xs)
wRe = np.real(ws)
wIm = np.imag(ws)

plt.plot(xs, wRe, label="Re")
plt.plot(xs, wIm, label="Im")
plt.axis([0, 4, -0.3, 1])
plt.legend(
    loc="upper right", frameon=False, labelspacing=0.001, fontsize=14, borderaxespad=0.1
)
plt.show()
_images/notebooks_dispersion_dispersion_function_19_0.png

This page was generated by nbsphinx from docs/notebooks/dispersion/hollweg_dispersion.ipynb.
Interactive online version: Binder badge.

Hollweg Dispersion Solver

This notebook details the functionality of the hollweg() function. This function computes the wave frequencies for given wavenumbers and plasma parameters based on the solution to the two fluid dispersion relation presented by Hollweg 1999, and further summarized by Bellan 2012. In his derivation Hollweg assumed a uniform magnetic field, zero D.C electric field, quasi-neutrality, and low-frequency waves (\(\omega \ll \omega_{ci}\)), which yielded the following expression

\[\begin{split}\left( \frac{\omega^2}{{k_z}^2 {v_A}^2} - 1 \right) \left[\omega^2 \left(\omega^2 - k^2 {v_A}^2 \right) - \beta k^2 {v_A}^2 \left( \omega^2 - {k_z}^2 {v_A}^2 \right) \right] \\ = \omega^2 \left(\omega^2 - k^2 {v_A}^2 \right) {k_x}^2 \left( \frac{{c_s}^2}{{\omega_{ci}}^2} - \frac{c^2}{{\omega_{pe}}^2} \frac{\omega^2}{{k_z}^2 {v_A}^2} \right)\end{split}\]

where

\[\beta = c_{s}^2 / v_{A}^2\]
\[k_{x} = k \sin \theta\]
\[k_{z} = k \cos \theta\]
\[\mathbf{B_{o}} = B_{o} \mathbf{\hat{z}}\]

\(\omega\) is the wave frequency, \(k\) is the wavenumber , \(v_{A}\) is the Alfvén velocity, \(c_{s}\) is the ion sound speed, \(\omega_{ci}\) is the ion gyrofrequency, and \(\omega_{pe}\) is the electron plasma frequency.

Note

Hollweg 1999 asserts this expression is valid for arbitrary \(c_{s} / v_{A}\) and \(k_{z} / k\). Contrarily, Bellan 2012 states in Section 1.7 that due to the inconsistent retention of the \(\omega / \omega_{ci} \ll 1\) terms the expression can only be valid if both \(c_{s} \ll v_{A}\) and the wave propagation is nearly perpendicular to the magnetic field (\(|\theta - \pi/2| \ll 0.1\) radians).

The hollweg() function numerically solves for the roots of this equation, which correspond to the Fast, Alfvén, and Acoustic wave modes.

Contents:
  1. Wave propagating at nearly 90 degrees

  2. Hollweg 1999 and Bellan 2012 comparison

  3. Reproduce Figure 2 from Hollweg 1999

[1]:
%matplotlib inline

import warnings

import astropy.units as u
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.ticker import MultipleLocator

from plasmapy.dispersion.analytical.two_fluid_ import two_fluid
from plasmapy.dispersion.numerical.hollweg_ import hollweg
from plasmapy.formulary import (
    Alfven_speed,
    gyrofrequency,
    inertial_length,
    ion_sound_speed,
    plasma_frequency,
)
from plasmapy.particles import Particle
from plasmapy.utils.exceptions import PhysicsWarning

warnings.filterwarnings(
    action="ignore",
    category=PhysicsWarning,
    module="plasmapy.dispersion.numerical.hollweg_",
)

plt.rcParams["figure.figsize"] = [10.5, 0.56 * 10.5]
Wave propagating at nearly 90 degrees

Below we define the required parameters to compute the wave frequencies.

[2]:
# define input parameters
inputs = {
    "k": np.logspace(-7, -2, 300) * u.rad / u.m,
    "theta": 89 * u.deg,
    "n_i": 5 * u.cm**-3,
    "B": 1.10232e-8 * u.T,
    "T_e": 1.6e6 * u.K,
    "T_i": 4.0e5 * u.K,
    "ion": Particle("p+"),
}

# a few useful plasma parameters
params = {
    "n_e": inputs["n_i"] * abs(inputs["ion"].charge_number),
    "cs": ion_sound_speed(
        inputs["T_e"],
        inputs["T_i"],
        inputs["ion"],
    ),
    "va": Alfven_speed(
        inputs["B"],
        inputs["n_i"],
        ion=inputs["ion"],
    ),
    "wci": gyrofrequency(inputs["B"], inputs["ion"]),
}
params["lpe"] = inertial_length(params["n_e"], "e-")
params["wpe"] = plasma_frequency(params["n_e"], "e-")

# compute
omegas1 = hollweg(**inputs)
omegas2 = two_fluid(**inputs)
np.set_printoptions(precision=4, threshold=20)

The computed wave frequencies (rad/s) are returned in a dictionary with the keys representing wave modes and the values (instances of Astropy Quantity) being the frequencies. Since our inputs were a 1D arrays of of \(k\)’s, the computed wave frequencies will be a 1D array of size equal to the size of the \(k\) array.

[3]:
(list(omegas1.keys()), omegas1["fast_mode"], omegas1["fast_mode"].shape)
[3]:
(['fast_mode', 'alfven_mode', 'acoustic_mode'],
 <Quantity [1.8619e-02+0.j, 1.9350e-02+0.j, 2.0109e-02+0.j, ...,
            1.0663e+03+0.j, 1.1072e+03+0.j, 1.1498e+03+0.j] rad / s>,
 (300,))

Let’s plot the results of each wave mode.

[4]:
fs = 14  # default font size
figwidth, figheight = plt.rcParams["figure.figsize"]
figheight = 1.6 * figheight
fig = plt.figure(figsize=[figwidth, figheight])

# normalize data
k_prime = inputs["k"] * params["lpe"]

# define colormap
cmap = plt.get_cmap("viridis")
slicedCM = cmap(np.linspace(0, 0.6, 3))

# plot
(p1,) = plt.plot(
    k_prime,
    np.real(omegas1["fast_mode"] / params["wpe"]),
    "--",
    c=slicedCM[0],
    ms=1,
    label="Fast",
)
ax = plt.gca()
(p2,) = ax.plot(
    k_prime,
    np.real(omegas1["alfven_mode"] / params["wpe"]),
    "--",
    c=slicedCM[1],
    ms=1,
    label="Alfvén",
)
(p3,) = ax.plot(
    k_prime,
    np.real(omegas1["acoustic_mode"] / params["wpe"]),
    "--",
    c=slicedCM[2],
    ms=1,
    label="Acoustic",
)
(p4,) = plt.plot(
    k_prime,
    np.real(omegas2["fast_mode"] / params["wpe"]),
    c=slicedCM[0],
    ms=1,
    label="Fast",
)
ax = plt.gca()
(p5,) = ax.plot(
    k_prime,
    np.real(omegas2["alfven_mode"] / params["wpe"]),
    c=slicedCM[1],
    ms=1,
    label="Alfvén",
)
(p6,) = ax.plot(
    k_prime,
    np.real(omegas2["acoustic_mode"] / params["wpe"]),
    c=slicedCM[2],
    ms=1,
    label="Acoustic",
)

# adjust axes
ax.set_xlabel(r"$kc / \omega_{pe}$", fontsize=fs)
ax.set_ylabel(r"$Re(\omega / \omega_{pe})$", fontsize=fs)
ax.set_yscale("log")
ax.set_xscale("log")
ax.tick_params(
    which="both",
    direction="in",
    width=1,
    labelsize=fs,
    right=True,
    length=5,
)

# annotate
styles = ["-", "--"]
s_labels = ["Hollweg", "Bellan"]
ax2 = ax.twinx()
for ss, lab in enumerate(styles):
    ax2.plot(np.NaN, np.NaN, ls=styles[ss], label=s_labels[ss], c="black")
ax2.get_yaxis().set_visible(False)
ax2.legend(fontsize=14, loc="lower right")

text1 = (
    rf"$c_s^2/v_A^2 = {params['cs'] ** 2 / params['va'] ** 2:.1f} \qquad "
    f"\\theta = {inputs['theta'].value:.0f}"
    "^{\\circ}$"
)
text2 = (
    "All 3 wave modes are plotted for Hollweg's relation (solid lines) and Bellan's solut- \n"
    f"ion (dashed lines) with $\\beta = 2.0$ and $\\theta = {inputs['theta'].value:.0f}"
    "^{\\circ}$."
)
plt.figtext(-0.08, -0.18, text2, ha="left", transform=ax.transAxes, fontsize=15.5)
ax.text(0.57, 0.95, text1, transform=ax.transAxes, fontsize=18)
ax.legend(handles=[p4, p1, p5, p2, p6, p3], fontsize=14, ncol=3, loc="upper left")
[4]:
<matplotlib.legend.Legend at 0x7fb993d41100>
_images/notebooks_dispersion_hollweg_dispersion_7_1.png
Hollweg 1999 and Bellan 2012 comparison

Figure 1 of Bellan 2012 chooses parameters such that \(\beta = 0.4\) and \(\Lambda = 0.4\). Below we define parameters to approximate Bellan’s assumptions.

[5]:
# define input parameters
inputs = {
    "B": 400e-4 * u.T,
    "ion": Particle("He+"),
    "n_i": 6.358e19 * u.m**-3,
    "T_e": 20 * u.eV,
    "T_i": 10 * u.eV,
    "theta": np.linspace(0, 90) * u.deg,
    "k": (2 * np.pi * u.rad) / (0.56547 * u.m),
}

# a few useful plasma parameters
params = {
    "n_e": inputs["n_i"] * abs(inputs["ion"].charge_number),
    "cs": ion_sound_speed(inputs["T_e"], inputs["T_i"], inputs["ion"]),
    "wci": gyrofrequency(inputs["B"], inputs["ion"]),
    "va": Alfven_speed(inputs["B"], inputs["n_i"], ion=inputs["ion"]),
}
params["beta"] = (params["cs"] / params["va"]).value ** 2
params["wpe"] = plasma_frequency(params["n_e"], "e-")
params["Lambda"] = (inputs["k"] * params["va"] / params["wci"]).value ** 2

(params["beta"], params["Lambda"])
[5]:
(0.4000832135717194, 0.4000017351804854)
[6]:
# compute omegas for Bellan and Hollweg
bellan_omegas = two_fluid(**inputs)
hollweg_omegas = hollweg(**inputs)
[7]:
# generate data for Bellan curves
bellan_plt_vals = {}
for mode, arr in bellan_omegas.items():
    norm = (np.absolute(arr) / (inputs["k"] * params["va"])).value ** 2
    bellan_plt_vals[mode] = {
        "x": norm * np.sin(inputs["theta"].to(u.rad).value),
        "y": norm * np.cos(inputs["theta"].to(u.rad).value),
    }

# generate data for Hollweg curves
hollweg_plt_vals = {}
for mode, arr in hollweg_omegas.items():
    norm = (np.absolute(arr) / (inputs["k"] * params["va"])).value ** 2
    hollweg_plt_vals[mode] = {
        "x": norm * np.sin(inputs["theta"].to(u.rad).value),
        "y": norm * np.cos(inputs["theta"].to(u.rad).value),
    }

Let’s plot all 3 wave modes

[8]:
fs = 14  # default font size
figwidth, figheight = plt.rcParams["figure.figsize"]
figheight = 1.6 * figheight
fig = plt.figure(figsize=[figwidth, figheight])

# define colormap
cmap = plt.get_cmap("viridis")
slicedCM = cmap(np.linspace(0, 0.6, 3))

# Bellan Fast mode
(p1,) = plt.plot(
    bellan_plt_vals["fast_mode"]["x"],
    bellan_plt_vals["fast_mode"]["y"],
    "--",
    c=slicedCM[0],
    linewidth=2,
    label="Fast",
)
ax = plt.gca()

# adjust axes
ax.set_xlabel(r"$(\omega / k v_{A})^{2} \, \sin \theta$", fontsize=fs)
ax.set_ylabel(r"$(\omega / k v_{A})^{2} \, \cos \theta$", fontsize=fs)
ax.set_xlim(0.0, 2.0)
ax.set_ylim(0.0, 2.0)
for spine in ax.spines.values():
    spine.set_linewidth(2)
ax.minorticks_on()
ax.tick_params(which="both", labelsize=fs, width=2)
ax.tick_params(which="major", length=10)
ax.tick_params(which="minor", length=5)
ax.xaxis.set_major_locator(MultipleLocator(0.5))
ax.xaxis.set_minor_locator(MultipleLocator(0.1))
ax.yaxis.set_major_locator(MultipleLocator(0.5))
ax.yaxis.set_minor_locator(MultipleLocator(0.1))

# Bellan Alfven mode
(p2,) = plt.plot(
    bellan_plt_vals["alfven_mode"]["x"],
    bellan_plt_vals["alfven_mode"]["y"],
    "--",
    c=slicedCM[1],
    linewidth=2,
    label="Alfvén",
)

# Bellan Acoustic mode
(p3,) = plt.plot(
    bellan_plt_vals["acoustic_mode"]["x"],
    bellan_plt_vals["acoustic_mode"]["y"],
    "--",
    c=slicedCM[2],
    linewidth=2,
    label="Acoustic",
)

# Hollweg Fast mode
(p4,) = plt.plot(
    hollweg_plt_vals["fast_mode"]["x"],
    hollweg_plt_vals["fast_mode"]["y"],
    c=slicedCM[0],
    linewidth=2,
    label="Fast",
)

# Hollweg Alfven mode
(p5,) = plt.plot(
    hollweg_plt_vals["alfven_mode"]["x"],
    hollweg_plt_vals["alfven_mode"]["y"],
    c=slicedCM[1],
    linewidth=2,
    label="Alfvén",
)

# Hollweg Acoustic mode
(p6,) = plt.plot(
    hollweg_plt_vals["acoustic_mode"]["x"],
    hollweg_plt_vals["acoustic_mode"]["y"],
    c=slicedCM[2],
    linewidth=2,
    label="Acoustic",
)

# annotations
r = np.linspace(0, 2, 200)
X = (r**2) * np.cos(0.1)
Y = (r**2) * np.sin(0.1)
plt.plot(X, Y, color="0.3")
ax.fill_between(X, 0, Y, hatch="\\\\", color="0.7", alpha=0.5)

# style legend
styles = ["-", "--"]
s_labels = ["Hollweg", "Bellan"]
ax2 = ax.twinx()
for ss, lab in enumerate(styles):
    ax2.plot(np.NaN, np.NaN, ls=styles[ss], label=s_labels[ss], c="black")
ax2.get_yaxis().set_visible(False)
ax2.legend(fontsize=17, loc="center right")

ax.legend(handles=[p4, p1, p5, p2, p6, p3], fontsize=16, ncol=3, loc="upper right")
plt.figtext(
    1.42,
    0.19,
    "$|\\theta - \\pi / 2| > 0.1 \\uparrow$",
    rotation=5.5,
    fontsize=20,
    transform=ax.transData,
)

# plot caption
txt = (
    "Fig 1. of Bellan 2012 shown with Hollweg's relation (solid lines) and Bellan's anal-\n"
    "ytic solution (dashed lines). All 3 wave modes are plotted for $\\beta= 0.4$ and $\\Lambda= 0.4$.\n"
    "The shaded region shows where $\\theta \\approx \\pi / 2$.\n"
)

plt.figtext(-0.1, -0.29, txt, ha="left", transform=ax.transAxes, fontsize=16);
_images/notebooks_dispersion_hollweg_dispersion_13_0.png
Reproduce Figure 2 from Hollweg 1999

Figure 2 of Hollweg 1999 plots the Alfvén mode and chooses parameters such that \(\beta = 1/20, 1/2, 2, 1/2000\). Below we define parameters to approximate these values.

[9]:
# define input parameters
# beta = 1/20
inputs0 = {
    "k": np.logspace(-7, -2, 400) * u.rad / u.m,
    "theta": 90 * u.deg,
    "n_i": 5 * u.cm**-3,
    "B": 6.971e-8 * u.T,
    "T_e": 1.6e6 * u.K,
    "T_i": 4.0e5 * u.K,
    "ion": Particle("p+"),
}
# beta = 1/2
inputs1 = {
    **inputs0,
    "B": 2.205e-8 * u.T,
}
# beta = 2
inputs2 = {
    **inputs0,
    "B": 1.10232e-8 * u.T,
}
# beta = 1/2000
inputs3 = {
    **inputs0,
    "B": 6.97178e-7 * u.T,
}

# a few useful plasma parameters

# parameters corresponding to inputs0
params0 = {
    "n_e": inputs0["n_i"] * abs(inputs0["ion"].charge_number),
    "cs": ion_sound_speed(
        inputs0["T_e"],
        inputs0["T_i"],
        inputs0["ion"],
    ),
    "va": Alfven_speed(
        inputs0["B"],
        inputs0["n_i"],
        ion=inputs0["ion"],
    ),
    "wci": gyrofrequency(inputs0["B"], inputs0["ion"]),
}
params0["lpe"] = inertial_length(params0["n_e"], "e-")
params0["wpe"] = plasma_frequency(params0["n_e"], "e-")
params0["L"] = params0["cs"] / abs(params0["wci"])

# parameters corresponding to inputs1
params1 = {
    "n_e": inputs1["n_i"] * abs(inputs1["ion"].charge_number),
    "cs": ion_sound_speed(
        inputs1["T_e"],
        inputs1["T_i"],
        inputs1["ion"],
    ),
    "va": Alfven_speed(
        inputs1["B"],
        inputs1["n_i"],
        ion=inputs1["ion"],
    ),
    "wci": gyrofrequency(inputs1["B"], inputs1["ion"]),
}
params1["lpe"] = inertial_length(params1["n_e"], "e-")
params1["wpe"] = plasma_frequency(params1["n_e"], "e-")
params1["L"] = params1["cs"] / abs(params1["wci"])

# parameters corresponding to inputs2
params2 = {
    "n_e": inputs2["n_i"] * abs(inputs2["ion"].charge_number),
    "cs": ion_sound_speed(
        inputs2["T_e"],
        inputs2["T_i"],
        inputs2["ion"],
    ),
    "va": Alfven_speed(
        inputs2["B"],
        inputs2["n_i"],
        ion=inputs2["ion"],
    ),
    "wci": gyrofrequency(inputs2["B"], inputs2["ion"]),
}
params2["lpe"] = inertial_length(params2["n_e"], "e-")
params2["wpe"] = plasma_frequency(params2["n_e"], "e-")
params2["L"] = params2["cs"] / abs(params2["wci"])

# parameters corresponding to inputs3
params3 = {
    "n_e": inputs3["n_i"] * abs(inputs3["ion"].charge_number),
    "cs": ion_sound_speed(
        inputs3["T_e"],
        inputs3["T_i"],
        inputs3["ion"],
    ),
    "va": Alfven_speed(
        inputs3["B"],
        inputs3["n_i"],
        ion=inputs3["ion"],
    ),
    "wci": gyrofrequency(inputs3["B"], inputs3["ion"]),
}
params3["lpe"] = inertial_length(params3["n_e"], "e-")
params3["wpe"] = plasma_frequency(params3["n_e"], "e-")
params3["L"] = params3["cs"] / abs(params3["wci"])

# confirm beta values
beta_vals = [
    (params0["cs"] / params0["va"]).value ** 2,
    (params1["cs"] / params1["va"]).value ** 2,
    (params2["cs"] / params2["va"]).value ** 2,
    (params3["cs"] / params3["va"]).value ** 2,
]
print(
    f"1/{1/beta_vals[0]:.4f}, "
    f"1/{1/beta_vals[1]:.4f}, "
    f"{beta_vals[2]:.4f}, "
    f"1/{1/beta_vals[3]:.4f}"
)
1/19.9955, 1/2.0006, 2.0001, 1/1999.9987

Figure 2 of Hollweg 1999 plots over some values that lie outside the valid regime which results in the PhysicsWarning’s below being raised.

[10]:
# compute omegas

# show warnings once
with warnings.catch_warnings():
    warnings.filterwarnings("once")
    omegas0 = hollweg(**inputs0)

omegas1 = hollweg(**inputs1)
omegas2 = hollweg(**inputs2)
omegas3 = hollweg(**inputs3)
/home/docs/checkouts/readthedocs.org/user_builds/plasmapy/envs/latest/lib/python3.12/site-packages/plasmapy/dispersion/numerical/hollweg_.py:313: PhysicsWarning: This solver is valid in the low-beta regime, c_s/v_A ≪ 1 according to Bellan, 2012, Sec. 1.7 (see documentation for DOI). A c_s/v_A value of 0.22 was calculated which may affect the validity of the solution.
  warnings.warn(
/home/docs/checkouts/readthedocs.org/user_builds/plasmapy/envs/latest/lib/python3.12/site-packages/plasmapy/dispersion/numerical/hollweg_.py:337: PhysicsWarning: This solver is valid in the regime ω/ω_ci ≪ 1. A ω value of 6798.41+0.00j and a ω/ω_ci value of 1018.12+0.00j were calculated which may affect the validity of the solution.
  warnings.warn(
[11]:
# define important quantities for plotting
theta = inputs0["theta"].to(u.rad).value

kz = np.cos(theta) * inputs0["k"]
ky = np.sin(theta) * inputs0["k"]

# normalize data
k_prime = [
    params0["L"] * ky,
    params1["L"] * ky,
    params2["L"] * ky,
    params3["L"] * ky,
]
big_omega = [
    abs(omegas0["alfven_mode"] / (params0["va"] * kz)),
    abs(omegas1["alfven_mode"] / (params1["va"] * kz)),
    abs(omegas2["alfven_mode"] / (params2["va"] * kz)),
    abs(omegas3["alfven_mode"] / (params3["va"] * kz)),
]
[12]:
fs = 14  # default font size
figwidth, figheight = plt.rcParams["figure.figsize"]
figheight = 1.6 * figheight
fig = plt.figure(figsize=[figwidth, figheight])

# define colormap
cmap = plt.get_cmap("viridis")
slicedCM = cmap(np.linspace(0, 0.6, 4))

# plot
plt.plot(
    k_prime[0],
    big_omega[0],
    c=slicedCM[0],
    ms=1,
)
ax = plt.gca()
ax.plot(
    k_prime[1],
    big_omega[1],
    c=slicedCM[1],
    ms=1,
)
ax.plot(
    k_prime[2],
    big_omega[2],
    c=slicedCM[2],
    ms=1,
)
ax.plot(
    k_prime[3],
    big_omega[3],
    c=slicedCM[3],
    ms=1,
)

# adjust axes
ax.set_xlabel(r"$k_{y}L$", fontsize=fs)
ax.set_ylabel(r"$|\Omega|$", fontsize=fs)
ax.set_yscale("linear")
ax.set_xscale("linear")
ax.set_xlim(0, 1.5)
ax.set_ylim(0.96, 1.8)
ax.tick_params(
    which="both",
    direction="in",
    width=1,
    labelsize=fs,
    right=True,
    length=5,
)

# add labels for beta
plt.text(1.51, 1.75, "1/20$=\\beta$", c=slicedCM[0], fontsize=fs)
plt.text(1.51, 1.63, "1/2", c=slicedCM[1], fontsize=fs)
plt.text(1.51, 1.44, "2", c=slicedCM[2], fontsize=fs)
plt.text(1.51, 0.97, "1/2000", c=slicedCM[3], fontsize=fs)

# plot caption
txt = (
    "Fig. 2 Hollweg 1999 reproduction. The Alfvén mode is plotted for an electron\n"
    "-proton plasma with $\\beta=$ 1/20, 1/2, 2, 1/2000, $|\\Omega|=|\\omega / k_{z} v_{A}|$, and $L=c_{s}$ $/ |\\omega_{ci}|$.\n"
)

plt.figtext(-0.08, -0.21, txt, ha="left", transform=ax.transAxes, fontsize=16);
_images/notebooks_dispersion_hollweg_dispersion_19_0.png

Note

Hollweg takes \(k_{\perp}=k_{y}\) for k propagating in the \(y\)-\(z\) plane as in the plot above. Contrarily, Bellan takes \(k_{\perp}=k_{x}\) for k propagating in the \(x\)-\(z\) plane.

This page was generated by nbsphinx from docs/notebooks/dispersion/stix_dispersion.ipynb.
Interactive online version: Binder badge.

Stix Dispersion Solver

This notebook details the functionality of the stix() function. This is an analytical solution of equation 8 in Bellan 2012, the function is defined by Stix 1992 in §1.2 to be:

\[(S \sin^2(θ) + P \cos^2(θ)) \left ( \frac{ck}{ω} \right)^4 - [ RL \sin^2(θ) + PS (1 + \cos^2(θ)) ] \left ( \frac{ck}{ω} \right)^2 + PRL = 0\]

where,

\[\begin{split}\mathbf{B}_0 = B_0 \mathbf{\hat{z}} \cos θ = \frac{k_z}{k} \\ \mathbf{k} = k_{\rm x} \hat{x} + k_{\rm z} \hat{z}\end{split}\]
\[S = 1 - \sum_s \frac{ω^2_{p,s}}{ω^2 - ω^2_{c,s}}\hspace{2.5cm} P = 1 - \sum_s \frac{ω^2_{p,s}}{ω^2}\hspace{2.5cm} D = \sum_s \frac{ω_{c,s}}{ω} \frac{ω^2_{p,s}}{ω^2 - ω_{c,s}^2}\]
\[R = S + D \hspace{1cm} L = S - D\]

\(ω\) is the wave frequency, \(k\) is the wavenumber, \(θ\) is the wave propagation angle with respect to the background magnetic field \(\mathbf{B}_0\), \(s\) corresponds to plasma species, \(ω_{p,s}\) is the plasma frequency of species and \(ω_{c,s}\) is the gyrofrequency of species \(s\).

Note

The derivation of this dispersion relation assumed:

  • zero temperature for all plasma species (\(T_s=0\))

  • quasineutrality

  • a uniform background magnetic field \(\mathbf{B_0} = B_0 \mathbf{\hat{z}}\)

  • no D.C. electric field \(\mathbf{E_0}=0\)

  • zero-order quantities for all plasma parameters (densities, electric-field, magnetic field, particle speeds, etc.) are constant in time and space

  • first-order perturbations in plasma parameters vary like \(\sim e^{\left [ i (\textbf{k}\cdot\textbf{r} - \omega t)\right ]}\)

Due to the cold plasma assumption, this equation is valid for all \(ω\) and \(k\) given \(\frac{ω}{k_z} ≫ v_{Th}\) for all thermal speeds \(v_{Th}\) of all plasma species and \(k_x r_L ≪ 1\) for all gyroradii \(r_L\) of all plasma species. The relation predicts \(k → 0\) when any one of P, R or L vanish (cutoffs) and \(k → ∞\) for perpendicular propagation during wave resonance \(S → 0\).

Contents
  1. Wave Normal to the Surface

  2. Comparison with Bellan

[1]:
import functools

import astropy.units as u
import matplotlib.pyplot as plt
import numpy as np
import scipy
from astropy.constants.si import c

from plasmapy.dispersion.analytical.stix_ import stix
from plasmapy.dispersion.analytical.two_fluid_ import two_fluid
from plasmapy.formulary import speeds
from plasmapy.particles import Particle

plt.rcParams["figure.figsize"] = [10.5, 0.56 * 10.5]
Wave Normal to the Surface

To calculate the normal surface waves propagating through a magnetized uniform cold plasma. The wave which is normal to the surface, is the locus of the phase velocity \(\textbf{v}_{phase} = \frac{ω}{k} \, \hat{k}\) where \(\hat{k} = \frac{\textbf{k}}{k}\). The equation for the wave normal surface can be derived via the prior equations, resulting in the form of

\[A u^4 + B u^2 + C = 0\]

where \(u = \frac{ω}{ck}\). To begin we define the required parameters to compute the wave numbers.

[2]:
# define input parameters
inputs_1 = {
    "theta": np.linspace(0, np.pi, 50) * u.rad,
    "ions": Particle("p"),
    "n_i": 1e12 * u.cm**-3,
    "B": 0.43463483142776164 * u.T,
    "w": 41632.94534008216 * u.rad / u.s,
}

# define a meshgrid based on the number of theta values
omegas, thetas = np.meshgrid(
    inputs_1["w"].value, inputs_1["theta"].value, indexing="ij"
)
omegas = np.dstack((omegas,) * 4).squeeze()
thetas = np.dstack((thetas,) * 4).squeeze()

# compute k values
k = stix(**inputs_1)

The computed wavenumbers in units (rad/m) are returned in a dictionary (shape \(N × M × 4\)), with the keys representing \(θ\) and the values (instances of Astropy Quantity) being the wavenumbers. The first dimension maps to the \(w\) array, the second dimension maps to the \(θ\) array, and the third dimension maps to the four roots of the Stix polynomial.

  • \(k[0]\) is the square root of the positive quadratic solution

  • \(k[1] = -k[0]\)

  • \(k[2]\) is the square root of the negative quadratic solution

  • \(k[3] = -k[2]\)

Below the values for \(u_x\) and \(u_z\) are calculated.

[3]:
# calculate ux and uz

u_v = {}

mask = np.imag(k) == 0

va_1 = speeds.va_(inputs_1["B"], inputs_1["n_i"], ion=inputs_1["ions"])
for arr in k:
    val = 0
    for item in arr:
        val = val + item**2
    norm = (np.sqrt(val) * va_1 / inputs_1["w"]).value ** 2
    u_v = {
        "ux": norm * omegas[mask] * np.sin(thetas[mask]) / (k.value[mask] * c.value),
        "uz": norm * omegas[mask] * np.cos(thetas[mask]) / (k.value[mask] * c.value),
    }

Let’s plot the results.

[4]:
# plot the results

fs = 14  # default font size
figwidth, figheight = plt.rcParams["figure.figsize"]
figheight = 1.6 * figheight
fig = plt.figure(figsize=[figwidth, figheight])

plt.scatter(
    u_v["ux"],
    u_v["uz"],
    label="Stix: Fig. 1-1",
)

# adjust axes
plt.xlabel(r"$u_x$", fontsize=fs)
plt.ylabel(r"$u_z$", fontsize=fs)

pad = 1.25
plt.ylim(min(u_v["uz"]) * pad, max(u_v["uz"]) * pad)
plt.xlim(min(u_v["ux"]) * pad, max(u_v["ux"]) * pad)

plt.tick_params(
    which="both",
    direction="in",
    labelsize=fs,
    right=True,
    length=5,
)

# plot caption
txt = (
    "Fig. 1-1: Waves normal surfaces, parameters represent \nthe shear Alfvén wave "
    "and the compressional Alfvén wave. \nThe zero-order magnetic field is directed along "
    " the z-axis."
)

plt.figtext(0.25, -0.04, txt, ha="left", fontsize=fs)
plt.legend(loc="upper left", markerscale=1, fontsize=fs)

plt.show()
/home/docs/checkouts/readthedocs.org/user_builds/plasmapy/envs/latest/lib/python3.12/site-packages/matplotlib/cbook.py:1699: ComplexWarning: Casting complex values to real discards the imaginary part
  return math.isfinite(val)
/home/docs/checkouts/readthedocs.org/user_builds/plasmapy/envs/latest/lib/python3.12/site-packages/matplotlib/collections.py:194: ComplexWarning: Casting complex values to real discards the imaginary part
  offsets = np.asanyarray(offsets, float)
/home/docs/checkouts/readthedocs.org/user_builds/plasmapy/envs/latest/lib/python3.12/site-packages/matplotlib/transforms.py:2855: ComplexWarning: Casting complex values to real discards the imaginary part
  vmin, vmax = map(float, [vmin, vmax])
_images/notebooks_dispersion_stix_dispersion_10_1.png

Here we can define the parameters for all the plots in Stix 1992 and then reproduce them in the same fashion.

[5]:
# define inputs
inputs_2 = {
    "theta": np.linspace(0, np.pi, 100) * u.rad,
    "ions": Particle("p"),
    "n_i": 1e12 * u.cm**-3,
    "B": 0.434 * u.T,
    "w": (37125810) * u.rad / u.s,
}

inputs_3 = {
    "theta": np.linspace(0, np.pi, 100) * u.rad,
    "ions": Particle("p"),
    "n_i": 1e12 * u.cm**-3,
    "B": 0.434534 * u.T,
    "w": (2 * 10**10) * u.rad / u.s,
}

inputs_4 = {
    "theta": np.linspace(0, np.pi, 100) * u.rad,
    "ions": Particle("p"),
    "n_i": 1e12 * u.cm**-3,
    "B": 0.434600 * u.T,
    "w": (54 * 10**9) * u.rad / u.s,
}

inputs_5 = {
    "theta": np.linspace(0, np.pi, 100) * u.rad,
    "ions": Particle("p"),
    "n_i": 1e12 * u.cm**-3,
    "B": 0.434634 * u.T,
    "w": (58 * 10**9) * u.rad / u.s,
}

# define a list of all inputs
stix_inputs = [inputs_1, inputs_2, inputs_3, inputs_4, inputs_5]

Following on, the same method implemented on the first set of input parameters can be implemented on the rest. Afterwards, the result for all inputs can be plotted.

[6]:
stix_plt = {}

ux = {}
uz = {}

for i in range(len(stix_inputs)):
    stix_plt[i] = {}


for i, inpt in enumerate(stix_inputs):
    omegas, thetas = np.meshgrid(inpt["w"].value, inpt["theta"].value, indexing="ij")
    omegas = np.dstack((omegas,) * 4).squeeze()
    thetas = np.dstack((thetas,) * 4).squeeze()

    k = stix(**inpt)

    mask = np.imag(k) == 0

    va = speeds.va_(inpt["B"], inpt["n_i"], ion=inpt["ions"])

    for arr in k:
        val = 0
        for item in arr:
            val = val + item**2
        norm = (np.sqrt(val) * va / inpt["w"]).value ** 2
        stix_plt[i] = {
            "ux": norm
            * omegas[mask]
            * np.sin(thetas[mask])
            / (k.value[mask] * c.value),
            "uz": norm
            * omegas[mask]
            * np.cos(thetas[mask])
            / (k.value[mask] * c.value),
        }

Plot the results.

[7]:
# create figure
fig, axs = plt.subplots(2, 2, figsize=[figwidth, figheight])


for i in range(2):
    for j in range(2):
        axs[i, j].scatter(
            stix_plt[i + 2 * j + 1]["ux"], stix_plt[i + 2 * j + 1]["uz"], label="dfwd"
        )
        axs[i, j].set_title("Stix: Fig. 1-" + str(i + 2 * j + 2), fontsize=fs)

        # adjust axes
        axs[i, j].set(
            ylabel=r"$u_z$",
            xlabel=r"$u_z$",
        )

        pad = 1.25
        axs[i, j].set_ylim(
            min(stix_plt[i + 2 * j + 1]["uz"]) * pad,
            max(stix_plt[i + 2 * j + 1]["uz"]) * pad,
        )
        axs[i, j].set_xlim(
            min(stix_plt[i + 2 * j + 1]["ux"]) * pad,
            max(stix_plt[i + 2 * j + 1]["ux"]) * pad,
        )

        axs[i, j].tick_params(
            which="both",
            direction="in",
            labelsize=fs,
            right=True,
            length=5,
        )


# plot caption
txt = "Wave normal surface reproduced from Stix."

plt.tight_layout()
plt.figtext(0.35, -0.02, txt, ha="left", fontsize=fs)


plt.show()
_images/notebooks_dispersion_stix_dispersion_16_0.png
Comparison with Bellan

Below we run a comparison between the solution provided in Bellan 2012 and our own solutions computed from stix(). To begin we first create a function that reproduces the Bellan plot.

[8]:
def norm_bellan_plot(**inputs):
    """Reproduce plot of Bellan dispersion relation."""
    w = inputs["w"]
    k = inputs["k"]
    theta = inputs["theta"]

    if w.shape == k.shape or w.size == 1 or k.size == 1:
        pass
    elif w.ndim > 2 or k.ndim > 2 or k.shape[0] != w.shape[0]:
        raise ValueError
    elif k.ndim > w.ndim:
        w = np.repeat(w[..., np.newaxis], k.shape[1], axis=1)
    elif k.ndim < w.ndim:
        k = np.repeat(k[..., np.newaxis], w.shape[1], axis=1)

    if theta.ndim != 1 or theta.size != w.shape[-1]:
        raise ValueError

    try:
        ion = inputs["ion"]
    except KeyError:
        ion = inputs["ions"][0]
    va = speeds.va_(inputs["B"], inputs["n_i"], ion=ion)

    mag = ((w / (k * va)).to(u.dimensionless_unscaled).value) ** 2
    theta = theta.to(u.radian).value

    xnorm = mag * np.sin(theta)
    ynorm = mag * np.cos(theta)

    return np.array([xnorm, ynorm])

Now we can solve the Bellan solution for identical plasma parameters, in the first instance a cold plasma limit of \(k_B T_e = 0.1\) eV and \(k_B T_p = 0.1\) eV are assumed. In the second instance a warm plasma limit of \(k_B T_e = 20\) eV and \(k_B T_p = 10\) eV are assumed.

[9]:
# defining all inputs
base_inputs = {
    "k": (2 * np.pi * u.rad) / (0.56547 * u.m),
    "theta": np.linspace(0, 0.49 * np.pi, 50) * u.rad,
    "ion": Particle("He-4 1+"),
    "n_i": 6.358e19 * u.m**-3,
    "B": 400e-4 * u.T,
}

hot_inputs = {
    **base_inputs,
    "T_e": 20 * u.eV,
    "T_i": 10 * u.eV,
}

cold_inputs = {
    **base_inputs,
    "T_e": 0.1 * u.eV,
    "T_i": 0.1 * u.eV,
}

# calculating the solution from two fluid
w_tf_hot = two_fluid(**hot_inputs)
w_tf_cold = two_fluid(**cold_inputs)
[10]:
plt_tf_hot = {}
for key, val in w_tf_hot.items():
    plt_tf_hot[key] = norm_bellan_plot(**{**hot_inputs, "w": val})

plt_tf_cold = {}
for key, val in w_tf_cold.items():
    plt_tf_cold[key] = norm_bellan_plot(**{**cold_inputs, "w": val})

Stix needs to recalculated using the Bellan inputs as a base.

[11]:
stix_inputs = {**base_inputs, "ions": [base_inputs["ion"]]}
del stix_inputs["k"]
del stix_inputs["ion"]

Bellan fixes \(k\) and then calculates \(ω\) for each mode and propagation angle \(θ\). This means we cannot simply take solutions from Bellan and get corresponding \(k\) values via Stix. In order to solve this problem we need to create a version of stix() that can be optimized by scipy.optimize.root_scalar().

[12]:
# partially bind plasma parameter keywords to stix()
_opt = stix_inputs.copy()
del _opt["theta"]
stix_partial = functools.partial(stix, **_opt)


def stix_optimize(w, theta, mode, k_expected):
    """Version of `stix` that can be optimized by `scipy.optimize.root_scalar`."""
    w = np.abs(w)
    results = stix_partial(w=w * u.rad / u.s, theta=theta * u.rad).value

    # only consider real and positive solutions
    real_mask = np.where(np.imag(results) == 0, True, False)
    pos_mask = np.where(np.real(results) > 0, True, False)
    mask = np.logical_and(real_mask, pos_mask)

    # get the correct k to compare
    if np.count_nonzero(mask) == 1:
        results = np.real(results[mask][0])
    elif mode == "fast_mode":
        # fast_mode has a larger phase velocity than
        # the alfven_mode, thus take the smaller k-value
        results = np.min(np.real(results[mask]))
    else:  # alfven_mode
        results = np.max(np.real(results[mask]))

    return results - k_expected

Let’s use the Cold case Bellan solution to solve for the Stix solution. Note only the fast_mode and slow_mode solutions are being used to seed the Stix solution because the acoustic_mode disappears in the cold plasma limit.

[13]:
theta_arr = cold_inputs["theta"].value
k_expected = base_inputs["k"].value
k_stix = {}
w_stix = {}
for mode in ("fast_mode", "alfven_mode"):
    w_arr = w_tf_cold[mode].value
    k_stix[mode] = []
    w_stix[mode] = []
    for ii in range(w_arr.size):
        w_guess = w_arr[ii]
        _theta = theta_arr[ii]
        result = scipy.optimize.root_scalar(
            stix_optimize,
            args=(_theta, mode, k_expected),
            x0=w_guess,
            x1=w_guess + 1e2,
        )

        # append the wavefrequency (result.root) that
        # corresponded to stix() returning k_expected
        w_stix[mode].append(np.real(result.root))

        # double check and store the k-value
        _k = stix(
            **{
                **stix_inputs,
                "w": np.real(result.root) * u.rad / u.s,
                "theta": theta_arr[ii] * u.rad,
            }
        ).value
        real_mask = np.where(np.imag(_k) == 0, True, False)
        pos_mask = np.where(np.real(_k) > 0, True, False)
        mask = np.logical_and(real_mask, pos_mask)

        _k = np.real(_k[mask])
        mask = np.isclose(_k, base_inputs["k"].value)
        k_stix[mode].append(_k[mask][0])

    k_stix[mode] = np.array(k_stix[mode])
    w_stix[mode] = np.array(w_stix[mode])

(
    k_expected,
    k_stix,
    w_stix,
)
[13]:
(11.111438815816198,
 {'fast_mode': array([11.11143882, 11.11143882, 11.11143882, 11.11143882, 11.11143882,
         11.11143882, 11.11143882, 11.11143882, 11.11143882, 11.11143882,
         11.11143882, 11.11143882, 11.11143882, 11.11143882, 11.11143882,
         11.11143882, 11.11143882, 11.11143882, 11.11143882, 11.11143882,
         11.11143882, 11.11143882, 11.11143882, 11.11143882, 11.11143882,
         11.11143882, 11.11143882, 11.11143882, 11.11143882, 11.11143882,
         11.11143882, 11.11143882, 11.11143882, 11.11143882, 11.11143882,
         11.11143882, 11.11143882, 11.11143882, 11.11143882, 11.11143882,
         11.11143882, 11.11143882, 11.11143882, 11.11143882, 11.11143882,
         11.11143882, 11.11143882, 11.11143882, 11.11143882, 11.11143882]),
  'alfven_mode': array([11.11143882, 11.11143882, 11.11143882, 11.11143882, 11.11143882,
         11.11143882, 11.11143882, 11.11143882, 11.11143882, 11.11143882,
         11.11143882, 11.11143882, 11.11143882, 11.11143882, 11.11143882,
         11.11143882, 11.11143882, 11.11143882, 11.11143882, 11.11143882,
         11.11143882, 11.11143882, 11.11143882, 11.11143882, 11.11143882,
         11.11143882, 11.11143882, 11.11143882, 11.11143882, 11.11143882,
         11.11143882, 11.11143882, 11.11143882, 11.11143882, 11.11143882,
         11.11143882, 11.11143882, 11.11143882, 11.11143882, 11.11143882,
         11.11143882, 11.11143882, 11.11143882, 11.11143882, 11.11143882,
         11.11143882, 11.11143882, 11.11143882, 11.11143882, 11.11143882])},
 {'fast_mode': array([832492.67093923, 832225.41983245, 831424.65541629, 830093.35115222,
         828236.48528083, 825861.07442708, 822976.22039535, 819593.16978312,
         815725.38575956, 811388.63092296, 806601.05952407, 801383.31646426,
         795758.6392925 , 789752.95787728, 783394.98447966, 776716.28459426,
         769751.31621087, 762537.42223423, 755114.75797978, 747526.1334238 ,
         739816.74892316, 732033.8043225 , 724225.96571732, 716442.68250546,
         708733.36014995, 701146.41085685, 693728.22349643, 686522.11261265,
         679567.3203566 , 672898.15064463, 666543.30895403, 660525.50346248,
         654861.33618235, 649561.48130428, 644631.11822484, 640070.56408932,
         635876.03837404, 632040.49056475, 628554.42948308, 625406.70603181,
         622585.21659703, 620077.50920212, 617871.28692075, 615954.81221471,
         614317.2217517 , 612948.76434044, 611840.97555855, 610986.80211157,
         610380.68751398, 610018.62874915]),
  'alfven_mode': array([446821.74544455, 446744.68217937, 446512.88191043, 446124.50625464,
         445576.46805747, 444864.39672569, 443982.5899558 , 442923.95223925,
         441679.92081177, 440240.38014901, 438593.56674162, 436725.96676382,
         434622.2104388 , 432264.96845572, 429634.85774751, 426710.36630245,
         423467.80940179, 419881.33259628, 415922.97955985, 411562.84520481,
         406769.33541317, 401509.55354398, 395749.82953427, 389456.39905716,
         382596.22742021, 375137.95611849, 367052.9308445 , 358316.2512548 ,
         348907.7688182 , 338812.95362154, 328023.55691657, 316538.0139173 ,
         304361.55843447, 291506.05239366, 277989.56304713, 263835.7433588 ,
         249073.08337343, 233734.10187313, 217854.54015126, 201472.60653188,
         184628.30480599, 167362.86489671, 149718.28163455, 131736.95832046,
         113461.4457862 ,  94934.26444988,  76197.79573168,  57294.2294464 ,
          38265.55483303,  19153.5842565 ])})

Create normalized arrays for plotting.

[14]:
plt_stix = {}
for key, val in k_stix.items():
    plt_stix[key] = norm_bellan_plot(
        **{**stix_inputs, "k": val * u.rad / u.m, "w": w_stix[key] * u.rad / u.s}
    )

Plot the results.

[15]:
fig = plt.figure(figsize=[figwidth, figheight])

mode = "fast_mode"
plt.plot(
    plt_stix[mode][0, ...],
    plt_stix[mode][1, ...],
    "--",
    linewidth=3,
    label="Fast Mode - Stix",
)
plt.plot(
    plt_tf_cold[mode][0, ...],
    plt_tf_cold[mode][1, ...],
    label="Fast Mode - Bellan Cold Plasma",
)
plt.plot(
    plt_tf_hot[mode][0, ...],
    plt_tf_hot[mode][1, ...],
    label="Fast Mode - Bellan Hot Plasma",
)

mode = "alfven_mode"
plt.plot(
    plt_stix[mode][0, ...],
    plt_stix[mode][1, ...],
    "--",
    linewidth=3,
    label="Alfvén Mode - Stix",
)
plt.plot(
    plt_tf_cold[mode][0, ...],
    plt_tf_cold[mode][1, ...],
    label="Alfvén Mode - Bellan Cold Plasma",
)
plt.plot(
    plt_tf_hot[mode][0, ...],
    plt_tf_hot[mode][1, ...],
    label="Alfvén Mode - Bellan Hot Plasma",
)

plt.legend(fontsize=fs)

plt.xlabel(r"$(ω / k v_A)^2 \, \sin θ$", fontsize=fs)
plt.ylabel(r"$(ω / k v_A)^2 \, \cos θ$", fontsize=fs)
plt.xlim(0.0, 2.0)
plt.ylim(0.0, 2.0);
_images/notebooks_dispersion_stix_dispersion_32_0.png
[ ]:

This page was generated by nbsphinx from docs/notebooks/dispersion/two_fluid_dispersion.ipynb.
Interactive online version: Binder badge.

Dispersion: A Full Two Fluid Solution

This notebook walks through the functionality of the two_fluid() function. This function computes the wave frequencies for given wavenumbers and plasma parameters based on the analytical solution presented by Bellan 2012 to the Stringer 1963 two fluid dispersion relation. The two fluid dispersion equaiton assumes a uniform magnetic field, a zero D.C. electric field, and low-frequency waves \(\omega / k c \ll 1\) which equates to

\[\begin{split}\left( \cos^2 \theta - Q \frac{\omega^2}{k^2 {v_A}^2} \right) \left[ \left( \cos^2 \theta - \frac{\omega^2}{k^2 {c_s}^2} \right) - Q \frac{\omega^2}{k^2 {v_A}^2} \left( 1 - \frac{\omega^2}{k^2 {c_s}^2} \right) \right] \\ = \left(1 - \frac{\omega^2}{k^2 {c_s}^2} \right) \frac{\omega^2}{{\omega_{ci}}^2} \cos^2 \theta\end{split}\]

where

\[Q = 1 + k^2 c^2/{\omega_{pe}}^2\]
\[\cos \theta = \frac{k_z}{k}\]
\[\mathbf{B_o} = B_{o} \mathbf{\hat{z}}\]

\(\omega\) is the wave frequency, \(k\) is the wavenumber, \(v_A\) is the Alfvén velocity, \(c_s\) is the sound speed, \(\omega_{ci}\) is the ion gyrofrequency, and \(\omega_{pe}\) is the electron plasma frequency.

The approach outlined in Section 5 of Bellan 2012 produces exact roots to the above dispersion equation for all three modes (fast, acoustic, and Alfvén) without having to make additional approximations. The following dispersion relation is what the two_fluid() function computes.

\[\frac{\omega}{\omega_{ci}} = \sqrt{ 2 \Lambda \sqrt{-\frac{P}{3}} \cos\left( \frac{1}{3} \cos^{-1}\left( \frac{3q}{2p} \sqrt{-\frac{3}{p}} \right) - \frac{2 \pi}{3}j \right) + \frac{\Lambda A}{3} }\]

where \(j = 0\) represents the fast mode, \(j = 1\) represents the Alfvén mode, and \(j = 2\) represents the acoustic mode. Additionally,

\[p = \frac{3B-A^2}{3} \; , \; q = \frac{9AB-2A^3-27C}{27}\]
\[A = \frac{Q + Q^2 \beta + Q \alpha + \alpha \Lambda}{Q^2} \; , \; B = \alpha \frac{1 + 2 Q \beta + \Lambda \beta}{Q^2} \; , \; C = \frac{\alpha^2 \beta}{Q^2}\]
\[\alpha = \cos^2 \theta \; , \; \beta = \left( \frac{c_s}{v_A}\right)^2 \; , \; \Lambda = \left( \frac{k v_{A}}{\omega_{ci}}\right)^2\]
Contents:
  1. Wave Propagating at 45 Degrees

  2. Wave frequencies on the k-theta plane

  3. Reproduce Figure 1 from Bellan 2012

[1]:
%matplotlib inline

import astropy.units as u
import matplotlib.pyplot as plt
import numpy as np
from astropy.constants.si import c
from matplotlib.ticker import MultipleLocator
from mpl_toolkits.axes_grid1 import make_axes_locatable

from plasmapy.dispersion.analytical.two_fluid_ import two_fluid
from plasmapy.formulary import speeds
from plasmapy.formulary.frequencies import gyrofrequency, plasma_frequency, wc_, wp_
from plasmapy.formulary.lengths import inertial_length
from plasmapy.particles import Particle

plt.rcParams["figure.figsize"] = [10.5, 0.56 * 10.5]
Wave Propagating at 45 Degrees

Below we define the required parameters to compute the wave frequencies.

[2]:
# define input parameters
inputs = {
    "k": np.linspace(10**-7, 10**-2, 10000) * u.rad / u.m,
    "theta": 45 * u.deg,
    "n_i": 5 * u.cm**-3,
    "B": 8.3e-9 * u.T,
    "T_e": 1.6e6 * u.K,
    "T_i": 4.0e5 * u.K,
    "ion": Particle("p+"),
}

# a few useful plasma parameters
params = {
    "n_e": inputs["n_i"] * abs(inputs["ion"].charge_number),
    "cs": speeds.ion_sound_speed(
        inputs["T_e"],
        inputs["T_i"],
        inputs["ion"],
    ),
    "va": speeds.Alfven_speed(
        inputs["B"],
        inputs["n_i"],
        ion=inputs["ion"],
    ),
    "wci": gyrofrequency(inputs["B"], inputs["ion"]),
}
params["lpe"] = inertial_length(params["n_e"], "e-")
params["wpe"] = plasma_frequency(params["n_e"], "e-")

The computed wave frequencies (\(rad/s\)) are returned in a dictionary with keys representing the wave modes and the values being an Astropy Quantity. Since our inputs had a scalar \(\theta\) and a 1D array of \(k\)’s, the computed wave frequencies will be a 1D array of size equal to the size of the \(k\) array.

[3]:
# compute
omegas = two_fluid(**inputs)

(list(omegas.keys()), omegas["fast_mode"], omegas["fast_mode"].shape)
[3]:
(['fast_mode', 'alfven_mode', 'acoustic_mode'],
 <Quantity [1.63839709e-02, 1.80262570e-01, 3.44262572e-01, ...,
            1.52032171e+03, 1.52047365e+03, 1.52062560e+03] rad / s>,
 (10000,))

Let’s plot the results of each wave mode.

[4]:
fs = 14  # default font size
figwidth, figheight = plt.rcParams["figure.figsize"]
figheight = 1.6 * figheight
fig = plt.figure(figsize=[figwidth, figheight])

# normalize data
k_prime = inputs["k"] * params["lpe"]

# plot
plt.plot(
    k_prime,
    np.real(omegas["fast_mode"] / params["wpe"]),
    "r.",
    ms=1,
    label="Fast",
)
ax = plt.gca()
ax.plot(
    k_prime,
    np.real(omegas["alfven_mode"] / params["wpe"]),
    "b.",
    ms=1,
    label="Alfvén",
)
ax.plot(
    k_prime,
    np.real(omegas["acoustic_mode"] / params["wpe"]),
    "g.",
    ms=1,
    label="Acoustic",
)

# adjust axes
ax.set_xlabel(r"$kc / \omega_{pe}$", fontsize=fs)
ax.set_ylabel(r"$Re(\omega / \omega_{pe})$", fontsize=fs)
ax.set_yscale("log")
ax.set_xscale("log")
ax.set_ylim(1e-6, 2e-2)
ax.tick_params(
    which="both",
    direction="in",
    width=1,
    labelsize=fs,
    right=True,
    length=5,
)

# annotate
text = (
    rf"$v_A/c_s = {params['va'] / params['cs']:.1f} \qquad "
    rf"c/v_A = 10^{np.log10(c / params['va']):.0f} \qquad "
    f"\\theta = {inputs['theta'].value:.0f}"
    "^{\\circ}$"
)
ax.text(0.25, 0.95, text, transform=ax.transAxes, fontsize=18)
ax.legend(loc="upper left", markerscale=5, fontsize=fs)
[4]:
<matplotlib.legend.Legend at 0x7f651a853dd0>
_images/notebooks_dispersion_two_fluid_dispersion_7_1.png
Wave frequencies on the k-theta plane

Let us now look at the distribution of \(\omega\) on a \(k\)-\(\theta\) plane.

[5]:
# define input parameters
inputs = {
    "k": np.linspace(10**-7, 10**-2, 10000) * u.rad / u.m,
    "theta": np.linspace(5, 85, 100) * u.deg,
    "n_i": 5 * u.cm**-3,
    "B": 8.3e-9 * u.T,
    "T_e": 1.6e6 * u.K,
    "T_i": 4.0e5 * u.K,
    "ion": Particle("p+"),
}

# a few useful plasma parameters
params = {
    "n_e": inputs["n_i"] * abs(inputs["ion"].charge_number),
    "cs": speeds.ion_sound_speed(
        inputs["T_e"],
        inputs["T_i"],
        inputs["ion"],
    ),
    "va": speeds.Alfven_speed(
        inputs["B"],
        inputs["n_i"],
        ion=inputs["ion"],
    ),
    "wci": gyrofrequency(inputs["B"], inputs["ion"]),
}
params["lpe"] = inertial_length(params["n_e"], "e-")
params["wpe"] = plasma_frequency(params["n_e"], "e-")

Since the \(\theta\) and \(k\) values are now 1-D arrays, the returned wave frequencies will be 2-D arrays with the first dimension matching the size of \(k\) and the second dimension matching the size of \(\theta\).

[6]:
# compute
omegas = two_fluid(**inputs)

(
    omegas["fast_mode"].shape,
    omegas["fast_mode"].shape[0] == inputs["k"].size,
    omegas["fast_mode"].shape[1] == inputs["theta"].size,
)
[6]:
((10000, 100), True, True)

Let’s plot (the fast mode)!

[7]:
fs = 14  # default font size
figwidth, figheight = plt.rcParams["figure.figsize"]
figheight = 1.6 * figheight
fig = plt.figure(figsize=[figwidth, figheight])

# normalize data
k_prime = inputs["k"] * params["lpe"]
zdata = np.transpose(np.real(omegas["fast_mode"].value)) / params["wpe"].value

# plot
im = plt.imshow(
    zdata,
    aspect="auto",
    origin="lower",
    extent=[
        np.min(k_prime.value),
        np.max(k_prime.value),
        np.min(inputs["theta"].value),
        np.max(inputs["theta"].value),
    ],
    interpolation=None,
    cmap=plt.cm.Spectral,
)
ax = plt.gca()

# # adjust axes
ax.set_xscale("linear")
ax.set_xlabel(r"$kc/\omega_{pe}$", fontsize=fs)
ax.set_ylabel(r"$\theta$ [$deg.$]", fontsize=fs)
ax.tick_params(
    which="both",
    direction="in",
    width=2,
    labelsize=fs,
    right=True,
    top=True,
    length=10,
)

# Add colorbar
divider = make_axes_locatable(ax)
cax = divider.append_axes("top", size="5%", pad=0.07)
cbar = plt.colorbar(
    im,
    cax=cax,
    orientation="horizontal",
    ticks=None,
    fraction=0.05,
    pad=0.0,
)
cbar.ax.tick_params(
    axis="x",
    direction="in",
    width=2,
    length=10,
    top=True,
    bottom=False,
    labelsize=fs,
    pad=0.0,
    labeltop=True,
    labelbottom=False,
)
cbar.ax.xaxis.set_label_position("top")
cbar.set_label(r"$\omega/\omega_{pe}$", fontsize=fs, labelpad=8)
_images/notebooks_dispersion_two_fluid_dispersion_13_0.png

Reproduce Figure 1 from Bellan 2012

Figure 1 of Bellan 2012 chooses parameters such that \(\beta = 0.4\) and \(\Lambda=0.4\). Below we define parameters to approximate Bellan’s assumptions.

[8]:
# define input parameters
inputs = {
    "B": 400e-4 * u.T,
    "ion": Particle("He+"),
    "n_i": 6.358e19 * u.m**-3,
    "T_e": 20 * u.eV,
    "T_i": 10 * u.eV,
    "theta": np.linspace(0, 90) * u.deg,
    "k": (2 * np.pi * u.rad) / (0.56547 * u.m),
}

# a few useful plasma parameters
params = {
    "n_e": inputs["n_i"] * abs(inputs["ion"].charge_number),
    "cs": speeds.cs_(inputs["T_e"], inputs["T_i"], inputs["ion"]),
    "wci": wc_(inputs["B"], inputs["ion"]),
    "va": speeds.va_(inputs["B"], inputs["n_i"], ion=inputs["ion"]),
}
params["beta"] = (params["cs"] / params["va"]).value ** 2
params["wpe"] = wp_(params["n_e"], "e-")
params["Lambda"] = (inputs["k"] * params["va"] / params["wci"]).value ** 2

(params["beta"], params["Lambda"])
[8]:
(0.4000832135717194, 0.4000017351804854)
[9]:
# compute
omegas = two_fluid(**inputs)
[10]:
# generate data for plots
plt_vals = {}
for mode, arr in omegas.items():
    norm = (np.absolute(arr) / (inputs["k"] * params["va"])).value ** 2
    plt_vals[mode] = {
        "x": norm * np.sin(inputs["theta"].to(u.rad).value),
        "y": norm * np.cos(inputs["theta"].to(u.rad).value),
    }
[11]:
fs = 14  # default font size
figwidth, figheight = plt.rcParams["figure.figsize"]
figheight = 1.6 * figheight
fig = plt.figure(figsize=[figwidth, figheight])

# Fast mode
plt.plot(
    plt_vals["fast_mode"]["x"],
    plt_vals["fast_mode"]["y"],
    linewidth=2,
    label="Fast",
)
ax = plt.gca()

# adjust axes
ax.set_xlabel(r"$(\omega / k v_A)^2 \, \sin \theta$", fontsize=fs)
ax.set_ylabel(r"$(\omega / k v_A)^2 \, \cos \theta$", fontsize=fs)
ax.set_xlim(0.0, 1.5)
ax.set_ylim(0.0, 2.0)
for spine in ax.spines.values():
    spine.set_linewidth(2)
ax.minorticks_on()
ax.tick_params(which="both", labelsize=fs, width=2)
ax.tick_params(which="major", length=10)
ax.tick_params(which="minor", length=5)
ax.xaxis.set_major_locator(MultipleLocator(0.5))
ax.xaxis.set_minor_locator(MultipleLocator(0.1))
ax.yaxis.set_major_locator(MultipleLocator(0.5))
ax.yaxis.set_minor_locator(MultipleLocator(0.1))


# Alfven mode
plt.plot(
    plt_vals["alfven_mode"]["x"],
    plt_vals["alfven_mode"]["y"],
    linewidth=2,
    label="Alfvén",
)

# Acoustic mode
plt.plot(
    plt_vals["acoustic_mode"]["x"],
    plt_vals["acoustic_mode"]["y"],
    linewidth=2,
    label="Acoustic",
)

# annotations
plt.legend(fontsize=fs, loc="upper right")
[11]:
<matplotlib.legend.Legend at 0x7f6517c6ae70>
_images/notebooks_dispersion_two_fluid_dispersion_18_1.png

Formulary

This page was generated by nbsphinx from docs/notebooks/formulary/ExB_drift.ipynb.
Interactive online version: Binder badge.

[1]:
%matplotlib inline

import math

import astropy.units as u
import matplotlib.pyplot as plt
import numpy as np

from plasmapy import formulary, particles

Physics of the E×B drift

Consider a single particle of mass \(m\) and charge \(q\) in a constant, uniform magnetic field \(\mathbf{B}=B\ \hat{\mathbf{z}}\). In the absence of external forces, it travels with velocity \(\mathbf{v}\) governed by the equation of motion

\[m\frac{d\mathbf{v}}{dt} = q\mathbf{v}\times\mathbf{B}\]

which equates the net force on the particle to the corresponding Lorentz force. Assuming the particle initially (at time \(t=0\)) has \(\mathbf{v}\) in the \(x,z\) plane (with \(v_y=0\)), solving reveals

\[v_x = v_\perp\cos\omega_c t \quad\mathrm{;}\quad v_y = -\frac{q}{\lvert q \rvert}v_\perp\sin \omega_c t\]

while the parallel velocity \(v_z\) is constant. This indicates that the particle gyrates in a circular orbit in the \(x,y\) plane with constant speed \(v_\perp\), angular frequency \(\omega_c = \frac{\lvert q\rvert B}{m}\), and Larmor radius \(r_L=\frac{v_\perp}{\omega_c}\).

As an example, take one proton p+ moving with velocity \(1\ m/s\) in the \(x\)-direction at \(t=0\):

[2]:
# Initialize proton in uniform B field
B = 5 * u.T
proton = particles.Particle("p+")
omega_c = formulary.frequencies.gyrofrequency(B, proton)
v_perp = 1 * u.m / u.s
r_L = formulary.lengths.gyroradius(B, proton, Vperp=v_perp)

We can define a function that evolves the particle’s position according to the relations above describing \(v_x,v_y\), and \(v_z\). The option to add a constant drift velocity \(v_d\) to the solution is included as an argument, though this drift velocity is zero by default:

[3]:
def single_particle_trajectory(v_d=np.array([0, 0, 0])):
    # Set time resolution & velocity such that proton goes 1 meter along B per rotation
    T = 2 * math.pi / omega_c.value  # rotation period
    v_parallel = 1 / T * u.m / u.s
    dt = T / 1e2 * u.s

    # Set initial particle position
    x = []
    y = []
    xt = 0 * u.m
    yt = -r_L

    # Evolve motion
    timesteps = np.arange(0, 10 * T, dt.value)
    for t in list(timesteps):
        v_x = v_perp * math.cos(omega_c.value * t) + v_d[0]
        v_y = v_perp * math.sin(omega_c.value * t) + v_d[1]
        xt += +v_x * dt
        yt += +v_y * dt
        x.append(xt.value)
        y.append(yt.value)
    x = np.array(x)
    y = np.array(y)
    z = v_parallel.value * timesteps

    return x, y, z

Executing with the default argument and plotting the particle trajectory gives the expected helical motion, with a radius equal to the Larmor radius:

[4]:
x, y, z = single_particle_trajectory()
[5]:
fig = plt.figure(figsize=(6, 6))
ax = fig.add_subplot(111, projection="3d")
ax.plot(x, y, z, label=r"$\mathbf{F}=0$")
ax.legend()
bound = 3 * r_L.value
ax.set_xlim([-bound, bound])
ax.set_ylim([-bound, bound])
ax.set_zlim([0, 10])
ax.set_xlabel("x [m]")
ax.set_ylabel("y [m]")
ax.set_zlabel("z [m]")
plt.show()
_images/notebooks_formulary_ExB_drift_8_0.png
[6]:
print(f"r_L = {r_L.value:.2e} m")
print(f"omega_c = {omega_c.value:.2e} rads/s")
r_L = 2.09e-09 m
omega_c = 4.79e+08 rads/s

How does this motion change when a constant external force \(\mathbf{F}\) is added? The new equation of motion is

\[m\frac{d\mathbf{v}}{dt} = q\mathbf{v}\times\mathbf{B} + \mathbf{F}\]

and we can find a solution by considering velocities of the form \(\mathbf{v}=\mathbf{v}_\parallel + \mathbf{v}_L + \mathbf{v}_d\). Here, \(\mathbf{v}_\parallel\) is the velocity parallel to the magnetic field, \(\mathbf{v}_L\) is the Larmor gyration velocity in the absence of \(\mathbf{F}\) found previously, and \(\mathbf{v}_d\) is some constant drift velocity perpendicular to the magnetic field. Then, we find that

\[F_\parallel = m\frac{dv_\parallel}{dt} \quad\mathrm{and}\quad \mathbf{F}_\perp = q\mathbf{B}\times \mathbf{v}_d\]

and applying the vector triple product yields

\[\mathbf{v}_d = \frac{1}{q}\frac{\mathbf{F}_\perp\times\mathbf{B}}{B^2}\]

In the case where the external force \(\mathbf{F} = q\mathbf{E}\) is due to a constant electric field, this is the constant \(\mathbf{E}\times\mathbf{B}\) drift velocity:

\[\boxed{ \mathbf{v}_d = \frac{\mathbf{E}\times\mathbf{B}}{B^2} }\]

Built in drift functions allow you to account for the new force added to the system in two different ways:

[7]:
E = 0.2 * u.V / u.m  # E-field magnitude
ey = np.array([0, 1, 0])
ez = np.array([0, 0, 1])
F = proton.charge * E  # force due to E-field

v_d = formulary.drifts.force_drift(F * ey, B * ez, proton.charge)
print("F drift velocity: ", v_d)
v_d = formulary.drifts.ExB_drift(E * ey, B * ez)
print("ExB drift velocity: ", v_d)
F drift velocity:  [0.04 0.   0.  ] m / s
ExB drift velocity:  [0.04 0.   0.  ] m / s

The resulting particle trajectory can be compared to the case without drifts by calling our previously defined function with the drift velocity now as an argument. As expected, there is a constant drift in the direction of \(\mathbf{E}\times\mathbf{B}\):

[8]:
x_d, y_d, z_d = single_particle_trajectory(v_d=v_d)
[9]:
fig = plt.figure(figsize=(6, 6))
ax = fig.add_subplot(111, projection="3d")
ax.plot(x, y, z, label=r"$\mathbf{F}=0$")
ax.plot(x_d, y_d, z_d, label=r"$\mathbf{F}=q\mathbf{E}$")

bound = 3 * r_L.value
ax.set_xlim([-bound, bound])
ax.set_ylim([-bound, bound])
ax.set_zlim([0, 10])
ax.set_xlabel("x [m]")
ax.set_ylabel("y [m]")
ax.set_zlabel("z [m]")
ax.legend()
plt.show()
_images/notebooks_formulary_ExB_drift_14_0.png
[10]:
print(f"r_L = {r_L.value:.2e} m")
print(f"omega_c = {omega_c.value:.2e} rads/s")
r_L = 2.09e-09 m
omega_c = 4.79e+08 rads/s

Of course, the implementation in our single_particle_trajectory() function requires the analytical solution for the velocity \(\mathbf{v}_d\). This solution can be compared with that implemented in the particle tracker notebook. It uses the Boris algorithm to evolve the particle along its trajectory in prescribed \(\mathbf{E}\) and \(\mathbf{B}\) fields, and thus does not require the analytical solution.

This page was generated by nbsphinx from docs/notebooks/formulary/braginskii.ipynb.
Interactive online version: Binder badge.

[1]:
%matplotlib inline

Braginskii coefficients

A short example of how to calculate classical transport coefficients from Bragiński’s theory.

[2]:
import astropy.units as u

from plasmapy.formulary import ClassicalTransport

We’ll use some sample ITER data, without much regard for whether the regime is even fit for classical transport theory:

[3]:
thermal_energy_per_electron = 8.8 * u.keV
electron_concentration = 10.1e19 / u.m**3

thermal_energy_per_ion = 8.0 * u.keV
ion_concentration = electron_concentration
ion = "D+"  # a crude approximation

We now make the default ClassicalTransport object:

[4]:
braginskii = ClassicalTransport(
    thermal_energy_per_electron,
    electron_concentration,
    thermal_energy_per_ion,
    ion_concentration,
    ion,
)
/home/docs/checkouts/readthedocs.org/user_builds/plasmapy/envs/latest/lib/python3.12/site-packages/plasmapy/utils/decorators/checks.py:1405: RelativityWarning: thermal_speed is yielding a velocity that is 18.561% of the speed of light. Relativistic effects may be important.
  warnings.warn(
/home/docs/checkouts/readthedocs.org/user_builds/plasmapy/envs/latest/lib/python3.12/site-packages/plasmapy/utils/decorators/checks.py:1405: RelativityWarning: V is yielding a velocity that is 18.561% of the speed of light. Relativistic effects may be important.
  warnings.warn(
/home/docs/checkouts/readthedocs.org/user_builds/plasmapy/envs/latest/lib/python3.12/site-packages/plasmapy/utils/decorators/checks.py:1405: RelativityWarning: thermal_speed is yielding a velocity that is 18.559% of the speed of light. Relativistic effects may be important.
  warnings.warn(
/home/docs/checkouts/readthedocs.org/user_builds/plasmapy/envs/latest/lib/python3.12/site-packages/plasmapy/utils/decorators/checks.py:1405: RelativityWarning: V is yielding a velocity that is 18.559% of the speed of light. Relativistic effects may be important.
  warnings.warn(

These variables are calculated during initialization and can be referred to straight away:

[5]:
print(braginskii.coulomb_log_ei)
print(braginskii.coulomb_log_ii)
print(braginskii.hall_e)
print(braginskii.hall_i)
18.015542112815666
20.41557520752423
0.0
0.0

These quantities are not calculated during initialization and can be referred to via methods. To signify the need to calculate them, we call them via ().

[6]:
print(braginskii.resistivity)
print(braginskii.thermoelectric_conductivity)
print(braginskii.electron_thermal_conductivity)
print(braginskii.ion_thermal_conductivity)
1.1541382845331304e-09 Ohm m
0.7110839986207994
1065176076.6192665 W / (K m)
21360464.46376672 W / (K m)

They also change with magnetization:

[7]:
mag_braginskii = ClassicalTransport(
    thermal_energy_per_electron,
    electron_concentration,
    thermal_energy_per_ion,
    ion_concentration,
    ion,
    B=0.1 * u.T,
)

print(mag_braginskii.resistivity)
print(mag_braginskii.thermoelectric_conductivity)
print(mag_braginskii.electron_thermal_conductivity)
print(mag_braginskii.ion_thermal_conductivity)
1.1541382845331304e-09 Ohm m
0.7110839986207994
1065176076.6192665 W / (K m)
21360464.46376672 W / (K m)

They also change with direction with respect to the magnetic field. Here, we choose to print out, as arrays, the (parallel, perpendicular, and cross) directions. Take a look at the docs to ClassicalTransport for more information on these.

[8]:
all_direction_braginskii = ClassicalTransport(
    thermal_energy_per_electron,
    electron_concentration,
    thermal_energy_per_ion,
    ion_concentration,
    ion,
    B=0.1 * u.T,
    field_orientation="all",
)

print(all_direction_braginskii.resistivity)
print(all_direction_braginskii.thermoelectric_conductivity)
print(all_direction_braginskii.electron_thermal_conductivity)
print(all_direction_braginskii.ion_thermal_conductivity)
[1.15413828e-09 2.25078755e-09 1.39690568e-15] Ohm m
[7.11083999e-01 6.76676822e-13 5.46328992e-07]
[1.06517608e+09 2.08451764e-04 3.06777888e+02] W / (K m)
[2.13604645e+07 4.24754851e-03 2.69422221e+02] W / (K m)

The viscosities return arrays:

[9]:
print(braginskii.electron_viscosity)
print(mag_braginskii.electron_viscosity)
print(braginskii.ion_viscosity)
print(mag_braginskii.ion_viscosity)
[16.29411376 16.28874805 16.28874805  0.          0.        ] Pa s
[1.62941138e+01 2.00480711e-25 8.01922844e-25 1.47442522e-12
 2.94885044e-12] Pa s
[1271.38945503 1267.52435833 1267.52435833    0.            0.        ] Pa s
[1.27138946e+03 5.99222933e-17 2.39689173e-16 2.57162285e-07
 5.14324570e-07] Pa s

This page was generated by nbsphinx from docs/notebooks/formulary/cold_plasma_tensor_elements.ipynb.
Interactive online version: Binder badge.

Cold Magnetized Plasma Dielectric Permittivity Tensor

This notebook shows how to calculate the values of the cold plasma tensor elements for various electromagnetic wave frequencies.

[1]:
%matplotlib inline
[2]:
import astropy.units as u
import matplotlib.pyplot as plt
import numpy as np
from astropy.visualization import quantity_support

from plasmapy.formulary import (
    cold_plasma_permittivity_LRP,
    cold_plasma_permittivity_SDP,
)

For more information, check out the documentation on cold_plasma_permittivity_SDP and cold_plasma_permittivity_LRP.

Let’s use astropy.units to define some parameters, such as the magnetic field magnitude, plasma species, and densities.

[3]:
B = 2 * u.T
species = ["e", "D+"]
n = [1e18 * u.m**-3, 1e18 * u.m**-3]

Let’s pick some frequencies in the radio frequency range.

[4]:
f_min = 1 * u.MHz
f_max = 200 * u.GHz

f = np.geomspace(f_min, f_max, num=5000).to(u.Hz)

Next we convert these frequencies to angular frequencies. To do this we must specify an equivalency between a cycle per second and a hertz. The reasons why are described in the section of PlasmaPy’s documentation on angular frequencies.

[5]:
ω_RF = f.to(u.rad / u.s, equivalencies=[(u.Hz, u.cycle / u.s)])

In a Jupyter notebook, we can type \omega and press tab to get “ω”.

Now we are ready to calculate the \(S\) (sum), \(D\) (difference), and \(P\) (plasma) components of the dielectric tensor in the “Stix” frame with \(B ∥ ẑ\). This notation is from Stix 1992.

[6]:
S, D, P = cold_plasma_permittivity_SDP(B=B, species=species, n=n, omega=ω_RF)

Next we will filter the negative and positive values so that they can be plotted separately. We’ll be doing this multiple times, so let’s create a function using the numpy.ma module for working with masked arrays.

[7]:
def filter_negative_and_positive_values(arr):
    """
    Return an array with only negative values of ``arr``
    and an array with only positive values of ``arr``.
    Each element of these arrays that does not meet the
    condition are replaced with `~numpy.nan`.
    """
    arr_neg = np.ma.masked_greater_equal(arr, 0).filled(np.nan)
    arr_pos = np.ma.masked_less_equal(arr, 0).filled(np.nan)
    return arr_neg, arr_pos

Let’s apply this function to S, D, and P.

[8]:
S_neg, S_pos = filter_negative_and_positive_values(S)
D_neg, D_pos = filter_negative_and_positive_values(D)
P_neg, P_pos = filter_negative_and_positive_values(P)

Because we are using Quantity objects, we will need to call quantity_support before plotting the results.

[9]:
quantity_support()
[9]:
<astropy.visualization.units.quantity_support.<locals>.MplQuantityConverter at 0x7f8532518f80>

While we’re at it, let’s specify a few colorblind friendly colors to use in the plots.

[10]:
red, blue, orange = "#920000", "#006ddb", "#db6d00"

Let’s plot the elements of the cold plasma dielectric tensor in the Stix frame.

[11]:
ylim = (1e-4, 1e8)

plt.figure(figsize=(12, 6))

plt.semilogx(f, abs(S_neg), red, lw=2, ls="--", label="S < 0")
plt.semilogx(f, abs(D_neg), blue, lw=2, ls="--", label="D < 0")
plt.semilogx(f, abs(P_neg), orange, lw=2, ls="--", label="P < 0")

plt.semilogx(f, S_pos, red, lw=2, label="S > 0")
plt.semilogx(f, D_pos, blue, lw=2, label="D > 0")
plt.semilogx(f, P_pos, orange, lw=2, label="P > 0")

plt.ylim(ylim)
plt.xlim(f_min, f_max)

plt.title(
    "Cold plasma dielectric permittivity tensor components in Stix frame", size=16
)

plt.xlabel("Frequency [Hz]", size=16)
plt.ylabel("Absolute value", size=16)

plt.yscale("log")

plt.grid(True, which="major")
plt.grid(True, which="minor")

plt.tick_params(labelsize=14)

plt.legend(fontsize=18, ncol=2, framealpha=1)

plt.show()
_images/notebooks_formulary_cold_plasma_tensor_elements_21_0.png

Next let’s get the dielectric permittivity tensor elements in the “rotating” basis where the tensor is diagonal and with \(B ∥ ẑ\). We will use cold_plasma_permittivity_LRP to get the left-handed circular polarization tensor element \(L\), the right-handed circular polarization tensor element \(R\), and the plasma component \(P\).

[12]:
L, R, P = cold_plasma_permittivity_LRP(B, species, n, ω_RF)

Let’s use the function from earlier to prepare for plotting.

[13]:
L_neg, L_pos = filter_negative_and_positive_values(L)
R_neg, R_pos = filter_negative_and_positive_values(R)
P_neg, P_pos = filter_negative_and_positive_values(P)

Now we can plot the tensor components rotating frame too.

[14]:
plt.figure(figsize=(12, 6))

plt.semilogx(f, abs(L_neg), red, lw=2, ls="--", label="L < 0")
plt.semilogx(f, abs(R_neg), blue, lw=2, ls="--", label="R < 0")
plt.semilogx(f, abs(P_neg), orange, lw=2, ls="--", label="P < 0")

plt.semilogx(f, L_pos, red, lw=2, label="L > 0")
plt.semilogx(f, R_pos, blue, lw=2, label="R > 0")
plt.semilogx(f, P_pos, orange, lw=2, label="P > 0")

plt.ylim(ylim)
plt.xlim(f_min, f_max)

plt.title(
    "Cold plasma dielectric permittivity tensor components in the rotating frame",
    size=16,
)

plt.xlabel("Frequency [Hz]", size=16)
plt.ylabel("Absolute value", size=16)

plt.yscale("log")

plt.grid(True, which="major")
plt.grid(True, which="minor")

plt.tick_params(labelsize=14)

plt.legend(fontsize=18, ncol=2, framealpha=1)

plt.show()
_images/notebooks_formulary_cold_plasma_tensor_elements_27_0.png

This page was generated by nbsphinx from docs/notebooks/formulary/coulomb.ipynb.
Interactive online version: Binder badge.

Coulomb logarithms

Coulomb collisions are collisions between two charged particles where the interaction is governed solely by the electric fields from the two particles. Coulomb collisions usually result in small deflections of particle trajectories. The deflection angle depends on the impact parameter of the collision, which is the perpendicular distance between the particle’s trajectory and the particle it is colliding with. High impact parameter collisions (which result in small deflection angles) occur much more frequently than low impact parameter collisions (which result in large deflection angles).

coulomb-collision.png

The minimum and maximum impact parameters (\(b_\min\) and \(b_\max\), respectively) represent the range of distances of closest approach. While a typical Coulomb collision results in only a slight change in trajectory, the effects of these collisions are cumulative, and it is necessary to integrate over the range of impact parameters to account for the effects of Coulomb collisions throughout the plasma. The Coulomb logarithm accounts for the range in impact parameters for the different collisions, and is given by

\[\ln{Λ} ≡ \ln\left(\frac{b_\max}{b_\min}\right).\]

But what should we use for the impact parameters?

Usually \(b_\max\) is given by the Debye length, \(λ_D\), which can be calculated with Debye_length(). On length scales \(≳ λ_D\), electric fields from individual particles get cancelled out due to screening effects. Consequently, Coulomb collisions with impact parameters \(≳ λ_D\) will rarely occur.

The inner impact parameter \(b_\min\) requires more nuance. One possibility would be to set \(b_\min\) to be the impact parameter corresponding to a 90° deflection angle, \(ρ_⟂\), which can be calculated with impact_parameter_perp(). Alternatively, \(b_\min\) could be set to be the de Broglie wavelength, \(λ_{dB}\), calculated using the reduced mass, \(μ\), of the particles. Typically,

\[b_\min = \max\left\{ρ_⟂, λ_{dB} \right\}.\]

The impact_parameter() function in plasmapy.formulary simultaneously calculates both \(b_\min\) and \(b_\max\). Let’s estimate \(b_\min\) and \(b_\max\) for proton-electron collisions in the solar corona.

[1]:
%matplotlib inline
%config InlineBackend.figure_formats = ['svg']

import astropy.units as u
import matplotlib.pyplot as plt
import numpy as np
from astropy.constants import hbar as 
from astropy.visualization import quantity_support

from plasmapy.formulary.collisions import (
    Coulomb_logarithm,
    impact_parameter,
    impact_parameter_perp,
)
from plasmapy.formulary.speeds import thermal_speed
from plasmapy.particles import reduced_mass
[2]:
solar_corona = {
    "T": 1e6 * u.K,
    "n_e": 1e15 * u.m**-3,
    "species": ["e-", "p+"],
}

bmin, bmax = impact_parameter(**solar_corona)

print(f"{bmin = :.2e}")
print(f"{bmax = :.2e}")
bmin = 1.05e-11 m
bmax = 2.18e-03 m

When we can calculate the Coulomb logarithm, we find that it is ∼20 (a common value for astrophysical plasma).

[3]:
Coulomb_logarithm(**solar_corona)
[3]:
19.150697742645406

Our next goals are visualize the Coulomb logarithm and impact parameters for different physical conditions. Let’s start by creating a function to plot \(\ln{Λ}\) against temperature. We will use quantity_support to enable plotting with Quantity objects.

[4]:
def plot_coulomb(T, n_e, species, **kwargs):
    """Plot the Coulomb logarithm and impact parameter length scales."""

    ln_Λ = Coulomb_logarithm(T, n_e, species, **kwargs)

    with quantity_support():
        fig, ax = plt.subplots()

        fig.set_size_inches(4, 4)

        ax.semilogx(T, ln_Λ)
        ax.set_xlabel("T (K)")
        ax.set_ylabel(r"$\ln{Λ}$")
        ax.set_title("Coulomb logarithm")

        fig.tight_layout()

Next we will create a function to plot \(b_\min\) and \(b_\max\) as functions of temperature, along with the \(ρ_⟂\) and \(λ_{dB}\).

[5]:
def plot_impact_parameters(T, n_e, species, **kwargs):
    """Plot the minimum & maximum impact parameters for Coulomb collisions."""

    bmin, bmax = impact_parameter(T, n_e, species, **kwargs)

    μ = reduced_mass(*species)
    V_the = thermal_speed(T, "e-")
    λ_dB =  / (2 * μ * V_the)

    ρ_perp = impact_parameter_perp(T, species)

    with quantity_support():
        fig, ax = plt.subplots()
        fig.set_size_inches(4, 4)

        ax.loglog(T, λ_dB, "--", label=r"$λ_\mathrm{dB}$")
        ax.loglog(T, ρ_perp, "-.", label=r"$ρ_\perp$")
        ax.loglog(T, bmin, "-", label=r"$b_\mathrm{min}$")
        ax.loglog(T, bmax, "-", label=r"$b_\mathrm{max}$")

        ax.set_xlabel("T (K)")
        ax.set_ylabel("b (m)")
        ax.set_title("Impact parameters")
        ax.legend()

        fig.tight_layout()

Let’s now plot the Coulomb logarithm and minimum/maximum impact parameters over a range of temperatures in a dense plasma.

[6]:
n_e = 1e25 * u.m**-3

dense_plasma = {
    "T": np.geomspace(1e5, 2e6) * u.K,
    "n_e": n_e,
    "species": ["e-", "p+"],
}

plot_coulomb(**dense_plasma)
_images/notebooks_formulary_coulomb_12_0.svg

To investigate what is happening at the sudden change in slope, let’s plot the impact parameters against temperature.

[7]:
plot_impact_parameters(**dense_plasma)
_images/notebooks_formulary_coulomb_14_0.svg

At low temperatures, \(b_\min\) is \(ρ_⟂\). At higher temperatures, \(b_\min\) is \(λ_{dB}\). What happens if we look at lower temperatures?

[8]:
cool_dense_plasma = {
    "T": np.geomspace(1e2, 2e4) * u.K,
    "n_e": n_e,
    "species": ["e-", "p+"],
}

plot_coulomb(**cool_dense_plasma)
/home/docs/checkouts/readthedocs.org/user_builds/plasmapy/envs/latest/lib/python3.12/site-packages/plasmapy/formulary/collisions/coulomb.py:473: CouplingWarning: The calculation of the Coulomb logarithm has found a value of min(ln Λ) = -5.9477 which is likely to be inaccurate due to strong coupling effects, in particular because method = 'classical' assumes weak coupling.
  warnings.warn(
_images/notebooks_formulary_coulomb_16_1.svg

The Coulomb logarithm becomes negative! 🙀 Let’s look at the impact parameters again to understand what’s happening.

[9]:
plot_impact_parameters(**cool_dense_plasma)
_images/notebooks_formulary_coulomb_18_0.svg

This unphysical situation occurs because \(b_\min > b_\max\) at low temperatures.

So how should we handle this? Fortunately, PlasmaPy’s implementation of Coulomb_logarithm() includes the methods described by Gericke, Murillo, and Schlanges (2002) for dense, strongly-coupled plasmas. For most cases, we recommend using method="hls_full_interp" in which \(b_\min\) is interpolated between \(λ_{dB}\) and \(ρ_⟂\), and \(b_\max\) is interpolated between \(λ_D\) and the ion sphere radius, \(a_i ≡ \left(\frac{3}{4} π n_i\right)^{⅓}\).

[10]:
cool_dense_plasma["z_mean"] = 1
cool_dense_plasma["method"] = "hls_full_interp"

plot_coulomb(**cool_dense_plasma)
/home/docs/checkouts/readthedocs.org/user_builds/plasmapy/envs/latest/lib/python3.12/site-packages/plasmapy/formulary/collisions/coulomb.py:481: CouplingWarning: The calculation of the Coulomb logarithm has found a value of min(ln Λ) = 0.0006. Coulomb logarithms of ≲ 4 may have increased uncertainty due to strong coupling effects.
  warnings.warn(
_images/notebooks_formulary_coulomb_21_1.svg

The Coulomb logarithm approaches zero as the temperature decreases, and does not become unphysically negative. This is the expected behavior, although there is increased uncertainty for Coulomb logarithms \(≲4\).

This page was generated by nbsphinx from docs/notebooks/formulary/distribution.ipynb.
Interactive online version: Binder badge.

[1]:
%matplotlib inline

1D Maxwellian distribution function

We import the usual modules, and the hero of this notebook, the Maxwellian 1D distribution:

[2]:
import astropy.units as u
import matplotlib.pyplot as plt
import numpy as np
from astropy.constants import k_B, m_e

Take a look at the docs to Maxwellian_1D() for more information on these.

Given we’ll be plotting, import astropy’s quantity support:

[3]:
from astropy.visualization import quantity_support

from plasmapy.formulary import Maxwellian_1D

quantity_support()
[3]:
<astropy.visualization.units.quantity_support.<locals>.MplQuantityConverter at 0x7fd56b086780>

As a first example, let’s get the probability density of finding an electron with a speed of 1 m/s if we have a plasma at a temperature of 30 000 K:

[4]:
p_dens = Maxwellian_1D(
    v=1 * u.m / u.s, T=30000 * u.K, particle="e", v_drift=0 * u.m / u.s
)
print(p_dens)
5.916328704912825e-07 s / m

Note the units! Integrated over speed, this will give us a probability. Let’s test that for a bunch of particles:

[5]:
T = 3e4 * u.K
dv = 10 * u.m / u.s
v = np.arange(-5e6, 5e6, 10) * u.m / u.s

Check that the integral over all speeds is 1 (the particle has to be somewhere):

[6]:
for particle in ["p", "e"]:
    pdf = Maxwellian_1D(v, T=T, particle=particle)
    integral = (pdf).sum() * dv
    print(f"Integral value for {particle}: {integral}")
    plt.plot(v, pdf, label=particle)
plt.legend()
Integral value for p: 1.0000000000000002
Integral value for e: 0.9999999999998787
[6]:
<matplotlib.legend.Legend at 0x7fd56977c500>
_images/notebooks_formulary_distribution_11_2.png

The standard deviation of this distribution should give us back the temperature:

[7]:
std = np.sqrt((Maxwellian_1D(v, T=T, particle="e") * v**2 * dv).sum())
T_theo = (std**2 / k_B * m_e).to(u.K)

print("T from standard deviation:", T_theo)
print("Initial T:", T)
T from standard deviation: 29999.999999792235 K
Initial T: 30000.0 K

This page was generated by nbsphinx from docs/notebooks/formulary/iter.ipynb.
Interactive online version: Binder badge.

Analysing ITER parameters

Let’s try to look at ITER plasma conditions using plasmapy.formulary.

[1]:
%matplotlib inline

import astropy.units as u
import matplotlib.pyplot as plt
import numpy as np

from plasmapy import formulary

The radius of electric field shielding clouds, also known as the Debye_length(), would be

[2]:
electron_temperature = 8.8 * u.keV
electron_concentration = 10.1e19 / u.m**3
print(formulary.Debye_length(electron_temperature, electron_concentration))
6.939046810942984e-05 m

Note that we can also neglect the unit for the concentration, as m\(^{-3}\) is the a standard unit for this kind of Quantity:

[3]:
print(formulary.Debye_length(electron_temperature, 10.1e19))
6.939046810942984e-05 m
WARNING: UnitsWarning: The argument 'n_e' to function Debye_length() has no specified units. Assuming units of 1 / m3. To silence this warning, explicitly pass in an astropy Quantity (e.g. 5. * astropy.units.cm) (see http://docs.astropy.org/en/stable/units/) [plasmapy.utils.decorators.validators]

Assuming the magnetic field as 5.3 T (which is the value at the major radius):

[4]:
B = 5.3 * u.T

print(formulary.gyrofrequency(B, particle="e"))

print(formulary.gyroradius(B, T=electron_temperature, particle="e"))
932174605709.2465 rad / s
6.0740821128350554e-05 m
/home/docs/checkouts/readthedocs.org/user_builds/plasmapy/envs/latest/lib/python3.12/site-packages/plasmapy/utils/decorators/checks.py:1405: RelativityWarning: thermal_speed is yielding a velocity that is 18.559% of the speed of light. Relativistic effects may be important.
  warnings.warn(

The electron inertial_length() would be

[5]:
print(formulary.inertial_length(electron_concentration, particle="e"))
0.0005287720427518426 m

In these conditions, they should reach thermal velocities of about

[6]:
print(formulary.thermal_speed(T=electron_temperature, particle="e"))
55637426.422858626 m / s
/home/docs/checkouts/readthedocs.org/user_builds/plasmapy/envs/latest/lib/python3.12/site-packages/plasmapy/utils/decorators/checks.py:1405: RelativityWarning: thermal_speed is yielding a velocity that is 18.559% of the speed of light. Relativistic effects may be important.
  warnings.warn(

The Langmuir wave plasma_frequency() should be on the order of

[7]:
print(formulary.plasma_frequency(electron_concentration, particle="e-"))
566959736448.652 rad / s

Let’s try to recreate some plots and get a feel for some of these quantities. We will also compare our data to real-world plasma situations.

[8]:
n_e = np.logspace(4, 30, 100) / u.m**3
plt.plot(n_e, formulary.plasma_frequency(n_e, particle="e-"))
plt.scatter(
    electron_concentration,
    formulary.plasma_frequency(electron_concentration, particle="e-"),
    label="Our Data",
)

# IRT1 Tokamak Data
# http://article.sapub.org/pdf/10.5923.j.jnpp.20110101.03.pdf
n_e = 1.2e19 / u.m**3
T_e = 136.8323 * u.eV
B = 0.82 * u.T
plt.scatter(n_e, formulary.plasma_frequency(n_e, particle="e-"), label="IRT1 Tokamak")

# Wendelstein 7-X Stellarator Data
# https://nucleus.iaea.org/sites/fusionportal/Shared%20Documents/FEC%202016/fec2016-preprints/preprint0541.pdf
n_e = 3e19 / u.m**3
T_e = 6 * u.keV
B = 3 * u.T
plt.scatter(
    n_e, formulary.plasma_frequency(n_e, particle="e-"), label="W7-X Stellerator"
)

# Solar Corona
n_e = 1e15 / u.m**3
T_e = 1 * u.MK
B = 0.005 * u.T
T_e.to(u.eV, equivalencies=u.temperature_energy())
plt.scatter(n_e, formulary.plasma_frequency(n_e, particle="e-"), label="Solar Corona")

# Interstellar (warm neutral) Medium
n_e = 1e6 / u.m**3
T_e = 5e3 * u.K
B = 0.005 * u.T
T_e.to(u.eV, equivalencies=u.temperature_energy())
plt.scatter(
    n_e, formulary.plasma_frequency(n_e, particle="e-"), label="Interstellar Medium"
)

# Solar Wind at 1 AU
n_e = 7e6 / u.m**3
T_e = 1e5 * u.K
B = 5e-9 * u.T
T_e.to(u.eV, equivalencies=u.temperature_energy())
plt.scatter(
    n_e, formulary.plasma_frequency(n_e, particle="e-"), label="Solar Wind (1AU)"
)

plt.xlabel("Electron Concentration (m^-3)")
plt.ylabel("Langmuir Wave Plasma Frequency (rad/s)")
plt.grid()
plt.xscale("log")
plt.yscale("log")
plt.legend()
plt.title("Log-scale plot of plasma frequencies")
plt.show()
_images/notebooks_formulary_iter_15_0.png

This page was generated by nbsphinx from docs/notebooks/formulary/magnetosphere.ipynb.
Interactive online version: Binder badge.

Plasma parameters in Earth’s magnetosphere

The Magnetospheric Multiscale Mission (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 satellites need to orbit in a tight configuration. But how tight does the tetrahedron have to be? Let’s use plasmapy.formulary to find out.

[1]:
import astropy.units as u

from plasmapy.formulary import gyroradius, inertial_length
Contents
  1. Physics background

  2. Length scales

Physics background

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.

In the classical Sweet-Parker model, reconnection occurs when oppositely directed magnetic fields are pressed towards each other in a plasma in an elongated current sheet. The reconnection rate is slow because of the bottleneck associated with conservation of mass.

sweet-parker.png

Resistivity is the mechanism that allows field line slippage in the Sweet-Parker model. However, the resistivity of space plasma is too low to allow fast reconnection to occur. In order to explain fast reconnection in Earth’s magnetosphere, it is necessary to invoke additional terms in the generalized Ohm’s law:

\[\mathbf{E} + \mathbf{V} × \mathbf{B} = \underbrace{η\mathbf{J}}_\mbox{resistivity} + \underbrace{\frac{\mathbf{J} × \mathbf{B}}{n_e e}}_{\mbox{Hall term}} + \underbrace{\frac{∇·\mathrm{P}_e}{n_e e}}_{\mbox{Elec.~pressure}} + \dots\]

Here, \(\mathbf{E}\) is the electric field, \(\mathbf{B}\) is the magnetic field, \(\mathbf{V}\) is the velocity, \(\mathbf{J}\) is the current density, \(η\) is the resistivity, \(n_e\) is the electron number density, \(e\) is the fundamental positive charge, and \(\mathrm{P}_e\) is the electron pressure tensor.

The Hall term becomes important on scales shorter than the ion inertial length, \(d_i ≡ c/ω_{pi}\), where \(ω_{ps}\) is the plasma frequency for species \(s\). When the Hall effect is important, the electrons and ions decouple from each other. During reconnection, the Hall effect leads to an outer ion diffusion of length \(d_i\) and an inner diffusion region about the size of the electron inertial length, \(d_e ≡ c/ω_{pe}\). Similarly, the electron pressure gradient term becomes important on scales \(≲ d_i\).

reconnection-from-zweibel-and-yamada-2009-2.png

Our goal in this notebook is to calculate \(d_i\) and \(d_e\) to get an idea of how far the MMS spacecraft should be separated from each other.

Length scales

Let’s choose some characteristic plasma parameters for the magnetosphere.

[2]:
n = 1 * u.cm**-3
B = 5 * u.nT
T = 10**4.5 * u.K

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.

[3]:
inertial_length(n=n, particle="p+").to("km")
[3]:
$227.71077 \; \mathrm{km}$

The ion diffusion regions should therefore be a few hundred kilometers thick. Let’s calculate the electron inertial length next.

[4]:
inertial_length(n=n, particle="e-").to("km")
[4]:
$5.3140933 \; \mathrm{km}$

The electron diffusion region should therefore have a characteristic length scale of a few kilometers, which is significantly smaller than the ion diffusion region.

We can also calculate the gyroradii for different protons and electrons based on the thermal velocity.

[5]:
gyroradius(B=B, particle="p+", T=T).to("km")
[5]:
$47.706234 \; \mathrm{km}$
[6]:
gyroradius(B=B, particle="e-", T=T).to("km")
[6]:
$1.1133278 \; \mathrm{km}$

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.

The images from this notebook are adapted from Zweibel & Yamada (2009).

This page was generated by nbsphinx from docs/notebooks/formulary/magnetostatics.ipynb.
Interactive online version: Binder badge.

Magnetostatic Fields

This notebook presents examples of using PlasmaPy’s magnetostatics module in plasmapy.formulary.

[1]:
%matplotlib inline

import astropy.units as u
import matplotlib.pyplot as plt
import numpy as np

from plasmapy.formulary import magnetostatics
from plasmapy.plasma.sources import Plasma3D

plt.rcParams["figure.figsize"] = [10.5, 10.5]

Common magnetostatic fields, like those from a magnetic dipole, can be generated and added to a plasma object.

[2]:
dipole = magnetostatics.MagneticDipole(
    np.array([0, 0, 1]) * u.A * u.m * u.m, np.array([0, 0, 0]) * u.m
)
print(dipole)
MagneticDipole(moment=[0. 0. 1.]m2 A, p0=[0. 0. 0.]m)

First, we will initialize a plasma on which the magnetic field will be calculated.

[3]:
plasma = Plasma3D(
    domain_x=np.linspace(-2, 2, 30) * u.m,
    domain_y=np.linspace(0, 0, 1) * u.m,
    domain_z=np.linspace(-2, 2, 20) * u.m,
)

Let’s then add the dipole field to it, and plot the results.

[4]:
plasma.add_magnetostatic(dipole)

X, Z = plasma.grid[0, :, 0, :], plasma.grid[2, :, 0, :]
U = plasma.magnetic_field[0, :, 0, :].value.T  # because grid uses 'ij' indexing
W = plasma.magnetic_field[2, :, 0, :].value.T  # because grid uses 'ij' indexing
[5]:
plt.figure()
plt.axis("square")
plt.xlim(-2, 2)
plt.ylim(-2, 2)
plt.xlabel("x")
plt.ylabel("z")
plt.title("Dipole magnetic field")
plt.streamplot(plasma.x.value, plasma.z.value, U, W)
plt.show()
_images/notebooks_formulary_magnetostatics_9_0.png

Next let’s calculate the magnetic field from a current-carrying loop with CircularWire.

[6]:
cw = magnetostatics.CircularWire(
    np.array([0, 0, 1]), np.array([0, 0, 0]) * u.m, 1 * u.m, 1 * u.A
)
print(cw)
CircularWire(normal=[0. 0. 1.], center=[0. 0. 0.]m, radius=1.0m, current=1.0A)

Let’s initialize another plasma object, add the magnetic field from the circular wire to it, and plot the result.

[7]:
plasma = Plasma3D(
    domain_x=np.linspace(-2, 2, 30) * u.m,
    domain_y=np.linspace(0, 0, 1) * u.m,
    domain_z=np.linspace(-2, 2, 20) * u.m,
)
[8]:
plasma.add_magnetostatic(cw)

X, Z = plasma.grid[0, :, 0, :], plasma.grid[2, :, 0, :]
U = plasma.magnetic_field[0, :, 0, :].value.T  # because grid uses 'ij' indexing
W = plasma.magnetic_field[2, :, 0, :].value.T  # because grid uses 'ij' indexing

plt.figure()
plt.axis("square")
plt.xlim(-2, 2)
plt.ylim(-2, 2)
plt.xlabel("x")
plt.ylabel("z")
plt.title("Magnetic field from a circular coil")
plt.tight_layout()
plt.streamplot(plasma.x.value, plasma.z.value, U, W)
plt.show()
_images/notebooks_formulary_magnetostatics_14_0.png

A circular wire can be described as parametric equation and converted to a GeneralWire. Let’s do that, and check that the resulting magnetic fields are close.

[9]:
gw_cw = cw.to_GeneralWire()

print(gw_cw.magnetic_field([0, 0, 0]) - cw.magnetic_field([0, 0, 0]))
[ 0.00000000e+00  0.00000000e+00 -4.13416205e-12] T

Finally, let’s use InfiniteStraightWire to calculate the magnetic field from an infinite straight wire, add it to a plasma object, and plot the results.

[10]:
iw = magnetostatics.InfiniteStraightWire(
    np.array([0, 1, 0]), np.array([0, 0, 0]) * u.m, 1 * u.A
)
print(iw)
InfiniteStraightWire(direction=[0. 1. 0.], p0=[0. 0. 0.]m, current=1.0A)
[11]:
plasma = Plasma3D(
    domain_x=np.linspace(-2, 2, 30) * u.m,
    domain_y=np.linspace(0, 0, 1) * u.m,
    domain_z=np.linspace(-2, 2, 20) * u.m,
)

plasma.add_magnetostatic(iw)

X, Z = plasma.grid[0, :, 0, :], plasma.grid[2, :, 0, :]
U = plasma.magnetic_field[0, :, 0, :].value.T  # because grid uses 'ij' indexing
W = plasma.magnetic_field[2, :, 0, :].value.T  # because grid uses 'ij' indexing

plt.figure()
plt.title("Magnetic field from an infinite straight wire")
plt.axis("square")
plt.xlim(-2, 2)
plt.ylim(-2, 2)
plt.xlabel("x")
plt.ylabel("z")
plt.tight_layout()
plt.streamplot(plasma.x.value, plasma.z.value, U, W)
plt.show()
_images/notebooks_formulary_magnetostatics_19_0.png

This page was generated by nbsphinx from docs/notebooks/formulary/solar_plasma_beta.ipynb.
Interactive online version: Binder badge.

Plasma beta in the solar atmosphere

This notebook demonstrates plasmapy.formulary by calculating plasma \(β\) in different regions of the solar atmosphere.

Contents
  1. Introduction

  2. Solar corona

  3. Solar chromosphere

  4. Quiet solar photosphere

  5. Sunspot photosphere

Introduction

Plasma beta (\(β\)) is one of the most fundamental plasma parameters. \(β\) is the ratio of the thermal plasma pressure to the magnetic pressure:

\[β = \frac{p_{therm}}{p_{mag}}.\]

How a plasma behaves depends strongly on \(β\). When \(β ≫ 1\), the magnetic field is not strong enough to significantly alter the dynamics, so the plasma motion is more gas-like. When \(β ≪ 1\), magnetic tension and magnetic pressure dominate the dynamics.

Let’s use plasmapy.formulary.beta to compare \(β\) in different parts of the solar atmosphere.

[1]:
import astropy.units as u

from plasmapy.formulary import beta
Solar corona

Let’s start by defining some plasma parameters for an active region in the solar corona.

[2]:
T_corona = 1 * u.MK
n_corona = 1e9 * u.cm**-3
B_corona = 50 * u.G

When we use these parameters in beta, we find that \(β\) is quite small which implies that the corona is magnetically dominated.

[3]:
beta(T_corona, n_corona, B_corona)
[3]:
$0.0013879798 \; \mathrm{}$
Solar chromosphere

Next let’s calculate \(β\) for the chromosphere. Bogod et al. (2015) found that the quiet chromosphere ranges from ∼40–200 G. We can get the temperature and number density of hydrogen from model C7 of Avrett & Loeser (2007) for 1 Mm above the photosphere.

[4]:
T_chromosphere = 6225 * u.K
n_chromosphere = 2.711e13 * u.cm**-3
B_chromosphere = [40, 200] * u.G
[5]:
beta(T_chromosphere, n_chromosphere, B_chromosphere)
[5]:
$[0.36599237,~0.014639695] \; \mathrm{}$

When \(B\) is small, plasma \(β\) is not too far from 1, which means that both magnetic and plasma pressure gradient forces are important when the chromospheric magnetic field is relatively weak. When near the higher range of \(B\), \(β\) is small so that the magnetic forces are more important than plasma pressure gradient forces.

Quiet solar photosphere

Let’s specify some characteristic plasma parameters for the solar photosphere.

[6]:
T_photosphere = 5800 * u.K
n_photosphere = 1e17 * u.cm**-3
B_photosphere = 400 * u.G

When we calculate β for the photosphere, we find that it is an order of magnitude larger than 1, so plasma pressure gradient forces are more important than magnetic forces.

[7]:
beta(T_photosphere, n_photosphere, B_photosphere)
[7]:
$12.578567 \; \mathrm{}$
Sunspot photosphere

The magnetic field in the solar photosphere is strongest in sunspots, so we would expect β to be lowest there. Let’s estimate some plasma parameters for a sunspot.

[8]:
T_sunspot = 4500 * u.K
B_sunspot = 2 * u.kG

When we calculate β, we find that both pressure gradient and magnetic forces will be important.

[9]:
beta(T_sunspot, n_photosphere, B_sunspot)
[9]:
$0.39036931 \; \mathrm{}$

This page was generated by nbsphinx from docs/notebooks/formulary/thermal_bremsstrahlung.ipynb.
Interactive online version: Binder badge.

Emission of Thermal Bremsstrahlung by a Maxwellian Plasma

The radiation.thermal_bremsstrahlung function calculates the bremsstrahlung spectrum emitted by the collision of electrons and ions in a thermal (Maxwellian) plasma. This function calculates this quantity in the Rayleigh-Jeans limit where \(\hbar\omega \ll k_B T_e\). In this regime, the power spectrum of the emitted radiation is

\begin{equation} \frac{dP}{d\omega} = \frac{8 \sqrt{2}}{3\sqrt{\pi}} \bigg ( \frac{e^2}{4 \pi \epsilon_0} \bigg )^3 \bigg ( m_e c^2 \bigg )^{-\frac{3}{2}} \bigg ( 1 - \frac{\omega_{pe}^2}{\omega^2} \bigg )^\frac{1}{2} \frac{Z_i^2 n_i n_e}{\sqrt{k_B T_e}} E_1(y) \end{equation}

where \(w_{pe}\) is the electron plasma frequency and \(E_1\) is the exponential integral

\begin{equation} E_1 (y) = - \int_{-y}^\infty \frac{e^{-t}}{t}dt \end{equation}

and y is the dimensionless argument

\begin{equation} y = \frac{1}{2} \frac{\omega^2 m_e}{k_{max}^2 k_B T_e} \end{equation}

where \(k_{max}\) is a maximum wavenumber arising from binary collisions approximated here as

\begin{equation} k_{max} = \frac{1}{\lambda_B} = \frac{\sqrt{m_e k_B T_e}}{\hbar} \end{equation}

where \(\lambda_B\) is the electron de Broglie wavelength. In some regimes other values for \(k_{max}\) may be appropriate, so its value may be set using a keyword. Bremsstrahlung emission is greatly reduced below the electron plasma frequency (where the plasma is opaque to EM radiation), so these expressions are only valid in the regime \(w < w_{pe}\).

[1]:
%matplotlib inline

import astropy.constants as const
import astropy.units as u
import matplotlib.pyplot as plt
import numpy as np

from plasmapy.formulary.radiation import thermal_bremsstrahlung

Create an array of frequencies over which to calculate the bremsstrahlung spectrum and convert these frequencies to photon energies for the purpose of plotting the results. Set the plasma density, temperature, and ion species.

[2]:
frequencies = np.arange(15, 16, 0.01)
frequencies = (10**frequencies) / u.s

energies = (frequencies * const.h.si).to(u.eV)

ne = 1e22 * u.cm**-3
Te = 1e2 * u.eV
ion = "C-12 4+"

Calculate the spectrum, then plot it.

[3]:
spectrum = thermal_bremsstrahlung(frequencies, ne, Te, ion=ion)

print(spectrum.unit)

lbl = f"$T_e$ = {Te.value:.1e} eV,\n" + f"$n_e$ = {ne.value:.1e} 1/cm^3"
plt.plot(energies, spectrum, label=lbl)
plt.title("Thermal Bremsstrahlung Spectrum")
plt.xlabel("Energy (eV)")
plt.ylabel("Power Spectral Density (W s/m^3)")
plt.legend()
plt.show()
kg / (m s2)
_images/notebooks_formulary_thermal_bremsstrahlung_6_1.png

The power spectrum is the power per angular frequency per volume integrated over \(4\pi\) sr of solid angle, and therefore has units of watts / (rad/s) / m\(^3\) * \(4\pi\) rad = W s/m\(^3\).

[4]:
spectrum = spectrum.to(u.W * u.s / u.m**3)
spectrum.unit
[4]:
$\mathrm{\frac{W\,s}{m^{3}}}$

This means that, for a given volume and time period, the total energy emitted can be determined by integrating the power spectrum

[5]:
t = 5 * u.ns
vol = 0.5 * u.cm**3
dw = 2 * np.pi * np.gradient(frequencies)  # Frequency step size
total_energy = (np.sum(spectrum * dw) * t * vol).to(u.J)
print(f"Total Energy: {total_energy.value:.2e} J")
Total Energy: 4.97e+04 J

This page was generated by nbsphinx from docs/notebooks/formulary/thermal_speed.ipynb.
Interactive online version: Binder badge.

Thermal Speed

[1]:
%matplotlib inline

import astropy.units as u
import matplotlib.pyplot as plt
import numpy as np

from plasmapy.formulary import (
    Maxwellian_speed_1D,
    Maxwellian_speed_2D,
    Maxwellian_speed_3D,
)
from plasmapy.formulary.speeds import thermal_speed

The thermal_speed function can be used to calculate the thermal velocity for a Maxwellian velocity distribution. There are three common definitions of the thermal velocity, which can be selected using the “method” keyword, which are defined for a 3D velocity distribution as

  • ‘most_probable’ \(v_{th} = \sqrt{\frac{2 k_B T}{m}}\)

  • ‘rms’ \(v_{th} = \sqrt{\frac{3 k_B T}{m}}\)

  • ‘mean_magnitude’ \(v_{th} = \sqrt{\frac{8 k_B T}{m\pi}}\)

The differences between these velocities can be seen by plotitng them on a 3D Maxwellian speed distribution

[2]:
T = 1e5 * u.K
speeds = np.linspace(0, 8e6, num=600) * u.m / u.s

pdf_3D = Maxwellian_speed_3D(speeds, T=T, particle="e-")

fig, ax = plt.subplots(figsize=(4, 3))

v_most_prob = thermal_speed(T=T, particle="e-", method="most_probable", ndim=3)
v_rms = thermal_speed(T=T, particle="e-", method="rms", ndim=3)
v_mean_magnitude = thermal_speed(T=T, particle="e-", method="mean_magnitude", ndim=3)

ax.plot(speeds / v_rms, pdf_3D, color="black", label="Maxwellian")

ax.axvline(x=v_most_prob / v_rms, color="blue", label="Most Probable")
ax.axvline(x=v_rms / v_rms, color="green", label="RMS")
ax.axvline(x=v_mean_magnitude / v_rms, color="red", label="Mean Magnitude")

ax.set_xlim(-0.1, 3)
ax.set_ylim(0, None)
ax.set_title("3D")
ax.set_xlabel("|v|/|v$_{rms}|$")
ax.set_ylabel("f(|v|)")
[2]:
Text(0, 0.5, 'f(|v|)')
_images/notebooks_formulary_thermal_speed_3_1.png

Similar speeds are defined for 1D and 2D distributions. The differences between these definitions can be illustrated by plotting them on their respective Maxwellian speed distributions.

[3]:
pdf_1D = Maxwellian_speed_1D(speeds, T=T, particle="e-")
pdf_2D = Maxwellian_speed_2D(speeds, T=T, particle="e-")

dim = [1, 2, 3]
pdfs = [pdf_1D, pdf_2D, pdf_3D]

plt.tight_layout()
fig, ax = plt.subplots(ncols=3, figsize=(10, 3))

for n, pdf in enumerate(pdfs):
    ndim = n + 1
    v_most_prob = thermal_speed(T=T, particle="e-", method="most_probable", ndim=ndim)
    v_rms = thermal_speed(T=T, particle="e-", method="rms", ndim=ndim)
    v_mean_magnitude = thermal_speed(
        T=T, particle="e-", method="mean_magnitude", ndim=ndim
    )

    ax[n].plot(speeds / v_rms, pdf, color="black", label="Maxwellian")

    ax[n].axvline(x=v_most_prob / v_rms, color="blue", label="Most Probable")
    ax[n].axvline(x=v_rms / v_rms, color="green", label="RMS")
    ax[n].axvline(x=v_mean_magnitude / v_rms, color="red", label="Mean Magnitude")

    ax[n].set_xlim(-0.1, 3)
    ax[n].set_ylim(0, None)
    ax[n].set_title(f"{ndim:d}D")
    ax[n].set_xlabel("|v|/|v$_{rms}|$")
    ax[n].set_ylabel("f(|v|)")


ax[2].legend(bbox_to_anchor=(1.9, 0.8), loc="upper right")
[3]:
<matplotlib.legend.Legend at 0x7f45ad40c530>
<Figure size 640x480 with 0 Axes>
_images/notebooks_formulary_thermal_speed_5_2.png

Particles

This page was generated by nbsphinx from docs/notebooks/particles/ace.ipynb.
Interactive online version: Binder badge.

Ionization states in an interplanetary coronal mass ejection

The ionization state distribution for an element refers to the fractions of that element at each ionic level. For example, the ionization state of helium in the solar wind might be 10% He\(^{0+}\), 70% He\(^{1+}\), and 20% He\(^{2+}\). This notebook introduces the data structures in plasmapy.particles for representing the ionization state of a plasma.

[1]:
import astropy.units as u
import matplotlib.pyplot as plt

from plasmapy.particles import IonizationState, IonizationStateCollection
The ionization state of a single element

Let’s create an IonizationState object for helium using the ionic fractions described above. We’ll specify the total number density of helium via the n_elem keyword argument.

[2]:
He_states = IonizationState("He-4", [0.1, 0.7, 0.2], n_elem=1e13 * u.m**-3)

The ionization state distribution is stored in the ionic_fractions attribute of IonizationState.

[3]:
He_states.ionic_fractions
[3]:
array([0.1, 0.7, 0.2])

We can get the symbols for each ionic level in He_states too.

[4]:
He_states.ionic_symbols
[4]:
['He-4 0+', 'He-4 1+', 'He-4 2+']

Because we provided the number density of the element as a whole, we can get back the number density of each ionic level.

[5]:
He_states.number_densities
[5]:
$[1 \times 10^{12},~7 \times 10^{12},~2 \times 10^{12}] \; \mathrm{\frac{1}{m^{3}}}$

We can also get the electron number density required to balance the positive charges for ions of this element.

[6]:
He_states.n_e
[6]:
$1.1 \times 10^{13} \; \mathrm{\frac{1}{m^{3}}}$

We can provide an IonizationState with a charge number as an index to get an IonicLevel object that contains most of these attributes, but for a single ionic level (like He\(^{1+}\)). This capability is useful if we wish to iterate over the ions of an element.

[7]:
for Z in range(3):
    print(He_states[Z])
IonicLevel('He-4 0+', ionic_fraction=0.1)
IonicLevel('He-4 1+', ionic_fraction=0.7)
IonicLevel('He-4 2+', ionic_fraction=0.2)

We can get information about the average charge state via Z_mean, Z_most_abundant, and Z_rms.

[8]:
He_states.Z_mean
[8]:
1.1
[9]:
He_states.Z_most_abundant
[9]:
[1]
[10]:
He_states.Z_rms
[10]:
1.224744871391589

We can calculate the properties of the average ionic level.

[11]:
He_states.average_ion()
[11]:
CustomParticle(mass=6.645477039375987e-27 kg, charge=1.7623942974e-19 C)

We can use the summarize() method to get information about the ionization state.

[12]:
He_states.summarize()
IonizationState instance for He-4 with Z_mean = 1.10
----------------------------------------------------------------
He-4  0+: 0.100    n_i = 1.00e+12 m**-3
He-4  1+: 0.700    n_i = 7.00e+12 m**-3
He-4  2+: 0.200    n_i = 2.00e+12 m**-3
----------------------------------------------------------------
n_elem = 1.00e+13 m**-3
n_e = 1.10e+13 m**-3
----------------------------------------------------------------
Ionization states of multiple elements

Now let’s look at some actual average hourly densities for ions of C, O, and Fe during an interplanetary coronal mass ejection (ICME) observed by the Advanced Composition Explorer (ACE) near 1 AU. The data were estimated from Figure 4 in Gilbert et al. (2012). This data set is noteworthy because there is information from very low charge states to very high charge states for several elements.

The electron ionization and radiative recombination rates for a given temperature are proportional to \(n_i n_e\), where \(n_i\) is the ion number density and \(n_e\) is the electron number density. Ionization and recombination are fast at high densities and slow at low densities. High density plasma can reach ionization equilibrium (when the recombination and ionization rates balance each other out) more quickly than low density plasma. Plasma undergoing rapid heating or cooling will be in non-equilibrium ionization because ionization and recombination can’t keep up with the temperature changes.

Quiescent plasma near the sun is typically close to ionization equilibrium because the density is high. As plasma moves away from the sun, the ionization and recombination rates drop rapidly because of the decreasing number density. The ionization states freeze out at several solar radii as the ionization and recombination time scales begin to exceed the time it takes for plasma to move from the sun to 1 AU.

The ionization states of the solar wind are powerful diagnostics of the thermodynamic history of solar wind and ICME plasma. The ionization states observed by ACE at 1 AU are essentially the same as at ∼\(5R_☉\).

[13]:
number_densities = {
    "C": [0, 5.7e-7, 4.3e-5, 3.6e-6, 2.35e-6, 1e-6, 1.29e-6] * u.cm**-3,
    "O": [0, 1.2e-7, 2.2e-4, 7.8e-6, 8.8e-7, 1e-6, 4e-6, 1.3e-6, 1.2e-7] * u.cm**-3,
    "Fe": [
        0,
        0,
        1.4e-8,
        1.1e-7,
        2.5e-7,
        2.2e-7,
        1.4e-7,
        1.2e-7,
        2.1e-7,
        2.1e-7,
        1.6e-7,
        8e-8,
        6.3e-8,
        4.2e-8,
        2.5e-8,
        2.3e-8,
        1.5e-8,
        3.1e-8,
        6.1e-9,
        2.3e-9,
        5.3e-10,
        2.3e-10,
        0,
        0,
        0,
        0,
        0,
    ]
    * u.cm**-3,
}

Let’s use this information as an input for IonizationStateCollection: a data structure for the ionization states of multiple elements.

[14]:
states = IonizationStateCollection(number_densities)

We can index this to get an IonizationState for one of the elements.

[15]:
states["C"]
[15]:
<IonizationState instance for C>

We can get the relative abundances of each of the elements.

[16]:
states.abundances
[16]:
{'C': 0.17942722921968796, 'O': 0.8146086249190311, 'Fe': 0.005964145861281176}
[17]:
states.log_abundances
[17]:
{'C': -0.7461116493492604, 'O': -0.08905099599945357, 'Fe': -2.224451743828212}

We can get the number densities as a dict (like what we provided) and the electron number density (assuming quasineutrality, but only for the elements contained in the data structure).

[18]:
states.number_densities
[18]:
{'C': <Quantity [ 0.  ,  0.57, 43.  ,  3.6 ,  2.35,  1.  ,  1.29] 1 / m3>,
 'O': <Quantity [0.0e+00, 1.2e-01, 2.2e+02, 7.8e+00, 8.8e-01, 1.0e+00, 4.0e+00,
            1.3e+00, 1.2e-01] 1 / m3>,
 'Fe': <Quantity [0.0e+00, 0.0e+00, 1.4e-02, 1.1e-01, 2.5e-01, 2.2e-01, 1.4e-01,
            1.2e-01, 2.1e-01, 2.1e-01, 1.6e-01, 8.0e-02, 6.3e-02, 4.2e-02,
            2.5e-02, 2.3e-02, 1.5e-02, 3.1e-02, 6.1e-03, 2.3e-03, 5.3e-04,
            2.3e-04, 0.0e+00, 0.0e+00, 0.0e+00, 0.0e+00, 0.0e+00] 1 / m3>}
[19]:
states.n_e
[19]:
$638.73093 \; \mathrm{\frac{1}{m^{3}}}$

We can summarize this information too, but let’s specify the minimum ionic fraction to print.

[20]:
states.summarize(minimum_ionic_fraction=0.02)
IonizationStateCollection instance for: C, O, Fe
----------------------------------------------------------------
C  2+: 0.830    n_i = 4.30e+01 m**-3
C  3+: 0.069    n_i = 3.60e+00 m**-3
C  4+: 0.045    n_i = 2.35e+00 m**-3
C  6+: 0.025    n_i = 1.29e+00 m**-3
----------------------------------------------------------------
O  2+: 0.935    n_i = 2.20e+02 m**-3
O  3+: 0.033    n_i = 7.80e+00 m**-3
----------------------------------------------------------------
Fe  3+: 0.064    n_i = 1.10e-01 m**-3
Fe  4+: 0.145    n_i = 2.50e-01 m**-3
Fe  5+: 0.128    n_i = 2.20e-01 m**-3
Fe  6+: 0.081    n_i = 1.40e-01 m**-3
Fe  7+: 0.070    n_i = 1.20e-01 m**-3
Fe  8+: 0.122    n_i = 2.10e-01 m**-3
Fe  9+: 0.122    n_i = 2.10e-01 m**-3
Fe 10+: 0.093    n_i = 1.60e-01 m**-3
Fe 11+: 0.046    n_i = 8.00e-02 m**-3
Fe 12+: 0.037    n_i = 6.30e-02 m**-3
Fe 13+: 0.024    n_i = 4.20e-02 m**-3
----------------------------------------------------------------
n_e = 6.39e+02 m**-3
----------------------------------------------------------------
[21]:
fig, axes = plt.subplots(1, 3, figsize=(10, 3), tight_layout=True)

for state, ax in zip(states, axes, strict=False):
    ax.bar(state.charge_numbers, state.ionic_fractions)
    ax.set_title(f"Ionization state for {state.base_particle}")
    ax.set_xlabel("ionic level")
    ax.set_ylabel("log$_{10}$ of ionic fraction")
    ax.set_yscale("log")
_images/notebooks_particles_ace_40_0.png

The wide range of average ionic levels for each element is strong evidence that the plasma observed by ACE originated from a wide range of temperatures. The lowest charge states are evidence of cool filament plasma while the high charge states are evidence of rapidly heated plasma.

Plasma Objects

This page was generated by nbsphinx from docs/notebooks/plasma/grids_cartesian.ipynb.
Interactive online version: Binder badge.

Grids: Uniformly-Spaced Cartesian Grids

Grids are a datastructure that represent one or more physical quantities that share spatial coordinates. For example, the density or magnetic field in a plasma as specified on a Cartesian grid. In addition to storing data, grids have built-in interpolator functions for estimating the values of quantities in between grid vertices.

Creating a grid
[1]:
%matplotlib inline

import astropy.units as u
import numpy as np

from plasmapy.plasma import grids

A grid can be created either by providing three arrays of spatial coordinates for vertices (eg. x,yz positions) or using a np.linspace-like syntax. For example, the two following methods are equivalent:

[2]:
# Method 1
xaxis, yaxis, zaxis = [np.linspace(-1 * u.cm, 1 * u.cm, num=20)] * 3
x, y, z = np.meshgrid(xaxis, yaxis, zaxis, indexing="ij")
grid = grids.CartesianGrid(x, y, z)

# Method 2
grid = grids.CartesianGrid(
    np.array([-1, -1, -1]) * u.cm, np.array([1, 1, 1]) * u.cm, num=(150, 150, 150)
)

The grid object provides access to a number of properties

[3]:
print(f"Is the grid uniformly spaced? {grid.is_uniform}")
print(f"Grid shape: {grid.shape}")
print(f"Grid units: {grid.units}")
print(f"Grid spacing on xaxis: {grid.dax0:.2f}")
Is the grid uniformly spaced? True
Grid shape: (150, 150, 150)
Grid units: [Unit("cm"), Unit("cm"), Unit("cm")]
Grid spacing on xaxis: 0.01 cm

The grid points themselves can be explicitly accessed in one of two forms

[4]:
x, y, z = grid.grids
x.shape
[4]:
(150, 150, 150)
[5]:
xyz = grid.grid
xyz.shape
[5]:
(150, 150, 150, 3)

And the axes can be accessed similarly.

[6]:
xaxis = grid.ax0
xaxis.shape
[6]:
(150,)
Adding Quantities

Now that the grid has been initialized, we can add quantities to it that represent physical properties defined on the grid vertices. Each quantity must be a u.Quantity array of the same shape as the grid.

[7]:
Ex = np.random.rand(*grid.shape) * u.V / u.m
Ey = np.random.rand(*grid.shape) * u.V / u.m
Ez = np.random.rand(*grid.shape) * u.V / u.m
Bz = np.random.rand(*grid.shape) * u.T
Bz.shape
[7]:
(150, 150, 150)

When quantities are added to the grid, they are associated with a key string (just like a dictionary). Any key string can be used, but PlasmaPy functions use a shared set of recognized quantities to automatically interperet quantities. Each entry is stored as a namedtuple with fields (“key”, “description”, “unit”). The full list of recognized quantities can be accessed in the module:

[8]:
for key in grid.recognized_quantities:
    rk = grid.recognized_quantities[key]
    key, description, unit = rk.key, rk.description, rk.unit
    print(f"{key} -> {description} ({unit})")
x -> x spatial position (m)
y -> y spatial position (m)
z -> z spatial position (m)
rho -> Mass density (kg / m3)
E_x -> Electric field (x component) (V / m)
E_y -> Electric field (y component) (V / m)
E_z -> Electric field (z component) (V / m)
B_x -> Magnetic field (x component) (T)
B_y -> Magnetic field (y component) (T)
B_z -> Magnetic field (z component) (T)
phi -> Electric Scalar Potential (V)

Quantities can be added to the grid as keyword arguments. The keyword becomes the key string for the quantity in the dataset.

[9]:
grid.add_quantities(B_z=Bz)
grid.add_quantities(E_x=Ex, E_y=Ey, E_z=Ez)

Adding an unrecognized quantity will lead to a warning, but the quantity will still be added to the grid and can still be accessed by the user later.

[10]:
custom_quantity = np.random.rand(*grid.shape) * u.T * u.mm
grid.add_quantities(int_B=custom_quantity)
/tmp/ipykernel_5641/1359294134.py:2: UserWarning: Warning: int_B is not recognized quantity key
  grid.add_quantities(int_B=custom_quantity)

A summary of the grid, including the currently-defined quantities, can be produced by printing the grid object

[11]:
print(grid)
*** Grid Summary ***
<class 'plasmapy.plasma.grids.CartesianGrid'>
Dimensions: (ax0: 150, ax1: 150, ax2: 150)
Uniformly Spaced: (dax0, dax1, dax2) = (0.013 cm, 0.013 cm, 0.013 cm)
-----------------------------
Coordinates:
        -> ax0 (cm) float64 (150,)
        -> ax1 (cm) float64 (150,)
        -> ax2 (cm) float64 (150,)
-----------------------------
Recognized Quantities:
        -> B_z (T) float64 (150, 150, 150)
        -> E_x (V / m) float64 (150, 150, 150)
        -> E_y (V / m) float64 (150, 150, 150)
        -> E_z (V / m) float64 (150, 150, 150)
-----------------------------
Unrecognized Quantities:
        -> int_B (T mm) float64 (150, 150, 150)

A simple list of the defined quantity keys on the grid can also be easily accessed

[12]:
print(grid.quantities)
['B_z', 'E_x', 'E_y', 'E_z', 'int_B']
Methods

A number of methods are built into grid objects, and are illustrated here.

The grid.on_grid method determines which points in an array are within the bounds of the grid. Since our example grid is a cube spanning from -1 to -1 cm on each axis, the first of the following points is on the grid while the second is not.

[13]:
pos = np.array([[0.1, -0.3, 0], [3, 0, 0]]) * u.cm
print(grid.on_grid(pos))
[ True False]

Similarly, the grid.vector_intersects function determines whether the line between two points passes through the grid.

[14]:
pt0 = np.array([3, 0, 0]) * u.cm
pt1 = np.array([-3, 0, 0]) * u.cm
pt2 = np.array([3, 10, 0]) * u.cm

print(f"Line from pt0 to pt1 intersects: {grid.vector_intersects(pt0, pt1)}")
print(f"Line from pt0 to pt2 intersects: {grid.vector_intersects(pt0, pt2)}")
Line from pt0 to pt1 intersects: True
Line from pt0 to pt2 intersects: False
Interpolating Quantities

Grid objects contain several interpolator methods to evaluate quantites at positions between the grid vertices. These interpolators use the fact that all of the quantities are defined on the same grid to perform faster interpolations using a nearest-neighbor scheme. When an interpolation at a position is requested, the grid indices closest to that position are calculated. Then, the quantity arrays are evaluated at the interpolated indices. Using this method, many quantities can be interpolated at the same positions in almost the same amount of time as is required to interpolate one quantity.

Positions are provided to the interpolator as a u.Quantity array of shape [N,3] where N is the number of positions and [i,:] represents the x,y,z coordinates of the ith position:

[15]:
pos = np.array([[0.1, -0.3, 0], [0.5, 0.25, 0.8]]) * u.cm
print(f"Pos shape: {pos.shape}")
print(f"Position 1: {pos[0,:]}")
print(f"Position 2: {pos[1,:]}")
Pos shape: (2, 3)
Position 1: [ 0.1 -0.3  0. ] cm
Position 2: [0.5  0.25 0.8 ] cm

The simplest interpolator directly returns the nearest-neighbor values for each quantity. Positions that are out-of-bounds return an interpolated value of zero.

[16]:
Ex_vals = grid.nearest_neighbor_interpolator(pos, "E_x")
print(f"Ex at position 1: {Ex_vals[0]:.2f}")
Ex at position 1: 0.90 V / m

Multiple values can be interpolated at the same time by including additional keys as arguments. In this case, the interpolator returns a tuple of arrays as a result.

[17]:
Ex_vals, Ey_vals, Ez_vals = grid.nearest_neighbor_interpolator(pos, "E_x", "E_y", "E_z")
print(f"E at position 1: ({Ex_vals[0]:.2f}, {Ey_vals[0]:.2f}, {Ez_vals[0]:.2f})")
E at position 1: (0.90 V / m, 0.43 V / m, 0.59 V / m)

For a higher-order interpolation, some grids (such as the CartesianGrid subclass) also include a volume-weighted interpolator. This interpolator averages the values on the eight grid vertices surrounding the position (weighted by their distance). The syntax for using this interpolator is the same:

[18]:
Ex_vals, Ey_vals, Ez_vals = grid.volume_averaged_interpolator(pos, "E_x", "E_y", "E_z")
print(f"E at position 1: ({Ex_vals[0]:.2f}, {Ey_vals[0]:.2f}, {Ez_vals[0]:.2f})")
E at position 1: (0.58 V / m, 0.29 V / m, 0.60 V / m)

If repeated identical calls are being made to the same interpolator at different positions (for example, in a simulation loop), setting the persistent keyword to True will increase performance by not repeatedly re-loading the same quantity arrays from the grid. Setting this keyword for a single interpolation will not improve performance, and is not recommended (and is only done here for illustration).

[19]:
Ex_vals, Ey_vals, Ez_vals = grid.volume_averaged_interpolator(
    pos, "E_x", "E_y", "E_z", persistent=True
)
print(f"E at position 1: ({Ex_vals[0]:.2f}, {Ey_vals[0]:.2f}, {Ez_vals[0]:.2f})")
E at position 1: (0.58 V / m, 0.29 V / m, 0.60 V / m)

This page was generated by nbsphinx from docs/notebooks/plasma/grids_nonuniform.ipynb.
Interactive online version: Binder badge.

Grids: Non-Uniform Grids

Some data cannot be easily represented on a grid of uniformly spaced vertices. It is still possible to create a grid object to represent such a dataset.

[1]:
%matplotlib inline

import astropy.units as u
import numpy as np

from plasmapy.plasma import grids
[2]:
grid = grids.NonUniformCartesianGrid(
    np.array([-1, -1, -1]) * u.cm, np.array([1, 1, 1]) * u.cm, num=(50, 50, 50)
)
/home/docs/checkouts/readthedocs.org/user_builds/plasmapy/envs/latest/lib/python3.12/site-packages/plasmapy/plasma/grids.py:613: FutureWarning: the `pandas.MultiIndex` object(s) passed as 'ax' coordinate(s) or data variable(s) will no longer be implicitly promoted and wrapped into multiple indexed coordinates in the future (i.e., one coordinate for each multi-index level + one dimension coordinate). If you want to keep this behavior, you need to first wrap it explicitly using `mindex_coords = xarray.Coordinates.from_pandas_multiindex(mindex_obj, 'dim')` and pass it as coordinates, e.g., `xarray.Dataset(coords=mindex_coords)`, `dataset.assign_coords(mindex_coords)` or `dataarray.assign_coords(mindex_coords)`.
  self.ds.coords["ax"] = mdx

Currently, all non-uniform data is stored as an unordered 1D array of points. Therefore, although the dataset created above falls approximately on a Cartesian grid, its treatment is identical to a completely unordered set of points

[3]:
grid.shape
[3]:
(125000,)

Many of the properties defined for uniform grids are inaccessible for non-uniform grids. For example, it is not possible to pull out an axis. However, the following properties still apply

[4]:
print(f"Grid points: {grid.grid.shape}")
print(f"Units: {grid.units}")
Grid points: (125000, 3)
Units: [Unit("cm"), Unit("cm"), Unit("cm")]

Properties can be added in the same way as on uniform grids.

[5]:
Bx = np.random.rand(*grid.shape) * u.T
grid.add_quantities(B_x=Bx)
print(grid)
*** Grid Summary ***
<class 'plasmapy.plasma.grids.NonUniformCartesianGrid'>
Dimensions: (ax: 125000)
Non-Uniform Spacing
-----------------------------
Coordinates:
        -> ax (cm) object (125000,)
-----------------------------
Recognized Quantities:
        -> B_x (T) float64 (125000,)
-----------------------------
Unrecognized Quantities:
-None-

Methods

Many of the methods defined for uniform grids also work for non-uniform grids, however there is usually a substantial performance penalty in the non-uniform case.

For example, grid.on_grid behaves similarly. In this case, the boundaries of the grid are defined by the furthest point away from the origin in each direction.

[6]:
pos = np.array([[0.1, -0.3, 0], [3, 0, 0]]) * u.cm
print(grid.on_grid(pos))
[ True False]

The same definition is used to define the grid boundaries in grid.vector_intersects

[7]:
pt0 = np.array([3, 0, 0]) * u.cm
pt1 = np.array([-3, 0, 0]) * u.cm
pt2 = np.array([3, 10, 0]) * u.cm

print(f"Line from pt0 to pt1 intersects: {grid.vector_intersects(pt0, pt1)}")
print(f"Line from pt0 to pt2 intersects: {grid.vector_intersects(pt0, pt2)}")
Line from pt0 to pt1 intersects: True
Line from pt0 to pt2 intersects: False
Interpolating Quantities

Nearest-neighbor interpolation also works identically. However, volume-weighted interpolation is not implemented for non-uniform grids.

[8]:
pos = np.array([[0.1, -0.3, 0], [0.5, 0.25, 0.8]]) * u.cm
print(f"Pos shape: {pos.shape}")
print(f"Position 1: {pos[0,:]}")
print(f"Position 2: {pos[1,:]}")

Bx_vals = grid.nearest_neighbor_interpolator(pos, "B_x")
print(f"Bx at position 1: {Bx_vals[0]:.2f}")
Pos shape: (2, 3)
Position 1: [ 0.1 -0.3  0. ] cm
Position 2: [0.5  0.25 0.8 ] cm
Bx at position 1: 0.76 T

Simulation

This page was generated by nbsphinx from docs/notebooks/simulation/particle_tracker.ipynb.
Interactive online version: Binder badge.

[1]:
%matplotlib inline

Particle Tracker

An example of PlasmaPy’s particle tracker class.

[2]:
import astropy.units as u
import matplotlib.pyplot as plt
import numpy as np

from plasmapy.formulary import ExB_drift, gyrofrequency
from plasmapy.particles import Particle
from plasmapy.plasma.grids import CartesianGrid
from plasmapy.simulation.particle_tracker import (
    IntervalSaveRoutine,
    ParticleTracker,
    TimeElapsedTerminationCondition,
)

Take a look at the docs to gyrofrequency() and ParticleTracker for more information

Initialize a :class:~plasmapy.plasma.grids.CartesianGrid object. This will be the source of electric and magnetic fields for our particles to move in.

[3]:
grid_length = 10
grid = CartesianGrid(-1 * u.m, 1 * u.m, num=grid_length)

Initialize the fields. We’ll take B in the x direction and E in the y direction, which gets us an E cross B drift in the negative z direction.

[4]:
Bx_fill = 4 * u.T
Bx = np.full(grid.shape, Bx_fill.value) * u.T

Ey_fill = 2 * u.V / u.m
Ey = np.full(grid.shape, Ey_fill.value) * u.V / u.m

grid.add_quantities(B_x=Bx, E_y=Ey)
ExB_drift(np.asarray([0, Ey_fill.value, 0]) * u.V / u.m, np.asarray([Bx_fill.value, 0, 0]) * u.T)
[4]:
$[0,~0,~-0.5] \; \mathrm{\frac{m}{s}}$

|ParticleTracker| takes arrays of particle positions and velocities of the shape [nparticles, 3], so these arrays represent one particle starting at the origin.

[5]:
x0 = [[0, 0, 0]] * u.m
v0 = [[1, 0, 0]] * u.m / u.s
particle = Particle("p+")

Initialize our stop condition and save routine. We can determine a relevant duration for the experiment by calculating the gyroperiod for the particle.

[6]:
particle_gyroperiod = 1 / gyrofrequency(Bx_fill, particle).to(u.Hz, equivalencies=u.dimensionless_angles())

simulation_duration = 100 * particle_gyroperiod
save_interval = particle_gyroperiod / 10

termination_condition = TimeElapsedTerminationCondition(simulation_duration)
save_routine = IntervalSaveRoutine(save_interval)

Initialize the trajectory calculation.

[7]:
simulation = ParticleTracker(grid, save_routine=save_routine, termination_condition=termination_condition, verbose=False)
/home/docs/checkouts/readthedocs.org/user_builds/plasmapy/envs/latest/lib/python3.12/site-packages/plasmapy/simulation/particle_tracker.py:633: RuntimeWarning: Quantities should go to zero at edges of grid to avoid non-physical effects, but a value of 2.00E+00 V / m was found on the edge of the E_y array. Consider applying a envelope function to force the quantities at the edge to go to zero.
  warnings.warn(
/home/docs/checkouts/readthedocs.org/user_builds/plasmapy/envs/latest/lib/python3.12/site-packages/plasmapy/simulation/particle_tracker.py:633: RuntimeWarning: Quantities should go to zero at edges of grid to avoid non-physical effects, but a value of 4.00E+00 T was found on the edge of the B_x array. Consider applying a envelope function to force the quantities at the edge to go to zero.
  warnings.warn(

We still have to initialize the particle’s velocity. We’ll limit ourselves to one in the x direction, parallel to the magnetic field B - that way, it won’t turn in the z direction.

[8]:
simulation.load_particles(x0, v0, particle)

Run the simulation.

[9]:
simulation.run()

We can take a look at the trajectory in the z direction:

[10]:
t, x, v = save_routine.results()
particle_trajectory = x[:, 0]
particle_position_z = particle_trajectory[:, 2]

plt.scatter(t, particle_position_z)
[10]:
<matplotlib.collections.PathCollection at 0x7f2f7c329e20>
_images/notebooks_simulation_particle_tracker_19_1.png

or plot the shape of the trajectory in 3D:

[11]:
fig = plt.figure()
ax = fig.add_subplot(projection='3d')

ax.plot(*particle_trajectory.T)
ax.set_xlabel("X")
ax.set_ylabel("Y")
ax.set_zlabel("Z")
[11]:
Text(0.5, 0, 'Z')
_images/notebooks_simulation_particle_tracker_21_1.png

As a test, we calculate the mean velocity in the z direction from the velocity:

[12]:
v_mean = v[:, :, 2].mean()
print(
    f"The calculated drift velocity is {v_mean:.4f} to compare with the "
    f"expected E0/B0 = {-(Ey_fill/Bx_fill).value:.4f}"
)
The calculated drift velocity is -0.5076 m / s to compare with the expected E0/B0 = -0.5000

Feedback and Communication

Matrix chat room

The primary communication channel for PlasmaPy is our Matrix chat room. There is also a Gitter bridge to this chat room.

GitHub Discussions page

PlasmaPy’s GitHub Discussions page is a great place to suggest ideas, bring up discussion topics, and ask questions in threaded public discussions.

Mailing list

We also have a low traffic mailing list for occasional announcements and infrequent discussions.

Suggestion box

We have a suggestion box if you would like to (optionally anonymously) suggest a feature/topic for consideration. These will be reposted as GitHub issues, as appropriate, for further discussion.

Meetings and events

Please check PlasmaPy meetings for regularly scheduled meetings and special events. The regularly scheduled meetings include a community meeting to discuss code development, a project meeting to discuss broader aspects of the PlasmaPy project like education and outreach to the broader plasma community, and meetings of PlasmaPy working groups. The special events include Plasma Hack Week.

Contributor Covenant Code of Conduct

We adopt the Contributor Covenant, version 2.1, which is reproduced below.

Our Pledge

We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation.

We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.

Our Standards

Examples of behavior that contributes to a positive environment for our community include:

  • Demonstrating empathy and kindness toward other people

  • Being respectful of differing opinions, viewpoints, and experiences

  • Giving and gracefully accepting constructive feedback

  • Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience

  • Focusing on what is best not just for us as individuals, but for the overall community

Examples of unacceptable behavior include:

  • The use of sexualized language or imagery, and sexual attention or advances of any kind

  • Trolling, insulting or derogatory comments, and personal or political attacks

  • Public or private harassment

  • Publishing others’ private information, such as a physical or email address, without their explicit permission

  • Other conduct which could reasonably be considered inappropriate in a professional setting

Enforcement Responsibilities

Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.

Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.

Scope

This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.

Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at conduct@plasmapy.org. All complaints will be reviewed and investigated promptly and fairly.

All community leaders are obligated to respect the privacy and security of the reporter of any incident.

Enforcement Guidelines

Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:

1. Correction

Community Impact: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.

Consequence: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.

2. Warning

Community Impact: A violation through a single incident or series of actions.

Consequence: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.

3. Temporary Ban

Community Impact: A serious violation of community standards, including sustained inappropriate behavior.

Consequence: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.

4. Permanent Ban

Community Impact: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.

Consequence: A permanent ban from any sort of public interaction within the community.

Attribution

This Code of Conduct is adapted from the Contributor Covenant, version 2.1, available at https://www.contributor-covenant.org/version/2/1/code_of_conduct.

Community Impact Guidelines were inspired by Mozilla’s code of conduct enforcement ladder.

For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.

Acknowledging and Citing

If you use PlasmaPy for a project that results in a publication, please cite the Zenodo record for the specific version of PlasmaPy used in your project. Citing a software package promotes scientific reproducibility, gives credit to its developers, and highlights the importance of software as a vital research product.

Version 2024.2.0 of PlasmaPy may be cited with the following reference:

PlasmaPy Community et al. (2024). PlasmaPy, version 2024.2.0, Zenodo, https://doi.org/10.5281/zenodo.10613904

This reference may be made, for example, by adding the following line to the methods or acknowledgments section of a paper.

This research made use of PlasmaPy version 2024.2.0, a community-developed open source Python package for plasma research and education (PlasmaPy Community et al. 2024).

We encourage authors to acknowledge the packages that PlasmaPy depends on, such as Astropy, NumPy, and SciPy.

Important

If you use a function that implements a technique originally from a research article referenced in its documentation, please also cite that research article.

Caution

A development version of PlasmaPy should not be cited or used in published work.

Tip

A paper written using AASTeX should use the \software{PlasmaPy} command to cite PlasmaPy. If the author instructions for a journal do not describe how to cite software, please follow the instructions on how to cite a dataset.

Note

All public releases of PlasmaPy are openly archived in the PlasmaPy Community on Zenodo.

Example highlights

Important

The Analysis and Diagnostic framework is in active development at the moment. For the foreseeable future, the API will be in continuous flux as functionality is added and modified. To follow the package development please visit our GitHub Project ( https://github.com/PlasmaPy/PlasmaPy/projects/19 ) and comment on any of the relevant issues and/or pull requests.

Analysis & Diagnostic Toolkits

Analyses and diagnostics go hand-in-hand, but have subtle differences. Thus, PlasmaPy gives each their own sub-packages, plasmapy.analysis and plasmapy.diagnostics respectively.

Think of the plasmapy.analysis as your toolbox. It has all the tools (functionality) you need to analyze your data. Functionality is built around numpy arrays and each function has a well-defined, focused task. For example, numpy.fft.fft() does one specific task: compute the one-dimensional discrete Fourier Transform. Similarly, plasmapy.analysis.swept_langmuir.find_floating_potential() only finds the floating potential for a given Langmuir trace. It does not have smoothing. It does not do any filtering. It does not do any signal conditioning. Its sole task is to find the floating potential of a single Langmuir trace.

Diagnostics have a much broader scope and leverage the tools defined in plasmapy.analysis to give a more integrated user experience when analyzing data. Diagnostics try to enhance the analysis workflow by focusing on some of following key areas…

  1. A more human-friendly way of managing data by building an interface around xarray arrays and datasets via custom diagnostic accessors.

    • xarray provides labeled multi-dimensional arrays and datasets.

    • Diagnostics self-manage the computed analysis data within a xarray dataset while maintaining the computed data’s relation to the original data.

  2. Quick viewing of analyzed data with default plotting routines.

  3. Fully defining the physical parameters of a diagnostic with purposely designed Probe classes that are integrated into the analysis workflow.

  4. Adding graphical user interfaces (GUIs) to the analysis workflow via notebook widgets.


Important

The Analysis and Diagnostic framework is in active development at the moment. For the foreseeable future, the API will be in continuous flux as functionality is added and modified. To follow the package development please visit our GitHub Project ( https://github.com/PlasmaPy/PlasmaPy/projects/19 ) and comment on any of the relevant issues and/or pull requests.

plasmapy.analysis

plasmapy.analysis.fit_functions

FitFunction classes designed to assist in curve fitting of swept Langmuir traces.

Classes

AbstractFitFunction([params, param_errors])

Abstract class for defining fit functions \(f(x)\) and the tools for fitting the function to a set of data.

Exponential([params, param_errors])

A sub-class of AbstractFitFunction to represent an exponential with an offset.

ExponentialPlusLinear([params, param_errors])

A sub-class of AbstractFitFunction to represent an exponential with an linear offset.

ExponentialPlusOffset([params, param_errors])

A sub-class of AbstractFitFunction to represent an exponential with a constant offset.

Linear([params, param_errors])

A sub-class of AbstractFitFunction to represent a linear function.

Inheritance diagram of plasmapy.analysis.fit_functions.AbstractFitFunction, plasmapy.analysis.fit_functions.Exponential, plasmapy.analysis.fit_functions.ExponentialPlusLinear, plasmapy.analysis.fit_functions.ExponentialPlusOffset, plasmapy.analysis.fit_functions.Linear
Example Notebook

Important

The Analysis and Diagnostic framework is in active development at the moment. For the foreseeable future, the API will be in continuous flux as functionality is added and modified. To follow the package development please visit our GitHub Project ( https://github.com/PlasmaPy/PlasmaPy/projects/19 ) and comment on any of the relevant issues and/or pull requests.

Swept Langmuir Analysis Module

Subpackage containing routines for analyzing swept Langmuir probe traces.

Example Notebooks
API
Sub-Packages & Modules

floating_potential

Functionality for determining the floating potential of a Langmuir sweep.

helpers

Helper functions for analyzing swept Langmuir traces.

ion_saturation_current

Functionality for determining the ion-saturation current of a Langmuir sweep.

Classes

ISatExtras(rsq, fitted_func, fitted_indices)

Create a tuple containing the extra parameters calculated by find_ion_saturation_current.

VFExtras(vf_err, rsq, fitted_func, islands, ...)

Create a tuple containing the extra parameters calculated by find_floating_potential.

Inheritance diagram of plasmapy.analysis.swept_langmuir.ion_saturation_current.ISatExtras, plasmapy.analysis.swept_langmuir.floating_potential.VFExtras
Functions

check_sweep(voltage, current[, strip_units])

Function for checking that the voltage and current arrays are properly formatted for analysis by plasmapy.analysis.swept_langmuir.

find_floating_potential(voltage, current[, ...])

Determine the floating potential (\(V_f\)) for a given current-voltage (IV) curve obtained from a swept Langmuir probe.

find_ion_saturation_current(voltage, current, *)

Determines the ion-saturation current (\(I_{sat}\)) for a given current-voltage (IV) curve obtained from a swept Langmuir probe.

Aliases

PlasmaPy provides aliases of the most common plasma functionality for user convenience. Aliases in PlasmaPy are denoted with a trailing underscore (e.g., alias_). For further details, please refer to the contributor guide’s section on aliases.

find_isat_(voltage, current, *[, fit_type, ...])

Alias to find_ion_saturation_current().

find_vf_(voltage, current[, threshold, ...])

Alias to find_floating_potential().

plasmapy.analysis.nullpoint

Functionality to find and analyze 3D magnetic null points.

Attention

This functionality is under development. Backward incompatible changes might occur in future releases.

Classes

NullPoint(null_loc, classification)

A class for defining a null point in 3D space.

Point(loc)

Abstract class for defining a point in 3D space.

Inheritance diagram of plasmapy.analysis.nullpoint.NullPoint, plasmapy.analysis.nullpoint.Point
Exceptions

NonZeroDivergence()

A class for handling the exception raised by passing in a magnetic field that violates the zero divergence constraint.

NullPointError

A class for handling the exceptions of the null point finder functionality.

Inheritance diagram of plasmapy.analysis.nullpoint.NonZeroDivergence, plasmapy.analysis.nullpoint.NullPointError
Warnings

MultipleNullPointWarning

A class for handling the warning raised by passing in a magnetic field grid that may contain multiple null points in close proximity due to low resolution.

NullPointWarning

A class for handling the warnings of the null point finder functionality.

Inheritance diagram of plasmapy.analysis.nullpoint.MultipleNullPointWarning, plasmapy.analysis.nullpoint.NullPointWarning
Functions

null_point_find([x_arr, y_arr, z_arr, ...])

Returns an array of NullPoint object, representing the null points of the given vector space.

trilinear_approx(vspace, cell)

Return a function whose input is a coordinate within a given grid cell and returns the trilinearly approximated vector value at that particular coordinate in that grid cell.

uniform_null_point_find(x_range, y_range, ...)

Return an array of NullPoint objects, representing the null points of the given vector space.

Notebooks
API

The analysis subpackage for PlasmaPy.

Sub-Packages & Modules

fit_functions

FitFunction classes designed to assist in curve fitting of swept Langmuir traces.

nullpoint

Functionality to find and analyze 3D magnetic null points.

swept_langmuir

Subpackage containing routines for analyzing swept Langmuir probe traces.

time_series

Subpackage containing routines for analyzing time series.

Important

The Analysis and Diagnostic framework is in active development at the moment. For the foreseeable future, the API will be in continuous flux as functionality is added and modified. To follow the package development please visit our GitHub Project ( https://github.com/PlasmaPy/PlasmaPy/projects/19 ) and comment on any of the relevant issues and/or pull requests.

plasmapy.diagnostics

Important

The Analysis and Diagnostic framework is in active development at the moment. For the foreseeable future, the API will be in continuous flux as functionality is added and modified. To follow the package development please visit our GitHub Project ( https://github.com/PlasmaPy/PlasmaPy/projects/19 ) and comment on any of the relevant issues and/or pull requests.

plasmapy.diagnostics.charged_particle_radiography
Charged Particle Detector Stacks

Objects representing stacks of film and/or filter layers for charged particle detectors.

Classes

Layer(thickness, energy_axis, stopping_power)

A layer in a detector film stack.

Stack(layers)

An ordered list of Layer objects.

Inheritance diagram of plasmapy.diagnostics.charged_particle_radiography.detector_stacks.Layer, plasmapy.diagnostics.charged_particle_radiography.detector_stacks.Stack
Synthetic Charged Particle Radiography

Routines for the analysis of proton radiographs. These routines can be broadly classified as either creating synthetic radiographs from prescribed fields or methods of ‘inverting’ experimentally created radiographs to reconstruct the original fields (under some set of assumptions).

Classes

Tracker(grids, source, detector[, ...])

Represents a charged particle radiography experiment with simulated or calculated E and B fields given at positions defined by a grid of spatial coordinates.

Inheritance diagram of plasmapy.diagnostics.charged_particle_radiography.synthetic_radiography.Tracker
Functions

synthetic_radiograph(obj[, size, bins, ...])

Calculate a "synthetic radiograph" (particle count histogram in the image plane).

API

The charged particle radiography subpackage contains functionality for analyzing charged particle radiographs and creating synthetic radiographs.

Sub-Packages & Modules

detector_stacks

Objects representing stacks of film and/or filter layers for charged particle detectors.

synthetic_radiography

Routines for the analysis of proton radiographs.

Important

The Analysis and Diagnostic framework is in active development at the moment. For the foreseeable future, the API will be in continuous flux as functionality is added and modified. To follow the package development please visit our GitHub Project ( https://github.com/PlasmaPy/PlasmaPy/projects/19 ) and comment on any of the relevant issues and/or pull requests.

Warning

This module will be deprecated in favor of plasmapy.analysis.swept_langmuir.

Langmuir analysis

Defines the Langmuir analysis module as part of the diagnostics package.

Classes

Characteristic(bias, current)

Class representing a single I-V probe characteristic for convenient experimental data access and computation.

Inheritance diagram of plasmapy.diagnostics.langmuir.Characteristic
Functions

extract_exponential_section(probe_characteristic)

Extract the section of exponential electron current growth from the probe characteristic.

extract_ion_section(probe_characteristic)

Extract the section dominated by ion collection from the probe characteristic.

extrapolate_electron_current(...[, ...])

Extrapolate the electron current from the Maxwellian electron temperature obtained in the exponential growth region.

extrapolate_ion_current_OML(...[, visualize])

Extrapolate the ion current from the ion density obtained with the OML method.

get_EEDF(probe_characteristic[, visualize])

Implement the Druyvesteyn method of obtaining the normalized Electron Energy Distribution Function (EEDF).

get_electron_density_LM(...)

Implement the Langmuir-Mottley (LM) method of obtaining the electron density.

get_electron_saturation_current(...)

Obtain an estimate of the electron saturation current corresponding to the obtained plasma potential.

get_electron_temperature(exponential_section)

Obtain the Maxwellian or bi-Maxwellian electron temperature using the exponential fit method.

get_floating_potential(probe_characteristic)

Implement the simplest but crudest method for obtaining an estimate of the floating potential from the probe characteristic.

get_ion_density_LM(ion_saturation_current, ...)

Implement the Langmuir-Mottley (LM) method of obtaining the ion density.

get_ion_density_OML(probe_characteristic, ...)

Implement the Orbital Motion Limit (OML) method of obtaining an estimate of the ion density.

get_ion_saturation_current(probe_characteristic)

Implement the simplest but crudest method for obtaining an estimate of the ion saturation current from the probe characteristic.

get_plasma_potential(probe_characteristic[, ...])

Implement the simplest but crudest method for obtaining an estimate of the plasma potential from the probe characteristic.

reduce_bimaxwellian_temperature(T_e, ...)

Reduce a bi-Maxwellian (dual) temperature to a single mean temperature for a given fraction.

swept_probe_analysis(probe_characteristic, ...)

Attempt to perform a basic swept probe analysis based on the provided characteristic and probe data.

Thomson scattering

Defines the Thomson scattering analysis module as part of plasmapy.diagnostics.

Functions

spectral_density(wavelengths, ...[, efract, ...])

Calculate the spectral density function for Thomson scattering of a probe laser beam by a multi-species Maxwellian plasma.

spectral_density_model(wavelengths, ...)

Returns a lmfit.model.Model function for Thomson spectral density function.

Lite-Functions

Lite-functions are optimized versions of existing plasmapy functions that are intended for applications where computational efficiency matters most. Lite-functions accept numbers and NumPy arrays that are implicitly assumed to be in SI units, and do not accept Quantity objects as inputs. For further details, please refer to the contributor guide’s section on lite-functions.

Caution

Lite-functions do not include the safeguards that are included in most plasmapy.formulary functions. When using lite-functions, it is vital to double-check your implementation!

spectral_density_lite(wavelengths, ...[, ...])

The lite-function version of spectral_density.

API

The diagnostics subpackage contains functionality for defining diagnostic parameters and processing data collected by diagnostics, synthetic or experimental.

Sub-Packages & Modules

charged_particle_radiography

The charged particle radiography subpackage contains functionality for analyzing charged particle radiographs and creating synthetic radiographs.

langmuir

Defines the Langmuir analysis module as part of the diagnostics package.

thomson

Defines the Thomson scattering analysis module as part of plasmapy.diagnostics.

Dispersion (plasmapy.dispersion)

The dispersion subpackage contains functionality associated with plasma dispersion relations, including numerical solvers and analytical solutions.

Attention

This functionality is under development. Backward incompatible changes might occur in future releases.

Sub-Packages & Modules

analytical

The analytical subpackage contains functionality associated with analytical dispersion solutions.

dispersion_functions

Module containing functionality focused on the plasma dispersion function \(Z(ζ)\).

numerical

The numerical subpackage contains functionality associated with numerical dispersion solvers.

Functions

plasma_dispersion_func(zeta)

Calculate the plasma dispersion function.

plasma_dispersion_func_deriv(zeta)

Calculate the derivative of the plasma dispersion function.

Examples

Formulary (plasmapy.formulary)

plasmapy.formulary provides theoretical formulas for calculation of physical quantities helpful for plasma physics.

Classical transport theory (plasmapy.formulary.braginskii)

Functions to calculate classical transport coefficients.

Attention

This functionality is under development. Backward incompatible changes might occur in future releases.

Introduction

Classical transport theory is derived by using kinetic theory to close the plasma two-fluid (electron and ion fluid) equations in the collisional limit. The first complete model in this form was done by Braginskii [1965].

As described in the next section, this module uses fitting functions from the literature [Braginskii, 1965, Epperlein and Haynes, 1986, Ji and Held, 2013, Spitzer, 1962, Spitzer and Härm, 1953] to calculate the transport coefficients, which are the resistivity, thermoelectric conductivity, thermal conductivity, and viscosity.

Keep in mind the following assumptions under which the transport equations are derived:

  1. The plasma is fully ionized, only consisting of ions and electrons. Neutral atoms are neglected.

  2. Turbulent transport does not dominate.

  3. The velocity distribution is close to Maxwellian. This implies:

    1. Collisional mean free path ≪ gradient scale length along field.

    2. Gyroradius ≪ gradient scale length perpendicular to field.

  4. The plasma is highly collisional: collisional frequency ≫ gyrofrequency.

When classical transport is not valid, e.g. due to the presence of strong gradients or turbulent transport, the transport is significantly increased by these other effects. Thus classical transport often serves as a lower bound on the losses / transport encountered in a plasma.

Transport Variables

For documentation on the individual transport variables, please take the following links to documentation of methods of ClassicalTransport.

Using the module

Given that many of the transport variables share a lot of the same computation and many are often needed to be calculated simultaneously, this module provides a ClassicalTransport class that can be initialized once with all of the variables necessary for calculation. It then provides all of the functionality as methods (please refer to its documentation).

If you only wish to calculate a single transport variable (or if just don’t like object-oriented interfaces), we have also provided wrapper functions in the main module namespace that use ClassicalTransport under the hood (see below, in the Functions section).

Warning

The API for this package is not yet stable.

Classical transport models

In this section, we present a broad overview of classical transport models implemented within this module.

Braginskii [Braginskii, 1965]

The original Braginskii treatment as presented in the highly cited review paper from 1965. Coefficients are found from expansion of the kinetic equation in Laguerre polynomials, truncated at the second term in their series expansion (\(k = 2\)). This theory allows for arbitrary Hall parameter and include results for Z = 1, 2, 3, 4, and infinity (the case of Lorentz gas completely stripped of electrons, and the stationary ion approximation).

Spitzer-Harm [Spitzer, 1962, Spitzer and Härm, 1953]

These coefficients were obtained from a numerical solution of the Fokker-Planck equation. They give one of the earliest and most accurate (in the Fokker-Planck sense) results for electron transport in simple plasma. They principally apply in the unmagnetized / parallel field case, although for resistivity Spitzer also calculated a famous result for a strong perpendicular magnetic field. Results are for Z = 1, 2, 4, 16, and infinity (Lorentz gas / stationary ion approximation).

Epperlein-Haines [Epperlein and Haynes, 1986]

Not yet implemented.

Ji-Held [Ji and Held, 2013]

This is a modern treatment of the classical transport problem that has been carried out with laudable care. It allows for arbitrary hall parameter and arbitrary \(Z\) for all coefficients. Similar to the Epperlein-Haines model, it corrects some known inaccuracies in the original Braginskii results, notably the asymptotic behavior of alpha-cross and beta_perp as Hall → +infinity. It also studies effects of electron collisions in the ion terms, which all other treatments have not. To neglect electron-electron collisions, leave \(μ = 0\). To consider them, specify mu and theta.

Classes

ClassicalTransport(T_e, n_e, T_i, n_i, ion, m_i)

Classical transport coefficients (e.g. Braginskii, 1965).

Inheritance diagram of plasmapy.formulary.braginskii.ClassicalTransport
Functions

electron_thermal_conductivity(T_e, n_e, T_i, ...)

Calculate the thermal conductivity for electrons.

electron_viscosity(T_e, n_e, T_i, n_i, ion)

Calculate the electron viscosity.

ion_thermal_conductivity(T_e, n_e, T_i, n_i, ion)

Calculate the thermal conductivity for ions.

ion_viscosity(T_e, n_e, T_i, n_i, ion[, ...])

Calculate the ion viscosity.

resistivity(T_e, n_e, T_i, n_i, ion[, m_i, ...])

Calculate the resistivity.

thermoelectric_conductivity(T_e, n_e, T_i, ...)

Calculate the thermoelectric conductivity.

Examples

plasmapy.formulary.braginskii

Collisions (plasmapy.formulary.collisions)

The collisions subpackage contains commonly used collisional formulae from plasma science.

Sub-Packages & Modules

coulomb

Functionality for calculating Coulomb parameters for different configurations.

dimensionless

Module of dimensionless parameters related to collisions.

frequencies

Frequency parameters related to collisions.

helio

The helio subpackage contains functionality for heliospheric plasma science, including the solar wind.

lengths

Module of length parameters related to collisions.

misc

Module of miscellaneous parameters related to collisions.

Classes

MaxwellianCollisionFrequencies(...)

Compute collision frequencies between two slowly flowing Maxwellian populations.

SingleParticleCollisionFrequencies(...)

Compute collision frequencies between test particles (labeled 'a') and field particles (labeled 'b').

Inheritance diagram of plasmapy.formulary.collisions.frequencies.MaxwellianCollisionFrequencies, plasmapy.formulary.collisions.frequencies.SingleParticleCollisionFrequencies
Functions

collision_frequency(T, n, species[, z_mean, ...])

Collision frequency of particles in a plasma.

Coulomb_cross_section(impact_param)

Cross-section for a large angle Coulomb collision.

Coulomb_logarithm(T, n_e, species[, z_mean, ...])

Compute the Coulomb logarithm.

coupling_parameter(T, n_e, species[, ...])

Ratio of the Coulomb energy to the kinetic (usually thermal) energy.

fundamental_electron_collision_freq(T_e, ...)

Average momentum relaxation rate for a slowly flowing Maxwellian distribution of electrons.

fundamental_ion_collision_freq(T_i, n_i, ion)

Average momentum relaxation rate for a slowly flowing Maxwellian distribution of ions.

impact_parameter(T, n_e, species[, z_mean, ...])

Impact parameters for classical and quantum Coulomb collision.

impact_parameter_perp(T, species[, V])

Distance of the closest approach for a 90° Coulomb collision.

Knudsen_number(characteristic_length, T, ...)

Knudsen number (dimensionless).

mean_free_path(T, n_e, species[, z_mean, V, ...])

Collisional mean free path (m).

mobility(T, n_e, species[, z_mean, V, method])

Return the electrical mobility.

Spitzer_resistivity(T, n, species[, z_mean, ...])

Spitzer resistivity of a plasma.

temp_ratio(*, r_0, r_n, n_1, n_2, v_1, T_1, T_2)

Calculate the thermalization ratio for a plasma in transit, taken from Maruca et al. [2013] and Johnson et al. [2023].

Examples

plasmapy.formulary.collisions

Density Plasma Parameters (plasmapy.formulary.densities)

Functions to calculate plasma density parameters.

Functions

critical_density(omega)

Calculate the plasma critical density for a radiation of a given frequency.

mass_density(density, particle[, z_ratio])

Calculate the mass density from a number density.

Aliases

PlasmaPy provides aliases of the most common plasma functionality for user convenience. Aliases in PlasmaPy are denoted with a trailing underscore (e.g., alias_). For further details, please refer to the contributor guide’s section on aliases.

rho_(density, particle[, z_ratio])

Alias to mass_density.

plasmapy.formulary.densities

Dielectric functions (plasmapy.formulary.dielectric)

Functions to calculate plasma dielectric parameters.

Classes

RotatingTensorElements(left, right, plasma)

Output type for cold_plasma_permittivity_LRP.

StixTensorElements(sum, difference, plasma)

Output type for cold_plasma_permittivity_SDP.

Inheritance diagram of plasmapy.formulary.dielectric.RotatingTensorElements, plasmapy.formulary.dielectric.StixTensorElements
Functions

cold_plasma_permittivity_LRP(B, species, n, ...)

Magnetized cold plasma dielectric permittivity tensor elements.

cold_plasma_permittivity_SDP(B, species, n, ...)

Magnetized cold plasma dielectric permittivity tensor elements.

permittivity_1D_Maxwellian(omega, kWave, T, ...)

Compute the classical dielectric permittivity for a 1D Maxwellian plasma.

Lite-Functions

Lite-functions are optimized versions of existing plasmapy functions that are intended for applications where computational efficiency matters most. Lite-functions accept numbers and NumPy arrays that are implicitly assumed to be in SI units, and do not accept Quantity objects as inputs. For further details, please refer to the contributor guide’s section on lite-functions.

Caution

Lite-functions do not include the safeguards that are included in most plasmapy.formulary functions. When using lite-functions, it is vital to double-check your implementation!

permittivity_1D_Maxwellian_lite(omega, ...)

The lite-function for permittivity_1D_Maxwellian.

Examples

plasmapy.formulary.dielectric

Dimensionless parameters (plasmapy.formulary.dimensionless)

Module of dimensionless plasma parameters.

These are especially important for determining what regime a plasma is in. (e.g., turbulent, quantum, collisional, etc.).

For example, plasmas at high (much larger than 1) Reynolds numbers are highly turbulent, while turbulence is negligible at low Reynolds numbers.

Functions

beta(T, n, B)

Compute the ratio of thermal pressure to magnetic pressure.

Debye_number(T_e, n_e)

Return the number of electrons within a sphere with a radius of the Debye length.

Hall_parameter(n, T, B, ion, particle[, ...])

Calculate the particle Hall parameter for a plasma.

Lundquist_number(L, B, density, sigma[, ...])

Compute the Lundquist number.

Mag_Reynolds(U, L, sigma)

Compute the magnetic Reynolds number.

quantum_theta(T, n_e)

Compare Fermi energy to thermal kinetic energy to check if quantum effects are important.

Reynolds_number(rho, U, L, mu)

Compute the Reynolds number.

Aliases

PlasmaPy provides aliases of the most common plasma functionality for user convenience. Aliases in PlasmaPy are denoted with a trailing underscore (e.g., alias_). For further details, please refer to the contributor guide’s section on aliases.

betaH_(n, T, B, ion, particle[, ...])

Alias to Hall_parameter.

nD_(T_e, n_e)

Alias to Debye_number.

Re_(rho, U, L, mu)

Alias to Reynolds_number.

Rm_(U, L, sigma)

Alias to Mag_Reynolds.

Examples

plasmapy.formulary.dimensionless

Distribution functions (plasmapy.formulary.distribution)

Common distribution functions for plasmas, such as the Maxwellian or Kappa distributions. Functionality is intended to include generation, fitting and calculation.

Functions

kappa_velocity_1D(v, T, kappa[, particle, ...])

Return the probability density at the velocity v in m/s to find a particle particle in a plasma of temperature T following the Kappa distribution function in 1D.

kappa_velocity_3D(vx, vy, vz, T, kappa[, ...])

Return the probability density function for finding a particle with velocity components v_x, v_y, and v_z in m/s in a suprathermal plasma of temperature T and parameter kappa which follows the 3D Kappa distribution function.

Maxwellian_1D(v, T[, particle, v_drift, ...])

Probability distribution function of velocity for a Maxwellian distribution in 1D.

Maxwellian_speed_1D(v, T[, particle, ...])

Probability distribution function of speed for a Maxwellian distribution in 1D.

Maxwellian_speed_2D(v, T[, particle, ...])

Probability distribution function of speed for a Maxwellian distribution in 2D.

Maxwellian_speed_3D(v, T[, particle, ...])

Probability distribution function of speed for a Maxwellian distribution in 3D.

Maxwellian_velocity_2D(vx, vy, T[, ...])

Probability distribution function of velocity for a Maxwellian distribution in 2D.

Maxwellian_velocity_3D(vx, vy, vz, T[, ...])

Probability distribution function of velocity for a Maxwellian distribution in 3D.

Examples

plasmapy.formulary.distribution

Particle drifts (plasmapy.formulary.drifts)

Functions for calculating particle drifts.

Functions

diamagnetic_drift(dp, B, n, q)

Calculate the diamagnetic fluid perpendicular drift.

ExB_drift(E, B)

Calculate the "electric cross magnetic" particle drift.

force_drift(F, B, q)

Calculate the general force drift for a particle in a magnetic field.

Aliases

PlasmaPy provides aliases of the most common plasma functionality for user convenience. Aliases in PlasmaPy are denoted with a trailing underscore (e.g., alias_). For further details, please refer to the contributor guide’s section on aliases.

vd_(dp, B, n, q)

Alias to diamagnetic_drift.

veb_(E, B)

Alias to ExB_drift.

vfd_(F, B, q)

Alias to force_drift.

Examples

plasmapy.formulary.drifts

Frequency Plasma Parameters (plasmapy.formulary.frequencies)

Functions to calculate fundamental plasma frequency parameters.

Functions

Buchsbaum_frequency(B, n1, n2, ion1, ion2[, ...])

Return the Buchsbaum frequency for a two-ion-species plasma.

gyrofrequency(B, particle[, signed, Z, ...])

Calculate the particle gyrofrequency in units of radians per second.

lower_hybrid_frequency(B, n_i, ion, *[, to_hz])

Return the lower hybrid frequency.

plasma_frequency(n, particle, *[, ...])

Calculate the particle plasma frequency.

upper_hybrid_frequency(B, n_e, *[, to_hz])

Return the upper hybrid frequency.

Aliases

PlasmaPy provides aliases of the most common plasma functionality for user convenience. Aliases in PlasmaPy are denoted with a trailing underscore (e.g., alias_). For further details, please refer to the contributor guide’s section on aliases.

oc_(B, particle[, signed, Z, mass_numb, to_hz])

Alias to gyrofrequency.

wc_(B, particle[, signed, Z, mass_numb, to_hz])

Alias to gyrofrequency.

wlh_(B, n_i, ion, *[, to_hz])

Alias to lower_hybrid_frequency.

wp_(n, particle, *[, mass_numb, Z, to_hz])

Alias to plasma_frequency.

wuh_(B, n_e, *[, to_hz])

Alias to upper_hybrid_frequency.

Lite-Functions

Lite-functions are optimized versions of existing plasmapy functions that are intended for applications where computational efficiency matters most. Lite-functions accept numbers and NumPy arrays that are implicitly assumed to be in SI units, and do not accept Quantity objects as inputs. For further details, please refer to the contributor guide’s section on lite-functions.

Caution

Lite-functions do not include the safeguards that are included in most plasmapy.formulary functions. When using lite-functions, it is vital to double-check your implementation!

plasma_frequency_lite(n, mass, Z[, to_hz])

The lite-function for plasma_frequency.

Examples

plasmapy.formulary.frequencies

Ionization related functionality (plasmapy.formulary.ionization)

Functions related to ionization states and the properties thereof.

Functions

ionization_balance(n, T_e)

Return the average ionization state of ions in a plasma assuming that the numbers of ions in each state are equal.

Saha(g_j, g_k, n_e, E_jk, T_e)

Return the ratio of populations of two ionization states.

Aliases

PlasmaPy provides aliases of the most common plasma functionality for user convenience. Aliases in PlasmaPy are denoted with a trailing underscore (e.g., alias_). For further details, please refer to the contributor guide’s section on aliases.

Z_bal_(n, T_e)

Alias for ionization_balance.

plasmapy.formulary.ionization

Length Plasma Parameters (plasmapy.formulary.lengths)

Functions to calculate fundamental plasma length parameters.

Functions

Debye_length(T_e, n_e)

Calculate the exponential scale length for charge screening in an electron plasma with stationary ions.

gyroradius(B, particle, *[, Vperp, T, ...])

Calculate the radius of circular motion for a charged particle in a uniform magnetic field (including relativistic effects by default).

inertial_length(n, particle, *[, mass_numb, Z])

Calculate a charged particle's inertial length.

Aliases

PlasmaPy provides aliases of the most common plasma functionality for user convenience. Aliases in PlasmaPy are denoted with a trailing underscore (e.g., alias_). For further details, please refer to the contributor guide’s section on aliases.

cwp_(n, particle, *[, mass_numb, Z])

Alias to inertial_length.

lambdaD_(T_e, n_e)

Alias to Debye_length.

rc_(B, particle, *[, Vperp, T, ...])

Alias to gyroradius.

rhoc_(B, particle, *[, Vperp, T, ...])

Alias to gyroradius.

Examples

plasmapy.formulary.lengths

Magnetostatics (plasmapy.formulary.magnetostatics)

Define MagneticStatics class to calculate common static magnetic fields as first raised in issue #100.

Classes

CircularWire(normal, center, radius, current)

Circular wire (coil) class.

FiniteStraightWire(p1, p2, current)

Finite length straight wire class.

GeneralWire(parametric_eq, t1, t2, current)

General wire class described by its parametric vector equation.

InfiniteStraightWire(direction, p0, current)

Infinite straight wire class.

MagneticDipole(moment, p0)

Simple magnetic dipole — two nearby opposite point charges.

MagnetoStatics()

Abstract class for magnetostatic fields.

Wire()

Abstract wire class for concrete wires to be inherited from.

Inheritance diagram of plasmapy.formulary.magnetostatics.CircularWire, plasmapy.formulary.magnetostatics.FiniteStraightWire, plasmapy.formulary.magnetostatics.GeneralWire, plasmapy.formulary.magnetostatics.InfiniteStraightWire, plasmapy.formulary.magnetostatics.MagneticDipole, plasmapy.formulary.magnetostatics.MagnetoStatics, plasmapy.formulary.magnetostatics.Wire
Examples

plasmapy.formulary.magnetostatics

Mathematics (plasmapy.formulary.mathematics)

Mathematical formulas relevant to plasma physics.

Functions

Fermi_integral(x, j)

Calculate the complete Fermi-Dirac integral.

rot_a_to_b(a, b)

Calculates the 3D rotation matrix that will rotate vector a to be aligned with vector b.

plasmapy.formulary.mathematics

Miscellaneous Plasma Parameters (plasmapy.formulary.misc)

Functions for miscellaneous plasma parameter calculations.

Functions

Bohm_diffusion(T_e, B)

Return the Bohm diffusion coefficient.

magnetic_energy_density(B)

Calculate the magnetic energy density.

magnetic_pressure(B)

Calculate the magnetic pressure.

thermal_pressure(T, n)

Return the thermal pressure for a Maxwellian distribution.

Aliases

PlasmaPy provides aliases of the most common plasma functionality for user convenience. Aliases in PlasmaPy are denoted with a trailing underscore (e.g., alias_). For further details, please refer to the contributor guide’s section on aliases.

DB_(T_e, B)

Alias to Bohm_diffusion.

pmag_(B)

Alias to magnetic_pressure.

pth_(T, n)

Alias to thermal_pressure.

ub_(B)

Alias to magnetic_energy_density.

plasmapy.formulary.misc

Quantum physics functions (plasmapy.formulary.quantum)

Functions for quantum parameters, including electron degenerate gases and warm dense matter.

Functions

chemical_potential(n_e, T)

Calculate the ideal chemical potential.

deBroglie_wavelength(V, particle)

Return the de Broglie wavelength.

Fermi_energy(n_e)

Calculate the kinetic energy in a degenerate electron gas.

quantum_theta(T, n_e)

Compare Fermi energy to thermal kinetic energy to check if quantum effects are important.

thermal_deBroglie_wavelength(T_e)

Calculate the thermal de Broglie wavelength for electrons.

Thomas_Fermi_length(n_e)

Calculate the exponential scale length for charge screening for cold and dense plasmas.

Wigner_Seitz_radius(n)

Calculate the Wigner-Seitz radius, which approximates the inter-particle spacing.

Aliases

PlasmaPy provides aliases of the most common plasma functionality for user convenience. Aliases in PlasmaPy are denoted with a trailing underscore (e.g., alias_). For further details, please refer to the contributor guide’s section on aliases.

Ef_(n_e)

Alias to Fermi_energy.

lambdaDB_(V, particle)

Alias to deBroglie_wavelength.

lambdaDB_th_(T_e)

Alias to thermal_deBroglie_wavelength.

plasmapy.formulary.quantum

Electromagnetic Radiation Functions (plasmapy.formulary.radiation)

Functions for calculating quantities associated with electromagnetic radiation.

Functions

thermal_bremsstrahlung(frequencies, n_e, T_e)

Calculate the bremsstrahlung emission spectrum for a Maxwellian plasma in the Rayleigh-Jeans limit \(ℏ ω ≪ k_B T_e\).

See Also

astropy.modeling.physical_models.BlackBody

Examples

plasmapy.formulary.radiation

Relativistic functions (plasmapy.formulary.relativity)

Functionality for calculating relativistic quantities.

Classes

RelativisticBody(particle, V, momentum, *, ...)

A physical body that is moving at a velocity relative to the speed of light.

Inheritance diagram of plasmapy.formulary.relativity.RelativisticBody
Functions

Lorentz_factor(V)

Return the Lorentz factor.

relativistic_energy(particle, V, *[, ...])

Calculate the sum of the mass energy and kinetic energy of a relativistic body.

plasmapy.formulary.relativity

Speed Plasma Parameters (plasmapy.formulary.speeds)

Functions to calculate fundamental plasma speed parameters.

Functions

Alfven_speed(B, density[, ion, mass_numb, Z])

Calculate the Alfvén speed.

ion_sound_speed(T_e, T_i, ion[, n_e, k, ...])

Return the ion sound speed for an electron-ion plasma.

kappa_thermal_speed(T, kappa, particle[, ...])

Return the most probable speed for a particle within a kappa distribution.

thermal_speed(T, particle[, method, mass, ndim])

Calculate the speed of thermal motion for particles with a Maxwellian distribution.

thermal_speed_coefficients(method, ndim)

Get the thermal speed coefficient corresponding to the desired thermal speed definition.

Aliases

PlasmaPy provides aliases of the most common plasma functionality for user convenience. Aliases in PlasmaPy are denoted with a trailing underscore (e.g., alias_). For further details, please refer to the contributor guide’s section on aliases.

cs_(T_e, T_i, ion[, n_e, k, gamma_e, gamma_i, Z])

Alias to ion_sound_speed.

va_(B, density[, ion, mass_numb, Z])

Alias to Alfven_speed.

vth_(T, particle[, method, mass, ndim])

Alias to thermal_speed().

vth_kappa_(T, kappa, particle[, method, ...])

Alias to kappa_thermal_speed.

Lite-Functions

Lite-functions are optimized versions of existing plasmapy functions that are intended for applications where computational efficiency matters most. Lite-functions accept numbers and NumPy arrays that are implicitly assumed to be in SI units, and do not accept Quantity objects as inputs. For further details, please refer to the contributor guide’s section on lite-functions.

Caution

Lite-functions do not include the safeguards that are included in most plasmapy.formulary functions. When using lite-functions, it is vital to double-check your implementation!

thermal_speed_lite(T, mass, coeff)

The lite-function for thermal_speed.

Examples

plasmapy.formulary.speeds

The subpackage makes heavy use of Quantity for handling conversions between different unit systems. This is especially important for electron-volts, commonly used in plasma physics to denote temperature, although it is technically a unit of energy.

Most functions expect Quantity objects as inputs, however some will use the validate_quantities decorator to automatically cast arguments to Quantity objects with the appropriate units. If that happens, you will be notified via a astropy.units.UnitsWarning.

Please note that well-maintained physical constant data with units and uncertainties can be found in astropy.constants.

Examples

For a general overview of how unit-based input works, take a look at the following examples:

API

The formulary subpackage contains commonly used formulae from plasma science.

Sub-Packages & Modules

braginskii

Functions to calculate classical transport coefficients.

collisions

The collisions subpackage contains commonly used collisional formulae from plasma science.

densities

Functions to calculate plasma density parameters.

dielectric

Functions to calculate plasma dielectric parameters.

dimensionless

Module of dimensionless plasma parameters.

distribution

Common distribution functions for plasmas, such as the Maxwellian or Kappa distributions.

drifts

Functions for calculating particle drifts.

frequencies

Functions to calculate fundamental plasma frequency parameters.

ionization

Functions related to ionization states and the properties thereof.

lengths

Functions to calculate fundamental plasma length parameters.

magnetostatics

Define MagneticStatics class to calculate common static magnetic fields as first raised in issue #100.

mathematics

Mathematical formulas relevant to plasma physics.

misc

Functions for miscellaneous plasma parameter calculations.

quantum

Functions for quantum parameters, including electron degenerate gases and warm dense matter.

radiation

Functions for calculating quantities associated with electromagnetic radiation.

relativity

Functionality for calculating relativistic quantities.

speeds

Functions to calculate fundamental plasma speed parameters.

Classes

CircularWire(normal, center, radius, current)

Circular wire (coil) class.

ClassicalTransport(T_e, n_e, T_i, n_i, ion, m_i)

Classical transport coefficients (e.g. Braginskii, 1965).

FiniteStraightWire(p1, p2, current)

Finite length straight wire class.

GeneralWire(parametric_eq, t1, t2, current)

General wire class described by its parametric vector equation.

InfiniteStraightWire(direction, p0, current)

Infinite straight wire class.

MagneticDipole(moment, p0)

Simple magnetic dipole — two nearby opposite point charges.

MagnetoStatics()

Abstract class for magnetostatic fields.

MaxwellianCollisionFrequencies(...)

Compute collision frequencies between two slowly flowing Maxwellian populations.

RelativisticBody(particle, V, momentum, *, ...)

A physical body that is moving at a velocity relative to the speed of light.

RotatingTensorElements(left, right, plasma)

Output type for cold_plasma_permittivity_LRP.

SingleParticleCollisionFrequencies(...)

Compute collision frequencies between test particles (labeled 'a') and field particles (labeled 'b').

StixTensorElements(sum, difference, plasma)

Output type for cold_plasma_permittivity_SDP.

Wire()

Abstract wire class for concrete wires to be inherited from.

Inheritance diagram of plasmapy.formulary.magnetostatics.CircularWire, plasmapy.formulary.braginskii.ClassicalTransport, plasmapy.formulary.magnetostatics.FiniteStraightWire, plasmapy.formulary.magnetostatics.GeneralWire, plasmapy.formulary.magnetostatics.InfiniteStraightWire, plasmapy.formulary.magnetostatics.MagneticDipole, plasmapy.formulary.magnetostatics.MagnetoStatics, plasmapy.formulary.collisions.frequencies.MaxwellianCollisionFrequencies, plasmapy.formulary.relativity.RelativisticBody, plasmapy.formulary.dielectric.RotatingTensorElements, plasmapy.formulary.collisions.frequencies.SingleParticleCollisionFrequencies, plasmapy.formulary.dielectric.StixTensorElements, plasmapy.formulary.magnetostatics.Wire
Functions

Alfven_speed(B, density[, ion, mass_numb, Z])

Calculate the Alfvén speed.

beta(T, n, B)

Compute the ratio of thermal pressure to magnetic pressure.

Bohm_diffusion(T_e, B)

Return the Bohm diffusion coefficient.

Buchsbaum_frequency(B, n1, n2, ion1, ion2[, ...])

Return the Buchsbaum frequency for a two-ion-species plasma.

chemical_potential(n_e, T)

Calculate the ideal chemical potential.

cold_plasma_permittivity_LRP(B, species, n, ...)

Magnetized cold plasma dielectric permittivity tensor elements.

cold_plasma_permittivity_SDP(B, species, n, ...)

Magnetized cold plasma dielectric permittivity tensor elements.

collision_frequency(T, n, species[, z_mean, ...])

Collision frequency of particles in a plasma.

Coulomb_cross_section(impact_param)

Cross-section for a large angle Coulomb collision.

Coulomb_logarithm(T, n_e, species[, z_mean, ...])

Compute the Coulomb logarithm.

coupling_parameter(T, n_e, species[, ...])

Ratio of the Coulomb energy to the kinetic (usually thermal) energy.

critical_density(omega)

Calculate the plasma critical density for a radiation of a given frequency.

deBroglie_wavelength(V, particle)

Return the de Broglie wavelength.

Debye_length(T_e, n_e)

Calculate the exponential scale length for charge screening in an electron plasma with stationary ions.

Debye_number(T_e, n_e)

Return the number of electrons within a sphere with a radius of the Debye length.

diamagnetic_drift(dp, B, n, q)

Calculate the diamagnetic fluid perpendicular drift.

electron_thermal_conductivity(T_e, n_e, T_i, ...)

Calculate the thermal conductivity for electrons.

electron_viscosity(T_e, n_e, T_i, n_i, ion)

Calculate the electron viscosity.

ExB_drift(E, B)

Calculate the "electric cross magnetic" particle drift.

Fermi_energy(n_e)

Calculate the kinetic energy in a degenerate electron gas.

Fermi_integral(x, j)

Calculate the complete Fermi-Dirac integral.

force_drift(F, B, q)

Calculate the general force drift for a particle in a magnetic field.

fundamental_electron_collision_freq(T_e, ...)

Average momentum relaxation rate for a slowly flowing Maxwellian distribution of electrons.

fundamental_ion_collision_freq(T_i, n_i, ion)

Average momentum relaxation rate for a slowly flowing Maxwellian distribution of ions.

gyrofrequency(B, particle[, signed, Z, ...])

Calculate the particle gyrofrequency in units of radians per second.

gyroradius(B, particle, *[, Vperp, T, ...])

Calculate the radius of circular motion for a charged particle in a uniform magnetic field (including relativistic effects by default).

Hall_parameter(n, T, B, ion, particle[, ...])

Calculate the particle Hall parameter for a plasma.

impact_parameter(T, n_e, species[, z_mean, ...])

Impact parameters for classical and quantum Coulomb collision.

impact_parameter_perp(T, species[, V])

Distance of the closest approach for a 90° Coulomb collision.

inertial_length(n, particle, *[, mass_numb, Z])

Calculate a charged particle's inertial length.

ion_sound_speed(T_e, T_i, ion[, n_e, k, ...])

Return the ion sound speed for an electron-ion plasma.

ion_thermal_conductivity(T_e, n_e, T_i, n_i, ion)

Calculate the thermal conductivity for ions.

ion_viscosity(T_e, n_e, T_i, n_i, ion[, ...])

Calculate the ion viscosity.

ionization_balance(n, T_e)

Return the average ionization state of ions in a plasma assuming that the numbers of ions in each state are equal.

kappa_thermal_speed(T, kappa, particle[, ...])

Return the most probable speed for a particle within a kappa distribution.

kappa_velocity_1D(v, T, kappa[, particle, ...])

Return the probability density at the velocity v in m/s to find a particle particle in a plasma of temperature T following the Kappa distribution function in 1D.

kappa_velocity_3D(vx, vy, vz, T, kappa[, ...])

Return the probability density function for finding a particle with velocity components v_x, v_y, and v_z in m/s in a suprathermal plasma of temperature T and parameter kappa which follows the 3D Kappa distribution function.

Knudsen_number(characteristic_length, T, ...)

Knudsen number (dimensionless).

Lorentz_factor(V)

Return the Lorentz factor.

lower_hybrid_frequency(B, n_i, ion, *[, to_hz])

Return the lower hybrid frequency.

Lundquist_number(L, B, density, sigma[, ...])

Compute the Lundquist number.

Mag_Reynolds(U, L, sigma)

Compute the magnetic Reynolds number.

magnetic_energy_density(B)

Calculate the magnetic energy density.

magnetic_pressure(B)

Calculate the magnetic pressure.

mass_density(density, particle[, z_ratio])

Calculate the mass density from a number density.

Maxwellian_1D(v, T[, particle, v_drift, ...])

Probability distribution function of velocity for a Maxwellian distribution in 1D.

Maxwellian_speed_1D(v, T[, particle, ...])

Probability distribution function of speed for a Maxwellian distribution in 1D.

Maxwellian_speed_2D(v, T[, particle, ...])

Probability distribution function of speed for a Maxwellian distribution in 2D.

Maxwellian_speed_3D(v, T[, particle, ...])

Probability distribution function of speed for a Maxwellian distribution in 3D.

Maxwellian_velocity_2D(vx, vy, T[, ...])

Probability distribution function of velocity for a Maxwellian distribution in 2D.

Maxwellian_velocity_3D(vx, vy, vz, T[, ...])

Probability distribution function of velocity for a Maxwellian distribution in 3D.

mean_free_path(T, n_e, species[, z_mean, V, ...])

Collisional mean free path (m).

mobility(T, n_e, species[, z_mean, V, method])

Return the electrical mobility.

permittivity_1D_Maxwellian(omega, kWave, T, ...)

Compute the classical dielectric permittivity for a 1D Maxwellian plasma.

plasma_frequency(n, particle, *[, ...])

Calculate the particle plasma frequency.

quantum_theta(T, n_e)

Compare Fermi energy to thermal kinetic energy to check if quantum effects are important.

relativistic_energy(particle, V, *[, ...])

Calculate the sum of the mass energy and kinetic energy of a relativistic body.

resistivity(T_e, n_e, T_i, n_i, ion[, m_i, ...])

Calculate the resistivity.

Reynolds_number(rho, U, L, mu)

Compute the Reynolds number.

rot_a_to_b(a, b)

Calculates the 3D rotation matrix that will rotate vector a to be aligned with vector b.

Saha(g_j, g_k, n_e, E_jk, T_e)

Return the ratio of populations of two ionization states.

Spitzer_resistivity(T, n, species[, z_mean, ...])

Spitzer resistivity of a plasma.

temp_ratio(*, r_0, r_n, n_1, n_2, v_1, T_1, T_2)

Calculate the thermalization ratio for a plasma in transit, taken from Maruca et al. [2013] and Johnson et al. [2023].

thermal_bremsstrahlung(frequencies, n_e, T_e)

Calculate the bremsstrahlung emission spectrum for a Maxwellian plasma in the Rayleigh-Jeans limit \(ℏ ω ≪ k_B T_e\).

thermal_deBroglie_wavelength(T_e)

Calculate the thermal de Broglie wavelength for electrons.

thermal_pressure(T, n)

Return the thermal pressure for a Maxwellian distribution.

thermal_speed(T, particle[, method, mass, ndim])

Calculate the speed of thermal motion for particles with a Maxwellian distribution.

thermal_speed_coefficients(method, ndim)

Get the thermal speed coefficient corresponding to the desired thermal speed definition.

thermoelectric_conductivity(T_e, n_e, T_i, ...)

Calculate the thermoelectric conductivity.

Thomas_Fermi_length(n_e)

Calculate the exponential scale length for charge screening for cold and dense plasmas.

upper_hybrid_frequency(B, n_e, *[, to_hz])

Return the upper hybrid frequency.

Wigner_Seitz_radius(n)

Calculate the Wigner-Seitz radius, which approximates the inter-particle spacing.

Aliases

PlasmaPy provides aliases of the most common plasma functionality for user convenience. Aliases in PlasmaPy are denoted with a trailing underscore (e.g., alias_). For further details, please refer to the contributor guide’s section on aliases.

betaH_(n, T, B, ion, particle[, ...])

Alias to Hall_parameter.

cs_(T_e, T_i, ion[, n_e, k, gamma_e, gamma_i, Z])

Alias to ion_sound_speed.

cwp_(n, particle, *[, mass_numb, Z])

Alias to inertial_length.

DB_(T_e, B)

Alias to Bohm_diffusion.

Ef_(n_e)

Alias to Fermi_energy.

lambdaD_(T_e, n_e)

Alias to Debye_length.

lambdaDB_(V, particle)

Alias to deBroglie_wavelength.

lambdaDB_th_(T_e)

Alias to thermal_deBroglie_wavelength.

nD_(T_e, n_e)

Alias to Debye_number.

oc_(B, particle[, signed, Z, mass_numb, to_hz])

Alias to gyrofrequency.

pmag_(B)

Alias to magnetic_pressure.

pth_(T, n)

Alias to thermal_pressure.

rc_(B, particle, *[, Vperp, T, ...])

Alias to gyroradius.

Re_(rho, U, L, mu)

Alias to Reynolds_number.

rho_(density, particle[, z_ratio])

Alias to mass_density.

rhoc_(B, particle, *[, Vperp, T, ...])

Alias to gyroradius.

Rm_(U, L, sigma)

Alias to Mag_Reynolds.

ub_(B)

Alias to magnetic_energy_density.

va_(B, density[, ion, mass_numb, Z])

Alias to Alfven_speed.

vd_(dp, B, n, q)

Alias to diamagnetic_drift.

veb_(E, B)

Alias to ExB_drift.

vfd_(F, B, q)

Alias to force_drift.

vth_(T, particle[, method, mass, ndim])

Alias to thermal_speed().

vth_kappa_(T, kappa, particle[, method, ...])

Alias to kappa_thermal_speed.

wc_(B, particle[, signed, Z, mass_numb, to_hz])

Alias to gyrofrequency.

wlh_(B, n_i, ion, *[, to_hz])

Alias to lower_hybrid_frequency.

wp_(n, particle, *[, mass_numb, Z, to_hz])

Alias to plasma_frequency.

wuh_(B, n_e, *[, to_hz])

Alias to upper_hybrid_frequency.

Z_bal_(n, T_e)

Alias for ionization_balance.

Lite-Functions

Lite-functions are optimized versions of existing plasmapy functions that are intended for applications where computational efficiency matters most. Lite-functions accept numbers and NumPy arrays that are implicitly assumed to be in SI units, and do not accept Quantity objects as inputs. For further details, please refer to the contributor guide’s section on lite-functions.

Caution

Lite-functions do not include the safeguards that are included in most plasmapy.formulary functions. When using lite-functions, it is vital to double-check your implementation!

permittivity_1D_Maxwellian_lite(omega, ...)

The lite-function for permittivity_1D_Maxwellian.

plasma_frequency_lite(n, mass, Z[, to_hz])

The lite-function for plasma_frequency.

thermal_speed_lite(T, mass, coeff)

The lite-function for thermal_speed.

Particles (plasmapy.particles)

Introduction

The particles subpackage provides access to information about atoms, ions, isotopes, and other particles.

Submodules

Particle objects

PlasmaPy contains several classes to represent particles, including Particle, CustomParticle, ParticleList, and DimensionlessParticle.

Particles

To create a Particle object, pass it a particle-like string that represents a particle.

>>> from plasmapy.particles import Particle
>>> electron = Particle('e-')

The Particle class accepts a variety of different str formats to represent particles. Atomic symbols are case-sensitive, but element names and many aliases are not.

>>> alpha = Particle('alpha')
>>> deuteron = Particle('D+')
>>> triton = Particle('tritium 1+')
>>> iron56 = Particle('Fe-56')
>>> helium = Particle('helium')
>>> muon = Particle('mu-')
>>> antimuon = Particle('antimuon')
>>> hydride = Particle('H-')

An int may be used as the first positional argument to Particle to represent an atomic number. For isotopes and ions, the mass number may be represented with the mass_numb keyword and the charge number may be represented with the Z keyword.

>>> proton = Particle(1, mass_numb=1, Z=1)

The most frequently used Particle objects may be imported directly from plasmapy.particles.

>>> from plasmapy.particles import proton, electron

The Particle objects that may be imported directly are: proton, electron, neutron, positron, deuteron, triton, and alpha.

Accessing particle properties

The properties of each particle may be accessed using the attributes of the corresponding Particle object.

>>> proton.atomic_number
1
>>> electron.charge_number
-1
>>> triton.mass_number
3

These properties are often returned as a Quantity in SI units.

>>> alpha.charge
<Quantity 3.20435324e-19 C>
>>> deuteron.mass
<Quantity 3.34358372e-27 kg>
>>> triton.half_life
<Quantity 3.888e+08 s>
>>> iron56.binding_energy.to('GeV')
<Quantity 0.49225958 GeV>

Strings representing particles may be accessed using the symbol, element, isotope, and ionic_symbol attributes.

>>> antimuon.symbol
'mu+'
>>> triton.element
'H'
>>> alpha.isotope
'He-4'
>>> deuteron.ionic_symbol
'D 1+'
Categories

The categories attribute returns a set with the classification categories corresponding to the particle.

>>> sorted(electron.categories)
['charged', 'electron', 'fermion', 'lepton', 'matter', 'stable']

Membership of a particle within a category may be checked using is_category.

>>> alpha.is_category('lepton')
False
>>> electron.is_category('fermion', 'lepton', 'charged')
True
>>> iron56.is_category(['element', 'isotope'])
True

The particle must be in all of the categories in the require keyword, at least one of the categories in the any_of keyword, and none of the categories in the exclude in order for it to return True.

>>> deuteron.is_category(require={'element', 'isotope', 'ion'})
True
>>> iron56.is_category(any_of=['charged', 'uncharged'])
False
>>> alpha.is_category(exclude='lepton')
True

Valid particle categories are listed in the docstring for is_category.

Conditionals and equality properties

Equality between particles may be tested either between two Particle objects, or between a Particle object and a str.

>>> Particle('H-1') == Particle('protium 1+')
False
>>> alpha == 'He-4 2+'
True

The is_electron and is_ion attributes provide a quick way to check whether or not a particle is an electron or ion, respectively.

>>> electron.is_electron
True
>>> hydride.is_electron
False
>>> deuteron.is_ion
True
.. _particle-class-antiparticles:
Returning antiparticles

The antiparticle of an elementary particle or antiparticle may be found by either using Python’s unary invert operator (~) or the antiparticle attribute of a Particle object.

>>> ~electron
Particle("e+")
>>> antimuon.antiparticle
Particle("mu-")
Custom particles

We can use CustomParticle to create particle objects with a mass, charge, and/or symbol that we provide. The mass and charge must be Quantity objects from astropy.units.

>>> import astropy.units as u
>>> from plasmapy.particles import CustomParticle
>>> cp = CustomParticle(mass = 9.3e-26 * u.kg, charge = 1.5e-18 * u.C, symbol = "Fe 9.5+")

CustomParticle has many of the same attributes and methods as Particle, and can often be used interchangeably.

>>> cp.charge
<Quantity 1.52e-18 C>
>>> cp.mass
<Quantity 9.3e-26 kg>
>>> cp.symbol
'Fe 9.5+'

If the charge and/or mass is not provided, the attribute will return nan in the appropriate units.

Molecules

We can use molecule to convert a chemical symbol into a CustomParticle object with the appropriate mass, charge, and symbol.

>>> from plasmapy.particles import molecule
>>> molecule("CO2 1+")  # carbon dioxide cation
CustomParticle(mass=7.30786637819994e-26 kg, charge=1.602176634e-19 C, symbol=CO2 1+)
Particle lists

ParticleList lets us work with multiple particles at once. A ParticleList can contain Particle and/or CustomParticle objects.

We can create a ParticleList by providing it with a particle-list-like object (i.e., a list containing particle-like objects). For example, we could provide ParticleList with a list of strings that represent individual particles.

>>> from plasmapy.particles import ParticleList
>>> helium_ions = ParticleList(["He-4 0+", "He-4 1+"])

ParticleList objects behave similarly to list objects, but convert its contents into the appropriate Particle or CustomParticle objects.

>>> helium_ions.append("alpha")
>>> print(helium_ions)
ParticleList(['He-4 0+', 'He-4 1+', 'He-4 2+'])
>>> helium_ions[1]
Particle("He-4 1+")

ParticleList shares many of the same attributes as Particle and CustomParticle. Attributes of Particle and CustomParticle that provide a scalar Quantity will provide a Quantity array from ParticleList.

>>> helium_ions.charge
<Quantity [0.00000000e+00, 1.60217663e-19, 3.20435327e-19] C>
>>> helium_ions.mass
<Quantity [6.64647907e-27, 6.64556813e-27, 6.64465719e-27] kg>

If we provide a Quantity with units of mass or charge, it will get converted into a CustomParticle.

>>> cp_list = ParticleList([1 * u.kg, 1 * u.C])
>>> cp_list[0]
CustomParticle(mass=1.0 kg, charge=nan C)
>>> cp_list.charge
<Quantity [nan,  1.] C>
>>> cp_list.mass
<Quantity [ 1., nan] kg>

We can create a CustomParticle with the mean mass and charge of the particles in a ParticleList with its average_particle method.

>>> helium_ions.average_particle()
CustomParticle(mass=6.645568133213004e-27 kg, charge=1.602176634e-19 C)

We can create a ParticleList by adding Particle, CustomParticle, and/or ParticleList objects together.

>>> helium_ions + cp + proton
ParticleList(['He-4 0+', 'He-4 1+', 'He-4 2+', 'Fe 9.5+', 'p+'])

The machinery contained with ParticleList lets us calculate plasma parameters from plasmapy.formulary for multiple particles at once.

>>> from plasmapy.formulary import gyroradius
>>> gyroradius(B = 5 * u.nT, particle=["e-", "p+"], Vperp = 100 * u.km/u.s)
<Quantity [1.13712608e+02, 2.08793710e+05] m>
Dimensionless particles

We can use DimensionlessParticle to represent particles that have been normalized (i.e., both the mass and charge are dimensionless).

>>> dp = DimensionlessParticle(mass=1, charge=-1)
>>> dp.charge
-1.0
>>> dp.mass
1.0

Because DimensionlessParticle objects do not directly represent physical particles without normalization information, they cannot be contained within a ParticleList or used in plasmapy.formulary.

Functions

In addition to the Particle class, the plasmapy.particles subpackage has a functional interface.

Symbols and names

Several functions in plasmapy.particles return string representations of particles, including atomic_symbol(), isotope_symbol(), ionic_symbol(), and element_name().

>>> from plasmapy.particles import *
>>> atomic_symbol('alpha')
'He'
>>> isotope_symbol('alpha')
'He-4'
>>> ionic_symbol('alpha')
'He-4 2+'
>>> particle_symbol('alpha')
'He-4 2+'
>>> element_name('alpha')
'helium'

The full symbol of the particle can be found using particle_symbol().

>>> particle_symbol('electron')
'e-'
Particle properties

The atomic_number() and mass_number() functions are analogous to the corresponding attributes in the Particle class.

>>> atomic_number('iron')
26
>>> mass_number('T+')
3

Charge information may be found using charge_number() and electric_charge().

>>> charge_number('H-')
-1
>>> electric_charge('muon antineutrino')
<Quantity 0. C>

These functions will raise a ChargeError for elements and isotopes that lack explicit charge information.

>>> electric_charge('H')
Traceback (most recent call last):
  ...
plasmapy.particles.exceptions.ChargeError: Charge information is required for electric_charge.

The standard atomic weight for the terrestrial environment may be accessed using standard_atomic_weight().

>>> standard_atomic_weight('Pb').to('u')
<Quantity 207.2 u>

The mass of a particle may be accessed through the particle_mass() function.

>>> particle_mass('deuteron')
<Quantity 3.34358372e-27 kg>
Isotopes

The relative isotopic abundance of each isotope in the terrestrial environment may be found using isotopic_abundance().

>>> isotopic_abundance('H-1')
0.999885
>>> isotopic_abundance('D')
0.000115

A list of all discovered isotopes in order of increasing mass number can be found with known_isotopes().

>>> known_isotopes('H')
['H-1', 'D', 'T', 'H-4', 'H-5', 'H-6', 'H-7']

The isotopes of an element with a non-zero isotopic abundance may be found with common_isotopes().

>>> common_isotopes('Fe')
['Fe-56', 'Fe-54', 'Fe-57', 'Fe-58']

All stable isotopes of an element may be found with stable_isotopes().

>>> stable_isotopes('Pb')
['Pb-204', 'Pb-206', 'Pb-207', 'Pb-208']
Stability

The is_stable() function returns True for stable particles and False for unstable particles.

>>> is_stable('e-')
True
>>> is_stable('T')
False

The half_life() function returns the particle’s half-life as a Quantity in units of seconds, if known.

>>> half_life('n')
<Quantity 881.5 s>

For stable particles (or particles that have not been discovered to be unstable), half_life() returns inf seconds.

>>> half_life('p+')
<Quantity inf s>

If the particle’s half-life is not known to sufficient precision, then half_life() returns a str with the estimated value while issuing a MissingParticleDataWarning.

Reduced mass

The reduced_mass() function is useful in cases of two-body collisions.

>>> reduced_mass('e-', 'p+')
<Quantity 9.10442514e-31 kg>
>>> reduced_mass('D+', 'T+')
<Quantity 2.00486597e-27 kg>
Nuclear Reactions
Binding energy

The binding energy of a nuclide may be accessed either as an attribute of a Particle object, or by using the nuclear_binding_energy() function.

>>> from plasmapy.particles import Particle, nuclear_binding_energy
>>> D = Particle('deuterium')
>>> D.binding_energy
<Quantity 3.56414847e-13 J>
>>> nuclear_binding_energy('D').to('GeV')
<Quantity 0.00222457 GeV>
Nuclear reaction energy

The energy released from a nuclear reaction may be found using the nuclear_reaction_energy() function. The input may be a str representing the reaction.

>>> from plasmapy.particles import nuclear_reaction_energy
>>> nuclear_reaction_energy('Be-8 + alpha --> carbon-12')
<Quantity 1.18025735e-12 J>

The reaction may also be inputted using the reactants and products keywords.

>>> nuclear_reaction_energy(reactants=['D', 'T'], products=['alpha', 'n'])
<Quantity 2.81812097e-12 J>
Ionization state data structures

The ionization state (or charge state) of a plasma refers to the fraction of an element that is at each ionization level. For example, the ionization state of a pure helium plasma could be 5% He0+, 94% He1+, and 1% He2+.

The ionization state of a single element

We may use the IonizationState class to represent the ionization state of a single element, such as for this example.

>>> from plasmapy.particles import IonizationState
>>> ionization_state = IonizationState("He", [0.05, 0.94, 0.01])

Ionization state information for helium may be accessed using the ionic_fractions attribute. These ionic fractions correspond to the charge_numbers attribute.

>>> ionization_state.ionic_fractions
array([0.05, 0.94, 0.01])
>>> ionization_state.charge_numbers
array([0, 1, 2])

The Z_mean attribute returns the mean charge number averaged over all particles in that element.

>>> ionization_state.Z_mean
0.96

The Z_rms attribute returns the root mean square charge number.

>>> ionization_state.Z_rms
0.9899...

The Z_most_abundant attribute returns a list of the most abundant ion(s). The list may contain more than one charge number in case of a tie.

>>> ionization_state.Z_most_abundant
[1]

The summarize method prints out the ionic fraction for the ions with an abundance of at least 1%.

>>> ionization_state.summarize()
IonizationState instance for He with Z_mean = 0.96
----------------------------------------------------------------
He  0+: 0.050
He  1+: 0.940
He  2+: 0.010
----------------------------------------------------------------

The number density of the element may be specified through the n_elem keyword argument.

>>> import astropy.units as u
>>> ionization_state = IonizationState(
...     "He", [0.05, 0.94, 0.01], n_elem = 1e19 * u.m ** -3,
... )

The n_e attribute provides the electron number density as a Quantity.

>>> ionization_state.n_e
<Quantity 9.6e+18 1 / m3>

The number_densities attribute provides the number density of each ion or neutral.

>>> ionization_state.number_densities
<Quantity [5.0e+17, 9.4e+18, 1.0e+17] 1 / m3>
Ionization states for multiple elements

The IonizationStateCollection class may be used to represent the ionization state for multiple elements. This can be used, for example, to describe the various impurities in a fusion plasma or the charge state distributions of different elements in the solar wind.

>>> from plasmapy.particles import IonizationStateCollection

The minimal input to IonizationStateCollection is a list of the elements or isotopes to represent. Integers in the list will be treated as atomic numbers.

>>> states = IonizationStateCollection(["H", 2])

To set the ionic fractions for hydrogen, we may do item assignment.

>>> states["H"] = [0.9, 0.1]

We may use indexing to retrieve an IonizationState instance for an element.

>>> states["H"]
<IonizationState instance for H>

The ionization states for all of the elements may be specified directly as arguments to the class.

>>> states = IonizationStateCollection(
...     {"H": [0.01, 0.99], "He": [0.04, 0.95, 0.01]},
...     abundances={"H": 1, "He": 0.08},
...     n0 = 5e19 * u.m ** -3,
... )

The ionic fractions will be stored as a dict.

>>> states.ionic_fractions
{'H': array([0.01, 0.99]), 'He': array([0.04, 0.95, 0.01])}

The number density for each element is the product of the number density scaling factor n0 with that element’s abundance. The number density for each ion is the product of n0, the corresponding element’s abundance, and the ionic fraction.

>>> states.n
<Quantity 5.e+19 1 / m3>
>>> states.abundances
{'H': 1.0, 'He': 0.08}
>>> states.number_densities["H"]
<Quantity [5.00e+17, 4.95e+19] 1 / m3>

The summarize method may also be used to summarize the ionization states.

>>> states.summarize()
----------------------------------------------------------------
H  1+: 0.990    n_i = 4.95e+19 m**-3
----------------------------------------------------------------
He  0+: 0.040    n_i = 1.60e+17 m**-3
He  1+: 0.950    n_i = 3.80e+18 m**-3
----------------------------------------------------------------
n_e = 5.34e+19 m**-3
T_e = 1.30e+04 K
----------------------------------------------------------------
Decorators
Passing Particle objects to functions and methods

When calculating plasma parameters, we frequently need to access the properties of the particles that make up that plasma. The particle_input() decorator allows functions and methods to easily access properties of different particles.

The particle_input() decorator takes valid representations of particles given in arguments to functions and passes through the corresponding Particle object. The arguments must be annotated with Particle so that the decorator knows to create the Particle object. The decorated function can then access particle properties by using Particle attributes. This decorator will raise an InvalidParticleError if the input does not correspond to a valid particle.

Here is an example of a decorated function.

from plasmapy.particles import Particle, particle_input


@particle_input
def particle_mass(particle: Particle):
    return particle.mass

This function can now accept either Particle objects or valid representations of particles.

>>> particle_mass('p+')  # string input
<Quantity 1.67262192e-27 kg>
>>> proton = Particle("proton")
>>> particle_mass(proton)  # Particle object input
<Quantity 1.67262192e-27 kg>

If only one positional or keyword argument is annotated with Particle, then the keywords mass_numb and Z may be used when the decorated function is called.

@particle_input
def charge_number(particle: Particle, Z: int = None, mass_numb: int = None) -> int:
    return particle.charge_number

The above example includes optional type hint annotations for Z and mass_numb and the returned value. The particle_input() decorator may be used in methods in classes as well:

class ExampleClass:
    @particle_input
    def particle_symbol(self, particle: Particle) -> str:
        return particle.symbol

On occasion it is necessary for a function to accept only certain categories of particles. The particle_input() decorator enables several ways to allow this.

If an annotated keyword is named element, isotope, or ion; then particle_input() will raise an InvalidElementError, InvalidIsotopeError, or InvalidIonError if the particle is not associated with an element, isotope, or ion; respectively.

@particle_input
def capitalized_element_name(element: Particle):
    return element.element_name


@particle_input
def number_of_neutrons(isotope: Particle):
    return isotope.mass_number - isotope.atomic_number


@particle_input
def number_of_bound_electrons(ion: Particle):
    return ion.atomic_number - ion.charge_number

The keywords require, any_of, and exclude to the decorator allow further customization of the particle categories allowed as inputs. These keywords are used as in is_category.

@particle_input(require="charged")
def sign_of_charge(charged_particle: Particle):
    """Require a charged particle."""
    return "+" if charged_particle.charge_number > 0 else "-"


@particle_input(any_of=["charged", "uncharged"])
def charge_number(particle: Particle) -> int:
    """Accept only particles with charge information."""
    return particle.charge_number


@particle_input(exclude={"antineutrino", "neutrino"})
def particle_mass(particle: Particle):
    """
    Exclude neutrinos/antineutrinos because these particles have
    weakly constrained masses.
    """
    return particle.mass

Examples

See Also

  • The mendeleev Python package provides access to properties of elements, isotopes, and ions in the periodic table of elements.

API

Sub-Packages & Modules

atomic

Functions that retrieve or are related to elemental or isotopic data.

data

Data used in constructing plasmapy.particles.

decorators

Decorators for plasmapy.particles.

exceptions

Collection of exceptions and warnings for plasmapy.particles.

ionization_state

Objects for storing ionization state data for a single element or for a single ionization level.

ionization_state_collection

A class for storing ionization state data for multiple elements or isotopes.

nuclear

Functions that are related to nuclear reactions.

particle_class

Classes to represent particles.

particle_collections

Collections of particle objects.

serialization

Functionality for JSON deserialization of particle objects.

symbols

Functions that deal with string representations of atomic symbols and numbers.

Classes

AbstractParticle()

An abstract base class that defines the interface for particles.

AbstractPhysicalParticle()

Base class for particles that are defined with physical units.

CustomParticle([mass, charge, symbol, Z])

A class to represent custom particles.

DimensionlessParticle(*[, mass, charge, symbol])

A class to represent dimensionless custom particles.

IonicLevel(ion[, ionic_fraction, ...])

Representation of the ionic fraction for a single ion.

IonizationState(particle[, ionic_fractions])

Representation of the ionization state distribution of a single element or isotope.

IonizationStateCollection(inputs, ...)

Describe the ionization state distributions of multiple elements or isotopes.

Particle(argument, *_[, mass_numb, Z])

A class for an individual particle or antiparticle.

ParticleJSONDecoder(*[, object_hook])

A custom JSONDecoder class to deserialize JSON objects into the appropriate particle objects.

ParticleList([particles])

A list like collection of Particle and CustomParticle objects.

Inheritance diagram of plasmapy.particles.particle_class.AbstractParticle, plasmapy.particles.particle_class.AbstractPhysicalParticle, plasmapy.particles.particle_class.CustomParticle, plasmapy.particles.particle_class.DimensionlessParticle, plasmapy.particles.ionization_state.IonicLevel, plasmapy.particles.ionization_state.IonizationState, plasmapy.particles.ionization_state_collection.IonizationStateCollection, plasmapy.particles.particle_class.Particle, plasmapy.particles.serialization.ParticleJSONDecoder, plasmapy.particles.particle_collections.ParticleList
Functions

atomic_number(element)

Return the number of protons in an atom, isotope, or ion.

atomic_symbol(element)

Return the atomic symbol.

charge_number(particle)

Return the charge number of a particle.

common_isotopes([argument, most_common_only])

Return a list of isotopes of an element with an isotopic abundances greater than zero, or if no input is provided, a list of all such isotopes for every element.

electric_charge(particle)

Return the electric charge (in coulombs) of a particle.

element_name(element)

Return the name of an element.

half_life(particle[, mass_numb])

Return the half-life in seconds for unstable isotopes and particles, and inf seconds for stable isotopes and particles.

ionic_levels(particle[, min_charge, max_charge])

Return a ParticleList that includes different ionic levels of a base atom.

ionic_symbol(particle, *[, mass_numb, Z])

Return the ionic symbol of an ion or neutral atom.

is_stable(particle[, mass_numb])

Return True for stable isotopes and particles and False for unstable isotopes.

isotope_symbol(isotope[, mass_numb])

Return the symbol representing an isotope.

isotopic_abundance(isotope[, mass_numb])

Return the isotopic abundances if known, and otherwise zero.

json_load_particle(fp, *[, cls])

Deserialize a JSON document into the appropriate particle object.

json_loads_particle(s, *[, cls])

Deserialize a JSON string into the appropriate particle object.

known_isotopes([argument])

Return a list of all known isotopes of an element, or a list of all known isotopes of every element if no input is provided.

mass_number(isotope)

Get the mass number (the number of protons and neutrons) of an isotope.

molecule(symbol[, Z])

Parse a molecule symbol into a CustomParticle or Particle.

nuclear_binding_energy(particle[, mass_numb])

Return the nuclear binding energy associated with an isotope.

nuclear_reaction_energy(*args, **kwargs)

Return the released energy from a nuclear reaction.

particle_input([callable_, require, any_of, ...])

Convert particle-like arguments into particle objects.

particle_mass(particle, *[, mass_numb, Z])

Return the mass of a particle.

particle_symbol(particle, *[, mass_numb, Z])

Return the symbol of a particle.

reduced_mass(test_particle, target_particle)

Find the reduced mass between two particles.

stable_isotopes([argument, unstable])

Return a list of all stable isotopes of an element, or if no input is provided, a list of all such isotopes for every element.

standard_atomic_weight(element)

Return the standard (conventional) atomic weight of an element based on the relative abundances of isotopes in terrestrial environments.

Variables & Attributes

alpha

A Particle instance representing an alpha particle (doubly charged helium-4).

deuteron

A Particle instance representing a positively charged deuterium ion.

electron

A Particle instance representing an electron.

neutron

A Particle instance representing a neutron.

ParticleLike

An object is particle-like if it can be identified as an instance of Particle or CustomParticle, or cast into one.

ParticleListLike

An object is particle-list-like if it can be identified as a ParticleList or cast into one.

positron

A Particle instance representing a positron.

proton

A Particle instance representing a proton.

triton

A Particle instance representing a positively charged tritium ion.

Simulation (plasmapy.simulation)

Attention

This functionality is under development. Backward incompatible changes might occur in future releases.

Introduction

The simulation subpackage provides basic, didactic reference implementations of popular methods of simulating plasmas, and interfaces to common simulation tools.

API

Sub-Packages & Modules

abstractions

Abstract classes for numerical simulations.

particle_integrators

Particle movement integrators, for particle simulations.

particle_tracker

Module containing the definition for the general particle tracker.

Classes

AbstractSimulation()

A prototype abstract interface for numerical simulations.

AbstractTimeDependentSimulation()

A prototype abstract interface for time-dependent numerical simulations.

ParticleTracker(grids[, ...])

A particle tracker for particles in electric and magnetic fields without inter-particle interactions.

Inheritance diagram of plasmapy.simulation.abstractions.AbstractSimulation, plasmapy.simulation.abstractions.AbstractTimeDependentSimulation, plasmapy.simulation.particle_tracker.ParticleTracker

Plasma Calculator

Attention

This functionality is under development. Backward incompatible changes might occur in future releases.

Overview

The Plasma Calculator is an interactive Jupyter notebook that is packaged with plasmapy, and allows users to input a set of plasma properties and immediately calculate multiple plasma parameters.

Note

This functionality is still under development and the API may change in future releases.

Using Plasma Calculator

To invoke the app use plasma-calculator in the command line. By default this opens the app in a browser, bootstrapping the notebook in a light theme and using port 8866 by default.

plasma-calculator takes optional arguments such as --dark, --port, and --no-browser. Pass flag -h or --help to get full list of supported arguments.

PlasmaPy Plasma

Attention

This functionality is under development. Backward incompatible changes might occur in future releases.

Overview

One of the core classes in PlasmaPy is Plasma. In order to make it easy to work with different plasma data in PlasmaPy, the Plasma object provides a number of methods for commonly existing plasmas in nature.

All Plasma objects are created using the Plasma factory Plasma.

A number of plasma data structures are supported by subclassing this base object. See Plasma Subclasses to see a list of all of them.

Creating Plasma Objects

Plasma objects are constructed using the special factory class Plasma:

>>> x = plasmapy.plasma.Plasma(T_e=T_e,
...                            n_e=n_e,
...                            Z=Z,
...                            particle=particle)  

The result of a call to Plasma will be either a GenericPlasma object, or a subclass of GenericPlasma which deals with a specific type of data, e.g. PlasmaBlob or Plasma3D (see Plasma Subclasses to see a list of all of them).

class plasmapy.plasma.plasma_factory.PlasmaFactory(default_widget_type=None, additional_validation_functions=None, registry=None)[source]

Plasma factory class. Used to create a variety of Plasma objects. Valid plasma structures are specified by registering them with the factory.

Attention

This functionality is under development. Backward incompatible changes might occur in future releases.

Using Plasma Objects

Once a Plasma object has been created using Plasma it will be a instance or a subclass of the GenericPlasma class. The documentation of GenericPlasma lists the attributes and methods that are available on all Plasma objects.

Plasma Classes

Defined in plasmapy.plasma.sources are a set of GenericPlasma subclasses which convert the keyword arguments data to the standard GenericPlasma interface. These subclasses also provide a method, which describes to the Plasma factory which the data match its plasma data structure.

Classes

BasePlasma()

Registration class for GenericPlasma and declares some abstract methods for data common in different kinds of plasmas.

ForceFreeFluxRope(B0, alpha)

Representation of the analytical Lundquist solution for force-free magnetic flux ropes [Lundquist, 1950].

GenericPlasma(**kwargs)

A Generic Plasma class.

HarrisSheet(B0, delta[, P0])

Define a Harris Sheet Equilibrium.

Inheritance diagram of plasmapy.plasma.plasma_base.BasePlasma, plasmapy.plasma.cylindrical_equilibria.ForceFreeFluxRope, plasmapy.plasma.plasma_base.GenericPlasma, plasmapy.plasma.equilibria1d.HarrisSheet

Plasma Subclasses

The Plasma3D class is a basic structure to contain spatial information about a plasma. To initialize a Plasma3D system, first create an instance of the Plasma3D class and then set the density, momentum, pressure and magnetic_field.

Note

This feature is currently under development.

The PlasmaBlob class is a basic structure to contain just plasma parameter information about a plasma with no associated spatial or temporal scales. To initialize a PlasmaBlob system, call it with arguments: electron temperature (T_e) and electron density (n_e). You may also optionally define the ionization (Z), and relevant plasma particle (particle).

Note

This feature is currently under development.

Classes

Plasma3D(domain_x, domain_y, domain_z, **kwargs)

Core class for describing and calculating plasma parameters with spatial dimensions.

PlasmaBlob(T_e, n_e[, Z, particle])

Class for describing and calculating plasma parameters without spatial/temporal description.

Inheritance diagram of plasmapy.plasma.sources.plasma3d.Plasma3D, plasmapy.plasma.sources.plasmablob.PlasmaBlob

Writing a new Plasma subclass

Any subclass of GenericPlasma which defines a method named is_datasource_for will automatically be registered with the Plasma factory. The is_datasource_for method describes the form of the data for which the GenericPlasma subclass is valid. For example, it might check the number and types of keyword arguments. This makes it straightforward to define your own GenericPlasma subclass for a new data structure or a custom data source like simulated data. These classes only have to be imported for this to work, as demonstrated by the following example.

import astropy.units as u
import plasmapy.plasma


class FuturePlasma(plasmapy.plasma.GenericPlasma):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)

    # Specify a classmethod that determines if the input data matches
    # this new subclass
    @classmethod
    def is_datasource_for(cls, **kwargs):
        """
        Determines if any of keyword arguments have a dimensionless value.
        """
        for _, value in kwargs.items():
            try:
                if value.unit == u.dimensionless_unscaled:
                    return True
            except AttributeError:
                pass

        return False

This class will now be available through the Plasma factory as long as this class has been defined, i.e. imported into the current session.

If you do not want to create a method named is_datasource_for you can manually register your class and matching method using the following method.

import plasmapy.plasma

plasmapy.plasma.Plasma.register(FuturePlasma, FuturePlasma.some_matching_method)

API

Sub-Packages & Modules

cylindrical_equilibria

Classes for representing cylindrical equilibria.

equilibria1d

Functionality for representing one-dimensional equilibria.

exceptions

Exceptions and warnings for functionality defined in plasmapy.plasma.

grids

Defines the AbstractGrid class and child classes.

plasma_base

Module for defining the base framework of the plasma classes.

plasma_factory

Module for defining the framework around the plasma factory.

sources

Classes

BasePlasma()

Registration class for GenericPlasma and declares some abstract methods for data common in different kinds of plasmas.

ForceFreeFluxRope(B0, alpha)

Representation of the analytical Lundquist solution for force-free magnetic flux ropes [Lundquist, 1950].

GenericPlasma(**kwargs)

A Generic Plasma class.

HarrisSheet(B0, delta[, P0])

Define a Harris Sheet Equilibrium.

Inheritance diagram of plasmapy.plasma.plasma_base.BasePlasma, plasmapy.plasma.cylindrical_equilibria.ForceFreeFluxRope, plasmapy.plasma.plasma_base.GenericPlasma, plasmapy.plasma.equilibria1d.HarrisSheet
Variables & Attributes

Plasma

Plasma factory class.

Core package utilities (plasmapy.utils)

Introduction

The utils subpackage contains functionality that is needed across multiple subpackages or does not fit nicely in any other subpackage. Functionality contained in utils includes:

API

plasmapy.utils.decorators Package

A module to contain various decorators used to build readable and useful code.

Sub-Packages & Modules

checks

Decorator for checking input/output arguments of functions.

converter

Decorators to convert units.

deprecation

Decorators to mark objects that are deprecated.

helpers

Miscellaneous decorators for various package uses.

lite_func

Module for defining functionality that marks and handle Lite-Function creation.

validators

Various decorators to validate input/output arguments to functions.

Classes

CheckBase([checks_on_return])

Base class for 'Check' decorator classes.

CheckUnits([checks_on_return])

A decorator class to 'check' — limit/control — the units of input and return arguments to a function or method.

CheckValues([checks_on_return])

A decorator class to 'check' — limit/control — the values of input and return arguments to a function or method.

ValidateQuantities([validations_on_return])

A decorator class to 'validate' -- control and convert -- the units and values of input and return arguments to a function or method.

Inheritance diagram of plasmapy.utils.decorators.checks.CheckBase, plasmapy.utils.decorators.checks.CheckUnits, plasmapy.utils.decorators.checks.CheckValues, plasmapy.utils.decorators.validators.ValidateQuantities
Functions

angular_freq_to_hz(fn)

A decorator that enables a function to convert its return value from angular frequency (rad/s) to frequency (Hz).

bind_lite_func(lite_func[, attrs])

Decorator to bind a lightweight "lite" version of a formulary function to the full formulary function, as well as any supporting attributes.

check_relativistic([func, betafrac])

Warns or raises an exception when the output of the decorated function is greater than betafrac times the speed of light.

check_units([func, checks_on_return])

A decorator to 'check' — limit/control — the units of input and return arguments to a function or method.

check_values([func, checks_on_return])

A decorator to 'check' — limit/control — the values of input and return arguments to a function or method.

deprecated(since[, message, name, ...])

A wrapper of astropy.utils.decorators.deprecated that by default assumes a warning type of PlasmaPyDeprecationWarning.

modify_docstring([func, prepend, append])

A decorator which programmatically prepends and/or appends the docstring of the decorated method/function.

preserve_signature(f)

A decorator for decorators, which preserves the signature of the function being wrapped.

validate_class_attributes([...])

A decorator responsible for raising errors if the expected arguments weren't provided during class instantiation.

validate_quantities([func, ...])

A decorator to 'validate' — control and convert — the units and values of input and return arguments to a function or method.

plasmapy.utils.exceptions Module

Exceptions and warnings specific to PlasmaPy.

Exceptions

InvalidRomanNumeralError

An exception to be raised when the input is not a valid Roman numeral.

OutOfRangeError

An exception to be raised for integers that outside of the range that can be converted to Roman numerals.

PhysicsError

The base exception for physics-related errors.

PlasmaPyError

Base class of PlasmaPy custom errors.

RelativityError

An exception for speeds greater than the speed of light.

RomanError

A base exception for errors from plasmapy.utils.roman.

Inheritance diagram of plasmapy.utils.exceptions.InvalidRomanNumeralError, plasmapy.utils.exceptions.OutOfRangeError, plasmapy.utils.exceptions.PhysicsError, plasmapy.utils.exceptions.PlasmaPyError, plasmapy.utils.exceptions.RelativityError, plasmapy.utils.exceptions.RomanError
Warnings

CouplingWarning

A warning for functions that rely on a particular coupling regime to be valid.

PhysicsWarning

The base warning for warnings related to non-physical situations.

PlasmaPyDeprecationWarning

A warning for deprecated features when the warning is intended for other Python developers.

PlasmaPyFutureWarning

A warning for deprecated features when the warning is intended for end users of PlasmaPy.

PlasmaPyWarning

Base class of PlasmaPy custom warnings.

RelativityWarning

A warning for when relativistic velocities are being used in or are returned by non-relativistic functionality.

Inheritance diagram of plasmapy.utils.exceptions.CouplingWarning, plasmapy.utils.exceptions.PhysicsWarning, plasmapy.utils.exceptions.PlasmaPyDeprecationWarning, plasmapy.utils.exceptions.PlasmaPyFutureWarning, plasmapy.utils.exceptions.PlasmaPyWarning, plasmapy.utils.exceptions.RelativityWarning
plasmapy.utils.code_repr Module

Tools for formatting strings, including for error messages.

Functions

attribute_call_string(cls, attr[, ...])

Approximate the command to instantiate a class, and access an attribute of the resulting class instance.

call_string(f[, args, kwargs, max_items])

Approximate a call of a function or class with positional and keyword arguments.

method_call_string(cls, method, *[, ...])

Approximate the command to instantiate a class, and then call a method in the resulting class instance.

plasmapy.utils.calculator Package

Script and utilities to launch the plasma calculator.

Functions

main()

Stub function for command line tool that launches the plasma calculator notebook.

plasmapy.utils.data Package

The plasmapy.utils.data subpackage contains functionality for downloading and retrieving data files.

Sub-Packages & Modules

downloader

Contains functionality for downloading files from a URL.

Classes

Downloader([directory, validate, api_token])

Accesses the PlasmaPy resource files.

Inheritance diagram of plasmapy.utils.data.downloader.Downloader

Changelog

This document lists the changes made during each release of PlasmaPy, including bug fixes and changes to the application programming interface (API).

Unreleased changes

PlasmaPy v0.1.dev50+ga4ea72d (2024-04-17)
New Features
Documentation Improvements
  • Added the internal category for changelog entries, which will be used to denote refactorings with minimal impact on the API, and updated the Changelog Guide to reflect these changes. (#2441)

  • Updated the docstring of particle_input() to indicate that annotations for optional parameters should now be ParticleLike | None or ParticleListLike | None. (#2505)

  • Added known limitations of particle_input() to its docstring. (#2516)

  • Removed references to the Twitter account, which is no longer used. (#2522)

  • Updated the docstring for gyroradius to finish an unfinished sentence. (#2560)

  • Updated the instructions in the Documentation Guide on how to build PlasmaPy’s documentation locally. (#2565)

  • Fix typo in description of mass_density. (#2588)

  • Add examples to docstring for thermal_bremsstrahlung. (#2618)

Backwards Incompatible Changes
Bug Fixes
Internal Changes and Refactorings
  • Changed type hint annotations to be consistent with PEP 604. Type unions are now made using the | operator rather than with typing.Union. (#2504)

  • Refactored, parametrized, and expanded the tests for Debye_length. (#2509)

  • Changed type hint annotations that used numbers.Integral, numbers.Real, or numbers.Complex to instead use int, float, or complex, respectively. (#2520)

  • Created a tox environment for regenerating requirements files used in continuous integration (CI) and by integrated development environments (IDEs). This environment is now what is being used in the automated pull requests to regenerate requirements files. Switching from pip-compile to uv pip compile now allows requirements files to be created for multiple versions of Python, as well as for minimal versions of dependencies. (#2523)

  • Refactored gyroradius to reduce the cognitive complexity of the function. (#2542)

  • Added and updated type hint annotations across plasmapy.formulary. (#2543)

  • Applied caching through GitHub Actions to speed up continuous integration tests and documentation builds. Because the Python environments used by tox to run tests no longer need to be recreated every time tests are run, caching speeds up several continuous integration tests by ∼2–3 minutes. (#2552)

  • Removed setup.py. (#2558)

  • Added sphinx-lint as a pre-commit hook to find reStructuredText errors. (#2561)

  • Enabled the tox-uv plugin to tox, so that package installation and the creation of virtual environments will be handled by uv instead of pip. This change makes it faster to run tests both locally and via GitHub Actions. (#2584)

  • Changed the project structure to an src layout to follow the updated recommendation from the Python Packaging Authority’s packaging guide. The motivation for this change is described in #2581. Source code previously in plasmapy/ is now located in src/plasmapy/ and tests are now in a separate tests/ directory. Tests previously in plasmapy/**/tests/ are now in tests/**/, where ** refers to an arbitrary number of subdirectories. For example, the source code of plasmapy.formulary is now located in src/plasmapy/formulary/ and the tests for plasmapy.formulary are now in tests/formulary/. (#2598)

Additional Changes

PlasmaPy v2024.2.0 (2024-02-06)

Backwards Incompatible Changes
Features
  • Added a notch argument to spectral_density, which allows users to output spectrum over one or multiple wavelength ranges to correspond to a notch filter commonly applied to experimentally measured Thomson scattering spectra. Changed the spectral_density_model function to allow notch to be applied during fitting. (#2058)

  • Changed unit annotations to Quantity type hint annotations. (#2421)

  • Enabled particle_input() to decorate functions which have variadic positional arguments followed by keyword arguments. See #2150. (#2428)

  • Added a random_seed keyword in the create_particles method of the Tracker class to make the function deterministic to resolve intermittent test failures. (#2487)

Bug Fixes
Improved Documentation
Trivial/Internal Changes
  • Added Python 3.12 to the suite of continuous integration tests. (#2368)

  • Added a GitHub Action that will create an issue containing a release checklist, which will largely supersede the release process outlined in the Contributor Guide. (#2376)

  • Separated the GitHub Action for checking hyperlinks in the documentation into its own GitHub Action. (#2392)

  • Replaced black with ruff in the pre-commit configuration. (#2394)

  • Modified the __exit__ method of HDF5Reader for context management. (#2402)

  • Added an initial configuration for mypy that temporarily ignores existing errors. (#2424)

  • Added a tox environment for running mypy. (#2431)

  • Added mypy to the suite of continuous integration checks. (#2432)

  • Used autotyping to implement type hint annotations for special methods like __init__ and __str__, and changed -> typing.NoReturn annotations to -> None. (#2437)

  • Used autotyping to add -> None return annotations to functions and methods with no return statement. (#2439)

  • Added a stub file containing type hint annotations for @wrapt.decorator. (#2442)

  • Improved type hint annotations for plasmapy.particles.decorators, which includes particle_input(), and the corresponding tests. (#2443)

  • Dropped the pre-commit hook for isort and enabled all isort rules in ruff. (#2453)

  • Added a py.typed marker to indicate that PlasmaPy contains type hint annotations as per PEP 561. (#2473)

  • Changed _nearest_neighbor_interpolator method in grids to interpolate quantities array instead of producing an intermediate index. (#2475)

  • Enabled the sphinx linkchecker in quiet mode to make it easier to find problem links from the console output. (#2476)

  • Bumped the minimum versions of dependencies to drop support for minor releases older than two years old. In particular, the minimum version of NumPy was bumped to 1.23.0. (#2488)

PlasmaPy v2023.10.0 (2023-10-20)

Backwards Incompatible Changes
Features
Bug Fixes
Improved Documentation
  • Updated the code contribution workflow in the Contributor Guide to describe how to use git pull. (#2193)

  • Expanded the troubleshooting section of the Documentation Guide to describe how to resolve warnings related to documents not being included in any toctrees. (#2257)

  • Added a step to the code contribution workflow about using git status to verify that there have been no changes to tracked files before creating and switching to a new branch. (#2263)

  • Added a page to the Contributor Guide about pre-commit, including how to troubleshoot test failures. (#2265)

  • Added CONTRIBUTING.md to PlasmaPy’s GitHub repository. This page refers contributors to PlasmaPy’s Contributor Guide. (#2266)

  • Enabled the sphinx.ext.duration extension to show the times required to process different pages during documentation builds. (#2268)

  • Enabled the sphinx.ext.viewcode extension for adding links in the documentation to pages containing the source code. (#2269)

  • Moved definitions of certain reStructuredText substitutions from docs/common_links.rst to the file docs/contributing/doc_guide.rst in order to speed up the documentation build (see #2277). (#2272)

  • Implemented sphinxcontrib-globalsubs to enable global reStructuredText substitutions to be used throughout the documentation, and moved the definition of substitutions from docs/common_links.rst to the global_substitutions dict in docs/_global_substitutions.py. (#2281)

  • Changed from astropy import units as u to import astropy.units as u and from astropy import constants as const to import astropy.constants as const throughout the code in order to increase consistency of import statements. (#2282)

  • Added and applied nbqa-ruff to our suite of pre-commit hooks so that ruff can perform code quality checks on our example notebooks. (#2302)

  • Renamed docs/cff_to_rst.py to docs/_cff_to_rst.py, and updated the functionality contained within that file for converting author information in CITATION.cff into a reStructuredText author list to be included in the documentation. (#2307)

  • Fixed broken hyperlinks and reStructuredText references. (#2308)

  • Replaced from plasmapy.particles import * in docs/notebooks/getting_started/particles.ipynb with imports of the actual functions and classes that were used. (#2311)

  • Applied minor refactorings and formatting improvements to docs/notebooks/dispersion/stix_dispersion.ipynb. (#2312)

  • Updated the Coding Guide by discussing when to use aliases and applied the :py: role so that in-line code gets formatted the same as Python code blocks. (#2324)

  • Updated the docstrings and type hint annotations in plasmapy.formulary.lengths. (#2356)

  • Refactored docs/conf.py to improve organization. (#2363)

  • Updated the narrative documentation on particle objects to include CustomParticle, DimensionlessParticle, and ParticleList objects. (#2377)

Trivial/Internal Changes

PlasmaPy v2023.5.1 (2023-06-07)

Trivial/Internal Changes
  • Loosened the requirement on ipykernel for compatibility with Google Colab. (#2202)

PlasmaPy v2023.5.0 (2023-05-31)

Backwards Incompatible Changes
  • The signature of relativistic_energy has changed. The parameter m has been replaced with particle, which now accepts a broader variety of particle-like arguments, including but not limited to a Quantity representing mass. The parameter v has been replaced with V for consistency with other functionality. (#1871)

  • Changed the minimum required version of Python from 3.8 to 3.9. Accordingly, increased the minimum versions of numpy to 1.21.0, pandas to 1.2.0, h5py to 3.1.0, scipy to 1.6.0, voila to 0.3.0, and xarray to 0.17.0. (#1885)

  • Made ParticleList raise a TypeError when provided with a string. This change was made to avoid potentially ambiguous situations like ParticleList("He") which was previously equivalent to ParticleList(["H", "e"]) instead of the possibly expected value of ParticleList(["He"]). (#1892)

  • In two_fluid, hollweg, and kinetic_alfven in plasmapy.dispersion, providing the charge number as a keyword argument (now Z, formerly z_mean) will no longer override the charge number provided in ion. (#2022, #2181, #2182)

  • particle_input() no longer enforces that parameters named ionic_level are ions or neutral atoms. For equivalent behavior, name the parameter ion instead. (#2034)

  • Removed plasmapy.utils.pytest_helpers from PlasmaPy’s public API. It is still available as plasmapy.utils._pytest_helpers, but might be removed in the future. (#2114)

  • Removed plasmapy.tests.helpers from PlasmaPy’s public API. It is still available as plasmapy.tests._helpers, but might be removed in the future. (#2114)

  • The ion_species parameter to thermal_bremsstrahlung has been renamed to ion in order to provide a more consistent API to functions that accept ions as arguments. (#2135)

Deprecations and Removals
Features
Bug Fixes
  • When attempting to create a Particle object representing a proton, calls like Particle("H", Z=1, mass_numb=1) no longer incorrectly issue a ParticleWarning for redundant particle information. (#1992)

  • Updated the docstring of kinetic_alfven. (#2016)

  • Fixed a slight error in plasma_frequency and Alfven_speed when the charge number was provided via z_mean (or now Z) and inconsistent with the charge number provided to particle (or zero, if particle represented an element or isotope with no charge information. Previously, if we represented a proton with particle="H-1" and z_mean=1, then the mass used to calculate the plasma frequency would have been the mass of a neutral hydrogen atom rather than the mass of a proton. However, using particle="p+" would have produced the correct mass. This behavior has been corrected by decorating this function with particle_input(). See also #2178 and #2179. (#2026)

  • The plasmapy.analysis.nullpoint._vector_space function now returns a list for its delta values instead of an array. (#2133)

Improved Documentation
  • Enabled sphinx-codeautolink to make code examples clickable and give quick access to API documentation. (#1410)

  • Added an example notebook on ionization states in the solar wind. (#1513)

  • Moved the location of the changelog pages for past releases from docs/whatsnew/ to docs/changelog/, and set up appropriate redirects. (#1639)

  • Removed outdated instructions on installing the development version of PlasmaPy contained in docs/contributing/install_dev.rst. (#1656)

  • Converted docs/CONTRIBUTING.rst to .github/contributing.md. (#1656)

  • Added a new page to the Contributor Guide on the code contribution workflow, replacing content previously contained in the Coding Guide. (#1656)

  • Added a page to the Contributor Guide on Getting Ready to Contribute. (#1656)

  • Updated docstrings in plasmapy.formulary.collisions.frequencies. (#1793)

  • Updated the docstring for particle_input(). (#1883)

  • Updated the introductory paragraphs to the Contributor Guide. (#2014)

  • Moved PlasmaPy’s vision statement from the online documentation to a Zenodo record. (#2017)

  • Restructured the Documentation Guide by putting information on writing documentation prior to instructions for building documentation. (#2038)

  • Restructured the Testing Guide by putting information on writing tests prior to instructions for running tests. (#2041)

  • Updated the introduction on the documentation landing page and the citation instructions. (#2055)

  • Updated the Changelog Guide. (#2059)

  • Added admonitions for functionality that is under development and for which backwards incompatible changes might occur in the future. (#2112)

  • Updated the code contribution workflow instructions in the Contributor Guide to reflect that first-time contributors should add themselves to the author list in CITATION.cff instead of in docs/about/credits.rst. (#2155)

  • Added functionality to automatically generate the author list included in docs/about/credits.rst directly from CITATION.cff. The script is located at docs/cff_to_rst.py. (#2156)

Trivial/Internal Changes
  • Included Python 3.11 in continuous integration tests. (#1775)

  • Turned the root-level requirements.txt into a lockfile for continuous integration purposes. (#1864)

  • Enabled the particle creation factory in plasmapy.particles._factory used by particle_input() to create CustomParticle instances of an element or isotope with a charge number that is a real number but not an integer. (#1884)

  • Implemented the new private CustomParticle constructor from #1881 into the private particle creation factory used by particle_input(). (#1884)

  • Dropped dlint from the tests requirements, as it is no longer being maintained. (#1906)

  • Modified particle_input() to allow CustomParticle-like objects with a defined charge to be passed through to decorated functions when a parameter to that function annotated with ParticleLike is named ion. Previously, only Particle objects representing ions or neutral atoms were allowed to pass through when the parameter was named ion. (#2034)

  • Updated package metadata in pyproject.toml. (#2075)

  • Set minimum versions for all explicitly listed dependencies. (#2075)

  • Enabled and applied changes for additional rule sets for ruff, and removed corresponding flake8 extensions. (#2080)

  • Changed from indexserver to PIP_INDEX_URL to index nightly numpy builds (#2138)

  • Updated the function and docstring of collisional_analysis. (#2151)

  • Dropped flake8 and its extensions as linters. Instead, ruff is now used as the primary linter. (#2170)

  • Expanded the variety of arguments that could be provided to a function decorated by angular_freq_to_hz, and refactored this decorator to use wrapt. (#2175)

PlasmaPy v2023.1.0 (2023-01-13)

Backwards Incompatible Changes
Deprecations and Removals
Features
Improved Documentation
Trivial/Internal Changes

PlasmaPy v0.9.1 (2022-11-15)

Trivial/Internal Changes
  • Removed a test of requirement file consistency to allow tests to pass on conda-forge.

PlasmaPy v0.9.0 (2022-11-11)

Backwards Incompatible Changes
Deprecations and Removals
Features
Bug Fixes
  • Modified tests in the class TestSyntheticRadiograph to try to fix an intermittent failure of test_optical_density_histogram. (#1685)

Improved Documentation
Trivial/Internal Changes

PlasmaPy v0.8.1 (2022-07-05)

This release of PlasmaPy includes 158 pull requests closing 60 issues by 37 people, of which 31 are new contributors.

The people who have contributed to the code for this release are:

  • Afzal Rao*

  • Alexis Jeandet*

  • Andrew Sheng*

  • Anna Lanteri*

  • Chris Hoang*

  • Christopher Arran*

  • Chun Hei Yip*

  • Dominik Stańczak

  • Elliot Johnson*

  • Erik Everson

  • flaixman*

  • Haman Bagherianlemraski*

  • Isaias McHardy*

  • itsraashi*

  • James Kent*

  • Joao Victor Martinelli*

  • Leo Murphy*

  • Luciano Silvestri*

  • Mahima Pannala*

  • Marco Gorelli*

  • Nick Murphy

  • Nicolas Lequette

  • Nikita Smirnov*

  • Peter Heuer

  • Pey Lian Lim*

  • Rajagopalan Gangadharan*

  • Raymon Skjørten Hansen*

  • Reynaldo Rojas Zelaya*

  • Riley Britten*

  • sandshrew118*

  • seanjunheng2*

  • Shane Brown*

  • Suzanne Nie*

  • Terrance Takho Lee*

  • Tien Vo*

  • Tiger Du

  • Tomás Stinson*

An asterisk indicates that this release contains their first contribution to PlasmaPy.

Backwards Incompatible Changes
  • In spectral_density, the arguments Te and Ti have been renamed T_e and T_i and are now required keyword-only arguments. (#974)

  • Moved the grid_resolution attribute from AbstractGrid to CartesianGrid and NonUniformCartesianGrid separately. This fixes a potential future bug, because this attribute is only valid as written when all axes share the same units. (#1295)

  • Changed the behavior of the __repr__ method of CustomParticle to display the symbol as well if it was provided. (#1397)

  • Removed a block of code that printed out special particle properties when plasmapy.particles.special_particles (renamed to plasmapy.particles._special_particles) was executed. (#1440)

  • Renamed plasmapy.particles.elements to plasmapy.particles._elements, plasmapy.particles.isotopes to plasmapy.particles._isotopes, plasmapy.particles.parsing to plasmapy.particles._parsing, and plasmapy.particles.special_particles to plasmapy.particles._special_particles. Consequently, these modules are no longer part of PlasmaPy’s public API. Most of these modules did not contain any public objects, except for plasmapy.particles.special_particles.ParticleZoo which was renamed to plasmapy.particles._special_particles.particle_zoo and removed from the public API. (#1440)

  • The parameters Z and mass_numb to Particle are now keyword-only. (#1456)

Deprecations and Removals
  • Officially deprecated plasmapy.formulary.parameters and scheduled its permanent removal for the v0.9.0 release. (#1453)

  • Dropped support for Python 3.7 in accordance with the deprecation policy laid out in NumPy Enhancement Proposal 29. (#1465)

  • The [all] option when using pip to install plasmapy is now deprecated and may be removed in a future release. Packages that were previously optional (h5py, lmfit, mpmath, and Numba) are now installed by default when running pip install plasmapy. To install all packages required for code development of PlasmaPy, instead run pip install plasmapy[developer]. (#1482)

  • Removed plasmapy.optional_deps. (#1482)

Features
Bug Fixes
Improved Documentation
  • Added a lite-function group to the configuration value automodapi_custom_groups that introduces the __lite_funcs__ dunder for listing the lite-functions in a module (akin to the __all__ dunder). (#1145)

  • Added a page in the Contributor Guide that describes how to add changelog entries. (#1198)

  • Created an example notebook that lets users input plasma properties and get plasma parameters. (#1229)

  • The file docs/_static/css/admonition_color_contrast.css was added to include color customizations for Sphinx admonitions that originally came from sphinx_rtd_theme_ext_color_contrast. (#1287)

  • Changed the color contrast of links and admonitions to be consistent with the Web Content Accessibility Guidelines 2 Level AA Conformance for contrast. (#1287)

  • Re-organized CSS files for the online documentation. The file docs/_static/rtd_theme_overrides.css was re-organized, renamed to docs/_static/css/plasmapy.css, and updated with comments to help someone unfamiliar with CSS to understand the file and syntax. (#1287)

  • Put references from plasmapy.formulary into docs/bibliography.bib in BibTeX format. (#1299)

  • Added a discussion of test parametrization with argument unpacking to the Testing Guide in the Contributor Guide. (#1316)

  • Adopted the Contributor Covenant Code of Conduct version 2.1 and updated the Contributor Covenant Code of Conduct page accordingly. (#1324)

  • Updated deprecated meeting and calendar links in README.md. (#1327)

  • Enabled the sphinx-hoverxref extension to Sphinx. (#1353)

  • Added bullet points on module level docstrings and __all__ to the documentation guide. (#1359)

  • Reverted the code syntax highlighting style back to the pygments default. The minimum version of pygments was set to 2.11.0 because the default style was changed to meet accessibility guidelines for contrast in this release. (#1361)

  • Described additional environments for building the documentation with make in the Documentation Guide. (#1373)

  • Moved references from individual docstrings to the Bibliography. (#1374)

  • Fixed the docstring of coupling_parameter. (#1379)

  • Added an example notebook that introduces how to use astropy.units. (#1380)

  • Added a “Getting Started” page to the documentation sidebar and a “Getting Started” section to the examples gallery. (#1380)

  • Added an example notebook that introduces how to use plasmapy.particles. (#1382)

  • Described the Plasma Calculator in the narrative documentation. (#1390)

  • Updated the cold magnetized plasma dielectric permittivity tensor notebook. (#1396)

  • Configured the Sphinx extension sphinx-hoverxref. (#1437)

  • Removed the following files from docs/api_static: plasmapy.particles.elements.rst, plasmapy.particles.isotopes.rst, plasmapy.particles.parsing.rst, and plasmapy.particles.special_particles.rst. These files corresponded to modules that were renamed with a leading underscore to indicate that they are no longer part of the public API. (#1440)

  • Updated the docstring for plasmapy.particles.particle_class.molecule. (#1455)

  • Hid the documentation page that contained the subpackage stability matrix. (#1466)

  • Added a discussion of doctests to the Documentation Guide. (#1478)

  • Removed the section on package requirements from the instructions on how to install plasmapy. (#1482)

  • Updated the instructions on how to install plasmapy. (#1482)

  • Defined autodoc_typehints_format="short" so signature type hints are displayed in short form, i.e. without the leading module names. (#1488)

  • Set minimum version of sphinx to v4.4. (#1488)

  • Defined the nitpick_ignore_regex configuration variable in docs/conf.py to specify regular expressions for objects to ignore in nitpicky documentation builds. (#1509)

  • Made numerous minor updates and fixes to reStructuredText links in docstrings and the narrative documentation. (#1509)

  • Described the GitHub Action for codespell in the Testing Guide. (#1530)

  • Added the sphinx-issues extension to Sphinx to simplify linking to GitHub issues, pull requests, users, and commits. (#1532)

  • Added the sphinx.ext.extlinks extension to Sphinx to simplify adding links to external domains which have a common base URL. (#1532)

  • Added the sphinx-notfound-page extension to Sphinx so that the documentation now has a 404 page in the same style as the rest of the documentation. (#1532)

  • Added a notebook on using beta from the plasmapy.formulary module to calculate plasma β in different parts of the solar atmosphere. (#1552)

  • Added an example notebook for the null point finder module. (#1554)

  • Added an example notebook that calculates plasma parameters associated with the Magnetospheric Multiscale Mission (MMS). (#1568)

  • Added an example notebook that discusses Coulomb collisions. (#1569)

  • Increased the strictness of the build_docs tox environment so that broken reStructuredText links now emit warnings which are then treated as errors, fixed the new errors, removed the build_docs_nitpicky tox environment, and updated the Documentation Guide accordingly. (#1587)

  • Renamed the magnetic_statics.ipynb notebook to magnetostatics.ipynb, and made some minor edits to its text and plotting code. (#1588)

  • Added examples sections to the documentation pages for several modules within plasmapy.formulary. (#1590)

  • Re-organized the directory structure for example notebooks. (#1590)

  • Alphabetized the author list in docs/about/credits.rst, and added missing authors from using git log and the pull request history. (#1599)

  • Renamed docs/developmentdocs/contributing, and set up redirects from the original hyperlinks to the new ones for the contributor guide. (#1605)

  • Added sphinx-reredirects as a Sphinx extension to allow website redirects. (#1605)

  • Added a robots.txt file to the online documentation to tell web crawlers to ignore all but stable and latest documentation builds when indexing for search engines. (#1607)

Trivial/Internal Changes
  • Streamlined preserve_signature such that it only binds __signature__ to the wrapped function, i.e. it no longer touches any other attribute of the wrapped function. (#1145)

  • Moved all tests associated with calculating the thermal speed from test file plasmapy/formulary/tests/test_parameters.py to plasmapy/formulary/tests/test_thermal_speed.py. (#1145)

  • Applied reStructuredText substitutions for plasmapy.particles and ParticleTracker in the narrative documentation. (#1158)

  • Added csslint to the pre-commit configuration to check the formatting and style of CSS files. (#1287)

  • Added Python 3.10 to the GitHub Actions test suite. (#1292)

  • Parametrized tests for plasmapy.formulary.parameters.ion_sound_speed. (#1313)

  • Added cron tests of the development versions of matplotlib and SciPy, while changing the cadence of cron tests to be run approximately fortnightly. (#1333)

  • Applied pytest.warns in several tests to catch warnings that are being issued during execution of the test suite. (#1345)

  • Split the tests running on pull requests into multiple stages. The various pytest test environments, including code coverage, now run conditionally given successful execution of a basic test environment and the linter checks. This change also prevents code coverage prompts from appearing twice, with incomplete information on the first time. (#1350)

  • Added a helper function that takes an iterable and creates a dict with physical types as keys and the corresponding objects from that iterable as values. This change updates the minimum required version of Astropy to 4.3.1. (#1360)

  • Added the module plasmapy.particles._factory which contains a private function that accepts arguments that can be provided to Particle, CustomParticle, or ParticleList and returns the appropriate instance of one of those three classes. (#1365)

  • Used the extract method refactoring pattern on the initialization of Particle objects. (#1366, #1368)

  • Refactored tests in plasmapy.particles. (#1369)

  • CustomParticle and DimensionlessParticle no longer emit a warning when the charge and/or mass is not provided and got assigned a value of nan in the appropriate units. (#1399)

  • Added unit test cases for manual entry of vector values in order to improve code coverage in the null point finder. (#1427)

  • Consolidated and parametrized tests associated with plasmapy.formulary.parameters.gyroradius. (#1430)

  • Within plasmapy.particles modules, the _elements, _isotopes, _parsing, and _special_particles modules are now imported directly. Before this, objects within these modules were typically imported. (#1440)

  • Renamed objects within the source code for plasmapy.particles to conform with PEP 8 naming conventions (e.g., ParticleZooClassParticleZoo, ParticleZooparticle_zoo, and Particlesparticles). (#1440)

  • Applied automated refactorings from Sourcery to plasmapy.utils. (#1463)

  • Applied automated refactorings from Sourcery to plasmapy.plasma. (#1464)

  • Bumped the minimum version of h5py to 3.0.0. (#1465)

  • Changed the raised exception to ImportError (from a general Exception) when attempting to import plasmapy from a Python version below the minimum supported version. (#1465)

  • Added a workflow to label pull requests based on size. (#1467, #1492)

  • Separated plasmapy.analysis.nullpoint.null_point_find into two functions named null_point_find and plasmapy.analysis.nullpoint.uniform_null_point_find. null_point_find finds the null points of a vector space whose values are manually entered. plasmapy.analysis.nullpoint.uniform_null_point_find finds the null points of a uniform vector space whose values are generated by a function provided by the user. (#1477)

  • Applied automated refactorings from Sourcery to plasmapy.particles. (#1479)

  • Applied automated refactorings from Sourcery to plasmapy.formulary. (#1480)

  • Bumped the minimum versions of mpmath to 1.2.1, numpy to 1.19.0, pandas to 1.0.0, pytest to 5.4.0, scipy to 1.5.0, and xarray to 0.15.0. (#1482)

  • Moved h5py, lmfit, mpmath, and Numba out of the extras requirements category and into the install requirements category. These packages are now installed when running pip install plasmapy. (#1482)

  • Added dlint, flake8, flake8-absolute-import, flake8-rst-docstrings, flake8-use-fstring, pydocstyle, and pygments into the tests requirements category and pre-commit into the extras requirements category. These dependencies are not required for basic installation with pip. (#1482)

  • Updated docs/environment.yml to use pip to install all requirements specified by requirements.txt when creating a Conda environment. (#1482)

  • Used codespell to fix typos. (#1493)

  • Used contextlib.suppress to suppress exceptions, instead of try & except blocks. (#1494)

  • Added a pre-commit hook that transforms relative imports to absolute imports, except in docs/plasmapy_sphinx. (#1499)

  • Added a test that import plasmapy does not raise an exception. (#1501)

  • Added a GitHub Action for codespell, and updated the corresponding tox environment to print out contextual information. (#1530)

  • Added plasmapy/utils/units_definitions.py to precompute units which were applied to optimize functionality in plasmapy/formulary/distribution.py. (#1531)

  • Replaced except Exception clauses in formulary, particles, and utils with specific exception statements. (#1541)

  • Added tests for passing array valued k and theta arguments to hollweg(), which was an added feature in #1529. (#1549)

  • Added flake8-implicit-str-concat and flake8-mutable as extensions for flake8. (#1557)

  • Added flake8-simplify as an extension for flake8. (#1558)

  • Applied automated refactorings from Sourcery to plasmapy.dispersion. (#1562)

  • Applied automated refactorings from Sourcery to plasmapy.diagnostics. (#1563)

  • Applied automated refactorings from Sourcery to plasmapy.analysis. (#1564)

  • Removed an extraneous print statement from collision_frequency that activated when the colliding particles were both electrons. (#1570)

  • Changed the type hints for z_mean in plasmapy.formulary.collisions functions from astropy.units.dimensionless_unscaled to Real. Consequently, z_mean will no longer be processed by validate_quantities. Previously, z_mean issued a warning when a real number was provided instead of a dimensionless Quantity. (#1570)

  • Updated the version of black to 22.3.0 in PlasmaPy’s pre-commit configuration. This update included a formatting change where spaces around power operators were removed for sufficiently simple operands (e.g., a ** ba**b). (#1582)

  • Renamed units_definitions to _units_definitions and units_helpers to _units_helpers in plasmapy.utils to mark these modules as private. (#1587)

  • Updated the codemeta.json file with metadata for the version 0.8.1 release. (#1606)

PlasmaPy v0.7.0 (2021-11-18)

This release of PlasmaPy contains 127 commits in 73 merged pull requests closing 37 issues from 19 people, 14 of which are first-time contributors to PlasmaPy.

  • 127 commits have been added since 0.6

  • 37 issues have been closed since 0.6

  • 73 pull requests have been merged since 0.6

  • 19 people have contributed since 0.6

  • 14 of which are new contributors

The people who have contributed to the code for this release are:

  • Alf Köhn-Seemann *

  • Andrew *

  • Armando Salcido *

  • Dominik Stańczak

  • FinMacDov *

  • Marco Gorelli *

  • Nick Murphy

  • Nicolas Lequette *

  • Peter Heuer

  • Quettle *

  • RAJAGOPALAN-GANGADHARAN *

  • Sjbrownian *

  • Tiger Du

  • Tomás Stinson *

  • bryancfoo *

  • dependabot[bot] *

  • haman80 *

  • pre-commit-ci[bot] *

  • rocco8773

Where a * indicates that this release contains their first contribution to PlasmaPy.

Backwards Incompatible Changes
  • Removed alias tfds_ to plasmapy.dispersion.two_fluid_dispersion.two_fluid_dispersion_solution, with the reasoning behind the removal outlined in the pull request. (#1101)

  • Removed the Tracker.synthetic_radiograph() method and created the standalone function :func:~plasmapy.diagnostics.charged_particle_radiography.synthetic_radiograph in its place. This new function takes either a ~plasmapy.diagnostics.charged_particle_radiography.Tracker object or a dictionary equivalent to ~plasmapy.diagnostics.charged_particle_radiography.Tracker.results_dict. (#1134)

  • Renamed subpackage plasmapy.diagnostics.proton_radiography to plasmapy.diagnostics.charged_particle_radiography, and renamed the SyntheticProtonRadiograph class within that module to ~plasmapy.diagnostics.charged_particle_radiography.Tracker. (#1134)

  • ~plasmapy.diagnostics.charged_particle_radiography.Tracker no longer supports making changes to an instantiated object and re-running the simulation. Subsequent simulations should be performed by instantiating a new ~plasmapy.diagnostics.charged_particle_radiography.Tracker object and running its simulation. (#1134)

  • For CartesianGrid the volume_averaged_interpolator now returns numpy.nan values for any interpolation not bounded by the grid points. (#1173)

  • Renamed file two_fluid_dispersion.py to two_fluid_.py and moved it into the plasmapy.dispersion.analytical subpackage. The function two_fluid_dispersion_solution() contained within that file was renamed to two_fluid. (#1208)

  • Changed ParticleList so that if it is provided with no arguments, then it creates an empty ParticleList. This behavior is analogous to how list and tuple work. (#1223)

  • Changed the behavior of Particle in equality comparisons. Comparing a Particle with an object that is not particle-like will now return False instead of raising a TypeError. (#1225)

  • Changed the behavior of CustomParticle so that it returns False when compared for equality with another type. Previously, a TypeError was raised. (#1315)

Deprecations and Removals
  • In plasmapy.particles, use of the term “integer charge” has been deprecated in favor of the term “charge number”. The integer_charge attribute of Particle has been deprecated in favor of charge_number. The integer_charge attribute of IonicLevel (formerly IonicFraction) has been deprecated in favor of charge_number. The integer_charges attribute of IonizationState has been deprecated in favor of charge_numbers. (#1136)

  • The particle attribute of Particle has been removed after having been deprecated in 0.6.0. (#1146)

  • Use more generalized keyword argument T instead of T_i in plasmapy.formulary.parameters.gyroradius. The T_i argument has been deprecated and will be removed in a subsequent release. (#1210)

Features
Bug Fixes
  • Made Particle instances pickleable. (#1122)

  • Fixed the behavior of plasmapy.formulary.mathematics.Chandrasekhar_G at very small and very large argument values. This change was reverted in #1233. (#1125)

  • Running ~plasmapy.diagnostics.charged_particle_radiography.synthetic_radiograph with the keyword optical_density=True will now return numpy.inf where the source profile intensity is zero. Previously, an incorrect value was returned since zero entries were replaced with values of 1 before taking the logarithm. (#1134)

  • Fixed a bug in the volume-averaged interpolator for CartesianGrid (volume_averaged_interpolator). The old method miss interpreted where the interpolation point was inside the nearest neighbor cell volume. So, if an interpolation point was at the lower bounds of the nearest neighbor cell volume, then the position was flipped and interpreted as being at the upper bounds of the cell volume, and visa-versa. (#1173)

  • Fixed the normalization of the wavevector in the Thomson spectral density function, spectral_density(). The previous version was not properly normalizing the wavevector to unity. (#1190)

  • Reverted most of #1084 and #1125, removing our implementation of the Chandrasekhar G function (for now!). This function may get brought back at a later date, once we have an implementation we numerically trust. (#1233)

Improved Documentation
  • Improved consistency of documentation style and made reStructuredText fixes in several subpackages. (#1073)

  • Added a pre-release section to the Release Guide. This section now includes steps for having a feature freeze about a week before the release, followed by a code freeze about two days before the release. (#1081)

  • Created the Sphinx extension package plasmapy_sphinx and used it to replace sphinx_automodapi. plasmapy_sphinx creates directives automodapi and automodsumm to replace the same directives defined by sphinx_automodapi. The documentation was updated so the slight syntax differences in the newly defined directives will still render the same as before. (#1105)

  • The term “integer charge” has been replaced in the documentation with the term “charge number”. (#1136)

  • Implemented a framework to define and use common Sphinx substitutions across the narrative documentation and docstrings. These substitutions are defined in docs/common_links.rst. (#1147)

  • Began a project glossary at docs/glossary.rst. (#1149)

  • Changed the default branch name to main. Locations in the code and documentation that referred to the default branch of PlasmaPy (and certain other packages) were changed to reflect the new name (including, for example, in the development guide in the documentation). (#1150)

  • Updated information on how to write and build documentation in the development guide. (#1156)

  • Updated information on how to write and run tests in the Contributor Guide. (#1163)

  • Created an outline of a page in the development guide to describe the workflow required to contribute to PlasmaPy. (#1178)

  • Added brief description about the physics of the upper-hybrid resonance to the docstring of the function plasmapy.formulary.parameters.upper_hybrid_frequency. (#1180)

  • Added a brief description about the physics of the lower-hybrid resonance to the docstring of the function plasmapy.formulary.parameters.lower_hybrid_frequency. (#1181)

  • Made the function plasmapy.formulary.parameters.gyrofrequency more general by removing the indications that it might only work for ions. (#1183)

  • Make plasmapy.analysis.fit_functions.AbstractFitFunction.FitParamTuple a property to fix the documentation build warning caused by the release of Sphinx v4.1.0. (#1199)

  • Included a step in the Release Guide to update Binder requirements so that the release of PlasmaPy on PyPI gets installed when opening example notebooks from the stable and release branches of the online documentation. (#1205)

  • Updated the Documentation Guide to include updates to tox environments for building the documentation. (#1206)

  • Fixed numerous broken reStructuredText links in prior changelogs. (#1207)

  • Improve the docstring for plasmapy.online_help. (#1213)

  • Renamed “Development Guide” to “Contributor Guide”, and temporarily removed the incomplete docs/development/workflow.rst from the toctree of the Contributor Guide. (#1217)

  • Fixed a typo in the docstring of plasmapy.formulary.parameters.Alfven_speed. (#1218)

  • Fixed broken reStructuredText links in docstrings for aliases in plasmapy.formulary. (#1238)

  • Fixed multiple broken and redirected links. (#1257)

  • Updated the Documentation Guide to include a description on how to add and cite references to PlasmaPy’s global bibliography BibTeX file, docs/bibliography.bib. (#1263)

  • Added sphinxcontrib-bibtex as a Sphinx extension to enable references to be stored in a BibTeX file. (#1263)

  • Began a documentation-wide Bibliography page. (#1263)

  • Updated the Documentation Guide to describe where formulae should go in docstrings and how to use Glossary entries. (#1264)

  • Updated and fixed hyperlinks in the documentation. (#1267)

  • Adopted the "xcode" code highlighting style for pygments to increase color contrast and improve web accessibility. (#1268)

  • Updated the feedback and communication page. (#1272)

  • Updated the requirements for the documentation build to include no restrictions on docutils and sphinx_rtd_theme >= 1.0.0. docutils == 0.17 is not compatible with sphinx_rtd_theme < 1.0 (see #1107 and #1230). (#1275)

  • Added a screenshot of the link for the Read the Docs preview of the documentation for a pull request. (#1298)

  • Incorporated citations in the two_fluid docstring into the PlasmaPy Bibliography framework. (#1301)

Trivial/Internal Changes
  • Simplified handling of package dependencies. Removed duplicated requirements files and centralized them instead. Developer dependencies can now be installed with either pip install plasmapy[developer] or pip install -r requirements.txt. (#789)

  • Reconfigured flake8 settings in CI. (#1062)

  • Added pydocstyle to continuous integration (CI), to hopefully make writing prettier docstrings easier. (#1062)

  • Added flake8-rst-docstrings to catch reStructuredText formatting errors in documentation in the linter stage of CI. (#1062)

  • Added pytest-regressions to testing dependencies, to make regression tests a little easier to write. (#1084)

  • Fixed a minor error in the \(\mathbf{E} × \mathbf{B}\) drift notebook. (#1088)

  • Upgrade nbqa to latest available version (0.6.0). (#1104)

  • Moved our custom pre-commit style testing suite to pre-commit.ci, taking advantage of the new pre-commit.ci autofix command that allows manually calling for pre-commit to be run by typing that command as a comment to a pull request. (#1106)

  • Added tests using hypothesis. (#1125)

  • Added to setup.cfg the configuration flake8.per-file-ignores=plasmapy/formulary/__init__.py:F403 to ignore warnings resulting from imports like from xx import *. (#1127)

  • Re-enabled several flake8 checks by removing the following codes from the flake8.extend-ignore configuration in setup.cfg: D100, D102, D103, D104, D200, D210, D301, D401, D407, D409, D412, E712, E713, F403, F541, RST213, RST306, and RST902. Addressed any failed linter checks from this modification. (#1127)

  • ~plasmapy.diagnostics.charged_particle_radiography.synthetic_radiograph now determines the default detector size to be the smallest detector plane centered on the origin that includes all particles. (#1134)

  • Added ion velocity input to the thomson.ipynb diagnostics notebook. (#1171)

  • Added tox and removed pytest as extra requirements. (#1195)

  • Updated tox test environments for building the documentation. Added the build_docs_nitpicky environment to check for broken reStructuredText links. (#1206)

  • Added the --keep-going flag to the build_docs* tox environments with the -W option so that test failures will not stop after the first warning (that is treated as an error). (#1206)

  • Make queries to plasmapy.online_help for "quantity" or "quantities" redirect to the help page for astropy.units (which was already the case for "unit" and "units"). (#1213)

  • Bumped the Python version for Read the Docs builds from 3.7 to 3.8. (#1248)

  • Refactored plasmapy/dispersion/tests/test_dispersion.py to use hypothesis for property based testing. (#1249)

  • Defined redirects to allow and anchors to avoid checking when using Sphinx to verify that hyperlinks are correct via make linkcheck. (#1267)

  • Replaced usage of eval inside IonizationStateCollection with getattr. (#1280)

  • Added using dlint to the linters testing environment in tox.ini as a static analysis tool to search for security issues. (#1280)

  • Enabled using flake8-use-fstring in the linters testing environment in tox.ini to enforce usage of formatted string literals (f-strings). (#1281)

  • Switched usage of str.format to formatted string literals (f-strings) in several files. (#1281)

  • Added flake8-absolute-import to the linters tox environment. (#1283)

  • Removed unused imports, and changed several imports from relative to absolute. (#1283)

  • Added pre-commit hooks to auto-format .ini, .toml, and .yaml files, and applied changes from those hooks to existing files. (#1284)

  • Changed the validated units for the theta input argument of two_fluid from degrees to radians. (#1301)

  • Replaced usage of distutils.version.StrictVersion with packaging.version.Version because distutils has been deprecated. As part of this change, packaging has been added as a dependency. (#1306)

  • Increased the minimum version of matplotlib to 3.3.0 and updated plasmapy.diagnostics.langmuir.swept_probe_analysis to be compatible with matplotlib 3.5.0. (#1334)

PlasmaPy v0.6.0 (2021-03-14)

The people who have contributed to the code for this release are:

  • Anthony Vo

  • Dhawal Modi *

  • Dominik Stańczak

  • Drozdov David *

  • Erik Everson

  • Kevin Montes *

  • Nick Murphy

  • Peter Heuer

  • Ramiz Qudsi

  • Tiger Du

Where a * indicates their first contribution to PlasmaPy.

Backwards Incompatible Changes
  • The State namedtuple was changed to the plasmapy.particles.IonicFraction class. (Note: #1046 subsequently changed that to IonicLevel). (#796)

  • Now, when the IonizationState class is provided with an ion, the ionic fraction for that ion is set to 100% for the corresponding element or isotope. (#796)

  • AtomicError was renamed to ParticleError and MissingAtomicDataError was renamed to MissingParticleDataError. (#796)

  • In plasmapy.particles, the IonizationStates class was renamed to IonizationStateCollection. Argument n of IonizationStates was changed to n0 in IonizationStateCollection. (#796)

  • Moved and refactored error message formatting functionality from plasmapy.utils.error_messages to plasmapy.utils.code_repr. (#920)

  • Renamed the available “methods” for computing the Coulomb logarithm in an attempt to make the names more explicit. This is implemented using the method keyword for functions Coulomb_logarithm and impact_parameter, and then propagated throughout the functionality in plasmapy.formulary.collisions. (#962)

  • Add dependency pandas >= 1.0.0. Modify xarray dependency to be xarray >= 0.14.0. (#963)

  • The AbstractGrid property grid is now dimensioned (has units) and cannot be accessed if all dimensions do not share the same units. (#981)

  • Renamed attribute is_uniform_grid on AbstractGrid to is_uniform. (#981)

  • Drop Python 3.6 support. (#987)

  • The __getitem__ method of AbstractGrid now returns a Quantity array instead of a reference to a xarray.DataArray. (#1027)

  • Renamed IonicFraction to IonicLevel. This lays groundwork for future changes, where that class is going to become more than a fraction. (#1046)

Deprecations and Removals
  • The particle attribute of Particle has been deprecated in favor of the new symbol attribute. The particle attribute now issues a FutureWarning to indicate that it will be removed in a future release. (#984)

Features
Bug Fixes
  • Fixed a minus sign bug in the Particle Tracker simulation that caused the E×B drift to go in the incorrect direction. (#953)

  • Bugfix plasmapy.analysis.fit_functions.Linear.root_solve() to handle the case where the slope is zero and no finite roots exist. (#959)

  • Fixed a bug that prevented nested iterations of a single IonizationState or IonizationStateCollection instance. (#1025)

  • Fixed a bug in grids.py for non-uniform grids that arose when xarray upgraded to v0.17.0 (#1027)

  • In plasmapy.diagnostics.proton_radiography.SyntheticProtonRadiograph, adaptive dt now calculates the cyclotron period using the provided particle charge and mass (previously assumed protons). (#1035)

  • In plasmapy.diagnostics.proton_radiography.SyntheticProtonRadiograph, the adaptive timestep algorithm now works when particles are provided using plasmapy.diagnostics.proton_radiography.SyntheticProtonRadiograph.load_particles. (#1035)

  • In plasmapy.diagnostics.proton_radiography.SyntheticProtonRadiograph, removed highly deflected particles so the call of plasmapy.diagnostics.proton_radiography.SyntheticProtonRadiograph.max_deflection does not raise an exception. (#1035)

Improved Documentation
  • Add narrative documentation on ionization state functionality. (#796)

  • Added description to plasmapy.formulary.parameters.Hall_parameter signature and equation in docstrings. (#934)

  • Updated documentation for the plasmapy.particles and plasmapy.utils subpackages. (#942)

  • Improves documentation of plasmapy/formulary/quantum.py by cleaning up docstrings of contained functionality. (#951)

  • Update all docstrings associated with computing the Coulomb logarithm and the possible methods of calculation. (#962)

  • Add two Jupyter notebooks for functionality contained in plasmapy.plasma.grids: grids_cartesian.ipynb and grids_nonuniform.ipynb. (#963)

  • Added the ExB drift notebook, which demonstrates the analytical solution for the drift and the implementation of the corresponding formulary drift functions, force_drift and ExB_drift. (#971)

  • Describe what constitutes a valid representation of a particle in the docstring for the plasmapy.particles.particle_class.ParticleLike typing construct. (#985)

  • Put the docstring for plasmapy.particles.particle_class.Particle.is_category into numpydoc format. (#1039)

  • Adds formulas (which were missing) to the docstrings of plasmapy.formulary.dimensionless.quantum_theta and beta. (#1041)

  • Add live rendering of changelog entries on documentation builds, based on sphinx-changelog. (#1052)

  • Created an example notebook demonstrating how the plasmapy.diagnostics.proton_radiography.SyntheticProtonRadiograph class can be used to generate synthetic proton radiographs with arbitrary source profiles. Add code documentation links to all proton radiograph notebooks. (#1054)

  • Update formatting and broken sphinx.ext.intersphinx links in plasmapy.formulary docstrings. (#1058)

  • Make minor fixes in plasmapy.particles docstrings. (#1064)

  • Organize the layout of the example Jupyter notebooks on the Read the Docs example page. (#1066)

  • Fix formatting and broken sphinx.ext.intersphinx links in docstrings in various places in the code base. Improve installation instructions in the docs; the subpackage stability matrix, and funding acknowledgments. (#1076)

Trivial/Internal Changes
  • Removed colorama as a dependency. (#920)

  • Moved remaining CI from CircleCI to GitHub Actions. (#996)

  • Add notebook CI through nbqa. (#997)

  • Remove lambda expressions from plasmapy.particles and plasmapy.utils. (#1013)

  • Add unicode particle aliases for electrons ("β-", "β⁻"), muons ("μ-", "μ⁻"), anti-muons ("μ+", "μ⁺"), tau particles ("τ", "τ-", "τ⁻"), anti-tau particles ("τ+", "τ⁺") electron neutrinos ("ν_e"), muon neutrinos ("ν_μ"), tau neutrinos ("ν_τ"), and alpha particles ("α"). (#1036)

  • A set containing all valid particle categories may now be accessed via plasmapy.particles.particle_class.Particle.is_category.valid_categories. (#1039)

  • Properly handled warnings in test_proton_radiography.py (#1050)

PlasmaPy v0.5.0 (2020-12-09)

This release of PlasmaPy contains 58 commits in 31 merged pull requests closing 16 issues from 8 people, 4 of which are first-time contributors to PlasmaPy.

The people who have contributed to the code for this release are:

  • Dominik Stańczak

  • Nick Murphy

  • Peter Heuer

  • Ramiz Qudsi *

  • Steve Richardson *

  • Tiger D *

    1. Cody Skinner *

  • rocco8773

Where a * indicates their first contribution to PlasmaPy.

Backwards Incompatible Changes
  • Created plasmapy.dispersion in accordance with PlasmaPy Enhancement Proposal 7 (PLEP 7) and migrated the dispersion functionality (dispersionfunction.py) from plasmapy.formulary to plasmapy.dispersion. (#910)

  • Removed default values for the ion and particle arguments of functions contained in plasmapy.formulary.parameters, in accordance with issue [#453](https://github.com/PlasmaPy/PlasmaPy/issues/453), and updated all relevant calls to modified functionality. (#911)

  • Moved test helper exceptions from plasmapy.utils.pytest_helpers to plasmapy.tests.helpers. (#919)

  • Updated plasmapy.formulary.parameters.mass_density so it calculates the mass density for a specific particle from a given number density. Original function calculated the total mass density (ion + electron). (#957)

Features
  • Added a function to calculate the power spectrum of thermal bremsstrahlung emitted by a Maxwellian plasma. (#892)

  • Added support for multiple electron components to diagnostics.thomson.spectral_density. Also fixed a bug for multiple ion populations. (#893)

  • Add dependency pygments >= 2.4.1. (#898)

  • Create the plasmapy.analysis package as per PLEP-7 and initialize the package with the fit_functions module. Fit functions are designed to wrap together an analytical function, a curve fitter, uncertainty propagation, and a root solver to make curve fitting a little less painful. (#908)

  • Created a new subpackage, plasmapy.tests.helpers, to contain test helper functionality. (#919)

  • Create decorator modify_docstring, which allows for programmatically prepending and/or appending a docstring. (#943)

Bug Fixes
  • Allowed implicit conversions of AstroPy units in inputs and outputs of validated functions to happen without warnings. Most notably, this removes warnings on eV inputs to temperature fields. (#886)

  • Updated plasmapy.formulary.parameters.Alfven_speed to properly use the updated plasmapy.formulary.parameters.mass_density and maintain the same behavior. Also add handling of the ion input keyword, so Particle and the Particle convertible representations can be used as inputs. (#957)

Improved Documentation
  • Improved the release guide after the release of 0.4.0. (#872)

  • Add various improvements to the documentation.
    • Replace home link with the plasmapy logo.

    • Add module and index navigation links to sidebar header.

    • Replace raw html on the main page that simulates a nbgallery with a real nbgallery directive.

    • Move link to view page source code from the header to footer.

    • Add link to footer the jumps the user back to the top of the page.

    • Create and add custom CSS stylesheet.

    • Create _templates directory and templates to customize page elements. (#875)

  • Add static stub files to docs/api_static so all modules of plasmapy are indexed. This is necessary to expose all of plasmapy since not all modules are indexed in the narrative documentation. (#878)

  • Decompose sub-package plasmapy/utils/roman/ into the plasmapy/utils/roman.py file. Move definition of roman specific exceptions into plasmapy.utils.exceptions. (#883)

  • Replaced references to Riot.im with references to Element.io or Matrix, as appropriate, following their recent rebranding. (#891)

  • Update the information on how to cite PlasmaPy, including in the release guide. (#900)

Trivial/Internal Changes
  • Apply isort to entire codebase, bringing it back to the pre-commit hook suite. (#857)

  • Expand package metadata contained in codemeta.json, following the CodeMeta standard. (#902)

  • Changed remaining instances of @u.quantity_input to @validate_quantities in response to issue #880. (#905)

  • Switched from Azure Pipelines to GitHub Actions for PR tests to make things easier for contributors. Moved away from Travis CI for test cron jobs. (#952)

PlasmaPy v0.4.0 (2020-07-20)

This release of PlasmaPy contains 50 commits in 46 merged pull requests closing 25 issues from 9 people, 5 of which are first-time contributors to PlasmaPy.

The people who have contributed to the code for this release are:

  • Ankur Chattopadhyay *

  • Anthony Vo *

  • Diego Diaz

  • Dominik Stańczak

  • Jakub Polak *

  • KhalilBryant *

  • Nick Murphy

  • Peter Heuer *

  • rocco8773

Where a * indicates their first contribution to PlasmaPy.

Backwards Incompatible Changes
  • Rename plasmapy.atomic to particles. In collisions and braginskii, change arguments named particles to species and arguments named ion_particle to ion for multiple functions. (#742)

  • Officially delete plasmapy.examples. (#822)

  • Move plasmapy.data to plasmapy.particle.data. (#823)

  • Renamed the plasmapy.classes subpackage to plasmapy.plasma. (#842)

Features
  • Added units to reprs of formulary.magnetostatics classes. (#743)

  • Create prototype abstract interfaces for plasma simulations (#753)

  • Created classes to represent custom and dimensionless particles in plasmapy.particles. (#755)

  • Create relativistic_energy() function, which uses the established Lorentz_factor() function to aid in the calculation of the relativistic energy of an object. (#805)

  • Create Reynolds_number() function. (#815)

  • Create Mag_Reynolds() function. (#820)

  • Create plasmapy.formulary.parameters.Bohm_diffusion function. (#830)

  • Added a new diagnostics module thomson containing a function spectral_density that calculates Thomson scattering spectra for Maxwellian plasmas in both the collective and non-collective regimes. As a followup to PR #835, set the minimal required Numpy version to 1.18.1 to finally fix unit dropping bugs. (#831)

  • Revised plasmapy.formulary.parameters.thermal_speed to support 1D and 2D distributions as well as 3D, and added an example notebook for this function. (#850)

  • Create plasmapy/formulary/ionization.py Create plasmapy.formulary.ionization.Z_bal function. (#851)

  • Create Saha() function. (#860)

  • Added aliases (with trailing underscores) for parameters in the formulary:

    • plasmapy.formulary.dimensionless.Reynolds_numberRe_

    • plasmapy.formulary.dimensionless.Mag_ReynoldsRm_

    • plasmapy.formulary.drifts.ExB_driftveb_

    • plasmapy.formulary.drifts.force_driftvfd_

    • plasmapy.formulary.parameters.mass_densityplasmapy.formulary.parameters.rho_

    • plasmapy.formulary.parameters.Alfven_speedplasmapy.formulary.parameters.va_

    • plasmapy.formulary.parameters.ion_sound_speedplasmapy.formulary.parameters.cs_

    • plasmapy.formulary.parameters.thermal_speedplasmapy.formulary.parameters.vth_

    • plasmapy.formulary.parameters.thermal_pressureplasmapy.formulary.parameters.pth_

    • plasmapy.formulary.parameters.kappa_thermal_speedplasmapy.formulary.parameters.vth_kappa_

    • plasmapy.formulary.parameters.inertial_lengthplasmapy.formulary.parameters.cwp_

    • plasmapy.formulary.parameters.Hall_parameterplasmapy.formulary.parameters.betaH_

    • plasmapy.formulary.parameters.gyrofrequencyplasmapy.formulary.parameters.oc_, plasmapy.formulary.parameters.wc_

    • plasmapy.formulary.parameters.gyroradiusplasmapy.formulary.parameters.rc_, plasmapy.formulary.parameters.rhoc_

    • plasmapy.formulary.parameters.plasma_frequencyplasmapy.formulary.parameters.wp_

    • plasmapy.formulary.parameters.Debye_lengthplasmapy.formulary.parameters.lambdaD_

    • plasmapy.formulary.parameters.Debye_numberplasmapy.formulary.parameters.nD_

    • plasmapy.formulary.parameters.magnetic_pressureplasmapy.formulary.parameters.pmag_

    • plasmapy.formulary.parameters.magnetic_energy_densityplasmapy.formulary.parameters.ub_

    • plasmapy.formulary.parameters.upper_hybrid_frequencyplasmapy.formulary.parameters.wuh_

    • plasmapy.formulary.parameters.lower_hybrid_frequencyplasmapy.formulary.parameters.wlh_

    • plasmapy.formulary.parameters.Bohm_diffusionplasmapy.formulary.parameters.DB_

    • plasmapy.formulary.quantum.deBroglie_wavelengthlambdaDB_

    • plasmapy.formulary.quantum.thermal_deBroglie_wavelengthlambdaDB_th_

    • plasmapy.formulary.quantum.Fermi_energyEf_ (#865)

  • Add json_dumps method to AbstractParticle to convert a particle object into a JSON string. Add json_dump method to AbstractParticle to serialize a particle object and writes it to a file. Add JSON decoder ParticleJSONDecoder to deserialize JSON objects into particle objects. Add plasmapy.particles.serialization.json_loads_particle function to convert JSON strings to particle objects (using ParticleJSONDecoder). Add plasmapy.particles.json_load_particle function to deserialize a JSON file into a particle object (using ParticleJSONDecoder). (#836)

Bug Fixes
  • Fix incorrect use of pkg.resources when defining plasmapy.__version__. Add setuptools to package dependencies. Add a definition of __version__ for developers using source files. (#774)

  • Repair notebook links that are defined in the nbsphinx_prolog sphinx configuration variable. (#828)

  • Increase the required Astropy version from 3.1 to 4.0, Numpy from 1.14 to 1.16.6, Scipy from 0.19 to 1.2 and lmfit from 0.9.7 to 1.0.1. This fixes long-standing issues with Numpy operations dropping units from AstroPy quantities. (#835)

Improved Documentation
    • Added documentation to file test_converters (#756)

    • Updated installation instructions. (#772)

  • Reorder documentation page (#777)

  • Fix failing documentation build due to duplicate docstrings for ParticleTracker.kinetic_energy_history and incompatibility of sphinx-automodapi with sphinx v3.0.0. (#780)

  • Automate definition of documentation release and version in docs/conf.py with plasmapy.__version__. (#781)

  • Add a docstring to __init__.py in plasmapy.formulary. (#788)

  • Replaced sphinx-gallery with nbsphinx, turning .py example files into .ipynb files and allowing for easier example submission. (#792)

  • Linked various instances of classes and functions in the .ipynb examples in docs/notebooks/ to the respective API docs. (#825)

  • Fixed a few documentation formatting errors. (#827)

  • Add notes on the PlasmaPy benchmarks repository to documentation. (#841)

  • Improve readability of the plasmapy.formulary page by replacing the toctree list with a cleaner reStructuredText table. (#867)

Trivial/Internal Changes
  • Remove mutable arguments from Particle.is_category method. (#751)

  • Remove all occurrences of default mutable arguments (#754)

  • Handle ModuleNotFoundError when trying to import __version__ but setuptools_scm has not generated the version.py file. This commonly happens during development when plasmapy is not installed in the python environment. (#763)

  • Updated pep8speaks/flake8 configuration and added .pre-commit-config.yaml to simplify automated style checks during development. (#770)

  • Removes some lint from setup.py and setup.cfg. Use pkg_resources for version checking in code. Remove version.py file in favor of pkg_resources. (#771)

  • Default settings for isort were set to be consistent with default settings for black. (#773)

  • Update community meeting and funding information in docs. (#784)

  • Improved pull request template to include more information about changelog entries. (#843)

  • Added GitHub actions that apply pre-commit and flake8 (separately) to incoming pull requests. (#845)

  • Apply pre-commit hooks to entire repository, so that GitHub actions do not shout at contributors needlessly. (#846)

  • Update CustomParticle so input parameters mass and charge can accept string representations of astropy Quantities. (#862)

PlasmaPy v0.3.1 (2020-02-01)

Bug Fixes
  • Fix various packaging issues encountered during the v0.3.1 release.

PlasmaPy v0.3.0 (2020-01-25)

Backwards Incompatible Changes
  • Create simulation subpackage; move Species particle tracker there; rename to particletracker (#665)

  • Changed plasmapy.classes.Species to plasmapy.simulation.ParticleTracker (#668)

  • Move pytest helper functionality from plasmapy.utils to plasmapy.utils.pytest_helpers (#674)

  • Move plasmapy.physics, plasmapy.mathematics and plasmapy.transport into the common plasmapy.formulary subpackage (#692)

  • Change ClassicalTransport methods into attributes (#705)

Deprecations and Removals
  • Remove parameters_cython.pyx, switching to Numba for the future of computationally intensive code in PlasmaPy (#650)

  • Remove plasmapy.constants, which was a thin wrapper around astropy.constants with no added value (#651)

Features
  • Generalize ion_sound_speed function to work for all values of \(k^2 \lambda_{D}^2\) (i.e. not just in the non-dispersive limit). (#700)

  • Optimize add__magnetostatics for a 16x speedup in tests! (#703)

Bug Fixes
  • Define preserve_signature decorator to help IDEs parse signatures of decorated functions. (#640)

  • Fix Pytest deprecations of message argument to raise and warn functions. (#666)

  • Fix h5py warning in OpenPMD module, opening files in read mode by default (#717)

Improved Documentation
  • Added real-world examples to examples/plot_physics.py and adjusted the plots to be more human-friendly. (#721)

  • Add examples images to the top of the main doc page in docsindex.rst (#655)

  • Added examples to the documentation to mass_density

    and Hall_parameter functions (#709)

  • Add docstrings to decorator plasmapy.utils.decorators.converter.angular_freq_to_hz(). (#729)

Trivial/Internal Changes
  • Replace decorator plasmapy.utils.decorators.checks.check_quantity with decorator plasmapy.utils.decorators.validators.validate_quantities(). Permanently delete decorator plasmapy.utils.decorators.checks.check_quantity and its supporting code. For functions plasmapy.formulary.quantum.chemical_potential() and plasmapy.formulary.quantum._chemical_potential_interp, add a raise NotImplementedError due to bug outlined in issue https://github.com/PlasmaPy/PlasmaPy/issues/726. Associated pytests are marked with pytest.mark.xfail and doctests are marked with doctests: +SKIP. (#722)

  • Add towncrier automated changelog creation support (#643)

  • Move existing “check” decorators to new plasmapy.utils.decorators module (#647)

  • Allow running our sphinx-gallery examples as Jupyter notebooks via Binder (#656)

  • Overhaul CI setup, following the example of SunPy (#657)

  • Patch sphinx_gallery.binder to output custom links to Binder instance (#658)

  • Remove the now unnecessary astropy_helpers submodule (#663)

  • Followup PR to CI overhaul (#664)

  • Add a Codemeta file (codemeta.json) (#676)

  • Overhaul and simplify CI, add Python 3.8 to tests, bump minimal required package versions, fix docs. (#712)

  • Update communication channels in docs (#715)

  • Code style fixes to the atomic subpackage (#716)

  • Clean up main package namespace, removing plasmapy.test (#718)

  • Reduce precision of tests and doctests to allow for refinements of fundamental constants. (#731)

  • Create decorators for checking/validating values and units of function/method input and return arguments. Defined decorators include check_values(), check_units(), and validate_quantities(). These decorators are fully defined by “decorator classes” CheckBase, CheckValues, CheckUnits, and ValidateQuantities. (#648)

  • Create a decorator to change output of physics functions from “radians/s” to “hz” (#667)

  • Added pytest.mark.slow to pytest markers. Updated documentation to notify developers of functionality. (#677)

PlasmaPy v0.2.0 (2019-05-31)

Version 0.2.0 is the second development release of PlasmaPy. Alongside a few new features, it brings plentiful refactoring, documentation and back stage improvements.

New Features
  • Implement machinery for a Plasma class factory based on PLEP 6

  • Create an openPMD Plasma subclass

  • Create classes to represent ionization state distributions for one or more elements or isotopes.

  • Add basic particle drifts to plasmapy.physics.drifts

  • Turn most dependencies into optional, subpackage-specific ones

Bug Fixes
  • Improve handling of NumPy arrays for plasma parameter and transport functions.

  • Vendor the roman package so as to allow installation via Conda

  • Decrease strictness of check_quantity to allow nan and inf by default

Changes to API
  • Move plasmapy.transport from plasmapy.physics to its own subpackage.

PlasmaPy v0.1.1 (2018-05-27)

Version 0.1.1 is a bugfix patch release correcting a number of issues that arose during the release process and adding two minor convenience features.

New Features
Bug Fixes
  • Bring back mistakenly removed Cython versions of plasma parameters.

  • Optimize check_relativistic.

  • Correct a failing import statement.

  • Fix a number of issues with the Maxwellian distribution in physics.distribution.

PlasmaPy v0.1.0 (2018-04-29)

Version 0.1.0 is the initial development release of PlasmaPy. This version is a prototype and a preview, and is not feature complete. Significant changes to the API are expected to occur between versions 0.1.0 and 0.2.0, including backward incompatible changes.

New Features
  • Composed PlasmaPy’s vision statement.

  • Adopted the Contributor Covenant Code of Conduct.

  • Created a guide on contributing to PlasmaPy.

  • Adopted a permissive BSD 3-clause license with protections against software patents.

  • Set up continuous integration testing with Travis CI, CircleCI, and AppVeyor, along with test coverage checks with Coveralls.

  • Decided upon code and docstring style conventions and set up automated code style checks with pep8speaks.

  • Developed online documentation for PlasmaPy that is hosted by Read the Docs.

    • Automated documentation builds with Sphinx.

    • Wrote narrative documentation for each subpackage.

  • Adopted use of units as a units package.

  • Created the plasmapy.atomic subpackage to provide easy access to commonly used atomic data.

    • Created a functional interface to access particle properties and find the energy released from nuclear reactions.

    • Created the plasmapy.atomic.Particle class as an object-oriented interface to the plasmapy.atomic subpackage.

    • Created the plasmapy.atomic.particle_input decorator.

  • Created the plasmapy.classes subpackage that includes the prototype plasmapy.classes.Plasma3D, plasmapy.classes.PlasmaBlob, and plasmapy.classes.Species classes.

  • Created the plasmapy.constants subpackage.

  • Created the plasmapy.mathematics subpackage that contains analytical functions commonly used in plasma physics.

  • Created the plasmapy.physics subpackage with its plasmapy.physics.transport module to calculate plasma parameters, transport coefficients, dielectric tensor elements, collision rates, and relativity/quantum physics parameters used in plasma physics.

  • Created the utils subpackage.

    • Created plasmapy.utils.check_quantity and plasmapy.utils.check_relativistic decorators.

    • Created custom exceptions.

    • Added import helper and test helper functionality.

  • Began development of the diagnostics subpackage.

    • Created a module to interpret Langmuir probe data.

  • Created a repository for PlasmaPy Enhancement Proposals.

  • Began using type hint annotations.

  • Set up architecture to incorporate Cython into performance-critical sections of code.

  • Incorporated import and setup tools from the astropy_helpers package.

  • Set up a page describing the Stability of Subpackages.

Changes to API
  • PlasmaPy now has an API.

Bug Fixes
  • Fixed bug in universe that cause solar neutrinos to oscillate between different flavors.

Authors and Credits

PlasmaPy Coordinating Committee

PlasmaPy Contributors

The people in the following list have contributed to PlasmaPy. Included in parentheses are ORCID author identifiers.

This list contains contributors to PlasmaPy’s core package and vision statement, including a few people who do not show up as PlasmaPy contributors on GitHub. If you made a contribution to PlasmaPy that was merged and your name is missing from the list, your information is incorrect, or you do not wish to be listed, then please submit a pull request.

Other Credits

The PlasmaPy Community thanks the SunPy and Astropy communities for inspiring this project in the first place, providing much helpful advice, and showing examples of how to build a community-wide open source scientific software package. The PlasmaPy Community also thanks the Python in Heliophysics Community.

Acknowledgements

Early development on PlasmaPy was partially supported by the U.S. Department of Energy through grant DE-SC0016363 that was funded through the NSF-DOE Partnership on Basic Plasma Science and Engineering; a Scholarly Studies grant awarded by the Smithsonian Institution; Google Summer of Code; and NASA Heliophysics Data Environment Enhancements (HDEE) grant 80NSSC20K0174. PlasmaPy is being developed with support from the U.S. National Science Foundation through grants 1931388, 1931393, 1931429, and 1931435 that were awarded through a collaborative proposal submitted to the Cyberinfrastructure for Sustained Scientific Innovation (CSSI) program.

All opinions, findings, conclusions, and recommendations expressed in this material are those of the authors and do not necessarily reflect the views of any of the funding agencies or organizations that have supported PlasmaPy development.

Bibliography

[1]

H. Alfvén. Existence of Electromagnetic-Hydrodynamic Waves. Nature, 150(3805):405–406, 1942. doi:10.1038/150405d0.

[2]

W. Baumjohann and R. A. Treumann. Basic Space Plasma Physics. Imperial College Press, 1997.

[3]

G. Bekefi. Radiation Processes in Plasmas. Wiley, 1966. ISBN 9780471063506.

[4]

P. M. Bellan. Improved basis set for low frequency plasma waves. Journal of Geophysical Research: Space Physics, 2012. doi:10.1029/2012ja017856.

[5]

D. S. Bernstein. Beyond Legacy Code: Nine Practices to Extend the Life (and Value) of Your Software. Pragmatic Bookshelf, 1st edition, 2015. ISBN 9781680500790. URL: https://pragprog.com/titles/dblegacy/beyond-legacy-code.

[6]

C. K. Birdsall and A. B. Langdon. Plasma Physics via Computer Simulation. CRC Press, 2004. doi:10.1201/9781315275048.

[7]

D. Bohm. The Characteristics of Electrical Discharges in Magnetic Fields. McGraw-Hill, 1949.

[8]

M. Bonitz. Quantum Kinetic Theory. Springer, 1998. doi:10.1007/978-3-319-24121-0.

[9]

J. P. Boris. Relativistic plasma simulation—Optimization of a hybrid code. In J. P. Boris and R. A. Shanny, editors, Proceedings of Fourth Conference on Numerical Simulation of Plasmas, 3–67. Naval Research Laboratory, 1970. URL: https://apps.dtic.mil/sti/citations/ADA023511.

[10]

S. I. Braginskii. Transport Processes in a Plasma. Reviews of Plasma Physics, 1:205, 1965.

[11]

S. J. Buchsbaum. Resonance in a Plasma with Two Ion Species. The Physics of Fluids, 3(3):418–420, 1960. doi:10.1063/1.1706052.

[12]

J. Callen. Draft Material For "Fundamentals of Plasma Physics" Book. Unpublished. URL: https://docs.google.com/document/d/e/2PACX-1vQmvQ_b8p0P2cYsWGMQYVd92OBLX9Sm6XGiCMRBidoVSoJffj2MBvWiwpix46mqlq_HQvHD5ofpfrNF/pub.

[13]

F. Chen. Introduction to Plasma Physics and Controlled Fusion. Springer, 3rd edition, 2016. doi:10.1007/978-3-319-22309-4.

[14]

E. M. Epperlein and M. G. Haynes. Plasma transport coefficients in a magnetic field by direct numerical solution of the Fokker–Planck equation. Physics of Fluids, 29:1029, 1986. doi:10.1063/1.865901.

[15]

B. D. Fried and S. D. Conte. The Plasma Dispersion Function: The Hilbert Transformation of the Gaussian. Academic Press, 1961. doi:10.1016/c2013-0-12176-9.

[16]

D. H. Froula, S. H. Glenzer, N. C. Luhmann, and J. Sheffield. Plasma Scattering of Electromagnetic Radiation. Academic Press, 2nd edition, 2011. ISBN 978-0-12-374877-5. doi:10.1016/c2009-0-20048-1.

[17]

W. Fundamenski and O. E. Garcia. Comparison of Coulomb Collision Rates in the Plasma Physics and Magnetically Confined Fusion Literature. Technical Report EFDA–JET–R(07)01, EFDA-JET, 2007. URL: https://scipub.euro-fusion.org/archives/jet-archive/comparison-of-coulomb-collision-rates-in-the-plasma-physics-and-magnetically-confined-fusion-literature.

[18]

D. O. Gericke, M. S. Murillo, and M. Schlanges. Dense plasma temperature equilibration in the binary collision approximation. Physical Review E, 65(3):036418, 2002. doi:10.1103/PhysRevE.65.036418.

[19]

A. Hasegawa and C. Uberoi. Alfven wave. DOE Critical Review Series. U.S. Department of Energy Office of Scientific and Technical Information, 1982. doi:10.2172/5259641.

[20]

A. Haynes and C. Parnell. A trilinear method for finding null points in a three-dimensional vector space. Physics of Plasmas, 14:082107, 08 2007. doi:10.1063/1.2756751.

[21]

P. Hellinger, L. Matteini, Š. Štverák, P. M. Trávníček, and E. Marsch. Heating and cooling of protons in the fast solar wind between 0.3 and 1 AU: Helios revisited. Journal of Geophysical Research: Space Physics, 2011. doi:10.1029/2011ja016674.

[22]

A. Hirose, A. Ito, S. M. Mahajan, and S. Ohsaki. Relation between Hall-magnetohydrodynamics and the kinetic Alfvén wave. Physics Letters A, 330(6):474–480, 2004. doi:10.1016/j.physleta.2004.08.021.

[23]

J. V. Hollweg. Kinetic Alfvén wave revisited. Journal of Geophysical Research, 1999. doi:10.1029/1998ja900132.

[24]

J.-Y. Ji and E. D. Held. Closure and transport theory for high-collisionality electron-ion plasmas. Physics of Plasmas, 2013. doi:10.1063/1.4801022.

[25]

E. Johnson, B. A. Maruca, M. McManus, K. G. Klein, E. R. Lichko, J. Verniero, K. W. Paulson, H. DeWeese, I. Dieguez, R. A. Qudsi, J. Kasper, M. Stevens, B. L. Alterman, L. B. Wilson III, R. Livi, A. Rahmati, and D. Larson. Anterograde collisional analysis of solar wind ions. The Astrophysical Journal, 950(1):51, jun 2023. doi:10.3847/1538-4357/accc32.

[26]

V. Khorikov. Unit Testing Principles, Practices, and Patterns. Manning Press, 1st edition, 2020. URL: https://www.manning.com/books/unit-testing.

[27]

S. Lundquist. Magneto-hydrostatic fields. Arkiv. fysik, 2(35):361, 1950.

[28]

R. L. Lysak and W. Lotko. On the kinetic dispersion relation for shear Alfvén waves. Journal of Geophysical Research: Space Physics, 101(A3):5085–5094, 1996. doi:10.1029/95ja03712.

[29]

B. A. Maruca, S. D. Bale, L. Sorriso-Valvo, J. C. Kasper, and M. L. Stevens. Collisional Thermalization of Hydrogen and Helium in Solar-Wind Plasma. Physical Review Letters, 2013. doi:10.1103/physrevlett.111.241101.

[30]

G. J. Morales and J. E. Maggs. Structure of kinetic Alfvén waves with small transverse scale length. Physics of Plasmas, 4:4118–4125, 1997.

[31]

R. A. B. Nilsen. Conditional averaging of overlapping pulses. Master thesis, UiT The Arctic University of Norway, 2023. URL: https://hdl.handle.net/10037/29416.

[32]

R. Osherove. The Art of Unit Testing: With Examples in .NET. Manning Press, 2nd edition, 2013. ISBN 9781617290893. URL: https://www.manning.com/books/the-art-of-unit-testing-second-edition.

[33]

E. R. Priest and T. Forbes. Magnetic Reconnection: MHD Theory and Applications. Cambridge University Press, 2000. ISBN 978-0-521-03394-7.

[34]

A. S. Richardson. NRL Plasma Formulary. Technical Report, Naval Research Laboratory, 2019. URL: https://www.nrl.navy.mil/News-Media/Publications/nrl-plasma-formulary.

[35]

D. Schaeffer. Generation of Quasi-Perpendicular Collisionless Shocks by a Laser-Driven Magnetic Piston. PhD thesis, University of California, Los Angeles, December 2014. doi:10.5281/zenodo.3766933.

[36]

J. Sheffield, D. Froula, S. H. Glenzer, and N. C. Luhmann, Jr. Plasma Scattering of Electromagnetic Radiation: Theory and Measurement Techniques. Academic Press, 2nd edition, 2011. ISBN 978-0-12-374877-5.

[37]

L. Spitzer. Physics of Fully Ionized Gases. Interscience, 2nd edition, 1962.

[38]

L. Spitzer and R. Härm. Transport phenomena in a Completely Ionized Gas. Physical Review, 89:977–981, 1953. doi:10.1103/PhysRev.89.977.

[39]

T. H. Stix. Waves in Plasmas. AIP-Press, 1992. URL: https://link.springer.com/book/9780883188590.

[40]

T. E. Stringer. Low-frequency waves in an unbounded plasma. Journal of Nuclear Energy. Part C, Plasma Physics, Accelerators, Thermonuclear Research, 5(2):89–107, 1963. doi:10.1088/0368-3281/5/2/304.

[41]

P. Thompson, M. K. Dougherty, and D. J. Southwood. Wave behaviour near critical frequencies in cold bi-ion plasmas. Planetary and Space Science, 43(5):625–634, 1995. doi:10.1016/0032-0633(94)00197-Y.

[42]

D. Verscharen, K. G. Klein, and B. A. Maruca. The multi-scale nature of the solar wind. Living Reviews in Solar Physics, 2019. doi:10.1007/s41116-019-0021-0.

[43]

S. T. Vincena, W. A. Farmer, J. E. Maggs, and G. J. Morales. Investigation of an ion-ion hybrid Alfvén wave resonator. Physics of Plasmas, 20(1):012111, 2013. doi:10.1063/1.4775777.

[44]

G. Wilson, D. A. Aruliah, C. T. Brown, N. P. Chue Hong, M. Davis, R. T. Guy, S. H. D. Haddock, K. D. Huff, I. M. Mitchell, M. D. Plumbley, B. Waugh, E. P. White, and P. Wilson. Best practices for scientific computing. PLoS Biology, 12(1):e1001745, 2014. doi:10.1371/journal.pbio.1001745.

[45]

G. Wilson, J. Bryan, K. Cranston, J. Kitzes, L. Nederbragt, and T. K. Teal. Good enough practices in scientific computing. PLOS Computational Biology, 13(6):e1005510, 2017. doi:10.1371/journal.pcbi.1005510.

Glossary

-like

Used to indicate an object of that type or that can instantiate that type. For example, "He 2+" is particle-like because it can be used to instantiate Particle.

alias
aliases

An abbreviated version of a commonly used function. For example, va_ is an alias for Alfven_speed. Aliases are named with a trailing underscore.

For further details, please refer to the contributor guide’s section on aliases.

args

An abbreviation for positional arguments.

atom-like

A particle-like object is atom-like if it is or could be cast into:

  • A Particle representing an element, isotope, or ionic level; or

  • A ParticleList including only elements, isotopes, or ionic levels.

For example, "p+", "He-4", "deuterium", "O 0+", Particle("Fe-56 16+)", ["He-4 1+", "He-4 2+"], and integers representing atomic numbers are all atom-like.

Examples of objects that are particle-like but not atom-like include "neutron", "e-", and ["e-", "e+"]. Additionally, ["He-4", "e-"] is not atom-like because this list contains an item that is not atom-like.

Please refer to the glossary entry for particle-like for a full description of valid representations of elements, isotopes, and ions.

charge number

The electrical charge of a particle in units of the elementary charge. The charge number of an ion or neutral particle is usually denoted as Z.

fit-function
fit-functions

Any instance of a subclass of AbstractFitFunction. Also see module fit_functions.

integration test

An integration test verifies that multiple software components work together as intended.

Compared to a unit test, an integration test is typically harder to write, slower to run, more difficult to maintain, and less useful at pinpointing the specific cause of a problem. However, integration tests are able to find problems that unit tests cannot. In particular, integration tests are able to find problems at the interfaces between different modules. On average, each integration test covers more lines of code than each related unit test. Because unit tests and integration tests complement each other, both are important constituents of a test suite.

keyword-only

An argument or parameter is keyword-only when the argument must be provided with the name of the corresponding parameter.

If z is a keyword-only parameter to f(z), then the argument 2 can be provided as f(z=2) but not f(2).

kwargs

An abbreviation for keyword arguments.

lite-function
lite-functions

An optimized version of an existing plasmapy function intended for applications where computational efficiency is most important. While most formulary functions accept Quantity objects created using astropy.units, lite-functions accept numbers and array_like inputs that are implicitly assumed to be in SI units. The name of a lite-function ends with _lite. A lite-function can be accessed as the lite attribute of the corresponding regular function.

Caution

Unlike most formulary functions, no validations are performed on the arguments provided to a lite-function for the sake of computational efficiency. When using lite-functions, it is vital to double-check your implementation!

For further details, please refer to the contributor guide’s section on lite-functions.

particle-like

An object is particle-like if it is a Particle or CustomParticle, or can be cast into one.

An element may be represented by a string containing the atomic symbol (case-sensitive), the name of the element, or an integer representing the atomic number. The element iron can be represented as "Fe", "iron", or 26.

An isotope may be represented by a string that contains an atomic symbol or element name, followed by a hyphen and the mass number (with no spaces in between). The isotope 56Fe can be represented as "Fe-56", or "iron-56". 2H can be represented by "D" or "deuterium", and 3H can be represented by "T" or "tritium".

An ion or neutral atom may be represented by a string that contains a representation of an element or isotope, followed by charge information which is typically an integer representing the charge number and a plus or minus sign to indicate the electrical charge. For example, a deuteron may be represented as "D 1+" and 56Fe1+ may be represented as "Fe-56 1+".

A special particle may be represented by a string that contains the name of the particle (case insensitive) or a standard symbol for it (case insensitive). A neutron can be represented as "n" or "neutron"; a proton can be represented as "p+", "p", or "proton"; and an electron can be represented by "e-", "e", or "electron".

DimensionlessParticle instances are not particle-like because, without normalization information, they do not uniquely identify a physical particle.

For more complete details, refer to ParticleLike.

particle-list-like

An object is particle-list-like if it is a ParticleList, or can be cast into one.

For more complete details, refer to ParticleListLike.

real number

Any numeric type that represents a real number. This could include a float, int, a dimensionless Quantity, or any of the numpy.number types. Note that if a PlasmaPy function expects a dimensional Quantity and a real number is provided, then the real number is often assumed to have the appropriate SI units.

temperature

Most functions in PlasmaPy accept temperature, \(T\), as a Quantity with units of temperature (e.g., kelvin) or energy (e.g., electron-volts). A value for energy that is provided will be divided by the Boltzmann constant, \(k_B\), to be converted into units of temperature.

unit test

A unit test verifies a single unit of behavior, does it quickly, and does it in isolation from other tests [Khorikov, 2020].

Unit tests are intended to provide fast feedback that help pinpoint the locations of errors. Unit tests often abide by the following pattern [Osherove, 2013]:

  1. Arrange: gather inputs and get the system to the state in which the test is expected to run.

  2. Act: make the system under test undertake the operation that is being tested.

  3. Assert: verify that the actual outcome of the act phase matches the expected outcome.

In a unit test for a function, the arrange phase involves collecting or constructing the inputs for the function. The act phase occurs when the function is called with those inputs. The assert phase is when the value returned by the function is compared to the expected result.

Performance Tips

Most of the time, readability is more important than the performance of scientific software. This page contains tips for improving the performance of PlasmaPy for situations where performance becomes a bottleneck.

Python versions

Upgrade to the newest version of Python to take advantage of ongoing performance improvements from the Faster CPython project. New versions of Python also have improved error message, which can speed up the debugging process too.

A new version of Python is released in October of each year, and can be used with PlasmaPy a few months later.

Using Astropy units

PlasmaPy makes heavy use of astropy.units and Quantity operations. See Astropy’s documentation for performance tips for Quantity operations.

Because PlasmaPy uses SI units internally, performance can be improved slightly by providing Quantity objects in SI units to functions in plasmapy.formulary. Unit conversions done by the validate_quantities() decorator then do not need to be performed.

Particles

Many of the functions in plasmapy.formulary accept particle-like arguments. Arguments that are not already a Particle, CustomParticle, or ParticleList are converted into one (usually via the particle_input() decorator. When a formulary function is repeatedly called, performance can be improved by creating the particle object ahead of time.

For example, suppose we are calculating the gyrofrequency of a proton. If we represent the particle as a string, then the function will need to create a Particle each time the function is called.

from plasmapy.formulary import gyrofrequency
import astropy.units as u

for i in range(1000):
    gyrofrequency(B=0.02 * u.T, particle="p+")  # create the Particle repeatedly

If we create the Particle and Quantity ahead of time, they will need to be create only once instead of repeatedly.

from plasmapy.particles import Particle

B = 0.02 * u.T
proton = Particle("p+")  # create the Particle once

for i in range(1000):
    gyrofrequency(B=B, particle=proton)

Lite-functions

PlasmaPy includes lite-functions for some plasmapy.formulary functions for situations when performance matters. For example, plasma_frequency_lite is the lite-function for plasma_frequency.

Lite-functions accept and return NumPy arrays (assumed to be in SI units) instead of Quantity objects. Lite-functions make use of just-in-time (JIT) compilation via Numba to achieve high performance. Because lite-functions do not include any validation of inputs, they should only be used for performance-critical applications.

If you need a lite-function version of a plasmapy.formulary function that has not already been implemented, please raise an issue.

Contributor Guide

Getting Ready to Contribute

Introduction

Thank you for considering contributing to PlasmaPy — we really appreciate it! This page goes through the steps that first-time contributors can take to get set up to contribute to PlasmaPy. After taking these steps, you’ll be ready to go through the code contribution workflow.

If you run into any problems, please feel free to reach out to us in our Matrix chat room or during our weekly office hours.

Pre-requisites
Opening a terminal

The commands in this page are intended to be run in a Unix terminal. If you are new to Unix, check out this Unix tutorial and these frequently used Unix commands.

These tabs describe how to open and use a terminal on different operating systems.

There are several options for terminals on Windows.

  • Powershell comes pre-installed with Windows. These instructions cover opening Powershell. We recommend Powershell for a quick start, if Windows is the only operating system you use, or if you have not used Unix before.

  • We recommend Windows Subsystem for Linux (WSL) if you are familiar with Unix, you use macOS or Linux too, or you expect to contribute to PlasmaPy extensively. These instructions cover installing WSL. If you choose WSL, follow the tabs for Linux/WSL below.

Installing Python

Note

PlasmaPy requires a version of Python between 3.10 and 3.12. We recommend using Python 3.12.

We suggest using Anaconda to install Python. Anaconda is a versatile package and environment management system which is widely used in the data science and scientific Python communities. Anaconda includes Anaconda Navigator as its graphical user interface (GUI) and Conda as its command line interface (CLI).

Note

There are many other equally good ways to install Python. Python’s website describes how to download Python. Real Python has instructions on installing Python for several different operating systems (if working within WSL, follow the Linux instructions).

Using git and GitHub

Code contributions to PlasmaPy are made using git and GitHub. Before contributing code to PlasmaPy, please take the following steps:

  1. Sign up on GitHub for a free account.

  2. Verify that git is installed by opening a terminal and running:

    git --version
    

    If there is an error, follow these instructions to install git.

  3. Optionally, configure git with your name with a command like:

    git config --global user.name "Your Name"
    

    You can also configure git with your email with a command like:

    git config --global user.email "your.email@example.com"
    

    You may also set your default editor with a command like the following, where notepad can be replaced with the name or path of your preferred editor:

    git config --global core.editor notepad
    

    For different editor and configuration options, check out git commands for setup and config.

  4. Add a new SSH key to your GitHub account. This step is needed for authentication purposes.

Initial setup
  1. Log in to GitHub.

  2. Go to PlasmaPy’s GitHub repository.

  3. Create a fork of PlasmaPy by clicking on Fork, followed by Create fork.

  4. Open a terminal. Then create and/or navigate to the folder in which you want to download PlasmaPy. For example, to put PlasmaPy into a new directory called repos/ in your home directory (denoted by ~), run:

    mkdir ~/repos
    cd ~/repos
    
  5. Clone the PlasmaPy repository with the following command, replacing YOUR-USERNAME with your GitHub username. This will create a subdirectory called PlasmaPy/ containing your local clone of the repository.

    git clone git@github.com:YOUR-USERNAME/PlasmaPy.git
    

    Tip

    If you have trouble connecting to GitHub, you may need to add a new SSH key to your GitHub account.

  6. Enter the newly created directory with:

    cd PlasmaPy
    
  7. Add a remote called upstream for PlasmaPy’s GitHub repository by using the following command.

    git remote add upstream git@github.com:PlasmaPy/PlasmaPy.git
    

    If you run git remote -v, you should see that origin corresponds to your fork and upstream corresponds to PlasmaPy’s GitHub repository.

Setting up a Python environment

If you plan to make multiple contributions, we recommend setting up a Python environment specifically for PlasmaPy. This section describes how to set up a Conda environment from the command line, which can be done after installing Conda or Anaconda Navigator as described in the section on getting Python. If you did not use Conda or Anaconda to install Python, we suggest using a virtual environment instead.

Tip

Using Conda/virtual environments helps avoid situations as in this xkcd comic.

  1. Open a terminal.

  2. Create a Conda environment named plasmapy-dev by running:

    conda create -n plasmapy-dev python=3.12
    

    The -n flag is used to specify the name of the environment. The 3.12 can be replaced with any version of Python from 3.10 to 3.12.

  3. Activate the environment with:

    conda activate plasmapy-dev
    

    The conda activate command will need to be run every time you open a terminal, or can be added to the appropriate configuration file (i.e., .bashrc for bash or .zshrc for zsh).

Installing your clone of PlasmaPy

This section covers how to make an editable installation of your clone of PlasmaPy. Making the PlasmaPy installation editable means that if you modify the source code, then those changes will be included when you import plasmapy.

  1. Open a terminal.

  2. Navigate to the directory for your clone of PlasmaPy, which should be named PlasmaPy. For example, if you ran the git clone command in the ~/repos/ directory, then run:

    cd ~/repos/PlasmaPy
    

    Note

    In Windows, the directory path will be C:\Users\<username>\repos\PlasmaPy.

  3. If you created a Conda environment for contributing to PlasmaPy, activate it with:

    conda activate plasmapy-dev
    
  4. Run the command to install PlasmaPy for your operating system:

    py -m pip install -e .[docs,tests]
    

    Note

    Replace py with python if you are not using conda.

    The -e specifies that this will be an editable installation.

    Tip

    If the above command does not work, try running

    pip install -r requirements.txt
    

    This command will install that packages that PlasmaPy depends on, but not PlasmaPy itself.

Hint

If you import a package after doing an editable installation, then changes made after the import step will not be immediately available during a Python session. To re-import the package, use importlib.reload:

>>> from importlib import reload
>>> import plasmapy
>>> # now change the source code
>>> reload(plasmapy)

Code Contribution Workflow

Introduction

This page describes the workflow for making a contribution to PlasmaPy via a pull request after having finished the steps for Getting Ready to Contribute.

If you run into any problems, please feel free to reach out to us in our Matrix chat room or during our weekly office hours. Thank you for contributing!

Tip

Issues labeled as a good first issue are a great place to get started with contributing.

Making a code contribution
Create a new branch
  1. Open a terminal.

  2. Navigate to the PlasmaPy/ directory that contains the clone of your repository.

  3. In the terminal, run:

    git status
    

    If the output ends with nothing to commit, working tree clean, then proceed to the next step.

    Tip

    If git status shows that any files are listed under Changes not staged for commit or Changes to be committed, then do one of the following before proceeding to the next step:

    1. Add and commit changes,

    2. Use git stash to temporarily file away the changes, or

    3. Use git reset --hard to permanently remove all changes to tracked files and return to the previous commit.

    If there are untracked files present, then you may delete the untracked files, add and commit changes, or proceed to the next step.

  4. Download the current status of PlasmaPy’s GitHub repository and your fork by running:

    git fetch --all
    
  5. Create and switch to a new branch by running:

    git checkout -b new-branch-name upstream/main
    

    where new-branch-name is changed to the name of the new branch. Here upstream is the name of the remote and main is the name of the original branch which the new branch will be based off of.

    Tip

    Use descriptive branch names like update-contribution-workflow.

  6. Connect your local branch to your fork of PlasmaPy on GitHub by running:

    git push --set-upstream origin new-branch-name
    
Add and commit changes

Next we can go through the cycle of making changes, which is usually repeated multiple times. To get a better idea of what is being done in each step, try running git status.

  1. Edit a file and save the changes.

  2. In a terminal, navigate to the directory with the changed file and run:

    git add filename
    

    where filename is replaced with the name of the edited file(s). Use git add * to add all files in the directory (except for files specified in .gitignore. This step lets us line up the changes that we want to record as a snapshot in history.

  3. To commit the changes, run:

    git commit -m "<commit message>"
    

    where <commit message> is replaced with a descriptive commit message such as "Add gyroradius function". Committing a change is like preserving a snapshot of what each file looks like at this point in history.

    Hint

    If it has been installed, pre-commit will perform automated checks and possibly auto-fixes. If pre-commit fails, then it’ll be necessary to fix any remaining problems and do the git add and git commit steps once more. Try using git diff and git diff --cached to view the changes, and and to scroll through previous commands in a terminal.

  4. To push the changes to GitHub, run:

    git push
    

Tip

Try using the git status command after each step to get a better idea of what is happening.

Note

The git workflow can be thought of as the process of mailing a package.

  • git add is like packing the contents of a package into a box. This step allows you to choose which changes to include in the next commit.

  • git commit is like sealing and labeling the package, and putting it in the outgoing mail.

  • git push is like sending the package off to its destination (i.e., GitHub).

Creating a pull request
  1. Run git push to make sure that branch on GitHub is up-to-date.

  2. Go to PlasmaPy’s GitHub repository.

  3. If you recently pushed new changes, a pale yellow box will appear near the top of the screen. In that box, click Compare & pull request.

    Note

    If you did not recently push any new changes, click on New pull request and then the link saying “compare across forks.” Select PlasmaPy/PlasmaPy for “base repository” and main for “base”. Choose your fork of PlasmaPy for “head repository” and the name of the branch for “compare”. Then click on Create pull request.

  4. Add a descriptive title, such as Add a function to calculate particle gyroradii.

  5. Write a description for the pull request (PR). Describe the changes, and why they are being made. Include information that you think would be helpful for reviewers, future users, and future contributors..

    Tip

    If your pull request will resolve an issue, include Closes #ISSUE-NUMBER in the pull request description, where ISSUE-NUMBER is replaced with the number of the issue.

  6. Select Create pull request.

    Tip

    If the pull request isn’t ready for review, select the next to Create pull request to enable you to create a draft pull request instead.

  7. Add a changelog entry, except for minor changes like typo fixes.

At this stage, a reviewer will perform a code review, unless it has been marked as a draft pull request. Thank you for contributing!

Pulling changes from GitHub

If your branch changes on GitHub, run

git pull

to pull the changes from GitHub to your computer. If you’d like to pull the changes from the main branch, instead run

git pull upstream main

If any of the changes conflict with each other, it will be necessary to resolve the merge conflict.

Note

After the pull request has been created, it can be updated by using git push to update the corresponding branch on GitHub.

Important

If this is your first contribution, please add yourself to the author list in CITATION.cff (which uses Citation File Format) to make sure that you get credit for your contribution. The entry should be of the form:

- given-names: <given names>
  family-names: <family names>
  affiliation: <affiliation>
  orcid: https://orcid.org/<ORCiD-iD>
  alias: <GitHub username>

All fields are optional except alias, which is your GitHub username. We encourage contributors to sign up for an ORCID iD: a unique, persistent identifier used by researchers, authors, and open source contributors.

Using pre-commit

Introduction

PlasmaPy uses pre-commit to automate code quality checks and perform automated fixes. The configuration for pre-commit is in .pre-commit-config.yaml.

Troubleshooting pre-commit failures

Many common pre-commit test failures related to formatting can be automatically fixed by adding a comment on a pull request that says pre-commit.ci autofix (like in this comment). This comment will lead to a new commit to the pull request branch that applies the automatic fixes made by the different pre-commit hooks.

After doing this, please do a git pull in your clone of PlasmaPy’s repository to pull back the auto fixes to your computer.

The following sections contain suggestions for how to fix pre-commit failures that were not corrected by commenting pre-commit.ci autofix on the issue.

Tip

Make sure all other tests are passing before manually fixing pre-commit test failures. Failures from pre-commit should generally be fixed after making sure all other tests are passing.

ruff

PlasmaPy uses ruff as its primary linter and code quality tool. ruff can quickly find code quality issues and is able to do many code quality fixes.

Every issue detected by ruff corresponds to a specific lint rule. For example, lint rule F401 removes unused import statements. If you encounter a confusing ruff rule, search ruff’s documentation page on rules for the rule code and click on its name for more information.

Problems flagged by C901 occur when a function is too complex (i.e., when it contains heavily nested control flow), which makes code much more difficult to maintain.

Tip

Reduce complexity by breaking up complicated functions into short functions that do exactly one thing with no side effects.

Disabling a ruff rule

While ruff usually suggests improvements, there will occasionally be times where a departure from a ruff rule is (at least temporarily) justified. In these cases, we can append a # noqa <rule-codes> comment to the end of a line (where <rule-codes> is replaced with the corresponding ruff rule codes, and noqa stands for “no quality assurance”) to tell ruff to ignore that error on that line.

For example, we can tell ruff to ignore a function with excessive code complexity (C901), too many branches (PLR0912), and too many statements (PLR0915) by adding the following noqa comment:

def overly_complicated_function():  # noqa: C901, PLR0912, PLR0915
    """A function with 100+ lines of code and lots of if/else branches."""

Important

When writing new code, it is almost always better to refactor the code to remove the error rather than add a noqa comment. In the above example, it would be better to refactor an overly complicated function into multiple short functions that do exactly one thing with no side effects so that the code is easier to understand, modify, and maintain. We should only add noqa statements when we have a good reason to.

codespell

PlasmaPy uses codespell to find typos in source code. Rather than checking if each word matches a dictionary entry, codespell tries to match words to a set of common misspellings. This approach greatly reduces the number of false positives, but will occasionally miss some uncommon misspellings.

If you encounter a false positive with codespell, add it to ignore-words-list under [codespell] in pyproject.toml.

Using pre-commit locally

pre-commit checks are performed on GitHub for every pull request, but it is also possible to set up pre-commit locally.

Tip

We recommend enabling pre-commit for the clone of PlasmaPy’s GitHub repository only after you have become comfortable with the code contribution workflow.

Enabling pre-commit

To enable pre-commit on your computer:

  1. Open a terminal.

  2. If you use a Conda or virtual environment for developing PlasmaPy, activate it (i.e., with conda activate plasmapy-dev).

  3. Make sure that pre-commit is installed to your Python environment by running:

    py -m pip install pre-commit
    
  4. Navigate to the PlasmaPy/ directory that contains your clone of PlasmaPy’s repository. For example, if you cloned PlasmaPy into the ~/repos/ directory, then run:

    cd ~/repos/PlasmaPy
    
  5. Enable pre-commit with:

    pre-commit install
    
Changes to the workflow

Once pre-commit has been installed for a repository, pre-commit will run every time you try to commit a change.

If any pre-commit checks fail, or if pre-commit changes any files, it will be necessary to redo git add on the changed files and git commit once again.

Tip

To commit a change without running pre-commit, use the -n flag (short for --no-verify) with git.

Tip

To run pre-commit on all files, use

pre-commit run --all-files

Coding Guide

Introduction

This guide describes common conventions, guidelines, and strategies for contributing code to PlasmaPy. The purpose of this guide is not to provide a set of rigid guidelines that must be adhered to, but rather to provide a common framework that helps us develop PlasmaPy together as a community.

Having a shared coding style makes it easier to understand code written by multiple contributors. The particulars of the coding style are not as important as consistency, readability, and maintainability.

This guide can (and should!) be regularly refined by the PlasmaPy community as we collectively learn new practices and our shared coding style changes. Please feel free to propose revisions to this guide by submitting a pull request or by bringing up an idea at a community meeting.

PlasmaPy generally follows the PEP 8 style guide for Python code, using auto-formatters such as black and isort that are executed using pre-commit.

Coding guidelines
  • Write short functions that do exactly one thing with no side effects.

  • Use NumPy array options instead of for loops to make code more compact, readable, and performant.

  • Instead of defining variables like a0, a1, & a2, define these values in a collection such as an ndarray or a list.

  • Use the property decorator instead of getters and setters.

  • Some plasma parameters depend on more than one Quantity of the same physical type. For example, when reading the following line of code, we cannot immediately tell which is the electron temperature and which is the ion temperature.

    f(1e6 * u.K, 2e6 * u.K)
    

    Spell out the parameter names to improve readability and reduce the likelihood of errors.

    f(T_i=1e6 * u.K, T_e=2e6 * u.K)
    

    Similarly, when a function has parameters named T_e and T_i, these parameters should be made keyword-only to avoid ambiguity and reduce the chance of errors.

    def f(*, T_i, T_e):
        ...
    
  • The __eq__ and __ne__ methods of a class should not raise exceptions. If the comparison for equality is being made between objects of different types, these methods should return False instead. This behavior is for consistency with operations like 1 == "1" which will return False.

  • Limit usage of lambda functions to one-liners, such as when defining the default factory of a defaultdict). For anything longer than one line, use def instead.

  • List and dictionary comprehensions can be used for simple for loops, like:

    >>> [x**2 for x in range(17) if x % 2 == 0]
    [0, 4, 16, 36, 64, 100, 144, 196, 256]
    
  • Avoid putting any significant implementation code in __init__.py files. Implementation details should be contained in a different file, and then imported into __init__.py.

  • Avoid defining global variables when possible.

  • Use assert statements only in tests.

  • Use formatted string literals (f-strings) instead of legacy formatting for strings.

    >>> package_name = "PlasmaPy"
    >>> print(f"The name of the package is {package_name}.")
    The name of the package is PlasmaPy.
    >>> print(f"{package_name=}")
    package_name='PlasmaPy'
    >>> print(f"{package_name!r}")  # shortcut for f"{repr(package_name)}"
    'PlasmaPy'
    
  • Functions that accept array_like or Quantity inputs should accept and return nan (not a number) values. This guideline applies when nan is the input as well as when nan values are included in an array.

    Tip

    Normally, numpy.nan == numpy.nan evaluates to False, which complicates testing nan behavior. The equal_nan keyword of functions like numpy.allclose and numpy.testing.assert_allclose makes it so that nan is considered equal to itself.

  • Do not use mutable objects as default values in the function or method declaration. This can lead to unexpected behavior.

    >>> def function(l=[]):
    ...     l.append("x")
    ...     print(l)
    ...
    >>> function()
    ['x']
    >>> function()
    ['x', 'x']
    
  • Use pathlib when working with paths to data files.

Names

Names are our most fundamental means of communicating the intent and purpose of code. Wisely chosen names can greatly improve the understandability of code, while inadequate names can obfuscate what the code is supposed to be doing.

  • PlasmaPy generally uses the PEP 8 conventions for variable names.

    • Use lowercase words separated by underscores for function and variable names (e.g., function_name and variable_name).

    • Use capitalized words without separators when naming a class (e.g., ClassName), but keep acronyms capitalized (e.g., MHDEquations).

    • Use capital letters words separated by underscores when naming constants (e.g., CONSTANT or CONSTANT_NAME).

    There are some situations in PlasmaPy which justify a departure from the PEP 8 conventions.

    • Functions based on plasma parameters that are named after people may be capitalized (e.g., Alfven_speed).

    • Capital letters may be used for a variable when it matches the standard usage in plasma science (e.g., B for magnetic field and T for temperature).

  • Choose names that are pronounceable to make them more memorable and compatible with text-to-speech technology.

  • Choose names will produce more relevant results when searching the internet.

  • Avoid unnecessary abbreviations, as these make code harder to read. Prefer clarity over brevity, except for code that is used frequently and interactively (e.g., cd or ls).

    Tip

    Measure the length of a variable not by the number of characters, but rather by the time needed to understand its meaning.

    By this measure, cggglm is significantly longer than solve_gauss_markov_linear_model.

  • Avoid ambiguity. Does temp mean “temperature”, “temporary”, or “template”?

  • Append _e to a variable name to indicate that it refers to electrons, _i for ions, and _p for protons (e.g., T_e, T_i, and T_p).

  • Only ASCII characters should be used in code that is part of the public API.

  • Python allows alphanumeric Unicode characters to be used in object names (e.g., πλάσμα or φυσική). These characters may be used for internal code when doing so improves readability (i.e., to match a commonly used symbol) and in Jupyter notebooks.

  • If a plasma parameter has multiple names, then use the name that provides the most physical insight. For example, gyrofrequency indicates gyration but Larmor_frequency does not.

  • It is usually preferable to name a variable after its name rather than its symbol. An object named Debye_length is more broadly understandable and searchable than lambda_D. However, there are some exceptions to this guideline.

    • Symbols used widely across plasma science can be used with low risk of confusion, such as \(T\) for temperature or \(β\) for plasma beta.

    • Symbols that are defined in docstrings can be used with decreased likelihood of confusion.

    • Sometimes code that represents an equation will be more readable if the Unicode characters for the symbols are used, especially for complex equations. For someone who is familiar with the symbols, λ = c / ν will be more readable than lambda = c / nu or wavelength = speed_of_light / frequency.

    • If an implementation is based on a journal article, then variable names may be based on the symbols used in that article. The article should be cited in the appropriate docstring so that it appears in the Bibliography.

  • To mark that an object is not part of PlasmaPy’s public API, begin its name with a leading underscore (e.g., _private_variable). Private variables should not be included in __all__.

  • Avoid single character variable names except for standard plasma physics symbols (e.g., B) or as indices in for loops.

  • Avoid encoding type information in a variable name.

  • Intermediate variable names can provide additional context and meaning. For example, suppose we have a conditional operating on a complicated expression:

    if u[0] < x < u[1] and v[0] < y < v[1] and w[0] < z < w[1]:
        ...
    

    Defining an intermediate variable allows us to communicate the meaning and intent of the expression.

    point_is_in_grid_cell = u[0] < x < u[1] and v[0] < y < v[1] and w[0] < z < w[1]
    
    if point_is_in_grid_cell:
        ...
    

    In for loops, this may take the form of assignment expressions with the walrus operator (:=).

Tip

It is common for an integrated development environment (IDE) to have a built-in tool for simultaneously renaming a variable throughout a project. For example, a rename refactoring in PyCharm can be done with Shift+F6 on Windows or Linux, and ⇧F6 or ⌥⌘R on macOS.

Comments

A well-placed and well-written comment can prevent future frustrations. However, comments are not inherently good. As code evolves, an unmaintained comment may become outdated, or get separated from the section of code that it was meant to describe. Cryptic and obsolete comments may end up confusing contributors. In the worst case, an unmaintained comment may contain inaccurate or misleading information (hence the saying that “a comment is a lie waiting to happen”).

Important

The code we write should read like a book. The full meaning of code’s functionality should be attainable by reading the code. Comments should only be used when the code itself cannot communicate its full meaning.

  • Refactor code to make it more readable, rather than explaining how it works [Wilson et al., 2014].

  • Instead of using a comment to define a variable, rename the variable to encode its meaning and intent. For example, code like:

    # collision frequency
    nu = 1e6 * u.s**-1
    

    could be achieved with no comment by doing:

    collision_frequency = 1e6 * u.s**-1
    
  • Use comments to communicate information that you wish you knew before starting to work on a particular section of code, including information that took some time to learn.

  • Use comments to communicate information that the code cannot, such as why an alternative approach was not taken.

  • Use comments to include references to books or articles that describe the equation, algorithm, or software design pattern that is being implemented. Even better, include these references in docstrings.

  • Provide enough contextual information in the comment for a new user to be able to understand it.

  • Remove commented out code before merging a pull request.

  • When updating code, be sure to review and update, if necessary, associated comments too!

  • When a comment is used as the header for a section of code, consider extracting that section of code into its own function. For example, we might start out with a function that includes multiple lines of code for each step.

    def analyze_experiment(data):
        # Step 1: calibrate the data
        ...
        # Step 2: normalize the data
        ...
    

    We can apply the extract function refactoring pattern by creating a separate function for each of these steps. The name of each function can often be extracted directly from the comment.

    def calibrate_data(data):
        ...
        return calibrated_data
    
    
    def normalize_data(data):
        ...
        return normalized_data
    
    
    def analyze_experiment(data):
        calibrated_data = calibrate_data(data)
        normalized_data = normalize_data(calibrated_data)
    

    This refactoring pattern is appropriate for long functions where the different steps can be cleanly separated from each other. This pattern leads to functions that are shorter, more reusable, and easier to test. The original function contains fewer low-level implementation details and thus gives a higher level view of what the function is doing. This pattern reduces cognitive complexity.

    The extract function refactoring pattern should be used judiciously, as taking it to an extreme and applying it at too fine of a scale can reduce readability and maintainability by producing overly fragmented code.

    Hint

    The extract function refactoring pattern might not be appropriate if the different sections of code are intertwined with each other (e.g., if both sections require the same intermediate variables). An alternative in such cases would be to create a class instead.

Error messages

Error messages are a vital but underappreciated form of documentation. A good error message can help someone pinpoint the source of a problem in seconds, while a cryptic or missing error message can lead to hours of frustration.

  • Use error messages to indicate the source of the problem while providing enough information for the user to troubleshoot it. When possible, make it clear what the user should do next.

  • Include diagnostic information when appropriate. For example, if an error occurred at a single index in an array operation, then including the index where the error happened can help the user better understand the cause of the error.

  • Write error messages that are concise when possible, as users often skim or skip long error messages.

  • Avoid including information that is irrelevant to the source of the problem.

  • Write error messages in language that is plain enough to be understandable to someone who is undertaking their first research project.

    • If necessary, technical information may be placed after a plain language summary statement.

    • Alternatively, an error message may reference a docstring or a page in the narrative documentation.

  • Write error messages that are friendly, supportive, and helpful. Error message should never be condescending or blame the user.

Project infrastructure
Imports
  • Use standard abbreviations for imported packages:

    import astropy.constants as const
    import astropy.units as u
    import matplotlib.pyplot as plt
    import numpy as np
    import pandas as pd
    
  • PlasmaPy uses isort to organize import statements via a pre-commit hook.

  • For infrequently used objects, import the package, subpackage, or module rather than the individual code object. Including more of the namespace provides contextual information that can make code easier to read. For example, json.loads is more readable than using only loads.

  • For frequently used objects (e.g., Particle) and type hint annotations (e.g., Optional and Real), import the object directly instead of importing the package, subpackage, or module. Including more of the namespace would increase clutter and decrease readability without providing commensurately more information.

  • Use absolute imports (e.g., from plasmapy.particles import Particle) rather than relative imports (e.g., from ..particles import Particle).

  • Do not use star imports (e.g., from package.subpackage import *), except in very limited situations.

Requirements
  • Package requirements are specified in pyproject.toml. tox.ini also contains a testing environment for the minimal dependencies.

  • Each release of PlasmaPy should support all minor versions of Python that have been released in the prior 42 months, and all minor versions of NumPy that have been released in the last 24 months. This schedule was proposed in NumPy Enhancement Proposal 29 for the scientific Python ecosystem, and has been adopted by upstream packages such as NumPy, matplotlib, and Astropy.

    Tip

    Tools like pyupgrade help automatically upgrade the code base to the minimum supported version of Python for the next release.

  • PlasmaPy should generally allow all feature releases of required dependencies made in the last ≲ 24 months, unless a more recent release includes a needed feature or bugfix.

  • Only set maximum or exact requirements (e.g., numpy <= 1.22.3 or scipy == 1.7.2) when absolutely necessary. After setting a maximum or exact requirement, create a GitHub issue to remove that requirement.

    Tip

    Maximum requirements can lead to version conflicts when installed alongside other packages. It is preferable to update PlasmaPy to become compatible with the latest versions of its dependencies than to set a maximum requirement.

  • Minor versions of Python are generally released in October of each year. However, it may take a few months before packages like NumPy and Numba become compatible with the newest minor version of Python.

Decorators
Transforming particle-like arguments into particle objects

Use particle_input() to transform arguments to relevant Particle, CustomParticle, or ParticleList objects (see Particles).

Validating Quantity arguments

Use validate_quantities() to enforce Quantity type hints:

@validate_quantities
def magnetic_pressure(B: u.Quantity[u.T]) -> u.Quantity[u.Pa]:
    return B**2 / (2 * const.mu0)

Use validate_quantities() to verify function arguments and impose relevant restrictions:

from plasmapy.utils.decorators.validators import validate_quantities

@validate_quantities(
    n={"can_be_negative": False},
    validations_on_return={"equivalencies": u.dimensionless_angles()},
)
def inertial_length(n: u.Quantity[u.m**-3], particle) -> u.Quantity[u.m]:
    ...
Special function categories
Aliases

An alias is an abbreviated version of a commonly used function. For example, va_ is an alias to Alfven_speed.

Aliases are intended to give users the option for shortening their code while maintaining some readability and explicit meaning. As such, aliases are given to functionality that already has a widely-used symbol in plasma literature.

Here is a minimal example of an alias f_ to function as would be defined in src/plasmapy/subpackage/module.py.

__all__ = ["function"]
__aliases__ = ["f_"]

__all__ += __aliases__


def function():
    ...


f_ = function
"""Alias to `~plasmapy.subpackage.module.function`."""
  • Aliases should only be defined for frequently used plasma parameters which already have a symbol that is widely used in the community’s literature. This is to ensure that the abbreviated function name is still reasonably understandable. For example, cwp_ is a shortcut for \(c/ω_p\).

  • The name of an alias should end with a trailing underscore.

  • An alias should be defined immediately after the original function.

  • Each alias should have a one-line docstring that refers users to the original function.

  • The name of the original function should be included in __all__ near the top of each module, and the name of the alias should be included in __aliases__, which will then get appended to __all__. This is done so both the alias and the original function get properly documented.

  • Aliases are intended for end users, and should not be used in PlasmaPy or other collaborative software development efforts because of reduced readability and searchability for someone new to plasma science.

Lite Functions

Most functions in plasmapy.formulary accept Quantity instances as arguments and use validate_quantities() to verify that Quantity arguments are valid. The use of Quantity operations and validations do not noticeably impact performance during typical interactive use, but the performance penalty can become significant for numerically intensive applications.

A lite-function is an optimized version of another plasmapy function that accepts numbers and NumPy arrays in assumed SI units. Lite-functions skip all validations and instead prioritize performance. Most lite-functions are defined in plasmapy.formulary.

Caution

Unlike most formulary functions, no validations are performed on the arguments provided to a lite-function for the sake of computational efficiency. When using lite-functions, it is vital to double-check your implementation!

Here is a minimal example of a lite-function function_lite that corresponds to function as would be defined in src/plasmapy/subpackage/module.py.

__all__ = ["function"]
__lite_funcs__ = ["function_lite"]

from numbers import Real

from numba import njit
from plasmapy.utils.decorators import bind_lite_func, preserve_signature

__all__ += __lite_funcs__


@preserve_signature
@njit
def function_lite(v: float) -> float:
    """
    The lite-function which accepts and returns real numbers in
    assumed SI units.
    """
    ...


@bind_lite_func(function_lite)
def function(v):
    """A function that accepts and returns Quantity arguments."""
    ...
  • The name of each lite-function should be the name of the original function with _lite appended at the end. For example, thermal_speed_lite is the lite-function associated with thermal_speed.

  • Lite-functions assume SI units for all arguments that represent physical quantities.

  • Lite-functions should be defined immediately before the normal version of the function.

  • Lite-functions should be used by their associate non-lite counterpart, except for well reasoned exceptions. This is done to reduce code duplication.

  • Lite-functions are bound to their normal version as the lite attribute using the bind_lite_func decorator. This allows the lite-function to also be accessed like thermal_speed.lite().

  • If a lite-function is decorated with something like @njit, then it should also be decorated with preserve_signature. This preserves the function signature so interpreters can still give hints about function arguments.

  • When possible, a lite-function should incorporate numba’s just-in-time compilation or utilize Cython. At a minimum any “extra” code beyond the raw calculation should be removed.

  • The name of the original function should be included in __all__ near the top of each module, and the name of the lite-function should be included in __lite_funcs__, which will then get appended to __all__. This is done so both the lite-function and the original function get properly documented.

Physics
Units

PlasmaPy uses astropy.units to assign physical units to values in the form of a Quantity.

>>> import astropy.units as u
>>> 5 * u.m / u.s
<Quantity 5. m / s>

Using astropy.units improves compatibility with Python packages in adjacent fields such as astronomy and heliophysics. To get started with astropy.units, check out this example notebook on units.

Caution

Some scipy functions silently drop units when used on Quantity instances.

  • Only SI units should be used within PlasmaPy, unless there is a strong justification to do otherwise. Example notebooks may occasionally use other unit systems to show the flexibility of astropy.units.

  • Use operations between Quantity instances except when needed for performance. To improve performance in Quantity operations, check out performance tips for astropy.units.

  • Use unit annotations with the validate_quantities() decorator to validate Quantity arguments and return values (see Validating Quantity arguments).

    Caution

    Recent versions of Astropy allow unit-aware Quantity annotations such as u.Quantity[u.m]. However, these annotations are not yet compatible with validate_quantities().

  • Avoid using electron-volts as a unit of temperature within PlasmaPy because it is defined as a unit of energy. However, functions in plasmapy.formulary and elsewhere should accept temperatures in units of electron-volts, which can be done using validate_quantities().

  • Non-standard unit conversions can be made using equivalencies such as temperature_energy.

    >>> (1 * u.eV).to(u.K, equivalencies=u.temperature_energy())
    11604.518...
    
  • The names of SI units should not be capitalized except at the beginning of a sentence, including when they are named after a person. The sole exception is “degree Celsius”.

Particles

The Particle class provides an object-oriented interface for accessing basic particle data. Particle accepts particle-like inputs.

>>> from plasmapy.particles import Particle
>>> alpha = Particle("He-4 2+")
>>> alpha.mass
<Quantity 6.6446...e-27 kg>
>>> alpha.charge
<Quantity 3.20435...e-19 C>

To get started with plasmapy.particles, check out this example notebook on particles.

  • Avoid using implicit default particle assumptions for function arguments (see issue #453).

  • The particle_input() decorator can automatically transform a particle-like argument into a Particle, CustomParticle, or ParticleList instance when the corresponding parameter is decorated with ParticleLike.

    from plasmapy.particles import ParticleLike, particle_input
    
    
    @particle_input
    def get_particle(particle: ParticleLike):
        return particle
    

    If we use get_particle on something particle-like, it will return the corresponding particle object.

    >>> return_particle("p+")
    Particle("p+")
    

    The documentation for particle_input() describes ways to ensure that the particle meets certain categorization criteria.

Equations and Physical Formulae
  • Physical formulae should be inputted without first evaluating all of the physical constants. For example, the following line of code obscures information about the physics being represented:

    omega_ce = 1.76e7*(B/u.G)*u.rad/u.s  # doctest: +SKIP
    

    In contrast, the following line of code shows the exact formula which makes the code much more readable.

    omega_ce = (e * B) / (m_e * c)  # doctest: +SKIP
    

    The origins of numerical coefficients in formulae should be documented.

  • Docstrings should describe the physics associated with these quantities in ways that are understandable to students who are taking their first course in plasma physics while still being useful to experienced plasma physicists.

Angular Frequencies

Unit conversions involving angles must be treated with care. Angles are dimensionless but do have units. Angular velocity is often given in units of radians per second, though dimensionally this is equivalent to inverse seconds. Astropy will treat radians dimensionlessly when using the dimensionless_angles equivalency, but dimensionless_angles does not account for the multiplicative factor of \(2π\) that is used when converting between frequency (1/s) and angular frequency (rad/s). An explicit way to do this conversion is to set up an equivalency between cycles/s and Hz:

import astropy.units as u
f_ce = omega_ce.to(u.Hz, equivalencies=[(u.cy/u.s, u.Hz)])  # doctest: +SKIP

However, dimensionless_angles does work when dividing a velocity by an angular frequency to get a length scale:

d_i = (c/omega_pi).to(u.m, equivalencies=u.dimensionless_angles())  # doctest: +SKIP
Example notebooks

Examples in PlasmaPy are written as Jupyter notebooks, taking advantage of their mature ecosystems. They are located in docs/notebooks. nbsphinx takes care of executing them at documentation build time and including them in the documentation.

Please note that it is necessary to store notebooks with their outputs stripped (use the “Edit -> Clear all” option in JupyterLab and the “Cell -> All Output -> Clear” option in the “classic” Jupyter Notebook). This accomplishes two goals:

  1. helps with versioning the notebooks, as binary image data is not stored in the notebook

  2. signals nbsphinx that it should execute the notebook.

Note

In the future, verifying and running this step may be automated via a GitHub bot. Currently, reviewers should ensure that submitted notebooks have outputs stripped.

If you have an example notebook that includes packages unavailable in the documentation building environment (e.g., bokeh) or runs some heavy computation that should not be executed on every commit, keep the outputs in the notebook but store it in the repository with a preexecuted_ prefix (e.g., preexecuted_full_3d_mhd_chaotic_turbulence_simulation.ipynb).

Compatibility with Prior Versions of Python, NumPy, and Astropy

PlasmaPy releases will generally abide by the following standards, which are adapted from NEP 29 for the support of old versions of Python, NumPy, and Astropy.

  • PlasmaPy should support at least the minor versions of Python initially released 42 months prior to a planned project release date.

  • PlasmaPy should support at least the 3 latest minor versions of Python.

  • PlasmaPy should support minor versions of NumPy initially released in the 24 months prior to a planned project release date or the oldest version that supports the minimum Python version (whichever is higher).

  • PlasmaPy should support at least the 3 latest minor versions of NumPy and Astropy.

The required major and minor version numbers of upstream packages may only change during major or minor releases of PlasmaPy, and never during patch releases.

Exceptions to these guidelines should only be made when there are major improvements or fixes to upstream functionality or when other required packages have stricter requirements.

Benchmarks

PlasmaPy has a set of asv benchmarks that monitor performance of its functionalities. This is meant to protect the package from performance regressions. The benchmarks can be viewed at benchmarks. They are generated from results located in benchmarks-repo. Detailed instructions on writing such benchmarks can be found at asv-docs. Up-to-date instructions on running the benchmark suite will be located in the README file of benchmarks-repo.

Changelog Guide

Introduction

A changelog tells users and contributors what notable changes have been made between each release. Pull requests to PlasmaPy need changelog entries before they can be merged, except when the changes are very minor. PlasmaPy uses towncrier to convert the changelog entries into the full changelog. Some example changelog entries are:

Added a page in the contributor guide that describes how to add
changelog entries.

The ``oldname`` argument to `plasmapy.subpackage.module.function` has
been deprecated and will be removed in a future release. Use
``newname`` instead.
Adding a changelog entry

Please follow these steps to add a changelog entry after submitting a pull request to PlasmaPy’s main branch.

  1. In the changelog directory, create a new file entitled ⟨number⟩.⟨type⟩.rst, where ⟨number⟩ is replaced with the pull request number and ⟨type⟩ is replaced with one of the following changelog types:

    • breaking: For backwards incompatible changes that would require users to change code. Not to be used for removal of deprecated features.

    • bugfix: For changes that fix bugs or problems with the code.

    • doc: For changes to the documentation.

    • feature: For new user-facing features and any new behavior.

    • internal: For refactoring of the internal mechanics of the code and tests in ways that do not change the application user interface.

    • removal: For feature deprecation and/or removal.

    • trivial: For minor changes that do not change the application programming interface.

    Pull request #1198 includes an update to the documentation, so the file should be named 1198.doc.rst. If you are unsure of which changelog type to use, please feel free to ask in your pull request.

    Note

    A doc changelog entry is not necessary if there is a corresponding feature changelog entry.

    Tip

    When a pull request includes multiple changes, use a separate changelog file for each distinct change.

    If the changes are in multiple categories, include a separate changelog file for each category. For example, pull request #1208 included both a breaking change and a new feature, and thus needed both 1208.breaking.rst and 1208.feature.rst.

    For multiple changes in a single category, use filenames like 1208.trivial.1.rst and 1208.trivial.2.rst.

  2. Open that file and write a short description of the changes that were made. As an example, 1198.doc.rst might include:

    Added a page in the contributor guide that describes how to add
    changelog entries.
    
  3. Commit the file and push the change to branch associated with the pull request on GitHub.

Changelog guidelines
  • Changelog entries will be read by users and developers of PlasmaPy and packages that depend on it, so please write each entry to be understandable to someone with limited familiarity of the package.

  • Changelog entries are not required for changes that are sufficiently minor, such as typo fixes or fixed hyperlinks. When this is the case, a package maintainer will add the no changelog entry needed label to the pull request.

  • Use the past tense to describe the change, and the present tense to describe how the functionality currently works.

  • A changelog entry may include multiple sentences to describe important context and consequences of the change. Because towncrier automatically reflows text, keep entries to a single paragraph.

  • Use intersphinx links to refer to objects within PlasmaPy, and include the full namespace. For example, use `~plasmapy.particles.particle_class.Particle` to refer to Particle. The tilde is included to hide all but the name of the object.

  • Show the full former namespace for objects that have been removed or moved, and use double back ticks so that the name is rendered as code without attempting to create a link.

    Removed the ``plasmapy.physics`` subpackage. The functionality from
    that subpackage is now in `plasmapy.formulary`.
    
  • Substitutions as defined in docs/_global_substitutions.py may be used in changelog entries.

  • The pull request number does not need to be included inside the changelog entry because it will be added automatically when the individual entries are converted into the full changelog.

  • When a changelog entry describes changes to functionality, it is not necessary to mention the corresponding changes to the tests.

  • If a change is supplanted by another change during the release cycle, keep the files for both changelog entries. When the change is significant, mention in the earlier entry that the change was superseded or reverted and include a link to the appropriate pull request.

Building the changelog

During the release cycle, towncrier is used to build the changelog. To install towncrier and the other packages needed to develop PlasmaPy, go to the top-level directory of your local clone of PlasmaPy and run:

pip install -e .[dev]

Configuration files for towncrier are in pyproject.toml.

To run towncrier, enter the top-level directory of PlasmaPy’s repository. To print out a preview of the changelog, run:

towncrier --draft

To convert the changelog entries into a changelog prior to the 0.7.0 release, run:

towncrier --version v0.7.0

This will create CHANGELOG.rst in the top-level directory, with the option to delete the individual changelog entry files. The full steps to update the changelog are described in the Release Guide.

Tip

towncrier can be used to create a new changelog entry and open it for editing using a command like:

towncrier create --edit ⟨number⟩.⟨type⟩.rst

Here, ⟨number⟩ is replaced with the pull request number and ⟨type⟩ is replaced with the one of the changelog types as described above.

Testing Guide

Summary
  • New functionality added to PlasmaPy must have tests.

  • Tests are located in the top-level tests/ directory. For example, tests of plasmapy.formulary are in tests/formulary/.

  • The names of test files begin with test_.

  • Tests are either functions beginning with test_ or classes beginning with Test.

  • Here is an example of a minimal pytest test that uses an assert statement:

    def test_multiplication():
        assert 2 * 3 == 6
    
  • To install the packages needed to run the tests:

    • Open a terminal.

    • Navigate to the top-level directory (probably named PlasmaPy/) in your local clone of PlasmaPy’s repository.

    • If you are on macOS or Linux, run:

      python -m pip install -e ".[tests]"
      

      If you are on Windows, run:

      py -m pip install -e .[tests]
      

      These commands will perform an editable installation of your local clone of PlasmaPy.

  • Run pytest in the command line in order to run tests in that directory and its subdirectories.

Introduction

Software testing is vital for software reliability and maintainability. Software tests help us to:

  • Find and fix bugs.

  • Prevent old bugs from getting re-introduced.

  • Provide confidence that our code is behaving correctly.

  • Define what “correct behavior” actually is.

  • Speed up code development and refactoring.

  • Show future contributors examples of how code was intended to be used.

  • Confirm that our code works on different operating systems and with different versions of software dependencies.

  • Enable us to change code with confidence that we are not unknowingly introducing bugs elsewhere in our program.

Tip

Writing tests takes time, but debugging takes more time.

Every code contribution to PlasmaPy with new functionality must also have corresponding tests. Creating or updating a pull request will activate PlasmaPy’s test suite to be run via GitHub Actions, along with some additional checks. The results of the test suite are shown at the bottom of each pull request. Click on Details next to each test run to find the reason for any test failures.

A unit test verifies a single unit of behavior, does it quickly, and does it in isolation from other tests [Khorikov, 2020]. A typical unit test is broken up into three parts: arrange, act, and assert [Osherove, 2013]. An integration test verifies that multiple software components work together as intended.

PlasmaPy’s tests are run using pytest and tox. Tests are located in the tests/ directory. For example, tests of plasmapy.formulary are located in tests/formulary and tests of plasmapy.formulary.speeds are located in tests/formulary/test_speeds.py.

Writing Tests

Every code contribution that adds new functionality requires both tests and documentation in order to be merged. Here we describe the process of write a test.

Locating tests

Tests are located in the top-level tests/ directory. The directory structure of tests/ largely mirrors that of src/plasmapy/, which contains the source code of PlasmaPy.

The tests of a subpackage named plasmapy.subpackage are located in the tests/subpackage/ directory. Tests for a module named plasmapy.subpackage.module are generally located in tests/subpackage/test_module.py. For example, tests for plasmapy.formulary are located in tests/formulary, and tests of plasmapy.formulary.speeds are located in tests/formulary/test_speeds.py.

Test functions within each file have names that begin with test_ and end with a description of the behavior that is being tested. For example, a test to checks that a Particle can be turned into an antiparticle might be named :test_create_antiparticle_from_particle. Because Particle is defined in src/plasmapy/particles/particle_class.py, this test would be located in tests/particles/test_particle_class.py.

Closely related tests may be grouped into classes. The name of a test class begins with Test and the methods to be tested begin with test_. For example, test_particle_class.py could define a TestParticle class containing the method test_charge_number.

Example code contained within docstrings is tested to make sure that the actual printed output matches the output included in the docstring.

More information on test organization, naming, and collection is provided in pytest’s documentation on test discovery conventions.

Assertions

A software test runs a section of code and checks that a particular condition is met. If the condition is not met, then the test fails. Here is a minimal software test:

def test_addition():
    assert 2 + 2 == 4

The most common way to check that a condition is met is through an assert statement, as in this example. If the expression that follows assert evaluates to False, then this statement will raise an AssertionError so that the test will fail. If the expression that follows assert evaluates to True, then this statement will do nothing and the test will pass.

When assert statements raise an AssertionError, pytest will display the values of the expressions evaluated in the assert statement. The automatic output from pytest is sufficient for simple tests like above. For more complex tests, we can add a descriptive error message to help us find the cause of a particular test failure.

def test_addition():
    actual = 2 + 2
    expected = 4
    assert actual == expected, f"2 + 2 returns {actual} instead of {expected}."

Tip

Use f-strings to improve error message readability.

Type hint annotations

PlasmaPy has begun using mypy to perform static type checking on type hint annotations. Adding a -> None return annotation lets mypy verify that tests do not have return statements.

def test_addition() -> None:
    assert 2 * 2 == 4
Floating point comparisons

Caution

Using == to compare floating point numbers can lead to brittle tests because of slight differences due to limited precision, rounding errors, and revisions to fundamental constants.

In order to avoid these difficulties, use numpy.testing.assert_allclose when comparing floating point numbers and arrays, and astropy.tests.helper.assert_quantity_allclose when comparing Quantity instances. The rtol keyword for each of these functions sets the acceptable relative tolerance. The value of rtol should be set ∼1–2 orders of magnitude greater than the expected relative uncertainty. For mathematical functions, a value of rtol=1e-14 is often appropriate. For quantities that depend on physical constants, a value between rtol=1e-8 and rtol=1e-5 may be required, depending on how much the accepted values for fundamental constants are likely to change.

Testing warnings and exceptions

Robust testing frameworks should test that functions and methods return the expected results, issue the expected warnings, and raise the expected exceptions. pytest contains functionality to test warnings and test exceptions.

To test that a function issues an appropriate warning, use pytest.warns.

import warnings

import pytest


def issue_warning() -> None:
    warnings.warn("warning message", UserWarning)


def test_that_a_warning_is_issued() -> None:
    with pytest.warns(UserWarning):
        issue_warning()

To test that a function raises an appropriate exception, use pytest.raises.

import pytest


def raise_exception() -> None:
    raise Exception


def test_that_an_exception_is_raised() -> None:
    with pytest.raises(Exception):
        raise_exception()
Test independence and parametrization

In this section, we’ll discuss the issue of parametrization based on an example of a proof of Gauss’s class number conjecture.

The proof goes along these lines:

  • If the generalized Riemann hypothesis is true, the conjecture is true.

  • If the generalized Riemann hypothesis is false, the conjecture is also true.

  • Therefore, the conjecture is true.

One way to use pytest would be to write sequential test in a single function.

def test_proof_by_riemann_hypothesis() -> None:
    assert proof_by_riemann(False)
    assert proof_by_riemann(True)  # will only be run if the previous test passes

If the first test were to fail, then the second test would never be run. We would therefore not know the potentially useful results of the second test. This drawback can be avoided by making independent tests so that both will be run.

def test_proof_if_riemann_false() -> None:
    assert proof_by_riemann(False)


def test_proof_if_riemann_true() -> None:
    assert proof_by_riemann(True)

However, this approach can lead to cumbersome, repeated code if you are calling the same function over and over. If you wish to run multiple tests for the same function, the preferred method is to decorate it with @pytest.mark.parametrize.

@pytest.mark.parametrize("truth_value", [True, False])
def test_proof_if_riemann(truth_value: bool) -> None:
    assert proof_by_riemann(truth_value)

This code snippet will run proof_by_riemann(truth_value) for each truth_value in [True, False]. Both of the above tests will be run regardless of failures. This approach is much cleaner for long lists of arguments, and has the advantage that you would only need to change the function call in one place if the function changes.

With qualitatively different tests you would use either separate functions or pass in tuples containing inputs and expected values.

@pytest.mark.parametrize("truth_value, expected", [(True, True), (False, True)])
def test_proof_if_riemann(truth_value: bool, expected: bool) -> None:
    assert proof_by_riemann(truth_value) == expected
Test parametrization with argument unpacking

When the number of arguments passed to a function varies, we can use argument unpacking in conjunction with test parametrization.

Suppose we want to test a function called add that accepts two positional arguments (a and b) and one optional keyword argument (reverse_order).

def add(a: float | str, b: float | str, reverse_order: bool = False) -> float | str:
    if reverse_order:
        return b + a
    return a + b

Hint

This function uses type hint annotations to indicate that a and b can be either a float or str, reverse_order should be a bool, and add should return a float or str.

Argument unpacking lets us provide positional arguments in a tuple or list (commonly referred to as args) and keyword arguments in a dict (commonly referred to as kwargs). Unpacking occurs when args is preceded by * and kwargs is preceded by **.

>>> args = ("1", "2")
>>> kwargs = {"reverse_order": True}
>>> add(*args, **kwargs)  # equivalent to add("1", "2", reverse_order=True)
'21'

We want to test add for three cases:

  • reverse_order is True,

  • reverse_order is False, and

  • reverse_order is not specified.

We can do this by parametrizing the test over args and kwargs, and unpacking them inside of the test function.

@pytest.mark.parametrize(
    "args, kwargs, expected",
    [
        # test that add("1", "2", reverse_order=False) == "12"
        (["1", "2"], {"reverse_order": False}, "12"),
        # test that add("1", "2", reverse_order=True) == "21"
        (["1", "2"], {"reverse_order": True}, "21"),
        # test that add("1", "2") == "12"
        (["1", "2"], {}, "12"),  # if no keyword arguments, use an empty dict
    ],
)
def test_add(args: list[str], kwargs: dict[str, bool], expected: str) -> None:
    assert add(*args, **kwargs) == expected

Hint

This function uses type hint annotations to indicate that args should be a list containing str objects, kwargs should be a dict containing str objects that map to bool objects, expected should be a str, and that there should be no return statement.

Fixtures

Fixtures provide a way to set up well-defined states in order to have consistent tests. We recommend using fixtures whenever you need to test multiple properties (thus, using multiple test functions) for a series of related objects.

Property-based testing

Suppose a function \(f(x)\) has a property that \(f(x) > 0\) for all \(x\). A property-based test would verify that f(x) — the code implementation of \(f(x)\) — returns positive output for multiple values of \(x\). The hypothesis package simplifies property-based testing for Python.

Best practices

The following list contains suggested practices for testing scientific software and making tests easier to run and maintain. These guidelines are not rigid, and should be treated as general principles should be balanced with each other rather than absolute principles.

  • Run tests frequently for continual feedback. If we edit a single section of code and discover a new test failure, then we know that the problem is related to that section of code. If we edit numerous sections of code before running tests, then we will have a much harder time isolating the section of code causing problems.

  • Turn bugs into test cases [Wilson et al., 2014]. It is said that “every every bug exists because of a missing test” [Bernstein, 2015]. After finding a bug, write a minimal failing test that reproduces that bug. Then fix the bug to get the test to pass. Keeping the new test in the test suite will prevent the same bug from being introduced again. Because bugs tend to be clustered around each other, consider adding tests related to the functionality affected by the bug.

  • Make tests fast. Tests are most valuable when they provide immediate feedback. A test suite that takes a long time to run increases the probability that we will lose track of what we are doing and slows down progress.

    Tip

    Decorate tests with @pytest.mark.slow if they take ≳0.3 seconds.

    @pytest.mark.slow
    def test_calculate_all_primes() -> None:
        calculate_all_primes()
    
  • Write tests that are easy to understand and change. To fully understand a test failure or modify existing functionality, a contributor will need to understand both the code being tested and the code that is doing the testing. Test code that is difficult to understand makes it harder to fix bugs, especially if the error message is missing or hard to understand, or if the bug is in the test itself. When test code is difficult to change, it is harder to change the corresponding production code. Test code should therefore be kept as high quality as production code.

  • Write code that is easy to test. Write short functions that do exactly one thing with no side effects. Break up long functions into multiple functions that are smaller and more focused. Use pure functions rather than functions that change the underlying state of the system or depend on non-local variables. Use test-driven development and write tests before writing the code to be tested. When a section of code is difficult to test, consider refactoring it to make it easier to test.

  • Separate easy-to-test code from hard-to-test code. Some functionality is inherently hard to test, such as graphical user interfaces. Often the hard-to-test behavior depends on particular functionality that is easy to test, such as function calls that return a well-determined value. Separating the hard-to-test code from the easy-to-test code maximizes the amount of code that can be tested thoroughly and isolates the code that must be tested manually. This strategy is known as the Humble Object pattern.

  • Make tests independent of each other. Tests that are coupled with each other lead to several potential problems. Side effects from one test could prevent another test from failing, and tests lose their ability to run in parallel. Tests can become coupled when the same mutable object is used in multiple tests. Keeping tests independent allows us to avoid these problems.

  • Make tests deterministic. When a test fails intermittently, it is hard to tell when it has actually been fixed. When a test is deterministic, we will always be able to tell if it is passing or failing. If a test depends on random numbers, use the same random seed for each automated test run.

    Tip

    Tests that fail intermittently can be decorated with the @pytest.mark.flaky decorator from pytest-rerunfailures to indicate that the test should be rerun in case of failures:

    @pytest.mark.flaky(reruns=5)  # see issue 1548
    def test_optical_density_histogram(): ...
    

    Each usage of this decorator should have a comment that either indicates why the test occasionally fails (for example, if the test must download data from an external source) or refers to an issue describing the intermittent failures.

  • Avoid testing implementation details. Fine-grained tests help us find and fix bugs. However, tests that are too fine-grained become brittle and lose resistance to refactoring. Avoid testing implementation details that are likely to be changed in future refactorings.

  • Avoid complex logic in tests. When the arrange or act sections of a test include conditional blocks, most likely the test is verifying more than one unit of behavior and should be split into multiple smaller tests.

  • Test a single unit of behavior in each unit test. This suggestion often implies that there should be a single assertion per unit test. However, multiple related assertions are appropriate when needed to verify a particular unit of behavior. However, having multiple assertions in a test often indicates that the test should be split up into multiple smaller and more focused tests.

  • If the act phase of a unit test is more than a single line of code, consider revising the functionality being tested so that it can be called in a single line of code [Khorikov, 2020].

Running tests

PlasmaPy’s tests can be run in the following ways:

  1. Creating and updating a pull request on GitHub.

  2. Running pytest from the command line.

  3. Running tox from the command line.

  4. Running tests from an integrated development environment (IDE).

We recommend that new contributors perform the tests via a pull request on GitHub. Creating a draft pull request and keeping it updated will ensure that the necessary checks are run frequently. This approach is also appropriate for pull requests with a limited scope. This advantage of this approach is that the tests are run automatically and do not require any extra work. The disadvantages are that running the tests on GitHub is often slow and that navigating the test results is sometimes difficult.

We recommend that experienced contributors run tests either by using pytest from the command line or by using your preferred IDE. Using tox is an alternative to pytest, but running tests with tox adds the overhead of creating an isolated environment for your test and can thus be slower.

Using GitHub

The recommended way for new contributors to run PlasmaPy’s full test suite is to create a pull request from your development branch to PlasmaPy’s GitHub repository. The test suite will be run automatically when the pull request is created and every time changes are pushed to the development branch on GitHub. Most of these checks have been automated using GitHub Actions.

The following image shows how the results of the checks will appear in each pull request near the end of the Conversation tab. Checks that pass are marked with ✔️, while tests that fail are marked with ❌. Click on Details for information about why a particular check failed.

Continuous integration test results during a pull request

The following checks are performed with each pull request.

  • Checks with labels like CI / Python 3.x (pull request) verify that PlasmaPy works with different versions of Python and other dependencies, and on different operating systems. These tests are set up using tox and run with pytest via GitHub Actions. When multiple tests fail, investigate these tests first.

    Tip

    Python 3.10, Python 3.11, and Python 3.12 include (or will include) significant improvements to common error messages.

  • The CI / Documentation (pull_request) check verifies that PlasmaPy’s documentation is able to build correctly from the pull request. Warnings are treated as errors.

  • The docs/readthedocs.org:plasmapy check allows us to preview how the documentation will appear if the pull request is merged. Click on Details to access this preview.

  • The check labeled changelog: found or changelog: absent indicates whether or not a changelog entry with the correct number is present, unless the pull request has been labeled with “No changelog entry needed”.

    • The changelog/README.rst file describes the process for adding a changelog entry to a pull request.

  • The codecov/patch and codecov/project checks generate test coverage reports that show which lines of code are run by the test suite and which are not. Codecov will automatically post its report as a comment to the pull request. The Codecov checks will be marked as passing when the test coverage is satisfactorily high. For more information, see the section on Code coverage.

  • The CI / Importing PlasmaPy (pull_request) checks that it is possible to run import plasmapy.

  • PlasmaPy uses black to format code and isort to sort import statements. The CI / Linters (pull_request) and pre-commit.ci - pr checks verify that the pull request meets these style requirements. These checks will fail when inconsistencies with the output from black or isort are found or when there are syntax errors. These checks can usually be ignored until the pull request is nearing completion.

    Tip

    The required formatting fixes can be applied automatically by writing a comment with the message pre-commit.ci autofix to the Conversation tab on a pull request, as long as there are no syntax errors. This approach is much more efficient than making the style fixes manually. Remember to git pull afterwards!

    Note

    When using pre-commit, a hook for codespell will check for and fix common misspellings. If you encounter any words caught by codespell that should not be fixed, please add these false positives to ignore-words-list under codespell in pyproject.toml.

  • The CI / Packaging (pull request) check verifies that no errors arise that would prevent an official release of PlasmaPy from being made.

  • The Pull Request Labeler / triage (pull_request_target) check applies appropriate GitHub labels to pull requests.

Note

For first-time contributors, existing maintainers may need to manually enable your `GitHub Action test runs. This is, believe it or not, indirectly caused by the invention of cryptocurrencies.

Note

The continuous integration checks performed for pull requests change frequently. If you notice that the above list has become out-of-date, please submit an issue that this section needs updating.

Using pytest

To install the packages necessary to run tests on your local computer (including tox and pytest), run:

pip install -e .[tests]

To run PlasmaPy’s tests from the command line, go to a directory within PlasmaPy’s repository and run:

pytest

This command will run all of the tests found within your current directory and all of its subdirectories. Because it takes time to run PlasmaPy’s tests, it is usually most convenient to specify that only a subset of the tests be run. To run the tests contained within a particular file or directory, include its name after pytest. If you are in the directory plasmapy/particles/tests/, then the tests in in test_atomic.py can be run with:

pytest test_atomic.py

The documentation for pytest describes how to invoke pytest and specify which tests will or will not be run. A few useful examples of flags you can use with it:

  • Use the --tb=short to shorten traceback reports, which is useful when there are multiple related errors. Use --tb=long for traceback reports with extra detail.

  • Use the -x flag to stop the tests after the first failure. To stop after \(n\) failures, use --maxfail=n where n is replaced with a positive integer.

  • Use the -m 'not slow' flag to skip running slow (defined by the @pytest.mark.slow marker) tests, which is useful when the slow tests are unrelated to your changes. To exclusively run slow tests, use -m slow.

  • Use the --pdb flag to enter the Python debugger upon test failures.

Using tox

PlasmaPy’s continuous integration tests on GitHub are typically run using tox, a tool for automating Python testing. Using tox simplifies testing PlasmaPy with different releases of Python, with different versions of PlasmaPy’s dependencies, and on different operating systems. While testing with tox is more robust than testing with pytest, using tox to run tests is typically slower because tox creates its own virtual environments.

To run PlasmaPy’s tests for a particular environment, run:

tox -e ⟨envname⟩

where ⟨envname⟩ is replaced with the name of the tox environment, as described below.

Some testing environments for tox are pre-defined. For example, you can replace ⟨envname⟩ with py39 if you are running Python 3.9.x, py310 if you are running Python 3.10.x, or py311 if you are running Python 3.11.x. Running tox with any of these environments requires that the appropriate version of Python has been installed and can be found by tox. To find the version of Python that you are using, go to the command line and run python --version.

Additional tox environments are defined in tox.ini in the top-level directory of PlasmaPy’s repository. To find which testing environments are available, run:

tox -a

For example, static type checking with mypy can be run locally with

tox -e mypy

Commands using tox can be run in any directory within PlasmaPy’s repository with the same effect.

Code coverage

Code coverage refers to a metric “used to describe the degree to which the source code of a program is executed when a particular test suite runs.” The most common code coverage metric is line coverage:

\[\mbox{line coverage} ≡ \frac{ \mbox{number of lines accessed by tests} }{ \mbox{total number of lines} }\]

Line coverage reports show which lines of code have been used in a test and which have not. These reports show which lines of code remain to be tested, and sometimes indicate sections of code that are unreachable.

Tip

Use test coverage reports to write tests that target untested sections of code and to find unreachable sections of code.

Caution

While a low value of line coverage indicates that the code is not adequately tested, a high value does not necessarily indicate that the testing is sufficient. A test that makes no assertions has little value, but could still have high test coverage.

PlasmaPy uses coverage.py and the pytest-cov plugin for pytest to measure code coverage and Codecov to provide reports on GitHub.

Generating coverage reports with pytest

Code coverage reports may be generated on your local computer to show which lines of code are covered by tests and which are not. To generate an HTML report, use the --cov flag for pytest:

pytest --cov
coverage html

Open htmlcov/index.html in your web browser to view the coverage reports.

Excluding lines in coverage reports

Occasionally there will be certain lines that should not be tested. For example, it would be impractical to create a new testing environment to check that an ImportError is raised when attempting to import a missing package. There are also situations that coverage tools are not yet able to handle correctly.

To exclude a line from a coverage report, end it with # coverage: ignore. Alternatively, we may add a line to exclude_lines in the [coverage:report] section of setup.cfg that consists of a a pattern that indicates that a line be excluded from coverage reports. In general, untested lines of code should remain marked as untested to give future developers a better idea of where tests should be added in the future and where potential bugs may exist.

Coverage configurations

Configurations for coverage tests are given in the [coverage:run] and [coverage:report] sections of setup.cfg. Codecov configurations are given in .codecov.yaml.

Using an integrated development environment

Most IDEs have built-in tools that simplify software testing. IDEs like PyCharm and Visual Studio allow test configurations to be run with a click of the mouse or a few keystrokes. While IDEs require time to learn, they are among the most efficient methods to interactively perform tests. Here are instructions for running tests in several popular IDEs:

Documentation Guide

Introduction

Documentation that is up-to-date and understandable is vital to the health of a software project. This page describes the documentation requirements and guidelines to be followed during the development of PlasmaPy and affiliated packages.

Tip

Updating documentation is one of the best ways to make a first contribution to an open source software project.

Note

If you discover areas within PlasmaPy’s documentation that are confusing or incomplete, please raise an issue! This really helps PlasmaPy not only by helping us improve the documentation for all, but also by creating opportunities for new contributors to make their first contribution to the project.

PlasmaPy’s documentation is hosted by Read the Docs and is available at these locations:

Tip

A preview of the documentation is generated every time a pull request is created or updated. You can access this preview by scrolling down to the checks at the bottom of a pull request, and clicking on Details next to docs/readthedocs.org:plasmapy.

Access to the preview of the documentation after a pull request
Markup Languages
ReStructuredText

PlasmaPy’s documentation is written using the reStructuredText markup language. reStructuredText is human readable when viewed within a source code file or when printed out using help. reStructuredText also contains markup that allows the text to be transformed into PlasmaPy’s documentation. reStructuredText files use the file extension .rst. Documentation contained within .py files are in the form of docstrings, which are written in reStructuredText.

ReStructuredText Examples

Here we show some examples of commonly used reStructuredText syntax in PlasmaPy. Please refer to the documentation for Sphinx and reStructuredText for a list of available roles and directives.

This is an example of including headings for the document title, sections, subsections, and so on. The lines surrounding each heading are the same length as that heading.

==============
Document title
==============

Heading 1
=========

Heading 2
---------

Heading 3
~~~~~~~~~

We can link to code objects by enclosing them in single backticks. This linking will work for Python objects as well as certain packages like NumPy, SciPy, Astropy, and pandas. This linking is described in the section on Cross-referencing external packages. In-line code samples are typically enclosed in double backticks. To get inline code highlighting, use the :py: role for Python code or :bash: for code run in a terminal.

Here `plasmapy.particles` provides a linked reference to the
module's documentation.

Adding a tilde at the beginning `~plasmapy.particles` still
provides a linked reference to the associated documentation
but shortens the display so only "particles" is displayed.

Double backticks are used to show inline code that is not
cross-referenced: ``plasmapy.particles``.

The ``:py:`` role can be used for inline code highlighting:
:py:`import astropy.units as u`.

This reStructuredText block renders as:

Here plasmapy.particles provides a linked reference to the module’s documentation.

Adding a tilde at the beginning particles still provides a linked reference to the associated documentation but shortens the display so only “particles” is displayed.

Double backticks are used to show inline code that is not cross-referenced: plasmapy.particles.

The :py: role can be used for inline code highlighting: import astropy.units as u.

Sphinx can format code blocks for Python and the Python console using the code-block directive.

.. code-block:: python

   def sample_function():
       return 42

.. code-block:: pycon

   >>> print(6 * 9)
   54

This reStructuredText block renders as:

def sample_function():
    return 42
>>> print(6 * 9)
54

Here are some examples for linking to websites.

`PlasmaPy Enhancement Proposals <https://github.com/PlasmaPy/PlasmaPy-PLEPs>`_
are used to propose major changes to PlasmaPy.

`Write the Docs`_ has a guide_ on writing software documentation.

.. _`Write the Docs`: https://www.writethedocs.org
.. _guide: https://www.writethedocs.org/

This reStructuredText block renders as:

PlasmaPy Enhancement Proposals are used to propose major changes to PlasmaPy.

Write the Docs has a guide on writing software documentation.

Displayed math may be created using the math directive using LaTeX syntax.

.. math::

   \alpha = \beta + \gamma

This reStructuredText block renders as:

\[\alpha = \beta + \gamma\]

Math can be in-line using the math role.

An example of in-line math is :math:`x`. Using Unicode characters
like :math:`α + β + γ` makes math easier to read in the source code.

This reStructuredText block renders as:

An example of in-line math is \(x\). Using Unicode characters like \(α + β + γ\) makes math easier to read in the source code.

Markdown

A few of PlasmaPy’s files are written using Markdown, such as README files and licenses from other packages. Markdown is simpler but more limited than reStructuredText. Markdown files use the file extension .md. Posts on GitHub are written in GitHub Flavored Markdown. The following code block contains a few common examples of Markdown formatting.

# Header 1

## Header 2

Here is a link to [PlasmaPy's documentation](https://docs.plasmapy.org).

We can make text **bold** or *italic*.

We can write in-line code like `x = 1` or create a Python code block:

```Python
y = 2
z = 3
```
Writing Documentation
Docstrings

A docstring is a comment at the beginning of a function or another object that provides information on how to use that function (see PEP 257). Docstrings are designated by surrounding the content with triple quotes """This is my docstring.""".

In order to improve readability and maintain consistency, PlasmaPy uses the numpydoc standard for docstrings. Docstring conventions for Python are more generally described in PEP 257.

Tip

If a docstring contains math that utilizes LaTeX syntax, begin the docstring with r""" instead of """.

In a normal string, backslashes are used to begin escape sequences, and a single backslash needs to be represented with \\. This complication is avoided by beginning the docstring with r""", which denotes the docstring as a raw string. For example, the raw string r""":math:`\alpha`""" will render the same as the normal string """:math:`\\alpha`""".

Example docstring

Here is an example docstring in the numpydoc format:

Example docstring
import warnings

import numpy as np


def subtract(a, b, *, switch_order=False):
    r"""
    Compute the difference between two integers.

    Add ∼1–3 sentences here for an extended summary of what the function
    does. This extended summary is a good place to briefly define the
    quantity that is being returned.

    .. math::

        f(a, b) = a - b

    Parameters
    ----------
    a : `float`
        The number from which ``b`` will be subtracted.

    b : `float`
        The number being subtracted from ``a``.

    switch_order : `bool`, |keyword-only|, default: `True`
        If `True`, return :math:`a - b`. If `False`, then return
        :math:`b - a`.

    Returns
    -------
    float
        The difference between ``a`` and ``b``.

    Raises
    ------
    `ValueError`
        If ``a`` or ``b`` is `~numpy.inf`.

    Warns
    -----
    `UserWarning`
        If ``a`` or ``b`` is `~numpy.nan`.

    See Also
    --------
    add : Add two numbers.

    Notes
    -----
    The "Notes" section provides extra information that cannot fit in the
    extended summary near the beginning of the docstring. This section
    should include a discussion of the physics behind a particular concept
    that should be understandable to someone who is taking their first
    plasma physics class. This section can include a derivation of the
    quantity being calculated or a description of a particular algorithm.

    Examples
    --------
    Include a few example usages of the function here. Start with simple
    examples and then increase complexity when necessary.

    >>> from package.subpackage.module import subtract
    >>> subtract(9, 6)
    3

    Here is an example of a multi-line function call.

    >>> subtract(
    ...     9, 6, switch_order=True,
    ... )
    -3

    PlasmaPy's test suite will check that these commands provide the output
    that follows each function call.
    """
    if np.isinf(a) or np.isinf(b):
        raise ValueError("Cannot perform subtraction operations involving infinity.")

    warnings.warn("The `subtract` function encountered a nan value.", UserWarning)

    return b - a if switch_order else a - b
Template docstring

This template docstring may be copied into new functions. Usually only some of the sections will be necessary for a particular function, and unnecessary sections should be deleted. Any sections that are included should be in the order provided.

Docstring template
def sample_function():
    r"""
    Compute ...

    Parameters
    ----------

    Returns
    -------

    Raises
    ------

    Warns
    -----

    See Also
    --------

    Notes
    -----

    References
    ----------

    Examples
    --------

    """
Doctests

PlasmaPy’s test suite runs code examples in docstrings to verify that the expected output in the docstring matches the actual output from running the code. These doctests verify that docstring examples faithfully represent the behavior of the code.

def double(x):
    """
    >>> double(4)  # this line is tested that it matches the output below
    8
    """
    return 2 * x

An ellipsis (...) denotes that the actual and expected outputs should only be compared to the available precision. This capability is needed for functions in plasmapy.formulary that depend on fundamental constants that are occasionally revised.

def f():
    """
    >>> import numpy as np
    >>> np.pi
    3.14159...
    >>> np.pi ** 100
    5.187...e+49
    """

To skip the execution of a line of code in a docstring during tests, end the line with # doctest: +SKIP. This is appropriate for lines where the output varies or an exception is raised.

def g():
    """
    >>> import random
    >>> random.random()  # doctest: +SKIP
    0.8905444
    >>> raise ValueError  # doctest: +SKIP
    """
Definitions

Define important terms in PlasmaPy’s Glossary, which is located at docs/glossary.rst. Here is an example of a term defined within the glossary directive.

.. glossary::

   kwargs
      An abbreviation for keyword arguments.

Using the term role allows us to link to the definitions of terms. Using :term:`kwargs` will link to kwargs in the Glossary. We can also refer to terms defined in the projects connected via intersphinx if they have not already been defined in PlasmaPy’s Glossary. Using :term:`role` will link to role and :term:`directive` will link to directive in Sphinx’s glossary.

Documentation guidelines

This section contains guidelines and best practices for writing documentation for PlasmaPy and affiliated packages.

  • Write documentation to be understandable to students taking their first course or beginning their first research project in plasma science. Include highly technical information only when necessary.

  • Use technical jargon sparingly. Define technical jargon when necessary.

  • Use the active voice in the present tense.

  • Keep the documentation style consistent within a file or module, and preferably across all of PlasmaPy’s documentation.

  • Update code and corresponding documentation at the same time.

  • Write sentences that are simple, concise, and direct rather than complicated, vague, or ambiguous. Prefer sentences with ≲ 20 words.

  • Avoid idioms, metaphors, and references that are specific to a particular culture.

  • Many words and software packages have more than one common spelling or acronym. Use the spelling that is used in the file you are modifying, which is preferably the spelling used throughout PlasmaPy’s documentation.

    • More generally, it is preferable to use the spelling that is used in Python’s documentation or the spelling that is used most commonly.

    • Represent names and acronyms for a software package or language as they are represented in the documentation for each project. Common examples include “Python”, “Astropy”, and “NumPy”, and “reStructuredTest”.

  • When referencing PlasmaPy functionality, write the full namespace path to where the functionality is defined, not where it is conveniently accessed. For example, write `~plasmapy.formulary.speeds.Alfven_speed` rather than `~plasmapy.formulary.Alfven_speed`.

    This does not necessarily need to be done when referencing external packages, since each package may have their own standard. For example, Astropy’s Quantity class is defined in `astropy.units.quantity.Quantity` but is also indexed at `~astropy.units.Quantity` so either option will link to the same documentation.

  • For readability, limit documentation line lengths to ≲ 72 characters. Longer line lengths may be used when necessary (e.g., for hyperlinks).

    Note

    Studies typically show that line lengths of 50–75 characters are optimal for readability.

  • Use indentations of 3 spaces for reStructuredText blocks.

  • Store images within the docs/_static/ directory, except for images that are generated during the Sphinx build. The docs/_static/ directory contains files that are used for the online documentation but are not generated during the Sphinx build.

  • Avoid linking to websites that might disappear due to link rot such as documents hosted on personal websites.

  • Include both the original references for a topic as well as accessible pedagogical references. Prefer references that are open access over references that require purchase of a subscription or are behind a paywall.

Note

Emphasize important points with admonitions like this one.

  • Start the names of all physical units with a lower case letter, except at the beginning of a sentence and for “degree Celsius”.

  • Physical unit symbols should not be formatted as math. If units are needed inside a math block, use LaTeX’s \text command as in the example below. The backslash followed by a space is needed to have a space between the number and the units.

    The speed of light is approximately :math:`3 × 10^8` m/s or
    
    .. math::
    
       3 × 10^{10}\ \text{cm/s}
    

    This reStructuredText block renders as:

    The speed of light is approximately \(3 × 10^8\) m/s or

    \[3 × 10^{10}\ \text{cm/s}\]
  • The names of chemical elements are lower case, except at the beginning of a sentence.

  • Particle and chemical symbols should be formatted as regular text. Use :sub: for subscripts and :sup: for superscripts.

    Because interpreted text must normally be surrounded by whitespace or punctuation, use a backslash followed by a space for the interpreted text to show up immediately next to the regular text. This is not necessary before a period or comma.

    The symbol for helium is He.
    
    The symbol for an electron is e\ :sup:`-`.
    
    An alpha particle may be represented as :sup:`4`\ He\ :sup:`1+`.
    

    This reStructuredText block renders as:

    The symbol for helium is He.

    The symbol for an electron is e-.

    An alpha particle may be represented as 4He1+.

  • Begin each .py file with a docstring that provides a high-level overview of what is contained in that module.

  • Place the __all__ dunder immediately after the docstring that begins a module and before the import statements (but after any from __future__ imports that must be at the beginning of a file). This dunder should be a list that contains the names of all objects in that module intended for use by users. Private objects (i.e., objects with names that begin with an underscore) should not be included in __all__. __all__ is a leftover from the now dissuaded practice of star imports (e.g., from package import *), but is still used by Sphinx for selecting objects to document. Only objects contained within __all__ will show up in the online documentation.

Docstring guidelines
  • All functions, classes, and objects that are part of the public API must have a docstring that follows the numpydoc standard. Refer to the numpydoc standard for how to write docstrings for classes, class attributes, and constants.

  • The short summary statement at the beginning of a docstring should be one line long, but may be longer if necessary.

  • The extended summary that immediately follows the short summary should be ≲ 4 sentences long. Any additional information should included in the “Notes” section.

  • Put any necessary highly technical information in the “Notes” section of a docstring.

  • The short summary should start on the line immediately following the triple quotes. There should not be any blank lines immediately before the closing triple quotes.

  • The first line of the docstring for a function or method should begin with a word like “Calculate” or “Compute” and end with a period.

  • The first line of an object that is not callable (for example, an attribute of a class decorated with property) should not begin with a verb and should end with a period.

  • Keep the docstring indented at the same level as the r""" or """ that begins the docstring, except for reStructuredText constructs like lists, math, and code blocks. Use an indentation of four spaces more than the declaration of the object.

    def f():
        """This is indented four spaces relative to the `def` statement."""
    
  • The first sentence of a docstring of a function should include a concise definition of the quantity being calculated, as in the following example.

    def beta(T, n, B):
        """Compute the ratio of thermal pressure to magnetic pressure."""
    

    When the definition of the quantity being calculated is unable to fit on ∼1–2 lines, include the definition in the extended summary instead.

    def beta(T, n, B):
        """
        Compute plasma beta.
    
        Plasma beta is the ratio of thermal pressure to magnetic pressure.
        """
    
  • When a function calculates a formula, put the formula in the extended summary section when it can be included concisely. Put complicated formulae, derivations, and extensive discussions of physics or math in the “Notes” section.

  • Private code objects (e.g., code objects that begin with a single underscore, like _private_object) should have docstrings. A docstring for a private code object may be a single line, and otherwise should be in numpydoc format.

  • Docstrings for private code objects do not get rendered in the online documentation, and should be intended for contributors.

Parameters

Describe each parameter in the “Parameters” section of the docstring using the following format:

parameter_name : type specification
    Parameter description.

Some examples are:

x : `float`
    Description of ``x``.

y : `int`
    Description of ``y``.

settings : `dict` of `str` to `int`
    Description of ``settings``.
Type specifications

The type specification may include:

  • Size and/or shape information

  • Type information

  • Valid choices for the parameter

  • Whether the parameter is keyword-only, optional, and/or positional-only

  • Default values

The type specification should not include information about the meaning of the parameter. Here are some example type specifications:

|particle-like|
`list` of `str`
|array_like| of `int`, default: [-1, 1]
|Quantity| [length], default: 10 m
|Quantity| [temperature, energy], |keyword-only|, default: 0 K
  • Use the substitution |array_like| to indicate that an argument must be array_like (i.e., convertible into an ndarray).

  • Use the substitution |particle-like| to indicate that a particle-like argument should be convertible into a Particle, CustomParticle, or ParticleList.

  • Use the |particle-list-like| to indicate that a particle-list-like argument should be convertible into a ParticleList.

  • Use |atom-like| to indicate that an argument must be atom-like (i.e., an element, isotope, and/or ion).

  • When the array must be \(n\)-dimensional, precede the type by nD where n is replaced by the number of dimensions.

    1D |array_like|
    3D |array_like|
    
  • If the shapes and sizes of the parameters are interrelated, then include that information in parentheses immediately before the type information. Include a trailing comma inside the parentheses when the parameter is 1D. Use : for a single dimension of arbitrary size and ... for an arbitrary number of dimensions of arbitrary size.

    (M,) |array_like|
    (N,) |array_like|
    (M, N) |array_like|
    (N, :) |array_like|
    (M, N, ...) |array_like|
    
  • If the parameter can only be specific values, enclose them in curly brackets. The options may be listed with the default value first, sorted alphanumerically, or ordered so as to maximize readability.

    {"classical postmodernist", "retro-futuristic"}
    {"p+", "e-"}, default: "p+"
    {1, 2, 3, 4}, default: 3
    
  • If a default is given, it is not necessary to state that the parameter is optional. When the default is None, use optional instead of default: `None`.

Tip

If a particular type specification is not covered above, look for conventions from the numpydoc style guide, the matplotlib documentation guide, or the LSST docstring guide.

Parameter descriptions

The parameter description should concisely describe the meaning of the parameter, as well as any requirements or restrictions on allowed values of the parameter (including those specified by validate_quantities() or particle_input(). The parameter description should not repeat information already in the type specification, but may include type information when:

  • The type specification does not fit with in the docstring line character limit;

  • Different types have different meanings, requirements, or restrictions; or

  • The docstring will be more understandable by doing so.

For functions that accept an arbitrary number of positional and/or keyword arguments, include them in the “Parameters” section with the preceding asterisk(s). Order *args and **kwargs as they appear in the signature.

*args : tuple, optional
    Description of positional arguments.

**kwargs : dict, optional
    Description of keyword arguments.
Exceptions and warnings
  • Docstrings may include a “Raises” section that describes which exceptions get raised and under what conditions, and a “Warns” section that describes which warnings will be issued and for what reasons.

    • The “Raises” and “Warns” sections should only include exceptions and warnings that are not obvious or have a high probability of occurring. For example, the “Raises” section should usually not include a TypeError for when an argument is not of the type that is listed in the “Parameters” section of the docstring.

    • The “Raises” section should include all exceptions that could reasonably be expected to require exception handling.

    • The “Raises” section should be more complete for functionality that is frequently used (e.g., Particle).

    • The “Raises” and “Warns” sections should typically only include exceptions and warnings that are raised or issued by the function itself. Exceptions and warnings from commonly used decorators like validate_quantities() and particle_input() should usually not be included in these sections, but may be included if there is strong justification to do so.

Attributes
  • Dunder methods (e.g., code objects like __add__ that begin and end with two underscores) only need docstrings if it is necessary to describe non-standard or potentially unexpected behavior. Custom behavior associated with dunder methods should be described in the class-level documentation.

    • Docstrings for most dunder methods are not rendered in the online documentation and should therefore be intended for contributors.

    • Docstrings for __init__, __new__, and __call__ are rendered in the documentation, and should be written for users. The docstrings for __init__ and __new__ are included in the class-level docstring, while the docstring for __call__ is included in the methods summary of a class.

  • When an attribute in a class has both a getter (which is the method decorated with property) and a setter decoration, then the getter and setter functionality should be documented in the docstring of the attribute decorated with @property.

    class Person:
        @property
        def age(self):
            """Document both getter and setter here."""
            return self._age
    
        @age.setter
        def age(self, n):
            self._age = n
    
Narrative documentation guidelines
  • Each top-level subpackage must have corresponding narrative documentation.

  • Use narrative documentation to describe how different functionality works together.

  • Narrative documentation should be used when the full scope of some functionality cannot be adequately described within only the docstrings of that functionality.

  • Use title case for page titles (e.g., “This is Title Case”) and sentence case for all other headings (e.g., “This is sentence case”).

Sphinx

Sphinx is the software used to generate PlasmaPy’s documentation from reStructuredText files and Python docstrings. It was originally created to write Python’s documentation and has become the de facto software for documenting Python packages. Most Python packages utilize Sphinx to generate their documentation.

Configuration

The docs/conf.py file contains the configuration information needed to customize Sphinx behavior. The documentation for Sphinx lists the configuration options that can be set.

The docs/_static/css/ directory contains CSS files with style overrides for the Read the Docs Sphinx Theme to customize the look and feel of the online documentation.

Sphinx extensions

PlasmaPy’s documentation is built with the following Sphinx extensions:

These extensions are specified in extensions configuration value in docs/conf.py.

Cross-referencing external packages

Intersphinx allows the automatic generation of links to the documentation of objects in other projects. This cross-package linking is made possible with the sphinx.ext.intersphinx extension and proper package indexing by the external package using sphinx.ext.autodoc.

When we include `astropy.units.Quantity` in the documentation, it will show up as astropy.units.Quantity with a link to the appropriate page in Astropy documentation. Similarly, `~astropy.units.Quantity` will show up as Quantity.

The external packages that we can cross-reference via the magic of intersphinx are defined in intersphinx_mapping in docs/conf.py. Intersphinx has already been set up in PlasmaPy to include the central Python documentation, as well as frequently used packages such as Astropy, lmfit, matplotlib, NumPy, pandas, SciPy, and Sphinx.

Tip

When adding new packages to intersphinx_mapping, please double check that the configuration has been set up correctly.

If a cross-link is not working as expected this is usually due to one of the following reasons:

  • A typo;

  • The package not being defined in intersphinx_mapping, or

  • The referenced source package not properly or fully indexing their own code, which is common in Python packages.

For some packages, the name of the package itself does not link correctly.

Substitutions

Some functions and classes are referred to repeatedly throughout the documentation. reStructuredText allows us to define substitutions

.. |Particle| replace:: `~plasmapy.particles.particle_class.Particle`

Here whenever |Particle| is used Sphinx will replace it with `~plasmapy.particles.particle_class.Particle` during build time.

PlasmaPy has certain common substitutions pre-defined so that they can be used elsewhere in the documentation. For example, we can write |Quantity| instead of `~astropy.units.Quantity`, and |Particle| instead of `~plasmapy.particles.particle_class.Particle`. For an up-to-date list of substitutions, please refer to docs/_global_subtitutions.py.

Since substitutions are performed by Sphinx when the documentation is built, any substitution used in docstrings will not show up when using Python’s help function (or the like). For example, when |Particle| is used in a docstring, help will show it as |Particle| rather than `~plasmapy.particles.particle_class.Particle`. Consequently, substitutions should not be used in docstrings when it is important that users have quick access to the full path of the object (such as in the See Also section).

Bibliography

PlasmaPy uses sphinxcontrib-bibtex to manage references for its documentation. This Sphinx extension allows us to store references in a BibTeX file which is then used to generate the Bibliography. References in the Bibliography are then citeable from anywhere in the documentation.

To add a new reference to the Bibliography, open docs/bibliography.bib and add the reference in BibTeX format. The citekey should generally be the surname of the first author (all lower case) followed by a colon and the year. A letter should be added after the year when needed to disambiguate multiple references. Include the DOI if the reference has one. If the reference does not have a DOI, then include the URL. The ISBN or ISSN number should be included for books. The misc field type should be used when citing data sets and software. Please follow the existing style in docs/bibliography.bib and alphabetize references by the surname of the first author. To preserve capitalization, enclose words or phrases within curly brackets (e.g., {NumPy}).

Use :cite:p:`citekey` to create a parenthetical citation and :cite:t:`citekey` to create a textual citation, where citekey is replaced with the BibTeX citekey. Multiple citekeys can also be used when separated by commas, like :cite:p:`citekey1, citekey2`. For example, :cite:p:`wilson:2014` will show up as [Wilson et al., 2014], :cite:t:`wilson:2014` will show up as Wilson et al. [2014], and :cite:p:`wilson:2014, wilson:2017` will show up as [Wilson et al., 2014, Wilson et al., 2017].

Creating a documentation stub file for a new module

When the narrative documentation does not index a subpackage (a directory) or module (a .py file) with automodule, automodapi, or the like, then a stub file must be created for that particular subpackage or module in docs/api_static/. For example, the stub file for plasmapy.particles.atomic is placed at docs/api_static/plasmapy.particles.atomic.rst and its contents look like:

:orphan:

`plasmapy.particles.atomic`
===========================

.. currentmodule:: plasmapy.particles.atomic

.. automodapi::  plasmapy.particles.atomic

A missing stub file may lead to either a reference target not found error or the absence of the module in the documentation build.

Note

If a pull request adds a new subpackage and a new module, then a stub file must be created for both of them.

For example, suppose a pull request creates the plasmapy.io subpackage in the src/plasmapy/io/ directory and the plasmapy.io.readers module via src/plasmapy/io/readers.py. It will then be necessary to create stub files at both docs/api_static/plasmapy.io.rst and docs/api_static/plasmapy.io.readers.rst.

Templating

Sphinx uses the Jinja templating engine to generate HTML code. Jinja may be used within the documentation when templating is necessary. For more details, please refer to Sphinx’s templating page.

Danger

There are certain tasks that one would expect to be straightforward with reStructuredText and Sphinx but are only possible by doing a horrible workaround that can take hours to figure out. This has given rise to the saying:

Sphinx rabbit holes often have dragons in them. 🐇 🕳️ 🐉

Remember: your happiness and well-being are more important than nested inline markup!

Building documentation

Tip

Because a documentation preview is generated automatically by Read the Docs for every pull request, it is not necessary to build the documentation locally on your own computer. New contributors can safely skip this section.

There are two methods for building the documentation: make and tox.

  • Using make will build the documentation based off of what is in the current directory structure. make is quicker for local builds than tox but requires you to install and set up all dependencies.

  • Using tox does not require setting up all dependencies ahead of time, but is more computationally intensive since it creates a virtual environment and builds the package before building the documentation. Consequently, PlasmaPy uses tox for building the documentation on continuous integration testing platforms.

Prerequisites

Prior to building the documentation, please follow the instructions on getting ready to contribute. Alternatively, the dependencies for building docs can be installed by entering the top-level directory of the repository and running:

pip install -e .[docs,tests]

It may also be necessary to install the following software:

Building documentation

PlasmaPy’s documentation can be built using tox, make, or sphinx-build. We recommend starting with tox.

We can use tox to build the documentation locally by running:

tox -e build_docs

To pass any options to sphinx-build, put them after --. For example, use tox -e build_docs -- -v to increase output verbosity.

Building with tox is well-suited for reproducible documentation builds in an isolated Python environment, which is why it is used in continuous integration tests. The tradeoff is that tox takes extra time, in particular when it creates the Python environment for the first time.

The documentation landing page can be opened with a web browser at docs/_build/html/index.html.

To check hyperlinks locally, run:

tox -e linkcheck

Tip

When writing documentation, please fix any new warnings that arise. To enforce this, the build_docs tox environment fails if there are any warnings.

Troubleshooting

This section describes how to fix common documentation errors and warnings. 🛠️

Reference target not found

Warnings like py:obj reference target not found occur when Sphinx attempts to interpret text as a Python object, but is unable to do so. For example, if a docstring includes `y`, Sphinx will attempt to link to an object named y. If there is no object named y, then Sphinx will issue this warning, which gets treated like an error.

If the text is meant to be an inline code example, surround it with double backticks instead of single backticks.

When the text is meant to represent a code object, this warning usually indicates a typo or a namespace error. For example, the warning resulting from `plasmapy.paritcles` could be resolved by fixing the typo and changing it to `plasmapy.particles`.

Important

For PlasmaPy objects, use the full namespace of the object (i.e., use `plasmapy.particles.particle_class.Particle` instead of `plasmapy.particles.Particle`) or a reStructuredText substitution like |Particle| as defined in docs/_global_subtitutions.py.

This warning may occur when a new module or subpackage is created without creating a stub file for it.

This warning sometimes occurs in the type specification of a parameter in a docstring. Sphinx attempts to link words in type specifications to code objects. Type lines are intended to provide concise information about allowed types, sizes, shapes, physical types, and default values of a parameter. To resolve this warning, first move information about the meaning of a parameter from the type specification into the parameter description that begins on the following line. To expand the list of allowed words or patterns in type specifications, add a regular expression to nitpick_ignore_regex in docs/conf.py.

This warning may also occur when there is an extra space between a Sphinx role and the argument it is intended to act on. For example, this warning would be fixed by changing :math: `y` to :math:`y`.

Missing documentation pages for new modules

When a new module or subpackage is created, it is usually necessary to create a stub file for it in docs/api_static/. A missing stub file can lead to either a reference target not found error or missing documentation pages.

Missing attribute errors

An AttributeError may occur when an import statement is missing in a __init__.py file. For example, the error

AttributeError: module 'plasmapy.subpackage' has no attribute 'module'

will occur when src/plasmapy/subpackage/__init__.py is missing from plasmapy.subpackage import module. Make sure that __all__ contains "module" as well.

List ends without a blank line

Warnings like the following:

WARNING: :40: (WARNING/2) Bullet list ends without a blank line; unexpected unindent.
WARNING: :47: (WARNING/2) Definition list ends without a blank line; unexpected unindent.

may show up when Sphinx attempts to interpret text as a list, but is unable to do so. This warning might not show the file that it occurs in.

If this documentation contains a list, make sure that it is followed by a blank line and follows the formatting described in Sphinx’s documentation on lists.

This warning may occur in other places due to an indentation or other formatting problem. Try checking out the formatting in the Example docstring above.

This warning can occur when a changelog entry contains lines that start with a backtick. Try editing each changelog entry so that it is on a single really long line, rewording the changelog entry, or using Substitutions.

Could not match a code example to HTML

This warning occurs when sphinx-codeautolink cannot match a code object to its corresponding documentation. Double check that the code is correct, and consider adding any missing import statements. The documentation for this extension contains examples on how to skip blocks with .. autolink-skip:: and how to do invisible imports with .. autolink-preface::.

If this warning occurs in the “Examples” section of a docstring, put .. autolink-skip: section at the beginning of that section (see #2554). These warnings sometimes only show up when rebuilding the documentation.

A related warning is “Could not match transformation of _ on source lines _-_”.

Errors that are unrelated to a pull request

Occasionally, documentation builds will start failing for reasons that have nothing to do with the changes made in a pull request. Such errors generally result from a new release of a package that is required for PlasmaPy’s documentation build.

Tip

If you are a new contributor and have encountered a strange documentation build failure, first check recent issues to see if one has already been created about it. If an issue has not already been created, please raise an issue about the documentation build failure.

To figure out if a new release caused the error, search PyPI for recently released packages, including packages related to Sphinx and any that came up in the error message. You can also check if the same documentation build failure happened in the weekly tests on the main branch. After identifying the package that caused the error, a pull request can be submitted that sets a temporary maximum allowed version of the package that can be revisited later.

Tip

When dealing with this kind of error, procrastination often pays off! 🎈 These errors usually get resolved after the upstream package makes a bugfix release, so it is typically better to wait a week before spending a large amount of time trying to fix it. 🕒

Document isn’t included in any toctree

In general, each source file in the documentation must be included in a table of contents (toctree). Otherwise, Sphinx will issue a warning like:

WARNING: document isn't included in any toctree

This warning may occur when adding a new .rst file or example Jupyter notebook without adding it to a toctree.

This warning can be resolved by:

  • Adding the file to the appropriate toctree, or

  • Adding the orphan metadata field at the top of the file (not recommended in most situations).

In the docs/ folder, the tables of contents are generally located in index.rst in the same directory as the source files. For example Jupyter notebooks, the tables of contents are in docs/examples.rst.

Release Guide

In the time leading up to the release, run the release GitHub Action. This workflow will create an issue containing the release checklist.

Thank you for your interest in contributing to PlasmaPy! ✨ The future of the project depends on people like you, so we deeply appreciate it! 🌱

This guide describes the fundamentals of contributing to PlasmaPy. If you are a first-time contributor, please follow the steps for getting ready to contribute before proceeding to the code contribution workflow. The contributions are made to PlasmaPy’s GitHub repository.

New functions and classes added to PlasmaPy must have documentation and tests. The Documentation Guide contains sections on reStructuredText and writing documentation. The Testing Guide has sections on writing tests and testing best practices. Unless it is minor, it will also be necessary to add a changelog entry.

The PlasmaPy community abides by the Contributor Covenant Code of Conduct.

Many ways to contribute

While this guide describes the process of contributing code, documentation, and tests to PlasmaPy, there are many other ways to contribute. Some of the many possibilities are to:

  • Contribute new code, documentation, and tests.

  • Refactor existing code and tests.

  • Improve the description of plasma physics in PlasmaPy’s documentation.

  • Write educational Jupyter notebook that introduce plasma concepts using PlasmaPy.

  • Create videos that show how to use PlasmaPy. 🎥

  • Participate in code reviews.

  • Help with project management.

  • Request new features.

  • Report bugs. 🐞

  • Improve PlasmaPy’s website.

  • Help organize events such as Plasma Hack Week. 📆

  • Provide feedback on how existing functionality could be improved.

  • Help update PlasmaPy’s development roadmap. 🛣️

  • Be part of the PlasmaPy community!

Please feel free to reach out to us in PlasmaPy’s Matrix chat room or during PlasmaPy’s weekly office hours.