PageRenderTime 71ms CodeModel.GetById 39ms RepoModel.GetById 1ms app.codeStats 0ms

/sunpy/timeseries/sources/goes.py

https://github.com/ayshih/sunpy
Python | 317 lines | 251 code | 9 blank | 57 comment | 7 complexity | 97e7bfdcaf9d75b51d22252cb723656f MD5 | raw file
  1. """
  2. This module provies GOES XRS `~sunpy.timeseries.TimeSeries` source.
  3. """
  4. from pathlib import Path
  5. from collections import OrderedDict
  6. import h5netcdf
  7. import matplotlib.dates as mdates
  8. import matplotlib.ticker as mticker
  9. import numpy as np
  10. import packaging.version
  11. from matplotlib import pyplot as plt
  12. from pandas import DataFrame
  13. import astropy.units as u
  14. from astropy.time import Time, TimeDelta
  15. import sunpy.io
  16. from sunpy import log
  17. from sunpy.extern import parse
  18. from sunpy.io.file_tools import UnrecognizedFileTypeError
  19. from sunpy.time import is_time_in_given_format, parse_time
  20. from sunpy.timeseries.timeseriesbase import GenericTimeSeries
  21. from sunpy.util.metadata import MetaDict
  22. from sunpy.visualization import peek_show
  23. __all__ = ['XRSTimeSeries']
  24. class XRSTimeSeries(GenericTimeSeries):
  25. """
  26. GOES XRS Time Series.
  27. Each GOES satellite there are two X-ray Sensors (XRS) which provide solar X ray fluxes
  28. for the wavelength bands of 0.5 to 4 Å (short channel) sand 1 to 8 Å (long channel).
  29. Most recent data is usually available one or two days late.
  30. Data is available starting on 1981/01/01.
  31. Examples
  32. --------
  33. >>> import sunpy.timeseries
  34. >>> import sunpy.data.sample # doctest: +REMOTE_DATA
  35. >>> goes = sunpy.timeseries.TimeSeries(sunpy.data.sample.GOES_XRS_TIMESERIES) # doctest: +REMOTE_DATA
  36. >>> goes.peek() # doctest: +SKIP
  37. References
  38. ----------
  39. * `GOES Mission Homepage <https://www.goes.noaa.gov>`_
  40. * `GOES XRS Homepage <https://www.swpc.noaa.gov/products/goes-x-ray-flux>`_
  41. * `GOES XRS Guide <https://ngdc.noaa.gov/stp/satellite/goes/doc/GOES_XRS_readme.pdf>`_
  42. * `NASCOM Data Archive <https://umbra.nascom.nasa.gov/goes/fits/>`_
  43. Notes
  44. -----
  45. * https://umbra.nascom.nasa.gov/goes/fits/goes_fits_files_notes.txt
  46. """
  47. # Class attributes used to specify the source class of the TimeSeries
  48. # and a URL to the mission website.
  49. _source = 'xrs'
  50. _url = "https://www.swpc.noaa.gov/products/goes-x-ray-flux"
  51. _netcdf_read_kw = {}
  52. h5netcdf_version = packaging.version.parse(h5netcdf.__version__)
  53. if h5netcdf_version == packaging.version.parse("0.9"):
  54. _netcdf_read_kw['decode_strings'] = True
  55. if h5netcdf_version >= packaging.version.parse("0.10"):
  56. _netcdf_read_kw['decode_vlen_strings'] = True
  57. def plot(self, axes=None, columns=None, **kwargs):
  58. """
  59. Plots the GOES XRS light curve.
  60. Parameters
  61. ----------
  62. axes : `matplotlib.axes.Axes`, optional
  63. The axes on which to plot the TimeSeries. Defaults to current axes.
  64. columns : list[str], optional
  65. If provided, only plot the specified columns.
  66. **kwargs : `dict`
  67. Additional plot keyword arguments that are handed to `~matplotlib.axes.Axes.plot`
  68. functions.
  69. Returns
  70. -------
  71. `~matplotlib.axes.Axes`
  72. The plot axes.
  73. """
  74. if not axes:
  75. axes = plt.gca()
  76. if columns is None:
  77. columns = ["xrsa", "xrsb"]
  78. self._validate_data_for_plotting()
  79. plot_settings = {"xrsa": ["blue", r"0.5--4.0 $\AA$"], "xrsb": ["red", r"1.0--8.0 $\AA$"]}
  80. data = self.to_dataframe()
  81. for channel in columns:
  82. axes.plot_date(
  83. data.index, data[channel], "-", label=plot_settings[channel][1], color=plot_settings[channel][0], lw=2, **kwargs
  84. )
  85. axes.set_yscale("log")
  86. axes.set_ylim(1e-9, 1e-2)
  87. axes.set_ylabel("Watts m$^{-2}$")
  88. locator = mdates.AutoDateLocator()
  89. formatter = mdates.ConciseDateFormatter(locator)
  90. axes.xaxis.set_major_locator(locator)
  91. axes.xaxis.set_major_formatter(formatter)
  92. ax2 = axes.twinx()
  93. ax2.set_yscale("log")
  94. ax2.set_ylim(1e-9, 1e-2)
  95. labels = ["A", "B", "C", "M", "X"]
  96. centers = np.logspace(-7.5, -3.5, len(labels))
  97. ax2.yaxis.set_minor_locator(mticker.FixedLocator(centers))
  98. ax2.set_yticklabels(labels, minor=True)
  99. ax2.set_yticklabels([])
  100. axes.yaxis.grid(True, "major")
  101. axes.xaxis.grid(False, "major")
  102. axes.legend()
  103. return axes
  104. @property
  105. def observatory(self):
  106. """
  107. Retrieves the goes satellite number by parsing the meta dictionary.
  108. """
  109. # Various pattern matches for the meta fields.
  110. pattern_inst = ("GOES 1-{SatelliteNumber:02d} {}")
  111. pattern_new = ("sci_gxrs-l2-irrad_g{SatelliteNumber:02d}_d{year:4d}{month:2d}{day:2d}_{}.nc")
  112. pattern_old = ("go{SatelliteNumber:02d}{}{month:2d}{day:2d}.fits")
  113. pattern_r = ("sci_xrsf-l2-flx1s_g{SatelliteNumber:02d}_d{year:4d}{month:2d}{day:2d}_{}.nc")
  114. pattern_telescop = ("GOES {SatelliteNumber:02d}")
  115. # The ordering of where we get the metadata from is important.
  116. # We alway want to check ID first as that will most likely have the correct information.
  117. # The other fields are fallback and sometimes have data in them that is "useless".
  118. id = (
  119. self.meta.metas[0].get("id", "").strip()
  120. or self.meta.metas[0].get("TELESCOP", "").strip()
  121. or self.meta.metas[0].get("Instrument", "").strip()
  122. )
  123. if isinstance(id, bytes):
  124. # Needed for old versions of h5netcdf
  125. id = id.decode('ascii')
  126. if id is None:
  127. log.debug("Unable to get a satellite number from 'Instrument', 'TELESCOP' or 'id' ")
  128. return None
  129. for pattern in [pattern_inst, pattern_new, pattern_old, pattern_r, pattern_telescop]:
  130. parsed = parse(pattern, id)
  131. if parsed is not None:
  132. return f"GOES-{parsed['SatelliteNumber']}"
  133. log.debug('Satellite Number not found in metadata')
  134. return None
  135. @peek_show
  136. def peek(self, columns=None, title="GOES X-ray flux", **kwargs):
  137. """
  138. Displays the GOES XRS light curve by calling `~sunpy.timeseries.sources.goes.XRSTimeSeries.plot`.
  139. .. plot::
  140. import sunpy.timeseries
  141. import sunpy.data.sample
  142. ts_goes = sunpy.timeseries.TimeSeries(sunpy.data.sample.GOES_XRS_TIMESERIES, source='XRS')
  143. ts_goes.peek()
  144. Parameters
  145. ----------
  146. columns : list[str], optional
  147. If provided, only plot the specified columns.
  148. title : `str`, optional
  149. The title of the plot. Defaults to "GOES X-ray flux".
  150. **kwargs : `dict`
  151. Additional plot keyword arguments that are handed to `~matplotlib.axes.Axes.plot`
  152. functions.
  153. """
  154. fig, ax = plt.subplots()
  155. axes = self.plot(columns=columns, axes=ax, **kwargs)
  156. axes.set_title(title)
  157. fig.autofmt_xdate()
  158. return fig
  159. @classmethod
  160. def _parse_file(cls, filepath):
  161. """
  162. Parses a GOES/XRS FITS file.
  163. Parameters
  164. ----------
  165. filepath : `str`
  166. The path to the file you want to parse.
  167. """
  168. if sunpy.io.detect_filetype(filepath) == "hdf5":
  169. return cls._parse_netcdf(filepath)
  170. try:
  171. hdus = sunpy.io.read_file(filepath)
  172. except UnrecognizedFileTypeError:
  173. raise ValueError(
  174. f"{Path(filepath).name} is not supported. Only fits and netCDF (nc) can be read.")
  175. else:
  176. return cls._parse_hdus(hdus)
  177. @classmethod
  178. def _parse_hdus(cls, hdulist):
  179. """
  180. Parses a GOES/XRS FITS `~astropy.io.fits.HDUList` from a FITS file.
  181. Parameters
  182. ----------
  183. hdulist : `astropy.io.fits.HDUList`
  184. A HDU list.
  185. """
  186. header = MetaDict(OrderedDict(hdulist[0].header))
  187. if len(hdulist) == 4:
  188. if is_time_in_given_format(hdulist[0].header['DATE-OBS'], '%d/%m/%Y'):
  189. start_time = Time.strptime(hdulist[0].header['DATE-OBS'], '%d/%m/%Y')
  190. elif is_time_in_given_format(hdulist[0].header['DATE-OBS'], '%d/%m/%y'):
  191. start_time = Time.strptime(hdulist[0].header['DATE-OBS'], '%d/%m/%y')
  192. else:
  193. raise ValueError("Date not recognized")
  194. xrsb = hdulist[2].data['FLUX'][0][:, 0]
  195. xrsa = hdulist[2].data['FLUX'][0][:, 1]
  196. seconds_from_start = hdulist[2].data['TIME'][0]
  197. elif 1 <= len(hdulist) <= 3:
  198. start_time = parse_time(header['TIMEZERO'], format='utime')
  199. seconds_from_start = hdulist[0].data[0]
  200. xrsb = hdulist[0].data[1]
  201. xrsa = hdulist[0].data[2]
  202. else:
  203. raise ValueError("Don't know how to parse this file")
  204. times = start_time + TimeDelta(seconds_from_start*u.second)
  205. times.precision = 9
  206. # remove bad values as defined in header comments
  207. xrsb[xrsb == -99999] = np.nan
  208. xrsa[xrsa == -99999] = np.nan
  209. # fix byte ordering
  210. newxrsa = xrsa.byteswap().newbyteorder()
  211. newxrsb = xrsb.byteswap().newbyteorder()
  212. data = DataFrame({'xrsa': newxrsa, 'xrsb': newxrsb},
  213. index=times.isot.astype('datetime64'))
  214. data.sort_index(inplace=True)
  215. # Add the units
  216. units = OrderedDict([('xrsa', u.W/u.m**2),
  217. ('xrsb', u.W/u.m**2)])
  218. return data, header, units
  219. @staticmethod
  220. def _parse_netcdf(filepath):
  221. """
  222. Parses the netCDF GOES files to return the data, header and associated units.
  223. Parameters
  224. ----------
  225. filepath : `~str`
  226. The path of the file to parse
  227. """
  228. with h5netcdf.File(filepath, mode="r", **XRSTimeSeries._netcdf_read_kw) as d:
  229. header = MetaDict(OrderedDict(d.attrs))
  230. if "a_flux" in d.variables:
  231. xrsa = np.array(d["a_flux"])
  232. xrsb = np.array(d["b_flux"])
  233. start_time_str = d["time"].attrs["units"]
  234. if not isinstance(start_time_str, str):
  235. # For h5netcdf<0.14
  236. start_time_str = start_time_str.astype(str)
  237. start_time_str = start_time_str.lstrip("seconds since").rstrip("UTC")
  238. # Perform the time addition in UTime format to ignore leap seconds
  239. times = Time(parse_time(start_time_str).utime + d["time"], format="utime")
  240. elif "xrsa_flux" in d.variables:
  241. xrsa = np.array(d["xrsa_flux"])
  242. xrsb = np.array(d["xrsb_flux"])
  243. start_time_str = d["time"].attrs["units"]
  244. if not isinstance(start_time_str, str):
  245. # For h5netcdf<0.14
  246. start_time_str = start_time_str.astype(str)
  247. start_time_str = start_time_str.lstrip("seconds since")
  248. # Perform the time addition in UTime format to ignore leap seconds
  249. times = Time(parse_time(start_time_str).utime + d["time"], format="utime")
  250. else:
  251. raise ValueError(f"The file {filepath} doesn't seem to be a GOES netcdf file.")
  252. data = DataFrame({"xrsa": xrsa, "xrsb": xrsb}, index=times.datetime)
  253. data = data.replace(-9999, np.nan)
  254. units = OrderedDict([("xrsa", u.W/u.m**2),
  255. ("xrsb", u.W/u.m**2)])
  256. return data, header, units
  257. @classmethod
  258. def is_datasource_for(cls, **kwargs):
  259. """
  260. Determines if header corresponds to a GOES lightcurve
  261. `~sunpy.timeseries.TimeSeries`.
  262. """
  263. if "source" in kwargs.keys():
  264. return kwargs["source"].lower().startswith(cls._source)
  265. if "meta" in kwargs.keys():
  266. return kwargs["meta"].get("TELESCOP", "").startswith("GOES")
  267. if "filepath" in kwargs.keys():
  268. try:
  269. if sunpy.io.detect_filetype(kwargs["filepath"]) == "hdf5":
  270. with h5netcdf.File(kwargs["filepath"], mode="r", **cls._netcdf_read_kw) as f:
  271. summary = f.attrs["summary"]
  272. if not isinstance(summary, str):
  273. # h5netcdf<0.14
  274. summary = summary.astype(str)
  275. return "XRS" in summary
  276. except Exception as e:
  277. log.debug(f'Reading {kwargs["filepath"]} failed with the following exception:\n{e}')
  278. return False