-
Notifications
You must be signed in to change notification settings - Fork 153
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Added ability to convert units in profile viewer #2296
Changes from all commits
9a9c392
36a5c7d
2cdac97
d4e9401
896c0bf
a4bbe92
947fd96
276ee7c
c1acfb0
fd04a2e
83839f2
5bcfc1f
29c4c76
908157f
e35a0e8
71b11a8
b2a310a
8d9c315
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
Unit conversion in glue | ||
======================= | ||
|
||
.. note:: Support for automatic unit conversion in glue is experimental - at the moment | ||
the ability to select units for the x and y axes is only available in the profile viewer. | ||
|
||
Data components can be assigned units as a string (or `None` to indicate no known units). | ||
By default, glue uses `astropy.units <https://docs.astropy.org/en/stable/units/index.html>`_ package | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you just wanna use intersphinx here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed |
||
to carry out unit conversions based on these units. However, it is possible to customize the | ||
unit conversion machinery, either to use a different unit transformation machinery, or to specify, | ||
e.g., equivalencies in the astropy unit conversion. To customize the unit conversion behavior, you | ||
will need to define a unit converter as shown below:: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So, this is a way to bypass built-in astropy equivalencies? 👀 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes |
||
|
||
from astropy import units as u | ||
from glue.core.units import unit_converter | ||
|
||
@unit_converter('custom-name') | ||
class MyCustomUnitConverter: | ||
|
||
def equivalent_units(self, data, cid, units): | ||
# Given a glue data object (data), a component ID (cid), and the units | ||
# of that component in the data object (units), this method should | ||
# return a flat list of units (as strings) that the data could be | ||
# converted to. This is used to construct the drop-down menus with the | ||
# available units to convert to. | ||
|
||
def to_unit(self, data, cid, values, original_units, target_units): | ||
# Given a glue data object (data), a component ID (cid), the values | ||
# to convert, and the original and target units of the values, this method | ||
# should return the converted values. Note that original_units | ||
# gives the units of the values array, which might not be the same | ||
# as the original native units of the component in the data. | ||
|
||
In both methods, the data and cid are passed in not to get values or units (those should be | ||
used from the other arguments to the methods) but rather to allow logic for the unit | ||
conversion that might depend on which component is being converted. An example of | ||
a simple unit converter based on `astropy.units`_ would be:: | ||
|
||
from astropy import units as u | ||
from glue.core.units import unit_converter | ||
|
||
@unit_converter('example-1') | ||
class ExampleUnitConverter: | ||
|
||
def equivalent_units(self, data, cid, units): | ||
return map(u.Unit(units).find_equivalent_units(include_prefix_units=True), str) | ||
|
||
def to_unit(self, data, cid, values, original_units, target_units): | ||
return (values * u.Unit(original_units)).to_value(target_units) | ||
|
||
This does not actually make use of ``data`` and ``cid``. An example that does would be:: | ||
|
||
from astropy import units as u | ||
from glue.core.units import unit_converter | ||
|
||
@unit_converter('example-2') | ||
class ExampleUnitConverter: | ||
|
||
def equivalent_units(self, data, cid, units): | ||
equivalencies = u.temperature() if 'temp' in cid.label.lower() else None | ||
return map(u.Unit(units).find_equivalent_units(include_prefix_units=True, equivalencies=equivalencies), str) | ||
|
||
def to_unit(self, data, cid, values, original_units, target_units): | ||
equivalencies = u.temperature() if 'temp' in cid.label.lower() else None | ||
return (values * u.Unit(original_units)).to_value(target_units, equivalencies=equivalencies) | ||
|
||
Once you have defined a unit conversion class, you can then opt-in to using it in glue by adjusting | ||
the following setting:: | ||
|
||
from glue.config import settings | ||
settings.UNIT_CONVERTER = 'example-2' |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
from glue.core.units import UnitConverter, find_unit_choices | ||
from glue.config import unit_converter, settings | ||
from glue.core import Data | ||
|
||
|
||
def setup_function(func): | ||
func.ORIGINAL_UNIT_CONVERTER = settings.UNIT_CONVERTER | ||
|
||
|
||
def teardown_function(func): | ||
settings.UNIT_CONVERTER = func.ORIGINAL_UNIT_CONVERTER | ||
|
||
|
||
def setup_module(): | ||
unit_converter.add('test-custom', SimpleCustomUnitConverter) | ||
|
||
|
||
def teardown_module(): | ||
unit_converter._members.pop('test-custom') | ||
|
||
|
||
def test_unit_converter_default(): | ||
|
||
data1 = Data(a=[1, 2, 3], b=[4, 5, 6]) | ||
data1.get_component('a').units = 'm' | ||
|
||
uc = UnitConverter() | ||
assert 'km' in uc.equivalent_units(data1, data1.id['a']) | ||
|
||
assert uc.to_unit(data1, data1.id['a'], 2000, 'km') == 2 | ||
assert uc.to_native(data1, data1.id['a'], 2, 'km') == 2000 | ||
|
||
assert uc.to_unit(data1, data1.id['a'], 2000, None) == 2000 | ||
assert uc.to_native(data1, data1.id['a'], 2, None) == 2 | ||
|
||
assert uc.equivalent_units(data1, data1.id['b']) == [] | ||
|
||
assert uc.to_unit(data1, data1.id['b'], 2000, 'km') == 2000 | ||
assert uc.to_native(data1, data1.id['b'], 2, 'km') == 2 | ||
|
||
|
||
def test_find_unit_choices_default(): | ||
|
||
assert find_unit_choices([]) == [] | ||
|
||
units1 = find_unit_choices([(None, None, 'm')]) | ||
assert 'km' in units1 | ||
assert 'yr' not in units1 | ||
|
||
units2 = find_unit_choices([(None, None, 'm'), (None, None, 's')]) | ||
assert 'km' in units2 | ||
assert 'yr' in units2 | ||
|
||
|
||
class SimpleCustomUnitConverter: | ||
|
||
def equivalent_units(self, data, cid, units): | ||
# We want to make sure we properly test data and cid so we make it | ||
# so that if cid contains 'fixed' we return only the original unit | ||
# and if the data label contains 'bilingual' then we return the full | ||
# set of units | ||
if cid.label == 'fixed': | ||
return [units] | ||
elif data.label == 'bilingual': | ||
return ['one', 'two', 'three', 'dix', 'vingt', 'trente'] | ||
elif units in ['one', 'two', 'three']: | ||
return ['one', 'two', 'three'] | ||
elif units in ['dix', 'vingt', 'trente']: | ||
return ['dix', 'vingt', 'trente'] | ||
else: | ||
raise ValueError(f'Unrecongized unit: {units}') | ||
|
||
numerical = { | ||
'one': 1, | ||
'two': 2, | ||
'three': 3, | ||
'dix': 10, | ||
'vingt': 20, | ||
'trente': 30 | ||
} | ||
|
||
def to_unit(self, data, cid, values, original_units, target_units): | ||
return values * self.numerical[target_units] / self.numerical[original_units] | ||
|
||
|
||
def test_unit_converter_custom(): | ||
|
||
settings.UNIT_CONVERTER = 'test-custom' | ||
|
||
data1 = Data(a=[1, 2, 3]) | ||
data1.get_component('a').units = 'two' | ||
|
||
uc = UnitConverter() | ||
assert uc.equivalent_units(data1, data1.id['a']) == ['one', 'two', 'three'] | ||
|
||
assert uc.to_unit(data1, data1.id['a'], 4, 'three') == 6 | ||
assert uc.to_native(data1, data1.id['a'], 6, 'three') == 4 | ||
|
||
|
||
def test_find_unit_choices_custom(): | ||
|
||
settings.UNIT_CONVERTER = 'test-custom' | ||
|
||
data1 = Data(fixed=[1, 2, 3], a=[2, 3, 4], b=[3, 4, 5], label='data1') | ||
data2 = Data(c=[4, 5, 6], d=[5, 6, 7], label='bilingual') | ||
|
||
assert find_unit_choices([]) == [] | ||
|
||
assert find_unit_choices([(data1, data1.id['fixed'], 'one')]) == ['one'] | ||
assert find_unit_choices([(data1, data1.id['a'], 'one')]) == ['one', 'two', 'three'] | ||
assert find_unit_choices([(data1, data1.id['a'], 'dix')]) == ['dix', 'vingt', 'trente'] | ||
assert find_unit_choices([(data2, data2.id['c'], 'one')]) == ['one', 'two', 'three', 'dix', 'vingt', 'trente'] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This breaks backward compatibility -- See failures in https://github.com/spacetelescope/jdaviz/actions/runs/4053913069/jobs/6975103932
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, will have a think if there is a way we can avoid breaking backward compatibility