FERS 0.1.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
finalizer.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-only
2//
3// Copyright (c) 2025-present FERS Contributors (see AUTHORS.md).
4//
5// See the GNU GPLv2 LICENSE file in the FERS project root for more information.
6
7#include "finalizer.h"
8
9#include <algorithm>
10#include <chrono>
11#include <cmath>
12#include <cstdint>
13#include <format>
14#include <optional>
15#include <span>
16#include <stdexcept>
17#include <utility>
18#include <vector>
19
20#include "core/logging.h"
22#include "core/parameters.h"
23#include "core/rendering_job.h"
24#include "core/sim_threading.h"
27#include "radar/receiver.h"
28#include "radar/transmitter.h"
29#include "signal/radar_signal.h"
30#include "timing/timing.h"
31
32namespace processing
33{
34 namespace
35 {
36 /// Converts a cached streaming source to reusable FMCW waveform metadata.
38 {
40 {
41 return core::FmcwMetadata{
42 .waveform_shape = "triangle",
43 .chirp_bandwidth = source.triangle != nullptr ? source.triangle->getChirpBandwidth() : 0.0,
44 .chirp_duration = source.chirp_duration,
45 .chirp_rate = source.chirp_rate,
46 .start_frequency_offset = source.start_freq_off,
47 .triangle_period = source.triangle_period,
48 .triangle_count = source.triangle_count.has_value()
49 ? std::optional<std::uint64_t>(static_cast<std::uint64_t>(*source.triangle_count))
50 : std::nullopt};
51 }
52
53 return core::FmcwMetadata{
54 .waveform_shape = "linear",
55 .chirp_bandwidth = source.fmcw != nullptr ? source.fmcw->getChirpBandwidth() : 0.0,
56 .chirp_duration = source.chirp_duration,
57 .chirp_period = source.chirp_period,
58 .chirp_rate = source.chirp_rate,
59 .chirp_rate_signed = source.signed_chirp_rate,
60 .chirp_direction = source.fmcw != nullptr
62 : std::string("up"),
63 .start_frequency_offset = source.start_freq_off,
64 .chirp_count = source.chirp_count.has_value()
65 ? std::optional<std::uint64_t>(static_cast<std::uint64_t>(*source.chirp_count))
66 : std::nullopt};
67 }
68
69 /// Builds one FMCW source schedule segment from an active source cache.
71 {
72 const RealType active_start = std::max(params::startTime(), source.segment_start);
73 const RealType active_end = std::min(params::endTime(), source.segment_end);
74 core::FmcwSourceSegmentMetadata segment{.start_time = source.segment_start, .end_time = source.segment_end};
76 {
77 segment.first_triangle_start_time = core::firstFmcwTriangleStart(source, active_start, active_end);
78 segment.emitted_triangle_count = core::countFmcwTriangleStarts(source, active_start, active_end);
79 }
80 else
81 {
82 segment.first_chirp_start_time = core::firstFmcwChirpStart(source, active_start, active_end);
83 segment.emitted_chirp_count = core::countFmcwChirpStarts(source, active_start, active_end);
84 }
85 return segment;
86 }
87
88 /// Finds the first source metadata entry for a transmitter/waveform pair.
89 std::vector<core::FmcwSourceMetadata>::iterator findFmcwSource(std::vector<core::FmcwSourceMetadata>& sources,
90 const SimId transmitter_id,
91 const SimId waveform_id)
92 {
93 return std::ranges::find_if(
94 sources, [&](const core::FmcwSourceMetadata& source)
95 { return source.transmitter_id == transmitter_id && source.waveform_id == waveform_id; });
96 }
97
98 /// Builds explicit per-source FMCW metadata from active streaming transmitters.
99 std::vector<core::FmcwSourceMetadata>
100 buildFmcwSources(const std::vector<core::ActiveStreamingSource>& streaming_sources)
101 {
102 std::vector<core::FmcwSourceMetadata> fmcw_sources;
103 for (const auto& streaming_source : streaming_sources)
104 {
105 if (!streaming_source.is_fmcw || streaming_source.transmitter == nullptr)
106 {
107 continue;
108 }
109
110 const auto* signal = streaming_source.transmitter->getSignal();
111 if (signal == nullptr)
112 {
113 continue;
114 }
115
116 const auto transmitter_id = streaming_source.transmitter->getId();
117 const auto waveform_id = signal->getId();
118 auto existing = findFmcwSource(fmcw_sources, transmitter_id, waveform_id);
119 if (existing == fmcw_sources.end())
120 {
121 core::FmcwSourceMetadata source{.transmitter_id = transmitter_id,
122 .transmitter_name = streaming_source.transmitter->getName(),
123 .waveform_id = waveform_id,
124 .waveform_name = signal->getName(),
125 .carrier_frequency = signal->getCarrier(),
127 source.segments.push_back(buildFmcwSourceSegment(streaming_source));
128 fmcw_sources.push_back(std::move(source));
129 continue;
130 }
131
133 }
134 return fmcw_sources;
135 }
136
137 /// Adds scalar compatibility chirp metadata to receiver streaming segments for one FMCW source.
139 const core::ActiveStreamingSource& source)
140 {
141 for (auto& segment : metadata.streaming_segments)
142 {
143 const RealType active_start = std::max(segment.start_time, source.segment_start);
144 const RealType active_end = std::min(segment.end_time, source.segment_end);
146 {
148 const auto emitted = core::countFmcwTriangleStarts(source, active_start, active_end);
149 if (first_triangle.has_value() || emitted > 0)
150 {
151 segment.first_triangle_start_time = first_triangle;
152 segment.emitted_triangle_count = emitted;
153 }
154 }
155 else
156 {
158 const auto emitted = core::countFmcwChirpStarts(source, active_start, active_end);
159 if (first_chirp.has_value() || emitted > 0)
160 {
161 segment.first_chirp_start_time = first_chirp;
162 segment.emitted_chirp_count = emitted;
163 }
164 }
165 }
166 }
167
168 /// Half-open time interval in simulation seconds.
169 using TimeSpan = std::pair<RealType, RealType>;
170
171 /// Merges overlapping or adjacent time spans.
172 void normalizeTimeSpans(std::vector<TimeSpan>& spans)
173 {
174 std::ranges::sort(spans, [](const TimeSpan& lhs, const TimeSpan& rhs) { return lhs.first < rhs.first; });
175 std::vector<TimeSpan> merged;
176 for (const auto& span : spans)
177 {
178 if (span.second <= span.first)
179 {
180 continue;
181 }
182 if (merged.empty() || span.first > merged.back().second)
183 {
184 merged.push_back(span);
185 continue;
186 }
187 merged.back().second = std::max(merged.back().second, span.second);
188 }
189 spans = std::move(merged);
190 }
191
192 /// Returns receiver active intervals clipped to simulation time.
193 std::vector<TimeSpan> receiverActiveTimeSpans(const radar::Receiver* receiver)
194 {
195 std::vector<TimeSpan> spans;
196 if (receiver->getSchedule().empty())
197 {
198 spans.emplace_back(params::startTime(), params::endTime());
199 return spans;
200 }
201
202 for (const auto& period : receiver->getSchedule())
203 {
204 const RealType start = std::max(params::startTime(), period.start);
205 const RealType end = std::min(params::endTime(), period.end);
206 if (start < end)
207 {
208 spans.emplace_back(start, end);
209 }
210 }
211 return spans;
212 }
213
214 /// Adds LO-active intervals for one source intersected with a receiver-active interval.
216 std::vector<TimeSpan>& output)
217 {
218 const RealType clipped_start = std::max({receiver_span.first, source.segment_start, params::startTime()});
219 const RealType clipped_end = std::min({receiver_span.second, source.segment_end, params::endTime()});
221 {
222 return;
223 }
224
226 {
227 if (source.chirp_period <= 0.0 || source.chirp_duration <= 0.0)
228 {
229 return;
230 }
232 ? std::size_t{0}
233 : static_cast<std::size_t>(
234 std::floor((clipped_start - source.segment_start) / source.chirp_period));
235 while (true)
236 {
237 if (source.chirp_count.has_value() && chirp_index >= *source.chirp_count)
238 {
239 return;
240 }
241 const RealType chirp_start =
242 source.segment_start + static_cast<RealType>(chirp_index) * source.chirp_period;
244 {
245 return;
246 }
247 const RealType chirp_end = std::min(chirp_start + source.chirp_duration, source.segment_end);
249 const RealType active_end = std::min(chirp_end, clipped_end);
251 {
252 output.emplace_back(active_start, active_end);
253 }
254 ++chirp_index;
255 }
256 }
257
258 output.emplace_back(clipped_start, clipped_end);
259 }
260
261 /// Returns exact LO-active time spans for a dechirped receiver.
262 std::vector<TimeSpan> dechirpActiveTimeSpans(const radar::Receiver* receiver)
263 {
264 std::vector<TimeSpan> spans;
266 for (const auto& receiver_span : receiver_spans)
267 {
268 for (const auto& source : receiver->getDechirpSources())
269 {
271 }
272 }
274 return spans;
275 }
276
277 void appendStreamingSegment(core::OutputFileMetadata& metadata, const std::size_t total_samples,
278 const RealType output_sample_rate, const RealType start_time,
279 const RealType end_time)
280 {
281 const auto start_sample = static_cast<std::uint64_t>(std::min<RealType>(
282 static_cast<RealType>(total_samples),
283 std::max<RealType>(0.0, std::ceil((start_time - params::startTime()) * output_sample_rate))));
284 const auto end_sample = static_cast<std::uint64_t>(std::min<RealType>(
285 static_cast<RealType>(total_samples),
286 std::max<RealType>(0.0, std::ceil((end_time - params::startTime()) * output_sample_rate))));
288 {
289 const core::StreamingSegmentMetadata segment{.start_time = start_time,
290 .end_time = end_time,
291 .sample_count = end_sample - start_sample,
292 .sample_start = start_sample,
293 .sample_end_exclusive = end_sample};
294 metadata.streaming_segments.push_back(segment);
295 }
296 }
297
299 const std::size_t total_samples, const RealType output_sample_rate)
300 {
301 const auto& schedule = receiver->getSchedule();
302 if (schedule.empty())
303 {
306 return;
307 }
308
309 for (const auto& period : schedule)
310 {
311 const RealType start = std::max(params::startTime(), period.start);
312 const RealType end = std::min(params::endTime(), period.end);
313 if (start < end)
314 {
315 appendStreamingSegment(metadata, total_samples, output_sample_rate, start, end);
316 }
317 }
318 }
319
321 const std::size_t total_samples, const RealType output_sample_rate,
322 const std::vector<TimeSpan>& dechirp_time_spans)
323 {
324 if (!receiver->isDechirpEnabled())
325 {
327 return;
328 }
329
330 for (const auto& span : dechirp_time_spans)
331 {
332 appendStreamingSegment(metadata, total_samples, output_sample_rate, span.first, span.second);
333 }
334 }
335
337 const std::vector<core::ActiveStreamingSource>& streaming_sources)
338 {
340 if (metadata.fmcw_sources.size() != 1)
341 {
342 return;
343 }
344
345 metadata.fmcw = metadata.fmcw_sources.front().waveform;
346 for (const auto& streaming_source : streaming_sources)
347 {
348 if (streaming_source.is_fmcw && streaming_source.transmitter != nullptr &&
349 streaming_source.transmitter->getId() == metadata.fmcw_sources.front().transmitter_id)
350 {
352 }
353 }
354 }
355
357 {
358 const auto& if_request = receiver->getFmcwIfChainRequest();
359 const auto& if_plan = receiver->getFmcwIfResamplerPlan();
360 metadata.fmcw_if_legacy_full_rate = !if_request.sample_rate_hz.has_value();
361 metadata.fmcw_if_decimation_enabled = if_plan.has_value();
362 if (if_request.sample_rate_hz.has_value())
363 {
364 metadata.fmcw_if_requested_sample_rate = if_request.sample_rate_hz;
365 }
366 if (!if_plan.has_value())
367 {
368 return;
369 }
370
371 metadata.fmcw_if_sample_rate = if_plan->actual_output_sample_rate_hz;
372 metadata.fmcw_if_input_sample_rate = if_plan->input_sample_rate_hz;
373 metadata.fmcw_if_resample_numerator = static_cast<unsigned>(if_plan->overall_ratio.numerator);
374 metadata.fmcw_if_resample_denominator = static_cast<unsigned>(if_plan->overall_ratio.denominator);
375 metadata.fmcw_if_decimation_factor = if_plan->actual_output_sample_rate_hz > 0.0
376 ? if_plan->input_sample_rate_hz / if_plan->actual_output_sample_rate_hz
377 : 0.0;
378 metadata.fmcw_if_filter_bandwidth = if_plan->filter_bandwidth_hz;
379 metadata.fmcw_if_filter_transition_width = if_plan->filter_transition_width_hz;
380 metadata.fmcw_if_filter_stopband = if_plan->stopband_attenuation_db;
381 metadata.fmcw_if_filter_group_delay_seconds = if_plan->group_delay_seconds;
382 metadata.fmcw_if_compensated_integer_delay_samples = if_plan->warmup_discard_samples;
383 metadata.fmcw_if_compensated_fractional_delay_samples = if_plan->fractional_output_delay_samples;
384 metadata.fmcw_if_warmup_discard_samples = if_plan->warmup_discard_samples;
385 metadata.fmcw_if_phase_refinement = static_cast<unsigned>(if_plan->phase_refinement);
386 metadata.fmcw_if_timing_error_seconds = if_plan->estimated_timing_error_seconds;
387 metadata.fmcw_if_phase_error_radians = if_plan->estimated_phase_error_radians;
388 metadata.fmcw_if_noise_variance =
389 params::boltzmannK() * receiver->getNoiseTemperature() * if_plan->actual_output_sample_rate_hz;
390 metadata.fmcw_if_group_delay_compensated = if_plan->group_delay_compensated;
391 }
392
394 {
395 const auto& reference = receiver->getDechirpReference();
399 {
400 metadata.fmcw_dechirp_reference_transmitter_id = reference.transmitter_id;
401 metadata.fmcw_dechirp_reference_transmitter_name = reference.transmitter_name;
402 }
404 {
405 metadata.fmcw_dechirp_reference_waveform_id = reference.waveform_id;
406 metadata.fmcw_dechirp_reference_waveform_name = reference.waveform_name;
407 if (!receiver->getDechirpSources().empty())
408 {
409 metadata.fmcw_dechirp_reference_waveform = buildFmcwMetadata(receiver->getDechirpSources().front());
410 }
411 }
412 }
413
414 /// Builds output metadata for a streaming receiver result file.
417 const std::size_t total_samples,
418 const std::vector<core::ActiveStreamingSource>& streaming_sources,
419 const RealType output_sample_rate, const std::vector<TimeSpan>& dechirp_time_spans = {})
420 {
421 core::OutputFileMetadata metadata{.receiver_id = receiver->getId(),
422 .receiver_name = receiver->getName(),
423 .mode = receiver->getMode() == radar::OperationMode::FMCW_MODE ? "fmcw"
424 : "cw",
425 .path = hdf5_filename,
426 .sampling_rate = output_sample_rate,
427 .total_samples = static_cast<std::uint64_t>(total_samples),
428 .sample_start = 0,
429 .sample_end_exclusive = static_cast<std::uint64_t>(total_samples)};
430
433
434 metadata.fmcw_dechirp_mode = std::string(radar::dechirpModeToken(receiver->getDechirpMode()));
435 if (receiver->isDechirpEnabled())
436 {
439 }
440
441 return metadata;
442 }
443
444 /// Converts a receiver mode to the stable sink descriptor token.
446 {
447 switch (receiver->getMode())
448 {
450 return "pulsed";
452 return "fmcw";
454 return "cw";
455 }
456 return "unknown";
457 }
458
459 [[nodiscard]] std::string coordinateFrameToken(const params::CoordinateFrame frame)
460 {
461 switch (frame)
462 {
464 return "ENU";
466 return "UTM";
468 return "ECEF";
469 }
470 return "ENU";
471 }
472
474 {
477 .origin_latitude = params::originLatitude(),
478 .origin_longitude = params::originLongitude(),
479 .origin_altitude = params::originAltitude(),
480 .utm_zone = params::utmZone(),
481 .utm_north_hemisphere = params::utmNorthHemisphere()};
482 }
483
486 {
488 const auto* platform = receiver->getPlatform();
489 if (platform == nullptr)
490 {
491 return state;
492 }
493
494 const RealType t0 = params::startTime();
495 state.platform_id = platform->getId();
496 state.platform_name = platform->getName();
497 try
498 {
499 const auto position = platform->getPosition(t0);
500 state.position_x = position.x;
501 state.position_y = position.y;
502 state.position_z = position.z;
503 }
504 catch (...)
505 {
506 }
507 try
508 {
509 const auto velocity = platform->getMotionPath()->getVelocity(t0);
510 state.velocity_x = velocity.x;
511 state.velocity_y = velocity.y;
512 state.velocity_z = velocity.z;
513 }
514 catch (...)
515 {
516 }
517 try
518 {
519 const auto rotation = platform->getRotation(t0);
520 state.azimuth = rotation.azimuth;
521 state.elevation = rotation.elevation;
522 }
523 catch (...)
524 {
525 }
526 return state;
527 }
528
531 const std::span<const core::ActiveStreamingSource> streaming_sources)
532 {
533 if (receiver->isDechirpEnabled() && !receiver->getDechirpSources().empty())
534 {
535 return &receiver->getDechirpSources().front();
536 }
537 const auto found = std::ranges::find_if(streaming_sources, [](const core::ActiveStreamingSource& source)
538 { return source.is_fmcw; });
539 return found == streaming_sources.end() ? nullptr : &*found;
540 }
541
543 {
544 return receiver == nullptr ? nullptr : dynamic_cast<const radar::Transmitter*>(receiver->getAttached());
545 }
546
549 {
550 if (signal == nullptr)
551 {
552 return;
553 }
554 context.waveform_id = signal->getId();
555 context.waveform_name = signal->getName();
556 context.carrier_frequency = signal->getCarrier();
557 context.power = signal->getPower();
558 context.pulse_width = signal->getLength();
559 context.native_sample_rate = signal->getRate();
560 context.native_sample_count = signal->getSampleCount();
561 }
562
565 {
566 if (signal == nullptr)
567 {
568 return;
569 }
570 context.waveform_id = signal->getId();
571 context.waveform_name = signal->getName();
572 context.carrier_frequency = signal->getCarrier();
573 context.power = signal->getPower();
574 }
575
577 {
579 if (receiver == nullptr || receiver->getMode() != radar::OperationMode::PULSED_MODE)
580 {
581 return context;
582 }
583
584 context.present = true;
585 context.window_length = receiver->getWindowLength();
586 context.window_prf = receiver->getWindowPrf();
587 context.window_skip = receiver->getWindowSkip();
588 context.window_count = receiver->getWindowCount();
589 if (const auto* transmitter = attachedTransmitter(receiver); transmitter != nullptr)
590 {
591 populateWaveformIdentity(context, transmitter->getSignal());
592 }
593 if (context.carrier_frequency == 0.0)
594 {
595 if (const auto timing = receiver->getTiming(); timing)
596 {
597 context.carrier_frequency = timing->getFrequency();
598 }
599 }
600 return context;
601 }
602
604 {
606 if (receiver == nullptr || receiver->getMode() != radar::OperationMode::CW_MODE)
607 {
608 return context;
609 }
610
611 context.present = true;
612 if (const auto* transmitter = attachedTransmitter(receiver); transmitter != nullptr)
613 {
614 populateWaveformIdentity(context, transmitter->getSignal());
615 }
616 if (context.carrier_frequency == 0.0)
617 {
618 if (const auto timing = receiver->getTiming(); timing)
619 {
620 context.carrier_frequency = timing->getFrequency();
621 }
622 }
623 return context;
624 }
625
628 const std::span<const core::ActiveStreamingSource> streaming_sources)
629 {
631 if (receiver == nullptr || receiver->getMode() != radar::OperationMode::FMCW_MODE)
632 {
633 return context;
634 }
635
636 context.dechirp_mode = std::string(radar::dechirpModeToken(receiver->getDechirpMode()));
637 const auto& reference = receiver->getDechirpReference();
639 context.dechirp_reference_transmitter_id = reference.transmitter_id;
640 context.dechirp_reference_transmitter_name = reference.transmitter_name;
641 context.dechirp_reference_waveform_id = reference.waveform_id;
642 context.dechirp_reference_waveform_name = reference.waveform_name;
643
644 const auto* source = findFmcwContextSource(receiver, streaming_sources);
645 if (source == nullptr)
646 {
647 return context;
648 }
649
650 const auto waveform = buildFmcwMetadata(*source);
651 context.present = true;
652 context.waveform_shape = waveform.waveform_shape;
653 context.chirp_bandwidth = waveform.chirp_bandwidth;
654 context.chirp_duration = waveform.chirp_duration;
655 context.chirp_period = waveform.chirp_period;
656 context.chirp_rate = waveform.chirp_rate;
657 context.chirp_rate_signed = waveform.chirp_rate_signed;
658 context.sweep_direction =
659 source->kind == core::StreamingWaveformKind::FmcwTriangle ? "up_down" : waveform.chirp_direction;
660 context.start_frequency_offset = waveform.start_frequency_offset;
661 context.triangle_period = waveform.triangle_period;
662 context.chirp_count = waveform.chirp_count;
663 context.triangle_count = waveform.triangle_count;
664 return context;
665 }
666 }
667
669 const radar::Receiver* receiver, const std::string& output_path, const std::size_t total_samples,
670 const std::vector<core::ActiveStreamingSource>& streaming_sources, const RealType output_sample_rate)
671 {
672 const auto dechirp_time_spans =
673 receiver->isDechirpEnabled() ? dechirpActiveTimeSpans(receiver) : std::vector<TimeSpan>{};
676 }
677
680 const std::span<const core::ActiveStreamingSource> streaming_sources)
681 {
683 .receiver_name = receiver->getName(),
685 .sample_rate = sample_rate,
686 .bandwidth = sample_rate > 0.0 ? sample_rate / 2.0 : 0.0,
687 .dechirped = receiver->isDechirpEnabled(),
688 .if_resampled = receiver->getFmcwIfResamplerPlan().has_value(),
689 .adc_bits = params::adcBits(),
690 .coordinate = buildCoordinateContext(),
691 .initial_platform_state = buildInitialPlatformState(receiver),
692 .pulsed = buildPulsedContext(receiver),
695 if (const auto timing = receiver->getTiming(); timing)
696 {
697 descriptor.reference_frequency = timing->getFrequency();
698 }
699 return descriptor;
700 }
701
703 const RealType first_sample_time, const RealType sample_rate,
704 const std::span<const ComplexType> samples,
705 const std::uint64_t sample_start,
706 std::shared_ptr<const core::OutputFileMetadata> file_metadata)
707 {
708 return buildReceiverSampleBlock(receiver, first_sample_time, sample_rate, samples, sample_start,
709 std::span<const core::ActiveStreamingSource>{}, std::move(file_metadata));
710 }
711
714 const RealType sample_rate, const std::span<const ComplexType> samples,
715 const std::uint64_t sample_start,
716 const std::span<const core::ActiveStreamingSource> streaming_sources,
717 std::shared_ptr<const core::OutputFileMetadata> file_metadata)
718 {
721 .first_sample_time = first_sample_time,
722 .sample_rate = sample_rate,
723 .samples = samples,
724 .sample_start = sample_start,
725 .valid_data = true,
726 .calibrated_time = true,
727 .reference_lock = true,
728 .file_metadata = std::move(file_metadata)};
729 }
730
731 void runPulsedFinalizer(radar::Receiver* receiver, const std::vector<std::unique_ptr<radar::Target>>* targets,
732 const std::shared_ptr<core::ProgressReporter>& reporter, const std::string& output_dir,
733 const std::shared_ptr<core::OutputMetadataCollector>& metadata_collector,
735 {
736 (void)output_dir;
737 (void)metadata_collector;
738 if (output_sink == nullptr)
739 {
740 throw std::invalid_argument("runPulsedFinalizer requires a receiver output sink");
741 }
742
743 const auto timing_model = receiver->getTiming()->clone();
744 if (!timing_model)
745 {
746 LOG(logging::Level::FATAL, "Failed to clone timing model for receiver '{}'", receiver->getName());
747 return;
748 }
749
750 const auto sink_stream_id =
752 bool sink_stream_open = false;
753 std::uint64_t sink_sample_start = 0;
754
755 unsigned chunk_index = 0;
756
757 LOG(logging::Level::INFO, "Finalizer thread started for receiver '{}'. Routing to output sink.",
758 receiver->getName());
759
760 auto last_report_time = std::chrono::steady_clock::now();
761 const auto report_interval = std::chrono::milliseconds(100);
763 const RealType dt = 1.0 / rate;
765
766 while (true)
767 {
769 if (!receiver->waitAndDequeueFinalizerJob(job))
770 {
771 break; // Shutdown signal received
772 }
773
774 const auto window_samples = static_cast<unsigned>(std::ceil(job.duration * rate));
775 std::vector pnoise(window_samples, 0.0);
776
777 RealType actual_start = job.ideal_start_time;
778 RealType frac_delay = 0.0;
779
780 if (timing_model->isEnabled())
781 {
783 std::ranges::generate(pnoise, [&] { return timing_model->getNextSample(); });
785 job.ideal_start_time, pnoise[0], timing_model->getFrequency(), rate);
786 }
787
788 std::vector<ComplexType> window_buffer(window_samples);
789
791 job.active_streaming_sources, targets, streaming_tracker_cache);
792
794
795 if (timing_model->isEnabled())
796 {
798 }
799
802 receiver->getNoiseTemperature(receiver->getRotation(actual_start)),
803 receiver->getRngEngine(), params::rate());
804 if (!sink_stream_open)
805 {
807 sink_stream_open = true;
808 }
809 const auto block =
811 output_sink->submitBlock(block);
812 sink_sample_start += static_cast<std::uint64_t>(window_buffer.size());
813 ++chunk_index;
814
815 if (reporter)
816 {
817 const auto now = std::chrono::steady_clock::now();
819 {
820 reporter->report(std::format("Exporting {}: Chunk {}", receiver->getName(), chunk_index),
821 static_cast<int>(chunk_index), 0);
823 }
824 }
825 }
826
828 {
829 output_sink->closeStream(sink_stream_id);
830 }
831
832 if (reporter)
833 {
834 reporter->report(std::format("Finished Exporting {}", receiver->getName()), 100, 100);
835 }
836 LOG(logging::Level::INFO, "Finalizer thread for receiver '{}' finished.", receiver->getName());
837 }
838
839}
const Transmitter & transmitter
const Receiver & receiver
Vec3 position
RealType getChirpBandwidth() const noexcept
Gets the chirp bandwidth in hertz.
FmcwChirpDirection getDirection() const noexcept
Gets the FMCW sweep direction.
RealType getChirpBandwidth() const noexcept
Gets the chirp bandwidth in hertz.
Class representing a radar signal with associated properties.
RealType x
The x component of the vector.
RealType z
The z component of the vector.
RealType y
The y component of the vector.
Manages radar signal reception and response processing.
Definition receiver.h:47
@ Transmitter
Use a named transmitter.
@ Attached
Use the attached transmitter.
@ Custom
Use a named top-level waveform with the receiver schedule.
Represents a radar transmitter system.
Definition transmitter.h:34
double RealType
Type for real numbers.
Definition config.h:27
Declares the functions for the asynchronous receiver finalization pipelines.
Declares focused, testable pipeline steps for receiver finalization.
Header file for the logging system.
#define LOG(level,...)
Definition logging.h:19
std::uint64_t countFmcwTriangleStarts(const ActiveStreamingSource &source, const RealType active_start, const RealType active_end)
Counts FMCW triangles that start inside the absolute interval.
std::uint64_t countFmcwChirpStarts(const ActiveStreamingSource &source, const RealType active_start, const RealType active_end)
Counts FMCW chirps that start inside the absolute interval.
std::optional< RealType > firstFmcwTriangleStart(const ActiveStreamingSource &source, const RealType active_start, const RealType active_end)
Returns the first FMCW triangle start inside the absolute interval, if one exists.
std::optional< RealType > firstFmcwChirpStart(const ActiveStreamingSource &source, const RealType active_start, const RealType active_end)
Returns the first FMCW chirp start inside the absolute interval, if one exists.
std::string_view fmcwChirpDirectionToken(const FmcwChirpDirection direction) noexcept
Converts a chirp direction to the schema token.
@ FATAL
Fatal level for severe error events.
@ INFO
Info level for informational messages.
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
RealType boltzmannK() noexcept
Get the Boltzmann constant.
Definition parameters.h:97
unsigned oversampleRatio() noexcept
Get the oversampling ratio.
Definition parameters.h:151
double originLongitude() noexcept
Gets the KML/geospatial export origin longitude.
Definition parameters.h:279
int utmZone() noexcept
Gets the configured KML UTM zone.
Definition parameters.h:333
CoordinateFrame
Defines the coordinate systems supported for KML/geospatial export.
Definition parameters.h:31
@ UTM
Universal Transverse Mercator.
@ ENU
East-North-Up local tangent plane (default)
@ ECEF
Earth-Centered, Earth-Fixed.
unsigned adcBits() noexcept
Get the ADC quantization bits.
Definition parameters.h:133
CoordinateFrame coordinateFrame() noexcept
Gets the KML/geospatial export coordinate frame.
Definition parameters.h:321
double originLatitude() noexcept
Gets the KML/geospatial export origin latitude.
Definition parameters.h:273
bool utmNorthHemisphere() noexcept
Gets the configured KML UTM hemisphere.
Definition parameters.h:339
double originAltitude() noexcept
Gets the KML/geospatial export origin altitude.
Definition parameters.h:285
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.
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 applyDownsampling(std::vector< ComplexType > &buffer)
Downsamples an IQ buffer to the configured output rate without quantization.
core::ReceiverSampleBlock buildReceiverSampleBlock(const radar::Receiver *receiver, const RealType first_sample_time, const RealType sample_rate, const std::span< const ComplexType > samples, const std::uint64_t sample_start, std::shared_ptr< const core::OutputFileMetadata > file_metadata)
Builds a non-owning output sample block over contiguous processed complex samples.
void runPulsedFinalizer(radar::Receiver *receiver, const std::vector< std::unique_ptr< radar::Target > > *targets, const std::shared_ptr< core::ProgressReporter > &reporter, const std::string &output_dir, const std::shared_ptr< core::OutputMetadataCollector > &metadata_collector, core::ReceiverOutputSink *output_sink)
The main function for a dedicated pulsed-mode receiver finalizer thread.
core::OutputFileMetadata buildStreamingOutputMetadata(const radar::Receiver *receiver, const std::string &output_path, const std::size_t total_samples, const std::vector< core::ActiveStreamingSource > &streaming_sources, const RealType output_sample_rate)
Builds HDF5 file metadata for a streaming receiver result emitted through the output sink.
void applyThermalNoiseAtSampleRate(std::span< ComplexType > window, const RealType noiseTemperature, std::mt19937 &rngEngine, const RealType sampleRateHz)
Applies circular complex thermal noise using a caller-specified complex-baseband sample rate.
core::ReceiverStreamDescriptor buildReceiverStreamDescriptor(const radar::Receiver *receiver, const RealType sample_rate, const std::span< const core::ActiveStreamingSource > streaming_sources)
Builds the receiver stream descriptor used by output sinks.
void renderWindow(std::vector< ComplexType > &window, const RealType length, const RealType start, const RealType fracDelay, const std::span< const std::unique_ptr< serial::Response > > responses)
Renders a time-window of I/Q data from a collection of raw radar responses.
std::string_view dechirpReferenceSourceToken(const Receiver::DechirpReferenceSource source) noexcept
Converts a dechirp reference source to its scenario token.
Definition receiver.cpp:76
@ PULSED_MODE
The component operates in a pulsed mode.
@ CW_MODE
The component operates in a continuous-wave mode.
@ FMCW_MODE
The component operates in an FMCW streaming mode.
std::string_view dechirpModeToken(const Receiver::DechirpMode mode) noexcept
Converts a dechirp mode to its scenario token.
Definition receiver.cpp:45
Defines the Parameters struct and provides methods for managing simulation parameters.
Classes for handling radar waveforms and signals.
Radar Receiver class for managing signal reception and response handling.
Defines the data packet for asynchronous receiver finalization.
Header for receiver-side signal processing and rendering.
uint64_t SimId
64-bit Unique Simulation ID.
Definition sim_id.h:18
math::Vec3 max
Header file for the main simulation runner.
Cached description of an active streaming transmitter segment.
RealType triangle_period
Cached full triangle period in seconds.
RealType segment_start
Segment start time in seconds.
RealType signed_chirp_rate
Cached signed FMCW chirp rate in hertz per second.
RealType chirp_duration
Cached FMCW chirp duration in seconds.
RealType chirp_period
Cached FMCW chirp period in seconds.
StreamingWaveformKind kind
Cached streaming waveform shape.
const fers_signal::FmcwChirpSignal * fmcw
Stable pointer to the linear FMCW waveform, if any.
bool is_fmcw
Compatibility flag for any FMCW source.
std::optional< std::size_t > chirp_count
Optional finite chirp count for the segment.
std::optional< std::size_t > triangle_count
Optional finite triangle count for the segment.
RealType segment_end
Segment end time in seconds.
const fers_signal::FmcwTriangleSignal * triangle
Stable pointer to the triangle waveform, if any.
RealType chirp_rate
Cached FMCW chirp rate in hertz per second.
RealType start_freq_off
Cached FMCW start frequency offset in hertz.
FMCW waveform metadata captured for a streaming output file.
std::string waveform_shape
FMCW waveform shape token: linear or triangle.
Metadata for one FMCW illuminator represented in a streaming output file.
SimId transmitter_id
FMCW transmitter SimId.
SimId waveform_id
FMCW waveform SimId.
Metadata for one active FMCW transmitter schedule segment.
Metadata for one receiver output file.
std::vector< FmcwSourceMetadata > fmcw_sources
FMCW illuminators represented in the output.
std::optional< RealType > fmcw_if_noise_variance
Post-resampling complex noise variance.
std::optional< RealType > fmcw_if_requested_sample_rate
Requested IF ADC rate in hertz.
std::optional< RealType > fmcw_if_timing_error_seconds
Estimated timing error.
std::optional< std::string > fmcw_dechirp_reference_transmitter_name
LO transmitter name.
SimId receiver_id
Receiver SimId that owns the output file.
std::string fmcw_dechirp_mode
Receiver dechirp mode for FMCW streaming outputs.
std::optional< FmcwMetadata > fmcw
Optional FMCW metadata for streaming outputs.
bool fmcw_if_legacy_full_rate
True for legacy full-rate dechirped IF output.
std::optional< std::uint64_t > fmcw_if_compensated_integer_delay_samples
Integer output-delay compensation.
std::optional< RealType > fmcw_if_decimation_factor
Input/output sample-rate ratio.
std::optional< RealType > fmcw_if_sample_rate
Realized IF output sample rate in hertz.
std::optional< RealType > fmcw_if_compensated_fractional_delay_samples
Fractional output-delay compensation.
std::optional< FmcwMetadata > fmcw_dechirp_reference_waveform
Custom LO waveform parameters.
std::optional< RealType > fmcw_if_input_sample_rate
Input simulation sample rate in hertz.
std::optional< SimId > fmcw_dechirp_reference_transmitter_id
Referenced LO transmitter ID.
std::optional< RealType > fmcw_if_filter_bandwidth
One-sided IF passband in hertz.
std::optional< RealType > fmcw_if_filter_stopband
IF stopband attenuation in dB.
std::optional< RealType > fmcw_if_phase_error_radians
Estimated IF edge phase error.
std::optional< unsigned > fmcw_if_resample_numerator
Reduced rational P.
std::optional< std::uint64_t > fmcw_if_warmup_discard_samples
Startup outputs discarded by the sink.
bool fmcw_if_decimation_enabled
True when IF-rate resampling is used.
bool fmcw_if_group_delay_compensated
True when IF output timestamps are aligned to t_start.
std::vector< StreamingSegmentMetadata > streaming_segments
Streaming segments written to the file.
std::optional< RealType > fmcw_if_filter_transition_width
IF transition width in hertz.
std::optional< RealType > fmcw_if_filter_group_delay_seconds
Total filter delay.
std::optional< std::string > fmcw_dechirp_reference_waveform_name
Custom LO waveform name.
std::string fmcw_dechirp_reference_source
Receiver dechirp reference source.
std::optional< unsigned > fmcw_if_phase_refinement
Polyphase refinement factor.
std::optional< unsigned > fmcw_if_resample_denominator
Reduced rational Q.
std::optional< SimId > fmcw_dechirp_reference_waveform_id
Custom LO waveform ID.
ReceiverStreamDescriptor stream
std::optional< std::uint64_t > chirp_count
std::optional< std::uint64_t > triangle_count
Per-receiver FMCW tracker state for direct and reflected streaming paths.
A data packet containing all information needed to process one receive window.
Metadata for one contiguous streaming output segment.
Timing source for simulation objects.
Header file for the Transmitter class in the radar namespace.