Files
tensor/signal/hilbert.go
T

73 lines
2.2 KiB
Go
Raw Normal View History

2026-09-03 10:00:00 +02:00
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package signal
import (
"math"
"sourcedock.dev/petrbalvin/tensor/internal/base"
"sourcedock.dev/petrbalvin/tensor/internal/core"
)
// AnalyticSignal builds the analytic signal z = x + i·H(x) of a real
// series, where H is the Hilbert transform: the negative frequencies
// are removed, the positive ones doubled and the DC and Nyquist bins
// left alone, so z carries the instantaneous amplitude and phase of
// the series. The Fourier definition treats the series as one period,
// so it is exact for an integer number of tones and produces the
// familiar edge swings on an aperiodic series, the same behaviour any
// Fourier-domain filter shows there.
func AnalyticSignal(data *core.Array) (*core.Array, error) {
const name = "AnalyticSignal"
if data.NDim() != 1 {
return nil, base.Errf("%s: the series must be a vector, got shape %s", name, base.ShapeText(data.Shape()))
}
if data.Dtype() == core.Complex {
return nil, base.Errf("%s: complex series are not supported", name)
}
n := data.Len()
if n == 0 {
return nil, base.Errf("%s: the series must not be empty", name)
}
spec, err := FFT(data)
if err != nil {
return nil, base.Errf("%s: %w", name, err)
}
bins := spec.RawComplexes()[:spec.Len()]
half := (n + 1) / 2
for k := range bins {
switch {
case k == 0 || (n%2 == 0 && k == n/2):
// DC and, for even lengths, Nyquist: single-sided bins.
case k < half:
bins[k] *= 2
default:
bins[k] = 0
}
}
out, err := IFFT(spec)
if err != nil {
return nil, base.Errf("%s: %w", name, err)
}
return out, nil
}
// Envelope returns the instantaneous amplitude of a series, the
// modulus of its analytic signal. For a narrowband series this traces
// the curve a peak detector would find, without the smoothing lag.
func Envelope(data *core.Array) (*core.Array, error) {
const name = "Envelope"
z, err := AnalyticSignal(data)
if err != nil {
return nil, base.Errf("%s: %w", name, err)
}
bins := z.RawComplexes()[:z.Len()]
out := core.New(core.Float, z.Len())
vals := out.RawFloats()
for i, c := range bins {
vals[i] = math.Hypot(real(c), imag(c))
}
return out, nil
}