Skip to content

Commit d3ba051

Browse files
Merge branch 'main' into feature/add-goertzel-algo
2 parents 94d82b3 + ffd17bb commit d3ba051

10 files changed

Lines changed: 562 additions & 182 deletions

File tree

ROADMAP.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ Difficulty legend:
2828
| # | Component | Target module | Difficulty |
2929
|----|------------------------------------------------------|---------------------------|------------|
3030
| 10 | Gain-scheduled controller | `controllers` | ★★☆☆☆ |
31-
| 11 | Convolution & correlation utilities | `analysis` | ★★☆☆☆ |
3231
| 12 | Polynomial least-squares curve fitting | `estimators/offline` | ★★☆☆☆ |
3332
| 14 | CIC (Cascaded Integrator-Comb) filter | `filters/passive` | ★★☆☆☆ |
3433
| 15 | Biquad / Second-Order-Section cascade | `filters/passive` | ★★★☆☆ |
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
# Convolution & Correlation
2+
3+
## Overview & Motivation
4+
5+
Two signals interacting in time — one being filtered, delayed, or matched against another — are
6+
described by convolution and correlation. Convolution is the time-domain action of any linear
7+
time-invariant (LTI) filter: given an input signal and a system's impulse response, their
8+
convolution is the output. Correlation measures the similarity between two signals as a function
9+
of relative shift (lag), making it the foundation of matched filtering, pulse detection, and
10+
time-delay estimation.
11+
12+
Both operations are indispensable in embedded DSP: they underlie FIR filtering, pitch detection,
13+
radar/sonar processing, and auto-regressive modeling. Bounded, no-heap implementations make
14+
these algorithms practical on microcontrollers.
15+
16+
## Mathematical Theory
17+
18+
### Linear Convolution
19+
20+
For finite sequences $x$ of length $M$ and $h$ of length $K$, the linear convolution $y = x * h$
21+
is a sequence of length $M + K - 1$ defined by:
22+
23+
$$y[n] = \sum_{k=0}^{M-1} x[k] \cdot h[n - k], \quad n = 0, \ldots, M+K-2$$
24+
25+
where $h[j] = 0$ for $j < 0$ or $j \geq K$.
26+
27+
### Circular Convolution
28+
29+
For two sequences of equal length $N$, circular (periodic) convolution wraps indices modulo $N$:
30+
31+
$$y[n] = \sum_{k=0}^{N-1} x[k] \cdot h[(n - k) \bmod N], \quad n = 0, \ldots, N-1$$
32+
33+
Circular convolution equals linear convolution only when both operands are zero-padded to at least
34+
$M + K - 1$ samples. Without padding, time-aliasing occurs.
35+
36+
### Cross-Correlation
37+
38+
Cross-correlation of $x$ (length $M$) and $y$ (length $K$) is defined as:
39+
40+
$$r_{xy}[\ell] = \sum_{n} x[n] \cdot y[n - \ell]$$
41+
42+
This equals linear convolution of $x$ with the time-reversed $y$:
43+
44+
$$r_{xy} = x * \overline{y}$$
45+
46+
### Auto-Correlation
47+
48+
Auto-correlation is cross-correlation of a signal with itself:
49+
50+
$$r_{xx}[\ell] = \sum_{n} x[n] \cdot x[n - \ell]$$
51+
52+
It is always symmetric about the zero-lag index, and its maximum occurs at zero lag.
53+
Dividing by $r_{xx}[0]$ yields a normalized coefficient in $[-1, 1]$.
54+
55+
### Convolution Theorem (Fast Path)
56+
57+
For long signals, the convolution theorem enables an $O(L \log L)$ computation:
58+
59+
$$x * h = \mathcal{F}^{-1}\!\left(\mathcal{F}(x) \cdot \mathcal{F}(h)\right)$$
60+
61+
where $\mathcal{F}$ denotes the DFT and $L$ is the next power of two $\geq M + K - 1$. The
62+
pointwise complex product replaces the $O(MK)$ direct sum.
63+
64+
## Complexity Analysis
65+
66+
| Operation | Time | Space (extra) | Notes |
67+
|----------------------|---------------|---------------|---------------------------------------|
68+
| Linear convolution | $O(MK)$ | $O(1)$ | Writes into caller-owned buffer |
69+
| Circular convolution | $O(N^2)$ | $O(1)$ | Same as linear for equal-length input |
70+
| Cross-correlation | $O(MK)$ | $O(K)$ | Reverses one operand on the stack |
71+
| Auto-correlation | $O(M^2)$ | $O(1)$ | Alias of cross-correlation |
72+
| Fast convolution | $O(L \log L)$ | $O(L)$ | $L = 2^{\lceil\log_2(M+K-1)\rceil}$ |
73+
74+
The fast path is beneficial when $MK > L \log_2 L$, roughly when both operands exceed 32–64
75+
samples.
76+
77+
## Step-by-Step Walkthrough
78+
79+
Linear convolution of $x = [1, 2, 3]$ and $h = [1, 1]$ (output length $= 4$):
80+
81+
| $n$ | Active $k$ range | Computation | $y[n]$ |
82+
|-----|------------------|-------------------------|--------|
83+
| 0 | $k=0$ | $1 \cdot 1$ | 1 |
84+
| 1 | $k=0,1$ | $2 \cdot 1 + 1 \cdot 1$ | 3 |
85+
| 2 | $k=1,2$ | $3 \cdot 1 + 2 \cdot 1$ | 5 |
86+
| 3 | $k=2$ | $3 \cdot 1$ | 3 |
87+
88+
Result: $[1, 3, 5, 3]$.
89+
90+
Auto-correlation of $[1, 1, 1, 1]$ produces the triangular sequence $[1, 2, 3, 4, 3, 2, 1]$,
91+
with the peak at the zero-lag center index (index 3 in 0-based notation).
92+
93+
## Pitfalls & Edge Cases
94+
95+
- **Time-aliasing**: Circular convolution of length $N$ equals linear convolution only when both
96+
operands are zero-padded to $M + K - 1 \leq N$. Failing to pad causes energy from the tail to
97+
wrap around and corrupt the beginning of the output.
98+
- **Lag estimation**: `ArgMaxLag` returns the index of the maximum in the cross-correlation
99+
output. For cross-correlation of two length-$M$ sequences the zero-lag corresponds to index
100+
$M - 1$; a delay of $d$ samples appears at index $M - 1 + d$.
101+
- **Normalization**: The raw auto-correlation at zero lag equals the signal energy. Dividing the
102+
entire sequence by $r[0]$ gives a coefficient bounded in $[-1, 1]$.
103+
- **Float precision**: Accumulating many products in single precision can lose significance for
104+
very long sequences. The MAC loop uses the native float accumulator; for sequences longer than
105+
a few hundred samples, extended precision may be warranted.
106+
107+
## Variants & Generalizations
108+
109+
- **Normalized cross-correlation**: Divide $r_{xy}[\ell]$ by $\sqrt{r_{xx}[0] \cdot r_{yy}[0]}$
110+
for an amplitude-invariant similarity measure.
111+
- **Overlap-add / overlap-save**: Partition long signals into short blocks and convolve each
112+
with $h$ via the FFT fast path, then reassemble. Reduces latency for streaming applications.
113+
- **Partial correlation**: Compute cross-correlation over a sliding window rather than the full
114+
signal length — useful for non-stationary signals.
115+
116+
## Applications
117+
118+
- **FIR filtering**: Direct-form FIR is linear convolution of the input with the filter's impulse
119+
response.
120+
- **Matched filtering**: Cross-correlate the received signal with a known template to detect it
121+
in noise.
122+
- **Time-delay estimation (TDOA)**: The lag of the cross-correlation peak between two sensors
123+
gives the inter-sensor arrival time difference.
124+
- **Pitch detection**: Auto-correlation of a speech frame reveals the fundamental period as the
125+
first large secondary peak.
126+
- **AR modeling**: Yule-Walker equations use auto-correlation values to fit auto-regressive
127+
parameters.
128+
129+
## Connections to Other Algorithms
130+
131+
- **FastFourierTransform**: The fast convolution path is a direct application of the FFT-based
132+
convolution theorem.
133+
- **GoertzelAlgorithm**: A single-frequency DFT bin — cross-correlating with a complex
134+
exponential at one frequency is equivalent.
135+
- **YuleWalker**: Constructs the AR system matrix from auto-correlation lags computed by
136+
`AutoCorrelation`.
137+
- **PowerSpectralDensity**: The power spectrum is the DFT of the auto-correlation sequence
138+
(Wiener–Khinchin theorem).
139+
140+
## References & Further Reading
141+
142+
- A. V. Oppenheim, R. W. Schafer, *Discrete-Time Signal Processing*, 3rd ed., Prentice Hall,
143+
2009, Chapters 2 & 8.
144+
- A. V. Oppenheim, A. S. Willsky, *Signals and Systems*, 2nd ed., Prentice Hall, 1997,
145+
Chapter 9.

numerical/analysis/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ target_link_libraries(numerical.analysis ${NUMERICAL_VISIBILITY}
1212
)
1313

1414
target_sources(numerical.analysis PRIVATE
15+
ConvolutionCorrelation.hpp
1516
DiscreteCosineTransform.hpp
1617
FastFourierTransform.hpp
1718
FastFourierTransformRadix2Impl.hpp
@@ -21,6 +22,7 @@ target_sources(numerical.analysis PRIVATE
2122
)
2223

2324
numerical_add_coverage_sources(numerical.analysis
25+
ConvolutionCorrelation.cpp
2426
FastFourierTransformRadix2Impl.cpp
2527
GoertzelAlgorithm.cpp
2628
PowerDensitySpectrum.cpp
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#include "numerical/analysis/ConvolutionCorrelation.hpp"
2+
3+
namespace analysis
4+
{
5+
template void LinearConvolution<float, 3, 3>(
6+
const infra::BoundedVector<float>::WithMaxSize<3>&,
7+
const infra::BoundedVector<float>::WithMaxSize<3>&,
8+
infra::BoundedVector<float>::WithMaxSize<5>&);
9+
10+
template void CircularConvolution<float, 4>(
11+
const infra::BoundedVector<float>::WithMaxSize<4>&,
12+
const infra::BoundedVector<float>::WithMaxSize<4>&,
13+
infra::BoundedVector<float>::WithMaxSize<4>&);
14+
15+
template void CrossCorrelation<float, 5, 5>(
16+
const infra::BoundedVector<float>::WithMaxSize<5>&,
17+
const infra::BoundedVector<float>::WithMaxSize<5>&,
18+
infra::BoundedVector<float>::WithMaxSize<9>&);
19+
20+
template void AutoCorrelation<float, 4>(
21+
const infra::BoundedVector<float>::WithMaxSize<4>&,
22+
infra::BoundedVector<float>::WithMaxSize<7>&);
23+
24+
template std::size_t ArgMaxLag<float, 9>(
25+
const infra::BoundedVector<float>::WithMaxSize<9>&);
26+
27+
template void FastConvolution<float, 3, 3, 8>(
28+
const infra::BoundedVector<float>::WithMaxSize<3>&,
29+
const infra::BoundedVector<float>::WithMaxSize<3>&,
30+
infra::BoundedVector<float>::WithMaxSize<5>&,
31+
FastFourierTransform<float>&);
32+
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
#pragma once
2+
3+
#if defined(__GNUC__) || defined(__clang__)
4+
#pragma GCC optimize("O3", "fast-math")
5+
#endif
6+
7+
#include "numerical/analysis/FastFourierTransform.hpp"
8+
#include "numerical/math/CompilerOptimizations.hpp"
9+
#include "numerical/math/ComplexNumber.hpp"
10+
#include <cstddef>
11+
#include <infra/util/BoundedVector.hpp>
12+
#include <type_traits>
13+
14+
namespace analysis
15+
{
16+
template<typename T, std::size_t M, std::size_t K>
17+
OPTIMIZE_FOR_SPEED void LinearConvolution(
18+
const typename infra::BoundedVector<T>::template WithMaxSize<M>& x,
19+
const typename infra::BoundedVector<T>::template WithMaxSize<K>& h,
20+
typename infra::BoundedVector<T>::template WithMaxSize<M + K - 1>& y)
21+
{
22+
static_assert(std::is_floating_point_v<T>, "ConvolutionCorrelation supports floating-point types only");
23+
y.clear();
24+
y.resize(M + K - 1, T{ 0 });
25+
for (std::size_t n = 0; n < M + K - 1; ++n)
26+
{
27+
T acc{ 0 };
28+
std::size_t kStart{ n >= K - 1 ? n - (K - 1) : 0 };
29+
std::size_t kEnd{ n < M - 1 ? n : M - 1 };
30+
for (std::size_t k = kStart; k <= kEnd; ++k)
31+
acc += x[k] * h[n - k];
32+
y[n] = acc;
33+
}
34+
}
35+
36+
template<typename T, std::size_t N>
37+
OPTIMIZE_FOR_SPEED void CircularConvolution(
38+
const typename infra::BoundedVector<T>::template WithMaxSize<N>& x,
39+
const typename infra::BoundedVector<T>::template WithMaxSize<N>& h,
40+
typename infra::BoundedVector<T>::template WithMaxSize<N>& y)
41+
{
42+
static_assert(std::is_floating_point_v<T>, "ConvolutionCorrelation supports floating-point types only");
43+
y.clear();
44+
y.resize(N, T{ 0 });
45+
for (std::size_t n = 0; n < N; ++n)
46+
{
47+
T acc{ 0 };
48+
for (std::size_t k = 0; k < N; ++k)
49+
acc += x[k] * h[(n + N - k) % N];
50+
y[n] = acc;
51+
}
52+
}
53+
54+
template<typename T, std::size_t M, std::size_t K>
55+
OPTIMIZE_FOR_SPEED void CrossCorrelation(
56+
const typename infra::BoundedVector<T>::template WithMaxSize<M>& x,
57+
const typename infra::BoundedVector<T>::template WithMaxSize<K>& y,
58+
typename infra::BoundedVector<T>::template WithMaxSize<M + K - 1>& r)
59+
{
60+
static_assert(std::is_floating_point_v<T>, "ConvolutionCorrelation supports floating-point types only");
61+
typename infra::BoundedVector<T>::template WithMaxSize<K> yRev;
62+
yRev.resize(K, T{ 0 });
63+
for (std::size_t i = 0; i < K; ++i)
64+
yRev[i] = y[K - 1 - i];
65+
LinearConvolution<T, M, K>(x, yRev, r);
66+
}
67+
68+
template<typename T, std::size_t M>
69+
void AutoCorrelation(
70+
const typename infra::BoundedVector<T>::template WithMaxSize<M>& x,
71+
typename infra::BoundedVector<T>::template WithMaxSize<2 * M - 1>& r)
72+
{
73+
static_assert(std::is_floating_point_v<T>, "ConvolutionCorrelation supports floating-point types only");
74+
CrossCorrelation<T, M, M>(x, x, r);
75+
}
76+
77+
template<typename T, std::size_t N>
78+
std::size_t ArgMaxLag(const typename infra::BoundedVector<T>::template WithMaxSize<N>& r)
79+
{
80+
static_assert(std::is_floating_point_v<T>, "ConvolutionCorrelation supports floating-point types only");
81+
std::size_t best{ 0 };
82+
for (std::size_t i = 1; i < N; ++i)
83+
if (r[i] > r[best])
84+
best = i;
85+
return best;
86+
}
87+
88+
template<typename T, std::size_t M, std::size_t K, std::size_t L>
89+
void FastConvolution(
90+
const typename infra::BoundedVector<T>::template WithMaxSize<M>& x,
91+
const typename infra::BoundedVector<T>::template WithMaxSize<K>& h,
92+
typename infra::BoundedVector<T>::template WithMaxSize<M + K - 1>& y,
93+
FastFourierTransform<T>& fft)
94+
{
95+
static_assert(std::is_floating_point_v<T>, "ConvolutionCorrelation supports floating-point types only");
96+
static_assert((L & (L - 1)) == 0, "L must be a power of two");
97+
static_assert(L >= M + K - 1, "L must be at least M+K-1");
98+
99+
typename infra::BoundedVector<T>::template WithMaxSize<L> xPad;
100+
typename infra::BoundedVector<T>::template WithMaxSize<L> hPad;
101+
xPad.resize(L, T{ 0 });
102+
hPad.resize(L, T{ 0 });
103+
104+
for (std::size_t i = 0; i < M; ++i)
105+
xPad[i] = x[i];
106+
for (std::size_t i = 0; i < K; ++i)
107+
hPad[i] = h[i];
108+
109+
typename infra::BoundedVector<math::Complex<T>>::template WithMaxSize<L> X;
110+
X.resize(L, math::Complex<T>{ T{ 0 }, T{ 0 } });
111+
{
112+
auto& rawX = fft.Forward(xPad);
113+
for (std::size_t i = 0; i < L; ++i)
114+
X[i] = rawX[i];
115+
}
116+
auto& H = fft.Forward(hPad);
117+
118+
typename infra::BoundedVector<math::Complex<T>>::template WithMaxSize<L> Y;
119+
Y.resize(L, math::Complex<T>{ T{ 0 }, T{ 0 } });
120+
for (std::size_t i = 0; i < L; ++i)
121+
Y[i] = X[i] * H[i];
122+
123+
auto& yFull = fft.Inverse(Y);
124+
125+
y.clear();
126+
y.resize(M + K - 1, T{ 0 });
127+
for (std::size_t i = 0; i < M + K - 1; ++i)
128+
y[i] = yFull[i];
129+
}
130+
}
131+
132+
#ifdef NUMERICAL_TOOLBOX_COVERAGE_BUILD
133+
namespace analysis
134+
{
135+
extern template void LinearConvolution<float, 3, 3>(
136+
const infra::BoundedVector<float>::WithMaxSize<3>&,
137+
const infra::BoundedVector<float>::WithMaxSize<3>&,
138+
infra::BoundedVector<float>::WithMaxSize<5>&);
139+
140+
extern template void CircularConvolution<float, 4>(
141+
const infra::BoundedVector<float>::WithMaxSize<4>&,
142+
const infra::BoundedVector<float>::WithMaxSize<4>&,
143+
infra::BoundedVector<float>::WithMaxSize<4>&);
144+
145+
extern template void CrossCorrelation<float, 5, 5>(
146+
const infra::BoundedVector<float>::WithMaxSize<5>&,
147+
const infra::BoundedVector<float>::WithMaxSize<5>&,
148+
infra::BoundedVector<float>::WithMaxSize<9>&);
149+
150+
extern template void AutoCorrelation<float, 4>(
151+
const infra::BoundedVector<float>::WithMaxSize<4>&,
152+
infra::BoundedVector<float>::WithMaxSize<7>&);
153+
154+
extern template std::size_t ArgMaxLag<float, 9>(
155+
const infra::BoundedVector<float>::WithMaxSize<9>&);
156+
157+
extern template void FastConvolution<float, 3, 3, 8>(
158+
const infra::BoundedVector<float>::WithMaxSize<3>&,
159+
const infra::BoundedVector<float>::WithMaxSize<3>&,
160+
infra::BoundedVector<float>::WithMaxSize<5>&,
161+
FastFourierTransform<float>&);
162+
}
163+
#endif

numerical/analysis/test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ target_link_libraries(numerical.analysis_test PUBLIC
99
)
1010

1111
target_sources(numerical.analysis_test PRIVATE
12+
TestConvolutionCorrelation.cpp
1213
TestDiscreteCosineTransform.cpp
1314
TestFastFourierTransformRadix2Impl.cpp
1415
TestGoertzelAlgorithm.cpp

0 commit comments

Comments
 (0)