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 FmcwChirpSignal::FmcwChirpSignal(const RealType chirp_bandwidth, const RealType chirp_duration,
56 const RealType chirp_period, const RealType start_frequency_offset,
57 std::optional<std::size_t> chirp_count, const FmcwChirpDirection direction) :
58 _chirp_bandwidth(chirp_bandwidth), _chirp_duration(chirp_duration), _chirp_period(chirp_period),
59 _start_frequency_offset(start_frequency_offset), _chirp_count(chirp_count),
60 _chirp_rate(chirp_bandwidth / chirp_duration), _direction(direction)
61 {
62 }
63
64 std::optional<std::size_t>
66 {
68 {
69 return std::nullopt;
70 }
71
72 const auto chirp_index = static_cast<std::size_t>(std::floor(time_since_segment_start / _chirp_period));
73 if (_chirp_count.has_value() && chirp_index >= *_chirp_count)
74 {
75 return std::nullopt;
76 }
77
78 const RealType chirp_time = time_since_segment_start - static_cast<RealType>(chirp_index) * _chirp_period;
79 // Exact arithmetic cannot make this negative, but floating-point boundary rounding can.
80 if (chirp_time < 0.0 || chirp_time >= _chirp_duration)
81 {
82 return std::nullopt;
83 }
84
85 return chirp_index;
86 }
87
88 std::optional<RealType>
90 {
91 const auto chirp_index = activeChirpIndexAt(time_since_segment_start);
92 if (!chirp_index.has_value())
93 {
94 return std::nullopt;
95 }
96
97 const RealType chirp_time = time_since_segment_start - static_cast<RealType>(*chirp_index) * _chirp_period;
98 return basebandPhaseForChirpTime(chirp_time);
99 }
100
102 {
103 return 2.0 * PI * _start_frequency_offset * chirp_time + PI * getSignedChirpRate() * chirp_time * chirp_time;
104 }
105
106 std::vector<ComplexType> FmcwChirpSignal::render(const std::vector<interp::InterpPoint>& /*points*/, unsigned& size,
107 const RealType /*fracWinDelay*/) const
108 {
109 size = 0;
110 return {};
111 }
112
113 FmcwTriangleSignal::FmcwTriangleSignal(const RealType chirp_bandwidth, const RealType chirp_duration,
114 const RealType start_frequency_offset,
115 std::optional<std::size_t> triangle_count) :
116 _chirp_bandwidth(chirp_bandwidth), _chirp_duration(chirp_duration),
117 _start_frequency_offset(start_frequency_offset), _triangle_count(triangle_count),
118 _chirp_rate(chirp_bandwidth / chirp_duration), _triangle_period(2.0 * chirp_duration),
119 _delta_phi_up(2.0 * PI * start_frequency_offset * chirp_duration +
120 PI * _chirp_rate * chirp_duration * chirp_duration)
121 {
122 }
123
125 {
126 if (triangle_time <= 0.0)
127 {
128 return 0.0;
129 }
130
131 const auto triangle_index = static_cast<std::size_t>(std::floor(triangle_time / _triangle_period));
132 const RealType local_triangle_time = triangle_time - static_cast<RealType>(triangle_index) * _triangle_period;
133 const bool down_leg = local_triangle_time >= _chirp_duration;
134 const RealType u = down_leg ? local_triangle_time - _chirp_duration : local_triangle_time;
135 const RealType phi_base =
136 static_cast<RealType>(triangle_index) * 2.0 * _delta_phi_up + (down_leg ? _delta_phi_up : 0.0);
137 if (!down_leg)
138 {
139 return phi_base + 2.0 * PI * _start_frequency_offset * u + PI * _chirp_rate * u * u;
140 }
141 return phi_base + 2.0 * PI * (_start_frequency_offset + _chirp_bandwidth) * u - PI * _chirp_rate * u * u;
142 }
143
144 std::optional<RealType>
146 {
147 if (time_since_segment_start < 0.0)
148 {
149 return std::nullopt;
150 }
151
152 const auto triangle_index = static_cast<std::size_t>(std::floor(time_since_segment_start / _triangle_period));
153 if (_triangle_count.has_value() && triangle_index >= *_triangle_count)
154 {
155 return std::nullopt;
156 }
157
159 time_since_segment_start - static_cast<RealType>(triangle_index) * _triangle_period;
161 {
162 return std::nullopt;
163 }
164 return basebandPhaseForTriangleTime(time_since_segment_start);
165 }
166
167 std::vector<ComplexType> FmcwTriangleSignal::render(const std::vector<interp::InterpPoint>& /*points*/,
168 unsigned& size, const RealType /*fracWinDelay*/) const
169 {
170 size = 0;
171 return {};
172 }
173
174 RadarSignal::RadarSignal(std::string name, const RealType power, const RealType carrierfreq, const RealType length,
175 std::unique_ptr<Signal> signal, const SimId id) :
176 _name(std::move(name)), _id(id == 0 ? SimIdGenerator::instance().generateId(ObjectType::Waveform) : id),
177 _power(power), _carrierfreq(carrierfreq), _length(length), _signal(std::move(signal))
178 {
179 if (!_signal)
180 {
181 throw std::runtime_error("Signal is empty");
182 }
183 }
184
185 std::vector<ComplexType> RadarSignal::render(const std::vector<interp::InterpPoint>& points, unsigned& size,
186 const RealType fracWinDelay) const
187 {
188 auto data = _signal->render(points, size, fracWinDelay);
189 const RealType scale = std::sqrt(_power);
190
191 std::ranges::for_each(data, [scale](auto& value) { value *= scale; });
192
193 return data;
194 }
195
196 std::vector<ComplexType> RadarSignal::renderSlice(const std::vector<interp::InterpPoint>& points,
198 const std::size_t sampleCount, const RealType fracWinDelay) const
199 {
200 auto data = _signal->renderSlice(points, outputStartTime, outputSampleRate, sampleCount, fracWinDelay);
201 const RealType scale = std::sqrt(_power);
202
203 std::ranges::for_each(data, [scale](auto& value) { value *= scale; });
204
205 return data;
206 }
207
208 bool RadarSignal::isCw() const noexcept { return dynamic_cast<const CwSignal*>(_signal.get()) != nullptr; }
209
211 {
212 return dynamic_cast<const FmcwChirpSignal*>(_signal.get()) != nullptr;
213 }
214
216 {
217 return dynamic_cast<const FmcwTriangleSignal*>(_signal.get()) != nullptr;
218 }
219
220 bool RadarSignal::isFmcwFamily() const noexcept { return _signal->isFmcwFamily(); }
221
223 {
224 return dynamic_cast<const FmcwChirpSignal*>(_signal.get());
225 }
226
228 {
229 return dynamic_cast<const FmcwTriangleSignal*>(_signal.get());
230 }
231
233 {
234 _size = 0;
235 _rate = 0;
236 }
237
238 void Signal::load(std::span<const ComplexType> inData, const unsigned samples, const RealType sampleRate)
239 {
240 clear();
241 const unsigned ratio = params::oversampleRatio();
242 const auto oversampled_samples = static_cast<std::size_t>(samples) * static_cast<std::size_t>(ratio);
243 if (oversampled_samples > std::numeric_limits<unsigned>::max())
244 {
245 throw std::overflow_error("Oversampled signal sample count exceeds unsigned range");
246 }
247 _data.resize(oversampled_samples);
248 _size = static_cast<unsigned>(oversampled_samples);
249 _rate = sampleRate * static_cast<RealType>(ratio);
250
251 if (ratio == 1)
252 {
253 std::ranges::copy(inData, _data.begin());
254 }
255 else
256 {
257 upsample(inData, samples, _data);
258 }
259 }
260
261 std::vector<ComplexType> Signal::render(const std::vector<interp::InterpPoint>& points, unsigned& size,
262 const double fracWinDelay) const
263 {
264 size = _size;
265 if (points.empty())
266 {
267 return std::vector<ComplexType>(_size);
268 }
269 return renderSlice(points, points.front().time, _rate, _size, fracWinDelay);
270 }
271
272 std::vector<ComplexType> Signal::renderSlice(const std::vector<interp::InterpPoint>& points,
274 const std::size_t sampleCount, const RealType fracWinDelay) const
275 {
276 auto out = std::vector<ComplexType>(sampleCount);
277 if (_size == 0 || _rate <= 0.0 || outputSampleRate <= 0.0 || points.empty())
278 {
279 return out;
280 }
281
282 const RealType timestep = 1.0 / outputSampleRate;
283 const int filt_length = static_cast<int>(params::renderFilterLength());
285
286 auto iter = points.begin();
287 auto next = points.size() > 1 ? std::next(iter) : iter;
288 const RealType idelay = std::round(_rate * iter->delay);
290
291 for (std::size_t i = 0; i < sampleCount; ++i)
292 {
293 while (sample_time > next->time && next != iter)
294 {
295 iter = next;
296 if (std::next(next) != points.end())
297 {
298 ++next;
299 }
300 else
301 {
302 break;
303 }
304 }
305
306 auto [amplitude, phase, fdelay, i_sample_unwrap] =
307 calculateWeightsAndDelays(iter, next, sample_time, idelay, fracWinDelay);
308 const RealType native_position = (sample_time - points.front().time) * _rate;
309 const auto source_index = static_cast<int>(std::floor(native_position));
312 {
313 source_fraction = 0.0;
314 }
315
317 const auto delay_unwrap = static_cast<int>(std::floor(combined_delay));
318 fdelay = combined_delay - static_cast<RealType>(delay_unwrap);
320
321 const auto& filt = interp.getFilter(fdelay);
322 const ComplexType accum =
323 performConvolution(source_index, filt.data(), filt_length, amplitude, i_sample_unwrap);
324 out[i] = std::exp(ComplexType(0.0, 1.0) * phase) * accum;
325
327 }
328
329 return out;
330 }
331
332 std::tuple<RealType, RealType, RealType, int>
333 Signal::calculateWeightsAndDelays(const std::vector<interp::InterpPoint>::const_iterator iter,
334 const std::vector<interp::InterpPoint>::const_iterator next,
335 const RealType sampleTime, const RealType idelay,
336 const RealType fracWinDelay) const noexcept
337 {
338 const RealType bw = iter < next ? (sampleTime - iter->time) / (next->time - iter->time) : 0.0;
339
340 const RealType amplitude = std::lerp(std::sqrt(iter->power), std::sqrt(next->power), bw);
341 const RealType phase = std::lerp(iter->phase, next->phase, bw);
342 RealType fdelay = -(std::lerp(iter->delay, next->delay, bw) * _rate - idelay + fracWinDelay);
343
344 const int i_sample_unwrap = static_cast<int>(std::floor(fdelay));
346
347 return {amplitude, phase, fdelay, i_sample_unwrap};
348 }
349
350 ComplexType Signal::performConvolution(const int i, const RealType* filt, const int filtLength,
351 const RealType amplitude, const int iSampleUnwrap) const noexcept
352 {
353 const int start = std::max(-filtLength / 2, -i);
354 const int end = std::min(filtLength / 2, static_cast<int>(_size) - i);
355
356 ComplexType accum(0.0, 0.0);
357
358 for (int j = start; j < end; ++j)
359 {
360 const int sample_idx = i + j + iSampleUnwrap;
361 const int filt_idx = j + filtLength / 2;
362 if (sample_idx >= 0 && sample_idx < static_cast<int>(_size) && filt_idx >= 0 && filt_idx < filtLength)
363 {
364 accum += amplitude * _data[static_cast<std::size_t>(sample_idx)] * filt[filt_idx];
365 }
366 }
367
368 return accum;
369 }
370}
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.
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 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.
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