CompositePaddingStrategy.java

package com.morphiqlabs.wavelet.padding;

import com.morphiqlabs.wavelet.exception.InvalidArgumentException;

/**
 * Composite padding strategy that applies different strategies to left and right sides.
 *
 * <p>This strategy enables asymmetric padding by using different methods for
 * extending the signal on each side. This is particularly useful when signal
 * characteristics differ at the boundaries. Ideal for:</p>
 * <ul>
 *   <li>Signals with different behavior at start and end</li>
 *   <li>Time series with initialization vs. termination phases</li>
 *   <li>Combining conservative and aggressive padding approaches</li>
 *   <li>Custom padding requirements for specific applications</li>
 * </ul>
 *
 * <p>Example use cases:</p>
 * <ul>
 *   <li>Financial data: constant padding at start (pre-market), trend at end (projection)</li>
 *   <li>Sensor data: zero padding at start (sensor warmup), statistical at end</li>
 *   <li>Audio: symmetric at start (smooth onset), zero at end (decay to silence)</li>
 * </ul>
 *
 * @param leftStrategy strategy applied to the left side
 * @param rightStrategy strategy applied to the right side
 * @param leftRatio proportion of padding applied to the left [0,1]
 */
public record CompositePaddingStrategy(
        PaddingStrategy leftStrategy,
        PaddingStrategy rightStrategy,
        double leftRatio
    ) implements PaddingStrategy {
    
    /**
     * Creates a composite strategy with equal padding on both sides.
     * 
     * @param leftStrategy strategy for left padding
     * @param rightStrategy strategy for right padding
     */
    public CompositePaddingStrategy(PaddingStrategy leftStrategy, PaddingStrategy rightStrategy) {
        this(leftStrategy, rightStrategy, 0.5);
    }
    
    /**
     * Validates parameters.
     */
    public CompositePaddingStrategy {
        if (leftStrategy == null) {
            throw new InvalidArgumentException("Left strategy cannot be null");
        }
        if (rightStrategy == null) {
            throw new InvalidArgumentException("Right strategy cannot be null");
        }
        if (leftRatio < 0 || leftRatio > 1) {
            throw new InvalidArgumentException("Left ratio must be between 0 and 1, got " + leftRatio);
        }
    }
    
    @Override
    public double[] pad(double[] signal, int targetLength) {
        if (signal == null) {
            throw new InvalidArgumentException("Signal cannot be null");
        }
        if (signal.length == 0) {
            throw new InvalidArgumentException("Signal cannot be empty");
        }
        if (targetLength < signal.length) {
            throw new InvalidArgumentException(
                    "Target length " + targetLength + " must be >= signal length " + signal.length);
        }
        
        if (targetLength == signal.length) {
            return signal.clone();
        }
        
        int totalPadding = targetLength - signal.length;
        int leftPadding = (int) Math.round(totalPadding * leftRatio);
        int rightPadding = totalPadding - leftPadding;
        
        double[] result = new double[targetLength];
        
        // Optimize memory by creating minimal temporary arrays
        if (leftPadding > 0) {
            // Create a small signal subset for left padding computation
            int contextSize = Math.min(signal.length, 10); // Use at most 10 points for context
            double[] leftContext = new double[contextSize];
            System.arraycopy(signal, 0, leftContext, 0, contextSize);
            
            // Apply strategy to minimal context
            double[] leftPadded = leftStrategy.pad(leftContext, contextSize + leftPadding);
            
            // Extract the padding portion
            if (isRightAlignedStrategy(leftStrategy)) {
                // Padding is at the end of the result
                for (int i = 0; i < leftPadding; i++) {
                    result[leftPadding - 1 - i] = leftPadded[contextSize + i];
                }
            } else {
                // Padding is at the beginning
                System.arraycopy(leftPadded, 0, result, 0, leftPadding);
            }
        }
        
        // Copy original signal
        System.arraycopy(signal, 0, result, leftPadding, signal.length);
        
        if (rightPadding > 0) {
            // Create a small signal subset for right padding computation
            int contextSize = Math.min(signal.length, 10); // Use at most 10 points for context
            int startIdx = Math.max(0, signal.length - contextSize);
            double[] rightContext = new double[contextSize];
            System.arraycopy(signal, startIdx, rightContext, 0, contextSize);
            
            // Apply strategy to minimal context
            double[] rightPadded = rightStrategy.pad(rightContext, contextSize + rightPadding);
            
            // Extract the padding portion
            if (isLeftAlignedStrategy(rightStrategy)) {
                // Padding is at the beginning
                System.arraycopy(rightPadded, 0, 
                               result, leftPadding + signal.length, rightPadding);
            } else {
                // Padding is at the end - but check array bounds
                int srcPos = Math.min(contextSize, rightPadded.length - rightPadding);
                System.arraycopy(rightPadded, srcPos, 
                               result, leftPadding + signal.length, rightPadding);
            }
        }
        
        return result;
    }
    
    /**
     * Check if strategy typically pads on the right.
     * Uses pattern matching for cleaner code.
     */
    private boolean isRightAlignedStrategy(PaddingStrategy strategy) {
        return switch (strategy) {
            case ConstantPaddingStrategy(var mode) -> 
                mode != ConstantPaddingStrategy.PaddingMode.LEFT;
            case LinearExtrapolationStrategy(var order, var mode) -> 
                mode != LinearExtrapolationStrategy.PaddingMode.LEFT;
            case PolynomialExtrapolationStrategy(var degree, var points, var mode) -> 
                mode != PolynomialExtrapolationStrategy.PaddingMode.LEFT;
            case StatisticalPaddingStrategy(var method, var windowSize, var mode) -> 
                mode != StatisticalPaddingStrategy.PaddingMode.LEFT;
            default -> true; // Most strategies pad on the right by default
        };
    }
    
    /**
     * Check if strategy typically pads on the left.
     * Uses pattern matching for cleaner code.
     */
    private boolean isLeftAlignedStrategy(PaddingStrategy strategy) {
        return switch (strategy) {
            case ConstantPaddingStrategy(var mode) -> 
                mode == ConstantPaddingStrategy.PaddingMode.LEFT;
            case LinearExtrapolationStrategy(var order, var mode) -> 
                mode == LinearExtrapolationStrategy.PaddingMode.LEFT;
            case PolynomialExtrapolationStrategy(var degree, var points, var mode) -> 
                mode == PolynomialExtrapolationStrategy.PaddingMode.LEFT;
            case StatisticalPaddingStrategy(var method, var windowSize, var mode) -> 
                mode == StatisticalPaddingStrategy.PaddingMode.LEFT;
            default -> false; // Most strategies don't pad on the left
        };
    }
    
    @Override
    public double[] trim(double[] result, int originalLength) {
        if (result.length == originalLength) {
            return result;
        }
        if (originalLength > result.length) {
            throw new InvalidArgumentException(
                    "Original length " + originalLength + " exceeds result length " + result.length);
        }
        
        // Trim based on the same ratio used for padding
        int totalPadding = result.length - originalLength;
        int leftTrim = (int) Math.round(totalPadding * leftRatio);
        
        double[] trimmed = new double[originalLength];
        System.arraycopy(result, leftTrim, trimmed, 0, originalLength);
        
        return trimmed;
    }
    
    @Override
    public String name() {
        return String.format("composite-%s-%s", 
                leftStrategy.name(), rightStrategy.name());
    }
    
    @Override
    public String description() {
        return String.format("Composite padding (left: %s [%.0f%%], right: %s [%.0f%%])",
                leftStrategy.name(), leftRatio * 100,
                rightStrategy.name(), (1 - leftRatio) * 100);
    }
    
    /**
     * Builder for creating composite padding strategies.
     */
    public static class Builder {
        private PaddingStrategy leftStrategy;
        private PaddingStrategy rightStrategy;
        private double leftRatio = 0.5;
        
        /**
         * Set the strategy for left padding.
         * 
         * @param strategy the left padding strategy
         * @return this builder
         */
        public Builder leftStrategy(PaddingStrategy strategy) {
            this.leftStrategy = strategy;
            return this;
        }
        
        /**
         * Set the strategy for right padding.
         * 
         * @param strategy the right padding strategy
         * @return this builder
         */
        public Builder rightStrategy(PaddingStrategy strategy) {
            this.rightStrategy = strategy;
            return this;
        }
        
        /**
         * Set the ratio of padding to apply on the left (0 to 1).
         * 
         * @param ratio fraction of total padding for left side
         * @return this builder
         */
        public Builder leftRatio(double ratio) {
            this.leftRatio = ratio;
            return this;
        }
        
        /**
         * Build the composite padding strategy.
         * 
         * @return the configured composite strategy
         */
        public CompositePaddingStrategy build() {
            if (leftStrategy == null) {
                throw new InvalidArgumentException("Left strategy must be specified");
            }
            if (rightStrategy == null) {
                throw new InvalidArgumentException("Right strategy must be specified");
            }
            return new CompositePaddingStrategy(leftStrategy, rightStrategy, leftRatio);
        }
    }
}