├── README.md ├── test_cwt.py ├── LICENSE └── cwt.py /README.md: -------------------------------------------------------------------------------- 1 | # CWT 2 | Python code for implementing the Continuous Wavelet Transform. The implementation is based off the source code in the MATLAB version of cwt in the Wavelet Toolbox. I found that most CWT implementations in Python only outputs the real part of the transform, which is not useful in most cases. 3 | -------------------------------------------------------------------------------- /test_cwt.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import cwt 3 | 4 | 5 | fs = 1e3 6 | t = np.linspace(0, 1, fs+1, endpoint=True) 7 | x = np.cos(2*np.pi*32*t) * np.logical_and(t >= 0.1, t < 0.3) + np.sin(2*np.pi*64*t) * (t > 0.7) 8 | wgnNoise = 0.05 * np.random.standard_normal(t.shape) 9 | x += wgnNoise 10 | c, f = cwt.cwt(x, 'morl', sampling_frequency=fs) 11 | 12 | fig, ax = plt.subplots() 13 | ax.imshow(np.absolute(wt), aspect='auto') 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Alexander Neergaard 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /cwt.py: -------------------------------------------------------------------------------- 1 | import math 2 | import numpy as np 3 | 4 | 5 | def cwt(data, wavelet_name, sampling_frequency=1.): 6 | """ 7 | cwt(data, scales, wavelet) 8 | 9 | One dimensional Continuous Wavelet Transform. 10 | 11 | Parameters 12 | ---------- 13 | data : array_like 14 | Input signal 15 | wavelet_name : Wavelet object or name 16 | Wavelet to use. Currently, only the Morlet wavelet is supported ('morl'). 17 | sampling_frequency : float 18 | Sampling frequency for frequencies output (optional) 19 | 20 | Returns 21 | ------- 22 | coefs : array_like 23 | Continous wavelet transform of the input signal for the given scales 24 | and wavelet 25 | frequencies : array_like 26 | if the unit of sampling period are seconds and given, than frequencies 27 | are in hertz. Otherwise Sampling period of 1 is assumed. 28 | 29 | Notes 30 | ----- 31 | Size of coefficients arrays is automatically calculated given the wavelet and the data length. Currently, only the 32 | Morlet wavelet is supported. 33 | 34 | Examples 35 | -------- 36 | fs = 1e3 37 | t = np.linspace(0, 1, fs+1, endpoint=True) 38 | x = np.cos(2*np.pi*32*t) * np.logical_and(t >= 0.1, t < 0.3) + np.sin(2*np.pi*64*t) * (t > 0.7) 39 | wgnNoise = 0.05 * np.random.standard_normal(t.shape) 40 | x += wgnNoise 41 | c, f = cwt.cwt(x, 'morl', sampling_frequency=fs, plot_scalogram=True) 42 | """ 43 | 44 | # Currently only supported for Morlet wavelets 45 | if wavelet_name == 'morl': 46 | data -= np.mean(data) 47 | n_orig = data.size 48 | nv = 10 49 | ds = 1 / nv 50 | fs = sampling_frequency 51 | dt = 1 / fs 52 | 53 | # Pad data symmetrically 54 | padvalue = n_orig // 2 55 | x = np.concatenate((np.flipud(data[0:padvalue]), data, np.flipud(data[-padvalue:]))) 56 | n = x.size 57 | 58 | # Define scales 59 | _, _, wavscales = getDefaultScales(wavelet_name, n_orig, ds) 60 | num_scales = wavscales.size 61 | 62 | # Frequency vector sampling the Fourier transform of the wavelet 63 | omega = np.arange(1, math.floor(n / 2) + 1, dtype=np.float64) 64 | omega *= (2 * np.pi) / n 65 | omega = np.concatenate((np.array([0]), omega, -omega[np.arange(math.floor((n - 1) / 2), 0, -1, dtype=int) - 1])) 66 | 67 | # Compute FFT of the (padded) time series 68 | f = np.fft.fft(x) 69 | 70 | # Loop through all the scales and compute wavelet Fourier transform 71 | psift, freq = waveft(wavelet_name, omega, wavscales) 72 | 73 | # Inverse transform to obtain the wavelet coefficients. 74 | cwtcfs = np.fft.ifft(np.kron(np.ones([num_scales, 1]), f) * psift) 75 | cfs = cwtcfs[:, padvalue:padvalue + n_orig] 76 | freq = freq * fs 77 | 78 | return cfs, freq 79 | else: 80 | raise Exception 81 | 82 | 83 | def getDefaultScales(wavelet, n, ds): 84 | """ 85 | getDefaultScales(wavelet, n, ds) 86 | 87 | Calculate default scales given a wavelet and a signal length. 88 | 89 | Parameters 90 | ---------- 91 | wavelet : string 92 | Name of wavelet 93 | n : int 94 | Number of samples in a given signal 95 | ds : float 96 | Scale resolution (inverse of number of voices in octave) 97 | 98 | Returns 99 | ------- 100 | s0 : int 101 | Smallest useful scale 102 | ds : float 103 | Scale resolution (inverse of number of voices in octave). Here for legacy reasons; implementing more wavelets 104 | will need this output. 105 | scales : array_like 106 | Array containing default scales. 107 | """ 108 | wname = wavelet 109 | nv = 1 / ds 110 | 111 | if wname == 'morl': 112 | 113 | # Smallest useful scale (default 2 for Morlet) 114 | s0 = 2 115 | 116 | # Determine longest useful scale for wavelet 117 | max_scale = n // (np.sqrt(2) * s0) 118 | if max_scale <= 1: 119 | max_scale = n // 2 120 | max_scale = np.floor(nv * np.log2(max_scale)) 121 | a0 = 2 ** ds 122 | scales = s0 * a0 ** np.arange(0, max_scale + 1) 123 | else: 124 | raise Exception 125 | 126 | return s0, ds, scales 127 | 128 | 129 | def waveft(wavelet, omega, scales): 130 | """ 131 | waveft(wavelet, omega, scales) 132 | 133 | Computes the Fourier transform of a wavelet at certain scales. 134 | 135 | Parameters 136 | ---------- 137 | wavelet : string 138 | Name of wavelet 139 | omega : array_like 140 | Array containing frequency values in Hz at which the transform is evaluated. 141 | scales : array_like 142 | Vector containing the scales used for the wavelet analysis. 143 | 144 | Returns 145 | ------- 146 | wft : array_like 147 | (num_scales x num_freq) Array containing the wavelet Fourier transform 148 | freq : array_like 149 | Array containing frequency values 150 | """ 151 | wname = wavelet 152 | num_freq = omega.size 153 | num_scales = scales.size 154 | wft = np.zeros([num_scales, num_freq]) 155 | 156 | if wname == 'morl': 157 | gC = 6 158 | mul = 2 159 | for jj, scale in enumerate(scales): 160 | expnt = -(scale * omega - gC) ** 2 / 2 * (omega > 0) 161 | wft[jj, ] = mul * np.exp(expnt) * (omega > 0) 162 | 163 | fourier_factor = gC / (2 * np.pi) 164 | frequencies = fourier_factor / scales 165 | 166 | else: 167 | raise Exception 168 | 169 | return wft, frequencies 170 | --------------------------------------------------------------------------------