-
Notifications
You must be signed in to change notification settings - Fork 4.3k
GH-38868: [C++][Python] Add Array::ToTensor and fixed size list support #50929
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
base: main
Are you sure you want to change the base?
Changes from all commits
032dfb6
8d04ba0
e68ce4e
36ac290
98b2a9e
5261798
f338b89
73215e0
4e390a5
862ab2d
7957226
5d0d7d5
6b55272
7ce9d1f
9791a66
8de5ed7
a805276
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 |
|---|---|---|
|
|
@@ -29,6 +29,7 @@ | |
| #include "arrow/array/validate.h" | ||
| #include "arrow/buffer.h" | ||
| #include "arrow/status.h" | ||
| #include "arrow/tensor.h" | ||
| #include "arrow/testing/builder.h" | ||
| #include "arrow/testing/gtest_util.h" | ||
| #include "arrow/type.h" | ||
|
|
@@ -1821,4 +1822,105 @@ TEST_F(TestFixedSizeListArray, FlattenRecursively) { | |
| *ArrayFromJSON(value_type_, "[0, 1, null, 3, 7, null, 2, 5]")); | ||
| } | ||
|
|
||
| namespace { | ||
|
|
||
| /// The innermost values of the nested fixed size lists. | ||
| std::shared_ptr<Array> LeafValues(std::shared_ptr<Array> array) { | ||
| while (array->type_id() == Type::FIXED_SIZE_LIST) { | ||
| const auto& fsl = checked_cast<const FixedSizeListArray&>(*array); | ||
| array = | ||
| fsl.values()->Slice(fsl.value_offset(0), array->length() * fsl.value_length()); | ||
| } | ||
| return array; | ||
| } | ||
|
|
||
| template <typename T> | ||
| void CheckToTensor(const std::shared_ptr<Array>& array, const std::vector<int64_t>& shape, | ||
| std::initializer_list<T> values) { | ||
| const auto value_type = CTypeTraits<T>::type_singleton(); | ||
| ASSERT_OK_AND_ASSIGN( | ||
| auto expected, | ||
| Tensor::Make(value_type, Buffer::Wrap(values.begin(), values.size()), shape)); | ||
|
|
||
| ASSERT_OK_AND_ASSIGN(auto tensor, array->ToTensor()); | ||
| ASSERT_OK(tensor->Validate()); | ||
|
|
||
| AssertTypeEqual(*value_type, *tensor->type()); | ||
| ASSERT_EQ(shape, tensor->shape()); | ||
| ASSERT_TRUE(tensor->is_row_major()); | ||
| ASSERT_TRUE(tensor->Equals(*expected)); | ||
|
|
||
| // The tensor shares the values buffer, it does not copy | ||
| const auto leaf = LeafValues(array); | ||
| ASSERT_EQ(leaf->data()->buffers[1]->data() + leaf->offset() * sizeof(T), | ||
| tensor->data()->data()); | ||
| } | ||
|
|
||
| } // namespace | ||
|
|
||
| TEST_F(TestFixedSizeListArray, ToTensor) { | ||
| auto array = ArrayFromJSON(fixed_size_list(int32(), 3), | ||
| "[[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]]"); | ||
| CheckToTensor<int32_t>(array, {4, 3}, {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}); | ||
|
|
||
| // Offset on the list array itself | ||
| CheckToTensor<int32_t>(array->Slice(2, 2), {2, 3}, {7, 8, 9, 10, 11, 12}); | ||
|
|
||
| // Offset on the values array | ||
| auto values = ArrayFromJSON(int32(), "[1, 2, 3, 4, 5, 6, 7, 8, 9]")->Slice(3); | ||
| ASSERT_OK_AND_ASSIGN(auto from_values, FixedSizeListArray::FromArrays(values, 3)); | ||
| CheckToTensor<int32_t>(from_values, {2, 3}, {4, 5, 6, 7, 8, 9}); | ||
|
|
||
| // Offsets on both the list array and its values | ||
| CheckToTensor<int32_t>(from_values->Slice(1), {1, 3}, {7, 8, 9}); | ||
| } | ||
|
|
||
| TEST_F(TestFixedSizeListArray, ToTensorNested) { | ||
| auto array = ArrayFromJSON(fixed_size_list(fixed_size_list(float32(), 2), 3), R"([ | ||
| [[1, 2], [3, 4], [5, 6]], | ||
| [[7, 8], [9, 10], [11, 12]], | ||
| [[13, 14], [15, 16], [17, 18]] | ||
| ])"); | ||
| CheckToTensor<float>(array, {3, 3, 2}, | ||
| {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18}); | ||
|
|
||
| CheckToTensor<float>(array->Slice(1, 2), {2, 3, 2}, | ||
| {7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18}); | ||
|
|
||
| // Offsets accumulate at every level: the innermost values are offset by 2, the | ||
| // middle lists by 3 * 2 and the outer lists by 1 * 3 * 2. | ||
| auto values = ArrayFromJSON(float32(), "[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]") | ||
| ->Slice(2, 12); | ||
| ASSERT_OK_AND_ASSIGN(auto inner, FixedSizeListArray::FromArrays(values, 2)); | ||
| ASSERT_OK_AND_ASSIGN(auto outer, FixedSizeListArray::FromArrays(inner, 3)); | ||
| CheckToTensor<float>(outer, {2, 3, 2}, {2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13}); | ||
| CheckToTensor<float>(outer->Slice(1), {1, 3, 2}, {8, 9, 10, 11, 12, 13}); | ||
| } | ||
|
|
||
| TEST_F(TestFixedSizeListArray, ToTensorNulls) { | ||
| // Nulls are ignored, leaving unspecified values in the output tensor. | ||
|
Member
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. Hmm... can we perhaps have an option to control that? |
||
| auto array = ArrayFromJSON(fixed_size_list(int32(), 2), "[[1, 2], null, [5, null]]"); | ||
| ASSERT_OK_AND_ASSIGN(auto tensor, array->ToTensor()); | ||
| ASSERT_OK(tensor->Validate()); | ||
| ASSERT_EQ(std::vector<int64_t>({3, 2}), tensor->shape()); | ||
| } | ||
|
|
||
| TEST_F(TestFixedSizeListArray, ToTensorZeroLength) { | ||
| auto array = ArrayFromJSON(fixed_size_list(int64(), 2), "[]"); | ||
| ASSERT_OK_AND_ASSIGN(auto tensor, array->ToTensor()); | ||
| ASSERT_OK(tensor->Validate()); | ||
| ASSERT_EQ(std::vector<int64_t>({0, 2}), tensor->shape()); | ||
| } | ||
|
|
||
| TEST_F(TestFixedSizeListArray, ToTensorUnsupportedType) { | ||
| ASSERT_RAISES( | ||
| NotImplemented, | ||
| ArrayFromJSON(fixed_size_list(utf8(), 1), R"([["a"], ["b"]])")->ToTensor()); | ||
| ASSERT_RAISES( | ||
| Invalid, | ||
| ArrayFromJSON(fixed_size_list(boolean(), 2), "[[true, false]]")->ToTensor()); | ||
| ASSERT_RAISES(Invalid, | ||
| ArrayFromJSON(fixed_size_list(date32(), 2), "[[1, 2]]")->ToTensor()); | ||
| } | ||
|
|
||
| } // namespace arrow | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,13 +33,15 @@ | |
| #include "arrow/array/util.h" | ||
| #include "arrow/buffer.h" | ||
| #include "arrow/status.h" | ||
| #include "arrow/tensor.h" | ||
| #include "arrow/type.h" | ||
| #include "arrow/type_fwd.h" | ||
| #include "arrow/type_traits.h" | ||
| #include "arrow/util/bit_util.h" | ||
| #include "arrow/util/bitmap_generate.h" | ||
| #include "arrow/util/bitmap_ops.h" | ||
| #include "arrow/util/checked_cast.h" | ||
| #include "arrow/util/int_util_overflow.h" | ||
| #include "arrow/util/list_util.h" | ||
| #include "arrow/util/logging_internal.h" | ||
| #include "arrow/util/unreachable.h" | ||
|
|
@@ -1001,6 +1003,48 @@ Result<std::shared_ptr<Array>> FixedSizeListArray::Flatten( | |
| return FlattenListArray(*this, memory_pool); | ||
| } | ||
|
|
||
| Result<std::shared_ptr<Tensor>> FixedSizeListArray::ToTensor() const { | ||
| const auto* data = this->data().get(); | ||
| auto type = this->type(); | ||
|
AntoinePrv marked this conversation as resolved.
|
||
| int64_t offset = data->offset; | ||
| int64_t length = data->length; | ||
| std::vector<int64_t> shape{length}; | ||
|
|
||
| // Iterate over nested fixed length container types. | ||
| // Each nested container increase the tensor dimension. | ||
| while (type->id() == Type::FIXED_SIZE_LIST) { | ||
| const auto* fsl = internal::checked_cast<const FixedSizeListType*>(type.get()); | ||
| type = fsl->value_type(); | ||
| data = data->child_data.front().get(); | ||
|
|
||
| if (internal::MultiplyWithOverflow(offset, int64_t{fsl->list_size()}, &offset) || | ||
| internal::AddWithOverflow(offset, data->offset, &offset) || | ||
| internal::MultiplyWithOverflow(length, int64_t{fsl->list_size()}, &length)) { | ||
|
Comment on lines
+1020
to
+1022
Member
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. I think the overflow checks are not necessary here either. Overflow cannot happen on a valid array (because its data needs to fit in memory, therefore be smaller than INT64_MAX). |
||
| return Status::Invalid("Flattened fixed size list does not fit in an int64"); | ||
| } | ||
| shape.push_back(fsl->list_size()); | ||
| } | ||
|
|
||
| // Only checking byte_width and leaving Tensor::Make error on unsupported types. | ||
| if (!is_fixed_width(*type)) { | ||
| return Status::NotImplemented("Expected a fixed width leaf type, got ", type->name()); | ||
| } | ||
|
|
||
| std::shared_ptr<Buffer> buffer = nullptr; | ||
| if (const auto& buf = data->buffers[1]; buf != NULLPTR) { | ||
| const int64_t byte_width = type->byte_width(); | ||
| int64_t boffset = 0; | ||
| int64_t blength = 0; | ||
| if (internal::MultiplyWithOverflow(offset, byte_width, &boffset) || | ||
| internal::MultiplyWithOverflow(length, byte_width, &blength)) { | ||
| return Status::Invalid("Array byte size does not fit in an int64"); | ||
| } | ||
| ARROW_ASSIGN_OR_RAISE(buffer, SliceBufferSafe(buf, boffset, blength)); | ||
| } | ||
|
AntoinePrv marked this conversation as resolved.
|
||
|
|
||
| return Tensor::Make(std::move(type), std::move(buffer), std::move(shape)); | ||
| } | ||
|
|
||
| // ---------------------------------------------------------------------- | ||
| // Struct | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,11 +25,14 @@ | |
|
|
||
| #include "arrow/array/array_base.h" | ||
| #include "arrow/array/data.h" | ||
| #include "arrow/buffer.h" | ||
| #include "arrow/stl_iterator.h" | ||
| #include "arrow/tensor.h" | ||
| #include "arrow/type.h" | ||
| #include "arrow/type_fwd.h" // IWYU pragma: export | ||
| #include "arrow/type_traits.h" | ||
| #include "arrow/util/bit_util.h" | ||
| #include "arrow/util/int_util_overflow.h" | ||
| #include "arrow/util/macros.h" | ||
| #include "arrow/util/visibility.h" | ||
|
|
||
|
|
@@ -128,6 +131,23 @@ class NumericArray : public PrimitiveArray { | |
|
|
||
| IteratorType end() const { return IteratorType(*this, length()); } | ||
|
|
||
| /// \brief Return a one dimensional Tensor. | ||
| Result<std::shared_ptr<Tensor>> ToTensor() const override { | ||
| // Could be non-templated | ||
| const int64_t byte_width = type()->byte_width(); | ||
| std::shared_ptr<Buffer> buffer; | ||
| if (data_->buffers[1] != NULLPTR) { | ||
| int64_t boffset = 0; | ||
| int64_t blength = 0; | ||
| if (internal::MultiplyWithOverflow(data_->offset, byte_width, &boffset) || | ||
| internal::MultiplyWithOverflow(length(), byte_width, &blength)) { | ||
| return Status::Invalid("Array byte size does not fit in an int64"); | ||
| } | ||
|
Comment on lines
+142
to
+145
Member
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. That can't happen for a valid array, so we needn't check for this. |
||
| ARROW_ASSIGN_OR_RAISE(buffer, SliceBufferSafe(data_->buffers[1], boffset, blength)); | ||
| } | ||
|
AntoinePrv marked this conversation as resolved.
|
||
| return Tensor::Make(type(), std::move(buffer), {length()}); | ||
| } | ||
|
|
||
| protected: | ||
| NumericArray() : values_(NULLPTR) {} | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -50,6 +50,7 @@ | |
| #include "arrow/result.h" | ||
| #include "arrow/scalar.h" | ||
| #include "arrow/status.h" | ||
| #include "arrow/tensor.h" | ||
| #include "arrow/testing/builder.h" | ||
| #include "arrow/testing/extension_type.h" | ||
| #include "arrow/testing/gtest_compat.h" | ||
|
|
@@ -1218,6 +1219,52 @@ TEST(TestPrimitiveArray, CtorNoValidityBitmap) { | |
| ASSERT_EQ(arr.data()->null_count, 0); | ||
| } | ||
|
|
||
| TEST(TestPrimitiveArray, ToTensor) { | ||
| const std::vector<int64_t> shape = {5}; | ||
| const std::vector<int64_t> strides = {sizeof(int32_t)}; | ||
|
|
||
| auto array = ArrayFromJSON(int32(), "[1, 2, 3, 4, 5]"); | ||
| ASSERT_OK_AND_ASSIGN(auto tensor, array->ToTensor()); | ||
| ASSERT_OK(tensor->Validate()); | ||
|
|
||
| EXPECT_EQ(int32(), tensor->type()); | ||
| EXPECT_EQ(shape, tensor->shape()); | ||
| EXPECT_EQ(strides, tensor->strides()); | ||
| EXPECT_TRUE(tensor->is_contiguous()); | ||
| EXPECT_TRUE( | ||
| TensorFromJSON(int32(), "[1, 2, 3, 4, 5]", shape, strides)->Equals(*tensor)); | ||
| } | ||
|
|
||
| TEST(TestPrimitiveArray, ToTensorSliced) { | ||
| const std::vector<int64_t> shape = {3}; | ||
| const std::vector<int64_t> strides = {sizeof(int64_t)}; | ||
|
|
||
| auto array = ArrayFromJSON(int64(), "[1, 2, 3, 4, 5]")->Slice(2); | ||
| ASSERT_OK_AND_ASSIGN(auto tensor, array->ToTensor()); | ||
| ASSERT_OK(tensor->Validate()); | ||
|
|
||
| EXPECT_EQ(shape, tensor->shape()); | ||
| EXPECT_TRUE(TensorFromJSON(int64(), "[3, 4, 5]", shape, strides)->Equals(*tensor)); | ||
| } | ||
|
|
||
| TEST(TestPrimitiveArray, ToTensorNulls) { | ||
| // Nulls are ignored, leaving unspecified values in the output tensor. | ||
|
Member
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. Same comment as in |
||
| const std::vector<int64_t> shape = {3}; | ||
|
|
||
| auto array = ArrayFromJSON(int32(), "[1, null, 3]"); | ||
| ASSERT_OK_AND_ASSIGN(auto tensor, array->ToTensor()); | ||
| ASSERT_OK(tensor->Validate()); | ||
|
|
||
| EXPECT_EQ(shape, tensor->shape()); | ||
| } | ||
|
|
||
| TEST(TestPrimitiveArray, ToTensorUnsupportedType) { | ||
| auto array = ArrayFromJSON(date32(), "[1, 2, 3]"); | ||
| ASSERT_RAISES(Invalid, array->ToTensor()); | ||
|
|
||
| ASSERT_RAISES(NotImplemented, ArrayFromJSON(utf8(), R"(["a"])")->ToTensor()); | ||
| } | ||
|
|
||
| class TestBuilder : public ::testing::Test { | ||
| protected: | ||
| MemoryPool* pool_ = default_memory_pool(); | ||
|
|
||
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.
API nit, but I think it would make more sense to expose Tensor facilities only in the corresponding headers, therefore have
Tensor::FromArrayrather thanArray::ToTensor.It would also mirror
FixedShapeTensorArray::FromTensor.