spark-test commented on code in PR #59590:
URL: https://github.com/apache/spark/pull/59590#discussion_r4496869277


##########
sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/aggregate/VectorAggSuite.scala:
##########
@@ -821,4 +858,200 @@ class VectorAggSuite extends SparkFunSuite {
       condition = "VECTOR_DIMENSION_MISMATCH",
       parameters = Map("functionName" -> "`vector_sum`", "leftDim" -> "2", 
"rightDim" -> "3"))
   }
+
+  test("VectorAvg - double: empty buffer returns null") {
+    val (agg, buffer) = createDoubleVectorAvg()
+    assertResult(null)(agg.eval(buffer))
+  }
+
+  test("VectorAvg - double: multiple vectors") {
+    val (agg, buffer) = createDoubleVectorAvg()
+    agg.update(buffer, createDoubleInputRow(Array(1.0, 2.0)))
+    agg.update(buffer, createDoubleInputRow(Array(2.0, 4.0)))
+    agg.update(buffer, createDoubleInputRow(Array(3.0, 6.0)))
+    assertResult(Array(2.0, 4.0))(evalAsDoubleArray(agg, buffer))
+  }
+
+  test("VectorAvg - double: result is independent of later buffer updates") {
+    val (agg, buffer) = createDoubleVectorAvg()
+    agg.update(buffer, createDoubleInputRow(Array(1.0, 2.0, 3.0)))
+    val result = agg.eval(buffer).asInstanceOf[ArrayData]
+
+    agg.update(buffer, createDoubleInputRow(Array(3.0, 4.0, 5.0)))
+    assertResult(Array(2.0, 3.0, 4.0))(evalAsDoubleArray(agg, buffer))
+    assertResult(Array(1.0, 2.0, 3.0))(result.toDoubleArray())
+  }
+
+  test("VectorAvg - double: keeps precision lost by float") {
+    val (agg, buffer) = createDoubleVectorAvg()
+    agg.update(buffer, createDoubleInputRow(Array(16777216.0, 1.0e300)))
+    agg.update(buffer, createDoubleInputRow(Array(16777218.0, 1.0e300)))
+    assertResult(Array(16777217.0, 1.0e300))(evalAsDoubleArray(agg, buffer))
+  }
+
+  test("VectorAvg - double: merge buffers with different counts") {
+    val (agg, buffer1) = createDoubleVectorAvg()
+    val (_, buffer2) = createDoubleVectorAvg()
+    agg.update(buffer1, createDoubleInputRow(Array(16777216.0, 10.0)))
+    agg.update(buffer2, createDoubleInputRow(Array(16777218.0, 1.0)))
+    agg.update(buffer2, createDoubleInputRow(Array(16777218.0, 2.0)))
+    agg.update(buffer2, createDoubleInputRow(Array(16777218.0, 3.0)))
+    agg.merge(buffer1, buffer2)
+    assertResult(Array(16777217.5, 4.0))(evalAsDoubleArray(agg, buffer1))
+  }
+
+  test("VectorAvg - double: merge with empty buffers") {
+    val (agg, buffer1) = createDoubleVectorAvg()
+    val (_, buffer2) = createDoubleVectorAvg()
+    agg.merge(buffer1, buffer2)
+    assertResult(null)(agg.eval(buffer1))
+
+    agg.update(buffer2, createDoubleInputRow(Array(1.5, 2.5)))
+    agg.merge(buffer1, buffer2)
+    assertResult(Array(1.5, 2.5))(evalAsDoubleArray(agg, buffer1))
+  }
+
+  test("VectorAvg - double: null vectors and vectors with null elements are 
skipped") {
+    val (agg, buffer) = createDoubleVectorAvg()
+    agg.update(buffer, createDoubleInputRow(Array(1.0, 2.0)))
+    agg.update(buffer, createNullInputRow())
+    agg.update(buffer, createDoubleInputRowWithNullElement(Array(null, 10.0)))
+    agg.update(buffer, createDoubleInputRow(Array(3.0, 4.0)))
+    assertResult(Array(2.0, 3.0))(evalAsDoubleArray(agg, buffer))
+  }
+
+  test("VectorAvg - double: empty vectors") {
+    val (agg, buffer) = createDoubleVectorAvg()
+    agg.update(buffer, createDoubleInputRow(Array.empty[Double]))
+    agg.update(buffer, createDoubleInputRow(Array.empty[Double]))
+    assertResult(Array.empty[Double])(evalAsDoubleArray(agg, buffer))
+  }
+
+  test("VectorAvg - double: special values") {
+    val (agg, buffer) = createDoubleVectorAvg()
+    agg.update(buffer, createDoubleInputRow(Array(1.0, 2.0, Double.MaxValue)))
+    agg.update(buffer,
+      createDoubleInputRow(Array(Double.NaN, Double.NegativeInfinity, 
Double.MaxValue)))
+    val result = evalAsDoubleArray(agg, buffer)
+    assert(result(0).isNaN)
+    assertResult(Double.NegativeInfinity)(result(1))
+    assertResult(Double.MaxValue)(result(2))
+  }
+
+  test("VectorAvg - double: opposite extreme values do not overflow") {
+    val (agg, buffer) = createDoubleVectorAvg()
+    agg.update(buffer, createDoubleInputRow(Array(Double.MaxValue, 
-Double.MaxValue)))
+    agg.update(buffer, createDoubleInputRow(Array(-Double.MaxValue, 
Double.MaxValue)))
+    assertResult(Array(0.0, 0.0))(evalAsDoubleArray(agg, buffer))
+
+    val (_, buffer1) = createDoubleVectorAvg()
+    val (_, buffer2) = createDoubleVectorAvg()
+    agg.update(buffer1, createDoubleInputRow(Array(Double.MaxValue, 
-Double.MaxValue)))
+    agg.update(buffer2, createDoubleInputRow(Array(-Double.MaxValue, 
Double.MaxValue)))
+    agg.merge(buffer1, buffer2)

Review Comment:
   **Nit (P3):** This merge combines two single-row buffers with opposite 
extremes, a case where the common sum-then-divide form `(leftAvg * currentCount 
+ rightAvg * inputCount) / newCount` also returns `0.0`. A bit-exact binary64 
evaluation shows that this rewrite passes every double merge assertion in this 
PR, including the `repartition(2)`, `observe` and non-finite cases. Yet it 
returns `Infinity` instead of `Double.MaxValue` when merging two 
`[Double.MaxValue]` partials. More generally, it overflows whenever a partial 
average times its row count exceeds `Double.MaxValue`. So no test guards the 
overflow safety that weighting each partial average gives multi-partition 
aggregation, `observe` and streaming state merges.
   
   The float tests have the same gap, but the float merge can itself overflow 
once counts exceed 2^24, while the double merge stays finite. So the property 
is worth pinning here. Some count splits, such as one-to-two or three-to-five, 
legitimately round one ulp below `Double.MaxValue`, so exact assertions need 
splits that land on it.
   
   **Verification:**
   - **Regression:** Merge two double vector_avg buffers whose partial averages 
hold Double.MaxValue and -Double.MaxValue in matching positions, with equal 
counts and with a one-to-three count split. The result must be exactly 
Double.MaxValue and -Double.MaxValue in those positions. A merge that 
multiplies each average by its count before dividing would produce infinities 
and fail.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/vectorExpressions.scala:
##########
@@ -778,7 +782,55 @@ object VectorAvg {
     }
   }
 
-  private val kernels: Seq[VectorAggregateKernel] = Seq(FloatAvg)
+  private object DoubleAvg extends VectorAggregateKernel.DoubleKernel {
+    override def updateElements(
+        accBytes: Array[Byte],
+        inputArray: ArrayData,
+        dim: Int,
+        newCount: Long): Unit = {
+      // Update running average: new_avg = old_avg + (new_value - old_avg) / 
new_count

Review Comment:
   **Nit (P3):** This states the plain running-average formula, but the code 
below departs from it in exactly the cases it is there to handle. For `oldAvg = 
+Infinity` and `value = 1.0` at count 2, the formula gives `NaN` while the code 
returns `+Infinity`. For `Double.MaxValue` and `-Double.MaxValue`, it gives 
`-Infinity` while the code returns `0.0`. The comment predates the overflow and 
non-finite branches and was not updated with them; `FloatAvg` documents the 
same cases. Suggested wording:
   
   ```suggestion
         // Finite deltas use: new_avg = old_avg + (new_value - old_avg) * 
invCount.
         // When subtracting finite operands overflows, both are scaled by 
invCount first.
         // When either operand is non-finite, addition gives the IEEE result 
in either order:
         // infinities absorb finite values and same-sign infinities; Inf + 
(-Inf) and NaN give NaN.
   ```
   
   **Verification:**
   - **Inspection:** The comment on the double vector_avg update loop states 
the finite-delta formula with the precomputed reciprocal of the count, and 
names the overflowing-subtraction and non-finite cases with their results. It 
must be consistent with the float kernel's documentation and with the code 
beneath it.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to