FERS 0.1.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
receiver.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 receiver.cpp
10 * @brief Implementation of the Receiver class.
11 */
12
13#include "receiver.h"
14
15#include <algorithm>
16#include <cmath>
17#include <cstdint>
18#include <stdexcept>
19#include <utility>
20
21#include "core/parameters.h"
22#include "serial/response.h"
23
24namespace radar
25{
26 namespace
27 {
28 [[nodiscard]] std::size_t ceilSampleIndexAtOrAfter(const RealType time, const RealType sample_rate)
29 {
30 if (sample_rate <= 0.0 || time <= params::startTime())
31 {
32 return 0;
33 }
34 const RealType raw_index = (time - params::startTime()) * sample_rate;
35 const RealType nearest = std::round(raw_index);
36 const RealType tolerance = 1.0e-12 * std::max<RealType>(1.0, std::abs(nearest));
37 if (std::abs(raw_index - nearest) <= tolerance)
38 {
39 return static_cast<std::size_t>(nearest);
40 }
41 return static_cast<std::size_t>(std::ceil(raw_index));
42 }
43 }
44
45 std::string_view dechirpModeToken(const Receiver::DechirpMode mode) noexcept
46 {
47 switch (mode)
48 {
50 return "physical";
52 return "ideal";
54 return "none";
55 }
56 return "none";
57 }
58
60 {
61 if (token == "none")
62 {
64 }
65 if (token == "physical")
66 {
68 }
69 if (token == "ideal")
70 {
72 }
73 throw std::runtime_error("Unsupported FMCW dechirp_mode '" + std::string(token) + "'.");
74 }
75
76 std::string_view dechirpReferenceSourceToken(const Receiver::DechirpReferenceSource source) noexcept
77 {
78 switch (source)
79 {
81 return "attached";
83 return "transmitter";
85 return "custom";
87 return "none";
88 }
89 return "none";
90 }
91
93 {
94 if (token == "attached")
95 {
97 }
98 if (token == "transmitter")
99 {
101 }
102 if (token == "custom")
103 {
105 }
106 throw std::runtime_error("Unsupported dechirp_reference source '" + std::string(token) + "'.");
107 }
108
109 Receiver::Receiver(Platform* platform, std::string name, const unsigned seed, const OperationMode mode,
110 const SimId id) noexcept :
111 Radar(platform, std::move(name), id == 0 ? SimIdGenerator::instance().generateId(ObjectType::Receiver) : id),
112 _mode(mode), _rng(seed)
113 {
114 }
115
116 void Receiver::addResponseToInbox(std::unique_ptr<serial::Response> response) noexcept
117 {
118 std::scoped_lock const lock(_inbox_mutex);
119 _inbox.push_back(std::move(response));
120 }
121
122 void Receiver::addInterferenceToLog(std::unique_ptr<serial::Response> response) noexcept
123 {
124 std::scoped_lock const lock(_interference_log_mutex);
125 _pulsed_interference_log.push_back(std::move(response));
126 }
127
129 {
130 std::scoped_lock const lock(_interference_log_mutex);
131 std::erase_if(_pulsed_interference_log,
132 [cutoff_time](const auto& response) { return response && response->endTime() <= cutoff_time; });
133 }
134
135 std::vector<std::unique_ptr<serial::Response>> Receiver::drainInbox() noexcept
136 {
137 std::scoped_lock const lock(_inbox_mutex);
138 std::vector<std::unique_ptr<serial::Response>> drained_responses;
139 drained_responses.swap(_inbox);
140 return drained_responses;
141 }
142
144 {
145 {
146 std::scoped_lock const lock(_finalizer_queue_mutex);
147 _finalizer_queue.push(std::move(job));
148 }
149 _finalizer_queue_cv.notify_one();
150 }
151
153 {
154 std::unique_lock lock(_finalizer_queue_mutex);
155 _finalizer_queue_cv.wait(lock, [this] { return !_finalizer_queue.empty(); });
156
157 job = std::move(_finalizer_queue.front());
158 _finalizer_queue.pop();
159
160 const bool is_shutdown_job = job.duration < 0.0;
161 return !is_shutdown_job;
162 }
163
165 {
166 return _noise_temperature + Radar::getNoiseTemperature(angle);
167 }
168
170 {
171 if (temp < -EPSILON)
172 {
173 LOG(logging::Level::FATAL, "Noise temperature for receiver {} is negative", getName());
174 throw std::runtime_error("Noise temperature must be positive");
175 }
176 _noise_temperature = temp;
177 }
178
179 void Receiver::setMode(const OperationMode mode) noexcept
180 {
181 _mode = mode;
182 if (_mode != OperationMode::FMCW_MODE)
183 {
184 setDechirpMode(DechirpMode::None);
185 _fmcw_if_chain = {};
186 }
187 }
188
189 void Receiver::setDechirpMode(const DechirpMode mode) noexcept
190 {
191 _dechirp_mode = mode;
192 if (_dechirp_mode == DechirpMode::None)
193 {
194 _dechirp_reference = {};
195 _dechirp_sources.clear();
196 _fmcw_if_chain = {};
197 _fmcw_if_plan.reset();
198 _fmcw_if_sink.reset();
199 _fmcw_if_samples_to_discard = 0;
200 _fmcw_if_input_cursor = 0;
201 _fmcw_if_output_cursor = 0;
202 _fmcw_if_segment_active = false;
203 }
204 }
205
207 {
208 _dechirp_reference = std::move(reference);
209 _dechirp_sources.clear();
210 }
211
213 {
214 _fmcw_if_chain = request;
215 _fmcw_if_plan.reset();
216 _fmcw_if_sink.reset();
217 _fmcw_if_samples_to_discard = 0;
218 _fmcw_if_input_cursor = 0;
219 _fmcw_if_output_cursor = 0;
220 _fmcw_if_segment_active = false;
221 }
222
224 {
225 _fmcw_if_plan = std::move(plan);
226 _fmcw_if_samples_to_discard = _fmcw_if_plan->warmup_discard_samples;
227 _fmcw_if_sink = std::make_unique<fers_signal::FmcwIfResamplingSink>(*_fmcw_if_plan);
228 _fmcw_if_input_cursor = 0;
229 _fmcw_if_output_cursor = 0;
230 _fmcw_if_segment_active = false;
231 }
232
234 {
235 if (_fmcw_if_sink == nullptr || !_fmcw_if_plan.has_value())
236 {
237 return;
238 }
239 consumeFmcwIfZerosUntil(ceilSampleIndexAtOrAfter(segment_start_time, _fmcw_if_plan->input_sample_rate_hz));
240 _fmcw_if_segment_active = true;
241 }
242
243 void Receiver::consumeFmcwIfBlock(const std::span<const ComplexType> block, const RealType block_start_time)
244 {
245 if (_fmcw_if_sink == nullptr || !_fmcw_if_plan.has_value())
246 {
247 throw std::logic_error("FMCW IF resampling sink has not been initialized.");
248 }
249 const auto input_sample_rate_hz = _fmcw_if_plan->input_sample_rate_hz;
250 if (!_fmcw_if_segment_active)
251 {
253 }
254 const auto block_start_index = ceilSampleIndexAtOrAfter(block_start_time, input_sample_rate_hz);
255 if (block_start_index < _fmcw_if_input_cursor)
256 {
257 throw std::logic_error("FMCW IF resampling input blocks must be supplied in chronological order.");
258 }
259 consumeFmcwIfZerosUntil(block_start_index);
260 _fmcw_if_sink->consume(block);
261 _fmcw_if_input_cursor += block.size();
262 auto emitted = _fmcw_if_sink->takeOutput();
263 appendFmcwIfOutput(std::move(emitted));
264 }
265
267 {
268 _fmcw_if_output_callback = std::move(callback);
269 }
270
271 void Receiver::appendFmcwIfOutput(std::vector<ComplexType> emitted)
272 {
273 if (_fmcw_if_samples_to_discard > 0)
274 {
275 const auto discard = std::min<std::uint64_t>(_fmcw_if_samples_to_discard, emitted.size());
276 emitted.erase(emitted.begin(), emitted.begin() + static_cast<std::ptrdiff_t>(discard));
277 _fmcw_if_samples_to_discard -= discard;
278 }
279 if (emitted.empty())
280 {
281 return;
282 }
283 const auto output_start = static_cast<std::uint64_t>(_fmcw_if_output_cursor);
284 if (_fmcw_if_output_callback)
285 {
286 _fmcw_if_output_callback(std::span<const ComplexType>(emitted.data(), emitted.size()), output_start);
287 }
288 _fmcw_if_output_cursor += emitted.size();
289 }
290
291 void Receiver::advanceFmcwIfOutputZeros(std::size_t sample_count)
292 {
293 if (_fmcw_if_samples_to_discard > 0)
294 {
295 const auto discard = std::min<std::uint64_t>(_fmcw_if_samples_to_discard, sample_count);
296 sample_count -= static_cast<std::size_t>(discard);
297 _fmcw_if_samples_to_discard -= discard;
298 }
299 if (sample_count == 0)
300 {
301 return;
302 }
303 _fmcw_if_output_cursor += sample_count;
304 }
305
307 {
308 if (_fmcw_if_sink == nullptr)
309 {
310 return;
311 }
312 _fmcw_if_segment_active = false;
313 }
314
316 {
317 if (_fmcw_if_sink == nullptr)
318 {
319 return;
320 }
322 if (_fmcw_if_plan.has_value())
323 {
324 const RealType flush_until_time = params::endTime() + _fmcw_if_plan->group_delay_seconds +
325 1.0 / _fmcw_if_plan->actual_output_sample_rate_hz;
326 consumeFmcwIfZerosUntil(ceilSampleIndexAtOrAfter(flush_until_time, _fmcw_if_plan->input_sample_rate_hz));
327 auto emitted = _fmcw_if_sink->finish();
328 appendFmcwIfOutput(std::move(emitted));
329 }
330 _fmcw_if_sink.reset();
331 }
332
333 void Receiver::consumeFmcwIfZerosUntil(const std::size_t target_input_cursor)
334 {
335 if (_fmcw_if_sink == nullptr)
336 {
337 throw std::logic_error("FMCW IF resampling sink has not been initialized.");
338 }
339 if (target_input_cursor <= _fmcw_if_input_cursor)
340 {
341 return;
342 }
343
344 const auto count = target_input_cursor - _fmcw_if_input_cursor;
345 auto result = _fmcw_if_sink->consumeZeroInput(count);
346 _fmcw_if_input_cursor = target_input_cursor;
347 appendFmcwIfOutput(std::move(result.emitted));
348 advanceFmcwIfOutputZeros(result.skipped_output_samples);
349 }
350
351 void Receiver::setResolvedDechirpSources(std::vector<core::ActiveStreamingSource> sources)
352 {
353 _dechirp_sources = std::move(sources);
354 }
355
356 void Receiver::setWindowProperties(const RealType length, const RealType prf, const RealType skip) noexcept
357 {
358 const auto rate = params::rate() * params::oversampleRatio();
359 _window_length = length;
360 _window_prf = 1 / (std::floor(rate / prf) / rate);
361 _window_skip = std::floor(rate * skip) / rate;
362 }
363
365 {
366 const RealType time = params::endTime() - params::startTime();
367 const RealType pulses = time * _window_prf;
368 return static_cast<unsigned>(std::ceil(pulses));
369 }
370
372 {
373 const RealType stime = static_cast<RealType>(window) / _window_prf + _window_skip;
374 if (!_timing)
375 {
376 LOG(logging::Level::FATAL, "Receiver must be associated with timing source");
377 throw std::logic_error("Receiver must be associated with timing source");
378 }
379 return stime;
380 }
381
382 void Receiver::setSchedule(std::vector<SchedulePeriod> schedule) { _schedule = std::move(schedule); }
383
384 std::optional<RealType> Receiver::getNextWindowTime(RealType time) const
385 {
386 // If no schedule is defined, assume always on.
387 if (_schedule.empty())
388 {
389 return time;
390 }
391 for (const auto& period : _schedule)
392 {
393 // If time is within this period, it's valid.
394 if (time >= period.start && time <= period.end)
395 {
396 return time;
397 }
398 // If time is before this period, skip to the start of this period.
399 if (time < period.start)
400 {
401 return period.start;
402 }
403 // If time is after this period, continue to next period.
404 }
405 // Time is after the last scheduled period.
406 return std::nullopt;
407 }
408}
static SimIdGenerator & instance()
Get the singleton instance of SimIdGenerator.
Definition sim_id.h:48
A class representing a vector in spherical coordinates.
const std::string & getName() const noexcept
Retrieves the name of the object.
Definition object.h:79
Represents a simulation platform with motion and rotation paths.
Definition platform.h:32
Represents a radar system on a platform.
Definition radar_obj.h:50
std::shared_ptr< timing::Timing > _timing
Timing source for the radar.
Definition radar_obj.h:144
virtual RealType getNoiseTemperature(const math::SVec3 &angle) const noexcept
Gets the noise temperature of the radar.
Definition radar_obj.cpp:31
void addInterferenceToLog(std::unique_ptr< serial::Response > response) noexcept
Adds a pulsed interference response to the receiver's streaming-mode log.
Definition receiver.cpp:122
void setMode(OperationMode mode) noexcept
Sets the operational mode of the receiver.
Definition receiver.cpp:179
void prunePulsedInterferenceEndingBefore(RealType cutoff_time) noexcept
Removes logged pulsed interference responses that ended before a receive time.
Definition receiver.cpp:128
std::vector< std::unique_ptr< serial::Response > > drainInbox() noexcept
Moves all responses from the inbox into a RenderingJob.
Definition receiver.cpp:135
void initializeFmcwIfResampling(fers_signal::FmcwIfResamplerPlan plan)
Creates the online FMCW IF resampling sink and clears the output buffer.
Definition receiver.cpp:223
DechirpReferenceSource
Source used to construct the receiver LO reference.
Definition receiver.h:71
@ Transmitter
Use a named transmitter.
@ Attached
Use the attached transmitter.
@ None
No reference configured.
@ Custom
Use a named top-level waveform with the receiver schedule.
unsigned getWindowCount() const noexcept
Gets the number of radar windows.
Definition receiver.cpp:364
void setDechirpReference(DechirpReference reference)
Stores the unresolved dechirp reference parsed from scenario input.
Definition receiver.cpp:206
void setResolvedDechirpSources(std::vector< core::ActiveStreamingSource > sources)
Replaces resolved receive-time LO source segments.
Definition receiver.cpp:351
void consumeFmcwIfBlock(std::span< const ComplexType > block, RealType block_start_time)
Feeds one completed high-rate dechirped block into the online IF sink.
Definition receiver.cpp:243
RealType getWindowStart(unsigned window) const
Retrieves the start time of a specific radar window.
Definition receiver.cpp:371
std::function< void(std::span< const ComplexType >, std::uint64_t)> FmcwIfOutputCallback
Definition receiver.h:49
void beginFmcwIfResamplingSegment(RealType segment_start_time)
Advances the continuous IF resampling timeline to the next active segment start.
Definition receiver.cpp:233
std::optional< RealType > getNextWindowTime(RealType time) const
Determines the next valid window start time at or after the given time.
Definition receiver.cpp:384
void setDechirpMode(DechirpMode mode) noexcept
Sets the receiver-side dechirp mode.
Definition receiver.cpp:189
void endFmcwIfResamplingSegment()
Marks the current scheduled IF segment inactive.
Definition receiver.cpp:306
void setSchedule(std::vector< SchedulePeriod > schedule)
Sets the active schedule for the receiver.
Definition receiver.cpp:382
void enqueueFinalizerJob(core::RenderingJob &&job)
Adds a completed RenderingJob to the finalizer queue.
Definition receiver.cpp:143
RealType getNoiseTemperature() const noexcept
Retrieves the noise temperature of the receiver.
Definition receiver.h:151
bool waitAndDequeueFinalizerJob(core::RenderingJob &job)
Waits for and dequeues a RenderingJob from the finalizer queue.
Definition receiver.cpp:152
void setWindowProperties(RealType length, RealType prf, RealType skip) noexcept
Sets the properties for radar windows.
Definition receiver.cpp:356
void setFmcwIfOutputCallback(FmcwIfOutputCallback callback)
Routes emitted IF samples to a live consumer instead of the HDF5 accumulation buffer.
Definition receiver.cpp:266
void addResponseToInbox(std::unique_ptr< serial::Response > response) noexcept
Adds a response to the receiver's pulsed-mode inbox.
Definition receiver.cpp:116
DechirpMode
Receiver-side FMCW dechirping mode.
Definition receiver.h:63
@ Ideal
Ignore timing phase noise during the dechirp mix.
@ None
Output raw pre-mix streaming IQ.
@ Physical
Preserve timing phase-noise decorrelation in the IF output.
void setFmcwIfChainRequest(FmcwIfChainRequest request) noexcept
Stores the receiver-local FMCW IF-chain request.
Definition receiver.cpp:212
void setNoiseTemperature(RealType temp)
Sets the noise temperature of the receiver.
Definition receiver.cpp:169
void flushFmcwIfResampling()
Flushes remaining samples from the online IF sink into the output buffer.
Definition receiver.cpp:315
Receiver(Platform *platform, std::string name, unsigned seed, OperationMode mode, const SimId id=0) noexcept
Constructs a Receiver object.
Definition receiver.cpp:109
double RealType
Type for real numbers.
Definition config.h:27
constexpr RealType EPSILON
Machine epsilon for real numbers.
Definition config.h:51
#define LOG(level,...)
Definition logging.h:19
@ FATAL
Fatal level for severe error events.
RealType endTime() noexcept
Get the end time for the simulation.
Definition parameters.h:109
RealType rate() noexcept
Get the rendering sample rate.
Definition parameters.h:121
RealType startTime() noexcept
Get the start time for the simulation.
Definition parameters.h:103
unsigned oversampleRatio() noexcept
Get the oversampling ratio.
Definition parameters.h:151
std::string_view dechirpReferenceSourceToken(const Receiver::DechirpReferenceSource source) noexcept
Converts a dechirp reference source to its scenario token.
Definition receiver.cpp:76
OperationMode
Defines the operational mode of a radar component.
Definition radar_obj.h:39
@ FMCW_MODE
The component operates in an FMCW streaming mode.
Receiver::DechirpReferenceSource parseDechirpReferenceSourceToken(const std::string_view token)
Parses a dechirp reference source scenario token.
Definition receiver.cpp:92
std::string_view dechirpModeToken(const Receiver::DechirpMode mode) noexcept
Converts a dechirp mode to its scenario token.
Definition receiver.cpp:45
Receiver::DechirpMode parseDechirpModeToken(const std::string_view token)
Parses a dechirp mode scenario token.
Definition receiver.cpp:59
Defines the Parameters struct and provides methods for managing simulation parameters.
Radar Receiver class for managing signal reception and response handling.
Classes for managing radar signal responses.
uint64_t SimId
64-bit Unique Simulation ID.
Definition sim_id.h:18
math::Vec3 max
A data packet containing all information needed to process one receive window.
Parsed and resolved dechirp reference details.
Definition receiver.h:80
Receiver-local FMCW IF-chain request parsed from scenario input.
Definition receiver.h:91