Adding New Wavelets to VectorWave
This guide explains how to add new wavelet types to the VectorWave library.
Understanding the Wavelet Hierarchy
VectorWave uses a sealed interface hierarchy to ensure type safety:
Wavelet (base interface)
├── DiscreteWavelet
│ ├── OrthogonalWavelet
│ └── BiorthogonalWavelet
└── ContinuousWavelet
└── ComplexContinuousWavelet
ServiceLoader Architecture
VectorWave uses Java's ServiceLoader mechanism for wavelet discovery, providing several benefits:
- No Circular Dependencies: Wavelets are loaded dynamically, avoiding static initialization issues
- Plugin Support: Add wavelets without modifying the core library
- Clean Separation: Each wavelet family can have its own provider
- Runtime Flexibility: Load wavelets conditionally based on configuration
Steps to Add a New Wavelet
1. Choose the Appropriate Interface
- OrthogonalWavelet: For wavelets where reconstruction filters are the same as decomposition filters (e.g., Haar, Daubechies, Symlets, Coiflets)
- BiorthogonalWavelet: For wavelets with different decomposition and reconstruction filters (e.g., Biorthogonal Splines)
- ContinuousWavelet: For wavelets defined by mathematical functions (e.g., Morlet, Mexican Hat)
2. Implement the Wavelet
Example: Adding an Orthogonal Wavelet
package com.morphiqlabs.wavelet.api;
public final class Meyer implements OrthogonalWavelet {
private static final double[] LOW_PASS_COEFFS = {
// Add Meyer wavelet coefficients here
0.0, 0.0, // ... actual coefficients
};
@Override
public String name() {
return "meyer";
}
@Override
public String description() {
return "Meyer wavelet";
}
@Override
public double[] lowPassDecomposition() {
return LOW_PASS_COEFFS.clone();
}
@Override
public double[] highPassDecomposition() {
// Generate using quadrature mirror filter
double[] h = LOW_PASS_COEFFS;
double[] g = new double[h.length];
for (int i = 0; i < h.length; i++) {
g[i] = (i % 2 == 0 ? 1 : -1) * h[h.length - 1 - i];
}
return g;
}
@Override
public int vanishingMoments() {
return 4; // Meyer specific value
}
}
Example: Adding a Biorthogonal Wavelet
package com.morphiqlabs.wavelet.api;
public final class BiorthogonalSpline22 implements BiorthogonalWavelet {
private static final double[] DECOMP_LOW_PASS = {
// Decomposition low-pass filter coefficients
};
private static final double[] RECON_LOW_PASS = {
// Reconstruction low-pass filter coefficients
};
@Override
public String name() {
return "bior2.2";
}
@Override
public double[] lowPassDecomposition() {
return DECOMP_LOW_PASS.clone();
}
@Override
public double[] highPassDecomposition() {
// Generate from reconstruction low-pass
return generateHighPass(RECON_LOW_PASS);
}
@Override
public double[] lowPassReconstruction() {
return RECON_LOW_PASS.clone();
}
@Override
public double[] highPassReconstruction() {
// Generate from decomposition low-pass
return generateHighPass(DECOMP_LOW_PASS);
}
@Override
public int vanishingMoments() {
return 2;
}
@Override
public int dualVanishingMoments() {
return 2;
}
@Override
public boolean isSymmetric() {
return true;
}
private double[] generateHighPass(double[] lowPass) {
double[] g = new double[lowPass.length];
for (int i = 0; i < lowPass.length; i++) {
g[i] = (i % 2 == 0 ? 1 : -1) * lowPass[lowPass.length - 1 - i];
}
return g;
}
}
Example: Adding a Continuous Wavelet
package com.morphiqlabs.wavelet.api;
public final class MexicanHatWavelet implements ContinuousWavelet {
private final double sigma;
public MexicanHatWavelet(double sigma) {
this.sigma = sigma;
}
@Override
public String name() {
return "mexh";
}
@Override
public double psi(double t) {
double t2 = t * t;
double sigma2 = sigma * sigma;
double norm = 2.0 / (Math.sqrt(3 * sigma) * Math.pow(Math.PI, 0.25));
return norm * (1.0 - t2 / sigma2) * Math.exp(-t2 / (2 * sigma2));
}
@Override
public double centerFrequency() {
return 0.25; // Mexican Hat center frequency
}
@Override
public double bandwidth() {
return sigma;
}
@Override
public boolean isComplex() {
return false;
}
@Override
public double[] discretize(int numCoeffs) {
// Discretize the continuous wavelet
double[] coeffs = new double[numCoeffs];
double t0 = -5.0 * sigma;
double dt = 10.0 * sigma / (numCoeffs - 1);
for (int i = 0; i < numCoeffs; i++) {
coeffs[i] = psi(t0 + i * dt);
}
// Normalize
double sum = 0;
for (double c : coeffs) {
sum += c * c;
}
double norm = Math.sqrt(sum);
for (int i = 0; i < numCoeffs; i++) {
coeffs[i] /= norm;
}
return coeffs;
}
}
3. Register the Wavelet with ServiceLoader
Starting from version 1.0, VectorWave uses ServiceLoader for automatic wavelet discovery. This eliminates circular dependencies and enables a plugin architecture.
Option A: Add to an Existing Provider (Core Library Contributors)
If you're contributing to the core library, add your wavelet to the appropriate existing provider:
// In OrthogonalWaveletProvider.java
@Override
public List<Wavelet> getWavelets() {
return List.of(
new Haar(),
// ... existing wavelets ...
new Meyer() // Add your new wavelet here
);
}
Option B: Create a New Provider (Recommended for Plugins)
For third-party wavelets or plugins, create your own provider:
- Create a Provider Class:
package com.example.wavelets;
import com.morphiqlabs.wavelet.api.Wavelet;
import com.morphiqlabs.wavelet.api.WaveletProvider;
import java.util.List;
public class MyWaveletProvider implements WaveletProvider {
@Override
public List<Wavelet> getWavelets() {
return List.of(
new Meyer(),
new BiorthogonalSpline22(),
new MexicanHatWavelet(1.0)
);
}
}
- Register with ServiceLoader:
Create a file
src/main/resources/META-INF/services/com.morphiqlabs.wavelet.api.WaveletProvider:
com.example.wavelets.MyWaveletProvider
- For Java Modules (optional):
Add to your
module-info.java:
module com.example.wavelets {
requires com.morphiqlabs.vectorwave;
provides com.morphiqlabs.wavelet.api.WaveletProvider
with com.example.wavelets.MyWaveletProvider;
}
Your wavelets will be automatically discovered when the JAR is on the classpath!
Manual Registration (Runtime)
For dynamic scenarios, you can also register wavelets at runtime:
// Register a single wavelet
WaveletRegistry.registerWavelet(new Meyer());
// Reload all providers (useful for plugin systems)
WaveletRegistry.reload();
4. Consider Optimization Paths
For optimal performance, consider implementing specialized optimizations:
- SIMD Optimizations: If your wavelet has specific patterns, add optimized paths in
VectorOps - Cache-aware Operations: For wavelets with large filters, consider cache-aware implementations
- Specialized Kernels: Add wavelet-specific kernels in
SpecializedKernelsfor common operations
Example:
// In SpecializedKernels.java
public static class MeyerKernel {
public static double[] optimizedTransform(double[] signal, Meyer wavelet) {
// Optimized implementation specific to Meyer wavelet
}
}
5. Add Tests
Create comprehensive tests for your wavelet:
@Test
void testMeyerWaveletProperties() {
Meyer meyer = new Meyer();
assertEquals("meyer", meyer.name());
assertNotNull(meyer.lowPassDecomposition());
assertNotNull(meyer.highPassDecomposition());
// Test perfect reconstruction
WaveletTransform transform = new WaveletTransform(meyer, PaddingStrategies.PERIODIC);
double[] signal = {1, 2, 3, 4, 5, 6, 7, 8};
TransformResult result = transform.forward(signal);
double[] reconstructed = transform.inverse(result);
assertArrayEquals(signal, reconstructed, 1e-10);
}
6. Update Documentation
- Add your wavelet to the README.md features list
- Update any relevant examples
- Document the wavelet's properties and use cases
Wavelet Coefficient Sources
When implementing wavelets, you'll need the filter coefficients. Good sources include:
- PyWavelets: Python library with extensive wavelet coefficients
- MATLAB Wavelet Toolbox: Comprehensive wavelet documentation
- Academic Papers: Original papers defining the wavelets
- "Ten Lectures on Wavelets" by Ingrid Daubechies: Classic reference
Best Practices
- Coefficient Precision: Use high-precision coefficients (at least 15 decimal places)
- Normalization: Ensure filters are properly normalized
- Perfect Reconstruction: Test that forward + inverse transform preserves the signal
- Memory Efficiency: Clone arrays when returning to ensure immutability
- Documentation: Include references to the source of coefficients
- Thread Safety: Ensure your wavelet implementation is thread-safe (typically by being immutable)
- Validation: Implement proper coefficient verification in
verifyCoefficients() - Performance: Consider platform-specific optimizations, especially for Apple Silicon and ARM
Common Pitfalls
- Sign Conventions: Different sources may use different sign conventions for high-pass filters
- Normalization: Some sources use different normalization factors
- Filter Length: Ensure filter lengths are appropriate for the wavelet
- Boundary Effects: Test with different padding strategys
Testing Your Wavelet
Use the provided test utilities and follow the existing test patterns:
// Test perfect reconstruction
double[] signal = {1, 2, 3, 4, 5, 6, 7, 8};
WaveletTransform transform = new WaveletTransform(myWavelet, PaddingStrategies.PERIODIC);
TransformResult result = transform.forward(signal);
double[] reconstructed = transform.inverse(result);
assertArrayEquals(signal, reconstructed, 1e-10);
// Test coefficient verification
Wavelet wavelet = new MyWavelet();
boolean valid = wavelet.verifyCoefficients();
assertTrue(valid, "Wavelet coefficients should be valid");
// Test with different padding strategies
for (PaddingStrategy strategy : java.util.List.of(
PaddingStrategies.PERIODIC,
PaddingStrategies.ZERO,
PaddingStrategies.SYMMETRIC)) {
transform = new WaveletTransform(myWavelet, strategy);
// Test transform
}
// Test with various signal sizes
int[] sizes = {8, 16, 32, 64, 128, 256, 512, 1024};
for (int size : sizes) {
double[] testSignal = new double[size];
// Initialize and test
}
Performance Testing
Add benchmarks for your wavelet:
@Benchmark
public TransformResult benchmarkMyWavelet(BenchmarkState state) {
return state.transform.forward(state.signal);
}
Integration with VectorWave Features
Ensure your wavelet works with all VectorWave features:
Multi-Level Transforms
MultiLevelWaveletTransform mlTransform = new MultiLevelWaveletTransform(
new MyWavelet(),
PaddingStrategies.PERIODIC
);
MultiLevelTransformResult result = mlTransform.decompose(signal, 3);
Denoising
WaveletDenoiser denoiser = new WaveletDenoiser(new MyWavelet());
double[] denoised = denoiser.denoise(noisySignal, ThresholdMethod.UNIVERSAL);
Streaming Support
StreamingWaveletTransform stream = new StreamingWaveletTransformImpl(
new MyWavelet(),
PaddingStrategies.PERIODIC
);
Parallel Processing
ParallelWaveletEngine engine = new ParallelWaveletEngine(new MyWavelet(), 4);
List<TransformResult> results = engine.transformBatch(signals);
Contributing
When contributing a new wavelet:
- Ensure all tests pass:
mvn test - Check code coverage:
mvn clean test jacoco:report - Include benchmark results comparing to existing wavelets
- Provide references for coefficient sources
- Update README.md to include your wavelet in the features list
- Consider adding a demo showcasing your wavelet's use cases
For questions or assistance, please open an issue on the project repository.