BatchValidation.java
package com.morphiqlabs.wavelet.util;
import com.morphiqlabs.wavelet.exception.InvalidSignalException;
import static com.morphiqlabs.wavelet.util.WaveletConstants.MIN_DECOMPOSITION_SIZE;
/**
* Batch validation utilities for multi-level wavelet transforms.
*
* <p>This class provides optimized validation for scenarios where multiple
* signals or coefficient arrays need to be validated together, such as in
* multi-level decomposition or reconstruction operations.</p>
*
* <p><strong>Performance Note:</strong> Batch validation reduces overhead by
* performing common checks once and leveraging data locality for cache efficiency.</p>
*/
public final class BatchValidation {
private BatchValidation() {
// Utility class, prevent instantiation
}
/**
* Validates multiple signals for multi-level transform operations.
* This method is optimized for validating a hierarchy of coefficient arrays
* produced by successive wavelet decompositions.
*
* @param signals array of signals to validate
* @param parameterNames names for error reporting (must match signals length)
* @param expectedLengths expected length for each signal (null to skip length check)
* @throws InvalidSignalException if any validation fails
*/
public static void validateMultiLevelSignals(double[][] signals,
String[] parameterNames,
int[] expectedLengths) {
if (signals == null) {
throw new InvalidSignalException("Signals array cannot be null.");
}
if (parameterNames == null || parameterNames.length != signals.length) {
throw new InvalidSignalException("Parameter names must match signals length.");
}
// Batch null/empty checks
for (int i = 0; i < signals.length; i++) {
if (signals[i] == null) {
throw InvalidSignalException.nullSignal(parameterNames[i]);
}
if (signals[i].length == 0) {
throw InvalidSignalException.emptySignal(parameterNames[i]);
}
}
// Batch length validation if specified
if (expectedLengths != null) {
if (expectedLengths.length != signals.length) {
throw new InvalidSignalException("Expected lengths must match signals length.");
}
for (int i = 0; i < signals.length; i++) {
if (signals[i].length != expectedLengths[i]) {
throw new InvalidSignalException(
String.format("%s has incorrect length. Expected: %d, actual: %d.",
parameterNames[i], expectedLengths[i], signals[i].length));
}
}
}
// Batch finite value checks - optimized for cache locality
for (int i = 0; i < signals.length; i++) {
ValidationUtils.validateFiniteValues(signals[i], parameterNames[i]);
}
}
/**
* Validates coefficient pairs for multi-level transforms.
* Optimized for validating approximation and detail coefficient pairs
* at multiple decomposition levels.
*
* @param approxCoeffs array of approximation coefficients at each level
* @param detailCoeffs array of detail coefficients at each level
* @param levelNames names of each level for error reporting
* @throws InvalidSignalException if validation fails
*/
public static void validateMultiLevelCoefficients(double[][] approxCoeffs,
double[][] detailCoeffs,
String[] levelNames) {
if (approxCoeffs == null || detailCoeffs == null) {
throw new InvalidSignalException("Coefficient arrays cannot be null.");
}
if (approxCoeffs.length != detailCoeffs.length) {
throw new InvalidSignalException("Approximation and detail arrays must have same number of levels.");
}
if (levelNames == null || levelNames.length != approxCoeffs.length) {
throw new InvalidSignalException("Level names must match number of levels.");
}
// Validate each level
for (int level = 0; level < approxCoeffs.length; level++) {
String approxName = levelNames[level] + " approximation";
String detailName = levelNames[level] + " detail";
// Null/empty checks
if (approxCoeffs[level] == null) {
throw InvalidSignalException.nullSignal(approxName);
}
if (detailCoeffs[level] == null) {
throw InvalidSignalException.nullSignal(detailName);
}
if (approxCoeffs[level].length == 0) {
throw InvalidSignalException.emptySignal(approxName);
}
if (detailCoeffs[level].length == 0) {
throw InvalidSignalException.emptySignal(detailName);
}
// Length matching
if (approxCoeffs[level].length != detailCoeffs[level].length) {
throw InvalidSignalException.mismatchedCoefficients(
approxCoeffs[level].length, detailCoeffs[level].length);
}
// Finite values - batch check both arrays
ValidationUtils.validateFiniteValues(approxCoeffs[level], approxName);
ValidationUtils.validateFiniteValues(detailCoeffs[level], detailName);
}
}
/**
* Validates transform levels for consistency.
* Ensures that coefficient arrays follow the expected halving pattern
* for multi-level decomposition.
*
* @param levels array where levels[i] contains coefficients at level i
* @param initialLength the length of the original signal
* @throws InvalidSignalException if levels don't follow expected pattern
*/
public static void validateLevelConsistency(double[][] levels, int initialLength) {
if (levels == null || levels.length == 0) {
throw new InvalidSignalException("Levels array cannot be null or empty.");
}
int expectedLength = initialLength / 2;
for (int i = 0; i < levels.length; i++) {
if (levels[i] == null) {
throw InvalidSignalException.nullSignal("Level " + i);
}
if (levels[i].length != expectedLength) {
throw new InvalidSignalException(
String.format("Level %d has incorrect length. Expected: %d, actual: %d.",
i, expectedLength, levels[i].length));
}
expectedLength /= 2;
// Ensure we don't go below minimum decomposition size
if (expectedLength < MIN_DECOMPOSITION_SIZE && i < levels.length - 1) {
throw new InvalidSignalException(
"Too many decomposition levels for signal length " + initialLength + ".");
}
}
}
/**
* Creates expected lengths array for multi-level decomposition.
*
* @param initialLength the original signal length
* @param levels number of decomposition levels
* @return array of expected lengths at each level
*/
public static int[] computeExpectedLengths(int initialLength, int levels) {
int[] lengths = new int[levels];
int currentLength = initialLength;
for (int i = 0; i < levels; i++) {
currentLength /= 2;
lengths[i] = currentLength;
}
return lengths;
}
}