-
Notifications
You must be signed in to change notification settings - Fork 14
implement the PintIndex
#163
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
Merged
+1,000
−562
Merged
Changes from all commits
Commits
Show all changes
61 commits
Select commit
Hold shift + click to select a range
b9baa9c
add a `PintMetaIndex` that for now can only `sel`
keewis 89c5e2a
add a function to compare indexers
keewis 17e9aec
expect indexer dicts for strip_indexer_units
keewis 30a1d80
move the indexer comparison function to the utils
keewis 34caf09
change extract_indexer_units to expect a dict
keewis 05aa5a6
fix a few calls to extract_indexer_units
keewis 2e0e5bd
one more call
keewis 818db6c
Merge branch 'main' into pint-meta-index
keewis a049d03
implement `create_variables` and `from_variables`
keewis 0706bc6
use the new index to attach units to dimension coordinates
keewis a603860
pass the dictionary of indexers instead iterating manually
keewis dea881c
use `Coordinates._construct_direct`
keewis 9427b20
Merge branch 'main' into pint-meta-index
keewis a54b94a
delegate `isel` to the wrapped index and wrap the result
keewis 23d1f76
Merge branch 'main' into pint-meta-index
keewis f0d0890
add a inline `repr` for the index
keewis fa9f1b3
stubs for the remaining methods
keewis fb01e32
rename the index class to `PintIndex`
keewis 9278a2d
add a utility method to wrap the output of the wrapped index's methods
keewis 3200bc8
implement `equals`
keewis 281f03c
implement `roll`, `rename`, and `__getitem__` by forwarding
keewis c5e9022
start adding tests
keewis 2c2c814
add tests for `create_variables`
keewis e5d8369
add tests for `sel`
keewis 50a7287
add tests for `isel`
keewis 2b3c5bb
improve the tests for `sel`
keewis 57ea8e5
add tests for `equals`
keewis 3eed8c9
add tests for `roll`
keewis aebaf37
add tests for `rename`
keewis 58c540f
add tests for `__getitem__`
keewis 9cb7e91
add tests for `_repr_inline_`
keewis 9822520
configure coverage, just in case
keewis 55ccb00
use `_replace` instead of manually constructing the new index
keewis 6bd6726
explicitly check that the pint index gets created
keewis c7d523b
also verify that non-quantity variables don't become `PintIndex`ed
keewis bae3c3e
Merge branch 'main' into pint-meta-index
keewis 9dde67d
don't use `.pint.sel`
keewis 235ca0e
Merge branch 'main' into pint-meta-index
keewis b927436
fix `PintIndex.from_variables` and properly test it
keewis c31e6b0
quantify the test data
keewis 415d059
explicity quantify the input of the `interp_like` tests
keewis 1939b2d
also strip the units of `other`
keewis 2538104
change expectations in the conversion tests
keewis eb2c405
refactor `attach_units_dataset`
keewis 0d46b66
get `convert_units` to accept indexes
keewis caf4668
strip indexes as well
keewis 7303960
change the `.pint.to` tests to not include indexes
keewis e88738d
extract the units of `other` in `.pint.interp_like`
keewis 0b400d0
quantify the input and expected data in the `reindex` tests
keewis 5bd3ec7
remove the left-over explicit quantification in the `interp` tests
keewis 77eef6d
get `.pint.reindex` to work by explicitly converting, stripping, and …
keewis c38eb5a
quantify the input and expected objects in the `reindex_like` tests
keewis 7277eb5
get `reindex_like` to work with indexes
keewis c7cf340
quantify expected only if we expect to make use of it
keewis 948d20f
quantify input and expected objects in the `sel` and `loc` tests
keewis 8c76cbc
get `.pint.sel` and `.pint.loc` to work with the indexes
keewis f9cb15c
remove the warning about indexed coordinates
keewis 49942bf
preserve the order of the variables
keewis 20dd15c
remove the remaining uses of `Coordinates._construct_direct`
keewis 5efb318
whats-new entry
keewis f53539a
expose the index
keewis File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
from xarray import Variable | ||
from xarray.core.indexes import Index, PandasIndex | ||
|
||
from . import conversion | ||
|
||
|
||
class PintIndex(Index): | ||
def __init__(self, *, index, units): | ||
"""create a unit-aware MetaIndex | ||
Parameters | ||
---------- | ||
index : xarray.Index | ||
The wrapped index object. | ||
units : mapping of hashable to unit-like | ||
The units of the indexed coordinates | ||
""" | ||
self.index = index | ||
self.units = units | ||
|
||
def _replace(self, new_index): | ||
return self.__class__(index=new_index, units=self.units) | ||
|
||
def create_variables(self, variables=None): | ||
index_vars = self.index.create_variables(variables) | ||
|
||
index_vars_units = {} | ||
for name, var in index_vars.items(): | ||
data = conversion.array_attach_units(var.data, self.units[name]) | ||
var_units = Variable(var.dims, data, attrs=var.attrs, encoding=var.encoding) | ||
index_vars_units[name] = var_units | ||
|
||
return index_vars_units | ||
|
||
@classmethod | ||
def from_variables(cls, variables, options): | ||
if len(variables) != 1: | ||
raise ValueError("can only create a default index from single variables") | ||
|
||
units = options.pop("units", None) | ||
index = PandasIndex.from_variables(variables, options=options) | ||
return cls(index=index, units={index.index.name: units}) | ||
|
||
@classmethod | ||
def concat(cls, indexes, dim, positions): | ||
raise NotImplementedError() | ||
|
||
@classmethod | ||
def stack(cls, variables, dim): | ||
raise NotImplementedError() | ||
|
||
def unstack(self): | ||
raise NotImplementedError() | ||
|
||
def sel(self, labels): | ||
converted_labels = conversion.convert_indexer_units(labels, self.units) | ||
stripped_labels = conversion.strip_indexer_units(converted_labels) | ||
|
||
return self.index.sel(stripped_labels) | ||
|
||
def isel(self, indexers): | ||
subset = self.index.isel(indexers) | ||
if subset is None: | ||
return None | ||
|
||
return self._replace(subset) | ||
|
||
def join(self, other, how="inner"): | ||
raise NotImplementedError() | ||
|
||
def reindex_like(self, other): | ||
raise NotImplementedError() | ||
|
||
def equals(self, other): | ||
if not isinstance(other, PintIndex): | ||
return False | ||
|
||
# for now we require exactly matching units to avoid the potentially expensive conversion | ||
if self.units != other.units: | ||
return False | ||
|
||
# last to avoid the potentially expensive comparison | ||
return self.index.equals(other.index) | ||
|
||
def roll(self, shifts): | ||
return self._replace(self.index.roll(shifts)) | ||
|
||
def rename(self, name_dict, dims_dict): | ||
return self._replace(self.index.rename(name_dict, dims_dict)) | ||
|
||
def __getitem__(self, indexer): | ||
return self._replace(self.index[indexer]) | ||
|
||
def _repr_inline_(self, max_width): | ||
return f"{self.__class__.__name__}({self.index.__class__.__name__})" |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,227 @@ | ||
import numpy as np | ||
import pandas as pd | ||
import pytest | ||
import xarray as xr | ||
from xarray.core.indexes import IndexSelResult, PandasIndex | ||
|
||
from pint_xarray import unit_registry as ureg | ||
from pint_xarray.index import PintIndex | ||
|
||
|
||
def indexer_equal(first, second): | ||
if type(first) is not type(second): | ||
return False | ||
|
||
if isinstance(first, np.ndarray): | ||
return np.all(first == second) | ||
else: | ||
return first == second | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"base_index", | ||
[ | ||
PandasIndex(pd.Index([1, 2, 3]), dim="x"), | ||
PandasIndex(pd.Index([0.1, 0.2, 0.3]), dim="x"), | ||
PandasIndex(pd.Index([1j, 2j, 3j]), dim="y"), | ||
], | ||
) | ||
@pytest.mark.parametrize("units", [ureg.Unit("m"), ureg.Unit("s")]) | ||
def test_init(base_index, units): | ||
index = PintIndex(index=base_index, units=units) | ||
|
||
assert index.index.equals(base_index) | ||
assert index.units == units | ||
|
||
|
||
def test_replace(): | ||
old_index = PandasIndex([1, 2, 3], dim="y") | ||
new_index = PandasIndex([0.1, 0.2, 0.3], dim="x") | ||
|
||
old = PintIndex(index=old_index, units=ureg.Unit("m")) | ||
new = old._replace(new_index) | ||
|
||
assert new.index.equals(new_index) | ||
assert new.units == old.units | ||
# no mutation | ||
assert old.index.equals(old_index) | ||
|
||
|
||
@pytest.mark.parametrize( | ||
["wrapped_index", "units", "expected"], | ||
( | ||
pytest.param( | ||
PandasIndex(pd.Index([1, 2, 3]), dim="x"), | ||
{"x": ureg.Unit("m")}, | ||
{"x": xr.Variable("x", ureg.Quantity([1, 2, 3], "m"))}, | ||
), | ||
pytest.param( | ||
PandasIndex(pd.Index([1j, 2j, 3j]), dim="y"), | ||
{"y": ureg.Unit("ms")}, | ||
{"y": xr.Variable("y", ureg.Quantity([1j, 2j, 3j], "ms"))}, | ||
), | ||
), | ||
) | ||
def test_create_variables(wrapped_index, units, expected): | ||
index = PintIndex(index=wrapped_index, units=units) | ||
|
||
actual = index.create_variables() | ||
|
||
assert list(actual.keys()) == list(expected.keys()) | ||
assert all([actual[k].equals(expected[k]) for k in expected.keys()]) | ||
|
||
|
||
@pytest.mark.parametrize( | ||
["labels", "expected"], | ||
( | ||
({"x": ureg.Quantity(1, "m")}, IndexSelResult(dim_indexers={"x": 0})), | ||
({"x": ureg.Quantity(3000, "mm")}, IndexSelResult(dim_indexers={"x": 2})), | ||
({"x": ureg.Quantity(0.002, "km")}, IndexSelResult(dim_indexers={"x": 1})), | ||
( | ||
{"x": ureg.Quantity([0.002, 0.004], "km")}, | ||
IndexSelResult(dim_indexers={"x": np.array([1, 3])}), | ||
), | ||
( | ||
{"x": slice(ureg.Quantity(2, "m"), ureg.Quantity(3000, "mm"))}, | ||
IndexSelResult(dim_indexers={"x": slice(1, 3)}), | ||
), | ||
), | ||
) | ||
def test_sel(labels, expected): | ||
index = PintIndex( | ||
index=PandasIndex(pd.Index([1, 2, 3, 4]), dim="x"), units={"x": ureg.Unit("m")} | ||
) | ||
|
||
actual = index.sel(labels) | ||
|
||
assert isinstance(actual, IndexSelResult) | ||
assert list(actual.dim_indexers.keys()) == list(expected.dim_indexers.keys()) | ||
assert all( | ||
[ | ||
indexer_equal(actual.dim_indexers[k], expected.dim_indexers[k]) | ||
for k in expected.dim_indexers.keys() | ||
] | ||
) | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"indexers", | ||
({"y": 0}, {"y": [1, 2]}, {"y": slice(0, None, 2)}, {"y": xr.Variable("y", [1])}), | ||
) | ||
def test_isel(indexers): | ||
wrapped_index = PandasIndex(pd.Index([1, 2, 3, 4]), dim="y") | ||
index = PintIndex(index=wrapped_index, units={"y": ureg.Unit("s")}) | ||
|
||
actual = index.isel(indexers) | ||
|
||
wrapped_ = wrapped_index.isel(indexers) | ||
if wrapped_ is not None: | ||
expected = PintIndex( | ||
index=wrapped_index.isel(indexers), units={"y": ureg.Unit("s")} | ||
) | ||
else: | ||
expected = None | ||
|
||
assert (actual is None and expected is None) or actual.equals(expected) | ||
|
||
|
||
@pytest.mark.parametrize( | ||
["other", "expected"], | ||
( | ||
( | ||
PintIndex( | ||
index=PandasIndex(pd.Index([1, 2, 3, 4]), dim="x"), | ||
units={"x": ureg.Unit("cm")}, | ||
), | ||
True, | ||
), | ||
(PandasIndex(pd.Index([1, 2, 3, 4]), dim="x"), False), | ||
( | ||
PintIndex( | ||
index=PandasIndex(pd.Index([1, 2, 3, 4]), dim="x"), | ||
units={"x": ureg.Unit("m")}, | ||
), | ||
False, | ||
), | ||
( | ||
PintIndex( | ||
index=PandasIndex(pd.Index([1, 2, 3, 4]), dim="y"), | ||
units={"y": ureg.Unit("cm")}, | ||
), | ||
False, | ||
), | ||
( | ||
PintIndex( | ||
index=PandasIndex(pd.Index([1, 3, 3, 4]), dim="x"), | ||
units={"x": ureg.Unit("cm")}, | ||
), | ||
False, | ||
), | ||
), | ||
) | ||
def test_equals(other, expected): | ||
index = PintIndex( | ||
index=PandasIndex(pd.Index([1, 2, 3, 4]), dim="x"), units={"x": ureg.Unit("cm")} | ||
) | ||
|
||
actual = index.equals(other) | ||
|
||
assert actual == expected | ||
|
||
|
||
@pytest.mark.parametrize( | ||
["shifts", "expected_index"], | ||
( | ||
({"x": 0}, PandasIndex(pd.Index([-2, -1, 0, 1, 2]), dim="x")), | ||
({"x": 1}, PandasIndex(pd.Index([2, -2, -1, 0, 1]), dim="x")), | ||
({"x": 2}, PandasIndex(pd.Index([1, 2, -2, -1, 0]), dim="x")), | ||
({"x": -1}, PandasIndex(pd.Index([-1, 0, 1, 2, -2]), dim="x")), | ||
({"x": -2}, PandasIndex(pd.Index([0, 1, 2, -2, -1]), dim="x")), | ||
), | ||
) | ||
def test_roll(shifts, expected_index): | ||
index = PintIndex( | ||
index=PandasIndex(pd.Index([-2, -1, 0, 1, 2]), dim="x"), | ||
units={"x": ureg.Unit("m")}, | ||
) | ||
|
||
actual = index.roll(shifts) | ||
expected = index._replace(expected_index) | ||
|
||
assert actual.equals(expected) | ||
|
||
|
||
@pytest.mark.parametrize("dims_dict", ({"y": "x"}, {"y": "z"})) | ||
@pytest.mark.parametrize("name_dict", ({"y2": "y3"}, {"y2": "y1"})) | ||
def test_rename(name_dict, dims_dict): | ||
wrapped_index = PandasIndex(pd.Index([1, 2], name="y2"), dim="y") | ||
index = PintIndex(index=wrapped_index, units={"y": ureg.Unit("m")}) | ||
|
||
actual = index.rename(name_dict, dims_dict) | ||
expected = PintIndex( | ||
index=wrapped_index.rename(name_dict, dims_dict), units=index.units | ||
) | ||
|
||
assert actual.equals(expected) | ||
|
||
|
||
@pytest.mark.parametrize("indexer", ([0], slice(0, 2))) | ||
def test_getitem(indexer): | ||
wrapped_index = PandasIndex(pd.Index([1, 2], name="y2"), dim="y") | ||
index = PintIndex(index=wrapped_index, units={"y": ureg.Unit("m")}) | ||
|
||
actual = index[indexer] | ||
expected = PintIndex(index=wrapped_index[indexer], units=index.units) | ||
|
||
assert actual.equals(expected) | ||
|
||
|
||
@pytest.mark.parametrize("wrapped_index", (PandasIndex(pd.Index([1, 2]), dim="x"),)) | ||
def test_repr_inline(wrapped_index): | ||
index = PintIndex(index=wrapped_index, units=ureg.Unit("m")) | ||
|
||
# TODO: parametrize | ||
actual = index._repr_inline_(90) | ||
|
||
assert "PintIndex" in actual | ||
assert wrapped_index.__class__.__name__ in actual |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.