From 683d989cc3cd3a3cb6cc73b2026c47651cb34b73 Mon Sep 17 00:00:00 2001 From: Duy Tuong Nguyen Date: Fri, 7 Jul 2023 15:03:05 -0400 Subject: [PATCH] Jdaviz Launcher: Identify compatible configs and request user to select config (#2267) * Prep launch method without autodetect * Identify config before launching * Codestyle * Support "empty" filepaths * Filepath fallback when autoconfig doesn't need to open file * Move open to launcher module * Missing list encapsulation * Changelog * Combine changelogs --- CHANGES.rst | 3 +- jdaviz/__init__.py | 2 +- jdaviz/cli.py | 4 +- jdaviz/core/data_formats.py | 70 ++++--------------- jdaviz/core/launcher.py | 129 +++++++++++++++++++++++++++++------- 5 files changed, 124 insertions(+), 84 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 484bde6a45..19194b37fb 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -13,7 +13,8 @@ New Features - The ``specviz.load_spectrum`` method is deprecated; use ``specviz.load_data`` instead. [#2273] -- Add first-pass launcher to select config and auto-identify data. [#2257] +- Add launcher to select and identify compatible configurations, + and require --layout argument when launching standalone. [#2257, #2267] Cubeviz ^^^^^^^ diff --git a/jdaviz/__init__.py b/jdaviz/__init__.py index fdddfd2e84..8553c6671e 100644 --- a/jdaviz/__init__.py +++ b/jdaviz/__init__.py @@ -19,7 +19,7 @@ from jdaviz.configs.cubeviz import Cubeviz # noqa: F401 from jdaviz.configs.imviz import Imviz # noqa: F401 from jdaviz.utils import enable_hot_reloading # noqa: F401 -from jdaviz.core.data_formats import open # noqa: F401 +from jdaviz.core.launcher import open # noqa: F401 # Clean up namespace. del os diff --git a/jdaviz/cli.py b/jdaviz/cli.py index 9fd87e1f96..755dd825ab 100644 --- a/jdaviz/cli.py +++ b/jdaviz/cli.py @@ -19,6 +19,7 @@ JDAVIZ_DIR = pathlib.Path(__file__).parent.resolve() DEFAULT_VERBOSITY = 'warning' DEFAULT_HISTORY_VERBOSITY = 'info' +ALL_JDAVIZ_CONFIGS = ['cubeviz', 'specviz', 'specviz2d', 'mosviz', 'imviz'] def main(filepaths=None, layout='default', instrument=None, browser='default', @@ -119,8 +120,7 @@ def _main(config=None): 'loaded from FILENAME.') filepaths_nargs = '*' if config is None: - parser.add_argument('--layout', default='', choices=['cubeviz', 'specviz', 'specviz2d', - 'mosviz', 'imviz'], + parser.add_argument('--layout', default='', choices=ALL_JDAVIZ_CONFIGS, help='Configuration to use.') if (config == "mosviz") or ("mosviz" in sys.argv): filepaths_nargs = 1 diff --git a/jdaviz/core/data_formats.py b/jdaviz/core/data_formats.py index 6ede5130a9..32f464f704 100644 --- a/jdaviz/core/data_formats.py +++ b/jdaviz/core/data_formats.py @@ -11,8 +11,6 @@ from stdatamodels import asdf_in_fits from jdaviz.core.config import list_configurations -from jdaviz import configs as jdaviz_configs -from jdaviz.cli import DEFAULT_VERBOSITY, DEFAULT_HISTORY_VERBOSITY __all__ = [ 'guess_dimensionality', @@ -156,8 +154,8 @@ def identify_helper(filename, ext=1): Returns ------- - helper_name : str - Name of the best-guess helper for ``filename``. + helper_name : list of str + Name of the best-guess compatible helpers for ``filename``. Fits HDUList : astropy.io.fits.HDUList The HDUList of the file opened to identify the helper @@ -172,7 +170,7 @@ def identify_helper(filename, ext=1): if filename.lower().endswith('asdf'): # ASDF files are only supported in jdaviz for # Roman WFI 2D images, so suggest imviz: - return ('imviz', None) + return (['imviz'], None) # Must use memmap=False to force close all handles and allow file overwrite hdul = fits.open(filename, memmap=False) @@ -208,10 +206,10 @@ def identify_helper(filename, ext=1): # could be 2D spectrum or 2D image. break tie with WCS: if has_spectral_axis: if n_axes > 1: - return ('specviz2d', hdul) - return ('specviz', hdul) + return (['specviz2d'], hdul) + return (['specviz'], hdul) elif not isinstance(data, fits.BinTableHDU): - return ('imviz', hdul) + return (['imviz'], hdul) # Ensure specviz is chosen when ``data`` is a table or recarray # and there's a "known" spectral column name: @@ -237,7 +235,7 @@ def identify_helper(filename, ext=1): # if at least one spectral column is found: if sum(found_spectral_columns): - return ('specviz', hdul) + return (['specviz'], hdul) # If the data could be spectral: for cls in [Spectrum1D, SpectrumList]: @@ -247,10 +245,10 @@ def identify_helper(filename, ext=1): # first catch known JWST spectrum types: if (n_axes == 3 and recognized_spectrum_format.find('s3d') > -1): - return ('cubeviz', hdul) + return (['cubeviz'], hdul) elif (n_axes == 2 and recognized_spectrum_format.find('x1d') > -1): - return ('specviz', hdul) + return (['specviz'], hdul) # we intentionally don't choose specviz2d for # data recognized as 's2d' as we did with the cases above, @@ -260,62 +258,22 @@ def identify_helper(filename, ext=1): # Use WCS to break the tie below: elif n_axes == 2: if has_spectral_axis: - return ('specviz2d', hdul) - return ('imviz', hdul) + return (['specviz2d'], hdul) + return (['imviz'], hdul) elif n_axes == 1: - return ('specviz', hdul) + return (['specviz'], hdul) try: # try using the specutils registry: valid_format, config = identify_data(filename) - return (config, hdul) + return ([config], hdul) except ValueError: # if file type not recognized: pass if n_axes == 2 and not has_spectral_axis: # at this point, non-spectral 2D data are likely images: - return ('imviz', hdul) + return (['imviz'], hdul) raise ValueError(f"No helper could be auto-identified for {filename}.") - - -def open(filename, show=True, **kwargs): - ''' - Automatically detect the correct configuration based on a given file, - load the data, and display the configuration - - Parameters - ---------- - filename : str (path-like) - Name for a local data file. - show : bool - Determines whether to immediately show the application - - All other arguments are interpreted as load_data arguments for - the autoidentified configuration class - - Returns - ------- - Jdaviz ConfigHelper : jdaviz.core.helpers.ConfigHelper - The autoidentified ConfigHelper for the given data - ''' - # Identify the correct config - helper_str, hdul = identify_helper(filename) - viz_class = getattr(jdaviz_configs, helper_str.capitalize()) - - # Create config instance - verbosity = kwargs.pop('verbosity', DEFAULT_VERBOSITY) - history_verbosity = kwargs.pop('history_verbosity', DEFAULT_HISTORY_VERBOSITY) - viz_helper = viz_class(verbosity=verbosity, history_verbosity=history_verbosity) - - # Load data - data = hdul if (hdul is not None) else filename - viz_helper.load_data(data, **kwargs) - - # Display app - if show: - viz_helper.show() - - return viz_helper diff --git a/jdaviz/core/launcher.py b/jdaviz/core/launcher.py index f079b72582..fc2fd7b093 100644 --- a/jdaviz/core/launcher.py +++ b/jdaviz/core/launcher.py @@ -2,7 +2,76 @@ from ipywidgets import jslink from jdaviz import configs as jdaviz_configs -from jdaviz.core.data_formats import open as jdaviz_open +from jdaviz.cli import DEFAULT_VERBOSITY, DEFAULT_HISTORY_VERBOSITY, ALL_JDAVIZ_CONFIGS +from jdaviz.core.data_formats import identify_helper + + +def open(filename, show=True, **kwargs): + ''' + Automatically detect the correct configuration based on a given file, + load the data, and display the configuration + + Parameters + ---------- + filename : str (path-like) + Name for a local data file. + show : bool + Determines whether to immediately show the application + + All other arguments are interpreted as load_data/load_spectrum arguments for + the autoidentified configuration class + + Returns + ------- + Jdaviz ConfigHelper : jdaviz.core.helpers.ConfigHelper + The autoidentified ConfigHelper for the given data + ''' + # Identify the correct config + compatible_helpers, hdul = identify_helper(filename) + if len(compatible_helpers) > 1: + raise NotImplementedError(f"Multiple helpers provided: {compatible_helpers}." + "Unsure which to launch") + else: + return _launch_config_with_data(compatible_helpers[0], hdul, show, **kwargs) + + +def _launch_config_with_data(config, data=None, show=True, **kwargs): + ''' + Launch jdaviz with a specific, known configuration and data + + Parameters + ---------- + config : str (path-like) + Name for a local data file. + data : str or any Jdaviz-compatible data + A filepath or Jdaviz-compatible data object (such as Spectrum1D or CCDData) + show : bool + Determines whether to immediately show the application + + All other arguments are interpreted as load_data/load_spectrum arguments for + the autoidentified configuration class + + Returns + ------- + Jdaviz ConfigHelper : jdaviz.core.helpers.ConfigHelper + The loaded ConfigHelper with data loaded + ''' + viz_class = getattr(jdaviz_configs, config.capitalize()) + + # Create config instance + verbosity = kwargs.pop('verbosity', DEFAULT_VERBOSITY) + history_verbosity = kwargs.pop('history_verbosity', DEFAULT_HISTORY_VERBOSITY) + viz_helper = viz_class(verbosity=verbosity, history_verbosity=history_verbosity) + + # Load data + if data not in (None, ''): + viz_helper.load_data(data, **kwargs) + + # Display app + if show: + viz_helper.show() + + return viz_helper def show_launcher(configs=['imviz', 'specviz', 'mosviz', 'cubeviz', 'specviz2d']): @@ -15,37 +84,49 @@ def show_launcher(configs=['imviz', 'specviz', 'mosviz', 'cubeviz', 'specviz2d'] children=['Welcome to Jdaviz']) intro_row.children = [welcome_text] - # Filepath row - filepath_row = v.Row() - text_field = v.TextField(label="File Path", v_model=None) - - def load_file(filepath): - if filepath: - helper = jdaviz_open(filepath, show=False) - main.children = [helper.app] - - open_data_btn = v.Btn(class_="ma-2", outlined=True, color="primary", - children=[v.Icon(children=["mdi-upload"])]) - open_data_btn.on_event('click', lambda btn, event, data: load_file(btn.value)) - jslink((text_field, 'v_model'), (open_data_btn, 'value')) - - filepath_row.children = [text_field, open_data_btn] - # Config buttons - def create_config(config): - viz_class = getattr(jdaviz_configs, config.capitalize()) - main.children = [viz_class().app] + def create_config(config, data=None): + helper = _launch_config_with_data(config, data, show=False) + main.children = [helper.app] - btns = [] + btns = {} + loaded_data = None for config in configs: config_btn = v.Btn(class_="ma-2", outlined=True, color="primary", children=[config.capitalize()]) - config_btn.on_event('click', lambda btn, event, data: create_config(btn.children[0])) - btns.append(config_btn) + config_btn.on_event('click', lambda btn, event, data: create_config(btn.children[0], + loaded_data)) + btns[config] = config_btn # Create button row btn_row = v.Row() - btn_row.children = btns + btn_row.children = list(btns.values()) + + # Filepath row + filepath_row = v.Row() + text_field = v.TextField(label="File Path", v_model=None) + + def enable_compatible_configs(filepath): + nonlocal loaded_data + if filepath in (None, ''): + compatible_helpers = ALL_JDAVIZ_CONFIGS + loaded_data = None + else: + compatible_helpers, loaded_data = identify_helper(filepath) + if len(compatible_helpers) > 0 and loaded_data is None: + loaded_data = filepath + + for config, btn in btns.items(): + btn.disabled = not (config in compatible_helpers) + + id_data_btn = v.Btn(class_="ma-2", outlined=True, color="primary", + children=[v.Icon(children=["mdi-magnify"])]) + id_data_btn.on_event('click', lambda btn, event, data: enable_compatible_configs(btn.value)) + jslink((text_field, 'v_model'), (id_data_btn, 'value')) + + filepath_row.children = [text_field, id_data_btn] + + # Create Launcher main.children = [intro_row, filepath_row, btn_row] return main