This is an automated email from the ASF dual-hosted git repository.
rok pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow.git
The following commit(s) were added to refs/heads/main by this push:
new 5d2a8fa709 GH-50197: [C++][Python] Add "hypot" compute kernel (#50198)
5d2a8fa709 is described below
commit 5d2a8fa7090bdbf9c23172c5c1f20331ebc117c3
Author: Shrivas Shankar <[email protected]>
AuthorDate: Tue Jun 30 20:12:36 2026 -0500
GH-50197: [C++][Python] Add "hypot" compute kernel (#50198)
### Rationale for this change
Arrow has `atan2` and `logb` but no `hypot`. Computing a Euclidean norm
currently needs `sqrt(add(multiply(x, x), multiply(y, y)))`, which can overflow
at the `multiply` step even when the result is representable.
### What changes are included in this PR?
Add a `hypot` kernel backed by `std::hypot` (float32/float64, with
integer/decimal inputs promoted to float64), the `Hypot()` C++ function, a
`FunctionDoc` and `compute.rst` entry, and a Python expression test. `pc.hypot`
is auto-exposed from the registry.
### Are these changes tested?
Yes, C++ tests for the basic cases, null/NaN/Inf handling, and an overflow
case confirming it doesn't fall back to a naive `sqrt(x*x + y*y)`.
### Are there any user-facing changes?
Yes, a new `hypot` function in C++ and Python. Additive only.
* GitHub Issue: #50197
Lead-authored-by: Shrivas Shankar <[email protected]>
Co-authored-by: Shrivas Shankar
<[email protected]>
Co-authored-by: Rok Mihevc <[email protected]>
Signed-off-by: Rok Mihevc <[email protected]>
---
cpp/src/arrow/compute/api_scalar.cc | 1 +
cpp/src/arrow/compute/api_scalar.h | 9 ++++
cpp/src/arrow/compute/kernels/scalar_arithmetic.cc | 20 +++++++++
.../compute/kernels/scalar_arithmetic_test.cc | 51 +++++++++++++++++++++-
docs/source/cpp/compute.rst | 6 +++
docs/source/python/api/compute.rst | 1 +
python/pyarrow/tests/test_compute.py | 12 +++++
7 files changed, 99 insertions(+), 1 deletion(-)
diff --git a/cpp/src/arrow/compute/api_scalar.cc
b/cpp/src/arrow/compute/api_scalar.cc
index b43eca542f..0aa8fd757a 100644
--- a/cpp/src/arrow/compute/api_scalar.cc
+++ b/cpp/src/arrow/compute/api_scalar.cc
@@ -805,6 +805,7 @@ SCALAR_ARITHMETIC_BINARY(ShiftLeft, "shift_left",
"shift_left_checked")
SCALAR_ARITHMETIC_BINARY(ShiftRight, "shift_right", "shift_right_checked")
SCALAR_ARITHMETIC_BINARY(Subtract, "subtract", "subtract_checked")
SCALAR_EAGER_BINARY(Atan2, "atan2")
+SCALAR_EAGER_BINARY(Hypot, "hypot")
SCALAR_EAGER_UNARY(Floor, "floor")
SCALAR_EAGER_UNARY(Ceil, "ceil")
SCALAR_EAGER_UNARY(Trunc, "trunc")
diff --git a/cpp/src/arrow/compute/api_scalar.h
b/cpp/src/arrow/compute/api_scalar.h
index 8b341e865a..c4238b956c 100644
--- a/cpp/src/arrow/compute/api_scalar.h
+++ b/cpp/src/arrow/compute/api_scalar.h
@@ -806,6 +806,15 @@ Result<Datum> Atan(const Datum& arg, ExecContext* ctx =
NULLPTR);
ARROW_EXPORT
Result<Datum> Atan2(const Datum& y, const Datum& x, ExecContext* ctx =
NULLPTR);
+/// \brief Compute the hypotenuse (Euclidean norm) of x and y, equivalent to
+/// sqrt(x^2 + y^2), without undue overflow or underflow at intermediate
stages.
+/// \param[in] x The x-values to compute the hypotenuse for.
+/// \param[in] y The y-values to compute the hypotenuse for.
+/// \param[in] ctx the function execution context, optional
+/// \return the elementwise hypotenuse of the values
+ARROW_EXPORT
+Result<Datum> Hypot(const Datum& x, const Datum& y, ExecContext* ctx =
NULLPTR);
+
/// \brief Compute the hyperbolic sine of the array values.
/// \param[in] arg The values to compute the hyperbolic sine for.
/// \param[in] ctx the function execution context, optional
diff --git a/cpp/src/arrow/compute/kernels/scalar_arithmetic.cc
b/cpp/src/arrow/compute/kernels/scalar_arithmetic.cc
index f09e209e81..cd60d5280b 100644
--- a/cpp/src/arrow/compute/kernels/scalar_arithmetic.cc
+++ b/cpp/src/arrow/compute/kernels/scalar_arithmetic.cc
@@ -368,6 +368,14 @@ struct Atan2 {
}
};
+struct Hypot {
+ template <typename T, typename Arg0, typename Arg1>
+ static enable_if_floating_value<Arg0, T> Call(KernelContext*, Arg0 x, Arg1
y, Status*) {
+ static_assert(std::is_same<T, Arg0>::value, "");
+ return std::hypot(x, y);
+ }
+};
+
struct LogNatural {
template <typename T, typename Arg>
static enable_if_floating_value<Arg, T> Call(KernelContext*, Arg arg,
Status*) {
@@ -1346,6 +1354,14 @@ const FunctionDoc atan2_doc{"Compute the inverse tangent
of y/x",
("The return value is in the range [-pi, pi]."),
{"y", "x"}};
+const FunctionDoc hypot_doc{
+ "Compute the hypotenuse (Euclidean norm) of x and y",
+ ("The result is equivalent to `sqrt(x^2 + y^2)`, but is computed without\n"
+ "undue overflow or underflow at intermediate stages of the computation.\n"
+ "If either x or y is +/-infinity, +infinity is returned, even if the\n"
+ "other argument is NaN."),
+ {"x", "y"}};
+
const FunctionDoc atanh_doc{"Compute the inverse hyperbolic tangent",
("NaN is returned for input values x with \\|x\\|
> 1.\n"
"At x = +/- 1, returns +/- infinity.\n"
@@ -1765,6 +1781,10 @@ void RegisterScalarArithmetic(FunctionRegistry*
registry) {
"sqrt_checked", sqrt_checked_doc);
DCHECK_OK(registry->AddFunction(std::move(sqrt_checked)));
+ // ----------------------------------------------------------------------
+ auto hypot = MakeArithmeticFunctionFloatingPoint<Hypot>("hypot", hypot_doc);
+ DCHECK_OK(registry->AddFunction(std::move(hypot)));
+
// ----------------------------------------------------------------------
auto sign =
MakeUnaryArithmeticFunctionWithFixedIntOutType<Sign, Int8Type>("sign",
sign_doc);
diff --git a/cpp/src/arrow/compute/kernels/scalar_arithmetic_test.cc
b/cpp/src/arrow/compute/kernels/scalar_arithmetic_test.cc
index 9367ad2c89..11ba956071 100644
--- a/cpp/src/arrow/compute/kernels/scalar_arithmetic_test.cc
+++ b/cpp/src/arrow/compute/kernels/scalar_arithmetic_test.cc
@@ -1148,6 +1148,10 @@ TEST(TestBinaryArithmetic, DispatchBest) {
CheckDispatchBest("atan2", {float32(), float64()}, {float64(), float64()});
// Integer always promotes to double
CheckDispatchBest("atan2", {float32(), int8()}, {float64(), float64()});
+
+ CheckDispatchBest("hypot", {float32(), float32()}, {float32(), float32()});
+ CheckDispatchBest("hypot", {float32(), float64()}, {float64(), float64()});
+ CheckDispatchBest("hypot", {int32(), uint8()}, {float64(), float64()});
}
TEST(TestBinaryArithmetic, Null) {
@@ -1159,7 +1163,8 @@ TEST(TestBinaryArithmetic, Null) {
}
}
- for (std::string name : {"atan2", "bit_wise_and", "bit_wise_or",
"bit_wise_xor"}) {
+ for (std::string name :
+ {"atan2", "bit_wise_and", "bit_wise_or", "bit_wise_xor", "hypot"}) {
AssertNullToNull(name);
}
}
@@ -2807,6 +2812,50 @@ TYPED_TEST(TestBinaryArithmeticFloating, TrigAtan2) {
-M_PI_2, 0, M_PI));
}
+TYPED_TEST(TestBinaryArithmeticFloating, Hypot) {
+ SKIP_IF_HALF_FLOAT();
+
+ this->SetNansEqual(true);
+ auto hypot = [](const Datum& x, const Datum& y, ArithmeticOptions,
ExecContext* ctx) {
+ return Hypot(x, y, ctx);
+ };
+ this->AssertBinop(hypot, "[]", "[]", "[]");
+ // Pythagorean triples; result is independent of the sign of either argument,
+ // and hypot(0, 0) == 0.
+ this->AssertBinop(hypot, "[3, -3, 5, -8, 0]", "[4, -4, -12, 15, 0]",
+ "[5, 5, 13, 17, 0]");
+ // Null propagation.
+ this->AssertBinop(hypot, "[1, null, 0]", "[null, 1, 0]", "[null, null, 0]");
+ // NaN propagates, unless the other argument is infinite (per C99/IEEE 754,
+ // hypot(+/-Inf, NaN) == +Inf).
+ this->AssertBinop(hypot, "[NaN, 1, NaN, Inf]", "[1, NaN, NaN, NaN]",
+ "[NaN, NaN, NaN, Inf]");
+ // +/-infinity in either argument yields +infinity.
+ this->AssertBinop(hypot, "[Inf, -Inf, 3]", "[4, 0, -Inf]", "[Inf, Inf,
Inf]");
+}
+
+// hypot avoids overflow/underflow at intermediate stages: for float32 the
+// squares below overflow to +Inf, so a naive sqrt(x*x + y*y) would return Inf,
+// while the kernel (like std::hypot) returns the correct finite result.
+TEST(TestBinaryArithmetic, HypotOverflowSafety) {
+ std::vector<float> xs = {3.0e30f, 5.0e37f, -2.0e30f};
+ std::vector<float> ys = {4.0e30f, 1.2e38f, 0.0f};
+ ASSERT_TRUE(std::isinf(xs[0] * xs[0])); // the naive intermediate overflows
+
+ std::vector<float> expected_vals;
+ for (size_t i = 0; i < xs.size(); ++i) {
+ expected_vals.push_back(std::hypot(xs[i], ys[i]));
+ }
+
+ std::shared_ptr<Array> x, y, expected;
+ ArrayFromVector<FloatType>(xs, &x);
+ ArrayFromVector<FloatType>(ys, &y);
+ ArrayFromVector<FloatType>(expected_vals, &expected);
+
+ ASSERT_OK_AND_ASSIGN(Datum result, Hypot(x, y));
+ AssertArraysEqual(*expected, *result.make_array(), /*verbose=*/true);
+}
+
TYPED_TEST(TestUnaryArithmeticFloating, TrigAtanh) {
SKIP_IF_HALF_FLOAT();
diff --git a/docs/source/cpp/compute.rst b/docs/source/cpp/compute.rst
index e4092af70c..1e067c5218 100644
--- a/docs/source/cpp/compute.rst
+++ b/docs/source/cpp/compute.rst
@@ -512,6 +512,8 @@ Mixed time resolution temporal inputs will be cast to
finest input resolution.
+------------------+--------+-------------------------+-------------------------------+-------+
| expm1 | Unary | Numeric | Float32/Float64
| |
+------------------+--------+-------------------------+-------------------------------+-------+
+| hypot | Binary | Numeric | Float32/Float64
| \(3) |
++------------------+--------+-------------------------+-------------------------------+-------+
| multiply | Binary | Numeric/Temporal | Numeric/Temporal
| \(1) |
+------------------+--------+-------------------------+-------------------------------+-------+
| multiply_checked | Binary | Numeric/Temporal | Numeric/Temporal
| \(1) |
@@ -560,6 +562,10 @@ Mixed time resolution temporal inputs will be cast to
finest input resolution.
values return NaN. Integral and decimal values return signedness as Int8 and
floating-point values return it with the same type as the input values.
+* \(3) Computes ``sqrt(x^2 + y^2)`` without undue overflow or underflow at
+ intermediate stages of the computation. If either argument is infinite, the
+ result is ``+Inf`` even if the other argument is NaN.
+
Bit-wise functions
~~~~~~~~~~~~~~~~~~
diff --git a/docs/source/python/api/compute.rst
b/docs/source/python/api/compute.rst
index f58856c5bd..6a4b04468d 100644
--- a/docs/source/python/api/compute.rst
+++ b/docs/source/python/api/compute.rst
@@ -95,6 +95,7 @@ throws an ``ArrowInvalid`` exception when overflow is
detected.
divide_checked
exp
expm1
+ hypot
multiply
multiply_checked
negate
diff --git a/python/pyarrow/tests/test_compute.py
b/python/pyarrow/tests/test_compute.py
index c8cd86b060..39167e76c8 100644
--- a/python/pyarrow/tests/test_compute.py
+++ b/python/pyarrow/tests/test_compute.py
@@ -3905,6 +3905,18 @@ def test_rank_normal_options():
assert result.to_pylist() == expected
[email protected]
+def test_hypot():
+ x = np.array([3.0, 0.0, -5.0, 1.5, 7.25])
+ y = np.array([4.0, 0.0, 12.0, -2.0, 0.0])
+ result = pc.hypot(pa.array(x), pa.array(y))
+ np.testing.assert_array_almost_equal(
+ result.to_numpy(zero_copy_only=False), np.hypot(x, y))
+
+ # scalar inputs
+ assert pc.hypot(pa.scalar(3.0), pa.scalar(4.0)).as_py() ==
pytest.approx(5.0)
+
+
def create_sample_expressions():
# We need a schema for substrait conversion
schema = pa.schema([pa.field("i64", pa.int64()), pa.field(