Repository: sunpy/sunpy

Issue:
Function to convert flare class into GOES flux
Thought this might be useful, but I didn't really know where to put it, so it's in goes.py for now. But I'm quite happy to move it elsewhere if anyone has a better idea.



Discussion:

Resolving patch:
diff --git a/sunpy/instr/goes.py b/sunpy/instr/goes.py
--- a/sunpy/instr/goes.py
+++ b/sunpy/instr/goes.py
@@ -69,8 +69,15 @@
 from sunpy.util.net import check_download_file
 from sunpy import sun
 
+GOES_CONVERSION_DICT = {'X': u.Quantity(1e-4, "W/m^2"),
+                        'M': u.Quantity(1e-5, "W/m^2"),
+                        'C': u.Quantity(1e-6, "W/m^2"),
+                        'B': u.Quantity(1e-7, "W/m^2"),
+                        'A': u.Quantity(1e-8, "W/m^2")}
+
 __all__ = ['get_goes_event_list', 'calculate_temperature_em',
-           'calculate_radiative_loss_rate', 'calculate_xray_luminosity']
+           'calculate_radiative_loss_rate', 'calculate_xray_luminosity', 'flux_to_flareclass',
+           'flareclass_to_flux']
 
 try:
     # Check required data files are present in user's default download dir
@@ -1268,3 +1275,96 @@ def _calc_xraylum(flux, date=None):
     else:
         xraylum = 4 * np.pi * sun.constants.au.to("m")**2 * flux
     return xraylum
+
+def flareclass_to_flux(flareclass):
+    """
+    Converts a GOES flare class into the corresponding X-ray flux.
+
+    Parameters
+    ----------
+    flareclass : str
+        The case-insensitive flare class (e.g., 'X3.2', 'm1.5', 'A9.6').
+
+    Returns
+    -------
+    flux : `~astropy.units.Quantity`
+        X-ray flux between 1 and 8 Angstroms as measured near Earth in W/m^2.
+
+    Raises
+    ------
+    TypeError
+        Input must be a string.
+
+    Examples
+    --------
+    >>> flareclass_to_flux('A1.0')
+    1e-08 W / m2
+    >>> flareclass_to_flux('c4.7')
+    4.7e-06 W / m2
+    >>> flareclass_to_flux('X2.4')
+    0.00024 W / m2
+    """
+    if type(flareclass) != type('str'):
+        raise TypeError("Input must be a string")
+    #TODO should probably make sure the string is in the expected format.
+
+    flareclass = flareclass.upper()
+    #invert the conversion dictionary
+    #conversion_dict = {v: k for k, v in GOES_CONVERSION_DICT.items()}
+    return float(flareclass[1:]) * GOES_CONVERSION_DICT[flareclass[0]]
+
+@u.quantity_input(goesflux=u.watt/u.m**2)
+def flux_to_flareclass(goesflux):
+    """
+    Converts X-ray flux into the corresponding GOES flare class.
+
+    Parameters
+    ----------
+    flux : `~astropy.units.Quantity`
+        X-ray flux between 1 and 8 Angstroms (usually measured by GOES) as
+        measured at the Earth in W/m^2
+
+    Returns
+    -------
+    flareclass : str
+        The flare class e.g.: 'X3.2', 'M1.5', 'A9.6'.
+
+    Raises
+    ------
+    ValueError
+        Flux cannot be negative.
+
+    References
+    ----------
+    `Solar Flare Classification <https://en.wikipedia.org/wiki/Solar_flare#Classification>`_
+
+    Examples
+    --------
+    >>> flux_to_flareclass(1e-08 * u.watt/u.m**2)
+    'A1'
+    >>> flux_to_flareclass(4.7e-06 * u.watt/u.m**2)
+    'C4.7'
+    >>> flux_to_flareclass(0.00024 * u.watt/u.m**2)
+    'X2.4'
+    >>> flux_to_flareclass(7.8e-09 * u.watt/u.m**2)
+    'A0.78'
+    >>> flux_to_flareclass(0.00682 * u.watt/u.m**2)
+    'X68.2'
+    """
+
+    if goesflux.value < 0:
+        raise ValueError("Flux cannot be negative")
+
+    decade = np.floor(np.log10(goesflux.to('W/m**2').value))
+    #invert the conversion dictionary
+    conversion_dict = {v: k for k, v in GOES_CONVERSION_DICT.items()}
+    if decade < -8:
+        str_class = "A"
+        decade = -8
+    elif decade > -4:
+        str_class = "X"
+        decade = -4
+    else:
+        str_class = conversion_dict.get(u.Quantity(10 ** decade, "W/m**2" ))
+    goes_subclass = 10 ** -decade * goesflux.to('W/m**2').value
+    return "{0}{1:.3g}".format(str_class, goes_subclass)
diff --git a/sunpy/util/util.py b/sunpy/util/util.py
--- a/sunpy/util/util.py
+++ b/sunpy/util/util.py
@@ -11,7 +11,7 @@
 from sunpy.extern.six.moves import map, zip
 
 __all__ = ['to_signed', 'unique', 'print_table',
-           'replacement_filename', 'goes_flare_class', 'merge', 'common_base',
+           'replacement_filename', 'merge', 'common_base',
            'minimal_pairs', 'polyfun_at',
            'expand_list', 'expand_list_generator', 'Deprecated']
 
@@ -34,37 +34,6 @@ def to_signed(dtype):
         dtype = "int{0:d}".format(min(dtype.itemsize * 2 * 8, 64))
     return np.dtype(dtype)
 
-def goes_flare_class(gcls):
-    """Convert GOES classes into a number to aid size comparison.  Units are
-    watts per meter squared.
-
-    Parameters
-    ----------
-    gcls : `str`
-        GOES class.
-
-    Returns
-    -------
-    float
-        Flux in implied units of Watts per meter squared.
-
-    .. todo::
-        return astropy quantities.
-    """
-    def calc(gcls):
-        powers_of_ten = {'A':1e-08, 'B':1e-07, 'C':1e-06, 'M':1e-05, 'X':1e-04}
-        power = gcls[0].upper()
-        if power in powers_of_ten:
-            return powers_of_ten[power] * float(gcls[1:])
-        else:
-            return None
-
-    if isinstance(gcls, bytes):
-        return calc(gcls)
-    if isinstance(gcls, list):
-        return [calc(x) for x in gcls]
-
-
 def unique(itr, key=None):
     """
     not documented yet


Test patch:
diff --git a/sunpy/instr/tests/test_goes.py b/sunpy/instr/tests/test_goes.py
--- a/sunpy/instr/tests/test_goes.py
+++ b/sunpy/instr/tests/test_goes.py
@@ -7,7 +7,9 @@
 import numpy as np
 from astropy.units.quantity import Quantity
 from astropy.tests.helper import assert_quantity_allclose
+from numpy.testing import assert_array_equal, assert_almost_equal
 from pandas.util.testing import assert_frame_equal
+import astropy.units as u
 
 from sunpy.time import TimeRange
 from sunpy import lightcurve
@@ -437,3 +439,33 @@ def test_goes_lx_obstime():
                        rtol=0.1)
     assert_quantity_allclose(lx_test["shortlum_cumul"],
                        lx_expected["shortlum_cumul"], rtol=0.1)
+
+def test_flux_to_classletter():
+    """Test converting fluxes into a class letter"""
+    fluxes = Quantity(10**(-np.arange(9, 2., -1)), 'W/m**2')
+    classesletter = ['A', 'A', 'B', 'C', 'M', 'X', 'X']
+    calculated_classesletter = [goes.flux_to_flareclass(f)[0] for f in fluxes]
+    calculated_classnumber = [float(goes.flux_to_flareclass(f)[1:]) for f in fluxes]
+    assert_array_equal(classesletter, calculated_classesletter)
+    assert_array_equal([0.1, 1, 1, 1, 1, 1, 10], calculated_classnumber)
+    # now test the Examples
+    assert goes.flux_to_flareclass(1e-08 * u.watt/u.m**2) == 'A1'
+    assert goes.flux_to_flareclass(0.00682 * u.watt/u.m**2) == 'X68.2'
+    assert goes.flux_to_flareclass(7.8e-09 * u.watt/u.m**2) == 'A0.78'
+    assert goes.flux_to_flareclass(0.00024 * u.watt/u.m**2) == 'X2.4'
+    assert goes.flux_to_flareclass(4.7e-06 * u.watt/u.m**2) == 'C4.7'
+    assert goes.flux_to_flareclass(6.9e-07 * u.watt/u.m**2) == 'B6.9'
+    assert goes.flux_to_flareclass(2.1e-05 * u.watt/u.m**2) == 'M2.1'
+
+def test_class_to_flux():
+    classes = ['A3.49', 'A0.23', 'M1', 'X2.3', 'M5.8', 'C2.3', 'B3.45', 'X20']
+    results = Quantity([3.49e-8, 2.3e-9, 1e-5, 2.3e-4, 5.8e-5, 2.3e-6, 3.45e-7, 2e-3], 'W/m2')
+    for c, r in zip(classes, results):
+        assert_almost_equal(r.value, goes.flareclass_to_flux(c).value)
+
+def test_joint_class_to_flux():
+    classes = ['A3.49', 'A0.23', 'M1', 'X2.3', 'M5.8', 'C2.3', 'B3.45', 'X20']
+    for c in classes:
+        assert c == goes.flux_to_flareclass(goes.flareclass_to_flux(c))
+
+# TODO add a test to check for raising error
diff --git a/sunpy/util/tests/test_util.py b/sunpy/util/tests/test_util.py
--- a/sunpy/util/tests/test_util.py
+++ b/sunpy/util/tests/test_util.py
@@ -12,14 +12,6 @@ def test_to_signed():
     """
     assert util.to_signed(np.dtype('uint32')) == np.dtype('int64')
 
-def test_goes_flare_class():
-    """
-    This should convert a list of GOES classes into a list of numbers.
-    """
-    lst = ['A1.0', 'M1.0', 'C1.0', 'B1.0', 'X1.0']
-    assert util.goes_flare_class(lst) == \
-        [1.0e-08, 1.0e-05, 1.0e-06, 1.0e-07, 1.0e-04]
-
 def test_unique():
     """
     This should add the unique values of itr to unique_list.