324 lines
11 KiB
Go
324 lines
11 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
|
|
// SPDX-License-Identifier: MIT
|
|||
|
|
|
|||
|
|
package signal
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"slices"
|
|||
|
|
|
|||
|
|
"sourcedock.dev/petrbalvin/tensor/internal/base"
|
|||
|
|
"sourcedock.dev/petrbalvin/tensor/internal/core"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// Daubechies discrete wavelets: db2 through db8 beside the
|
|||
|
|
// Haar transform the package already carries in DWT and IDWT. Each
|
|||
|
|
// family is an orthonormal two-channel filter bank driven over levels
|
|||
|
|
// of halving blocks, packed in the same [A_L, D_L, …, D_1] layout the
|
|||
|
|
// Haar transform uses; each dbN pair has N vanishing moments and 2N
|
|||
|
|
// taps, so higher families resolve smoother signals into sparser
|
|||
|
|
// details but tie the boundary condition to longer blocks.
|
|||
|
|
//
|
|||
|
|
// The tables below are not trusted on authority: they were produced
|
|||
|
|
// by the spectral factorisation of Daubechies' polynomial
|
|||
|
|
// P(y) = Σ_k C(N−1+k, k)·y^k, and the tests certify them against the
|
|||
|
|
// defining conditions, the unit norm, the shift-2 orthogonality, the
|
|||
|
|
// N vanishing moments and the spectral identity
|
|||
|
|
// |H(ω)|² = 2·cos^{2N}(ω/2)·P(sin²(ω/2)), so a mistyped digit cannot
|
|||
|
|
// survive.
|
|||
|
|
|
|||
|
|
// Daubechies names a member of the Daubechies wavelet family: dbN
|
|||
|
|
// carries N vanishing moments and a 2N-tap filter pair. Haar remains
|
|||
|
|
// the db1 case and keeps its own exact transform in DWT and IDWT.
|
|||
|
|
type Daubechies string
|
|||
|
|
|
|||
|
|
const (
|
|||
|
|
// DB2 is the 4-tap Daubechies wavelet with 2 vanishing moments.
|
|||
|
|
DB2 Daubechies = "db2"
|
|||
|
|
// DB3 is the 6-tap Daubechies wavelet with 3 vanishing moments.
|
|||
|
|
DB3 Daubechies = "db3"
|
|||
|
|
// DB4 is the 8-tap Daubechies wavelet with 4 vanishing moments.
|
|||
|
|
DB4 Daubechies = "db4"
|
|||
|
|
// DB5 is the 10-tap Daubechies wavelet with 5 vanishing moments.
|
|||
|
|
DB5 Daubechies = "db5"
|
|||
|
|
// DB6 is the 12-tap Daubechies wavelet with 6 vanishing moments.
|
|||
|
|
DB6 Daubechies = "db6"
|
|||
|
|
// DB7 is the 14-tap Daubechies wavelet with 7 vanishing moments.
|
|||
|
|
DB7 Daubechies = "db7"
|
|||
|
|
// DB8 is the 16-tap Daubechies wavelet with 8 vanishing moments.
|
|||
|
|
DB8 Daubechies = "db8"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// The scaling filters h, the low-pass analysis side, normalised to
|
|||
|
|
// Σ h = √2. Treat them as read-only.
|
|||
|
|
var (
|
|||
|
|
db2Coeffs = []float64{
|
|||
|
|
0.48296291314453421, 0.83651630373780794, 0.22414386804201339, -0.1294095225512604,
|
|||
|
|
}
|
|||
|
|
db3Coeffs = []float64{
|
|||
|
|
0.33267055295008258, 0.80689150931109266, 0.45987750211849149, -0.13501102001025453,
|
|||
|
|
-0.085441273882026644, 0.035226291885709533,
|
|||
|
|
}
|
|||
|
|
db4Coeffs = []float64{
|
|||
|
|
0.23037781330889651, 0.71484657055291556, 0.63088076792985903, -0.027983769416859948,
|
|||
|
|
-0.18703481171909306, 0.030841381835560764, 0.032883011666885203, -0.010597401785069032,
|
|||
|
|
}
|
|||
|
|
db5Coeffs = []float64{
|
|||
|
|
0.1601023979741929, 0.60382926979718965, 0.72430852843777283, 0.13842814590132091,
|
|||
|
|
-0.24229488706638203, -0.032244869584638361, 0.077571493840045691, -0.0062414902127982726,
|
|||
|
|
-0.012580751999081997, 0.0033357252854737704,
|
|||
|
|
}
|
|||
|
|
db6Coeffs = []float64{
|
|||
|
|
0.11154074335010949, 0.49462389039845328, 0.75113390802109559, 0.31525035170919741,
|
|||
|
|
-0.22626469396543983, -0.12976686756726177, 0.097501605587322904, 0.027522865530305803,
|
|||
|
|
-0.031582039317486044, 0.00055384220116149461, 0.0047772575109455116, -0.0010773010853084798,
|
|||
|
|
}
|
|||
|
|
db7Coeffs = []float64{
|
|||
|
|
0.07785205408500917, 0.39653931948191723, 0.72913209084623509, 0.46978228740519357,
|
|||
|
|
-0.14390600392856487, -0.22403618499387515, 0.071309219266830329, 0.080612609151083051,
|
|||
|
|
-0.0380299369350144, -0.016574541630666881, 0.012550998556099839, 0.00042957797292136738,
|
|||
|
|
-0.0018016407040474906, 0.00035371379997452024,
|
|||
|
|
}
|
|||
|
|
db8Coeffs = []float64{
|
|||
|
|
0.054415842243104001, 0.3128715909143, 0.67563073629728954, 0.58535468365420718,
|
|||
|
|
-0.015829105256349327, -0.28401554296154741, 0.00047248457391386436, 0.12874742662047808,
|
|||
|
|
-0.017369301001807447, -0.044088253930794685, 0.013981027917398216, 0.0087460940474057974,
|
|||
|
|
-0.0048703529934515776, -0.00039174037337694672, 0.00067544940645056933, -0.00011747678412476953,
|
|||
|
|
}
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// daubechiesTaps resolves a family name to its scaling filter.
|
|||
|
|
func daubechiesTaps(name string, family Daubechies) ([]float64, error) {
|
|||
|
|
switch family {
|
|||
|
|
case DB2:
|
|||
|
|
return db2Coeffs, nil
|
|||
|
|
case DB3:
|
|||
|
|
return db3Coeffs, nil
|
|||
|
|
case DB4:
|
|||
|
|
return db4Coeffs, nil
|
|||
|
|
case DB5:
|
|||
|
|
return db5Coeffs, nil
|
|||
|
|
case DB6:
|
|||
|
|
return db6Coeffs, nil
|
|||
|
|
case DB7:
|
|||
|
|
return db7Coeffs, nil
|
|||
|
|
case DB8:
|
|||
|
|
return db8Coeffs, nil
|
|||
|
|
default:
|
|||
|
|
return nil, base.Errf("%s: unknown Daubechies family %q (want db2 through db8)", name, string(family))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// DWTMode picks the boundary treatment of the Daubechies transform.
|
|||
|
|
type DWTMode int
|
|||
|
|
|
|||
|
|
const (
|
|||
|
|
// DWTPeriodic treats the signal as one period of a periodic
|
|||
|
|
// sequence: the default. Every block keeps its exact energy and
|
|||
|
|
// the length must offer every level a multiple of the filter
|
|||
|
|
// length to work on.
|
|||
|
|
DWTPeriodic DWTMode = iota
|
|||
|
|
// DWTZeroPad extends the signal with zeros at the tail to the
|
|||
|
|
// next length the level tree needs, so any length transforms;
|
|||
|
|
// the coefficients past the signal's own span carry the
|
|||
|
|
// response of the step to zero at the seam.
|
|||
|
|
DWTZeroPad
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// DaubechiesDWT returns the discrete Daubechies wavelet transform of
|
|||
|
|
// the rank-1 real signal x over levels scales, packed as
|
|||
|
|
// [A_levels, D_levels, …, D_1]: the deepest approximation first, each
|
|||
|
|
// detail band after it, the finest last. mode picks the boundary
|
|||
|
|
// treatment, DWTPeriodic by default: it refuses a length that does
|
|||
|
|
// not leave every live block a multiple of the 2N-tap filter (the
|
|||
|
|
// deepest block must hold at least one filter length), while
|
|||
|
|
// DWTZeroPad zero-pads the tail to the next such length first. The
|
|||
|
|
// periodic transform conserves energy exactly; a zero-padded one
|
|||
|
|
// conserves the energy of the padded signal. levels must be at
|
|||
|
|
// least 1.
|
|||
|
|
func DaubechiesDWT(x *core.Array, family Daubechies, levels int, mode DWTMode) (*core.Array, error) {
|
|||
|
|
const name = "DaubechiesDWT"
|
|||
|
|
src, h, levels, mode, err := dwtPrepare(name, x, family, levels, mode)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
L := len(h)
|
|||
|
|
n := x.Len()
|
|||
|
|
// The periodic contract gates the length; the zero-padded one
|
|||
|
|
// extends the tail to the smallest length that offers every
|
|||
|
|
// level a multiple of the filter length instead.
|
|||
|
|
length := n
|
|||
|
|
if mode == DWTPeriodic {
|
|||
|
|
if err := dwtLengthGate(name, n, L, levels); err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
// The padded block must stay inside the allocator's reach: the
|
|||
|
|
// shift wraps past the addressable range first, and a span that
|
|||
|
|
// survives the wrap but tops the makeslice ceiling of 2^45
|
|||
|
|
// float64 elements panics in the make below instead of refusing
|
|||
|
|
// here.
|
|||
|
|
span := L << (levels - 1)
|
|||
|
|
if span <= 0 || span >= 1<<45 {
|
|||
|
|
return nil, base.Errf("%s: %d levels need a padded length no machine could hold", name, levels)
|
|||
|
|
}
|
|||
|
|
length = ((n-1)/span + 1) * span
|
|||
|
|
}
|
|||
|
|
work := make([]float64, length)
|
|||
|
|
copy(work, src)
|
|||
|
|
g := daubechiesHighPass(h)
|
|||
|
|
// Each pass halves the live block: the approximation and the
|
|||
|
|
// detail land in scratch, then the block rearranges into
|
|||
|
|
// [A, D] exactly as the packed layout wants.
|
|||
|
|
scratch := make([]float64, length)
|
|||
|
|
m := length
|
|||
|
|
for range levels {
|
|||
|
|
half := m / 2
|
|||
|
|
a, d := scratch[:half], scratch[half:m]
|
|||
|
|
dwtForwardLevel(work[:m], a, d, h, g)
|
|||
|
|
copy(work[:half], a)
|
|||
|
|
copy(work[half:m], d)
|
|||
|
|
m = half
|
|||
|
|
}
|
|||
|
|
return core.FromFloats(work, length)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// DaubechiesIDWT inverts DaubechiesDWT over the same family, level
|
|||
|
|
// count and mode, undoing the packed layout deepest band first. The
|
|||
|
|
// coefficient vector must satisfy the length contract every forward
|
|||
|
|
// transform produced: a multiple of the filter length at every level
|
|||
|
|
// of the tree, whichever mode the forward ran. A zero-padded forward
|
|||
|
|
// transform therefore inverts to the padded length, whose head is
|
|||
|
|
// the original signal; the mode argument gates exactly this
|
|||
|
|
// contract, the inverse bank itself is the periodic one.
|
|||
|
|
func DaubechiesIDWT(coef *core.Array, family Daubechies, levels int, mode DWTMode) (*core.Array, error) {
|
|||
|
|
const name = "DaubechiesIDWT"
|
|||
|
|
src, h, levels, _, err := dwtPrepare(name, coef, family, levels, mode)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
if err := dwtLengthGate(name, coef.Len(), len(h), levels); err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
g := daubechiesHighPass(h)
|
|||
|
|
work := make([]float64, len(src))
|
|||
|
|
copy(work, src)
|
|||
|
|
m := len(work) >> levels
|
|||
|
|
for range levels {
|
|||
|
|
half := m
|
|||
|
|
m = 2 * m
|
|||
|
|
a := slices.Clone(work[:half])
|
|||
|
|
d := slices.Clone(work[half:m])
|
|||
|
|
for j := range m {
|
|||
|
|
var av, dv float64
|
|||
|
|
for k := range h {
|
|||
|
|
// The synthesis taps land at even steps only:
|
|||
|
|
// e/2 is the band index the tap reads.
|
|||
|
|
e := j - k
|
|||
|
|
if e < 0 {
|
|||
|
|
e += m
|
|||
|
|
}
|
|||
|
|
if e&1 == 1 {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
av += h[k] * a[e>>1]
|
|||
|
|
dv += g[k] * d[e>>1]
|
|||
|
|
}
|
|||
|
|
work[j] = av + dv
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return core.FromFloats(work, len(work))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// dwtPrepare runs the shared contract of the Daubechies transforms
|
|||
|
|
// ahead of the length question: rank-1 real non-empty input, a known
|
|||
|
|
// family, at least one level, a known mode. It returns the widened
|
|||
|
|
// input, the scaling filter and the validated level count and mode.
|
|||
|
|
func dwtPrepare(name string, x *core.Array, family Daubechies, levels int, mode DWTMode) (src []float64, h []float64, levelsOut int, modeOut DWTMode, err error) {
|
|||
|
|
if x.NDim() != 1 {
|
|||
|
|
return nil, nil, 0, 0, base.Errf("%s: needs a rank-1 signal, got shape %s", name, base.ShapeText(x.Shape()))
|
|||
|
|
}
|
|||
|
|
if x.Dtype() == core.Complex {
|
|||
|
|
return nil, nil, 0, 0, base.Errf("%s: complex signals are not supported", name)
|
|||
|
|
}
|
|||
|
|
if x.Len() == 0 {
|
|||
|
|
return nil, nil, 0, 0, base.Errf("%s: an empty signal has no transform", name)
|
|||
|
|
}
|
|||
|
|
h, err = daubechiesTaps(name, family)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, nil, 0, 0, err
|
|||
|
|
}
|
|||
|
|
if levels < 1 {
|
|||
|
|
return nil, nil, 0, 0, base.Errf("%s: levels must be at least 1, got %d", name, levels)
|
|||
|
|
}
|
|||
|
|
switch mode {
|
|||
|
|
case DWTPeriodic, DWTZeroPad:
|
|||
|
|
default:
|
|||
|
|
return nil, nil, 0, 0, base.Errf("%s: unknown boundary mode %d", name, int(mode))
|
|||
|
|
}
|
|||
|
|
return widenFloats(x), h, levels, mode, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// dwtLengthGate enforces the tree contract on a length: the deepest
|
|||
|
|
// block, n>>levels−1 samples, must be a whole multiple of the filter
|
|||
|
|
// length and at least one filter long, which the halvings before it
|
|||
|
|
// then inherit. Shifts past the word width answer 0 and fail the
|
|||
|
|
// gate, which is exactly the refusal a too deep tree wants.
|
|||
|
|
func dwtLengthGate(name string, n, L, levels int) error {
|
|||
|
|
blk := n >> (levels - 1)
|
|||
|
|
if levels-1 >= 64 || (blk<<(levels-1)) != n || blk < L || blk%L != 0 {
|
|||
|
|
return base.Errf("%s: %d levels need every live block to hold a multiple of the %d-tap filter, which %d samples do not offer",
|
|||
|
|
name, levels, L, n)
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// daubechiesHighPass derives the wavelet (high-pass) half of the bank
|
|||
|
|
// from the scaling filter: g[k] = (−1)^k·h[L−1−k].
|
|||
|
|
func daubechiesHighPass(h []float64) []float64 {
|
|||
|
|
L := len(h)
|
|||
|
|
g := make([]float64, L)
|
|||
|
|
for k := range L {
|
|||
|
|
sign := 1.0
|
|||
|
|
if k%2 == 1 {
|
|||
|
|
sign = -1
|
|||
|
|
}
|
|||
|
|
g[k] = sign * h[L-1-k]
|
|||
|
|
}
|
|||
|
|
return g
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// dwtForwardLevel runs one periodic analysis level over the block
|
|||
|
|
// x of even length m: a[i] = Σ h[k]·x[(2i+k) mod m] and the same with
|
|||
|
|
// g for d. Outputs whose taps never wrap walk the block directly;
|
|||
|
|
// the ones near the block's end correct the few negative indices.
|
|||
|
|
func dwtForwardLevel(x, a, d, h, g []float64) {
|
|||
|
|
m := len(x)
|
|||
|
|
L := len(h)
|
|||
|
|
half := m / 2
|
|||
|
|
limit := (m - L) / 2
|
|||
|
|
for i := 0; i <= limit; i++ {
|
|||
|
|
base := 2 * i
|
|||
|
|
var av, dv float64
|
|||
|
|
for k := range L {
|
|||
|
|
xv := x[base+k]
|
|||
|
|
av += h[k] * xv
|
|||
|
|
dv += g[k] * xv
|
|||
|
|
}
|
|||
|
|
a[i], d[i] = av, dv
|
|||
|
|
}
|
|||
|
|
for i := limit + 1; i < half; i++ {
|
|||
|
|
base := 2*i - m
|
|||
|
|
var av, dv float64
|
|||
|
|
for k := range L {
|
|||
|
|
idx := base + k
|
|||
|
|
if idx < 0 {
|
|||
|
|
idx += m
|
|||
|
|
}
|
|||
|
|
xv := x[idx]
|
|||
|
|
av += h[k] * xv
|
|||
|
|
dv += g[k] * xv
|
|||
|
|
}
|
|||
|
|
a[i], d[i] = av, dv
|
|||
|
|
}
|
|||
|
|
}
|