vinooganesh commented on code in PR #3397:
URL: https://github.com/apache/parquet-java/pull/3397#discussion_r3575348445


##########
parquet-column/src/main/java/org/apache/parquet/column/values/alp/AlpEncoderDecoder.java:
##########
@@ -0,0 +1,359 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.parquet.column.values.alp;
+
+import static org.apache.parquet.column.values.alp.AlpConstants.*;
+
+/**
+ * Core ALP (Adaptive Lossless floating-Point) encoding and decoding logic.
+ *
+ * <p>ALP works by converting floating-point values to integers using decimal 
scaling,
+ * then applying Frame of Reference encoding and bit-packing.
+ * Values that cannot be losslessly converted are stored as exceptions.
+ *
+ * <p>Encoding formula: encoded = fastRound(value * POW10[e] * 
POW10_NEGATIVE[f])
+ * <p>Decoding formula: value = encoded * POW10[f] * POW10_NEGATIVE[e]
+ *
+ * <p>The order of operations is critical for IEEE 754 correctness. Both 
formulas must
+ * be evaluated as single expressions — storing the intermediate 
multiplication result
+ * in a variable before the second multiply changes IEEE 754 rounding and 
produces extra
+ * exceptions. Uses multiply-by-reciprocal (via POW10_NEGATIVE) for C++ wire 
compatibility.
+ *
+ * <p>Exception conditions:
+ * <ul>
+ *   <li>NaN values</li>
+ *   <li>Infinity values</li>
+ *   <li>Negative zero (-0.0)</li>
+ *   <li>Out of integer range</li>
+ *   <li>Round-trip failure (decode(encode(v)) != v)</li>
+ * </ul>
+ */
+final class AlpEncoderDecoder {
+
+  private static final double ENCODING_UPPER_LIMIT = 9223372036854774784.0;
+  private static final double ENCODING_LOWER_LIMIT = -9223372036854774784.0;
+  private static final float FLOAT_ENCODING_UPPER_LIMIT = 2147483520.0f;
+  private static final float FLOAT_ENCODING_LOWER_LIMIT = -2147483520.0f;
+
+  private AlpEncoderDecoder() {
+    // Utility class
+  }
+
+  /** NaN, Inf, and -0.0 can never be encoded regardless of exponent/factor. */
+  static boolean isFloatException(float value) {
+    if (Float.isNaN(value)) {
+      return true;
+    }
+    if (Float.isInfinite(value)) {
+      return true;
+    }
+    return Float.floatToRawIntBits(value) == FLOAT_NEGATIVE_ZERO_BITS;
+  }
+
+  /** Check round-trip: encode then decode, and see if we get the same bits 
back. */
+  static boolean isFloatException(float value, int exponent, int factor) {
+    if (isFloatException(value)) {
+      return true;
+    }
+    // Check before rounding: overflow or non-finite after scaling
+    float scaled = value * FLOAT_POW10[exponent] * 
FLOAT_POW10_NEGATIVE[factor];
+    if (!Float.isFinite(scaled) || scaled > FLOAT_ENCODING_UPPER_LIMIT || 
scaled < FLOAT_ENCODING_LOWER_LIMIT) {
+      return true;
+    }
+    int encoded = encodeFloat(value, exponent, factor);
+    float decoded = decodeFloat(encoded, exponent, factor);
+    return Float.floatToRawIntBits(value) != Float.floatToRawIntBits(decoded);
+  }
+
+  /** Round float to nearest integer using magic-number trick with sign 
branching. */
+  static int fastRoundFloat(float value) {
+    if (value >= 0) {
+      return (int) ((value + MAGIC_FLOAT) - MAGIC_FLOAT);
+    } else {
+      return (int) ((value - MAGIC_FLOAT) + MAGIC_FLOAT);
+    }
+  }
+
+  /** Encode: fastRound(value * POW10[e] * POW10_NEGATIVE[f]) — single 
expression. */
+  static int encodeFloat(float value, int exponent, int factor) {
+    return fastRoundFloat(value * FLOAT_POW10[exponent] * 
FLOAT_POW10_NEGATIVE[factor]);
+  }
+
+  /** Decode: encoded * POW10[f] * POW10_NEGATIVE[e] — single expression. */
+  static float decodeFloat(int encoded, int exponent, int factor) {
+    return encoded * FLOAT_POW10[factor] * FLOAT_POW10_NEGATIVE[exponent];
+  }
+
+  static boolean isDoubleException(double value) {
+    if (Double.isNaN(value)) {
+      return true;
+    }
+    if (Double.isInfinite(value)) {
+      return true;
+    }
+    return Double.doubleToRawLongBits(value) == DOUBLE_NEGATIVE_ZERO_BITS;
+  }
+
+  static boolean isDoubleException(double value, int exponent, int factor) {
+    if (isDoubleException(value)) {
+      return true;
+    }
+    // Check before rounding: overflow or non-finite after scaling
+    double scaled = value * DOUBLE_POW10[exponent] * 
DOUBLE_POW10_NEGATIVE[factor];
+    if (!Double.isFinite(scaled) || scaled > ENCODING_UPPER_LIMIT || scaled < 
ENCODING_LOWER_LIMIT) {
+      return true;
+    }
+    long encoded = encodeDouble(value, exponent, factor);
+    double decoded = decodeDouble(encoded, exponent, factor);
+    return Double.doubleToRawLongBits(value) != 
Double.doubleToRawLongBits(decoded);
+  }
+
+  /** Round double to nearest integer using magic-number trick with sign 
branching. */
+  static long fastRoundDouble(double value) {
+    if (value >= 0) {
+      return (long) ((value + MAGIC_DOUBLE) - MAGIC_DOUBLE);
+    } else {
+      return (long) ((value - MAGIC_DOUBLE) + MAGIC_DOUBLE);
+    }
+  }
+
+  /** Encode: fastRound(value * POW10[e] * POW10_NEGATIVE[f]) — single 
expression. */
+  static long encodeDouble(double value, int exponent, int factor) {
+    return fastRoundDouble(value * DOUBLE_POW10[exponent] * 
DOUBLE_POW10_NEGATIVE[factor]);
+  }
+
+  /** Decode: encoded * POW10[f] * POW10_NEGATIVE[e] — single expression. */
+  static double decodeDouble(long encoded, int exponent, int factor) {
+    return encoded * DOUBLE_POW10[factor] * DOUBLE_POW10_NEGATIVE[exponent];
+  }
+
+  /** Number of bits needed to represent maxDelta as an unsigned value. */
+  static int bitWidthForInt(int maxDelta) {
+    if (maxDelta == 0) {
+      return 0;
+    }
+    return Integer.SIZE - Integer.numberOfLeadingZeros(maxDelta);
+  }
+
+  static int bitWidthForLong(long maxDelta) {
+    if (maxDelta == 0) {
+      return 0;
+    }
+    return Long.SIZE - Long.numberOfLeadingZeros(maxDelta);
+  }
+
+  public static class EncodingParams {
+    final int exponent;
+    final int factor;
+    final int numExceptions;
+
+    EncodingParams(int exponent, int factor, int numExceptions) {
+      this.exponent = exponent;
+      this.factor = factor;
+      this.numExceptions = numExceptions;
+    }
+  }
+
+  /**
+   * Try all (exponent, factor) combos and pick the one with the smallest 
estimated compressed size.
+   *
+   * <p>Estimated size (in bits) = {@code length * bitWidth + exceptions * 
(Float.SIZE + Short.SIZE)},
+   * where bitWidth is the number of bits needed to represent the unsigned 
range of non-exception
+   * encoded values after frame-of-reference subtraction. This matches the C++ 
ALP cost model and
+   * produces better compression ratios than minimizing exception count alone.
+   */
+  static EncodingParams findBestFloatParams(float[] values, int offset, int 
length) {
+    int bestExponent = 0;
+    int bestFactor = 0;
+    int bestExceptions = length;
+    long bestEstimatedSize = Long.MAX_VALUE;
+
+    for (int e = 0; e <= FLOAT_MAX_EXPONENT; e++) {
+      for (int f = 0; f <= e; f++) {
+        int exceptions = 0;
+        int minEncoded = Integer.MAX_VALUE;
+        int maxEncoded = Integer.MIN_VALUE;
+        for (int i = 0; i < length; i++) {
+          float value = values[offset + i];
+          if (isFloatException(value, e, f)) {
+            exceptions++;
+          } else {
+            int encoded = encodeFloat(value, e, f);
+            if (encoded < minEncoded) minEncoded = encoded;
+            if (encoded > maxEncoded) maxEncoded = encoded;
+          }
+        }
+        int nonExceptions = length - exceptions;
+        if (nonExceptions == 0) continue;
+        long delta = (nonExceptions < 2) ? 0 :
+            Integer.toUnsignedLong(maxEncoded) - 
Integer.toUnsignedLong(minEncoded);
+        int bitsPerValue = (delta == 0) ? 0 : (64 - 
Long.numberOfLeadingZeros(delta));
+        long estimatedSize = (long) length * bitsPerValue
+            + (long) exceptions * (Float.SIZE + Short.SIZE);
+        if (estimatedSize < bestEstimatedSize
+            || (estimatedSize == bestEstimatedSize
+                && (e > bestExponent || (e == bestExponent && f > 
bestFactor)))) {
+          bestEstimatedSize = estimatedSize;
+          bestExponent = e;
+          bestFactor = f;
+          bestExceptions = exceptions;
+          if (bestExceptions == 0 && bitsPerValue == 0) {
+            return new EncodingParams(bestExponent, bestFactor, 0);
+          }
+        }
+      }
+    }
+    return new EncodingParams(bestExponent, bestFactor, bestExceptions);
+  }
+
+  /** Same as findBestFloatParams but only tries the cached preset combos. */
+  static EncodingParams findBestFloatParamsWithPresets(float[] values, int 
offset, int length, int[][] presets) {
+    int bestExponent = presets[0][0];

Review Comment:
   Added E and F index constants (0 and 1) and use them in the pair arrays.



##########
parquet-column/src/main/java/org/apache/parquet/column/values/alp/AlpEncoderDecoder.java:
##########
@@ -0,0 +1,359 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.parquet.column.values.alp;
+
+import static org.apache.parquet.column.values.alp.AlpConstants.*;
+
+/**
+ * Core ALP (Adaptive Lossless floating-Point) encoding and decoding logic.
+ *
+ * <p>ALP works by converting floating-point values to integers using decimal 
scaling,
+ * then applying Frame of Reference encoding and bit-packing.
+ * Values that cannot be losslessly converted are stored as exceptions.
+ *
+ * <p>Encoding formula: encoded = fastRound(value * POW10[e] * 
POW10_NEGATIVE[f])
+ * <p>Decoding formula: value = encoded * POW10[f] * POW10_NEGATIVE[e]
+ *
+ * <p>The order of operations is critical for IEEE 754 correctness. Both 
formulas must
+ * be evaluated as single expressions — storing the intermediate 
multiplication result
+ * in a variable before the second multiply changes IEEE 754 rounding and 
produces extra
+ * exceptions. Uses multiply-by-reciprocal (via POW10_NEGATIVE) for C++ wire 
compatibility.
+ *
+ * <p>Exception conditions:
+ * <ul>
+ *   <li>NaN values</li>
+ *   <li>Infinity values</li>
+ *   <li>Negative zero (-0.0)</li>
+ *   <li>Out of integer range</li>
+ *   <li>Round-trip failure (decode(encode(v)) != v)</li>
+ * </ul>
+ */
+final class AlpEncoderDecoder {
+
+  private static final double ENCODING_UPPER_LIMIT = 9223372036854774784.0;
+  private static final double ENCODING_LOWER_LIMIT = -9223372036854774784.0;
+  private static final float FLOAT_ENCODING_UPPER_LIMIT = 2147483520.0f;
+  private static final float FLOAT_ENCODING_LOWER_LIMIT = -2147483520.0f;
+
+  private AlpEncoderDecoder() {
+    // Utility class
+  }
+
+  /** NaN, Inf, and -0.0 can never be encoded regardless of exponent/factor. */
+  static boolean isFloatException(float value) {
+    if (Float.isNaN(value)) {
+      return true;
+    }
+    if (Float.isInfinite(value)) {
+      return true;
+    }
+    return Float.floatToRawIntBits(value) == FLOAT_NEGATIVE_ZERO_BITS;
+  }
+
+  /** Check round-trip: encode then decode, and see if we get the same bits 
back. */
+  static boolean isFloatException(float value, int exponent, int factor) {
+    if (isFloatException(value)) {
+      return true;
+    }
+    // Check before rounding: overflow or non-finite after scaling
+    float scaled = value * FLOAT_POW10[exponent] * 
FLOAT_POW10_NEGATIVE[factor];
+    if (!Float.isFinite(scaled) || scaled > FLOAT_ENCODING_UPPER_LIMIT || 
scaled < FLOAT_ENCODING_LOWER_LIMIT) {
+      return true;
+    }
+    int encoded = encodeFloat(value, exponent, factor);
+    float decoded = decodeFloat(encoded, exponent, factor);
+    return Float.floatToRawIntBits(value) != Float.floatToRawIntBits(decoded);
+  }
+
+  /** Round float to nearest integer using magic-number trick with sign 
branching. */
+  static int fastRoundFloat(float value) {
+    if (value >= 0) {
+      return (int) ((value + MAGIC_FLOAT) - MAGIC_FLOAT);
+    } else {
+      return (int) ((value - MAGIC_FLOAT) + MAGIC_FLOAT);
+    }
+  }
+
+  /** Encode: fastRound(value * POW10[e] * POW10_NEGATIVE[f]) — single 
expression. */
+  static int encodeFloat(float value, int exponent, int factor) {
+    return fastRoundFloat(value * FLOAT_POW10[exponent] * 
FLOAT_POW10_NEGATIVE[factor]);
+  }
+
+  /** Decode: encoded * POW10[f] * POW10_NEGATIVE[e] — single expression. */
+  static float decodeFloat(int encoded, int exponent, int factor) {
+    return encoded * FLOAT_POW10[factor] * FLOAT_POW10_NEGATIVE[exponent];
+  }
+
+  static boolean isDoubleException(double value) {
+    if (Double.isNaN(value)) {
+      return true;
+    }
+    if (Double.isInfinite(value)) {
+      return true;
+    }
+    return Double.doubleToRawLongBits(value) == DOUBLE_NEGATIVE_ZERO_BITS;
+  }
+
+  static boolean isDoubleException(double value, int exponent, int factor) {
+    if (isDoubleException(value)) {
+      return true;
+    }
+    // Check before rounding: overflow or non-finite after scaling
+    double scaled = value * DOUBLE_POW10[exponent] * 
DOUBLE_POW10_NEGATIVE[factor];
+    if (!Double.isFinite(scaled) || scaled > ENCODING_UPPER_LIMIT || scaled < 
ENCODING_LOWER_LIMIT) {
+      return true;
+    }
+    long encoded = encodeDouble(value, exponent, factor);
+    double decoded = decodeDouble(encoded, exponent, factor);
+    return Double.doubleToRawLongBits(value) != 
Double.doubleToRawLongBits(decoded);
+  }
+
+  /** Round double to nearest integer using magic-number trick with sign 
branching. */
+  static long fastRoundDouble(double value) {
+    if (value >= 0) {
+      return (long) ((value + MAGIC_DOUBLE) - MAGIC_DOUBLE);
+    } else {
+      return (long) ((value - MAGIC_DOUBLE) + MAGIC_DOUBLE);
+    }
+  }
+
+  /** Encode: fastRound(value * POW10[e] * POW10_NEGATIVE[f]) — single 
expression. */
+  static long encodeDouble(double value, int exponent, int factor) {
+    return fastRoundDouble(value * DOUBLE_POW10[exponent] * 
DOUBLE_POW10_NEGATIVE[factor]);
+  }
+
+  /** Decode: encoded * POW10[f] * POW10_NEGATIVE[e] — single expression. */
+  static double decodeDouble(long encoded, int exponent, int factor) {
+    return encoded * DOUBLE_POW10[factor] * DOUBLE_POW10_NEGATIVE[exponent];
+  }
+
+  /** Number of bits needed to represent maxDelta as an unsigned value. */
+  static int bitWidthForInt(int maxDelta) {
+    if (maxDelta == 0) {
+      return 0;
+    }
+    return Integer.SIZE - Integer.numberOfLeadingZeros(maxDelta);
+  }
+
+  static int bitWidthForLong(long maxDelta) {
+    if (maxDelta == 0) {
+      return 0;
+    }
+    return Long.SIZE - Long.numberOfLeadingZeros(maxDelta);
+  }
+
+  public static class EncodingParams {
+    final int exponent;
+    final int factor;
+    final int numExceptions;
+
+    EncodingParams(int exponent, int factor, int numExceptions) {
+      this.exponent = exponent;
+      this.factor = factor;
+      this.numExceptions = numExceptions;
+    }
+  }
+
+  /**
+   * Try all (exponent, factor) combos and pick the one with the smallest 
estimated compressed size.
+   *
+   * <p>Estimated size (in bits) = {@code length * bitWidth + exceptions * 
(Float.SIZE + Short.SIZE)},
+   * where bitWidth is the number of bits needed to represent the unsigned 
range of non-exception
+   * encoded values after frame-of-reference subtraction. This matches the C++ 
ALP cost model and
+   * produces better compression ratios than minimizing exception count alone.
+   */
+  static EncodingParams findBestFloatParams(float[] values, int offset, int 
length) {
+    int bestExponent = 0;
+    int bestFactor = 0;
+    int bestExceptions = length;
+    long bestEstimatedSize = Long.MAX_VALUE;
+
+    for (int e = 0; e <= FLOAT_MAX_EXPONENT; e++) {
+      for (int f = 0; f <= e; f++) {
+        int exceptions = 0;
+        int minEncoded = Integer.MAX_VALUE;
+        int maxEncoded = Integer.MIN_VALUE;
+        for (int i = 0; i < length; i++) {
+          float value = values[offset + i];
+          if (isFloatException(value, e, f)) {
+            exceptions++;
+          } else {
+            int encoded = encodeFloat(value, e, f);
+            if (encoded < minEncoded) minEncoded = encoded;
+            if (encoded > maxEncoded) maxEncoded = encoded;
+          }
+        }
+        int nonExceptions = length - exceptions;
+        if (nonExceptions == 0) continue;
+        long delta = (nonExceptions < 2) ? 0 :
+            Integer.toUnsignedLong(maxEncoded) - 
Integer.toUnsignedLong(minEncoded);
+        int bitsPerValue = (delta == 0) ? 0 : (64 - 
Long.numberOfLeadingZeros(delta));
+        long estimatedSize = (long) length * bitsPerValue
+            + (long) exceptions * (Float.SIZE + Short.SIZE);
+        if (estimatedSize < bestEstimatedSize
+            || (estimatedSize == bestEstimatedSize
+                && (e > bestExponent || (e == bestExponent && f > 
bestFactor)))) {
+          bestEstimatedSize = estimatedSize;
+          bestExponent = e;
+          bestFactor = f;
+          bestExceptions = exceptions;
+          if (bestExceptions == 0 && bitsPerValue == 0) {
+            return new EncodingParams(bestExponent, bestFactor, 0);
+          }
+        }
+      }
+    }
+    return new EncodingParams(bestExponent, bestFactor, bestExceptions);
+  }
+
+  /** Same as findBestFloatParams but only tries the cached preset combos. */
+  static EncodingParams findBestFloatParamsWithPresets(float[] values, int 
offset, int length, int[][] presets) {
+    int bestExponent = presets[0][0];
+    int bestFactor = presets[0][1];
+    int bestExceptions = length;
+    long bestEstimatedSize = Long.MAX_VALUE;
+
+    for (int[] preset : presets) {
+      int e = preset[0];
+      int f = preset[1];
+      int exceptions = 0;
+      int minEncoded = Integer.MAX_VALUE;
+      int maxEncoded = Integer.MIN_VALUE;
+      for (int i = 0; i < length; i++) {
+        float value = values[offset + i];
+        if (isFloatException(value, e, f)) {
+          exceptions++;
+        } else {
+          int encoded = encodeFloat(value, e, f);
+          if (encoded < minEncoded) minEncoded = encoded;
+          if (encoded > maxEncoded) maxEncoded = encoded;
+        }
+      }
+      int nonExceptions = length - exceptions;
+      if (nonExceptions == 0) continue;
+      long delta = (nonExceptions < 2) ? 0 :

Review Comment:
   Signed range here too, same fix as the unsigned question above: the delta is 
(long) maxEncoded - (long) minEncoded.



##########
parquet-column/src/main/java/org/apache/parquet/column/values/alp/AlpEncoderDecoder.java:
##########
@@ -0,0 +1,359 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.parquet.column.values.alp;
+
+import static org.apache.parquet.column.values.alp.AlpConstants.*;
+
+/**
+ * Core ALP (Adaptive Lossless floating-Point) encoding and decoding logic.
+ *
+ * <p>ALP works by converting floating-point values to integers using decimal 
scaling,
+ * then applying Frame of Reference encoding and bit-packing.
+ * Values that cannot be losslessly converted are stored as exceptions.
+ *
+ * <p>Encoding formula: encoded = fastRound(value * POW10[e] * 
POW10_NEGATIVE[f])
+ * <p>Decoding formula: value = encoded * POW10[f] * POW10_NEGATIVE[e]
+ *
+ * <p>The order of operations is critical for IEEE 754 correctness. Both 
formulas must
+ * be evaluated as single expressions — storing the intermediate 
multiplication result
+ * in a variable before the second multiply changes IEEE 754 rounding and 
produces extra
+ * exceptions. Uses multiply-by-reciprocal (via POW10_NEGATIVE) for C++ wire 
compatibility.
+ *
+ * <p>Exception conditions:
+ * <ul>
+ *   <li>NaN values</li>
+ *   <li>Infinity values</li>
+ *   <li>Negative zero (-0.0)</li>
+ *   <li>Out of integer range</li>
+ *   <li>Round-trip failure (decode(encode(v)) != v)</li>
+ * </ul>
+ */
+final class AlpEncoderDecoder {
+
+  private static final double ENCODING_UPPER_LIMIT = 9223372036854774784.0;
+  private static final double ENCODING_LOWER_LIMIT = -9223372036854774784.0;
+  private static final float FLOAT_ENCODING_UPPER_LIMIT = 2147483520.0f;
+  private static final float FLOAT_ENCODING_LOWER_LIMIT = -2147483520.0f;
+
+  private AlpEncoderDecoder() {
+    // Utility class
+  }
+
+  /** NaN, Inf, and -0.0 can never be encoded regardless of exponent/factor. */
+  static boolean isFloatException(float value) {
+    if (Float.isNaN(value)) {
+      return true;
+    }
+    if (Float.isInfinite(value)) {
+      return true;
+    }
+    return Float.floatToRawIntBits(value) == FLOAT_NEGATIVE_ZERO_BITS;
+  }
+
+  /** Check round-trip: encode then decode, and see if we get the same bits 
back. */
+  static boolean isFloatException(float value, int exponent, int factor) {
+    if (isFloatException(value)) {
+      return true;
+    }
+    // Check before rounding: overflow or non-finite after scaling
+    float scaled = value * FLOAT_POW10[exponent] * 
FLOAT_POW10_NEGATIVE[factor];
+    if (!Float.isFinite(scaled) || scaled > FLOAT_ENCODING_UPPER_LIMIT || 
scaled < FLOAT_ENCODING_LOWER_LIMIT) {
+      return true;
+    }
+    int encoded = encodeFloat(value, exponent, factor);
+    float decoded = decodeFloat(encoded, exponent, factor);
+    return Float.floatToRawIntBits(value) != Float.floatToRawIntBits(decoded);
+  }
+
+  /** Round float to nearest integer using magic-number trick with sign 
branching. */
+  static int fastRoundFloat(float value) {
+    if (value >= 0) {
+      return (int) ((value + MAGIC_FLOAT) - MAGIC_FLOAT);
+    } else {
+      return (int) ((value - MAGIC_FLOAT) + MAGIC_FLOAT);
+    }
+  }
+
+  /** Encode: fastRound(value * POW10[e] * POW10_NEGATIVE[f]) — single 
expression. */
+  static int encodeFloat(float value, int exponent, int factor) {
+    return fastRoundFloat(value * FLOAT_POW10[exponent] * 
FLOAT_POW10_NEGATIVE[factor]);
+  }
+
+  /** Decode: encoded * POW10[f] * POW10_NEGATIVE[e] — single expression. */
+  static float decodeFloat(int encoded, int exponent, int factor) {
+    return encoded * FLOAT_POW10[factor] * FLOAT_POW10_NEGATIVE[exponent];
+  }
+
+  static boolean isDoubleException(double value) {
+    if (Double.isNaN(value)) {
+      return true;
+    }
+    if (Double.isInfinite(value)) {
+      return true;
+    }
+    return Double.doubleToRawLongBits(value) == DOUBLE_NEGATIVE_ZERO_BITS;
+  }
+
+  static boolean isDoubleException(double value, int exponent, int factor) {
+    if (isDoubleException(value)) {
+      return true;
+    }
+    // Check before rounding: overflow or non-finite after scaling
+    double scaled = value * DOUBLE_POW10[exponent] * 
DOUBLE_POW10_NEGATIVE[factor];
+    if (!Double.isFinite(scaled) || scaled > ENCODING_UPPER_LIMIT || scaled < 
ENCODING_LOWER_LIMIT) {
+      return true;
+    }
+    long encoded = encodeDouble(value, exponent, factor);
+    double decoded = decodeDouble(encoded, exponent, factor);
+    return Double.doubleToRawLongBits(value) != 
Double.doubleToRawLongBits(decoded);
+  }
+
+  /** Round double to nearest integer using magic-number trick with sign 
branching. */
+  static long fastRoundDouble(double value) {
+    if (value >= 0) {
+      return (long) ((value + MAGIC_DOUBLE) - MAGIC_DOUBLE);
+    } else {
+      return (long) ((value - MAGIC_DOUBLE) + MAGIC_DOUBLE);
+    }
+  }
+
+  /** Encode: fastRound(value * POW10[e] * POW10_NEGATIVE[f]) — single 
expression. */
+  static long encodeDouble(double value, int exponent, int factor) {
+    return fastRoundDouble(value * DOUBLE_POW10[exponent] * 
DOUBLE_POW10_NEGATIVE[factor]);
+  }
+
+  /** Decode: encoded * POW10[f] * POW10_NEGATIVE[e] — single expression. */
+  static double decodeDouble(long encoded, int exponent, int factor) {
+    return encoded * DOUBLE_POW10[factor] * DOUBLE_POW10_NEGATIVE[exponent];
+  }
+
+  /** Number of bits needed to represent maxDelta as an unsigned value. */
+  static int bitWidthForInt(int maxDelta) {
+    if (maxDelta == 0) {
+      return 0;
+    }
+    return Integer.SIZE - Integer.numberOfLeadingZeros(maxDelta);
+  }
+
+  static int bitWidthForLong(long maxDelta) {
+    if (maxDelta == 0) {
+      return 0;
+    }
+    return Long.SIZE - Long.numberOfLeadingZeros(maxDelta);
+  }
+
+  public static class EncodingParams {
+    final int exponent;
+    final int factor;
+    final int numExceptions;
+
+    EncodingParams(int exponent, int factor, int numExceptions) {
+      this.exponent = exponent;
+      this.factor = factor;
+      this.numExceptions = numExceptions;
+    }
+  }
+
+  /**
+   * Try all (exponent, factor) combos and pick the one with the smallest 
estimated compressed size.
+   *
+   * <p>Estimated size (in bits) = {@code length * bitWidth + exceptions * 
(Float.SIZE + Short.SIZE)},
+   * where bitWidth is the number of bits needed to represent the unsigned 
range of non-exception
+   * encoded values after frame-of-reference subtraction. This matches the C++ 
ALP cost model and
+   * produces better compression ratios than minimizing exception count alone.
+   */
+  static EncodingParams findBestFloatParams(float[] values, int offset, int 
length) {
+    int bestExponent = 0;
+    int bestFactor = 0;
+    int bestExceptions = length;
+    long bestEstimatedSize = Long.MAX_VALUE;
+
+    for (int e = 0; e <= FLOAT_MAX_EXPONENT; e++) {
+      for (int f = 0; f <= e; f++) {
+        int exceptions = 0;
+        int minEncoded = Integer.MAX_VALUE;
+        int maxEncoded = Integer.MIN_VALUE;
+        for (int i = 0; i < length; i++) {
+          float value = values[offset + i];
+          if (isFloatException(value, e, f)) {
+            exceptions++;
+          } else {
+            int encoded = encodeFloat(value, e, f);
+            if (encoded < minEncoded) minEncoded = encoded;
+            if (encoded > maxEncoded) maxEncoded = encoded;
+          }
+        }
+        int nonExceptions = length - exceptions;
+        if (nonExceptions == 0) continue;
+        long delta = (nonExceptions < 2) ? 0 :
+            Integer.toUnsignedLong(maxEncoded) - 
Integer.toUnsignedLong(minEncoded);
+        int bitsPerValue = (delta == 0) ? 0 : (64 - 
Long.numberOfLeadingZeros(delta));
+        long estimatedSize = (long) length * bitsPerValue
+            + (long) exceptions * (Float.SIZE + Short.SIZE);
+        if (estimatedSize < bestEstimatedSize
+            || (estimatedSize == bestEstimatedSize
+                && (e > bestExponent || (e == bestExponent && f > 
bestFactor)))) {
+          bestEstimatedSize = estimatedSize;
+          bestExponent = e;
+          bestFactor = f;
+          bestExceptions = exceptions;
+          if (bestExceptions == 0 && bitsPerValue == 0) {
+            return new EncodingParams(bestExponent, bestFactor, 0);
+          }
+        }
+      }
+    }
+    return new EncodingParams(bestExponent, bestFactor, bestExceptions);
+  }
+
+  /** Same as findBestFloatParams but only tries the cached preset combos. */
+  static EncodingParams findBestFloatParamsWithPresets(float[] values, int 
offset, int length, int[][] presets) {
+    int bestExponent = presets[0][0];
+    int bestFactor = presets[0][1];
+    int bestExceptions = length;
+    long bestEstimatedSize = Long.MAX_VALUE;
+
+    for (int[] preset : presets) {
+      int e = preset[0];
+      int f = preset[1];
+      int exceptions = 0;
+      int minEncoded = Integer.MAX_VALUE;
+      int maxEncoded = Integer.MIN_VALUE;
+      for (int i = 0; i < length; i++) {
+        float value = values[offset + i];
+        if (isFloatException(value, e, f)) {
+          exceptions++;
+        } else {
+          int encoded = encodeFloat(value, e, f);
+          if (encoded < minEncoded) minEncoded = encoded;
+          if (encoded > maxEncoded) maxEncoded = encoded;
+        }
+      }
+      int nonExceptions = length - exceptions;
+      if (nonExceptions == 0) continue;
+      long delta = (nonExceptions < 2) ? 0 :
+          Integer.toUnsignedLong(maxEncoded) - 
Integer.toUnsignedLong(minEncoded);
+      int bitsPerValue = (delta == 0) ? 0 : (64 - 
Long.numberOfLeadingZeros(delta));
+      long estimatedSize = (long) length * bitsPerValue
+          + (long) exceptions * (Float.SIZE + Short.SIZE);
+      if (estimatedSize < bestEstimatedSize
+          || (estimatedSize == bestEstimatedSize
+              && (e > bestExponent || (e == bestExponent && f > bestFactor)))) 
{
+        bestEstimatedSize = estimatedSize;
+        bestExponent = e;
+        bestFactor = f;
+        bestExceptions = exceptions;
+        if (bestExceptions == 0 && bitsPerValue == 0) {
+          return new EncodingParams(bestExponent, bestFactor, 0);
+        }
+      }
+    }
+    return new EncodingParams(bestExponent, bestFactor, bestExceptions);
+  }
+
+  /** Try all (exponent, factor) combos and pick the one with the smallest 
estimated compressed size. */
+  static EncodingParams findBestDoubleParams(double[] values, int offset, int 
length) {
+    int bestExponent = 0;
+    int bestFactor = 0;
+    int bestExceptions = length;
+    long bestEstimatedSize = Long.MAX_VALUE;
+
+    for (int e = 0; e <= DOUBLE_MAX_EXPONENT; e++) {
+      for (int f = 0; f <= e; f++) {
+        int exceptions = 0;
+        long minEncoded = Long.MAX_VALUE;
+        long maxEncoded = Long.MIN_VALUE;
+        for (int i = 0; i < length; i++) {
+          double value = values[offset + i];
+          if (isDoubleException(value, e, f)) {
+            exceptions++;
+          } else {
+            long encoded = encodeDouble(value, e, f);
+            if (encoded < minEncoded) minEncoded = encoded;
+            if (encoded > maxEncoded) maxEncoded = encoded;
+          }
+        }
+        int nonExceptions = length - exceptions;
+        if (nonExceptions == 0) continue;
+        // delta as signed subtraction; Long.numberOfLeadingZeros handles the 
unsigned bit width
+        // correctly even when the subtraction overflows (large range → 
penalized with 64 bits).
+        long delta = (nonExceptions < 2) ? 0 : (maxEncoded - minEncoded);
+        int bitsPerValue = (delta == 0) ? 0 : (64 - 
Long.numberOfLeadingZeros(delta));
+        long estimatedSize = (long) length * bitsPerValue
+            + (long) exceptions * (Double.SIZE + Short.SIZE);
+        if (estimatedSize < bestEstimatedSize
+            || (estimatedSize == bestEstimatedSize
+                && (e > bestExponent || (e == bestExponent && f > 
bestFactor)))) {
+          bestEstimatedSize = estimatedSize;
+          bestExponent = e;
+          bestFactor = f;
+          bestExceptions = exceptions;
+          if (bestExceptions == 0 && bitsPerValue == 0) {
+            return new EncodingParams(bestExponent, bestFactor, 0);
+          }
+        }
+      }
+    }
+    return new EncodingParams(bestExponent, bestFactor, bestExceptions);
+  }

Review Comment:
   Collapsed into one pickBestFloat method. findBestFloatParams and 
findBestFloatParamsWithPresets both delegate to it, just with different pair 
lists (all pairs vs the cached presets).



##########
parquet-column/src/main/java/org/apache/parquet/column/values/alp/AlpEncoderDecoder.java:
##########
@@ -0,0 +1,359 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.parquet.column.values.alp;
+
+import static org.apache.parquet.column.values.alp.AlpConstants.*;
+
+/**
+ * Core ALP (Adaptive Lossless floating-Point) encoding and decoding logic.
+ *
+ * <p>ALP works by converting floating-point values to integers using decimal 
scaling,
+ * then applying Frame of Reference encoding and bit-packing.
+ * Values that cannot be losslessly converted are stored as exceptions.
+ *
+ * <p>Encoding formula: encoded = fastRound(value * POW10[e] * 
POW10_NEGATIVE[f])
+ * <p>Decoding formula: value = encoded * POW10[f] * POW10_NEGATIVE[e]
+ *
+ * <p>The order of operations is critical for IEEE 754 correctness. Both 
formulas must
+ * be evaluated as single expressions — storing the intermediate 
multiplication result
+ * in a variable before the second multiply changes IEEE 754 rounding and 
produces extra
+ * exceptions. Uses multiply-by-reciprocal (via POW10_NEGATIVE) for C++ wire 
compatibility.
+ *
+ * <p>Exception conditions:
+ * <ul>
+ *   <li>NaN values</li>
+ *   <li>Infinity values</li>
+ *   <li>Negative zero (-0.0)</li>
+ *   <li>Out of integer range</li>
+ *   <li>Round-trip failure (decode(encode(v)) != v)</li>
+ * </ul>
+ */
+final class AlpEncoderDecoder {
+
+  private static final double ENCODING_UPPER_LIMIT = 9223372036854774784.0;
+  private static final double ENCODING_LOWER_LIMIT = -9223372036854774784.0;
+  private static final float FLOAT_ENCODING_UPPER_LIMIT = 2147483520.0f;
+  private static final float FLOAT_ENCODING_LOWER_LIMIT = -2147483520.0f;
+
+  private AlpEncoderDecoder() {
+    // Utility class
+  }
+
+  /** NaN, Inf, and -0.0 can never be encoded regardless of exponent/factor. */
+  static boolean isFloatException(float value) {
+    if (Float.isNaN(value)) {
+      return true;
+    }
+    if (Float.isInfinite(value)) {
+      return true;
+    }
+    return Float.floatToRawIntBits(value) == FLOAT_NEGATIVE_ZERO_BITS;
+  }
+
+  /** Check round-trip: encode then decode, and see if we get the same bits 
back. */
+  static boolean isFloatException(float value, int exponent, int factor) {
+    if (isFloatException(value)) {
+      return true;
+    }
+    // Check before rounding: overflow or non-finite after scaling
+    float scaled = value * FLOAT_POW10[exponent] * 
FLOAT_POW10_NEGATIVE[factor];
+    if (!Float.isFinite(scaled) || scaled > FLOAT_ENCODING_UPPER_LIMIT || 
scaled < FLOAT_ENCODING_LOWER_LIMIT) {
+      return true;
+    }
+    int encoded = encodeFloat(value, exponent, factor);
+    float decoded = decodeFloat(encoded, exponent, factor);
+    return Float.floatToRawIntBits(value) != Float.floatToRawIntBits(decoded);
+  }
+
+  /** Round float to nearest integer using magic-number trick with sign 
branching. */
+  static int fastRoundFloat(float value) {
+    if (value >= 0) {
+      return (int) ((value + MAGIC_FLOAT) - MAGIC_FLOAT);
+    } else {
+      return (int) ((value - MAGIC_FLOAT) + MAGIC_FLOAT);
+    }
+  }
+
+  /** Encode: fastRound(value * POW10[e] * POW10_NEGATIVE[f]) — single 
expression. */
+  static int encodeFloat(float value, int exponent, int factor) {
+    return fastRoundFloat(value * FLOAT_POW10[exponent] * 
FLOAT_POW10_NEGATIVE[factor]);
+  }
+
+  /** Decode: encoded * POW10[f] * POW10_NEGATIVE[e] — single expression. */
+  static float decodeFloat(int encoded, int exponent, int factor) {
+    return encoded * FLOAT_POW10[factor] * FLOAT_POW10_NEGATIVE[exponent];
+  }
+
+  static boolean isDoubleException(double value) {
+    if (Double.isNaN(value)) {
+      return true;
+    }
+    if (Double.isInfinite(value)) {
+      return true;
+    }
+    return Double.doubleToRawLongBits(value) == DOUBLE_NEGATIVE_ZERO_BITS;
+  }
+
+  static boolean isDoubleException(double value, int exponent, int factor) {
+    if (isDoubleException(value)) {
+      return true;
+    }
+    // Check before rounding: overflow or non-finite after scaling
+    double scaled = value * DOUBLE_POW10[exponent] * 
DOUBLE_POW10_NEGATIVE[factor];
+    if (!Double.isFinite(scaled) || scaled > ENCODING_UPPER_LIMIT || scaled < 
ENCODING_LOWER_LIMIT) {
+      return true;
+    }
+    long encoded = encodeDouble(value, exponent, factor);
+    double decoded = decodeDouble(encoded, exponent, factor);
+    return Double.doubleToRawLongBits(value) != 
Double.doubleToRawLongBits(decoded);
+  }
+
+  /** Round double to nearest integer using magic-number trick with sign 
branching. */
+  static long fastRoundDouble(double value) {
+    if (value >= 0) {
+      return (long) ((value + MAGIC_DOUBLE) - MAGIC_DOUBLE);
+    } else {
+      return (long) ((value - MAGIC_DOUBLE) + MAGIC_DOUBLE);
+    }
+  }
+
+  /** Encode: fastRound(value * POW10[e] * POW10_NEGATIVE[f]) — single 
expression. */
+  static long encodeDouble(double value, int exponent, int factor) {
+    return fastRoundDouble(value * DOUBLE_POW10[exponent] * 
DOUBLE_POW10_NEGATIVE[factor]);
+  }
+
+  /** Decode: encoded * POW10[f] * POW10_NEGATIVE[e] — single expression. */
+  static double decodeDouble(long encoded, int exponent, int factor) {
+    return encoded * DOUBLE_POW10[factor] * DOUBLE_POW10_NEGATIVE[exponent];
+  }
+
+  /** Number of bits needed to represent maxDelta as an unsigned value. */
+  static int bitWidthForInt(int maxDelta) {

Review Comment:
   The writer uses BytesUtils.getWidthFromMaxInt where the delta fits in an 
int. The estimator computes the FOR span as a long (the signed range can exceed 
int), so it uses a long-width computation there, which is the bitWidthForLong 
helper, documented as the long counterpart since BytesUtils only has the int 
version.



##########
parquet-column/src/main/java/org/apache/parquet/column/values/alp/AlpValuesWriter.java:
##########
@@ -0,0 +1,635 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.parquet.column.values.alp;
+
+import static org.apache.parquet.column.values.alp.AlpConstants.*;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.parquet.bytes.ByteBufferAllocator;
+import org.apache.parquet.bytes.BytesInput;
+import org.apache.parquet.bytes.CapacityByteArrayOutputStream;
+import org.apache.parquet.column.Encoding;
+import org.apache.parquet.column.values.ValuesWriter;
+import org.apache.parquet.column.values.bitpacking.BytePacker;
+import org.apache.parquet.column.values.bitpacking.BytePackerForLong;
+import org.apache.parquet.column.values.bitpacking.Packer;
+
+/**
+ * ALP (Adaptive Lossless floating-Point) values writer.
+ *
+ * <p>ALP encoding converts floating-point values to integers using decimal 
scaling,
+ * then applies Frame of Reference encoding and bit-packing.
+ * Values that cannot be losslessly converted are stored as exceptions.
+ *
+ * <p>Writing is incremental: values are buffered in a fixed-size vector 
buffer,
+ * and each full vector is encoded and flushed to the output stream 
immediately.
+ * On {@link #getBytes()}, any remaining partial vector is flushed, and the
+ * final page bytes are assembled.
+ *
+ * <p>Interleaved Page Layout:
+ * <pre>
+ * ┌─────────┬──────────────────────┬──────────────┬──────────────┬─────┐
+ * │ Header  │ Offset Array         │ Vector 0     │ Vector 1     │ ... │
+ * │ 7 bytes │ 4B &times; numVectors │ (interleaved)│ (interleaved)│     │
+ * └─────────┴──────────────────────┴──────────────┴──────────────┴─────┘
+ * </pre>
+ *
+ * <p>Each vector contains interleaved:
+ * AlpInfo(4B) + ForInfo(5B/9B) + PackedValues + ExceptionPositions + 
ExceptionValues
+ */
+public abstract class AlpValuesWriter extends ValuesWriter {
+
+  protected final int initialCapacity;
+  protected final int pageSize;
+  protected final ByteBufferAllocator allocator;
+  protected final int vectorSize;
+  protected final int logVectorSize;
+
+  AlpValuesWriter(int initialCapacity, int pageSize, ByteBufferAllocator 
allocator, int vectorSize) {
+    AlpConstants.validateVectorSize(vectorSize);
+    this.initialCapacity = initialCapacity;
+    this.pageSize = pageSize;
+    this.allocator = allocator;
+    this.vectorSize = vectorSize;
+    this.logVectorSize = Integer.numberOfTrailingZeros(vectorSize);
+  }
+
+  @Override
+  public Encoding getEncoding() {
+    return Encoding.ALP;
+  }
+
+  /** Float writer. Buffers one vector at a time, encodes and flushes when 
full. */
+  public static class FloatAlpValuesWriter extends AlpValuesWriter {
+    private final float[] vectorBuffer;
+    private int bufferCount;
+    private int totalCount;
+    private CapacityByteArrayOutputStream encodedVectors;
+    private final List<Integer> vectorByteSizes;
+
+    // Preset caching: collect evenly-spaced sample vectors across the 
rowgroup,
+    // then build presets using estimated compressed size (matching C++ 
AlpSampler).
+    private int vectorsProcessed;
+    private int[][] cachedPresets;
+    private final List<float[]> rowgroupSamples;
+    private final int rowgroupSampleJump;
+
+    // Reusable per-vector buffers
+    private final int[] encodedBuffer;
+    private final short[] excPosBuffer;
+    private final float[] excValBuffer;
+    private final byte[] metadataBuf;
+    private final byte[] packBuf;
+    private final int[] packPadBuf;
+
+    public FloatAlpValuesWriter(int initialCapacity, int pageSize, 
ByteBufferAllocator allocator) {
+      this(initialCapacity, pageSize, allocator, DEFAULT_VECTOR_SIZE);
+    }
+
+    public FloatAlpValuesWriter(int initialCapacity, int pageSize, 
ByteBufferAllocator allocator, int vectorSize) {
+      super(initialCapacity, pageSize, allocator, vectorSize);
+      this.vectorBuffer = new float[vectorSize];
+      this.bufferCount = 0;
+      this.totalCount = 0;
+      this.encodedVectors = new CapacityByteArrayOutputStream(initialCapacity, 
pageSize, allocator);
+      this.vectorByteSizes = new ArrayList<>();
+      this.vectorsProcessed = 0;
+      this.cachedPresets = null;
+      this.rowgroupSamples = new ArrayList<>();
+      // Space samples evenly: one sample every jump vectors across the 
rowgroup.
+      // Math.max(1, ...) guards against very small rowgroups or large vector 
sizes.
+      this.rowgroupSampleJump = Math.max(1, SAMPLER_ROWGROUP_SIZE / 
SAMPLER_SAMPLE_VECTORS_PER_ROWGROUP / vectorSize);
+      // Pre-allocate reusable buffers
+      this.encodedBuffer = new int[vectorSize];
+      this.excPosBuffer = new short[vectorSize];
+      this.excValBuffer = new float[vectorSize];
+      this.metadataBuf = new byte[Math.max(ALP_INFO_SIZE, 
FLOAT_FOR_INFO_SIZE)];
+      this.packBuf = new byte[Integer.SIZE]; // max bit width for int
+      this.packPadBuf = new int[8];
+    }
+
+    @Override
+    public void writeFloat(float v) {
+      vectorBuffer[bufferCount++] = v;
+      totalCount++;
+      if (bufferCount == vectorSize) {
+        encodeAndFlushVector(bufferCount);
+        bufferCount = 0;
+      }
+    }
+
+    private void encodeAndFlushVector(int vectorLen) {
+      // Use cached presets after the sampling phase, full search before
+      AlpEncoderDecoder.EncodingParams params;
+      if (cachedPresets != null) {
+        params = 
AlpEncoderDecoder.findBestFloatParamsWithPresets(vectorBuffer, 0, vectorLen, 
cachedPresets);
+      } else {
+        params = AlpEncoderDecoder.findBestFloatParams(vectorBuffer, 0, 
vectorLen);

Review Comment:
   Reordered so the sampling and cache-building path comes first (cachedPresets 
== null) and the cached path is the else, matching the flow you sketched.



##########
parquet-column/src/main/java/org/apache/parquet/column/values/alp/AlpValuesWriter.java:
##########
@@ -0,0 +1,635 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.parquet.column.values.alp;
+
+import static org.apache.parquet.column.values.alp.AlpConstants.*;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.parquet.bytes.ByteBufferAllocator;
+import org.apache.parquet.bytes.BytesInput;
+import org.apache.parquet.bytes.CapacityByteArrayOutputStream;
+import org.apache.parquet.column.Encoding;
+import org.apache.parquet.column.values.ValuesWriter;
+import org.apache.parquet.column.values.bitpacking.BytePacker;
+import org.apache.parquet.column.values.bitpacking.BytePackerForLong;
+import org.apache.parquet.column.values.bitpacking.Packer;
+
+/**
+ * ALP (Adaptive Lossless floating-Point) values writer.
+ *
+ * <p>ALP encoding converts floating-point values to integers using decimal 
scaling,
+ * then applies Frame of Reference encoding and bit-packing.
+ * Values that cannot be losslessly converted are stored as exceptions.
+ *
+ * <p>Writing is incremental: values are buffered in a fixed-size vector 
buffer,
+ * and each full vector is encoded and flushed to the output stream 
immediately.
+ * On {@link #getBytes()}, any remaining partial vector is flushed, and the
+ * final page bytes are assembled.
+ *
+ * <p>Interleaved Page Layout:
+ * <pre>
+ * ┌─────────┬──────────────────────┬──────────────┬──────────────┬─────┐
+ * │ Header  │ Offset Array         │ Vector 0     │ Vector 1     │ ... │
+ * │ 7 bytes │ 4B &times; numVectors │ (interleaved)│ (interleaved)│     │
+ * └─────────┴──────────────────────┴──────────────┴──────────────┴─────┘
+ * </pre>
+ *
+ * <p>Each vector contains interleaved:
+ * AlpInfo(4B) + ForInfo(5B/9B) + PackedValues + ExceptionPositions + 
ExceptionValues
+ */
+public abstract class AlpValuesWriter extends ValuesWriter {
+
+  protected final int initialCapacity;
+  protected final int pageSize;
+  protected final ByteBufferAllocator allocator;
+  protected final int vectorSize;
+  protected final int logVectorSize;
+
+  AlpValuesWriter(int initialCapacity, int pageSize, ByteBufferAllocator 
allocator, int vectorSize) {
+    AlpConstants.validateVectorSize(vectorSize);
+    this.initialCapacity = initialCapacity;
+    this.pageSize = pageSize;
+    this.allocator = allocator;
+    this.vectorSize = vectorSize;
+    this.logVectorSize = Integer.numberOfTrailingZeros(vectorSize);
+  }
+
+  @Override
+  public Encoding getEncoding() {
+    return Encoding.ALP;
+  }
+
+  /** Float writer. Buffers one vector at a time, encodes and flushes when 
full. */
+  public static class FloatAlpValuesWriter extends AlpValuesWriter {
+    private final float[] vectorBuffer;
+    private int bufferCount;
+    private int totalCount;
+    private CapacityByteArrayOutputStream encodedVectors;
+    private final List<Integer> vectorByteSizes;
+
+    // Preset caching: collect evenly-spaced sample vectors across the 
rowgroup,
+    // then build presets using estimated compressed size (matching C++ 
AlpSampler).
+    private int vectorsProcessed;
+    private int[][] cachedPresets;
+    private final List<float[]> rowgroupSamples;
+    private final int rowgroupSampleJump;
+
+    // Reusable per-vector buffers
+    private final int[] encodedBuffer;
+    private final short[] excPosBuffer;
+    private final float[] excValBuffer;
+    private final byte[] metadataBuf;
+    private final byte[] packBuf;
+    private final int[] packPadBuf;
+
+    public FloatAlpValuesWriter(int initialCapacity, int pageSize, 
ByteBufferAllocator allocator) {
+      this(initialCapacity, pageSize, allocator, DEFAULT_VECTOR_SIZE);
+    }
+
+    public FloatAlpValuesWriter(int initialCapacity, int pageSize, 
ByteBufferAllocator allocator, int vectorSize) {
+      super(initialCapacity, pageSize, allocator, vectorSize);
+      this.vectorBuffer = new float[vectorSize];
+      this.bufferCount = 0;
+      this.totalCount = 0;
+      this.encodedVectors = new CapacityByteArrayOutputStream(initialCapacity, 
pageSize, allocator);
+      this.vectorByteSizes = new ArrayList<>();
+      this.vectorsProcessed = 0;
+      this.cachedPresets = null;
+      this.rowgroupSamples = new ArrayList<>();
+      // Space samples evenly: one sample every jump vectors across the 
rowgroup.
+      // Math.max(1, ...) guards against very small rowgroups or large vector 
sizes.
+      this.rowgroupSampleJump = Math.max(1, SAMPLER_ROWGROUP_SIZE / 
SAMPLER_SAMPLE_VECTORS_PER_ROWGROUP / vectorSize);
+      // Pre-allocate reusable buffers
+      this.encodedBuffer = new int[vectorSize];
+      this.excPosBuffer = new short[vectorSize];
+      this.excValBuffer = new float[vectorSize];
+      this.metadataBuf = new byte[Math.max(ALP_INFO_SIZE, 
FLOAT_FOR_INFO_SIZE)];
+      this.packBuf = new byte[Integer.SIZE]; // max bit width for int
+      this.packPadBuf = new int[8];
+    }
+
+    @Override
+    public void writeFloat(float v) {
+      vectorBuffer[bufferCount++] = v;
+      totalCount++;
+      if (bufferCount == vectorSize) {
+        encodeAndFlushVector(bufferCount);
+        bufferCount = 0;
+      }
+    }
+
+    private void encodeAndFlushVector(int vectorLen) {
+      // Use cached presets after the sampling phase, full search before
+      AlpEncoderDecoder.EncodingParams params;
+      if (cachedPresets != null) {
+        params = 
AlpEncoderDecoder.findBestFloatParamsWithPresets(vectorBuffer, 0, vectorLen, 
cachedPresets);
+      } else {
+        params = AlpEncoderDecoder.findBestFloatParams(vectorBuffer, 0, 
vectorLen);
+        // Collect one sample every rowgroupSampleJump vectors so that samples 
are
+        // evenly distributed across the rowgroup (matching C++ AlpSampler 
spacing).
+        if (vectorsProcessed % rowgroupSampleJump == 0
+            && rowgroupSamples.size() < SAMPLER_SAMPLE_VECTORS_PER_ROWGROUP) {
+          rowgroupSamples.add(Arrays.copyOf(vectorBuffer, vectorLen));
+        }
+      }
+
+      vectorsProcessed++;
+      if (cachedPresets == null && rowgroupSamples.size() >= 
SAMPLER_SAMPLE_VECTORS_PER_ROWGROUP) {
+        buildPresetCache();
+      }
+
+      int excIdx = 0;
+
+      // We need a valid encoded value to fill exception slots (placeholder).
+      // Any non-exception value works; it gets overwritten on decode.

Review Comment:
   Rewrote the comment along the lines you suggested: the placeholder is the 
first non-exception encoding (0 when the whole vector is exceptions), which 
avoids introducing a spurious FOR minimum, and the reader overwrites those 
slots from the exception payload so it only affects FOR/bit width.



-- 
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