diff --git a/.github/workflows/ci_workflows.yml b/.github/workflows/ci_workflows.yml index 779802221..cfb1e7442 100644 --- a/.github/workflows/ci_workflows.yml +++ b/.github/workflows/ci_workflows.yml @@ -37,11 +37,6 @@ jobs: python: '3.12' toxenv: py312-test - - name: Python 3.12 (MacOS X) - os: macos-latest - python: '3.12' - toxenv: py312-test - - name: Python 3.10 os: ubuntu-latest python: '3.10' @@ -114,6 +109,12 @@ jobs: toxenv: py312-test-devdeps toxposargs: --remote-data=any + # doctest failure due to different no. of significant digits on arm64 (#1146) + - name: Python 3.12 (macOS) + os: macos-latest + python: '3.12' + toxenv: py312-test + steps: - name: Checkout code uses: actions/checkout@v4 diff --git a/docs/conf.py b/docs/conf.py index 1d45a0bda..a5791f624 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -57,8 +57,8 @@ # check_sphinx_version("1.2.1") # Include other packages to link against -intersphinx_mapping['astropy'] = ('https://docs.astropy.org/en/latest/', None) -intersphinx_mapping['gwcs'] = ('https://gwcs.readthedocs.io/en/latest/', None) +intersphinx_mapping['astropy'] = ('https://docs.astropy.org/en/stable/', None) +intersphinx_mapping['gwcs'] = ('https://gwcs.readthedocs.io/en/stable/', None) intersphinx_mapping['reproject'] = ('https://reproject.readthedocs.io/en/stable/', None) intersphinx_mapping['mpl_animators'] = ('https://docs.sunpy.org/projects/mpl-animators/en/stable/', None) diff --git a/docs/spectrum1d.rst b/docs/spectrum1d.rst index 7fc6d089b..34f771f41 100644 --- a/docs/spectrum1d.rst +++ b/docs/spectrum1d.rst @@ -3,15 +3,15 @@ Working with Spectrum1Ds ======================== As described in more detail in :doc:`types_of_spectra`, the core data class in -specutils for a single spectrum is `~specutils.Spectrum1D`. This object +specutils for a single spectrum is :class:`~specutils.Spectrum1D`. This object can represent either one or many spectra, all with the same ``spectral_axis``. This section describes some of the basic features of this class. Basic Spectrum Creation ----------------------- -The simplest way to create a `~specutils.Spectrum1D` is to -create it explicitly from arrays or `~astropy.units.Quantity` objects: +The simplest way to create a :class:`~specutils.Spectrum1D` is to create +it explicitly from arrays or :class:`~astropy.units.Quantity` objects: .. plot:: :include-source: @@ -66,8 +66,8 @@ information to automatically identify a loader. >>> from specutils import Spectrum1D >>> import urllib - >>> specs = urllib.request.urlopen('https://data.sdss.org/sas/dr14/sdss/spectro/redux/26/spectra/0751/spec-0751-52251-0160.fits') # doctest: +REMOTE_DATA - >>> Spectrum1D.read(specs, format="SDSS-III/IV spec") # doctest: +REMOTE_DATA + >>> spec = urllib.request.urlopen('https://data.sdss.org/sas/dr14/sdss/spectro/redux/26/spectra/0751/spec-0751-52251-0160.fits') # doctest: +REMOTE_DATA + >>> Spectrum1D.read(spec, format="SDSS-III/IV spec") # doctest: +REMOTE_DATA (length=3841); uncertainty=InverseVariance)> Note that the same spectrum could be more conveniently downloaded via @@ -94,7 +94,10 @@ installed, which is an optional dependency for ``specutils``. Call the help function for a specific loader to access further documentation on that format and optional parameters accepted by the ``read`` function, -e.g. as ``Spectrum1D.read.help('tabular-fits')``. +e.g. as ``Spectrum1D.read.help('tabular-fits')``. Additional optional parameters +are generally passed through to the backend functions performing the actual +reading operation, which depend on the loader. For loaders for FITS files for example, +this will often be :func:`astropy.io.fits.open`. More information on creating custom loaders for formats not covered by the above list can be found in the :doc:`custom loading ` page. @@ -115,9 +118,25 @@ any format, will default to the ``wcs1d-fits`` loader if the `~specutils.Spectru has a compatible WCS, and to ``tabular-fits`` otherwise, or if writing to another than the primary HDU (``hdu=0``) has been selected. For better control of the file type, the ``format`` parameter should be explicitly passed. +Again, additional optional parameters are forwarded to the backend writing functions, +which for the FITS writers is :meth:`astropy.io.fits.HDUList.writeto`. | More information on creating custom writers can be found in :ref:`custom_writer`. +Metadata +-------- + +The :attr:`specutils.Spectrum1D.meta` attribute provides a dictionary to store +additional information on the data, like origin, date and other circumstances. +For spectra read from files containing header-like attributes like a FITS +:class:`~astropy.io.fits.Header` or :attr:`astropy.table.Table.meta`, +loaders are conventionally storing this in ``Spectrum1D.meta['header']``. + +The two provided FITS writers (``tabular-fits`` and ``wcs1d-fits``) save the contents of +``Spectrum1D.meta['header']`` (which should be an :class:`astropy.io.fits.Header` +or any object, like a `dict`, that can instantiate one) as the header of the +:class:`~astropy.io.fits.hdu.PrimaryHDU`. + Including Uncertainties ----------------------- diff --git a/specutils/io/default_loaders/ascii.py b/specutils/io/default_loaders/ascii.py index 03e0e43f2..ded0ec1cd 100644 --- a/specutils/io/default_loaders/ascii.py +++ b/specutils/io/default_loaders/ascii.py @@ -51,7 +51,7 @@ def ascii_loader(file_name, column_mapping=None, **kwargs): # If no column mapping is given, attempt to parse the ascii files using # unit information if column_mapping is None: - return generic_spectrum_from_table(tab, **kwargs) + return generic_spectrum_from_table(tab) return spectrum_from_column_mapping(tab, column_mapping) @@ -95,6 +95,6 @@ def ipac_loader(file_name, column_mapping=None, **kwargs): # If no column mapping is given, attempt to parse the ascii files using # unit information if column_mapping is None: - return generic_spectrum_from_table(tab, **kwargs) + return generic_spectrum_from_table(tab) return spectrum_from_column_mapping(tab, column_mapping) diff --git a/specutils/io/default_loaders/generic_ecsv_reader.py b/specutils/io/default_loaders/generic_ecsv_reader.py index ef485e26e..a686b78b3 100644 --- a/specutils/io/default_loaders/generic_ecsv_reader.py +++ b/specutils/io/default_loaders/generic_ecsv_reader.py @@ -45,6 +45,6 @@ def generic_ecsv(file_name, column_mapping=None, **kwargs): table = Table.read(file_name, format='ascii.ecsv') if column_mapping is None: - return generic_spectrum_from_table(table, **kwargs) + return generic_spectrum_from_table(table) return spectrum_from_column_mapping(table, column_mapping) diff --git a/specutils/io/default_loaders/tabular_fits.py b/specutils/io/default_loaders/tabular_fits.py index c11c072c9..89bb742d8 100644 --- a/specutils/io/default_loaders/tabular_fits.py +++ b/specutils/io/default_loaders/tabular_fits.py @@ -43,16 +43,16 @@ def identify_tabular_fits(origin, *args, **kwargs): dtype=Spectrum1D, extensions=['fits', 'fit'], priority=6) def tabular_fits_loader(file_obj, column_mapping=None, hdu=1, store_data_header=False, **kwargs): """ - Load spectrum from a FITS file. + Load spectrum from a FITS file tabular extension. Parameters ---------- - file_obj: str, file-like, or HDUList + file_obj : str, file-like, or :class:`~astropy.io.fits.HDUList` FITS file name, object (provided from name by Astropy I/O Registry), - or HDUList (as resulting from astropy.io.fits.open()). - hdu: int + or HDU list (as resulting from `~astropy.io.fits.open`). + hdu : int The HDU of the fits file (default: 1st extension) to read from - store_data_header: bool + store_data_header : bool Defaults to ``False``, which stores the primary header in ``Spectrum1D.meta['header']``. Set to ``True`` to instead store the header from the specified data HDU. column_mapping : dict @@ -61,14 +61,20 @@ def tabular_fits_loader(file_obj, column_mapping=None, hdu=1, store_data_header= information. The dictionary keys should be the FITS file column names while the values should be a two-tuple where the first element is the associated `Spectrum1D` keyword argument, and the second element is the - unit for the ASCII file column:: + unit for the file column (or ``None`` to take unit from the table header):: - column_mapping = {'FLUX': ('flux', 'Jy')} + column_mapping = {'FLUX': ('flux', 'Jy'), + 'WAVE': ('spectral_axis', 'um')} + + **kwargs + Additional optional keywords passed to + :func:`~specutils.io.parsing_utils.read_fileobj_or_hdulist`, and when + reading from a file-like object, through to :func:`~astropy.io.fits.open`. Returns ------- - data: Spectrum1D - The spectrum that is represented by the data in this table. + data : :class:`Spectrum1D` + The spectrum that is represented by the data in the input table. """ # Parse the wcs information. The wcs will be passed to the column finding # routines to search for spectral axis information in the file. @@ -90,7 +96,7 @@ def tabular_fits_loader(file_obj, column_mapping=None, hdu=1, store_data_header= # If no column mapping is given, attempt to parse the file using # unit information if column_mapping is None: - return generic_spectrum_from_table(tab, wcs=wcs, **kwargs) + return generic_spectrum_from_table(tab, wcs=wcs) return spectrum_from_column_mapping(tab, column_mapping, wcs=wcs) @@ -102,15 +108,15 @@ def tabular_fits_writer(spectrum, file_name, hdu=1, update_header=False, store_d Parameters ---------- - spectrum: Spectrum1D - file_name: str - The path to the FITS file - hdu: int + spectrum : :class:`Spectrum1D` + file_name : str, file-like or `pathlib.Path` + File to write to. If a file object, must be opened in a writeable mode. + hdu : int Header Data Unit in FITS file to write to (currently only extension HDU 1) - update_header: bool + update_header : bool Write all compatible items in ``Spectrum1D.meta`` directly to FITS header; this will overwrite any identically named keys from ``Spectrum1D.meta['header']``. - store_data_header: bool + store_data_header : bool If ``True``, store ``Spectrum1D.meta['header']`` in the header of the target data HDU instead of the primary header (default ``False``). wunit : str or `~astropy.units.Unit` @@ -121,6 +127,9 @@ def tabular_fits_writer(spectrum, file_name, hdu=1, update_header=False, store_d Floating point type for storing spectral axis array ftype : str or `~numpy.dtype` Floating point type for storing flux array + hdulist : :class:`~astropy.io.fits.HDUList` + **kwargs + Additional optional keywords passed to :func:`~astropy.io.fits.HDUList.writeto`. """ if hdu < 1: raise ValueError(f'FITS does not support BINTABLE extension in HDU {hdu}.') diff --git a/specutils/io/default_loaders/wcs_fits.py b/specutils/io/default_loaders/wcs_fits.py index 9ac9d435c..cfeda5707 100644 --- a/specutils/io/default_loaders/wcs_fits.py +++ b/specutils/io/default_loaders/wcs_fits.py @@ -92,7 +92,9 @@ def wcs1d_fits_loader(file_obj, spectral_axis_unit=None, flux_unit=None, The ``uncertainty_type`` of `~astropy.nddata.NDUncertainty` (one of 'std', 'var', 'ivar'; default: try to infer from HDU EXTNAME). **kwargs - Extra keywords for :func:`~specutils.io.parsing_utils.read_fileobj_or_hdulist`. + Additional optional keywords passed to + :func:`~specutils.io.parsing_utils.read_fileobj_or_hdulist`, and when + reading from a file-like object, through to :func:`~astropy.io.fits.open`. Returns ------- @@ -225,22 +227,24 @@ def wcs1d_fits_writer(spectrum, file_name, hdu=0, update_header=False, Parameters ---------- spectrum : :class:`~specutils.Spectrum1D` - file_name : str - The path to the FITS file + file_name : str, file-like or `pathlib.Path` + File to write to. If a file object, must be opened in a writeable mode. hdu : int, optional - Header Data Unit in FITS file to write to (base 0; default primary HDU) + Header Data Unit in FITS file to write to (base 0; default primary HDU). update_header : bool, optional - Update FITS header with all compatible entries in `spectrum.meta` + Update FITS header with all compatible entries in `spectrum.meta`. flux_name : str, optional - HDU name to store flux spectrum under (default 'FLUX') + HDU name to store flux spectrum under (default 'FLUX'). mask_name : str or `None`, optional - HDU name to store mask under (default 'MASK'; `None`: do not save) + HDU name to store mask under (default 'MASK'; `None`: do not save). uncertainty_name : str or `None`, optional - HDU name to store uncertainty under (default set from type; `None`: do not save) + HDU name to store uncertainty under (default set from type; `None`: do not save). unit : str or :class:`~astropy.units.Unit`, optional - Unit for the flux (and associated uncertainty; defaults to ``spectrum.flux.unit``) + Unit for the flux (and associated uncertainty; defaults to ``spectrum.flux.unit``). dtype : str or :class:`~numpy.dtype`, optional - Floating point type for storing flux array (defaults to ``spectrum.flux.dtype``) + Floating point type for storing flux array (defaults to ``spectrum.flux.dtype``). + **kwargs + Additional optional keywords passed to :func:`~astropy.io.fits.HDUList.writeto`. """ # Create HDU list from WCS try: diff --git a/specutils/io/parsing_utils.py b/specutils/io/parsing_utils.py index 1485bb0d8..8990c3636 100644 --- a/specutils/io/parsing_utils.py +++ b/specutils/io/parsing_utils.py @@ -68,10 +68,10 @@ def spectrum_from_column_mapping(table, column_mapping, wcs=None, verbose=False) information. The dictionary keys should be the table column names while the values should be a two-tuple where the first element is the associated `Spectrum1D` keyword argument, and the second element is the - unit for the file column (or ``None`` to take unit from the table):: + unit for the file column (or ``None`` to take unit from the table header):: column_mapping = {'FLUX': ('flux', 'Jy'), - 'WAVE': ('spectral_axis'spectral_axisu', 'um')} + 'WAVE': ('spectral_axis', 'um')} wcs : :class:`~astropy.wcs.WCS` or :class:`gwcs.WCS` WCS object passed to the Spectrum1D initializer. @@ -138,27 +138,29 @@ def spectrum_from_column_mapping(table, column_mapping, wcs=None, verbose=False) return Spectrum1D(**spec_kwargs, wcs=wcs, meta={'header': table.meta}) -def generic_spectrum_from_table(table, wcs=None, **kwargs): +def generic_spectrum_from_table(table, wcs=None): """ Load spectrum from an Astropy table into a Spectrum1D object. Uses the following logic to figure out which column is which: * Spectral axis (dispersion) is the first column with units - compatible with u.spectral() or with length units such as 'pix'. + compatible with ``u.spectral()`` or with length units such as 'pix'. + Need not be present, if a valid ``wcs`` parameter is passed. * Flux is taken from the first column with units compatible with - u.spectral_density(), or with other likely culprits such as + ``u.spectral_density()``, or with other likely culprits such as 'adu' or 'cts/s'. * Uncertainty comes from the next column with the same units as flux. Parameters ---------- - file_name: str - The path to the ECSV file + table : :class:`~astropy.table.Table` + Table containing a column of ``flux``, and optionally ``spectral_axis`` + and ``uncertainty`` as defined above. wcs : :class:`~astropy.wcs.WCS` A FITS WCS object. If this is present, the machinery will fall back - to using the wcs to find the dispersion information. + and default to using the ``wcs`` to find the dispersion information. Returns ------- @@ -212,7 +214,7 @@ def _find_spectral_column(table, columns_to_search, spectral_axis): additional_valid_units = [u.Unit('adu'), u.Unit('ct/s'), u.Unit('count')] found_column = None - # First, search for a column with units compatible with Janskies + # First, search for a column with units compatible with Jansky for c in columns_to_search: try: # Check for multi-D flux columns