diff --git a/.github/workflows/ci_cron_weekly.yml b/.github/workflows/ci_cron_weekly.yml index 664dea7664..e25f229d73 100644 --- a/.github/workflows/ci_cron_weekly.yml +++ b/.github/workflows/ci_cron_weekly.yml @@ -54,3 +54,24 @@ jobs: python -m pip install tox - name: Test with tox run: tox -e py311-test-devdeps-romandeps + + ci_cron_tests_stable_strauss: + name: Python 3.12 with stable versions of dependencies and Strauss + runs-on: ubuntu-latest + if: (github.repository == 'spacetelescope/jdaviz' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || contains(github.event.pull_request.labels.*.name, 'Extra CI'))) + steps: + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + - name: Set up python + uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 + with: + python-version: '3.12' + - name: Install base dependencies + run: | + sudo apt-get install libportaudio2 + python -m pip install --upgrade pip + python -m pip install tox + - name: Test with tox + run: tox -e py312-test-straussdeps diff --git a/CHANGES.rst b/CHANGES.rst index 01e6579375..220f04c671 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -11,6 +11,8 @@ New Features Cubeviz ^^^^^^^ +- Add Sonify Data plugin which uses the Strauss package to turn a data cube into sound. [#3269] + Imviz ^^^^^ diff --git a/docs/cubeviz/plugins.rst b/docs/cubeviz/plugins.rst index 3cbf2cba16..fb24f0727c 100644 --- a/docs/cubeviz/plugins.rst +++ b/docs/cubeviz/plugins.rst @@ -338,6 +338,19 @@ have valid flux units. For 3D data, the current :ref:`slice` is used. :ref:`Imviz Aperture Photometry ` Imviz documentation describing the concept of aperture photometry in Jdaviz. +.. _cubeviz-sonify-data: + +Sonify Data +=========== + +This plugin uses the `Strauss `_ package +to turn data cubes into audio grids (by pressing the +:guilabel:`Sonify Data` button) that can be played while the spectrum-at-spaxel tool is active +and the mouse is hovering over the flux viewer. A range of the cube can be sonified by creating +and selecting a spectral subset from the :guilabel:`Spectral range` dropdown and then pressing +the :guilabel:`Sonify Data` button. The output device for sound can be changed by using the +:guilabel:`Sound device` dropdown. + .. _cubeviz-export-plot: Export diff --git a/jdaviz/configs/cubeviz/cubeviz.yaml b/jdaviz/configs/cubeviz/cubeviz.yaml index 0df406e8e4..ded85fa5b8 100644 --- a/jdaviz/configs/cubeviz/cubeviz.yaml +++ b/jdaviz/configs/cubeviz/cubeviz.yaml @@ -33,6 +33,7 @@ tray: - specviz-line-analysis - cubeviz-moment-maps - imviz-aper-phot-simple + - cubeviz-sonify-data - export - about viewer_area: diff --git a/jdaviz/configs/cubeviz/plugins/__init__.py b/jdaviz/configs/cubeviz/plugins/__init__.py index 4cc5c65a3a..35b3aba82f 100644 --- a/jdaviz/configs/cubeviz/plugins/__init__.py +++ b/jdaviz/configs/cubeviz/plugins/__init__.py @@ -4,4 +4,5 @@ from .moment_maps.moment_maps import * # noqa from .slice.slice import * # noqa from .spectral_extraction.spectral_extraction import * # noqa +from .sonify_data.sonify_data import * # noqa from .tools import * # noqa diff --git a/jdaviz/configs/cubeviz/plugins/cube_listener.py b/jdaviz/configs/cubeviz/plugins/cube_listener.py new file mode 100644 index 0000000000..d862b78a8b --- /dev/null +++ b/jdaviz/configs/cubeviz/plugins/cube_listener.py @@ -0,0 +1,150 @@ +import numpy as np +from contextlib import contextmanager +import sys +import os +import time + +try: + from strauss.sonification import Sonification + from strauss.sources import Events + from strauss.score import Score + from strauss.generator import Spectralizer +except ImportError: + pass + +# smallest fraction of the max audio amplitude that can be represented by a 16-bit signed integer +MINVOL = 1/(2**15 - 1) + + +@contextmanager +def suppress_stderr(): + with open(os.devnull, "w") as devnull: + old_stderr = sys.stderr + sys.stderr = devnull + try: + yield + finally: + sys.stderr = old_stderr + + +def sonify_spectrum(spec, duration, overlap=0.05, system='mono', srate=44100, fmin=40, fmax=1300, + eln=False): + notes = [["A2"]] + score = Score(notes, duration) + # set up spectralizer generator + generator = Spectralizer(samprate=srate) + + # Lets pick the mapping frequency range for the spectrum... + generator.modify_preset({'min_freq': fmin, 'max_freq': fmax, + 'fit_spec_multiples': False, + 'interpolation_type': 'preserve_power', + 'equal_loudness_normalisation': eln}) + + data = {'spectrum': [spec], 'pitch': [1]} + + # again, use maximal range for the mapped parameters + lims = {'spectrum': ('0', '100')} + + # set up source + sources = Events(data.keys()) + sources.fromdict(data) + sources.apply_mapping_functions(map_lims=lims) + + # render and play sonification! + soni = Sonification(score, sources, generator, system, samprate=srate) + soni.render() + soni._make_seamless(overlap) + + return soni.loop_channels['0'].values + + +class CubeListenerData: + def __init__(self, cube, wlens, samplerate=44100, duration=1, overlap=0.05, buffsize=1024, + bdepth=16, wl_bounds=None, wl_unit=None, audfrqmin=50, audfrqmax=1500, + eln=False, vol=None): + self.siglen = int(samplerate*(duration-overlap)) + self.cube = cube + self.dur = duration + self.bdepth = bdepth + self.srate = samplerate + self.maxval = pow(2, bdepth-1) - 1 + self.fadedx = 0 + + if vol is None: + self.atten_level = 1 + else: + self.atten_level = int(np.clip((vol/100)**2, MINVOL, 1)) + + self.wl_bounds = wl_bounds + self.wl_unit = wl_unit + self.wlens = wlens + + # control fades + fade = np.linspace(0, 1, buffsize+1) + self.ifade = fade[:-1] + self.ofade = fade[::-1][:-1] + + # mapping frequency limits in Hz + self.audfrqmin = audfrqmin + self.audfrqmax = audfrqmax + + # do we normalise for equal loudness? + self.eln = eln + + self.idx1 = 0 + self.idx2 = 0 + self.cbuff = False + self.cursig = np.zeros(self.siglen, dtype='int16') + self.newsig = np.zeros(self.siglen, dtype='int16') + + if self.cursig.nbytes * pow(1024, -3) > 2: + raise Exception("Cube projected to be > 2Gb!") + + self.sigcube = np.zeros((*self.cube.shape[:2], self.siglen), dtype='int16') + + def set_wl_bounds(self, w1, w2): + """ + set the wavelength bounds for indexing spectra + """ + wsrt = np.sort([w1, w2]) + self.wl_bounds = tuple(wsrt) + + def sonify_cube(self): + """ + Iterate through the cube, convert each spectrum to a signal, and store + in class attributes + """ + lo2hi = self.wlens.argsort()[::-1] + + t0 = time.time() + for i in range(self.cube.shape[0]): + for j in range(self.cube.shape[1]): + with suppress_stderr(): + if self.cube[i, j, lo2hi].any(): + sig = sonify_spectrum(self.cube[i, j, lo2hi], self.dur, + srate=self.srate, + fmin=self.audfrqmin, + fmax=self.audfrqmax, + eln=self.eln) + sig = (sig*self.maxval).astype('int16') + self.sigcube[i, j, :] = sig + else: + continue + self.cursig[:] = self.sigcube[self.idx1, self.idx2, :] + self.newsig[:] = self.cursig[:] + t1 = time.time() + print(f"Took {t1-t0}s to process {self.cube.shape[0]*self.cube.shape[1]} spaxels") + + def player_callback(self, outdata, frames, time, status): + cur = self.cursig + new = self.newsig + sdx = int(time.outputBufferDacTime*self.srate) + dxs = np.arange(sdx, sdx+frames).astype(int) % self.sigcube.shape[-1] + if self.cbuff: + outdata[:, 0] = (cur[dxs] * self.ofade).astype('int16') + outdata[:, 0] += (new[dxs] * self.ifade).astype('int16') + self.cursig[:] = self.newsig[:] + self.cbuff = False + else: + outdata[:, 0] = self.cursig[dxs] + outdata[:, 0] //= self.atten_level diff --git a/jdaviz/configs/cubeviz/plugins/sonify_data/__init__.py b/jdaviz/configs/cubeviz/plugins/sonify_data/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py new file mode 100644 index 0000000000..2c14c1479b --- /dev/null +++ b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py @@ -0,0 +1,130 @@ +from traitlets import Bool, List, Unicode, observe +import astropy.units as u + +from jdaviz.core.custom_traitlets import IntHandleEmpty, FloatHandleEmpty +from jdaviz.core.registries import tray_registry +from jdaviz.core.template_mixin import (PluginTemplateMixin, DatasetSelectMixin, + SpectralSubsetSelectMixin, with_spinner) +from jdaviz.core.user_api import PluginUserApi + + +__all__ = ['SonifyData'] + +try: + import strauss # noqa + import sounddevice as sd +except ImportError: + class Empty: + pass + sd = Empty() + sd.default = Empty() + sd.default.device = [-1, -1] + _has_strauss = False +else: + _has_strauss = True + + +@tray_registry('cubeviz-sonify-data', label="Sonify Data", + viewer_requirements=['spectrum', 'image']) +class SonifyData(PluginTemplateMixin, DatasetSelectMixin, SpectralSubsetSelectMixin): + """ + See the :ref:`Sonify Data Plugin Documentation ` for more details. + + Only the following attributes and methods are available through the + :ref:`public plugin API `: + + * :meth:`~jdaviz.core.template_mixin.PluginTemplateMixin.show` + * :meth:`~jdaviz.core.template_mixin.PluginTemplateMixin.open_in_tray` + * :meth:`~jdaviz.core.template_mixin.PluginTemplateMixin.close_in_tray` + """ + template_file = __file__, "sonify_data.vue" + + sample_rate = IntHandleEmpty(44100).tag(sync=True) + buffer_size = IntHandleEmpty(2048).tag(sync=True) + assidx = FloatHandleEmpty(2.5).tag(sync=True) + ssvidx = FloatHandleEmpty(0.65).tag(sync=True) + eln = Bool(False).tag(sync=True) + audfrqmin = FloatHandleEmpty(50).tag(sync=True) + audfrqmax = FloatHandleEmpty(1500).tag(sync=True) + pccut = IntHandleEmpty(20).tag(sync=True) + volume = IntHandleEmpty(100).tag(sync=True) + stream_active = Bool(True).tag(sync=True) + has_strauss = Bool(_has_strauss).tag(sync=True) + + # TODO: can we referesh the list, so sounddevices are up-to-date when dropdown clicked? + sound_devices_items = List().tag(sync=True) + sound_devices_selected = Unicode('').tag(sync=True) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._plugin_description = 'Sonify a data cube' + self.docs_description = 'Sonify a data cube using the Strauss package.' + if not self.has_strauss or sd.default.device[1] < 0: + self.disabled_msg = ('To use Sonify Data, install strauss and restart Jdaviz. You ' + 'can do this by running pip install strauss in the command' + ' line and then launching Jdaviz. Currently, this plugin only' + ' works on devices with valid sound output.') + + else: + devices, indexes = self.build_device_lists() + self.sound_device_indexes = dict(zip(devices, indexes)) + self.sound_devices_items = devices + self.sound_devices_selected = dict(zip(indexes, devices))[sd.default.device[1]] + + # TODO: Remove hardcoded range and flux viewer + self.spec_viewer = self.app.get_viewer('spectrum-viewer') + self.flux_viewer = self.app.get_viewer('flux-viewer') + + @property + def user_api(self): + expose = [] + return PluginUserApi(self, expose) + + @with_spinner() + def vue_sonify_cube(self, *args): + if self.disabled_msg: + raise ValueError('Unable to sonify cube') + + # Get index of selected device + selected_device_index = self.sound_device_indexes[self.sound_devices_selected] + + # Apply spectral subset bounds + if self.spectral_subset_selected is not self.spectral_subset.default_text: + display_unit = self.spec_viewer.state.x_display_unit + min_wavelength = self.spectral_subset.selected_obj.lower.to_value(u.Unit(display_unit)) + max_wavelength = self.spectral_subset.selected_obj.upper.to_value(u.Unit(display_unit)) + self.flux_viewer.update_listener_wls(min_wavelength, max_wavelength, display_unit) + + self.flux_viewer.get_sonified_cube(self.sample_rate, self.buffer_size, + selected_device_index, self.assidx, self.ssvidx, + self.pccut, self.audfrqmin, + self.audfrqmax, self.eln) + + # Automatically select spectrum-at-spaxel tool + spec_at_spaxel_tool = self.flux_viewer.toolbar.tools['jdaviz:spectrumperspaxel'] + self.flux_viewer.toolbar.active_tool = spec_at_spaxel_tool + + def vue_start_stop_stream(self, *args): + self.stream_active = not self.stream_active + self.flux_viewer.stream_active = not self.flux_viewer.stream_active + + @observe('volume') + def update_volume_level(self, event): + self.flux_viewer.update_volume_level(event['new']) + + @observe('sound_devices_selected') + def update_sound_device(self, event): + if event['new'] != event['old']: + didx = dict(zip(*self.build_device_lists()))[event['new']] + self.flux_viewer.update_sound_device(didx) + + def build_device_lists(self): + # dedicated function to build the current *output* + # device and index lists + devices = [] + device_indexes = [] + for index, device in enumerate(sd.query_devices()): + if device['max_output_channels'] > 0 and device['name'] not in devices: + devices.append(device['name']) + device_indexes.append(index) + return devices, device_indexes diff --git a/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.vue b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.vue new file mode 100644 index 0000000000..6443eedde0 --- /dev/null +++ b/jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.vue @@ -0,0 +1,152 @@ + \ No newline at end of file diff --git a/jdaviz/configs/cubeviz/plugins/sonify_data/tests/__init__.py b/jdaviz/configs/cubeviz/plugins/sonify_data/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jdaviz/configs/cubeviz/plugins/sonify_data/tests/test_sonify_data.py b/jdaviz/configs/cubeviz/plugins/sonify_data/tests/test_sonify_data.py new file mode 100644 index 0000000000..0689f4ca96 --- /dev/null +++ b/jdaviz/configs/cubeviz/plugins/sonify_data/tests/test_sonify_data.py @@ -0,0 +1,47 @@ +import os + +import astropy.units as u +import pytest +from specutils import SpectralRegion + +pytest.importorskip("strauss") +IN_GITHUB_ACTIONS = os.environ.get("CI", "false") == "true" + + +@pytest.mark.skipif(IN_GITHUB_ACTIONS, reason="Test requires computer with audio output.") +def test_sonify_data(cubeviz_helper, spectrum1d_cube_larger): + cubeviz_helper.load_data(spectrum1d_cube_larger, data_label="test") + sonify_plg = cubeviz_helper.app.get_tray_item_from_name('cubeviz-sonify-data') + assert sonify_plg.stream_active + + # Create sonified data cube + sonify_plg.vue_sonify_cube() + assert sonify_plg.flux_viewer.sonification_wl_bounds is None + assert sonify_plg.flux_viewer.sonified_cube is not None + + # Test changing volume + sonify_plg.volume = 90 + assert sonify_plg.flux_viewer.volume_level == 90 + + # Test using spectral subset for setting sonification bounds + spec_region = SpectralRegion(4.62360028e-07*u.m, 4.62920561e-07*u.m) + subset_plugin = cubeviz_helper.plugins['Subset Tools']._obj + subset_plugin.import_region(spec_region) + sonify_plg.spectral_subset_selected = 'Subset 1' + sonify_plg.vue_sonify_cube() + assert sonify_plg.flux_viewer.sonification_wl_bounds == (4.62360028e-07, 4.62920561e-07) + + # Stop/start stream + sonify_plg.vue_start_stop_stream() + assert sonify_plg.flux_viewer.stream_active is False + sonify_plg.vue_start_stop_stream() + assert sonify_plg.flux_viewer.stream_active + + +@pytest.mark.skipif(not IN_GITHUB_ACTIONS, reason="Plugin disabled only in CI") +def test_sonify_data_disabled(cubeviz_helper, spectrum1d_cube_larger): + cubeviz_helper.load_data(spectrum1d_cube_larger, data_label="test") + sonify_plg = cubeviz_helper.app.get_tray_item_from_name('cubeviz-sonify-data') + assert sonify_plg.disabled_msg + with pytest.raises(ValueError, match='Unable to sonify cube'): + sonify_plg.vue_sonify_cube() diff --git a/jdaviz/configs/cubeviz/plugins/tools.py b/jdaviz/configs/cubeviz/plugins/tools.py index d43c5e51a5..d49660ba9c 100644 --- a/jdaviz/configs/cubeviz/plugins/tools.py +++ b/jdaviz/configs/cubeviz/plugins/tools.py @@ -103,6 +103,7 @@ def activate(self): def deactivate(self): for k in ("y_min", "y_max"): self._profile_viewer.state.remove_callback(k, self.on_limits_change) + self.viewer.stop_stream() super().deactivate() def on_limits_change(self, *args): @@ -112,6 +113,7 @@ def on_limits_change(self, *args): def on_mouse_move(self, data): if data['event'] == 'mouseleave': self._mark.visible = False + self.viewer.stop_stream() self._reset_profile_viewer_bounds() self._is_moving = False return @@ -158,6 +160,7 @@ def _mouse_move_worker(self, x, y): if x >= spectrum.flux.shape[0] or x < 0 or y >= spectrum.flux.shape[1] or y < 0: self._reset_profile_viewer_bounds() self._mark.visible = False + self.viewer.stop_stream() else: y_values = spectrum.flux[x, y, :].value if np.all(np.isnan(y_values)): @@ -165,5 +168,9 @@ def _mouse_move_worker(self, x, y): return self._mark.update_xy(spectrum.spectral_axis.value, y_values) self._mark.visible = True + + self.viewer.start_stream() + self.viewer.update_sonified_cube(x, y) + self._profile_viewer.set_limits( y_min=np.nanmin(y_values) * 0.8, y_max=np.nanmax(y_values) * 1.2) diff --git a/jdaviz/configs/cubeviz/plugins/viewers.py b/jdaviz/configs/cubeviz/plugins/viewers.py index 96238b96ad..7bba966582 100644 --- a/jdaviz/configs/cubeviz/plugins/viewers.py +++ b/jdaviz/configs/cubeviz/plugins/viewers.py @@ -6,6 +6,14 @@ from jdaviz.configs.default.plugins.viewers import JdavizViewerMixin from jdaviz.configs.specviz.plugins.viewers import SpecvizProfileView from jdaviz.core.freezable_state import FreezableBqplotImageViewerState +from jdaviz.configs.cubeviz.plugins.cube_listener import CubeListenerData, MINVOL +import numpy as np +from astropy import units as u + +try: + import sounddevice as sd +except ImportError: + pass __all__ = ['CubevizImageView', 'CubevizProfileView'] @@ -30,6 +38,7 @@ class CubevizImageView(JdavizViewerMixin, WithSliceSelection, BqplotImageView): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + # provide reference from state back to viewer to use for zoom syncing self.state._set_viewer(self) @@ -39,6 +48,13 @@ def __init__(self, *args, **kwargs): # Hide axes by default self.state.show_axes = False + self.sonified_cube = None + self.stream = None + self.sonification_wl_bounds = None + self.sonification_wl_unit = None + self.volume_level = None + self.stream_active = True + self.data_menu._obj.dataset.add_filter('is_cube_or_image') @property @@ -82,6 +98,85 @@ def data(self, cls=None): if hasattr(layer_state, 'layer') and isinstance(layer_state.layer, BaseData)] + def start_stream(self): + if self.stream and not self.stream.closed and self.stream_active: + self.stream.start() + + def stop_stream(self): + if self.stream and not self.stream.closed and self.stream_active: + self.stream.stop() + + def update_sonified_cube(self, x, y): + if (not self.sonified_cube or not hasattr(self.sonified_cube, 'newsig') or + not hasattr(self.sonified_cube, 'sigcube')): + return + self.sonified_cube.newsig = self.sonified_cube.sigcube[x, y, :] + self.sonified_cube.cbuff = True + + def update_listener_wls(self, w1, w2, wunit): + self.sonification_wl_bounds = (w1, w2) + self.sonification_wl_unit = wunit + + def update_sound_device(self, device_index): + if not self.sonified_cube: + return + + self.stop_stream() + self.stream = sd.OutputStream(samplerate=self.sample_rate, blocksize=self.buffer_size, + device=device_index, channels=1, dtype='int16', + latency='low', callback=self.sonified_cube.player_callback) + + def update_volume_level(self, level): + if not self.sonified_cube: + return + self.volume_level = level + self.sonified_cube.atten_level = int(1/np.clip((level/100.)**2, MINVOL, 1)) + + def get_sonified_cube(self, sample_rate, buffer_size, device, assidx, ssvidx, + pccut, audfrqmin, audfrqmax, eln): + spectrum = self.active_image_layer.layer.get_object(statistic=None) + wlens = spectrum.wavelength.to('m').value + flux = spectrum.flux.value + self.sample_rate = sample_rate + self.buffer_size = buffer_size + + if self.sonification_wl_bounds: + wl_unit = getattr(u, self.sonification_wl_unit) + si_wl_bounds = (self.sonification_wl_bounds * wl_unit).to('m') + wdx = np.logical_and(wlens >= si_wl_bounds[0].value, + wlens <= si_wl_bounds[1].value) + wlens = wlens[wdx] + flux = flux[:, :, wdx] + + pc_cube = np.percentile(np.nan_to_num(flux), np.clip(pccut, 0, 99), axis=-1) + + # clip zeros and remove NaNs + clipped_arr = np.nan_to_num(np.clip(flux, 0, np.inf), copy=False) + + # make a rough white-light image from the clipped array + whitelight = np.expand_dims(clipped_arr.sum(-1), axis=2) + + # subtract any percentile cut + clipped_arr -= np.expand_dims(pc_cube, axis=2) + + # and re-clip + clipped_arr = np.clip(clipped_arr, 0, np.inf) + + self.sonified_cube = CubeListenerData(clipped_arr ** assidx, wlens, duration=0.8, + samplerate=sample_rate, buffsize=buffer_size, + wl_bounds=self.sonification_wl_bounds, + wl_unit=self.sonification_wl_unit, + audfrqmin=audfrqmin, audfrqmax=audfrqmax, + eln=eln, vol=self.volume_level) + self.sonified_cube.sonify_cube() + self.sonified_cube.sigcube = ( + self.sonified_cube.sigcube * pow(whitelight / whitelight.max(), + ssvidx)).astype('int16') + self.stream = sd.OutputStream(samplerate=sample_rate, blocksize=buffer_size, device=device, + channels=1, dtype='int16', latency='low', + callback=self.sonified_cube.player_callback) + self.sonified_cube.cbuff = True + @viewer_registry("cubeviz-profile-viewer", label="Profile 1D (Cubeviz)") class CubevizProfileView(SpecvizProfileView, WithSliceIndicator): diff --git a/jdaviz/configs/imviz/plugins/footprints/footprints.vue b/jdaviz/configs/imviz/plugins/footprints/footprints.vue index 19cc450b96..39ba1d4f09 100644 --- a/jdaviz/configs/imviz/plugins/footprints/footprints.vue +++ b/jdaviz/configs/imviz/plugins/footprints/footprints.vue @@ -91,7 +91,8 @@ Footprint Definition - To use JWST or Roman footprints, install pysiaf and restart jdaviz. + To use JWST or Roman footprints, install pysiaf and restart jdaviz. This can be done by going to the command line + and running `pip install pysiaf` and then launching Jdaviz. =0.22.0", ] +strauss = [ + "strauss", +] [build-system] requires = [ @@ -135,6 +138,8 @@ filterwarnings = [ "ignore:The unit 'Angstrom' has been deprecated in the VOUnit standard\\. Suggested.* 0\\.1nm\\.", "ignore:((.|\n)*)Sentinel is not a public part of the traitlets API((.|\n)*)", "ignore:datetime\\.datetime\\.utcfromtimestamp:DeprecationWarning", # asdf + dateutil<=2.8.2 + Python 3.12 + "ignore:'audioop' is deprecated and slated for removal in Python 3.13", + "ignore:Importing display from IPython.core.display is deprecated since IPython 7.14, please import from IPython.display", "ignore::DeprecationWarning:glue", "ignore::DeprecationWarning:asteval", "ignore:::specutils.spectra.spectrum1d", diff --git a/standalone/hooks/hook-strauss.py b/standalone/hooks/hook-strauss.py new file mode 100644 index 0000000000..b2e2ecd907 --- /dev/null +++ b/standalone/hooks/hook-strauss.py @@ -0,0 +1,4 @@ +from PyInstaller.utils.hooks import collect_data_files, copy_metadata + +datas = collect_data_files('strauss') +datas += copy_metadata('strauss') diff --git a/tox.ini b/tox.ini index 07eecb30c0..e33e4b5c33 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = - py{310,311,312}-test{,-alldeps,-devdeps,-predeps}{-romandeps}{,-cov} + py{310,311,312}-test{,-alldeps,-devdeps,-predeps}{-romandeps,-straussdeps}{,-cov} linkcheck codestyle pep517 @@ -34,6 +34,7 @@ description = alldeps: with all optional dependencies devdeps: with the latest developer version of key dependencies romandeps: with dependencies specific to supporting Roman + straussdeps: with dependencies specific to supporting Strauss cov: and test coverage # The following provides some specific pinnings for key packages @@ -67,6 +68,7 @@ deps = extras = test romandeps: roman + straussdeps: strauss alldeps: all commands =