SignalUtils.java
package com.morphiqlabs.wavelet.util;
/**
* Utility class for common signal processing operations.
*
* <p>This class provides static methods for signal manipulation operations
* that are used across different wavelet transform implementations.</p>
*/
public final class SignalUtils {
private SignalUtils() {
// Prevent instantiation
}
/**
* Applies a circular shift to a signal.
*
* <p><strong>Direction Convention:</strong></p>
* <ul>
* <li><strong>Positive shift (right):</strong> Each element moves to a higher index.
* Elements that would go past the end wrap around to the beginning.
* Result: The last 'shift' elements move to the front.</li>
* <li><strong>Negative shift (left):</strong> Each element moves to a lower index.
* Elements that would go before the beginning wrap around to the end.
* Result: The first 'shift' elements move to the back.</li>
* </ul>
*
* <p><strong>Mathematical Definition:</strong><br>
* For element at original index i, the new index is: (i + shift) % length<br>
* This means: result[new_index] = original[i], where new_index = (i + shift) % length</p>
*
* <p>This operation is commonly used in wavelet transforms to compensate
* for phase shifts introduced by certain wavelet filters, particularly
* biorthogonal wavelets.</p>
*
* <p><strong>Examples:</strong></p>
* <pre>
* Original: [1,2,3,4,5]
*
* shift=2 (right): [4,5,1,2,3]
* - Last 2 elements (4,5) moved to front
* - Element at original index 0 (value 1) → new index 2
* - Element at original index 1 (value 2) → new index 3
* - Element at original index 2 (value 3) → new index 4
* - Element at original index 3 (value 4) → new index 0
* - Element at original index 4 (value 5) → new index 1
*
* shift=-2 (left): [3,4,5,1,2]
* - First 2 elements (1,2) moved to back
* - Element at original index 0 (value 1) → new index 3
* - Element at original index 1 (value 2) → new index 4
* - Element at original index 2 (value 3) → new index 0
* - Element at original index 3 (value 4) → new index 1
* - Element at original index 4 (value 5) → new index 2
*
* shift=0: [1,2,3,4,5] (no change)
* shift=5: [1,2,3,4,5] (full rotation, no change)
* </pre>
*
* @param signal the input signal
* @param shift the number of positions to shift (positive = right, negative = left)
* @return the shifted signal (new array)
* @throws NullPointerException if signal is null
* @throws IllegalArgumentException if signal is empty
*/
public static double[] circularShift(double[] signal, int shift) {
if (signal == null) {
throw new NullPointerException("Signal cannot be null");
}
if (signal.length == 0) {
throw new IllegalArgumentException("Signal cannot be empty");
}
int n = signal.length;
double[] shifted = new double[n];
// Normalize the shift to be in the valid range [0, n)
shift = normalizeShift(shift, n);
// Perform the circular shift
// Each element at index i moves to index (i + shift) % n
for (int i = 0; i < n; i++) {
shifted[(i + shift) % n] = signal[i];
}
return shifted;
}
/**
* Normalizes a shift value to be in the range [0, n).
*
* <p>This method handles both positive and negative shifts, as well as
* shifts larger than the array length. The normalization ensures that:</p>
* <ul>
* <li>Negative shifts are converted to equivalent positive shifts</li>
* <li>Shifts larger than n are reduced to their equivalent within one rotation</li>
* <li>The result is always in the range [0, n)</li>
* </ul>
*
* <p>Mathematical explanation:</p>
* <ol>
* <li>{@code shift % n} gives a value in range (-n, n)</li>
* <li>Adding n ensures the value is positive: range (0, 2n)</li>
* <li>Final {@code % n} brings it back to range [0, n)</li>
* </ol>
*
* <p>Examples:</p>
* <ul>
* <li>shift=2, n=8 → 2 (no change needed)</li>
* <li>shift=-2, n=8 → 6 (left by 2 = right by 6)</li>
* <li>shift=10, n=8 → 2 (10 mod 8)</li>
* <li>shift=-10, n=8 → 6 (-10 mod 8 = -2, then normalized to 6)</li>
* </ul>
*
* @param shift the shift amount (can be any integer)
* @param n the array length (must be positive)
* @return normalized shift in range [0, n)
*/
private static int normalizeShift(int shift, int n) {
// The formula ((shift % n) + n) % n handles all cases:
// - Positive shifts: (shift % n) is already in [0, n), adding n gives [n, 2n),
// final % n brings back to [0, n)
// - Negative shifts: (shift % n) is in (-n, 0], adding n gives (0, n],
// final % n gives [0, n)
// - Large shifts: first % n reduces to (-n, n), then normalized as above
return ((shift % n) + n) % n;
}
}