Skip to content

Commit

Permalink
Merge pull request #2480 from astrofrog/attribute-display-limits
Browse files Browse the repository at this point in the history
Implement support for unit conversion in attribute limits in image viewer
  • Loading branch information
astrofrog authored Apr 16, 2024
2 parents b9ca12f + 3a82065 commit b3dfc2f
Show file tree
Hide file tree
Showing 4 changed files with 132 additions and 5 deletions.
42 changes: 40 additions & 2 deletions glue/core/state_objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from glue.core.state import saver, loader
from glue.core.component_id import PixelComponentID
from glue.core.exceptions import IncompatibleAttribute
from glue.core.units import UnitConverter

__all__ = ['State', 'StateAttributeCacheHelper',
'StateAttributeLimitsHelper', 'StateAttributeSingleValueHelper', 'StateAttributeHistogramHelper']
Expand Down Expand Up @@ -273,6 +274,8 @@ class StateAttributeLimitsHelper(StateAttributeCacheHelper):
log : bool
Whether the limits are in log mode (in which case only positive values
are used when finding the limits)
units : str, optional
The units to compute the limits in.
Notes
-----
Expand All @@ -288,7 +291,7 @@ class StateAttributeLimitsHelper(StateAttributeCacheHelper):
"""

values_names = ('lower', 'upper')
modifiers_names = ('log', 'percentile')
modifiers_names = ('log', 'percentile', 'display_units')

def __init__(self, state, attribute, random_subset=10000, margin=0, **kwargs):

Expand All @@ -312,16 +315,44 @@ def __init__(self, state, attribute, random_subset=10000, margin=0, **kwargs):

def update_values(self, force=False, use_default_modifiers=False, **properties):

if not force and not any(prop in properties for prop in ('attribute', 'percentile', 'log')):
if not force and not any(prop in properties for prop in ('attribute', 'percentile', 'log', 'display_units')):
self.set(percentile='Custom')
return

if use_default_modifiers:
percentile = 100
log = False
display_units = None
else:
percentile = getattr(self, 'percentile', None) or 100
log = getattr(self, 'log', None) or False
display_units = getattr(self, 'display_units', None) or None

previous_units = getattr(self, '_previous_units', '') or ''

self._previous_units = display_units

if set(properties) == {'display_units'}:

converter = UnitConverter()

current_limits = np.hstack([self.lower, self.upper])

if previous_units == '':
limits_native = current_limits
else:
limits_native = converter.to_native(self.data,
self.component_id,
current_limits,
previous_units)

lower, upper = converter.to_unit(self.data,
self.component_id,
limits_native,
display_units)

self.set(lower=lower, upper=upper)
return

if not force and (percentile == 'Custom' or not hasattr(self, 'data') or self.data is None):

Expand Down Expand Up @@ -361,6 +392,13 @@ def update_values(self, force=False, use_default_modifiers=False, **properties):
lower = np.floor(lower - 0.5) + 0.5
upper = np.ceil(upper + 0.5) - 0.5

if display_units:
limits = np.hstack([lower, upper])
converter = UnitConverter()
lower, upper = converter.to_unit(self.data, self.component_id,
np.hstack([lower, upper]),
display_units)

if log:
value_range = np.log10(upper / lower)
lower /= 10.**(value_range * self.margin)
Expand Down
13 changes: 12 additions & 1 deletion glue/viewers/image/layer_artist.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
PixelAlignedDataChangedMessage)
from glue.viewers.image.frb_artist import imshow
from glue.core.fixed_resolution_buffer import ARRAY_CACHE, PIXEL_CACHE
from glue.core.units import UnitConverter


class BaseImageLayerArtist(MatplotlibLayerArtist, HubListener):
Expand Down Expand Up @@ -160,8 +161,18 @@ def _update_visual_attributes(self):
else:
self.composite.mode = 'color'

# As the levels may be specified in a different unit we should convert
# them to the native data units.

converter = UnitConverter()

clim = tuple(converter.to_native(self.state.layer,
self.state.attribute,
np.hstack([self.state.v_min, self.state.v_max]),
self.state.attribute_display_unit))

self.composite.set(self.uuid,
clim=(self.state.v_min, self.state.v_max),
clim=clim,
visible=self.state.visible,
zorder=self.state.zorder,
color=self.state.color,
Expand Down
32 changes: 31 additions & 1 deletion glue/viewers/image/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from glue.core.data_combo_helper import ManualDataComboHelper, ComponentIDComboHelper
from glue.core.exceptions import IncompatibleDataException
from glue.viewers.common.stretch_state_mixin import StretchStateMixin
from glue.core.units import find_unit_choices

__all__ = ['ImageViewerState', 'ImageLayerState', 'ImageSubsetLayerState', 'AggregateSlice']

Expand Down Expand Up @@ -490,6 +491,7 @@ class ImageLayerState(BaseImageLayerState, StretchStateMixin):
attribute = DDSCProperty(docstring='The attribute shown in the layer')
v_min = DDCProperty(docstring='The lower level shown')
v_max = DDCProperty(docstring='The upper level shown')
attribute_display_unit = DDSCProperty(docstring='The units to use to define the levels')
percentile = DDSCProperty(docstring='The percentile value used to '
'automatically calculate levels')
contrast = DDCProperty(1, docstring='The contrast of the layer')
Expand All @@ -508,7 +510,8 @@ def __init__(self, layer=None, viewer_state=None, **kwargs):

self.attribute_lim_helper = StateAttributeLimitsHelper(self, attribute='attribute',
percentile='percentile',
lower='v_min', upper='v_max')
lower='v_min', upper='v_max',
display_units='attribute_display_unit')

self.attribute_att_helper = ComponentIDComboHelper(self, 'attribute',
numeric=True, categorical=False)
Expand All @@ -525,6 +528,19 @@ def __init__(self, layer=None, viewer_state=None, **kwargs):

self.setup_stretch_callback()

def format_unit(unit):
if unit is None:
return 'Native units'
else:
return unit

ImageLayerState.attribute_display_unit.set_display_func(self, format_unit)

self.add_callback('attribute', self._update_attribute_display_unit_choices)
# self.add_callback('attribute_display_unit', self._convert_attribute_limits_units, echo_old=True)

self._update_attribute_display_unit_choices()

self.add_callback('global_sync', self._update_syncing)
self.add_callback('layer', self._update_attribute)

Expand Down Expand Up @@ -577,6 +593,20 @@ def reset_contrast_bias(self):
self.contrast = 1
self.bias = 0.5

def _update_attribute_display_unit_choices(self, *args):

if self.layer is None or self.attribute is None:
ImageLayerState.attribute_display_unit.set_choices(self, [])
return

component = self.layer.get_component(self.attribute)
if component.units:
c_choices = find_unit_choices([(self.layer, self.attribute, component.units)])
else:
c_choices = ['']
ImageLayerState.attribute_display_unit.set_choices(self, c_choices)
self.attribute_display_unit = component.units


class ImageSubsetLayerState(BaseImageLayerState):
"""
Expand Down
50 changes: 49 additions & 1 deletion glue/viewers/image/tests/test_state.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import pytest

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

from glue.core import Data, DataCollection
from glue.core.coordinates import Coordinates, IdentityCoordinates
Expand Down Expand Up @@ -370,3 +370,51 @@ def check_consistency(*args, **kwargs):
viewer_state.reference_data = data2
assert viewer_state.x_att is data2.pixel_component_ids[2]
assert viewer_state.y_att is data2.pixel_component_ids[1]


def test_attribute_units():

# Unit test to make sure that the unit conversion works correctly for
# v_min/v_max.

viewer_state = ImageViewerState()

data1 = Data(x=np.arange(100).reshape((10, 10)))
data1.get_component('x').units = 'km'

layer_state1 = ImageLayerState(layer=data1, viewer_state=viewer_state)
viewer_state.layers.append(layer_state1)

assert layer_state1.percentile == 100
assert layer_state1.v_min == 0
assert layer_state1.v_max == 99

layer_state1.attribute_display_unit = 'm'

assert layer_state1.v_min == 0
assert layer_state1.v_max == 99000

assert layer_state1.percentile == 100

layer_state1.percentile = 95

assert_allclose(layer_state1.v_min, 2475)
assert_allclose(layer_state1.v_max, 96525)

assert layer_state1.percentile == 95

layer_state1.attribute_display_unit = 'km'

assert_allclose(layer_state1.v_min, 2.475)
assert_allclose(layer_state1.v_max, 96.525)

layer_state1.attribute_display_unit = 'm'

layer_state1.v_max = 50000

assert layer_state1.percentile == 'Custom'

layer_state1.attribute_display_unit = 'km'

assert_allclose(layer_state1.v_min, 2.475)
assert_allclose(layer_state1.v_max, 50)

0 comments on commit b3dfc2f

Please sign in to comment.