Rand Stats

Math::Wavelet

zef:ash

Math::Wavelet

Wavelet transforms in pure Raku: the single-level and multilevel DWT, the stationary transform, the 2-D transform, thresholding and wavelet denoising, over the 106 discrete wavelets PyWavelets has; and the continuous transform, over its 21 continuous ones — the Gaussian derivatives, the Mexican hat, Morlet, and their complex relatives. It has no dependencies and no native library, and agrees with PyWavelets to ten significant digits.

Version 0.0.2. The interface below is implemented and tested on both engines; what it leaves out is under Scope.

use Math::Wavelet;

my @x = (^64).map({ sin($_ / 4) + ($_ %% 3 ?? 0.1 !! -0.05) });

my ($cA, $cD) = dwt(@x, 'db4');                 # one level
my @y         = idwt($cA, $cD, 'db4');          # and back

my @c = wavedec(@x, 'sym5', :level(3));         # [cA3, cD3, cD2, cD1]
my @r = waverec(@c, 'sym5');
my @t = threshold(@c, 0.1, :mode<hard>);

my @clean = denoise(@x, 'db4');                 # VisuShrink, soft

my @image = (^8).map(-> $r { (^8).map({ $r * $_ }).Array });
my ($a, ($h, $v, $d)) = dwt2(@image, 'haar');   # an image is an array of rows

my ($W, $f) = cwt(@x, [1, 2, 4, 8], 'mexh');    # a row of coefficients per scale
rakudo -Ilib examples/denoise.raku
rakupp -Ilib examples/denoise.raku

Install

Either installer takes it, into the same ~/.raku store:

rakupp install Math::Wavelet
zef install Math::Wavelet

The modules

use Math::Wavelet exports every routine below. The modules behind it can also be used one at a time:

modulewhat it is
Math::Waveletthe one use line
Math::Wavelet::Filtera wavelet: its four filters and what is known about it
Math::Wavelet::DWTdwt, idwt, wavedec, waverec and the extension modes
Math::Wavelet::SWTswt, iswt
Math::Wavelet::DWT2dwt2, idwt2, wavedec2, waverec2
Math::Wavelet::Thresholdthreshold, noise-sigma, denoise
Math::Wavelet::Continuousa continuous wavelet: ψ, its support and what is known about it
Math::Wavelet::CWTcwt, integrate-wavelet, central-frequency, scale2frequency, frequency2scale
Math::Wavelet::Tablethe filter coefficients, generated from PyWavelets

Wavelets

A discrete wavelet is given by name, or as a Math::Wavelet::Filter:

familynames
Haarhaar
Daubechiesdb1 … db38
Symletssym2 … sym20
Coifletscoif1 … coif17
Biorthogonalbior1.1 bior1.3 bior1.5 bior2.2 bior2.4 bior2.6 bior2.8 bior3.1 bior3.3 bior3.5 bior3.7 bior3.9 bior4.4 bior5.5 bior6.8
Reverse biorthogonalrbio with the same numbers
Discrete Meyerdmey
my $w = wavelet('sym4');
say $w.family;                     # Symlets
say $w.dec-len;                    # 8
say $w.vanishing-moments-psi;      # 4
say wavelist('coif').elems;        # 17
say wavelist.elems;                # 127: wavelist(:kind<discrete>) is 106

wavelist takes PyWavelets' :kind, all (the default), discrete or continuous, and wavelet answers a Math::Wavelet::Continuous for a continuous name. The continuous wavelets are under The continuous transform.

method
.dec-lo .dec-hi .rec-lo .rec-hithe four filters
.filter-bankthe four as a list, in that order
.dec-len, .rec-lenfilter length
.name .family .short-family .symmetry
.orthogonal, .biorthogonal
.vanishing-moments-psi, .vanishing-moments-phi0 where PyWavelets has none
.scaled($k)a copy with every filter multiplied by $k

A wavelet of your own is either the orthogonal filter bank that one lowpass filter defines, or all four filters given explicitly:

my $own  = Math::Wavelet::Filter.from-lowpass(@dec-lo, :name<mine>);
my $bank = Math::Wavelet::Filter.new(:@dec-lo, :@dec-hi, :@rec-lo, :@rec-hi);

The coefficients are PyWavelets' own doubles, bit for bit. dmey is a 62-tap truncation of the Meyer filter, so it reconstructs only to within about half a percent. The same is true in PyWavelets.

The one-dimensional transform

dwt(@x, $w, :$mode)(cA, cD)
idwt($cA, $cD, $w, :$mode)either half may be Nil, read as zeros
wavedec(@x, $w, :$mode, :$level)[cA_n, cD_n, …, cD_1]; the level defaults to the maximum
waverec(@coeffs, $w, :$mode)
dwt-max-level($n, $w)the deepest useful level for $n samples
dwt-coeff-len($n, $w, :$mode)the length of each half

Every routine takes a name or a Math::Wavelet::Filter for $w, and returns Arrays of Num. Outside periodization, a level of n samples produces ⌊(n + F − 1) / 2⌋ coefficients in each half for an F-tap filter, and the inverse gives back 2·len − F + 2 samples. waverec drops the extra trailing approximation coefficient that an odd length leaves, the same way PyWavelets does.

Signal extension

What the transform assumes lies beyond each end of the signal. The default is symmetric.

modeextends x0 x1 … xN with
zero0 0 | x0 … xN | 0 0
constantx0 x0 | x0 … xN | xN xN
symmetricx1 x0 | x0 … xN | xN xN−1
reflectx2 x1 | x0 … xN | xN−1 xN−2
periodicxN−1 xN | x0 … xN | x0 x1
smooththe first and last slopes, continued
antisymmetric−x1 −x0 | x0 … xN | −xN −xN−1
antireflect2x0 − x1 | x0 … xN | 2xN − xN−1
periodizationperiodic, with exactly ⌈n/2⌉ coefficients per half

periodization is the mode in which an orthogonal wavelet is an orthogonal transform: the coefficients carry exactly the signal's energy and there are exactly as many of them as samples. Every other mode adds F − 2 coefficients per level for the boundary, or F − 1 when the length is odd.

The stationary transform

The undecimated transform keeps every coefficient at every level, so it is shift-invariant, at the cost of n coefficients per band per level.

swt(@x, $w, :$level, :$start-level, :$trim-approx, :$norm)[[cA_n, cD_n], …, [cA_1, cD_1]]
iswt(@coeffs, $w, :$norm)from either output shape
swt-max-level($n)how many times $n halves evenly

:trim-approx returns [cA_n, cD_n, …, cD_1] instead. :norm scales the filters by 1/√2, which makes an orthogonal wavelet's transform preserve energy. The length must be divisible by 2^(level + start-level).

Two dimensions

An image is an array of rows. dwt2 filters down the columns first, then along the rows. That is PyWavelets' order, and it gives PyWavelets' band names:

dwt2(@m, $w, :$mode)(cA, (cH, cV, cD)) — horizontal, vertical, diagonal detail
idwt2(($cA, ($cH, $cV, $cD)), $w, :$mode)any band may be Nil
wavedec2(@m, $w, :$mode, :$level)[cA_n, (cH_n, cV_n, cD_n), …, (cH_1, cV_1, cD_1)]
waverec2(@coeffs, $w, :$mode)

Thresholding and denoising

threshold(@x, $value, :$mode, :$substitute)element-wise, through nested arrays
noise-sigma(@detail)`median(
denoise(@x, $w, :$level, :$mode, :$threshold, :$extension)

The threshold modes are PyWavelets':

modex becomes
soft (default)x·(1 − v/|x|), and substitute where |x| < v
hardx, and substitute where |x| < v
garrotex·(1 − v²/x²), and substitute where |x| < v
greaterx, and substitute where x < v
lessx, and substitute where x > v

denoise is VisuShrink. It estimates σ from the finest detail level, thresholds every detail level at σ·√(2 ln n) (or at :threshold, if given), leaves the approximation alone, and reconstructs n samples. :mode is the threshold mode, and :extension the signal-extension mode.

Examples

The continuous transform

cwt(@x, $scales, $w, :$sampling-period, :$method)(coefficients, frequencies): a row of +@x coefficients per scale
integrate-wavelet($w, :$precision)(∫ψ, x) over 2**$precision samples
central-frequency($w, :$precision)where ψ's spectrum peaks, in cycles per sample at scale 1
scale2frequency($w, $scale), frequency2scale($w, $f)either takes one value or a list

$scales is one positive number or a list of them, not necessarily integers. A coefficient is a Num, or a Complex for a complex wavelet. The frequencies are in cycles per $sampling-period, which defaults to one sample.

The algorithm is PyWavelets': ψ is sampled at 2¹⁰ points and integrated once, the integral is resampled at each scale, convolved with the signal, and differenced. :method<conv> (the default) convolves directly, which costs n·m for a kernel of m taps, and m grows with the scale. :method<fft> convolves through a radix-2 FFT. The signal's spectrum is computed once per transform size, and a complex kernel costs no more than a real one. The two methods agree to rounding. For 40 scales over 512 samples of mexh, conv takes 3.2 s on Raku++ and 1.5 s on Rakudo, and fft takes 1.1 s on Raku++ and 1.3 s on Rakudo.

Continuous wavelets

familynamesψ(x)
Gaussiangaus1 … gaus8the p-th derivative of e^(−x²), normalised
Mexican hatmexh(2 / (√3 π^¼)) (1 − x²) e^(−x²/2)
Morletmorlcos(5x) e^(−x²/2)
Complex Gaussiancgau1 … cgau8the p-th derivative of e^(−ix − x²), normalised
ShannonshanB-C√B sinc(Bx) e^(2πiCx)
Frequency B-splinefbspM-B-C√B sinc(Bx/M)^M e^(2πiCx)
Complex MorletcmorB-C(πB)^(−½) e^(−x²/B) e^(2πiCx)

B is the bandwidth and C the centre frequency, and M is the spline order, an integer of at least 1: cmor1.5-1.0, fbsp2-1-0.5. A bare shan, fbsp or cmor takes PyWavelets' defaults, which PyWavelets itself now warns about. The explicit form is the one to write.

my $w = wavelet('cmor1.5-1.0');
say $w.family;                     # Complex Morlet wavelets
say $w.complex-cwt;                # True
say $w.bandwidth-frequency;        # 1.5
my ($psi, $x) = $w.wavefun(:level(8));   # 256 samples over [-8, 8]
say $w.psi(0);                     # 0.4606+0i
method
.psi($x)ψ at one point: a Num, or a Complex
.wavefun(:$level, :$length)(psi, x): 2**$level samples, or $length, over the support
.lower-bound, .upper-boundthe support that wavefun and cwt sample
.center-frequency, .bandwidth-frequency, .fbsp-orderfor shan, fbsp and cmor; undefined otherwise
.name .family .short-family .symmetry .complex-cwt

The discrete transforms refuse a continuous wavelet, and cwt refuses a discrete one, saying so.

Scope

Left out of 0.0.1 on purpose:

the continuous transform of a matrixcwt takes one signal; PyWavelets' axis is not there
central-frequency of a discrete waveletPyWavelets runs the cascade algorithm for it; here it takes a continuous one
wavelet packetsthe full decomposition tree, and best-basis selection
n-dimensional transformsdwtn and swt2; 2-D is dwt2 and wavedec2 only
complex datathe transforms take real numbers; a complex wavelet gives complex coefficients
per-axis wavelets or modesone wavelet and one mode for both axes of an image

The stationary transform inverts only from start level 0, as in PyWavelets.

Compatibility

engineversion01-dwt02-multilevel03-swt04-dwt205-threshold06-wavelets07-cwt
Rakudov2026.091144/1144534/534136/13674/7441/41228/228565/565
Raku++5.3.0, a development build1144/1144534/534136/13674/7441/41228/228565/565

Neither version is an established floor; no older engine has been tried. The expected values in t/vectors come from PyWavelets 1.8.0, through tools/gen-vectors.py, and the filter table from the same version, through tools/gen-table.py. continuous.vec and cwt.vec came later, from the same PyWavelets on NumPy 2.5.4. That build regenerates the older files with a few last-digit differences, such as 0 against -2.4e-16, so those files were kept as they were.

Author

Andrew Shitov (zef:ash).

Licence

Artistic-2.0.


Why the module is shaped this way, and what running it under two engines turned up, is in notes/Math-Wavelet.md.