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