diff --git a/History.rst b/History.rst index 27e3e98..4858908 100644 --- a/History.rst +++ b/History.rst @@ -1,14 +1,18 @@ Change Log for SPEC_PLOTS ========================= -v1.35.1 - 2023 Aug. 4 +v1.36.0 - 2024 Feb. 9 +----------------- +* Added basic support HST Hubble Advanced Spectral Products (HASP) + +v1.35.1 - 2023 Aug. 4 ----------------- * Added support for flux uncertainties being in FLUX_ERROR column instead of ERROR column for JWST x1d FITS files. * Fixed readability of y-axis tick labels by adjusting font size, angle, and padding. -v1.35.0 - 2023 Jul. 31 +v1.35.0 - 2023 Jul. 31 ----------------- * Added required config file for ReadTheDocs. * Updated package dependencies to newer versions. diff --git a/setup.py b/setup.py index ad358f5..d92e5c7 100644 --- a/setup.py +++ b/setup.py @@ -20,6 +20,7 @@ license="MIT", packages=["spec_plots", "spec_plots.utils", "spec_plots.utils.specutils", "spec_plots.utils.specutils_cos", + "spec_plots.utils.specutils_hasp", "spec_plots.utils.specutils_jwst", "spec_plots.utils.specutils_stis"], install_requires=["astropy>=5.2.2", "matplotlib>=3.7.1", "numpy>=1.24.3", diff --git a/spec_plots.egg-info/PKG-INFO b/spec_plots.egg-info/PKG-INFO index 19f4440..6f8cf0f 100644 --- a/spec_plots.egg-info/PKG-INFO +++ b/spec_plots.egg-info/PKG-INFO @@ -1,9 +1,13 @@ Metadata-Version: 2.1 -Name: spec-plots -Version: 1.35.1 +Name: spec_plots +Version: 1.36.0 Summary: Create preview plots of HST or JWST spectra. Home-page: https://github.com/spacetelescope/spec_plots Author: Scott W. Fleming Author-email: fleming@stsci.edu License: MIT Classifier: Programming Language :: Python :: 3 +Requires-Dist: astropy>=5.2.2 +Requires-Dist: matplotlib>=3.7.1 +Requires-Dist: numpy>=1.24.3 +Requires-Dist: future>=0.18.3 diff --git a/spec_plots.egg-info/SOURCES.txt b/spec_plots.egg-info/SOURCES.txt index a99e71d..8808835 100644 --- a/spec_plots.egg-info/SOURCES.txt +++ b/spec_plots.egg-info/SOURCES.txt @@ -36,6 +36,10 @@ spec_plots/utils/specutils_cos/get_segment_names.py spec_plots/utils/specutils_cos/make_fits.py spec_plots/utils/specutils_cos/plotspec.py spec_plots/utils/specutils_cos/readspec.py +spec_plots/utils/specutils_hasp/__init__.py +spec_plots/utils/specutils_hasp/haspspectrum.py +spec_plots/utils/specutils_hasp/plotspec.py +spec_plots/utils/specutils_hasp/readspec.py spec_plots/utils/specutils_jwst/__init__.py spec_plots/utils/specutils_jwst/jwstspectrum.py spec_plots/utils/specutils_jwst/plotspec.py diff --git a/spec_plots/__init__.py b/spec_plots/__init__.py index 88a153d..6353fe3 100644 --- a/spec_plots/__init__.py +++ b/spec_plots/__init__.py @@ -8,4 +8,4 @@ from __future__ import absolute_import -__version__ = '1.35.1' +__version__ = '1.36.0' diff --git a/spec_plots/docs/API/conf.py b/spec_plots/docs/API/conf.py index fc593a9..0700410 100644 --- a/spec_plots/docs/API/conf.py +++ b/spec_plots/docs/API/conf.py @@ -12,7 +12,9 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import mock,os,sys +import unittest.mock as mock +import os +import sys MOCK_MODULES = ['astropy', 'astropy.io', 'matplotlib', 'matplotlib.pyplot', 'matplotlib.ticker', 'matplotlib.collections', 'numpy', 'future', 'future.builtins'] for mod_name in MOCK_MODULES: @@ -26,6 +28,7 @@ sys.path.insert(0, os.path.abspath('../../utils/')) sys.path.insert(0, os.path.abspath('../../utils/specutils/')) sys.path.insert(0, os.path.abspath('../../utils/specutils_cos/')) +sys.path.insert(0, os.path.abspath('../../utils/specutils_hasp/')) sys.path.insert(0, os.path.abspath('../../utils/specutils_jwst/')) sys.path.insert(0, os.path.abspath('../../utils/specutils_stis/')) @@ -55,7 +58,7 @@ # General information about the project. project = u'spec_plots' -copyright = u'2014,2015,2016 Scott W. Fleming' +copyright = u'2014,2015,2016,2017,2018,2019,2020,2021,2022,2023,2024 Scott W. Fleming' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/spec_plots/docs/API/index.rst b/spec_plots/docs/API/index.rst index 5899ab2..f5be603 100644 --- a/spec_plots/docs/API/index.rst +++ b/spec_plots/docs/API/index.rst @@ -3,7 +3,7 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -Documentation for MAST's openSAIL project: spec_plots +Documentation for MAST's spectral preview generator: spec_plots ===================================================== Contents: diff --git a/spec_plots/docs/API/modules.rst b/spec_plots/docs/API/modules.rst index 54e709f..46b77e8 100644 --- a/spec_plots/docs/API/modules.rst +++ b/spec_plots/docs/API/modules.rst @@ -6,5 +6,6 @@ Utility Modules, Including Instrument-Specific READ and PLOT Modules utils.specutils utils.specutils_cos + utils.specutils_hasp utils.specutils_jwst utils.specutils_stis diff --git a/spec_plots/docs/API/utils.specutils_hasp.rst b/spec_plots/docs/API/utils.specutils_hasp.rst new file mode 100644 index 0000000..71e835d --- /dev/null +++ b/spec_plots/docs/API/utils.specutils_hasp.rst @@ -0,0 +1,20 @@ +utils.specutils_hasp package +============================ + +haspspectrum +--------------------------------------- + +.. automodule:: utils.specutils_hasp.haspspectrum + :members: + :undoc-members: + :show-inheritance: + +plotspec +----------------------------------- + +.. autofunction:: utils.specutils_hasp.plotspec + +readspec +----------------------------------- + +.. autofunction:: utils.specutils_hasp.readspec diff --git a/spec_plots/docs/README_DEVELOPERS.txt b/spec_plots/docs/README_DEVELOPERS.txt index 5976cdc..e01a8e8 100644 --- a/spec_plots/docs/README_DEVELOPERS.txt +++ b/spec_plots/docs/README_DEVELOPERS.txt @@ -16,7 +16,7 @@ Best Practices for Development Of A New Branch 8.) After doing a final commit of the branch, make sure all Github Issues are closed for that branch, or re-assigned if punting to a later build for that Issue. -9.) After regression testing is finished and all Issues assigned to this branch are closed or moved to a future build, initiate a pull request and merge into master. +9.) After regression testing is finished and all Issues assigned to this branch are closed or moved to a future build, initiate a pull request and merge into main. 10.) To upload to PyPI with twine: twine upload dist/*, with the -u and -p options. diff --git a/spec_plots/make_hst_spec_previews.py b/spec_plots/make_hst_spec_previews.py index 58cea4d..10c2f8a 100755 --- a/spec_plots/make_hst_spec_previews.py +++ b/spec_plots/make_hst_spec_previews.py @@ -17,8 +17,9 @@ from __future__ import absolute_import from __future__ import print_function import argparse -from os import path from builtins import str +from os import path +import sys #-------------------- # External Imports #-------------------- @@ -27,7 +28,7 @@ #-------------------- # Package Imports #-------------------- -from spec_plots.utils import specutils, specutils_cos, specutils_stis +from spec_plots.utils import specutils, specutils_cos, specutils_stis, specutils_hasp from spec_plots import __version__ FLUX_SCALE_FACTOR_DEFAULT = 10. @@ -43,7 +44,7 @@ #-------------------- -class HSTSpecPrevError(Exception, object): +class HSTSpecPrevError(Exception): """ This class defines a generic Exception to use for errors raised in MAKE_HST_SPEC_PREVIEWS. It simply prints the given string when raising the @@ -99,7 +100,8 @@ def check_input_options(args): def get_instrument_name(input_file): """ - Retrieves the instrument name from a FITS file primary header. + Determines whether the input file is for HASP (Hubbble Advanced + Spectral Product). :param input_file: Name of input FITS file. @@ -124,11 +126,28 @@ def get_instrument_name(input_file): except KeyError: print("*** MAKE_HST_SPEC_PREVIEWS ERROR: INSTRUME keyword not" " found in this file's primary header: " + input_file) - exit(1) + sys.exit() return this_instrument.strip().upper() #-------------------- +def is_hasp_product(input_file): + """ + Retrieves the instrument name from a FITS file primary header. + + :param input_file: Name of input FITS file. + + :type input_file: str + + :returns: bool -- Returns True if the filename ends in _cspec.fits. + """ + + if input_file.endswith("_cspec.fits"): + return True + return False + +#-------------------- + def make_hst_spec_previews(input_file, flux_scale_factor= FLUX_SCALE_FACTOR_DEFAULT, fluxerr_scale_factor= FLUXERR_SCALE_FACTOR_DEFAULT, n_consecutive= @@ -227,13 +246,50 @@ def make_hst_spec_previews(input_file, flux_scale_factor= if ofile is not None: if ofile[-4:] == '.png': print(" Output file: " + ofile) - print(" Output file: " + ofile.strip('\.png') + + print(" Output file: " + ofile.split('.png')[0] + '_thumb.png') else: print(" Output file: " + ofile) else: print(" Plotting to screen.") + # Handle previews for HASP products. + if is_hasp_product(input_file): + if verbose: + print("HASP product") + # Get wavelengths, fluxes, flux uncertainties. + hasp_spectrum = specutils_hasp.readspec(input_file) + + # Make "large-size" plot. + for out_type, out_file in zip(output_type, output_files): + if out_type != 'fits': + # Make "large-size" plot + hasp_plot_metrics = specutils.calc_plot_metrics("hasp", + hasp_spectrum.wavelengths, + hasp_spectrum.fluxes, + hasp_spectrum.fluxerrs, + hasp_spectrum.dqs, + 1, flux_scale_factor, fluxerr_scale_factor) + specutils_hasp.plotspec(hasp_spectrum, + out_type, out_file, + flux_scale_factor, fluxerr_scale_factor, + hasp_plot_metrics, + dpi_val=dpi_val, output_size=1024, + debug=debug, full_ylabels=full_ylabels, + optimize=optimize) + + # Make "thumbnail-size" plot, if requested. Notice that in this + # case we always plot just the first association, by passing only + # `stitched_spectra[0]`. + if not debug: + specutils_hasp.plotspec(hasp_spectrum, + out_type, out_file, + flux_scale_factor, fluxerr_scale_factor, + hasp_plot_metrics, + dpi_val=dpi_val, + output_size=128, optimize=optimize) + return + # Read in the FITS file to determine which instrument it comes from. # Print the name of the instrument found in the header if verbose is turned # on. diff --git a/spec_plots/make_jwst_spec_previews.py b/spec_plots/make_jwst_spec_previews.py index 00f4e85..2d35b1b 100755 --- a/spec_plots/make_jwst_spec_previews.py +++ b/spec_plots/make_jwst_spec_previews.py @@ -17,8 +17,9 @@ from __future__ import absolute_import from __future__ import print_function import argparse -from os import path from builtins import str +from os import path +import sys #-------------------- # External Imports #-------------------- @@ -43,7 +44,7 @@ #-------------------- -class JWSTSpecPrevError(Exception, object): +class JWSTSpecPrevError(Exception): """ This class defines a generic Exception to use for errors raised in MAKE_JWST_SPEC_PREVIEWS. It simply prints the given string when raising the @@ -124,7 +125,7 @@ def get_instrument_name(input_file): except KeyError: print("*** MAKE_JWST_SPEC_PREVIEWS ERROR: INSTRUME keyword not" " found in this file's primary header: " + input_file) - exit(1) + sys.exit() return this_instrument.strip().upper() #-------------------- @@ -228,7 +229,7 @@ def make_jwst_spec_previews(input_file, flux_scale_factor= if ofile is not None: if ofile[-4:] == '.png': print(" Output file: " + ofile) - print(" Output file: " + ofile.strip('\.png') + + print(" Output file: " + ofile.split('.png')[0] + '_thumb.png') else: print(" Output file: " + ofile) diff --git a/spec_plots/utils/specutils/calc_plot_metrics.py b/spec_plots/utils/specutils/calc_plot_metrics.py index 72ea615..0848438 100644 --- a/spec_plots/utils/specutils/calc_plot_metrics.py +++ b/spec_plots/utils/specutils/calc_plot_metrics.py @@ -103,4 +103,3 @@ def calc_plot_metrics(instrument, wls, fls, flerrs, dqs, n_consecutive, optimal_xaxis_range, "avoid_regions":avoid_regions, "y_axis_range":y_axis_range, "line_collection":linecoll} #-------------------- - diff --git a/spec_plots/utils/specutils/edge_trim.py b/spec_plots/utils/specutils/edge_trim.py index 40e0ab8..ab66fb6 100644 --- a/spec_plots/utils/specutils/edge_trim.py +++ b/spec_plots/utils/specutils/edge_trim.py @@ -118,6 +118,7 @@ def _set_plot_xrange_test(instrument, flux_values, flux_err_values, median_flux, # Return array of boolean values for the edge_trim test. if numpy.isfinite(median_flux): bool_results = [((instrument == "cos" and x != 0. and check_fluxes) or + (instrument == "hasp" and x != 0. and check_fluxes) or (instrument == "stis" and x != 0. and check_fluxes) or (instrument == "miri" and x != 0. and check_fluxes) or (not check_fluxes)) @@ -132,6 +133,8 @@ def _set_plot_xrange_test(instrument, flux_values, flux_err_values, median_flux, is_bad_dq(instrument, z) and check_dqs) or (instrument == "cos" and not is_bad_dq(instrument, z) and check_dqs) or + (instrument == "hasp" and not + is_bad_dq(instrument, z) and check_dqs) or (instrument == "miri" and not is_bad_dq(instrument, z) and check_dqs) or (not check_dqs)) @@ -217,7 +220,7 @@ def edge_trim(instrument, fluxes, fluxerrs, dqs, n_consecutive, median_flux, # First run the xrange test on the first and last 100 elements (for # speed). If no good indices are found within those ranges, then and only - # then will we run it on the entire array. """ + # then will we run it on the entire array. where_good_fluxes_nodq = numpy.where(numpy.asarray(_set_plot_xrange_test( instrument, numpy.concatenate((fluxes[0:edge_size], fluxes[-1*edge_size:])), diff --git a/spec_plots/utils/specutils/is_bad_dq.py b/spec_plots/utils/specutils/is_bad_dq.py index 97216a8..9872720 100644 --- a/spec_plots/utils/specutils/is_bad_dq.py +++ b/spec_plots/utils/specutils/is_bad_dq.py @@ -43,21 +43,22 @@ def is_bad_dq(instrument, dqs): if instrument == "cos": if isinstance(dqs, numpy.ndarray): return numpy.asarray([x < 1 for x in dqs]) - else: - return dqs < 1 + return dqs < 1 - elif instrument == "stis": + if instrument == "hasp": if isinstance(dqs, numpy.ndarray): - return numpy.asarray([x != 0 and x != 16 for x in dqs]) - else: - return dqs != 0 and dqs != 16 + return numpy.asarray([x < 1 for x in dqs]) + return dqs < 1 + + if instrument == "stis": + if isinstance(dqs, numpy.ndarray): + return numpy.asarray([x not in (0, 16) for x in dqs]) + return dqs not in (0, 16) - elif instrument in ["miri", "nirspec", "niriss"]: + if instrument in ["miri", "nirspec", "niriss"]: if isinstance(dqs, numpy.ndarray): return numpy.asarray([x < 1 for x in dqs]) - else: - return dqs < 1 + return dqs < 1 - else: - return numpy.asarray([]) + return numpy.asarray([]) #-------------------- diff --git a/spec_plots/utils/specutils/set_plot_yrange.py b/spec_plots/utils/specutils/set_plot_yrange.py index 83db3ce..67c0f81 100644 --- a/spec_plots/utils/specutils/set_plot_yrange.py +++ b/spec_plots/utils/specutils/set_plot_yrange.py @@ -108,10 +108,8 @@ def set_plot_yrange(wavelengths, fluxes, avoid_regions=None, wl_range=None): if min_flux != max_flux: if min_flux - ybuffer > 0.: return [min_flux-ybuffer, max_flux+ybuffer] - else: - # We don't want the y-axis range to be TOO far negative, so limit - # it to be close to the lowest data point. """ - return [1.1*min_flux, max_flux+ybuffer] - else: - return [min_flux-1., max_flux+1.] + # We don't want the y-axis range to be TOO far negative, so limit + # it to be close to the lowest data point. + return [1.1*min_flux, max_flux+ybuffer] + return [min_flux-1., max_flux+1.] #-------------------- diff --git a/spec_plots/utils/specutils_cos/cosspectrum.py b/spec_plots/utils/specutils_cos/cosspectrum.py index eb04fc0..705ee85 100644 --- a/spec_plots/utils/specutils_cos/cosspectrum.py +++ b/spec_plots/utils/specutils_cos/cosspectrum.py @@ -9,7 +9,6 @@ # Built-In Imports #-------------------- from __future__ import absolute_import -from builtins import object from builtins import str #-------------------- # External Imports @@ -22,7 +21,7 @@ #-------------------- -class COSSpectrum(object): +class COSSpectrum(): """ Defines a COS spectrum, including wavelegnth, flux, flux errors, and DQ_WGT values. A COS spectrum consists of N segments (N = {2,3}) stored as a dict @@ -131,7 +130,7 @@ def __init__(self, optical_element, band=None, cos_segments=None, #-------------------- #-------------------- -class COSSegment(object): +class COSSegment(): """ Defines a spectrum from a COS segment. The data (wavelength, flux, flux errors) are stored as numpy ndarrays. A scalar int property provides the diff --git a/spec_plots/utils/specutils_cos/plotspec.py b/spec_plots/utils/specutils_cos/plotspec.py index fee56cc..2e995ad 100644 --- a/spec_plots/utils/specutils_cos/plotspec.py +++ b/spec_plots/utils/specutils_cos/plotspec.py @@ -18,7 +18,7 @@ #-------------------- import matplotlib from matplotlib.ticker import FormatStrFormatter -import matplotlib.pyplot as pyplot +from matplotlib import pyplot from matplotlib import rc import numpy #-------------------- @@ -134,7 +134,7 @@ def plotspec(cos_spectrum, output_type, output_file, sys.stderr.write("*** MAKE_HST_SPEC_PREVIEWS ERROR: Output" " directory could not be created, " + repr(this_error.strerror)+"\n") - exit(1) + sys.exit() else: raise @@ -384,7 +384,7 @@ def plotspec(cos_spectrum, output_type, output_file, # Display or plot to the desired device. if output_type != "screen": if output_size == 128: - revised_output_file = output_file.strip('\.png') + '_thumb.png' + revised_output_file = output_file.split('.png')[0] + '_thumb.png' else: revised_output_file = output_file diff --git a/spec_plots/utils/specutils_cos/readspec.py b/spec_plots/utils/specutils_cos/readspec.py index b0399d4..6b21fd3 100644 --- a/spec_plots/utils/specutils_cos/readspec.py +++ b/spec_plots/utils/specutils_cos/readspec.py @@ -10,6 +10,7 @@ #-------------------- from __future__ import absolute_import from __future__ import print_function +import sys #-------------------- # External Imports #-------------------- @@ -55,7 +56,7 @@ def readspec(input_file): except KeyError: print("*** MAKE_HST_SPEC_PREVIEWS ERROR: SEGMENT column not found" " in first extension's binary table.") - exit(1) + sys.exit() # Determine which band this is (NUV, FUV). band = check_segments(segment_arr, input_file) @@ -66,7 +67,7 @@ def readspec(input_file): except KeyError: print("*** MAKE_HST_SPEC_PREVIEWS ERROR: OPT_ELEM keyword not" " found in the primary header.") - exit(1) + sys.exit() # Extract the number of elements (n_wavelengths, n_fluxes, etc.) for # each segment. The dimension will match the array of segment names. @@ -75,7 +76,7 @@ def readspec(input_file): except KeyError: print("*** MAKE_HST_SPEC_PREVIEWS ERROR: NELEM column not found" " in first extension's binary table.") - exit(1) + sys.exit() # Extract wavelength, fluxes, flux uncertainties, and DQ flags for # each segment. These will be mxn tables, where m is the number of @@ -85,28 +86,28 @@ def readspec(input_file): except KeyError: print("*** MAKE_HST_SPEC_PREVIEWS ERROR: WAVELENGTH column not" " found in first extension's binary table.") - exit(1) + sys.exit() try: flux_table = cos_tabledata.field("FLUX") except KeyError: print("*** MAKE_HST_SPEC_PREVIEWS ERROR: FLUX column not found in" " first extension's binary table.") - exit(1) + sys.exit() try: fluxerr_table = cos_tabledata.field("ERROR") except KeyError: print("*** MAKE_HST_SPEC_PREVIEWS ERROR: ERROR column not found" " in first extension's binary table.") - exit(1) + sys.exit() try: dq_table = cos_tabledata.field("DQ_WGT") except KeyError: print("*** MAKE_HST_SPEC_PREVIEWS ERROR: DQ_WGT column not found" " in first extension's binary table.") - exit(1) + sys.exit() # Create COSSegment objects to populate the COSSpectrum object with. if band == 'FUV': diff --git a/spec_plots/utils/specutils_hasp/__init__.py b/spec_plots/utils/specutils_hasp/__init__.py new file mode 100644 index 0000000..f46c92f --- /dev/null +++ b/spec_plots/utils/specutils_hasp/__init__.py @@ -0,0 +1,19 @@ +""" +.. module:: __init__ + + :synopsis: Used to treat "specutils_stis" as a directory and package. + +.. moduleauthor:: Rob Swaters +""" + +#-------------------- +# Built-In Imports +#-------------------- +from __future__ import absolute_import +#-------------------- +# Package Imports +#-------------------- +from spec_plots import __version__ +from spec_plots.utils.specutils_hasp.haspspectrum import HASPSpectrum +from spec_plots.utils.specutils_hasp.plotspec import plotspec +from spec_plots.utils.specutils_hasp.readspec import readspec diff --git a/spec_plots/utils/specutils_hasp/haspspectrum.py b/spec_plots/utils/specutils_hasp/haspspectrum.py new file mode 100644 index 0000000..a372444 --- /dev/null +++ b/spec_plots/utils/specutils_hasp/haspspectrum.py @@ -0,0 +1,103 @@ +""" +.. module:: haspspectrum + :synopsis: Defines the class for HASP spectra. + +.. moduleauthor:: Rob Swaters +""" + +#-------------------- +# Built-In Imports +#-------------------- +from __future__ import absolute_import +#-------------------- +# External Imports +#-------------------- +import numpy +#-------------------- +# Package Imports +#-------------------- +from spec_plots import __version__ + +#-------------------- + +class HASPSpectrum(): + """ + Defines a HASP 1D spectrum, including wavelegnth, flux, and flux errors. + + :raises: ValueError + """ + + def __init__(self, nelem=None, wavelengths=None, fluxes=None, fluxerrs=None, + dqs=None, plot_info=None, orig_file=None): + """ + Create a HASPSpectrum object, default to empty values. Allows user + to preallocate space if they desire by setting "nelem" but not providing + lists/arrays on input right away. + + :param nelem: Number of elements for this segment's spectrum. + + :type nelem: int + + :param wavelengths: List of wavelengths in this segment's spectrum. + + :type wavelengths: list + + :param fluxes: List of fluxes in this segment's spectrum. + + :type fluxes: list + + :param fluxerrs: List of flux uncertainties in this segment's spectrum. + + :type fluxerrs: list + + :param dqs: List of data quality flags. + + :type dqs: list + + :param plot_info: dictionary with information for the preview plot. + + :type plot_info: dict + + :param orig_file: Original FITS file read to create the spectrum + (includes full path). + + :type orig_file: str + """ + + # Record the original file name along with the list of associations. + self.orig_file = orig_file + + # Should it be required to have `nelem` > 0 *OR* specify + # arrays on input? Otherwise they are pre-allocated to empty + # lists. + if nelem is not None: + self.nelem = nelem + else: + self.nelem = 0 + + if wavelengths is not None: + self.wavelengths = numpy.asarray(wavelengths) + else: + self.wavelengths = numpy.zeros(self.nelem) + + if fluxes is not None: + self.fluxes = numpy.asarray(fluxes) + else: + self.fluxes = numpy.zeros(self.nelem) + + if fluxerrs is not None: + self.fluxerrs = numpy.asarray(fluxerrs) + else: + self.fluxerrs = numpy.zeros(self.nelem) + + if dqs is not None: + self.dqs = numpy.asarray(dqs) + else: + self.dqs = numpy.zeros(self.nelem) + + if plot_info is not None: + self.plot_info = plot_info + else: + self.plot_info = {} + +#-------------------- diff --git a/spec_plots/utils/specutils_hasp/plotspec.py b/spec_plots/utils/specutils_hasp/plotspec.py new file mode 100644 index 0000000..a578395 --- /dev/null +++ b/spec_plots/utils/specutils_hasp/plotspec.py @@ -0,0 +1,347 @@ +""" +.. module:: plotspec + :synopsis: Creates preview plots for the provided HASP spectrum. + +.. moduleauthor:: Rob Swaters +""" + +#-------------------- +# Built-In Imports +#-------------------- +from __future__ import absolute_import +from __future__ import division +import copy +import os +import sys +from builtins import str +#-------------------- +# External Imports +#-------------------- +import matplotlib +from matplotlib.ticker import FormatStrFormatter +from matplotlib import pyplot +from matplotlib import rc +import numpy +#-------------------- +# Package Imports +#-------------------- +from spec_plots.utils.specutils.specutilserror import SpecUtilsError +from spec_plots.utils.specutils.debug_oplot import debug_oplot +from spec_plots.utils.specutils.calc_covering_fraction import ( + calc_covering_fraction) +from spec_plots import __version__ + +if matplotlib.get_backend().lower() != 'agg': + pyplot.switch_backend('Agg') +#-------------------- + +#-------------------- +def plotspec(hasp_spectrum, output_type, + output_file, flux_scale_factor, + fluxerr_scale_factor, plot_metrics, dpi_val=96., output_size=1024, + debug=False, full_ylabels=False, optimize=True): + """ + Accepts a HASPSpectrum object from the READSPEC function and produces + preview plots. + + :param hasp_spectrum: HASP spectrum as returned by READSPEC. + + :type hasp_spectrum: HASPSpectrum + + :param output_type: What kind of output to make? + + :type output_type: str + + :param output_file: Name of output file (including full path). + + :type output_file: str + + :param flux_scale_factor: Max. allowed ratio between the flux and a median + flux value, used in edge trimming. Default = 10. + + :type flux_scale_factor: float + + :param fluxerr_scale_factor: Max. allowed ratio between the flux uncertainty + and a median flux uncertainty value, used in edge trimming. + Default = 5. + + :type fluxerr_scale_factor: float + + :param plot_metrics: Collection of plot metrics (flux statistics, axis + ranges, etc.) to use when making the plots. These are computed using + `utils.specutils.calc_plot_metrics()`. + + :type plot_metrics: list + + :param dpi_val: The DPI value of your device's monitor. Affects the size of + the output plots. Default = 96. (applicable to most modern monitors). + + :type dpi_val: float + + :param output_size: Size of plot in pixels (plots are square in dimensions). + Defaults to 1024. + + :param output_size: int + + :param debug: Should the output plots include debugging information + (color-coded data points based on rejection criteria, shaded exclude + regions)? Default = False. + + :type debug: bool + + :param full_ylabels: Should the y-labels contain the full values (including + the power of 10 in scientific notation)? Default = False. + + :type full_ylabels: bool + + :param optimize: If set to True, will use a slightly optimized version of + determining the plot covering fraction. + + :type optimize: bool + + :raises: OSError, utils.specutils.SpecUtilsError + + .. note:: + + This function assumes a screen resolution of 96 DPI in order to + generate plots of the desired sizes. This is because matplotlib works + in units of inches and DPI rather than pixels. + """ + + # Make sure the plot size is set to an integer value. + if output_size is not None: + if not isinstance(output_size, int): + output_size = int(round(output_size)) + + # Make sure the output path exists, if not, create it. + if output_type != 'screen': + if (os.path.dirname(output_file) != "" and + not os.path.isdir(os.path.dirname(output_file))): + try: + os.mkdir(os.path.dirname(output_file)) + except OSError as this_error: + if this_error.errno == 13: + sys.stderr.write("*** MAKE_HST_SPEC_PREVIEWS ERROR:" + " Output directory could not be created," + " "+repr(this_error.strerror)+"\n") + sys.exit + else: + raise + + # If the figure size is large, then plot up to three associations, + # otherwise force only one association on the plot. + is_bigplot = output_size > 128 + + # Start plot figure. + this_figure, these_plotareas = pyplot.subplots(nrows=1, ncols=1, + figsize=( + output_size/dpi_val, + output_size/dpi_val), + dpi=dpi_val) + + # Make sure the subplots are in a numpy array (I think by default it is + # not if there is only one). + if not isinstance(these_plotareas, numpy.ndarray): + these_plotareas = numpy.asarray([these_plotareas]) + + # Adjust the plot geometry (margins, etc.) based on plot size. + if is_bigplot: + this_figure.subplots_adjust(hspace=0.35, top=0.915) + else: + this_figure.subplots_adjust(top=0.85, bottom=0.3, left=0.25, right=0.8) + + # Unlike COS and STIS, there is only one plotarea + covering_fraction = 0. + this_plotarea = these_plotareas[0] + + # Get the wavelengths, fluxes, flux uncertainties, and data quality + # flags out of the stitched spectrum for this association. + try: + all_wls = hasp_spectrum.wavelengths + all_fls = hasp_spectrum.fluxes + all_flerrs = hasp_spectrum.fluxerrs + all_dqs = hasp_spectrum.dqs + title_addendum = hasp_spectrum.plot_info["title"] + except KeyError as the_error: + raise SpecUtilsError("The provided stitched spectrum does not have" + " the expected format, missing key " + + str(the_error)+".") + + # Only plot information in the plot title if the plot is large (and + # therefore sufficient space exists on the plot). + if is_bigplot: + this_plotarea.set_title(title_addendum, loc="right", size="small", + color="red") + + # Extract the optimal x-axis plot range from the plot_metrics dict, + # since we use it a lot. + optimal_xaxis_range = plot_metrics["optimal_xaxis_range"] + + # Plot the spectrum, but only if valid wavelength ranges for x-axis + # are returned, otherwise plot a special "Fluxes Are All Zero" plot. + if all(numpy.isfinite(optimal_xaxis_range)): + # We plot the spectrum as a regular line for use + # in calc_covering_fraction, it will be removed later. + this_line = this_plotarea.plot(all_wls, all_fls, 'b') + + # Update y-axis range, but only adjust the ranges if this isn't + # an all-zero flux case (and not in debug mode, in which case I want + # to see the entire y-axis range). + if not debug: + this_plotarea.set_ylim(plot_metrics["y_axis_range"]) + + covering_fraction = calc_covering_fraction(this_figure, + these_plotareas, 0, + optimize=optimize) + # Note: here we remove the line we plotted before, it was only + # so that calc_covering_fraction would have someting to draw on the + # canvas and thereby determine which pixels were "blue" (i.e., part + # of the plotted spectrum vs. background). + this_line.pop(0).remove() + # Now we plot the spectrum as a LineCollection so that the + # transparency will have the desired effect, but, this is not + # rendered on the canvas inside calc_covering_fraction, hence why we + # need to plot it both as a regular line first. + # Note: because we are re-using a LineCollection object in the + # array of plot_metrics (specifically, when creating the + # thumbnail-sized plot), we have to use a copy of the LineCollection + # object, otherwise it will have Axes, Figure, etc. all defined and + # resetting them to None does not work. Since this is only an issue + # with thumbnail-sizes, this is only relevant for the first + # LineCollection. + this_collection = this_plotarea.add_collection(copy.copy( + plot_metrics["line_collection"])) + + if covering_fraction > 30.: + this_collection.set_alpha(0.1) + + # Turn on plot grid lines. + this_plotarea.grid(True, linestyle='dashed') + + if is_bigplot: + this_figure.suptitle(os.path.basename( + hasp_spectrum.plot_info['orig_file']), fontsize=18, color='r') + # Uncomment the lines below to include the covering fraction + # as part of the suptitle. + + if debug: + # Overplot points color-coded based on rejection criteria. + debug_oplot(this_plotarea, "hasp", all_wls, all_fls, + all_flerrs, all_dqs, + plot_metrics["median_flux"], + plot_metrics["median_fluxerr"], + flux_scale_factor, fluxerr_scale_factor, + plot_metrics["fluxerr_95th"]) + + # Overplot the x-axis edges that are trimmed to define the + # y-axis plot range as a shaded area. + this_plotarea.axvspan(numpy.nanmin(all_wls), + optimal_xaxis_range[0], + facecolor="lightgrey", alpha=0.5) + this_plotarea.axvspan(optimal_xaxis_range[1], + numpy.nanmax(all_wls), + facecolor="lightgrey", alpha=0.5) + + # Overplot the avoid regions in a light color as a shaded area. + for region in plot_metrics["avoid_regions"]: + this_plotarea.axvspan(region.minwl, region.maxwl, + facecolor="lightgrey", alpha=0.5) + + # This is where we ensure the x-axis range is based on the full + # x-axis range, rather than using the optimum x-axis range. This is + # done so that all the plots for a similar instrument setting will + # have the same starting and ending plot values. + min_wl = numpy.nanmin(all_wls) + max_wl = numpy.nanmax(all_wls) + xplot_buffer = (max_wl - min_wl) * 0.05 + this_plotarea.set_xlim([min_wl-xplot_buffer, max_wl+xplot_buffer]) + + # Only use two tick labels (min and max wavelengths) for + # thumbnails, because there isn't enough space otherwise. + if not is_bigplot: + rc('font', size=10) + minwl = numpy.nanmin(all_wls) + maxwl = numpy.nanmax(all_wls) + this_plotarea.set_xticks([minwl, maxwl]) + this_plotarea.set_xticklabels(this_plotarea.get_xticks(), + rotation=25.) + this_plotarea.xaxis.set_major_formatter(FormatStrFormatter( + "%6.1f")) + else: + # Make sure the font properties go back to normal. + pyplot.rcdefaults() + this_plotarea.tick_params(axis='x', labelsize=14) + this_plotarea.set_xlabel(r"Wavelength $(\AA)$", fontsize=16, + color='k') + this_plotarea.set_ylabel(r"Flux $\mathrm{(erg/s/cm^2\!/\AA)}$", + fontsize=16, color='k') + + # If requested, include the powers of 10 part of the y-axis + # tickmarks. + if full_ylabels: + this_plotarea.yaxis.set_major_formatter(FormatStrFormatter( + '%3.2E')) + + else: + # Otherwise this is a spectrum that has all zero fluxes, or some + # other problem, and we make a default plot. Define the optimal + # x-axis range to span the original spectrum. + optimal_xaxis_range = [numpy.nanmin(all_wls), numpy.nanmax(all_wls)] + this_plotarea.set_xlim(optimal_xaxis_range) + + # Make the plot background grey to distinguish that this is a + # `special` plot. Turn off y-tick labels. + this_plotarea.set_facecolor("lightgrey") + this_plotarea.set_yticklabels([]) + + # Configure the plot units, text size, and other markings based + # on whether this is a large or thumbnail-sized plot. + if not is_bigplot: + rc('font', size=10) + minwl = numpy.nanmin(all_wls) + maxwl = numpy.nanmax(all_wls) + this_plotarea.set_xticks([minwl, maxwl]) + this_plotarea.set_xticklabels(this_plotarea.get_xticks(), + rotation=25.) + this_plotarea.xaxis.set_major_formatter(FormatStrFormatter( + "%6.1f")) + textsize = "small" + plottext = "Fluxes are \n all 0." + else: + # Make sure the font properties go back to normal. + pyplot.rcdefaults() + this_plotarea.tick_params(axis='x', labelsize=14) + this_plotarea.set_xlabel(r"Wavelength $(\AA)$", fontsize=16, + color='k') + this_plotarea.set_ylabel(r"Flux $\mathrm{(erg/s/cm^2\!/\AA)}$", + fontsize=16, color='k') + + # If requested, include the powers of 10 part of the y-axis + # tickmarks. + if full_ylabels: + this_plotarea.yaxis.set_major_formatter(FormatStrFormatter( + '%3.2E')) + + textsize = "x-large" + plottext = "Fluxes are all 0." + + # Place the text with the informational message in the center of + # the plot. + this_plotarea.text(0.5, 0.5, plottext, horizontalalignment="center", + verticalalignment="center", + transform=this_plotarea.transAxes, size=textsize) + + # Display or plot to the desired format. + if output_type != "screen": + if output_size == 128: + revised_output_file = output_file.split('.png')[0] + '_thumb.png' + else: + revised_output_file = output_file + + # Save figure. + this_figure.savefig(revised_output_file, format=output_type, + dpi=dpi_val) + + elif output_type == "screen": + pyplot.show() +#-------------------- diff --git a/spec_plots/utils/specutils_hasp/readspec.py b/spec_plots/utils/specutils_hasp/readspec.py new file mode 100644 index 0000000..4e8d0b2 --- /dev/null +++ b/spec_plots/utils/specutils_hasp/readspec.py @@ -0,0 +1,61 @@ +""" +.. module:: readspec + :synopsis: Reads in a HASP spectrum from a FITS file. + +.. moduleauthor:: Rob Swaters +""" + +#-------------------- +# Built-In Imports +#-------------------- +from __future__ import absolute_import +#-------------------- +# External Imports +#-------------------- +from astropy.io import fits +#-------------------- +# Package Imports +#-------------------- +from spec_plots.utils.specutils_hasp.haspspectrum import HASPSpectrum +from spec_plots import __version__ + +#-------------------- + +#-------------------- +def readspec(input_file): + """ + Reads in a HASP spectrum FITS file (*_cspec.fits) and returns the + wavelengths, fluxes, and flux uncertainties. + + :param input_file: Name of input FITS file. + + :type input_file: str + + :returns: HASPSpectrum -- The spectroscopic data (wavelength, flux, flux + error, etc): + """ + + with fits.open(input_file) as hdulist: + # Create an initially empty list that will contain each extension's + # (association's) spectrum object. + + try: + title = hdulist[0].header['TARGNAME'] + except KeyError: + title = "" + + plot_info = {"title": title, "orig_file": input_file} + + # Data are in the first extension. + data_table = hdulist[1].data + + return HASPSpectrum( + nelem=len(data_table["WAVELENGTH"][0]), + wavelengths=data_table["WAVELENGTH"][0], + fluxes=data_table["FLUX"][0], + fluxerrs=data_table["ERROR"][0], + dqs=[0 if x > 0 else 1 for x in data_table["EFF_EXPTIME"][0]], + plot_info=plot_info + ) + +#-------------------- diff --git a/spec_plots/utils/specutils_jwst/jwstspectrum.py b/spec_plots/utils/specutils_jwst/jwstspectrum.py index 2a48192..d7ab4f3 100644 --- a/spec_plots/utils/specutils_jwst/jwstspectrum.py +++ b/spec_plots/utils/specutils_jwst/jwstspectrum.py @@ -9,7 +9,6 @@ # Built-In Imports #-------------------- from __future__ import absolute_import -from builtins import object #-------------------- # Package Imports #-------------------- @@ -17,7 +16,7 @@ #-------------------- -class JWSTSpectrum(object): +class JWSTSpectrum(): """ Defines a JWST spectrum, including wavelegnth, flux, flux errors, and DQ values. diff --git a/spec_plots/utils/specutils_jwst/plotspec.py b/spec_plots/utils/specutils_jwst/plotspec.py index f116e84..e248d51 100644 --- a/spec_plots/utils/specutils_jwst/plotspec.py +++ b/spec_plots/utils/specutils_jwst/plotspec.py @@ -19,8 +19,7 @@ #-------------------- import matplotlib from matplotlib.ticker import FormatStrFormatter -import matplotlib.pyplot as pyplot -from matplotlib import rc +from matplotlib import pyplot import numpy #-------------------- # Package Imports @@ -123,7 +122,7 @@ def plotspec(jwst_spectrum, output_type, output_file, flux_scale_factor, sys.stderr.write("*** MAKE_JWST_SPEC_PREVIEWS ERROR:" " Output directory could not be created," " "+repr(this_error.strerror)+"\n") - exit(1) + sys.exit() else: raise @@ -314,7 +313,7 @@ def plotspec(jwst_spectrum, output_type, output_file, flux_scale_factor, # Display or plot to the desired format. if output_type != "screen": if output_size == 128: - revised_output_file = output_file.strip('\.png') + '_thumb.png' + revised_output_file = output_file.split('.png')[0] + '_thumb.png' else: revised_output_file = output_file diff --git a/spec_plots/utils/specutils_jwst/readspec.py b/spec_plots/utils/specutils_jwst/readspec.py index 2ce4e4f..a712bcc 100644 --- a/spec_plots/utils/specutils_jwst/readspec.py +++ b/spec_plots/utils/specutils_jwst/readspec.py @@ -10,6 +10,7 @@ #-------------------- from __future__ import absolute_import from __future__ import print_function +import sys #-------------------- # External Imports #-------------------- @@ -51,14 +52,14 @@ def readspec(input_file): except KeyError: print("*** MAKE_JWST_SPEC_PREVIEWS ERROR: WAVELENGTH column not" " found in first extension's binary table.") - exit(1) + sys.exit() try: flux_table = jwst_tabledata.field("FLUX") except KeyError: print("*** MAKE_JWST_SPEC_PREVIEWS ERROR: FLUX column not found in" " first extension's binary table.") - exit(1) + sys.exit() try: fluxerr_table = jwst_tabledata.field("ERROR") @@ -69,14 +70,14 @@ def readspec(input_file): print("*** MAKE_JWST_SPEC_PREVIEWS ERROR: neither ERROR " "nor FLUX_ERROR column found in first " "extension's binary table.") - exit(1) + sys.exit() try: dq_table = jwst_tabledata.field("DQ") except KeyError: print("*** MAKE_JWST_SPEC_PREVIEWS ERROR: DQ column not found" " in first extension's binary table.") - exit(1) + sys.exit() # Create JWSTSpectrum object. return_spec = JWSTSpectrum(wavelength_table, flux_table, fluxerr_table, diff --git a/spec_plots/utils/specutils_stis/plotspec.py b/spec_plots/utils/specutils_stis/plotspec.py index 6d8b50c..f8ac188 100644 --- a/spec_plots/utils/specutils_stis/plotspec.py +++ b/spec_plots/utils/specutils_stis/plotspec.py @@ -19,7 +19,7 @@ #-------------------- import matplotlib from matplotlib.ticker import FormatStrFormatter -import matplotlib.pyplot as pyplot +from matplotlib import pyplot from matplotlib import rc import numpy #-------------------- @@ -136,7 +136,7 @@ def plotspec(stis_spectrum, association_indices, stitched_spectra, output_type, sys.stderr.write("*** MAKE_HST_SPEC_PREVIEWS ERROR:" " Output directory could not be created," " "+repr(this_error.strerror)+"\n") - exit(1) + sys.exit() else: raise @@ -376,7 +376,7 @@ def plotspec(stis_spectrum, association_indices, stitched_spectra, output_type, # Display or plot to the desired format. if output_type != "screen": if output_size == 128: - revised_output_file = output_file.strip('\.png') + '_thumb.png' + revised_output_file = output_file.split('.png')[0] + '_thumb.png' else: revised_output_file = output_file diff --git a/spec_plots/utils/specutils_stis/stis1dspectrum.py b/spec_plots/utils/specutils_stis/stis1dspectrum.py index 95cf42a..0260ed3 100644 --- a/spec_plots/utils/specutils_stis/stis1dspectrum.py +++ b/spec_plots/utils/specutils_stis/stis1dspectrum.py @@ -9,7 +9,6 @@ # Built-In Imports #-------------------- from __future__ import absolute_import -from builtins import object #-------------------- # External Imports #-------------------- @@ -21,7 +20,7 @@ #-------------------- -class STIS1DSpectrum(object): +class STIS1DSpectrum(): """ Defines a STIS 1D spectrum (either "x1d" extracted or "sx1" summed extracted), including wavelegnth, flux, and flux errors. A STIS 1D @@ -61,7 +60,7 @@ def __init__(self, association_spectra, orig_file=None): #-------------------- #-------------------- -class STISExposureSpectrum(object): +class STISExposureSpectrum(): """ Defines a STIS exposure spectrum, which consists of "M" STISOrderSpectrum objects. @@ -87,7 +86,7 @@ def __init__(self, order_spectra): #-------------------- #-------------------- -class STISOrderSpectrum(object): +class STISOrderSpectrum(): """ Defines a STIS order spectrum, including wavelength, flux, flux errors, and data quality flags, which are stored as numpy arrays. A scalar int