Skip to content

Commit

Permalink
Merge pull request #311 from astrofrog/profile-viewer-units
Browse files Browse the repository at this point in the history
Initial support for unit conversion in profile viewer
  • Loading branch information
astrofrog authored Feb 3, 2023
2 parents 20fa361 + ad5828f commit 2b521c9
Show file tree
Hide file tree
Showing 21 changed files with 437 additions and 63 deletions.
7 changes: 2 additions & 5 deletions .github/workflows/ci_workflows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,11 @@ jobs:
- libhdf5-dev
envs: |
- linux: py37-test
- linux: py38-test
- linux: py39-test
- linux: py310-test
- linux: py311-test
- macos: py37-test
- macos: py38-test
- macos: py39-test
# One of these to be switched to arm64 running natively once the PLAT var is supported.
Expand All @@ -52,12 +50,11 @@ jobs:
pytest: false

envs: |
- linux: py37-notebooks
- windows: py37-notebooks
- windows: py39-notebooks
- macos: py38-notebooks
- linux: py310-notebooks
- linux: py37-docs
- linux: py311-docs
- macos: py39-docs
- windows: py38-docs
Expand Down
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@
intersphinx_cache_limit = 10 # days to keep the cached inventories
intersphinx_mapping = {
# 'sphinx': ('https://www.sphinx-doc.org/en/latest/', None),
'python': ('https://docs.python.org/3.7', None),
'python': ('https://docs.python.org/3.11', None),
# 'matplotlib': ('https://matplotlib.org', None),
# 'numpy': ('https://docs.scipy.org/doc/numpy', None),
# 'astropy': ('http://docs.astropy.org/en/stable/', None),
Expand Down
4 changes: 2 additions & 2 deletions docs/installing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ installing it in an isolated Python environment for now. If you are using conda,
you can create an environment and install the latest version of glue-jupyter in
it using::

conda create -n glue-jupyter -c glueviz/label/dev python=3.7 glue-jupyter
conda create -n glue-jupyter -c glueviz/label/dev python=3.11 glue-jupyter

To switch to the environment, use::

Expand All @@ -27,7 +27,7 @@ latest version of glue-jupyter, or if you find conda too slow to install all the
dependencies, you can also create the environment with conda (or another Python
environment manager)::

conda create -n glue-jupyter python=3.7
conda create -n glue-jupyter python=3.11

then switch to the environment as above and install glue-jupyter and all its
dependencies with::
Expand Down
17 changes: 11 additions & 6 deletions glue_jupyter/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,19 +67,24 @@ def __init__(self, data_collection=None, session=None, settings=None):
REQUIRED_PLUGINS.clear()
load_plugins()

self.output = widgets.Output()
self.widget_data_collection = widgets.SelectMultiple()
self.widget_subset_select = SubsetSelect(session=self.session)
self.widget_subset_mode = SelectionModeMenu(session=self.session)
self.widget = widgets.VBox(children=[self.widget_subset_mode, self.output])

self._settings['new_subset_on_selection_tool_change'] = [False, bool]
self._settings['single_global_active_tool'] = [True, bool]
self._settings['disable_output_widget'] = [False, bool]

if settings is not None:
for key, value in settings.items():
self.set_setting(key, value)

self.output = None if self.get_setting('disable_output_widget') else widgets.Output()
self.widget_data_collection = widgets.SelectMultiple()
self.widget_subset_select = SubsetSelect(session=self.session)
self.widget_subset_mode = SelectionModeMenu(session=self.session)

if self.output is None:
self.widget = widgets.VBox(children=[self.widget_subset_mode])
else:
self.widget = widgets.VBox(children=[self.widget_subset_mode, self.output])

self._viewer_refs = []

def _ipython_display_(self):
Expand Down
20 changes: 11 additions & 9 deletions glue_jupyter/bqplot/common/tools.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from contextlib import nullcontext

import os
from bqplot import PanZoom
from bqplot.interacts import BrushSelector, BrushIntervalSelector
Expand Down Expand Up @@ -132,7 +134,7 @@ def __init__(self, viewer, roi=None, finalize_callback=None, **kwargs):
def update_selection(self, *args):
if self.interact.brushing:
return
with self.viewer._output_widget:
with self.viewer._output_widget or nullcontext():
if self.interact.selected_x is not None and self.interact.selected_y is not None:
x = self.interact.selected_x
y = self.interact.selected_y
Expand All @@ -142,7 +144,7 @@ def update_selection(self, *args):
self.finalize_callback()

def update_from_roi(self, roi):
with self.viewer._output_widget:
with self.viewer._output_widget or nullcontext():
if isinstance(roi, RectangularROI):
self.interact.selected_x = [roi.xmin, roi.xmax]
self.interact.selected_y = [roi.ymin, roi.ymax]
Expand All @@ -161,7 +163,7 @@ def on_selection_change(self, *args):
self.finalize_callback()

def activate(self):
with self.viewer._output_widget:
with self.viewer._output_widget or nullcontext():
self.interact.selected_x = None
self.interact.selected_y = None
super().activate()
Expand Down Expand Up @@ -203,7 +205,7 @@ def __init__(self, viewer, roi=None, finalize_callback=None, **kwargs):
def update_selection(self, *args):
if self.interact.brushing:
return
with self.viewer._output_widget:
with self.viewer._output_widget or nullcontext():
if self.interact.selected_x is not None and self.interact.selected_y is not None:
x = self.interact.selected_x
y = self.interact.selected_y
Expand Down Expand Up @@ -242,7 +244,7 @@ def on_selection_change(self, *args):
self.finalize_callback()

def activate(self):
with self.viewer._output_widget:
with self.viewer._output_widget or nullcontext():
self.interact.selected_x = None
self.interact.selected_y = None
super().activate()
Expand Down Expand Up @@ -299,15 +301,15 @@ def __init__(self, viewer, **kwargs):
self.interact.observe(self.update_selection, "brushing")

def update_selection(self, *args):
with self.viewer._output_widget:
with self.viewer._output_widget or nullcontext():
if self.interact.selected is not None:
x = self.interact.selected
if x is not None and len(x):
roi = RangeROI(min=min(x), max=max(x), orientation='x')
self.viewer.apply_roi(roi)

def activate(self):
with self.viewer._output_widget:
with self.viewer._output_widget or nullcontext():
self.interact.selected = None
super().activate()

Expand All @@ -331,15 +333,15 @@ def __init__(self, viewer, **kwargs):
self.interact.observe(self.update_selection, "brushing")

def update_selection(self, *args):
with self.viewer._output_widget:
with self.viewer._output_widget or nullcontext():
if self.interact.selected is not None:
y = self.interact.selected
if y is not None and len(y):
roi = RangeROI(min=min(y), max=max(y), orientation='y')
self.viewer.apply_roi(roi)

def activate(self):
with self.viewer._output_widget:
with self.viewer._output_widget or nullcontext():
self.interact.selected = None
super().activate()

Expand Down
38 changes: 15 additions & 23 deletions glue_jupyter/bqplot/common/viewer.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import bqplot
from functools import partial
from contextlib import nullcontext

from glue.core.subset import roi_to_subset_state
from glue.core.command import ApplySubsetState
Expand Down Expand Up @@ -65,28 +66,13 @@ def __init__(self, session, state=None):
self.state.remove_callback('layers', self._sync_layer_artist_container)
self.state.add_callback('layers', self._sync_layer_artist_container, priority=10000)

def update_axes(*ignore):
try:
# Extract units from data
x_unit = self.state.reference_data.get_component(self.state.x_att_world).units
except AttributeError:
# If no data loaded yet, ignore units
x_unit = ""
finally:
# Append units to axis label
self.axis_x.label = str(self.state.x_att) + " " + str(x_unit)
if self.is2d:
self.axis_y.label = str(self.state.y_att)
try:
y_unit = self.state.reference_data.get_component(self.state.y_att_world).units
except AttributeError:
y_unit = ""
finally:
self.axis_y.label = str(self.state.y_att) + " " + str(y_unit)

self.state.add_callback('x_att', update_axes)
if self.is2d:
self.state.add_callback('y_att', update_axes)
self.state.add_callback('x_axislabel', self.update_x_axislabel)
# self.state.add_callback('x_axislabel_weight', self.update_x_axislabel)
# self.state.add_callback('x_axislabel_size', self.update_x_axislabel)

self.state.add_callback('y_axislabel', self.update_y_axislabel)
# self.state.add_callback('y_axislabel_weight', self.update_y_axislabel)
# self.state.add_callback('y_axislabel_size', self.update_y_axislabel)

self.scale_x.observe(self.update_glue_scales, names=['min', 'max'])
self.scale_y.observe(self.update_glue_scales, names=['min', 'max'])
Expand All @@ -103,6 +89,12 @@ def update_axes(*ignore):

self.create_layout()

def update_x_axislabel(self, *event):
self.axis_x.label = self.state.x_axislabel

def update_y_axislabel(self, *event):
self.axis_y.label = self.state.y_axislabel

def _update_bqplot_limits(self, *args):

if self._last_limits == (self.state.x_min, self.state.x_max,
Expand Down Expand Up @@ -224,7 +216,7 @@ def _sync_show_axes(self):

def apply_roi(self, roi, use_current=False):
# TODO: partial copy paste from glue/viewers/matplotlib/qt/data_viewer.py
with self._output_widget:
with self._output_widget or nullcontext():
if len(self.layers) > 0:
subset_state = self._roi_to_subset_state(roi)
cmd = ApplySubsetState(data_collection=self._data,
Expand Down
17 changes: 17 additions & 0 deletions glue_jupyter/bqplot/histogram/viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,23 @@ class BqplotHistogramView(BqplotBaseView):

tools = ['bqplot:home', 'bqplot:panzoom', 'bqplot:xrange']

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.state.add_callback('x_att', self._update_axes)
self.state.add_callback('x_log', self._update_axes)
self.state.add_callback('normalize', self._update_axes)
self._update_axes()

def _update_axes(self, *args):

if self.state.x_att is not None:
self.state.x_axislabel = str(self.state.x_att)

if self.state.normalize:
self.state.y_axislabel = 'Normalized number'
else:
self.state.y_axislabel = 'Number'

def _roi_to_subset_state(self, roi):
# TODO: copy paste from glue/viewers/histogram/qt/data_viewer.py
# TODO Does subset get applied to all data or just visible data?
Expand Down
10 changes: 10 additions & 0 deletions glue_jupyter/bqplot/image/viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,22 @@ def __init__(self, session):
self.state.add_callback('reference_data', self._reset_limits, echo_old=True)
self.state.add_callback('x_att', self._reset_limits, echo_old=True)
self.state.add_callback('y_att', self._reset_limits, echo_old=True)
self.state.add_callback('x_att_world', self._update_axes)
self.state.add_callback('y_att_world', self._update_axes)

self._setup_view_listener()

on_change([(self.state, 'aspect')])(self._sync_figure_aspect)
self._sync_figure_aspect()

def _update_axes(self, *args):

if self.state.x_att_world is not None:
self.state.x_axislabel = str(self.state.x_att_world)

if self.state.y_att_world is not None:
self.state.y_axislabel = str(self.state.y_att_world)

def _setup_view_listener(self):
self._vl = ViewListener(widget=self.figure,
css_selector=".plotarea_events")
Expand Down
3 changes: 2 additions & 1 deletion glue_jupyter/bqplot/profile/layer_artist.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,8 @@ def _update_profile(self, force=False, **kwargs):
if force or any(prop in changed for prop in ('layer', 'x_att', 'attribute',
'function', 'normalize',
'v_min', 'v_max',
'as_steps')):
'as_steps',
'x_display_unit', 'y_display_unit')):
self._calculate_profile(reset=force)
force = True

Expand Down
95 changes: 95 additions & 0 deletions glue_jupyter/bqplot/profile/tests/test_viewer.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
from astropy.wcs import WCS

import numpy as np
from numpy.testing import assert_allclose, assert_equal

from glue.core import Data
from glue.core.roi import XRangeROI
from glue.plugins.wcs_autolinking.wcs_autolinking import WCSLink


def test_non_hex_colors(app, dataxyz):

# Make sure non-hex colors such as '0.4' and 'red', which are valid
Expand All @@ -22,3 +32,88 @@ def test_remove(app, data_image, data_volume):
assert len(s.figure.marks) == 2
s.remove_data(data_volume)
assert len(s.figure.marks) == 0


def test_unit_conversion(app):

wcs1 = WCS(naxis=1)
wcs1.wcs.ctype = ['FREQ']
wcs1.wcs.crval = [1]
wcs1.wcs.cdelt = [1]
wcs1.wcs.crpix = [1]
wcs1.wcs.cunit = ['GHz']

d1 = Data(f1=[1, 2, 3])
d1.get_component('f1').units = 'Jy'
d1.coords = wcs1

wcs2 = WCS(naxis=1)
wcs2.wcs.ctype = ['WAVE']
wcs2.wcs.crval = [10]
wcs2.wcs.cdelt = [10]
wcs2.wcs.crpix = [1]
wcs2.wcs.cunit = ['cm']

d2 = Data(f2=[2000, 1000, 3000])
d2.get_component('f2').units = 'mJy'
d2.coords = wcs2

session = app.session

data_collection = session.data_collection
data_collection.append(d1)
data_collection.append(d2)
data_collection.add_link(WCSLink(d1, d2))

viewer = app.profile1d(data=d1)
viewer.add_data(d2)

assert viewer.layers[0].enabled
assert viewer.layers[1].enabled

x, y = viewer.state.layers[0].profile
assert_allclose(x, [1.e9, 2.e9, 3.e9])
assert_allclose(y, [1, 2, 3])

x, y = viewer.state.layers[1].profile
assert_allclose(x, 299792458 / np.array([0.1, 0.2, 0.3]))
assert_allclose(y, [2000, 1000, 3000])

assert viewer.state.x_min == 1.e9
assert viewer.state.x_max == 3.e9
assert viewer.state.y_min == 1.
assert viewer.state.y_max == 3.

roi = XRangeROI(1.4e9, 2.1e9)
viewer.apply_roi(roi)

assert len(d1.subsets) == 1
assert_equal(d1.subsets[0].to_mask(), [0, 1, 0])

assert len(d2.subsets) == 1
assert_equal(d2.subsets[0].to_mask(), [0, 1, 0])

viewer.state.x_display_unit = 'GHz'
viewer.state.y_display_unit = 'mJy'

x, y = viewer.state.layers[0].profile
assert_allclose(x, [1, 2, 3])
assert_allclose(y, [1000, 2000, 3000])

x, y = viewer.state.layers[1].profile
assert_allclose(x, 2.99792458 / np.array([1, 2, 3]))
assert_allclose(y, [2000, 1000, 3000])

assert viewer.state.x_min == 1.
assert viewer.state.x_max == 3.
assert viewer.state.y_min == 1000.
assert viewer.state.y_max == 3000.

roi = XRangeROI(0.5, 1.2)
viewer.apply_roi(roi)

assert len(d1.subsets) == 1
assert_equal(d1.subsets[0].to_mask(), [1, 0, 0])

assert len(d2.subsets) == 1
assert_equal(d2.subsets[0].to_mask(), [0, 0, 1])
Loading

0 comments on commit 2b521c9

Please sign in to comment.