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
70 {
71 if (source.sfcw == nullptr)
72 {
73 return {};
74 }
75 const RealType effective_bandwidth = source.sfcw->effectiveBandwidth();
76 const RealType step_size = std::abs(source.sfcw->getStepSize());
77 return core::SfcwMetadata{
79 .start_frequency_offset = source.sfcw->getStartFrequencyOffset(),
80 .step_size = source.sfcw->getStepSize(),
81 .step_count = static_cast<std::uint64_t>(source.sfcw->getStepCount()),
82 .dwell_time = source.sfcw->getDwellTime(),
83 .step_period = source.sfcw->getStepPeriod(),
84 .sweep_count = source.sfcw->getSweepCount().has_value()
85 ? std::optional<std::uint64_t>(static_cast<std::uint64_t>(*source.sfcw->getSweepCount()))
86 : std::nullopt,
87 .first_frequency = source.sfcw->firstFrequency(source.carrier_freq),
88 .last_frequency = source.sfcw->lastFrequency(source.carrier_freq),
89 .frequency_span = source.sfcw->frequencySpan(),
90 .effective_bandwidth = effective_bandwidth,
91 .range_resolution = effective_bandwidth > 0.0 ? params::c() / (2.0 * effective_bandwidth) : 0.0,
92 .unambiguous_range = step_size > 0.0 ? params::c() / (2.0 * step_size) : 0.0};
93 }
94
95 /// Builds one FMCW source schedule segment from an active source cache.
97 {
98 const RealType active_start = std::max(params::startTime(), source.segment_start);
99 const RealType active_end = std::min(params::endTime(), source.segment_end);
100 core::FmcwSourceSegmentMetadata segment{.start_time = source.segment_start, .end_time = source.segment_end};
102 {
103 segment.first_triangle_start_time = core::firstFmcwTriangleStart(source, active_start, active_end);
104 segment.emitted_triangle_count = core::countFmcwTriangleStarts(source, active_start, active_end);
105 }
106 else
107 {
108 segment.first_chirp_start_time = core::firstFmcwChirpStart(source, active_start, active_end);
109 segment.emitted_chirp_count = core::countFmcwChirpStarts(source, active_start, active_end);
110 }
111 return segment;
112 }
113
114 /// Finds the first source metadata entry for a transmitter/waveform pair.
115 std::vector<core::FmcwSourceMetadata>::iterator findFmcwSource(std::vector<core::FmcwSourceMetadata>& sources,
116 const SimId transmitter_id,
117 const SimId waveform_id)
118 {
119 return std::ranges::find_if(
120 sources, [&](const core::FmcwSourceMetadata& source)
121 { return source.transmitter_id == transmitter_id && source.waveform_id == waveform_id; });
122 }
123
124 /// Builds explicit per-source FMCW metadata from active streaming transmitters.
125 std::vector<core::FmcwSourceMetadata>
126 buildFmcwSources(const std::vector<core::ActiveStreamingSource>& streaming_sources)
127 {
128 std::vector<core::FmcwSourceMetadata> fmcw_sources;
129 for (const auto& streaming_source : streaming_sources)
130 {
131 if (!streaming_source.is_fmcw || streaming_source.transmitter == nullptr)
132 {
133 continue;
134 }
135
136 const auto* signal = streaming_source.transmitter->getSignal();
137 if (signal == nullptr)
138 {
139 continue;
140 }
141
142 const auto transmitter_id = streaming_source.transmitter->getId();
143 const auto waveform_id = signal->getId();
144 auto existing = findFmcwSource(fmcw_sources, transmitter_id, waveform_id);
145 if (existing == fmcw_sources.end())
146 {
147 core::FmcwSourceMetadata source{.transmitter_id = transmitter_id,
148 .transmitter_name = streaming_source.transmitter->getName(),
149 .waveform_id = waveform_id,
150 .waveform_name = signal->getName(),
151 .carrier_frequency = signal->getCarrier(),
153 source.segments.push_back(buildFmcwSourceSegment(streaming_source));
154 fmcw_sources.push_back(std::move(source));
155 continue;
156 }
157
159 }
160 return fmcw_sources;
161 }
162
164 {
165 const RealType active_start = std::max(params::startTime(), source.segment_start);
166 const RealType active_end = std::min(params::endTime(), source.segment_end);
168 .start_time = source.segment_start,
169 .end_time = source.segment_end,
170 .first_step_start_time = core::firstSfcwStepStart(source, active_start, active_end),
171 .emitted_step_count = core::countSfcwStepStarts(source, active_start, active_end)};
172 }
173
174 std::vector<core::SfcwSourceMetadata>::iterator findSfcwSource(std::vector<core::SfcwSourceMetadata>& sources,
175 const SimId transmitter_id,
176 const SimId waveform_id)
177 {
178 return std::ranges::find_if(
179 sources, [&](const core::SfcwSourceMetadata& source)
180 { return source.transmitter_id == transmitter_id && source.waveform_id == waveform_id; });
181 }
182
183 std::vector<core::SfcwSourceMetadata>
184 buildSfcwSources(const std::vector<core::ActiveStreamingSource>& streaming_sources)
185 {
186 std::vector<core::SfcwSourceMetadata> sfcw_sources;
187 for (const auto& streaming_source : streaming_sources)
188 {
189 if (!streaming_source.is_sfcw || streaming_source.transmitter == nullptr)
190 {
191 continue;
192 }
193
194 const auto* signal = streaming_source.transmitter->getSignal();
195 if (signal == nullptr)
196 {
197 continue;
198 }
199
200 const auto transmitter_id = streaming_source.transmitter->getId();
201 const auto waveform_id = signal->getId();
202 auto existing = findSfcwSource(sfcw_sources, transmitter_id, waveform_id);
203 if (existing == sfcw_sources.end())
204 {
205 core::SfcwSourceMetadata source{.transmitter_id = transmitter_id,
206 .transmitter_name = streaming_source.transmitter->getName(),
207 .waveform_id = waveform_id,
208 .waveform_name = signal->getName(),
210 source.segments.push_back(buildSfcwSourceSegment(streaming_source));
211 sfcw_sources.push_back(std::move(source));
212 continue;
213 }
214
216 }
217 return sfcw_sources;
218 }
219
220 /// Adds scalar compatibility chirp metadata to receiver streaming segments for one FMCW source.
222 const core::ActiveStreamingSource& source)
223 {
224 for (auto& segment : metadata.streaming_segments)
225 {
226 const RealType active_start = std::max(segment.start_time, source.segment_start);
227 const RealType active_end = std::min(segment.end_time, source.segment_end);
229 {
231 const auto emitted = core::countFmcwTriangleStarts(source, active_start, active_end);
232 if (first_triangle.has_value() || emitted > 0)
233 {
234 segment.first_triangle_start_time = first_triangle;
235 segment.emitted_triangle_count = emitted;
236 }
237 }
238 else
239 {
241 const auto emitted = core::countFmcwChirpStarts(source, active_start, active_end);
242 if (first_chirp.has_value() || emitted > 0)
243 {
244 segment.first_chirp_start_time = first_chirp;
245 segment.emitted_chirp_count = emitted;
246 }
247 }
248 }
249 }
250
252 const core::ActiveStreamingSource& source)
253 {
254 for (auto& segment : metadata.streaming_segments)
255 {
256 const RealType active_start = std::max(segment.start_time, source.segment_start);
257 const RealType active_end = std::min(segment.end_time, source.segment_end);
259 const auto emitted = core::countSfcwStepStarts(source, active_start, active_end);
260 if (first_step.has_value() || emitted > 0)
261 {
262 segment.first_sfcw_step_start_time = first_step;
263 segment.emitted_sfcw_step_count = emitted;
264 }
265 }
266 }
267
268 /// Half-open time interval in simulation seconds.
269 using TimeSpan = std::pair<RealType, RealType>;
270
271 /// Merges overlapping or adjacent time spans.
272 void normalizeTimeSpans(std::vector<TimeSpan>& spans)
273 {
274 std::ranges::sort(spans, [](const TimeSpan& lhs, const TimeSpan& rhs) { return lhs.first < rhs.first; });
275 std::vector<TimeSpan> merged;
276 for (const auto& span : spans)
277 {
278 if (span.second <= span.first)
279 {
280 continue;
281 }
282 if (merged.empty() || span.first > merged.back().second)
283 {
284 merged.push_back(span);
285 continue;
286 }
287 merged.back().second = std::max(merged.back().second, span.second);
288 }
289 spans = std::move(merged);
290 }
291
292 /// Returns receiver active intervals clipped to simulation time.
293 std::vector<TimeSpan> receiverActiveTimeSpans(const radar::Receiver* receiver)
294 {
295 std::vector<TimeSpan> spans;
296 if (receiver->getSchedule().empty())
297 {
298 spans.emplace_back(params::startTime(), params::endTime());
299 return spans;
300 }
301
302 for (const auto& period : receiver->getSchedule())
303 {
304 const RealType start = std::max(params::startTime(), period.start);
305 const RealType end = std::min(params::endTime(), period.end);
306 if (start < end)
307 {
308 spans.emplace_back(start, end);
309 }
310 }
311 return spans;
312 }
313
314 /// Adds LO-active intervals for one source intersected with a receiver-active interval.
316 std::vector<TimeSpan>& output)
317 {
318 const RealType clipped_start = std::max({receiver_span.first, source.segment_start, params::startTime()});
319 const RealType clipped_end = std::min({receiver_span.second, source.segment_end, params::endTime()});
321 {
322 return;
323 }
324
326 {
327 if (source.chirp_period <= 0.0 || source.chirp_duration <= 0.0)
328 {
329 return;
330 }
332 ? std::size_t{0}
333 : static_cast<std::size_t>(
334 std::floor((clipped_start - source.segment_start) / source.chirp_period));
335 while (true)
336 {
337 if (source.chirp_count.has_value() && chirp_index >= *source.chirp_count)
338 {
339 return;
340 }
341 const RealType chirp_start =
342 source.segment_start + static_cast<RealType>(chirp_index) * source.chirp_period;
344 {
345 return;
346 }
347 const RealType chirp_end = std::min(chirp_start + source.chirp_duration, source.segment_end);
349 const RealType active_end = std::min(chirp_end, clipped_end);
351 {
352 output.emplace_back(active_start, active_end);
353 }
354 ++chirp_index;
355 }
356 }
357
358 output.emplace_back(clipped_start, clipped_end);
359 }
360
361 /// Returns exact LO-active time spans for a dechirped receiver.
362 std::vector<TimeSpan> dechirpActiveTimeSpans(const radar::Receiver* receiver)
363 {
364 std::vector<TimeSpan> spans;
366 for (const auto& receiver_span : receiver_spans)
367 {
368 for (const auto& source : receiver->getDechirpSources())
369 {
371 }
372 }
374 return spans;
375 }
376
377 void appendStreamingSegment(core::OutputFileMetadata& metadata, const std::size_t total_samples,
378 const RealType output_sample_rate, const RealType start_time,
379 const RealType end_time)
380 {
381 const auto start_sample = static_cast<std::uint64_t>(std::min<RealType>(
382 static_cast<RealType>(total_samples),
383 std::max<RealType>(0.0, std::ceil((start_time - params::startTime()) * output_sample_rate))));
384 const auto end_sample = static_cast<std::uint64_t>(std::min<RealType>(
385 static_cast<RealType>(total_samples),
386 std::max<RealType>(0.0, std::ceil((end_time - params::startTime()) * output_sample_rate))));
388 {
389 const core::StreamingSegmentMetadata segment{.start_time = start_time,
390 .end_time = end_time,
391 .sample_count = end_sample - start_sample,
392 .sample_start = start_sample,
393 .sample_end_exclusive = end_sample};
394 metadata.streaming_segments.push_back(segment);
395 }
396 }
397
399 const std::size_t total_samples, const RealType output_sample_rate)
400 {
401 const auto& schedule = receiver->getSchedule();
402 if (schedule.empty())
403 {
406 return;
407 }
408
409 for (const auto& period : schedule)
410 {
411 const RealType start = std::max(params::startTime(), period.start);
412 const RealType end = std::min(params::endTime(), period.end);
413 if (start < end)
414 {
415 appendStreamingSegment(metadata, total_samples, output_sample_rate, start, end);
416 }
417 }
418 }
419
421 const std::size_t total_samples, const RealType output_sample_rate,
422 const std::vector<TimeSpan>& dechirp_time_spans)
423 {
424 if (!receiver->isDechirpEnabled())
425 {
427 return;
428 }
429
430 for (const auto& span : dechirp_time_spans)
431 {
432 appendStreamingSegment(metadata, total_samples, output_sample_rate, span.first, span.second);
433 }
434 }
435
437 const std::vector<core::ActiveStreamingSource>& streaming_sources)
438 {
440 if (metadata.fmcw_sources.size() != 1)
441 {
442 return;
443 }
444
445 metadata.fmcw = metadata.fmcw_sources.front().waveform;
446 for (const auto& streaming_source : streaming_sources)
447 {
448 if (streaming_source.is_fmcw && streaming_source.transmitter != nullptr &&
449 streaming_source.transmitter->getId() == metadata.fmcw_sources.front().transmitter_id)
450 {
452 }
453 }
454 }
455
457 const std::vector<core::ActiveStreamingSource>& streaming_sources)
458 {
460 if (metadata.sfcw_sources.size() != 1)
461 {
462 return;
463 }
464
465 metadata.sfcw = metadata.sfcw_sources.front().waveform;
466 for (const auto& streaming_source : streaming_sources)
467 {
468 if (streaming_source.is_sfcw && streaming_source.transmitter != nullptr &&
469 streaming_source.transmitter->getId() == metadata.sfcw_sources.front().transmitter_id)
470 {
472 }
473 }
474 }
475
477 {
478 const auto& if_request = receiver->getFmcwIfChainRequest();
479 const auto& if_plan = receiver->getFmcwIfResamplerPlan();
480 metadata.fmcw_if_legacy_full_rate = !if_request.sample_rate_hz.has_value();
481 metadata.fmcw_if_decimation_enabled = if_plan.has_value();
482 if (if_request.sample_rate_hz.has_value())
483 {
484 metadata.fmcw_if_requested_sample_rate = if_request.sample_rate_hz;
485 }
486 if (!if_plan.has_value())
487 {
488 return;
489 }
490
491 metadata.fmcw_if_sample_rate = if_plan->actual_output_sample_rate_hz;
492 metadata.fmcw_if_input_sample_rate = if_plan->input_sample_rate_hz;
493 metadata.fmcw_if_resample_numerator = static_cast<unsigned>(if_plan->overall_ratio.numerator);
494 metadata.fmcw_if_resample_denominator = static_cast<unsigned>(if_plan->overall_ratio.denominator);
495 metadata.fmcw_if_decimation_factor = if_plan->actual_output_sample_rate_hz > 0.0
496 ? if_plan->input_sample_rate_hz / if_plan->actual_output_sample_rate_hz
497 : 0.0;
498 metadata.fmcw_if_filter_bandwidth = if_plan->filter_bandwidth_hz;
499 metadata.fmcw_if_filter_transition_width = if_plan->filter_transition_width_hz;
500 metadata.fmcw_if_filter_stopband = if_plan->stopband_attenuation_db;
501 metadata.fmcw_if_filter_group_delay_seconds = if_plan->group_delay_seconds;
502 metadata.fmcw_if_compensated_integer_delay_samples = if_plan->warmup_discard_samples;
503 metadata.fmcw_if_compensated_fractional_delay_samples = if_plan->fractional_output_delay_samples;
504 metadata.fmcw_if_warmup_discard_samples = if_plan->warmup_discard_samples;
505 metadata.fmcw_if_phase_refinement = static_cast<unsigned>(if_plan->phase_refinement);
506 metadata.fmcw_if_timing_error_seconds = if_plan->estimated_timing_error_seconds;
507 metadata.fmcw_if_phase_error_radians = if_plan->estimated_phase_error_radians;
508 metadata.fmcw_if_noise_variance =
509 params::boltzmannK() * receiver->getNoiseTemperature() * if_plan->actual_output_sample_rate_hz;
510 metadata.fmcw_if_group_delay_compensated = if_plan->group_delay_compensated;
511 }
512
514 {
515 const auto& reference = receiver->getDechirpReference();
519 {
520 metadata.fmcw_dechirp_reference_transmitter_id = reference.transmitter_id;
521 metadata.fmcw_dechirp_reference_transmitter_name = reference.transmitter_name;
522 }
524 {
525 metadata.fmcw_dechirp_reference_waveform_id = reference.waveform_id;
526 metadata.fmcw_dechirp_reference_waveform_name = reference.waveform_name;
527 if (!receiver->getDechirpSources().empty())
528 {
529 metadata.fmcw_dechirp_reference_waveform = buildFmcwMetadata(receiver->getDechirpSources().front());
530 }
531 }
532 }
533
534 /// Builds output metadata for a streaming receiver result file.
537 const std::size_t total_samples,
538 const std::vector<core::ActiveStreamingSource>& streaming_sources,
539 const RealType output_sample_rate, const std::vector<TimeSpan>& dechirp_time_spans = {})
540 {
542 .receiver_id = receiver->getId(),
543 .receiver_name = receiver->getName(),
544 .mode = receiver->getMode() == radar::OperationMode::FMCW_MODE
545 ? "fmcw"
546 : (receiver->getMode() == radar::OperationMode::SFCW_MODE ? "sfcw" : "cw"),
547 .path = hdf5_filename,
548 .sampling_rate = output_sample_rate,
549 .total_samples = static_cast<std::uint64_t>(total_samples),
550 .sample_start = 0,
551 .sample_end_exclusive = static_cast<std::uint64_t>(total_samples)};
552
556
557 metadata.fmcw_dechirp_mode = std::string(radar::dechirpModeToken(receiver->getDechirpMode()));
558 if (receiver->isDechirpEnabled())
559 {
562 }
563
564 return metadata;
565 }
566
567 /// Converts a receiver mode to the stable sink descriptor token.
569 {
570 switch (receiver->getMode())
571 {
573 return "pulsed";
575 return "fmcw";
577 return "sfcw";
579 return "cw";
580 }
581 return "unknown";
582 }
583
584 [[nodiscard]] std::string coordinateFrameToken(const params::CoordinateFrame frame)
585 {
586 switch (frame)
587 {
589 return "ENU";
591 return "UTM";
593 return "ECEF";
594 }
595 return "ENU";
596 }
597
599 {
602 .origin_latitude = params::originLatitude(),
603 .origin_longitude = params::originLongitude(),
604 .origin_altitude = params::originAltitude(),
605 .utm_zone = params::utmZone(),
606 .utm_north_hemisphere = params::utmNorthHemisphere()};
607 }
608
611 {
613 const auto* platform = receiver->getPlatform();
614 if (platform == nullptr)
615 {
616 return state;
617 }
618
619 const RealType t0 = params::startTime();
620 state.platform_id = platform->getId();
621 state.platform_name = platform->getName();
622 try
623 {
624 const auto position = platform->getPosition(t0);
625 state.position_x = position.x;
626 state.position_y = position.y;
627 state.position_z = position.z;
628 }
629 catch (...)
630 {
631 }
632 try
633 {
634 const auto velocity = platform->getMotionPath()->getVelocity(t0);
635 state.velocity_x = velocity.x;
636 state.velocity_y = velocity.y;
637 state.velocity_z = velocity.z;
638 }
639 catch (...)
640 {
641 }
642 try
643 {
644 const auto rotation = platform->getRotation(t0);
645 state.azimuth = rotation.azimuth;
646 state.elevation = rotation.elevation;
647 }
648 catch (...)
649 {
650 }
651 return state;
652 }
653
656 const std::span<const core::ActiveStreamingSource> streaming_sources)
657 {
658 if (receiver->isDechirpEnabled() && !receiver->getDechirpSources().empty())
659 {
660 return &receiver->getDechirpSources().front();
661 }
662 const auto found = std::ranges::find_if(streaming_sources, [](const core::ActiveStreamingSource& source)
663 { return source.is_fmcw; });
664 return found == streaming_sources.end() ? nullptr : &*found;
665 }
666
668 {
669 return receiver == nullptr ? nullptr : dynamic_cast<const radar::Transmitter*>(receiver->getAttached());
670 }
671
674 {
675 if (signal == nullptr)
676 {
677 return;
678 }
679 context.waveform_id = signal->getId();
680 context.waveform_name = signal->getName();
681 context.carrier_frequency = signal->getCarrier();
682 context.power = signal->getPower();
683 context.pulse_width = signal->getLength();
684 context.native_sample_rate = signal->getRate();
685 context.native_sample_count = signal->getSampleCount();
686 }
687
690 {
691 if (signal == nullptr)
692 {
693 return;
694 }
695 context.waveform_id = signal->getId();
696 context.waveform_name = signal->getName();
697 context.carrier_frequency = signal->getCarrier();
698 context.power = signal->getPower();
699 }
700
703 {
704 if (signal == nullptr)
705 {
706 return;
707 }
708 context.waveform_id = signal->getId();
709 context.waveform_name = signal->getName();
710 context.carrier_frequency = signal->getCarrier();
711 context.power = signal->getPower();
712 const auto* sfcw = signal->getSteppedFrequencySignal();
713 if (sfcw == nullptr)
714 {
715 return;
716 }
717 const RealType effective_bandwidth = sfcw->effectiveBandwidth();
718 const RealType step_size = std::abs(sfcw->getStepSize());
719 context.start_frequency_offset = sfcw->getStartFrequencyOffset();
720 context.step_size = sfcw->getStepSize();
721 context.step_count = static_cast<std::uint64_t>(sfcw->getStepCount());
722 context.dwell_time = sfcw->getDwellTime();
723 context.step_period = sfcw->getStepPeriod();
724 context.sweep_period = sfcw->getSweepPeriod();
725 context.sweep_count = sfcw->getSweepCount().has_value()
726 ? std::optional<std::uint64_t>(static_cast<std::uint64_t>(*sfcw->getSweepCount()))
727 : std::nullopt;
728 context.first_frequency = sfcw->firstFrequency(signal->getCarrier());
729 context.last_frequency = sfcw->lastFrequency(signal->getCarrier());
730 context.frequency_span = sfcw->frequencySpan();
731 context.effective_bandwidth = effective_bandwidth;
732 context.range_resolution = effective_bandwidth > 0.0 ? params::c() / (2.0 * effective_bandwidth) : 0.0;
733 context.unambiguous_range = step_size > 0.0 ? params::c() / (2.0 * step_size) : 0.0;
734 }
735
737 {
739 if (receiver == nullptr || receiver->getMode() != radar::OperationMode::PULSED_MODE)
740 {
741 return context;
742 }
743
744 context.present = true;
745 context.window_length = receiver->getWindowLength();
746 context.window_prf = receiver->getWindowPrf();
747 context.window_skip = receiver->getWindowSkip();
748 context.window_count = receiver->getWindowCount();
749 if (const auto* transmitter = attachedTransmitter(receiver); transmitter != nullptr)
750 {
751 populateWaveformIdentity(context, transmitter->getSignal());
752 }
753 if (context.carrier_frequency == 0.0)
754 {
755 if (const auto timing = receiver->getTiming(); timing)
756 {
757 context.carrier_frequency = timing->getFrequency();
758 }
759 }
760 return context;
761 }
762
764 {
766 if (receiver == nullptr || receiver->getMode() != radar::OperationMode::CW_MODE)
767 {
768 return context;
769 }
770
771 context.present = true;
772 if (const auto* transmitter = attachedTransmitter(receiver); transmitter != nullptr)
773 {
774 populateWaveformIdentity(context, transmitter->getSignal());
775 }
776 if (context.carrier_frequency == 0.0)
777 {
778 if (const auto timing = receiver->getTiming(); timing)
779 {
780 context.carrier_frequency = timing->getFrequency();
781 }
782 }
783 return context;
784 }
785
788 const std::span<const core::ActiveStreamingSource> streaming_sources)
789 {
791 if (receiver == nullptr || receiver->getMode() != radar::OperationMode::SFCW_MODE)
792 {
793 return context;
794 }
795
796 context.present = true;
797 if (const auto* transmitter = attachedTransmitter(receiver); transmitter != nullptr)
798 {
799 populateWaveformIdentity(context, transmitter->getSignal());
800 }
801 if (context.waveform_id == 0)
802 {
803 const auto found = std::ranges::find_if(streaming_sources, [](const core::ActiveStreamingSource& source)
804 { return source.is_sfcw && source.transmitter != nullptr; });
805 if (found != streaming_sources.end())
806 {
807 populateWaveformIdentity(context, found->transmitter->getSignal());
808 }
809 }
810 return context;
811 }
812
815 const std::span<const core::ActiveStreamingSource> streaming_sources)
816 {
818 if (receiver == nullptr || receiver->getMode() != radar::OperationMode::FMCW_MODE)
819 {
820 return context;
821 }
822
823 context.dechirp_mode = std::string(radar::dechirpModeToken(receiver->getDechirpMode()));
824 const auto& reference = receiver->getDechirpReference();
826 context.dechirp_reference_transmitter_id = reference.transmitter_id;
827 context.dechirp_reference_transmitter_name = reference.transmitter_name;
828 context.dechirp_reference_waveform_id = reference.waveform_id;
829 context.dechirp_reference_waveform_name = reference.waveform_name;
830
831 const auto* source = findFmcwContextSource(receiver, streaming_sources);
832 if (source == nullptr)
833 {
834 return context;
835 }
836
837 const auto waveform = buildFmcwMetadata(*source);
838 context.present = true;
839 context.waveform_shape = waveform.waveform_shape;
840 context.chirp_bandwidth = waveform.chirp_bandwidth;
841 context.chirp_duration = waveform.chirp_duration;
842 context.chirp_period = waveform.chirp_period;
843 context.chirp_rate = waveform.chirp_rate;
844 context.chirp_rate_signed = waveform.chirp_rate_signed;
845 context.sweep_direction =
846 source->kind == core::StreamingWaveformKind::FmcwTriangle ? "up_down" : waveform.chirp_direction;
847 context.start_frequency_offset = waveform.start_frequency_offset;
848 context.triangle_period = waveform.triangle_period;
849 context.chirp_count = waveform.chirp_count;
850 context.triangle_count = waveform.triangle_count;
851 return context;
852 }
853 }
854
856 const radar::Receiver* receiver, const std::string& output_path, const std::size_t total_samples,
857 const std::vector<core::ActiveStreamingSource>& streaming_sources, const RealType output_sample_rate)
858 {
859 const auto dechirp_time_spans =
860 receiver->isDechirpEnabled() ? dechirpActiveTimeSpans(receiver) : std::vector<TimeSpan>{};
863 }
864
867 const std::span<const core::ActiveStreamingSource> streaming_sources)
868 {
870 .receiver_name = receiver->getName(),
872 .sample_rate = sample_rate,
873 .bandwidth = sample_rate > 0.0 ? sample_rate / 2.0 : 0.0,
874 .dechirped = receiver->isDechirpEnabled(),
875 .if_resampled = receiver->getFmcwIfResamplerPlan().has_value(),
876 .adc_bits = params::adcBits(),
877 .coordinate = buildCoordinateContext(),
878 .initial_platform_state = buildInitialPlatformState(receiver),
879 .pulsed = buildPulsedContext(receiver),
883 if (const auto timing = receiver->getTiming(); timing)
884 {
885 descriptor.reference_frequency = timing->getFrequency();
886 }
887 return descriptor;
888 }
889
891 const RealType first_sample_time, const RealType sample_rate,
892 const std::span<const ComplexType> samples,
893 const std::uint64_t sample_start,
894 std::shared_ptr<const core::OutputFileMetadata> file_metadata)
895 {
896 return buildReceiverSampleBlock(receiver, first_sample_time, sample_rate, samples, sample_start,
897 std::span<const core::ActiveStreamingSource>{}, std::move(file_metadata));
898 }
899
902 const RealType sample_rate, const std::span<const ComplexType> samples,
903 const std::uint64_t sample_start,
904 const std::span<const core::ActiveStreamingSource> streaming_sources,
905 std::shared_ptr<const core::OutputFileMetadata> file_metadata)
906 {
909 .first_sample_time = first_sample_time,
910 .sample_rate = sample_rate,
911 .samples = samples,
912 .sample_start = sample_start,
913 .valid_data = true,
914 .calibrated_time = true,
915 .reference_lock = true,
916 .file_metadata = std::move(file_metadata)};
917 }
918
919 void runPulsedFinalizer(radar::Receiver* receiver, const std::vector<std::unique_ptr<radar::Target>>* targets,
920 const std::shared_ptr<core::ProgressReporter>& reporter, const std::string& output_dir,
921 const std::shared_ptr<core::OutputMetadataCollector>& metadata_collector,
923 {
924 (void)output_dir;
925 (void)metadata_collector;
926 if (output_sink == nullptr)
927 {
928 throw std::invalid_argument("runPulsedFinalizer requires a receiver output sink");
929 }
930
931 const auto timing_model = receiver->getTiming()->clone();
932 if (!timing_model)
933 {
934 LOG(logging::Level::FATAL, "Failed to clone timing model for receiver '{}'", receiver->getName());
935 return;
936 }
937
938 const auto sink_stream_id =
940 bool sink_stream_open = false;
941 std::uint64_t sink_sample_start = 0;
942
943 unsigned chunk_index = 0;
944
945 LOG(logging::Level::INFO, "Finalizer thread started for receiver '{}'. Routing to output sink.",
946 receiver->getName());
947
948 auto last_report_time = std::chrono::steady_clock::now();
949 const auto report_interval = std::chrono::milliseconds(100);
951 const RealType dt = 1.0 / rate;
953
954 while (true)
955 {
957 if (!receiver->waitAndDequeueFinalizerJob(job))
958 {
959 break; // Shutdown signal received
960 }
961
962 const auto window_samples = static_cast<unsigned>(std::ceil(job.duration * rate));
963 std::vector pnoise(window_samples, 0.0);
964
965 RealType actual_start = job.ideal_start_time;
966 RealType frac_delay = 0.0;
967
968 if (timing_model->isEnabled())
969 {
971 std::ranges::generate(pnoise, [&] { return timing_model->getNextSample(); });
973 job.ideal_start_time, pnoise[0], timing_model->getFrequency(), rate);
974 }
975
976 std::vector<ComplexType> window_buffer(window_samples);
977
979 job.active_streaming_sources, targets, streaming_tracker_cache);
980
982
983 if (timing_model->isEnabled())
984 {
986 }
987
990 receiver->getNoiseTemperature(receiver->getRotation(actual_start)),
991 receiver->getRngEngine(), params::rate());
992 if (!sink_stream_open)
993 {
995 sink_stream_open = true;
996 }
997 const auto block =
999 output_sink->submitBlock(block);
1000 sink_sample_start += static_cast<std::uint64_t>(window_buffer.size());
1001 ++chunk_index;
1002
1003 if (reporter)
1004 {
1005 const auto now = std::chrono::steady_clock::now();
1007 {
1008 reporter->report(std::format("Exporting {}: Chunk {}", receiver->getName(), chunk_index),
1009 static_cast<int>(chunk_index), 0);
1011 }
1012 }
1013 }
1014
1015 if (sink_stream_open)
1016 {
1017 output_sink->closeStream(sink_stream_id);
1018 }
1019
1020 if (reporter)
1021 {
1022 reporter->report(std::format("Finished Exporting {}", receiver->getName()), 100, 100);
1023 }
1024 LOG(logging::Level::INFO, "Finalizer thread for receiver '{}' finished.", receiver->getName());
1025 }
1026
1027}
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 effectiveBandwidth() const noexcept
Gets DFT-convention effective bandwidth in hertz.
const std::optional< std::size_t > & getSweepCount() const noexcept
Gets optional finite sweep count.
RealType getStepSize() const noexcept
Gets the uniform frequency step in hertz.
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.
RealType getStartFrequencyOffset() const noexcept
Gets the first-step offset from carrier in hertz.
std::size_t getStepCount() const noexcept
Gets the number of steps per sweep.
RealType getStepPeriod() const noexcept
Gets step repetition period in seconds.
RealType getDwellTime() const noexcept
Gets active dwell time per step in seconds.
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 countSfcwStepStarts(const ActiveStreamingSource &source, const RealType active_start, const RealType active_end)
Counts SFCW active dwells 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 > firstSfcwStepStart(const ActiveStreamingSource &source, const RealType active_start, const RealType active_end)
Returns the first SFCW step 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
RealType c() noexcept
Get the speed of light.
Definition parameters.h:91
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
@ SFCW_MODE
The component operates in a stepped-frequency CW streaming mode.
@ 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 carrier_freq
Cached carrier frequency in hertz.
const fers_signal::SteppedFrequencySignal * sfcw
Stable pointer to the stepped-frequency waveform, if any.
RealType segment_start
Segment start time in seconds.
RealType signed_chirp_rate
Cached signed FMCW chirp rate in hertz per second.
const radar::Transmitter * transmitter
Transmitter active during this segment.
RealType chirp_duration
Cached FMCW chirp duration in seconds.
bool is_sfcw
Compatibility flag for any stepped-frequency source.
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::vector< SfcwSourceMetadata > sfcw_sources
SFCW illuminators represented in the output.
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< SfcwMetadata > sfcw
Optional SFCW metadata for streaming outputs.
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.
SFCW waveform metadata captured for a streaming output file.
RealType carrier_frequency
Waveform carrier frequency in hertz.
Metadata for one SFCW illuminator represented in a streaming output file.
SimId transmitter_id
SFCW transmitter SimId.
SimId waveform_id
SFCW waveform SimId.
Metadata for one active SFCW transmitter schedule segment.
RealType start_time
Transmitter segment start time in seconds.
Metadata for one contiguous streaming output segment.
Timing source for simulation objects.
Header file for the Transmitter class in the radar namespace.