From 9f6d11c6d759bc0787dee60b2de8b8292ffebd1e Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Thu, 16 Jan 2025 14:41:21 -0500 Subject: [PATCH 01/14] remove unused specviz2d parser * 2d->1d spectra have been handled by the spectral extraction plugin for a while and logic no longer goes through here --- jdaviz/configs/specviz2d/plugins/__init__.py | 1 - jdaviz/configs/specviz2d/plugins/parsers.py | 76 -------------------- 2 files changed, 77 deletions(-) delete mode 100644 jdaviz/configs/specviz2d/plugins/parsers.py diff --git a/jdaviz/configs/specviz2d/plugins/__init__.py b/jdaviz/configs/specviz2d/plugins/__init__.py index 76a3e76214..389f6386e0 100644 --- a/jdaviz/configs/specviz2d/plugins/__init__.py +++ b/jdaviz/configs/specviz2d/plugins/__init__.py @@ -1,2 +1 @@ -from .parsers import * # noqa from .spectral_extraction.spectral_extraction import * # noqa diff --git a/jdaviz/configs/specviz2d/plugins/parsers.py b/jdaviz/configs/specviz2d/plugins/parsers.py deleted file mode 100644 index 199e0fd622..0000000000 --- a/jdaviz/configs/specviz2d/plugins/parsers.py +++ /dev/null @@ -1,76 +0,0 @@ -from pathlib import Path - -from specutils import Spectrum1D -from astropy.io import fits -import astropy.units as u -import numpy as np - -from jdaviz.core.registries import data_parser_registry -from jdaviz.utils import standardize_metadata, PRIHDR_KEY - -__all__ = ['spec2d_1d_parser'] - - -def _check_is_file(path): - return isinstance(path, str) and Path(path).is_file() - - -@data_parser_registry("spec2d-1d-parser") -def spec2d_1d_parser(app, data_obj, data_label=None, show_in_viewer=True): - """ - Generate a quicklook 1D spectrum from an input 2D spectrum by summing - over the cross-dispersion axis. - - Notes - ----- - This currently only works with JWST-type data in which the data is in the - second hdu of the fits file. - - Parameters - ---------- - app : `~jdaviz.app.Application` - The application-level object used to reference the viewers. - data_obj : str or list or spectrum-like - File path, list, or spectrum-like object to be read as a new row in - the mosviz table. - data_labels : str, optional - The label applied to the glue data component. - """ - if _check_is_file(data_obj): - with fits.open(data_obj) as hdulist: - data = hdulist[1].data - header = hdulist[1].header - prihdr = hdulist[0].header - - # Should only be 2D, so DISPAXIS-1 should be 0 or -1 and sum over the - # correct axis. If Unit doesn't understand the BUNIT we leave flux - # unitless - try: - flux = np.sum(data, header['DISPAXIS']-1)*u.Unit(header["BUNIT"]) - except ValueError: - flux = u.Quantity(np.sum(data, header['DISPAXIS']-1)) - - if "WAVEND" in header and "WAVSTART" in header: - step = (header["WAVEND"] - header["WAVSTART"]) / flux.size - spectral_axis = np.arange(header["WAVSTART"], header["WAVEND"], - step) * u.m - else: - # u.Unit("m") is used if WAVEND and WAVSTART are present so - # we use it here as well, even though the actual unit is pixels - spectral_axis = np.arange(1, flux.size + 1, 1) * u.m - - metadata = standardize_metadata(header) - metadata[PRIHDR_KEY] = standardize_metadata(prihdr) - - data_obj = Spectrum1D(flux, spectral_axis=spectral_axis, meta=metadata) - - data_label = app.return_data_label(data_label, alt_name="specviz2d_data") - app.data_collection[data_label] = data_obj - - else: - raise NotImplementedError("Spectrum2d parser only takes a filename") - - if show_in_viewer: - app.add_data_to_viewer( - app._default_spectrum_viewer_reference_name, data_label - ) From a2082f35cd3948ea6eb696d3ebe519e6079c8a70 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Tue, 21 Jan 2025 10:19:59 -0500 Subject: [PATCH 02/14] use existing data_format detection logic --- jdaviz/configs/mosviz/plugins/parsers.py | 8 ++++---- jdaviz/configs/specviz/helper.py | 23 +++++++++++++++-------- jdaviz/core/data_formats.py | 9 ++++++--- 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/jdaviz/configs/mosviz/plugins/parsers.py b/jdaviz/configs/mosviz/plugins/parsers.py index 31579799ac..689044f1bc 100644 --- a/jdaviz/configs/mosviz/plugins/parsers.py +++ b/jdaviz/configs/mosviz/plugins/parsers.py @@ -301,10 +301,10 @@ def mos_spec2d_parser(app, data_obj, data_labels=None, add_to_table=True, """ spectrum_2d_viewer_reference_name = ( - app._jdaviz_helper._default_spectrum_2d_viewer_reference_name + getattr(app._jdaviz_helper, "_default_spectrum_2d_viewer_reference_name", None) ) table_viewer_reference_name = ( - app._jdaviz_helper._default_table_viewer_reference_name + getattr(app._jdaviz_helper, "_default_table_viewer_reference_name", None) ) # Note: This is also used by Specviz2D @@ -388,13 +388,13 @@ def _parse_as_spectrum1d(hdulist, ext, transpose): label = data_labels[index] app.data_collection[label] = data - if add_to_table: + if add_to_table and table_viewer_reference_name is not None: _add_to_table( app, data_labels, '2D Spectra', table_viewer_reference_name=table_viewer_reference_name ) - if show_in_viewer: + if show_in_viewer and spectrum_2d_viewer_reference_name is not None: if len(data_labels) > 1: raise ValueError("More than one data label provided, unclear " + "which to show in viewer") diff --git a/jdaviz/configs/specviz/helper.py b/jdaviz/configs/specviz/helper.py index 066456198b..7f123ea803 100644 --- a/jdaviz/configs/specviz/helper.py +++ b/jdaviz/configs/specviz/helper.py @@ -74,16 +74,23 @@ def load_data(self, data, data_label=None, format=None, show_in_viewer=True, `~astropy.utils.data.download_file` or `~astroquery.mast.Conf.timeout`). """ + from jdaviz.core import data_formats + format, conf = data_formats.get_valid_format(data) + parsers = {'specviz': 'specviz-spectrum1d-parser', 'specviz2d': 'mosviz-spec2d-parser'} + parser_kwargs = {'mosviz-spec2d-parser': {}, + 'specviz-spectrum1d-parser': {'format': format, + 'show_in_viewer': show_in_viewer, + 'concat_by_file': concat_by_file, + 'cache': cache, + 'local_path': local_path, + 'timeout': timeout, + 'load_as_list': load_as_list}} + parser = parsers.get(conf) + kwargs = parser_kwargs.get(parser, {}) super().load_data(data, - parser_reference='specviz-spectrum1d-parser', + parser_reference=parser, data_label=data_label, - format=format, - show_in_viewer=show_in_viewer, - concat_by_file=concat_by_file, - cache=cache, - local_path=local_path, - timeout=timeout, - load_as_list=load_as_list) + **kwargs) def get_spectra(self, data_label=None, spectral_subset=None, apply_slider_redshift="Warn"): """Returns the current data loaded into the main viewer diff --git a/jdaviz/core/data_formats.py b/jdaviz/core/data_formats.py index 32f464f704..88ceff8741 100644 --- a/jdaviz/core/data_formats.py +++ b/jdaviz/core/data_formats.py @@ -75,9 +75,12 @@ def get_valid_format(filename): config : str The recommended application configuration """ - - valid_file_format = identify_spectrum_format(filename, SpectrumList) - ndim = guess_dimensionality(filename) + if isinstance(filename, (str, pathlib.Path)): + valid_file_format = identify_spectrum_format(filename, SpectrumList) + ndim = guess_dimensionality(filename) + elif isinstance(filename, (Spectrum1D)): + valid_file_format = None + ndim = filename.flux.ndim if valid_file_format: recommended_config = file_to_config_mapping.get(valid_file_format, 'default') From fb23045235eb6838c713e30647cdd032d3da6400 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Tue, 21 Jan 2025 11:36:07 -0500 Subject: [PATCH 03/14] add custom parser for specreduce trace objects --- jdaviz/configs/mosviz/plugins/parsers.py | 30 +++++++++++++++++++++++- jdaviz/configs/specviz/helper.py | 6 ++--- jdaviz/core/data_formats.py | 22 +++++++++++++++++ 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/jdaviz/configs/mosviz/plugins/parsers.py b/jdaviz/configs/mosviz/plugins/parsers.py index 689044f1bc..59893a3657 100644 --- a/jdaviz/configs/mosviz/plugins/parsers.py +++ b/jdaviz/configs/mosviz/plugins/parsers.py @@ -18,7 +18,7 @@ from jdaviz.core.events import SnackbarMessage from jdaviz.utils import standardize_metadata, PRIHDR_KEY, download_uri_to_path -__all__ = ['mos_spec1d_parser', 'mos_spec2d_parser', 'mos_image_parser'] +__all__ = ['mos_spec1d_parser', 'mos_spec2d_parser', 'mos_image_parser', 'specreduce_trace_parser'] FALLBACK_NAME = "Unspecified" EXPECTED_FILES = {"niriss": ['1D Spectra C', '1D Spectra R', @@ -403,6 +403,34 @@ def _parse_as_spectrum1d(hdulist, ext, transpose): return len(data_obj) +@data_parser_registry("specreduce-trace") +def specreduce_trace_parser(app, data_obj, data_label=None, show_in_viewer=False): + """ + Loads a specreduce trace object. + + Parameters + ---------- + app : `~jdaviz.app.Application` + The application-level object used to reference the viewers. + data_obj : str or list or spectrum-like + File path, list, or spectrum-like object to be read as a new row in + the mosviz table. + data_label : str, optional + The label applied to the glue data component. + show_in_viewer : bool + Show data in viewer(s). + """ + spectrum_2d_viewer_reference_name = ( + getattr(app._jdaviz_helper, "_default_spectrum_2d_viewer_reference_name", None) + ) + + app.add_data(data_obj, data_label=data_label) + if show_in_viewer and spectrum_2d_viewer_reference_name: + app.add_data_to_viewer( + spectrum_2d_viewer_reference_name, data_label + ) + + def _load_fits_image_from_filename(filename, app): with fits.open(filename) as hdulist: # We do not use the generated labels diff --git a/jdaviz/configs/specviz/helper.py b/jdaviz/configs/specviz/helper.py index 7f123ea803..c9ffa8d51f 100644 --- a/jdaviz/configs/specviz/helper.py +++ b/jdaviz/configs/specviz/helper.py @@ -75,9 +75,10 @@ def load_data(self, data, data_label=None, format=None, show_in_viewer=True, `~astroquery.mast.Conf.timeout`). """ from jdaviz.core import data_formats - format, conf = data_formats.get_valid_format(data) - parsers = {'specviz': 'specviz-spectrum1d-parser', 'specviz2d': 'mosviz-spec2d-parser'} + parser = data_formats.get_parser(data) + parser_kwargs = {'mosviz-spec2d-parser': {}, + 'specreduce-trace': {'show_in_viewer': show_in_viewer}, 'specviz-spectrum1d-parser': {'format': format, 'show_in_viewer': show_in_viewer, 'concat_by_file': concat_by_file, @@ -85,7 +86,6 @@ def load_data(self, data, data_label=None, format=None, show_in_viewer=True, 'local_path': local_path, 'timeout': timeout, 'load_as_list': load_as_list}} - parser = parsers.get(conf) kwargs = parser_kwargs.get(parser, {}) super().load_data(data, parser_reference=parser, diff --git a/jdaviz/core/data_formats.py b/jdaviz/core/data_formats.py index 88ceff8741..8f19ce64f2 100644 --- a/jdaviz/core/data_formats.py +++ b/jdaviz/core/data_formats.py @@ -8,6 +8,7 @@ from specutils.io.registers import identify_spectrum_format from specutils import Spectrum1D, SpectrumList, SpectrumCollection +from specreduce.tracing import Trace from stdatamodels import asdf_in_fits from jdaviz.core.config import list_configurations @@ -90,6 +91,27 @@ def get_valid_format(filename): return valid_file_format, recommended_config +def get_parser(obj): + """ + Identify the data parser from a filename or data object + + Parameters + ---------- + obj : str or `pathlib.Path` or file-like object + The filename of the loaded data + + Returns + ------- + parser : str + The parser for the data object + """ + if isinstance(obj, Trace): + return 'specreduce-trace' + _, config = get_valid_format(obj) + parsers = {'specviz': 'specviz-spectrum1d-parser', 'specviz2d': 'mosviz-spec2d-parser'} + return parsers.get(config) + + def identify_data(filename, current=None): """Identify the data format and application configuration from a filename. From b2ed1ed5221e042577058d89eebb9fae5d85f331 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Tue, 21 Jan 2025 12:46:56 -0500 Subject: [PATCH 04/14] pass data_labels for mosviz parser --- jdaviz/configs/specviz/helper.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/jdaviz/configs/specviz/helper.py b/jdaviz/configs/specviz/helper.py index c9ffa8d51f..12b2c8986f 100644 --- a/jdaviz/configs/specviz/helper.py +++ b/jdaviz/configs/specviz/helper.py @@ -77,19 +77,21 @@ def load_data(self, data, data_label=None, format=None, show_in_viewer=True, from jdaviz.core import data_formats parser = data_formats.get_parser(data) - parser_kwargs = {'mosviz-spec2d-parser': {}, - 'specreduce-trace': {'show_in_viewer': show_in_viewer}, + parser_kwargs = {'mosviz-spec2d-parser': {'show_in_viewer': show_in_viewer, + 'data_labels': data_label}, + 'specreduce-trace': {'show_in_viewer': show_in_viewer, + 'data_label': data_label}, 'specviz-spectrum1d-parser': {'format': format, 'show_in_viewer': show_in_viewer, 'concat_by_file': concat_by_file, 'cache': cache, 'local_path': local_path, 'timeout': timeout, - 'load_as_list': load_as_list}} + 'load_as_list': load_as_list, + 'data_label': data_label}} kwargs = parser_kwargs.get(parser, {}) super().load_data(data, parser_reference=parser, - data_label=data_label, **kwargs) def get_spectra(self, data_label=None, spectral_subset=None, apply_slider_redshift="Warn"): From e9ed7989e63c83f73019543f4662a3bc50790898 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Tue, 21 Jan 2025 12:57:35 -0500 Subject: [PATCH 05/14] new functionality behind dev flag --- jdaviz/configs/specviz/helper.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/jdaviz/configs/specviz/helper.py b/jdaviz/configs/specviz/helper.py index 12b2c8986f..e3a0bcd77e 100644 --- a/jdaviz/configs/specviz/helper.py +++ b/jdaviz/configs/specviz/helper.py @@ -6,6 +6,7 @@ from glue.core.subset_group import GroupedSubset from specutils import SpectralRegion, Spectrum1D +from jdaviz.core import data_formats from jdaviz.core.helpers import ConfigHelper from jdaviz.core.events import RedshiftMessage from jdaviz.configs.default.plugins.line_lists.line_list_mixin import LineListMixin @@ -33,6 +34,7 @@ class Specviz(ConfigHelper, LineListMixin): _default_configuration = "specviz" _default_spectrum_viewer_reference_name = "spectrum-viewer" + _dev_deconfig = False def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -74,9 +76,9 @@ def load_data(self, data, data_label=None, format=None, show_in_viewer=True, `~astropy.utils.data.download_file` or `~astroquery.mast.Conf.timeout`). """ - from jdaviz.core import data_formats parser = data_formats.get_parser(data) - + if not self._dev_deconfig and parser != 'specviz-spectrum1d-parser': + raise NotImplementedError("Only Spectrum1D data is supported in Specviz.") parser_kwargs = {'mosviz-spec2d-parser': {'show_in_viewer': show_in_viewer, 'data_labels': data_label}, 'specreduce-trace': {'show_in_viewer': show_in_viewer, From 5e52cf3d8d2f23faf20f1f0b2d3c9d6bfd4d8fc7 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Tue, 21 Jan 2025 13:07:02 -0500 Subject: [PATCH 06/14] remove automodapi for unused specviz2d parsers --- docs/reference/api_parsers.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/reference/api_parsers.rst b/docs/reference/api_parsers.rst index 99665e245c..d82a0070d6 100644 --- a/docs/reference/api_parsers.rst +++ b/docs/reference/api_parsers.rst @@ -15,8 +15,5 @@ Parsers API .. automodapi:: jdaviz.configs.specviz.plugins.parsers :no-inheritance-diagram: -.. automodapi:: jdaviz.configs.specviz2d.plugins.parsers - :no-inheritance-diagram: - .. automodapi:: jdaviz.configs.rampviz.plugins.parsers :no-inheritance-diagram: \ No newline at end of file From 182fc119cb2cc10c37a7b03c5267d795df6d044c Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Tue, 21 Jan 2025 13:11:47 -0500 Subject: [PATCH 07/14] avoid calling get_valid_format for data objects --- jdaviz/core/data_formats.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/jdaviz/core/data_formats.py b/jdaviz/core/data_formats.py index 8f19ce64f2..e959391ade 100644 --- a/jdaviz/core/data_formats.py +++ b/jdaviz/core/data_formats.py @@ -76,12 +76,8 @@ def get_valid_format(filename): config : str The recommended application configuration """ - if isinstance(filename, (str, pathlib.Path)): - valid_file_format = identify_spectrum_format(filename, SpectrumList) - ndim = guess_dimensionality(filename) - elif isinstance(filename, (Spectrum1D)): - valid_file_format = None - ndim = filename.flux.ndim + valid_file_format = identify_spectrum_format(filename, SpectrumList) + ndim = guess_dimensionality(filename) if valid_file_format: recommended_config = file_to_config_mapping.get(valid_file_format, 'default') @@ -107,6 +103,12 @@ def get_parser(obj): """ if isinstance(obj, Trace): return 'specreduce-trace' + elif isinstance(obj, Spectrum1D): + if obj.flux.ndim == 1: + return 'specviz-spectrum1d-parser' + else: + # TODO: how to determine if multiple spectra or an image? + return 'mosviz-spec2d-parser' _, config = get_valid_format(obj) parsers = {'specviz': 'specviz-spectrum1d-parser', 'specviz2d': 'mosviz-spec2d-parser'} return parsers.get(config) From c64f14ccd58e76f41ac0c05689b941220919c90b Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Wed, 22 Jan 2025 12:22:04 -0500 Subject: [PATCH 08/14] handle show_in_viewer in load_data instead of parsers --- jdaviz/configs/specviz/helper.py | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/jdaviz/configs/specviz/helper.py b/jdaviz/configs/specviz/helper.py index e3a0bcd77e..ca491f2ac0 100644 --- a/jdaviz/configs/specviz/helper.py +++ b/jdaviz/configs/specviz/helper.py @@ -79,12 +79,20 @@ def load_data(self, data, data_label=None, format=None, show_in_viewer=True, parser = data_formats.get_parser(data) if not self._dev_deconfig and parser != 'specviz-spectrum1d-parser': raise NotImplementedError("Only Spectrum1D data is supported in Specviz.") - parser_kwargs = {'mosviz-spec2d-parser': {'show_in_viewer': show_in_viewer, + + default_labels = {'mosviz-spec2d-parser': '2D Spectrum', + 'specreduce-trace': 'Trace', + 'specviz-spectrum1d-parser': 'Spectrum 1D'} + if data_label is None: + data_label = default_labels.get(parser, 'Unknown') + data_label = self.app.return_unique_name(data_label) + + parser_kwargs = {'mosviz-spec2d-parser': {'show_in_viewer': False, 'data_labels': data_label}, - 'specreduce-trace': {'show_in_viewer': show_in_viewer, + 'specreduce-trace': {'show_in_viewer': False, 'data_label': data_label}, 'specviz-spectrum1d-parser': {'format': format, - 'show_in_viewer': show_in_viewer, + 'show_in_viewer': False, 'concat_by_file': concat_by_file, 'cache': cache, 'local_path': local_path, @@ -96,6 +104,17 @@ def load_data(self, data, data_label=None, format=None, show_in_viewer=True, parser_reference=parser, **kwargs) + if show_in_viewer: + # loop through existing viewers and show in any that support this data type + added = 0 + for viewer in self.viewers.values(): + if data_label in viewer.data_menu.data_labels_unloaded: + added += 1 + viewer.data_menu.add_data(data_label) + if added == 0: + # TODO: in the future open a new viewer with some default type based on the data + print(f"*** No viewer found to display \'{data_label}\'") + def get_spectra(self, data_label=None, spectral_subset=None, apply_slider_redshift="Warn"): """Returns the current data loaded into the main viewer From 03449c5c17021df6a9b656d9089c43acf2c39b59 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Wed, 22 Jan 2025 12:23:38 -0500 Subject: [PATCH 09/14] support load_as_list for 2d spectra --- jdaviz/configs/specviz/helper.py | 2 +- jdaviz/core/data_formats.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/jdaviz/configs/specviz/helper.py b/jdaviz/configs/specviz/helper.py index ca491f2ac0..dd3791cce0 100644 --- a/jdaviz/configs/specviz/helper.py +++ b/jdaviz/configs/specviz/helper.py @@ -76,7 +76,7 @@ def load_data(self, data, data_label=None, format=None, show_in_viewer=True, `~astropy.utils.data.download_file` or `~astroquery.mast.Conf.timeout`). """ - parser = data_formats.get_parser(data) + parser = data_formats.get_parser(data, load_as_list=load_as_list) if not self._dev_deconfig and parser != 'specviz-spectrum1d-parser': raise NotImplementedError("Only Spectrum1D data is supported in Specviz.") diff --git a/jdaviz/core/data_formats.py b/jdaviz/core/data_formats.py index e959391ade..13ec295bbe 100644 --- a/jdaviz/core/data_formats.py +++ b/jdaviz/core/data_formats.py @@ -87,7 +87,7 @@ def get_valid_format(filename): return valid_file_format, recommended_config -def get_parser(obj): +def get_parser(obj, load_as_list=False): """ Identify the data parser from a filename or data object @@ -107,7 +107,8 @@ def get_parser(obj): if obj.flux.ndim == 1: return 'specviz-spectrum1d-parser' else: - # TODO: how to determine if multiple spectra or an image? + if load_as_list: + return 'specviz-spectrumlist-parser' return 'mosviz-spec2d-parser' _, config = get_valid_format(obj) parsers = {'specviz': 'specviz-spectrum1d-parser', 'specviz2d': 'mosviz-spec2d-parser'} From bcef7718244a37e479bb86a062efd5cf651d8450 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Wed, 22 Jan 2025 12:47:00 -0500 Subject: [PATCH 10/14] support lists of objects when parsing --- jdaviz/configs/specviz/helper.py | 2 +- jdaviz/configs/specviz/tests/test_helper.py | 4 ++-- jdaviz/core/data_formats.py | 18 ++++++++++++++++-- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/jdaviz/configs/specviz/helper.py b/jdaviz/configs/specviz/helper.py index dd3791cce0..321ada4efa 100644 --- a/jdaviz/configs/specviz/helper.py +++ b/jdaviz/configs/specviz/helper.py @@ -82,7 +82,7 @@ def load_data(self, data, data_label=None, format=None, show_in_viewer=True, default_labels = {'mosviz-spec2d-parser': '2D Spectrum', 'specreduce-trace': 'Trace', - 'specviz-spectrum1d-parser': 'Spectrum 1D'} + 'specviz-spectrum1d-parser': 'Spectrum'} if data_label is None: data_label = default_labels.get(parser, 'Unknown') data_label = self.app.return_unique_name(data_label) diff --git a/jdaviz/configs/specviz/tests/test_helper.py b/jdaviz/configs/specviz/tests/test_helper.py index 3c0f7f5223..bd144f3e86 100644 --- a/jdaviz/configs/specviz/tests/test_helper.py +++ b/jdaviz/configs/specviz/tests/test_helper.py @@ -60,7 +60,7 @@ def test_load_spectrum_list_no_labels(self): self.spec_app.load_data(self.spec_list) assert len(self.spec_app.app.data_collection) == 4 for i in (1, 2, 3): - assert "specviz_data" in self.spec_app.app.data_collection[i].label + assert "Spectrum" in self.spec_app.app.data_collection[i].label def test_load_spectrum_list_with_labels(self): # now load three more spectra from a SpectrumList, with labels: @@ -412,7 +412,7 @@ def test_load_2d_flux(specviz_helper): # 1D Spectrum1D objects to load in Specviz. spec = Spectrum1D(spectral_axis=np.linspace(4000, 6000, 10)*u.Angstrom, flux=np.ones((4, 10))*u.Unit("1e-17 erg / (Angstrom cm2 s)")) - specviz_helper.load_data(spec, data_label="test") + specviz_helper.load_data(spec, data_label="test", load_as_list=True) assert len(specviz_helper.app.data_collection) == 4 assert specviz_helper.app.data_collection[0].label == "test [0]" diff --git a/jdaviz/core/data_formats.py b/jdaviz/core/data_formats.py index 13ec295bbe..56b05468a4 100644 --- a/jdaviz/core/data_formats.py +++ b/jdaviz/core/data_formats.py @@ -101,15 +101,29 @@ def get_parser(obj, load_as_list=False): parser : str The parser for the data object """ + if isinstance(obj, (SpectrumList, SpectrumCollection)): + return 'specviz-spectrum1d-parser' if isinstance(obj, Trace): return 'specreduce-trace' - elif isinstance(obj, Spectrum1D): + if isinstance(obj, Spectrum1D): if obj.flux.ndim == 1: return 'specviz-spectrum1d-parser' else: if load_as_list: - return 'specviz-spectrumlist-parser' + return 'specviz-spectrum1d-parser' return 'mosviz-spec2d-parser' + if isinstance(obj, fits.HDUList): + columns = [c.name.lower() for hduitem in obj for c in getattr(hduitem, 'columns', [])] + if 'wavelength' in columns and 'flux' in columns: + return 'specviz-spectrum1d-parser' + else: + raise ValueError("cannot find valid parser for HDUList") + if isinstance(obj, list): + parsers = [get_parser(o, load_as_list=load_as_list) for o in obj] + if len(set(parsers)) > 1: + raise ValueError("cannot find single parser for list of objects") + return parsers[0] + _, config = get_valid_format(obj) parsers = {'specviz': 'specviz-spectrum1d-parser', 'specviz2d': 'mosviz-spec2d-parser'} return parsers.get(config) From c06bf777533e2d285998042d2fe640f54b05fc0e Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Thu, 23 Jan 2025 13:48:58 -0500 Subject: [PATCH 11/14] support downloading from URI --- jdaviz/configs/specviz/helper.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/jdaviz/configs/specviz/helper.py b/jdaviz/configs/specviz/helper.py index 321ada4efa..be362527b6 100644 --- a/jdaviz/configs/specviz/helper.py +++ b/jdaviz/configs/specviz/helper.py @@ -1,3 +1,4 @@ +import pathlib import warnings from astropy import units as u @@ -10,6 +11,7 @@ from jdaviz.core.helpers import ConfigHelper from jdaviz.core.events import RedshiftMessage from jdaviz.configs.default.plugins.line_lists.line_list_mixin import LineListMixin +from jdaviz.utils import download_uri_to_path __all__ = ['Specviz'] @@ -87,6 +89,12 @@ def load_data(self, data, data_label=None, format=None, show_in_viewer=True, data_label = default_labels.get(parser, 'Unknown') data_label = self.app.return_unique_name(data_label) + if isinstance(data, str): + path = pathlib.Path(data) + if not path.is_file() and not path.is_dir(): + data = download_uri_to_path(data, cache=cache, + local_path=local_path, timeout=timeout) + parser_kwargs = {'mosviz-spec2d-parser': {'show_in_viewer': False, 'data_labels': data_label}, 'specreduce-trace': {'show_in_viewer': False, @@ -94,9 +102,6 @@ def load_data(self, data, data_label=None, format=None, show_in_viewer=True, 'specviz-spectrum1d-parser': {'format': format, 'show_in_viewer': False, 'concat_by_file': concat_by_file, - 'cache': cache, - 'local_path': local_path, - 'timeout': timeout, 'load_as_list': load_as_list, 'data_label': data_label}} kwargs = parser_kwargs.get(parser, {}) From 4727516be7d2c918afca3352b7d21de311b7d6a7 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Thu, 23 Jan 2025 15:20:12 -0500 Subject: [PATCH 12/14] WIP: deconfigged data parser registry --- jdaviz/configs/specviz/helper.py | 81 +++++++-------- jdaviz/core/parsers.py | 173 +++++++++++++++++++++++++++++++ jdaviz/core/registries.py | 14 ++- 3 files changed, 221 insertions(+), 47 deletions(-) create mode 100644 jdaviz/core/parsers.py diff --git a/jdaviz/configs/specviz/helper.py b/jdaviz/configs/specviz/helper.py index be362527b6..e5d1c3a9c2 100644 --- a/jdaviz/configs/specviz/helper.py +++ b/jdaviz/configs/specviz/helper.py @@ -45,27 +45,24 @@ def __init__(self, *args, **kwargs): self.app.hub.subscribe(self, RedshiftMessage, handler=self._redshift_listener) - def load_data(self, data, data_label=None, format=None, show_in_viewer=True, - concat_by_file=False, cache=None, local_path=None, timeout=None, - load_as_list=False): + def load_data(self, input, data_label=None, parser=None, + show_in_viewer=True, + cache=None, local_path=None, timeout=None): """ Load data into Specviz. Parameters ---------- - data : str, `~specutils.Spectrum1D`, or `~specutils.SpectrumList` + input : str, `~specutils.Spectrum1D`, or `~specutils.SpectrumList` Spectrum1D, SpectrumList, or path to compatible data file. data_label : str The Glue data label found in the ``DataCollection``. - format : str - Loader format specification used to indicate data format in - `~specutils.Spectrum1D.read` io method. + parser : str + The name of the parser to use to load the data. Will automatically + be determined if only a single registered parser is capable of parsing + ``input``. show_in_viewer : bool Show data in viewer(s). - concat_by_file : bool - If True and there is more than one available extension, concatenate - the extensions within each spectrum file passed to the parser and - add a concatenated spectrum to the data collection. cache : None, bool, or str Cache the downloaded file if the data are retrieved by a query to a URL or URI. @@ -78,47 +75,39 @@ def load_data(self, data, data_label=None, format=None, show_in_viewer=True, `~astropy.utils.data.download_file` or `~astroquery.mast.Conf.timeout`). """ - parser = data_formats.get_parser(data, load_as_list=load_as_list) - if not self._dev_deconfig and parser != 'specviz-spectrum1d-parser': + from jdaviz.core import parsers + + if isinstance(input, str): + path = pathlib.Path(input) + if not path.is_file() and not path.is_dir(): + input = download_uri_to_path(input, cache=cache, + local_path=local_path, timeout=timeout) + + parser = parsers.parse(input, parser=parser) + if not self._dev_deconfig and parser.registry_name != '1D Spectrum': raise NotImplementedError("Only Spectrum1D data is supported in Specviz.") - default_labels = {'mosviz-spec2d-parser': '2D Spectrum', - 'specreduce-trace': 'Trace', - 'specviz-spectrum1d-parser': 'Spectrum'} if data_label is None: - data_label = default_labels.get(parser, 'Unknown') - data_label = self.app.return_unique_name(data_label) + data_label = parser.default_data_label - if isinstance(data, str): - path = pathlib.Path(data) - if not path.is_file() and not path.is_dir(): - data = download_uri_to_path(data, cache=cache, - local_path=local_path, timeout=timeout) - - parser_kwargs = {'mosviz-spec2d-parser': {'show_in_viewer': False, - 'data_labels': data_label}, - 'specreduce-trace': {'show_in_viewer': False, - 'data_label': data_label}, - 'specviz-spectrum1d-parser': {'format': format, - 'show_in_viewer': False, - 'concat_by_file': concat_by_file, - 'load_as_list': load_as_list, - 'data_label': data_label}} - kwargs = parser_kwargs.get(parser, {}) - super().load_data(data, - parser_reference=parser, - **kwargs) + data_labels = [] + for data in parser.glue_data: + data_label = self.app.return_unique_name(data_label) + data_labels.append(data_label) + self.app.add_data(data, data_label) if show_in_viewer: - # loop through existing viewers and show in any that support this data type - added = 0 - for viewer in self.viewers.values(): - if data_label in viewer.data_menu.data_labels_unloaded: - added += 1 - viewer.data_menu.add_data(data_label) - if added == 0: - # TODO: in the future open a new viewer with some default type based on the data - print(f"*** No viewer found to display \'{data_label}\'") + for data_label in data_labels: + # loop through existing viewers and show in any that support this data type + added = 0 + for viewer in self.viewers.values(): + if data_label in viewer.data_menu.data_labels_unloaded: + added += 1 + viewer.data_menu.add_data(data_label) + if added == 0: + # TODO: in the future open a new viewer with some default type based on the data + # using parser.default_viewer + print(f"*** No viewer found to display \'{data_label}\'") def get_spectra(self, data_label=None, spectral_subset=None, apply_slider_redshift="Warn"): """Returns the current data loaded into the main viewer diff --git a/jdaviz/core/parsers.py b/jdaviz/core/parsers.py new file mode 100644 index 0000000000..9b030724d0 --- /dev/null +++ b/jdaviz/core/parsers.py @@ -0,0 +1,173 @@ +from functools import cached_property + +from jdaviz.core.registries import parser_registry + +from specutils import Spectrum1D +from specutils.io.registers import identify_spectrum_format + +__all__ = ['parse'] + + +class BaseParser: + registry_name = None # populated by parser_registry decorator + + def __init__(self, input, shared_info={}): + self.shared_info = shared_info # dictionary of parsed objects to be re-used between parsers + self._input = input + # trigger the cached_property + _ = self.can_parse + + @property + def input(self): + return self._input + + @cached_property + def default_data_label(self): + # optional override by subclass + # has access to self.input and anything stored by can_parse + return self.registry_name + + @cached_property + def default_viewer(self): + # override by subclass + # has access to self.input and anything stored by can_parse + return 'scatter' + + @cached_property + def can_parse(self): + # override by subclass, ONLY use self.input + # anything that needs to be opened can be stored internally for other methods (i.e. _input_to_object) + return False + + def _input_to_object(self): + # override by subclass + # has access to self.input and anything stored by can_parse + # can return single object or list (in which case _object_to_glue_data should also handle) + return self.input + + @cached_property + def object(self): + if not self.can_parse: + raise ValueError("Cannot parse input") + return self._input_to_object() + + def _object_to_glue_data(self): + # override by subclass + # has access to self.input and self.object and can return single entry or list + return self.object + + @cached_property + def glue_data(self): + if not self.can_parse: + raise ValueError("Cannot parse input") + data = self._object_to_glue_data() + if not isinstance(data, list): + data = [data] + return data + + +@parser_registry('1D Spectrum') +class Spectrum1DParser(BaseParser): + def default_viewer(self): + return 'spectrum-viewer' + + @cached_property + def can_parse(self): + if 'Spectrum1D' in self.shared_info: + pass + elif isinstance(self.input, Spectrum1D): + self.shared_info['Spectrum1D'] = self.input + if isinstance(self.input, str): + if 'specutils_format' not in self.shared_info: + try: + format = identify_spectrum_format(self.input, Spectrum1D) + except ValueError: + return False + else: + self.shared_info['specutils_format'] = format + if self.shared_info['specutils_format'] in ('JWST x1d',): + self.shared_info['Spectrum1D'] = Spectrum1D.read(self.input) + else: + return False + else: + return False + return self.shared_info['Spectrum1D'].flux.ndim == 1 + + def _input_to_object(self): + # has access to self.input and anything stored by can_parse + return self.shared_info['Spectrum1D'] + + +@parser_registry('2D Spectrum') +class Spectrum2DParser(BaseParser): + def default_viewer(self): + return 'spectrum-2d-viewer' + + @cached_property + def can_parse(self): + if 'Spectrum1D' in self.shared_info: + pass + elif isinstance(self.input, str): + if 'specutils_format' not in self.shared_info: + try: + format = identify_spectrum_format(self.input, Spectrum1D) + except ValueError: + return False + else: + self.shared_info['specutils_format'] = format + if self.shared_info['specutils_format'] in ('JWST s2d',): + self.shared_info['Spectrum1D'] = Spectrum1D.read(self.input) + else: + return False + elif isinstance(self.input, Spectrum1D): + self.shared_info['Spectrum1D'] = self.input + else: + return False + return self.shared_info['Spectrum1D'].flux.ndim == 2 + + def _input_to_object(self): + # has access to self.input and anything stored by can_parse + return self.shared_info['Spectrum1D'] + + +@parser_registry('2D Spectral Trace') +class SpectralTraceParser(BaseParser): + def default_viewer(self): + return 'spectrum-2d-viewer' + + @cached_property + def can_parse(self): + from specreduce.tracing import Trace + return isinstance(self.input, Trace) + + +def parse(input, parser=None): + # if input is a string, but not path/file, then download first + + # by default, search through all registered parsers, but also allow + # passing a Parser class or name of registered parser. + # could also be extended to allow pre-parsing of MOS data + if isinstance(parser, BaseParser): + all_parsers = {parser.__name__: parser} + if isinstance(parser, str): + if parser not in parser_registry.members: + raise ValueError(f"\'{parser}\' not one of {list(parser_registry.members.keys())}") + all_parsers = {parser: parser_registry.members.get(parser)} + else: + all_parsers = parser_registry.members + + # loop through all registered parsers to see which can parse the file + valid_parsers = {} + shared_info = {} + for parser_name, Parser in all_parsers.items(): + this_parser = Parser(input, shared_info) + shared_info.update(this_parser.shared_info) + if this_parser.can_parse: + valid_parsers[parser_name] = this_parser + if not len(valid_parsers): + all_parsers_str = ', '.join(list(all_parsers.keys())) + raise ValueError(f"No valid parsers found for input, tried {all_parsers_str}.") + if len(valid_parsers) > 1: + valid_parsers_str = ', '.join(list(valid_parsers.keys())) + raise ValueError(f"Multiple valid parsers found, please pass parser as one of: {valid_parsers_str}") # noqa + return list(valid_parsers.values())[0] \ No newline at end of file diff --git a/jdaviz/core/registries.py b/jdaviz/core/registries.py index 84ddc4370b..1b69d242f4 100644 --- a/jdaviz/core/registries.py +++ b/jdaviz/core/registries.py @@ -8,7 +8,7 @@ __all__ = ['convert', 'UniqueDictRegistry', 'ViewerRegistry', 'TrayRegistry', 'ToolRegistry', 'MenuRegistry', 'DataParserRegistry', 'viewer_registry', 'tray_registry', 'tool_registry', 'menu_registry', - 'data_parser_registry'] + 'data_parser_registry', 'parser_registry'] def _to_snake(s): @@ -221,8 +221,20 @@ def decorator(func): return decorator +class ParserRegistry(UniqueDictRegistry): + """Registry containing data parsing classes + """ + def __call__(self, name=None): + def decorator(cls): + cls.registry_name = name + self.add(name, cls) + return cls + return decorator + + viewer_registry = ViewerRegistry() tray_registry = TrayRegistry() tool_registry = ToolRegistry() menu_registry = MenuRegistry() data_parser_registry = DataParserRegistry() +parser_registry = ParserRegistry() # replacement for data_parser_registry as part of deconfigging From b285cbdcd8f7973a796d3b6984e86ffee5200654 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Fri, 24 Jan 2025 09:09:57 -0500 Subject: [PATCH 13/14] 3-step parser infrastructure --- jdaviz/configs/specviz/helper.py | 54 +++--- jdaviz/core/parsers.py | 285 ++++++++++++++++--------------- jdaviz/core/registries.py | 15 +- 3 files changed, 184 insertions(+), 170 deletions(-) diff --git a/jdaviz/configs/specviz/helper.py b/jdaviz/configs/specviz/helper.py index e5d1c3a9c2..bb96ba3b67 100644 --- a/jdaviz/configs/specviz/helper.py +++ b/jdaviz/configs/specviz/helper.py @@ -45,9 +45,10 @@ def __init__(self, *args, **kwargs): self.app.hub.subscribe(self, RedshiftMessage, handler=self._redshift_listener) - def load_data(self, input, data_label=None, parser=None, - show_in_viewer=True, - cache=None, local_path=None, timeout=None): + def load_data(self, input, data_label=None, + resolver=None, parser=None, loader=None, + resolver_kwargs={}, parser_kwargs={}, loader_kwargs={}, + show_in_viewer=True): """ Load data into Specviz. @@ -57,44 +58,31 @@ def load_data(self, input, data_label=None, parser=None, Spectrum1D, SpectrumList, or path to compatible data file. data_label : str The Glue data label found in the ``DataCollection``. - parser : str - The name of the parser to use to load the data. Will automatically - be determined if only a single registered parser is capable of parsing - ``input``. show_in_viewer : bool Show data in viewer(s). - cache : None, bool, or str - Cache the downloaded file if the data are retrieved by a query - to a URL or URI. - local_path : str, optional - Cache remote files to this path. This is only used if data is - requested from `astroquery.mast`. - timeout : float, optional - If downloading from a remote URI, set the timeout limit for - remote requests in seconds (passed to - `~astropy.utils.data.download_file` or - `~astroquery.mast.Conf.timeout`). """ from jdaviz.core import parsers - if isinstance(input, str): - path = pathlib.Path(input) - if not path.is_file() and not path.is_dir(): - input = download_uri_to_path(input, cache=cache, - local_path=local_path, timeout=timeout) + data_labels = [] + inputs = input if isinstance(input, list) else [input] - parser = parsers.parse(input, parser=parser) - if not self._dev_deconfig and parser.registry_name != '1D Spectrum': - raise NotImplementedError("Only Spectrum1D data is supported in Specviz.") + for input in inputs: + objects, loader = parsers.parse(input, + resolver=resolver, parser=parser, loader=loader, + resolver_kwargs=resolver_kwargs, + parser_kwargs=parser_kwargs, + loader_kwargs=loader_kwargs) - if data_label is None: - data_label = parser.default_data_label + if not self._dev_deconfig and loader.registry_name != '1D Spectrum': + raise NotImplementedError("Only Spectrum1D data is supported in Specviz.") - data_labels = [] - for data in parser.glue_data: - data_label = self.app.return_unique_name(data_label) - data_labels.append(data_label) - self.app.add_data(data, data_label) + if data_label is None: + data_label = getattr(loader, 'default_data_label', 'data') + + for data in objects: + data_label = self.app.return_unique_name(data_label) + data_labels.append(data_label) + self.app.add_data(data, data_label) if show_in_viewer: for data_label in data_labels: diff --git a/jdaviz/core/parsers.py b/jdaviz/core/parsers.py index 9b030724d0..1e73c0efc8 100644 --- a/jdaviz/core/parsers.py +++ b/jdaviz/core/parsers.py @@ -1,173 +1,192 @@ +import os +#import numpy as np from functools import cached_property -from jdaviz.core.registries import parser_registry +from jdaviz.core.registries import resolver_registry, parser_registry, loader_registry +from jdaviz.utils import download_uri_to_path from specutils import Spectrum1D -from specutils.io.registers import identify_spectrum_format __all__ = ['parse'] -class BaseParser: - registry_name = None # populated by parser_registry decorator +""" +Three registries for 3 steps: +1. Resolver: for resolving string to a path or object(s) + input: string + output: path to file or directory or object itself (skip parsing step) +2. Parser: for parsing a file into an object(s) + input: string or path to file or directory + output: python object +3. Loader: for manipulating the object(s) and passing to glue (potentially in a loop) + input: python object + output: glue data or python object able to directly be input to glue parsers +""" - def __init__(self, input, shared_info={}): - self.shared_info = shared_info # dictionary of parsed objects to be re-used between parsers + +class BaseParsingStep: + def __init__(self, input): self._input = input - # trigger the cached_property - _ = self.can_parse @property def input(self): return self._input - @cached_property - def default_data_label(self): - # optional override by subclass - # has access to self.input and anything stored by can_parse - return self.registry_name +# def validate_call_kwargs(self, kwargs): +# # hmmm not sure where this belongs (in original is_valid or while calling) +# import inspect +# allowed_kwargs = inspect.signature(self.__call__).parameters.keys() +# valid = np.all([k in allowed_kwargs for k in kwargs]) +# if not valid: +# raise ValueError(f"Invalid keyword arguments passed to __call__, allowed: {allowed_kwargs}") - @cached_property - def default_viewer(self): + @property + def is_valid(self): # override by subclass - # has access to self.input and anything stored by can_parse - return 'scatter' - - @cached_property - def can_parse(self): - # override by subclass, ONLY use self.input - # anything that needs to be opened can be stored internally for other methods (i.e. _input_to_object) return False - def _input_to_object(self): - # override by subclass - # has access to self.input and anything stored by can_parse - # can return single object or list (in which case _object_to_glue_data should also handle) + def __call__(self): return self.input - @cached_property - def object(self): - if not self.can_parse: - raise ValueError("Cannot parse input") - return self._input_to_object() - def _object_to_glue_data(self): - # override by subclass - # has access to self.input and self.object and can return single entry or list - return self.object +class BaseParsingStepLoader(BaseParsingStep): + @property + def default_data_label(self): + return self.registry_name - @cached_property - def glue_data(self): - if not self.can_parse: - raise ValueError("Cannot parse input") - data = self._object_to_glue_data() - if not isinstance(data, list): - data = [data] - return data +class ParsingStepSearch: + valid = {} + + def __init__(self, registry, parser, input, kwargs): + self._step = registry._step + self._input = input + self._kwargs = kwargs + + if isinstance(parser, BaseParsingStep): + all_parsers = {parser.__name__: parser} + if isinstance(parser, str): + if parser not in registry.members: + raise ValueError(f"\'{parser}\' not one of {list(registry.members.keys())}") + all_parsers = {parser: registry.members.get(parser)} + else: + all_parsers = registry.members + self.all_parsers = all_parsers -@parser_registry('1D Spectrum') -class Spectrum1DParser(BaseParser): - def default_viewer(self): - return 'spectrum-viewer' + # loop through all registered parsers to see which can parse the file + valid_parsers = {} + for parser_name, Parser in all_parsers.items(): + this_parser = Parser(input) + if this_parser.is_valid: + valid_parsers[parser_name] = this_parser + self.valid_parsers = valid_parsers @cached_property - def can_parse(self): - if 'Spectrum1D' in self.shared_info: - pass - elif isinstance(self.input, Spectrum1D): - self.shared_info['Spectrum1D'] = self.input - if isinstance(self.input, str): - if 'specutils_format' not in self.shared_info: - try: - format = identify_spectrum_format(self.input, Spectrum1D) - except ValueError: - return False - else: - self.shared_info['specutils_format'] = format - if self.shared_info['specutils_format'] in ('JWST x1d',): - self.shared_info['Spectrum1D'] = Spectrum1D.read(self.input) - else: - return False - else: - return False - return self.shared_info['Spectrum1D'].flux.ndim == 1 + def single_match(self): + if not len(self.valid_parsers): + all_parsers_str = ', '.join(list(self.all_parsers.keys())) + raise ValueError(f"No valid {self._step}s found for input, tried {all_parsers_str}.") + if len(self.valid_parsers) > 1: + valid_parsers_str = ', '.join(list(self.valid_parsers.keys())) + raise ValueError(f"Multiple valid {self._step}s found, please pass {self._step} as one of: {valid_parsers_str}") # noqa + return list(self.valid_parsers.values())[0] + + def __call__(self): + print(f"{self._step} using {self.single_match.registry_name}") + return self.single_match(**self._kwargs) + + +@resolver_registry('Local Path') +class LocalFileResolver(BaseParsingStep): + @property + def is_valid(self): + return os.path.exists(self.input) - def _input_to_object(self): - # has access to self.input and anything stored by can_parse - return self.shared_info['Spectrum1D'] + def __call__(self): + return self.input -@parser_registry('2D Spectrum') -class Spectrum2DParser(BaseParser): - def default_viewer(self): - return 'spectrum-2d-viewer' +@resolver_registry('URL') +class URLResolver(BaseParsingStep): + @property + def is_valid(self): + return not os.path.exists(self.input) - @cached_property - def can_parse(self): - if 'Spectrum1D' in self.shared_info: - pass - elif isinstance(self.input, str): - if 'specutils_format' not in self.shared_info: - try: - format = identify_spectrum_format(self.input, Spectrum1D) - except ValueError: - return False - else: - self.shared_info['specutils_format'] = format - if self.shared_info['specutils_format'] in ('JWST s2d',): - self.shared_info['Spectrum1D'] = Spectrum1D.read(self.input) - else: - return False - elif isinstance(self.input, Spectrum1D): - self.shared_info['Spectrum1D'] = self.input - else: + def __call__(self, cache=True, local_path=None, timeout=60): + return download_uri_to_path(self.input, cache=cache, + local_path=local_path, timeout=timeout) + + +@parser_registry('specutils.Spectrum') +class SpecutilsSpectrumParser(BaseParsingStep): + def is_valid(self): + if isinstance(self.input, Spectrum1D): + return True + if not isinstance(self.input, str): + return False + try: + self.spectrum1d + except Exception as e: + print("spectrum1d read failed", str(e)) return False - return self.shared_info['Spectrum1D'].flux.ndim == 2 + return True - def _input_to_object(self): - # has access to self.input and anything stored by can_parse - return self.shared_info['Spectrum1D'] + @cached_property + def spectrum1d(self): + return Spectrum1D.read(self.input) + def __call__(self): + if isinstance(self.input, Spectrum1D): + return self.input + return self.spectrum1d -@parser_registry('2D Spectral Trace') -class SpectralTraceParser(BaseParser): - def default_viewer(self): - return 'spectrum-2d-viewer' - @cached_property - def can_parse(self): +@loader_registry('1D Spectrum') +class Spectrum1DLoader(BaseParsingStepLoader): + @property + def is_valid(self): + return isinstance(self.input, Spectrum1D) and self.input.flux.ndim == 1 + + +@loader_registry('2D Spectrum') +class Spectrum2DLoader(BaseParsingStepLoader): + @property + def is_valid(self): + return isinstance(self.input, Spectrum1D) and self.input.flux.ndim == 2 + + +@loader_registry('Specreduce Trace') +class SpecreduceTraceLoader(BaseParsingStepLoader): + @property + def is_valid(self): from specreduce.tracing import Trace return isinstance(self.input, Trace) + @property + def default_data_label(self): + return 'Trace' + + +def parse(input, + resolver=None, parser=None, loader=None, + resolver_kwargs={}, parser_kwargs={}, loader_kwargs={}): + + if isinstance(input, str) or resolver is not None: + resolver_search = ParsingStepSearch(resolver_registry, resolver, input, resolver_kwargs) + input = resolver_search() + + if isinstance(input, str) or parser is not None: + parser_search = ParsingStepSearch(parser_registry, parser, input, parser_kwargs) + input = parser_search() + + loader_search = ParsingStepSearch(loader_registry, loader, input, loader_kwargs) + # need access to the loader itself for viewer/data_label defaults + loader = loader_search.single_match + # identical to loader(**loader_kwargs) + objects = loader_search() + if not isinstance(objects, list): + objects = [objects] + return objects, loader + -def parse(input, parser=None): - # if input is a string, but not path/file, then download first - - # by default, search through all registered parsers, but also allow - # passing a Parser class or name of registered parser. - # could also be extended to allow pre-parsing of MOS data - if isinstance(parser, BaseParser): - all_parsers = {parser.__name__: parser} - if isinstance(parser, str): - if parser not in parser_registry.members: - raise ValueError(f"\'{parser}\' not one of {list(parser_registry.members.keys())}") - all_parsers = {parser: parser_registry.members.get(parser)} - else: - all_parsers = parser_registry.members - - # loop through all registered parsers to see which can parse the file - valid_parsers = {} - shared_info = {} - for parser_name, Parser in all_parsers.items(): - this_parser = Parser(input, shared_info) - shared_info.update(this_parser.shared_info) - if this_parser.can_parse: - valid_parsers[parser_name] = this_parser - if not len(valid_parsers): - all_parsers_str = ', '.join(list(all_parsers.keys())) - raise ValueError(f"No valid parsers found for input, tried {all_parsers_str}.") - if len(valid_parsers) > 1: - valid_parsers_str = ', '.join(list(valid_parsers.keys())) - raise ValueError(f"Multiple valid parsers found, please pass parser as one of: {valid_parsers_str}") # noqa - return list(valid_parsers.values())[0] \ No newline at end of file +# TODO: concept of "only_if_requested" for spectrum2d loaded as list \ No newline at end of file diff --git a/jdaviz/core/registries.py b/jdaviz/core/registries.py index 1b69d242f4..f0f5a1c834 100644 --- a/jdaviz/core/registries.py +++ b/jdaviz/core/registries.py @@ -8,7 +8,8 @@ __all__ = ['convert', 'UniqueDictRegistry', 'ViewerRegistry', 'TrayRegistry', 'ToolRegistry', 'MenuRegistry', 'DataParserRegistry', 'viewer_registry', 'tray_registry', 'tool_registry', 'menu_registry', - 'data_parser_registry', 'parser_registry'] + 'data_parser_registry', + 'resolver_registry', 'parser_registry', 'loader_registry'] def _to_snake(s): @@ -221,9 +222,13 @@ def decorator(func): return decorator -class ParserRegistry(UniqueDictRegistry): +class ParserStepRegistry(UniqueDictRegistry): """Registry containing data parsing classes """ + def __init__(self, *args, **kwargs): + self._step = kwargs.pop('step') + super().__init__(*args, **kwargs) + def __call__(self, name=None): def decorator(cls): cls.registry_name = name @@ -236,5 +241,7 @@ def decorator(cls): tray_registry = TrayRegistry() tool_registry = ToolRegistry() menu_registry = MenuRegistry() -data_parser_registry = DataParserRegistry() -parser_registry = ParserRegistry() # replacement for data_parser_registry as part of deconfigging +data_parser_registry = DataParserRegistry() # remove once deconfigging is completed +resolver_registry = ParserStepRegistry(step='resolver') +parser_registry = ParserStepRegistry(step='parser') +loader_registry = ParserStepRegistry(step='loader') \ No newline at end of file From ad9fce34a5d0c44cd45684ec6de52c913b524cd1 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Fri, 24 Jan 2025 10:19:27 -0500 Subject: [PATCH 14/14] basic SpectrumList support --- jdaviz/core/parsers.py | 83 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 68 insertions(+), 15 deletions(-) diff --git a/jdaviz/core/parsers.py b/jdaviz/core/parsers.py index 1e73c0efc8..fc1db2b55b 100644 --- a/jdaviz/core/parsers.py +++ b/jdaviz/core/parsers.py @@ -5,7 +5,7 @@ from jdaviz.core.registries import resolver_registry, parser_registry, loader_registry from jdaviz.utils import download_uri_to_path -from specutils import Spectrum1D +from specutils import Spectrum1D, SpectrumList __all__ = ['parse'] @@ -54,6 +54,12 @@ class BaseParsingStepLoader(BaseParsingStep): def default_data_label(self): return self.registry_name + @property + def default_viewer(self): + # returns the registry name of the default viewer + # only used if `show_in_viewer=True` and no existing viewers can accept the data + return 'specviz-profile-viewer' + class ParsingStepSearch: valid = {} @@ -63,8 +69,10 @@ def __init__(self, registry, parser, input, kwargs): self._input = input self._kwargs = kwargs - if isinstance(parser, BaseParsingStep): - all_parsers = {parser.__name__: parser} + # TODO: could eventually allow passing unregistered parser classes/objects + # which could then be used to allow pre-parsing of MOS data (selecting individual rows) +# if isinstance(parser, BaseParsingStep): +# all_parsers = {parser.__name__: parser} if isinstance(parser, str): if parser not in registry.members: raise ValueError(f"\'{parser}\' not one of {list(registry.members.keys())}") @@ -96,6 +104,8 @@ def __call__(self): return self.single_match(**self._kwargs) +### RESOLVERS (string -> path or object(s)) + @resolver_registry('Local Path') class LocalFileResolver(BaseParsingStep): @property @@ -117,29 +127,41 @@ def __call__(self, cache=True, local_path=None, timeout=60): local_path=local_path, timeout=timeout) +### PARSERS (path -> object(s)) + @parser_registry('specutils.Spectrum') class SpecutilsSpectrumParser(BaseParsingStep): + SpecutilsCls = Spectrum1D + + @property def is_valid(self): - if isinstance(self.input, Spectrum1D): - return True - if not isinstance(self.input, str): - return False try: - self.spectrum1d + self.object except Exception as e: - print("spectrum1d read failed", str(e)) + print(f"{self.SpecutilsCls.__name__} read failed", str(e)) return False return True @cached_property - def spectrum1d(self): - return Spectrum1D.read(self.input) + def object(self): + if isinstance(self.input, self.SpecutilsCls): + return self.input + return self.SpecutilsCls.read(self.input) def __call__(self): - if isinstance(self.input, Spectrum1D): - return self.input - return self.spectrum1d + return self.object + + +@parser_registry('specutils.SpectrumList') +class SpecutilsSpectrumListParser(SpecutilsSpectrumParser): + SpecutilsCls = SpectrumList + + @property + def is_valid(self): + return super().is_valid and len(self.object) > 1 + +### LOADERS (object(s) -> object(s) ready for ingesting in glue) @loader_registry('1D Spectrum') class Spectrum1DLoader(BaseParsingStepLoader): @@ -155,6 +177,37 @@ def is_valid(self): return isinstance(self.input, Spectrum1D) and self.input.flux.ndim == 2 +@loader_registry('1D Spectrum List') +class Spectrum1DListLoader(BaseParsingStepLoader): + @property + def is_valid(self): + # TODO: should this be split into two loaders? + # should a loader take a single input type, output a single output type, or just have a consistent data_label and viewer? + return isinstance(self.input, SpectrumList) or (isinstance(self.input, Spectrum1D) and self.input.flux.ndim == 2) + + def __call__(self): + if isinstance(self.input, SpectrumList): + return self.input + elif isinstance(self.input, Spectrum1D): + def this_row(field, i): + if field is None: + return None + return field[i, :] + + return SpectrumList([Spectrum1D(spectral_axis=self.input.spectral_axis, + flux=this_row(self.input.flux, i), + uncertainty=this_row(self.input.uncertainty, i), + mask=this_row(self.input.mask, i), + meta=self.input.meta) + for i in range(self.input.flux.shape[0])]) + else: + raise NotImplementedError() + + @property + def default_data_label(self): + return '1D Spectrum' + + @loader_registry('Specreduce Trace') class SpecreduceTraceLoader(BaseParsingStepLoader): @property @@ -189,4 +242,4 @@ def parse(input, return objects, loader -# TODO: concept of "only_if_requested" for spectrum2d loaded as list \ No newline at end of file +# TODO: concept of "only_if_requested"/priority/enabled for spectrum2d loaded as list \ No newline at end of file