StreamingDenoiserConfig.java

package com.morphiqlabs.wavelet.streaming;

import com.morphiqlabs.wavelet.api.BoundaryMode;
import com.morphiqlabs.wavelet.api.Wavelet;
import com.morphiqlabs.wavelet.denoising.WaveletDenoiser.ThresholdMethod;
import com.morphiqlabs.wavelet.denoising.WaveletDenoiser.ThresholdType;
import com.morphiqlabs.wavelet.exception.InvalidArgumentException;
import com.morphiqlabs.wavelet.exception.ErrorContext;

/**
 * Configuration for streaming wavelet denoisers.
 * 
 * <p>This class provides a fluent builder API for configuring streaming denoisers
 * with support for both MODWT-based implementations.</p>
 */
public final class StreamingDenoiserConfig {
    
    private final Wavelet wavelet;
    private final int blockSize;
    private final double overlapFactor;
    private final BoundaryMode boundaryMode;
    private final ThresholdMethod thresholdMethod;
    private final ThresholdType thresholdType;
    private final boolean adaptiveThreshold;
    private final double thresholdMultiplier;
    
    /**
     * Size of the window used for noise estimation.
     * 
     * <p>The noise window can be larger than the block size, which allows
     * for more stable noise estimates across multiple blocks. Different
     * implementations handle this differently:</p>
     * <ul>
     *   <li>MODWTStreamingDenoiser uses stratified sampling for large windows</li>
     *   <li>FastStreamingDenoiser may clip the window size as needed</li>
     * </ul>
     */
    private final int noiseWindowSize;
    
    private StreamingDenoiserConfig(Builder builder) {
        this.wavelet = builder.wavelet;
        this.blockSize = builder.blockSize;
        this.overlapFactor = builder.overlapFactor;
        this.boundaryMode = builder.boundaryMode;
        this.thresholdMethod = builder.thresholdMethod;
        this.thresholdType = builder.thresholdType;
        this.adaptiveThreshold = builder.adaptiveThreshold;
        this.thresholdMultiplier = builder.thresholdMultiplier;
        this.noiseWindowSize = builder.noiseWindowSize;
    }
    
    // Getters
    public Wavelet getWavelet() { return wavelet; }
    public int getBlockSize() { return blockSize; }
    public double getOverlapFactor() { return overlapFactor; }
    public BoundaryMode getBoundaryMode() { return boundaryMode; }
    public ThresholdMethod getThresholdMethod() { return thresholdMethod; }
    public ThresholdType getThresholdType() { return thresholdType; }
    public boolean isAdaptiveThreshold() { return adaptiveThreshold; }
    public double getThresholdMultiplier() { return thresholdMultiplier; }
    
    /**
     * Gets the size of the window used for noise estimation.
     * 
     * <p>The noise window can be larger than the block size, which allows
     * for more stable noise estimates across multiple blocks. Different
     * implementations handle this differently:</p>
     * <ul>
     *   <li>MODWTStreamingDenoiser uses stratified sampling for large windows</li>
     *   <li>FastStreamingDenoiser may clip the window size as needed</li>
     * </ul>
     * 
     * <p>Larger windows generally provide more stable noise estimates but may
     * increase latency. The optimal size depends on the signal characteristics
     * and application requirements.</p>
     * 
     * @return the noise estimation window size in samples
     */
    public int getNoiseWindowSize() { return noiseWindowSize; }
    
    /**
     * Creates a default configuration suitable for real-time audio processing.
     */
    public static StreamingDenoiserConfig defaultAudioConfig() {
        return new Builder()
            .blockSize(512)
            .overlapFactor(0.5)
            .thresholdMethod(ThresholdMethod.UNIVERSAL)
            .adaptiveThreshold(true)
            .build();
    }
    
    /**
     * Creates a default configuration suitable for financial data processing.
     */
    public static StreamingDenoiserConfig defaultFinancialConfig() {
        return new Builder()
            .blockSize(256)
            .overlapFactor(0.25)
            .thresholdMethod(ThresholdMethod.SURE)
            .thresholdMultiplier(0.8)
            .build();
    }
    
    /**
     * Builder for StreamingDenoiserConfig.
     */
    public static class Builder {
        private Wavelet wavelet = com.morphiqlabs.wavelet.api.Daubechies.DB4;
        private int blockSize = 256;
        private double overlapFactor = 0.5;
        private BoundaryMode boundaryMode = BoundaryMode.PERIODIC;
        private ThresholdMethod thresholdMethod = ThresholdMethod.UNIVERSAL;
        private ThresholdType thresholdType = ThresholdType.SOFT;
        private boolean adaptiveThreshold = false;
        private double thresholdMultiplier = 1.0;
        private int noiseWindowSize = 128;
        
        public Builder wavelet(Wavelet wavelet) {
            if (wavelet == null) {
                throw new InvalidArgumentException(
                    ErrorContext.builder("Wavelet cannot be null")
                        .withContext("field", "wavelet")
                        .withContext("value", "null")
                        .withContext("constraint", "non-null")
                        .withSuggestion("Use a valid wavelet like Daubechies.DB4")
                        .build()
                );
            }
            this.wavelet = wavelet;
            return this;
        }
        
        public Builder blockSize(int blockSize) {
            if (blockSize <= 0) {
                throw new InvalidArgumentException(
                    ErrorContext.builder("Block size must be positive")
                        .withContext("field", "blockSize")
                        .withContext("value", blockSize)
                        .withContext("constraint", "positive")
                        .withSuggestion("Use a block size like 256 or 512")
                        .build()
                );
            }
            this.blockSize = blockSize;
            return this;
        }
        
        public Builder overlapFactor(double overlapFactor) {
            if (overlapFactor < 0 || overlapFactor >= 1) {
                throw new InvalidArgumentException(
                    ErrorContext.builder("Overlap factor must be between 0 and 1")
                        .withContext("field", "overlapFactor")
                        .withContext("value", overlapFactor)
                        .withContext("constraint", "[0, 1)")
                        .withSuggestion("Use 0.5 for 50% overlap")
                        .build()
                );
            }
            this.overlapFactor = overlapFactor;
            return this;
        }
        
        public Builder boundaryMode(BoundaryMode boundaryMode) {
            if (boundaryMode == null) {
                throw new InvalidArgumentException(
                    ErrorContext.builder("Boundary mode cannot be null")
                        .withContext("field", "boundaryMode")
                        .withContext("value", "null")
                        .withContext("constraint", "non-null")
                        .withSuggestion("Use BoundaryMode.PERIODIC for streaming")
                        .build()
                );
            }
            this.boundaryMode = boundaryMode;
            return this;
        }
        
        public Builder thresholdMethod(ThresholdMethod method) {
            if (method == null) {
                throw new InvalidArgumentException(
                    ErrorContext.builder("Threshold method cannot be null")
                        .withContext("field", "thresholdMethod")
                        .withContext("value", "null")
                        .withContext("constraint", "non-null")
                        .withSuggestion("Use ThresholdMethod.UNIVERSAL or SURE")
                        .build()
                );
            }
            this.thresholdMethod = method;
            return this;
        }
        
        public Builder thresholdType(ThresholdType type) {
            if (type == null) {
                throw new InvalidArgumentException(
                    ErrorContext.builder("Threshold type cannot be null")
                        .withContext("field", "thresholdType")
                        .withContext("value", "null")
                        .withContext("constraint", "non-null")
                        .withSuggestion("Use ThresholdType.SOFT for smoother results")
                        .build()
                );
            }
            this.thresholdType = type;
            return this;
        }
        
        public Builder adaptiveThreshold(boolean adaptive) {
            this.adaptiveThreshold = adaptive;
            return this;
        }
        
        public Builder thresholdMultiplier(double multiplier) {
            if (multiplier <= 0) {
                throw new InvalidArgumentException(
                    ErrorContext.builder("Threshold multiplier must be positive")
                        .withContext("field", "thresholdMultiplier")
                        .withContext("value", multiplier)
                        .withContext("constraint", "positive")
                        .withSuggestion("Use 1.0 for standard threshold, 0.8 for less aggressive")
                        .build()
                );
            }
            this.thresholdMultiplier = multiplier;
            return this;
        }
        
        /**
         * Sets the size of the window used for noise estimation.
         * 
         * <p>The noise window can be larger than the block size, which allows
         * for more stable noise estimates across multiple blocks. Different
         * implementations handle this differently:</p>
         * <ul>
         *   <li>MODWTStreamingDenoiser uses stratified sampling for large windows</li>
         *   <li>FastStreamingDenoiser clips the window size as needed for performance</li>
         * </ul>
         * 
         * <p><strong>Implementation Notes:</strong></p>
         * <ul>
         *   <li>No upper bound validation is enforced (unlike previous versions)</li>
         *   <li>Algorithms benefit from larger windows that span multiple blocks for stability</li>
         *   <li>MODWTStreamingDenoiser uses stratified sampling when window exceeds data size</li>
         *   <li>FastStreamingDenoiser automatically clips to maintain low latency</li>
         *   <li>Each implementation handles oversized windows gracefully based on their requirements</li>
         * </ul>
         * 
         * @param windowSize the noise estimation window size (must be positive)
         * @return this builder
         */
        public Builder noiseWindowSize(int windowSize) {
            if (windowSize <= 0) {
                throw new InvalidArgumentException(
                    ErrorContext.builder("Noise window size must be positive")
                        .withContext("field", "noiseWindowSize")
                        .withContext("value", windowSize)
                        .withContext("constraint", "positive")
                        .withSuggestion("Use at least 64 samples for stable noise estimation")
                        .build()
                );
            }
            this.noiseWindowSize = windowSize;
            return this;
        }
        
        public StreamingDenoiserConfig build() {
            // Validate configuration
            if (overlapFactor > 0 && blockSize < 64) {
                throw new InvalidArgumentException(
                    ErrorContext.builder("Block size too small for overlap")
                        .withContext("field", "blockSize")
                        .withContext("value", blockSize)
                        .withContext("constraint", "≥ 64 when using overlap")
                        .withContext("overlapFactor", overlapFactor)
                        .withSuggestion("Increase block size or disable overlap")
                        .build()
                );
            }
            
            // Note: No upper bound validation on noiseWindowSize (see noiseWindowSize() JavaDoc for details)
            
            return new StreamingDenoiserConfig(this);
        }
    }
    
    @Override
    public String toString() {
        return String.format(
            "StreamingDenoiserConfig[wavelet=%s, blockSize=%d, overlap=%.1f%%, " +
            "threshold=%s/%s, adaptive=%s, multiplier=%.2f]",
            wavelet.name(), blockSize, overlapFactor * 100,
            thresholdMethod, thresholdType, adaptiveThreshold, thresholdMultiplier
        );
    }
}