Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Jdaviz Launcher: Identify compatible configs and request user to select config #2267

Merged
merged 9 commits into from
Jul 7, 2023
Merged
3 changes: 2 additions & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
^^^^^^^
Expand Down
2 changes: 1 addition & 1 deletion jdaviz/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions jdaviz/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,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',
Expand Down Expand Up @@ -116,8 +117,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
Expand Down
70 changes: 14 additions & 56 deletions jdaviz/core/data_formats.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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:
Expand All @@ -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]:
Expand All @@ -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,
Expand All @@ -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
129 changes: 105 additions & 24 deletions jdaviz/core/launcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']):
Expand All @@ -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