@@ -256,3 +256,123 @@ of frequency directly.
256256
257257
258258.. plot :: pyplots/cwt_scaling_demo.py
259+
260+
261+ .. _CWT normalization :
262+
263+ Normalization of the CWT coefficients
264+ -------------------------------------
265+
266+ ``cwt `` works entirely in units of samples. Writing :math: `x[n]` for the input
267+ signal and taking both the scale :math: `a` and the translation :math: `b` to be
268+ expressed in samples, the returned coefficients are
269+
270+ .. math ::
271+
272+ C[a, b] = \frac {1 }{\sqrt {a}}\sum _n x[n]\,
273+ \psi ^*\!\left (\frac {n - b}{a}\right ).
274+
275+ No sampling interval appears in this expression. In particular, the
276+ ``sampling_period `` argument of :func: `pywt.cwt ` rescales only the returned
277+ ``frequencies ``; the coefficients themselves do not depend on it.
278+
279+ Relation to the continuous-time transform
280+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
281+
282+ The continuous-time definition of the CWT, with a scale :math: `a_s` and a
283+ translation :math: `b_s` in seconds, is
284+
285+ .. math ::
286+
287+ W_x(a_s, b_s) = \frac {1 }{\sqrt {a_s}}
288+ \int x(t)\,\psi ^*\!\left (\frac {t - b_s}{a_s}\right )\,\mathrm {d}t.
289+
290+ For data sampled at :math: `t_n = n\,\mathrm {d}t`, approximating that integral by
291+ a Riemann sum introduces a factor :math: `\mathrm {d}t`. Substituting
292+ :math: `a_s = a\,\mathrm {d}t` and :math: `b_s = b\,\mathrm {d}t` then gives
293+
294+ .. math ::
295+
296+ W_x(a_s, b_s) \approx \frac {\mathrm {d}t}{\sqrt {a\,\mathrm {d}t}}
297+ \sum _n x[n]\,\psi ^*\!\left (\frac {n - b}{a}\right )
298+ = \sqrt {\mathrm {d}t}\, C[a, b].
299+
300+ So the coefficients returned by ``cwt `` have to be **multiplied ** by
301+ :math: `\sqrt {\mathrm {d}t}` (equivalently, divided by :math: `\sqrt {f_s}`) to be
302+ expressed in physical-time units. PyWavelets does not apply that factor,
303+ because doing so would make the coefficients depend on ``sampling_period ``.
304+
305+ A common source of confusion
306+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
307+
308+ A frequent mistake when comparing a hand-written convolution against ``cwt `` is
309+ to *mix * the two conventions above: to use a scale in seconds,
310+ :math: `a_s = a\,\mathrm {d}t`, in the :math: `1 /\sqrt {a_s}` prefactor, while
311+ summing over raw samples without the :math: `\mathrm {d}t` coming from the
312+ integral. Such a result is too large by a factor
313+ :math: `1 /\sqrt {\mathrm {d}t} = \sqrt {f_s}`, and an otherwise unexplained factor
314+ :math: `1 /\sqrt {f_s}` then has to be inserted by hand to make the amplitudes
315+ agree with ``cwt ``. Either keep everything in samples, as :math: `C[a, b]` does,
316+ or write out the full Riemann sum including :math: `\mathrm {d}t` — but do not
317+ combine the two.
318+
319+ The example below evaluates :math: `C[a, b]` directly and compares it to
320+ ``cwt ``:
321+
322+ .. try_examples ::
323+
324+ >>> import numpy as np
325+ >>> import pywt
326+ >>> fs = 3000.0 # sampling rate in Hz
327+ >>> t = np.arange(3000 ) / fs
328+ >>> x = np.sin(2 * np.pi * 40 * t)
329+ >>> wavelet = pywt.ContinuousWavelet(' cmor14-2' )
330+ >>> a = pywt.frequency2scale(wavelet, 40 / fs) # scale, in samples
331+ >>> coefs, freqs = pywt.cwt(x, a, wavelet, sampling_period = 1 / fs)
332+
333+ Now the same transform, written out as a convolution over samples. ``Fb `` and
334+ ``Fc `` are the bandwidth and center frequency of ``'cmor14-2' ``:
335+
336+ >>> Fb, Fc = 14.0 , 2.0
337+ >>> lb, ub = wavelet.lower_bound, wavelet.upper_bound
338+ >>> k = np.arange(int (a * (ub - lb)) + 1 )
339+ >>> u = lb + (k + 0.5 ) / a
340+ >>> psi = np.exp(2j * np.pi* Fc* u) * np.exp(- u** 2 / Fb) / np.sqrt(np.pi * Fb)
341+ >>> conv = np.convolve(x, np.conj(psi)[::- 1 ]) / np.sqrt(a)
342+ >>> trim = (conv.size - x.size) // 2
343+ >>> manual = conv[trim:trim + x.size]
344+ >>> rel = np.max(np.abs(manual - coefs[0 ])) / np.max(np.abs(coefs[0 ]))
345+ >>> bool (rel < 1e-3 )
346+ True
347+
348+ Note that :math: `\psi ` is conjugated *and * reversed before the convolution, and
349+ that it is evaluated at bin midpoints, ``(k + 0.5) / a ``. The half-sample offset
350+ is there because ``cwt `` convolves with the integral of :math: `\psi ` and then
351+ differences the result, which effectively averages :math: `\psi ` over each
352+ sample bin rather than sampling it pointwise.
353+
354+ Limits of the agreement
355+ ^^^^^^^^^^^^^^^^^^^^^^^
356+
357+ :math: `C[a, b]` is what ``cwt `` computes in the limit of a finely sampled
358+ wavelet. Two discretization effects prevent an analytic implementation from
359+ reproducing it exactly:
360+
361+ * :math: `\psi ` is only evaluated over
362+ ``[wavelet.lower_bound, wavelet.upper_bound] ``, so its tails are truncated
363+ (see :ref: `Choosing scales `). With the default bounds of :math: `[-8 , 8 ]`,
364+ ``cmor14-2 `` still retains about 1% of its peak amplitude at the edges, and
365+ that truncation dominates the residual in the example above.
366+ * ``cwt `` resamples the precomputed integral of :math: `\psi ` onto the grid for a
367+ given scale by truncated indexing rather than by interpolation. The resulting
368+ jitter grows with the scale and is reduced by raising ``precision ``; it is the
369+ "zipper-like" effect mentioned in the ``precision `` documentation of
370+ :func: `pywt.cwt `. It largely averages out for narrowband signals such as the
371+ one above, but is clearly visible for broadband input.
372+
373+ Finally, note that the amplitude of the wavelets themselves does not follow a
374+ single convention across families — ``mexh `` and ``gaus `` are normalized to unit
375+ energy, ``morl `` carries no normalization constant at all, and ``cmor ``,
376+ ``shan `` and ``fbsp `` use yet other conventions (see the formulas above). CWT
377+ coefficient magnitudes are therefore not directly comparable between wavelet
378+ families.
0 commit comments