73 lines
2.2 KiB
Go
73 lines
2.2 KiB
Go
// 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
|
||
|
|
}
|