FERS 0.1.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
finalizer_pipeline.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-only
2//
3// Copyright (c) 2026-present FERS Contributors (see AUTHORS.md).
4//
5// See the GNU GPLv2 LICENSE file in the FERS project root for more information.
6
8
9#include <algorithm>
10#include <array>
11#include <cmath>
12#include <cstddef>
13#include <highfive/highfive.hpp>
14#include <limits>
15#include <optional>
16#include <ranges>
17#include <stdexcept>
18#include <unordered_map>
19
20#include "core/logging.h"
22#include "core/parameters.h"
24#include "radar/receiver.h"
25#include "radar/target.h"
26#include "radar/transmitter.h"
27#include "serial/hdf5_handler.h"
28#include "serial/response.h"
29#include "signal/dsp_filters.h"
31#include "timing/timing.h"
32
34{
35 namespace
36 {
37 /// Prepares caller-owned tracker storage for one independent receive window without shrinking capacity.
39 const std::size_t target_count)
40 {
41 if (tracker_cache.direct.size() < source_count)
42 {
43 tracker_cache.direct.resize(source_count);
44 }
45 if (tracker_cache.reflected.size() < source_count)
46 {
47 tracker_cache.reflected.resize(source_count);
48 }
49
50 for (std::size_t source_index = 0; source_index < source_count; ++source_index)
51 {
52 tracker_cache.direct[source_index] = {};
53
56 {
58 }
59 for (std::size_t target_index = 0; target_index < target_count; ++target_index)
60 {
62 }
63 }
64 }
65 }
66
68 {
69
70 if ((timing_model == nullptr) || !timing_model->isEnabled())
71 {
72 return;
73 }
74
75 // Convert the duration-derived sample advance to a signed temporary first.
76 // The physical calculation can legitimately produce a negative or zero result
77 // (for example due to floor() or an inter-pulse gap that is not positive),
78 // but skipSamples() models advancing by a non-negative sample count only.
79 // After validating that the computed value is > 0, cast to std::size_t for
80 // the skipSamples() call chain:
81 // Timing::skipSamples -> ClockModelGenerator::skipSamples ->
82 // MultirateGenerator::skipSamples.
83 // This preserves the sign until validation and avoids passing invalid negative
84 // counts into the timing/noise generators.
85 if (timing_model->getSyncOnPulse())
86 {
87 timing_model->reset();
88 const auto skip_samples = static_cast<long long>(std::floor(rate * receiver->getWindowSkip()));
89 if (skip_samples > 0)
90 {
91 timing_model->skipSamples(static_cast<std::size_t>(skip_samples));
92 }
93 }
94 else
95 {
96 const RealType inter_pulse_skip_duration = 1.0 / receiver->getWindowPrf() - receiver->getWindowLength();
97 const auto samples_to_skip = static_cast<long long>(std::floor(rate * inter_pulse_skip_duration));
98 if (samples_to_skip > 0)
99 {
100 timing_model->skipSamples(static_cast<std::size_t>(samples_to_skip));
101 }
102 }
103 }
104
105 std::tuple<RealType, RealType> calculateJitteredStart(const RealType ideal_start, const RealType first_phase_noise,
106 const RealType carrier_freq, const RealType rate)
107 {
108 const RealType actual_start = ideal_start + first_phase_noise / (2.0 * PI * carrier_freq);
109 const RealType rounded_start = std::round(actual_start * rate) / rate;
110 const RealType fractional_delay = actual_start * rate - std::round(actual_start * rate);
112 }
113
114 void applyStreamingInterference(std::span<ComplexType> window, const RealType actual_start, const RealType dt,
116 const std::vector<core::ActiveStreamingSource>& streaming_sources,
117 const std::vector<std::unique_ptr<radar::Target>>* targets,
119 const simulation::CwPhaseNoiseLookup* phase_noise_lookup)
120 {
121 const simulation::CwPhaseNoiseLookup* lookup = phase_noise_lookup;
122 std::optional<simulation::CwPhaseNoiseLookup> owned_lookup;
123 if (lookup == nullptr)
124 {
125 std::unordered_map<SimId, std::shared_ptr<timing::Timing>> unique_timings;
126 unique_timings.try_emplace(receiver->getTiming()->getId(), receiver->getTiming());
127 for (const auto& streaming_source : streaming_sources)
128 {
129 unique_timings.try_emplace(streaming_source.transmitter->getTiming()->getId(),
130 streaming_source.transmitter->getTiming());
131 }
132
133 std::vector<std::shared_ptr<timing::Timing>> timings;
134 timings.reserve(unique_timings.size());
135 for (const auto& entry : unique_timings)
136 {
137 timings.push_back(entry.second);
138 }
139
140 const RealType end_time =
141 actual_start + dt * static_cast<RealType>(window.empty() ? 0 : (window.size() - 1));
144 }
145
147
149 for (auto& window_sample : window)
150 {
152 for (std::size_t source_index = 0; source_index < streaming_sources.size(); ++source_index)
153 {
156 {
159 }
160 for (std::size_t target_index = 0; target_index < targets->size(); ++target_index)
161 {
162 const auto& target_ptr = (*targets)[target_index];
166 }
167 }
169 t_sample += dt;
170 }
171 }
172
173 void applyPulsedInterference(std::vector<ComplexType>& iq_buffer,
174 const std::vector<std::unique_ptr<serial::Response>>& interference_log)
175 {
176 const std::array active_spans{SampleSpan{.start = 0, .end_exclusive = iq_buffer.size()}};
178 params::rate() * static_cast<RealType>(params::oversampleRatio()));
179 }
180
181 void applyPulsedInterference(std::vector<ComplexType>& iq_buffer,
182 const std::vector<std::unique_ptr<serial::Response>>& interference_log,
183 const std::span<const SampleSpan> active_spans, const RealType output_sample_rate)
184 {
185 for (const auto& response : interference_log)
186 {
187 unsigned psize = 0;
188 RealType prate = std::numeric_limits<RealType>::quiet_NaN();
189 const auto rendered_pulse = response->renderBinary(prate, psize, 0.0);
190 const RealType rate_tolerance = std::numeric_limits<RealType>::epsilon() *
191 std::max(std::abs(prate), std::abs(output_sample_rate)) * 16.0;
192 if (std::abs(prate - output_sample_rate) > rate_tolerance)
193 {
194 throw std::runtime_error(
195 "Pulsed interference sample rate must match the streaming output sample rate.");
196 }
197
198 const RealType pulse_end_time = response->startTime() + static_cast<RealType>(psize) / prate;
199 const auto pulse_start_index =
200 static_cast<long long>(std::floor((response->startTime() - params::startTime()) * output_sample_rate));
201 const auto pulse_end_index =
202 static_cast<long long>(std::ceil((pulse_end_time - params::startTime()) * output_sample_rate));
203 const auto buffer_end_index = static_cast<long long>(iq_buffer.size());
204
205 for (const auto& span : active_spans)
206 {
207 const auto span_start = static_cast<long long>(std::min(span.start, iq_buffer.size()));
208 const auto span_end = static_cast<long long>(std::min(span.end_exclusive, iq_buffer.size()));
209 const auto dest_begin = std::max({span_start, pulse_start_index, 0LL});
210 const auto dest_end = std::min({span_end, pulse_end_index, buffer_end_index});
211 if (dest_begin >= dest_end)
212 {
213 continue;
214 }
215
216 const auto copy_count = static_cast<std::size_t>(dest_end - dest_begin);
218 for (std::size_t i = 0; i < copy_count; ++i, ++source_index)
219 {
220 if (source_index >= 0 && source_index < static_cast<long long>(rendered_pulse.size()))
221 {
222 iq_buffer[static_cast<std::size_t>(dest_begin) + i] +=
223 rendered_pulse[static_cast<std::size_t>(source_index)];
224 }
225 }
226 }
227 }
228 }
229
230 void addPhaseNoiseToWindow(std::span<const RealType> noise, std::span<ComplexType> window)
231 {
232 for (auto [n, w] : std::views::zip(noise, window))
233 {
234 w *= std::polar(1.0, n);
235 }
236 }
237
243
244 void applyDownsampling(std::vector<ComplexType>& buffer)
245 {
246 if (params::oversampleRatio() > 1)
247 {
249 }
250 }
251
252 void exportStreamingToHdf5(const std::string& filename, const std::vector<ComplexType>& iq_buffer,
253 const RealType fullscale, const RealType ref_freq,
254 const core::OutputFileMetadata* metadata, const RealType sample_rate)
255 {
256 std::scoped_lock const lock(serial::hdf5_global_mutex);
257 try
258 {
259 HighFive::File file(filename, HighFive::File::Truncate);
260
261 std::vector<RealType> i_data(iq_buffer.size());
262 std::vector<RealType> q_data(iq_buffer.size());
263 std::ranges::transform(iq_buffer, i_data.begin(), [](const auto& c) { return c.real(); });
264 std::ranges::transform(iq_buffer, q_data.begin(), [](const auto& c) { return c.imag(); });
265
266 HighFive::DataSet i_dataset = file.createDataSet<RealType>("I_data", HighFive::DataSpace::From(i_data));
267 i_dataset.write(i_data);
268 HighFive::DataSet q_dataset = file.createDataSet<RealType>("Q_data", HighFive::DataSpace::From(q_data));
269 q_dataset.write(q_data);
270
271 file.createAttribute("sampling_rate", sample_rate > 0.0 ? sample_rate : params::rate());
272 file.createAttribute("start_time", params::startTime());
273 file.createAttribute("fullscale", fullscale);
274 file.createAttribute("reference_carrier_frequency", ref_freq);
275 if (metadata != nullptr)
276 {
278 }
279
280 LOG(logging::Level::INFO, "Successfully exported streaming data to '{}'", filename);
281 }
282 catch (const HighFive::Exception& err)
283 {
284 LOG(logging::Level::FATAL, "Error writing streaming data to HDF5 file '{}': {}", filename, err.what());
285 }
286 }
287
288}
const Receiver & receiver
Header for radar channel propagation and interaction models.
Manages radar signal reception and response processing.
Definition receiver.h:47
@ FLAG_NODIRECT
Disable direct-path reception.
Represents a timing source for simulation.
Definition timing.h:36
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.
Declares focused, testable pipeline steps for receiver finalization.
Header file for HDF5 data export and import functions.
Header file for the logging system.
#define LOG(level,...)
Definition logging.h:19
std::vector< ComplexType > downsample(std::span< const ComplexType > in)
Low-pass filters and decimates an oversampled complex waveform back to base rate.
@ FATAL
Fatal level for severe error events.
@ INFO
Info level for informational messages.
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
void applyStreamingInterference(std::span< ComplexType > window, const RealType actual_start, const RealType dt, const radar::Receiver *receiver, const std::vector< core::ActiveStreamingSource > &streaming_sources, const std::vector< std::unique_ptr< radar::Target > > *targets, core::ReceiverTrackerCache &tracker_cache, const simulation::CwPhaseNoiseLookup *phase_noise_lookup)
Applies streaming interference to a time window.
void addPhaseNoiseToWindow(std::span< const RealType > noise, std::span< ComplexType > window)
Applies a pre-generated sequence of phase noise samples to an I/Q buffer.
RealType applyDownsamplingAndQuantization(std::vector< ComplexType > &buffer)
Downsamples and quantizes an IQ buffer.
void advanceTimingModel(timing::Timing *timing_model, const radar::Receiver *receiver, const RealType rate)
Advances the receiver's timing model to the start of the next processing window.
std::tuple< RealType, RealType > calculateJitteredStart(const RealType ideal_start, const RealType first_phase_noise, const RealType carrier_freq, const RealType rate)
Calculates the jittered start time and fractional delay from a phase noise sample.
void applyPulsedInterference(std::vector< ComplexType > &iq_buffer, const std::vector< std::unique_ptr< serial::Response > > &interference_log)
Renders and applies pulsed interference to a streaming IQ buffer.
void exportStreamingToHdf5(const std::string &filename, const std::vector< ComplexType > &iq_buffer, const RealType fullscale, const RealType ref_freq, const core::OutputFileMetadata *metadata, const RealType sample_rate)
Exports a finalized streaming IQ buffer to an HDF5 file.
void applyDownsampling(std::vector< ComplexType > &buffer)
Downsamples an IQ buffer to the configured output rate without quantization.
RealType quantizeAndScaleWindow(std::span< ComplexType > window)
Simulates ADC quantization and scales a window of complex I/Q samples.
std::mutex hdf5_global_mutex
Global mutex to protect all HDF5 C-library calls, which are not thread-safe.
void writeOutputFileMetadataAttributes(HighFive::File &file, const core::OutputFileMetadata &metadata)
Writes additive FERS output metadata attributes to an open HDF5 file.
ComplexType calculateStreamingDirectPathContribution(const core::ActiveStreamingSource &source, const Receiver *recv, const RealType timeK, const CwPhaseNoiseLookup *const phase_noise_lookup, core::FmcwChirpBoundaryTracker *const chirp_tracker, const StreamingTimingPhaseMode timing_phase_mode)
Calculates a direct-path contribution from a cached streaming source.
ComplexType calculateStreamingReflectedPathContribution(const core::ActiveStreamingSource &source, const Receiver *recv, const Target *targ, const RealType timeK, const CwPhaseNoiseLookup *const phase_noise_lookup, core::FmcwChirpBoundaryTracker *const chirp_tracker, const StreamingTimingPhaseMode timing_phase_mode)
Calculates a reflected-path contribution from a cached streaming source.
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.
Header for receiver-side signal processing and rendering.
math::Vec3 max
RealType c
Metadata for one receiver output file.
Per-receiver FMCW tracker state for direct and reflected streaming paths.
Half-open sample span used for dechirped LO-active finalization ranges.
std::size_t start
Inclusive first sample index in the span.
Lookup table for CW phase noise across timing sources.
static CwPhaseNoiseLookup build(std::span< const std::shared_ptr< timing::Timing > > timings, RealType start_time, RealType end_time)
Builds a phase-noise lookup for the requested timing sources and time range.
Defines classes for radar targets and their Radar Cross-Section (RCS) models.
Timing source for simulation objects.
Header file for the Transmitter class in the radar namespace.