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_data_packet_count = stats.late_data_packet_count,
85 .late_context_packet_count = stats.late_context_packet_count,
86 .context_packet_count = stats.context_packets,
87 .first_sample_time = stats.first_sample_time,
88 .end_sample_time = stats.end_sample_time,
89 .first_timestamp = stats.first_timestamp,
90 .end_timestamp = stats.end_timestamp};
91 }
92
93 [[nodiscard]] std::string fmcwCountToken(const std::optional<std::size_t>& count)
94 {
95 return count.has_value() ? std::format("{}", *count) : std::string("unbounded");
96 }
97
99 const std::string& direction, const std::string& configured_count,
101 {
104 const auto total_chirp_count = countFmcwChirpStarts(source, active_start, source.segment_end);
105 LOG(Level::INFO,
106 "FMCW transmitter '{}' shape=linear {} B={} Hz T_c={} s T_rep={} s f_0={} Hz alpha={} Hz/s "
107 "duty_cycle={} chirp_count={} total_chirp_count={} average_power={} W",
111 }
112
114 const std::string& direction, const std::string& configured_count,
116 {
117 std::uint64_t total_chirp_count = 0;
118 for (const auto& period : transmitter.getSchedule())
119 {
120 const RealType active_start = std::max(params::startTime(), period.start);
121 const auto source =
122 makeActiveSource(&transmitter, period.start, std::min(params::endTime(), period.end));
123 const auto segment_chirp_count = countFmcwChirpStarts(source, active_start, source.segment_end);
125 LOG(Level::INFO,
126 "FMCW transmitter '{}' segment [{}, {}] shape=linear {} B={} Hz T_c={} s T_rep={} s f_0={} "
127 "Hz alpha={} Hz/s duty_cycle={} chirp_count={} segment_chirp_count={} total_chirp_count={} "
128 "average_power={} W",
129 transmitter.getName(), period.start, source.segment_end, direction, fmcw.getChirpBandwidth(),
132 }
133 }
134
137 {
138 const RealType duty_cycle = fmcw.getChirpDuration() / fmcw.getChirpPeriod();
139 const RealType average_power = waveform.getPower() * duty_cycle;
141 const auto configured_count = fmcwCountToken(fmcw.getChirpCount());
142 if (transmitter.getSchedule().empty())
143 {
146 return;
147 }
149 }
150
152 const fers_signal::FmcwTriangleSignal& triangle,
153 const std::string& configured_count, const RealType average_power)
154 {
157 const auto total_triangle_count = countFmcwTriangleStarts(source, active_start, source.segment_end);
158 LOG(Level::INFO,
159 "FMCW transmitter '{}' shape=triangle B={} Hz T_c={} s T_tri={} s f_0={} Hz alpha={} Hz/s "
160 "duty_cycle=1 triangle_count={} total_triangle_count={} average_power={} W",
161 transmitter.getName(), triangle.getChirpBandwidth(), triangle.getChirpDuration(),
162 triangle.getTrianglePeriod(), triangle.getStartFrequencyOffset(), triangle.getChirpRate(),
164 }
165
167 const fers_signal::FmcwTriangleSignal& triangle,
168 const std::string& configured_count, const RealType average_power)
169 {
170 std::uint64_t total_triangle_count = 0;
171 for (const auto& period : transmitter.getSchedule())
172 {
173 const RealType active_start = std::max(params::startTime(), period.start);
174 const auto source =
175 makeActiveSource(&transmitter, period.start, std::min(params::endTime(), period.end));
176 const auto segment_triangle_count = countFmcwTriangleStarts(source, active_start, source.segment_end);
178 LOG(Level::INFO,
179 "FMCW transmitter '{}' segment [{}, {}] shape=triangle B={} Hz T_c={} s T_tri={} s f_0={} "
180 "Hz alpha={} Hz/s duty_cycle=1 triangle_count={} segment_triangle_count={} "
181 "total_triangle_count={} average_power={} W",
182 transmitter.getName(), period.start, source.segment_end, triangle.getChirpBandwidth(),
183 triangle.getChirpDuration(), triangle.getTrianglePeriod(), triangle.getStartFrequencyOffset(),
186 }
187 }
188
190 const fers_signal::FmcwTriangleSignal& triangle)
191 {
192 const RealType average_power = waveform.getPower();
193 const auto configured_count = fmcwCountToken(triangle.getTriangleCount());
194 if (transmitter.getSchedule().empty())
195 {
197 return;
198 }
200 }
201
204 const std::string& configured_count, const RealType duty_cycle,
206 {
209 const auto total_step_count = countSfcwStepStarts(source, active_start, source.segment_end);
210 LOG(Level::INFO,
211 "SFCW transmitter '{}' steps={} df={} Hz dwell={} s step_period={} s sweep_period={} s "
212 "f_first={} Hz f_last={} Hz B_eff={} Hz range_resolution={} m unambiguous_range={} m duty_cycle={} "
213 "sweep_count={} total_step_count={} average_power={} W",
214 transmitter.getName(), sfcw.getStepCount(), sfcw.getStepSize(), sfcw.getDwellTime(),
215 sfcw.getStepPeriod(), sfcw.getSweepPeriod(), sfcw.firstFrequency(waveform.getCarrier()),
216 sfcw.lastFrequency(waveform.getCarrier()), sfcw.effectiveBandwidth(),
217 params::c() / (2.0 * sfcw.effectiveBandwidth()), params::c() / (2.0 * std::abs(sfcw.getStepSize())),
219 }
220
223 const std::string& configured_count, const RealType duty_cycle,
225 {
226 std::uint64_t total_step_count = 0;
227 for (const auto& period : transmitter.getSchedule())
228 {
229 const RealType active_start = std::max(params::startTime(), period.start);
230 const auto source =
231 makeActiveSource(&transmitter, period.start, std::min(params::endTime(), period.end));
232 const auto segment_step_count = countSfcwStepStarts(source, active_start, source.segment_end);
234 LOG(Level::INFO,
235 "SFCW transmitter '{}' segment [{}, {}] steps={} df={} Hz dwell={} s step_period={} s "
236 "sweep_period={} s f_first={} Hz f_last={} Hz B_eff={} Hz range_resolution={} m "
237 "unambiguous_range={} m duty_cycle={} sweep_count={} segment_step_count={} total_step_count={} "
238 "average_power={} W",
239 transmitter.getName(), period.start, source.segment_end, sfcw.getStepCount(), sfcw.getStepSize(),
240 sfcw.getDwellTime(), sfcw.getStepPeriod(), sfcw.getSweepPeriod(),
241 sfcw.firstFrequency(waveform.getCarrier()), sfcw.lastFrequency(waveform.getCarrier()),
242 sfcw.effectiveBandwidth(), params::c() / (2.0 * sfcw.effectiveBandwidth()),
243 params::c() / (2.0 * std::abs(sfcw.getStepSize())), duty_cycle, configured_count,
245 }
246 }
247
250 {
251 const RealType duty_cycle = sfcw.getDwellTime() / sfcw.getStepPeriod();
252 const RealType average_power = waveform.getPower() * duty_cycle;
253 const auto configured_count = fmcwCountToken(sfcw.getSweepCount());
254 if (transmitter.getSchedule().empty())
255 {
257 return;
258 }
260 }
261
262 [[nodiscard]] bool isStreamingReceiver(const Receiver* const receiver) noexcept
263 {
264 return receiver != nullptr &&
265 (receiver->getMode() == OperationMode::CW_MODE || receiver->getMode() == OperationMode::FMCW_MODE ||
266 receiver->getMode() == OperationMode::SFCW_MODE);
267 }
268
269 [[nodiscard]] bool activePastUserEnd(const Receiver* const receiver) noexcept
270 {
271 if (receiver == nullptr)
272 {
273 return false;
274 }
275 if (receiver->getSchedule().empty())
276 {
277 return true;
278 }
279 return std::ranges::any_of(receiver->getSchedule(),
280 [](const auto& period) { return period.end > params::endTime(); });
281 }
282
283 [[nodiscard]] std::size_t streamingSampleIndexAtOrAfter(const RealType time, const RealType dt_sim)
284 {
285 if (dt_sim <= 0.0 || time <= params::startTime())
286 {
287 return 0;
288 }
289 return static_cast<std::size_t>(std::ceil((time - params::startTime()) / dt_sim));
290 }
291
292 struct PositionBounds
293 {
296 bool valid{false};
297 bool unbounded{false};
298 };
299
300 [[nodiscard]] bool isFinite(const math::Vec3& point) noexcept
301 {
302 return std::isfinite(point.x) && std::isfinite(point.y) && std::isfinite(point.z);
303 }
304
305 void includePoint(PositionBounds& bounds, const math::Vec3& point) noexcept
306 {
307 if (!isFinite(point))
308 {
309 bounds.unbounded = true;
310 return;
311 }
312 if (!bounds.valid)
313 {
314 bounds.min = point;
315 bounds.max = point;
316 bounds.valid = true;
317 return;
318 }
319 bounds.min.x = std::min(bounds.min.x, point.x);
320 bounds.min.y = std::min(bounds.min.y, point.y);
321 bounds.min.z = std::min(bounds.min.z, point.z);
322 bounds.max.x = std::max(bounds.max.x, point.x);
323 bounds.max.y = std::max(bounds.max.y, point.y);
324 bounds.max.z = std::max(bounds.max.z, point.z);
325 }
326
327 [[nodiscard]] RealType axisValue(const math::Vec3& point, const std::size_t axis) noexcept
328 {
329 switch (axis)
330 {
331 case 0:
332 return point.x;
333 case 1:
334 return point.y;
335 default:
336 return point.z;
337 }
338 }
339
340 [[nodiscard]] RealType axisValue(const std::array<RealType, 3>& values, const std::size_t axis) noexcept
341 {
342 switch (axis)
343 {
344 case 0:
345 return values[0];
346 case 1:
347 return values[1];
348 default:
349 return values[2];
350 }
351 }
352
353 [[nodiscard]] RealType& axisValue(std::array<RealType, 3>& values, const std::size_t axis) noexcept
354 {
355 switch (axis)
356 {
357 case 0:
358 return values[0];
359 case 1:
360 return values[1];
361 default:
362 return values[2];
363 }
364 }
365
366 [[nodiscard]] RealType axisDistanceBound(const PositionBounds& lhs, const PositionBounds& rhs,
367 const std::size_t axis) noexcept
368 {
369 const RealType lhs_min = axisValue(lhs.min, axis);
370 const RealType lhs_max = axisValue(lhs.max, axis);
371 const RealType rhs_min = axisValue(rhs.min, axis);
372 const RealType rhs_max = axisValue(rhs.max, axis);
373 return std::max(std::abs(lhs_max - rhs_min), std::abs(rhs_max - lhs_min));
374 }
375
376 [[nodiscard]] RealType maxDistanceBetweenBounds(const PositionBounds& lhs, const PositionBounds& rhs) noexcept
377 {
378 if (lhs.unbounded || rhs.unbounded || !lhs.valid || !rhs.valid)
379 {
380 return std::numeric_limits<RealType>::infinity();
381 }
382 const RealType dx = axisDistanceBound(lhs, rhs, 0);
383 const RealType dy = axisDistanceBound(lhs, rhs, 1);
384 const RealType dz = axisDistanceBound(lhs, rhs, 2);
385 return std::sqrt(dx * dx + dy * dy + dz * dz);
386 }
387
388 [[nodiscard]] std::array<RealType, 3> coordinateAxes(const math::Coord& coord) noexcept
389 {
390 return {coord.pos.x, coord.pos.y, coord.pos.z};
391 }
392
393 void includeCubicVelocityRoot(PositionBounds& bounds, const math::Path& path, const RealType segment_start,
395 const RealType upper_u)
396 {
398 {
399 return;
400 }
401 includePoint(bounds, path.getPosition(segment_start + root_u * segment_length));
402 }
403
404 void includeCubicPositionExtrema(PositionBounds& bounds, const math::Path& path,
405 const std::vector<math::Coord>& coords,
406 const std::vector<math::Coord>& second_derivatives, const std::size_t index,
407 const RealType lower_u, const RealType upper_u)
408 {
409 const RealType segment_length = coords[index + 1].t - coords[index].t;
410 if (segment_length <= EPSILON)
411 {
412 return;
413 }
414 const auto left = coordinateAxes(coords[index]);
415 const auto right = coordinateAxes(coords[index + 1]);
416 const auto dd_left = coordinateAxes(second_derivatives[index]);
417 const auto dd_right = coordinateAxes(second_derivatives[index + 1]);
419
420 for (std::size_t axis = 0; axis < 3; ++axis)
421 {
424 const RealType a = 0.5 * h2 * (dd_right_axis - dd_left_axis);
425 const RealType b = h2 * dd_left_axis;
426 const RealType c = (axisValue(right, axis) - axisValue(left, axis)) +
427 (h2 / 6.0) * (-2.0 * dd_left_axis - dd_right_axis);
428
429 if (std::abs(a) <= EPSILON)
430 {
431 if (std::abs(b) > EPSILON)
432 {
434 upper_u);
435 }
436 continue;
437 }
438
439 const RealType discriminant = b * b - 4.0 * a * c;
440 if (discriminant < -EPSILON)
441 {
442 continue;
443 }
444 const RealType sqrt_discriminant = std::sqrt(std::max(0.0, discriminant));
446 (-b - sqrt_discriminant) / (2.0 * a), lower_u, upper_u);
448 (-b + sqrt_discriminant) / (2.0 * a), lower_u, upper_u);
449 }
450 }
451
452 [[nodiscard]] PositionBounds pathPositionBounds(const math::Path& path, const RealType start,
453 const RealType end)
454 {
455 PositionBounds bounds;
456 if (start >= end)
457 {
458 return bounds;
459 }
460
461 try
462 {
463 includePoint(bounds, path.getPosition(start));
464 includePoint(bounds, path.getPosition(end));
465 }
466 catch (const math::PathException&)
467 {
468 bounds.unbounded = true;
469 return bounds;
470 }
471
472 const auto& coords = path.getCoords();
474 {
475 return bounds;
476 }
477
478 for (const auto& coord : coords)
479 {
480 if (coord.t >= start && coord.t <= end)
481 {
483 }
484 }
485
486 if (path.getType() != math::Path::InterpType::INTERP_CUBIC || coords.size() < 2)
487 {
488 return bounds;
489 }
490
491 std::vector<math::Coord> second_derivatives;
492 try
493 {
495 }
496 catch (const math::PathException&)
497 {
498 bounds.unbounded = true;
499 return bounds;
500 }
501
502 for (std::size_t index = 0; index + 1 < coords.size(); ++index)
503 {
504 const RealType segment_start = coords[index].t;
505 const RealType segment_end = coords[index + 1].t;
506 const RealType segment_length = segment_end - segment_start;
508 {
509 continue;
510 }
511
512 const RealType lower_u =
513 std::clamp((std::max(start, segment_start) - segment_start) / segment_length, 0.0, 1.0);
514 const RealType upper_u =
515 std::clamp((std::min(end, segment_end) - segment_start) / segment_length, 0.0, 1.0);
516 if (lower_u <= upper_u)
517 {
519 }
520 }
521 return bounds;
522 }
523
524 struct QuadraticVelocityExtremum
525 {
533 };
534
535 void includeQuadraticVelocityExtremum(std::array<RealType, 3>& max_abs_velocity, const std::size_t axis,
536 const QuadraticVelocityExtremum& extremum) noexcept
537 {
538 if (extremum.root_u < extremum.lower_u || extremum.root_u > extremum.upper_u ||
539 extremum.segment_length <= EPSILON)
540 {
541 return;
542 }
543 const RealType velocity =
544 (extremum.a * extremum.root_u * extremum.root_u + extremum.b * extremum.root_u + extremum.c) /
545 extremum.segment_length;
546 if (std::isfinite(velocity))
547 {
549 axis_max_velocity = std::max(axis_max_velocity, std::abs(velocity));
550 }
551 else
552 {
553 axisValue(max_abs_velocity, axis) = std::numeric_limits<RealType>::infinity();
554 }
555 }
556
557 void includeCubicVelocityBounds(std::array<RealType, 3>& max_abs_velocity,
558 const std::vector<math::Coord>& coords,
559 const std::vector<math::Coord>& second_derivatives, const std::size_t index,
560 const RealType lower_u, const RealType upper_u)
561 {
562 const RealType segment_length = coords[index + 1].t - coords[index].t;
563 if (segment_length <= EPSILON)
564 {
565 return;
566 }
567 const auto left = coordinateAxes(coords[index]);
568 const auto right = coordinateAxes(coords[index + 1]);
569 const auto dd_left = coordinateAxes(second_derivatives[index]);
570 const auto dd_right = coordinateAxes(second_derivatives[index + 1]);
572
573 for (std::size_t axis = 0; axis < 3; ++axis)
574 {
577 const RealType a = 0.5 * h2 * (dd_right_axis - dd_left_axis);
578 const RealType b = h2 * dd_left_axis;
579 const RealType c = (axisValue(right, axis) - axisValue(left, axis)) +
580 (h2 / 6.0) * (-2.0 * dd_left_axis - dd_right_axis);
582 QuadraticVelocityExtremum{.a = a,
583 .b = b,
584 .c = c,
585 .segment_length = segment_length,
586 .root_u = lower_u,
587 .lower_u = lower_u,
588 .upper_u = upper_u});
590 QuadraticVelocityExtremum{.a = a,
591 .b = b,
592 .c = c,
593 .segment_length = segment_length,
594 .root_u = upper_u,
595 .lower_u = lower_u,
596 .upper_u = upper_u});
597
598 if (std::abs(a) > EPSILON)
599 {
601 QuadraticVelocityExtremum{.a = a,
602 .b = b,
603 .c = c,
604 .segment_length = segment_length,
605 .root_u = -b / (2.0 * a),
606 .lower_u = lower_u,
607 .upper_u = upper_u});
608 }
609 }
610 }
611
612 [[nodiscard]] RealType pathSpeedBound(const math::Path& path, const RealType start, const RealType end)
613 {
614 if (start >= end)
615 {
616 return 0.0;
617 }
618
619 const auto& coords = path.getCoords();
620 if (coords.empty() || path.getType() == math::Path::InterpType::INTERP_STATIC || coords.size() < 2)
621 {
622 return 0.0;
623 }
624
626 {
627 RealType max_speed = 0.0;
628 for (std::size_t index = 0; index + 1 < coords.size(); ++index)
629 {
630 const RealType segment_start = coords[index].t;
631 const RealType segment_end = coords[index + 1].t;
632 const RealType segment_length = segment_end - segment_start;
634 {
635 continue;
636 }
637 max_speed =
638 std::max(max_speed, (coords[index + 1].pos - coords[index].pos).length() / segment_length);
639 }
640 return max_speed;
641 }
642
643 std::vector<math::Coord> second_derivatives;
644 try
645 {
647 }
648 catch (const math::PathException&)
649 {
650 return std::numeric_limits<RealType>::infinity();
651 }
652
653 std::array<RealType, 3> max_abs_velocity{0.0, 0.0, 0.0};
654 for (std::size_t index = 0; index + 1 < coords.size(); ++index)
655 {
656 const RealType segment_start = coords[index].t;
657 const RealType segment_end = coords[index + 1].t;
658 const RealType segment_length = segment_end - segment_start;
660 {
661 continue;
662 }
663 const RealType lower_u =
664 std::clamp((std::max(start, segment_start) - segment_start) / segment_length, 0.0, 1.0);
665 const RealType upper_u =
666 std::clamp((std::min(end, segment_end) - segment_start) / segment_length, 0.0, 1.0);
667 if (lower_u <= upper_u)
668 {
670 }
671 }
672 return std::sqrt(max_abs_velocity[0] * max_abs_velocity[0] + max_abs_velocity[1] * max_abs_velocity[1] +
674 }
675
676 [[nodiscard]] std::optional<RealType>
680 {
683 {
684 return std::nullopt;
685 }
686
688 {
691 {
692 return std::nullopt;
693 }
694
698 {
699 return std::nullopt;
700 }
701 return std::min(interval_end, deadline);
702 }
703
704 if (!std::isfinite(max_delay_bound))
705 {
706 return interval_end;
707 }
710 {
711 return std::nullopt;
712 }
713 return deadline;
714 }
715
716 [[nodiscard]] std::optional<RealType> directPathCleanupDeadline(const ActiveStreamingSource& source,
717 const Receiver* const rx,
720 {
721 const auto* const tx = source.transmitter;
722 if (tx == nullptr || rx == nullptr || tx->getPlatform() == rx->getPlatform() || params::c() <= 0.0)
723 {
724 return std::nullopt;
725 }
726
727 const auto* const tx_path = tx->getPlatform()->getMotionPath();
728 const auto* const rx_path = rx->getPlatform()->getMotionPath();
730 (rx_path->getPosition(interval_start) - tx_path->getPosition(interval_start)).length();
734 params::c();
737 return deadlineFromTailKinematics(source.segment_end, interval_start, interval_end,
739 }
740
741 [[nodiscard]] std::optional<RealType> reflectedPathCleanupDeadline(const ActiveStreamingSource& source,
742 const Receiver* const rx,
743 const radar::Target* const target,
746 {
747 const auto* const tx = source.transmitter;
748 if (tx == nullptr || rx == nullptr || target == nullptr || params::c() <= 0.0 ||
749 tx->getPlatform() == target->getPlatform() || rx->getPlatform() == target->getPlatform())
750 {
751 return std::nullopt;
752 }
753
754 const auto* const tx_path = tx->getPlatform()->getMotionPath();
755 const auto* const rx_path = rx->getPlatform()->getMotionPath();
756 const auto* const target_path = target->getPlatform()->getMotionPath();
757 const auto tx_position = tx_path->getPosition(interval_start);
758 const auto rx_position = rx_path->getPosition(interval_start);
759 const auto target_position = target_path->getPosition(interval_start);
761 (target_position - tx_position).length() + (rx_position - target_position).length();
762
768 params::c();
773
774 return deadlineFromTailKinematics(source.segment_end, interval_start, interval_end,
776 }
777
778 /// Builds an active streaming source for a transmitter at an event timestamp.
779 std::optional<ActiveStreamingSource> streamingSourceAtEvent(const Transmitter* const transmitter,
780 const RealType timestamp,
782 {
783 if (transmitter == nullptr || !transmitter->isStreamingMode())
784 {
785 return std::nullopt;
786 }
787
788 const auto& schedule = transmitter->getSchedule();
789 if (schedule.empty())
790 {
791 const RealType segment_start = params::startTime();
792 auto source = makeActiveSource(transmitter, segment_start, internal_stop_time);
793 if (timestamp >= segment_start && timestamp < source.segment_end)
794 {
795 return source;
796 }
797 return std::nullopt;
798 }
799
800 // TODO: O(N) Schedule Lookups - Since the schedule is guaranteed to be sorted (enforced by
801 // `processRawSchedule`), should be using `std::lower_bound` or binary search to find the relevant period in
802 // $O(\log N)$ time.
803 for (const auto& period : schedule)
804 {
805 const RealType active_start = std::max(params::startTime(), period.start);
806 auto source = makeActiveSource(transmitter, period.start, std::min(internal_stop_time, period.end));
807 if (timestamp >= active_start && timestamp < source.segment_end)
808 {
809 return source;
810 }
811 }
812 return std::nullopt;
813 }
814 }
815
816 SimulationEngine::SimulationEngine(World* world, pool::ThreadPool& pool, std::shared_ptr<ProgressReporter> reporter,
817 std::string output_dir,
818 std::shared_ptr<OutputMetadataCollector> metadata_collector,
819 ReceiverOutputSink* output_sink, std::function<bool()> cancel_callback,
820 const bool eager_context_stream_open) :
821 _world(world), _pool(pool), _reporter(std::move(reporter)), _metadata_collector(std::move(metadata_collector)),
822 _output_sink(output_sink), _cancel_callback(std::move(cancel_callback)),
823 _eager_context_stream_open(eager_context_stream_open), _last_report_time(std::chrono::steady_clock::now()),
824 _next_context_heartbeat_time(params::startTime() + 1.0), _output_dir(std::move(output_dir)),
825 _internal_stop_time(params::endTime())
826 {
827 _streaming_tracker_caches.resize(_world->getReceivers().size());
828 _if_pulse_tracker_caches.resize(_world->getReceivers().size());
829 _fmcw_if_block_buffers.resize(_world->getReceivers().size());
830 _fmcw_if_block_start_times.resize(_world->getReceivers().size(), params::startTime());
831 _streaming_output_block_buffers.resize(_world->getReceivers().size());
832 _streaming_output_processed_buffers.resize(_world->getReceivers().size());
833 _streaming_output_block_start_times.resize(_world->getReceivers().size(), params::startTime());
834 _streaming_output_block_start_indices.resize(_world->getReceivers().size(), 0);
835 _streaming_downsamplers.resize(_world->getReceivers().size());
836 _streaming_downsample_base_indices.resize(_world->getReceivers().size(), 0);
837 _streaming_downsample_segment_start_times.resize(_world->getReceivers().size(), params::startTime());
838 _streaming_output_sample_cursors.resize(_world->getReceivers().size(), 0);
839 _streaming_output_stream_ids.resize(_world->getReceivers().size(), 0);
840 _streaming_output_stream_open.resize(_world->getReceivers().size(), false);
841 _streaming_output_file_metadata.resize(_world->getReceivers().size());
842 for (auto& block : _streaming_output_block_buffers)
843 {
845 }
846 for (auto& block : _streaming_output_processed_buffers)
847 {
849 }
850 }
851
853 {
854 if (_reporter)
855 {
856 _reporter->report("Initializing event-driven simulation...", 0, 100);
857 }
858
859 initializeFmcwIfResamplers();
860
862
863 initializeFinalizers();
864
865 LOG(Level::INFO, "Starting unified event-driven simulation loop.");
866 logStreamingSummaries();
867
868 auto& event_queue = _world->getEventQueue();
869 auto& state = _world->getSimulationState();
870 const RealType end_time = _internal_stop_time;
871
872 while (!event_queue.empty() && state.t_current <= end_time)
873 {
874 if (isCancellationRequested())
875 {
876 break;
877 }
878 const Event event = event_queue.top();
879 event_queue.pop();
880
881 processStreamingPhysics(event.timestamp);
882 if (isCancellationRequested())
883 {
884 break;
885 }
886 flushFmcwIfBlocks();
887 flushStreamingOutputBlocks();
888
889 state.t_current = event.timestamp;
890
891 processEvent(event);
892 updateProgress();
893 }
894
895 const bool has_active_if_overrender =
896 std::ranges::any_of(_world->getReceivers(), [](const auto& receiver)
897 { return receiver->isActive() && receiver->hasFmcwIfResamplingSink(); });
898 if (!isCancellationRequested() && has_active_if_overrender)
899 {
900 processStreamingPhysics(end_time);
901 }
902 flushFmcwIfBlocks();
903 flushStreamingOutputBlocks();
904
905 shutdown();
906 }
907
908 void SimulationEngine::logStreamingSummaries() const
909 {
910 for (const auto& transmitter_ptr : _world->getTransmitters())
911 {
912 const auto* waveform = transmitter_ptr->getSignal();
913 if (waveform == nullptr)
914 {
915 continue;
916 }
917
918 if (const auto* fmcw = waveform->getFmcwChirpSignal(); fmcw != nullptr)
919 {
920 logFmcwChirpSummary(*transmitter_ptr, *waveform, *fmcw);
921 }
922 else if (const auto* triangle = waveform->getFmcwTriangleSignal(); triangle != nullptr)
923 {
924 logFmcwTriangleSummary(*transmitter_ptr, *waveform, *triangle);
925 }
926 else if (const auto* sfcw = waveform->getSteppedFrequencySignal(); sfcw != nullptr)
927 {
928 logSfcwSummary(*transmitter_ptr, *waveform, *sfcw);
929 }
930 }
931 }
932
933 void SimulationEngine::initializeFinalizers()
934 {
935 if (_output_sink == nullptr)
936 {
937 return;
938 }
939 for (const auto& receiver_ptr : _world->getReceivers())
940 {
941 if (receiver_ptr->getMode() == OperationMode::PULSED_MODE)
942 {
943 _finalizer_threads.emplace_back(processing::runPulsedFinalizer, receiver_ptr.get(),
944 &_world->getTargets(), _reporter, _output_dir, _metadata_collector,
945 _output_sink);
946 }
947 }
948 }
949
950 void SimulationEngine::initializeFmcwIfResamplers()
951 {
952 _internal_stop_time = params::endTime();
953 for (std::size_t receiver_index = 0; receiver_index < _world->getReceivers().size(); ++receiver_index)
954 {
955 initializeFmcwIfResampler(receiver_index);
956 }
957
958 if (_internal_stop_time > params::endTime())
959 {
960 extendDechirpSourcesForIfOverrender();
961 }
962 }
963
964 void SimulationEngine::initializeFmcwIfResampler(const std::size_t receiver_index)
965 {
966 const auto& receiver_ptr = _world->getReceivers()[receiver_index];
967 if (!receiver_ptr->isDechirpEnabled() || !receiver_ptr->hasFmcwIfSampleRate())
968 {
969 return;
970 }
971
972 const auto& request = receiver_ptr->getFmcwIfChainRequest();
973 const RealType output_rate = request.sample_rate_hz.value_or(0.0);
974 const RealType bandwidth = request.filter_bandwidth_hz.value_or(0.40 * output_rate);
976 .input_sample_rate_hz = params::rate() * static_cast<RealType>(params::oversampleRatio()),
977 .output_sample_rate_hz = output_rate,
978 .filter_bandwidth_hz = bandwidth,
979 .filter_transition_width_hz = request.filter_transition_width_hz};
981 const RealType block_time = static_cast<RealType>(fmcw_if_block_size) / resampler_request.input_sample_rate_hz;
982 const RealType over_render = plan.group_delay_seconds + 1.0 / plan.actual_output_sample_rate_hz + block_time;
983 _internal_stop_time = std::max(_internal_stop_time, params::endTime() + over_render);
984 if (_output_sink != nullptr)
985 {
986 receiver_ptr->setFmcwIfOutputCallback(
987 [this, receiver_index](const std::span<const ComplexType> samples, const std::uint64_t sample_start)
988 {
989 const auto& receiver = _world->getReceivers()[receiver_index];
990 const auto& if_plan = receiver->getFmcwIfResamplerPlan();
991 if (!if_plan.has_value())
992 {
993 return;
994 }
995 const RealType first_sample_time = params::startTime() +
996 static_cast<RealType>(sample_start) / if_plan->actual_output_sample_rate_hz;
997 emitStreamingOutputBlock(receiver_index, first_sample_time, if_plan->actual_output_sample_rate_hz,
998 samples, sample_start);
999 });
1000 }
1001 const RealType actual_output_sample_rate_hz = plan.actual_output_sample_rate_hz;
1002 const auto overall_ratio = plan.overall_ratio;
1003 const RealType filter_bandwidth_hz = plan.filter_bandwidth_hz;
1004 const RealType filter_transition_width_hz = plan.filter_transition_width_hz;
1005 receiver_ptr->initializeFmcwIfResampling(std::move(plan));
1006 LOG(Level::INFO,
1007 "Receiver '{}' enabled FMCW IF resampling: input_rate={} Hz requested_output_rate={} Hz "
1008 "actual_output_rate={} Hz ratio={}/{} passband={} Hz transition={} Hz.",
1009 receiver_ptr->getName(), resampler_request.input_sample_rate_hz, output_rate, actual_output_sample_rate_hz,
1010 overall_ratio.numerator, overall_ratio.denominator, filter_bandwidth_hz, filter_transition_width_hz);
1011 }
1012
1013 void SimulationEngine::extendDechirpSourcesForIfOverrender()
1014 {
1015 for (const auto& receiver_ptr : _world->getReceivers())
1016 {
1017 if (!receiver_ptr->hasFmcwIfSampleRate())
1018 {
1019 continue;
1020 }
1021
1022 auto dechirp_sources = receiver_ptr->getDechirpSources();
1023 for (auto& source : dechirp_sources)
1024 {
1025 if (std::abs(source.segment_end - params::endTime()) > 1.0e-12)
1026 {
1027 continue;
1028 }
1029 if (source.transmitter == nullptr || source.transmitter->getSchedule().empty())
1030 {
1031 source.segment_end = _internal_stop_time;
1032 continue;
1033 }
1034 for (const auto& period : source.transmitter->getSchedule())
1035 {
1036 if (period.start <= params::endTime() && period.end > params::endTime())
1037 {
1038 source.segment_end = std::min(_internal_stop_time, period.end);
1039 break;
1040 }
1041 }
1042 }
1043 receiver_ptr->setResolvedDechirpSources(std::move(dechirp_sources));
1044 }
1045 }
1046
1047 void SimulationEngine::ensureCwPhaseNoiseLookup()
1048 {
1049 if (_cw_phase_noise_lookup)
1050 {
1051 return;
1052 }
1053
1054 const auto timings = collectCwPhaseNoiseTimings(*_world);
1056 for (const auto& source : _world->getSimulationState().active_streaming_transmitters)
1057 {
1058 lookup_start = std::min(lookup_start, source.segment_start);
1059 }
1060 _cw_phase_noise_lookup = std::make_unique<simulation::CwPhaseNoiseLookup>(
1061 simulation::CwPhaseNoiseLookup::build(timings, lookup_start, _internal_stop_time));
1062 }
1063
1065 {
1066 auto& state = _world->getSimulationState();
1067 auto& t_current = state.t_current;
1068
1069 if (t_event <= t_current)
1070 {
1071 return;
1072 }
1073
1075 const auto first_index = streamingSampleIndexAtOrAfter(t_current, dt_sim);
1077 const auto sample_count = final_index - first_index;
1078 const auto progress_report_stride = std::max<std::size_t>(1, sample_count / 1000);
1079
1080 ensureCwPhaseNoiseLookup();
1081
1082 while (t_current < t_event && !isCancellationRequested())
1083 {
1084 cleanupInactiveStreamingSources(t_current);
1085
1086 const RealType chunk_end = streamingChunkEnd(t_current, t_event);
1087 if (chunk_end <= t_current)
1088 {
1089 break;
1090 }
1091
1092 const auto start_index = streamingSampleIndexAtOrAfter(t_current, dt_sim);
1095 {
1096 if (shouldStopStreamingChunk(sample_index, start_index))
1097 {
1098 break;
1099 }
1101 }
1102
1103 t_current = chunk_end;
1104 emitContextHeartbeatsThrough(t_current);
1105 }
1106 cleanupInactiveStreamingSources(t_current);
1107 }
1108
1109 std::optional<RealType> SimulationEngine::nextStreamingCleanupDeadline(const RealType from_time)
1110 {
1111 const auto& active_streaming_transmitters = _world->getSimulationState().active_streaming_transmitters;
1112 std::optional<RealType> next_deadline;
1113 for (const auto& source : active_streaming_transmitters)
1114 {
1115 if (source.segment_end > from_time)
1116 {
1117 continue;
1118 }
1119 const auto cleanup_deadline = streamingSourceCleanupDeadline(source, from_time);
1120 if (cleanup_deadline.has_value() && *cleanup_deadline > from_time &&
1121 (!next_deadline.has_value() || *cleanup_deadline < *next_deadline))
1122 {
1124 }
1125 }
1126 return next_deadline;
1127 }
1128
1129 RealType SimulationEngine::streamingChunkEnd(const RealType from_time, const RealType event_time)
1130 {
1131 if (const auto cleanup_deadline = nextStreamingCleanupDeadline(from_time);
1133 {
1134 return *cleanup_deadline;
1135 }
1136 return event_time;
1137 }
1138
1139 bool SimulationEngine::shouldStopStreamingChunk(const std::size_t sample_index, const std::size_t chunk_start_index)
1140 {
1141 return ((sample_index - chunk_start_index) % 1024) == 0 && isCancellationRequested();
1142 }
1143
1144 void SimulationEngine::processStreamingSample(const std::size_t sample_index, const std::size_t first_index,
1145 const std::size_t final_index,
1146 const std::size_t progress_report_stride, const RealType dt_sim)
1147 {
1148 const RealType t_step = params::startTime() + static_cast<RealType>(sample_index) * dt_sim;
1149 appendActiveReceiverStreamingSamples(sample_index, t_step);
1150
1151 if (_output_sink != nullptr && t_step >= _next_context_heartbeat_time)
1152 {
1153 emitContextHeartbeatsThrough(t_step);
1154 }
1156 {
1157 reportSimulationProgress(t_step);
1158 }
1159 }
1160
1161 void SimulationEngine::appendActiveReceiverStreamingSamples(const std::size_t sample_index, const RealType t_step)
1162 {
1163 for (std::size_t receiver_index = 0; receiver_index < _world->getReceivers().size(); ++receiver_index)
1164 {
1165 appendReceiverStreamingSample(receiver_index, sample_index, t_step);
1166 }
1167 if (std::ranges::any_of(_streaming_output_block_buffers,
1168 [](const auto& block) { return block.size() >= streaming_output_block_size; }))
1169 {
1170 flushStreamingOutputBlocks(true);
1171 }
1172 }
1173
1174 void SimulationEngine::appendReceiverStreamingSample(const std::size_t receiver_index,
1175 const std::size_t sample_index, const RealType t_step)
1176 {
1177 const auto& receiver_ptr = _world->getReceivers()[receiver_index];
1178 if (!isStreamingReceiver(receiver_ptr.get()) || !receiver_ptr->isActive())
1179 {
1180 return;
1181 }
1182
1183 const auto& active_streaming_transmitters = _world->getSimulationState().active_streaming_transmitters;
1184 ComplexType const sample = calculateStreamingSample(receiver_ptr.get(), t_step, active_streaming_transmitters,
1185 _streaming_tracker_caches[receiver_index]);
1186 if (receiver_ptr->hasFmcwIfResamplingSink())
1187 {
1188 appendFmcwIfSample(receiver_index, t_step, sample);
1189 }
1190 else if (_output_sink != nullptr)
1191 {
1192 appendStreamingOutputSample(receiver_index, sample_index, t_step, sample);
1193 }
1194 }
1195
1196 void SimulationEngine::appendFmcwIfSample(const std::size_t receiver_index, const RealType t_step,
1197 const ComplexType sample)
1198 {
1199 auto& block = _fmcw_if_block_buffers[receiver_index];
1200 if (block.empty())
1201 {
1202 _fmcw_if_block_start_times[receiver_index] = t_step;
1203 }
1204 block.push_back(sample);
1205 if (block.size() >= fmcw_if_block_size)
1206 {
1207 flushFmcwIfBlock(receiver_index);
1208 }
1209 }
1210
1211 void SimulationEngine::appendStreamingOutputSample(const std::size_t receiver_index, const std::size_t sample_index,
1212 const RealType t_step, const ComplexType sample)
1213 {
1214 if (_eager_context_stream_open)
1215 {
1216 ensureStreamingOutputStreamOpen(receiver_index, t_step, streamingOutputSampleRate(receiver_index));
1217 }
1218 auto& block = _streaming_output_block_buffers[receiver_index];
1219 if (block.empty())
1220 {
1221 _streaming_output_block_start_times[receiver_index] = t_step;
1222 _streaming_output_block_start_indices[receiver_index] = static_cast<std::uint64_t>(sample_index);
1223 }
1224 block.push_back(sample);
1225 }
1226
1227 void SimulationEngine::flushStreamingOutputBlocks(const bool full_blocks_only)
1228 {
1229 std::vector<ReceiverSampleBlock> batch;
1230 batch.reserve(_streaming_output_block_buffers.size());
1231 for (std::size_t receiver_index = 0; receiver_index < _streaming_output_block_buffers.size(); ++receiver_index)
1232 {
1233 if (!full_blocks_only ||
1234 _streaming_output_block_buffers[receiver_index].size() >= streaming_output_block_size)
1235 {
1236 flushStreamingOutputBlock(receiver_index, false, &batch);
1237 }
1238 }
1239 if (_output_sink != nullptr && !batch.empty())
1240 {
1241 _output_sink->submitBlocks(batch);
1242 }
1243 }
1244
1245 void SimulationEngine::flushStreamingOutputBlock(const std::size_t receiver_index, const bool finish_downsampler,
1246 std::vector<ReceiverSampleBlock>* batch)
1247 {
1248 if (_output_sink == nullptr || receiver_index >= _world->getReceivers().size())
1249 {
1250 return;
1251 }
1252
1253 auto& block = _streaming_output_block_buffers[receiver_index];
1254 if (block.empty())
1255 {
1256 if (finish_downsampler && _streaming_downsamplers[receiver_index])
1257 {
1258 auto& downsampler = *_streaming_downsamplers[receiver_index];
1259 const auto output_start_index = downsampler.outputSampleCount();
1260 downsampler.finish();
1261 auto output = downsampler.takeOutput();
1262 if (!output.empty())
1263 {
1265 const RealType output_start_time = _streaming_downsample_segment_start_times[receiver_index] +
1267 emitStreamingOutputBlock(receiver_index, output_start_time, output_sample_rate, output,
1268 _streaming_downsample_base_indices[receiver_index] + output_start_index,
1269 batch);
1270 }
1271 _streaming_downsamplers[receiver_index].reset();
1272 }
1273 return;
1274 }
1275
1276 const auto& receiver = _world->getReceivers()[receiver_index];
1277 const bool dechirped = receiver->isDechirpEnabled();
1279 const RealType block_start_time = _streaming_output_block_start_times[receiver_index];
1280 const auto input_start_index = _streaming_output_block_start_indices[receiver_index];
1281
1282 applyPulsedInterferenceToStreamingBlock(receiver_index, block, block_start_time, input_sample_rate, dechirped);
1283
1286 std::uint64_t output_sample_start = input_start_index;
1287 std::vector<ComplexType> downsampled_block;
1288 if (!dechirped && params::oversampleRatio() > 1)
1289 {
1290 auto& downsampler = streamingDownsampler(receiver_index, input_start_index, block_start_time);
1291 const auto output_start_index = downsampler.outputSampleCount();
1292 downsampler.consume(block);
1294 {
1295 downsampler.finish();
1296 }
1297 downsampled_block = downsampler.takeOutput();
1299 output_sample_start = _streaming_downsample_base_indices[receiver_index] + output_start_index;
1300 output_start_time = _streaming_downsample_segment_start_times[receiver_index] +
1302 }
1303 else if (!dechirped)
1304 {
1308 }
1309
1310 const auto output_samples = !downsampled_block.empty()
1311 ? std::span<const ComplexType>(downsampled_block.data(), downsampled_block.size())
1312 : std::span<const ComplexType>(block.data(), block.size());
1313 if (!output_samples.empty() && (!downsampled_block.empty() || dechirped || params::oversampleRatio() <= 1))
1314 {
1317 }
1318 block.clear();
1319 if (finish_downsampler && _streaming_downsamplers[receiver_index])
1320 {
1321 _streaming_downsamplers[receiver_index].reset();
1322 }
1323 }
1324
1325 fers_signal::DownsamplingSink& SimulationEngine::streamingDownsampler(const std::size_t receiver_index,
1326 const std::uint64_t input_start_index,
1328 {
1329 if (!_streaming_downsamplers[receiver_index])
1330 {
1331 _streaming_downsamplers[receiver_index] = std::make_unique<fers_signal::DownsamplingSink>();
1332 _streaming_downsample_base_indices[receiver_index] =
1333 input_start_index / std::max<unsigned>(1, _streaming_downsamplers[receiver_index]->ratio());
1334 _streaming_downsample_segment_start_times[receiver_index] = segment_start_time;
1335 }
1336 return *_streaming_downsamplers[receiver_index];
1337 }
1338
1339 RealType SimulationEngine::streamingOutputSampleRate(const std::size_t receiver_index) const
1340 {
1341 if (receiver_index >= _world->getReceivers().size())
1342 {
1343 return 0.0;
1344 }
1345
1346 const auto& receiver = _world->getReceivers()[receiver_index];
1348 {
1349 const auto& if_plan = receiver->getFmcwIfResamplerPlan();
1350 return if_plan.has_value() ? if_plan->actual_output_sample_rate_hz : 0.0;
1351 }
1353 {
1354 return params::rate() * static_cast<RealType>(params::oversampleRatio());
1355 }
1356 return params::rate();
1357 }
1358
1359 void SimulationEngine::ensureStreamingOutputStreamOpen(const std::size_t receiver_index,
1360 const RealType first_sample_time, const RealType sample_rate)
1361 {
1362 if (_output_sink == nullptr || receiver_index >= _world->getReceivers().size() || sample_rate <= 0.0)
1363 {
1364 return;
1365 }
1366 if (_streaming_output_stream_ids[receiver_index] != 0 && _streaming_output_stream_open[receiver_index] &&
1367 _streaming_output_file_metadata[receiver_index])
1368 {
1369 return;
1370 }
1371
1372 const auto& receiver = _world->getReceivers()[receiver_index];
1373 auto streaming_sources = collectStreamingSourcesForWindow(params::startTime(), params::endTime());
1374 if (_streaming_output_stream_ids[receiver_index] == 0)
1375 {
1376 _streaming_output_stream_ids[receiver_index] = _output_sink->registerStream(
1378 }
1379 if (!_streaming_output_file_metadata[receiver_index])
1380 {
1381 _streaming_output_file_metadata[receiver_index] =
1382 std::make_shared<OutputFileMetadata>(processing::buildStreamingOutputMetadata(
1383 receiver.get(), "", expectedStreamingOutputSamples(sample_rate), streaming_sources, sample_rate));
1384 }
1385 if (!_streaming_output_stream_open[receiver_index])
1386 {
1387 _output_sink->openStream(_streaming_output_stream_ids[receiver_index], first_sample_time);
1388 _streaming_output_stream_open[receiver_index] = true;
1389 }
1390 }
1391
1392 void SimulationEngine::emitStreamingOutputBlock(const std::size_t receiver_index, const RealType first_sample_time,
1393 const RealType sample_rate,
1394 const std::span<const ComplexType> samples,
1395 const std::uint64_t sample_start,
1396 std::vector<ReceiverSampleBlock>* batch)
1397 {
1398 if (_output_sink == nullptr || samples.empty() || receiver_index >= _world->getReceivers().size())
1399 {
1400 return;
1401 }
1402
1403 const auto& receiver = _world->getReceivers()[receiver_index];
1404 auto& processed = _streaming_output_processed_buffers[receiver_index];
1405 processed.assign(samples.begin(), samples.end());
1407 sample_rate);
1408
1409 auto streaming_sources = collectStreamingSourcesForWindow(params::startTime(), params::endTime());
1410 ensureStreamingOutputStreamOpen(receiver_index, first_sample_time, sample_rate);
1411
1412 const auto block = processing::buildReceiverSampleBlock(receiver.get(), first_sample_time, sample_rate,
1413 processed, sample_start, streaming_sources,
1414 _streaming_output_file_metadata[receiver_index]);
1415 if (batch != nullptr)
1416 {
1417 batch->push_back(block);
1418 }
1419 else
1420 {
1421 _output_sink->submitBlock(block);
1422 }
1423 _streaming_output_sample_cursors[receiver_index] = sample_start + static_cast<std::uint64_t>(processed.size());
1424 }
1425
1426 void SimulationEngine::emitContextHeartbeatsThrough(const RealType simulation_time)
1427 {
1428 if (_output_sink == nullptr)
1429 {
1430 return;
1431 }
1432 if (_next_context_heartbeat_time > simulation_time)
1433 {
1434 return;
1435 }
1436
1437 if (simulation_time - _next_context_heartbeat_time < 1.0)
1438 {
1439 _output_sink->emitContextHeartbeat(_next_context_heartbeat_time);
1440 _next_context_heartbeat_time += 1.0;
1441 return;
1442 }
1443
1444 _output_sink->emitContextHeartbeat(simulation_time);
1445 _next_context_heartbeat_time = simulation_time + 1.0;
1446 }
1447
1448 void SimulationEngine::flushFmcwIfBlocks()
1449 {
1450 for (std::size_t receiver_index = 0; receiver_index < _fmcw_if_block_buffers.size(); ++receiver_index)
1451 {
1452 flushFmcwIfBlock(receiver_index);
1453 }
1454 }
1455
1456 void SimulationEngine::flushFmcwIfBlock(const std::size_t receiver_index)
1457 {
1458 if (receiver_index >= _world->getReceivers().size())
1459 {
1460 return;
1461 }
1462 auto& block = _fmcw_if_block_buffers[receiver_index];
1463 if (block.empty())
1464 {
1465 return;
1466 }
1467 const auto& receiver = _world->getReceivers()[receiver_index];
1469 {
1470 block.clear();
1471 return;
1472 }
1473
1474 applyPulsedInterferenceToFmcwIfBlock(receiver_index, block, _fmcw_if_block_start_times[receiver_index]);
1475 receiver->consumeFmcwIfBlock(block, _fmcw_if_block_start_times[receiver_index]);
1476 block.clear();
1477 }
1478
1479 void SimulationEngine::applyPulsedInterferenceToFmcwIfBlock(const std::size_t receiver_index,
1480 std::span<ComplexType> block,
1482 {
1483 applyPulsedInterferenceToStreamingBlock(receiver_index, block, block_start_time,
1484 params::rate() * static_cast<RealType>(params::oversampleRatio()),
1485 true);
1486 }
1487
1488 void SimulationEngine::addPulsedInterferenceSamples(std::span<ComplexType> block,
1489 std::span<const ComplexType> rendered_pulse,
1490 const long long dest_begin, const long long dest_end,
1491 const std::size_t crop_offset, const RealType block_start_time,
1492 const RealType sample_rate, const bool dechirp_mix,
1493 Receiver* receiver, ReceiverTrackerCache& tracker_cache) const
1494 {
1495 for (long long dest = dest_begin; dest < dest_end; ++dest)
1496 {
1497 const RealType t_sample = block_start_time + static_cast<RealType>(dest) / sample_rate;
1498 const auto source_index = crop_offset + static_cast<std::size_t>(dest - dest_begin);
1499 if (source_index >= rendered_pulse.size())
1500 {
1501 continue;
1502 }
1503 if (dechirp_mix)
1504 {
1505 const auto mixer = calculateDechirpMixer(receiver, t_sample, tracker_cache);
1506 if (!mixer.has_value())
1507 {
1508 continue;
1509 }
1510 block[static_cast<std::size_t>(dest)] += *mixer * std::conj(rendered_pulse[source_index]);
1511 }
1512 else
1513 {
1514 block[static_cast<std::size_t>(dest)] += rendered_pulse[source_index];
1515 }
1516 }
1517 }
1518
1519 void SimulationEngine::applyPulsedInterferenceToStreamingBlock(const std::size_t receiver_index,
1520 std::span<ComplexType> block,
1522 const RealType sample_rate, const bool dechirp_mix)
1523 {
1524 if (block.empty() || receiver_index >= _world->getReceivers().size())
1525 {
1526 return;
1527 }
1528
1529 const auto& receiver = _world->getReceivers()[receiver_index];
1530 if (!std::isfinite(sample_rate) || sample_rate <= 0.0)
1531 {
1532 return;
1533 }
1534 const RealType block_end_time = block_start_time + static_cast<RealType>(block.size()) / sample_rate;
1535 auto& tracker_cache = _if_pulse_tracker_caches[receiver_index];
1536
1538 for (const auto& response : receiver->getPulsedInterferenceLog())
1539 {
1540 const RealType pulse_rate = response->sampleRate();
1541 const unsigned pulse_size = response->sampleCount();
1542 if (pulse_rate <= 0.0 || pulse_size == 0)
1543 {
1544 continue;
1545 }
1546
1547 const RealType pulse_start_time = response->startTime();
1550 {
1551 continue;
1552 }
1553
1556 const auto dest_begin = static_cast<long long>(
1557 std::max<RealType>(0.0, std::ceil((overlap_start - block_start_time) * sample_rate)));
1558 const auto dest_end = static_cast<long long>(std::min<RealType>(
1559 static_cast<RealType>(block.size()), std::ceil((overlap_end - block_start_time) * sample_rate)));
1560 if (dest_begin >= dest_end)
1561 {
1562 continue;
1563 }
1564
1565 const auto interp_padding = static_cast<long long>(params::renderFilterLength()) / 2 + 1;
1566 const long long padded_begin = dest_begin - interp_padding;
1567 const long long padded_end = dest_end + interp_padding;
1568 const RealType render_start = block_start_time + static_cast<RealType>(padded_begin) / sample_rate;
1569 const auto render_count = static_cast<std::size_t>(padded_end - padded_begin);
1570 const auto rendered_pulse = response->renderSlice(sample_rate, render_start, render_count, 0.0);
1571 const auto crop_offset = static_cast<std::size_t>(dest_begin - padded_begin);
1572 addPulsedInterferenceSamples(block, rendered_pulse, dest_begin, dest_end, crop_offset, block_start_time,
1573 sample_rate, dechirp_mix, receiver.get(), tracker_cache);
1574 }
1575 }
1576
1577 std::optional<ComplexType> SimulationEngine::calculateDechirpMixer(Receiver* rx, const RealType t_step,
1578 ReceiverTrackerCache& tracker_cache) const
1579 {
1580 ComplexType reference_sample{0.0, 0.0};
1581 const auto& dechirp_sources = rx->getDechirpSources();
1582 if (tracker_cache.dechirp_reference.size() < dechirp_sources.size())
1583 {
1584 tracker_cache.dechirp_reference.resize(dechirp_sources.size());
1585 }
1586
1587 if (!tracker_cache.last_dechirp_time.has_value() || t_step < *tracker_cache.last_dechirp_time)
1588 {
1589 tracker_cache.active_dechirp_source_index = 0;
1590 std::ranges::fill(tracker_cache.dechirp_reference, FmcwChirpBoundaryTracker{});
1591 }
1592 tracker_cache.last_dechirp_time = t_step;
1593
1594 bool reference_active = false;
1595 auto& source_index = tracker_cache.active_dechirp_source_index;
1596 while (source_index < dechirp_sources.size() && t_step >= dechirp_sources[source_index].segment_end)
1597 {
1598 ++source_index;
1599 }
1600 if (source_index < dechirp_sources.size())
1601 {
1603 if (t_step >= reference_source.segment_start && t_step < reference_source.segment_end &&
1606 {
1607 reference_active = true;
1608 }
1609 }
1610
1611 if (!reference_active)
1612 {
1613 return std::nullopt;
1614 }
1615
1617 if (rx->getDechirpMode() == Receiver::DechirpMode::Physical && _cw_phase_noise_lookup)
1618 {
1619 receiver_phase = _cw_phase_noise_lookup->sample(rx->getTiming().get(), t_step);
1620 }
1621 return reference_sample * std::polar(1.0, receiver_phase);
1622 }
1623
1624 ComplexType SimulationEngine::calculateStreamingSample(Receiver* rx, const RealType t_step,
1625 const std::vector<ActiveStreamingSource>& streaming_sources,
1626 ReceiverTrackerCache& tracker_cache) const
1627 {
1628 const bool dechirping = rx->isDechirpEnabled();
1629 std::optional<ComplexType> dechirp_mixer;
1630 if (dechirping)
1631 {
1632 dechirp_mixer = calculateDechirpMixer(rx, t_step, tracker_cache);
1633 if (!dechirp_mixer.has_value())
1634 {
1635 return {0.0, 0.0};
1636 }
1637 }
1638
1640 : (rx->getDechirpMode() == Receiver::DechirpMode::Ideal
1643
1644 ComplexType total_sample{0.0, 0.0};
1645 for (std::size_t source_index = 0; source_index < streaming_sources.size(); ++source_index)
1646 {
1648 if (!rx->checkFlag(Receiver::RecvFlag::FLAG_NODIRECT))
1649 {
1651 streaming_source, rx, t_step, _cw_phase_noise_lookup.get(), &tracker_cache.direct[source_index],
1653 }
1654 for (std::size_t target_index = 0; target_index < _world->getTargets().size(); ++target_index)
1655 {
1656 const auto& target_ptr = _world->getTargets()[target_index];
1658 streaming_source, rx, target_ptr.get(), t_step, _cw_phase_noise_lookup.get(),
1660 }
1661 }
1662
1663 if (!dechirping)
1664 {
1665 return total_sample;
1666 }
1667
1668 // Mixing Convention: s_IF = s_ref * conj(s_rx)
1669 // This convention is chosen to ensure that:
1670 // 1. Stationary targets (positive delay tau) result in a POSITIVE beat frequency (f_b = alpha * tau).
1671 // 2. In physical dechirp mode, phase noise from the same LO source partially cancels
1672 // at short ranges (Range Correlation Effect).
1673 // 3. For an up-chirp, a receding target (negative RF Doppler) results in a
1674 // higher IF frequency (f_IF = f_b + |f_d|).
1675 return *dechirp_mixer * std::conj(total_sample);
1676 }
1677
1678 void SimulationEngine::appendStreamingTrackerSource()
1679 {
1680 const std::size_t target_count = _world->getTargets().size();
1681
1682 for (auto& cache : _streaming_tracker_caches)
1683 {
1684 cache.direct.emplace_back();
1685 cache.reflected.emplace_back(target_count);
1686 }
1687 }
1688
1689 void SimulationEngine::eraseStreamingTrackerSource(const std::size_t source_index)
1690 {
1691 for (auto& cache : _streaming_tracker_caches)
1692 {
1693 if (source_index < cache.direct.size())
1694 {
1695 cache.direct.erase(cache.direct.begin() + static_cast<std::ptrdiff_t>(source_index));
1696 }
1697 if (source_index < cache.reflected.size())
1698 {
1699 cache.reflected.erase(cache.reflected.begin() + static_cast<std::ptrdiff_t>(source_index));
1700 }
1701 }
1702 }
1703
1704 void SimulationEngine::cleanupInactiveStreamingSources(const RealType from_time)
1705 {
1707 for (std::size_t source_index = sources.size(); source_index > 0; --source_index)
1708 {
1709 const std::size_t index = source_index - 1;
1710 if (sources[index].segment_end > from_time)
1711 {
1712 continue;
1713 }
1714 const auto cleanup_deadline = streamingSourceCleanupDeadline(sources[index], from_time);
1715 if (cleanup_deadline.has_value() && from_time < *cleanup_deadline)
1716 {
1717 continue;
1718 }
1719
1720 sources.erase(sources.begin() + static_cast<std::ptrdiff_t>(index));
1721 eraseStreamingTrackerSource(index);
1722 }
1723 }
1724
1725 std::optional<RealType> SimulationEngine::streamingSourceCleanupDeadline(const ActiveStreamingSource& source,
1726 const RealType from_time) const
1727 {
1728 if (source.transmitter == nullptr || source.carrier_freq <= 0.0)
1729 {
1730 return std::nullopt;
1731 }
1732
1733 std::optional<RealType> latest_deadline;
1734 for (const auto& receiver_ptr : _world->getReceivers())
1735 {
1736 const auto receiver_deadline = receiverCleanupDeadline(source, receiver_ptr.get(), from_time);
1737 if (receiver_deadline.has_value() &&
1739 {
1741 }
1742 }
1743 return latest_deadline;
1744 }
1745
1746 std::optional<RealType> SimulationEngine::receiverCleanupDeadline(const ActiveStreamingSource& source,
1747 const Receiver* const rx,
1748 const RealType from_time) const
1749 {
1750 if (!isStreamingReceiver(rx))
1751 {
1752 return std::nullopt;
1753 }
1754
1755 const auto update_latest = [](std::optional<RealType>& latest, const std::optional<RealType> candidate)
1756 {
1757 if (candidate.has_value() && (!latest.has_value() || *candidate > *latest))
1758 {
1759 latest = candidate;
1760 }
1761 };
1762
1763 const auto interval_deadline = [&](const RealType interval_start,
1764 const RealType interval_end) -> std::optional<RealType>
1765 {
1766 const RealType start = std::max({params::startTime(), from_time, interval_start});
1767 const RealType end = std::min(params::endTime(), interval_end);
1768 if (start >= end)
1769 {
1770 return std::nullopt;
1771 }
1772
1773 std::optional<RealType> latest;
1774 if (!rx->checkFlag(Receiver::RecvFlag::FLAG_NODIRECT))
1775 {
1776 update_latest(latest, directPathCleanupDeadline(source, rx, start, end));
1777 }
1778 for (const auto& target_ptr : _world->getTargets())
1779 {
1780 update_latest(latest, reflectedPathCleanupDeadline(source, rx, target_ptr.get(), start, end));
1781 }
1782 return latest;
1783 };
1784
1785 std::optional<RealType> latest_deadline;
1786 const auto& schedule = rx->getSchedule();
1787 if (schedule.empty())
1788 {
1790 return latest_deadline;
1791 }
1792
1793 for (const auto& period : schedule)
1794 {
1796 }
1797 return latest_deadline;
1798 }
1799
1801 {
1802 // NOLINTBEGIN(cppcoreguidelines-pro-type-static-cast-downcast)
1803 switch (event.type)
1804 {
1806 handleTxPulsedStart(static_cast<Transmitter*>(event.source_object), event.timestamp);
1807 break;
1809 handleRxPulsedWindowStart(static_cast<Receiver*>(event.source_object), event.timestamp);
1810 break;
1812 handleRxPulsedWindowEnd(static_cast<Receiver*>(event.source_object), event.timestamp);
1813 break;
1815 if (const auto source = streamingSourceAtEvent(static_cast<Transmitter*>(event.source_object),
1816 event.timestamp, _internal_stop_time);
1817 source.has_value())
1818 {
1819 handleTxStreamingStart(*source);
1820 }
1821 break;
1823 handleTxStreamingEnd(static_cast<Transmitter*>(event.source_object));
1824 break;
1826 handleRxStreamingStart(static_cast<Receiver*>(event.source_object));
1827 break;
1829 handleRxStreamingEnd(static_cast<Receiver*>(event.source_object));
1830 break;
1831 }
1832 // NOLINTEND(cppcoreguidelines-pro-type-static-cast-downcast)
1833 }
1834
1835 void SimulationEngine::routeResponse(Receiver* rx, std::unique_ptr<serial::Response> response) const
1836 {
1837 if (!response)
1838 {
1839 return;
1840 }
1841 if (rx->getMode() == OperationMode::PULSED_MODE)
1842 {
1843 rx->addResponseToInbox(std::move(response));
1844 }
1845 else
1846 {
1847 rx->addInterferenceToLog(std::move(response));
1848 }
1849 }
1850
1852 {
1853 for (const auto& rx_ptr : _world->getReceivers())
1854 {
1855 if (!rx_ptr->checkFlag(Receiver::RecvFlag::FLAG_NODIRECT))
1856 {
1857 routeResponse(rx_ptr.get(), simulation::calculateResponse(tx, rx_ptr.get(), tx->getSignal(), t_event));
1858 }
1859 for (const auto& target_ptr : _world->getTargets())
1860 {
1861 routeResponse(
1862 rx_ptr.get(),
1863 simulation::calculateResponse(tx, rx_ptr.get(), tx->getSignal(), t_event, target_ptr.get()));
1864 }
1865 }
1866
1867 const RealType next_theoretical_time = t_event + 1.0 / tx->getPrf();
1868 if (const auto next_pulse_opt = tx->getNextPulseTime(next_theoretical_time);
1870 {
1872 }
1873 }
1874
1876 {
1877 rx->setActive(true);
1878 _world->getEventQueue().push({t_event + rx->getWindowLength(), EventType::RX_PULSED_WINDOW_END, rx});
1879 }
1880
1882 {
1883 rx->setActive(false);
1884 const auto active_streaming_sources =
1885 collectStreamingSourcesForWindow(t_event - rx->getWindowLength(), t_event);
1886
1887 RenderingJob job{.ideal_start_time = t_event - rx->getWindowLength(),
1888 .duration = rx->getWindowLength(),
1889 .responses = rx->drainInbox(),
1890 .active_streaming_sources = active_streaming_sources};
1891
1892 rx->enqueueFinalizerJob(std::move(job));
1893
1894 const RealType next_theoretical = t_event - rx->getWindowLength() + 1.0 / rx->getWindowPrf();
1895 if (const auto next_start = rx->getNextWindowTime(next_theoretical);
1897 {
1899 }
1900 }
1901
1903 {
1904 _world->getSimulationState().active_streaming_transmitters.push_back(source);
1905 appendStreamingTrackerSource();
1906 }
1907
1909 {
1910 (void)tx;
1911 // A transmitter stop is a transmit-time boundary, not an instantaneous receive-time cutoff.
1912 // Ended sources are removed only after all future receive-time samples fail the retarded-time gate.
1913 cleanupInactiveStreamingSources(_world->getSimulationState().t_current);
1914 }
1915
1917 {
1918 rx->setActive(true);
1919 const auto receiver_it = std::ranges::find_if(_world->getReceivers(), [rx](const auto& receiver_ptr)
1920 { return receiver_ptr.get() == rx; });
1921 if (receiver_it != _world->getReceivers().end())
1922 {
1923 const auto receiver_index = static_cast<std::size_t>(receiver_it - _world->getReceivers().begin());
1924 _streaming_downsamplers[receiver_index].reset();
1925 if (_eager_context_stream_open)
1926 {
1927 ensureStreamingOutputStreamOpen(receiver_index, _world->getSimulationState().t_current,
1928 streamingOutputSampleRate(receiver_index));
1929 }
1930 }
1931 if (rx->hasFmcwIfResamplingSink())
1932 {
1933 rx->beginFmcwIfResamplingSegment(_world->getSimulationState().t_current);
1934 }
1935 }
1936
1938 {
1939 const auto receiver_it = std::ranges::find_if(_world->getReceivers(), [rx](const auto& receiver_ptr)
1940 { return receiver_ptr.get() == rx; });
1941 if (receiver_it != _world->getReceivers().end())
1942 {
1943 const auto receiver_index = static_cast<std::size_t>(receiver_it - _world->getReceivers().begin());
1944 flushFmcwIfBlock(receiver_index);
1945 flushStreamingOutputBlock(receiver_index, true);
1946 }
1947 if (rx->hasFmcwIfResamplingSink() && _world->getSimulationState().t_current >= params::endTime() &&
1948 _world->getSimulationState().t_current < _internal_stop_time && activePastUserEnd(rx))
1949 {
1950 return;
1951 }
1952 if (rx->hasFmcwIfResamplingSink())
1953 {
1954 rx->endFmcwIfResamplingSegment();
1955 }
1956 if (_output_sink != nullptr && receiver_it != _world->getReceivers().end())
1957 {
1958 const auto receiver_index = static_cast<std::size_t>(receiver_it - _world->getReceivers().begin());
1959 if (_streaming_output_stream_open[receiver_index])
1960 {
1961 _output_sink->closeStream(_streaming_output_stream_ids[receiver_index]);
1962 _streaming_output_stream_open[receiver_index] = false;
1963 }
1964 }
1965 rx->setActive(false);
1966 }
1967
1968 void SimulationEngine::updateProgress() { reportSimulationProgress(_world->getSimulationState().t_current); }
1969
1970 bool SimulationEngine::isCancellationRequested()
1971 {
1972 if (_cancelled)
1973 {
1974 return true;
1975 }
1976 if (_cancel_callback && _cancel_callback())
1977 {
1978 _cancelled = true;
1979 LOG(Level::INFO, "Simulation cancellation requested.");
1980 if (_reporter)
1981 {
1982 _reporter->report("Simulation cancelled", 100, 100);
1983 }
1984 return true;
1985 }
1986 return false;
1987 }
1988
1989 void SimulationEngine::reportSimulationProgress(const RealType t_current)
1990 {
1991 if (!_reporter)
1992 {
1993 return;
1994 }
1995
1996 const RealType start_time = params::startTime();
1997 const RealType end_time = params::endTime();
1998 const RealType duration = end_time - start_time;
1999 const RealType progress_fraction = duration > 0.0 ? (t_current - start_time) / duration : 1.0;
2000 const int progress = static_cast<int>(
2001 std::clamp(progress_fraction * 100.0, static_cast<RealType>(0.0), static_cast<RealType>(100.0)));
2002
2003 if (const auto now = std::chrono::steady_clock::now();
2004 progress != _last_reported_percent || now - _last_report_time >= std::chrono::milliseconds(100))
2005 {
2006 _reporter->report(std::format("Simulating... {:.2f}s / {:.2f}s", t_current, end_time), progress, 100);
2007 _last_reported_percent = progress;
2008 _last_report_time = now;
2009 }
2010 }
2011
2012 std::vector<ActiveStreamingSource> SimulationEngine::collectStreamingSourcesForWindow(const RealType start_time,
2013 const RealType end_time) const
2014 {
2015 // A segment that ended before this window can still be in flight at the receiver.
2016 (void)start_time;
2017 std::vector<ActiveStreamingSource> sources;
2018 for (const auto& transmitter_ptr : _world->getTransmitters())
2019 {
2020 if (!transmitter_ptr->isStreamingMode())
2021 {
2022 continue;
2023 }
2024
2025 const auto append_candidate = [&](const RealType segment_start, const RealType segment_end)
2026 {
2027 auto source = makeActiveSource(transmitter_ptr.get(), segment_start, segment_end);
2028 if (source.segment_start < source.segment_end && source.segment_start < end_time)
2029 {
2030 sources.push_back(source);
2031 }
2032 };
2033
2034 if (transmitter_ptr->getSchedule().empty())
2035 {
2037 continue;
2038 }
2039
2040 for (const auto& period : transmitter_ptr->getSchedule())
2041 {
2042 append_candidate(period.start, std::min(params::endTime(), period.end));
2043 }
2044 }
2045 return sources;
2046 }
2047
2048 void SimulationEngine::shutdown()
2049 {
2050 LOG(Level::INFO, "Simulation compute loop finished. Waiting for receiver finalization tasks...");
2051 if (_reporter)
2052 {
2053 _reporter->report("Simulation compute finished. Waiting for receiver finalization...", 100, 100);
2054 }
2055
2056 for (std::size_t receiver_index = 0; receiver_index < _world->getReceivers().size(); ++receiver_index)
2057 {
2058 const auto& receiver_ptr = _world->getReceivers()[receiver_index];
2060 {
2061 if (_output_sink != nullptr)
2062 {
2063 flushFmcwIfBlock(receiver_index);
2064 receiver_ptr->flushFmcwIfResampling();
2065 flushStreamingOutputBlock(receiver_index, true);
2066 if (_streaming_output_stream_open[receiver_index])
2067 {
2068 _output_sink->closeStream(_streaming_output_stream_ids[receiver_index]);
2069 _streaming_output_stream_open[receiver_index] = false;
2070 }
2071 }
2072 }
2073 else if (receiver_ptr->getMode() == OperationMode::PULSED_MODE)
2074 {
2075 RenderingJob shutdown_job{};
2076 shutdown_job.duration = -1.0;
2077 receiver_ptr->enqueueFinalizerJob(std::move(shutdown_job));
2078 }
2079 }
2080
2081 _pool.wait();
2082 for (auto& finalizer_thread : _finalizer_threads)
2083 {
2084 if (finalizer_thread.joinable())
2085 {
2086 finalizer_thread.join();
2087 }
2088 }
2089
2090 LOG(Level::INFO, "All finalization tasks complete.");
2091 }
2092
2094 const std::function<void(const std::string&, int, int)>& progress_callback,
2095 const std::string& output_dir, const OutputConfig& output_config,
2096 std::function<bool()> cancel_callback, bool* cancelled,
2098 {
2099 if (cancelled != nullptr)
2100 {
2101 *cancelled = false;
2102 }
2103 auto reporter = std::make_shared<ProgressReporter>(progress_callback);
2104 auto metadata_collector = std::make_shared<OutputMetadataCollector>(output_dir);
2105 std::unique_ptr<ReceiverOutputSink> output_sink;
2107 {
2109 output_sink->initializeRun(output_config, params::params.simulation_name);
2110 }
2111 else
2112 {
2113 output_sink = serial::makeHdf5OutputSink(output_dir, metadata_collector);
2114 output_sink->initializeRun(output_config, params::params.simulation_name);
2115 }
2116
2117 SimulationEngine engine(world, pool, reporter, output_dir, metadata_collector, output_sink.get(),
2118 std::move(cancel_callback), isVita49Enabled(output_config));
2119 engine.run();
2120 if (cancelled != nullptr)
2121 {
2122 *cancelled = engine.cancelled();
2123 }
2125 {
2126 LOG(Level::INFO, "Waiting for VITA output stream drain...");
2127 reporter->report("Waiting for VITA output stream drain...", 100, 100);
2128 }
2129 const auto stats = output_sink->finalize();
2130 reporter->report(engine.cancelled() ? "Simulation cancelled" : "Simulation complete", 100, 100);
2131 LOG(Level::INFO, "Event-driven simulation loop finished.");
2132 auto metadata = metadata_collector->snapshot();
2133 if (output_sink)
2134 {
2136 {
2138 if (stats.epoch_unix_nanoseconds.has_value())
2139 {
2140 vita49_metadata.epoch_unix_nanoseconds = stats.epoch_unix_nanoseconds;
2141 }
2142 for (const auto& stream : stats.streams)
2143 {
2144 vita49_metadata.streams.push_back(streamStatsToMetadata(stream));
2145 }
2146 metadata.vita49 = std::move(vita49_metadata);
2147 }
2148 }
2149 return metadata;
2150 }
2151}
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 void submitBlocks(const std::span< const ReceiverSampleBlock > blocks)
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 SteppedFrequencySignal * getSteppedFrequencySignal() const noexcept
Gets the stepped-frequency implementation, if this signal owns one.
const class FmcwTriangleSignal * getFmcwTriangleSignal() const noexcept
Gets the FMCW triangle implementation, if this signal owns one.
RealType getCarrier() const noexcept
Gets the carrier frequency of the radar signal.
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.
Stepped-frequency continuous-wave signal implementation.
RealType effectiveBandwidth() const noexcept
Gets DFT-convention effective bandwidth in hertz.
RealType getSweepPeriod() const noexcept
Gets full sweep period in seconds.
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 firstFrequency(RealType carrier_frequency) const noexcept
Gets first-step RF frequency 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.
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.
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.
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 calculateStreamingReferenceSample(const core::ActiveStreamingSource &source, const RealType timeK, core::FmcwChirpBoundaryTracker *const chirp_tracker, ComplexType &sample_out)
Evaluates the complete complex reference envelope, including file-backed amplitude modulation.
@ 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.