diff --git a/glue/core/state_objects.py b/glue/core/state_objects.py index 149ff43f9..26f98b28d 100644 --- a/glue/core/state_objects.py +++ b/glue/core/state_objects.py @@ -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'] @@ -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 ----- @@ -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): @@ -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): @@ -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) diff --git a/glue/viewers/image/layer_artist.py b/glue/viewers/image/layer_artist.py index abee0cad2..badef124d 100644 --- a/glue/viewers/image/layer_artist.py +++ b/glue/viewers/image/layer_artist.py @@ -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): @@ -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, diff --git a/glue/viewers/image/state.py b/glue/viewers/image/state.py index 45f29dba0..e9eac8aaa 100644 --- a/glue/viewers/image/state.py +++ b/glue/viewers/image/state.py @@ -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'] @@ -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') @@ -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) @@ -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) @@ -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): """ diff --git a/glue/viewers/image/tests/test_state.py b/glue/viewers/image/tests/test_state.py index b35540ef1..055c995c5 100644 --- a/glue/viewers/image/tests/test_state.py +++ b/glue/viewers/image/tests/test_state.py @@ -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 @@ -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)