-
Notifications
You must be signed in to change notification settings - Fork 143
fix: ak.array_equal on unions with reordered index into list child #4321
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
Open
aashirvad08
wants to merge
7
commits into
scikit-hep:main
Choose a base branch
from
aashirvad08:aashirvad08/union-equal
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
f261681
fix ak.array_equal on union
aashirvad08 865346b
Merge branch 'main' into aashirvad08/union-equal
aashirvad08 d71d249
Merge branch 'main' into aashirvad08/union-equal
TaiSakuma 35fe8d8
Merge branch 'main' into aashirvad08/union-equal
ianna 2798c58
fix: use eager carries when comparing union and option children
aashirvad08 611cbf2
Merge branch 'main' into aashirvad08/union-equal
aashirvad08 1a351f0
fix: cast repeat counts to intp for 32-bit platforms
aashirvad08 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,296 @@ | ||
| # BSD 3-Clause License; see https://github.com/scikit-hep/awkward/blob/main/LICENSE | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import itertools | ||
|
|
||
| import numpy as np | ||
| import pytest | ||
|
|
||
| import awkward as ak | ||
|
|
||
|
|
||
| def test_reordered_index_list_of_records(): | ||
| """Issue #4316, first reproducer: a union whose list-of-records child is reordered.""" | ||
| z = ak.Array([[{"x": 1}], 2, []]) | ||
| p = z[[1, 2, 0]] | ||
| q = ak.Array(p.tolist()) | ||
|
|
||
| assert p.tolist() == q.tolist() | ||
| assert ak.array_equal(p, q) | ||
| assert ak.array_equal(q, p) | ||
| assert ak.array_equal(p, q, same_content_types=False) | ||
| assert ak.almost_equal(p, q) | ||
|
|
||
|
|
||
| def _zero_field_union(index, offsets): | ||
| return ak.Array( | ||
| ak.contents.UnionArray( | ||
| ak.index.Index8(np.array([1, 1], dtype=np.int8)), | ||
| ak.index.Index64(np.array(index, dtype=np.int64)), | ||
| [ | ||
| ak.contents.RecordArray([], [], 0), | ||
| ak.contents.ListOffsetArray( | ||
| ak.index.Index64(np.array(offsets, dtype=np.int64)), | ||
| ak.contents.RecordArray([], [], 1), | ||
| ), | ||
| ], | ||
| ) | ||
| ) | ||
|
|
||
|
|
||
| def test_reordered_index_hand_built_union(): | ||
| """Issue #4316, second reproducer: identical forms, one union index reordered. | ||
|
|
||
| `right` reaches the same values through a permuted union index, so | ||
| `UnionArray.project` has to reorder the list child's content. The lazy | ||
| carry that does so turns the child's `RecordArray` into an `IndexedArray` | ||
| on one side only. | ||
| """ | ||
| left = _zero_field_union([0, 1], [0, 1, 1]) | ||
| right = _zero_field_union([1, 0], [0, 0, 1]) | ||
|
|
||
| assert left.layout.form == right.layout.form | ||
| assert left.tolist() == [[{}], []] | ||
| assert right.tolist() == [[{}], []] | ||
| assert ak.array_equal(left, right) | ||
| assert ak.array_equal(right, left) | ||
| assert ak.array_equal(left, right, same_content_types=False) | ||
| assert ak.almost_equal(left, right) | ||
|
|
||
|
|
||
| def _unmasked_records_union(index, offsets): | ||
| return ak.Array( | ||
| ak.contents.UnionArray( | ||
| ak.index.Index8(np.array([1, 1, 0], dtype=np.int8)), | ||
| ak.index.Index64(np.array(index, dtype=np.int64)), | ||
| [ | ||
| ak.contents.NumpyArray(np.array([2], dtype=np.int64)), | ||
| ak.contents.ListOffsetArray( | ||
| ak.index.Index64(np.array(offsets, dtype=np.int64)), | ||
| ak.contents.UnmaskedArray( | ||
| ak.contents.RecordArray( | ||
| [ak.contents.NumpyArray(np.array([1], dtype=np.int64))], | ||
| ["x"], | ||
| ) | ||
| ), | ||
| ), | ||
| ], | ||
| ) | ||
| ) | ||
|
|
||
|
|
||
| def test_reordered_index_unmasked_records(): | ||
| """A list child of nullable records, which is how pyarrow makes a struct. | ||
|
|
||
| Compacting the reordered side's lists carries the records, and a lazy | ||
| carry would turn the `UnmaskedArray` over them into an | ||
| `IndexedOptionArray` on that side only. | ||
| """ | ||
| left = _unmasked_records_union([0, 1, 0], [0, 1, 1]) | ||
| right = _unmasked_records_union([1, 0, 0], [0, 0, 1]) | ||
|
|
||
| assert left.layout.form == right.layout.form | ||
| assert left.tolist() == [[{"x": 1}], [], 2] | ||
| assert right.tolist() == [[{"x": 1}], [], 2] | ||
| assert ak.array_equal(left, right) | ||
| assert ak.array_equal(right, left) | ||
| assert ak.array_equal(left, right, same_content_types=False) | ||
| assert ak.almost_equal(left, right) | ||
|
|
||
|
|
||
| def test_reordered_index_unmasked_records_from_arrow(): | ||
| """The same layout, as `ak.from_arrow` builds it from a dense union.""" | ||
| pa = pytest.importorskip("pyarrow") | ||
|
|
||
| def dense(offsets, lists): | ||
| return ak.from_arrow( | ||
| pa.UnionArray.from_dense( | ||
| pa.array([1, 1, 0], pa.int8()), | ||
| pa.array(offsets, pa.int32()), | ||
| [ | ||
| pa.array([2], pa.int64()), | ||
| pa.array(lists, pa.list_(pa.struct([("x", pa.int64())]))), | ||
| ], | ||
| ) | ||
| ) | ||
|
|
||
| left = dense([0, 1, 0], [[{"x": 1}], []]) | ||
| right = dense([1, 0, 0], [[], [{"x": 1}]]) | ||
|
|
||
| assert left.tolist() == right.tolist() | ||
| assert ak.array_equal(left, right) | ||
| assert ak.array_equal(right, left) | ||
| assert ak.almost_equal(left, right) | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| "data", | ||
| [ | ||
| pytest.param([[1, 2], 3, []], id="list-of-numbers"), | ||
| pytest.param([[{"x": 1}], 2, []], id="list-of-records"), | ||
| pytest.param([[(1, 2.5)], 2, []], id="list-of-tuples"), | ||
| pytest.param([[[1, 2]], 2, []], id="list-of-lists"), | ||
| pytest.param([["one"], 2, []], id="list-of-strings"), | ||
| pytest.param([[b"one"], 2, []], id="list-of-bytestrings"), | ||
| pytest.param([[{"x": 1}, None], 2, []], id="list-of-optional-records"), | ||
| pytest.param([[[{"x": 1}], []], 2, []], id="list-of-lists-of-records"), | ||
| pytest.param( | ||
| [[{"y": [1, 2]}, {"y": []}], 2, [{"y": [3]}]], | ||
| id="list-of-records-of-lists", | ||
| ), | ||
| pytest.param([[{"s": "one"}], 2, [{"s": ""}]], id="list-of-records-of-strings"), | ||
| ], | ||
| ) | ||
| @pytest.mark.parametrize("permutation", list(itertools.permutations(range(3)))) | ||
| def test_reordered_index_every_permutation(data, permutation): | ||
| """Reordering a union's index must not change the outcome for any child type.""" | ||
| p = ak.Array(data)[list(permutation)] | ||
| q = ak.Array(p.tolist()) | ||
|
|
||
| assert p.tolist() == q.tolist() | ||
| assert ak.array_equal(p, q) | ||
| assert ak.array_equal(q, p) | ||
| assert ak.array_equal(p, q, same_content_types=False) | ||
| assert ak.almost_equal(p, q) | ||
|
|
||
|
|
||
| def test_reordered_index_unequal_arrays(): | ||
| """A reordered union index must not make unequal arrays compare equal.""" | ||
| p = ak.Array([[{"x": 1}], 2, []])[[1, 2, 0]] | ||
| assert p.tolist() == [2, [], [{"x": 1}]] | ||
|
|
||
| different_value = ak.Array([2, [], [{"x": 999}]]) | ||
| assert not ak.array_equal(p, different_value) | ||
| assert not ak.array_equal(different_value, p) | ||
| assert not ak.almost_equal(p, different_value) | ||
|
|
||
| different_lists = ak.Array([2, [{"x": 1}], []]) | ||
| assert not ak.array_equal(p, different_lists) | ||
| assert not ak.array_equal(different_lists, p) | ||
| assert not ak.almost_equal(p, different_lists) | ||
|
|
||
|
|
||
| def _union_of(list_content): | ||
| return ak.Array( | ||
| ak.contents.UnionArray( | ||
| ak.index.Index8(np.array([0, 1, 1], dtype=np.int8)), | ||
| ak.index.Index64(np.array([0, 1, 0], dtype=np.int64)), | ||
| [ | ||
| ak.contents.NumpyArray(np.array([2], dtype=np.int64)), | ||
| list_content, | ||
| ], | ||
| ) | ||
| ) | ||
|
|
||
|
|
||
| def test_union_child_list_classes_still_compared(): | ||
| """`same_content_types=True` must still separate regular from variable lists.""" | ||
| var = _union_of( | ||
| ak.contents.ListOffsetArray( | ||
| ak.index.Index64(np.array([0, 1, 2], dtype=np.int64)), | ||
| ak.contents.NumpyArray(np.array([1, 3], dtype=np.int64)), | ||
| ) | ||
| ) | ||
| regular = _union_of( | ||
| ak.contents.RegularArray( | ||
| ak.contents.NumpyArray(np.array([1, 3], dtype=np.int64)), 1 | ||
| ) | ||
| ) | ||
|
|
||
| assert var.tolist() == regular.tolist() == [2, [3], [1]] | ||
| assert not ak.array_equal(var, regular) | ||
| assert not ak.array_equal(regular, var) | ||
| assert ak.array_equal(var, regular, check_regular=False) | ||
|
|
||
|
|
||
| def test_categorical_content_class_still_compared(): | ||
| """Packing keeps a categorical `IndexedArray`, so the class check must too.""" | ||
| categorical = ak.contents.IndexedArray( | ||
| ak.index.Index64(np.array([0, 1, 0], dtype=np.int64)), | ||
| ak.contents.NumpyArray(np.array([2, 3], dtype=np.int64)), | ||
| parameters={"__array__": "categorical"}, | ||
| ) | ||
| plain = ak.contents.NumpyArray(np.array([2, 3, 2], dtype=np.int64)) | ||
|
|
||
| assert not ak.array_equal(categorical, plain) | ||
| assert not ak.array_equal(plain, categorical) | ||
| assert ak.array_equal(categorical, plain, same_content_types=False) | ||
| assert ak.array_equal(categorical, categorical) | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("check_parameters", [True, False]) | ||
| def test_categorical_union_child_class_still_compared(check_parameters): | ||
| """A categorical union child compares as it does outside a union. | ||
|
|
||
| The lazy carry of the plain records used to be an `IndexedArray` too, so | ||
| `ak.array_equal` returned True here, although it returns False for the | ||
| same two contents outside a union. | ||
| """ | ||
| categorical = ak.contents.IndexedArray( | ||
| ak.index.Index64(np.array([1, 0], dtype=np.int64)), | ||
| ak.contents.RecordArray( | ||
| [ak.contents.NumpyArray(np.array([5, 6], dtype=np.int64))], ["y"] | ||
| ), | ||
| parameters={"__array__": "categorical"}, | ||
| ) | ||
| plain = ak.contents.RecordArray( | ||
| [ak.contents.NumpyArray(np.array([6, 5], dtype=np.int64))], ["y"] | ||
| ) | ||
| left = _union_of(categorical) | ||
| right = _union_of(plain) | ||
|
|
||
| assert left.tolist() == right.tolist() | ||
| assert not ak.array_equal(categorical, plain, check_parameters=check_parameters) | ||
| assert not ak.array_equal(left, right, check_parameters=check_parameters) | ||
| assert not ak.array_equal(right, left, check_parameters=check_parameters) | ||
| assert ak.array_equal( | ||
| left, right, check_parameters=check_parameters, same_content_types=False | ||
| ) | ||
| assert ak.almost_equal(left, right, check_parameters=check_parameters) | ||
|
|
||
|
|
||
| def _regular_of_optional_records(): | ||
| records = ak.contents.RecordArray( | ||
| [ak.contents.NumpyArray(np.array([1, 3], dtype=np.int64))], ["x"] | ||
| ) | ||
| unmasked = ak.contents.UnmaskedArray(records) | ||
| indexed = ak.contents.IndexedOptionArray( | ||
| ak.index.Index64(np.array([0, 1], dtype=np.int64)), records | ||
| ) | ||
| return ( | ||
| ak.contents.RegularArray(unmasked, 1), | ||
| ak.contents.RegularArray(indexed, 1), | ||
| ) | ||
|
|
||
|
|
||
| def test_union_child_option_classes_still_compared(): | ||
| """Projecting a union child must keep an `UnmaskedArray` below it. | ||
|
|
||
| A lazy carry turns it into an `IndexedOptionArray` on both sides, which | ||
| would make it pass the class check against a real one. | ||
| """ | ||
| unmasked, indexed = _regular_of_optional_records() | ||
| left = _union_of(unmasked) | ||
| right = _union_of(indexed) | ||
|
|
||
| assert left.tolist() == right.tolist() | ||
| assert not ak.array_equal(unmasked, indexed) | ||
| assert not ak.array_equal(left, right) | ||
| assert not ak.array_equal(right, left) | ||
| assert ak.array_equal(left, right, same_content_types=False) | ||
| assert ak.almost_equal(left, right) | ||
|
|
||
|
|
||
| def test_option_content_classes_still_compared(): | ||
| """Projecting an option's content must keep an `UnmaskedArray` below it.""" | ||
| unmasked, indexed = _regular_of_optional_records() | ||
| mask = ak.index.Index8(np.array([1, 0], dtype=np.int8)) | ||
| left = ak.contents.ByteMaskedArray(mask, unmasked, valid_when=True) | ||
| right = ak.contents.ByteMaskedArray(mask, indexed, valid_when=True) | ||
|
|
||
| assert left.to_packed().form != right.to_packed().form | ||
| assert not ak.array_equal(left, right) | ||
| assert not ak.array_equal(right, left) | ||
| assert ak.array_equal(left, right, same_content_types=False) | ||
| assert ak.almost_equal(left, right) | ||
Oops, something went wrong.
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.
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 is not used and can be removed.