From 9f7929d061a8dd22f161ca8934f60fd1c56608b2 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Thu, 28 Mar 2024 12:34:54 +0000 Subject: [PATCH 1/4] Start implementing support for unit conversion in attribute limits in image viewer --- glue/core/state_objects.py | 18 ++++++++- glue/viewers/image/state.py | 34 ++++++++++++++++- glue/viewers/image/tests/test_state.py | 51 +++++++++++++++++++++++++- 3 files changed, 99 insertions(+), 4 deletions(-) diff --git a/glue/core/state_objects.py b/glue/core/state_objects.py index 149ff43f9..4b74f1cb1 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,20 @@ 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 + + print('display_units', repr(display_units)) if not force and (percentile == 'Custom' or not hasattr(self, 'data') or self.data is None): @@ -361,6 +368,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/state.py b/glue/viewers/image/state.py index 45f29dba0..6e9ddae77 100644 --- a/glue/viewers/image/state.py +++ b/glue/viewers/image/state.py @@ -1,6 +1,8 @@ import uuid from collections import defaultdict +import numpy as np + from glue.core import BaseData from glue.config import colormaps from glue.viewers.matplotlib.state import (MatplotlibDataViewerState, @@ -13,6 +15,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, UnitConverter __all__ = ['ImageViewerState', 'ImageLayerState', 'ImageSubsetLayerState', 'AggregateSlice'] @@ -490,6 +493,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 +512,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 +530,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 +595,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..a8e482028 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,52 @@ 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) From cda32c846f98ac525e31b19080556dc972f255ea Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Thu, 28 Mar 2024 13:26:26 +0000 Subject: [PATCH 2/4] Address case where just unit is changed in the state attribute limits helper --- glue/core/state_objects.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/glue/core/state_objects.py b/glue/core/state_objects.py index 4b74f1cb1..26f98b28d 100644 --- a/glue/core/state_objects.py +++ b/glue/core/state_objects.py @@ -328,7 +328,31 @@ def update_values(self, force=False, use_default_modifiers=False, **properties): log = getattr(self, 'log', None) or False display_units = getattr(self, 'display_units', None) or None - print('display_units', repr(display_units)) + 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): From 28a47d6a403bd2a4c510603025473be5c119158e Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Thu, 28 Mar 2024 13:29:24 +0000 Subject: [PATCH 3/4] Fix code style --- glue/viewers/image/state.py | 4 +--- glue/viewers/image/tests/test_state.py | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/glue/viewers/image/state.py b/glue/viewers/image/state.py index 6e9ddae77..e9eac8aaa 100644 --- a/glue/viewers/image/state.py +++ b/glue/viewers/image/state.py @@ -1,8 +1,6 @@ import uuid from collections import defaultdict -import numpy as np - from glue.core import BaseData from glue.config import colormaps from glue.viewers.matplotlib.state import (MatplotlibDataViewerState, @@ -15,7 +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, UnitConverter +from glue.core.units import find_unit_choices __all__ = ['ImageViewerState', 'ImageLayerState', 'ImageSubsetLayerState', 'AggregateSlice'] diff --git a/glue/viewers/image/tests/test_state.py b/glue/viewers/image/tests/test_state.py index a8e482028..055c995c5 100644 --- a/glue/viewers/image/tests/test_state.py +++ b/glue/viewers/image/tests/test_state.py @@ -372,7 +372,6 @@ def check_consistency(*args, **kwargs): 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 From 3a820655794c6f27cfb536db667637bb59b67baa Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Thu, 28 Mar 2024 14:24:50 +0000 Subject: [PATCH 4/4] Properly handle image limit units in layer artist --- glue/viewers/image/layer_artist.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) 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,