FERS 0.1.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
sim_threading.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-only
2//
3// Copyright (c) 2006-2008 Marc Brooker and Michael Inggs
4// Copyright (c) 2008-present FERS Contributors (see AUTHORS.md).
5//
6// See the GNU GPLv2 LICENSE file in the FERS project root for more information.
7
8/**
9 * @file sim_threading.cpp
10 * @brief Implements the core event-driven simulation engine.
11 *
12 * This file contains the primary simulation loop, which orchestrates the entire
13 * simulation process. It operates on a unified, event-driven model capable of
14 * handling both pulsed and continuous-wave (CW) radar systems concurrently.
15 */
16
17#include "sim_threading.h"
18
19#include <algorithm>
20#include <array>
21#include <atomic>
22#include <chrono>
23#include <cmath>
24#include <complex>
25#include <cstddef>
26#include <cstdint>
27#include <format>
28#include <limits>
29#include <optional>
30#include <utility>
31
32#include "logging.h"
33#include "math/path_utils.h"
34#include "memory_projection.h"
35#include "parameters.h"
39#include "radar/receiver.h"
40#include "radar/target.h"
41#include "radar/transmitter.h"
43#include "serial/response.h"
45#include "signal/if_resampler.h"
46#include "signal/radar_signal.h"
47#include "sim_events.h"
49#include "thread_pool.h"
50#include "timing/timing.h"
51#include "world.h"
52
53using logging::Level;
55using radar::Receiver;
57
58namespace core
59{
60 namespace
61 {
62 constexpr std::size_t fmcw_if_block_size = 1024;
63 constexpr std::size_t streaming_output_block_size = 4096;
64
65 [[nodiscard]] std::size_t expectedStreamingOutputSamples(const RealType sample_rate)
66 {
67 return static_cast<std::size_t>(
68 std::ceil(std::max<RealType>(0.0, params::endTime() - params::startTime()) * sample_rate));
69 }
70
71 [[nodiscard]] Vita49StreamMetadata streamStatsToMetadata(const ReceiverStreamStats& stats)
72 {
73 return Vita49StreamMetadata{.receiver_id = stats.receiver_id,
74 .receiver_name = stats.receiver_name,
75 .stream_id = stats.stream_id,
76 .mode = stats.mode,
77 .sample_rate = stats.sample_rate,
78 .reference_frequency = stats.reference_frequency,
79 .packets_emitted = stats.packets_emitted,
80 .samples_emitted = stats.samples_emitted,
81 .packets_dropped = stats.packets_dropped,
82 .samples_dropped = stats.samples_dropped,
83 .over_range_count = stats.over_range_count,
84 .late_packet_count = stats.late_packet_count,
85 .context_packet_count = stats.context_packets,
86 .first_sample_time = stats.first_sample_time,
87 .end_sample_time = stats.end_sample_time,
88 .first_timestamp = stats.first_timestamp,
89 .end_timestamp = stats.end_timestamp};
90 }
91
92 [[nodiscard]] std::string fmcwCountToken(const std::optional<std::size_t>& count)
93 {
94 return count.has_value() ? std::format("{}", *count) : std::string("unbounded");
95 }
96
98 const std::string& direction, const std::string& configured_count,
100 {
103 const auto total_chirp_count = countFmcwChirpStarts(source, active_start, source.segment_end);
104 LOG(Level::INFO,
105 "FMCW transmitter '{}' shape=linear {} B={} Hz T_c={} s T_rep={} s f_0={} Hz alpha={} Hz/s "
106 "duty_cycle={} chirp_count={} total_chirp_count={} average_power={} W",
110 }
111
113 const std::string& direction, const std::string& configured_count,
115 {
116 std::uint64_t total_chirp_count = 0;
117 for (const auto& period : transmitter.getSchedule())
118 {
119 const RealType active_start = std::max(params::startTime(), period.start);
120 const auto source =
121 makeActiveSource(&transmitter, period.start, std::min(params::endTime(), period.end));
122 const auto segment_chirp_count = countFmcwChirpStarts(source, active_start, source.segment_end);
124 LOG(Level::INFO,
125 "FMCW transmitter '{}' segment [{}, {}] shape=linear {} B={} Hz T_c={} s T_rep={} s f_0={} "
126 "Hz alpha={} Hz/s duty_cycle={} chirp_count={} segment_chirp_count={} total_chirp_count={} "
127 "average_power={} W",
128 transmitter.getName(), period.start, source.segment_end, direction, fmcw.getChirpBandwidth(),
131 }
132 }
133
136 {
137 const RealType duty_cycle = fmcw.getChirpDuration() / fmcw.getChirpPeriod();
138 const RealType average_power = waveform.getPower() * duty_cycle;
140 const auto configured_count = fmcwCountToken(fmcw.getChirpCount());
141 if (transmitter.getSchedule().empty())
142 {
145 return;
146 }
148 }
149
151 const fers_signal::FmcwTriangleSignal& triangle,
152 const std::string& configured_count, const RealType average_power)
153 {
156 const auto total_triangle_count = countFmcwTriangleStarts(source, active_start, source.segment_end);
157 LOG(Level::INFO,
158 "FMCW transmitter '{}' shape=triangle B={} Hz T_c={} s T_tri={} s f_0={} Hz alpha={} Hz/s "
159 "duty_cycle=1 triangle_count={} total_triangle_count={} average_power={} W",
160 transmitter.getName(), triangle.getChirpBandwidth(), triangle.getChirpDuration(),
161 triangle.getTrianglePeriod(), triangle.getStartFrequencyOffset(), triangle.getChirpRate(),
163 }
164
166 const fers_signal::FmcwTriangleSignal& triangle,
167 const std::string& configured_count, const RealType average_power)
168 {
169 std::uint64_t total_triangle_count = 0;
170 for (const auto& period : transmitter.getSchedule())
171 {
172 const RealType active_start = std::max(params::startTime(), period.start);
173 const auto source =
174 makeActiveSource(&transmitter, period.start, std::min(params::endTime(), period.end));
175 const auto segment_triangle_count = countFmcwTriangleStarts(source, active_start, source.segment_end);
177 LOG(Level::INFO,
178 "FMCW transmitter '{}' segment [{}, {}] shape=triangle B={} Hz T_c={} s T_tri={} s f_0={} "
179 "Hz alpha={} Hz/s duty_cycle=1 triangle_count={} segment_triangle_count={} "
180 "total_triangle_count={} average_power={} W",
181 transmitter.getName(), period.start, source.segment_end, triangle.getChirpBandwidth(),
182 triangle.getChirpDuration(), triangle.getTrianglePeriod(), triangle.getStartFrequencyOffset(),
185 }
186 }
187
189 const fers_signal::FmcwTriangleSignal& triangle)
190 {
191 const RealType average_power = waveform.getPower();
192 const auto configured_count = fmcwCountToken(triangle.getTriangleCount());
193 if (transmitter.getSchedule().empty())
194 {
196 return;
197 }
199 }
200
201 [[nodiscard]] bool isStreamingReceiver(const Receiver* const receiver) noexcept
202 {
203 return receiver != nullptr &&
204 (receiver->getMode() == OperationMode::CW_MODE || receiver->getMode() == OperationMode::FMCW_MODE);
205 }
206
207 [[nodiscard]] bool activePastUserEnd(const Receiver* const receiver) noexcept
208 {
209 if (receiver == nullptr)
210 {
211 return false;
212 }
213 if (receiver->getSchedule().empty())
214 {
215 return true;
216 }
217 return std::ranges::any_of(receiver->getSchedule(),
218 [](const auto& period) { return period.end > params::endTime(); });
219 }
220
221 [[nodiscard]] std::size_t streamingSampleIndexAtOrAfter(const RealType time, const RealType dt_sim)
222 {
223 if (dt_sim <= 0.0 || time <= params::startTime())
224 {
225 return 0;
226 }
227 return static_cast<std::size_t>(std::ceil((time - params::startTime()) / dt_sim));
228 }
229
230 struct PositionBounds
231 {
234 bool valid{false};
235 bool unbounded{false};
236 };
237
238 [[nodiscard]] bool isFinite(const math::Vec3& point) noexcept
239 {
240 return std::isfinite(point.x) && std::isfinite(point.y) && std::isfinite(point.z);
241 }
242
243 void includePoint(PositionBounds& bounds, const math::Vec3& point) noexcept
244 {
245 if (!isFinite(point))
246 {
247 bounds.unbounded = true;
248 return;
249 }
250 if (!bounds.valid)
251 {
252 bounds.min = point;
253 bounds.max = point;
254 bounds.valid = true;
255 return;
256 }
257 bounds.min.x = std::min(bounds.min.x, point.x);
258 bounds.min.y = std::min(bounds.min.y, point.y);
259 bounds.min.z = std::min(bounds.min.z, point.z);
260 bounds.max.x = std::max(bounds.max.x, point.x);
261 bounds.max.y = std::max(bounds.max.y, point.y);
262 bounds.max.z = std::max(bounds.max.z, point.z);
263 }
264
265 [[nodiscard]] RealType axisValue(const math::Vec3& point, const std::size_t axis) noexcept
266 {
267 switch (axis)
268 {
269 case 0:
270 return point.x;
271 case 1:
272 return point.y;
273 default:
274 return point.z;
275 }
276 }
277
278 [[nodiscard]] RealType axisValue(const std::array<RealType, 3>& values, const std::size_t axis) noexcept
279 {
280 switch (axis)
281 {
282 case 0:
283 return values[0];
284 case 1:
285 return values[1];
286 default:
287 return values[2];
288 }
289 }
290
291 [[nodiscard]] RealType& axisValue(std::array<RealType, 3>& values, const std::size_t axis) noexcept
292 {
293 switch (axis)
294 {
295 case 0:
296 return values[0];
297 case 1:
298 return values[1];
299 default:
300 return values[2];
301 }
302 }
303
304 [[nodiscard]] RealType axisDistanceBound(const PositionBounds& lhs, const PositionBounds& rhs,
305 const std::size_t axis) noexcept
306 {
307 const RealType lhs_min = axisValue(lhs.min, axis);
308 const RealType lhs_max = axisValue(lhs.max, axis);
309 const RealType rhs_min = axisValue(rhs.min, axis);
310 const RealType rhs_max = axisValue(rhs.max, axis);
311 return std::max(std::abs(lhs_max - rhs_min), std::abs(rhs_max - lhs_min));
312 }
313
314 [[nodiscard]] RealType maxDistanceBetweenBounds(const PositionBounds& lhs, const PositionBounds& rhs) noexcept
315 {
316 if (lhs.unbounded || rhs.unbounded || !lhs.valid || !rhs.valid)
317 {
318 return std::numeric_limits<RealType>::infinity();
319 }
320 const RealType dx = axisDistanceBound(lhs, rhs, 0);
321 const RealType dy = axisDistanceBound(lhs, rhs, 1);
322 const RealType dz = axisDistanceBound(lhs, rhs, 2);
323 return std::sqrt(dx * dx + dy * dy + dz * dz);
324 }
325
326 [[nodiscard]] std::array<RealType, 3> coordinateAxes(const math::Coord& coord) noexcept
327 {
328 return {coord.pos.x, coord.pos.y, coord.pos.z};
329 }
330
331 void includeCubicVelocityRoot(PositionBounds& bounds, const math::Path& path, const RealType segment_start,
333 const RealType upper_u)
334 {
336 {
337 return;
338 }
339 includePoint(bounds, path.getPosition(segment_start + root_u * segment_length));
340 }
341
342 void includeCubicPositionExtrema(PositionBounds& bounds, const math::Path& path,
343 const std::vector<math::Coord>& coords,
344 const std::vector<math::Coord>& second_derivatives, const std::size_t index,
345 const RealType lower_u, const RealType upper_u)
346 {
347 const RealType segment_length = coords[index + 1].t - coords[index].t;
348 if (segment_length <= EPSILON)
349 {
350 return;
351 }
352 const auto left = coordinateAxes(coords[index]);
353 const auto right = coordinateAxes(coords[index + 1]);
354 const auto dd_left = coordinateAxes(second_derivatives[index]);
355 const auto dd_right = coordinateAxes(second_derivatives[index + 1]);
357
358 for (std::size_t axis = 0; axis < 3; ++axis)
359 {
362 const RealType a = 0.5 * h2 * (dd_right_axis - dd_left_axis);
363 const RealType b = h2 * dd_left_axis;
364 const RealType c = (axisValue(right, axis) - axisValue(left, axis)) +
365 (h2 / 6.0) * (-2.0 * dd_left_axis - dd_right_axis);
366
367 if (std::abs(a) <= EPSILON)
368 {
369 if (std::abs(b) > EPSILON)
370 {
372 upper_u);
373 }
374 continue;
375 }
376
377 const RealType discriminant = b * b - 4.0 * a * c;
378 if (discriminant < -EPSILON)
379 {
380 continue;
381 }
382 const RealType sqrt_discriminant = std::sqrt(std::max(0.0, discriminant));
384 (-b - sqrt_discriminant) / (2.0 * a), lower_u, upper_u);
386 (-b + sqrt_discriminant) / (2.0 * a), lower_u, upper_u);
387 }
388 }
389
390 [[nodiscard]] PositionBounds pathPositionBounds(const math::Path& path, const RealType start,
391 const RealType end)
392 {
393 PositionBounds bounds;
394 if (start >= end)
395 {
396 return bounds;
397 }
398
399 try
400 {
401 includePoint(bounds, path.getPosition(start));
402 includePoint(bounds, path.getPosition(end));
403 }
404 catch (const math::PathException&)
405 {
406 bounds.unbounded = true;
407 return bounds;
408 }
409
410 const auto& coords = path.getCoords();
412 {
413 return bounds;
414 }
415
416 for (const auto& coord : coords)
417 {
418 if (coord.t >= start && coord.t <= end)
419 {
421 }
422 }
423
424 if (path.getType() != math::Path::InterpType::INTERP_CUBIC || coords.size() < 2)
425 {
426 return bounds;
427 }
428
429 std::vector<math::Coord> second_derivatives;
430 try
431 {
433 }
434 catch (const math::PathException&)
435 {
436 bounds.unbounded = true;
437 return bounds;
438 }
439
440 for (std::size_t index = 0; index + 1 < coords.size(); ++index)
441 {
442 const RealType segment_start = coords[index].t;
443 const RealType segment_end = coords[index + 1].t;
444 const RealType segment_length = segment_end - segment_start;
446 {
447 continue;
448 }
449
450 const RealType lower_u =
451 std::clamp((std::max(start, segment_start) - segment_start) / segment_length, 0.0, 1.0);
452 const RealType upper_u =
453 std::clamp((std::min(end, segment_end) - segment_start) / segment_length, 0.0, 1.0);
454 if (lower_u <= upper_u)
455 {
457 }
458 }
459 return bounds;
460 }
461
462 struct QuadraticVelocityExtremum
463 {
471 };
472
473 void includeQuadraticVelocityExtremum(std::array<RealType, 3>& max_abs_velocity, const std::size_t axis,
474 const QuadraticVelocityExtremum& extremum) noexcept
475 {
476 if (extremum.root_u < extremum.lower_u || extremum.root_u > extremum.upper_u ||
477 extremum.segment_length <= EPSILON)
478 {
479 return;
480 }
481 const RealType velocity =
482 (extremum.a * extremum.root_u * extremum.root_u + extremum.b * extremum.root_u + extremum.c) /
483 extremum.segment_length;
484 if (std::isfinite(velocity))
485 {
487 axis_max_velocity = std::max(axis_max_velocity, std::abs(velocity));
488 }
489 else
490 {
491 axisValue(max_abs_velocity, axis) = std::numeric_limits<RealType>::infinity();
492 }
493 }
494
495 void includeCubicVelocityBounds(std::array<RealType, 3>& max_abs_velocity,
496 const std::vector<math::Coord>& coords,
497 const std::vector<math::Coord>& second_derivatives, const std::size_t index,
498 const RealType lower_u, const RealType upper_u)
499 {
500 const RealType segment_length = coords[index + 1].t - coords[index].t;
501 if (segment_length <= EPSILON)
502 {
503 return;
504 }
505 const auto left = coordinateAxes(coords[index]);
506 const auto right = coordinateAxes(coords[index + 1]);
507 const auto dd_left = coordinateAxes(second_derivatives[index]);
508 const auto dd_right = coordinateAxes(second_derivatives[index + 1]);
510
511 for (std::size_t axis = 0; axis < 3; ++axis)
512 {
515 const RealType a = 0.5 * h2 * (dd_right_axis - dd_left_axis);
516 const RealType b = h2 * dd_left_axis;
517 const RealType c = (axisValue(right, axis) - axisValue(left, axis)) +
518 (h2 / 6.0) * (-2.0 * dd_left_axis - dd_right_axis);
520 QuadraticVelocityExtremum{.a = a,
521 .b = b,
522 .c = c,
523 .segment_length = segment_length,
524 .root_u = lower_u,
525 .lower_u = lower_u,
526 .upper_u = upper_u});
528 QuadraticVelocityExtremum{.a = a,
529 .b = b,
530 .c = c,
531 .segment_length = segment_length,
532 .root_u = upper_u,
533 .lower_u = lower_u,
534 .upper_u = upper_u});
535
536 if (std::abs(a) > EPSILON)
537 {
539 QuadraticVelocityExtremum{.a = a,
540 .b = b,
541 .c = c,
542 .segment_length = segment_length,
543 .root_u = -b / (2.0 * a),
544 .lower_u = lower_u,
545 .upper_u = upper_u});
546 }
547 }
548 }
549
550 [[nodiscard]] RealType pathSpeedBound(const math::Path& path, const RealType start, const RealType end)
551 {
552 if (start >= end)
553 {
554 return 0.0;
555 }
556
557 const auto& coords = path.getCoords();
558 if (coords.empty() || path.getType() == math::Path::InterpType::INTERP_STATIC || coords.size() < 2)
559 {
560 return 0.0;
561 }
562
564 {
565 RealType max_speed = 0.0;
566 for (std::size_t index = 0; index + 1 < coords.size(); ++index)
567 {
568 const RealType segment_start = coords[index].t;
569 const RealType segment_end = coords[index + 1].t;
570 const RealType segment_length = segment_end - segment_start;
572 {
573 continue;
574 }
575 max_speed =
576 std::max(max_speed, (coords[index + 1].pos - coords[index].pos).length() / segment_length);
577 }
578 return max_speed;
579 }
580
581 std::vector<math::Coord> second_derivatives;
582 try
583 {
585 }
586 catch (const math::PathException&)
587 {
588 return std::numeric_limits<RealType>::infinity();
589 }
590
591 std::array<RealType, 3> max_abs_velocity{0.0, 0.0, 0.0};
592 for (std::size_t index = 0; index + 1 < coords.size(); ++index)
593 {
594 const RealType segment_start = coords[index].t;
595 const RealType segment_end = coords[index + 1].t;
596 const RealType segment_length = segment_end - segment_start;
598 {
599 continue;
600 }
601 const RealType lower_u =
602 std::clamp((std::max(start, segment_start) - segment_start) / segment_length, 0.0, 1.0);
603 const RealType upper_u =
604 std::clamp((std::min(end, segment_end) - segment_start) / segment_length, 0.0, 1.0);
605 if (lower_u <= upper_u)
606 {
608 }
609 }
610 return std::sqrt(max_abs_velocity[0] * max_abs_velocity[0] + max_abs_velocity[1] * max_abs_velocity[1] +
612 }
613
614 [[nodiscard]] std::optional<RealType>
618 {
621 {
622 return std::nullopt;
623 }
624
626 {
629 {
630 return std::nullopt;
631 }
632
636 {
637 return std::nullopt;
638 }
639 return std::min(interval_end, deadline);
640 }
641
642 if (!std::isfinite(max_delay_bound))
643 {
644 return interval_end;
645 }
648 {
649 return std::nullopt;
650 }
651 return deadline;
652 }
653
654 [[nodiscard]] std::optional<RealType> directPathCleanupDeadline(const ActiveStreamingSource& source,
655 const Receiver* const rx,
658 {
659 const auto* const tx = source.transmitter;
660 if (tx == nullptr || rx == nullptr || tx->getPlatform() == rx->getPlatform() || params::c() <= 0.0)
661 {
662 return std::nullopt;
663 }
664
665 const auto* const tx_path = tx->getPlatform()->getMotionPath();
666 const auto* const rx_path = rx->getPlatform()->getMotionPath();
668 (rx_path->getPosition(interval_start) - tx_path->getPosition(interval_start)).length();
672 params::c();
675 return deadlineFromTailKinematics(source.segment_end, interval_start, interval_end,
677 }
678
679 [[nodiscard]] std::optional<RealType> reflectedPathCleanupDeadline(const ActiveStreamingSource& source,
680 const Receiver* const rx,
681 const radar::Target* const target,
684 {
685 const auto* const tx = source.transmitter;
686 if (tx == nullptr || rx == nullptr || target == nullptr || params::c() <= 0.0 ||
687 tx->getPlatform() == target->getPlatform() || rx->getPlatform() == target->getPlatform())
688 {
689 return std::nullopt;
690 }
691
692 const auto* const tx_path = tx->getPlatform()->getMotionPath();
693 const auto* const rx_path = rx->getPlatform()->getMotionPath();
694 const auto* const target_path = target->getPlatform()->getMotionPath();
695 const auto tx_position = tx_path->getPosition(interval_start);
696 const auto rx_position = rx_path->getPosition(interval_start);
697 const auto target_position = target_path->getPosition(interval_start);
699 (target_position - tx_position).length() + (rx_position - target_position).length();
700
706 params::c();
711
712 return deadlineFromTailKinematics(source.segment_end, interval_start, interval_end,
714 }
715
716 /// Builds an active streaming source for a transmitter at an event timestamp.
717 std::optional<ActiveStreamingSource> streamingSourceAtEvent(const Transmitter* const transmitter,
718 const RealType timestamp,
720 {
721 if (transmitter == nullptr || !transmitter->isStreamingMode())
722 {
723 return std::nullopt;
724 }
725
726 const auto& schedule = transmitter->getSchedule();
727 if (schedule.empty())
728 {
729 const RealType segment_start = params::startTime();
730 auto source = makeActiveSource(transmitter, segment_start, internal_stop_time);
731 if (timestamp >= segment_start && timestamp < source.segment_end)
732 {
733 return source;
734 }
735 return std::nullopt;
736 }
737
738 // TODO: O(N) Schedule Lookups - Since the schedule is guaranteed to be sorted (enforced by
739 // `processRawSchedule`), should be using `std::lower_bound` or binary search to find the relevant period in
740 // $O(\log N)$ time.
741 for (const auto& period : schedule)
742 {
743 const RealType active_start = std::max(params::startTime(), period.start);
744 auto source = makeActiveSource(transmitter, period.start, std::min(internal_stop_time, period.end));
745 if (timestamp >= active_start && timestamp < source.segment_end)
746 {
747 return source;
748 }
749 }
750 return std::nullopt;
751 }
752 }
753
754 SimulationEngine::SimulationEngine(World* world, pool::ThreadPool& pool, std::shared_ptr<ProgressReporter> reporter,
755 std::string output_dir,
756 std::shared_ptr<OutputMetadataCollector> metadata_collector,
757 ReceiverOutputSink* output_sink, std::function<bool()> cancel_callback,
758 const bool eager_context_stream_open) :
759 _world(world), _pool(pool), _reporter(std::move(reporter)), _metadata_collector(std::move(metadata_collector)),
760 _output_sink(output_sink), _cancel_callback(std::move(cancel_callback)),
761 _eager_context_stream_open(eager_context_stream_open), _last_report_time(std::chrono::steady_clock::now()),
762 _next_context_heartbeat_time(params::startTime() + 1.0), _output_dir(std::move(output_dir)),
763 _internal_stop_time(params::endTime())
764 {
765 _streaming_tracker_caches.resize(_world->getReceivers().size());
766 _if_pulse_tracker_caches.resize(_world->getReceivers().size());
767 _fmcw_if_block_buffers.resize(_world->getReceivers().size());
768 _fmcw_if_block_start_times.resize(_world->getReceivers().size(), params::startTime());
769 _streaming_output_block_buffers.resize(_world->getReceivers().size());
770 _streaming_output_processed_buffers.resize(_world->getReceivers().size());
771 _streaming_output_block_start_times.resize(_world->getReceivers().size(), params::startTime());
772 _streaming_output_block_start_indices.resize(_world->getReceivers().size(), 0);
773 _streaming_downsamplers.resize(_world->getReceivers().size());
774 _streaming_downsample_base_indices.resize(_world->getReceivers().size(), 0);
775 _streaming_downsample_segment_start_times.resize(_world->getReceivers().size(), params::startTime());
776 _streaming_output_sample_cursors.resize(_world->getReceivers().size(), 0);
777 _streaming_output_stream_ids.resize(_world->getReceivers().size(), 0);
778 _streaming_output_stream_open.resize(_world->getReceivers().size(), false);
779 _streaming_output_file_metadata.resize(_world->getReceivers().size());
780 for (auto& block : _streaming_output_block_buffers)
781 {
783 }
784 for (auto& block : _streaming_output_processed_buffers)
785 {
787 }
788 }
789
791 {
792 if (_reporter)
793 {
794 _reporter->report("Initializing event-driven simulation...", 0, 100);
795 }
796
797 initializeFmcwIfResamplers();
798
800
801 initializeFinalizers();
802
803 LOG(Level::INFO, "Starting unified event-driven simulation loop.");
804 logStreamingSummaries();
805
806 auto& event_queue = _world->getEventQueue();
807 auto& state = _world->getSimulationState();
808 const RealType end_time = _internal_stop_time;
809
810 while (!event_queue.empty() && state.t_current <= end_time)
811 {
812 if (isCancellationRequested())
813 {
814 break;
815 }
816 const Event event = event_queue.top();
817 event_queue.pop();
818
819 processStreamingPhysics(event.timestamp);
820 if (isCancellationRequested())
821 {
822 break;
823 }
824 flushFmcwIfBlocks();
825 flushStreamingOutputBlocks();
826
827 state.t_current = event.timestamp;
828
829 processEvent(event);
830 updateProgress();
831 }
832
833 const bool has_active_if_overrender =
834 std::ranges::any_of(_world->getReceivers(), [](const auto& receiver)
835 { return receiver->isActive() && receiver->hasFmcwIfResamplingSink(); });
836 if (!isCancellationRequested() && has_active_if_overrender)
837 {
838 processStreamingPhysics(end_time);
839 }
840 flushFmcwIfBlocks();
841 flushStreamingOutputBlocks();
842
843 shutdown();
844 }
845
846 void SimulationEngine::logStreamingSummaries() const
847 {
848 for (const auto& transmitter_ptr : _world->getTransmitters())
849 {
850 const auto* waveform = transmitter_ptr->getSignal();
851 if (waveform == nullptr || !waveform->isFmcwFamily())
852 {
853 continue;
854 }
855
856 if (const auto* fmcw = waveform->getFmcwChirpSignal(); fmcw != nullptr)
857 {
858 logFmcwChirpSummary(*transmitter_ptr, *waveform, *fmcw);
859 }
860 else if (const auto* triangle = waveform->getFmcwTriangleSignal(); triangle != nullptr)
861 {
862 logFmcwTriangleSummary(*transmitter_ptr, *waveform, *triangle);
863 }
864 }
865 }
866
867 void SimulationEngine::initializeFinalizers()
868 {
869 if (_output_sink == nullptr)
870 {
871 return;
872 }
873 for (const auto& receiver_ptr : _world->getReceivers())
874 {
875 if (receiver_ptr->getMode() == OperationMode::PULSED_MODE)
876 {
877 _finalizer_threads.emplace_back(processing::runPulsedFinalizer, receiver_ptr.get(),
878 &_world->getTargets(), _reporter, _output_dir, _metadata_collector,
879 _output_sink);
880 }
881 }
882 }
883
884 void SimulationEngine::initializeFmcwIfResamplers()
885 {
886 _internal_stop_time = params::endTime();
887 for (std::size_t receiver_index = 0; receiver_index < _world->getReceivers().size(); ++receiver_index)
888 {
889 initializeFmcwIfResampler(receiver_index);
890 }
891
892 if (_internal_stop_time > params::endTime())
893 {
894 extendDechirpSourcesForIfOverrender();
895 }
896 }
897
898 void SimulationEngine::initializeFmcwIfResampler(const std::size_t receiver_index)
899 {
900 const auto& receiver_ptr = _world->getReceivers()[receiver_index];
901 if (!receiver_ptr->isDechirpEnabled() || !receiver_ptr->hasFmcwIfSampleRate())
902 {
903 return;
904 }
905
906 const auto& request = receiver_ptr->getFmcwIfChainRequest();
907 const RealType output_rate = request.sample_rate_hz.value_or(0.0);
908 const RealType bandwidth = request.filter_bandwidth_hz.value_or(0.40 * output_rate);
910 .input_sample_rate_hz = params::rate() * static_cast<RealType>(params::oversampleRatio()),
911 .output_sample_rate_hz = output_rate,
912 .filter_bandwidth_hz = bandwidth,
913 .filter_transition_width_hz = request.filter_transition_width_hz};
915 const RealType block_time = static_cast<RealType>(fmcw_if_block_size) / resampler_request.input_sample_rate_hz;
916 const RealType over_render = plan.group_delay_seconds + 1.0 / plan.actual_output_sample_rate_hz + block_time;
917 _internal_stop_time = std::max(_internal_stop_time, params::endTime() + over_render);
918 if (_output_sink != nullptr)
919 {
920 receiver_ptr->setFmcwIfOutputCallback(
921 [this, receiver_index](const std::span<const ComplexType> samples, const std::uint64_t sample_start)
922 {
923 const auto& receiver = _world->getReceivers()[receiver_index];
924 const auto& if_plan = receiver->getFmcwIfResamplerPlan();
925 if (!if_plan.has_value())
926 {
927 return;
928 }
929 const RealType first_sample_time = params::startTime() +
930 static_cast<RealType>(sample_start) / if_plan->actual_output_sample_rate_hz;
931 emitStreamingOutputBlock(receiver_index, first_sample_time, if_plan->actual_output_sample_rate_hz,
932 samples, sample_start);
933 });
934 }
935 const RealType actual_output_sample_rate_hz = plan.actual_output_sample_rate_hz;
936 const auto overall_ratio = plan.overall_ratio;
937 const RealType filter_bandwidth_hz = plan.filter_bandwidth_hz;
938 const RealType filter_transition_width_hz = plan.filter_transition_width_hz;
939 receiver_ptr->initializeFmcwIfResampling(std::move(plan));
940 LOG(Level::INFO,
941 "Receiver '{}' enabled FMCW IF resampling: input_rate={} Hz requested_output_rate={} Hz "
942 "actual_output_rate={} Hz ratio={}/{} passband={} Hz transition={} Hz.",
943 receiver_ptr->getName(), resampler_request.input_sample_rate_hz, output_rate, actual_output_sample_rate_hz,
944 overall_ratio.numerator, overall_ratio.denominator, filter_bandwidth_hz, filter_transition_width_hz);
945 }
946
947 void SimulationEngine::extendDechirpSourcesForIfOverrender()
948 {
949 for (const auto& receiver_ptr : _world->getReceivers())
950 {
951 if (!receiver_ptr->hasFmcwIfSampleRate())
952 {
953 continue;
954 }
955
956 auto dechirp_sources = receiver_ptr->getDechirpSources();
957 for (auto& source : dechirp_sources)
958 {
959 if (std::abs(source.segment_end - params::endTime()) > 1.0e-12)
960 {
961 continue;
962 }
963 if (source.transmitter == nullptr || source.transmitter->getSchedule().empty())
964 {
965 source.segment_end = _internal_stop_time;
966 continue;
967 }
968 for (const auto& period : source.transmitter->getSchedule())
969 {
970 if (period.start <= params::endTime() && period.end > params::endTime())
971 {
972 source.segment_end = std::min(_internal_stop_time, period.end);
973 break;
974 }
975 }
976 }
977 receiver_ptr->setResolvedDechirpSources(std::move(dechirp_sources));
978 }
979 }
980
981 void SimulationEngine::ensureCwPhaseNoiseLookup()
982 {
983 if (_cw_phase_noise_lookup)
984 {
985 return;
986 }
987
988 const auto timings = collectCwPhaseNoiseTimings(*_world);
990 for (const auto& source : _world->getSimulationState().active_streaming_transmitters)
991 {
992 lookup_start = std::min(lookup_start, source.segment_start);
993 }
994 _cw_phase_noise_lookup = std::make_unique<simulation::CwPhaseNoiseLookup>(
995 simulation::CwPhaseNoiseLookup::build(timings, lookup_start, _internal_stop_time));
996 }
997
999 {
1000 auto& state = _world->getSimulationState();
1001 auto& t_current = state.t_current;
1002
1003 if (t_event <= t_current)
1004 {
1005 return;
1006 }
1007
1009 const auto first_index = streamingSampleIndexAtOrAfter(t_current, dt_sim);
1011 const auto sample_count = final_index - first_index;
1012 const auto progress_report_stride = std::max<std::size_t>(1, sample_count / 1000);
1013
1014 ensureCwPhaseNoiseLookup();
1015
1016 while (t_current < t_event && !isCancellationRequested())
1017 {
1018 cleanupInactiveStreamingSources(t_current);
1019
1020 const RealType chunk_end = streamingChunkEnd(t_current, t_event);
1021 if (chunk_end <= t_current)
1022 {
1023 break;
1024 }
1025
1026 const auto start_index = streamingSampleIndexAtOrAfter(t_current, dt_sim);
1029 {
1030 if (shouldStopStreamingChunk(sample_index, start_index))
1031 {
1032 break;
1033 }
1035 }
1036
1037 t_current = chunk_end;
1038 emitContextHeartbeatsThrough(t_current);
1039 }
1040 cleanupInactiveStreamingSources(t_current);
1041 }
1042
1043 std::optional<RealType> SimulationEngine::nextStreamingCleanupDeadline(const RealType from_time)
1044 {
1045 const auto& active_streaming_transmitters = _world->getSimulationState().active_streaming_transmitters;
1046 std::optional<RealType> next_deadline;
1047 for (const auto& source : active_streaming_transmitters)
1048 {
1049 if (source.segment_end > from_time)
1050 {
1051 continue;
1052 }
1053 const auto cleanup_deadline = streamingSourceCleanupDeadline(source, from_time);
1054 if (cleanup_deadline.has_value() && *cleanup_deadline > from_time &&
1055 (!next_deadline.has_value() || *cleanup_deadline < *next_deadline))
1056 {
1058 }
1059 }
1060 return next_deadline;
1061 }
1062
1063 RealType SimulationEngine::streamingChunkEnd(const RealType from_time, const RealType event_time)
1064 {
1065 if (const auto cleanup_deadline = nextStreamingCleanupDeadline(from_time);
1067 {
1068 return *cleanup_deadline;
1069 }
1070 return event_time;
1071 }
1072
1073 bool SimulationEngine::shouldStopStreamingChunk(const std::size_t sample_index, const std::size_t chunk_start_index)
1074 {
1075 return ((sample_index - chunk_start_index) % 1024) == 0 && isCancellationRequested();
1076 }
1077
1078 void SimulationEngine::processStreamingSample(const std::size_t sample_index, const std::size_t first_index,
1079 const std::size_t final_index,
1080 const std::size_t progress_report_stride, const RealType dt_sim)
1081 {
1082 const RealType t_step = params::startTime() + static_cast<RealType>(sample_index) * dt_sim;
1083 appendActiveReceiverStreamingSamples(sample_index, t_step);
1084
1085 if (_output_sink != nullptr && t_step >= _next_context_heartbeat_time)
1086 {
1087 emitContextHeartbeatsThrough(t_step);
1088 }
1090 {
1091 reportSimulationProgress(t_step);
1092 }
1093 }
1094
1095 void SimulationEngine::appendActiveReceiverStreamingSamples(const std::size_t sample_index, const RealType t_step)
1096 {
1097 for (std::size_t receiver_index = 0; receiver_index < _world->getReceivers().size(); ++receiver_index)
1098 {
1099 appendReceiverStreamingSample(receiver_index, sample_index, t_step);
1100 }
1101 }
1102
1103 void SimulationEngine::appendReceiverStreamingSample(const std::size_t receiver_index,
1104 const std::size_t sample_index, const RealType t_step)
1105 {
1106 const auto& receiver_ptr = _world->getReceivers()[receiver_index];
1107 if ((receiver_ptr->getMode() != OperationMode::CW_MODE &&
1108 receiver_ptr->getMode() != OperationMode::FMCW_MODE) ||
1109 !receiver_ptr->isActive())
1110 {
1111 return;
1112 }
1113
1114 const auto& active_streaming_transmitters = _world->getSimulationState().active_streaming_transmitters;
1115 ComplexType const sample = calculateStreamingSample(receiver_ptr.get(), t_step, active_streaming_transmitters,
1116 _streaming_tracker_caches[receiver_index]);
1117 if (receiver_ptr->hasFmcwIfResamplingSink())
1118 {
1119 appendFmcwIfSample(receiver_index, t_step, sample);
1120 }
1121 else if (_output_sink != nullptr)
1122 {
1123 appendStreamingOutputSample(receiver_index, sample_index, t_step, sample);
1124 }
1125 }
1126
1127 void SimulationEngine::appendFmcwIfSample(const std::size_t receiver_index, const RealType t_step,
1128 const ComplexType sample)
1129 {
1130 auto& block = _fmcw_if_block_buffers[receiver_index];
1131 if (block.empty())
1132 {
1133 _fmcw_if_block_start_times[receiver_index] = t_step;
1134 }
1135 block.push_back(sample);
1136 if (block.size() >= fmcw_if_block_size)
1137 {
1138 flushFmcwIfBlock(receiver_index);
1139 }
1140 }
1141
1142 void SimulationEngine::appendStreamingOutputSample(const std::size_t receiver_index, const std::size_t sample_index,
1143 const RealType t_step, const ComplexType sample)
1144 {
1145 if (_eager_context_stream_open)
1146 {
1147 ensureStreamingOutputStreamOpen(receiver_index, t_step, streamingOutputSampleRate(receiver_index));
1148 }
1149 auto& block = _streaming_output_block_buffers[receiver_index];
1150 if (block.empty())
1151 {
1152 _streaming_output_block_start_times[receiver_index] = t_step;
1153 _streaming_output_block_start_indices[receiver_index] = static_cast<std::uint64_t>(sample_index);
1154 }
1155 block.push_back(sample);
1156 if (block.size() >= streaming_output_block_size)
1157 {
1158 flushStreamingOutputBlock(receiver_index);
1159 }
1160 }
1161
1162 void SimulationEngine::flushStreamingOutputBlocks()
1163 {
1164 for (std::size_t receiver_index = 0; receiver_index < _streaming_output_block_buffers.size(); ++receiver_index)
1165 {
1166 flushStreamingOutputBlock(receiver_index);
1167 }
1168 }
1169
1170 void SimulationEngine::flushStreamingOutputBlock(const std::size_t receiver_index, const bool finish_downsampler)
1171 {
1172 if (_output_sink == nullptr || receiver_index >= _world->getReceivers().size())
1173 {
1174 return;
1175 }
1176
1177 auto& block = _streaming_output_block_buffers[receiver_index];
1178 if (block.empty())
1179 {
1180 if (finish_downsampler && _streaming_downsamplers[receiver_index])
1181 {
1182 auto& downsampler = *_streaming_downsamplers[receiver_index];
1183 const auto output_start_index = downsampler.outputSampleCount();
1184 downsampler.finish();
1185 auto output = downsampler.takeOutput();
1186 if (!output.empty())
1187 {
1189 const RealType output_start_time = _streaming_downsample_segment_start_times[receiver_index] +
1191 emitStreamingOutputBlock(receiver_index, output_start_time, output_sample_rate, output,
1192 _streaming_downsample_base_indices[receiver_index] + output_start_index);
1193 }
1194 _streaming_downsamplers[receiver_index].reset();
1195 }
1196 return;
1197 }
1198
1199 const auto& receiver = _world->getReceivers()[receiver_index];
1200 const bool dechirped = receiver->isDechirpEnabled();
1202 const RealType block_start_time = _streaming_output_block_start_times[receiver_index];
1203 const auto input_start_index = _streaming_output_block_start_indices[receiver_index];
1204
1205 applyPulsedInterferenceToStreamingBlock(receiver_index, block, block_start_time, input_sample_rate, dechirped);
1206
1209 std::uint64_t output_sample_start = input_start_index;
1210 std::vector<ComplexType> downsampled_block;
1211 if (!dechirped && params::oversampleRatio() > 1)
1212 {
1213 auto& downsampler = streamingDownsampler(receiver_index, input_start_index, block_start_time);
1214 const auto output_start_index = downsampler.outputSampleCount();
1215 downsampler.consume(block);
1217 {
1218 downsampler.finish();
1219 }
1220 downsampled_block = downsampler.takeOutput();
1222 output_sample_start = _streaming_downsample_base_indices[receiver_index] + output_start_index;
1223 output_start_time = _streaming_downsample_segment_start_times[receiver_index] +
1225 }
1226 else if (!dechirped)
1227 {
1231 }
1232
1233 const auto output_samples = !downsampled_block.empty()
1234 ? std::span<const ComplexType>(downsampled_block.data(), downsampled_block.size())
1235 : std::span<const ComplexType>(block.data(), block.size());
1236 if (!output_samples.empty() && (!downsampled_block.empty() || dechirped || params::oversampleRatio() <= 1))
1237 {
1240 }
1241 block.clear();
1242 if (finish_downsampler && _streaming_downsamplers[receiver_index])
1243 {
1244 _streaming_downsamplers[receiver_index].reset();
1245 }
1246 }
1247
1248 fers_signal::DownsamplingSink& SimulationEngine::streamingDownsampler(const std::size_t receiver_index,
1249 const std::uint64_t input_start_index,
1251 {
1252 if (!_streaming_downsamplers[receiver_index])
1253 {
1254 _streaming_downsamplers[receiver_index] = std::make_unique<fers_signal::DownsamplingSink>();
1255 _streaming_downsample_base_indices[receiver_index] =
1256 input_start_index / std::max<unsigned>(1, _streaming_downsamplers[receiver_index]->ratio());
1257 _streaming_downsample_segment_start_times[receiver_index] = segment_start_time;
1258 }
1259 return *_streaming_downsamplers[receiver_index];
1260 }
1261
1262 RealType SimulationEngine::streamingOutputSampleRate(const std::size_t receiver_index) const
1263 {
1264 if (receiver_index >= _world->getReceivers().size())
1265 {
1266 return 0.0;
1267 }
1268
1269 const auto& receiver = _world->getReceivers()[receiver_index];
1271 {
1272 const auto& if_plan = receiver->getFmcwIfResamplerPlan();
1273 return if_plan.has_value() ? if_plan->actual_output_sample_rate_hz : 0.0;
1274 }
1276 {
1277 return params::rate() * static_cast<RealType>(params::oversampleRatio());
1278 }
1279 return params::rate();
1280 }
1281
1282 void SimulationEngine::ensureStreamingOutputStreamOpen(const std::size_t receiver_index,
1283 const RealType first_sample_time, const RealType sample_rate)
1284 {
1285 if (_output_sink == nullptr || receiver_index >= _world->getReceivers().size() || sample_rate <= 0.0)
1286 {
1287 return;
1288 }
1289 if (_streaming_output_stream_ids[receiver_index] != 0 && _streaming_output_stream_open[receiver_index] &&
1290 _streaming_output_file_metadata[receiver_index])
1291 {
1292 return;
1293 }
1294
1295 const auto& receiver = _world->getReceivers()[receiver_index];
1296 auto streaming_sources = collectStreamingSourcesForWindow(params::startTime(), params::endTime());
1297 if (_streaming_output_stream_ids[receiver_index] == 0)
1298 {
1299 _streaming_output_stream_ids[receiver_index] = _output_sink->registerStream(
1301 }
1302 if (!_streaming_output_file_metadata[receiver_index])
1303 {
1304 _streaming_output_file_metadata[receiver_index] =
1305 std::make_shared<OutputFileMetadata>(processing::buildStreamingOutputMetadata(
1306 receiver.get(), "", expectedStreamingOutputSamples(sample_rate), streaming_sources, sample_rate));
1307 }
1308 if (!_streaming_output_stream_open[receiver_index])
1309 {
1310 _output_sink->openStream(_streaming_output_stream_ids[receiver_index], first_sample_time);
1311 _streaming_output_stream_open[receiver_index] = true;
1312 }
1313 }
1314
1315 void SimulationEngine::emitStreamingOutputBlock(const std::size_t receiver_index, const RealType first_sample_time,
1316 const RealType sample_rate,
1317 const std::span<const ComplexType> samples,
1318 const std::uint64_t sample_start)
1319 {
1320 if (_output_sink == nullptr || samples.empty() || receiver_index >= _world->getReceivers().size())
1321 {
1322 return;
1323 }
1324
1325 const auto& receiver = _world->getReceivers()[receiver_index];
1326 auto& processed = _streaming_output_processed_buffers[receiver_index];
1327 processed.assign(samples.begin(), samples.end());
1329 sample_rate);
1330
1331 auto streaming_sources = collectStreamingSourcesForWindow(params::startTime(), params::endTime());
1332 ensureStreamingOutputStreamOpen(receiver_index, first_sample_time, sample_rate);
1333
1334 const auto block = processing::buildReceiverSampleBlock(receiver.get(), first_sample_time, sample_rate,
1335 processed, sample_start, streaming_sources,
1336 _streaming_output_file_metadata[receiver_index]);
1337 _output_sink->submitBlock(block);
1338 _streaming_output_sample_cursors[receiver_index] = sample_start + static_cast<std::uint64_t>(processed.size());
1339 }
1340
1341 void SimulationEngine::emitContextHeartbeatsThrough(const RealType simulation_time)
1342 {
1343 if (_output_sink == nullptr)
1344 {
1345 return;
1346 }
1347 if (_next_context_heartbeat_time > simulation_time)
1348 {
1349 return;
1350 }
1351
1352 if (simulation_time - _next_context_heartbeat_time < 1.0)
1353 {
1354 _output_sink->emitContextHeartbeat(_next_context_heartbeat_time);
1355 _next_context_heartbeat_time += 1.0;
1356 return;
1357 }
1358
1360 _next_context_heartbeat_time = simulation_time + 1.0;
1361 }
1362
1363 void SimulationEngine::flushFmcwIfBlocks()
1364 {
1365 for (std::size_t receiver_index = 0; receiver_index < _fmcw_if_block_buffers.size(); ++receiver_index)
1366 {
1367 flushFmcwIfBlock(receiver_index);
1368 }
1369 }
1370
1371 void SimulationEngine::flushFmcwIfBlock(const std::size_t receiver_index)
1372 {
1373 if (receiver_index >= _world->getReceivers().size())
1374 {
1375 return;
1376 }
1377 auto& block = _fmcw_if_block_buffers[receiver_index];
1378 if (block.empty())
1379 {
1380 return;
1381 }
1382 const auto& receiver = _world->getReceivers()[receiver_index];
1384 {
1385 block.clear();
1386 return;
1387 }
1388
1389 applyPulsedInterferenceToFmcwIfBlock(receiver_index, block, _fmcw_if_block_start_times[receiver_index]);
1390 receiver->consumeFmcwIfBlock(block, _fmcw_if_block_start_times[receiver_index]);
1391 block.clear();
1392 }
1393
1394 void SimulationEngine::applyPulsedInterferenceToFmcwIfBlock(const std::size_t receiver_index,
1395 std::span<ComplexType> block,
1397 {
1398 applyPulsedInterferenceToStreamingBlock(receiver_index, block, block_start_time,
1399 params::rate() * static_cast<RealType>(params::oversampleRatio()),
1400 true);
1401 }
1402
1403 void SimulationEngine::addPulsedInterferenceSamples(std::span<ComplexType> block,
1404 std::span<const ComplexType> rendered_pulse,
1405 const long long dest_begin, const long long dest_end,
1406 const std::size_t crop_offset, const RealType block_start_time,
1407 const RealType sample_rate, const bool dechirp_mix,
1408 Receiver* receiver, ReceiverTrackerCache& tracker_cache) const
1409 {
1410 for (long long dest = dest_begin; dest < dest_end; ++dest)
1411 {
1412 const RealType t_sample = block_start_time + static_cast<RealType>(dest) / sample_rate;
1413 const auto source_index = crop_offset + static_cast<std::size_t>(dest - dest_begin);
1414 if (source_index >= rendered_pulse.size())
1415 {
1416 continue;
1417 }
1418 if (dechirp_mix)
1419 {
1420 const auto mixer = calculateDechirpMixer(receiver, t_sample, tracker_cache);
1421 if (!mixer.has_value())
1422 {
1423 continue;
1424 }
1425 block[static_cast<std::size_t>(dest)] += *mixer * std::conj(rendered_pulse[source_index]);
1426 }
1427 else
1428 {
1429 block[static_cast<std::size_t>(dest)] += rendered_pulse[source_index];
1430 }
1431 }
1432 }
1433
1434 void SimulationEngine::applyPulsedInterferenceToStreamingBlock(const std::size_t receiver_index,
1435 std::span<ComplexType> block,
1437 const RealType sample_rate, const bool dechirp_mix)
1438 {
1439 if (block.empty() || receiver_index >= _world->getReceivers().size())
1440 {
1441 return;
1442 }
1443
1444 const auto& receiver = _world->getReceivers()[receiver_index];
1445 if (!std::isfinite(sample_rate) || sample_rate <= 0.0)
1446 {
1447 return;
1448 }
1449 const RealType block_end_time = block_start_time + static_cast<RealType>(block.size()) / sample_rate;
1450 auto& tracker_cache = _if_pulse_tracker_caches[receiver_index];
1451
1453 for (const auto& response : receiver->getPulsedInterferenceLog())
1454 {
1455 const RealType pulse_rate = response->sampleRate();
1456 const unsigned pulse_size = response->sampleCount();
1457 if (pulse_rate <= 0.0 || pulse_size == 0)
1458 {
1459 continue;
1460 }
1461
1462 const RealType pulse_start_time = response->startTime();
1465 {
1466 continue;
1467 }
1468
1471 const auto dest_begin = static_cast<long long>(
1472 std::max<RealType>(0.0, std::ceil((overlap_start - block_start_time) * sample_rate)));
1473 const auto dest_end = static_cast<long long>(std::min<RealType>(
1474 static_cast<RealType>(block.size()), std::ceil((overlap_end - block_start_time) * sample_rate)));
1475 if (dest_begin >= dest_end)
1476 {
1477 continue;
1478 }
1479
1480 const auto interp_padding = static_cast<long long>(params::renderFilterLength()) / 2 + 1;
1481 const long long padded_begin = dest_begin - interp_padding;
1482 const long long padded_end = dest_end + interp_padding;
1483 const RealType render_start = block_start_time + static_cast<RealType>(padded_begin) / sample_rate;
1484 const auto render_count = static_cast<std::size_t>(padded_end - padded_begin);
1485 const auto rendered_pulse = response->renderSlice(sample_rate, render_start, render_count, 0.0);
1486 const auto crop_offset = static_cast<std::size_t>(dest_begin - padded_begin);
1487 addPulsedInterferenceSamples(block, rendered_pulse, dest_begin, dest_end, crop_offset, block_start_time,
1488 sample_rate, dechirp_mix, receiver.get(), tracker_cache);
1489 }
1490 }
1491
1492 std::optional<ComplexType> SimulationEngine::calculateDechirpMixer(Receiver* rx, const RealType t_step,
1493 ReceiverTrackerCache& tracker_cache) const
1494 {
1496 const auto& dechirp_sources = rx->getDechirpSources();
1497 if (tracker_cache.dechirp_reference.size() < dechirp_sources.size())
1498 {
1499 tracker_cache.dechirp_reference.resize(dechirp_sources.size());
1500 }
1501
1502 if (!tracker_cache.last_dechirp_time.has_value() || t_step < *tracker_cache.last_dechirp_time)
1503 {
1504 tracker_cache.active_dechirp_source_index = 0;
1505 std::ranges::fill(tracker_cache.dechirp_reference, FmcwChirpBoundaryTracker{});
1506 }
1507 tracker_cache.last_dechirp_time = t_step;
1508
1509 bool reference_active = false;
1510 auto& source_index = tracker_cache.active_dechirp_source_index;
1511 while (source_index < dechirp_sources.size() && t_step >= dechirp_sources[source_index].segment_end)
1512 {
1513 ++source_index;
1514 }
1515 if (source_index < dechirp_sources.size())
1516 {
1518 if (t_step >= reference_source.segment_start && t_step < reference_source.segment_end &&
1521 {
1522 reference_active = true;
1523 }
1524 }
1525
1526 if (!reference_active)
1527 {
1528 return std::nullopt;
1529 }
1530
1532 if (rx->getDechirpMode() == Receiver::DechirpMode::Physical && _cw_phase_noise_lookup)
1533 {
1534 receiver_phase = _cw_phase_noise_lookup->sample(rx->getTiming().get(), t_step);
1535 }
1536 return std::polar(1.0, reference_phase + receiver_phase);
1537 }
1538
1539 ComplexType SimulationEngine::calculateStreamingSample(Receiver* rx, const RealType t_step,
1540 const std::vector<ActiveStreamingSource>& streaming_sources,
1541 ReceiverTrackerCache& tracker_cache) const
1542 {
1543 const bool dechirping = rx->isDechirpEnabled();
1544 std::optional<ComplexType> dechirp_mixer;
1545 if (dechirping)
1546 {
1547 dechirp_mixer = calculateDechirpMixer(rx, t_step, tracker_cache);
1548 if (!dechirp_mixer.has_value())
1549 {
1550 return {0.0, 0.0};
1551 }
1552 }
1553
1555 : (rx->getDechirpMode() == Receiver::DechirpMode::Ideal
1558
1559 ComplexType total_sample{0.0, 0.0};
1560 for (std::size_t source_index = 0; source_index < streaming_sources.size(); ++source_index)
1561 {
1563 if (!rx->checkFlag(Receiver::RecvFlag::FLAG_NODIRECT))
1564 {
1566 streaming_source, rx, t_step, _cw_phase_noise_lookup.get(), &tracker_cache.direct[source_index],
1568 }
1569 for (std::size_t target_index = 0; target_index < _world->getTargets().size(); ++target_index)
1570 {
1571 const auto& target_ptr = _world->getTargets()[target_index];
1573 streaming_source, rx, target_ptr.get(), t_step, _cw_phase_noise_lookup.get(),
1575 }
1576 }
1577
1578 if (!dechirping)
1579 {
1580 return total_sample;
1581 }
1582
1583 // Mixing Convention: s_IF = s_ref * conj(s_rx)
1584 // This convention is chosen to ensure that:
1585 // 1. Stationary targets (positive delay tau) result in a POSITIVE beat frequency (f_b = alpha * tau).
1586 // 2. In physical dechirp mode, phase noise from the same LO source partially cancels
1587 // at short ranges (Range Correlation Effect).
1588 // 3. For an up-chirp, a receding target (negative RF Doppler) results in a
1589 // higher IF frequency (f_IF = f_b + |f_d|).
1590 return *dechirp_mixer * std::conj(total_sample);
1591 }
1592
1593 void SimulationEngine::appendStreamingTrackerSource()
1594 {
1595 const std::size_t target_count = _world->getTargets().size();
1596
1597 for (auto& cache : _streaming_tracker_caches)
1598 {
1599 cache.direct.emplace_back();
1600 cache.reflected.emplace_back(target_count);
1601 }
1602 }
1603
1604 void SimulationEngine::eraseStreamingTrackerSource(const std::size_t source_index)
1605 {
1606 for (auto& cache : _streaming_tracker_caches)
1607 {
1608 if (source_index < cache.direct.size())
1609 {
1610 cache.direct.erase(cache.direct.begin() + static_cast<std::ptrdiff_t>(source_index));
1611 }
1612 if (source_index < cache.reflected.size())
1613 {
1614 cache.reflected.erase(cache.reflected.begin() + static_cast<std::ptrdiff_t>(source_index));
1615 }
1616 }
1617 }
1618
1619 void SimulationEngine::cleanupInactiveStreamingSources(const RealType from_time)
1620 {
1622 for (std::size_t source_index = sources.size(); source_index > 0; --source_index)
1623 {
1624 const std::size_t index = source_index - 1;
1625 if (sources[index].segment_end > from_time)
1626 {
1627 continue;
1628 }
1629 const auto cleanup_deadline = streamingSourceCleanupDeadline(sources[index], from_time);
1630 if (cleanup_deadline.has_value() && from_time < *cleanup_deadline)
1631 {
1632 continue;
1633 }
1634
1635 sources.erase(sources.begin() + static_cast<std::ptrdiff_t>(index));
1636 eraseStreamingTrackerSource(index);
1637 }
1638 }
1639
1640 std::optional<RealType> SimulationEngine::streamingSourceCleanupDeadline(const ActiveStreamingSource& source,
1641 const RealType from_time) const
1642 {
1643 if (source.transmitter == nullptr || source.carrier_freq <= 0.0)
1644 {
1645 return std::nullopt;
1646 }
1647
1648 std::optional<RealType> latest_deadline;
1649 for (const auto& receiver_ptr : _world->getReceivers())
1650 {
1651 const auto receiver_deadline = receiverCleanupDeadline(source, receiver_ptr.get(), from_time);
1652 if (receiver_deadline.has_value() &&
1654 {
1656 }
1657 }
1658 return latest_deadline;
1659 }
1660
1661 std::optional<RealType> SimulationEngine::receiverCleanupDeadline(const ActiveStreamingSource& source,
1662 const Receiver* const rx,
1663 const RealType from_time) const
1664 {
1665 if (!isStreamingReceiver(rx))
1666 {
1667 return std::nullopt;
1668 }
1669
1670 const auto update_latest = [](std::optional<RealType>& latest, const std::optional<RealType> candidate)
1671 {
1672 if (candidate.has_value() && (!latest.has_value() || *candidate > *latest))
1673 {
1674 latest = candidate;
1675 }
1676 };
1677
1678 const auto interval_deadline = [&](const RealType interval_start,
1679 const RealType interval_end) -> std::optional<RealType>
1680 {
1681 const RealType start = std::max({params::startTime(), from_time, interval_start});
1682 const RealType end = std::min(params::endTime(), interval_end);
1683 if (start >= end)
1684 {
1685 return std::nullopt;
1686 }
1687
1688 std::optional<RealType> latest;
1689 if (!rx->checkFlag(Receiver::RecvFlag::FLAG_NODIRECT))
1690 {
1691 update_latest(latest, directPathCleanupDeadline(source, rx, start, end));
1692 }
1693 for (const auto& target_ptr : _world->getTargets())
1694 {
1695 update_latest(latest, reflectedPathCleanupDeadline(source, rx, target_ptr.get(), start, end));
1696 }
1697 return latest;
1698 };
1699
1700 std::optional<RealType> latest_deadline;
1701 const auto& schedule = rx->getSchedule();
1702 if (schedule.empty())
1703 {
1705 return latest_deadline;
1706 }
1707
1708 for (const auto& period : schedule)
1709 {
1711 }
1712 return latest_deadline;
1713 }
1714
1716 {
1717 // NOLINTBEGIN(cppcoreguidelines-pro-type-static-cast-downcast)
1718 switch (event.type)
1719 {
1721 handleTxPulsedStart(static_cast<Transmitter*>(event.source_object), event.timestamp);
1722 break;
1724 handleRxPulsedWindowStart(static_cast<Receiver*>(event.source_object), event.timestamp);
1725 break;
1727 handleRxPulsedWindowEnd(static_cast<Receiver*>(event.source_object), event.timestamp);
1728 break;
1730 if (const auto source = streamingSourceAtEvent(static_cast<Transmitter*>(event.source_object),
1731 event.timestamp, _internal_stop_time);
1732 source.has_value())
1733 {
1734 handleTxStreamingStart(*source);
1735 }
1736 break;
1738 handleTxStreamingEnd(static_cast<Transmitter*>(event.source_object));
1739 break;
1741 handleRxStreamingStart(static_cast<Receiver*>(event.source_object));
1742 break;
1744 handleRxStreamingEnd(static_cast<Receiver*>(event.source_object));
1745 break;
1746 }
1747 // NOLINTEND(cppcoreguidelines-pro-type-static-cast-downcast)
1748 }
1749
1750 void SimulationEngine::routeResponse(Receiver* rx, std::unique_ptr<serial::Response> response) const
1751 {
1752 if (!response)
1753 {
1754 return;
1755 }
1756 if (rx->getMode() == OperationMode::PULSED_MODE)
1757 {
1758 rx->addResponseToInbox(std::move(response));
1759 }
1760 else
1761 {
1762 rx->addInterferenceToLog(std::move(response));
1763 }
1764 }
1765
1767 {
1768 for (const auto& rx_ptr : _world->getReceivers())
1769 {
1770 if (!rx_ptr->checkFlag(Receiver::RecvFlag::FLAG_NODIRECT))
1771 {
1772 routeResponse(rx_ptr.get(), simulation::calculateResponse(tx, rx_ptr.get(), tx->getSignal(), t_event));
1773 }
1774 for (const auto& target_ptr : _world->getTargets())
1775 {
1776 routeResponse(
1777 rx_ptr.get(),
1778 simulation::calculateResponse(tx, rx_ptr.get(), tx->getSignal(), t_event, target_ptr.get()));
1779 }
1780 }
1781
1782 const RealType next_theoretical_time = t_event + 1.0 / tx->getPrf();
1783 if (const auto next_pulse_opt = tx->getNextPulseTime(next_theoretical_time);
1785 {
1787 }
1788 }
1789
1791 {
1792 rx->setActive(true);
1793 _world->getEventQueue().push({t_event + rx->getWindowLength(), EventType::RX_PULSED_WINDOW_END, rx});
1794 }
1795
1797 {
1798 rx->setActive(false);
1799 const auto active_streaming_sources =
1800 collectStreamingSourcesForWindow(t_event - rx->getWindowLength(), t_event);
1801
1802 RenderingJob job{.ideal_start_time = t_event - rx->getWindowLength(),
1803 .duration = rx->getWindowLength(),
1804 .responses = rx->drainInbox(),
1805 .active_streaming_sources = active_streaming_sources};
1806
1807 rx->enqueueFinalizerJob(std::move(job));
1808
1809 const RealType next_theoretical = t_event - rx->getWindowLength() + 1.0 / rx->getWindowPrf();
1810 if (const auto next_start = rx->getNextWindowTime(next_theoretical);
1812 {
1814 }
1815 }
1816
1818 {
1819 _world->getSimulationState().active_streaming_transmitters.push_back(source);
1820 appendStreamingTrackerSource();
1821 }
1822
1824 {
1825 (void)tx;
1826 // A transmitter stop is a transmit-time boundary, not an instantaneous receive-time cutoff.
1827 // Ended sources are removed only after all future receive-time samples fail the retarded-time gate.
1828 cleanupInactiveStreamingSources(_world->getSimulationState().t_current);
1829 }
1830
1832 {
1833 rx->setActive(true);
1834 const auto receiver_it = std::ranges::find_if(_world->getReceivers(), [rx](const auto& receiver_ptr)
1835 { return receiver_ptr.get() == rx; });
1836 if (receiver_it != _world->getReceivers().end())
1837 {
1838 const auto receiver_index = static_cast<std::size_t>(receiver_it - _world->getReceivers().begin());
1839 _streaming_downsamplers[receiver_index].reset();
1840 if (_eager_context_stream_open)
1841 {
1842 ensureStreamingOutputStreamOpen(receiver_index, _world->getSimulationState().t_current,
1843 streamingOutputSampleRate(receiver_index));
1844 }
1845 }
1846 if (rx->hasFmcwIfResamplingSink())
1847 {
1848 rx->beginFmcwIfResamplingSegment(_world->getSimulationState().t_current);
1849 }
1850 }
1851
1853 {
1854 const auto receiver_it = std::ranges::find_if(_world->getReceivers(), [rx](const auto& receiver_ptr)
1855 { return receiver_ptr.get() == rx; });
1856 if (receiver_it != _world->getReceivers().end())
1857 {
1858 const auto receiver_index = static_cast<std::size_t>(receiver_it - _world->getReceivers().begin());
1859 flushFmcwIfBlock(receiver_index);
1860 flushStreamingOutputBlock(receiver_index, true);
1861 }
1862 if (rx->hasFmcwIfResamplingSink() && _world->getSimulationState().t_current >= params::endTime() &&
1863 _world->getSimulationState().t_current < _internal_stop_time && activePastUserEnd(rx))
1864 {
1865 return;
1866 }
1867 if (rx->hasFmcwIfResamplingSink())
1868 {
1869 rx->endFmcwIfResamplingSegment();
1870 }
1871 if (_output_sink != nullptr && receiver_it != _world->getReceivers().end())
1872 {
1873 const auto receiver_index = static_cast<std::size_t>(receiver_it - _world->getReceivers().begin());
1874 if (_streaming_output_stream_open[receiver_index])
1875 {
1876 _output_sink->closeStream(_streaming_output_stream_ids[receiver_index]);
1877 _streaming_output_stream_open[receiver_index] = false;
1878 }
1879 }
1880 rx->setActive(false);
1881 }
1882
1883 void SimulationEngine::updateProgress() { reportSimulationProgress(_world->getSimulationState().t_current); }
1884
1885 bool SimulationEngine::isCancellationRequested()
1886 {
1887 if (_cancelled)
1888 {
1889 return true;
1890 }
1891 if (_cancel_callback && _cancel_callback())
1892 {
1893 _cancelled = true;
1894 LOG(Level::INFO, "Simulation cancellation requested.");
1895 if (_reporter)
1896 {
1897 _reporter->report("Simulation cancelled", 100, 100);
1898 }
1899 return true;
1900 }
1901 return false;
1902 }
1903
1904 void SimulationEngine::reportSimulationProgress(const RealType t_current)
1905 {
1906 if (!_reporter)
1907 {
1908 return;
1909 }
1910
1911 const RealType start_time = params::startTime();
1912 const RealType end_time = params::endTime();
1913 const RealType duration = end_time - start_time;
1914 const RealType progress_fraction = duration > 0.0 ? (t_current - start_time) / duration : 1.0;
1915 const int progress = static_cast<int>(
1916 std::clamp(progress_fraction * 100.0, static_cast<RealType>(0.0), static_cast<RealType>(100.0)));
1917
1918 if (const auto now = std::chrono::steady_clock::now();
1919 progress != _last_reported_percent || now - _last_report_time >= std::chrono::milliseconds(100))
1920 {
1921 _reporter->report(std::format("Simulating... {:.2f}s / {:.2f}s", t_current, end_time), progress, 100);
1922 _last_reported_percent = progress;
1923 _last_report_time = now;
1924 }
1925 }
1926
1927 std::vector<ActiveStreamingSource> SimulationEngine::collectStreamingSourcesForWindow(const RealType start_time,
1928 const RealType end_time) const
1929 {
1930 // A segment that ended before this window can still be in flight at the receiver.
1931 (void)start_time;
1932 std::vector<ActiveStreamingSource> sources;
1933 for (const auto& transmitter_ptr : _world->getTransmitters())
1934 {
1935 if (!transmitter_ptr->isStreamingMode())
1936 {
1937 continue;
1938 }
1939
1940 const auto append_candidate = [&](const RealType segment_start, const RealType segment_end)
1941 {
1942 auto source = makeActiveSource(transmitter_ptr.get(), segment_start, segment_end);
1943 if (source.segment_start < source.segment_end && source.segment_start < end_time)
1944 {
1945 sources.push_back(source);
1946 }
1947 };
1948
1949 if (transmitter_ptr->getSchedule().empty())
1950 {
1952 continue;
1953 }
1954
1955 for (const auto& period : transmitter_ptr->getSchedule())
1956 {
1957 append_candidate(period.start, std::min(params::endTime(), period.end));
1958 }
1959 }
1960 return sources;
1961 }
1962
1963 void SimulationEngine::shutdown()
1964 {
1965 LOG(Level::INFO, "Simulation compute loop finished. Waiting for receiver finalization tasks...");
1966 if (_reporter)
1967 {
1968 _reporter->report("Simulation compute finished. Waiting for receiver finalization...", 100, 100);
1969 }
1970
1971 for (std::size_t receiver_index = 0; receiver_index < _world->getReceivers().size(); ++receiver_index)
1972 {
1973 const auto& receiver_ptr = _world->getReceivers()[receiver_index];
1974 if (receiver_ptr->getMode() == OperationMode::CW_MODE ||
1975 receiver_ptr->getMode() == OperationMode::FMCW_MODE)
1976 {
1977 if (_output_sink != nullptr)
1978 {
1979 flushFmcwIfBlock(receiver_index);
1980 receiver_ptr->flushFmcwIfResampling();
1981 flushStreamingOutputBlock(receiver_index, true);
1982 if (_streaming_output_stream_open[receiver_index])
1983 {
1984 _output_sink->closeStream(_streaming_output_stream_ids[receiver_index]);
1985 _streaming_output_stream_open[receiver_index] = false;
1986 }
1987 }
1988 }
1989 else if (receiver_ptr->getMode() == OperationMode::PULSED_MODE)
1990 {
1991 RenderingJob shutdown_job{};
1992 shutdown_job.duration = -1.0;
1993 receiver_ptr->enqueueFinalizerJob(std::move(shutdown_job));
1994 }
1995 }
1996
1997 _pool.wait();
1998 for (auto& finalizer_thread : _finalizer_threads)
1999 {
2000 if (finalizer_thread.joinable())
2001 {
2002 finalizer_thread.join();
2003 }
2004 }
2005
2006 LOG(Level::INFO, "All finalization tasks complete.");
2007 }
2008
2010 const std::function<void(const std::string&, int, int)>& progress_callback,
2011 const std::string& output_dir, const OutputConfig& output_config,
2012 std::function<bool()> cancel_callback, bool* cancelled,
2014 {
2015 if (cancelled != nullptr)
2016 {
2017 *cancelled = false;
2018 }
2019 auto reporter = std::make_shared<ProgressReporter>(progress_callback);
2020 auto metadata_collector = std::make_shared<OutputMetadataCollector>(output_dir);
2021 std::unique_ptr<ReceiverOutputSink> output_sink;
2023 {
2025 output_sink->initializeRun(output_config, params::params.simulation_name);
2026 }
2027 else
2028 {
2029 output_sink = serial::makeHdf5OutputSink(output_dir, metadata_collector);
2030 output_sink->initializeRun(output_config, params::params.simulation_name);
2031 }
2032
2033 SimulationEngine engine(world, pool, reporter, output_dir, metadata_collector, output_sink.get(),
2034 std::move(cancel_callback), isVita49Enabled(output_config));
2035 engine.run();
2036 if (cancelled != nullptr)
2037 {
2038 *cancelled = engine.cancelled();
2039 }
2041 {
2042 LOG(Level::INFO, "Waiting for VITA output stream drain...");
2043 reporter->report("Waiting for VITA output stream drain...", 100, 100);
2044 }
2045 const auto stats = output_sink->finalize();
2046 reporter->report(engine.cancelled() ? "Simulation cancelled" : "Simulation complete", 100, 100);
2047 LOG(Level::INFO, "Event-driven simulation loop finished.");
2048 auto metadata = metadata_collector->snapshot();
2049 if (output_sink)
2050 {
2052 {
2054 if (stats.epoch_unix_nanoseconds.has_value())
2055 {
2056 vita49_metadata.epoch_unix_nanoseconds = stats.epoch_unix_nanoseconds;
2057 }
2058 for (const auto& stream : stats.streams)
2059 {
2060 vita49_metadata.streams.push_back(streamStatsToMetadata(stream));
2061 }
2062 metadata.vita49 = std::move(vita49_metadata);
2063 }
2064 }
2065 return metadata;
2066 }
2067}
const Transmitter & transmitter
const Receiver & receiver
Header for radar channel propagation and interaction models.
virtual void emitContextHeartbeat(RealType simulation_time)=0
virtual void submitBlock(const ReceiverSampleBlock &block)=0
virtual std::uint32_t registerStream(const ReceiverStreamDescriptor &stream)=0
virtual void closeStream(std::uint32_t stream_id)=0
virtual void openStream(std::uint32_t stream_id, RealType first_sample_time)=0
Encapsulates the state and logic of the event-driven simulation loop.
void handleRxStreamingStart(radar::Receiver *rx)
Handles a streaming receiver starting to record.
void handleTxStreamingEnd(radar::Transmitter *tx)
Handles a streaming transmitter turning off.
void handleRxPulsedWindowEnd(radar::Receiver *rx, RealType t_event)
Handles the closing of a pulsed receiver's listening window, triggering finalization.
SimulationEngine(World *world, pool::ThreadPool &pool, std::shared_ptr< ProgressReporter > reporter, std::string output_dir, std::shared_ptr< OutputMetadataCollector > metadata_collector=nullptr, ReceiverOutputSink *output_sink=nullptr, std::function< bool()> cancel_callback=nullptr, bool eager_context_stream_open=false)
Constructs the simulation engine.
void handleRxPulsedWindowStart(radar::Receiver *rx, RealType t_event)
Handles the opening of a pulsed receiver's listening window.
void run()
Starts and runs the main simulation loop until completion.
void processEvent(const Event &event)
Dispatches a discrete simulation event to its specific handler.
void handleTxStreamingStart(const ActiveStreamingSource &source)
Handles a streaming transmitter turning on.
void handleRxStreamingEnd(radar::Receiver *rx)
Handles a streaming receiver stopping recording.
void processStreamingPhysics(RealType t_event)
Advances the time-stepped inner loop for active streaming systems.
void handleTxPulsedStart(radar::Transmitter *tx, RealType t_event)
Handles the start of a pulsed transmission.
The World class manages the simulator environment.
Definition world.h:39
const std::vector< std::unique_ptr< radar::Target > > & getTargets() const noexcept
Retrieves the list of radar targets.
Definition world.h:226
SimulationState & getSimulationState() noexcept
Gets a mutable reference to the global simulation state.
Definition world.h:322
std::priority_queue< Event, std::vector< Event >, EventComparator > & getEventQueue() noexcept
Gets a mutable reference to the global event queue.
Definition world.h:313
const std::vector< std::unique_ptr< radar::Receiver > > & getReceivers() const noexcept
Retrieves the list of radar receivers.
Definition world.h:236
RealType earliestPhaseNoiseLookupStart() const
Finds the earliest simulation time that can require CW phase-noise samples.
Definition world.cpp:209
Stateful FIR decimator for chunked streaming output.
Definition dsp_filters.h:51
FMCW linear chirp signal implementation.
RealType getChirpDuration() const noexcept
Gets the chirp duration in seconds.
RealType getChirpBandwidth() const noexcept
Gets the chirp bandwidth in hertz.
RealType getChirpPeriod() const noexcept
Gets the chirp period in seconds.
FmcwChirpDirection getDirection() const noexcept
Gets the FMCW sweep direction.
const std::optional< std::size_t > & getChirpCount() const noexcept
Gets the optional finite chirp count.
RealType getChirpRate() const noexcept
Gets the chirp rate in hertz per second.
RealType getStartFrequencyOffset() const noexcept
Gets the start frequency offset relative to carrier in hertz.
FMCW symmetric triangular modulation signal implementation.
RealType getStartFrequencyOffset() const noexcept
Gets the start frequency offset relative to carrier in hertz.
RealType getChirpBandwidth() const noexcept
Gets the chirp bandwidth in hertz.
RealType getChirpRate() const noexcept
Gets the chirp rate magnitude in hertz per second.
const std::optional< std::size_t > & getTriangleCount() const noexcept
Gets the optional finite triangle count.
RealType getChirpDuration() const noexcept
Gets the per-leg chirp duration in seconds.
RealType getTrianglePeriod() const noexcept
Gets the full up/down triangle period in seconds.
Class representing a radar signal with associated properties.
const class FmcwTriangleSignal * getFmcwTriangleSignal() const noexcept
Gets the FMCW triangle implementation, if this signal owns one.
bool isFmcwFamily() const noexcept
Returns true when this signal belongs to the FMCW waveform family.
const class FmcwChirpSignal * getFmcwChirpSignal() const noexcept
Gets the FMCW chirp implementation, if this signal owns one.
RealType getPower() const noexcept
Gets the power of the radar signal.
Exception class for handling path-related errors.
Definition path_utils.h:32
Represents a path with coordinates and allows for various interpolation methods.
Definition path.h:31
Vec3 getPosition(RealType t) const
Retrieves the position at a given time along the path.
Definition path.cpp:36
const std::vector< Coord > & getCoords() const noexcept
Gets the list of coordinates in the path.
Definition path.h:84
@ INTERP_STATIC
Hold the first coordinate for all query times.
@ INTERP_LINEAR
Linearly interpolate between neighboring coordinates.
@ INTERP_CUBIC
Cubically interpolate between neighboring coordinates.
InterpType getType() const noexcept
Retrieves the current interpolation type of the path.
Definition path.h:77
A class representing a vector in rectangular coordinates.
RealType x
The x component of the vector.
RealType z
The z component of the vector.
RealType y
The y component of the vector.
A simple thread pool implementation.
Definition thread_pool.h:29
void wait()
Waits for all tasks in the thread pool to finish.
Definition thread_pool.h:82
const std::string & getName() const noexcept
Retrieves the name of the object.
Definition object.h:79
Manages radar signal reception and response processing.
Definition receiver.h:47
std::mt19937 & getRngEngine() noexcept
Gets the receiver's internal random number generator engine.
Definition receiver.h:202
bool hasFmcwIfResamplingSink() const noexcept
Returns true when this receiver is using the online FMCW IF resampling sink.
Definition receiver.h:242
void prunePulsedInterferenceEndingBefore(RealType cutoff_time) noexcept
Removes logged pulsed interference responses that ended before a receive time.
Definition receiver.cpp:128
const std::vector< SchedulePeriod > & getSchedule() const noexcept
Retrieves the list of active reception periods.
Definition receiver.h:382
bool isDechirpEnabled() const noexcept
Returns true when the receiver emits dechirped IF data.
Definition receiver.h:215
void consumeFmcwIfBlock(std::span< const ComplexType > block, RealType block_start_time)
Feeds one completed high-rate dechirped block into the online IF sink.
Definition receiver.cpp:243
const std::optional< fers_signal::FmcwIfResamplerPlan > & getFmcwIfResamplerPlan() const noexcept
Gets the active or most recently used IF resampling plan, if any.
Definition receiver.h:245
RealType getNoiseTemperature() const noexcept
Retrieves the noise temperature of the receiver.
Definition receiver.h:151
OperationMode getMode() const noexcept
Gets the operational mode of the receiver.
Definition receiver.h:209
Base class for radar targets.
Definition target.h:118
Represents a radar transmitter system.
Definition transmitter.h:34
bool isStreamingMode() const noexcept
Returns true when the transmitter uses a continuous streaming mode.
Definition transmitter.h:78
const std::vector< SchedulePeriod > & getSchedule() const noexcept
Retrieves the list of active transmission periods.
double RealType
Type for real numbers.
Definition config.h:27
constexpr RealType EPSILON
Machine epsilon for real numbers.
Definition config.h:51
std::complex< RealType > ComplexType
Type for complex numbers.
Definition config.h:35
Declares the functions for the asynchronous receiver finalization pipelines.
Declares focused, testable pipeline steps for receiver finalization.
Internal FMCW IF rational resampler planning and streaming sink.
Header file for the logging system.
#define LOG(level,...)
Definition logging.h:19
Startup memory and output-size projection helpers for simulations.
std::uint64_t countFmcwTriangleStarts(const ActiveStreamingSource &source, const RealType active_start, const RealType active_end)
Counts FMCW triangles that start inside the absolute interval.
void logSimulationMemoryProjection(const World &world)
Logs the projected simulation memory footprint for the provided world.
ActiveStreamingSource makeActiveSource(const radar::Transmitter *const tx, const RealType segment_start, const RealType segment_end)
Builds an active-source cache from a streaming transmitter and segment bounds.
OutputMetadata runEventDrivenSim(World *world, pool::ThreadPool &pool, const std::function< void(const std::string &, int, int)> &progress_callback, const std::string &output_dir, const OutputConfig &output_config, std::function< bool()> cancel_callback, bool *cancelled, ReceiverOutputTelemetryCallback telemetry_callback)
Runs the unified, event-driven radar simulation.
std::function< void(const std::optional< OutputStats > &, std::span< const ReceiverOutputPacketTrace >)> ReceiverOutputTelemetryCallback
bool isVita49Enabled(const OutputConfig &config) noexcept
@ RX_PULSED_WINDOW_START
A pulsed receiver opens its listening window.
@ RX_PULSED_WINDOW_END
A pulsed receiver closes its listening window.
@ TX_STREAMING_END
A streaming transmitter stops transmitting.
@ RX_STREAMING_END
A streaming receiver stops listening.
@ TX_STREAMING_START
A streaming transmitter starts transmitting.
@ TX_PULSED_START
A pulsed transmitter begins emitting a pulse.
@ RX_STREAMING_START
A streaming receiver starts listening.
Vita49OutputMetadata vita49MetadataFromConfig(const Vita49OutputConfig &config)
Builds the static VITA metadata section from runtime output configuration.
std::vector< std::shared_ptr< timing::Timing > > collectCwPhaseNoiseTimings(const World &world)
Collects unique timing sources used by CW/FMCW transmitters and receivers.
std::uint64_t countFmcwChirpStarts(const ActiveStreamingSource &source, const RealType active_start, const RealType active_end)
Counts FMCW chirps that start inside the absolute interval.
FmcwIfResamplerPlan planFmcwIfResampler(const FmcwIfResamplerRequest &request)
std::string_view fmcwChirpDirectionToken(const FmcwChirpDirection direction) noexcept
Converts a chirp direction to the schema token.
RealType endTime() noexcept
Get the end time for the simulation.
Definition parameters.h:109
RealType rate() noexcept
Get the rendering sample rate.
Definition parameters.h:121
RealType startTime() noexcept
Get the start time for the simulation.
Definition parameters.h:103
unsigned oversampleRatio() noexcept
Get the oversampling ratio.
Definition parameters.h:151
unsigned renderFilterLength() noexcept
Get the render filter length.
Definition parameters.h:139
Parameters params
Global simulation parameter state.
Definition parameters.h:85
RealType c() noexcept
Get the speed of light.
Definition parameters.h:91
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.
OperationMode
Defines the operational mode of a radar component.
Definition radar_obj.h:39
std::unique_ptr< core::ReceiverOutputSink > makeVita49OutputSink(core::ReceiverOutputTelemetryCallback telemetry_callback)
std::unique_ptr< core::ReceiverOutputSink > makeHdf5OutputSink(std::string output_dir, std::shared_ptr< core::OutputMetadataCollector > metadata_collector)
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.
bool calculateStreamingReferencePhase(const core::ActiveStreamingSource &source, const RealType timeK, core::FmcwChirpBoundaryTracker *const chirp_tracker, RealType &phase_out)
Evaluates a receive-time streaming waveform phase for receiver LO/dechirp references.
@ TransmitterOnly
Incoming RF/baseband signal before receiver LO subtraction.
@ None
Ignore timing phase noise entirely.
@ ReceiverRelative
Existing raw streaming convention: transmitter phase minus receiver LO phase.
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.
std::unique_ptr< serial::Response > calculateResponse(const Transmitter *trans, const Receiver *recv, const RadarSignal *signal, const RealType startTime, const Target *targ)
Creates a Response object by simulating a signal's interaction over its duration.
Defines the Parameters struct and provides methods for managing simulation parameters.
Utility functions for path interpolation and exception handling.
Classes for handling radar waveforms and signals.
Radar Receiver class for managing signal reception and response handling.
Classes for managing radar signal responses.
Header for receiver-side signal processing and rendering.
Defines the core structures for the event-driven simulation engine.
math::Vec3 max
bool valid
RealType lower_u
RealType root_u
RealType c
bool unbounded
RealType b
RealType upper_u
RealType segment_length
math::Vec3 min
RealType a
Header file for the main simulation runner.
Cached description of an active streaming transmitter segment.
Represents a single event in the simulation's time-ordered queue.
Definition sim_events.h:45
RealType timestamp
The simulation time at which the event occurs.
Definition sim_events.h:46
EventType type
The type of the event.
Definition sim_events.h:47
radar::Radar * source_object
Pointer to the object that generated the event.
Definition sim_events.h:48
Metadata summary for the full simulation output set.
A data packet containing all information needed to process one receive window.
std::vector< ActiveStreamingSource > active_streaming_transmitters
A global list of all currently active streaming transmitters.
RealType t_current
The master simulation clock, advanced by the event loop.
Represents a position in 3D space with an associated time.
Definition coord.h:24
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.
A simple thread pool implementation.
Timing source for simulation objects.
Header file for the Transmitter class in the radar namespace.
Header file for the World class in the simulator.