ValidationUtils.java

package com.morphiqlabs.wavelet.util;

import com.morphiqlabs.wavelet.exception.InvalidArgumentException;
import com.morphiqlabs.wavelet.exception.InvalidSignalException;

import static com.morphiqlabs.wavelet.util.WaveletConstants.MAX_SAFE_POWER_OF_TWO;
import static com.morphiqlabs.wavelet.util.WaveletConstants.calculateNextPowerOfTwo;

/**
 * Utility class for validating wavelet transform inputs.
 *
 * <p><strong>Performance Note:</strong> This class is designed with performance as a priority.
 * Some methods have preconditions that must be met by callers to avoid redundant checks.
 * Methods clearly document when they do NOT perform null/empty checks for performance reasons.
 * Callers are responsible for ensuring preconditions are met or calling appropriate validation
 * methods in the correct order.</p>
 */
public final class ValidationUtils {

    /**
     * Maximum signal length for optimized validation path.
     * Signals up to this size use a single-pass validation for better performance.
     * This threshold is chosen based on typical cache sizes and performance characteristics.
     */
    private static final int SMALL_SIGNAL_THRESHOLD = 1024;

    private ValidationUtils() {
        throw new AssertionError("No instances");
    }

    /**
     * Returns the largest power of 2 that can be represented as a positive int in Java.
     * This is useful for validation logic that requires checking against this limit.
     *
     * @return the largest safe power of 2 as a positive int
     */
    public static int getMaxSafePowerOfTwo() {
        return MAX_SAFE_POWER_OF_TWO;
    }

    /**
     * Validates that a signal is suitable for wavelet transform.
     * Checks for null, empty, minimum length, power-of-two length, and finite values.
     *
     * <p>This method performs validation in the following order:
     * <ol>
     *   <li>Null and empty checks via {@link #validateNotNullOrEmpty(double[], String)}</li>
     *   <li>Minimum length check (must be at least 2)</li>
     *   <li>Power-of-two length validation</li>
     *   <li>Finite value checks via {@link #validateFiniteValues(double[], String)}</li>
     * </ol>
     * The ordering ensures that {@code validateFiniteValues}'s precondition (non-null, non-empty array)
     * is satisfied by the prior null/empty validation.
     *
     * @param signal        the signal to validate
     * @param parameterName the name of the parameter for error messages
     * @throws InvalidSignalException if the signal is invalid
     */
    public static void validateSignal(double[] signal, String parameterName) {
        // Optimized path for small signals: combine all checks in single pass
        if (signal != null && isPowerOfTwo(signal.length) && signal.length >= 2 && signal.length <= SMALL_SIGNAL_THRESHOLD) {
            // Fast path: single pass validation for small power-of-2 signals
            for (int i = 0; i < signal.length; i++) {
                double value = signal[i];
                if (Double.isNaN(value)) {
                    throw InvalidSignalException.nanValue(parameterName, i);
                }
                if (Double.isInfinite(value)) {
                    throw InvalidSignalException.infinityValue(parameterName, i, value);
                }
            }
            return;
        }

        // Standard validation path for large signals or when fast path conditions not met
        // Check null and empty using common method
        validateNotNullOrEmpty(signal, parameterName);

        // Check minimum length (wavelet transform requires at least 2 samples)
        if (signal.length < 2) {
            throw new InvalidSignalException("Signal must have at least 2 samples for wavelet transform, but has " + signal.length);
        }

        // Check power of two
        if (!isPowerOfTwo(signal.length)) {
            throw InvalidSignalException.notPowerOfTwo(signal.length);
        }

        // Check for NaN and infinity (precondition satisfied by validateNotNullOrEmpty above)
        validateFiniteValues(signal, parameterName);
    }

    /**
     * Validates that all values in an array are finite (not NaN or infinity).
     *
     * <strong>Precondition:</strong> The caller MUST ensure the array is not null or empty.
     * This method does NOT check for null/empty arrays for performance reasons.
     * Call validateNotNullOrEmpty() before this method if the array hasn't been validated.
     *
     * @param values        the array to validate (must not be null or empty)
     * @param parameterName the name of the parameter for error messages
     * @throws InvalidSignalException         if any value is NaN or infinity
     * @throws NullPointerException           if values is null (unchecked)
     * @throws ArrayIndexOutOfBoundsException if values is empty (unchecked)
     */
    public static void validateFiniteValues(double[] values, String parameterName) {
        int index = 0;
        for (double value : values) {
            if (Double.isNaN(value)) {
                throw InvalidSignalException.nanValue(parameterName, index);
            }
            if (Double.isInfinite(value)) {
                throw InvalidSignalException.infinityValue(parameterName, index, value);
            }
            index++;
        }
    }

    /**
     * Validates that an array is not null and not empty.
     *
     * @param array         the array to validate
     * @param parameterName the name of the parameter for error messages
     * @throws InvalidSignalException if the array is null or empty
     */
    public static void validateNotNullOrEmpty(double[] array, String parameterName) {
        if (array == null) {
            throw InvalidSignalException.nullSignal(parameterName);
        }
        if (array.length == 0) {
            throw InvalidSignalException.emptySignal(parameterName);
        }
    }

    /**
     * Validates that two arrays have matching lengths.
     *
     * <strong>Precondition:</strong> The caller MUST ensure both arrays are not null.
     * This method does NOT check for null arrays for performance reasons.
     * Call validateNotNullOrEmpty() on both arrays before this method if they haven't been validated.
     *
     * @param approxCoeffs approximation coefficients (must not be null)
     * @param detailCoeffs detail coefficients (must not be null)
     * @throws InvalidSignalException if the arrays have different lengths
     * @throws NullPointerException   if either array is null (unchecked)
     */
    public static void validateMatchingLengths(double[] approxCoeffs, double[] detailCoeffs) {
        if (approxCoeffs.length != detailCoeffs.length) {
            throw InvalidSignalException.mismatchedCoefficients(approxCoeffs.length, detailCoeffs.length);
        }
    }

    /**
     * Checks if a number is a power of two.
     *
     * <p>This method uses a bit manipulation algorithm to determine if the input
     * number {@code n} is a power of two. The algorithm works as follows:
     * <ul>
     *   <li>{@code n > 0} ensures that the number is positive, as negative numbers
     *       and zero cannot be powers of two.</li>
     *   <li>{@code (n & (n - 1)) == 0} checks if {@code n} has exactly one bit set
     *       in its binary representation. For powers of two, this condition is true
     *       because subtracting 1 from a power of two flips all the bits after the
     *       single set bit, resulting in no overlap when ANDed with the original number.</li>
     * </ul>
     * For example:
     * <pre>
     * n = 4 (binary 100), n - 1 = 3 (binary 011), n {@literal &} (n - 1) = 0
     * n = 5 (binary 101), n - 1 = 4 (binary 100), n {@literal &} (n - 1) = 4 (not 0)
     * </pre>
     *
     * @param n the number to check
     * @return true if {@code n} is a power of two, false otherwise
     */
    public static boolean isPowerOfTwo(int n) {
        return n > 0 && (n & (n - 1)) == 0;
    }

    /**
     * Checks if the next power of two calculation would overflow for the given input.
     *
     * @param n the input number
     * @return true if nextPowerOfTwo(n) would overflow, false otherwise
     */
    public static boolean wouldNextPowerOfTwoOverflow(int n) {
        return n > MAX_SAFE_POWER_OF_TWO;
    }

    /**
     * Calculates the next power of two greater than or equal to n.
     *
     * @param n the input number (must be positive)
     * @return the next power of two >= n
     * @throws InvalidArgumentException if n {@literal <} 1 or n {@literal >} 2^30 (1,073,741,824)
     */
    public static int nextPowerOfTwo(int n) {
        if (n < 1) {
            throw InvalidArgumentException.notPositive(n);
        }

        try {
            return calculateNextPowerOfTwo(n);
        } catch (IllegalArgumentException e) {
            // Convert to our exception type
            throw InvalidArgumentException.tooLarge(n, MAX_SAFE_POWER_OF_TWO,
                    "Next power of two would be 2^31, which cannot be represented as a positive int.");
        }
    }

    /**
     * Validates that a block size is a power of two, as required for wavelet transforms.
     * This method provides a centralized validation with consistent error messaging
     * for streaming wavelet components.
     *
     * @param blockSize     the block size to validate
     * @param componentName the name of the component for error messages (e.g., "StreamingDenoiser")
     * @throws InvalidArgumentException if the block size is not a power of two
     */
    public static void validateBlockSizeForWavelet(int blockSize, String componentName) {
        if (!isPowerOfTwo(blockSize)) {
            throw new InvalidArgumentException(
                    String.format("%s requires block size to be a power of 2, got: %d",
                            componentName, blockSize));
        }
    }
}