// Copyright (c) 2026 Petr Balvín (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 }