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(

Reply via email to