From 5e1e0a50b50a676abd52584c0dd9828e5b97c6db Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Thu, 18 Apr 2019 16:23:44 -0700 Subject: [PATCH 01/68] refactor class BaPSFConstant - > LaPDConstant --- bapsflib/lapd/constants/constants.py | 33 ++++++++----------- .../lapd/constants/tests/test_constants.py | 14 ++++---- .../src/bapsflib.lapd.constants.constants.rst | 2 +- 3 files changed, 22 insertions(+), 27 deletions(-) diff --git a/bapsflib/lapd/constants/constants.py b/bapsflib/lapd/constants/constants.py index 7f8bf456..99e68e59 100644 --- a/bapsflib/lapd/constants/constants.py +++ b/bapsflib/lapd/constants/constants.py @@ -18,26 +18,21 @@ from typing import Tuple -class BaPSFConstant(Constant): - """BaPSF Constant""" - default_reference = 'Basic Plasma Science Facility' +class LaPDConstant(Constant): + """LaPD Constant""" + default_reference = 'Large Plasma Device' _registry = {} _has_incompatible_units = set() - def __new__(cls, abbrev, name, value, unit, uncertainty, - reference=default_reference, system=None): - return super().__new__(cls, abbrev, name, value, unit, - uncertainty, reference, system) - -#: BaPSF Constant: nominal distance between LaPD ports -port_spacing = BaPSFConstant('port_spacing', 'LaPD port spacing', - 31.95, 'cm', 1.0, system='cgs') +#: LaPD Constant: nominal distance between LaPD ports +port_spacing = LaPDConstant('port_spacing', 'LaPD port spacing', + 31.95, 'cm', 1.0, system='cgs') port_spacing.__doc__ += ': nominal distance between LaPD ports' -#: BaPSF Constant: LaPD :math:`z = 0` reference port (most Northern +#: LaPD Constant: LaPD :math:`z = 0` reference port (most Northern #: port and :math:`+z` points South towards south cathode) -ref_port = BaPSFConstant('ref_port', 'LaPD reference port number', 53, +ref_port = LaPDConstant('ref_port', 'LaPD reference port number', 53, u.dimensionless_unscaled, 0, system=None) ref_port.__doc__ += (": LaPD :math:`z = 0` reference port (most " "Northern port and :math:`+z` points South " @@ -81,19 +76,19 @@ def operation_date(self, val: datetime.datetime): warnings.simplefilter('ignore', AstropyWarning) if val.year <= val.year: - self._diameter = BaPSFConstant( + self._diameter = LaPDConstant( 'diameter', "Diameter of LaPD's south 'main' cathode", 60.0, 'cm', 1.0, system='cgs' ) - self._z = BaPSFConstant( + self._z = LaPDConstant( 'z', "Axial location of LaPD's south 'main' cathode", 1700.0, 'cm', 1.0, system='cgs' ) - self._anode_z = BaPSFConstant( + self._anode_z = LaPDConstant( 'anode_z', "Axial location of LaPD's south 'main' anode", 1650.0, 'cm', @@ -103,17 +98,17 @@ def operation_date(self, val: datetime.datetime): self._lifespan = (NotImplemented, NotImplemented) @property - def anode_z(self) -> BaPSFConstant: + def anode_z(self) -> LaPDConstant: """LaPD z location of the anode""" return self._anode_z @property - def diameter(self) -> BaPSFConstant: + def diameter(self) -> LaPDConstant: """Diameter of LaPD's south 'main' cathode""" return self._diameter @property - def z(self) -> BaPSFConstant: + def z(self) -> LaPDConstant: """LaPD z location of the cathode""" return self._z diff --git a/bapsflib/lapd/constants/tests/test_constants.py b/bapsflib/lapd/constants/tests/test_constants.py index 9876f74e..1987bb4c 100644 --- a/bapsflib/lapd/constants/tests/test_constants.py +++ b/bapsflib/lapd/constants/tests/test_constants.py @@ -16,7 +16,7 @@ from astropy.constants import Constant from bapsflib.lapd.constants import (port_spacing, ref_port) -from bapsflib.lapd.constants.constants import BaPSFConstant +from bapsflib.lapd.constants.constants import LaPDConstant class TestConstants(ut.TestCase): @@ -24,19 +24,19 @@ class TestConstants(ut.TestCase): Test LaPD constants. (:mod:`bapsflib.lapd.constants.constants`) """ - def test_BaPSFConstant(self): - self.assertTrue(issubclass(BaPSFConstant, Constant)) - self.assertEqual(BaPSFConstant.default_reference, - "Basic Plasma Science Facility") + def test_LaPDConstant(self): + self.assertTrue(issubclass(LaPDConstant, Constant)) + self.assertEqual(LaPDConstant.default_reference, + "Large Plasma Device") def test_port_spacing(self): - self.assertIsInstance(port_spacing, BaPSFConstant) + self.assertIsInstance(port_spacing, LaPDConstant) self.assertEqual(port_spacing.value, 31.95) self.assertEqual(port_spacing.unit, u.cm) self.assertEqual(port_spacing.uncertainty, 1.0) def test_ref_port(self): - self.assertIsInstance(ref_port, BaPSFConstant) + self.assertIsInstance(ref_port, LaPDConstant) self.assertEqual(ref_port.value, 53) self.assertEqual(ref_port.unit, u.dimensionless_unscaled) diff --git a/docs/src/bapsflib.lapd.constants.constants.rst b/docs/src/bapsflib.lapd.constants.constants.rst index d91003de..5357b57c 100644 --- a/docs/src/bapsflib.lapd.constants.constants.rst +++ b/docs/src/bapsflib.lapd.constants.constants.rst @@ -11,7 +11,7 @@ bapsflib\.lapd\.constants\.constants .. autosummary:: :nosignatures: - BaPSFConstant + LaPDConstant SouthCathode .. rubric:: Constants From 2cdbefd479a04ebe48df83211c1d0e37a644c822 Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Fri, 19 Apr 2019 11:39:33 -0700 Subject: [PATCH 02/68] initial commit for bapsflib.plasma.constants --- bapsflib/plasma/constants.py | 70 ++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 bapsflib/plasma/constants.py diff --git a/bapsflib/plasma/constants.py b/bapsflib/plasma/constants.py new file mode 100644 index 00000000..59768a76 --- /dev/null +++ b/bapsflib/plasma/constants.py @@ -0,0 +1,70 @@ +# This file is part of the bapsflib package, a Python toolkit for the +# BaPSF group at UCLA. +# +# http://plasma.physics.ucla.edu/ +# +# Copyright 2017-2019 Erik T. Everson and contributors +# +# License: Standard 3-clause BSD; see "LICENSES/LICENSE.txt" for full +# license terms and contributor agreement. +# +"""Useful Physical Constants + +Constants are imported from :mod:`astropy.constants.codata2014`. +""" +from astropy.constants.codata2014 import ( + c, + e, e_gauss, e_emu, e_esu, + eps0, + g0, + k_B, + m_e, m_n, m_p, u, + mu0, +) +from numpy import pi + + +__all__ = ['c', + 'e', 'e_gauss', 'e_emu', 'e_esu', + 'eps0', + 'g0', + 'k_B', + 'm_e', 'm_n', 'm_p', 'u', + 'mu0', + 'pi'] + +''' +class BaPSFConstant(Constant): + """BaPSF Constant""" + default_reference = 'Basic Plasma Facility' + _registry = {} + _has_incompatible_units = set() + + +AMU = BaPSFConstant(**{ + 'abbrev': 'AMU', + 'name': 'atomic mass unit', + 'value': const.u.cgs.value, + 'unit': const.u.cgs.unit, + 'uncertainty': (const.u.uncertainty * const.u.unit).cgs, + 'system': 'cgs', +}) + +C = BaPSFConstant(**{ + 'abbrev': 'C', + 'name': 'speed of light in vacuum', + 'value': const.c.cgs.value, + 'unit': const.c.cgs.unit, + 'uncertainty': (const.c.uncertainty * const.c.unit).cgs, + 'system': 'cgs', +}) + +e = BaPSFConstant(**{ + 'abbrev': 'e', + 'name': 'elementary charge', + 'value': const.e.cgs.value, + 'unit': const.e.cgs.unit, + 'uncertainty': (const.e.uncertainty * const.e.unit).cgs, + 'system': 'cgs', +}) +''' From 7dbc693d15fa263633fd8917cb2f6acd422c2430 Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Fri, 19 Apr 2019 13:23:46 -0700 Subject: [PATCH 03/68] add plasma py to requirements --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 7236ac35..77212abd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,6 +15,7 @@ codecov >= 2.0.15 coverage >= 4.5.1 h5py >= 2.6 numpy >= 1.12 +plasmapy >= 0.1.0 scipy >= 1.0.0 # # Add Sphinx/Documentation requirements From b1fe7dc9b1ea25c7d24e19aff871df3429d5e5c2 Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Fri, 19 Apr 2019 18:42:08 -0700 Subject: [PATCH 04/68] define class `BaPSFConstant` for BaPSF constants --- bapsflib/utils/__init__.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/bapsflib/utils/__init__.py b/bapsflib/utils/__init__.py index 4d21f527..bb762467 100644 --- a/bapsflib/utils/__init__.py +++ b/bapsflib/utils/__init__.py @@ -9,5 +9,13 @@ # license terms and contributor agreement. # from . import (errors, warnings) +from astropy.constants import Constant -__all__ = ['errors', 'warnings'] +__all__ = ['BaPSFConstant', 'errors', 'warnings'] + + +class BaPSFConstant(Constant): + """Factory Class for BaPSF Constants""" + default_reference = 'Basic Plasma Facility' + _registry = {} + _has_incompatible_units = set() From 3ed2fbdc209837ab407101836fe9cdd52a6ed4a1 Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Fri, 19 Apr 2019 18:44:34 -0700 Subject: [PATCH 05/68] only import elementary charge constants `e` and `e_gauss`; convert constants `e`, `e_gauss`, and `u` to BaPSFConstant to provided clearer metadata; update docstring constant table with a 'system' units column --- bapsflib/plasma/constants.py | 98 ++++++++++++++++++++++++------------ 1 file changed, 65 insertions(+), 33 deletions(-) diff --git a/bapsflib/plasma/constants.py b/bapsflib/plasma/constants.py index 59768a76..c6f14e73 100644 --- a/bapsflib/plasma/constants.py +++ b/bapsflib/plasma/constants.py @@ -14,18 +14,19 @@ """ from astropy.constants.codata2014 import ( c, - e, e_gauss, e_emu, e_esu, + e, e_gauss, eps0, g0, k_B, m_e, m_n, m_p, u, mu0, ) +from bapsflib.utils import BaPSFConstant from numpy import pi __all__ = ['c', - 'e', 'e_gauss', 'e_emu', 'e_esu', + 'e', 'e_gauss', 'eps0', 'g0', 'k_B', @@ -33,38 +34,69 @@ 'mu0', 'pi'] -''' -class BaPSFConstant(Constant): - """BaPSF Constant""" - default_reference = 'Basic Plasma Facility' - _registry = {} - _has_incompatible_units = set() +# rename some attributes for clarity +e = BaPSFConstant(**{ + 'abbrev': 'e', + 'name': 'Elementary charge', + 'value': e.value, + 'unit': e.unit.name, + 'uncertainty': e.uncertainty, + 'system': e.system, +}) +e_gauss = BaPSFConstant(**{ + 'abbrev': 'e_gauss', + 'name': 'Elementary charge', + 'value': e_gauss.value, + 'unit': e_gauss.unit.name, + 'uncertainty': e_gauss.uncertainty, + 'system': e_gauss.system, +}) +u = BaPSFConstant(**{ + 'abbrev': 'u', + 'name': 'Atomic mass unit', + 'value': u.value, + 'unit': u.unit.name, + 'uncertainty': u.uncertainty, + 'system': u.system, +}) +# clean up +del BaPSFConstant -AMU = BaPSFConstant(**{ - 'abbrev': 'AMU', - 'name': 'atomic mass unit', - 'value': const.u.cgs.value, - 'unit': const.u.cgs.unit, - 'uncertainty': (const.u.uncertainty * const.u.unit).cgs, - 'system': 'cgs', -}) +# The following code is modified from astropy.constants to produce a +# table containing information on the constants. -C = BaPSFConstant(**{ - 'abbrev': 'C', - 'name': 'speed of light in vacuum', - 'value': const.c.cgs.value, - 'unit': const.c.cgs.unit, - 'uncertainty': (const.c.uncertainty * const.c.unit).cgs, - 'system': 'cgs', -}) +# initialize table +_tb_div = ((8 * '=') + ' ' + + (17 * '=') + ' ' + + (10 * '=') + ' ' + + (7 * '=') + ' ' + + (44 * '=')) +_lines = [ + 'The following constants are available:\n', + _tb_div, + '{0:^8} {1:^17} {2:^10} {3:^7} {4}'.format('Name', 'Value', 'Units', + 'System', 'Description'), + _tb_div, + '{0:^8} {1:^17.12f} {2:^10} {3:^7} {4}'.format( + 'pi', pi, '', '', + 'Ratio of circumference to diameter of circle'), +] -e = BaPSFConstant(**{ - 'abbrev': 'e', - 'name': 'elementary charge', - 'value': const.e.cgs.value, - 'unit': const.e.cgs.unit, - 'uncertainty': (const.e.uncertainty * const.e.unit).cgs, - 'system': 'cgs', -}) -''' +# add lines to table +_constants = [eval(item) for item in dir() if item[0] != '_' and item != 'pi'] +_const = None +for _const in _constants: + _lines.append('{0:^8} {1:^17.12g} {2:^10} {3:^7} {4}' + .format(_const.abbrev, _const.value, + _const._unit_string, _const.system, + _const.name)) + +_lines.append(_lines[1]) + +# add table to docstrings +__doc__ += '\n' +__doc__ += '\n'.join(_lines) + +# remove clutter from namespace +del _const, _constants, _lines, _tb_div, From c4a737c25743df75dfdaa82faba2b552f946e892 Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Fri, 19 Apr 2019 18:48:14 -0700 Subject: [PATCH 06/68] initial commit --- docs/src/bapsflib.plasma.constants.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 docs/src/bapsflib.plasma.constants.rst diff --git a/docs/src/bapsflib.plasma.constants.rst b/docs/src/bapsflib.plasma.constants.rst new file mode 100644 index 00000000..ad9f1261 --- /dev/null +++ b/docs/src/bapsflib.plasma.constants.rst @@ -0,0 +1,16 @@ +bapsflib\.plasma\.\constants +============================ +.. py:currentmodule:: bapsflib.plasma.constants + +.. automodule:: bapsflib.plasma.constants + +.. .. rubric:: Constants + +.. .. autosummary:: + :nosignatures: + +.. port_spacing + ref_port + +.. .. autodata:: port_spacing +.. .. autodata:: ref_port \ No newline at end of file From 550d4c132ed205db3c1b945d99800fd025fca5ea Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Wed, 24 Apr 2019 18:10:08 -0700 Subject: [PATCH 07/68] initial commit for bapsflib.plasma.parameters; added functions for cyclotron frequency, electron-cyclotron frequency, ion-cyclotron frequency, and a generic plasma frequency --- bapsflib/plasma/parameters.py | 141 ++++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 bapsflib/plasma/parameters.py diff --git a/bapsflib/plasma/parameters.py b/bapsflib/plasma/parameters.py new file mode 100644 index 00000000..a86432ef --- /dev/null +++ b/bapsflib/plasma/parameters.py @@ -0,0 +1,141 @@ +# This file is part of the bapsflib package, a Python toolkit for the +# BaPSF group at UCLA. +# +# http://plasma.physics.ucla.edu/ +# +# Copyright 2017-2018 Erik T. Everson and contributors +# +# License: Standard 3-clause BSD; see "LICENSES/LICENSE.txt" for full +# license terms and contributor agreement. +# +# TODO: add plasma betas (electron, ion, and total) +# TODO: add Coulomb Logarithm +# TODO: add collision frequencies +# TODO: add mean-free-paths +# +""" +Plasma parameters + +All units are in Gaussian cgs except for temperature, which is +expressed in eV. (same as the NRL Plasma Formulary) +""" +import astropy.units as u +import numpy as np + +from . import constants as conts +from plasmapy import utils +from typing import Union + + +# ---- Frequencies ---- +@utils.check_quantity({'q': {'units': u.statcoulomb, + "can_be_negative": True}, + 'B': {'units': u.Gauss, + "can_be_negative": False}, + 'm': {'units': u.g, + "can_be_negative": False}}) +def cyclotron_frequency(q: u.Quantity, B: u.Quantity, m: u.Quantity, + to_Hz=False, **kwargs) -> u.Quantity: + """ + particle cyclotron frequency (rad/s) + + .. math:: + + \\Omega_{c} = \\frac{q B}{m c} + + :param q: particle charge (in statcoulomb) + :param B: magnetic-field (in Gauss) + :param m: particle mass (in grams) + :param to_Hz: :code:`False` (DEFAULT). Set to :code:`True` to + return frequency in Hz (i.e. divide by :math:`2 * \\pi`) + """ + # ensure args have correct units + q = q.to(u.Fr) + B = B.to(u.gauss) + m = m.to(u.g) + + # calculate + _oc = ((q.value * B.value) / (m.value * conts.c.cgs.value)) + if to_Hz: + _oc = (_oc / (2.0 * conts.pi)) * u.Hz + else: + _oc = _oc * (u.rad / u.s) + return _oc + + +@utils.check_quantity({'n': {'units': u.cm ** -3, + "can_be_negative": False}, + 'q': {'units': u.statcoulomb, + "can_be_negative": True}, + 'm': {'units': u.g, + "can_be_negative": False}}) +def plasma_frequency_generic( + n: u.Quantity, q: u.Quantity, m: u.Quantity, + to_Hz=False, **kwargs) -> u.Quantity: + """ + generalized plasma frequency (rad/s) + + .. math:: + + \\omega_{p}^{2} = \\frac{4 \\pi n q^{2}}{m} + + :param n: particle number density (in number/cm^3) + :param q: particle charge (in statcoulombs) + :param m: particle mass (in g) + :param to_Hz: :code:`False` (DEFAULT). Set to :code:`True` to + return frequency in Hz (i.e. divide by :math:`2 * \\pi`) + """ + # ensure args have correct units + n = n.to(u.cm ** -3) + q = q.to(u.Fr) + m = m.to(u.g) + + # calculate + _op = np.sqrt((4.0 * conts.pi * n.value * (q.value * q.value)) + / m.value) + if to_Hz: + _op = (_op / (2.0 * conts.pi)) * u.Hz + else: + _op = _op * (u.rad / u.s) + return _op + + +@utils.check_quantity({'B': {'units': u.Gauss, + "can_be_negative": False}}) +def oce(B: u.Quantity, **kwargs) -> u.Quantity: + """ + electron-cyclotron frequency (rad/s) + + .. math:: + + \\Omega_{ce} = -\\frac{|e| B}{m_{e} c} + + :param B: magnetic-field (in Gauss) + :param kwargs: supports any keywords used by + :func:`cyclotron_frequency` + """ + return cyclotron_frequency(-conts.e_gauss, B, conts.m_e, + **kwargs['kwargs']) + + +@utils.check_quantity({'B': {'units': u.Gauss, + "can_be_negative": False}, + 'm_i': {'units': u.g, + "can_be_negative": False}}) +def oci(Z: Union[int, float], B: u.Quantity, m_i: u.Quantity, + **kwargs) -> u.Quantity: + """ + ion-cyclotron frequency (rad/s) + + .. math:: + + \\Omega_{ci} = \\frac{Z |e| B}{m_{i} c} + + :param Z: charge number + :param B: magnetic-field (in Gauss) + :param m_i: ion mass (in grams) + :param kwargs: supports any keywords used by + :func:`cyclotron_frequency` + """ + return cyclotron_frequency(Z * conts.e_gauss, B, m_i, + **kwargs['kwargs']) From 48fe9e8886d48517b48e7de4a314d0e6742e02cc Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Wed, 24 Apr 2019 18:10:57 -0700 Subject: [PATCH 08/68] initial commit for bapsflib.plasma.parameters auto-documentation --- docs/src/bapsflib.plasma.parameters.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 docs/src/bapsflib.plasma.parameters.rst diff --git a/docs/src/bapsflib.plasma.parameters.rst b/docs/src/bapsflib.plasma.parameters.rst new file mode 100644 index 00000000..cf87feb3 --- /dev/null +++ b/docs/src/bapsflib.plasma.parameters.rst @@ -0,0 +1,16 @@ +bapsflib\.plasma\.parameters +============================ + +.. automodule:: bapsflib.plasma.parameters + :show-inheritance: + :members: + :undoc-members: + + .. rubric:: Functions + + .. autosummary:: + :nosignatures: + + cyclotron_frequency + oce + oci From 3a93a659522a3286bce2e41d5b14b1c4855689ca Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Wed, 24 Apr 2019 18:12:11 -0700 Subject: [PATCH 09/68] removed import of 'core' module and added imports of 'constants' and 'parameters' modules (also added to __all__) --- bapsflib/plasma/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bapsflib/plasma/__init__.py b/bapsflib/plasma/__init__.py index 2d1c1498..3365b3f5 100644 --- a/bapsflib/plasma/__init__.py +++ b/bapsflib/plasma/__init__.py @@ -8,6 +8,7 @@ # License: Standard 3-clause BSD; see "LICENSES/LICENSE.txt" for full # license terms and contributor agreement. # -from . import core +from . import constants +from . import parameters -__all__ = ['core'] +__all__ = ['constants', 'parameters'] From c5da4c13b1ba37cf6568f71cddbf3f83b348425c Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Wed, 24 Apr 2019 18:13:05 -0700 Subject: [PATCH 10/68] updated to reflect the creation of the bapsflib.plasma.constants and bapsflib.plasma.parameters modules --- docs/src/bapsflib.plasma.rst | 31 +++++++++++++------------------ 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/docs/src/bapsflib.plasma.rst b/docs/src/bapsflib.plasma.rst index 3963e726..ce37c448 100644 --- a/docs/src/bapsflib.plasma.rst +++ b/docs/src/bapsflib.plasma.rst @@ -1,33 +1,28 @@ bapsflib\.plasma -================= +================ .. warning:: Package Currently Under Development .. automodule:: bapsflib.plasma - :members: - :undoc-members: - :show-inheritance: - -Subpackages ------------ .. toctree:: - :maxdepth: 1 + :maxdepth: 2 + :titlesonly: + :caption: Sub-Packages & Modules + bapsflib.plasma.constants + bapsflib.plasma.parameters -Modules -------- +.. bapsflib.plasma.plasma -.. contents:: - :depth: 2 - :local: +.. .. rubric:: Classes -bapsflib\.plasma\.core -^^^^^^^^^^^^^^^^^^^^^^ +.. .. autosummary:: bapsflib.Plasma.SimplePlasma + :nosignatures: -.. automodule:: bapsflib.plasma.core +.. .. autoclass:: bapsflib.lapd.SimplePlasma :members: :undoc-members: - :special-members: - :exclude-members: __dict__, __init__, __module__, __weakref__ :show-inheritance: + :inherited-members: + :exclude-members: From 39345e3067ecfd0d0aa5751223b981a49f19f6b9 Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Wed, 24 Apr 2019 18:13:42 -0700 Subject: [PATCH 11/68] added the bapsflib.plasma package to the auto-documentation --- docs/src/bapsflib.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/src/bapsflib.rst b/docs/src/bapsflib.rst index fcdbaa1d..4fd9266e 100644 --- a/docs/src/bapsflib.rst +++ b/docs/src/bapsflib.rst @@ -13,5 +13,4 @@ bapsflib ./bapsflib._hdf ./bapsflib.lapd - -.. ./bapsflib.plasma + ./bapsflib.plasma From a976247e1e5b607d93488d346f17b7c881f179d1 Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Wed, 24 Apr 2019 18:32:05 -0700 Subject: [PATCH 12/68] refactor `conts` -> `const`; add plasma parameter functions for the electron-plasma frequency (`ope`) and the ion-plasma frequency (`opi`) --- bapsflib/plasma/parameters.py | 125 ++++++++++++++++++++++------------ 1 file changed, 83 insertions(+), 42 deletions(-) diff --git a/bapsflib/plasma/parameters.py b/bapsflib/plasma/parameters.py index a86432ef..71f8e70a 100644 --- a/bapsflib/plasma/parameters.py +++ b/bapsflib/plasma/parameters.py @@ -22,7 +22,7 @@ import astropy.units as u import numpy as np -from . import constants as conts +from . import constants as const from plasmapy import utils from typing import Union @@ -55,51 +55,14 @@ def cyclotron_frequency(q: u.Quantity, B: u.Quantity, m: u.Quantity, m = m.to(u.g) # calculate - _oc = ((q.value * B.value) / (m.value * conts.c.cgs.value)) + _oc = ((q.value * B.value) / (m.value * const.c.cgs.value)) if to_Hz: - _oc = (_oc / (2.0 * conts.pi)) * u.Hz + _oc = (_oc / (2.0 * const.pi)) * u.Hz else: _oc = _oc * (u.rad / u.s) return _oc -@utils.check_quantity({'n': {'units': u.cm ** -3, - "can_be_negative": False}, - 'q': {'units': u.statcoulomb, - "can_be_negative": True}, - 'm': {'units': u.g, - "can_be_negative": False}}) -def plasma_frequency_generic( - n: u.Quantity, q: u.Quantity, m: u.Quantity, - to_Hz=False, **kwargs) -> u.Quantity: - """ - generalized plasma frequency (rad/s) - - .. math:: - - \\omega_{p}^{2} = \\frac{4 \\pi n q^{2}}{m} - - :param n: particle number density (in number/cm^3) - :param q: particle charge (in statcoulombs) - :param m: particle mass (in g) - :param to_Hz: :code:`False` (DEFAULT). Set to :code:`True` to - return frequency in Hz (i.e. divide by :math:`2 * \\pi`) - """ - # ensure args have correct units - n = n.to(u.cm ** -3) - q = q.to(u.Fr) - m = m.to(u.g) - - # calculate - _op = np.sqrt((4.0 * conts.pi * n.value * (q.value * q.value)) - / m.value) - if to_Hz: - _op = (_op / (2.0 * conts.pi)) * u.Hz - else: - _op = _op * (u.rad / u.s) - return _op - - @utils.check_quantity({'B': {'units': u.Gauss, "can_be_negative": False}}) def oce(B: u.Quantity, **kwargs) -> u.Quantity: @@ -114,7 +77,7 @@ def oce(B: u.Quantity, **kwargs) -> u.Quantity: :param kwargs: supports any keywords used by :func:`cyclotron_frequency` """ - return cyclotron_frequency(-conts.e_gauss, B, conts.m_e, + return cyclotron_frequency(-const.e_gauss, B, const.m_e, **kwargs['kwargs']) @@ -137,5 +100,83 @@ def oci(Z: Union[int, float], B: u.Quantity, m_i: u.Quantity, :param kwargs: supports any keywords used by :func:`cyclotron_frequency` """ - return cyclotron_frequency(Z * conts.e_gauss, B, m_i, + return cyclotron_frequency(Z * const.e_gauss, B, m_i, **kwargs['kwargs']) + + +@utils.check_quantity({'n_e': {'units': u.cm ** -3, + 'can_be_negative': False}}) +def ope(n_e: u.Quantity, **kwargs) -> u.Quantity: + """ + electron-plasma frequency (in rad/s) + + .. math:: + + \\omega_{pe}^{2} = \\frac{4 \\pi n_{e} e^{2}}{m_e} + + :param n_e: electron number density (in number/cm^3) + :param kwargs: supports any keywords used by + :func:`plasma_frequency_generic` + """ + return plasma_frequency_generic(n_e, const.e_gauss, const.m_e, + **kwargs['kwargs']) + + +@utils.check_quantity({'n_i': {'units': u.cm ** -3, + 'can_be_negative': False}, + 'm_i': {'units': u.g, + 'can_be_negative': False}}) +def opi(n_i: u.Quantity, Z: Union[int, float], m_i: u.Quantity, + **kwargs) -> u.Quantity: + """ + ion-plasma frequency (in rad/s) + + .. math:: + + \\omega_{pi}^{2} = \\frac{4 \\pi n_{i} (Z e)^{2}}{m_i} + + :param n_i: ion number density (in number/cm^3) + :param Z: charge number + :param m_i: ion mass (in g) + :param kwargs: supports any keywords used by + :func:`plasma_frequency_generic` + """ + return plasma_frequency_generic(n_i, Z * const.e_gauss, m_i, + **kwargs['kwargs']) + + +@utils.check_quantity({'n': {'units': u.cm ** -3, + "can_be_negative": False}, + 'q': {'units': u.statcoulomb, + "can_be_negative": True}, + 'm': {'units': u.g, + "can_be_negative": False}}) +def plasma_frequency_generic( + n: u.Quantity, q: u.Quantity, m: u.Quantity, + to_Hz=False, **kwargs) -> u.Quantity: + """ + generalized plasma frequency (rad/s) + + .. math:: + + \\omega_{p}^{2} = \\frac{4 \\pi n q^{2}}{m} + + :param n: particle number density (in number/cm^3) + :param q: particle charge (in statcoulombs) + :param m: particle mass (in g) + :param to_Hz: :code:`False` (DEFAULT). Set to :code:`True` to + return frequency in Hz (i.e. divide by :math:`2 * \\pi`) + """ + # ensure args have correct units + n = n.to(u.cm ** -3) + q = q.to(u.Fr) + m = m.to(u.g) + + # calculate + _op = np.sqrt((4.0 * const.pi * n.value * (q.value * q.value)) + / m.value) + if to_Hz: + _op = (_op / (2.0 * const.pi)) * u.Hz + else: + _op = _op * (u.rad / u.s) + return _op From c65df60b1b13c58a719b5a0f628a93cb11cea598 Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Wed, 24 Apr 2019 18:32:49 -0700 Subject: [PATCH 13/68] updated auto-documentation --- docs/src/bapsflib.plasma.parameters.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/src/bapsflib.plasma.parameters.rst b/docs/src/bapsflib.plasma.parameters.rst index cf87feb3..c944ba2e 100644 --- a/docs/src/bapsflib.plasma.parameters.rst +++ b/docs/src/bapsflib.plasma.parameters.rst @@ -14,3 +14,6 @@ bapsflib\.plasma\.parameters cyclotron_frequency oce oci + ope + opi + plasma_frequency_generic From cf273d984264dcaf390e7fb95087471692dcc242 Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Wed, 24 Apr 2019 19:11:09 -0700 Subject: [PATCH 14/68] add plasma parameter for the Lower-Hybrid Resonance Frequency (`lower_hybrid_frequency` and [alias] `oLH`) --- bapsflib/plasma/parameters.py | 57 +++++++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 3 deletions(-) diff --git a/bapsflib/plasma/parameters.py b/bapsflib/plasma/parameters.py index 71f8e70a..3e4d8ed6 100644 --- a/bapsflib/plasma/parameters.py +++ b/bapsflib/plasma/parameters.py @@ -63,6 +63,46 @@ def cyclotron_frequency(q: u.Quantity, B: u.Quantity, m: u.Quantity, return _oc +@utils.check_quantity({'B': {'units': u.gauss, + 'can_be_negative': False}, + 'n_i': {'units': u.cm ** -3, + 'can_be_negative': False}, + 'm_i': {'units': u.g, + 'can_be_negative': False}}) +def lower_hybrid_frequency(B: u.Quantity, n_i: u.Quantity, + m_i: u.Quantity, Z: Union[int, float], + to_Hz=False, **kwargs) -> u.Quantity: + """ + Lower-Hybrid Resonance Frequency (rad/s) + + .. math:: + \\frac{1}{\\omega_{LH}^{2}}= + \\frac{1}{\\Omega_{ci}^{2}+\\omega_{pi}^{2}} + + \\frac{1}{\\lvert \\Omega_{ce}\\Omega_{ci} \\rvert} + + .. note:: + + This form is for a quasi-neutral plasma that satisfies + + .. math:: + \\frac{Z m_{e}}{m_{i}} \\ll + 1 - \\left(\\frac{V_{A}}{c}\\right)^{2} + + :param B: magnetic field (in Gauss) + :param m_i: ion mass (in g) + :param n_i: ion number density (in :math:`cm^{-3}`) + :param Z: ion charge number + :param to_Hz: :code:`False` (DEFAULT). Set to :code:`True` to + return frequency in Hz (i.e. divide by :math:`2 * \\pi`) + """ + _oci = oci(Z, B, m_i, to_Hz=to_Hz, **kwargs) + _opi = opi(n_i, Z, m_i, to_Hz=to_Hz, **kwargs) + _oce = oce(B, to_Hz=to_Hz, **kwargs) + first_term = 1.0 / ((_oci ** 2) + (_opi ** 2)) + second_term = 1.0 / np.abs(_oce * _oci) + _olh = np.sqrt(1.0 / (first_term + second_term)) + return _olh + @utils.check_quantity({'B': {'units': u.Gauss, "can_be_negative": False}}) def oce(B: u.Quantity, **kwargs) -> u.Quantity: @@ -104,6 +144,17 @@ def oci(Z: Union[int, float], B: u.Quantity, m_i: u.Quantity, **kwargs['kwargs']) +def oLH(B: u.Quantity, n_i: u.Quantity, + m_i: u.Quantity, Z: Union[int, float], + to_Hz=False, **kwargs) -> u.Quantity: + """ + Lower-Hybrid Resonance Frequency (rad/s) + + [alias for :func:`lower_hybrid_frequency`] + """ + return lower_hybrid_frequency(B, m_i, n_i, Z, to_Hz=to_Hz, **kwargs) + + @utils.check_quantity({'n_e': {'units': u.cm ** -3, 'can_be_negative': False}}) def ope(n_e: u.Quantity, **kwargs) -> u.Quantity: @@ -114,7 +165,7 @@ def ope(n_e: u.Quantity, **kwargs) -> u.Quantity: \\omega_{pe}^{2} = \\frac{4 \\pi n_{e} e^{2}}{m_e} - :param n_e: electron number density (in number/cm^3) + :param n_e: electron number density (in :math:`cm^{-3}`) :param kwargs: supports any keywords used by :func:`plasma_frequency_generic` """ @@ -135,7 +186,7 @@ def opi(n_i: u.Quantity, Z: Union[int, float], m_i: u.Quantity, \\omega_{pi}^{2} = \\frac{4 \\pi n_{i} (Z e)^{2}}{m_i} - :param n_i: ion number density (in number/cm^3) + :param n_i: ion number density (in :math:`cm^{-3}`) :param Z: charge number :param m_i: ion mass (in g) :param kwargs: supports any keywords used by @@ -161,7 +212,7 @@ def plasma_frequency_generic( \\omega_{p}^{2} = \\frac{4 \\pi n q^{2}}{m} - :param n: particle number density (in number/cm^3) + :param n: particle number density (in :math:`cm^{-3}`) :param q: particle charge (in statcoulombs) :param m: particle mass (in g) :param to_Hz: :code:`False` (DEFAULT). Set to :code:`True` to From af1e6f49a43f42ea31a3f9653c6fbe6df9534397 Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Wed, 24 Apr 2019 19:13:20 -0700 Subject: [PATCH 15/68] add `lower_hybrid_frequency` and `oLH` to auto-documentation --- docs/src/bapsflib.plasma.parameters.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/src/bapsflib.plasma.parameters.rst b/docs/src/bapsflib.plasma.parameters.rst index c944ba2e..447e2ef8 100644 --- a/docs/src/bapsflib.plasma.parameters.rst +++ b/docs/src/bapsflib.plasma.parameters.rst @@ -12,8 +12,10 @@ bapsflib\.plasma\.parameters :nosignatures: cyclotron_frequency + lower_hybrid_frequency oce oci + oLH ope opi plasma_frequency_generic From d8e93e37798c2b1d4362a66028303b5e222d4db2 Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Thu, 25 Apr 2019 09:46:42 -0700 Subject: [PATCH 16/68] add plasma parameter for the Upper-Hybrid frequency (`upper_hybrid_frequency` and [alias] `oUH`) --- bapsflib/plasma/parameters.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/bapsflib/plasma/parameters.py b/bapsflib/plasma/parameters.py index 3e4d8ed6..0ba9dffe 100644 --- a/bapsflib/plasma/parameters.py +++ b/bapsflib/plasma/parameters.py @@ -196,6 +196,16 @@ def opi(n_i: u.Quantity, Z: Union[int, float], m_i: u.Quantity, **kwargs['kwargs']) +def oUH(B: u.Quantity, n_e: u.Quantity, + to_Hz=False, **kwargs) -> u.Quantity: + """ + Upper-Hybrid Resonance Frequency (rad/s) + + [alias for :func:`upper_hybrid_frequency`] + """ + return upper_hybrid_frequency(B, n_e, to_Hz=to_Hz, **kwargs) + + @utils.check_quantity({'n': {'units': u.cm ** -3, "can_be_negative": False}, 'q': {'units': u.statcoulomb, @@ -231,3 +241,27 @@ def plasma_frequency_generic( else: _op = _op * (u.rad / u.s) return _op + + +@utils.check_quantity({'B': {'units': u.gauss, + 'can_be_negative': False}, + 'n_e': {'units': u.cm ** -3, + 'can_be_negative': False}}) +def upper_hybrid_frequency(B: u.Quantity, n_e: u.Quantity, + to_Hz=False, **kwargs) -> u.Quantity: + """ + Upper-Hybrid Resonance frequency (rad/s) + + .. math:: + + \\omega_{UH}^{2} =\\omega_{pe}^{2} + \\Omega_{ce}^{2} + + :param B: magnetic field (in Gauss) + :param n_e: electron number density (in :math:`cm^{-3}`) + :param to_Hz: :code:`False` (DEFAULT). Set to :code:`True` to + return frequency in Hz (i.e. divide by :math:`2 * \\pi`) + """ + _oce = oce(B, to_Hz=to_Hz, **kwargs) + _ope = ope(n_e, to_Hz=to_Hz, **kwargs) + _ouh = np.sqrt((_ope ** 2) + (_oce ** 2)) + return _ouh From 21ea8842af4b19521df496ee89a9d86c65e683f7 Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Thu, 25 Apr 2019 09:47:10 -0700 Subject: [PATCH 17/68] add `upper_hybrid_frequency` and `oUH` to auto-documentation --- docs/src/bapsflib.plasma.parameters.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/src/bapsflib.plasma.parameters.rst b/docs/src/bapsflib.plasma.parameters.rst index 447e2ef8..1ee24253 100644 --- a/docs/src/bapsflib.plasma.parameters.rst +++ b/docs/src/bapsflib.plasma.parameters.rst @@ -6,16 +6,18 @@ bapsflib\.plasma\.parameters :members: :undoc-members: - .. rubric:: Functions + .. rubric:: Frequencies .. autosummary:: :nosignatures: cyclotron_frequency - lower_hybrid_frequency oce oci + lower_hybrid_frequency oLH + plasma_frequency_generic ope opi - plasma_frequency_generic + upper_hybrid_frequency + oUH From 2dfa0040a2be12c8faec7727362c7c5e22758b68 Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Thu, 25 Apr 2019 10:50:04 -0700 Subject: [PATCH 18/68] make docstrings case consistent; add plasma parameters of inertial length (`inertial_length`, `lpi`, and `lpe`) --- bapsflib/plasma/parameters.py | 74 +++++++++++++++++++++++++++++++---- 1 file changed, 67 insertions(+), 7 deletions(-) diff --git a/bapsflib/plasma/parameters.py b/bapsflib/plasma/parameters.py index 0ba9dffe..db345e70 100644 --- a/bapsflib/plasma/parameters.py +++ b/bapsflib/plasma/parameters.py @@ -37,7 +37,7 @@ def cyclotron_frequency(q: u.Quantity, B: u.Quantity, m: u.Quantity, to_Hz=False, **kwargs) -> u.Quantity: """ - particle cyclotron frequency (rad/s) + generalized cyclotron frequency (rad/s) .. math:: @@ -73,7 +73,7 @@ def lower_hybrid_frequency(B: u.Quantity, n_i: u.Quantity, m_i: u.Quantity, Z: Union[int, float], to_Hz=False, **kwargs) -> u.Quantity: """ - Lower-Hybrid Resonance Frequency (rad/s) + Lower-Hybrid resonance Frequency (rad/s) .. math:: \\frac{1}{\\omega_{LH}^{2}}= @@ -117,7 +117,7 @@ def oce(B: u.Quantity, **kwargs) -> u.Quantity: :param kwargs: supports any keywords used by :func:`cyclotron_frequency` """ - return cyclotron_frequency(-const.e_gauss, B, const.m_e, + return cyclotron_frequency(-const.e_gauss, B, const.m_e.cgs, **kwargs['kwargs']) @@ -148,7 +148,7 @@ def oLH(B: u.Quantity, n_i: u.Quantity, m_i: u.Quantity, Z: Union[int, float], to_Hz=False, **kwargs) -> u.Quantity: """ - Lower-Hybrid Resonance Frequency (rad/s) + Lower-Hybrid resonance frequency (rad/s) [alias for :func:`lower_hybrid_frequency`] """ @@ -169,7 +169,7 @@ def ope(n_e: u.Quantity, **kwargs) -> u.Quantity: :param kwargs: supports any keywords used by :func:`plasma_frequency_generic` """ - return plasma_frequency_generic(n_e, const.e_gauss, const.m_e, + return plasma_frequency_generic(n_e, const.e_gauss, const.m_e.cgs, **kwargs['kwargs']) @@ -199,7 +199,7 @@ def opi(n_i: u.Quantity, Z: Union[int, float], m_i: u.Quantity, def oUH(B: u.Quantity, n_e: u.Quantity, to_Hz=False, **kwargs) -> u.Quantity: """ - Upper-Hybrid Resonance Frequency (rad/s) + Upper-Hybrid resonance frequency (rad/s) [alias for :func:`upper_hybrid_frequency`] """ @@ -250,7 +250,7 @@ def plasma_frequency_generic( def upper_hybrid_frequency(B: u.Quantity, n_e: u.Quantity, to_Hz=False, **kwargs) -> u.Quantity: """ - Upper-Hybrid Resonance frequency (rad/s) + Upper-Hybrid resonance frequency (rad/s) .. math:: @@ -265,3 +265,63 @@ def upper_hybrid_frequency(B: u.Quantity, n_e: u.Quantity, _ope = ope(n_e, to_Hz=to_Hz, **kwargs) _ouh = np.sqrt((_ope ** 2) + (_oce ** 2)) return _ouh + + +# ---- Lengths ---- +@utils.check_quantity({'n': {'units': u.cm ** -3, + 'can_be_negative': False}, + 'q': {'units': u.statcoulomb, + 'can_be_negative': True}, + 'm': {'units': u.g, + 'can_be_negative': False}}) +def inertial_length(n: u.Quantity, q: u.Quantity, m: u.Quantity, + **kwargs) -> u.Quantity: + """ + generalized inertial length (cm) + + .. math:: + + l =\\frac{c}{\\omega_{p}} + + :param n: particle number density (in :math:`cm^{-3}`) + :param q: particle charge (in statcoulomb) + :param m: particle mass (in g) + """ + _op = plasma_frequency_generic(n, q, m, **kwargs) + _l = (const.c.cgs.value / _op.value) * u.cm + return _l + + +@utils.check_quantity({'n_e': {'units': u.cm ** -3, + 'can_be_negative': False}}) +def lpe(n_e: u.Quantity, **kwargs) -> u.Quantity: + """ + electron-inertial length (cm) + + .. math:: + + l_{pe} =\\frac{c}{\\omega_{pe}} + + :param n_e: electron number density (in :math:`cm^{-3}`) + """ + return inertial_length(n_e, -const.e_gauss, const.m_e, **kwargs) + + +@utils.check_quantity({'n_i': {'units': u.cm ** -3, + 'can_be_negative': False}, + 'm_i': {'units': u.g, + 'can_be_negative': False}}) +def lpi(n_i: u.Quantity, Z: Union[int, float], m_i: u.Quantity, + **kwargs) -> u.Quantity: + """ + ion-inertial length (cm) + + .. math:: + + l_{pi} =\\frac{c}{\\omega_{pi}} + + :param n_i: ion number density (in :math:`cm^{-3}`) + :param Z: charge number + :param m_i: ion mass (in g) + """ + return inertial_length(n_i, Z * const.e_gauss, m_i, **kwargs) From 639a25e23553ce43e262b5207825c8714359eb35 Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Thu, 25 Apr 2019 10:50:37 -0700 Subject: [PATCH 19/68] add inertial length to auto-documentation; create a rubric title for "Lengths" --- docs/src/bapsflib.plasma.parameters.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/src/bapsflib.plasma.parameters.rst b/docs/src/bapsflib.plasma.parameters.rst index 1ee24253..4c0ca6db 100644 --- a/docs/src/bapsflib.plasma.parameters.rst +++ b/docs/src/bapsflib.plasma.parameters.rst @@ -21,3 +21,12 @@ bapsflib\.plasma\.parameters opi upper_hybrid_frequency oUH + + .. rubric:: Lengths + + .. autosummary:: + :nosignatures: + + inertial_length + lpe + lpi From 8c14f5ff51586af21be807dfb2bd3afa88db69b5 Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Thu, 25 Apr 2019 11:15:41 -0700 Subject: [PATCH 20/68] refactor plasma parameter `plasma_frequency_generic` -> `plasma_frequency` --- bapsflib/plasma/parameters.py | 16 ++++++++-------- docs/src/bapsflib.plasma.parameters.rst | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/bapsflib/plasma/parameters.py b/bapsflib/plasma/parameters.py index db345e70..ae0e6185 100644 --- a/bapsflib/plasma/parameters.py +++ b/bapsflib/plasma/parameters.py @@ -167,10 +167,10 @@ def ope(n_e: u.Quantity, **kwargs) -> u.Quantity: :param n_e: electron number density (in :math:`cm^{-3}`) :param kwargs: supports any keywords used by - :func:`plasma_frequency_generic` + :func:`plasma_frequency` """ - return plasma_frequency_generic(n_e, const.e_gauss, const.m_e.cgs, - **kwargs['kwargs']) + return plasma_frequency(n_e, const.e_gauss, const.m_e.cgs, + **kwargs['kwargs']) @utils.check_quantity({'n_i': {'units': u.cm ** -3, @@ -190,10 +190,10 @@ def opi(n_i: u.Quantity, Z: Union[int, float], m_i: u.Quantity, :param Z: charge number :param m_i: ion mass (in g) :param kwargs: supports any keywords used by - :func:`plasma_frequency_generic` + :func:`plasma_frequency` """ - return plasma_frequency_generic(n_i, Z * const.e_gauss, m_i, - **kwargs['kwargs']) + return plasma_frequency(n_i, Z * const.e_gauss, m_i, + **kwargs['kwargs']) def oUH(B: u.Quantity, n_e: u.Quantity, @@ -212,7 +212,7 @@ def oUH(B: u.Quantity, n_e: u.Quantity, "can_be_negative": True}, 'm': {'units': u.g, "can_be_negative": False}}) -def plasma_frequency_generic( +def plasma_frequency( n: u.Quantity, q: u.Quantity, m: u.Quantity, to_Hz=False, **kwargs) -> u.Quantity: """ @@ -287,7 +287,7 @@ def inertial_length(n: u.Quantity, q: u.Quantity, m: u.Quantity, :param q: particle charge (in statcoulomb) :param m: particle mass (in g) """ - _op = plasma_frequency_generic(n, q, m, **kwargs) + _op = plasma_frequency(n, q, m, **kwargs) _l = (const.c.cgs.value / _op.value) * u.cm return _l diff --git a/docs/src/bapsflib.plasma.parameters.rst b/docs/src/bapsflib.plasma.parameters.rst index 4c0ca6db..48bc50b7 100644 --- a/docs/src/bapsflib.plasma.parameters.rst +++ b/docs/src/bapsflib.plasma.parameters.rst @@ -16,7 +16,7 @@ bapsflib\.plasma\.parameters oci lower_hybrid_frequency oLH - plasma_frequency_generic + plasma_frequency ope opi upper_hybrid_frequency From 7ee412afbc95ea34377651052408f182b5f6534e Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Thu, 25 Apr 2019 11:16:40 -0700 Subject: [PATCH 21/68] rework docstrings for `oLH` and `oUP` such that the "alias" tag shows up in the autosummary table --- bapsflib/plasma/parameters.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/bapsflib/plasma/parameters.py b/bapsflib/plasma/parameters.py index ae0e6185..593be55c 100644 --- a/bapsflib/plasma/parameters.py +++ b/bapsflib/plasma/parameters.py @@ -148,8 +148,7 @@ def oLH(B: u.Quantity, n_i: u.Quantity, m_i: u.Quantity, Z: Union[int, float], to_Hz=False, **kwargs) -> u.Quantity: """ - Lower-Hybrid resonance frequency (rad/s) - + Lower-Hybrid resonance frequency (rad/s) -- [alias for :func:`lower_hybrid_frequency`] """ return lower_hybrid_frequency(B, m_i, n_i, Z, to_Hz=to_Hz, **kwargs) @@ -199,8 +198,7 @@ def opi(n_i: u.Quantity, Z: Union[int, float], m_i: u.Quantity, def oUH(B: u.Quantity, n_e: u.Quantity, to_Hz=False, **kwargs) -> u.Quantity: """ - Upper-Hybrid resonance frequency (rad/s) - + Upper-Hybrid resonance frequency (rad/s) -- [alias for :func:`upper_hybrid_frequency`] """ return upper_hybrid_frequency(B, n_e, to_Hz=to_Hz, **kwargs) From 986813ec2795022ea16bcdb880f1f2e54243459b Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Thu, 25 Apr 2019 11:17:17 -0700 Subject: [PATCH 22/68] add plasma parameter Debye length (`Debye_length` and [alias] `lD`) --- bapsflib/plasma/parameters.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/bapsflib/plasma/parameters.py b/bapsflib/plasma/parameters.py index 593be55c..d6825ee1 100644 --- a/bapsflib/plasma/parameters.py +++ b/bapsflib/plasma/parameters.py @@ -266,6 +266,29 @@ def upper_hybrid_frequency(B: u.Quantity, n_e: u.Quantity, # ---- Lengths ---- +@utils.check_quantity({'kTe': {'units': u.eV, + 'can_be_negative': False}, + 'n': {'units': u.cm ** -3, + 'can_be_negative': False}}) +def Debye_length(kTe: u.Quantity, n: u.Quantity, + **kwargs) -> u.Quantity: + """ + Debye length (in cm) + + .. math:: + + \\lambda_{D} = \\sqrt{\\frac{k_{B} T_{e}}{4 \\pi n e^{2}}} + + :param kTe: electron temperature (in eV) + :param n: number density (in :math:`cm^{-3}`) + """ + # ensure args have correct units + kTe = kTe.to(u.erg) + n = n.to(u.cm ** -3) + _lD = np.sqrt(kTe / (4.0 * const.pi * n * (const.e_gauss ** 2))) + return _lD.cgs + + @utils.check_quantity({'n': {'units': u.cm ** -3, 'can_be_negative': False}, 'q': {'units': u.statcoulomb, @@ -290,6 +313,13 @@ def inertial_length(n: u.Quantity, q: u.Quantity, m: u.Quantity, return _l +def lD(kTe: u.Quantity, n: u.Quantity, **kwargs) -> u.Quantity: + """ + Debye length (in cm) -- [alias for :func:`Debye_length`] + """ + return Debye_length(kTe, n, **kwargs) + + @utils.check_quantity({'n_e': {'units': u.cm ** -3, 'can_be_negative': False}}) def lpe(n_e: u.Quantity, **kwargs) -> u.Quantity: From fd7a726322954a141315083a53dda478c3f1faf0 Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Thu, 25 Apr 2019 11:17:41 -0700 Subject: [PATCH 23/68] add `Debye_length` and `lD` to the auto-documentation --- docs/src/bapsflib.plasma.parameters.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/src/bapsflib.plasma.parameters.rst b/docs/src/bapsflib.plasma.parameters.rst index 48bc50b7..0c0b6172 100644 --- a/docs/src/bapsflib.plasma.parameters.rst +++ b/docs/src/bapsflib.plasma.parameters.rst @@ -27,6 +27,8 @@ bapsflib\.plasma\.parameters .. autosummary:: :nosignatures: + Debye_length + lD inertial_length lpe lpi From 04a13aa96c1f2814234a3d2fbe3ed1ae83b4fc91 Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Thu, 25 Apr 2019 16:37:51 -0700 Subject: [PATCH 24/68] create Exception `PhysicsError` --- bapsflib/plasma/parameters.py | 1 + bapsflib/utils/errors.py | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/bapsflib/plasma/parameters.py b/bapsflib/plasma/parameters.py index d6825ee1..2a5e0fba 100644 --- a/bapsflib/plasma/parameters.py +++ b/bapsflib/plasma/parameters.py @@ -23,6 +23,7 @@ import numpy as np from . import constants as const +from bapsflib.utils.errors import PhysicsError from plasmapy import utils from typing import Union diff --git a/bapsflib/utils/errors.py b/bapsflib/utils/errors.py index 3c2370b2..73f7996a 100644 --- a/bapsflib/utils/errors.py +++ b/bapsflib/utils/errors.py @@ -34,3 +34,8 @@ class HDFReadControlError(HDFReadError): class HDFReadMSIError(HDFReadError): """Exception for failed HDF5 reading of digitizer.""" pass + + +class PhysicsError(Exception): + """Exception for physics errors.""" + pass From 93a40a18dd578cbf694d9077a34393d79114c592 Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Thu, 25 Apr 2019 16:38:26 -0700 Subject: [PATCH 25/68] add plasma parameter ion sound speed (`ion_sound_speed` and [alias] `cs`) --- bapsflib/plasma/parameters.py | 72 +++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/bapsflib/plasma/parameters.py b/bapsflib/plasma/parameters.py index 2a5e0fba..c5cffebe 100644 --- a/bapsflib/plasma/parameters.py +++ b/bapsflib/plasma/parameters.py @@ -354,3 +354,75 @@ def lpi(n_i: u.Quantity, Z: Union[int, float], m_i: u.Quantity, :param m_i: ion mass (in g) """ return inertial_length(n_i, Z * const.e_gauss, m_i, **kwargs) + + +# ---- Velocities ---- +def cs(kTe: u.Quantity, m_i: u.Quantity, + kTi: u.Quantity = None, + gamma_e: Union[int, float] = None, + gamma_i: Union[int, float] = None, + Z: Union[int, float] = None, **kwargs) -> u.Quantity: + """ + ion-sound speed (cm/s) -- [alias for :func:`ion_sound_speed` + """ + for name, val in zip(('kTi', 'gamma_e', 'gamma_i', 'Z'), + (kTi, gamma_e, gamma_i, Z)): + if val is not None: + kwargs[name] = val + print(kwargs) + + return ion_sound_speed(kTe, m_i, **kwargs) + + +@utils.check_relativistic +@utils.check_quantity({'kTe': {'units': u.eV, + 'can_be_negative': False}, + 'kTi': {'units': u.eV, + 'can_be_negative': False}, + 'm_i': {'units': u.g, + 'can_be_negative': False}}) +def ion_sound_speed(kTe: u.Quantity, m_i: u.Quantity, + kTi: u.Quantity = (0.0 * u.eV), + gamma_e: Union[int, float] = 1, + gamma_i: Union[int, float] = 3, + Z: Union[int, float] = 1, **kwargs) -> u.Quantity: + """ + ion sound speed (cm/s) + + .. math:: + + c_{s}^{2} = \\frac{\\gamma_{e} Z k_{B} T_{e} + + \\gamma_i k_{B} T_{i}}{m_{i}} + + :param kTe: electron temperature (in eV) + :param kTi: ion temperature (in eV) + :param m_i: ion mass (in g) + :param gamma_e: adiabatic index for electrons + (:math:`\\gamma_e=3` DEFAULT) + :param gamma_i: adiabatic index for ions + (:math:`\\gamma_i=3` DEFAULT) + :param Z: ion charge number (:math:`Z=1` DEFAULT) + """ + # condition keywords + for name, val in zip(('gamma_i', 'gamma_e', 'Z'), + (gamma_i, gamma_e, Z)): + if not isinstance(val, (int, np.integer, float, np.floating)): + raise TypeError(name + ' must be of type int or float') + elif name == 'Z' and val <= 0: + raise PhysicsError( + 'The ion charge number Z must be greater than 0.') + elif val < 1: + raise PhysicsError('The adiabatic index ' + name + + ' must be greater than or equal to 1.') + + # convert temperature to required units + kTe = kTe.to(u.erg) + kTi = kTi.to(u.erg) + m_i = m_i.cgs + + # calculate + _cs = gamma_e * Z * kTe + if kTi != 0: + _cs += gamma_i * kTi + _cs = np.sqrt(_cs / m_i).to(u.cm / u.s) + return _cs From 8e0b3e2c7c8ff5b658258865050c1231609ce955 Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Thu, 25 Apr 2019 16:39:10 -0700 Subject: [PATCH 26/68] create "Velocities" function table; add `ion_sound_speed` and `cs` to auto-documentation --- docs/src/bapsflib.plasma.parameters.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/src/bapsflib.plasma.parameters.rst b/docs/src/bapsflib.plasma.parameters.rst index 0c0b6172..66c6ffa7 100644 --- a/docs/src/bapsflib.plasma.parameters.rst +++ b/docs/src/bapsflib.plasma.parameters.rst @@ -32,3 +32,11 @@ bapsflib\.plasma\.parameters inertial_length lpe lpi + + .. rubric:: Velocities + + .. autosummary:: + :nosignatures: + + ion_sound_speed + cs From 37ab77f9a5463708edceede47d84f9afee396e02 Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Thu, 25 Apr 2019 18:20:34 -0700 Subject: [PATCH 27/68] add __all__; improve arg and kwarg passthroughs --- bapsflib/plasma/parameters.py | 86 +++++++++++++++++++++++++++-------- 1 file changed, 68 insertions(+), 18 deletions(-) diff --git a/bapsflib/plasma/parameters.py b/bapsflib/plasma/parameters.py index c5cffebe..db065915 100644 --- a/bapsflib/plasma/parameters.py +++ b/bapsflib/plasma/parameters.py @@ -28,6 +28,15 @@ from typing import Union +__all__ = ['cyclotron_frequency', 'oce', 'oci', + 'lower_hybrid_frequency', 'oLH', + 'plasma_frequency', 'ope', 'opi', + 'upper_hybrid_frequency', 'oUH', + 'Debye_length', 'lD', + 'inertial_length', 'lpe', 'lpi', + 'ion_sound_speed', 'cs'] + + # ---- Frequencies ---- @utils.check_quantity({'q': {'units': u.statcoulomb, "can_be_negative": True}, @@ -71,7 +80,7 @@ def cyclotron_frequency(q: u.Quantity, B: u.Quantity, m: u.Quantity, 'm_i': {'units': u.g, 'can_be_negative': False}}) def lower_hybrid_frequency(B: u.Quantity, n_i: u.Quantity, - m_i: u.Quantity, Z: Union[int, float], + m_i: u.Quantity, Z: Union[int, float] = 1, to_Hz=False, **kwargs) -> u.Quantity: """ Lower-Hybrid resonance Frequency (rad/s) @@ -92,12 +101,20 @@ def lower_hybrid_frequency(B: u.Quantity, n_i: u.Quantity, :param B: magnetic field (in Gauss) :param m_i: ion mass (in g) :param n_i: ion number density (in :math:`cm^{-3}`) - :param Z: ion charge number + :param Z: ion charge number (:math:`Z=1` DEFAULT) :param to_Hz: :code:`False` (DEFAULT). Set to :code:`True` to return frequency in Hz (i.e. divide by :math:`2 * \\pi`) """ - _oci = oci(Z, B, m_i, to_Hz=to_Hz, **kwargs) - _opi = opi(n_i, Z, m_i, to_Hz=to_Hz, **kwargs) + # condition Z + if not isinstance(Z, (int, np.integer, float, np.floating)): + raise TypeError('Z must be of type int or float') + elif Z <= 0: + raise PhysicsError( + 'The ion charge number Z must be greater than 0.') + + # calculate + _oci = oci(B, m_i, Z=Z, to_Hz=to_Hz, **kwargs) + _opi = opi(n_i, m_i, Z=Z, to_Hz=to_Hz, **kwargs) _oce = oce(B, to_Hz=to_Hz, **kwargs) first_term = 1.0 / ((_oci ** 2) + (_opi ** 2)) second_term = 1.0 / np.abs(_oce * _oci) @@ -126,7 +143,7 @@ def oce(B: u.Quantity, **kwargs) -> u.Quantity: "can_be_negative": False}, 'm_i': {'units': u.g, "can_be_negative": False}}) -def oci(Z: Union[int, float], B: u.Quantity, m_i: u.Quantity, +def oci(B: u.Quantity, m_i: u.Quantity, Z: Union[int, float] = 1, **kwargs) -> u.Quantity: """ ion-cyclotron frequency (rad/s) @@ -135,24 +152,36 @@ def oci(Z: Union[int, float], B: u.Quantity, m_i: u.Quantity, \\Omega_{ci} = \\frac{Z |e| B}{m_{i} c} - :param Z: charge number :param B: magnetic-field (in Gauss) :param m_i: ion mass (in grams) + :param Z: ion charge number (:math:`Z=1` DEFAULT) :param kwargs: supports any keywords used by :func:`cyclotron_frequency` """ + # condition Z + if not isinstance(Z, (int, np.integer, float, np.floating)): + raise TypeError('Z must be of type int or float') + elif Z <= 0: + raise PhysicsError( + 'The ion charge number Z must be greater than 0.') + return cyclotron_frequency(Z * const.e_gauss, B, m_i, **kwargs['kwargs']) -def oLH(B: u.Quantity, n_i: u.Quantity, - m_i: u.Quantity, Z: Union[int, float], - to_Hz=False, **kwargs) -> u.Quantity: +def oLH(B: u.Quantity, n_i: u.Quantity, m_i: u.Quantity, + Z: Union[int, float] = None, to_Hz: bool = False, + **kwargs) -> u.Quantity: """ Lower-Hybrid resonance frequency (rad/s) -- [alias for :func:`lower_hybrid_frequency`] """ - return lower_hybrid_frequency(B, m_i, n_i, Z, to_Hz=to_Hz, **kwargs) + # add specified keywords to kwargs + for name, val in zip(('Z', 'to_Hz'), (Z, to_Hz)): + if val is not None: + kwargs[name] = val + + return lower_hybrid_frequency(B, m_i, n_i, **kwargs) @utils.check_quantity({'n_e': {'units': u.cm ** -3, @@ -177,8 +206,8 @@ def ope(n_e: u.Quantity, **kwargs) -> u.Quantity: 'can_be_negative': False}, 'm_i': {'units': u.g, 'can_be_negative': False}}) -def opi(n_i: u.Quantity, Z: Union[int, float], m_i: u.Quantity, - **kwargs) -> u.Quantity: +def opi(n_i: u.Quantity, m_i: u.Quantity, + Z: Union[int, float] = 1, **kwargs) -> u.Quantity: """ ion-plasma frequency (in rad/s) @@ -187,22 +216,34 @@ def opi(n_i: u.Quantity, Z: Union[int, float], m_i: u.Quantity, \\omega_{pi}^{2} = \\frac{4 \\pi n_{i} (Z e)^{2}}{m_i} :param n_i: ion number density (in :math:`cm^{-3}`) - :param Z: charge number :param m_i: ion mass (in g) + :param Z: ion charge number (:math:`Z=1` DEFAULT) :param kwargs: supports any keywords used by :func:`plasma_frequency` """ + # condition Z + if not isinstance(Z, (int, np.integer, float, np.floating)): + raise TypeError('Z must be of type int or float') + elif Z <= 0: + raise PhysicsError( + 'The ion charge number Z must be greater than 0.') + return plasma_frequency(n_i, Z * const.e_gauss, m_i, **kwargs['kwargs']) def oUH(B: u.Quantity, n_e: u.Quantity, - to_Hz=False, **kwargs) -> u.Quantity: + to_Hz: bool = False, **kwargs) -> u.Quantity: """ Upper-Hybrid resonance frequency (rad/s) -- [alias for :func:`upper_hybrid_frequency`] """ - return upper_hybrid_frequency(B, n_e, to_Hz=to_Hz, **kwargs) + # add specified keywords to kwargs + for name, val in zip(('to_Hz', ), (to_Hz, )): + if val is not None: + kwargs[name] = val + + return upper_hybrid_frequency(B, n_e, **kwargs) @utils.check_quantity({'n': {'units': u.cm ** -3, @@ -286,6 +327,8 @@ def Debye_length(kTe: u.Quantity, n: u.Quantity, # ensure args have correct units kTe = kTe.to(u.erg) n = n.to(u.cm ** -3) + + # calculate _lD = np.sqrt(kTe / (4.0 * const.pi * n * (const.e_gauss ** 2))) return _lD.cgs @@ -340,8 +383,8 @@ def lpe(n_e: u.Quantity, **kwargs) -> u.Quantity: 'can_be_negative': False}, 'm_i': {'units': u.g, 'can_be_negative': False}}) -def lpi(n_i: u.Quantity, Z: Union[int, float], m_i: u.Quantity, - **kwargs) -> u.Quantity: +def lpi(n_i: u.Quantity, m_i: u.Quantity, + Z: Union[int, float] = 1, **kwargs) -> u.Quantity: """ ion-inertial length (cm) @@ -353,6 +396,13 @@ def lpi(n_i: u.Quantity, Z: Union[int, float], m_i: u.Quantity, :param Z: charge number :param m_i: ion mass (in g) """ + # condition Z + if not isinstance(Z, (int, np.integer, float, np.floating)): + raise TypeError('Z must be of type int or float') + elif Z <= 0: + raise PhysicsError( + 'The ion charge number Z must be greater than 0.') + return inertial_length(n_i, Z * const.e_gauss, m_i, **kwargs) @@ -365,11 +415,11 @@ def cs(kTe: u.Quantity, m_i: u.Quantity, """ ion-sound speed (cm/s) -- [alias for :func:`ion_sound_speed` """ + # add specified keywords to kwargs for name, val in zip(('kTi', 'gamma_e', 'gamma_i', 'Z'), (kTi, gamma_e, gamma_i, Z)): if val is not None: kwargs[name] = val - print(kwargs) return ion_sound_speed(kTe, m_i, **kwargs) From d911da6f9fca9bd2b28827e1c9f9e32501513ed2 Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Thu, 25 Apr 2019 18:55:27 -0700 Subject: [PATCH 28/68] slowly removing core.py and elements as bapsflib.plasma.constants and bapsflib.plasma.parameters are built up --- bapsflib/plasma/core.py | 325 ++-------------------------------------- 1 file changed, 9 insertions(+), 316 deletions(-) diff --git a/bapsflib/plasma/core.py b/bapsflib/plasma/core.py index bd5056ef..d2d7d689 100644 --- a/bapsflib/plasma/core.py +++ b/bapsflib/plasma/core.py @@ -13,15 +13,17 @@ # TODO: add collision frequencies # TODO: add mean-free-paths # -"""Core plasma paramters in (cgs).""" +"""Core plasma parameters in (cgs).""" +import astropy.constants as const +import astropy.units as u import math from scipy import constants -pconst = constants.physical_constants - +#pconst = constants.physical_constants +''' class FloatUnit(float): """Template class for floats with a unit attribute.""" @@ -66,295 +68,9 @@ def __init__(self, value, cgs_unit): def unit(self): """units of constant""" return self._unit - - -#: atomic mass unit (g) -AMU = FloatUnit(1000.0 * constants.m_u, 'g') - -#: speed of light (cm/s) -C = FloatUnit(100.0 * constants.c, 'cm s^-1') - -#: fundamental charge (statcoul) -E = FloatUnit(4.8032e-10, 'statcoul') - -#: Boltzmann constant (erg/K) -KB = FloatUnit(1.3807e-16, 'erg k^-1') - -#: electron mass (g) -ME = FloatUnit(1000.0 * constants.m_e, 'g') - -#: proton mass (g) -MP = FloatUnit(1000.0 * constants.m_p, 'g') - - -# ---- frequency constants ---- -def fce(Bo, **kwargs): - """ - electron-cyclotron frequency (Hz) - - .. math:: - - f_{ce} = \\frac{\Omega_{ce}}{2 \pi} - = -\\frac{|e| B_{o}}{2 \pi m_{e} c} - - :param float Bo: magnetic field (in Gauss) - - .. note:: see function :func:`oce` - """ - _fce = oce(Bo) / (2.0 * math.pi) - return FloatUnit(_fce, 'Hz') - - -def fci(Bo, m_i, Z, **kwargs): - """ - ion-cyclotron frequency (Hz) - - .. math:: - - f_{ci} = \\frac{\Omega_{ci}}{2 \pi} - = \\frac{Z |e| B_{o}}{2 \pi m_{i} c} - - :param float Bo: magnetic-field (in Gauss) - :param float m_i: ion-mass (in g) - :param int Z: charge number - - .. note:: see function :func:`oci` - """ - _fci = oci(Bo, m_i, Z) / (2.0 * constants.pi) - return FloatUnit(_fci, 'Hz') - - -def fLH(Bo, m_i, n_i, Z, **kwargs): - """ - Lower-Hybrid Resonance frequency (Hz) - - .. math:: - f_{LH} = \\frac{\omega_{LH}}{2 \pi} - - :param float Bo: magnetic field (in Gauss) - :param float m_i: ion mass (in g) - :param float n_i: ion number density (in :math:`cm^{-3}`) - :param int Z: ion charge number - - .. note:: for details see function :func:`oLH` - """ - _fLH = oLH(Bo, m_i, n_i, Z) / (2.0 * math.pi) - return FloatUnit(_fLH, 'Hz') - - -def fpe(n_e, **kwargs): - """ - electron-plasma frequency (Hz) - - .. math:: - - f_{pe} = \\frac{\omega_{pe}}{2 \pi} - = \sqrt{\\frac{n_{e} e^{2}}{\pi m_{e}}} - - :param float n_e: electron number density (in :math:`cm^{-3}`) - - .. note:: see function :func:`ope` - """ - _fpe = ope(n_e) / (2.0 * math.pi) - return FloatUnit(_fpe, 'Hz') - - -def fpi(m_i, n_i, Z, **kwargs): - """ - ion-plasma frequency (Hz) - - .. math:: - - f_{pi} = \\frac{\omega_{pi}}{2 \pi} - = \sqrt{\\frac{n_{i} (Z e)^{2}}{\pi m_{i}}} - - :param float m_i: ion mass (in g) - :param float n_i: ion number density (in :math:`cm^{-3}`) - :param int Z: ion charge number - - .. note:: see function :func:`opi` - """ - _fpi = opi(m_i, n_i, Z) / (2.0 * math.pi) - return FloatUnit(_fpi, 'Hz') - - -def fUH(Bo, n_e, **kwargs): - """ - Upper-Hybrid Resonance frequency (Hz) - - .. math:: - - f_{UH} = \\frac{\omega_{UH}}{2 \pi} - - :param float Bo: magnetic field (in Gauss) - :param float n_e: electron number density (in :math:`cm^{-3}`) - - .. note:: see function :func:`oUH` - """ - _fUH = oUH(Bo, n_e) / (2.0 * math.pi) - return FloatUnit(_fUH, 'Hz') - - -def oce(Bo, **kwargs): - """ - electron-cyclotron frequency (rad/s) - - .. math:: - - \Omega_{ce} = -\\frac{|e| B_{o}}{m_{e} c} - - :param float Bo: magnetic-field (in Gauss) - """ - _oce = (-E * Bo) / (ME * C) - return FloatUnit(_oce, 'rad s^-1') - - -def oci(Bo, m_i, Z, **kwargs): - """ - ion-cyclotron frequency (rads / s) - - .. math:: - - \Omega_{ci} = \\frac{Z |e| B_{o}}{m_{i} c} - - :param float Bo: magnetic-field (in Gauss) - :param float m_i: ion-mass (in g) - :param int Z: charge number - """ - _oci = (Z * E * Bo) / (m_i * C) - return FloatUnit(_oci, 'rad s^-1') - - -def oLH(Bo, m_i, n_i, Z, **kwargs): - """ - Lower-Hybrid Resonance frequency (rad/s) - - .. math:: - \\frac{1}{\omega_{LH}^{2}}= - \\frac{1}{\Omega_{i}^{2}+\omega_{pi}^{2}} - + \\frac{1}{\\lvert \Omega_{e}\Omega_{i} \\rvert} - - .. note:: - - This form is for a quasi-neutral plasma that satisfies - - .. math:: - \\frac{Z m_{e}}{m_{i}} \ll - 1 - \\left(\\frac{V_{A}}{c}\\right)^{2} - - :param float Bo: magnetic field (in Gauss) - :param float m_i: ion mass (in g) - :param float n_i: ion number density (in :math:`cm^{-3}`) - :param int Z: ion charge number - """ - _args = {'Bo': Bo, 'm_i': m_i, 'n_i': n_i, 'Z': Z} - _opi = opi(**_args) - _oce = oce(**_args) - _oci = oci(**_args) - first_term = 1.0 / ((_oci ** 2) + (_opi ** 2)) - second_term = 1.0 / math.fabs(_oce * _oci) - _olh = math.sqrt(1.0 / (first_term + second_term)) - return FloatUnit(_olh, 'rad s^-1') - - -def ope(n_e, **kwargs): - """ - electron-plasma frequency (in rad/s) - - .. math:: - - \omega_{pe}^{2} = \\frac{4 \pi n_{e} e^2}{m_{e}} - - :param float n_e: electron number density (in :math:`cm^{-3}`) - """ - _ope = math.sqrt(4 * math.pi * n_e * E * E / ME) - return FloatUnit(_ope, 'rad s^-1') - - -def opi(m_i, n_i, Z, **kwargs): - """ - ion-plasma frequency (in rad/s) - - .. math:: - - \omega_{pi}^{2} = \\frac{4 \pi n_{i} (Z e)^{2}}{m_{i}} - - :param float m_i: ion mass (in g) - :param float n_i: ion number density (in :math:`cm^{-3}`) - :param int Z: ion charge number - """ - _opi = math.sqrt(4 * math.pi * n_i * (Z * E) * (Z * E) / m_i) - return FloatUnit(_opi, 'rad s^-1') - - -def oUH(Bo, n_e, **kwargs): - """ - Upper-Hybrid Resonance frequency (rad/s) - - .. math:: - - \omega_{UH}^{2} =\omega_{pe}^{2} + \Omega_{ce}^{2} - - :param float Bo: magnetic field (in Gauss) - :param float n_e: electron number density (in :math:`cm^{-3}`) - """ - _ope = ope(n_e) - _oce = oce(Bo) - _ouh = math.sqrt((_ope ** 2) + (_oce ** 2)) - return FloatUnit(_ouh, 'rad s^-1') - - +''' +''' # ---- length constants ---- -def lD(kT, n, **kwargs): - """ - Debye Length (in cm) - - .. math:: - - \lambda_{D} = \sqrt{\\frac{k_{B} T}{4 \pi n e^{2}}} - - :param float kT: temperature (in eV) - :param float n: number density (in :math:`cm^{-3}`) - """ - kT = kT * constants.e * 1.e7 # eV to ergs - _lD = math.sqrt(kT / (4.0 * math.pi * n)) / E - return FloatUnit(_lD, 'cm') - - -def lpe(n_e, **kwargs): - """ - electron-inertial length (cm) - - .. math:: - - l_{pe} = \\frac{c}{\omega_{pe}} - - :param float n_e: electron number density (in :math:`cm^{-3}`) - - .. note:: see function :func:`ope` - """ - _lpe = C / ope(n_e) - return FloatUnit(_lpe, 'cm') - - -def lpi(m_i, n_i, Z, **kwargs): - """ - ion-inertial length (cm) - - .. math:: - - l_{pi} = \\frac{c}{\omega_{pi}} - - :param float m_i: ion mass (in g) - :param float n_i: ion number density (in :math:`cm^{-3}`) - :param int Z: ion charge number - - .. note:: see function :func:`opi` - """ - _lpi = C / opi(m_i, n_i, Z) - return FloatUnit(_lpi, 'cm') - - def rce(Bo, kTe, **kwargs): """ electron gyroradius (cm) @@ -392,30 +108,6 @@ def rci(Bo, kTi, m_i, Z, **kwargs): # ---- velocity constants ---- -def cs(kTe, m_i, Z, gamma=1.0, **kwargs): - """ - ion sound speed (cm/s) - - .. math:: - - C_{s} = \sqrt{\\frac{\gamma Z k T_{e}}{m_{i}}} - - .. note:: - - :math:`\gamma=1` for the case of :math:`T_{i}\ll T_{e}` - (DEFAULT) - - :param float kTe: electron temperature (in eV) - :param float m_i: ion mass (in g) - :param int Z: charge number - :param float gamma: adiabatic index - """ - # TODO: double check adiabatic index default value - kTe = kTe * constants.e * 1.e7 # eV to ergs - _cs = math.sqrt(gamma * Z * kTe / m_i) - return FloatUnit(_cs, 'cm s^-1') - - def VA(Bo, m_i, n_i, **kwargs): """ Alfvén Velocity (in cm/s) @@ -460,4 +152,5 @@ def vTi(kTi, m_i, **kwargs): """ kTi = kTi * constants.e * 1.e7 # eV to erg _vTi = math.sqrt(kTi / m_i) - return FloatUnit(_vTi, 'cm s^-1') \ No newline at end of file + return FloatUnit(_vTi, 'cm s^-1') +''' From 6b71b0aff5d00a7d069b7e174caefd48a7792b0f Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Fri, 26 Apr 2019 16:05:56 -0700 Subject: [PATCH 29/68] ensure sphinx uses python3 to make documentation --- docs/Makefile | 2 +- docs/make.bat | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Makefile b/docs/Makefile index e93c1894..d58bbf41 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -3,7 +3,7 @@ # You can set these variables from the command line. SPHINXOPTS = -SPHINXBUILD = python3 -msphinx +SPHINXBUILD = python3 -m sphinx SPHINXPROJ = bapsflib SOURCEDIR = . BUILDDIR = _build diff --git a/docs/make.bat b/docs/make.bat index 8f2ab92f..4ef23c18 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -5,7 +5,7 @@ pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=python -msphinx + set SPHINXBUILD=python3 -m sphinx ) set SOURCEDIR=. set BUILDDIR=_build From c13b175ef7cffedb7a11c7959b4680f7184631b2 Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Fri, 26 Apr 2019 16:06:55 -0700 Subject: [PATCH 30/68] add plasma parameter Alfven speed (`Alfven_speed` and [alias] `VA`) --- bapsflib/plasma/parameters.py | 56 +++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/bapsflib/plasma/parameters.py b/bapsflib/plasma/parameters.py index db065915..74711c32 100644 --- a/bapsflib/plasma/parameters.py +++ b/bapsflib/plasma/parameters.py @@ -34,6 +34,7 @@ 'upper_hybrid_frequency', 'oUH', 'Debye_length', 'lD', 'inertial_length', 'lpe', 'lpi', + 'Alfven_speed', 'VA', 'ion_sound_speed', 'cs'] @@ -407,6 +408,48 @@ def lpi(n_i: u.Quantity, m_i: u.Quantity, # ---- Velocities ---- +#@utils.check_relativistic +@utils.check_quantity({'B': {'units': u.gauss, + 'can_be_negative': False}, + 'n_e': {'units': u.cm ** -3, + 'can_be_negative': False}, + 'm_i': {'units': u.g, + 'can_be_negative': False}}) +def Alfven_speed(B: u.Quantity, n_e: u.Quantity, m_i: u.Quantity, + Z: Union[int, float] = 1, **kwargs) -> u.Quantity: + """ + Alfvén speed (in cm/s) + + .. math:: + + V_{A} &= \\frac{B} + {\\sqrt{4 \\pi (n_{i} m_{i} + n_{e} m_{e})}} \\ + + &= \\frac{B}{\\sqrt{4 \\pi n_{e} (\\frac{1}{Z}m_{i} + m_{e})}} + + :param B: magnetic field (in Gauss) + :param n_e: electron number density (in :math:`cm^{3}`) + :param m_i: ion mass (in g) + :param Z: ion charge number (:math:`Z=1` DEFAULT) + """ + # condition Z + if not isinstance(Z, (int, np.integer, float, np.floating)): + raise TypeError('Z must be of type int or float') + elif Z <= 0: + raise PhysicsError( + 'The ion charge number Z must be greater than 0.') + + # ensure correct units + B = B.to(u.gauss) + n_e = n_e.to(u.cm ** -3) + m_i = m_i.to(u.g) + + # calculated + _va = B / np.sqrt(4.0 * const.pi * n_e * ((m_i / Z) + + const.m_e.cgs)) + return _va.value * (u.cm / u.s) + + def cs(kTe: u.Quantity, m_i: u.Quantity, kTi: u.Quantity = None, gamma_e: Union[int, float] = None, @@ -476,3 +519,16 @@ def ion_sound_speed(kTe: u.Quantity, m_i: u.Quantity, _cs += gamma_i * kTi _cs = np.sqrt(_cs / m_i).to(u.cm / u.s) return _cs + + +def VA(B: u.Quantity, n_e: u.Quantity, m_i: u.Quantity, + Z: Union[int, float] = None, **kwargs) -> u.Quantity: + """ + Alfvén speed (in cm/s) -- [alias for :func:`Alfven_speed`] + """ + # add specified keywords to kwargs + for name, val in zip(('Z',), (Z,)): + if val is not None: + kwargs[name] = val + + return Alfven_speed(B, n_e, m_i, **kwargs) From f365e1c752898d253c5bd22e4fb9ced0fbdc6e0f Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Fri, 26 Apr 2019 16:07:27 -0700 Subject: [PATCH 31/68] add `Alfven_speed` and `VA` to auto-documentation --- docs/src/bapsflib.plasma.parameters.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/src/bapsflib.plasma.parameters.rst b/docs/src/bapsflib.plasma.parameters.rst index 66c6ffa7..1f365cc6 100644 --- a/docs/src/bapsflib.plasma.parameters.rst +++ b/docs/src/bapsflib.plasma.parameters.rst @@ -38,5 +38,7 @@ bapsflib\.plasma\.parameters .. autosummary:: :nosignatures: + Alfven_speed + VA ion_sound_speed cs From 70a1482b1e0cb0d239b4ef7ba78de35e0f780e50 Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Fri, 26 Apr 2019 16:44:15 -0700 Subject: [PATCH 32/68] remove redundant decorators --- bapsflib/plasma/parameters.py | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/bapsflib/plasma/parameters.py b/bapsflib/plasma/parameters.py index 74711c32..cebdd694 100644 --- a/bapsflib/plasma/parameters.py +++ b/bapsflib/plasma/parameters.py @@ -122,8 +122,7 @@ def lower_hybrid_frequency(B: u.Quantity, n_i: u.Quantity, _olh = np.sqrt(1.0 / (first_term + second_term)) return _olh -@utils.check_quantity({'B': {'units': u.Gauss, - "can_be_negative": False}}) + def oce(B: u.Quantity, **kwargs) -> u.Quantity: """ electron-cyclotron frequency (rad/s) @@ -140,10 +139,6 @@ def oce(B: u.Quantity, **kwargs) -> u.Quantity: **kwargs['kwargs']) -@utils.check_quantity({'B': {'units': u.Gauss, - "can_be_negative": False}, - 'm_i': {'units': u.g, - "can_be_negative": False}}) def oci(B: u.Quantity, m_i: u.Quantity, Z: Union[int, float] = 1, **kwargs) -> u.Quantity: """ @@ -185,8 +180,6 @@ def oLH(B: u.Quantity, n_i: u.Quantity, m_i: u.Quantity, return lower_hybrid_frequency(B, m_i, n_i, **kwargs) -@utils.check_quantity({'n_e': {'units': u.cm ** -3, - 'can_be_negative': False}}) def ope(n_e: u.Quantity, **kwargs) -> u.Quantity: """ electron-plasma frequency (in rad/s) @@ -203,10 +196,6 @@ def ope(n_e: u.Quantity, **kwargs) -> u.Quantity: **kwargs['kwargs']) -@utils.check_quantity({'n_i': {'units': u.cm ** -3, - 'can_be_negative': False}, - 'm_i': {'units': u.g, - 'can_be_negative': False}}) def opi(n_i: u.Quantity, m_i: u.Quantity, Z: Union[int, float] = 1, **kwargs) -> u.Quantity: """ @@ -365,8 +354,6 @@ def lD(kTe: u.Quantity, n: u.Quantity, **kwargs) -> u.Quantity: return Debye_length(kTe, n, **kwargs) -@utils.check_quantity({'n_e': {'units': u.cm ** -3, - 'can_be_negative': False}}) def lpe(n_e: u.Quantity, **kwargs) -> u.Quantity: """ electron-inertial length (cm) @@ -380,10 +367,6 @@ def lpe(n_e: u.Quantity, **kwargs) -> u.Quantity: return inertial_length(n_e, -const.e_gauss, const.m_e, **kwargs) -@utils.check_quantity({'n_i': {'units': u.cm ** -3, - 'can_be_negative': False}, - 'm_i': {'units': u.g, - 'can_be_negative': False}}) def lpi(n_i: u.Quantity, m_i: u.Quantity, Z: Union[int, float] = 1, **kwargs) -> u.Quantity: """ @@ -408,7 +391,7 @@ def lpi(n_i: u.Quantity, m_i: u.Quantity, # ---- Velocities ---- -#@utils.check_relativistic +@utils.check_relativistic @utils.check_quantity({'B': {'units': u.gauss, 'can_be_negative': False}, 'n_e': {'units': u.cm ** -3, From 7903a8962a29c9896839432f8483e0c002140864 Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Fri, 26 Apr 2019 16:51:22 -0700 Subject: [PATCH 33/68] add plasma parameter thermal speed (`thermal_speed`, `vTe`, and `vTi`) --- bapsflib/plasma/parameters.py | 56 ++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/bapsflib/plasma/parameters.py b/bapsflib/plasma/parameters.py index cebdd694..d7e4257f 100644 --- a/bapsflib/plasma/parameters.py +++ b/bapsflib/plasma/parameters.py @@ -35,7 +35,8 @@ 'Debye_length', 'lD', 'inertial_length', 'lpe', 'lpi', 'Alfven_speed', 'VA', - 'ion_sound_speed', 'cs'] + 'ion_sound_speed', 'cs', + 'thermal_speed', 'vTe', 'vTi'] # ---- Frequencies ---- @@ -504,6 +505,32 @@ def ion_sound_speed(kTe: u.Quantity, m_i: u.Quantity, return _cs +@utils.check_relativistic +@utils.check_quantity({'kT': {'units': u.eV, + 'can_be_negative': False}, + 'm': {'units': u.g, + 'can_be_negative': False}}) +def thermal_speed(kT: u.Quantity, m: u.Quantity, + **kwargs) -> u.Quantity: + """ + generalized thermal speed (in cm/s) + + .. math:: + + v_{th} = \\sqrt{\\frac{k_{B} T}{m}} + + :param kT: temperature (in eV) + :param m: particle mass (in g) + """ + # ensure proper units + kT = kT.to(u.erg) + m = m.to(u.g) + + # calculate + _v = np.sqrt(kT / m) + return _v.to(u.cm / u.s) + + def VA(B: u.Quantity, n_e: u.Quantity, m_i: u.Quantity, Z: Union[int, float] = None, **kwargs) -> u.Quantity: """ @@ -515,3 +542,30 @@ def VA(B: u.Quantity, n_e: u.Quantity, m_i: u.Quantity, kwargs[name] = val return Alfven_speed(B, n_e, m_i, **kwargs) + + +def vTe(kTe: u.Quantity, **kwargs) -> u.Quantity: + """ + electron thermal speed (in cm/s) + + .. math:: + + v_{T_{e}} = \\sqrt{\\frac{k_{B} T_{e}}{m_{e}}} + + :param kTe: electron temperature (in eV) + """ + return thermal_speed(kTe, const.m_e, **kwargs) + + +def vTi(kTi: u.Quantity, m_i: u.Quantity, **kwargs) -> u.Quantity: + """ + ion thermal speed (in cm/s) + + .. math:: + + v_{T_{i}} = \\sqrt{\\frac{k_{B} T_{i}}{m_{i}}} + + :param kTi: ion temperature (in eV) + :param m_i: ion mass (in g) + """ + return thermal_speed(kTi, m_i, **kwargs) From b1aa20f667edc0099a2386482178d8bda49e4591 Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Fri, 26 Apr 2019 16:51:45 -0700 Subject: [PATCH 34/68] add `thermal_speed`, `vTe`, and `vTe` to auto-documentation --- docs/src/bapsflib.plasma.parameters.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/src/bapsflib.plasma.parameters.rst b/docs/src/bapsflib.plasma.parameters.rst index 1f365cc6..474aaba5 100644 --- a/docs/src/bapsflib.plasma.parameters.rst +++ b/docs/src/bapsflib.plasma.parameters.rst @@ -42,3 +42,6 @@ bapsflib\.plasma\.parameters VA ion_sound_speed cs + thermal_speed + vTe + vTi From 6b14616c993707df66781c2da32b710fb062bd7e Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Fri, 26 Apr 2019 17:56:38 -0700 Subject: [PATCH 35/68] add plasma parameter cyclotron radius (`cyclotron_radius`, `rce`, and `rci`) --- bapsflib/plasma/parameters.py | 73 +++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/bapsflib/plasma/parameters.py b/bapsflib/plasma/parameters.py index d7e4257f..63598ae4 100644 --- a/bapsflib/plasma/parameters.py +++ b/bapsflib/plasma/parameters.py @@ -12,6 +12,7 @@ # TODO: add Coulomb Logarithm # TODO: add collision frequencies # TODO: add mean-free-paths +# TODO: add examples to docstrings # """ Plasma parameters @@ -32,6 +33,7 @@ 'lower_hybrid_frequency', 'oLH', 'plasma_frequency', 'ope', 'opi', 'upper_hybrid_frequency', 'oUH', + 'cyclotron_radius', 'rce', 'rci', 'Debye_length', 'lD', 'inertial_length', 'lpe', 'lpi', 'Alfven_speed', 'VA', @@ -324,6 +326,32 @@ def Debye_length(kTe: u.Quantity, n: u.Quantity, return _lD.cgs +@utils.check_quantity({'vperp': {'units': u.cm / u.s, + 'can_be_negative': True}}) +def cyclotron_radius(vperp: u.Quantity, q: u.Quantity, B: u.Quantity, + m: u.Quantity, **kwargs) -> u.Quantity: + """ + generalized cyclotron radius (in cm) + + .. math:: + + r_{c} = \\frac{m c v_{\\perp}}{|q| B} + = \\frac{v_{\\perp}}{\\Omega_{c}} + + :param vperp: velocity component perpendicular to B (in cm/s) + :param q: particle charge (in statcoulombs) + :param B: magnetic field (in Gauss) + :param m: particle mass (in g) + """ + # ensure correct units + vperp = np.abs(vperp.to(u.cm / u.s)) + + # calculate + _oc = np.abs(cyclotron_frequency(q, B, m)) # type: u.Quantity + _r = vperp / (_oc.value * (1 / u.s)) + return _r.to(u.cm) + + @utils.check_quantity({'n': {'units': u.cm ** -3, 'can_be_negative': False}, 'q': {'units': u.statcoulomb, @@ -391,6 +419,51 @@ def lpi(n_i: u.Quantity, m_i: u.Quantity, return inertial_length(n_i, Z * const.e_gauss, m_i, **kwargs) +def rce(kTe: u.Quantity, B: u.Quantity, **kwargs) -> u.Quantity: + """ + electron-cyclotron radius (cm) + + .. math:: + + r_{ce} = \\frac{v_{Te}}{|\\Omega_{ce}|} + = \\sqrt{\\frac{k_{B} T_{e}}{m_{e}}} + \\left( \\frac{m_{e} c}{|e| B} \\right) + + :param kTe: electron temperature (in eV) + :param B: magnetic field (in Gauss) + """ + _v = vTe(kTe) + return cyclotron_radius(_v, const.e_gauss, B, const.m_e) + + +def rci(kTi: u.Quantity, B: u.Quantity, m_i: u.Quantity, + Z: Union[int, float] = 1, **kwargs) -> u.Quantity: + """ + ion-cyclotron radius (cm) + + .. math:: + + r_{ci} = \\frac{v_{Ti}}{|\\Omega_{ci}|} + = \\sqrt{\\frac{k_{B} T_{i}}{m_{i}}} + \\left( \\frac{m_{i} c}{Z |e| B} \\right) + + :param kTi: ion temperature (in eV) + :param B: magnetic field (in Gauss) + :param m_i: ion mass (in g) + :param Z: ion charge number (:math:`Z=1` DEFAULT) + """ + # condition Z + if not isinstance(Z, (int, np.integer, float, np.floating)): + raise TypeError('Z must be of type int or float') + elif Z <= 0: + raise PhysicsError( + 'The ion charge number Z must be greater than 0.') + + # calculate + _v = vTi(kTi, m_i) + return cyclotron_radius(_v, Z * const.e_gauss, B, m_i) + + # ---- Velocities ---- @utils.check_relativistic @utils.check_quantity({'B': {'units': u.gauss, From e3463dc3e176fdf4366258509326143d1fe70b87 Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Fri, 26 Apr 2019 17:57:06 -0700 Subject: [PATCH 36/68] add `cyclotron_radius`, `rce`, and `rci` to the auto-documentation --- docs/src/bapsflib.plasma.parameters.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/src/bapsflib.plasma.parameters.rst b/docs/src/bapsflib.plasma.parameters.rst index 474aaba5..18d234cd 100644 --- a/docs/src/bapsflib.plasma.parameters.rst +++ b/docs/src/bapsflib.plasma.parameters.rst @@ -27,6 +27,9 @@ bapsflib\.plasma\.parameters .. autosummary:: :nosignatures: + cyclotron_radius + rce + rci Debye_length lD inertial_length From 656472950bf52a2f59512657e7a77dfc8752da03 Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Fri, 26 Apr 2019 17:59:31 -0700 Subject: [PATCH 37/68] slowly removing code from module, working towards deleting bapsflib/plasma/core.py --- bapsflib/plasma/core.py | 93 ----------------------------------------- 1 file changed, 93 deletions(-) diff --git a/bapsflib/plasma/core.py b/bapsflib/plasma/core.py index d2d7d689..78f46de0 100644 --- a/bapsflib/plasma/core.py +++ b/bapsflib/plasma/core.py @@ -15,14 +15,6 @@ # """Core plasma parameters in (cgs).""" -import astropy.constants as const -import astropy.units as u -import math - -from scipy import constants - -#pconst = constants.physical_constants - ''' class FloatUnit(float): """Template class for floats with a unit attribute.""" @@ -69,88 +61,3 @@ def unit(self): """units of constant""" return self._unit ''' -''' -# ---- length constants ---- -def rce(Bo, kTe, **kwargs): - """ - electron gyroradius (cm) - - .. math:: - - r_{ce} = \\frac{v_{T_{e}}}{\Omega_{ce}} - - :param float Bo: magnetic field (in Gauss) - :param float kTe: electron temperature (in eV) - - .. note:: see functions :func:`vTe` and :func:`oce` - """ - _rce = vTe(kTe) / abs(oce(Bo)) - return FloatUnit(_rce, 'cm') - - -def rci(Bo, kTi, m_i, Z, **kwargs): - """ - ion gyroradius (cm) - - .. math:: - - r_{ci} = \\frac{v_{T_{i}}}{\Omega_{ci}} - - :param float Bo: magnetic field (in Gauss) - :param float kTi: ion temperature (in eV) - :param float m_i: ion mass (in g) - :param int Z: ion charge number - - .. note:: see functions :func:`vTi` and :func:`oci` - """ - _rci = vTi(kTi, m_i) / oci(Bo, m_i, Z) - return FloatUnit(_rci, 'cm') - - -# ---- velocity constants ---- -def VA(Bo, m_i, n_i, **kwargs): - """ - Alfvén Velocity (in cm/s) - - .. math:: - - V_{A} = \\frac{B_{o}}{\sqrt{4 \pi n_{i} m_{i}}} - - :param float Bo: magnetic field (in Gauss) - :param float m_i: ion mass (in g) - :param float n_i: ion number density (in :math:`cm^{-3}`) - """ - _VA = Bo / math.sqrt(4.0 * math.pi * n_i * m_i) - return FloatUnit(_VA, 'cm s^-1') - - -def vTe(kTe, **kwargs): - """ - electron thermal velocity (in cm/s) - - .. math:: - - v_{T_{e}} = \sqrt{\\frac{k T_{e}}{m_{e}}} - - :param float kTe: electron temperature (in eV) - """ - kTe = kTe * constants.e * 1.e7 # eV to erg - _vTe = math.sqrt(kTe / ME) - return FloatUnit(_vTe, 'cm s^-1') - - -def vTi(kTi, m_i, **kwargs): - """ - ion thermal velocity (in cm/s) - - .. math:: - - v_{T_{i}} = \sqrt{\\frac{k T_{i}}{m_{i}}} - - :param float kTi: ion temperature (in eV) - :param float m_i: ion mass (in g) - """ - kTi = kTi * constants.e * 1.e7 # eV to erg - _vTi = math.sqrt(kTi / m_i) - return FloatUnit(_vTi, 'cm s^-1') -''' From 26c4055098f198dc7234984101301bad7b224925 Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Fri, 26 Apr 2019 18:44:57 -0700 Subject: [PATCH 38/68] add plasma parameter beta (`beta`) --- bapsflib/plasma/parameters.py | 36 +++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/bapsflib/plasma/parameters.py b/bapsflib/plasma/parameters.py index 63598ae4..507450d7 100644 --- a/bapsflib/plasma/parameters.py +++ b/bapsflib/plasma/parameters.py @@ -38,7 +38,8 @@ 'inertial_length', 'lpe', 'lpi', 'Alfven_speed', 'VA', 'ion_sound_speed', 'cs', - 'thermal_speed', 'vTe', 'vTi'] + 'thermal_speed', 'vTe', 'vTi', + 'beta'] # ---- Frequencies ---- @@ -482,7 +483,8 @@ def Alfven_speed(B: u.Quantity, n_e: u.Quantity, m_i: u.Quantity, V_{A} &= \\frac{B} {\\sqrt{4 \\pi (n_{i} m_{i} + n_{e} m_{e})}} \\ - &= \\frac{B}{\\sqrt{4 \\pi n_{e} (\\frac{1}{Z}m_{i} + m_{e})}} + &= \\frac{B} + {\\sqrt{4 \\pi n_{e} (\\frac{1}{Z}m_{i} + m_{e})}} :param B: magnetic field (in Gauss) :param n_e: electron number density (in :math:`cm^{3}`) @@ -642,3 +644,33 @@ def vTi(kTi: u.Quantity, m_i: u.Quantity, **kwargs) -> u.Quantity: :param m_i: ion mass (in g) """ return thermal_speed(kTi, m_i, **kwargs) + + +# ---- Dimensionless ---- +@utils.check_quantity({'n': {'units': u.cm ** -3, + 'can_be_negative': False}, + 'kT': {'units': u.eV, + 'can_be_negative': False}, + 'B': {'units': u.Gauss, + 'can_be_negative': False}}) +def beta(n: u.Quantity, kT: u.Quantity, B: u.Quantity, + **kwargs) -> u.Quantity: + """ + plasma beta of particle species :math:`s` + + .. math:: + + \\beta_{s} = \\frac{n_{s} k_{B} T_{s}}{B^{2} / (8 \\pi)} + + :param n: particle number density (in :math:`cm^{-3}`) + :param kT: particle temperature (in eV) + :param B: magnetic field (in Gauss) + """ + # ensure proper units + n = n.to(u.cm ** -3) + kT = kT.to(u.erg) + B = B.to(u.Gauss) + + _TP = n * kT + _MP = B ** 2 / (8.0 * const.pi) + return (_TP.value / _MP.value) * u.dimensionless_unscaled From 5fb44283aeaaa9c10996163fc8053e2085be7fde Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Fri, 26 Apr 2019 18:45:15 -0700 Subject: [PATCH 39/68] add `beta` to auto-documentation --- docs/src/bapsflib.plasma.parameters.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/src/bapsflib.plasma.parameters.rst b/docs/src/bapsflib.plasma.parameters.rst index 18d234cd..0158e13b 100644 --- a/docs/src/bapsflib.plasma.parameters.rst +++ b/docs/src/bapsflib.plasma.parameters.rst @@ -48,3 +48,10 @@ bapsflib\.plasma\.parameters thermal_speed vTe vTi + + .. rubric:: Dimensionless + + .. autosummary:: + :nosignatures: + + beta From 9f0b58fddead242313d91a2eeae83e4aa161682a Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Tue, 7 May 2019 14:32:56 -0700 Subject: [PATCH 40/68] remove plasma related methods from HDFReadData --- bapsflib/_hdf/utils/hdfreaddata.py | 359 ----------------------------- 1 file changed, 359 deletions(-) diff --git a/bapsflib/_hdf/utils/hdfreaddata.py b/bapsflib/_hdf/utils/hdfreaddata.py index 5987e1e2..f2700126 100644 --- a/bapsflib/_hdf/utils/hdfreaddata.py +++ b/bapsflib/_hdf/utils/hdfreaddata.py @@ -15,7 +15,6 @@ import os import time -from bapsflib.plasma import core from typing import Union from warnings import warn @@ -592,21 +591,6 @@ def __new__(cls, else: obj._info['controls'] = {} - # plasma parameter dict - obj._plasma = { - 'Bo': None, - 'kT': None, - 'kTe': None, - 'kTi': None, - 'gamma': core.FloatUnit(1.0, 'arb'), - 'm_e': core.ME, - 'm_i': None, - 'n': None, - 'n_e': None, - 'n_i': None, - 'Z': None - } # pragma: no cover - # convert to voltage # - 'signal' dtype is assigned based on keep_bit # @@ -661,21 +645,6 @@ def __array_finalize__(self, obj): 'controls': {}, }) - # Define plasma attribute - self._plasma = getattr(obj, '_plasma', { - 'Bo': None, - 'kT': None, - 'kTe': None, - 'kTi': None, - 'gamma': core.FloatUnit(1.0, 'arb'), - 'm_e': core.ME, - 'm_i': None, - 'n': None, - 'n_e': None, - 'n_i': None, - 'Z': None - }) # pragma: no cover - def convert_signal(self, to_volt=False, to_bits=False, force=False): """converts signal from volts (bits) to bits (volts)""" # @@ -800,336 +769,8 @@ def dv(self) -> Union[u.Quantity, None]: (2. ** self.info['bit'] - 1.)) return dv - @property - def plasma(self): # pragma: no cover - """ - Dictionary of plasma parameters. (All quantities are in cgs - units except temperature is in eV) - - +----------------+---------------------------------------------+ - | Base Values | - +================+=============================================+ - | :const:`Bo` | magnetic field | - +----------------+---------------------------------------------+ - | :const:`kT` | temperature (generic) | - +----------------+---------------------------------------------+ - | :const:`kTe` | electron temperature | - +----------------+---------------------------------------------+ - | :const:`kTi` | ion temperature | - +----------------+---------------------------------------------+ - | :const:`gamma` | adiabatic index | - +----------------+---------------------------------------------+ - | :const:`m_e` | electron mass | - +----------------+---------------------------------------------+ - | :const:`m_i` | ion mass | - +----------------+---------------------------------------------+ - | :const:`n` | plasma number density | - +----------------+---------------------------------------------+ - | :const:`n_e` | electron number density | - +----------------+---------------------------------------------+ - | :const:`n_i` | ion number density | - +----------------+---------------------------------------------+ - | :const:`Z` | ion charge number | - +----------------+---------------------------------------------+ - | Calculated Values | - +----------------+---------------------------------------------+ - | :const:`fce` | electron cyclotron frequency | - +----------------+---------------------------------------------+ - | :const:`fci` | ion cyclotron frequency | - +----------------+---------------------------------------------+ - | :const:`fpe` | electron plasma frequency | - +----------------+---------------------------------------------+ - | :const:`fpi` | ion plasma frequency | - +----------------+---------------------------------------------+ - | :const:`fUH` | Upper-Hybrid Resonance frequency | - +----------------+---------------------------------------------+ - | :const:`lD` | Debye Length | - +----------------+---------------------------------------------+ - | :const:`lpe` | electron-inertial length | - +----------------+---------------------------------------------+ - | :const:`lpi` | ion-inertial length | - +----------------+---------------------------------------------+ - | :const:`rce` | electron gyroradius | - +----------------+---------------------------------------------+ - | :const:`rci` | ion gyroradius | - +----------------+---------------------------------------------+ - | :const:`cs` | ion sound speed | - +----------------+---------------------------------------------+ - | :const:`VA` | Alfven speed | - +----------------+---------------------------------------------+ - | :const:`vTe` | electron thermal velocity | - +----------------+---------------------------------------------+ - | :const:`vTi` | ion thermal velocity | - +----------------+---------------------------------------------+ - """ - return self._plasma - - def set_plasma(self, Bo, kTe, kTi, m_i, n_e, Z, gamma=None, - **kwargs): # pragma: no cover - """ - Set :attr:`plasma` and add key frequency, length, and velocity - parameters. (all quantities in cgs except temperature is in eV) - - :param float Bo: magnetic field (in Gauss) - :param float kTe: electron temperature (in eV) - :param float kTi: ion temperature (in eV) - :param float m_i: ion mass (in g) - :param float n_e: electron number density (in cm^-3) - :param int Z: ion charge number - :param float gamma: adiabatic index (arb.) - """ - # define base values - self._plasma['Bo'] = core.FloatUnit(Bo, 'G') - self._plasma['kTe'] = core.FloatUnit(kTe, 'eV') - self._plasma['kTi'] = core.FloatUnit(kTi, 'eV') - self._plasma['m_i'] = core.FloatUnit(m_i, 'g') - self._plasma['n_e'] = core.FloatUnit(n_e, 'cm^-3') - self._plasma['Z'] = core.IntUnit(Z, 'arb') - - # define ion number density - self._plasma['n_i'] = core.FloatUnit( - self._plasma['n_e'] / self._plasma['Z'], 'cm^-3') - - # define gamma (adiabatic index) - # - default = 1.0 - if gamma is not None: - self._plasma['gamma'] = core.FloatUnit(gamma, 'arb') - - # define plasma temperature - # - if omitted then assumed kTe - # TODO: double check assumption - if 'kT' in kwargs: - self._plasma['kT'] = core.FloatUnit(kwargs['kT'], 'eV') - else: - self._plasma['kT'] = core.FloatUnit(kTe, 'eV') - - # define plasma number density - # - if omitted then assumed n_e - if 'n' in kwargs: - self._plasma['n'] = core.FloatUnit(kwargs['n'], 'cm^-3') - else: - self._plasma['n'] = core.FloatUnit(n_e, 'cm^-3') - - # add key plasma constants - self._update_plasma_constants() - - def set_plasma_value(self, key, value): # pragma: no cover - """ - Re-define one of the base plasma values (Bo, gamma, kT, kTe, - kTi, m_i, n, n_e, or Z) in the :attr:`plasma` dictionary. - - :param str key: one of the base plasma values - :param value: value for key - """ - # set plasma value - if key == 'Bo': - self._plasma['Bo'] = core.FloatUnit(value, 'G') - elif key == 'gamma': - self._plasma['gamma'] = core.FloatUnit(value, 'arb') - elif key in ['kT', 'kTe', 'kTi']: - self._plasma[key] = core.FloatUnit(value, 'eV') - - if key == 'kTe' and self._plasma['kt'] is None: - self._plasma['kT'] = self._plasma[key] - elif key == 'm_i': - self._plasma[key] = core.FloatUnit(value, 'g') - elif key in ['n', 'n_e']: - self._plasma[key] = core.FloatUnit(value, 'cm^-3') - - # re-calc n_i and n - if key == 'n_e': - self._plasma['n_i'] = core.FloatUnit( - self._plasma['n_e'] / self._plasma['Z'], 'cm^-3') - - if self._plasma['n'] is None: - self._plasma['n'] = self._plasma['n_e'] - elif key == 'Z': - self._plasma[key] = core.IntUnit(value, 'arb') - - # re-calc n_i - self._plasma['n_i'] = \ - core.FloatUnit(self._plasma['n_e'] / self._plasma['Z'], - 'cm^-3') - - # update key plasma constants - self._update_plasma_constants() - - def _update_plasma_constants(self): # pragma: no cover - """ - Updates the calculated plasma constants (fci, fce, fpe, etc.) in - :attr:`plasma`. - """ - # add key frequencies - self._plasma['fce'] = core.fce(**self._plasma) - self._plasma['fci'] = core.fci(**self._plasma) - self._plasma['fpe'] = core.fpe(**self._plasma) - self._plasma['fpi'] = core.fpi(**self._plasma) - self._plasma['fUH'] = core.fUH(**self._plasma) - self._plasma['fLH'] = core.fLH(**self._plasma) - - # add key lengths - self._plasma['lD'] = core.lD(**self._plasma) - self._plasma['lpe'] = core.lpe(**self._plasma) - self._plasma['lpi'] = core.lpi(**self._plasma) - self._plasma['rce'] = core.rce(**self._plasma) - self._plasma['rci'] = core.rci(**self._plasma) - - # add key velocities - self._plasma['cs'] = core.cs(**self._plasma) - self._plasma['VA'] = core.VA(**self._plasma) - self._plasma['vTe'] = core.vTe(**self._plasma) - self._plasma['vTi'] = core.vTi(**self._plasma) - # add example to __new__ docstring HDFReadData.__new__.__doc__ += "\n" for line in HDFReadData.__example_doc__.splitlines(): HDFReadData.__new__.__doc__ += " " + line + "\n" - - -''' -def condition_shotnum(shotnum, dheader, shotnumkey, - intersection_set): - """ - Conditions **shotnum** against the digitizer header dataset. - - :param shotnum: desired HDF5 shot number(s) - :type shotnum: list(int) - :param dheader: digitizer header dataset - :param dheader: :class:`h5py.Dataset` - :param str shotnumkey: field name in **dheader** that contains the - shot numbers - :param bool intersection_set: Set :code:`True` to intersect - **shotnum** with the shot numbers in :code:`dheader[shotnumkey]` - :return: index, shotnum, sni - - .. note:: - - The returned :class:`numpy.ndarray`'s (:const:`index`, - :const:`shotnum`, and :const:`sni`) follow the rule:: - - shotnum[sni] = dheader[index, shotnumkey] - """ - # Inputs: - # shotnum list(int) - the desired shot number(s) - # cheader h5py.Dataset - the digi header dataset - # shotnumkey str - field name for the shot number - # column in dheader - # intersection_set bool - intersect shotnum with - # dheader[shotnumkey] - # - # Returns: - # index np.array(dtype=uint32) - cdset row index for the - # specified shotnum - # shotnum np.array(dtype=uint32) - shot numbers - # sni np.array(dtype=bool) - shotnum mask such that: - # shotnum[sni] = cdset[index, shotnumkey] - # - # remove shot numbers less-than or equal to 0 - shotnum.sort() - shotnum = list(set(shotnum)) - shotnum.sort() - if min(shotnum) <= 0: - # remove values less-than or equal to 0 - new_sn = [sn for sn in shotnum if sn > 0] - shotnum = new_sn - - # remove shot numbers greater-than largest shot number - # in dataset - if intersection_set: - last_sn = dheader[-1, shotnumkey] - if max(shotnum) > last_sn: - new_sn = [sn for sn in shotnum if sn <= last_sn] - shotnum = new_sn - - # ensure shotnum is not empty - if len(shotnum) == 0: - raise ValueError('Valid shotnum not passed.') - - # convert shotnum to np.array - # - shotnum is always a list up to this point - shotnum = np.array(shotnum).view() - - # Calc. corresponding `index` and `sni` for shotnum - # - intersection will after initial calculation - # - if dheader.shape[0] == 1: - # only one possible shot number - only_sn = dheader[0, shotnumkey] - sni = np.where(shotnum == only_sn, True, False) - index = np.array([0]) \ - if True in sni else np.empty(shape=0, dtype=np.uint32) - else: - # get 1st and last shot number for further - # conditioning - first_sn, last_sn = dheader[[-1, 0], shotnumkey] - - if last_sn - first_sn + 1 == dheader.shape[0]: - # shot numbers are sequential - index = shotnum - first_sn - - # build sni - # - this will also mask index s.t. index has - # no values outside dheader.shape - sni = np.where(index < dheader.shape[0], True, False) - index = index[sni] - else: - # shot numbers are NOT sequential - # TODO: check for more efficient read in methods - # - step_front_read = shotnum[-1] - first_sn - step_end_read = last_sn - shotnum[0] - - if dheader.shape[0] <= 1 + min(step_front_read, - step_end_read): - # dheader.shape is smaller than the - # theoretical sequential reads from either - # end of the array - dset_sn = dheader[shotnumkey].view() - sni = np.isin(shotnum, dset_sn) - - # define index - index = np.where(np.isin(dset_sn, shotnum))[0] - elif step_front_read <= step_end_read: - # extracting from the beginning of the array is the - # smallest - some_dset_sn = dheader[0:step_front_read + 1, - shotnumkey] - sni = np.isin(shotnum, some_dset_sn) - - # define index - index = np.where(np.isin(some_dset_sn, shotnum))[0] - else: - # extracting from the end of the array is the smallest - start, stop, step = \ - slice(-step_end_read - 1, - None, - None).indices(dheader.shape[0]) - some_dset_sn = dheader[start::, shotnumkey] - sni = np.isin(shotnum, some_dset_sn) - - # define index - # NOTE: if index is empty (i.e. index.shape[0] == 0) - # then adding an int still returns an empty array - index = np.where(np.isin(some_dset_sn, shotnum))[0] - index += start - - # filter shotnum and ensure obj will not be empty - if intersection_set: - # check for empty shotnum - if True not in sni: - raise ValueError( - 'Input shotnum would result in a null array') - - # filter - shotnum = shotnum[sni] - sni = np.ones(shotnum.shape, dtype=bool) - else: - # empty shotnum - if shotnum.shape[0] == 0: - raise ValueError( - 'Input shotnum would result in a null array') - - # return calculated arrays - return index.view(), shotnum.view(), sni.view() -''' From dd5cee70914c785ce34bad27f0a89dcdb91b50fa Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Tue, 7 May 2019 14:47:46 -0700 Subject: [PATCH 41/68] delete bapsflib/plasma/core.py --- bapsflib/plasma/core.py | 63 ----------------------------------------- 1 file changed, 63 deletions(-) delete mode 100644 bapsflib/plasma/core.py diff --git a/bapsflib/plasma/core.py b/bapsflib/plasma/core.py deleted file mode 100644 index 78f46de0..00000000 --- a/bapsflib/plasma/core.py +++ /dev/null @@ -1,63 +0,0 @@ -# This file is part of the bapsflib package, a Python toolkit for the -# BaPSF group at UCLA. -# -# http://plasma.physics.ucla.edu/ -# -# Copyright 2017-2018 Erik T. Everson and contributors -# -# License: Standard 3-clause BSD; see "LICENSES/LICENSE.txt" for full -# license terms and contributor agreement. -# -# TODO: add plasma betas (electron, ion, and total) -# TODO: add Coulomb Logarithm -# TODO: add collision frequencies -# TODO: add mean-free-paths -# -"""Core plasma parameters in (cgs).""" - -''' -class FloatUnit(float): - """Template class for floats with a unit attribute.""" - - def __new__(cls, value, cgs_unit): - """ - :param float value: value of constant - :param str cgs_unit: string representation of of cgs unit - :return: value of constant - :rtype: float - """ - obj = super().__new__(cls, value) - obj._unit = cgs_unit - return obj - - def __init__(self, value, cgs_unit): - super().__init__() - - @property - def unit(self): - """units of constant""" - return self._unit - - -class IntUnit(int): - """Template class for ints with a unit attribute.""" - - def __new__(cls, value, cgs_unit): - """ - :param int value: value of constant - :param str cgs_unit: string representation of of cgs unit - :return: value of constant - :rtype: int - """ - obj = super().__new__(cls, value) - obj._unit = cgs_unit - return obj - - def __init__(self, value, cgs_unit): - super().__init__() - - @property - def unit(self): - """units of constant""" - return self._unit -''' From 374734014eaa56472cf821898d83b1bf6d8d0e74 Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Wed, 8 May 2019 11:40:16 -0700 Subject: [PATCH 42/68] initial commit for bapsflib helper functions --- bapsflib/utils/helpers.py | 363 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 363 insertions(+) create mode 100644 bapsflib/utils/helpers.py diff --git a/bapsflib/utils/helpers.py b/bapsflib/utils/helpers.py new file mode 100644 index 00000000..5f111817 --- /dev/null +++ b/bapsflib/utils/helpers.py @@ -0,0 +1,363 @@ +# This file is part of the bapsflib package, a Python toolkit for the +# BaPSF group at UCLA. +# +# http://plasma.physics.ucla.edu/ +# +# Copyright 2017-2019 Erik T. Everson and contributors +# +# License: Standard 3-clause BSD; see "LICENSES/LICENSE.txt" for full +# license terms and contributor agreement. +# +""" +Helper functions for the :mod:`bapsflib` package. +""" + +__all__ = ['check_relativistic', 'check_quantity'] + +import astropy.units as u +import functools +import inspect +import numpy as np +import warnings + +from astropy.units import UnitsWarning +from plasmapy.utils import check_relativistic +from textwrap import dedent +from typing import (Dict, Union) + + +# this is modified from plasmapy.utils.checks.check_quantity +# TODO: replace with PlasmaPy version when PlasmaPy v0.2.0 is released +def check_quantity(**validations: Dict[str, Union[bool, u.Unit]]): + """ + Verify that the function's arguments have correct units. + This decorator raises an exception if an annotated argument in the + decorated function is an `~astropy.units.Quantity` with incorrect + units or of incorrect kind. You can prevent arguments from being + input as Nones, NaNs, negatives, infinities and complex numbers. + + If a number (non-Quantity) value is inserted in place of a value + with units, assume the input is an SI Quantity and cast it to one. + + This is probably best illustrated with an example: + + Examples + -------- + >>> from astropy import units as u + >>> @check_quantity(x={"units": u.m, + ... "can_be_negative": False, + ... "can_be_complex": True, + ... "can_be_inf": True}) + ... def func(x): + ... return x + + >>> func(1 * u.m) + + + >>> func(1 * u.s) + Traceback (most recent call last): + ... + astropy.units.core.UnitConversionError: The argument x to func should be a Quantity with the following units: m + + >>> import pytest # to show the UnitsWarning + >>> with pytest.warns(u.UnitsWarning, match="Assuming units of m."): + ... func(1) + + + >>> func(-1 * u.m) + Traceback (most recent call last): + ... + ValueError: The argument x to function func cannot contain negative numbers. + + >>> func(np.inf * u.m) + + + >>> func(None) + Traceback (most recent call last): + ... + ValueError: The argument x to function func cannot contain Nones. + + Parameters + ---------- + dict + Arguments to be validated passed in as keyword arguments, + with values as validation dictionaries, with structure as + in the example. Valid keys for each argument are: + 'units': `astropy.units.Unit`, + 'can_be_negative': `bool`, + 'can_be_complex': `bool`, + 'can_be_inf': `bool`, + 'can_be_nan': `bool`, + 'none_shall_pass': `bool` + + Raises + ------ + `TypeError` + If the argument is not a `~astropy.units.Quantity`, units is + not entirely units or `argname` does not have a type + annotation. + + `~astropy.units.UnitConversionError` + If the argument is not in acceptable units. + + `~astropy.units.UnitsError` + If after the assumption checks, the argument is still not in + acceptable units. + + `ValueError` + If the argument contains `~numpy.nan` or other invalid values + as determined by the keywords. + + Warns + ----- + `~astropy.units.UnitsWarning` + If a `~astropy.units.Quantity` is not provided and unique units + are provided, a `UnitsWarning` will be raised and the inputted + units will be assumed. + + Notes + ----- + This functionality may end up becoming deprecated due to + noncompliance with the `IEEE 754 standard + `_ + and in favor of `~astropy.units.quantity_input`. + + Returns + ------- + function + Decorated function. + + See also + -------- + _check_quantity + """ + def decorator(f): + wrapped_sign = inspect.signature(f) + fname = f.__name__ + + assigned = list(functools.WRAPPER_ASSIGNMENTS) + assigned.append('__signature__') + @functools.wraps(f, assigned=assigned) + def wrapper(*args, **kwargs): + # combine args and kwargs into dictionary + bound_args = wrapped_sign.bind(*args, **kwargs) + bound_args.apply_defaults() + given_params_values = bound_args.arguments + given_params = set(given_params_values.keys()) + + # names of params to check + validated_params = set(validations.keys()) + + missing_params = [ + param for param in (validated_params - given_params) + ] + + if len(missing_params) > 0: + params_str = ", ".join(missing_params) + raise TypeError( + f"Call to {fname} is missing " + f"validated params {params_str}") + + for param_to_check, validation_settings \ + in validations.items(): + value_to_check = given_params_values[param_to_check] + + can_be_negative = \ + validation_settings.get('can_be_negative', True) + can_be_complex = \ + validation_settings.get('can_be_complex', False) + can_be_inf = \ + validation_settings.get('can_be_inf', True) + can_be_nan = \ + validation_settings.get('can_be_nan', True) + none_shall_pass = \ + validation_settings.get('none_shall_pass', False) + + validated_value = _check_quantity( + value_to_check, + param_to_check, + fname, + validation_settings['units'], + can_be_negative=can_be_negative, + can_be_complex=can_be_complex, + can_be_inf=can_be_inf, + can_be_nan=can_be_nan, + none_shall_pass=none_shall_pass, + ) + given_params_values[param_to_check] = validated_value + + return f(**given_params_values) + + if not hasattr(wrapper, '__signature__'): + wrapper.__signature__ = inspect.signature(f) + return wrapper + return decorator + + +# this is modified from plasmapy.utils.checks._check_quantity +# TODO: replace with PlasmaPy version when PlasmaPy v0.2.0 is released +def _check_quantity(arg, argname, funcname, units, + can_be_negative=True, can_be_complex=False, + can_be_inf=True, can_be_nan=True, + none_shall_pass=False): + """ + Raise an exception if an object is not a `~astropy.units.Quantity` + with correct units and valid numerical values. + + Parameters + ---------- + arg : ~astropy.units.Quantity + The object to be tested. + + argname : str + The name of the argument to be printed in error messages. + + funcname : str + The name of the original function to be printed in error messages. + + units : `~astropy.units.Unit` or list of `~astropy.unit.Unit` + Acceptable units for `arg`. + + can_be_negative : bool, optional + `True` if the `~astropy.units.Quantity` can be negative, + `False` otherwise. Defaults to `True`. + + can_be_complex : bool, optional + `True` if the `~astropy.units.Quantity` can be a complex number, + `False` otherwise. Defaults to `False`. + + can_be_inf : bool, optional + `True` if the `~astropy.units.Quantity` can contain infinite + values, `False` otherwise. Defaults to `True`. + + can_be_nan : bool, optional + `True` if the `~astropy.units.Quantity` can contain NaN + values, `False` otherwise. Defaults to `True`. + + none_shall_pass : bool, optional + `True` if the `~astropy.units.Quantity` can contain None + values, `False` otherwise. Defaults to `True`. + + Raises + ------ + TypeError + If the argument is not a `~astropy.units.Quantity` or units is + not entirely units. + + ~astropy.units.UnitConversionError + If the argument is not in acceptable units. + + ~astropy.units.UnitsError + If after the assumption checks, the argument is still not in acceptable + units. + + ValueError + If the argument contains any `~numpy.nan` or other invalid + values as determined by the keywords. + + Warns + ----- + ~astropy.units.UnitsWarning + If a `~astropy.units.Quantity` is not provided and unique units + are provided, a `UnitsWarning` will be raised and the inputted + units will be assumed. + + Examples + -------- + >>> from astropy import units as u + >>> import pytest + >>> _check_quantity(4*u.T, 'B', 'f', u.T) + + >>> with pytest.warns(u.UnitsWarning, match="No units are specified"): + ... assert _check_quantity(4, 'B', 'f', u.T) == 4 * u.T + """ + + # TODO: Replace `funcname` with func.__name__? + + if not isinstance(units, list): + units = [units] + + for unit in units: + if not isinstance(unit, (u.Unit, u.CompositeUnit, u.IrreducibleUnit)): + raise TypeError( + "The keyword 'units' to check_quantity must be " + "a unit or a list/tuple containing only units.") + + # Create a generic error message + + typeerror_message = ( + f"The argument {argname} to {funcname} should be a Quantity with " + ) + + if len(units) == 1: + typeerror_message += f"the following units: {str(units[0])}" + else: + typeerror_message += "one of the following units: " + for unit in units: + typeerror_message += str(unit) + if unit != units[-1]: + typeerror_message += ", " + if none_shall_pass: + typeerror_message += "or None " + + if isinstance(arg, (u.Unit, u.CompositeUnit, u.IrreducibleUnit)): + raise TypeError(typeerror_message) + + # Make sure arg is a quantity with correct units + + unit_casting_warning = dedent( + f"""No units are specified for {argname} = {arg} in {funcname}. Assuming units of {str(units[0])}. + To silence this warning, explicitly pass in an Astropy Quantity (from astropy.units) + (see http://docs.astropy.org/en/stable/units/)""") + + # TODO include explicit note on how to pass in Astropy Quantity + + valueerror_message = ( + f"The argument {argname} to function {funcname} cannot contain" + ) + + if arg is None and none_shall_pass: + return arg + elif arg is None: + raise ValueError(f"{valueerror_message} Nones.") + if not isinstance(arg, (u.Quantity)): + if len(units) != 1: + raise TypeError(typeerror_message) + else: + try: + arg = arg * units[0] + except (u.UnitsError, ValueError): + raise TypeError(typeerror_message) + else: + warnings.warn(UnitsWarning(unit_casting_warning)) + if not isinstance(arg, u.Quantity): + raise u.UnitsError("{} is still not a Quantity after checks!".format(arg)) + + in_acceptable_units = [] + + for unit in units: + try: + arg.unit.to(unit, equivalencies=u.temperature_energy()) + except Exception: + in_acceptable_units.append(False) + else: + in_acceptable_units.append(True) + + if not np.any(in_acceptable_units): + raise u.UnitConversionError(typeerror_message) + + # Make sure that the quantity has valid numerical values + if np.any(np.isnan(arg.value)) and not can_be_nan: + raise ValueError(f"{valueerror_message} NaNs.") + elif np.any(np.iscomplex(arg.value)) and not can_be_complex: + raise ValueError(f"{valueerror_message} complex numbers.") + elif not can_be_negative: + # Allow NaNs through without raising a warning + with np.errstate(invalid='ignore'): + isneg = np.any(arg.value < 0) + if isneg: + raise ValueError(f"{valueerror_message} negative numbers.") + elif not can_be_inf and np.any(np.isinf(arg.value)): + raise ValueError(f"{valueerror_message} infs.") + + return arg From 42ee27c704bdf49539ff9679a46156a6eb442987 Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Wed, 8 May 2019 11:40:48 -0700 Subject: [PATCH 43/68] PEP 8 line continuation fix --- bapsflib/plasma/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bapsflib/plasma/constants.py b/bapsflib/plasma/constants.py index c6f14e73..84093380 100644 --- a/bapsflib/plasma/constants.py +++ b/bapsflib/plasma/constants.py @@ -76,7 +76,7 @@ 'The following constants are available:\n', _tb_div, '{0:^8} {1:^17} {2:^10} {3:^7} {4}'.format('Name', 'Value', 'Units', - 'System', 'Description'), + 'System', 'Description'), _tb_div, '{0:^8} {1:^17.12f} {2:^10} {3:^7} {4}'.format( 'pi', pi, '', '', From ad23cbca14e71f0ed24badbe9ed2c476d7192d17 Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Wed, 8 May 2019 17:58:36 -0700 Subject: [PATCH 44/68] define an astropy.unit.equivalencies that converts Kelvin, Celsius, Fahrenheit, and eV --- bapsflib/utils/helpers.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/bapsflib/utils/helpers.py b/bapsflib/utils/helpers.py index 5f111817..51c7e439 100644 --- a/bapsflib/utils/helpers.py +++ b/bapsflib/utils/helpers.py @@ -26,6 +26,44 @@ from typing import (Dict, Union) +def temperature_and_energy(): + """ + Convert between Kelvin, Celsius, Fahrenheit, and eV. (An + `astropy.units.equivalencies`) + """ + # combine standard temperature and energy equivalencies + equiv = u.temperature() # type: list + equiv.extend(u.temperature_energy()) + + # construct functions for non-Kelvin to energy (and visa-versa) + # conversions + def convert_C_to_eV(x): + # convert Celsius to eV + x = (x * u.deg_C).to(u.K, equivalencies=equiv) + return x.to_value(u.eV, equivalencies=equiv) + + def convert_eV_to_C(x): + # convert eV to Celsius + x = (x * u.eV).to(u.K, equivalencies=equiv) + return x.to_value(u.deg_C, equivalencies=equiv) + + def convert_F_to_eV(x): + # convert Fahrenheit to eV + x = (x * u.imperial.deg_F).to(u.K, equivalencies=equiv) + return x.to_value(u.eV, equivalencies=equiv) + + def convert_eV_to_F(x): + # convert eV to Fahrenheit to eV + x = (x * u.eV).to(u.K, equivalencies=equiv) + return x.to_value(u.imperial.deg_F, equivalencies=equiv) + + # add new equivalencies to the complete list + equiv.append((u.deg_C, u.eV, convert_C_to_eV, convert_eV_to_C)) + equiv.append((u.imperial.deg_F, u.eV, + convert_F_to_eV, convert_eV_to_F)) + return equiv + + # this is modified from plasmapy.utils.checks.check_quantity # TODO: replace with PlasmaPy version when PlasmaPy v0.2.0 is released def check_quantity(**validations: Dict[str, Union[bool, u.Unit]]): From 0535971b79bba6cd6163c97a4eaaacefe2b78cc1 Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Wed, 8 May 2019 19:06:19 -0700 Subject: [PATCH 45/68] add an equivalencies kwarg to `_check_quantity`; fix some PEP 8 issues; make `check_quantity` and `_check_quantity` enforce specified unit --- bapsflib/utils/helpers.py | 70 ++++++++++++++++++++++++++++++--------- 1 file changed, 55 insertions(+), 15 deletions(-) diff --git a/bapsflib/utils/helpers.py b/bapsflib/utils/helpers.py index 51c7e439..881118c1 100644 --- a/bapsflib/utils/helpers.py +++ b/bapsflib/utils/helpers.py @@ -200,6 +200,8 @@ def wrapper(*args, **kwargs): in validations.items(): value_to_check = given_params_values[param_to_check] + equivalencies = \ + validation_settings.get('equivalencies', None) can_be_negative = \ validation_settings.get('can_be_negative', True) can_be_complex = \ @@ -216,6 +218,7 @@ def wrapper(*args, **kwargs): param_to_check, fname, validation_settings['units'], + equivalencies=equivalencies, can_be_negative=can_be_negative, can_be_complex=can_be_complex, can_be_inf=can_be_inf, @@ -235,6 +238,7 @@ def wrapper(*args, **kwargs): # this is modified from plasmapy.utils.checks._check_quantity # TODO: replace with PlasmaPy version when PlasmaPy v0.2.0 is released def _check_quantity(arg, argname, funcname, units, + equivalencies=None, can_be_negative=True, can_be_complex=False, can_be_inf=True, can_be_nan=True, none_shall_pass=False): @@ -251,7 +255,8 @@ def _check_quantity(arg, argname, funcname, units, The name of the argument to be printed in error messages. funcname : str - The name of the original function to be printed in error messages. + The name of the original function to be printed in error + messages. units : `~astropy.units.Unit` or list of `~astropy.unit.Unit` Acceptable units for `arg`. @@ -286,8 +291,8 @@ def _check_quantity(arg, argname, funcname, units, If the argument is not in acceptable units. ~astropy.units.UnitsError - If after the assumption checks, the argument is still not in acceptable - units. + If after the assumption checks, the argument is still not in + acceptable units. ValueError If the argument contains any `~numpy.nan` or other invalid @@ -309,22 +314,34 @@ def _check_quantity(arg, argname, funcname, units, >>> with pytest.warns(u.UnitsWarning, match="No units are specified"): ... assert _check_quantity(4, 'B', 'f', u.T) == 4 * u.T """ - - # TODO: Replace `funcname` with func.__name__? - + # condition units argument if not isinstance(units, list): units = [units] - for unit in units: - if not isinstance(unit, (u.Unit, u.CompositeUnit, u.IrreducibleUnit)): + if not isinstance(unit, (u.Unit, u.CompositeUnit, + u.IrreducibleUnit)): raise TypeError( "The keyword 'units' to check_quantity must be " "a unit or a list/tuple containing only units.") - # Create a generic error message + # condition equivalencies argument + if equivalencies is None: + equivalencies = [None] * len(units) + elif isinstance(equivalencies, list): + if all(isinstance(el, tuple) for el in equivalencies): + equivalencies = [equivalencies] + if len(equivalencies) == 1: + equivalencies = equivalencies * len(units) + elif len(equivalencies) != len(units): + raise ValueError( + f"The length of specified equivalencies " + f"({len(equivalencies)}) must be 1 or equal to the " + f"number of specified units ({len(units)})") + # Create a generic error message typeerror_message = ( - f"The argument {argname} to {funcname} should be a Quantity with " + f"The argument {argname} to {funcname} should be a " + f"Quantity with " ) if len(units) == 1: @@ -344,21 +361,25 @@ def _check_quantity(arg, argname, funcname, units, # Make sure arg is a quantity with correct units unit_casting_warning = dedent( - f"""No units are specified for {argname} = {arg} in {funcname}. Assuming units of {str(units[0])}. - To silence this warning, explicitly pass in an Astropy Quantity (from astropy.units) + f"""No units are specified for {argname} = {arg} in + {funcname}. Assuming units of {str(units[0])}. + To silence this warning, explicitly pass in an Astropy + Quantity (from astropy.units) (see http://docs.astropy.org/en/stable/units/)""") # TODO include explicit note on how to pass in Astropy Quantity + # initialize error string valueerror_message = ( f"The argument {argname} to function {funcname} cannot contain" ) + # ensure arg is astropy.units.Quantity or return None (if allowed) if arg is None and none_shall_pass: return arg elif arg is None: raise ValueError(f"{valueerror_message} Nones.") - if not isinstance(arg, (u.Quantity)): + elif not isinstance(arg, u.Quantity): if len(units) != 1: raise TypeError(typeerror_message) else: @@ -368,21 +389,40 @@ def _check_quantity(arg, argname, funcname, units, raise TypeError(typeerror_message) else: warnings.warn(UnitsWarning(unit_casting_warning)) + + # check arg was converted to an astropy.units.Quantity if not isinstance(arg, u.Quantity): - raise u.UnitsError("{} is still not a Quantity after checks!".format(arg)) + raise u.UnitsError( + "{} is still not a Quantity after checks!".format(arg)) in_acceptable_units = [] + for unit, equiv in zip(units, equivalencies): + try: + arg.unit.to(unit, equivalencies=equiv) + except u.UnitConversionError: + in_acceptable_units.append(False) + else: + in_acceptable_units.append(True) + + if np.count_nonzero(in_acceptable_units) != 1: + raise u.UnitConversionError(typeerror_message) + else: + unit = np.array(units)[in_acceptable_units][0] + equiv = np.array(equivalencies)[in_acceptable_units][0] + arg = arg.to(unit, equivalencies=equiv) + ''' for unit in units: try: arg.unit.to(unit, equivalencies=u.temperature_energy()) - except Exception: + except u.UnitConversionError: in_acceptable_units.append(False) else: in_acceptable_units.append(True) if not np.any(in_acceptable_units): raise u.UnitConversionError(typeerror_message) + ''' # Make sure that the quantity has valid numerical values if np.any(np.isnan(arg.value)) and not can_be_nan: From 20f86ce67d5403e4bb8b62393c2a1b48b55e5e79 Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Wed, 8 May 2019 19:06:49 -0700 Subject: [PATCH 46/68] add temperature_and_energy() to __all__ --- bapsflib/utils/helpers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bapsflib/utils/helpers.py b/bapsflib/utils/helpers.py index 881118c1..28c40c38 100644 --- a/bapsflib/utils/helpers.py +++ b/bapsflib/utils/helpers.py @@ -12,7 +12,8 @@ Helper functions for the :mod:`bapsflib` package. """ -__all__ = ['check_relativistic', 'check_quantity'] +__all__ = ['check_relativistic', 'check_quantity', + 'temperature_and_energy'] import astropy.units as u import functools From 612fe5c01344628e7f082a102f3fac8d4a145fa9 Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Thu, 9 May 2019 09:58:12 -0700 Subject: [PATCH 47/68] move package decorators to their own module (bapsflib/utils/decorators.py) --- bapsflib/plasma/parameters.py | 123 ++++++----- bapsflib/utils/decorators.py | 403 ++++++++++++++++++++++++++++++++++ bapsflib/utils/helpers.py | 386 +------------------------------- 3 files changed, 467 insertions(+), 445 deletions(-) create mode 100644 bapsflib/utils/decorators.py diff --git a/bapsflib/plasma/parameters.py b/bapsflib/plasma/parameters.py index 507450d7..cfce1a60 100644 --- a/bapsflib/plasma/parameters.py +++ b/bapsflib/plasma/parameters.py @@ -24,8 +24,9 @@ import numpy as np from . import constants as const +from bapsflib.utils.decorators import (check_quantity, + check_relativistic) from bapsflib.utils.errors import PhysicsError -from plasmapy import utils from typing import Union @@ -43,12 +44,12 @@ # ---- Frequencies ---- -@utils.check_quantity({'q': {'units': u.statcoulomb, - "can_be_negative": True}, - 'B': {'units': u.Gauss, - "can_be_negative": False}, - 'm': {'units': u.g, - "can_be_negative": False}}) +@check_quantity(**{'q': {'units': u.statcoulomb, + 'can_be_negative': True}, + 'B': {'units': u.Gauss, + 'can_be_negative': False}, + 'm': {'units': u.g, + 'can_be_negative': False}}) def cyclotron_frequency(q: u.Quantity, B: u.Quantity, m: u.Quantity, to_Hz=False, **kwargs) -> u.Quantity: """ @@ -78,12 +79,12 @@ def cyclotron_frequency(q: u.Quantity, B: u.Quantity, m: u.Quantity, return _oc -@utils.check_quantity({'B': {'units': u.gauss, - 'can_be_negative': False}, - 'n_i': {'units': u.cm ** -3, - 'can_be_negative': False}, - 'm_i': {'units': u.g, - 'can_be_negative': False}}) +@check_quantity(**{'B': {'units': u.gauss, + 'can_be_negative': False}, + 'n_i': {'units': u.cm ** -3, + 'can_be_negative': False}, + 'm_i': {'units': u.g, + 'can_be_negative': False}}) def lower_hybrid_frequency(B: u.Quantity, n_i: u.Quantity, m_i: u.Quantity, Z: Union[int, float] = 1, to_Hz=False, **kwargs) -> u.Quantity: @@ -240,12 +241,12 @@ def oUH(B: u.Quantity, n_e: u.Quantity, return upper_hybrid_frequency(B, n_e, **kwargs) -@utils.check_quantity({'n': {'units': u.cm ** -3, - "can_be_negative": False}, - 'q': {'units': u.statcoulomb, - "can_be_negative": True}, - 'm': {'units': u.g, - "can_be_negative": False}}) +@check_quantity(**{'n': {'units': u.cm ** -3, + 'can_be_negative': False}, + 'q': {'units': u.statcoulomb, + 'can_be_negative': True}, + 'm': {'units': u.g, + 'can_be_negative': False}}) def plasma_frequency( n: u.Quantity, q: u.Quantity, m: u.Quantity, to_Hz=False, **kwargs) -> u.Quantity: @@ -277,10 +278,10 @@ def plasma_frequency( return _op -@utils.check_quantity({'B': {'units': u.gauss, - 'can_be_negative': False}, - 'n_e': {'units': u.cm ** -3, - 'can_be_negative': False}}) +@check_quantity(**{'B': {'units': u.gauss, + 'can_be_negative': False}, + 'n_e': {'units': u.cm ** -3, + 'can_be_negative': False}}) def upper_hybrid_frequency(B: u.Quantity, n_e: u.Quantity, to_Hz=False, **kwargs) -> u.Quantity: """ @@ -302,10 +303,10 @@ def upper_hybrid_frequency(B: u.Quantity, n_e: u.Quantity, # ---- Lengths ---- -@utils.check_quantity({'kTe': {'units': u.eV, - 'can_be_negative': False}, - 'n': {'units': u.cm ** -3, - 'can_be_negative': False}}) +@check_quantity(**{'kTe': {'units': u.eV, + 'can_be_negative': False}, + 'n': {'units': u.cm ** -3, + 'can_be_negative': False}}) def Debye_length(kTe: u.Quantity, n: u.Quantity, **kwargs) -> u.Quantity: """ @@ -327,8 +328,8 @@ def Debye_length(kTe: u.Quantity, n: u.Quantity, return _lD.cgs -@utils.check_quantity({'vperp': {'units': u.cm / u.s, - 'can_be_negative': True}}) +@check_quantity(**{'vperp': {'units': u.cm / u.s, + 'can_be_negative': True}}) def cyclotron_radius(vperp: u.Quantity, q: u.Quantity, B: u.Quantity, m: u.Quantity, **kwargs) -> u.Quantity: """ @@ -353,12 +354,12 @@ def cyclotron_radius(vperp: u.Quantity, q: u.Quantity, B: u.Quantity, return _r.to(u.cm) -@utils.check_quantity({'n': {'units': u.cm ** -3, - 'can_be_negative': False}, - 'q': {'units': u.statcoulomb, - 'can_be_negative': True}, - 'm': {'units': u.g, - 'can_be_negative': False}}) +@check_quantity(**{'n': {'units': u.cm ** -3, + 'can_be_negative': False}, + 'q': {'units': u.statcoulomb, + 'can_be_negative': True}, + 'm': {'units': u.g, + 'can_be_negative': False}}) def inertial_length(n: u.Quantity, q: u.Quantity, m: u.Quantity, **kwargs) -> u.Quantity: """ @@ -466,13 +467,13 @@ def rci(kTi: u.Quantity, B: u.Quantity, m_i: u.Quantity, # ---- Velocities ---- -@utils.check_relativistic -@utils.check_quantity({'B': {'units': u.gauss, - 'can_be_negative': False}, - 'n_e': {'units': u.cm ** -3, - 'can_be_negative': False}, - 'm_i': {'units': u.g, - 'can_be_negative': False}}) +@check_relativistic +@check_quantity(**{'B': {'units': u.gauss, + 'can_be_negative': False}, + 'n_e': {'units': u.cm ** -3, + 'can_be_negative': False}, + 'm_i': {'units': u.g, + 'can_be_negative': False}}) def Alfven_speed(B: u.Quantity, n_e: u.Quantity, m_i: u.Quantity, Z: Union[int, float] = 1, **kwargs) -> u.Quantity: """ @@ -526,13 +527,14 @@ def cs(kTe: u.Quantity, m_i: u.Quantity, return ion_sound_speed(kTe, m_i, **kwargs) -@utils.check_relativistic -@utils.check_quantity({'kTe': {'units': u.eV, - 'can_be_negative': False}, - 'kTi': {'units': u.eV, - 'can_be_negative': False}, - 'm_i': {'units': u.g, - 'can_be_negative': False}}) +# @utils.check_relativistic +@check_relativistic +@check_quantity(**{'kTe': {'units': u.eV, + 'can_be_negative': False}, + 'kTi': {'units': u.eV, + 'can_be_negative': False}, + 'm_i': {'units': u.g, + 'can_be_negative': False}}) def ion_sound_speed(kTe: u.Quantity, m_i: u.Quantity, kTi: u.Quantity = (0.0 * u.eV), gamma_e: Union[int, float] = 1, @@ -580,11 +582,12 @@ def ion_sound_speed(kTe: u.Quantity, m_i: u.Quantity, return _cs -@utils.check_relativistic -@utils.check_quantity({'kT': {'units': u.eV, - 'can_be_negative': False}, - 'm': {'units': u.g, - 'can_be_negative': False}}) +# @utils.check_relativistic +@check_relativistic +@check_quantity(**{'kT': {'units': u.eV, + 'can_be_negative': False}, + 'm': {'units': u.g, + 'can_be_negative': False}}) def thermal_speed(kT: u.Quantity, m: u.Quantity, **kwargs) -> u.Quantity: """ @@ -647,12 +650,12 @@ def vTi(kTi: u.Quantity, m_i: u.Quantity, **kwargs) -> u.Quantity: # ---- Dimensionless ---- -@utils.check_quantity({'n': {'units': u.cm ** -3, - 'can_be_negative': False}, - 'kT': {'units': u.eV, - 'can_be_negative': False}, - 'B': {'units': u.Gauss, - 'can_be_negative': False}}) +@check_quantity(**{'n': {'units': u.cm ** -3, + 'can_be_negative': False}, + 'kT': {'units': u.eV, + 'can_be_negative': False}, + 'B': {'units': u.Gauss, + 'can_be_negative': False}}) def beta(n: u.Quantity, kT: u.Quantity, B: u.Quantity, **kwargs) -> u.Quantity: """ diff --git a/bapsflib/utils/decorators.py b/bapsflib/utils/decorators.py new file mode 100644 index 00000000..31aebf10 --- /dev/null +++ b/bapsflib/utils/decorators.py @@ -0,0 +1,403 @@ +# This file is part of the bapsflib package, a Python toolkit for the +# BaPSF group at UCLA. +# +# http://plasma.physics.ucla.edu/ +# +# Copyright 2017-2019 Erik T. Everson and contributors +# +# License: Standard 3-clause BSD; see "LICENSES/LICENSE.txt" for full +# license terms and contributor agreement. +# +""" +Decorators for the :mod:`bapsflib` package. +""" + +__all__ = ['check_quantity', 'check_relativistic'] + +import astropy.units as u +import functools +import inspect +import numpy as np +import warnings + +from astropy.units import UnitsWarning +from plasmapy.utils import check_relativistic +from textwrap import dedent +from typing import (Dict, Union) + + +# this is modified from plasmapy.utils.checks.check_quantity +# TODO: replace with PlasmaPy version when PlasmaPy v0.2.0 is released +def check_quantity(**validations: Dict[str, Union[bool, u.Unit]]): + """ + Verify that the function's arguments have correct units. + This decorator raises an exception if an annotated argument in the + decorated function is an `~astropy.units.Quantity` with incorrect + units or of incorrect kind. You can prevent arguments from being + input as Nones, NaNs, negatives, infinities and complex numbers. + + If a number (non-Quantity) value is inserted in place of a value + with units, assume the input is an SI Quantity and cast it to one. + + This is probably best illustrated with an example: + + Examples + -------- + >>> from astropy import units as u + >>> @check_quantity(x={"units": u.m, + ... "can_be_negative": False, + ... "can_be_complex": True, + ... "can_be_inf": True}) + ... def func(x): + ... return x + + >>> func(1 * u.m) + + + >>> func(1 * u.s) + Traceback (most recent call last): + ... + astropy.units.core.UnitConversionError: The argument x to func should be a Quantity with the following units: m + + >>> import pytest # to show the UnitsWarning + >>> with pytest.warns(u.UnitsWarning, match="Assuming units of m."): + ... func(1) + + + >>> func(-1 * u.m) + Traceback (most recent call last): + ... + ValueError: The argument x to function func cannot contain negative numbers. + + >>> func(np.inf * u.m) + + + >>> func(None) + Traceback (most recent call last): + ... + ValueError: The argument x to function func cannot contain Nones. + + Parameters + ---------- + dict + Arguments to be validated passed in as keyword arguments, + with values as validation dictionaries, with structure as + in the example. Valid keys for each argument are: + 'units': `astropy.units.Unit`, + 'can_be_negative': `bool`, + 'can_be_complex': `bool`, + 'can_be_inf': `bool`, + 'can_be_nan': `bool`, + 'none_shall_pass': `bool` + + Raises + ------ + `TypeError` + If the argument is not a `~astropy.units.Quantity`, units is + not entirely units or `argname` does not have a type + annotation. + + `~astropy.units.UnitConversionError` + If the argument is not in acceptable units. + + `~astropy.units.UnitsError` + If after the assumption checks, the argument is still not in + acceptable units. + + `ValueError` + If the argument contains `~numpy.nan` or other invalid values + as determined by the keywords. + + Warns + ----- + `~astropy.units.UnitsWarning` + If a `~astropy.units.Quantity` is not provided and unique units + are provided, a `UnitsWarning` will be raised and the inputted + units will be assumed. + + Notes + ----- + This functionality may end up becoming deprecated due to + noncompliance with the `IEEE 754 standard + `_ + and in favor of `~astropy.units.quantity_input`. + + Returns + ------- + function + Decorated function. + + See also + -------- + _check_quantity + """ + def decorator(f): + wrapped_sign = inspect.signature(f) + fname = f.__name__ + + assigned = list(functools.WRAPPER_ASSIGNMENTS) + assigned.append('__signature__') + @functools.wraps(f, assigned=assigned) + def wrapper(*args, **kwargs): + # combine args and kwargs into dictionary + bound_args = wrapped_sign.bind(*args, **kwargs) + bound_args.apply_defaults() + given_params_values = bound_args.arguments + given_params = set(given_params_values.keys()) + + # names of params to check + validated_params = set(validations.keys()) + + missing_params = [ + param for param in (validated_params - given_params) + ] + + if len(missing_params) > 0: + params_str = ", ".join(missing_params) + raise TypeError( + f"Call to {fname} is missing " + f"validated params {params_str}") + + for param_to_check, validation_settings \ + in validations.items(): + value_to_check = given_params_values[param_to_check] + + equivalencies = \ + validation_settings.get('equivalencies', None) + can_be_negative = \ + validation_settings.get('can_be_negative', True) + can_be_complex = \ + validation_settings.get('can_be_complex', False) + can_be_inf = \ + validation_settings.get('can_be_inf', True) + can_be_nan = \ + validation_settings.get('can_be_nan', True) + none_shall_pass = \ + validation_settings.get('none_shall_pass', False) + + validated_value = _check_quantity( + value_to_check, + param_to_check, + fname, + validation_settings['units'], + equivalencies=equivalencies, + can_be_negative=can_be_negative, + can_be_complex=can_be_complex, + can_be_inf=can_be_inf, + can_be_nan=can_be_nan, + none_shall_pass=none_shall_pass, + ) + given_params_values[param_to_check] = validated_value + + return f(**given_params_values) + + if not hasattr(wrapper, '__signature__'): + wrapper.__signature__ = inspect.signature(f) + return wrapper + return decorator + + +# this is modified from plasmapy.utils.checks._check_quantity +# TODO: replace with PlasmaPy version when PlasmaPy v0.2.0 is released +def _check_quantity(arg, argname, funcname, units, + equivalencies=None, + can_be_negative=True, can_be_complex=False, + can_be_inf=True, can_be_nan=True, + none_shall_pass=False): + """ + Raise an exception if an object is not a `~astropy.units.Quantity` + with correct units and valid numerical values. + + Parameters + ---------- + arg : ~astropy.units.Quantity + The object to be tested. + + argname : str + The name of the argument to be printed in error messages. + + funcname : str + The name of the original function to be printed in error + messages. + + units : `~astropy.units.Unit` or list of `~astropy.unit.Unit` + Acceptable units for `arg`. + + can_be_negative : bool, optional + `True` if the `~astropy.units.Quantity` can be negative, + `False` otherwise. Defaults to `True`. + + can_be_complex : bool, optional + `True` if the `~astropy.units.Quantity` can be a complex number, + `False` otherwise. Defaults to `False`. + + can_be_inf : bool, optional + `True` if the `~astropy.units.Quantity` can contain infinite + values, `False` otherwise. Defaults to `True`. + + can_be_nan : bool, optional + `True` if the `~astropy.units.Quantity` can contain NaN + values, `False` otherwise. Defaults to `True`. + + none_shall_pass : bool, optional + `True` if the `~astropy.units.Quantity` can contain None + values, `False` otherwise. Defaults to `True`. + + Raises + ------ + TypeError + If the argument is not a `~astropy.units.Quantity` or units is + not entirely units. + + ~astropy.units.UnitConversionError + If the argument is not in acceptable units. + + ~astropy.units.UnitsError + If after the assumption checks, the argument is still not in + acceptable units. + + ValueError + If the argument contains any `~numpy.nan` or other invalid + values as determined by the keywords. + + Warns + ----- + ~astropy.units.UnitsWarning + If a `~astropy.units.Quantity` is not provided and unique units + are provided, a `UnitsWarning` will be raised and the inputted + units will be assumed. + + Examples + -------- + >>> from astropy import units as u + >>> import pytest + >>> _check_quantity(4*u.T, 'B', 'f', u.T) + + >>> with pytest.warns(u.UnitsWarning, match="No units are specified"): + ... assert _check_quantity(4, 'B', 'f', u.T) == 4 * u.T + """ + # condition units argument + if not isinstance(units, list): + units = [units] + for unit in units: + if not isinstance(unit, (u.Unit, u.CompositeUnit, + u.IrreducibleUnit)): + raise TypeError( + "The keyword 'units' to check_quantity must be " + "a unit or a list/tuple containing only units.") + + # condition equivalencies argument + if equivalencies is None: + equivalencies = [None] * len(units) + elif isinstance(equivalencies, list): + if all(isinstance(el, tuple) for el in equivalencies): + equivalencies = [equivalencies] + if len(equivalencies) == 1: + equivalencies = equivalencies * len(units) + elif len(equivalencies) != len(units): + raise ValueError( + f"The length of specified equivalencies " + f"({len(equivalencies)}) must be 1 or equal to the " + f"number of specified units ({len(units)})") + + # Create a generic error message + typeerror_message = ( + f"The argument {argname} to {funcname} should be a " + f"Quantity with " + ) + + if len(units) == 1: + typeerror_message += f"the following units: {str(units[0])}" + else: + typeerror_message += "one of the following units: " + for unit in units: + typeerror_message += str(unit) + if unit != units[-1]: + typeerror_message += ", " + if none_shall_pass: + typeerror_message += "or None " + + if isinstance(arg, (u.Unit, u.CompositeUnit, u.IrreducibleUnit)): + raise TypeError(typeerror_message) + + # Make sure arg is a quantity with correct units + + unit_casting_warning = dedent( + f"""No units are specified for {argname} = {arg} in + {funcname}. Assuming units of {str(units[0])}. + To silence this warning, explicitly pass in an Astropy + Quantity (from astropy.units) + (see http://docs.astropy.org/en/stable/units/)""") + + # TODO include explicit note on how to pass in Astropy Quantity + + # initialize error string + valueerror_message = ( + f"The argument {argname} to function {funcname} cannot contain" + ) + + # ensure arg is astropy.units.Quantity or return None (if allowed) + if arg is None and none_shall_pass: + return arg + elif arg is None: + raise ValueError(f"{valueerror_message} Nones.") + elif not isinstance(arg, u.Quantity): + if len(units) != 1: + raise TypeError(typeerror_message) + else: + try: + arg = arg * units[0] + except (u.UnitsError, ValueError): + raise TypeError(typeerror_message) + else: + warnings.warn(UnitsWarning(unit_casting_warning)) + + # check arg was converted to an astropy.units.Quantity + if not isinstance(arg, u.Quantity): + raise u.UnitsError( + "{} is still not a Quantity after checks!".format(arg)) + + in_acceptable_units = [] + + for unit, equiv in zip(units, equivalencies): + try: + arg.unit.to(unit, equivalencies=equiv) + except u.UnitConversionError: + in_acceptable_units.append(False) + else: + in_acceptable_units.append(True) + + if np.count_nonzero(in_acceptable_units) != 1: + raise u.UnitConversionError(typeerror_message) + else: + unit = np.array(units)[in_acceptable_units][0] + equiv = np.array(equivalencies)[in_acceptable_units][0] + arg = arg.to(unit, equivalencies=equiv) + ''' + for unit in units: + try: + arg.unit.to(unit, equivalencies=u.temperature_energy()) + except u.UnitConversionError: + in_acceptable_units.append(False) + else: + in_acceptable_units.append(True) + + if not np.any(in_acceptable_units): + raise u.UnitConversionError(typeerror_message) + ''' + + # Make sure that the quantity has valid numerical values + if np.any(np.isnan(arg.value)) and not can_be_nan: + raise ValueError(f"{valueerror_message} NaNs.") + elif np.any(np.iscomplex(arg.value)) and not can_be_complex: + raise ValueError(f"{valueerror_message} complex numbers.") + elif not can_be_negative: + # Allow NaNs through without raising a warning + with np.errstate(invalid='ignore'): + isneg = np.any(arg.value < 0) + if isneg: + raise ValueError(f"{valueerror_message} negative numbers.") + elif not can_be_inf and np.any(np.isinf(arg.value)): + raise ValueError(f"{valueerror_message} infs.") + + return arg diff --git a/bapsflib/utils/helpers.py b/bapsflib/utils/helpers.py index 28c40c38..329abd67 100644 --- a/bapsflib/utils/helpers.py +++ b/bapsflib/utils/helpers.py @@ -12,19 +12,9 @@ Helper functions for the :mod:`bapsflib` package. """ -__all__ = ['check_relativistic', 'check_quantity', - 'temperature_and_energy'] +__all__ = ['temperature_and_energy'] import astropy.units as u -import functools -import inspect -import numpy as np -import warnings - -from astropy.units import UnitsWarning -from plasmapy.utils import check_relativistic -from textwrap import dedent -from typing import (Dict, Union) def temperature_and_energy(): @@ -65,378 +55,4 @@ def convert_eV_to_F(x): return equiv -# this is modified from plasmapy.utils.checks.check_quantity -# TODO: replace with PlasmaPy version when PlasmaPy v0.2.0 is released -def check_quantity(**validations: Dict[str, Union[bool, u.Unit]]): - """ - Verify that the function's arguments have correct units. - This decorator raises an exception if an annotated argument in the - decorated function is an `~astropy.units.Quantity` with incorrect - units or of incorrect kind. You can prevent arguments from being - input as Nones, NaNs, negatives, infinities and complex numbers. - - If a number (non-Quantity) value is inserted in place of a value - with units, assume the input is an SI Quantity and cast it to one. - - This is probably best illustrated with an example: - - Examples - -------- - >>> from astropy import units as u - >>> @check_quantity(x={"units": u.m, - ... "can_be_negative": False, - ... "can_be_complex": True, - ... "can_be_inf": True}) - ... def func(x): - ... return x - - >>> func(1 * u.m) - - - >>> func(1 * u.s) - Traceback (most recent call last): - ... - astropy.units.core.UnitConversionError: The argument x to func should be a Quantity with the following units: m - - >>> import pytest # to show the UnitsWarning - >>> with pytest.warns(u.UnitsWarning, match="Assuming units of m."): - ... func(1) - - - >>> func(-1 * u.m) - Traceback (most recent call last): - ... - ValueError: The argument x to function func cannot contain negative numbers. - - >>> func(np.inf * u.m) - - - >>> func(None) - Traceback (most recent call last): - ... - ValueError: The argument x to function func cannot contain Nones. - - Parameters - ---------- - dict - Arguments to be validated passed in as keyword arguments, - with values as validation dictionaries, with structure as - in the example. Valid keys for each argument are: - 'units': `astropy.units.Unit`, - 'can_be_negative': `bool`, - 'can_be_complex': `bool`, - 'can_be_inf': `bool`, - 'can_be_nan': `bool`, - 'none_shall_pass': `bool` - - Raises - ------ - `TypeError` - If the argument is not a `~astropy.units.Quantity`, units is - not entirely units or `argname` does not have a type - annotation. - - `~astropy.units.UnitConversionError` - If the argument is not in acceptable units. - - `~astropy.units.UnitsError` - If after the assumption checks, the argument is still not in - acceptable units. - - `ValueError` - If the argument contains `~numpy.nan` or other invalid values - as determined by the keywords. - - Warns - ----- - `~astropy.units.UnitsWarning` - If a `~astropy.units.Quantity` is not provided and unique units - are provided, a `UnitsWarning` will be raised and the inputted - units will be assumed. - - Notes - ----- - This functionality may end up becoming deprecated due to - noncompliance with the `IEEE 754 standard - `_ - and in favor of `~astropy.units.quantity_input`. - - Returns - ------- - function - Decorated function. - - See also - -------- - _check_quantity - """ - def decorator(f): - wrapped_sign = inspect.signature(f) - fname = f.__name__ - - assigned = list(functools.WRAPPER_ASSIGNMENTS) - assigned.append('__signature__') - @functools.wraps(f, assigned=assigned) - def wrapper(*args, **kwargs): - # combine args and kwargs into dictionary - bound_args = wrapped_sign.bind(*args, **kwargs) - bound_args.apply_defaults() - given_params_values = bound_args.arguments - given_params = set(given_params_values.keys()) - - # names of params to check - validated_params = set(validations.keys()) - - missing_params = [ - param for param in (validated_params - given_params) - ] - - if len(missing_params) > 0: - params_str = ", ".join(missing_params) - raise TypeError( - f"Call to {fname} is missing " - f"validated params {params_str}") - - for param_to_check, validation_settings \ - in validations.items(): - value_to_check = given_params_values[param_to_check] - - equivalencies = \ - validation_settings.get('equivalencies', None) - can_be_negative = \ - validation_settings.get('can_be_negative', True) - can_be_complex = \ - validation_settings.get('can_be_complex', False) - can_be_inf = \ - validation_settings.get('can_be_inf', True) - can_be_nan = \ - validation_settings.get('can_be_nan', True) - none_shall_pass = \ - validation_settings.get('none_shall_pass', False) - - validated_value = _check_quantity( - value_to_check, - param_to_check, - fname, - validation_settings['units'], - equivalencies=equivalencies, - can_be_negative=can_be_negative, - can_be_complex=can_be_complex, - can_be_inf=can_be_inf, - can_be_nan=can_be_nan, - none_shall_pass=none_shall_pass, - ) - given_params_values[param_to_check] = validated_value - - return f(**given_params_values) - - if not hasattr(wrapper, '__signature__'): - wrapper.__signature__ = inspect.signature(f) - return wrapper - return decorator - - -# this is modified from plasmapy.utils.checks._check_quantity -# TODO: replace with PlasmaPy version when PlasmaPy v0.2.0 is released -def _check_quantity(arg, argname, funcname, units, - equivalencies=None, - can_be_negative=True, can_be_complex=False, - can_be_inf=True, can_be_nan=True, - none_shall_pass=False): - """ - Raise an exception if an object is not a `~astropy.units.Quantity` - with correct units and valid numerical values. - - Parameters - ---------- - arg : ~astropy.units.Quantity - The object to be tested. - - argname : str - The name of the argument to be printed in error messages. - - funcname : str - The name of the original function to be printed in error - messages. - - units : `~astropy.units.Unit` or list of `~astropy.unit.Unit` - Acceptable units for `arg`. - - can_be_negative : bool, optional - `True` if the `~astropy.units.Quantity` can be negative, - `False` otherwise. Defaults to `True`. - - can_be_complex : bool, optional - `True` if the `~astropy.units.Quantity` can be a complex number, - `False` otherwise. Defaults to `False`. - - can_be_inf : bool, optional - `True` if the `~astropy.units.Quantity` can contain infinite - values, `False` otherwise. Defaults to `True`. - - can_be_nan : bool, optional - `True` if the `~astropy.units.Quantity` can contain NaN - values, `False` otherwise. Defaults to `True`. - - none_shall_pass : bool, optional - `True` if the `~astropy.units.Quantity` can contain None - values, `False` otherwise. Defaults to `True`. - - Raises - ------ - TypeError - If the argument is not a `~astropy.units.Quantity` or units is - not entirely units. - - ~astropy.units.UnitConversionError - If the argument is not in acceptable units. - - ~astropy.units.UnitsError - If after the assumption checks, the argument is still not in - acceptable units. - - ValueError - If the argument contains any `~numpy.nan` or other invalid - values as determined by the keywords. - - Warns - ----- - ~astropy.units.UnitsWarning - If a `~astropy.units.Quantity` is not provided and unique units - are provided, a `UnitsWarning` will be raised and the inputted - units will be assumed. - - Examples - -------- - >>> from astropy import units as u - >>> import pytest - >>> _check_quantity(4*u.T, 'B', 'f', u.T) - - >>> with pytest.warns(u.UnitsWarning, match="No units are specified"): - ... assert _check_quantity(4, 'B', 'f', u.T) == 4 * u.T - """ - # condition units argument - if not isinstance(units, list): - units = [units] - for unit in units: - if not isinstance(unit, (u.Unit, u.CompositeUnit, - u.IrreducibleUnit)): - raise TypeError( - "The keyword 'units' to check_quantity must be " - "a unit or a list/tuple containing only units.") - - # condition equivalencies argument - if equivalencies is None: - equivalencies = [None] * len(units) - elif isinstance(equivalencies, list): - if all(isinstance(el, tuple) for el in equivalencies): - equivalencies = [equivalencies] - if len(equivalencies) == 1: - equivalencies = equivalencies * len(units) - elif len(equivalencies) != len(units): - raise ValueError( - f"The length of specified equivalencies " - f"({len(equivalencies)}) must be 1 or equal to the " - f"number of specified units ({len(units)})") - - # Create a generic error message - typeerror_message = ( - f"The argument {argname} to {funcname} should be a " - f"Quantity with " - ) - - if len(units) == 1: - typeerror_message += f"the following units: {str(units[0])}" - else: - typeerror_message += "one of the following units: " - for unit in units: - typeerror_message += str(unit) - if unit != units[-1]: - typeerror_message += ", " - if none_shall_pass: - typeerror_message += "or None " - - if isinstance(arg, (u.Unit, u.CompositeUnit, u.IrreducibleUnit)): - raise TypeError(typeerror_message) - - # Make sure arg is a quantity with correct units - - unit_casting_warning = dedent( - f"""No units are specified for {argname} = {arg} in - {funcname}. Assuming units of {str(units[0])}. - To silence this warning, explicitly pass in an Astropy - Quantity (from astropy.units) - (see http://docs.astropy.org/en/stable/units/)""") - - # TODO include explicit note on how to pass in Astropy Quantity - - # initialize error string - valueerror_message = ( - f"The argument {argname} to function {funcname} cannot contain" - ) - - # ensure arg is astropy.units.Quantity or return None (if allowed) - if arg is None and none_shall_pass: - return arg - elif arg is None: - raise ValueError(f"{valueerror_message} Nones.") - elif not isinstance(arg, u.Quantity): - if len(units) != 1: - raise TypeError(typeerror_message) - else: - try: - arg = arg * units[0] - except (u.UnitsError, ValueError): - raise TypeError(typeerror_message) - else: - warnings.warn(UnitsWarning(unit_casting_warning)) - - # check arg was converted to an astropy.units.Quantity - if not isinstance(arg, u.Quantity): - raise u.UnitsError( - "{} is still not a Quantity after checks!".format(arg)) - - in_acceptable_units = [] - - for unit, equiv in zip(units, equivalencies): - try: - arg.unit.to(unit, equivalencies=equiv) - except u.UnitConversionError: - in_acceptable_units.append(False) - else: - in_acceptable_units.append(True) - - if np.count_nonzero(in_acceptable_units) != 1: - raise u.UnitConversionError(typeerror_message) - else: - unit = np.array(units)[in_acceptable_units][0] - equiv = np.array(equivalencies)[in_acceptable_units][0] - arg = arg.to(unit, equivalencies=equiv) - ''' - for unit in units: - try: - arg.unit.to(unit, equivalencies=u.temperature_energy()) - except u.UnitConversionError: - in_acceptable_units.append(False) - else: - in_acceptable_units.append(True) - - if not np.any(in_acceptable_units): - raise u.UnitConversionError(typeerror_message) - ''' - - # Make sure that the quantity has valid numerical values - if np.any(np.isnan(arg.value)) and not can_be_nan: - raise ValueError(f"{valueerror_message} NaNs.") - elif np.any(np.iscomplex(arg.value)) and not can_be_complex: - raise ValueError(f"{valueerror_message} complex numbers.") - elif not can_be_negative: - # Allow NaNs through without raising a warning - with np.errstate(invalid='ignore'): - isneg = np.any(arg.value < 0) - if isneg: - raise ValueError(f"{valueerror_message} negative numbers.") - elif not can_be_inf and np.any(np.isinf(arg.value)): - raise ValueError(f"{valueerror_message} infs.") - return arg From ec70e07b751ed0604060a6e6ce7d1c14d41100b2 Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Thu, 9 May 2019 09:58:40 -0700 Subject: [PATCH 48/68] move __all__ to top (PEP 8) --- bapsflib/plasma/parameters.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/bapsflib/plasma/parameters.py b/bapsflib/plasma/parameters.py index cfce1a60..f84873e7 100644 --- a/bapsflib/plasma/parameters.py +++ b/bapsflib/plasma/parameters.py @@ -20,15 +20,6 @@ All units are in Gaussian cgs except for temperature, which is expressed in eV. (same as the NRL Plasma Formulary) """ -import astropy.units as u -import numpy as np - -from . import constants as const -from bapsflib.utils.decorators import (check_quantity, - check_relativistic) -from bapsflib.utils.errors import PhysicsError -from typing import Union - __all__ = ['cyclotron_frequency', 'oce', 'oci', 'lower_hybrid_frequency', 'oLH', @@ -42,6 +33,15 @@ 'thermal_speed', 'vTe', 'vTi', 'beta'] +import astropy.units as u +import numpy as np + +from . import constants as const +from bapsflib.utils.decorators import (check_quantity, + check_relativistic) +from bapsflib.utils.errors import PhysicsError +from typing import Union + # ---- Frequencies ---- @check_quantity(**{'q': {'units': u.statcoulomb, From ea71397e1d203a2ccaf35aed81c8b7462bf1400a Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Thu, 9 May 2019 10:01:07 -0700 Subject: [PATCH 49/68] update to version 1.1.0.dev (dropping support for python 3.5 since PlasmaPy does not support python 3.5) --- bapsflib/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bapsflib/__init__.py b/bapsflib/__init__.py index e0049ea2..c4e32801 100644 --- a/bapsflib/__init__.py +++ b/bapsflib/__init__.py @@ -26,4 +26,4 @@ from . import plasma # --- Define version --------------------------------------------------- -__version__ = '1.0.1.dev' +__version__ = '1.1.0.dev' From 9520d8352e9122f0b886fd0c2f0f440a19ac6b3a Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Thu, 9 May 2019 10:03:16 -0700 Subject: [PATCH 50/68] update CI, README.md, and setup.py to reflected dropped support for python 3.5 --- .travis.yml | 1 - README.md | 2 +- appveyor.yml | 2 -- setup.py | 3 +-- 4 files changed, 2 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 05a17507..c390a196 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,7 +14,6 @@ # languages language: python python: - - "3.5" - "3.6" # Enable 3.7 without globally enabling sudo and dist: xenial for other # build jobs diff --git a/README.md b/README.md index 1b06ec40..63c5774c 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ [![h5py](https://img.shields.io/badge/powered%20by-h5py-%235e9ffa.svg)](https://www.h5py.org/) -The **bapsflib** package is developed on Python 3.5+ and is intend to +The **bapsflib** package is developed on Python 3.6+ and is intend to be a toolkit for reading, manipulating, and analyzing data collected at the Basic Plasma Science Facility ([BaPSF](http://plasma.physics.ucla.edu/)) at UCLA. diff --git a/appveyor.yml b/appveyor.yml index 70498eb7..d0ce13a6 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -3,10 +3,8 @@ environment: matrix: - - PYTHON: "C:\\Python35" - PYTHON: "C:\\Python36" - PYTHON: "C:\\Python37" - - PYTHON: "C:\\Python35-x64" - PYTHON: "C:\\Python36-x64" - PYTHON: "C:\\Python37-x64" diff --git a/setup.py b/setup.py index 76d8f633..2f13cc62 100644 --- a/setup.py +++ b/setup.py @@ -31,7 +31,6 @@ "Operating System :: Microsoft :: Windows", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Topic :: Education", @@ -81,7 +80,7 @@ def find_version(*file_paths): 'h5py>=2.6', 'numpy>=1.7', 'scipy>=1.0.0'], - python_requires='>=3.5', + python_requires='>=3.6', author='Erik T. Everson', author_email='eteveson@gmail.com', license='3-clause BSD', From 662e999e06dce8065ddc53b481a6612f55251885 Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Thu, 9 May 2019 10:43:48 -0700 Subject: [PATCH 51/68] setup sphinx extension intersphinx --- docs/conf.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 67cd556a..b46a0e93 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -42,9 +42,16 @@ extensions = ['sphinx.ext.autodoc', 'sphinx.ext.githubpages', 'sphinx.ext.mathjax', - 'sphinx.ext.autosummary'] + 'sphinx.ext.autosummary', + 'sphinx.ext.intersphinx'] numfig = True # enable figure and table numbering autosummary_generate = True +intersphinx_mapping = { + 'python': ('https://docs.python.org/3/', None), + 'astropy': ('http://docs.astropy.org/en/stable/', None), + 'h5py': ('http://docs.h5py.org/en/stable/', None), + 'numpy': ('https://docs.scipy.org/doc/numpy', None), +} # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] From fbf49acdd5a2484f6b9ea0140250d49efdbefa7b Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Thu, 9 May 2019 11:08:54 -0700 Subject: [PATCH 52/68] create package bapsflib.utils.units; add equivalencies module to units package; move func temperature_and_energy to new equivalencies module; add equivalencies to bapsflib.utils imports --- bapsflib/utils/__init__.py | 6 ++- bapsflib/utils/helpers.py | 40 ------------------- bapsflib/utils/units/__init__.py | 18 +++++++++ bapsflib/utils/units/equivalencies.py | 57 +++++++++++++++++++++++++++ 4 files changed, 79 insertions(+), 42 deletions(-) create mode 100644 bapsflib/utils/units/__init__.py create mode 100644 bapsflib/utils/units/equivalencies.py diff --git a/bapsflib/utils/__init__.py b/bapsflib/utils/__init__.py index bb762467..f6493334 100644 --- a/bapsflib/utils/__init__.py +++ b/bapsflib/utils/__init__.py @@ -8,11 +8,13 @@ # License: Standard 3-clause BSD; see "LICENSES/LICENSE.txt" for full # license terms and contributor agreement. # +__all__ = ['BaPSFConstant', 'errors', 'temperature_and_energy', + 'warnings'] + from . import (errors, warnings) +from .units import temperature_and_energy from astropy.constants import Constant -__all__ = ['BaPSFConstant', 'errors', 'warnings'] - class BaPSFConstant(Constant): """Factory Class for BaPSF Constants""" diff --git a/bapsflib/utils/helpers.py b/bapsflib/utils/helpers.py index 329abd67..1f5ce26a 100644 --- a/bapsflib/utils/helpers.py +++ b/bapsflib/utils/helpers.py @@ -12,47 +12,7 @@ Helper functions for the :mod:`bapsflib` package. """ -__all__ = ['temperature_and_energy'] -import astropy.units as u - - -def temperature_and_energy(): - """ - Convert between Kelvin, Celsius, Fahrenheit, and eV. (An - `astropy.units.equivalencies`) - """ - # combine standard temperature and energy equivalencies - equiv = u.temperature() # type: list - equiv.extend(u.temperature_energy()) - - # construct functions for non-Kelvin to energy (and visa-versa) - # conversions - def convert_C_to_eV(x): - # convert Celsius to eV - x = (x * u.deg_C).to(u.K, equivalencies=equiv) - return x.to_value(u.eV, equivalencies=equiv) - - def convert_eV_to_C(x): - # convert eV to Celsius - x = (x * u.eV).to(u.K, equivalencies=equiv) - return x.to_value(u.deg_C, equivalencies=equiv) - - def convert_F_to_eV(x): - # convert Fahrenheit to eV - x = (x * u.imperial.deg_F).to(u.K, equivalencies=equiv) - return x.to_value(u.eV, equivalencies=equiv) - - def convert_eV_to_F(x): - # convert eV to Fahrenheit to eV - x = (x * u.eV).to(u.K, equivalencies=equiv) - return x.to_value(u.imperial.deg_F, equivalencies=equiv) - - # add new equivalencies to the complete list - equiv.append((u.deg_C, u.eV, convert_C_to_eV, convert_eV_to_C)) - equiv.append((u.imperial.deg_F, u.eV, - convert_F_to_eV, convert_eV_to_F)) - return equiv diff --git a/bapsflib/utils/units/__init__.py b/bapsflib/utils/units/__init__.py new file mode 100644 index 00000000..cb6b5457 --- /dev/null +++ b/bapsflib/utils/units/__init__.py @@ -0,0 +1,18 @@ +# This file is part of the bapsflib package, a Python toolkit for the +# BaPSF group at UCLA. +# +# http://plasma.physics.ucla.edu/ +# +# Copyright 2017-2019 Erik T. Everson and contributors +# +# License: Standard 3-clause BSD; see "LICENSES/LICENSE.txt" for full +# license terms and contributor agreement. +# +""" +Package to help facilitate the use of :mod:`astropy.units` in the +:mod:`bapsflib` package. +""" + +__all__ = ['temperature_and_energy'] + +from .equivalencies import temperature_and_energy diff --git a/bapsflib/utils/units/equivalencies.py b/bapsflib/utils/units/equivalencies.py new file mode 100644 index 00000000..ac4de5a9 --- /dev/null +++ b/bapsflib/utils/units/equivalencies.py @@ -0,0 +1,57 @@ +# This file is part of the bapsflib package, a Python toolkit for the +# BaPSF group at UCLA. +# +# http://plasma.physics.ucla.edu/ +# +# Copyright 2017-2019 Erik T. Everson and contributors +# +# License: Standard 3-clause BSD; see "LICENSES/LICENSE.txt" for full +# license terms and contributor agreement. +# +""" +:mod:`bapsflib` defined equivalencies for :mod:`astropy.units` +""" + +__all__ = ['temperature_and_energy'] + +import astropy.units as u + +from typing import (List, Tuple) + + +def temperature_and_energy(): + """ + Convert between Kelvin, Celsius, Fahrenheit, and eV. (An + :mod:`astropy.units.equivalencies`) + """ + # combine standard temperature and energy equivalencies + equiv = u.temperature() # type: List[Tuple] + equiv.extend(u.temperature_energy()) + + # construct functions for non-Kelvin to energy (and visa-versa) + # conversions + def convert_C_to_eV(x): + # convert Celsius to eV + x = (x * u.deg_C).to(u.K, equivalencies=equiv) + return x.to_value(u.eV, equivalencies=equiv) + + def convert_eV_to_C(x): + # convert eV to Celsius + x = (x * u.eV).to(u.K, equivalencies=equiv) + return x.to_value(u.deg_C, equivalencies=equiv) + + def convert_F_to_eV(x): + # convert Fahrenheit to eV + x = (x * u.imperial.deg_F).to(u.K, equivalencies=equiv) + return x.to_value(u.eV, equivalencies=equiv) + + def convert_eV_to_F(x): + # convert eV to Fahrenheit to eV + x = (x * u.eV).to(u.K, equivalencies=equiv) + return x.to_value(u.imperial.deg_F, equivalencies=equiv) + + # add new equivalencies to the complete list + equiv.append((u.deg_C, u.eV, convert_C_to_eV, convert_eV_to_C)) + equiv.append((u.imperial.deg_F, u.eV, + convert_F_to_eV, convert_eV_to_F)) + return equiv From ace5e54099aac7215ae9d0cf6eb47a1820514380 Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Thu, 9 May 2019 11:21:33 -0700 Subject: [PATCH 53/68] create decorators module in bapsflib.utils.units; move `check_quantity` and `check_relativistic` to new module; include in imports and __all__ for bapsflib.utils.units.__init__ and bapsflib.utils.__init__ --- bapsflib/utils/__init__.py | 5 +- bapsflib/utils/decorators.py | 388 +-------------------------- bapsflib/utils/units/__init__.py | 4 +- bapsflib/utils/units/decorators.py | 403 +++++++++++++++++++++++++++++ 4 files changed, 410 insertions(+), 390 deletions(-) create mode 100644 bapsflib/utils/units/decorators.py diff --git a/bapsflib/utils/__init__.py b/bapsflib/utils/__init__.py index f6493334..8d809a59 100644 --- a/bapsflib/utils/__init__.py +++ b/bapsflib/utils/__init__.py @@ -8,10 +8,11 @@ # License: Standard 3-clause BSD; see "LICENSES/LICENSE.txt" for full # license terms and contributor agreement. # -__all__ = ['BaPSFConstant', 'errors', 'temperature_and_energy', - 'warnings'] +__all__ = ['BaPSFConstant', 'check_quantity', 'check_relativistic', + 'errors', 'temperature_and_energy', 'warnings'] from . import (errors, warnings) +from .decorators import (check_quantity, check_relativistic) from .units import temperature_and_energy from astropy.constants import Constant diff --git a/bapsflib/utils/decorators.py b/bapsflib/utils/decorators.py index 31aebf10..115d423c 100644 --- a/bapsflib/utils/decorators.py +++ b/bapsflib/utils/decorators.py @@ -14,390 +14,4 @@ __all__ = ['check_quantity', 'check_relativistic'] -import astropy.units as u -import functools -import inspect -import numpy as np -import warnings - -from astropy.units import UnitsWarning -from plasmapy.utils import check_relativistic -from textwrap import dedent -from typing import (Dict, Union) - - -# this is modified from plasmapy.utils.checks.check_quantity -# TODO: replace with PlasmaPy version when PlasmaPy v0.2.0 is released -def check_quantity(**validations: Dict[str, Union[bool, u.Unit]]): - """ - Verify that the function's arguments have correct units. - This decorator raises an exception if an annotated argument in the - decorated function is an `~astropy.units.Quantity` with incorrect - units or of incorrect kind. You can prevent arguments from being - input as Nones, NaNs, negatives, infinities and complex numbers. - - If a number (non-Quantity) value is inserted in place of a value - with units, assume the input is an SI Quantity and cast it to one. - - This is probably best illustrated with an example: - - Examples - -------- - >>> from astropy import units as u - >>> @check_quantity(x={"units": u.m, - ... "can_be_negative": False, - ... "can_be_complex": True, - ... "can_be_inf": True}) - ... def func(x): - ... return x - - >>> func(1 * u.m) - - - >>> func(1 * u.s) - Traceback (most recent call last): - ... - astropy.units.core.UnitConversionError: The argument x to func should be a Quantity with the following units: m - - >>> import pytest # to show the UnitsWarning - >>> with pytest.warns(u.UnitsWarning, match="Assuming units of m."): - ... func(1) - - - >>> func(-1 * u.m) - Traceback (most recent call last): - ... - ValueError: The argument x to function func cannot contain negative numbers. - - >>> func(np.inf * u.m) - - - >>> func(None) - Traceback (most recent call last): - ... - ValueError: The argument x to function func cannot contain Nones. - - Parameters - ---------- - dict - Arguments to be validated passed in as keyword arguments, - with values as validation dictionaries, with structure as - in the example. Valid keys for each argument are: - 'units': `astropy.units.Unit`, - 'can_be_negative': `bool`, - 'can_be_complex': `bool`, - 'can_be_inf': `bool`, - 'can_be_nan': `bool`, - 'none_shall_pass': `bool` - - Raises - ------ - `TypeError` - If the argument is not a `~astropy.units.Quantity`, units is - not entirely units or `argname` does not have a type - annotation. - - `~astropy.units.UnitConversionError` - If the argument is not in acceptable units. - - `~astropy.units.UnitsError` - If after the assumption checks, the argument is still not in - acceptable units. - - `ValueError` - If the argument contains `~numpy.nan` or other invalid values - as determined by the keywords. - - Warns - ----- - `~astropy.units.UnitsWarning` - If a `~astropy.units.Quantity` is not provided and unique units - are provided, a `UnitsWarning` will be raised and the inputted - units will be assumed. - - Notes - ----- - This functionality may end up becoming deprecated due to - noncompliance with the `IEEE 754 standard - `_ - and in favor of `~astropy.units.quantity_input`. - - Returns - ------- - function - Decorated function. - - See also - -------- - _check_quantity - """ - def decorator(f): - wrapped_sign = inspect.signature(f) - fname = f.__name__ - - assigned = list(functools.WRAPPER_ASSIGNMENTS) - assigned.append('__signature__') - @functools.wraps(f, assigned=assigned) - def wrapper(*args, **kwargs): - # combine args and kwargs into dictionary - bound_args = wrapped_sign.bind(*args, **kwargs) - bound_args.apply_defaults() - given_params_values = bound_args.arguments - given_params = set(given_params_values.keys()) - - # names of params to check - validated_params = set(validations.keys()) - - missing_params = [ - param for param in (validated_params - given_params) - ] - - if len(missing_params) > 0: - params_str = ", ".join(missing_params) - raise TypeError( - f"Call to {fname} is missing " - f"validated params {params_str}") - - for param_to_check, validation_settings \ - in validations.items(): - value_to_check = given_params_values[param_to_check] - - equivalencies = \ - validation_settings.get('equivalencies', None) - can_be_negative = \ - validation_settings.get('can_be_negative', True) - can_be_complex = \ - validation_settings.get('can_be_complex', False) - can_be_inf = \ - validation_settings.get('can_be_inf', True) - can_be_nan = \ - validation_settings.get('can_be_nan', True) - none_shall_pass = \ - validation_settings.get('none_shall_pass', False) - - validated_value = _check_quantity( - value_to_check, - param_to_check, - fname, - validation_settings['units'], - equivalencies=equivalencies, - can_be_negative=can_be_negative, - can_be_complex=can_be_complex, - can_be_inf=can_be_inf, - can_be_nan=can_be_nan, - none_shall_pass=none_shall_pass, - ) - given_params_values[param_to_check] = validated_value - - return f(**given_params_values) - - if not hasattr(wrapper, '__signature__'): - wrapper.__signature__ = inspect.signature(f) - return wrapper - return decorator - - -# this is modified from plasmapy.utils.checks._check_quantity -# TODO: replace with PlasmaPy version when PlasmaPy v0.2.0 is released -def _check_quantity(arg, argname, funcname, units, - equivalencies=None, - can_be_negative=True, can_be_complex=False, - can_be_inf=True, can_be_nan=True, - none_shall_pass=False): - """ - Raise an exception if an object is not a `~astropy.units.Quantity` - with correct units and valid numerical values. - - Parameters - ---------- - arg : ~astropy.units.Quantity - The object to be tested. - - argname : str - The name of the argument to be printed in error messages. - - funcname : str - The name of the original function to be printed in error - messages. - - units : `~astropy.units.Unit` or list of `~astropy.unit.Unit` - Acceptable units for `arg`. - - can_be_negative : bool, optional - `True` if the `~astropy.units.Quantity` can be negative, - `False` otherwise. Defaults to `True`. - - can_be_complex : bool, optional - `True` if the `~astropy.units.Quantity` can be a complex number, - `False` otherwise. Defaults to `False`. - - can_be_inf : bool, optional - `True` if the `~astropy.units.Quantity` can contain infinite - values, `False` otherwise. Defaults to `True`. - - can_be_nan : bool, optional - `True` if the `~astropy.units.Quantity` can contain NaN - values, `False` otherwise. Defaults to `True`. - - none_shall_pass : bool, optional - `True` if the `~astropy.units.Quantity` can contain None - values, `False` otherwise. Defaults to `True`. - - Raises - ------ - TypeError - If the argument is not a `~astropy.units.Quantity` or units is - not entirely units. - - ~astropy.units.UnitConversionError - If the argument is not in acceptable units. - - ~astropy.units.UnitsError - If after the assumption checks, the argument is still not in - acceptable units. - - ValueError - If the argument contains any `~numpy.nan` or other invalid - values as determined by the keywords. - - Warns - ----- - ~astropy.units.UnitsWarning - If a `~astropy.units.Quantity` is not provided and unique units - are provided, a `UnitsWarning` will be raised and the inputted - units will be assumed. - - Examples - -------- - >>> from astropy import units as u - >>> import pytest - >>> _check_quantity(4*u.T, 'B', 'f', u.T) - - >>> with pytest.warns(u.UnitsWarning, match="No units are specified"): - ... assert _check_quantity(4, 'B', 'f', u.T) == 4 * u.T - """ - # condition units argument - if not isinstance(units, list): - units = [units] - for unit in units: - if not isinstance(unit, (u.Unit, u.CompositeUnit, - u.IrreducibleUnit)): - raise TypeError( - "The keyword 'units' to check_quantity must be " - "a unit or a list/tuple containing only units.") - - # condition equivalencies argument - if equivalencies is None: - equivalencies = [None] * len(units) - elif isinstance(equivalencies, list): - if all(isinstance(el, tuple) for el in equivalencies): - equivalencies = [equivalencies] - if len(equivalencies) == 1: - equivalencies = equivalencies * len(units) - elif len(equivalencies) != len(units): - raise ValueError( - f"The length of specified equivalencies " - f"({len(equivalencies)}) must be 1 or equal to the " - f"number of specified units ({len(units)})") - - # Create a generic error message - typeerror_message = ( - f"The argument {argname} to {funcname} should be a " - f"Quantity with " - ) - - if len(units) == 1: - typeerror_message += f"the following units: {str(units[0])}" - else: - typeerror_message += "one of the following units: " - for unit in units: - typeerror_message += str(unit) - if unit != units[-1]: - typeerror_message += ", " - if none_shall_pass: - typeerror_message += "or None " - - if isinstance(arg, (u.Unit, u.CompositeUnit, u.IrreducibleUnit)): - raise TypeError(typeerror_message) - - # Make sure arg is a quantity with correct units - - unit_casting_warning = dedent( - f"""No units are specified for {argname} = {arg} in - {funcname}. Assuming units of {str(units[0])}. - To silence this warning, explicitly pass in an Astropy - Quantity (from astropy.units) - (see http://docs.astropy.org/en/stable/units/)""") - - # TODO include explicit note on how to pass in Astropy Quantity - - # initialize error string - valueerror_message = ( - f"The argument {argname} to function {funcname} cannot contain" - ) - - # ensure arg is astropy.units.Quantity or return None (if allowed) - if arg is None and none_shall_pass: - return arg - elif arg is None: - raise ValueError(f"{valueerror_message} Nones.") - elif not isinstance(arg, u.Quantity): - if len(units) != 1: - raise TypeError(typeerror_message) - else: - try: - arg = arg * units[0] - except (u.UnitsError, ValueError): - raise TypeError(typeerror_message) - else: - warnings.warn(UnitsWarning(unit_casting_warning)) - - # check arg was converted to an astropy.units.Quantity - if not isinstance(arg, u.Quantity): - raise u.UnitsError( - "{} is still not a Quantity after checks!".format(arg)) - - in_acceptable_units = [] - - for unit, equiv in zip(units, equivalencies): - try: - arg.unit.to(unit, equivalencies=equiv) - except u.UnitConversionError: - in_acceptable_units.append(False) - else: - in_acceptable_units.append(True) - - if np.count_nonzero(in_acceptable_units) != 1: - raise u.UnitConversionError(typeerror_message) - else: - unit = np.array(units)[in_acceptable_units][0] - equiv = np.array(equivalencies)[in_acceptable_units][0] - arg = arg.to(unit, equivalencies=equiv) - ''' - for unit in units: - try: - arg.unit.to(unit, equivalencies=u.temperature_energy()) - except u.UnitConversionError: - in_acceptable_units.append(False) - else: - in_acceptable_units.append(True) - - if not np.any(in_acceptable_units): - raise u.UnitConversionError(typeerror_message) - ''' - - # Make sure that the quantity has valid numerical values - if np.any(np.isnan(arg.value)) and not can_be_nan: - raise ValueError(f"{valueerror_message} NaNs.") - elif np.any(np.iscomplex(arg.value)) and not can_be_complex: - raise ValueError(f"{valueerror_message} complex numbers.") - elif not can_be_negative: - # Allow NaNs through without raising a warning - with np.errstate(invalid='ignore'): - isneg = np.any(arg.value < 0) - if isneg: - raise ValueError(f"{valueerror_message} negative numbers.") - elif not can_be_inf and np.any(np.isinf(arg.value)): - raise ValueError(f"{valueerror_message} infs.") - - return arg +from .units import (check_quantity, check_relativistic) diff --git a/bapsflib/utils/units/__init__.py b/bapsflib/utils/units/__init__.py index cb6b5457..62478751 100644 --- a/bapsflib/utils/units/__init__.py +++ b/bapsflib/utils/units/__init__.py @@ -13,6 +13,8 @@ :mod:`bapsflib` package. """ -__all__ = ['temperature_and_energy'] +__all__ = ['check_quantity', 'check_relativistic', + 'temperature_and_energy'] +from .decorators import (check_quantity, check_relativistic) from .equivalencies import temperature_and_energy diff --git a/bapsflib/utils/units/decorators.py b/bapsflib/utils/units/decorators.py new file mode 100644 index 00000000..4a4a5f42 --- /dev/null +++ b/bapsflib/utils/units/decorators.py @@ -0,0 +1,403 @@ +# This file is part of the bapsflib package, a Python toolkit for the +# BaPSF group at UCLA. +# +# http://plasma.physics.ucla.edu/ +# +# Copyright 2017-2018 Erik T. Everson and contributors +# +# License: Standard 3-clause BSD; see "LICENSES/LICENSE.txt" for full +# license terms and contributor agreement. +# +""" +Decorators associated with assisting in the usage of +:mod:`astropy.units`. +""" +__all__ = ['check_quantity', 'check_relativistic'] + +import astropy.units as u +import functools +import inspect +import numpy as np +import warnings + +from astropy.units import UnitsWarning +from plasmapy.utils import check_relativistic +from textwrap import dedent +from typing import (Dict, Union) + + +# this is modified from plasmapy.utils.checks.check_quantity +# TODO: replace with PlasmaPy version when PlasmaPy v0.2.0 is released +def check_quantity(**validations: Dict[str, Union[bool, u.Unit]]): + """ + Verify that the function's arguments have correct units. + This decorator raises an exception if an annotated argument in the + decorated function is an `~astropy.units.Quantity` with incorrect + units or of incorrect kind. You can prevent arguments from being + input as Nones, NaNs, negatives, infinities and complex numbers. + + If a number (non-Quantity) value is inserted in place of a value + with units, assume the input is an SI Quantity and cast it to one. + + This is probably best illustrated with an example: + + Examples + -------- + >>> from astropy import units as u + >>> @check_quantity(x={"units": u.m, + ... "can_be_negative": False, + ... "can_be_complex": True, + ... "can_be_inf": True}) + ... def func(x): + ... return x + + >>> func(1 * u.m) + + + >>> func(1 * u.s) + Traceback (most recent call last): + ... + astropy.units.core.UnitConversionError: The argument x to func should be a Quantity with the following units: m + + >>> import pytest # to show the UnitsWarning + >>> with pytest.warns(u.UnitsWarning, match="Assuming units of m."): + ... func(1) + + + >>> func(-1 * u.m) + Traceback (most recent call last): + ... + ValueError: The argument x to function func cannot contain negative numbers. + + >>> func(np.inf * u.m) + + + >>> func(None) + Traceback (most recent call last): + ... + ValueError: The argument x to function func cannot contain Nones. + + Parameters + ---------- + dict + Arguments to be validated passed in as keyword arguments, + with values as validation dictionaries, with structure as + in the example. Valid keys for each argument are: + 'units': `astropy.units.Unit`, + 'can_be_negative': `bool`, + 'can_be_complex': `bool`, + 'can_be_inf': `bool`, + 'can_be_nan': `bool`, + 'none_shall_pass': `bool` + + Raises + ------ + `TypeError` + If the argument is not a `~astropy.units.Quantity`, units is + not entirely units or `argname` does not have a type + annotation. + + `~astropy.units.UnitConversionError` + If the argument is not in acceptable units. + + `~astropy.units.UnitsError` + If after the assumption checks, the argument is still not in + acceptable units. + + `ValueError` + If the argument contains `~numpy.nan` or other invalid values + as determined by the keywords. + + Warns + ----- + `~astropy.units.UnitsWarning` + If a `~astropy.units.Quantity` is not provided and unique units + are provided, a `UnitsWarning` will be raised and the inputted + units will be assumed. + + Notes + ----- + This functionality may end up becoming deprecated due to + noncompliance with the `IEEE 754 standard + `_ + and in favor of `~astropy.units.quantity_input`. + + Returns + ------- + function + Decorated function. + + See also + -------- + _check_quantity + """ + def decorator(f): + wrapped_sign = inspect.signature(f) + fname = f.__name__ + + assigned = list(functools.WRAPPER_ASSIGNMENTS) + assigned.append('__signature__') + @functools.wraps(f, assigned=assigned) + def wrapper(*args, **kwargs): + # combine args and kwargs into dictionary + bound_args = wrapped_sign.bind(*args, **kwargs) + bound_args.apply_defaults() + given_params_values = bound_args.arguments + given_params = set(given_params_values.keys()) + + # names of params to check + validated_params = set(validations.keys()) + + missing_params = [ + param for param in (validated_params - given_params) + ] + + if len(missing_params) > 0: + params_str = ", ".join(missing_params) + raise TypeError( + f"Call to {fname} is missing " + f"validated params {params_str}") + + for param_to_check, validation_settings \ + in validations.items(): + value_to_check = given_params_values[param_to_check] + + equivalencies = \ + validation_settings.get('equivalencies', None) + can_be_negative = \ + validation_settings.get('can_be_negative', True) + can_be_complex = \ + validation_settings.get('can_be_complex', False) + can_be_inf = \ + validation_settings.get('can_be_inf', True) + can_be_nan = \ + validation_settings.get('can_be_nan', True) + none_shall_pass = \ + validation_settings.get('none_shall_pass', False) + + validated_value = _check_quantity( + value_to_check, + param_to_check, + fname, + validation_settings['units'], + equivalencies=equivalencies, + can_be_negative=can_be_negative, + can_be_complex=can_be_complex, + can_be_inf=can_be_inf, + can_be_nan=can_be_nan, + none_shall_pass=none_shall_pass, + ) + given_params_values[param_to_check] = validated_value + + return f(**given_params_values) + + if not hasattr(wrapper, '__signature__'): + wrapper.__signature__ = inspect.signature(f) + return wrapper + return decorator + + +# this is modified from plasmapy.utils.checks._check_quantity +# TODO: replace with PlasmaPy version when PlasmaPy v0.2.0 is released +def _check_quantity(arg, argname, funcname, units, + equivalencies=None, + can_be_negative=True, can_be_complex=False, + can_be_inf=True, can_be_nan=True, + none_shall_pass=False): + """ + Raise an exception if an object is not a `~astropy.units.Quantity` + with correct units and valid numerical values. + + Parameters + ---------- + arg : ~astropy.units.Quantity + The object to be tested. + + argname : str + The name of the argument to be printed in error messages. + + funcname : str + The name of the original function to be printed in error + messages. + + units : `~astropy.units.Unit` or list of `~astropy.unit.Unit` + Acceptable units for `arg`. + + can_be_negative : bool, optional + `True` if the `~astropy.units.Quantity` can be negative, + `False` otherwise. Defaults to `True`. + + can_be_complex : bool, optional + `True` if the `~astropy.units.Quantity` can be a complex number, + `False` otherwise. Defaults to `False`. + + can_be_inf : bool, optional + `True` if the `~astropy.units.Quantity` can contain infinite + values, `False` otherwise. Defaults to `True`. + + can_be_nan : bool, optional + `True` if the `~astropy.units.Quantity` can contain NaN + values, `False` otherwise. Defaults to `True`. + + none_shall_pass : bool, optional + `True` if the `~astropy.units.Quantity` can contain None + values, `False` otherwise. Defaults to `True`. + + Raises + ------ + TypeError + If the argument is not a `~astropy.units.Quantity` or units is + not entirely units. + + ~astropy.units.UnitConversionError + If the argument is not in acceptable units. + + ~astropy.units.UnitsError + If after the assumption checks, the argument is still not in + acceptable units. + + ValueError + If the argument contains any `~numpy.nan` or other invalid + values as determined by the keywords. + + Warns + ----- + ~astropy.units.UnitsWarning + If a `~astropy.units.Quantity` is not provided and unique units + are provided, a `UnitsWarning` will be raised and the inputted + units will be assumed. + + Examples + -------- + >>> from astropy import units as u + >>> import pytest + >>> _check_quantity(4*u.T, 'B', 'f', u.T) + + >>> with pytest.warns(u.UnitsWarning, match="No units are specified"): + ... assert _check_quantity(4, 'B', 'f', u.T) == 4 * u.T + """ + # condition units argument + if not isinstance(units, list): + units = [units] + for unit in units: + if not isinstance(unit, (u.Unit, u.CompositeUnit, + u.IrreducibleUnit)): + raise TypeError( + "The keyword 'units' to check_quantity must be " + "a unit or a list/tuple containing only units.") + + # condition equivalencies argument + if equivalencies is None: + equivalencies = [None] * len(units) + elif isinstance(equivalencies, list): + if all(isinstance(el, tuple) for el in equivalencies): + equivalencies = [equivalencies] + if len(equivalencies) == 1: + equivalencies = equivalencies * len(units) + elif len(equivalencies) != len(units): + raise ValueError( + f"The length of specified equivalencies " + f"({len(equivalencies)}) must be 1 or equal to the " + f"number of specified units ({len(units)})") + + # Create a generic error message + typeerror_message = ( + f"The argument {argname} to {funcname} should be a " + f"Quantity with " + ) + + if len(units) == 1: + typeerror_message += f"the following units: {str(units[0])}" + else: + typeerror_message += "one of the following units: " + for unit in units: + typeerror_message += str(unit) + if unit != units[-1]: + typeerror_message += ", " + if none_shall_pass: + typeerror_message += "or None " + + if isinstance(arg, (u.Unit, u.CompositeUnit, u.IrreducibleUnit)): + raise TypeError(typeerror_message) + + # Make sure arg is a quantity with correct units + + unit_casting_warning = dedent( + f"""No units are specified for {argname} = {arg} in + {funcname}. Assuming units of {str(units[0])}. + To silence this warning, explicitly pass in an Astropy + Quantity (from astropy.units) + (see http://docs.astropy.org/en/stable/units/)""") + + # TODO include explicit note on how to pass in Astropy Quantity + + # initialize error string + valueerror_message = ( + f"The argument {argname} to function {funcname} cannot contain" + ) + + # ensure arg is astropy.units.Quantity or return None (if allowed) + if arg is None and none_shall_pass: + return arg + elif arg is None: + raise ValueError(f"{valueerror_message} Nones.") + elif not isinstance(arg, u.Quantity): + if len(units) != 1: + raise TypeError(typeerror_message) + else: + try: + arg = arg * units[0] + except (u.UnitsError, ValueError): + raise TypeError(typeerror_message) + else: + warnings.warn(UnitsWarning(unit_casting_warning)) + + # check arg was converted to an astropy.units.Quantity + if not isinstance(arg, u.Quantity): + raise u.UnitsError( + "{} is still not a Quantity after checks!".format(arg)) + + in_acceptable_units = [] + + for unit, equiv in zip(units, equivalencies): + try: + arg.unit.to(unit, equivalencies=equiv) + except u.UnitConversionError: + in_acceptable_units.append(False) + else: + in_acceptable_units.append(True) + + if np.count_nonzero(in_acceptable_units) != 1: + raise u.UnitConversionError(typeerror_message) + else: + unit = np.array(units)[in_acceptable_units][0] + equiv = np.array(equivalencies)[in_acceptable_units][0] + arg = arg.to(unit, equivalencies=equiv) + ''' + for unit in units: + try: + arg.unit.to(unit, equivalencies=u.temperature_energy()) + except u.UnitConversionError: + in_acceptable_units.append(False) + else: + in_acceptable_units.append(True) + + if not np.any(in_acceptable_units): + raise u.UnitConversionError(typeerror_message) + ''' + + # Make sure that the quantity has valid numerical values + if np.any(np.isnan(arg.value)) and not can_be_nan: + raise ValueError(f"{valueerror_message} NaNs.") + elif np.any(np.iscomplex(arg.value)) and not can_be_complex: + raise ValueError(f"{valueerror_message} complex numbers.") + elif not can_be_negative: + # Allow NaNs through without raising a warning + with np.errstate(invalid='ignore'): + isneg = np.any(arg.value < 0) + if isneg: + raise ValueError(f"{valueerror_message} negative numbers.") + elif not can_be_inf and np.any(np.isinf(arg.value)): + raise ValueError(f"{valueerror_message} infs.") + + return arg From 32e4e4baf258d50c16848473911e56c7ec10f5bd Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Thu, 9 May 2019 11:21:50 -0700 Subject: [PATCH 54/68] remove excess lines (PEP 8) --- bapsflib/utils/helpers.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/bapsflib/utils/helpers.py b/bapsflib/utils/helpers.py index 1f5ce26a..fb0e658c 100644 --- a/bapsflib/utils/helpers.py +++ b/bapsflib/utils/helpers.py @@ -11,8 +11,3 @@ """ Helper functions for the :mod:`bapsflib` package. """ - - - - - From 4124c8f0483abf0e173105ed416eadd146de808b Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Thu, 9 May 2019 13:45:07 -0700 Subject: [PATCH 55/68] improve typing annotations; clarify wording of docstrings; incorporate an enfoce kwarg to foce unit conversion or just check equivalency --- bapsflib/utils/units/decorators.py | 100 +++++++++++++++++------------ 1 file changed, 59 insertions(+), 41 deletions(-) diff --git a/bapsflib/utils/units/decorators.py b/bapsflib/utils/units/decorators.py index 4a4a5f42..78c4eea4 100644 --- a/bapsflib/utils/units/decorators.py +++ b/bapsflib/utils/units/decorators.py @@ -23,21 +23,24 @@ from astropy.units import UnitsWarning from plasmapy.utils import check_relativistic from textwrap import dedent -from typing import (Dict, Union) +from typing import (Dict, List, Union) # this is modified from plasmapy.utils.checks.check_quantity # TODO: replace with PlasmaPy version when PlasmaPy v0.2.0 is released -def check_quantity(**validations: Dict[str, Union[bool, u.Unit]]): +def check_quantity(**validations: + Dict[str, Union[bool, List, None, u.Unit]]): """ Verify that the function's arguments have correct units. This decorator raises an exception if an annotated argument in the - decorated function is an `~astropy.units.Quantity` with incorrect - units or of incorrect kind. You can prevent arguments from being - input as Nones, NaNs, negatives, infinities and complex numbers. + decorated function is an :class:`~astropy.units.Quantity` + with incorrect units or of incorrect kind. You can prevent + arguments from being input as Nones, NaNs, negatives, + infinities, and complex numbers. If a number (non-Quantity) value is inserted in place of a value - with units, assume the input is an SI Quantity and cast it to one. + with units, then coerce it to the expected unit (if only one unit + type is expected) or raise a TypeError. This is probably best illustrated with an example: @@ -83,7 +86,9 @@ def check_quantity(**validations: Dict[str, Union[bool, u.Unit]]): Arguments to be validated passed in as keyword arguments, with values as validation dictionaries, with structure as in the example. Valid keys for each argument are: - 'units': `astropy.units.Unit`, + 'units': :class:`astropy.units.Unit`, + 'equivalencies': :mod:`astropy.units.equivalencies`, + 'enforce': `bool`, 'can_be_negative': `bool`, 'can_be_complex': `bool`, 'can_be_inf': `bool`, @@ -92,28 +97,28 @@ def check_quantity(**validations: Dict[str, Union[bool, u.Unit]]): Raises ------ - `TypeError` - If the argument is not a `~astropy.units.Quantity`, units is - not entirely units or `argname` does not have a type + :class:`TypeError` + If the argument is not a :class:`~astropy.units.Quantity`, + units is not entirely units or `argname` does not have a type annotation. - `~astropy.units.UnitConversionError` + :class:`~astropy.units.UnitConversionError` If the argument is not in acceptable units. - `~astropy.units.UnitsError` + :class:`~astropy.units.UnitsError` If after the assumption checks, the argument is still not in acceptable units. - `ValueError` - If the argument contains `~numpy.nan` or other invalid values - as determined by the keywords. + :class:`ValueError` + If the argument contains :attr:`~numpy.nan` or other invalid + values as determined by the keywords. Warns ----- - `~astropy.units.UnitsWarning` - If a `~astropy.units.Quantity` is not provided and unique units - are provided, a `UnitsWarning` will be raised and the inputted - units will be assumed. + :class:`~astropy.units.UnitsWarning` + If a :class:`~astropy.units.Quantity` is not provided and + unique units are provided, a `UnitsWarning` will be raised + and the inputted units will be assumed. Notes ----- @@ -129,12 +134,14 @@ def check_quantity(**validations: Dict[str, Union[bool, u.Unit]]): See also -------- - _check_quantity + :func:`_check_quantity` """ def decorator(f): wrapped_sign = inspect.signature(f) fname = f.__name__ + # add '__signature__' to methods that are copied from + # wrapped_function onto wrapper assigned = list(functools.WRAPPER_ASSIGNMENTS) assigned.append('__signature__') @functools.wraps(f, assigned=assigned) @@ -191,8 +198,11 @@ def wrapper(*args, **kwargs): return f(**given_params_values) + # add '__signature__' if it does not exist + # - this will preserve parameter hints in IDE's if not hasattr(wrapper, '__signature__'): wrapper.__signature__ = inspect.signature(f) + return wrapper return decorator @@ -201,26 +211,38 @@ def wrapper(*args, **kwargs): # TODO: replace with PlasmaPy version when PlasmaPy v0.2.0 is released def _check_quantity(arg, argname, funcname, units, equivalencies=None, + enforce=True, can_be_negative=True, can_be_complex=False, can_be_inf=True, can_be_nan=True, none_shall_pass=False): """ - Raise an exception if an object is not a `~astropy.units.Quantity` - with correct units and valid numerical values. + To be used with decorator :func:`check_quantity`. + + Raise an exception if an object is not a + :class:`~astropy.units.Quantity` with correct units and + valid numerical values. Parameters ---------- - arg : ~astropy.units.Quantity + arg : :class:`~astropy.units.Quantity` The object to be tested. argname : str - The name of the argument to be printed in error messages. + The name of the variable passed in as arg. + (used for error messages) funcname : str - The name of the original function to be printed in error - messages. + The name of the decorated function. (used for error messages) + + equivalencies : Union[None, List[Tuple]] + An :mod:`astropy.units.equivalencies` used to help with + unit conversion. - units : `~astropy.units.Unit` or list of `~astropy.unit.Unit` + enforce : bool + (:code:`True` DEFAULT) Force the input :data:`arg` to be + converted into the desired :data:`units` + + units : :class:`~astropy.units.Unit` or list of :class:`~astropy.unit.Unit` Acceptable units for `arg`. can_be_negative : bool, optional @@ -367,24 +389,20 @@ def _check_quantity(arg, argname, funcname, units, else: in_acceptable_units.append(True) - if np.count_nonzero(in_acceptable_units) != 1: + nacceptable = np.count_nonzero(in_acceptable_units) + if nacceptable == 0: + # NO equivalent units raise u.UnitConversionError(typeerror_message) - else: + elif nacceptable == 1 and enforce: + # convert to desired unit unit = np.array(units)[in_acceptable_units][0] equiv = np.array(equivalencies)[in_acceptable_units][0] arg = arg.to(unit, equivalencies=equiv) - ''' - for unit in units: - try: - arg.unit.to(unit, equivalencies=u.temperature_energy()) - except u.UnitConversionError: - in_acceptable_units.append(False) - else: - in_acceptable_units.append(True) - - if not np.any(in_acceptable_units): - raise u.UnitConversionError(typeerror_message) - ''' + elif nacceptable >= 1 and enforce: + # too many equivalent units + raise u.UnitConversionError( + "arg is equivalent to too units and can not be " + "coerced into one") # Make sure that the quantity has valid numerical values if np.any(np.isnan(arg.value)) and not can_be_nan: From 8ccf787693d6d469cd19d03aae58ea8c82d63404 Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Thu, 9 May 2019 16:20:37 -0700 Subject: [PATCH 56/68] for func `_check_quantity`...refactor argument `arg` -> `val` and `argname` -> `val_name`; improve descriptive comments; improve conditioning for `equivalencies` kwarg; streamline process of function and increase readability of function --- bapsflib/utils/units/decorators.py | 139 ++++++++++++++++------------- 1 file changed, 77 insertions(+), 62 deletions(-) diff --git a/bapsflib/utils/units/decorators.py b/bapsflib/utils/units/decorators.py index 78c4eea4..f7f11615 100644 --- a/bapsflib/utils/units/decorators.py +++ b/bapsflib/utils/units/decorators.py @@ -209,12 +209,17 @@ def wrapper(*args, **kwargs): # this is modified from plasmapy.utils.checks._check_quantity # TODO: replace with PlasmaPy version when PlasmaPy v0.2.0 is released -def _check_quantity(arg, argname, funcname, units, - equivalencies=None, - enforce=True, - can_be_negative=True, can_be_complex=False, - can_be_inf=True, can_be_nan=True, - none_shall_pass=False): +def _check_quantity(val: u.Quantity, + val_name: str, + funcname: str, + units: u.UnitBase, + equivalencies: Union[List, None] = None, + enforce: bool = True, + can_be_negative: bool = True, + can_be_complex: bool = False, + can_be_inf: bool = True, + can_be_nan: bool = True, + none_shall_pass: bool = False): """ To be used with decorator :func:`check_quantity`. @@ -224,11 +229,11 @@ def _check_quantity(arg, argname, funcname, units, Parameters ---------- - arg : :class:`~astropy.units.Quantity` + val : :class:`~astropy.units.Quantity` The object to be tested. - argname : str - The name of the variable passed in as arg. + val_name: str + The name of the variable passed in as :data:`val`. (used for error messages) funcname : str @@ -239,11 +244,11 @@ def _check_quantity(arg, argname, funcname, units, unit conversion. enforce : bool - (:code:`True` DEFAULT) Force the input :data:`arg` to be + (:code:`True` DEFAULT) Force the input :data:`val` to be converted into the desired :data:`units` units : :class:`~astropy.units.Unit` or list of :class:`~astropy.unit.Unit` - Acceptable units for `arg`. + Acceptable units for :data:`val`. can_be_negative : bool, optional `True` if the `~astropy.units.Quantity` can be negative, @@ -298,36 +303,53 @@ def _check_quantity(arg, argname, funcname, units, >>> with pytest.warns(u.UnitsWarning, match="No units are specified"): ... assert _check_quantity(4, 'B', 'f', u.T) == 4 * u.T """ - # condition units argument + # -- condition `units` argument -- if not isinstance(units, list): units = [units] for unit in units: - if not isinstance(unit, (u.Unit, u.CompositeUnit, - u.IrreducibleUnit)): + if not isinstance(unit, + (u.Unit, u.CompositeUnit, u.IrreducibleUnit)): raise TypeError( - "The keyword 'units' to check_quantity must be " - "a unit or a list/tuple containing only units.") + "The `units` arg must be a list of astropy units." + ) - # condition equivalencies argument + # -- condition `equivalencies` keyword -- if equivalencies is None: equivalencies = [None] * len(units) elif isinstance(equivalencies, list): if all(isinstance(el, tuple) for el in equivalencies): equivalencies = [equivalencies] + + # ensure passed equivalencies list is structured properly + # [[(), ...], ...] + for equivs in equivalencies: + for equiv in equivs: + err_str = ( + "All equivalencies must be a list of 4 element " + "tuples structured like (unit1, unit2, " + "func_unit1_to_unit2, func_unit2_to_unit1)" + ) + if not isinstance(equiv, tuple): + raise TypeError(err_str) + elif len(equiv) != 4: + raise TypeError(err_str) + + # ensure number of equivalencies lists match the number of + # equivalent units to check if len(equivalencies) == 1: equivalencies = equivalencies * len(units) elif len(equivalencies) != len(units): raise ValueError( - f"The length of specified equivalencies " + f"The length of the specified equivalencies list " f"({len(equivalencies)}) must be 1 or equal to the " f"number of specified units ({len(units)})") - # Create a generic error message + # -- condition `val` argument -- + # create a TypeError message typeerror_message = ( - f"The argument {argname} to {funcname} should be a " + f"The argument {val_name} to {funcname} should be a " f"Quantity with " ) - if len(units) == 1: typeerror_message += f"the following units: {str(units[0])}" else: @@ -339,56 +361,49 @@ def _check_quantity(arg, argname, funcname, units, if none_shall_pass: typeerror_message += "or None " - if isinstance(arg, (u.Unit, u.CompositeUnit, u.IrreducibleUnit)): - raise TypeError(typeerror_message) - - # Make sure arg is a quantity with correct units - - unit_casting_warning = dedent( - f"""No units are specified for {argname} = {arg} in - {funcname}. Assuming units of {str(units[0])}. - To silence this warning, explicitly pass in an Astropy - Quantity (from astropy.units) - (see http://docs.astropy.org/en/stable/units/)""") - - # TODO include explicit note on how to pass in Astropy Quantity - - # initialize error string + # initialize a ValueError meassage valueerror_message = ( - f"The argument {argname} to function {funcname} cannot contain" + f"The argument {val_name} to function {funcname} can " + f"not contain" ) - # ensure arg is astropy.units.Quantity or return None (if allowed) - if arg is None and none_shall_pass: - return arg - elif arg is None: + # ensure `val` is astropy.units.Quantity or return None (if allowed) + if val is None and none_shall_pass: + return val + elif val is None: raise ValueError(f"{valueerror_message} Nones.") - elif not isinstance(arg, u.Quantity): + elif not isinstance(val, u.Quantity): if len(units) != 1: raise TypeError(typeerror_message) else: try: - arg = arg * units[0] - except (u.UnitsError, ValueError): + val = val * units[0] + except TypeError: raise TypeError(typeerror_message) else: - warnings.warn(UnitsWarning(unit_casting_warning)) - - # check arg was converted to an astropy.units.Quantity - if not isinstance(arg, u.Quantity): - raise u.UnitsError( - "{} is still not a Quantity after checks!".format(arg)) - + if not isinstance(val, u.Quantity): + raise TypeError(typeerror_message) + + warnings.warn(UnitsWarning( + f"No units are specified for {val_name} = {val} " + f"in {funcname}. Assuming units of " + f"{str(units[0])}. To silence this warning, " + f"explicitly pass in an Astropy Quantity " + f"(e.g. 5. * astropy.units.cm) " + f"(see http://docs.astropy.org/en/stable/units/)" + )) + + # Look for unit equivalencies for `val` in_acceptable_units = [] - for unit, equiv in zip(units, equivalencies): try: - arg.unit.to(unit, equivalencies=equiv) + val.unit.to(unit, equivalencies=equiv) except u.UnitConversionError: in_acceptable_units.append(False) else: in_acceptable_units.append(True) + # convert `val` to desired units if enforce=True nacceptable = np.count_nonzero(in_acceptable_units) if nacceptable == 0: # NO equivalent units @@ -397,25 +412,25 @@ def _check_quantity(arg, argname, funcname, units, # convert to desired unit unit = np.array(units)[in_acceptable_units][0] equiv = np.array(equivalencies)[in_acceptable_units][0] - arg = arg.to(unit, equivalencies=equiv) + val = val.to(unit, equivalencies=equiv) elif nacceptable >= 1 and enforce: # too many equivalent units raise u.UnitConversionError( - "arg is equivalent to too units and can not be " - "coerced into one") + "`val`'s units is equivalent to too many units in " + "`units`, `val` can not be coerced into one") - # Make sure that the quantity has valid numerical values - if np.any(np.isnan(arg.value)) and not can_be_nan: + # ensure `val` has valid numerical values + if np.any(np.isnan(val.value)) and not can_be_nan: raise ValueError(f"{valueerror_message} NaNs.") - elif np.any(np.iscomplex(arg.value)) and not can_be_complex: + elif np.any(np.iscomplex(val.value)) and not can_be_complex: raise ValueError(f"{valueerror_message} complex numbers.") elif not can_be_negative: # Allow NaNs through without raising a warning with np.errstate(invalid='ignore'): - isneg = np.any(arg.value < 0) + isneg = np.any(val.value < 0) if isneg: raise ValueError(f"{valueerror_message} negative numbers.") - elif not can_be_inf and np.any(np.isinf(arg.value)): + elif not can_be_inf and np.any(np.isinf(val.value)): raise ValueError(f"{valueerror_message} infs.") - return arg + return val From dd4dd63236c5afb774263f27bbf7ae4760a74c0a Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Thu, 9 May 2019 16:31:41 -0700 Subject: [PATCH 57/68] add a `pragma: no cover` for an if-statement that should never be reached --- bapsflib/utils/units/decorators.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bapsflib/utils/units/decorators.py b/bapsflib/utils/units/decorators.py index f7f11615..4fae4f83 100644 --- a/bapsflib/utils/units/decorators.py +++ b/bapsflib/utils/units/decorators.py @@ -381,7 +381,10 @@ def _check_quantity(val: u.Quantity, except TypeError: raise TypeError(typeerror_message) else: - if not isinstance(val, u.Quantity): + if not isinstance(val, u.Quantity): # pragma: no cover + # this should never be reached...if so, except + # is not setup correctly + # raise TypeError(typeerror_message) warnings.warn(UnitsWarning( From 69958b20cdd94020ba8d50d49bb414fa806d6371 Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Thu, 9 May 2019 17:33:04 -0700 Subject: [PATCH 58/68] add ValueError to the try-except for converting val to astropy.units.Quantity --- bapsflib/utils/units/decorators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bapsflib/utils/units/decorators.py b/bapsflib/utils/units/decorators.py index 4fae4f83..f813603c 100644 --- a/bapsflib/utils/units/decorators.py +++ b/bapsflib/utils/units/decorators.py @@ -378,7 +378,7 @@ def _check_quantity(val: u.Quantity, else: try: val = val * units[0] - except TypeError: + except (ValueError, TypeError): raise TypeError(typeerror_message) else: if not isinstance(val, u.Quantity): # pragma: no cover From 07fa1fd6cc3c23d12c5a3571cab112b670d2a227 Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Thu, 9 May 2019 17:33:58 -0700 Subject: [PATCH 59/68] replace np.iscomplex with np.iscomplexobj, iscomplex will return False if the imaginary component is zero --- bapsflib/utils/units/decorators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bapsflib/utils/units/decorators.py b/bapsflib/utils/units/decorators.py index f813603c..9b8d788e 100644 --- a/bapsflib/utils/units/decorators.py +++ b/bapsflib/utils/units/decorators.py @@ -425,7 +425,7 @@ def _check_quantity(val: u.Quantity, # ensure `val` has valid numerical values if np.any(np.isnan(val.value)) and not can_be_nan: raise ValueError(f"{valueerror_message} NaNs.") - elif np.any(np.iscomplex(val.value)) and not can_be_complex: + elif np.any(np.iscomplexobj(val.value)) and not can_be_complex: raise ValueError(f"{valueerror_message} complex numbers.") elif not can_be_negative: # Allow NaNs through without raising a warning From 3ffecea35a110e4ba43b71bf3d32e6c52b7ce0c7 Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Thu, 9 May 2019 19:52:54 -0700 Subject: [PATCH 60/68] improve type annotations --- bapsflib/utils/units/decorators.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bapsflib/utils/units/decorators.py b/bapsflib/utils/units/decorators.py index 9b8d788e..3718221a 100644 --- a/bapsflib/utils/units/decorators.py +++ b/bapsflib/utils/units/decorators.py @@ -209,17 +209,17 @@ def wrapper(*args, **kwargs): # this is modified from plasmapy.utils.checks._check_quantity # TODO: replace with PlasmaPy version when PlasmaPy v0.2.0 is released -def _check_quantity(val: u.Quantity, +def _check_quantity(val: Union[u.Quantity, None, float, np.ndarray], val_name: str, funcname: str, - units: u.UnitBase, + units: Union[u.UnitBase, List[u.UnitBase]], equivalencies: Union[List, None] = None, enforce: bool = True, can_be_negative: bool = True, can_be_complex: bool = False, can_be_inf: bool = True, can_be_nan: bool = True, - none_shall_pass: bool = False): + none_shall_pass: bool = False) -> u.Quantity: """ To be used with decorator :func:`check_quantity`. From 0b8b7795ecbb9cb059c289efa2f7dbc2eadc5101 Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Thu, 9 May 2019 19:53:56 -0700 Subject: [PATCH 61/68] allow for equivalencies elements that are tuples of length 2 --- bapsflib/utils/units/decorators.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bapsflib/utils/units/decorators.py b/bapsflib/utils/units/decorators.py index 3718221a..22ea6df5 100644 --- a/bapsflib/utils/units/decorators.py +++ b/bapsflib/utils/units/decorators.py @@ -325,13 +325,14 @@ def _check_quantity(val: Union[u.Quantity, None, float, np.ndarray], for equivs in equivalencies: for equiv in equivs: err_str = ( - "All equivalencies must be a list of 4 element " - "tuples structured like (unit1, unit2, " + "All equivalencies must be a list of 2 or 4 " + "element tuples structured like (unit1, unit2) " + "or (unit1, unit2, " "func_unit1_to_unit2, func_unit2_to_unit1)" ) if not isinstance(equiv, tuple): raise TypeError(err_str) - elif len(equiv) != 4: + elif len(equiv) not in (2, 4): raise TypeError(err_str) # ensure number of equivalencies lists match the number of From 41c4776c1a3abb1caee702fe39639c94737c9faf Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Thu, 9 May 2019 19:54:32 -0700 Subject: [PATCH 62/68] do not allow equivalencies are are not lists or None (raise ValueError) --- bapsflib/utils/units/decorators.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bapsflib/utils/units/decorators.py b/bapsflib/utils/units/decorators.py index 22ea6df5..5d882c8f 100644 --- a/bapsflib/utils/units/decorators.py +++ b/bapsflib/utils/units/decorators.py @@ -344,6 +344,10 @@ def _check_quantity(val: Union[u.Quantity, None, float, np.ndarray], f"The length of the specified equivalencies list " f"({len(equivalencies)}) must be 1 or equal to the " f"number of specified units ({len(units)})") + else: + raise ValueError( + f"The specified equivalencies {equivalencies} is not" + f"valid for astropy.unit.to()") # -- condition `val` argument -- # create a TypeError message From 10f1373bee9a744b6c3cdb7190fcc3358c777ac2 Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Thu, 9 May 2019 19:55:07 -0700 Subject: [PATCH 63/68] initial commit for test class TestCheckQuantity --- bapsflib/utils/units/tests/__init__.py | 10 + bapsflib/utils/units/tests/test_decorators.py | 297 ++++++++++++++++++ 2 files changed, 307 insertions(+) create mode 100644 bapsflib/utils/units/tests/__init__.py create mode 100644 bapsflib/utils/units/tests/test_decorators.py diff --git a/bapsflib/utils/units/tests/__init__.py b/bapsflib/utils/units/tests/__init__.py new file mode 100644 index 00000000..9ce148ee --- /dev/null +++ b/bapsflib/utils/units/tests/__init__.py @@ -0,0 +1,10 @@ +# This file is part of the bapsflib package, a Python toolkit for the +# BaPSF group at UCLA. +# +# http://plasma.physics.ucla.edu/ +# +# Copyright 2017-2019 Erik T. Everson and contributors +# +# License: Standard 3-clause BSD; see "LICENSES/LICENSE.txt" for full +# license terms and contributor agreement. +# diff --git a/bapsflib/utils/units/tests/test_decorators.py b/bapsflib/utils/units/tests/test_decorators.py new file mode 100644 index 00000000..369548ce --- /dev/null +++ b/bapsflib/utils/units/tests/test_decorators.py @@ -0,0 +1,297 @@ +# This file is part of the bapsflib package, a Python toolkit for the +# BaPSF group at UCLA. +# +# http://plasma.physics.ucla.edu/ +# +# Copyright 2017-2019 Erik T. Everson and contributors +# +# License: Standard 3-clause BSD; see "LICENSES/LICENSE.txt" for full +# license terms and contributor agreement. +# +import astropy.units as u +import inspect +import numpy as np +import unittest as ut + +from ..decorators import (_check_quantity, check_quantity) + + +class TestCheckQuantity(ut.TestCase): + """ + Test case for function :func:`_check_quantity` and decorator + :func:`check_quantity` + """ + # What to cover: + # 1. Errors raised + # * TypeError if all elements of the `units` arg is + # not and instance of u.Unit, u.CompositeUnit, or + # u.IrreducibleUnit + # * TypeError if equivalencies element is not a tuple + # * TypeError if equivalencies element is a tuple, but + # not length 4 + # * ValueError if len(equivalencies) != 1 and != len(units) + # * ValueError if val is None and none_shall_pass=False + # * ValueError if len(units) != 1 and `val` is not u.Quantity + # * TypeError if len(units) == 1 and `val` is not u.Quantity + # and `val` can not be cast into a u.Quantity + # * u.UnitConversionError if no unit equivalencies are found + # * u.UnitConversionError if multiple equivalences are found + # and enforce=True + # * ValueError if val is np.nan and can_be_nan=False + # * ValueError if val is complex and can_be_complex=False + # * ValueError if val is negative and can_be_negative=False + # * ValueError if val is inf and can_be_inf=False + # 2. UserWarnings + # * if len(units) == 1 and `val` is not u.Quantity and + # `val` can be cast into a u.Quantity + # + + def setUp(self) -> None: + super().setUp() + + def tearDown(self) -> None: + super().tearDown() + + def test__check_quantity_default(self): + """Test default values :func:`_check_quantity`.""" + sig = inspect.signature(_check_quantity) + + # _check_quantity is a function + self.assertTrue(inspect.isfunction(_check_quantity)) + + # check defaults for keywords + pairs = [ + ('equivalencies', None), + ('enforce', True), + ('can_be_negative', True), + ('can_be_complex', False), + ('can_be_inf', True), + ('can_be_nan', True), + ('none_shall_pass', False), + ] + for pair in pairs: + self.assertIn(pair[0], sig.parameters.keys()) + if pair[0] is None: + self.assertIsNone(sig.parameters[pair[0]].default) + else: + self.assertEqual(sig.parameters[pair[0]].default, + pair[1]) + + def test__check_quantity_errors(self): + """ + Test scenarios the cause :func:`_check_quantity` to raise + errors. + """ + # Errors to check: + # X 1. TypeError if all elements of the `units` arg is + # not and instance of u.Unit, u.CompositeUnit, or + # u.IrreducibleUnit + # X 2. TypeError if equivalencies element is not a tuple + # X 3. TypeError if equivalencies element is a tuple, but + # not length 4 + # X 4. ValueError if len(equivalencies) != 1 and != len(units) + # X 5. ValueError if val is None and none_shall_pass=False + # X 6. TypeError if len(units) != 1 and `val` is not + # u.Quantity + # X 7. TypeError if len(units) == 1 and `val` is not u.Quantity + # and `val` can not be cast into a u.Quantity + # X 8. u.UnitConversionError if no unit equivalencies are found + # X 9. u.UnitConversionError if multiple equivalences are found + # and enforce=True + # X 10. ValueError if val is np.nan and can_be_nan=False + # X 11. ValueError if val is complex and can_be_complex=False + # X 12. ValueError if val is negative and can_be_negative=False + # X 13. ValueError if val is inf and can_be_inf=False + # 14. ValueError if equivalencies is not a list or None + # + # TypeError if all elements of the `units` arg is (1) + # not and instance of u.Unit, u.CompositeUnit, or + # u.IrreducibleUnit + self.assertRaises(TypeError, _check_quantity, + 5. * u.cm, 'a', 'foo', 'not a unit') + self.assertRaises(TypeError, _check_quantity, + 5. * u.cm, 'a', 'foo', [u.cm, 'not a unit']) + + # TypeError if equivalencies element is not a tuple (2) + self.assertRaises(TypeError, _check_quantity, + 5. * u.cm, 'a', 'foo', u.cm, + equivalencies=[{'not a tuple': 1}]) + + # TypeError if equivalencies element is a tuple, but (3) + # not length 4 + self.assertRaises(TypeError, _check_quantity, + 5. * u.cm, 'a', 'foo', u.cm, + equivalencies=[(1, 2, 3, )]) + + # ValueError if len(equivalencies) != 1 and != len(units) (4) + self.assertRaises(ValueError, _check_quantity, + 5. * u.cm, 'a', 'foo', [u.cm, u.g, u.s], + equivalencies=[[(None, None, None, None)], + [(None, None, None, None)]]) + + # ValueError if val is None and none_shall_pass=False (5) + self.assertRaises(ValueError, _check_quantity, + None, 'a', 'foo', u.cm, none_shall_pass=False) + + # TypeError if len(units) != 1 and `val` is not (6) + # u.Quantity + self.assertRaises(TypeError, _check_quantity, + 5., 'a', 'foo', [u.cm, u.g]) + + # TypeError if len(units) == 1 and `val` is not (7) + # u.Quantity and `val` can not be cast into a u.Quantity + # + # 'five' * u.cm raise a ValueError caught by try-except + self.assertRaises(TypeError, _check_quantity, + 'five', 'a', 'foo', u.cm) + # {'five': 5} * u.cm raise a TypeError caught by try-except + self.assertRaises(TypeError, _check_quantity, + {'five': 5}, 'a', 'foo', u.cm) + + # u.UnitConversionError if no unit equivalencies are found (8) + self.assertRaises(u.UnitConversionError, _check_quantity, + 5. * u.cm, 'a', 'foo', u.g) + self.assertRaises(u.UnitConversionError, _check_quantity, + 5. * u.cm, 'a', 'foo', [u.g, u.s]) + + # u.UnitConversionError if multiple equivalences are (9) + # found and enforce=True + self.assertRaises(u.UnitConversionError, _check_quantity, + 5. * u.cm, 'a', 'foo', [u.cm, u.km, u.g], + enforce=True) + + # ValueError if val is np.nan and can_be_nan=False (10) + self.assertRaises(ValueError, _check_quantity, + np.nan * u.cm, 'a', 'foo', u.cm, + can_be_nan=False) + + # ValueError if val is complex and can_be_complex=False (11) + self.assertRaises(ValueError, _check_quantity, + np.complex(5.) * u.cm, 'a', 'foo', u.cm, + can_be_complex=False) + + # ValueError if val is negative and can_be_negative=False (12) + self.assertRaises(ValueError, _check_quantity, + -5. * u.g, 'a', 'foo', u.g, + can_be_negative=False) + + # ValueError if val is inf and can_be_inf = False (13) + self.assertRaises(ValueError, _check_quantity, + np.inf * u.cm, 'a', 'foo', u.cm, + can_be_inf=False) + + # ValueError if equivalencies is not a list or None (14) + self.assertRaises(ValueError, _check_quantity, + np.inf * u.cm, 'a', 'foo', u.cm, + equivalencies=('not', 'correct')) + + def test__check_quantity_behavior(self): + """Test behavior of :func:`_check_quantity` keywords.""" + # -- keyword `can_be_complex` -- + vals = [np.complex(5., 2.) * u.cm, + np.array([1, 4, 6], dtype=np.complex) * u.cm] + for val in vals: + check_val = _check_quantity(val, 'val', 'foo', u.m, + can_be_complex=True) + self.assertTrue( + np.all((val.value / 100.0) * u.m == check_val)) + self.assertTrue(np.all(np.iscomplexobj(check_val.value))) + + # -- keyword `can_be_inf` -- + vals = [np.inf * u.cm, + np.array([1, np.inf, 6], dtype=np.float16) * u.cm] + for val in vals: + check_val = _check_quantity(val, 'val', 'foo', u.m, + can_be_inf=True) + self.assertTrue( + np.all((val.value / 100.0) * u.m == check_val)) + self.assertEqual(np.count_nonzero(np.isinf(val)), + np.count_nonzero(np.isinf(check_val))) + + # -- keyword `can_be_nan` -- + vals = [np.nan * u.cm, + np.array([1, np.nan, 6], dtype=np.float16) * u.cm] + for val in vals: + check_val = _check_quantity(val, 'val', 'foo', u.m, + can_be_nan=True) + self.assertEqual(u.m, check_val.unit) + if isinstance(val, np.ndarray) and val.size != 1: + val_mask = np.isnan(val) + check_val_mask = np.isnan(check_val) + self.assertEqual(np.count_nonzero(val_mask), + np.count_nonzero(check_val_mask)) + self.assertTrue(np.array_equal( + val[np.logical_not(val_mask)] / 100.0, + check_val[np.logical_not(check_val_mask)] + )) + else: + self.assertTrue(np.isnan(check_val)) + + # -- keyword `can_be_negative` -- + vals = [-5. * u.cm, + np.array([-1., 3., -22.], dtype=np.float16) * u.cm] + for val in vals: + check_val = _check_quantity( + val, 'val', 'foo', u.m, can_be_negative=True) + self.assertTrue(np.array_equal( + (val.value / 100.0) * u.m, + check_val)) + + # val is positive and negative are not allowed + val = 5.0 * u.cm + check_val = _check_quantity( + val, 'val', 'foo', u.m, can_be_negative=False) + self.assertEqual((val.value / 100.0) * u.m, check_val) + + # -- keyword `enforce` -- + vals = [5. * u.cm, + np.array([1., 3., 22.], dtype=np.float16) * u.cm] + for val in vals: + check_val = _check_quantity( + val, 'val', 'foo', u.cm, enforce=True) + self.assertTrue(np.array_equal(val, check_val)) + + check_val = _check_quantity( + val, 'val', 'foo', u.km, enforce=True) + self.assertTrue(np.array_equal(val.to(u.km), check_val)) + + check_val = _check_quantity( + val, 'val', 'foo', u.km, enforce=False) + self.assertTrue(np.array_equal(val, check_val)) + + # -- keyword `equivalencies` -- + # equivalencies is one list but multiple units are specific + val = 5. * u.K + check_val = _check_quantity( + val, 'val', 'foo', [u.cm, u.deg_C], + equivalencies=u.temperature()) + self.assertEqual( + val.to(u.deg_C, equivalencies=u.temperature()), check_val) + + # number of equivalencies and units are equal but more that 1 + check_val = _check_quantity( + val, 'val', 'foo', [u.deg_C, u.g], + equivalencies=[u.temperature(), u.molar_mass_amu()]) + self.assertEqual( + val.to(u.deg_C, equivalencies=u.temperature()), check_val) + + # -- keyword `non_shall_pass` -- + val = None + check_val = _check_quantity( + val, 'val', 'foo', u.cm, none_shall_pass=True) + self.assertIsNone(check_val) + + def test__check_quantity_warnings(self): + """ + Test behavior that cause :func:`_check_quantity` to issue + a UserWarning + """ + # value does not have units and only one unit is specified + val = 5 + with self.assertWarns(u.UnitsWarning): + check_val = _check_quantity(val, 'val', 'foo', u.cm) + self.assertEqual(val * u.cm, check_val) + + +if __name__ == '__main__': + ut.main() From 2692368b0c922b813d840ce9678b98cfa20c0002 Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Thu, 9 May 2019 19:57:57 -0700 Subject: [PATCH 64/68] allow for pass-through of enforce from `check_quantity` to `_check_quantity` --- bapsflib/utils/units/decorators.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bapsflib/utils/units/decorators.py b/bapsflib/utils/units/decorators.py index 5d882c8f..790d8295 100644 --- a/bapsflib/utils/units/decorators.py +++ b/bapsflib/utils/units/decorators.py @@ -169,6 +169,8 @@ def wrapper(*args, **kwargs): in validations.items(): value_to_check = given_params_values[param_to_check] + enforce = \ + validation_settings.get('enforce', True) equivalencies = \ validation_settings.get('equivalencies', None) can_be_negative = \ @@ -187,6 +189,7 @@ def wrapper(*args, **kwargs): param_to_check, fname, validation_settings['units'], + enforce=enforce, equivalencies=equivalencies, can_be_negative=can_be_negative, can_be_complex=can_be_complex, From eb2657448d2e0bb01f104c86b0accf4c5142398e Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Fri, 10 May 2019 12:50:51 -0700 Subject: [PATCH 65/68] remove unused imports --- bapsflib/utils/units/decorators.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bapsflib/utils/units/decorators.py b/bapsflib/utils/units/decorators.py index 790d8295..afb5bf1f 100644 --- a/bapsflib/utils/units/decorators.py +++ b/bapsflib/utils/units/decorators.py @@ -22,7 +22,6 @@ from astropy.units import UnitsWarning from plasmapy.utils import check_relativistic -from textwrap import dedent from typing import (Dict, List, Union) From bedcdb29197c644ea01ed69ed72b7fc155ccdced Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Fri, 10 May 2019 12:51:54 -0700 Subject: [PATCH 66/68] define test method `test_check_quantity_decorator` to cover behavior of the `chec_quantity` decorator --- bapsflib/utils/units/tests/test_decorators.py | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/bapsflib/utils/units/tests/test_decorators.py b/bapsflib/utils/units/tests/test_decorators.py index 369548ce..20b9680e 100644 --- a/bapsflib/utils/units/tests/test_decorators.py +++ b/bapsflib/utils/units/tests/test_decorators.py @@ -13,6 +13,8 @@ import numpy as np import unittest as ut +from unittest import mock + from ..decorators import (_check_quantity, check_quantity) @@ -292,6 +294,74 @@ def test__check_quantity_warnings(self): check_val = _check_quantity(val, 'val', 'foo', u.cm) self.assertEqual(val * u.cm, check_val) + @mock.patch(_check_quantity.__module__ + '._check_quantity', + side_effect=_check_quantity, autospec=True) + def test_check_quantity_decorator(self, mock_cq): + + # create mock function (mock_func) and function to mock (foo) + def foo(x): + return x + mock_func = mock.Mock(side_effect=foo, name='mock_func', + autospec=True) + mock_func.__name__ = 'mock_func' + mock_func.__signature__ = inspect.signature(foo) + + # -- Examine various validations and pass-through -- + validations = [ + {'units': u.cm, + 'enforce': True, + 'equivalencies': None, + 'can_be_negative': True, + 'can_be_complex': False, + 'can_be_inf': True, + 'can_be_nan': True, + 'none_shall_pass': False}, + {'units': u.cm, + 'enforce': False, + 'equivalencies': u.temperature(), + 'can_be_negative': False, + 'can_be_complex': True, + 'can_be_inf': False, + 'can_be_nan': False, + 'none_shall_pass': True}, + ] + for validation in validations: + func = check_quantity(**{'x': validation})(mock_func) + self.assertTrue(hasattr(func, '__signature__')) + + val_in = 5. * u.km + val = func(val_in) + self.assertEqual(val, 500000. * u.cm) + self.assertTrue(mock_cq.called) + self.assertTrue(mock_func.called) + + # correct args passed to _check_quantity + self.assertEqual(len(mock_cq.call_args[0]), 4) + self.assertEqual(mock_cq.call_args[0][0], val_in) + self.assertEqual(mock_cq.call_args[0][1], 'x') + self.assertEqual(mock_cq.call_args[0][2], 'mock_func') + self.assertEqual(mock_cq.call_args[0][3], + validation['units']) + + # correct kwargs passed to _check_quantity + for key, val in validation.items(): + if key == 'units': + continue + self.assertIn(key, mock_cq.call_args[1]) + self.assertEqual(val, mock_cq.call_args[1][key]) + + # reset mocks + mock_cq.reset_mock() + mock_func.reset_mock() + + # -- TypeError if function is missing validation parameters -- + func = check_quantity(**{'y': {'units': u.cm}})(foo) + self.assertTrue(hasattr(func, '__signature__')) + self.assertRaises(TypeError, func, 5.*u.cm) + + + + if __name__ == '__main__': ut.main() From ea340fa5ea7672b1fadf0985a7c470a753014402 Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Fri, 10 May 2019 20:31:16 -0700 Subject: [PATCH 67/68] move __all__ to top of imports (PEP 8) --- bapsflib/plasma/constants.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/bapsflib/plasma/constants.py b/bapsflib/plasma/constants.py index 84093380..d82552e0 100644 --- a/bapsflib/plasma/constants.py +++ b/bapsflib/plasma/constants.py @@ -12,6 +12,15 @@ Constants are imported from :mod:`astropy.constants.codata2014`. """ +__all__ = ['c', + 'e', 'e_gauss', + 'eps0', + 'g0', + 'k_B', + 'm_e', 'm_n', 'm_p', 'u', + 'mu0', + 'pi'] + from astropy.constants.codata2014 import ( c, e, e_gauss, @@ -24,16 +33,6 @@ from bapsflib.utils import BaPSFConstant from numpy import pi - -__all__ = ['c', - 'e', 'e_gauss', - 'eps0', - 'g0', - 'k_B', - 'm_e', 'm_n', 'm_p', 'u', - 'mu0', - 'pi'] - # rename some attributes for clarity e = BaPSFConstant(**{ 'abbrev': 'e', From 68e30f7a115ffd3157fd766132d3bdc96fc7e152 Mon Sep 17 00:00:00 2001 From: rocco8773 Date: Fri, 14 Jun 2019 17:18:56 -0700 Subject: [PATCH 68/68] import `check_quantity` and `check_relativistic` --- bapsflib/utils/decorators.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bapsflib/utils/decorators.py b/bapsflib/utils/decorators.py index 96044b20..d4826ba0 100644 --- a/bapsflib/utils/decorators.py +++ b/bapsflib/utils/decorators.py @@ -19,6 +19,8 @@ from typing import Union +from .units.decorators import (check_quantity, check_relativistic) + def with_bf(wfunc=None, *, filename: Union[str, None] = None,