Skip to content

Commit 5a3ebff

Browse files
add hilbert transform
1 parent 7d5f1ee commit 5a3ebff

12 files changed

Lines changed: 505 additions & 191 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Refer to the documentation to quickly integrate and utilize the library's signal
1616

1717
| Category | Description |
1818
|--------------------------------------------------------------------|----------------------------------------------------------------------|
19-
| [Analysis](doc/analysis/README.md) | FFT, Real-Input FFT (RFFT), Power Spectral Density, DCT, Window Functions, Signal Detectors, Convolution & Correlation, Goertzel Algorithm, Decibels |
19+
| [Analysis](doc/analysis/README.md) | FFT, Real-Input FFT (RFFT), Power Spectral Density, DCT, Window Functions, Signal Detectors, Convolution & Correlation, Goertzel Algorithm, Decibels, Hilbert Transform / Analytic Signal |
2020
| [Control Analysis](doc/control_analysis/README.md) | Frequency Response, Root Locus, Controllability/Observability Matrices & Gramians, Continuous-to-Discrete, Transfer Function ↔ State Space |
2121
| [Controllers](doc/controllers/README.md) | Bang-Bang/Hysteresis, PID, LQR, LQI (Integral/Servo State Feedback), MPC, Saturation, Rate Limiter, Slew-Limited Saturation, Feedforward/2-DOF, Gain-Scheduled Controller, Lead-Lag Compensator, Luenberger Observer |
2222
| [Dynamics](doc/dynamics/README.md) | Euler-Lagrange, Newton-Euler, Recursive Newton-Euler, ABA |

ROADMAP.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ Difficulty legend:
2929
|----|------------------------------------------------------|---------------------------|------------|
3030
| 35 | Disturbance Observer (DOB) | `robust_control` (new) | ★★★★☆ |
3131
| 36 | Active Disturbance Rejection Control (ADRC + ESO) | `robust_control` (new) | ★★★★☆ |
32-
| 37 | Hilbert transform / analytic signal / envelope | `analysis` | ★★★★☆ |
3332
| 38 | Discrete Wavelet Transform (Haar / Daubechies) | `analysis` | ★★★★☆ |
3433
| 39 | Square-root / Information Kalman filter | `filters/active` | ★★★★☆ |
3534
| 40 | Feedback linearization | `nonlinear_control` (new) | ★★★★☆ |

doc/analysis/HilbertTransform.md

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# Hilbert Transform / Analytic Signal
2+
3+
## Overview & Motivation
4+
5+
Every real-valued signal contains both positive and negative frequency components that carry redundant information. The Hilbert transform discards the negative half and pairs the original signal with a version shifted by exactly −90° at every frequency, producing a **complex analytic signal** whose magnitude tracks instantaneous amplitude and whose angle tracks instantaneous phase.
6+
7+
This is the standard technique for AM/DSB demodulation, vibration envelope detection, bearing-fault analysis, single-sideband modulation, and instantaneous-frequency measurement — all tasks where the underlying amplitude or phase varies slowly relative to a carrier and must be tracked in real time.
8+
9+
## Mathematical Theory
10+
11+
### Hilbert Transform
12+
13+
For a real signal $x(t)$ the Hilbert transform is the convolution
14+
15+
$$\mathcal{H}\{x\}(t) = \frac{1}{\pi} \, \text{p.v.} \int_{-\infty}^{\infty} \frac{x(\tau)}{t - \tau} \, d\tau$$
16+
17+
which is equivalently a multiplication of each frequency component by $-j \operatorname{sgn}(f)$, i.e. a $-90°$ phase rotation for positive frequencies and $+90°$ for negative.
18+
19+
### Analytic Signal
20+
21+
The analytic signal is
22+
23+
$$z(t) = x(t) + j\,\mathcal{H}\{x\}(t)$$
24+
25+
Its one-sided spectrum satisfies $Z(f) = 0$ for $f < 0$, $Z(0) = X(0)$, and $Z(f) = 2X(f)$ for $f > 0$.
26+
27+
### Instantaneous Attributes
28+
29+
| Quantity | Formula |
30+
|----------|---------|
31+
| Amplitude (envelope) | $A(t) = |z(t)| = \sqrt{x^2 + \mathcal{H}^2\{x\}}$ |
32+
| Phase | $\phi(t) = \arg z(t) = \operatorname{atan2}(\mathcal{H}\{x\}, x)$ |
33+
| Frequency | $f_i(t) = \frac{1}{2\pi}\frac{d\phi}{dt}$ (phase unwrapped before differencing) |
34+
35+
### FFT Method (Marple)
36+
37+
Given the $N$-point DFT $X[k]$ of a real sequence, the analytic signal is recovered by:
38+
39+
$$H[k] = \begin{cases} X[0] & k = 0 \\ 2X[k] & 1 \le k < N/2 \\ X[N/2] & k = N/2 \\ 0 & N/2 < k < N \end{cases}$$
40+
41+
followed by the inverse DFT of $H$.
42+
43+
### FIR Approximation
44+
45+
A Type III/IV antisymmetric FIR filter with impulse response
46+
47+
$$h[k] = \frac{2}{\pi k} w[k], \quad k \text{ odd}; \quad h[k] = 0, \quad k \text{ even}$$
48+
49+
(with a Hamming window $w[k]$) approximates the ideal $-90°$ phase shift across its passband. The delayed original signal (center-tap copy) is paired with the filtered output to form the approximate analytic signal.
50+
51+
## Complexity Analysis
52+
53+
| Method | Time per call | Space | Notes |
54+
|--------|--------------|-------|-------|
55+
| FFT (block) | $O(N \log N)$ | $2N$ complex words | Exact, latency $N$ |
56+
| FIR (streaming) | $O(P)$ per sample | $P$ words state | Approx., latency $(P-1)/2$ |
57+
| Feature extraction | $O(1)$ | None | atan2, sqrt, subtract |
58+
59+
$P$ = number of FIR taps.
60+
61+
## Step-by-Step Walkthrough
62+
63+
**FFT method on $N = 8$, input $x = \cos(2\pi n / 8)$:**
64+
65+
1. Forward DFT yields $X[1] = 4$, $X[7] = 4$, all others zero.
66+
2. Apply one-sided weighting: $H[1] = 8$, $H[7] = 0$.
67+
3. Inverse DFT of $H$ produces the imaginary part $\sin(2\pi n / 8)$.
68+
4. Analytic signal: $z[n] = \cos(2\pi n/8) + j\sin(2\pi n/8)$, amplitude $\equiv 1$.
69+
70+
## Pitfalls & Edge Cases
71+
72+
- **DC and Nyquist bins** — their imaginary parts must remain zero; the one-sided formula preserves this.
73+
- **Phase unwrapping** — before computing instantaneous frequency, the phase difference must be mapped to $(-\pi, \pi]$ to suppress $2\pi$ jumps.
74+
- **Block-edge artefacts** — the FFT method treats the block as periodic; trim the first and last few samples when asserting accuracy.
75+
- **FIR group delay** — the FIR output is delayed by $(P-1)/2$ samples relative to the input; align before comparing to the FFT method.
76+
- **FIR bandwidth** — the FIR Hilbert approximation degrades near DC and Nyquist; use only over the filter's flat passband.
77+
78+
## Variants & Generalizations
79+
80+
- **Block FFT method** — exact, no ripple, requires a complete block; suitable for offline or buffered processing.
81+
- **FIR streaming method** — causal, constant memory, approximate; length and window choice trade accuracy against latency.
82+
- **Quadrature-oscillator method** — for narrowband signals, a simple IIR resonator can approximate the 90° shift with even lower cost.
83+
84+
## Applications
85+
86+
- AM/DSB envelope demodulation and AGC.
87+
- Vibration and bearing-fault analysis (envelope of resonance band).
88+
- Single-sideband (SSB) radio modulation/demodulation.
89+
- Instantaneous frequency tracking in FM receivers and Doppler radar.
90+
- Phase and frequency estimation in PLLs.
91+
92+
## Connections to Other Algorithms
93+
94+
- Uses `FastFourierTransform` / `FastFourierTransformRadix2Impl` as the block back-end.
95+
- `RealFastFourierTransform` is an alternative back-end for purely real inputs.
96+
- `ConvolutionCorrelation` provides the FIR convolution primitive used by the streaming path.
97+
- `SignalDetectors` (RMS envelope, peak hold) offers cheaper non-coherent envelope estimation.
98+
99+
## References & Further Reading
100+
101+
- S. L. Marple, "Computing the Discrete-Time Analytic Signal via FFT," *IEEE Transactions on Signal Processing*, 47(9), 2600–2603, 1999.
102+
- A. V. Oppenheim and R. W. Schafer, *Discrete-Time Signal Processing*, 3rd ed., Prentice Hall, Ch. 12.
103+
- S. W. Smith, *The Scientist and Engineer's Guide to Digital Signal Processing*, Ch. 9 (available free online).

doc/analysis/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Signal analysis algorithms for frequency-domain decomposition and spectral estim
1313
| [Signal Detectors](SignalDetectors.md) | Peak hold, zero-crossing counter, and RMS envelope detectors for real-time signal monitoring |
1414
| [Decibels](Decibels.md) | `ToDecibels` / `FromDecibels` conversion helpers with zero-floor guard, plus attenuation and ripple utilities |
1515
| [Goertzel Algorithm](GoertzelAlgorithm.md) | Single-bin DFT via a second-order recurrence for O(N) tone detection with O(1) memory |
16+
| [Hilbert Transform](HilbertTransform.md) | Analytic signal and instantaneous amplitude/phase/frequency via FFT one-sided spectrum or FIR approximation |
1617

1718
## Sub-domains
1819

numerical/analysis/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ target_sources(numerical.analysis PRIVATE
1818
FastFourierTransform.hpp
1919
FastFourierTransformRadix2Impl.hpp
2020
GoertzelAlgorithm.hpp
21+
HilbertTransform.hpp
2122
PowerDensitySpectrum.hpp
2223
RealFastFourierTransform.hpp
2324
SignalDetectors.hpp
@@ -28,6 +29,7 @@ numerical_add_coverage_sources(numerical.analysis
2829
DiscreteCosineTransform.cpp
2930
FastFourierTransformRadix2Impl.cpp
3031
GoertzelAlgorithm.cpp
32+
HilbertTransform.cpp
3133
PowerDensitySpectrum.cpp
3234
RealFastFourierTransform.cpp
3335
SignalDetectors.cpp
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#include "numerical/analysis/HilbertTransform.hpp"
2+
3+
namespace analysis
4+
{
5+
template class AnalyticSignalFft<float, 64>;
6+
template class HilbertFir<float, 31>;
7+
}
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
#pragma once
2+
3+
#if defined(__GNUC__) || defined(__clang__)
4+
#pragma GCC optimize("O3", "fast-math")
5+
#endif
6+
7+
#include "infra/util/BoundedVector.hpp"
8+
#include "numerical/analysis/FastFourierTransform.hpp"
9+
#include "numerical/math/CompilerOptimizations.hpp"
10+
#include "numerical/math/ComplexNumber.hpp"
11+
#include <array>
12+
#include <cmath>
13+
#include <numbers>
14+
#include <type_traits>
15+
16+
namespace analysis
17+
{
18+
template<typename T, std::size_t N>
19+
class AnalyticSignalFft
20+
{
21+
static_assert(std::is_floating_point_v<T>, "AnalyticSignalFft supports floating-point types");
22+
static_assert(N >= 4 && (N & (N - 1)) == 0, "AnalyticSignalFft length N must be a power of two >= 4");
23+
24+
public:
25+
using Complex = math::Complex<T>;
26+
using VectorComplex = infra::BoundedVector<Complex>;
27+
using VectorReal = infra::BoundedVector<T>;
28+
29+
explicit AnalyticSignalFft(FastFourierTransform<T>& fft);
30+
31+
OPTIMIZE_FOR_SPEED VectorComplex& Analytic(VectorReal& x);
32+
33+
static T InstantaneousAmplitude(Complex a);
34+
static T InstantaneousPhase(Complex a);
35+
static T InstantaneousFrequency(T phaseNow, T phasePrev, T ts);
36+
37+
private:
38+
FastFourierTransform<T>& fft;
39+
typename VectorComplex::template WithMaxSize<N> spectrum;
40+
typename VectorComplex::template WithMaxSize<N> analytic;
41+
};
42+
43+
template<typename T, std::size_t Taps>
44+
class HilbertFir
45+
{
46+
static_assert(std::is_floating_point_v<T>, "HilbertFir supports floating-point types");
47+
static_assert(Taps >= 3 && (Taps % 2) == 1, "HilbertFir requires an odd number of taps >= 3");
48+
49+
public:
50+
using Complex = math::Complex<T>;
51+
52+
HilbertFir();
53+
54+
OPTIMIZE_FOR_SPEED Complex Filter(T x);
55+
56+
private:
57+
static constexpr std::size_t centerTap{ (Taps - 1) / 2 };
58+
59+
std::array<T, Taps> coeff{};
60+
std::array<T, Taps> delay{};
61+
std::size_t writeIndex{ 0 };
62+
};
63+
64+
/// AnalyticSignalFft implementation ///
65+
66+
template<typename T, std::size_t N>
67+
AnalyticSignalFft<T, N>::AnalyticSignalFft(FastFourierTransform<T>& fft)
68+
: fft{ fft }
69+
{
70+
spectrum.resize(N);
71+
analytic.resize(N);
72+
}
73+
74+
template<typename T, std::size_t N>
75+
OPTIMIZE_FOR_SPEED typename AnalyticSignalFft<T, N>::VectorComplex& AnalyticSignalFft<T, N>::Analytic(VectorReal& x)
76+
{
77+
VectorComplex& X{ fft.Forward(x) };
78+
79+
spectrum[0] = X[0];
80+
spectrum[N / 2] = X[N / 2];
81+
82+
for (std::size_t k{ 1 }; k < N / 2; ++k)
83+
spectrum[k] = Complex{ T(2) * X[k].Real(), T(2) * X[k].Imaginary() };
84+
85+
for (std::size_t k{ N / 2 + 1 }; k < N; ++k)
86+
spectrum[k] = Complex{ T(0), T(0) };
87+
88+
typename VectorComplex::template WithMaxSize<N> hilbertSpectrum{};
89+
hilbertSpectrum.resize(N);
90+
for (std::size_t k{ 0 }; k < N; ++k)
91+
hilbertSpectrum[k] = Complex{ spectrum[k].Imaginary(), -spectrum[k].Real() };
92+
93+
VectorReal& hilbertTd{ fft.Inverse(hilbertSpectrum) };
94+
95+
for (std::size_t n{ 0 }; n < N; ++n)
96+
analytic[n] = Complex{ x[n], hilbertTd[n] };
97+
98+
return analytic;
99+
}
100+
101+
template<typename T, std::size_t N>
102+
T AnalyticSignalFft<T, N>::InstantaneousAmplitude(Complex a)
103+
{
104+
return std::sqrt(a.Real() * a.Real() + a.Imaginary() * a.Imaginary());
105+
}
106+
107+
template<typename T, std::size_t N>
108+
T AnalyticSignalFft<T, N>::InstantaneousPhase(Complex a)
109+
{
110+
return std::atan2(a.Imaginary(), a.Real());
111+
}
112+
113+
template<typename T, std::size_t N>
114+
T AnalyticSignalFft<T, N>::InstantaneousFrequency(T phaseNow, T phasePrev, T ts)
115+
{
116+
T dphi{ phaseNow - phasePrev };
117+
while (dphi > std::numbers::pi_v<T>)
118+
dphi -= T(2) * std::numbers::pi_v<T>;
119+
while (dphi < -std::numbers::pi_v<T>)
120+
dphi += T(2) * std::numbers::pi_v<T>;
121+
return dphi / (T(2) * std::numbers::pi_v<T> * ts);
122+
}
123+
124+
/// HilbertFir implementation ///
125+
126+
template<typename T, std::size_t Taps>
127+
HilbertFir<T, Taps>::HilbertFir()
128+
{
129+
coeff.fill(T(0));
130+
delay.fill(T(0));
131+
for (std::size_t i{ 0 }; i < Taps; ++i)
132+
{
133+
int k{ static_cast<int>(i) - static_cast<int>(centerTap) };
134+
if (k != 0 && (k % 2) != 0)
135+
{
136+
T w{ T(0.54) - T(0.46) * std::cos(T(2) * std::numbers::pi_v<T> * static_cast<T>(i) / static_cast<T>(Taps - 1)) };
137+
coeff[i] = (T(2) / (std::numbers::pi_v<T> * static_cast<T>(k))) * w;
138+
}
139+
}
140+
}
141+
142+
template<typename T, std::size_t Taps>
143+
OPTIMIZE_FOR_SPEED typename HilbertFir<T, Taps>::Complex HilbertFir<T, Taps>::Filter(T x)
144+
{
145+
delay[writeIndex] = x;
146+
147+
T imag{ T(0) };
148+
for (std::size_t i{ 0 }; i < Taps; ++i)
149+
{
150+
std::size_t idx{ (writeIndex + Taps - i) % Taps };
151+
imag += coeff[i] * delay[idx];
152+
}
153+
154+
std::size_t realIdx{ (writeIndex + Taps - centerTap) % Taps };
155+
T real{ delay[realIdx] };
156+
157+
writeIndex = (writeIndex + 1) % Taps;
158+
159+
return Complex{ real, imag };
160+
}
161+
162+
#ifdef NUMERICAL_TOOLBOX_COVERAGE_BUILD
163+
extern template class AnalyticSignalFft<float, 64>;
164+
extern template class HilbertFir<float, 31>;
165+
#endif
166+
}

numerical/analysis/test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ target_sources(numerical.analysis_test PRIVATE
1414
TestDiscreteCosineTransform.cpp
1515
TestFastFourierTransformRadix2Impl.cpp
1616
TestGoertzelAlgorithm.cpp
17+
TestHilbertTransform.cpp
1718
TestPowerDensitySpectrum.cpp
1819
TestRealFastFourierTransform.cpp
1920
TestSignalDetectors.cpp

0 commit comments

Comments
 (0)