This is an automated email from the ASF dual-hosted git repository.

zhengruifeng pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/master by this push:
     new 417b37cc5de6 [SPARK-58694][PS] Use native Spark functions for NumPy 
logaddexp and logaddexp2
417b37cc5de6 is described below

commit 417b37cc5de6fed8587b0ec16e84364ed0a266c2
Author: Ruifeng Zheng <[email protected]>
AuthorDate: Wed Aug 12 18:27:51 2026 +0800

    [SPARK-58694][PS] Use native Spark functions for NumPy logaddexp and 
logaddexp2
    
    ### What changes were proposed in this pull request?
    
    This PR replaces the pandas UDF implementations of `np.logaddexp` and 
`np.logaddexp2` in the pandas API on Spark with native Spark SQL expressions. 
The expressions use a numerically stable max-plus-log1p form and explicitly 
handle null, NaN, and infinite inputs.
    
    ### Why are the changes needed?
    
    Using native Spark expressions avoids Python and Arrow execution overhead 
while preserving NumPy-compatible results.
    
    ### Does this PR introduce _any_ user-facing change?
    
    No.
    
    ### How was this patch tested?
    
    - Added `test_np_logaddexp`, covering finite values, signed zero, NaN, 
infinities, and extreme finite magnitudes for both ufuncs.
    - `conda run -n spark-dev-313 ruff check 
python/pyspark/pandas/numpy_compat.py 
python/pyspark/pandas/tests/test_numpy_compat.py`
    - `conda run -n spark-dev-313 ruff format --check 
python/pyspark/pandas/numpy_compat.py 
python/pyspark/pandas/tests/test_numpy_compat.py`
    
    The PySpark test could not run locally because the available Java 11 
runtime cannot load the locally built Java 17 Spark artifacts.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: Codex (GPT-5)
    
    Closes #57898 from zhengruifeng/pandas-native-logaddexp-logaddexp2-dev2.
    
    Authored-by: Ruifeng Zheng <[email protected]>
    Signed-off-by: Ruifeng Zheng <[email protected]>
---
 python/pyspark/pandas/numpy_compat.py            | 28 ++++++++++---
 python/pyspark/pandas/tests/test_numpy_compat.py | 50 ++++++++++++++++++++++++
 2 files changed, 72 insertions(+), 6 deletions(-)

diff --git a/python/pyspark/pandas/numpy_compat.py 
b/python/pyspark/pandas/numpy_compat.py
index f361e02712f9..6611abb3b44c 100644
--- a/python/pyspark/pandas/numpy_compat.py
+++ b/python/pyspark/pandas/numpy_compat.py
@@ -134,6 +134,26 @@ def _fmod_func(c1: Column, c2: Column) -> Column:
     )
 
 
+def _logaddexp_func(c1: Column, c2: Column, base2: bool = False) -> Column:
+    c1_double = c1.cast("double")
+    c2_double = c2.cast("double")
+    difference = F.abs(c1_double - c2_double)
+    maximum = F.greatest(c1_double, c2_double)
+    if base2:
+        log_term = F.log1p(F.pow(F.lit(2.0), -difference)) / F.log(F.lit(2.0))
+    else:
+        log_term = F.log1p(F.exp(-difference))
+
+    return (
+        F.when(c1_double.isNull() | F.isnan(c1_double), c1_double)
+        .when(c2_double.isNull() | F.isnan(c2_double), c2_double)
+        .when((c1_double == float("inf")) | (c2_double == float("inf")), 
F.lit(float("inf")))
+        .when(c1_double == float("-inf"), c2_double + F.lit(0.0))
+        .when(c2_double == float("-inf"), c1_double + F.lit(0.0))
+        .otherwise(maximum + log_term)
+    )
+
+
 def _floor_divide_func(c1: Column, c2: Column) -> Column:
     c1_double = c1.cast("double")
     c2_double = c2.cast("double")
@@ -221,12 +241,8 @@ binary_np_spark_mappings = {
     "left_shift": lambda c1, c2: F.when((c2 < 0) | (c2 >= 64), 
F.lit(0)).otherwise(
         F.call_function("shiftleft", c1, c2)
     ),
-    "logaddexp": pandas_udf(  # type: ignore[call-overload]
-        lambda s1, s2: np.logaddexp(s1, s2), DoubleType()
-    ),
-    "logaddexp2": pandas_udf(  # type: ignore[call-overload]
-        lambda s1, s2: np.logaddexp2(s1, s2), DoubleType()
-    ),
+    "logaddexp": _logaddexp_func,
+    "logaddexp2": lambda c1, c2: _logaddexp_func(c1, c2, base2=True),
     "logical_and": lambda c1, c2: c1.cast(BooleanType()) & 
c2.cast(BooleanType()),
     "logical_or": lambda c1, c2: c1.cast(BooleanType()) | 
c2.cast(BooleanType()),
     "logical_xor": lambda c1, c2: (
diff --git a/python/pyspark/pandas/tests/test_numpy_compat.py 
b/python/pyspark/pandas/tests/test_numpy_compat.py
index 79c1fb1da7e6..36eb4444fc53 100644
--- a/python/pyspark/pandas/tests/test_numpy_compat.py
+++ b/python/pyspark/pandas/tests/test_numpy_compat.py
@@ -330,6 +330,56 @@ class NumPyCompatTestsMixin:
             )
             self.assert_eq(result, np.floor_divide(pdf.x1, pdf.x2), 
almost=True)
 
+    def test_np_logaddexp(self):
+        for pdf in (
+            pd.DataFrame(
+                {
+                    "x1": [-64, -2, -1, 0, 1, 2, 64],
+                    "x2": [2, 3, -2, -3, -3, 0, 2],
+                }
+            ),
+            pd.DataFrame(
+                {
+                    "x1": [
+                        -np.inf,
+                        -np.inf,
+                        -2.0,
+                        -2.0,
+                        -0.0,
+                        0.0,
+                        2.0,
+                        np.inf,
+                        np.inf,
+                        np.nan,
+                        -1000.0,
+                        -np.inf,
+                        -0.0,
+                    ],
+                    "x2": [
+                        -np.inf,
+                        3.0,
+                        -np.inf,
+                        2.0,
+                        0.0,
+                        -0.0,
+                        np.inf,
+                        2.0,
+                        np.inf,
+                        2.0,
+                        1000.0,
+                        -0.0,
+                        -np.inf,
+                    ],
+                }
+            ),
+        ):
+            psdf = ps.from_pandas(pdf)
+            for np_func in (np.logaddexp, np.logaddexp2):
+                result = np_func(psdf.x1, psdf.x2)
+                expected = np_func(pdf.x1, pdf.x2)
+                self.assert_eq(result, expected, almost=True)
+                self.assert_eq(np.signbit(result.to_pandas()), 
np.signbit(expected))
+
     def test_np_fmax_fmin(self):
         for pdf in (
             pd.DataFrame({"x1": [-2, -1, 0, 1, 2], "x2": [2, 1, 0, -1, -2]}),


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to