
PlasmaPy Documentation
PlasmaPy is an open source community-developed Python 3.9+ 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.9 and 3.11. If you do not have Python installed already, here are the instructions to download Python and install it.
Installing PlasmaPy with pip
To install the most recent release of plasmapy
on PyPI with pip into
an existing Python 3.9+ 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.9+ 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 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:
.
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
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]:
We can even create Quantity objects that are explicitly dimensionless.
[8]:
3 * u.dimensionless_unscaled
[8]:
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]:
[10]:
[2, 3, 4] * u.m / u.s
[10]:
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]:
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
Hα = balmer_series[0]
print(Hα)
656.279 nm
[16]:
np.max(balmer_series)
[16]:
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]:
[18]:
velocity.to(u.m / u.s)
[18]:
The si and cgs attributes convert the Quantity to SI or CGS units, respectively.
[19]:
velocity.si
[19]:
[20]:
velocity.cgs
[20]:
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]:
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]:
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]:
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]:
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]:
[32]:
ions = ParticleList(["O 1+", "O 2+", "O 3+"])
ions.mass
[32]:
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]:
[36]:
gyrofrequency(B=B, particle="e-")
[36]:
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]:
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]:
[39]:
Debye_length(T_e=86.17 * u.eV, n_e=1e3 * u.cm**-3)
[39]:
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:
.
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
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]:
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]:
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]:
[10]:
particle_mass("iron-56+++++++++++++")
[10]:
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]:
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]:
[14]:
electron.charge
[14]:
[15]:
electron.charge_number
[15]:
-1
[16]:
iron56_nuclide.binding_energy
[16]:
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]:
[24]:
custom_particle.charge
[24]:
[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]:
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]:
[29]:
iron_ions.charge
[29]:
[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]:
[34]:
acetic_acid_anion = molecule("CH3COOH 1-")
acetic_acid_anion.charge
[34]:
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)
{'neutrino', 'antimatter', 'stable', 'actinide', 'antilepton', 'antibaryon', 'transition metal', 'isotope', 'proton', 'alkali metal', 'lanthanide', 'positron', 'metal', 'baryon', 'metalloid', 'charged', 'ion', 'alkaline earth metal', 'post-transition metal', 'uncharged', 'halogen', 'lepton', 'boson', 'nonmetal', 'custom', 'matter', 'unstable', 'electron', 'element', 'neutron', 'fermion', 'antineutrino', 'noble gas'}
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]:
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:
.
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:
An analytical function that is callable with given parameters or fitted parameters.
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.
Error propagation calculations.
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:
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')

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=3.2282154587626497, alpha=0.12333665122536913, m=-0.4229937621676024, b=-6.302360456631053),
3.2282154587626497,
0.12333665122536913)
[8]:
(explin.param_errors, explin.param_errors.a, explin.param_errors.alpha)
[8]:
(FitParamTuple(a=0.6334368408497599, alpha=0.009881148941137735, m=0.03515590632969007, b=0.6722838068320208),
0.6334368408497599,
0.009881148941137735)
[9]:
explin.rsq
[9]:
0.9462677229192916
Fit function is callable
Now that parameters are set, the fit function is callable.
[10]:
explin(0)
[10]:
-3.0741449978684035
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.02573391, -3.04043805, -3.05293882, -3.06317499, -3.07108364,
-3.07660009, -3.07965786, -3.08018865, -3.07812222, -3.07338642]),
array([0.87608537, 0.88567295, 0.89581127, 0.9065193 , 0.91781657,
0.92972325, 0.94226008, 0.95544847, 0.96931046, 0.98386878]))
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.02573391, -3.04043805, -3.05293882, -3.06317499, -3.07108364,
-3.07660009, -3.07965786, -3.08018865, -3.07812222, -3.07338642]),
array([0.87611417, 0.88569414, 0.89582591, 0.90652851, 0.91782156,
0.92972526, 0.94226043, 0.95544853, 0.96931166, 0.9838726 ]))
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):
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.946$\na = 3.228 $\\pm$ 0.633\nalpha = 0.123 $\\pm$ 0.010\nm = -0.423 $\\pm$ 0.035\nb = -6.302 $\\pm$ 0.672\n')

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.445955047240634, 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:
.
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:
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.
Reduction: First, every grid cell is checked for a simple condition so that we can rule out cells that cannot contain a null point.
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.
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/stable/lib/python3.11/site-packages/plasmapy/analysis/nullpoint.py:1297: MultipleNullPointWarning: Multiple null points suspected. Trilinear method may not work as intended.
warnings.warn(
/home/docs/checkouts/readthedocs.org/user_builds/plasmapy/envs/stable/lib/python3.11/site-packages/plasmapy/analysis/nullpoint.py:1317: 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:
.
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:
How find_floating_potential()
works
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.”
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, ifthreshold=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 thanmin_points
, then the function is incapable of identifying \(V_f\) and will returnnumpy.nan
values; otherwise, the span will form one larger crossing-island.
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 untilmin_points
is satisfied.If
fit_type="linear"
, then ascipy.stats.linregress
fit is applied to the points that make up the crossing-island.If
fit_type="exponential"
, then ascipy.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
, thenthreshold
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 orfactor * array_size
, wherefactor = 0.1
for"linear"
and0.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 asmin_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 passvoltage
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 asvf
)
[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 functionextras.fitted_func
is a callable representation of the fitted functionI = extras.fitted_func(V)
.extras.fitted_func
is an instance of a sub-class ofAbstractFitFunction
. (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 parametersextras.fitted_func.param_errors
is a named tuple of the fitted parameter errorsextras.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]):
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",
)

This page was generated by
nbsphinx from
docs/notebooks/diagnostics/charged_particle_radiography_film_stacks.ipynb.
Interactive online version:
.
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 get_file
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
tissue_path = get_file("NIST_PSTAR_tissue_equivalent.txt", directory=temp_dir)
aluminum_path = get_file("NIST_PSTAR_aluminum.txt", directory=temp_dir)
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();

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

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]);

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:
.
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");

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.
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:
Particles that will never hit the field grid are ignored (until a later step, when they will be automatically advanced to the detector plane).
Particles are advanced to the time when the first particle enters the simulation volume. This is done in one step to save computation time.
While particles are on the grid, the particle pusher advances them each timestep by executing the following steps:
The fields at each particle’s location are interpolated using the interpolators defined in the AbstractGrid subclasses.
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.An implementation of the Boris particle push algorithm is used to advance the velocities and positions of the particles in the interpolated fields.
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();

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 thesave_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)

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%

This page was generated by
nbsphinx from
docs/notebooks/diagnostics/charged_particle_radiography_particle_tracing_custom_source.ipynb.
Interactive online version:
.
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
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,
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).
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);

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);

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);

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");

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);

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);

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);

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);

[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);

This page was generated by
nbsphinx from
docs/notebooks/diagnostics/charged_particle_radiography_particle_tracing_wire_mesh.ipynb.
Interactive online version:
.
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)

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)

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)

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)

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)

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");

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)

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:
.
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/stable/lib/python3.11/site-packages/plasmapy/diagnostics/langmuir.py:35: 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>}


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>}

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/stable/lib/python3.11/site-packages/plasmapy/diagnostics/langmuir.py:1074: 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>}


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>}


This page was generated by
nbsphinx from
docs/notebooks/diagnostics/thomson.ipynb.
Interactive online version:
.
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");

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)")

Plots of the spectral density function (Skw) which determines the amount of light scattered into different wavelengths.
In the non-collective regime only the electron feature is visible.
In the weakly collective regime (alpha approaches 1) an ion feature starts to appear and the electron feature is distorted
In the collective regime both features split into two peaks, corresponding to scattering off of forward and backwards propagating plasma oscillations.
The introduction of drift velocities introduces several Doppler shifts in the calculations, resulting in a shifted spectrum.
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:
.
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.
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
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();

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

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.
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
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.2e+00 (true value 1.0e+01)
Number of fit iterations:468
Reduced Chisquared:0.0011
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");

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: 19.8 (true value 20.0)
T_i_1: 51.1 (true value 50.0)
ifract_0: 0.3 (true value 0.3)
ifract_1: 0.7 (true value 0.7)
ion_speed_1: 199198.7 (true value 200000.0)
Number of fit iterations:1280.0
Reduced Chisquared:0.0004
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");

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

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: 5.8 (true value 5)
Number of fit iterations:113
Reduced Chisquared:0.0002

Dispersion
This page was generated by
nbsphinx from
docs/notebooks/dispersion/dispersion_function.ipynb.
Interactive online version:
.
[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)

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)

Let’s find the area where the dispersion function has a lesser than zero real part:
[7]:
plot_complex(X, Y, F.real < 0)

We can visualize the derivative:
[8]:
F = plasma_dispersion_func_deriv(Z)
plot_complex(X, Y, F)

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)

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

This page was generated by
nbsphinx from
docs/notebooks/dispersion/hollweg_dispersion.ipynb.
Interactive online version:
.
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
where
\(\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]:
%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 0x7f422cb3b550>

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);

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/stable/lib/python3.11/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/stable/lib/python3.11/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);

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:
.
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:
where,
\(ω\) 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]:
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
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/stable/lib/python3.11/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/stable/lib/python3.11/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/stable/lib/python3.11/site-packages/matplotlib/transforms.py:2855: ComplexWarning: Casting complex values to real discards the imaginary part
vmin, vmax = map(float, [vmin, vmax])

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

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);

[ ]:
This page was generated by
nbsphinx from
docs/notebooks/dispersion/two_fluid_dispersion.ipynb.
Interactive online version:
.
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
where
\(\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.
where \(j = 0\) represents the fast mode, \(j = 1\) represents the Alfvén mode, and \(j = 2\) represents the acoustic mode. Additionally,
Contents:
[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 0x7f9af51c85d0>

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)

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 0x7f9af2c48990>

Formulary
This page was generated by
nbsphinx from
docs/notebooks/formulary/ExB_drift.ipynb.
Interactive online version:
.
[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
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
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()

[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
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
and applying the vector triple product yields
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:
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()

[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:
.
[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/stable/lib/python3.11/site-packages/plasmapy/utils/decorators/checks.py:1404: 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/stable/lib/python3.11/site-packages/plasmapy/utils/decorators/checks.py:1404: 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/stable/lib/python3.11/site-packages/plasmapy/utils/decorators/checks.py:1404: 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/stable/lib/python3.11/site-packages/plasmapy/utils/decorators/checks.py:1404: 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:
.
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 0x7f7ceb253e10>
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()

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

This page was generated by
nbsphinx from
docs/notebooks/formulary/coulomb.ipynb.
Interactive online version:
.
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).
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
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,
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.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]:
from astropy.constants import hbar as ℏ
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)
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)
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/stable/lib/python3.11/site-packages/plasmapy/formulary/collisions/coulomb.py:474: 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(
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)
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/stable/lib/python3.11/site-packages/plasmapy/formulary/collisions/coulomb.py:482: 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(
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:
.
[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 0x7f0e4d9df950>
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 0x7f0e4c4f1950>

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:
.
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/stable/lib/python3.11/site-packages/plasmapy/utils/decorators/checks.py:1404: 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/stable/lib/python3.11/site-packages/plasmapy/utils/decorators/checks.py:1404: 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()

This page was generated by
nbsphinx from
docs/notebooks/formulary/magnetosphere.ipynb.
Interactive online version:
.
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
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.
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:
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\).
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]:
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]:
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]:
[6]:
gyroradius(B=B, particle="e-", T=T).to("km")
[6]:
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:
.
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()

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

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

This page was generated by
nbsphinx from
docs/notebooks/formulary/solar_plasma_beta.ipynb.
Interactive online version:
.
Plasma beta in the solar atmosphere
This notebook demonstrates plasmapy.formulary by calculating plasma \(β\) in different regions of the solar atmosphere.
Contents
Introduction
Plasma beta (\(β\)) is one of the most fundamental plasma parameters. \(β\) is the ratio of the thermal plasma pressure to the magnetic pressure:
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]:
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]:
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]:
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]:
This page was generated by
nbsphinx from
docs/notebooks/formulary/thermal_bremsstrahlung.ipynb.
Interactive online version:
.
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()
C6 s3 / (J(1/2) kg(3/2) F3 m6)

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]:
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:
.
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|)')

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 0x7f1bb7753150>
<Figure size 640x480 with 0 Axes>

Particles
This page was generated by
nbsphinx from
docs/notebooks/particles/ace.ipynb.
Interactive online version:
.
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]:
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]:
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]:
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):
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")

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:
.
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_5682/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.58 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.58 V / m, 0.64 V / m, 0.76 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.49 V / m, 0.55 V / m, 0.70 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.49 V / m, 0.55 V / m, 0.70 V / m)
This page was generated by
nbsphinx from
docs/notebooks/plasma/grids_nonuniform.ipynb.
Interactive online version:
.
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/stable/lib/python3.11/site-packages/plasmapy/plasma/grids.py:602: 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.09 T
Simulation
This page was generated by
nbsphinx from
docs/notebooks/simulation/particle_tracker.ipynb.
Interactive online version:
.
[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]:
|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/stable/lib/python3.11/site-packages/plasmapy/plasma/grids.py:190: RuntimeWarning: B_y is not specified for the provided grid.This quantity will be assumed to be zero.
warnings.warn(
/home/docs/checkouts/readthedocs.org/user_builds/plasmapy/envs/stable/lib/python3.11/site-packages/plasmapy/plasma/grids.py:190: RuntimeWarning: B_z is not specified for the provided grid.This quantity will be assumed to be zero.
warnings.warn(
/home/docs/checkouts/readthedocs.org/user_builds/plasmapy/envs/stable/lib/python3.11/site-packages/plasmapy/plasma/grids.py:190: RuntimeWarning: E_z is not specified for the provided grid.This quantity will be assumed to be zero.
warnings.warn(
/home/docs/checkouts/readthedocs.org/user_builds/plasmapy/envs/stable/lib/python3.11/site-packages/plasmapy/plasma/grids.py:190: RuntimeWarning: E_x is not specified for the provided grid.This quantity will be assumed to be zero.
warnings.warn(
/home/docs/checkouts/readthedocs.org/user_builds/plasmapy/envs/stable/lib/python3.11/site-packages/plasmapy/simulation/particle_tracker.py:617: 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/stable/lib/python3.11/site-packages/plasmapy/simulation/particle_tracker.py:617: 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 0x7f09bdfce550>

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')

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, we ask that you 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.
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…
A more human-friendly way of managing data by building an interface around xarray arrays and datasets via custom diagnostic accessors.
Quick viewing of analyzed data with default plotting routines.
Fully defining the physical parameters of a diagnostic with purposely designed
Probe
classes that are integrated into the analysis workflow.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
|
Abstract class for defining fit functions \(f(x)\) and the tools for fitting the function to a set of data. |
|
A sub-class of |
|
A sub-class of |
|
A sub-class of |
|
A sub-class of |

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
Functionality for determining the floating potential of a Langmuir sweep. |
|
Helper functions for analyzing swept Langmuir traces. |
|
Functionality for determining the ion-saturation current of a Langmuir sweep. |
Classes
|
Create a |
|
Create a |

Functions
|
Function for checking that the voltage and current arrays are properly formatted for analysis by |
|
Determine the floating potential (\(V_f\)) for a given current-voltage (IV) curve obtained from a swept Langmuir probe. |
|
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.
|
Alias to |
|
Alias to |
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
|
A class for defining a null point in 3D space. |
|
Abstract class for defining a point in 3D space. |

Exceptions
A class for handling the exception raised by passing in a magnetic field that violates the zero divergence constraint. |
|
A class for handling the exceptions of the null point finder functionality. |

Warnings
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. |
|
A class for handling the warnings of the null point finder functionality. |

Functions
|
Returns an array of |
|
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. |
|
Return an array of |
Notebooks
API
The analysis subpackage for PlasmaPy.
Sub-Packages & Modules
|
|
Functionality to find and analyze 3D magnetic null points. |
|
Subpackage containing routines for analyzing swept Langmuir probe traces. |
|
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
|
A layer in a detector film stack. |
|
An ordered list of |

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
|
Represents a charged particle radiography experiment with simulated or calculated E and B fields given at positions defined by a grid of spatial coordinates. |

Functions
|
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
Objects representing stacks of film and/or filter layers for charged particle detectors. |
|
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
|
Class representing a single I-V probe characteristic for convenient experimental data access and computation. |

Functions
|
Extract the section of exponential electron current growth from the probe characteristic. |
|
Extract the section dominated by ion collection from the probe characteristic. |
|
Extrapolate the electron current from the Maxwellian electron temperature obtained in the exponential growth region. |
|
Extrapolate the ion current from the ion density obtained with the OML method. |
|
Implement the Druyvesteyn method of obtaining the normalized Electron Energy Distribution Function (EEDF). |
Implement the Langmuir-Mottley (LM) method of obtaining the electron density. |
|
Obtain an estimate of the electron saturation current corresponding to the obtained plasma potential. |
|
|
Obtain the Maxwellian or bi-Maxwellian electron temperature using the exponential fit method. |
|
Implement the simplest but crudest method for obtaining an estimate of the floating potential from the probe characteristic. |
|
Implement the Langmuir-Mottley (LM) method of obtaining the ion density. |
|
Implement the Orbital Motion Limit (OML) method of obtaining an estimate of the ion density. |
|
Implement the simplest but crudest method for obtaining an estimate of the ion saturation current from the probe characteristic. |
|
Implement the simplest but crudest method for obtaining an estimate of the plasma potential from the probe characteristic. |
|
Reduce a bi-Maxwellian (dual) temperature to a single mean temperature for a given fraction. |
|
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
|
Calculate the spectral density function for Thomson scattering of a probe laser beam by a multi-species Maxwellian plasma. |
|
Returns a |
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!
|
The lite-function version of |
API
The diagnostics subpackage contains functionality for defining diagnostic parameters and processing data collected by diagnostics, synthetic or experimental.
Sub-Packages & Modules
The charged particle radiography subpackage contains functionality for analyzing charged particle radiographs and creating synthetic radiographs. |
|
Defines the Langmuir analysis module as part of the diagnostics package. |
|
Defines the Thomson scattering analysis module as part of |
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
The |
|
Module containing functionality focused on the plasma dispersion function \(Z(ζ)\). |
|
The |
Functions
|
Calculate the plasma dispersion function. |
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 (
|
|
Classical transport coefficients (e.g. Braginskii, 1965). |

Functions
|
Calculate the thermal conductivity for electrons. |
|
Calculate the electron viscosity. |
|
Calculate the thermal conductivity for ions. |
|
Calculate the ion viscosity. |
|
Calculate the resistivity. |
|
Calculate the thermoelectric conductivity. |
Examples
Collisions (plasmapy.formulary.collisions
)
The collisions
subpackage contains commonly
used collisional formulae from plasma science.
Sub-Packages & Modules
Functionality for calculating Coulomb parameters for different configurations. |
|
Module of dimensionless parameters related to collisions. |
|
Frequency parameters related to collisions. |
|
The |
|
Module of length parameters related to collisions. |
|
Module of miscellaneous parameters related to collisions. |
Classes
Compute collision frequencies between two slowly flowing Maxwellian populations. |
|
Compute collision frequencies between test particles (labeled 'a') and field particles (labeled 'b'). |

Functions
|
Collision frequency of particles in a plasma. |
|
Cross-section for a large angle Coulomb collision. |
|
Compute the Coulomb logarithm. |
|
Ratio of the Coulomb energy to the kinetic (usually thermal) energy. |
|
Average momentum relaxation rate for a slowly flowing Maxwellian distribution of electrons. |
|
Average momentum relaxation rate for a slowly flowing Maxwellian distribution of ions. |
|
Impact parameters for classical and quantum Coulomb collision. |
|
Distance of the closest approach for a 90° Coulomb collision. |
|
Knudsen number (dimensionless). |
|
Collisional mean free path (m). |
|
Return the electrical mobility. |
|
Spitzer resistivity of a plasma. |
|
Calculate the thermalization ratio for a plasma in transit, taken from Maruca et al. [2013] and Johnson et al. [2023]. |
Examples
Density Plasma Parameters (plasmapy.formulary.densities
)
Functions to calculate plasma density parameters.
Functions
|
Calculate the plasma critical density for a radiation of a given frequency. |
|
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.
|
Alias to |
Dielectric functions (plasmapy.formulary.dielectric
)
Functions to calculate plasma dielectric parameters.
Functions
|
Magnetized cold plasma dielectric permittivity tensor elements. |
|
Magnetized cold plasma dielectric permittivity tensor elements. |
|
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!
|
The lite-function for |
Examples
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
|
Compute the ratio of thermal pressure to magnetic pressure. |
|
Return the number of electrons within a sphere with a radius of the Debye length. |
|
Calculate the |
|
Compute the Lundquist number. |
|
Compute the magnetic Reynolds number. |
|
Compare Fermi energy to thermal kinetic energy to check if quantum effects are important. |
|
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.
|
Alias to |
|
Alias to |
|
Alias to |
|
Alias to |
Examples
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
|
Return the probability density at the velocity |
|
Return the probability density function for finding a particle with velocity components |
|
Probability distribution function of velocity for a Maxwellian distribution in 1D. |
|
Probability distribution function of speed for a Maxwellian distribution in 1D. |
|
Probability distribution function of speed for a Maxwellian distribution in 2D. |
|
Probability distribution function of speed for a Maxwellian distribution in 3D. |
|
Probability distribution function of velocity for a Maxwellian distribution in 2D. |
|
Probability distribution function of velocity for a Maxwellian distribution in 3D. |
Examples
Particle drifts (plasmapy.formulary.drifts
)
Functions for calculating particle drifts.
Functions
|
Calculate the diamagnetic fluid perpendicular drift. |
|
Calculate the "electric cross magnetic" particle drift. |
|
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.
|
Alias to |
|
Alias to |
|
Alias to |
Examples
Frequency Plasma Parameters (plasmapy.formulary.frequencies
)
Functions to calculate fundamental plasma frequency parameters.
Functions
|
Return the Buchsbaum frequency for a two-ion-species plasma. |
|
Calculate the particle gyrofrequency in units of radians per second. |
|
Return the lower hybrid frequency. |
|
Calculate the particle plasma frequency. |
|
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.
|
Alias to |
|
Alias to |
|
Alias to |
|
Alias to |
|
Alias to |
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!
|
The lite-function for |
Examples
Ionization related functionality (plasmapy.formulary.ionization
)
Functions related to ionization states and the properties thereof.
Functions
|
Return the average ionization state of ions in a plasma assuming that the numbers of ions in each state are equal. |
|
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.
|
Alias for |
Length Plasma Parameters (plasmapy.formulary.lengths
)
Functions to calculate fundamental plasma length parameters.
Functions
|
Calculate the exponential scale length for charge screening in an electron plasma with stationary ions. |
|
Calculate the radius of circular motion for a charged particle in a uniform magnetic field (including relativistic effects by default). |
|
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.
|
Alias to |
|
Alias to |
|
Alias to |
|
Alias to |
Examples
Magnetostatics (plasmapy.formulary.magnetostatics
)
Define MagneticStatics class to calculate common static magnetic fields as first raised in issue #100.
Classes
|
Circular wire (coil) class. |
|
Finite length straight wire class. |
|
General wire class described by its parametric vector equation. |
|
Infinite straight wire class. |
|
Simple magnetic dipole — two nearby opposite point charges. |
Abstract class for magnetostatic fields. |
|
|
Abstract wire class for concrete wires to be inherited from. |

Examples
Mathematics (plasmapy.formulary.mathematics
)
Mathematical formulas relevant to plasma physics.
Functions
|
Calculate the complete Fermi-Dirac integral. |
|
Calculates the 3D rotation matrix that will rotate vector |
Miscellaneous Plasma Parameters (plasmapy.formulary.misc
)
Functions for miscellaneous plasma parameter calculations.
Functions
|
Return the Bohm diffusion coefficient. |
Calculate the magnetic energy density. |
|
Calculate the magnetic pressure. |
|
|
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.
|
Alias to |
|
Alias to |
|
Alias to |
|
Alias to |
Quantum physics functions (plasmapy.formulary.quantum
)
Functions for quantum parameters, including electron degenerate gases and warm dense matter.
Functions
|
Calculate the ideal chemical potential. |
|
Return the de Broglie wavelength. |
|
Calculate the kinetic energy in a degenerate electron gas. |
|
Compare Fermi energy to thermal kinetic energy to check if quantum effects are important. |
Calculate the thermal de Broglie wavelength for electrons. |
|
|
Calculate the exponential scale length for charge screening for cold and dense plasmas. |
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.
|
Alias to |
|
Alias to |
|
Alias to |
Electromagnetic Radiation Functions (plasmapy.formulary.radiation
)
Functions for calculating quantities associated with electromagnetic radiation.
Functions
|
Calculate the bremsstrahlung emission spectrum for a Maxwellian plasma in the Rayleigh-Jeans limit \(ℏ ω ≪ k_B T_e\). |
Examples
Relativistic functions (plasmapy.formulary.relativity
)
Functionality for calculating relativistic quantities.
Classes
|
A physical body that is moving at a velocity relative to the speed of light. |

Functions
Return the Lorentz factor. |
|
|
Calculate the sum of the mass energy and kinetic energy of a relativistic body. |
Speed Plasma Parameters (plasmapy.formulary.speeds
)
Functions to calculate fundamental plasma speed parameters.
Functions
|
Calculate the Alfvén speed. |
|
Return the ion sound speed for an electron-ion plasma. |
|
Return the most probable speed for a particle within a kappa distribution. |
|
Calculate the speed of thermal motion for particles with a Maxwellian distribution. |
|
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.
|
Alias to |
|
Alias to |
|
Alias to |
|
Alias to |
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!
|
The lite-function for |
Examples
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
Functions to calculate classical transport coefficients. |
|
The |
|
Functions to calculate plasma density parameters. |
|
Functions to calculate plasma dielectric parameters. |
|
Module of dimensionless plasma parameters. |
|
Common distribution functions for plasmas, such as the Maxwellian or Kappa distributions. |
|
Functions for calculating particle drifts. |
|
Functions to calculate fundamental plasma frequency parameters. |
|
Functions related to ionization states and the properties thereof. |
|
Functions to calculate fundamental plasma length parameters. |
|
Define MagneticStatics class to calculate common static magnetic fields as first raised in issue #100. |
|
Mathematical formulas relevant to plasma physics. |
|
Functions for miscellaneous plasma parameter calculations. |
|
Functions for quantum parameters, including electron degenerate gases and warm dense matter. |
|
Functions for calculating quantities associated with electromagnetic radiation. |
|
Functionality for calculating relativistic quantities. |
|
Functions to calculate fundamental plasma speed parameters. |
Classes
|
Circular wire (coil) class. |
|
Classical transport coefficients (e.g. Braginskii, 1965). |
|
Finite length straight wire class. |
|
General wire class described by its parametric vector equation. |
|
Infinite straight wire class. |
|
Simple magnetic dipole — two nearby opposite point charges. |
Abstract class for magnetostatic fields. |
|
Compute collision frequencies between two slowly flowing Maxwellian populations. |
|
|
A physical body that is moving at a velocity relative to the speed of light. |
Compute collision frequencies between test particles (labeled 'a') and field particles (labeled 'b'). |
|
|
Abstract wire class for concrete wires to be inherited from. |

Functions
|
Calculate the Alfvén speed. |
|
Compute the ratio of thermal pressure to magnetic pressure. |
|
Return the Bohm diffusion coefficient. |
|
Return the Buchsbaum frequency for a two-ion-species plasma. |
|
Calculate the ideal chemical potential. |
|
Magnetized cold plasma dielectric permittivity tensor elements. |
|
Magnetized cold plasma dielectric permittivity tensor elements. |
|
Collision frequency of particles in a plasma. |
|
Cross-section for a large angle Coulomb collision. |
|
Compute the Coulomb logarithm. |
|
Ratio of the Coulomb energy to the kinetic (usually thermal) energy. |
|
Calculate the plasma critical density for a radiation of a given frequency. |
|
Return the de Broglie wavelength. |
|
Calculate the exponential scale length for charge screening in an electron plasma with stationary ions. |
|
Return the number of electrons within a sphere with a radius of the Debye length. |
|
Calculate the diamagnetic fluid perpendicular drift. |
|
Calculate the thermal conductivity for electrons. |
|
Calculate the electron viscosity. |
|
Calculate the "electric cross magnetic" particle drift. |
|
Calculate the kinetic energy in a degenerate electron gas. |
|
Calculate the complete Fermi-Dirac integral. |
|
Calculate the general force drift for a particle in a magnetic field. |
|
Average momentum relaxation rate for a slowly flowing Maxwellian distribution of electrons. |
|
Average momentum relaxation rate for a slowly flowing Maxwellian distribution of ions. |
|
Calculate the particle gyrofrequency in units of radians per second. |
|
Calculate the radius of circular motion for a charged particle in a uniform magnetic field (including relativistic effects by default). |
|
Calculate the |
|
Impact parameters for classical and quantum Coulomb collision. |
|
Distance of the closest approach for a 90° Coulomb collision. |
|
Calculate a charged particle's inertial length. |
|
Return the ion sound speed for an electron-ion plasma. |
|
Calculate the thermal conductivity for ions. |
|
Calculate the ion viscosity. |
|
Return the average ionization state of ions in a plasma assuming that the numbers of ions in each state are equal. |
|
Return the most probable speed for a particle within a kappa distribution. |
|
Return the probability density at the velocity |
|
Return the probability density function for finding a particle with velocity components |
|
Knudsen number (dimensionless). |
Return the Lorentz factor. |
|
|
Return the lower hybrid frequency. |
|
Compute the Lundquist number. |
|
Compute the magnetic Reynolds number. |
Calculate the magnetic energy density. |
|
Calculate the magnetic pressure. |
|
|
Calculate the mass density from a number density. |
|
Probability distribution function of velocity for a Maxwellian distribution in 1D. |
|
Probability distribution function of speed for a Maxwellian distribution in 1D. |
|
Probability distribution function of speed for a Maxwellian distribution in 2D. |
|
Probability distribution function of speed for a Maxwellian distribution in 3D. |
|
Probability distribution function of velocity for a Maxwellian distribution in 2D. |
|
Probability distribution function of velocity for a Maxwellian distribution in 3D. |
|
Collisional mean free path (m). |
|
Return the electrical mobility. |
|
Compute the classical dielectric permittivity for a 1D Maxwellian plasma. |
|
Calculate the particle plasma frequency. |
|
Compare Fermi energy to thermal kinetic energy to check if quantum effects are important. |
|
Calculate the sum of the mass energy and kinetic energy of a relativistic body. |
|
Calculate the resistivity. |
|
Compute the Reynolds number. |
|
Calculates the 3D rotation matrix that will rotate vector |
|
Return the ratio of populations of two ionization states. |
|
Spitzer resistivity of a plasma. |
|
Calculate the thermalization ratio for a plasma in transit, taken from Maruca et al. [2013] and Johnson et al. [2023]. |
|
Calculate the bremsstrahlung emission spectrum for a Maxwellian plasma in the Rayleigh-Jeans limit \(ℏ ω ≪ k_B T_e\). |
Calculate the thermal de Broglie wavelength for electrons. |
|
|
Return the thermal pressure for a Maxwellian distribution. |
|
Calculate the speed of thermal motion for particles with a Maxwellian distribution. |
|
Get the thermal speed coefficient corresponding to the desired thermal speed definition. |
|
Calculate the thermoelectric conductivity. |
|
Calculate the exponential scale length for charge screening for cold and dense plasmas. |
|
Return the upper hybrid frequency. |
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.
|
Alias to |
|
Alias to |
|
Alias to |
|
Alias to |
|
Alias to |
|
Alias to |
|
Alias to |
|
Alias to |
|
Alias to |
|
Alias to |
|
Alias to |
|
Alias to |
|
Alias to |
|
Alias to |
|
Alias to |
|
Alias to |
|
Alias to |
|
Alias to |
|
Alias to |
|
Alias to |
|
Alias to |
|
Alias to |
|
Alias to |
|
Alias to |
|
Alias to |
|
Alias to |
|
Alias to |
|
Alias to |
|
Alias for |
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!
|
The lite-function for |
|
The lite-function for |
|
The lite-function for |
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
Functions that retrieve or are related to elemental or isotopic data. |
|
Data used in constructing |
|
Decorators for |
|
Collection of exceptions and warnings for |
|
Objects for storing ionization state data for a single element or for a single ionization level. |
|
A class for storing ionization state data for multiple elements or isotopes. |
|
Functions that are related to nuclear reactions. |
|
Classes to represent particles. |
|
Collections of particle objects. |
|
Functionality for JSON deserialization of particle objects. |
|
Functions that deal with string representations of atomic symbols and numbers. |
Classes
An abstract base class that defines the interface for particles. |
|
Base class for particles that are defined with physical units. |
|
|
A class to represent custom particles. |
|
A class to represent dimensionless custom particles. |
|
Representation of the ionic fraction for a single ion. |
|
Representation of the ionization state distribution of a single element or isotope. |
|
Describe the ionization state distributions of multiple elements or isotopes. |
|
A class for an individual particle or antiparticle. |
|
A custom |
|
A |

Functions
|
Return the number of protons in an atom, isotope, or ion. |
|
Return the atomic symbol. |
|
Return the charge number of a particle. |
|
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. |
|
Return the electric charge (in coulombs) of a particle. |
|
Return the name of an element. |
|
Return the half-life in seconds for unstable isotopes and particles, and |
|
Return a |
|
Return the ionic symbol of an ion or neutral atom. |
|
Return |
|
Return the symbol representing an isotope. |
|
Return the isotopic abundances if known, and otherwise zero. |
|
Deserialize a JSON document into the appropriate particle object. |
|
Deserialize a JSON string into the appropriate particle object. |
|
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. |
|
Get the mass number (the number of protons and neutrons) of an isotope. |
|
Parse a molecule symbol into a |
|
Return the nuclear binding energy associated with an isotope. |
|
Return the released energy from a nuclear reaction. |
|
Convert particle-like arguments into particle objects. |
|
Return the mass of a particle. |
|
Return the symbol of a particle. |
|
Find the reduced mass between two particles. |
|
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. |
|
Return the standard (conventional) atomic weight of an element based on the relative abundances of isotopes in terrestrial environments. |
Variables & Attributes
A |
|
A |
|
A |
|
A |
|
An |
|
An |
|
A |
|
A |
|
A |
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
Abstract classes for numerical simulations. |
|
Particle movement integrators, for particle simulations. |
|
Module containing the definition for the general particle tracker. |
Classes
A prototype abstract interface for numerical simulations. |
|
A prototype abstract interface for time-dependent numerical simulations. |
|
|
A particle tracker for particles in electric and magnetic fields without inter-particle interactions. |

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
Registration class for |
|
|
Representation of the analytical Lundquist solution for force-free magnetic flux ropes [Lundquist, 1950]. |
|
A Generic Plasma class. |
|
Define a Harris Sheet Equilibrium. |

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
attr: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
|
Core class for describing and calculating plasma parameters with spatial dimensions. |
|
Class for describing and calculating plasma parameters without spatial/temporal description. |

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
Classes for representing cylindrical equilibria. |
|
Functionality for representing one-dimensional equilibria. |
|
Exceptions and warnings for functionality defined in |
|
Defines the AbstractGrid class and child classes. |
|
Module for defining the base framework of the plasma classes. |
|
Module for defining the framework around the plasma factory. |
|
Classes
Registration class for |
|
|
Representation of the analytical Lundquist solution for force-free magnetic flux ropes [Lundquist, 1950]. |
|
A Generic Plasma class. |
|
Define a Harris Sheet Equilibrium. |

Variables & Attributes
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:
Warnings and exceptions used in PlasmaPy, such as
RelativityWarning
orPhysicsError
.Decorators we use for reusable physical
Quantity
computation and checking, such asvalidate_quantities
andcheck_relativistic
.Helper utilities for importing and testing packages.
Functionality for downloading files from PlasmaPy’s data repository.
API
plasmapy.utils.decorators Package
A module to contain various decorators used to build readable and useful code.
Sub-Packages & Modules
Decorator for checking input/output arguments of functions. |
|
Decorators to convert units. |
|
Decorators to mark objects that are deprecated. |
|
Miscellaneous decorators for various package uses. |
|
|
Module for defining functionality that marks and handle Lite-Function creation. |
Various decorators to validate input/output arguments to functions. |
Classes
|
Base class for 'Check' decorator classes. |
|
A decorator class to 'check' — limit/control — the units of input and return arguments to a function or method. |
|
A decorator class to 'check' — limit/control — the values of input and return arguments to a function or method. |
|
A decorator class to 'validate' -- control and convert -- the units and values of input and return arguments to a function or method. |

Functions
A decorator that enables a function to convert its return value from angular frequency (rad/s) to frequency (Hz). |
|
|
Decorator to bind a lightweight "lite" version of a formulary function to the full formulary function, as well as any supporting attributes. |
|
Warns or raises an exception when the output of the decorated function is greater than |
|
A decorator to 'check' — limit/control — the units of input and return arguments to a function or method. |
|
A decorator to 'check' — limit/control — the values of input and return arguments to a function or method. |
|
A wrapper of |
|
A decorator which programmatically prepends and/or appends the docstring of the decorated method/function. |
A decorator for decorators, which preserves the signature of the function being wrapped. |
|
|
A decorator responsible for raising errors if the expected arguments weren't provided during class instantiation. |
|
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
An exception to be raised when the input is not a valid Roman numeral. |
|
An exception to be raised for integers that outside of the range that can be converted to Roman numerals. |
|
The base exception for physics-related errors. |
|
Base class of PlasmaPy custom errors. |
|
An exception for speeds greater than the speed of light. |
|
A base exception for errors from |

Warnings
A warning for functions that rely on a particular coupling regime to be valid. |
|
The base warning for warnings related to non-physical situations. |
|
A warning for deprecated features when the warning is intended for other Python developers. |
|
A warning for deprecated features when the warning is intended for end users of PlasmaPy. |
|
Base class of PlasmaPy custom warnings. |
|
A warning for when relativistic velocities are being used in or are returned by non-relativistic functionality. |

plasmapy.utils.code_repr Module
Tools for formatting strings, including for error messages.
Functions
|
Approximate the command to instantiate a class, and access an attribute of the resulting class instance. |
|
Approximate a call of a function or class with positional and keyword arguments. |
|
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
|
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
Contains functionality for downloading files from a URL. |
Functions
|
Download a file from a URL (if the file does not already exist) and return the full local path to the file. |
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+g7880775 (2024-02-06)
No significant changes.
PlasmaPy v2024.2.0 (2024-02-06)
Backwards Incompatible Changes
Created new
ParticleTracker
object for general particle pushing simulations involving electromagnetic fields. The tracker replaces the old~plasmapy.simulation.particletracker.ParticleTracker
. (#2245)Imports of classes and functions from
plasmapy.utils
must now be made directly from the subpackages and modules, rather than fromplasmapy.utils
itself. (#2403)Moved
mass_density
and its aliasrho_
fromplasmapy.formulary.misc
toplasmapy.formulary.densities
. (#2410)
Features
Added a
notch
argument tospectral_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 thespectral_density_model
function to allownotch
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 thecreate_particles
method of theTracker
class to make the function deterministic to resolve intermittent test failures. (#2487)
Bug Fixes
Enabled
particle_input()
to be compatible with postponed evaluation of annotations (see PEP 563). (#2479)
Improved Documentation
Switched from
sphinx_toolbox.collapse
tosphinx_collapse
for including collapsible content in our documentation. (#2387)Added a discussion of type hint annotations in the Testing Guide within the Contributor Guide. (#2440)
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 ofHDF5Reader
for context management. (#2402)Added an initial configuration for mypy that temporarily ignores existing errors. (#2424)
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 noreturn
statement. (#2439)Added a stub file containing type hint annotations for
@wrapt.decorator
. (#2442)Improved type hint annotations for
plasmapy.particles.decorators
, which includesparticle_input()
, and the corresponding tests. (#2443)Dropped the pre-commit hook for
isort
and enabled allisort
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 ingrids
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
Renamed the
plasmapy.dispersion.dispersionfunction
module toplasmapy.dispersion.dispersion_functions
. Both ofplasma_dispersion_func
andplasma_dispersion_func_deriv
are temporarily still available, but will issue aPlasmaPyFutureWarning
and will be removed in a subsequent release. (#2271)Removed the lite-functions for
plasma_dispersion_func
andplasma_dispersion_func_deriv
. Instead, the performance of the original functions has been improved by using atry
andexcept
block instead of having multipleif
statements to check for preconditions. (#2361)Providing a real number to the
charge
parameter inCustomParticle
will now result in aInvalidParticleError
instead of a deprecation warning. Now,charge
must be aQuantity
with units of electrical charge. To express the charge as a multiple of the elementary charge, provide a real number to theZ
parameter instead. (#2369)
Features
Added the
HarrisSheet
class to calculate magnetic field, current density, and plasma pressure for 1D Harris sheets. (#2068)Added module
plasmapy.dispersion.analytical.mhd_waves_
with classes for storing and calculating parameters of magnetohydrodynamic waves. (#2206)Added the
plasmapy.analysis.time_series.conditional_averaging
module including theConditionalEvents
class for calculating the conditional average and variance of time series. (#2275)Added the
ForceFreeFluxRope
class to calculate magnetic field for the Lundquist solution for force-free cylindrical equilibria. (#2289)
Bug Fixes
Fixed a bug that had been causing incorrect results in
temp_ratio
. (#2248)Enabled the
time_step
parameter inExcessStatistics
class to be aQuantity
with a unit. (#2300)
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 filedocs/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 fromdocs/common_links.rst
to theglobal_substitutions
dict
indocs/_global_substitutions.py
. (#2281)Changed
from astropy import units as u
toimport astropy.units as u
andfrom astropy import constants as const
toimport 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
todocs/_cff_to_rst.py
, and updated the functionality contained within that file for converting author information inCITATION.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 *
indocs/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
, andParticleList
objects. (#2377)
Trivial/Internal Changes
Modernized
MANIFEST.in
. (#2189)Applied automated refactorings from Sourcery. (#2219)
Distributions defined in the
distribution
module will now raise aValueError
for an improperunits
parameter. (#2229)Added “decorators” section to the Coding Guide. (#2231)
Improved the error message issued by
Alfven_speed
when the argument provided todensity
has a physical type of number density andion
is not provided. (#2262)Exposed
plasmapy.dispersion.analytical
andplasmapy.dispersion.numerical
to theplasmapy.dispersion
namespace. (#2271)Expanded the ruff settings to include more linter rules. (#2295)
Add ruff linter rules that check for
print
andpprint
, as thelogging
library is generally preferred for production code. (#2296)Updated and corrected author information in
CITATION.cff
. (#2307)Reduced the number of warnings emitted by
plasmapy.particles
during tests by decorating test functions withpytest.mark.filterwarnings
. (#2314)Fixed a
pytest
deprecation warning that had been issued byplasmapy.utils._pytest_helpers/pytest_helpers.run_test
so thatNone
is no longer passed to thepytest.warns
context manager. (#2314)Changed the default configuration for
pytest
so that if a test is marked as expected to fail actually passes, then that test will issue an error to indicate that thepytest.mark.xfail
mark can be removed. (#2315)Added a weekly linkcheck test that verifies that hyperlinks in the documentation are up-to-date. (#2328)
Enabled
validate_quantities()
to accept annotations of the formu.Quantity[u.m]
, where we have previously runimport astropy.units as u
. (#2346)Both
plasma_dispersion_func
andplasma_dispersion_func_deriv
now allowinf
andnan
arguments without raising aValueError
. (#2361)Modified the
charge_number
attribute ofCustomParticle
to return a real number rather than a dimensionlessQuantity
. (#2377)Made minor updates to
plasmapy/__init__.py
, including to the top-level package docstring. (#2378)Improved the consistency and specificity of the names of various GitHub Actions. (#2379)
Added a pre-commit hook to validate GitHub Actions. (#2380)
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 parameterm
has been replaced withparticle
, which now accepts a broader variety of particle-like arguments, including but not limited to aQuantity
representing mass. The parameterv
has been replaced withV
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
to1.21.0
,pandas
to1.2.0
,h5py
to3.1.0
,scipy
to1.6.0
,voila
to0.3.0
, andxarray
to0.17.0
. (#1885)Made
ParticleList
raise aTypeError
when provided with a string. This change was made to avoid potentially ambiguous situations likeParticleList("He")
which was previously equivalent toParticleList(["H", "e"])
instead of the possibly expected value ofParticleList(["He"])
. (#1892)In
two_fluid
,hollweg
, andkinetic_alfven
inplasmapy.dispersion
, providing the charge number as a keyword argument (nowZ
, formerlyz_mean
) will no longer override the charge number provided inion
. (#2022, #2181, #2182)particle_input()
no longer enforces that parameters namedionic_level
are ions or neutral atoms. For equivalent behavior, name the parameterion
instead. (#2034)Removed
plasmapy.utils.pytest_helpers
from PlasmaPy’s public API. It is still available asplasmapy.utils._pytest_helpers
, but might be removed in the future. (#2114)Removed
plasmapy.tests.helpers
from PlasmaPy’s public API. It is still available asplasmapy.tests._helpers
, but might be removed in the future. (#2114)The
ion_species
parameter tothermal_bremsstrahlung
has been renamed toion
in order to provide a more consistent API to functions that accept ions as arguments. (#2135)
Deprecations and Removals
In
plasmapy.dispersion
, thez_mean
parameter totwo_fluid
,hollweg
, andkinetic_alfven
has been deprecated. Provide the charge number toZ
instead. (#2022, #2181, #2182)When a function decorated with
particle_input()
is provided withz_mean
as a keyword argument, it will changez_mean
toZ
and issue aPlasmaPyDeprecationWarning
if the decorated function acceptsZ
as a parameter. This capability is intended to temporarily preserve the current behavior of several functions inplasmapy.dispersion
andplasmapy.formulary
as they get decorated withparticle_input()
over the next few releases. (#2027)The
z_mean
parameter toion_sound_speed
andAlfven_speed
has been deprecated and may be removed in a future release. UseZ
instead. (#2134, #2179)
Features
Added
kinetic_alfven
, which numerically solves dispersion relations for kinetic Alfvén waves. (#1665)Added the
stix_dispersion.ipynb
notebook which contains Stix cold-plasma dispersion examples. (#1693)Added the
Buchsbaum_frequency
function. (#1828)Decorated
gyrofrequency
withparticle_input()
so that it can accept a broader variety of particle-like arguments. (#1869)After having been decorated with
particle_input()
, therelativistic_energy
function now accepts a broader variety of particle-like objects rather than onlyQuantity
objects representing mass. (#1871)After having been decorated with
particle_input()
,RelativisticBody
now accepts a broader variety of particle-like objects. (#1871)Enabled
particle_input()
to accept values of the charge number that are real numbers but not integers. This capability can now be used by many of the functions inplasmapy.formulary
and elsewhere that are decorated withparticle_input()
. (#1884)Decorated
reduced_mass
withparticle_input()
so that it can now accept a broader variety of particle-like arguments. (#1921)Added the
plasmapy.analysis.time_series.excess_statistics
module including theExcessStatistics
class for calculating excess statistics of time series. (#1984)Added
plasmapy.formulary.collisions.helio.collisional_analysis
. (#1986)Enabled
ParticleList
to acceptQuantity
objects of physical type mass or electrical charge. (#1987)The following functions have been decorated with
particle_input()
and now accept a broader variety of particle-like arguments (see also #341):Refactored
gyroradius
to reduce cognitive complexity and increase readability. (#2031)Added
mass_numb
andZ
as parameters to functions decorated withparticle_input()
inplasmapy.formulary.lengths
andplasmapy.formulary.distribution
. (#2140)
Bug Fixes
When attempting to create a
Particle
object representing a proton, calls likeParticle("H", Z=1, mass_numb=1)
no longer incorrectly issue aParticleWarning
for redundant particle information. (#1992)Updated the docstring of
kinetic_alfven
. (#2016)Fixed a slight error in
plasma_frequency
andAlfven_speed
when the charge number was provided viaz_mean
(or nowZ
) and inconsistent with the charge number provided toparticle
(or zero, ifparticle
represented an element or isotope with no charge information. Previously, if we represented a proton withparticle="H-1"
andz_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, usingparticle="p+"
would have produced the correct mass. This behavior has been corrected by decorating this function withparticle_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/
todocs/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 indocs/about/credits.rst
. (#2155)Added functionality to automatically generate the author list included in
docs/about/credits.rst
directly fromCITATION.cff
. The script is located atdocs/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 byparticle_input()
to createCustomParticle
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 byparticle_input()
. (#1884)Dropped
dlint
from the tests requirements, as it is no longer being maintained. (#1906)Modified
particle_input()
to allowCustomParticle
-like objects with a defined charge to be passed through to decorated functions when a parameter to that function annotated withParticleLike
is namedion
. Previously, onlyParticle
objects representing ions or neutral atoms were allowed to pass through when the parameter was namedion
. (#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
toPIP_INDEX_URL
to index nightlynumpy
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 usewrapt
. (#2175)
PlasmaPy v2023.1.0 (2023-01-13)
Backwards Incompatible Changes
Moved the charged particle radiography analysis codes into a new module
charged_particle_radiography
containing synthetic radiography tools insynthetic_radiography
and detector stack calculation tools indetector_stacks
. (#1274)Changed the
gyroradius
function so that it takes relativistic effects into account by default. (#1813)
Deprecations and Removals
Changed the
gyroradius
function so it no longer accepts deprecatedT_i
. (#1824)Removed
plasmapy.formulary.parameters
, which was deprecated in the0.7.0
release. The functionality in that module had previously been migrated to modules that are broken down by physical type, such as:plasmapy.formulary.densities
,plasmapy.formulary.dimensionless
,plasmapy.formulary.frequencies
,plasmapy.formulary.lengths
,plasmapy.formulary.misc
, andplasmapy.formulary.speeds
. (#1833)Deprecated providing a real number to the
charge
parameter ofCustomParticle
to represent the charge number. UseZ
instead. (#1866)
Features
Added the
Stack
andLayer
objects to thecharged_particle_radiography
module, which represent a stack of detector media layers. Thedeposition_curves
andenergy_bands
methods ofStack
calculate the particle energies deposited in each detector layer. (#1274)Tracker
now supports multiple field grids, provided as an iterable. (#1799)Added the
plasmapy.analysis.time_series.running_moments
module including two functions for calculating running moments of time series. (#1803)Added
lorentzfactor
as an optional keyword-only argument togyroradius
. Also addedrelativistic
as an optional keyword-only argument which can be set toFalse
for the non-relativistic approximation. (#1813)Modified
Particle
attributes to returnnan
in the appropriate units when undefined rather than raising exceptions. (#1825)Added the
charge_number
attribute toCustomParticle
. (#1866)Added
Z
as a keyword-only parameter representing the charge number toCustomParticle
. (#1866)
Improved Documentation
Updated docstrings and annotations in
plasmapy.diagnostics.thomson
. (#1756)Updated the discussion on type descriptions and parameter descriptions for docstrings in the Documentation Guide. (#1757)
Updated troubleshooting sections of the Documentation Guide. (#1817)
Added a summary section to the Testing Guide. (#1823)
Updated the Changelog Guide. (#1826)
Reorganized the Coding Guide. (#1856)
Added a documentation page on performance tips. (#1887)
Trivial/Internal Changes
Updated warning messages in
Coulomb_logarithm
. (#1586)Transferred most of the contents of
setup.py
andsetup.cfg
topyproject.toml
(see PEP 518 and PEP 621). Simplifiedextras
requirements (pip install plasmapy[all]
and[extras]
are gone). (#1758)Added blacken-docs to the pre-commit configuration. (#1807)
Removed
pytest-xdist
from the testing requirements (see also #750). (#1822)Refactored tests of
Lorentz_factor
andrelativistic_energy
. (#1844)Applied refactorings from ruff and
refurb
toplasmapy.utils
. (#1845)Applied changes from
refurb
toplasmapy.particles
. (#1846)Applied changes from
refurb
toplasmapy.formulary
. (#1847)Apply changes from ruff and
refurb
toplasmapy.analysis
,plasmapy.diagnostics
,plasmapy.dispersion
, andplasmapy.plasma
. (#1853)Added the
strict
andallowed_physical_types
parameters toplasmapy.utils._units_helpers._get_physical_type_dict
. (#1880)Added a private constructor method to
CustomParticle
with an API that is better suited for use inParticleList
and the particle factory function used byparticle_input()
. (#1881)Dropped the dependency on
cached-property
in favor offunctools.cached_property
. (#1886)
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
Removed the
none_shall_pass
parameter fromparticle_input()
. Instead,typing.Optional
should be used to create the annotation (e.g.,Optional[ParticleLike]
). (#1057)Renamed the first parameter of
particle_input()
fromwrapped_function
towrapped
. (#1057)Refactored the return pattern of
find_floating_potential
to follow the(vf, extras)
pattern, wherevf
is the computed floating potential andextras
is an instance ofVFExtras
containing extra parameters from the computation. (#1565)Moved
plasmapy.particles.particle_collections.ionic_levels
toplasmapy.particles.atomic.ionic_levels
. (#1697)Deprecated
plasmapy.formulary.collisions.fundamental_electron_collision_freq
. TheMaxwellian_avg_ei_collision_freq
attribute ofMaxwellianCollisionFrequencies
should be used instead. (#1703)Deprecated
plasmapy.formulary.collisions.fundamental_ion_collision_freq
. TheMaxwellian_avg_ii_collision_freq
attribute ofMaxwellianCollisionFrequencies
should be used instead. (#1703)The parameters
Z
andmass_numb
are now keyword-only forionic_symbol
andparticle_symbol
. (#1718)Moved the
valid_categories
attribute ofis_category
toplasmapy.particles.particle_class.valid_categories
. (#1720)Changed the behavior of
IonicLevel
,IonizationState
, andIonizationStateCollection
so that an equality comparison with anobject
of a different type returnsFalse
instead of raising aTypeError
. (#1721)When the argument provided to
GeneralWire
is not callable, aTypeError
will be raised instead of aValueError
. (#1782)In
spectral_density
andspectral_density_model
, aTypeError
is now raised whenions
is an unsupported type. (#1782)In
AbstractGrid
, aTypeError
is now raised instead of aValueError
whenstart
orstop
are not of the appropriate type. (#1783)
Deprecations and Removals
The capability of
particle_input()
to process arguments annotated withParticle
or(Particle, Particle)
is now deprecated and will be removed in a future release. UseParticleLike
as an annotation instead. (#1057)The
integer_charges
attribute ofIonizationState
has been removed after being deprecated inv0.7.0
. Usecharge_numbers
instead. (#1633)The
integer_charge
attributes ofParticle
andIonicLevel
have been removed after being deprecated inv0.7.0
. Use thecharge_number
attribute instead. (#1633)The
plasmapy.particles.atomic.integer_charge
function has been removed after being deprecated inv0.7.0
. Usecharge_number
instead. (#1633)Deprecated
plasmapy.formulary.collisions.frequencies.collision_frequency
in favor of collision frequency classes inplasmapy.formulary.collisions
. See also #1703. (#1676)
Features
Expanded the functionality of the
particle_input()
decorator to convert particle-like and particle-list-like arguments intoParticle
,CustomParticle
, andParticleList
objects. This change is part of an ongoing effort to improve compatibility of functions in subpackages likeplasmapy.particles
andplasmapy.formulary
withCustomParticle
andParticleList
objects. (#1057)Added the
find_ion_saturation_current
function to theswept_langmuir
module. The function fits the tail of a swept Langmuir probe trace and returns the linear fit corresponding to the ion-saturation current. (#1469)Created
plasmapy.utils.data
to contain functionality for downloading data from PlasmaPy’s data repository. This module contains a new prototype functionplasmapy.utils.data.downloader.get_file
which downloads a file from the repository. (#1486)Added the
RelativisticBody
class to facilitate calculation of the relativistic properties of a body in motion. (#1540)Added
inplace
as an optional argument toboris_push
. (#1556)Added a function to calculate the dimensionless Lundquist number. (#1642)
Created the
plasmapy.formulary.densities
module. (#1664)Added
critical_density
to calculate the critical density of a plasma for a given frequency of radiation. (#1664)Added the
plasmapy.formulary.collisions.CollisionFrequencies
class. This class can be used to calculate collision frequencies for two interacting species in a plasma. Superseded by #1703. (#1676)Reimplemented
chemical_potential
. (#1678)Allowed
Lorentz_factor
to accept and returnnan
values. (#1681)Added a test for
Hall_parameter
inplasmapy/formulary/test/test_dimensionless.py
. (#1689)Replaced usage of
os.path
with the more modernpathlib
. (#1690)Replaced
pkg_resources
with the more modernimportlib.metadata
. (#1692)Added the
categories
attribute toCustomParticle
, and added the"custom"
particle category. (#1700)Moved the
is_category
method ofParticle
toAbstractPhysicalParticle
. This method is now inherited by bothParticle
andCustomParticle
. (#1700)Added
MaxwellianCollisionFrequencies
for calculating relevant collision frequencies for Maxwellian populations. (#1703)Refactored
collisions
. The filecollisions.py
was converted into a subpackage (directory) and it’s contents was split into appropriately categorized and named sub-modules (files). (#1769)
Bug Fixes
Modified tests in the class
TestSyntheticRadiograph
to try to fix an intermittent failure oftest_optical_density_histogram
. (#1685)
Improved Documentation
Added the Hollweg dispersion notebook. (#1392)
Creates an example notebook for fitting Thomson scattering spectra using the
spectral_density_model
function. (#1520)Updated the Release Guide following the
0.8.1
release. (#1615)Added
docs/whatsnew/dev.rst
as a stub file for the changelogs between releases. (#1623)Added customizations for towncrier in
pyproject.toml
. (#1626)Updated the introductory paragraphs of the Coding Guide. (#1649)
Added a section to the Coding Guide on best practices for naming variables. (#1650)
Updated the section of the contributor guide on pre-commit, and moved it to
docs/contributing/install_dev.rst
. (#1651)Added sections to the Coding Guide on units and particles. (#1655)
Updated the section of the Coding Guide on code style. (#1657)
Added sections to the Coding Guide on lite-functions and aliases. (#1658)
Added sections to the Coding Guide on imports and requirements. (#1659)
Added sections on best practices for comments and error messages to the Coding Guide. (#1660)
Updated the section of the Documentation Guide with more detail on the “Parameters”, “Raises”, and “Warns” sections of docstrings. (#1667)
Added a guideline to the Coding Guide specifying how
nan
values should be treated in functions that accept array_like orQuantity
inputs. (#1673)Added an admonition to the Changelog Guide that describes how to change reStructuredText links for removed code objects into inline literals in old changelog entries. (#1674)
Split the patent clause from the license file (
LICENSE.md
) into its own file (PATENT.md
). (#1686)Added explanatory text to the “Notes” sections in the docstrings for functions within
magnetostatics
. (#1695)Enabled
:py:
as a reStructuredText role for inline code formatting in the documentation. (#1698)Updated docstrings and annotations for
ParticleList
and its methods. (#1713)Updated docstrings and annotations in
plasmapy.particles
, including by marking parameters as particle-like or atom-like. (#1718)Added a section to the Documentation Guide on troubleshooting. (#1752)
Trivial/Internal Changes
Moved the functionality responsible for converting particle-like arguments to particle objects from the
particle_input()
decorator into a separate class that is now used insideparticle_input()
. (#1057)The
particle_input()
decorator now processes arguments annotated withParticleLike
. (#1057)Added
tomli
to thetests
category of requirements. (#1500)Added tests to verify that the requirements given in the
.txt
files in therequirements
directory are consistent with the requirements given insetup.cfg
andpyproject.toml
. (#1500)Restricted the required version of sphinx-gallery to
< 0.11.0
, sincesphinx-gallery
changed their thumbnail containers to flex containers. See pull request sphinx-gallery/#906 and issue sphinx-gallery/#905 for more detail. (#1654)Moved the
plasmapy.formulary.dimensionless.quantum_theta
function toplasmapy.formulary.quantum.quantum_theta
. This function can still be called from theplasmapy.formulary.dimensionless
module without issue. (#1671)Reimplemented
plasmapy.formulary.quantum._chemical_potential_interp
. (#1678)Re-enabled value testing for the
quantum
keyword argument incoupling_parameter
. (#1678)Added the
validate_class_attributes
decorator to thedecorators
module. This decorator is useful for class methods that require optional parameters to be specified during class instantiation. (#1703)Made minor improvements to
plasmapy.formulary.collisions.CollisionFrequencies
. (#1705)Changed the towncrier requirement to
>= 19.2.0, < 22.8.0
. Superseded by #1717. (#1710)Changed the minimum version of towncrier to 22.8.0 and the minimum version of
sphinx_changelog
to 1.2.0. (#1717)Changed
chemical_potential
to use the Broyden-Fletcher-Goldfarb-Shanno algorithm to implicitly solve for the ideal chemical potential. (#1726)Simplified the pull request template. (#1729)
Added a GitHub Action to automatically comment on pull requests with a code review checklist. (#1729)
The following functions are now decorated by
particle_input()
:Hall_parameter
,kappa_velocity_1D
,kappa_velocity_3D
,Maxwellian_1D
,Maxwellian_velocity_2D
,Maxwellian_velocity_3D
,Maxwellian_speed_1D
,Maxwellian_speed_2D
,Maxwellian_speed_3D
,gyroradius
, anddeBroglie_wavelength
. (#1732)Changed
particle_input()
to raise aUnitConversionError
when the annotated argument has a physical type other than mass or electrical charge. (#1732)Set up issue forms on PlasmaPy’s GitHub repository to replace issue templates. (#1733)
Made
pytest
aninstall
requirement instead of atesting
requirement. (#1749)Added a step to validate
CITATION.cff
as part of thelinters
tox testing environment. (#1771)Added
cffconvert
to thetesting
requirements. (#1771)Deleted
codemeta.json
, which recorded project metadata using the CodeMeta metadata schema. Instead, project metadata is now stored inCITATION.cff
which uses the Citation File Format and was created in #1640. See also #676 and #794. (#1772)Added the
flake8
extensionsflake8-use-pathlib
,flake8-builtins
, andflake8-comments
to the testing requirements. (#1777)Added
tryceratops
as aflake8
extension. (#1782)
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 argumentsTe
andTi
have been renamedT_e
andT_i
and are now required keyword-only arguments. (#974)Moved the
grid_resolution
attribute fromAbstractGrid
toCartesianGrid
andNonUniformCartesianGrid
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 ofCustomParticle
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 toplasmapy.particles._special_particles
) was executed. (#1440)Renamed
plasmapy.particles.elements
toplasmapy.particles._elements
,plasmapy.particles.isotopes
toplasmapy.particles._isotopes
,plasmapy.particles.parsing
toplasmapy.particles._parsing
, andplasmapy.particles.special_particles
toplasmapy.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 forplasmapy.particles.special_particles.ParticleZoo
which was renamed toplasmapy.particles._special_particles.particle_zoo
and removed from the public API. (#1440)The parameters
Z
andmass_numb
toParticle
are now keyword-only. (#1456)
Deprecations and Removals
Officially deprecated
plasmapy.formulary.parameters
and scheduled its permanent removal for thev0.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 installplasmapy
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 runningpip install plasmapy
. To install all packages required for code development of PlasmaPy, instead runpip install plasmapy[developer]
. (#1482)Removed
plasmapy.optional_deps
. (#1482)
Features
spectral_density
andspectral_density_model
now supportparticle_collections
objects as input to theions
keywords. (#974)Created a lite-function for
spectral_density
,spectral_density_lite
. (#974)Added a fitting function for 1D spectra,
spectral_density_model
, to the Thomson scattering diagnostic module. (#974)Created function
plasmapy.formulary.parameters.thermal_speed_coefficients
to supportplasmapy.formulary.parameters.thermal_speed_lite
usage by calculating the various thermal speed coefficients.plasmapy.formulary.parameters.thermal_speed_coefficients
is also bound toplasmapy.formulary.parameters.thermal_speed
as thecoefficients
attribute. (#1145)Created decorator
bind_lite_func
for handling the binding of lite-functions and any supporting functions to a parent function. (#1145)Introduced the concept of lite-functions, by creating the lite-function
plasmapy.formulary.parameters.thermal_speed_lite
, which is a simplified and Numba jitted version ofplasmapy.formulary.parameters.thermal_speed
. These functions are intended for computational use and as such have no validation of input or output values.plasmapy.formulary.parameters.thermal_speed_lite
is also bound toplasmapy.formulary.parameters.thermal_speed
as thelite
attribute. (#1145)Added the
hollweg_.py
module to thenumerical
subpackage to numerically solve the dispersion relation using Hollweg’s method [Bellan, 2012, Hollweg, 1999]. (#1189)Implemented non-breaking speed improvements on the methods
nearest_neighbor_interpolator
andvolume_averaged_interpolator
forCartesianGrid
. The new interpolators now require that the grid axes be sorted, which is always true for uniform grids. Added a new test to ensure this stays true. (#1295)Refactored the interpolator methods on objects defined in
grids
. All interpolators are now defined in the subclasses ofAbstractGrid
. Calling the interpolator methods onAbstractGrid
raises aNotImplementedError
exception. (#1295)Created lite-function
plasmapy.formulary.parameters.plasma_frequency_lite
. (#1308)Added the
molecule
function to buildCustomParticle
objects from astr
representing a molecule symbol. (#1309)Added the
is_category
method forParticleList
objects. This method is analogous to theis_category
method forParticle
objects. (#1378)Created the prototype analysis tool
plasmapy.analysis.nullpoint
for finding the null points in a vector space using the trilinear interpolation method of Haynes and Parnell [2007]. (#1383)Created
plasmapy.formulary.lengths
to contain length related plasma parameters, and migratedDebye_length
,gyroradius
, andinertial_length
fromplasmapy.formulary.parameters
to the new module. Related aliases were also migrated. (#1434)Created
plasmapy.formulary.frequencies
to contain frequency related plasma parameters, and migratedgyrofrequency
,plasma_frequency
,plasma_frequency_lite
,lower_hybrid_frequency
, andupper_hybrid_frequency
fromplasmapy.formulary.parameters
to the new module. Related aliases were also migrated. (#1439)Migrated
Debye_number
, andHall_parameter
fromplasmapy.formulary.parameters
toplasmapy.formulary.dimensionless
. Related aliases were also migrated. (#1444)Created
plasmapy.formulary.speeds
to contain frequency related plasma parameters, and migratedAlfven_speed
,ion_sound_speed
,kappa_thermal_speed
,thermal_speed
,thermal_speed_coefficients
, andthermal_speed_lite
fromplasmapy.formulary.parameters
to the new module. Related aliases were also migrated. (#1448)Created
plasmapy.formulary.misc
to contain functionality for miscellaneous plasma parameters, and migrated~plasmapy.formulary.misc._grab_charge
,Bohm_diffusion
,magnetic_energy_density
,magnetic_pressure
,plasmapy.formulary.misc.mass_density
, andthermal_pressure
fromplasmapy.formulary.parameters
to the new module. Related aliases were also migrated. (#1453)Created lite-functions
plasmapy.dispersion.dispersion_functions.plasma_dispersion_func_lite
andplasmapy.dispersion.dispersion_functions.plasma_dispersion_func_deriv_lite
forplasma_dispersion_func
andplasma_dispersion_func_deriv
respectively. (#1473)Created lite-function
plasmapy.formulary.dielectric.permittivity_1D_Maxwellian_lite
forplasmapy.formulary.dielectric.permittivity_1D_Maxwellian
. (#1476)Added the
stix_.py
module to theanalytical
subpackage which contains the Stix cold-plasma dispersion solutionstix()
, [Bellan, 2012, Stix, 1992]. (#1511)Particle("Li").ionize()
no longer results in aChargeError
. Instead, ionization of a neutral atom is assumed. (#1514)Created the
ParticleListLike
typing construct and added particle-list-like to the Glossary. (#1528)Added a null point classifier function which determines the type of a given 3D magnetic null point. (#1554)
Added support for arbitrarily shaped input arrays to the function
plasmapy.formulary.collisions.lengths.impact_parameter
. (#1604)
Bug Fixes
Fixed a bug in the
_make_grid
method ofAbstractGrid
that would fail to smoothly handle invalid user input if thestart
,stop
, ornum
keywords were not the correct type. (#1295)Fixed a bug with
Particle
whereParticle("p+") == Particle("H", Z=1, mass_numb=1)
led to aParticleError
. (#1366)For
plasmapy.formulary.parameters.gyroradius
, updated the default keyword arguments and conditional for issuing thePlasmaPyFutureWarning
. This addresses the incorrect behavior where aValueError
is raised if an array is passed to the deprecated keywordT_i
. (#1430)Exposed
plasmapy.formulary.misc
to theplasmapy.formulary
namespace. (#1471)Replaced misuse of
max_exp_bias - max_exp_bias
withmax_exp_bias - min_exp_bias
when creating seed parameters for the bimaxwellian fit function insideget_electron_temperature()
. (#1487)Corrected the improper inversion of the electron temperature for the non-bimaxwellian case for
get_electron_temperature()
. The electron temperature, and not the slope, is a fit parameter of the curve used byget_electron_temperature()
, so there is no need for the inversion. The returned value is now the electron temperature and not its reciprocal. (#1487)Exposed the
analysis
anddispersion
subpackages to theplasmapy
namespace. (#1512)Changed the
curve_fit()
method onplasmapy.analysis.fit_functions.Linear
so that the arbitrary keyword arguments get passed toscipy.stats.linregress
. Previously,curve_fit()
had accepted arbitrary keyword arguments but did not pass them along tolinregress
. (#1518)Fixed a bug in
hollweg()
that did not allow for argumentstheta
andk
to simultaneously be arrays. (#1529)Fixed the
Z
dependence infundamental_electron_collision_freq
, by replacingn_e
withn_i
while callingcollision_frequency
. (#1546)Updated the regular expression matching used by
Particle
to parse and identify a particle-like string. This fixes the bug where a string with a trailing space (e.g."Ar "
) was converted into a negatively charged ion (e.g."Ar -1"
). (#1555)Exposed
plasmapy.formulary.radiation
and functions therein to theplasmapy.formulary
namespace. (#1572)
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 todocs/_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
intodocs/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 ofpygments
was set to2.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)
Removed the following files from
docs/api_static
:plasmapy.particles.elements.rst
,plasmapy.particles.isotopes.rst
,plasmapy.particles.parsing.rst
, andplasmapy.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
tov4.4
. (#1488)Defined the
nitpick_ignore_regex
configuration variable indocs/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 theplasmapy.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 thebuild_docs_nitpicky
tox environment, and updated the Documentation Guide accordingly. (#1587)Renamed the
magnetic_statics.ipynb
notebook tomagnetostatics.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 usinggit log
and the pull request history. (#1599)Renamed
docs/development
→docs/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 butstable
andlatest
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
toplasmapy/formulary/tests/test_thermal_speed.py
. (#1145)Applied reStructuredText substitutions for
plasmapy.particles
andParticleTracker
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 toParticle
,CustomParticle
, orParticleList
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
andDimensionlessParticle
no longer emit a warning when the charge and/or mass is not provided and got assigned a value ofnan
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.,ParticleZooClass
→ParticleZoo
,ParticleZoo
→particle_zoo
, andParticles
→particles
). (#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
to3.0.0
. (#1465)Changed the raised exception to
ImportError
(from a generalException
) when attempting to importplasmapy
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 namednull_point_find
andplasmapy.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
to1.19.0
,pandas
to1.0.0
,pytest
to5.4.0
,scipy
to1.5.0
, and xarray to0.15.0
. (#1482)Moved h5py, lmfit, mpmath, and Numba out of the
extras
requirements category and into theinstall
requirements category. These packages are now installed when runningpip install plasmapy
. (#1482)Added
dlint
,flake8
,flake8-absolute-import
,flake8-rst-docstrings
,flake8-use-fstring
,pydocstyle
, andpygments
into thetests
requirements category and pre-commit into theextras
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 byrequirements.txt
when creating a Conda environment. (#1482)Used
contextlib.suppress
to suppress exceptions, instead oftry
&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 inplasmapy/formulary/distribution.py
. (#1531)Replaced
except Exception
clauses informulary
,particles
, andutils
with specific exception statements. (#1541)Added tests for passing array valued
k
andtheta
arguments tohollweg()
, 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 fromcollision_frequency
that activated when the colliding particles were both electrons. (#1570)Changed the type hints for
z_mean
inplasmapy.formulary.collisions
functions fromastropy.units.dimensionless_unscaled
toReal
. Consequently,z_mean
will no longer be processed byvalidate_quantities
. Previously,z_mean
issued a warning when a real number was provided instead of a dimensionlessQuantity
. (#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 ** b
→a**b
). (#1582)Renamed
units_definitions
to_units_definitions
andunits_helpers
to_units_helpers
inplasmapy.utils
to mark these modules as private. (#1587)Updated the
codemeta.json
file with metadata for the version0.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_
toplasmapy.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
toplasmapy.diagnostics.charged_particle_radiography
, and renamed theSyntheticProtonRadiograph
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
thevolume_averaged_interpolator
now returnsnumpy.nan
values for any interpolation not bounded by the grid points. (#1173)Renamed file
two_fluid_dispersion.py
totwo_fluid_.py
and moved it into theplasmapy.dispersion.analytical
subpackage. The functiontwo_fluid_dispersion_solution()
contained within that file was renamed totwo_fluid
. (#1208)Changed
ParticleList
so that if it is provided with no arguments, then it creates an emptyParticleList
. This behavior is analogous to howlist
andtuple
work. (#1223)Changed the behavior of
Particle
in equality comparisons. Comparing aParticle
with an object that is not particle-like will now returnFalse
instead of raising aTypeError
. (#1225)Changed the behavior of
CustomParticle
so that it returnsFalse
when compared for equality with another type. Previously, aTypeError
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”. Theinteger_charge
attribute ofParticle
has been deprecated in favor ofcharge_number
. Theinteger_charge
attribute ofIonicLevel
(formerlyIonicFraction
) has been deprecated in favor ofcharge_number
. Theinteger_charges
attribute ofIonizationState
has been deprecated in favor ofcharge_numbers
. (#1136)The
particle
attribute ofParticle
has been removed after having been deprecated in 0.6.0. (#1146)Use more generalized keyword argument
T
instead ofT_i
inplasmapy.formulary.parameters.gyroradius
. TheT_i
argument has been deprecated and will be removed in a subsequent release. (#1210)
Features
Added the
average_ion
method toIonizationState
. (#1028)Added the
average_ion
method toIonizationStateCollection
. (#1028)Added the
plasmapy.formulary.mathematics.Chandrasekhar_G
function, which is helpful in neoclassical transport theory. This change was reverted in #1233. (#1084)Enabled slicing of
IonizationState
instances to return a list ofIonicLevel
instances. (#1130)IonizationState
instances can now be compared to anIonizationState
of a different element without raising an exception. (#1130)Allowed
len
to be used onIonizationState
instances. (#1130)IonicLevel
andIonizationState
now accept an additional, optional ion temperature argument for each of the ionic levels. (#1130)Added the :meth:
~plasmapy.diagnostics.charged_particle_radiography.Tracker.save_results
method to~plasmapy.diagnostics.charged_particle_radiography.Tracker
for saving results to the.npz
file format (seenumpy.lib.format
for details on the file format). (#1134)Added the
plasmapy.utils.decorators.deprecation
module. The module includesdeprecated
, which is a decorator that is based onastropy.utils.decorators.deprecated
. (#1136)Created the
to_list
method ofIonizationState
to provide aParticleList
instance that contains the different ionic levels. (#1154)The behavior of the function
plasmapy.formulary.parameters.gyroradius
has been changed. Ifnumpy.nan
values are provided forT_i
orVperp
, then instead of raising a slightly misleading error,numpy.nan
in the appropriate units is returned. (#1187)Added the
average_particle
method toParticleList
. This method returns a particle with the mean mass and charge of theParticleList
. Theuse_rms_charge
anduse_rms_mass
keyword arguments make this method calculate the root mean square charge and mass, respectively. Theabundances
keyword argument allows the calculation of the mean or root mean square to be weighted. (#1204)Restructured the
plasmapy.dispersion
subpackage by creating theanalytical
subpackage to contain functionality related to analytical dispersion solutions. (#1208)Implemented
__eq__
,__ne__
and__hash__
to allowCustomParticle
instances to be used asdict
keys. (#1216)Added the
plasmapy.particles.particle_collections.ionic_levels
function to create aParticleList
initialized with different ionic levels of an element or isotope. (#1223)
Bug Fixes
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 keywordoptical_density=True
will now returnnumpy.inf
where the source profile intensity is zero. Previously, an incorrect value was returned since zero entries were replaced with values of1
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 replacesphinx_automodapi
.plasmapy_sphinx
creates directivesautomodapi
andautomodsumm
to replace the same directives defined bysphinx_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 Sphinxv4.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 thetoctree
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 forpygments
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
andsphinx_rtd_theme >= 1.0.0
.docutils == 0.17
is not compatible withsphinx_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]
orpip 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 newpre-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 configurationflake8.per-file-ignores=plasmapy/formulary/__init__.py:F403
to ignore warnings resulting from imports likefrom xx import *
. (#1127)Re-enabled several
flake8
checks by removing the following codes from theflake8.extend-ignore
configuration insetup.cfg
:D100
,D102
,D103
,D104
,D200
,D210
,D301
,D401
,D407
,D409
,D412
,E712
,E713
,F403
,F541
,RST213
,RST306
, andRST902
. 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)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 thebuild_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 forastropy.units
(which was already the case for"unit"
and"units"
). (#1213)Bumped the Python version for Read the Docs builds from
3.7
to3.8
. (#1248)Refactored
plasmapy/dispersion/tests/test_dispersion.py
to usehypothesis
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
insideIonizationStateCollection
withgetattr
. (#1280)Added using dlint to the
linters
testing environment intox.ini
as a static analysis tool to search for security issues. (#1280)Enabled using flake8-use-fstring in the
linters
testing environment intox.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 oftwo_fluid
from degrees to radians. (#1301)Replaced usage of
distutils.version.StrictVersion
withpackaging.version.Version
becausedistutils
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 theplasmapy.particles.IonicFraction
class. (Note: #1046 subsequently changed that toIonicLevel
). (#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 toParticleError
andMissingAtomicDataError
was renamed toMissingParticleDataError
. (#796)In
plasmapy.particles
, theIonizationStates
class was renamed toIonizationStateCollection
. Argumentn
ofIonizationStates
was changed ton0
inIonizationStateCollection
. (#796)Moved and refactored error message formatting functionality from
plasmapy.utils.error_messages
toplasmapy.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 functionsCoulomb_logarithm
andimpact_parameter
, and then propagated throughout the functionality inplasmapy.formulary.collisions
. (#962)Add dependency
pandas >= 1.0.0
. Modifyxarray
dependency to bexarray >= 0.14.0
. (#963)The
AbstractGrid
propertygrid
is now dimensioned (has units) and cannot be accessed if all dimensions do not share the same units. (#981)Renamed attribute
is_uniform_grid
onAbstractGrid
tois_uniform
. (#981)Drop Python 3.6 support. (#987)
The
__getitem__
method ofAbstractGrid
now returns aQuantity
array instead of a reference to axarray.DataArray
. (#1027)Renamed
IonicFraction
toIonicLevel
. This lays groundwork for future changes, where that class is going to become more than a fraction. (#1046)
Deprecations and Removals
The
particle
attribute ofParticle
has been deprecated in favor of the newsymbol
attribute. Theparticle
attribute now issues aFutureWarning
to indicate that it will be removed in a future release. (#984)
Features
Created the
AbstractNormalizations
class to serve as an abstract interface for future classes that represent normalizations. (#859)Create the analysis sub-package
plasmapy.analysis.swept_langmuir
for analysis code related to analyzing swept Langmuir traces. Sub-package is initiated with functionality for calculating the floating potential,find_floating_potential
. (#889)Added a proton radiography diagnostic module containing a tool for generating synthetic proton radiographs from simulated or calculated fields using a particle tracking algorithm. (#895)
Created new grid objects for representing plasma quantities as functions of space. (#909)
Added functions in
plasmapy.utils.code_repr
to reproduce strings that represent a call to a method or attribute of an object. These functions are used, for example, in error messages. (#920)Add the function
plasmapy.dispersion.two_fluid_dispersion.two_fluid_dispersion_solution
toplasmapy.dispersion
, which gives an analytical solution to the dispersion relation as derived by Bellan [2012]. (#932)Refactor out the
boris_push
tracking integrator algorithm fromplasmapy.simulation.particletracker.ParticleTracker
. (#953)For
plasmapy.plasma.grids
functionality, add better support for recognizing and handling physical quantities (e.g. spatial position, magnetic field, etc.) added to a grid object. (#963)For
plasmapy.plasma.grids
functionality, improve interpolation performance on non-uniform grids. (#963)Added the
diamagnetic_drift
function todrifts
. (#972)Add properties
grid_resolution
andquantities
toAbstractGrid
. (#981)Make several speed improvements to the functionality in
grids
, including the addition of keywordpersistent
toAbstractGrid
(and child class) methodsnearest_neighbor_interpolator
andvolume_averaged_interpolator
. This keyword allows the interpolators to assume the last grid setup and contents if input arguments have not changed. (#981)Add methods
on_grid
andvector_intersects
toAbstractGrid
. (#981)The
Particle
class now contains an attribute namedsymbol
that is intended to replaceparticle
. Thesymbol
attribute has been added as a property toAbstractParticle
,CustomParticle
, andDimensionlessParticle
. (#984)Added new
can_be_zero
check parameter toCheckValues
and its dependents (check_values
,ValidateQuantities
,validate_quantities
). (#999)Both
plasmapy.particles.particle_class.CustomParticle
andplasmapy.particles.particle_class.DimensionlessParticle
now allow users to define a custom symbol via thesymbol
keyword argument, which can then be accessed by thesymbol
attribute in each of these classes. (#1015)The greater than (
>
) operator can now be used betweenParticle
and/orParticleList
instances to get the nuclear reaction energy. (#1017)Create
plasmapy.particles.particle_collections.ParticleList
as a list-like collection for instances ofplasmapy.particles.particle_class.Particle
andplasmapy.particles.particle_class.CustomParticle
. AddingParticle
and/orCustomParticle
instances will now create aParticleList
. (#1017)Added method
require_quantities
toAbstractGrid
that verifies a list of quantities is present on the grid. Method is also incorporated intoplasmapy.diagnostics.proton_radiography.SyntheticProtonRadiograph
. (#1027)Added the
add_wire_mesh()
method toplasmapy.diagnostics.proton_radiography.SyntheticProtonRadiograph
to allow the creation of synthetic proton radiographs that include a wire mesh reference grid. (#1049)Created a function,
rot_a_to_b
, that calculates the rotation matrix that will rotate one 3D vector onto another. (#1054)Made
is_uniform
a properly-documented public attribute ofAbstractGrid
. (#1072)
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
orIonizationStateCollection
instance. (#1025)Fixed a bug in
grids.py
for non-uniform grids that arose whenxarray
upgraded tov0.17.0
(#1027)In
plasmapy.diagnostics.proton_radiography.SyntheticProtonRadiograph
, adaptivedt
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 usingplasmapy.diagnostics.proton_radiography.SyntheticProtonRadiograph.load_particles
. (#1035)In
plasmapy.diagnostics.proton_radiography.SyntheticProtonRadiograph
, removed highly deflected particles so the call ofplasmapy.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
andplasmapy.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
andgrids_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
andExB_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
intonumpydoc
format. (#1039)Adds formulas (which were missing) to the docstrings of
plasmapy.formulary.dimensionless.quantum_theta
andbeta
. (#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 inplasmapy.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 fromplasmapy.particles
andplasmapy.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 *
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
) fromplasmapy.formulary
toplasmapy.dispersion
. (#910)Removed default values for the
ion
andparticle
arguments of functions contained inplasmapy.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
toplasmapy.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 thefit_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 updatedplasmapy.formulary.parameters.mass_density
and maintain the same behavior. Also add handling of theion
input keyword, soParticle
and theParticle
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 realnbgallery
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 ofplasmapy
are indexed. This is necessary to expose all ofplasmapy
since not all modules are indexed in the narrative documentation. (#878)Decompose sub-package
plasmapy/utils/roman/
into theplasmapy/utils/roman.py
file. Move definition ofroman
specific exceptions intoplasmapy.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
toparticles
. Incollisions
andbraginskii
, change arguments named particles tospecies
and arguments namedion_particle
toion
for multiple functions. (#742)Officially delete
plasmapy.examples
. (#822)Move
plasmapy.data
toplasmapy.particle.data
. (#823)Renamed the
plasmapy.classes
subpackage toplasmapy.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 establishedLorentz_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 functionspectral_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
Createplasmapy.formulary.ionization.Z_bal
function. (#851)Added aliases (with trailing underscores) for parameters in the formulary:
plasmapy.formulary.parameters.mass_density
→plasmapy.formulary.parameters.rho_
plasmapy.formulary.parameters.Alfven_speed
→plasmapy.formulary.parameters.va_
plasmapy.formulary.parameters.ion_sound_speed
→plasmapy.formulary.parameters.cs_
plasmapy.formulary.parameters.thermal_speed
→plasmapy.formulary.parameters.vth_
plasmapy.formulary.parameters.thermal_pressure
→plasmapy.formulary.parameters.pth_
plasmapy.formulary.parameters.kappa_thermal_speed
→plasmapy.formulary.parameters.vth_kappa_
plasmapy.formulary.parameters.inertial_length
→plasmapy.formulary.parameters.cwp_
plasmapy.formulary.parameters.Hall_parameter
→plasmapy.formulary.parameters.betaH_
plasmapy.formulary.parameters.gyrofrequency
→plasmapy.formulary.parameters.oc_
,plasmapy.formulary.parameters.wc_
plasmapy.formulary.parameters.gyroradius
→plasmapy.formulary.parameters.rc_
,plasmapy.formulary.parameters.rhoc_
plasmapy.formulary.parameters.plasma_frequency
→plasmapy.formulary.parameters.wp_
plasmapy.formulary.parameters.Debye_length
→plasmapy.formulary.parameters.lambdaD_
plasmapy.formulary.parameters.Debye_number
→plasmapy.formulary.parameters.nD_
plasmapy.formulary.parameters.magnetic_pressure
→plasmapy.formulary.parameters.pmag_
plasmapy.formulary.parameters.magnetic_energy_density
→plasmapy.formulary.parameters.ub_
plasmapy.formulary.parameters.upper_hybrid_frequency
→plasmapy.formulary.parameters.wuh_
plasmapy.formulary.parameters.lower_hybrid_frequency
→plasmapy.formulary.parameters.wlh_
plasmapy.formulary.parameters.Bohm_diffusion
→plasmapy.formulary.parameters.DB_
plasmapy.formulary.quantum.thermal_deBroglie_wavelength
→lambdaDB_th_
Add
json_dumps
method toAbstractParticle
to convert a particle object into a JSON string. Addjson_dump
method toAbstractParticle
to serialize a particle object and writes it to a file. Add JSON decoderParticleJSONDecoder
to deserialize JSON objects into particle objects. Addplasmapy.particles.serialization.json_loads_particle
function to convert JSON strings to particle objects (usingParticleJSONDecoder
). Addplasmapy.particles.json_load_particle
function to deserialize a JSON file into a particle object (usingParticleJSONDecoder
). (#836)
Bug Fixes
Fix incorrect use of
pkg.resources
when definingplasmapy.__version__
. Addsetuptools
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 ofsphinx-automodapi
withsphinx
v3.0.0
. (#780)Automate definition of documentation
release
andversion
indocs/conf.py
withplasmapy.__version__
. (#781)Add a docstring to
__init__.py
inplasmapy.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 indocs/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 thetoctree
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__
butsetuptools_scm
has not generated theversion.py
file. This commonly happens during development whenplasmapy
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 parametersmass
andcharge
can accept string representations of astropyQuantities
. (#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 toparticletracker
(#665)Changed
plasmapy.classes.Species
toplasmapy.simulation.ParticleTracker
(#668)Move pytest helper functionality from
plasmapy.utils
toplasmapy.utils.pytest_helpers
(#674)Move
plasmapy.physics
,plasmapy.mathematics
andplasmapy.transport
into the commonplasmapy.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 aroundastropy.constants
with no added value (#651)
Features
Bug Fixes
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)
- Added examples to the documentation to
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 decoratorplasmapy.utils.decorators.validators.validate_quantities()
. Permanently delete decoratorplasmapy.utils.decorators.checks.check_quantity
and its supporting code. For functionsplasmapy.formulary.quantum.chemical_potential()
andplasmapy.formulary.quantum._chemical_potential_interp
, add araise NotImplementedError
due to bug outlined in issue https://github.com/PlasmaPy/PlasmaPy/issues/726. Associated pytests are marked withpytest.mark.xfail
and doctests are marked withdoctests: +SKIP
. (#722)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()
, andvalidate_quantities()
. These decorators are fully defined by “decorator classes”CheckBase
,CheckValues
,CheckUnits
, andValidateQuantities
. (#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 6Create an openPMD
Plasma
subclassCreate 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
Changes to API
Move
plasmapy.transport
fromplasmapy.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
Add
plasmapy.__citation__
containing a BibTeX reference.
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 theplasmapy.atomic
subpackage.Created the
plasmapy.atomic.particle_input
decorator.
Created the
plasmapy.classes
subpackage that includes the prototypeplasmapy.classes.Plasma3D
,plasmapy.classes.PlasmaBlob
, andplasmapy.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 itsplasmapy.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
andplasmapy.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.
Bibliography
H. Alfvén. Existence of Electromagnetic-Hydrodynamic Waves. Nature, 150(3805):405–406, 1942. doi:10.1038/150405d0.
W. Baumjohann and R. A. Treumann. Basic Space Plasma Physics. Imperial College Press, 1997.
G. Bekefi. Radiation Processes in Plasmas. Wiley, 1966. ISBN 9780471063506.
P. M. Bellan. Improved basis set for low frequency plasma waves. Journal of Geophysical Research: Space Physics, 2012. doi:10.1029/2012ja017856.
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.
C. K. Birdsall and A. B. Langdon. Plasma Physics via Computer Simulation. CRC Press, 2004. doi:10.1201/9781315275048.
D. Bohm. The Characteristics of Electrical Discharges in Magnetic Fields. McGraw-Hill, 1949.
M. Bonitz. Quantum Kinetic Theory. Springer, 1998. doi:10.1007/978-3-319-24121-0.
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.
S. I. Braginskii. Transport Processes in a Plasma. Reviews of Plasma Physics, 1:205, 1965.
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.
J. Callen. Draft Material For "Fundamentals of Plasma Physics" Book. Unpublished. URL: https://docs.google.com/document/d/e/2PACX-1vQmvQ_b8p0P2cYsWGMQYVd92OBLX9Sm6XGiCMRBidoVSoJffj2MBvWiwpix46mqlq_HQvHD5ofpfrNF/pub.
F. Chen. Introduction to Plasma Physics and Controlled Fusion. Springer, 3rd edition, 2016. doi:10.1007/978-3-319-22309-4.
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.
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.
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.
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.
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.
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.
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.
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.
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.
J. V. Hollweg. Kinetic Alfvén wave revisited. Journal of Geophysical Research, 1999. doi:10.1029/1998ja900132.
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.
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.
V. Khorikov. Unit Testing Principles, Practices, and Patterns. Manning Press, 1st edition, 2020. URL: https://www.manning.com/books/unit-testing.
S. Lundquist. Magneto-hydrostatic fields. Arkiv. fysik, 2(35):361, 1950.
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.
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.
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.
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.
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.
E. R. Priest and T. Forbes. Magnetic Reconnection: MHD Theory and Applications. Cambridge University Press, 2000. ISBN 978-0-521-03394-7.
A. S. Richardson. NRL Plasma Formulary. Technical Report, Naval Research Laboratory, 2019. URL: https://www.nrl.navy.mil/News-Media/Publications/nrl-plasma-formulary.
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.
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.
L. Spitzer. Physics of Fully Ionized Gases. Interscience, 2nd edition, 1962.
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.
T. H. Stix. Waves in Plasmas. AIP-Press, 1992. URL: https://link.springer.com/book/9780883188590.
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.
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.
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.
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.
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.
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 instantiateParticle
.- alias
- aliases
An abbreviated version of a commonly used function. For example,
va_
is an alias forAlfven_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; orA
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 thislist
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 modulefit_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 tof(z)
, then the argument2
can be provided asf(z=2)
but notf(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 mostformulary
functions acceptQuantity
objects created usingastropy.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 thelite
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 aParticle
orCustomParticle
, 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"
, or26
.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 aParticleList
, 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 dimensionlessQuantity
, or any of thenumpy.number
types. Note that if a PlasmaPy function expects a dimensionalQuantity
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]:
Arrange: gather inputs and get the system to the state in which the test is expected to run.
Act: make the system under test undertake the operation that is being tested.
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.
In the Finder, go to Applications. Enter the Utilities folder and double click on Terminal.
Open a terminal by using Ctrl + Alt + T.
Installing Python
Note
PlasmaPy requires a version of Python between 3.9 and 3.11. We recommend using Python 3.11.
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).
If you prefer a GUI, follow these instructions on installing Anaconda Navigator.
If you prefer a CLI, follow these instructions on installing Conda.
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:
Sign up on GitHub for a free account.
Verify that git is installed by opening a terminal and running:
git --version
If there is an error, follow these instructions to install git.
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.
Add a new SSH key to your GitHub account. This step is needed for authentication purposes.
Initial setup
Log in to GitHub.
Go to PlasmaPy’s GitHub repository.
Create a fork of PlasmaPy by clicking on Fork, followed by Create fork.
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
Clone the PlasmaPy repository with the following command, replacing
YOUR-USERNAME
with your GitHub username. This will create a subdirectory calledPlasmaPy/
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.
Enter the newly created directory with:
cd PlasmaPy
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 thatorigin
corresponds to your fork andupstream
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.
Create a Conda environment named
plasmapy-dev
by running:conda create -n plasmapy-dev python=3.11
The
-n
flag is used to specify the name of the environment. The3.11
can be replaced with any version of Python from 3.9 to 3.11.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
.
Navigate to the directory for your clone of PlasmaPy, which should be named
PlasmaPy
. For example, if you ran thegit clone
command in the~/repos/
directory, then run:cd ~/repos/PlasmaPy
Note
In Windows, the directory path will be
C:\Users\<username>\repos\PlasmaPy
.If you created a Conda environment for contributing to PlasmaPy, activate it with:
conda activate plasmapy-dev
Run the command to install PlasmaPy for your operating system:
py -m pip install -e .[docs,tests]
python -m pip install -e '.[docs,tests]'
python -m pip install -e .[docs,tests]
Note
Replace
py
withpython
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
Navigate to the
PlasmaPy/
directory that contains the clone of your repository.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 underChanges not staged for commit
orChanges to be committed
, then do one of the following before proceeding to the next step:Use git stash to temporarily file away the changes, or
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.
Download the current status of PlasmaPy’s GitHub repository and your fork by running:
git fetch --all
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. Hereupstream
is the name of the remote andmain
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
.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
.
Edit a file and save the changes.
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). Usegit 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.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
andgit commit
steps once more. Try usinggit diff
andgit diff --cached
to view the changes, and ↑ and ↓ to scroll through previous commands in a terminal.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
Run
git push
to make sure that branch on GitHub is up-to-date.Go to PlasmaPy’s GitHub repository.
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” andmain
for “base”. Choose your fork of PlasmaPy for “head repository” and the name of the branch for “compare”. Then click on Create pull request.Add a descriptive title, such as
Add a function to calculate particle gyroradii
.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, whereISSUE-NUMBER
is replaced with the number of the issue.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.
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:
If you use a Conda or virtual environment for developing PlasmaPy, activate it (i.e., with
conda activate plasmapy-dev
).Make sure that pre-commit is installed to your Python environment by running:
py -m pip install pre-commit
python -m pip install pre-commit
python -m pip install pre-commit
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
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 anndarray
or alist
.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
andT_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 returnFalse
instead. This behavior is for consistency with operations like1 == "1"
which will returnFalse
.Limit usage of
lambda
functions to one-liners, such as when defining the default factory of adefaultdict
). For anything longer than one line, usedef
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 returnnan
(not a number) values. This guideline applies whennan
is the input as well as whennan
values are included in an array.Tip
Normally,
numpy.nan == numpy.nan
evaluates toFalse
, which complicates testingnan
behavior. Theequal_nan
keyword of functions likenumpy.allclose
andnumpy.testing.assert_allclose
makes it so thatnan
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
andvariable_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
orCONSTANT_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 andT
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 thansolve_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
, andT_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 butLarmor_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 thanlambda_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 thanlambda = c / nu
orwavelength = 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 infor
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.
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 onlyloads
.For frequently used objects (e.g.,
Particle
) and type hint annotations (e.g.,Optional
andReal
), 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
orscipy == 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 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
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: Real) -> Real:
"""
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 withthermal_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 thebind_lite_func
decorator. This allows the lite-function to also be accessed likethermal_speed.lite()
.If a lite-function is decorated with something like
@njit
, then it should also be decorated withpreserve_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.
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 inQuantity
operations, check out performance tips forastropy.units
.Use unit annotations with the
validate_quantities()
decorator to validateQuantity
arguments and return values (see Validating Quantity arguments).Caution
Recent versions of Astropy allow unit-aware
Quantity
annotations such asu.Quantity[u.m]
. However, these annotations are not yet compatible withvalidate_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 usingvalidate_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 aParticle
,CustomParticle
, orParticleList
instance when the corresponding parameter is decorated withParticleLike
.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:
helps with versioning the notebooks, as binary image data is not stored in the notebook
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.
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.removal
: For feature deprecation and/or removal.trivial
: For changes that have no user-facing effects.
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 correspondingfeature
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
and1208.feature.rst
.For multiple changes in a single category, use filenames like
1208.trivial.1.rst
and1208.trivial.2.rst
.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.
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. When this is the case, a package maintainer will add the :guilabel`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 toParticle
. 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.
Tip
When removing or moving an object, reStructuredText links that follow the original namespace will break, causing the documentation build to fail.
Text in single back ticks is used to link to code objects, while text
in double back ticks is treated as an inline literal. To remedy
this problem in old changelog entries, change the broken link into an
inline literal by surrounding it with double back ticks instead.
Remove the tilde if present. For example,
`~plasmapy.subpackage.module.function`
should be changed to:
``plasmapy.subpackage.module.function``
Outside of the changelog, the namespace should be corrected rather than changed into an inline literal.
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 also have tests.
Tests are located in files that begin with
test_
which are inside subdirectories namedtests/
.Tests are either functions beginning with
test_
or classes beginning withTest
.Here is an example of a minimal
pytest
test that uses anassert
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 set up using the pytest
framework. The tests for a
subpackage are located in its tests/
subdirectory in files with
names of the form test_*.py
. For example, tests for
plasmapy.formulary.speeds
are located at
plasmapy/formulary/tests/test_speeds.py
relative to the top
of the package. Example code contained within docstrings is tested to
make sure that the actual printed output matches what is in the
docstring.
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
The tests for each subpackage are contained in its tests/
subdirectory. For example, the tests for plasmapy.particles
are
located in plasmapy/particles/tests/
. Test files begin with
test_
and generally contain either the name of the module or a
description of the behavior that is being tested. For example, tests for
Particle
are located at
plasmapy/particles/tests/test_particle_class.py
.
The functions that are to be tested in each test file are prepended with
test_
and end with a description of the behavior that is being
tested. For example, a test that checks that a Particle
can be turned
into an antiparticle could be named test_particle_inversion
.
Strongly related tests may also be grouped into classes. The name of
such a class begins with Test
and the methods to be tested begin
with test_
. For example, test_particle_class.py
could define
the TestParticle
class containing the method test_charge_number
.
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:
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
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.
Decorate unavoidably slow tests with
@pytest.mark.slow
:@pytest.mark.slow def test_calculating_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.
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:
Creating and updating a pull request on GitHub.
Running
pytest
from the command line.Running tox from the command line.
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.

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 togit pull
afterwards!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
wheren
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:
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:
The documentation corresponding to the most recent release to PyPI is labeled
stable
and is found at https://docs.plasmapy.org or https://docs.plasmapy.org/en/stable.The documentation corresponding to the ongoing development on the
main
branch in PlasmaPy’s GitHub repository, which is often ahead of the most recent release, is labeledlatest
and can be found at https://docs.plasmapy.org/en/latest.
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.

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:
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.
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. Thedocs/_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.
When including references, use a link that includes a persistent identifier such as a digital object identifier (DOI) when one is available (e.g., https://doi.org/10.5281/zenodo.4602818).
Wikipedia articles may be linked to when they contain a well-developed and accurate description of a concept.
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 anyfrom __future__
imports that must be at the beginning of a file). This dunder should be alist
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 anndarray
).Use the substitution
|particle-like|
to indicate that a particle-like argument should be convertible into aParticle
,CustomParticle
, orParticleList
.Use the
|particle-list-like|
to indicate that a particle-list-like argument should be convertible into aParticleList
.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
wheren
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
, useoptional
instead ofdefault: `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()
andparticle_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 asetter
decoration, then the getter andsetter
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:
sphinx.ext.autodoc
for including documentation from docstrings.sphinx.ext.extlinks
for shortening links to external sites (e.g.,:orcid:
and:wikipedia:
).sphinx.ext.graphviz
to allow Graphviz graphs to be included.sphinx.ext.intersphinx
for linking to other projects’ documentation.sphinx.ext.mathjax
for math rendering with MathJax.sphinx.ext.napoleon
for allowing NumPy style docstrings.sphinx.ext.todo
to supporttodo
directives.sphinx.ext.viewcode
to generate links to pages showing source code.sphinxcontrib-bibtex
to enable usage of a BibTeX file to create the Bibliography.sphinx_copybutton
to add a “copy” button for code blocks.sphinx_gallery.load_style
for using sphinx-gallery styles.sphinx_changelog
for rendering towncrier changelogs.sphinx-tabs
for creating tabbed content.sphinx-hoverxref
for showing floating windows on cross references of the documentation.sphinx-notfound-page
to add a 404 page for the documentation.sphinx-issues
to add roles for linking to GitHub (:commit:
,:issue:
,:pr:
, and:user:
).sphinx-reredirects
to enable hyperlink redirects.sphinx-toolbox
for handy tools for Sphinx documentationplasmapy_sphinx
for customizations created for use in PlasmaPy and affiliated packages. Note thatplasmapy_sphinx
is expected to be broken out into its own package in the future.
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
, orThe 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 plasmapy/io/
directory and the
plasmapy.io.readers
module via 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
To install all Python dependencies required to develop PlasmaPy on your local computer, enter the top-level directory of the cloned repository and run:
pip install -e ".[tests,docs]"
It may also be necessary to install the following software:
Building documentation with make
If you have make installed, then you can build the documentation by
entering the docs/
directory and running:
make html
Including the -j n
flag in the make
command will
enable a parallel build, where n
is replaced with the number
of processes or auto
. To skip building example notebooks, use
make html-nonb
instead.
You can access the documentation landing page by opening
docs/_build/html/index.html
with your browser of choice.
To remove all files previously generated by make
, run:
make clean
This command is needed when you make a change to a file that does not
trigger Sphinx to rebuild the file that you altered, for example
modifying a CSS file. Using make clean-api
instead will only
remove the API portion of the documentation build.
To check that hyperlinks are correct, run:
make linkcheck
Building documentation with tox
You can use tox to locally build the documentation by running:
tox -e build_docs
You can access the documentation landing page by opening
docs/_build/html/index.html
with your browser of choice.
To pass any options to sphinx-build, put them after --
, as in the
following example:
tox -e build_docs -- -j=auto -q
The -j=auto
option tells sphinx-build to build the
documentation in parallel, with the number of processes being
automatically determined. The -q
flag makes sphinx-build
print out only warnings and errors, which makes them easier to find and
debug.
You can alternatively shorten the documentation build by running:
tox -e build_docs_no_examples
This command will build the documentation without executing the example notebooks.
To check hyperlinks, run:
tox -e linkcheck
Tip
When writing documentation, please make sure to fix any warnings that
arise. To enforce this, the build_docs
tox environment will
fail after completing the documentation build 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 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::
.
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.
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.
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:
could be achieved with no comment by doing:
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.
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.
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.