Repository: yt-project/unyt
Pull request #38 (2018-07-25 05:37:07)

Add Spatial frequency, with spectral equivalence support
I spoke to @ngoldbaum briefly at Scipy2018 about the spectral equivalence and the units my particular sector of spectroscopy uses. In particular we use wavenumbers  (1/cm) extensively.

This PR adds a spatial frequency dimension `1 / (length)`, and then makes the `spectral` equivalence able to convert to and from spatial_freqency units.

I considered adding `wn` as a unit (equivalent to `1/cm`), but concluded that would likely be better suited to my more specific packages. If the maintainers would like to add it, let me know, perfectly willing to submit such a PR.




Patch:
diff --git a/docs/usage.rst b/docs/usage.rst
index 4747fb9..57254a2 100644
--- a/docs/usage.rst
+++ b/docs/usage.rst
@@ -450,7 +450,8 @@ for the problem you are working on.
 The :mod:`unyt` library implements the following equivalencies:
 
 * "thermal": conversions between temperature and energy (:math:`E = k_BT`)
-* "spectral": conversions between wavelength, frequency, and energy for photons
+* "spectral": conversions between wavelength, spatial frequency, frequency, and
+  energy for photons
   (:math:`E = h\nu = hc/\lambda`, :math:`c = \lambda\nu`)
 * "mass_energy": conversions between mass and energy (:math:`E = mc^2`)
 * "lorentz": conversions between velocity and Lorentz factor
@@ -471,7 +472,7 @@ array, use the :meth:`unit_array.list_equivalencies
   schwarzschild: mass <-> length
   compton: mass <-> length
   >>> km.list_equivalencies()
-  spectral: length <-> frequency <-> energy
+  spectral: length <-> spatial_frequency <-> frequency <-> energy
   schwarzschild: mass <-> length
   compton: mass <-> length
 
diff --git a/unyt/array.py b/unyt/array.py
index 23fb31a..915a3ba 100644
--- a/unyt/array.py
+++ b/unyt/array.py
@@ -1022,7 +1022,7 @@ class unyt_array(np.ndarray):
         -------
         >>> from unyt import km
         >>> (1.0*km).list_equivalencies()
-        spectral: length <-> frequency <-> energy
+        spectral: length <-> spatial_frequency <-> frequency <-> energy
         schwarzschild: mass <-> length
         compton: mass <-> length
         """
diff --git a/unyt/dimensions.py b/unyt/dimensions.py
index a1d3665..24afccd 100644
--- a/unyt/dimensions.py
+++ b/unyt/dimensions.py
@@ -45,6 +45,9 @@ rate = 1 / time
 # frequency (alias for rate)
 frequency = rate
 
+# spatial frequency
+spatial_frequency = 1 / length
+
 #: solid_angle
 solid_angle = angle * angle
 #: velocity
@@ -135,7 +138,7 @@ derived_dimensions = [
     luminous_flux, area, current_cgs, charge_mks, electric_field_mks,
     magnetic_field_mks, electric_potential_cgs, electric_potential_mks,
     resistance_cgs, resistance_mks, magnetic_flux_mks, magnetic_flux_cgs,
-    luminance]
+    luminance, spatial_frequency]
 
 
 #: a list containing all dimensions
diff --git a/unyt/equivalencies.py b/unyt/equivalencies.py
index 8e0a26e..7a93b59 100644
--- a/unyt/equivalencies.py
+++ b/unyt/equivalencies.py
@@ -24,6 +24,7 @@ from unyt.dimensions import (
     density,
     number_density,
     flux,
+    spatial_frequency,
 )
 from unyt.exceptions import InvalidUnitEquivalence
 
@@ -182,9 +183,9 @@ class MassEnergyEquivalence(Equivalence):
 class SpectralEquivalence(Equivalence):
     """Equivalence between wavelength, frequency, and energy of a photon.
 
-    Given a photon with wavelength :math:`\\lambda`, frequency :math:`\\nu`
-    and Energy :math:`E`, these quantities are related by the following
-    forumlae:
+    Given a photon with wavelength :math:`\\lambda`, spatial frequency
+    :math:`\\bar\\nu`, frequency :math:`\\nu` and Energy :math:`E`,
+    these quantities are related by the following forumlae:
 
     .. math::
 
@@ -195,7 +196,7 @@ class SpectralEquivalence(Equivalence):
     Example
     ------
     >>> print(SpectralEquivalence())
-    spectral: length <-> frequency <-> energy
+    spectral: length <-> spatial_frequency <-> frequency <-> energy
     >>> from unyt import angstrom, km
     >>> (3*angstrom).to_equivalent('keV', 'spectral')
     unyt_quantity(4.13280644, 'keV')
@@ -203,7 +204,7 @@ class SpectralEquivalence(Equivalence):
     unyt_quantity(0.29979246, 'MHz')
     """
     type_name = "spectral"
-    _dims = (length, rate, energy,)
+    _dims = (length, rate, energy, spatial_frequency)
 
     def _convert(self, x, new_dims):
         from unyt import physical_constants as pc
@@ -212,19 +213,33 @@ class SpectralEquivalence(Equivalence):
                 return np.divide(pc.clight*pc.hmks, x, out=self._get_out(x))
             elif x.units.dimensions == rate:
                 return np.multiply(x, pc.hmks, out=self._get_out(x))
+            elif x.units.dimensions == spatial_frequency:
+                return np.multiply(x, pc.hmks*pc.clight, out=self._get_out(x))
         elif new_dims == length:
             if x.units.dimensions == rate:
                 return np.divide(pc.clight, x, out=self._get_out(x))
             elif x.units.dimensions == energy:
                 return np.divide(pc.hmks*pc.clight, x, out=self._get_out(x))
+            elif x.units.dimensions == spatial_frequency:
+                return np.divide(1, x, out=self._get_out(x))
         elif new_dims == rate:
             if x.units.dimensions == length:
                 return np.divide(pc.clight, x, out=self._get_out(x))
             elif x.units.dimensions == energy:
                 return np.divide(x, pc.hmks, out=self._get_out(x))
+            elif x.units.dimensions == spatial_frequency:
+                return np.multiply(x, pc.clight, out=self._get_out(x))
+        elif new_dims == spatial_frequency:
+            if x.units.dimensions == length:
+                return np.divide(1, x, out=self._get_out(x))
+            elif x.units.dimensions == energy:
+                return np.divide(x, pc.clight*pc.hmks, out=self._get_out(x))
+            elif x.units.dimensions == rate:
+                return np.divide(x, pc.clight, out=self._get_out(x))
 
     def __str__(self):
-        return "spectral: length <-> frequency <-> energy"
+        return ("spectral: length <-> spatial_frequency <-> frequency "
+                + "<-> energy")
 
 
 class SoundSpeedEquivalence(Equivalence):
diff --git a/unyt/unit_object.py b/unyt/unit_object.py
index a6a0de6..5872b73 100644
--- a/unyt/unit_object.py
+++ b/unyt/unit_object.py
@@ -600,7 +600,7 @@ class Unit(object):
         --------
         >>> from unyt import km
         >>> km.units.list_equivalencies()
-        spectral: length <-> frequency <-> energy
+        spectral: length <-> spatial_frequency <-> frequency <-> energy
         schwarzschild: mass <-> length
         compton: mass <-> length
         """


Test patch:
diff --git a/unyt/tests/test_unyt_array.py b/unyt/tests/test_unyt_array.py
index c8e9527..6b5b143 100644
--- a/unyt/tests/test_unyt_array.py
+++ b/unyt/tests/test_unyt_array.py
@@ -1365,6 +1365,16 @@ def test_equivalencies():
     assert lam.units == u.eV.units
     assert hnu.units == u.erg.units
 
+    # wavelength to spatial frequency
+
+    lam = 4000*u.angstrom
+    nubar = lam.in_units('1/angstrom', 'spectral')
+    assert_allclose_units(nubar, 1/lam)
+    lam.convert_to_units('1/cm', 'spectral')
+    assert_allclose_units(lam, nubar)
+    assert lam.units == (1 / u.cm).units
+    assert nubar.units == (1 / u.angstrom).units
+
     # frequency to wavelength
 
     nu = 1*u.MHz
@@ -1375,6 +1385,16 @@ def test_equivalencies():
     assert lam.units == u.km.units
     assert nu.units == u.m.units
 
+    # frequency to spatial frequency
+
+    nu = 1*u.MHz
+    nubar = nu.to('1/km', 'spectral')
+    assert_allclose_units(nubar, nu/u.clight)
+    nu.convert_to_units('1/m', 'spectral')
+    assert_allclose_units(nubar, nu)
+    assert nubar.units == (1 / u.km).units
+    assert nu.units == (1 / u.m).units
+
     # frequency to photon energy
 
     nu = 1*u.MHz
@@ -1405,6 +1425,46 @@ def test_equivalencies():
     assert E.units == u.angstrom.units
     assert lam.units == u.nm.units
 
+    # photon energy to spatial frequency
+
+    E = 13.6*u.eV
+    nubar = E.to('1/nm', 'spectral')
+    assert_allclose_units(nubar, E/(u.hmks*u.clight))
+    E.convert_to_units('1/angstrom', 'spectral')
+    assert_allclose_units(E, nubar)
+    assert E.units == (1 / u.angstrom).units
+    assert nubar.units == (1 / u.nm).units
+
+    # spatial frequency to frequency
+
+    nubar = 1500. / u.cm
+    nu = nubar.to('Hz', 'spectral')
+    assert_allclose_units(nu, nubar*u.clight)
+    nubar.convert_to_units('MHz', 'spectral')
+    assert_allclose_units(nu, nubar)
+    assert nubar.units == u.MHz.units
+    assert nu.units == u.Hz.units
+
+    # spatial frequency to wavelength
+
+    nubar = 1500. / u.cm
+    lam = nubar.to('nm', 'spectral')
+    assert_allclose_units(lam, 1/nubar)
+    nubar.convert_to_units('angstrom', 'spectral')
+    assert_allclose_units(nubar, lam)
+    assert nubar.units == u.angstrom.units
+    assert lam.units == u.nm.units
+
+    # spatial frequency to photon energy
+
+    nubar = 1500. / u.cm
+    E = nubar.to('erg', 'spectral')
+    assert_allclose_units(E, u.hmks*u.clight*nubar)
+    nubar.convert_to_units('J', 'spectral')
+    assert_allclose_units(nubar, E)
+    assert nubar.units == u.J.units
+    assert E.units == u.erg.units
+
     # Sound-speed
 
     # tempearature <-> velocity