FERS 0.1.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
radar_signal.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-only
2//
3// Copyright (c) 2006-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 radar_signal.cpp
10 * @brief Classes for handling radar waveforms and signals.
11 */
12
13#include "radar_signal.h"
14
15#include <algorithm>
16#include <cmath>
17#include <complex>
18#include <iterator>
19#include <limits>
20#include <stdexcept>
21#include <utility>
22
23#include "core/parameters.h"
24#include "dsp_filters.h"
27
28namespace fers_signal
29{
30 std::string_view fmcwChirpDirectionToken(const FmcwChirpDirection direction) noexcept
31 {
32 return direction == FmcwChirpDirection::Down ? "down" : "up";
33 }
34
36 {
37 if (direction == "up")
38 {
40 }
41 if (direction == "down")
42 {
44 }
45 throw std::runtime_error("Unsupported FMCW chirp direction '" + std::string(direction) + "'.");
46 }
47
48 std::vector<ComplexType> CwSignal::render(const std::vector<interp::InterpPoint>& /*points*/, unsigned& size,
49 const RealType /*fracWinDelay*/) const
50 {
51 size = 0;
52 return {};
53 }
54
55 SteppedFrequencySignal::SteppedFrequencySignal(const RealType start_frequency_offset, const RealType step_size,
56 const std::size_t step_count, const RealType dwell_time,
57 const RealType step_period, std::optional<std::size_t> sweep_count) :
58 _start_frequency_offset(start_frequency_offset), _step_size(step_size), _step_count(step_count),
59 _dwell_time(dwell_time), _step_period(step_period), _sweep_count(sweep_count)
60 {
61 }
62
63 RealType SteppedFrequencySignal::firstFrequency(const RealType carrier_frequency) const noexcept
64 {
65 return carrier_frequency + _start_frequency_offset;
66 }
67
68 RealType SteppedFrequencySignal::lastFrequency(const RealType carrier_frequency) const noexcept
69 {
70 if (_step_count == 0)
71 {
72 return firstFrequency(carrier_frequency);
73 }
74 return firstFrequency(carrier_frequency) + static_cast<RealType>(_step_count - 1U) * _step_size;
75 }
76
78 {
79 if (_step_count == 0)
80 {
81 return 0.0;
82 }
83 return std::abs(static_cast<RealType>(_step_count - 1U) * _step_size);
84 }
85
87 {
88 return static_cast<RealType>(_step_count) * std::abs(_step_size);
89 }
90
92 {
93 if (!_sweep_count.has_value())
94 {
95 return std::nullopt;
96 }
97 return static_cast<RealType>(*_sweep_count) * getSweepPeriod();
98 }
99
100 std::optional<SteppedFrequencySignal::StepState>
102 const RealType carrier_frequency) const noexcept
103 {
104 if (time_since_segment_start < 0.0 || _step_count == 0 || _step_period <= 0.0 || _dwell_time <= 0.0)
105 {
106 return std::nullopt;
107 }
108
109 const RealType sweep_period = getSweepPeriod();
110 const auto sweep_index = static_cast<std::size_t>(std::floor(time_since_segment_start / sweep_period));
111 if (_sweep_count.has_value() && sweep_index >= *_sweep_count)
112 {
113 return std::nullopt;
114 }
115
116 const RealType local_sweep_time = time_since_segment_start - static_cast<RealType>(sweep_index) * sweep_period;
117 const auto step_index = static_cast<std::size_t>(std::floor(local_sweep_time / _step_period));
118 if (step_index >= _step_count)
119 {
120 return std::nullopt;
121 }
122
123 const RealType step_start_time =
124 static_cast<RealType>(sweep_index) * sweep_period + static_cast<RealType>(step_index) * _step_period;
125 const RealType local_step_time = time_since_segment_start - step_start_time;
127 {
128 return std::nullopt;
129 }
130
131 return StepState{.step_index = step_index,
132 .sweep_index = sweep_index,
133 .step_start_time = step_start_time,
134 .dwell_end_time = step_start_time + _dwell_time,
135 .step_end_time = step_start_time + _step_period,
136 .rf_frequency =
137 firstFrequency(carrier_frequency) + static_cast<RealType>(step_index) * _step_size};
138 }
139
140 std::vector<ComplexType> SteppedFrequencySignal::render(const std::vector<interp::InterpPoint>& /*points*/,
141 unsigned& size, const RealType /*fracWinDelay*/) const
142 {
143 size = 0;
144 return {};
145 }
146
147 FmcwChirpSignal::FmcwChirpSignal(const RealType chirp_bandwidth, const RealType chirp_duration,
148 const RealType chirp_period, const RealType start_frequency_offset,
149 std::optional<std::size_t> chirp_count, const FmcwChirpDirection direction) :
150 _chirp_bandwidth(chirp_bandwidth), _chirp_duration(chirp_duration), _chirp_period(chirp_period),
151 _start_frequency_offset(start_frequency_offset), _chirp_count(chirp_count),
152 _chirp_rate(chirp_bandwidth / chirp_duration), _direction(direction)
153 {
154 }
155
156 std::optional<std::size_t>
158 {
159 if (time_since_segment_start < 0.0)
160 {
161 return std::nullopt;
162 }
163
164 const auto chirp_index = static_cast<std::size_t>(std::floor(time_since_segment_start / _chirp_period));
165 if (_chirp_count.has_value() && chirp_index >= *_chirp_count)
166 {
167 return std::nullopt;
168 }
169
170 const RealType chirp_time = time_since_segment_start - static_cast<RealType>(chirp_index) * _chirp_period;
171 // Exact arithmetic cannot make this negative, but floating-point boundary rounding can.
172 if (chirp_time < 0.0 || chirp_time >= _chirp_duration)
173 {
174 return std::nullopt;
175 }
176
177 return chirp_index;
178 }
179
180 std::optional<RealType>
182 {
183 const auto chirp_index = activeChirpIndexAt(time_since_segment_start);
184 if (!chirp_index.has_value())
185 {
186 return std::nullopt;
187 }
188
189 const RealType chirp_time = time_since_segment_start - static_cast<RealType>(*chirp_index) * _chirp_period;
190 return basebandPhaseForChirpTime(chirp_time);
191 }
192
194 {
195 return 2.0 * PI * _start_frequency_offset * chirp_time + PI * getSignedChirpRate() * chirp_time * chirp_time;
196 }
197
198 std::vector<ComplexType> FmcwChirpSignal::render(const std::vector<interp::InterpPoint>& /*points*/, unsigned& size,
199 const RealType /*fracWinDelay*/) const
200 {
201 size = 0;
202 return {};
203 }
204
205 FmcwTriangleSignal::FmcwTriangleSignal(const RealType chirp_bandwidth, const RealType chirp_duration,
206 const RealType start_frequency_offset,
207 std::optional<std::size_t> triangle_count) :
208 _chirp_bandwidth(chirp_bandwidth), _chirp_duration(chirp_duration),
209 _start_frequency_offset(start_frequency_offset), _triangle_count(triangle_count),
210 _chirp_rate(chirp_bandwidth / chirp_duration), _triangle_period(2.0 * chirp_duration),
211 _delta_phi_up(2.0 * PI * start_frequency_offset * chirp_duration +
212 PI * _chirp_rate * chirp_duration * chirp_duration)
213 {
214 }
215
217 {
218 if (triangle_time <= 0.0)
219 {
220 return 0.0;
221 }
222
223 const auto triangle_index = static_cast<std::size_t>(std::floor(triangle_time / _triangle_period));
224 const RealType local_triangle_time = triangle_time - static_cast<RealType>(triangle_index) * _triangle_period;
225 const bool down_leg = local_triangle_time >= _chirp_duration;
226 const RealType u = down_leg ? local_triangle_time - _chirp_duration : local_triangle_time;
227 const RealType phi_base =
228 static_cast<RealType>(triangle_index) * 2.0 * _delta_phi_up + (down_leg ? _delta_phi_up : 0.0);
229 if (!down_leg)
230 {
231 return phi_base + 2.0 * PI * _start_frequency_offset * u + PI * _chirp_rate * u * u;
232 }
233 return phi_base + 2.0 * PI * (_start_frequency_offset + _chirp_bandwidth) * u - PI * _chirp_rate * u * u;
234 }
235
236 std::optional<RealType>
238 {
239 if (time_since_segment_start < 0.0)
240 {
241 return std::nullopt;
242 }
243
244 const auto triangle_index = static_cast<std::size_t>(std::floor(time_since_segment_start / _triangle_period));
245 if (_triangle_count.has_value() && triangle_index >= *_triangle_count)
246 {
247 return std::nullopt;
248 }
249
251 time_since_segment_start - static_cast<RealType>(triangle_index) * _triangle_period;
253 {
254 return std::nullopt;
255 }
256 return basebandPhaseForTriangleTime(time_since_segment_start);
257 }
258
259 std::vector<ComplexType> FmcwTriangleSignal::render(const std::vector<interp::InterpPoint>& /*points*/,
260 unsigned& size, const RealType /*fracWinDelay*/) const
261 {
262 size = 0;
263 return {};
264 }
265
266 RadarSignal::RadarSignal(std::string name, const RealType power, const RealType carrierfreq, const RealType length,
267 std::unique_ptr<Signal> signal, const SimId id) :
268 _name(std::move(name)), _id(id == 0 ? SimIdGenerator::instance().generateId(ObjectType::Waveform) : id),
269 _power(power), _carrierfreq(carrierfreq), _length(length), _signal(std::move(signal))
270 {
271 if (!_signal)
272 {
273 throw std::runtime_error("Signal is empty");
274 }
275 }
276
277 std::vector<ComplexType> RadarSignal::render(const std::vector<interp::InterpPoint>& points, unsigned& size,
278 const RealType fracWinDelay) const
279 {
280 auto data = _signal->render(points, size, fracWinDelay);
281 const RealType scale = std::sqrt(_power);
282
283 std::ranges::for_each(data, [scale](auto& value) { value *= scale; });
284
285 return data;
286 }
287
288 std::vector<ComplexType> RadarSignal::renderSlice(const std::vector<interp::InterpPoint>& points,
290 const std::size_t sampleCount, const RealType fracWinDelay) const
291 {
292 auto data = _signal->renderSlice(points, outputStartTime, outputSampleRate, sampleCount, fracWinDelay);
293 const RealType scale = std::sqrt(_power);
294
295 std::ranges::for_each(data, [scale](auto& value) { value *= scale; });
296
297 return data;
298 }
299
300 bool RadarSignal::isCw() const noexcept { return dynamic_cast<const CwSignal*>(_signal.get()) != nullptr; }
301
303 {
304 return dynamic_cast<const FmcwChirpSignal*>(_signal.get()) != nullptr;
305 }
306
308 {
309 return dynamic_cast<const FmcwTriangleSignal*>(_signal.get()) != nullptr;
310 }
311
312 bool RadarSignal::isFmcwFamily() const noexcept { return _signal->isFmcwFamily(); }
313
315 {
316 return dynamic_cast<const SteppedFrequencySignal*>(_signal.get()) != nullptr;
317 }
318
320 {
321 return dynamic_cast<const FmcwChirpSignal*>(_signal.get());
322 }
323
325 {
326 return dynamic_cast<const FmcwTriangleSignal*>(_signal.get());
327 }
328
330 {
331 return dynamic_cast<const SteppedFrequencySignal*>(_signal.get());
332 }
333
335 {
336 _size = 0;
337 _rate = 0;
338 }
339
340 void Signal::load(std::span<const ComplexType> inData, const unsigned samples, const RealType sampleRate)
341 {
342 clear();
343 const unsigned ratio = params::oversampleRatio();
344 const auto oversampled_samples = static_cast<std::size_t>(samples) * static_cast<std::size_t>(ratio);
345 if (oversampled_samples > std::numeric_limits<unsigned>::max())
346 {
347 throw std::overflow_error("Oversampled signal sample count exceeds unsigned range");
348 }
349 _data.resize(oversampled_samples);
350 _size = static_cast<unsigned>(oversampled_samples);
351 _rate = sampleRate * static_cast<RealType>(ratio);
352
353 if (ratio == 1)
354 {
355 std::ranges::copy(inData, _data.begin());
356 }
357 else
358 {
359 upsample(inData, samples, _data);
360 }
361 }
362
363 std::vector<ComplexType> Signal::render(const std::vector<interp::InterpPoint>& points, unsigned& size,
364 const double fracWinDelay) const
365 {
366 size = _size;
367 if (points.empty())
368 {
369 return std::vector<ComplexType>(_size);
370 }
371 return renderSlice(points, points.front().time, _rate, _size, fracWinDelay);
372 }
373
374 std::vector<ComplexType> Signal::renderSlice(const std::vector<interp::InterpPoint>& points,
376 const std::size_t sampleCount, const RealType fracWinDelay) const
377 {
378 auto out = std::vector<ComplexType>(sampleCount);
379 if (_size == 0 || _rate <= 0.0 || outputSampleRate <= 0.0 || points.empty())
380 {
381 return out;
382 }
383
384 const RealType timestep = 1.0 / outputSampleRate;
385 const int filt_length = static_cast<int>(params::renderFilterLength());
387
388 auto iter = points.begin();
389 auto next = points.size() > 1 ? std::next(iter) : iter;
390 const RealType idelay = std::round(_rate * iter->delay);
392
393 for (std::size_t i = 0; i < sampleCount; ++i)
394 {
395 while (sample_time > next->time && next != iter)
396 {
397 iter = next;
398 if (std::next(next) != points.end())
399 {
400 ++next;
401 }
402 else
403 {
404 break;
405 }
406 }
407
408 auto [amplitude, phase, fdelay, i_sample_unwrap] =
409 calculateWeightsAndDelays(iter, next, sample_time, idelay, fracWinDelay);
410 const RealType native_position = (sample_time - points.front().time) * _rate;
411 const auto source_index = static_cast<int>(std::floor(native_position));
414 {
415 source_fraction = 0.0;
416 }
417
419 const auto delay_unwrap = static_cast<int>(std::floor(combined_delay));
420 fdelay = combined_delay - static_cast<RealType>(delay_unwrap);
422
423 const auto& filt = interp.getFilter(fdelay);
424 const ComplexType accum =
425 performConvolution(source_index, filt.data(), filt_length, amplitude, i_sample_unwrap);
426 out[i] = std::exp(ComplexType(0.0, 1.0) * phase) * accum;
427
429 }
430
431 return out;
432 }
433
434 std::tuple<RealType, RealType, RealType, int>
435 Signal::calculateWeightsAndDelays(const std::vector<interp::InterpPoint>::const_iterator iter,
436 const std::vector<interp::InterpPoint>::const_iterator next,
437 const RealType sampleTime, const RealType idelay,
438 const RealType fracWinDelay) const noexcept
439 {
440 const RealType bw = iter < next ? (sampleTime - iter->time) / (next->time - iter->time) : 0.0;
441
442 const RealType amplitude = std::lerp(std::sqrt(iter->power), std::sqrt(next->power), bw);
443 const RealType phase = std::lerp(iter->phase, next->phase, bw);
444 RealType fdelay = -(std::lerp(iter->delay, next->delay, bw) * _rate - idelay + fracWinDelay);
445
446 const int i_sample_unwrap = static_cast<int>(std::floor(fdelay));
448
449 return {amplitude, phase, fdelay, i_sample_unwrap};
450 }
451
452 ComplexType Signal::performConvolution(const int i, const RealType* filt, const int filtLength,
453 const RealType amplitude, const int iSampleUnwrap) const noexcept
454 {
455 const int start = std::max(-filtLength / 2, -i);
456 const int end = std::min(filtLength / 2, static_cast<int>(_size) - i);
457
458 ComplexType accum(0.0, 0.0);
459
460 for (int j = start; j < end; ++j)
461 {
462 const int sample_idx = i + j + iSampleUnwrap;
463 const int filt_idx = j + filtLength / 2;
464 if (sample_idx >= 0 && sample_idx < static_cast<int>(_size) && filt_idx >= 0 && filt_idx < filtLength)
465 {
466 accum += amplitude * _data[static_cast<std::size_t>(sample_idx)] * filt[filt_idx];
467 }
468 }
469
470 return accum;
471 }
472}
Thread-safe Meyers singleton for generating unique object IDs.
Definition sim_id.h:42
Continuous-wave signal implementation.
std::vector< ComplexType > render(const std::vector< interp::InterpPoint > &points, unsigned &size, RealType fracWinDelay) const override
Renders the signal data.
FMCW linear chirp signal implementation.
std::optional< RealType > instantaneousBasebandPhase(RealType time_since_segment_start) const noexcept
Computes instantaneous baseband phase at a time since segment start.
RealType basebandPhaseForChirpTime(RealType chirp_time) const noexcept
Computes baseband phase for a time inside a chirp.
std::vector< ComplexType > render(const std::vector< interp::InterpPoint > &points, unsigned &size, RealType fracWinDelay) const override
Renders an FMCW waveform from interpolation points.
FmcwChirpSignal(RealType chirp_bandwidth, RealType chirp_duration, RealType chirp_period, RealType start_frequency_offset=0.0, std::optional< std::size_t > chirp_count=std::nullopt, FmcwChirpDirection direction=FmcwChirpDirection::Up)
Constructs an FMCW chirp signal with timing and sweep parameters.
std::optional< std::size_t > activeChirpIndexAt(RealType time_since_segment_start) const noexcept
Returns the active chirp index for a time since the segment start.
FMCW symmetric triangular modulation signal implementation.
RealType basebandPhaseForTriangleTime(RealType triangle_time) const noexcept
Computes baseband phase at a time since the triangle train start.
std::optional< RealType > instantaneousBasebandPhase(RealType time_since_segment_start) const noexcept
Computes instantaneous baseband phase at a time since segment start.
std::vector< ComplexType > render(const std::vector< interp::InterpPoint > &points, unsigned &size, RealType fracWinDelay) const override
Renders an FMCW waveform from interpolation points.
FmcwTriangleSignal(RealType chirp_bandwidth, RealType chirp_duration, RealType start_frequency_offset=0.0, std::optional< std::size_t > triangle_count=std::nullopt)
Constructs an FMCW triangular modulation signal.
const class SteppedFrequencySignal * getSteppedFrequencySignal() const noexcept
Gets the stepped-frequency implementation, if this signal owns one.
RadarSignal(std::string name, RealType power, RealType carrierfreq, RealType length, std::unique_ptr< Signal > signal, const SimId id=0)
Constructs a RadarSignal object.
std::vector< ComplexType > renderSlice(const std::vector< interp::InterpPoint > &points, RealType outputStartTime, RealType outputSampleRate, std::size_t sampleCount, RealType fracWinDelay) const
Renders a bounded absolute-time slice on the requested output grid.
const class FmcwTriangleSignal * getFmcwTriangleSignal() const noexcept
Gets the FMCW triangle implementation, if this signal owns one.
std::vector< ComplexType > render(const std::vector< interp::InterpPoint > &points, unsigned &size, RealType fracWinDelay) const
Renders the radar signal.
bool isFmcwTriangle() const noexcept
Returns true when this signal is an FMCW triangular modulation signal.
bool isFmcwFamily() const noexcept
Returns true when this signal belongs to the FMCW waveform family.
bool isSteppedFrequency() const noexcept
Returns true when this signal is a stepped-frequency CW waveform.
bool isFmcwChirp() const noexcept
Returns true when this signal is an FMCW linear chirp signal.
bool isCw() const noexcept
Returns true when this signal is a continuous-wave signal.
const class FmcwChirpSignal * getFmcwChirpSignal() const noexcept
Gets the FMCW chirp implementation, if this signal owns one.
virtual std::vector< ComplexType > renderSlice(const std::vector< interp::InterpPoint > &points, RealType outputStartTime, RealType outputSampleRate, std::size_t sampleCount, RealType fracWinDelay) const
Renders a bounded absolute-time slice on the requested output grid.
void clear() noexcept
Clears the internal signal data.
void load(std::span< const ComplexType > inData, unsigned samples, RealType sampleRate)
Loads complex radar waveform data.
virtual std::vector< ComplexType > render(const std::vector< interp::InterpPoint > &points, unsigned &size, double fracWinDelay) const
Renders the signal data based on interpolation points.
Stepped-frequency continuous-wave signal implementation.
RealType effectiveBandwidth() const noexcept
Gets DFT-convention effective bandwidth in hertz.
std::optional< StepState > activeStepAt(RealType time_since_segment_start, RealType carrier_frequency) const noexcept
Returns the active step for a time since the segment start.
std::optional< RealType > totalDuration() const noexcept
Gets finite waveform duration, if sweep_count is configured.
RealType getSweepPeriod() const noexcept
Gets full sweep period in seconds.
RealType lastFrequency(RealType carrier_frequency) const noexcept
Gets final-step RF frequency in hertz.
RealType frequencySpan() const noexcept
Gets first-to-last absolute span in hertz.
RealType firstFrequency(RealType carrier_frequency) const noexcept
Gets first-step RF frequency in hertz.
SteppedFrequencySignal(RealType start_frequency_offset, RealType step_size, std::size_t step_count, RealType dwell_time, RealType step_period, std::optional< std::size_t > sweep_count=std::nullopt)
Constructs a uniform stepped-frequency CW signal.
std::vector< ComplexType > render(const std::vector< interp::InterpPoint > &points, unsigned &size, RealType fracWinDelay) const override
Renders the signal data. For SFCW signals, this is a no-op.
static InterpFilter & getInstance() noexcept
Retrieves the singleton instance of the InterpFilter class.
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
Header file for Digital Signal Processing (DSP) filters and upsampling/downsampling functionality.
Interpolation filter implementation using Kaiser windowing.
Defines a structure to store interpolation point data for signal processing.
FmcwChirpDirection parseFmcwChirpDirection(const std::string_view direction)
Parses a schema chirp direction token.
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::string_view fmcwChirpDirectionToken(const FmcwChirpDirection direction) noexcept
Converts a chirp direction to the schema token.
FmcwChirpDirection
Sweep direction for a linear FMCW chirp.
@ Down
Instantaneous baseband frequency decreases over the chirp.
@ Up
Instantaneous baseband frequency increases over the chirp.
unsigned oversampleRatio() noexcept
Get the oversampling ratio.
Definition parameters.h:151
unsigned renderFilterLength() noexcept
Get the render filter length.
Definition parameters.h:139
Defines the Parameters struct and provides methods for managing simulation parameters.
Classes for handling radar waveforms and signals.
uint64_t SimId
64-bit Unique Simulation ID.
Definition sim_id.h:18
ObjectType
Categorizes objects for ID generation.
Definition sim_id.h:25
math::Vec3 max
Active SFCW dwell selected for one local waveform time.
std::size_t step_index
Zero-based step index inside a sweep.