FERS 0.1.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
dsp_filters.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-only
2//
3// Copyright (c) 2007-2008 Marc Brooker and Michael Inggs
4// Copyright (c) 2008-present FERS Contributors (see AUTHORS.md).
5//
6// See the GNU GPLv2 LICENSE file in the FERS project root for more information.
7
8/**
9 * @file dsp_filters.cpp
10 * @brief Implementation file for Digital Signal Processing (DSP) filters and upsampling/downsampling functionality.
11 */
12
13#include "dsp_filters.h"
14
15#include <algorithm>
16#include <array>
17#include <cmath>
18#include <complex>
19#include <cstddef>
20#include <numeric>
21#include <ranges>
22#include <set>
23#include <stdexcept>
24
25#include "core/logging.h"
26#include "core/parameters.h"
27
28constexpr RealType BLACKMAN_A0 = 0.42;
29constexpr RealType BLACKMAN_A1 = 0.5;
30constexpr RealType BLACKMAN_A2 = 0.08;
31
32namespace
33{
34 /**
35 * @brief Sinc function for FIR filter design.
36 *
37 * @param x Input value.
38 * @return Sinc value at x.
39 */
40 constexpr RealType sinc(const RealType x) noexcept { return x == 0 ? 1.0 : std::sin(x * PI) / (x * PI); }
41
42 /**
43 * @brief Stores a generated Blackman-windowed FIR design and its coefficient sum.
44 *
45 * The coefficient sum approximates the interpolator DC gain and is used to detect
46 * under-specified oversampling configurations.
47 */
48 struct BlackmanFirDesign
49 {
50 std::vector<RealType> coeffs;
51 RealType coeff_sum = 0.0;
52 };
53
54 /**
55 * @brief Enforces production oversampling limits for the fixed-length FIR design.
56 *
57 * @param ratio Requested oversampling ratio.
58 * @throws std::runtime_error if the configured ratio is unsupported.
59 */
60 void validateOversamplingConfig(const unsigned ratio) { params::validateOversampleRatio(ratio); }
61
62 /**
63 * @brief Emits a one-time warning when the FIR tap budget is too small for the ratio.
64 *
65 * @param ratio Oversampling ratio used to design the FIR.
66 * @param filter_length Configured half-length of the production FIR kernel.
67 * @param coeff_sum Sum of the generated FIR coefficients.
68 */
69 void warnIfUnderspecified(const unsigned ratio, const unsigned filter_length, const RealType coeff_sum)
70 {
71 if (ratio <= 1)
72 {
73 return;
74 }
75
76 const RealType relative_error =
77 std::abs(coeff_sum - static_cast<RealType>(ratio)) / static_cast<RealType>(ratio);
78 if (relative_error <= 0.01)
79 {
80 return;
81 }
82
83 // TODO: warning deduplication via static set has data race potential
84 static std::set<std::pair<unsigned, unsigned>> warned_pairs;
85 const auto [_, inserted] = warned_pairs.emplace(ratio, filter_length);
86 if (!inserted)
87 {
88 return;
89 }
90
91 const RealType dc_gain = coeff_sum / static_cast<RealType>(ratio);
93 LOG(logging::Level::WARNING, "Oversampling FIR under-spec'd for ratio=", ratio,
94 ", filter_length=", filter_length, ", fir_sum=", coeff_sum, ", estimated_roundtrip_gain=", roundtrip_gain,
95 ". Practical envelope roughly filter_length >= 4 * ratio.");
96 }
97
98 /**
99 * @brief Generates a Blackman-windowed low-pass FIR for the oversampling stage.
100 *
101 * @param cutoff Normalized cutoff frequency for the anti-imaging / anti-alias filter.
102 * @param ratio Oversampling ratio associated with this design.
103 * @param filter_length Configured half-length of the FIR kernel.
104 * @return FIR coefficients plus their sum for DC-gain auditing.
105 */
106 BlackmanFirDesign blackmanFir(const RealType cutoff, const unsigned ratio, const unsigned filter_length)
107 {
108 const unsigned filt_length = filter_length * 2;
109 std::vector<RealType> coeffs(filt_length);
110 const RealType n = filt_length / 2.0;
111 const RealType pi_n = PI / n;
112
113 // We use the Blackman window, for a suitable tradeoff between rolloff and stopband attenuation
114 // Equivalent Kaiser beta = 7.04 (Oppenhiem and Schaffer, Hamming)
115 std::ranges::for_each(coeffs,
116 [cutoff, n, pi_n, i = 0u](RealType& coeff) mutable
117 {
118 const RealType sinc_val = sinc(cutoff * (i - n));
119 const RealType window = BLACKMAN_A0 - BLACKMAN_A1 * std::cos(pi_n * i) +
120 BLACKMAN_A2 * std::cos(2 * pi_n * i);
122 ++i;
123 });
124
125 BlackmanFirDesign design{std::move(coeffs)};
126 design.coeff_sum = std::accumulate(design.coeffs.begin(), design.coeffs.end(), 0.0);
127 warnIfUnderspecified(ratio, filter_length, design.coeff_sum);
128 return design;
129 }
130}
131
132namespace fers_signal
133{
134 /**
135 * @brief Upsamples a complex waveform with zero-stuffing followed by Blackman FIR filtering.
136 *
137 * The production path uses a fixed-length single-stage FIR, so unsupported ratios
138 * fail fast before filtering begins.
139 *
140 * @param in Input span of base-rate complex samples.
141 * @param size Number of input samples to process from @p in.
142 * @param out Output span receiving @p size * oversampleRatio() filtered samples.
143 * @throws std::runtime_error if the configured oversampling ratio is unsupported.
144 */
145 void upsample(const std::span<const ComplexType> in, const unsigned size, std::span<ComplexType> out)
146 {
147 const unsigned ratio = params::oversampleRatio();
148 // TODO: this would be better as a multirate upsampler
149 // This implementation is functional but suboptimal.
150 // Users requiring higher accuracy should oversample outside FERS until this is addressed.
152 const unsigned filter_length = params::renderFilterLength();
153 const auto design = blackmanFir(1 / static_cast<RealType>(ratio), ratio, filter_length);
154 const auto filt_length = static_cast<unsigned>(design.coeffs.size());
155
156 const auto oversampled_size = static_cast<std::size_t>(size) * static_cast<std::size_t>(ratio);
157 std::vector tmp(oversampled_size + static_cast<std::size_t>(filt_length), ComplexType{0.0, 0.0});
158
159 for (unsigned i = 0; i < size; ++i)
160 {
161 tmp[static_cast<std::size_t>(i) * static_cast<std::size_t>(ratio)] = in[i];
162 }
163
164 const FirFilter filt(design.coeffs);
165 filt.filter(tmp);
166
167 const auto delay = static_cast<std::size_t>(filt_length / 2);
168 const std::span<const ComplexType> filtered_output(tmp.data() + delay, oversampled_size);
169 std::ranges::copy(filtered_output, out.begin());
170 }
171
172 /**
173 * @brief Low-pass filters and decimates an oversampled complex waveform back to base rate.
174 *
175 * The same fixed-length FIR design is reused for anti-alias filtering, and unsupported
176 * ratios fail fast before filtering begins.
177 *
178 * @param in Input span of oversampled complex samples.
179 * @return Base-rate complex samples truncated to floor(in.size() / oversampleRatio()).
180 * @throws std::invalid_argument if @p in is empty.
181 * @throws std::runtime_error if the configured oversampling ratio is unsupported.
182 */
183 std::vector<ComplexType> downsample(std::span<const ComplexType> in)
184 {
185 if (in.empty())
186 {
187 throw std::invalid_argument("Input span is empty in Downsample");
188 }
189
190 const unsigned ratio = params::oversampleRatio();
191 // TODO: Replace with a more efficient multirate downsampling implementation.
193 const unsigned filter_length = params::renderFilterLength();
194 const auto design = blackmanFir(1 / static_cast<RealType>(ratio), ratio, filter_length);
195 const auto filt_length = design.coeffs.size();
196
197 std::vector tmp(in.size() + filt_length, ComplexType{0, 0});
198
199 std::ranges::copy(in, tmp.begin());
200
201 const FirFilter filt(design.coeffs);
202 filt.filter(tmp);
203
204 const auto downsampled_size = in.size() / ratio;
205 std::vector<ComplexType> out(downsampled_size);
206 const auto filter_delay = filt_length / 2;
207 for (std::size_t i = 0; i < downsampled_size; ++i)
208 {
209 const auto source_index = i * static_cast<std::size_t>(ratio) + filter_delay;
210 out[i] = tmp[source_index] / static_cast<RealType>(ratio);
211 }
212
213 return out;
214 }
215
216 DownsamplingSink::DownsamplingSink() : _ratio(params::oversampleRatio())
217 {
219 if (_ratio <= 1)
220 {
221 return;
222 }
223
224 const unsigned filter_length = params::renderFilterLength();
225 const auto design = blackmanFir(1 / static_cast<RealType>(_ratio), _ratio, filter_length);
226 _coeffs = design.coeffs;
227 _line.assign(_coeffs.size(), ComplexType{0.0, 0.0});
228 _delay = _coeffs.size() / 2u;
229 }
230
231 void DownsamplingSink::consume(const std::span<const ComplexType> block)
232 {
233 if (_finished)
234 {
235 throw std::logic_error("Cannot consume downsampler input after finish");
236 }
237 if (block.empty())
238 {
239 return;
240 }
241
242 if (_ratio <= 1)
243 {
244 _pending.insert(_pending.end(), block.begin(), block.end());
245 _input_sample_count += block.size();
246 _processed_sample_count += block.size();
247 _next_output_index += block.size();
248 return;
249 }
250
251 for (const auto& sample : block)
252 {
253 const auto filtered = processOne(sample);
254 ++_input_sample_count;
255 maybeEmit(filtered);
256 ++_processed_sample_count;
257 }
258 }
259
261 {
262 if (_finished)
263 {
264 return;
265 }
266 _finished = true;
267 if (_ratio <= 1)
268 {
269 return;
270 }
271
272 _target_output_count = _input_sample_count / _ratio;
273 while (_next_output_index < _target_output_count)
274 {
275 const auto filtered = processOne(ComplexType{0.0, 0.0});
276 maybeEmit(filtered);
277 ++_processed_sample_count;
278 }
279 }
280
281 std::vector<ComplexType> DownsamplingSink::takeOutput()
282 {
283 std::vector<ComplexType> out;
284 out.swap(_pending);
285 return out;
286 }
287
289 {
290 std::ranges::fill(_line, ComplexType{0.0, 0.0});
291 _pending.clear();
292 _input_sample_count = 0;
293 _processed_sample_count = 0;
294 _next_output_index = 0;
295 _target_output_count = 0;
296 _finished = false;
297 }
298
299 ComplexType DownsamplingSink::processOne(const ComplexType sample)
300 {
301 _line[0] = sample;
302 const auto filtered = std::transform_reduce(
303 _line.begin(), _line.end(), _coeffs.begin(), ComplexType{0.0, 0.0}, std::plus<ComplexType>{},
304 [](const ComplexType& value, const RealType coeff) { return value * coeff; });
305 if (_line.size() > 1u)
306 {
307 std::move_backward(_line.begin(), _line.end() - 1, _line.end());
308 }
309 return filtered;
310 }
311
312 void DownsamplingSink::maybeEmit(const ComplexType filtered)
313 {
314 const auto required_input_index = _next_output_index * _ratio + static_cast<std::uint64_t>(_delay);
315 if (_processed_sample_count == required_input_index)
316 {
317 _pending.push_back(filtered / static_cast<RealType>(_ratio));
318 ++_next_output_index;
319 }
320 }
321
323 _a(denCoeffs, denCoeffs + order), _b(numCoeffs, numCoeffs + order), _w(order, 0.0), _order(order)
324 {
325 }
326
327 RealType IirFilter::filter(const RealType sample) noexcept
328 {
329 std::ranges::rotate(_w, _w.end() - 1);
330
331 _w[0] = sample;
332
333 for (unsigned j = 1; j < _order; ++j)
334 {
335 _w[0] -= _a[j] * _w[j];
336 }
337
338 return std::inner_product(_b.begin(), _b.end(), _w.begin(), 0.0);
339 }
340
341 void IirFilter::filter(std::span<RealType> samples) noexcept
342 {
343 for (auto& sample : samples)
344 {
345 std::ranges::rotate(_w, _w.end() - 1);
346
347 _w[0] = sample;
348
349 for (unsigned j = 1; j < _order; ++j)
350 {
351 _w[0] -= _a[j] * _w[j];
352 }
353
354 sample = std::inner_product(_b.begin(), _b.end(), _w.begin(), 0.0);
355 }
356 }
357
358 void FirFilter::filter(std::vector<ComplexType>& samples) const
359 {
360 std::vector line(_order, ComplexType{0.0, 0.0});
361
362 for (auto& sample : samples)
363 {
364 line[0] = sample;
365 ComplexType result{0.0, 0.0};
366
367 result = std::transform_reduce(line.begin(), line.end(), _filter.begin(), ComplexType{0.0, 0.0},
368 std::plus<ComplexType>{},
369 [](const ComplexType& x, const RealType coeff) { return x * coeff; });
370
371 sample = result;
372
373 std::ranges::rotate(std::views::reverse(line), line.rbegin() + 1);
374 }
375 }
376
378 {
379 /// 11th order elliptic lowpass at 0.1fs
380 constexpr std::array den_coeffs = {1.0,
381 -10.301102119865,
382 48.5214567642597,
383 -137.934509572412,
384 262.914952985445,
385 -352.788381841481,
386 340.027874008585,
387 -235.39260470286,
388 114.698499845697,
389 -37.4634653062448,
390 7.38208765922137,
391 -0.664807695826097};
392
393 constexpr std::array num_coeffs = {2.7301694322809e-06, -1.8508123430239e-05, 5.75739466753894e-05,
394 -0.000104348734423658, 0.000111949190289715, -4.9384188225528e-05,
395 -4.9384188225522e-05, 0.00011194919028971, -0.000104348734423656,
396 5.75739466753884e-05, -1.85081234302388e-05, 2.73016943228086e-06};
397
398 _filter = std::make_unique<IirFilter>(den_coeffs.data(), num_coeffs.data(), den_coeffs.size());
399 }
400
401 void DecadeUpsampler::upsample(const RealType sample, std::span<RealType> out) const
402 {
403 if (out.size() != 10)
404 {
405 throw std::invalid_argument("Output span must have a size of 10.");
406 }
407 out[0] = sample;
408 std::fill(out.begin() + 1, out.end(), 0);
409 _filter->filter(out);
410 }
411}
void upsample(RealType sample, std::span< RealType > out) const
Upsamples a single sample.
std::vector< ComplexType > takeOutput()
void consume(std::span< const ComplexType > block)
Implements a Finite Impulse Response (FIR) filter.
RealType filter(RealType) override
Filters a single real-valued sample; FIR scalar filtering is unsupported.
IirFilter(const RealType *denCoeffs, const RealType *numCoeffs, unsigned order) noexcept
Constructs an IIR filter with given numerator and denominator coefficients and order.
RealType filter(RealType sample) noexcept override
Filters a single sample.
double RealType
Type for real numbers.
Definition config.h:27
std::complex< RealType > ComplexType
Type for complex numbers.
Definition config.h:35
constexpr RealType PI
Mathematical constant π (pi).
Definition config.h:43
constexpr RealType BLACKMAN_A2
constexpr RealType BLACKMAN_A1
constexpr RealType BLACKMAN_A0
Header file for Digital Signal Processing (DSP) filters and upsampling/downsampling functionality.
Header file for the logging system.
#define LOG(level,...)
Definition logging.h:19
void upsample(const std::span< const ComplexType > in, const unsigned size, std::span< ComplexType > out)
Upsamples a complex waveform with zero-stuffing followed by Blackman FIR filtering.
std::vector< ComplexType > downsample(std::span< const ComplexType > in)
Low-pass filters and decimates an oversampled complex waveform back to base rate.
@ WARNING
Warning level for potentially harmful situations.
unsigned oversampleRatio() noexcept
Get the oversampling ratio.
Definition parameters.h:151
unsigned renderFilterLength() noexcept
Get the render filter length.
Definition parameters.h:139
void validateOversampleRatio(const unsigned ratio)
Validates that an oversampling ratio is supported.
Definition parameters.h:164
Defines the Parameters struct and provides methods for managing simulation parameters.
math::Vec3 max