FERS 0.1.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
channel_model.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 channel_model.cpp
10 * @brief Implementation of radar channel propagation and interaction models.
11 *
12 * This file provides the implementations for the functions that model the radar
13 * channel, as declared in channel_model.h. It contains the core physics calculations
14 * that determine signal properties based on geometry, velocity, and object characteristics.
15 */
16
17#include "channel_model.h"
18
19#include <algorithm>
20#include <cmath>
21#include <limits>
22#include <string_view>
23#include <unordered_map>
24
25#include "core/logging.h"
26#include "core/parameters.h"
27#include "core/sim_id.h"
28#include "core/world.h"
30#include "math/geometry_ops.h"
31#include "radar/radar_obj.h"
32#include "radar/receiver.h"
33#include "radar/target.h"
34#include "radar/transmitter.h"
35#include "serial/response.h"
36#include "signal/radar_signal.h"
37#include "timing/timing.h"
38
40using logging::Level;
41using math::SVec3;
42using math::Vec3;
43using radar::Receiver;
44using radar::Target;
46
47namespace
48{
49 /// Initializes an FMCW chirp tracker from a retarded time.
52 {
53 tracker.initialized = true;
54 if (t_ret >= source.segment_start)
55 {
57 tracker.n_current = static_cast<std::size_t>(std::floor(time_since_segment_start / source.chirp_period));
58 tracker.t_n = source.segment_start + static_cast<RealType>(tracker.n_current) * source.chirp_period;
59 return;
60 }
61
62 tracker.n_current = 0;
63 tracker.t_n = source.segment_start;
64 }
65
66 /// Initializes an FMCW triangle tracker from a retarded time.
69 {
70 tracker.triangle_initialized = true;
71 if (t_ret < source.segment_start)
72 {
73 tracker.triangle_index = 0;
74 tracker.triangle_leg = 0;
75 tracker.triangle_t_leg = source.segment_start;
76 tracker.triangle_phi_base = 0.0;
77 return;
78 }
79
80 const RealType delta = t_ret - source.segment_start;
81 tracker.triangle_index = static_cast<std::size_t>(std::floor(delta / source.triangle_period));
83 delta - static_cast<RealType>(tracker.triangle_index) * source.triangle_period;
84 tracker.triangle_leg = local_triangle_time < source.chirp_duration ? 0U : 1U;
85 tracker.triangle_t_leg = source.segment_start +
86 static_cast<RealType>(tracker.triangle_index) * source.triangle_period +
87 (tracker.triangle_leg == 1U ? source.chirp_duration : 0.0);
88 tracker.triangle_phi_base =
89 std::fmod(static_cast<RealType>(tracker.triangle_index) * source.mod_phi_tri, 2.0 * PI) +
90 (tracker.triangle_leg == 1U ? source.mod_phi_up : 0.0);
91 if (tracker.triangle_phi_base >= 2.0 * PI)
92 {
93 tracker.triangle_phi_base -= 2.0 * PI;
94 }
95 if (tracker.triangle_phi_base < 0.0)
96 {
97 tracker.triangle_phi_base += 2.0 * PI;
98 }
99 }
100
101 /**
102 * @struct LinkGeometry
103 * @brief Holds geometric properties of a path segment between two points.
104 */
105 struct LinkGeometry
106 {
107 Vec3 u_vec; ///< Unit vector pointing from Source to Destination.
108 RealType dist{}; ///< Distance between Source and Destination.
109 };
110
111 /**
112 * @brief Computes the geometry (distance and direction) between two points.
113 * @param p_from Starting position.
114 * @param p_to Ending position.
115 * @return LinkGeometry containing distance and unit vector.
116 * @throws RangeError if the distance is too small (<= EPSILON).
117 */
118 LinkGeometry computeLink(const Vec3& p_from, const Vec3& p_to)
119 {
120 const Vec3 vec = p_to - p_from;
121 const RealType dist = vec.length();
122
123 if (dist <= EPSILON)
124 {
125 // LOG(Level::FATAL) is handled by the caller or generic exception handler if needed,
126 // but for RangeError strictly we just throw here to keep it pure.
127 // However, existing code logged FATAL before throwing.
128 // We'll throw RangeError, and let callers decide if they want to log or return 0.
130 }
131
132 return {vec / dist, dist};
133 }
134
135 /**
136 * @brief Calculates the antenna gain for a specific direction and time.
137 * @param radar The radar object (Transmitter or Receiver).
138 * @param direction_vec The unit vector pointing AWAY from the antenna towards the target/receiver.
139 * @param time The simulation time for rotation lookup.
140 * @param lambda The signal wavelength.
141 * @return The linear gain value.
142 */
144 {
145 return radar->getGain(SVec3(direction_vec), radar->getRotation(time), lambda);
146 }
147
148 /**
149 * @brief Computes the power scaling factor for a direct path (Friis Transmission Equation).
150 * @param tx_gain Transmitter gain (linear).
151 * @param rx_gain Receiver gain (linear).
152 * @param lambda Wavelength (meters).
153 * @param dist Distance (meters).
154 * @param no_prop_loss If true, distance-based attenuation is ignored.
155 * @return The power scaling factor (Pr / Pt).
156 */
158 bool no_prop_loss)
159 {
160 const RealType numerator = tx_gain * rx_gain * lambda * lambda;
161 RealType denominator = 16.0 * PI * PI; // (4 * PI)^2
162
163 if (!no_prop_loss)
164 {
165 denominator *= dist * dist;
166 }
167
168 return numerator / denominator;
169 }
170
171 /**
172 * @brief Computes the power scaling factor for a reflected path (Bistatic Radar Range Equation).
173 * @param tx_gain Transmitter gain (linear).
174 * @param rx_gain Receiver gain (linear).
175 * @param rcs Target Radar Cross Section (m^2).
176 * @param lambda Wavelength (meters).
177 * @param r_tx Distance from Transmitter to Target.
178 * @param r_rx Distance from Target to Receiver.
179 * @param no_prop_loss If true, distance-based attenuation is ignored.
180 * @return The power scaling factor (Pr / Pt).
181 */
184 {
185 const RealType numerator = tx_gain * rx_gain * rcs * lambda * lambda;
186 RealType denominator = 64.0 * PI * PI * PI; // (4 * PI)^3
187
188 if (!no_prop_loss)
189 {
190 denominator *= r_tx * r_tx * r_rx * r_rx;
191 }
192
193 return numerator / denominator;
194 }
195
196 /**
197 * @brief Computes the non-coherent phase shift due to timing offsets.
198 *
199 * Used for CW simulation where LO effects are modeled analytically.
200 *
201 * @param tx The transmitter.
202 * @param rx The receiver.
203 * @param time The current simulation time.
204 * @return The phase shift in radians.
205 */
207 {
208 const auto tx_timing = tx->getTiming();
209 const auto rx_timing = rx->getTiming();
210 const RealType delta_f = tx_timing->getFreqOffset() - rx_timing->getFreqOffset();
211 const RealType delta_phi = tx_timing->getPhaseOffset() - rx_timing->getPhaseOffset();
212 return 2 * PI * delta_f * time + delta_phi;
213 }
214
215 /// Computes deterministic timing phase for one timing source when no lookup is available.
217 {
218 if (timing == nullptr)
219 {
220 return 0.0;
221 }
222 return 2.0 * PI * timing->getFreqOffset() * time + timing->getPhaseOffset();
223 }
224
225 /// Computes timing phase with an optional lookup and streaming phase-application mode.
227 const RealType tx_time, const simulation::CwPhaseNoiseLookup* phase_noise_lookup,
229 {
231 {
232 return 0.0;
233 }
235 {
236 if (phase_noise_lookup == nullptr)
237 {
238 return computeSingleTimingPhase(tx->getTiming().get(), tx_time);
239 }
240 return phase_noise_lookup->sample(tx->getTiming().get(), tx_time);
241 }
242 if (phase_noise_lookup == nullptr)
243 {
245 }
246 return phase_noise_lookup->phaseDifference(rx->getTiming().get(), rx_time, tx->getTiming().get(), tx_time);
247 }
248
249 /// Checks whether received power is above the thermal noise floor.
251 {
252 // Use configured rate or default to 1Hz if unconfigured to prevent divide-by-zero or silly values
253 const RealType bw = params::rate() > 0 ? params::rate() : 1.0;
254 const RealType noise_floor = params::boltzmannK() * (temp_kelvin > 0 ? temp_kelvin : 290.0) * bw;
255 return power_watts > noise_floor;
256 }
257
258 /**
259 * @brief Converts power in watts to decibels milliwatts (dBm).
260 *
261 * @param watts Power in watts.
262 * @return Power in dBm. Returns -999.0 dBm for non-positive input.
263 */
265 {
266 if (watts <= 0)
267 {
268 return -999.0;
269 }
270 return 10.0 * std::log10(watts * 1000.0);
271 }
272
273 /**
274 * @brief Converts power in watts to decibels (dB).
275 *
276 * @param watts Power in watts.
277 * @return Power in decibels (dB). Returns -999.0 dB for non-positive input.
278 */
280 {
281 if (watts <= 0)
282 {
283 return -999.0;
284 }
285 return 10.0 * std::log10(watts);
286 }
287
288 /**
289 * @brief Checks if a component is active at the given time based on its schedule.
290 *
291 * @param schedule The component's operating schedule.
292 * @param time The current simulation time.
293 * @return true If the schedule is empty (implied always on) or if time is within a period.
294 * @return false If the schedule is populated but the time is outside all periods.
295 */
296 bool isComponentActive(const std::vector<radar::SchedulePeriod>& schedule, RealType time)
297 {
298 if (schedule.empty())
299 {
300 return true;
301 }
302 for (const auto& period : schedule)
303 {
304 if (time >= period.start && time <= period.end)
305 {
306 return true;
307 }
308 }
309 return false;
310 }
311
312 /// Builds a compatibility streaming-source cache for classic CW paths.
314 {
315 auto source = core::makeActiveSource(trans, params::startTime(), std::numeric_limits<RealType>::max());
317 {
318 source.segment_start = std::numeric_limits<RealType>::lowest();
319 }
320 return source;
321 }
322
325 {
327 const auto chirp_index = static_cast<std::size_t>(std::floor(time_since_segment_start / source.chirp_period));
328 if (source.chirp_count.has_value() && chirp_index >= *source.chirp_count)
329 {
330 return false;
331 }
334 {
335 return false;
336 }
337 phase_out = -2.0 * PI * source.carrier_freq * tau + source.two_pi_f0 * chirp_time +
339 return true;
340 }
341
345 {
346 if (!chirp_tracker.initialized)
347 {
349 }
350
351 while (t_ret >= chirp_tracker.t_n + source.chirp_period)
352 {
353 chirp_tracker.t_n += source.chirp_period;
354 ++chirp_tracker.n_current;
355 }
356
357 if (source.chirp_count.has_value() && chirp_tracker.n_current >= *source.chirp_count)
358 {
359 return false;
360 }
361
362 const RealType u_ret = t_ret - chirp_tracker.t_n;
364 {
365 return false;
366 }
367
368 phase_out =
369 -2.0 * PI * source.carrier_freq * tau + source.two_pi_f0 * u_ret + source.s_pi_alpha * u_ret * u_ret;
370 return true;
371 }
372
375 {
376 const RealType delta = t_ret - source.segment_start;
377 const auto triangle_index = static_cast<std::size_t>(std::floor(delta / source.triangle_period));
378 if (source.triangle_count.has_value() && triangle_index >= *source.triangle_count)
379 {
380 return false;
381 }
382 const RealType local_triangle_time = delta - static_cast<RealType>(triangle_index) * source.triangle_period;
383 const bool down_leg = local_triangle_time >= source.chirp_duration;
386 {
387 return false;
388 }
389 const RealType phi_base = std::fmod(static_cast<RealType>(triangle_index) * source.mod_phi_tri, 2.0 * PI) +
390 (down_leg ? source.mod_phi_up : 0.0);
391 const RealType modular_phi_base = phi_base >= 2.0 * PI ? phi_base - 2.0 * PI : phi_base;
392 const RealType linear_coeff = down_leg ? source.two_pi_f0_plus_B : source.two_pi_f0;
393 const RealType quad_coeff = down_leg ? source.neg_pi_alpha : source.pi_alpha;
396 return true;
397 }
398
401 {
402 while (t_ret >= chirp_tracker.triangle_t_leg + source.chirp_duration)
403 {
404 chirp_tracker.triangle_t_leg += source.chirp_duration;
405 chirp_tracker.triangle_leg = 1U - chirp_tracker.triangle_leg;
406 if (chirp_tracker.triangle_leg == 0U)
407 {
408 ++chirp_tracker.triangle_index;
409 }
410 chirp_tracker.triangle_phi_base += source.mod_phi_up;
411 if (chirp_tracker.triangle_phi_base >= 2.0 * PI)
412 {
413 chirp_tracker.triangle_phi_base -= 2.0 * PI;
414 }
415 }
416 }
417
421 {
422 if (!chirp_tracker.triangle_initialized)
423 {
425 }
426
428 if (source.triangle_count.has_value() && chirp_tracker.triangle_index >= *source.triangle_count)
429 {
430 return false;
431 }
432
433 const RealType u_ret = t_ret - chirp_tracker.triangle_t_leg;
435 {
436 return false;
437 }
438
439 const bool down_leg = chirp_tracker.triangle_leg == 1U;
440 const RealType linear_coeff = down_leg ? source.two_pi_f0_plus_B : source.two_pi_f0;
441 const RealType quad_coeff = down_leg ? source.neg_pi_alpha : source.pi_alpha;
442 phase_out = -2.0 * PI * source.carrier_freq * tau + chirp_tracker.triangle_phi_base + linear_coeff * u_ret +
444 return true;
445 }
446
447 /// Computes streaming phase for CW or FMCW sources at a receiver time.
450 {
451 if (source.carrier_freq <= 0.0)
452 {
453 return false;
454 }
455
456 const RealType t_ret = rx_time - tau;
458 {
459 return false;
460 }
461
463 {
464 if (chirp_tracker == nullptr)
465 {
467 }
469 }
470
472 {
473 if (chirp_tracker == nullptr)
474 {
476 }
478 }
479
480 phase_out = -2.0 * PI * source.carrier_freq * tau;
481 return true;
482 }
483
484 /// Returns average radiated power for preview visualization.
486 {
487 if (waveform == nullptr)
488 {
489 return 0.0;
490 }
491 if (const auto* fmcw = waveform->getFmcwChirpSignal(); fmcw != nullptr)
492 {
493 return waveform->getPower() * (fmcw->getChirpDuration() / fmcw->getChirpPeriod());
494 }
495 if (waveform->isFmcwTriangle())
496 {
497 return waveform->getPower();
498 }
499 return waveform->getPower();
500 }
501
502 /// Formats received or radiated power as a dBm preview label.
503 std::string formatPreviewDbmLabel(const RealType watts, const std::string_view prefix = {})
504 {
505 return std::format("{}{:.1f} dBm", prefix, wattsToDbm(watts));
506 }
507
508 /// Formats target illumination density as a dBW-per-square-meter preview label.
510 {
511 return std::format("{:.1f} dBW/m\u00B2", wattsToDb(watts));
512 }
513}
514
515namespace simulation
516{
518 {
519 if (samples.empty())
520 {
521 return 0.0;
522 }
523 if ((time <= start_time) || (samples.size() == 1) || (dt <= 0.0))
524 {
525 return samples.front();
526 }
527
528 const RealType position = (time - start_time) / dt;
529 const auto last_index = static_cast<RealType>(samples.size() - 1);
530 if (position >= last_index)
531 {
532 return samples.back();
533 }
534
535 const auto lower_index = static_cast<std::size_t>(position);
536 const RealType fraction = position - static_cast<RealType>(lower_index);
537 return samples[lower_index] + fraction * (samples[lower_index + 1] - samples[lower_index]);
538 }
539
540 CwPhaseNoiseLookup CwPhaseNoiseLookup::build(const std::span<const std::shared_ptr<timing::Timing>> timings,
541 const RealType start_time, const RealType end_time)
542 {
544 lookup.start_time = start_time;
545 lookup.end_time = std::max(start_time, end_time);
546
547 const RealType sample_rate = params::rate() * params::oversampleRatio();
548 lookup.dt = sample_rate > 0.0 ? (1.0 / sample_rate) : 1.0;
549
550 const auto sample_count =
551 static_cast<std::size_t>(std::ceil((lookup.end_time - lookup.start_time) / lookup.dt) + 1.0);
552 constexpr std::size_t phase_noise_warning_threshold_bytes = 500ULL * 1024ULL * 1024ULL;
553 const auto bytes_per_buffer = sample_count * sizeof(RealType);
554
555 for (const auto& timing : timings)
556 {
557 if (!timing || lookup.buffers.contains(timing->getId()))
558 {
559 continue;
560 }
561
563 buffer.start_time = lookup.start_time;
564 buffer.dt = lookup.dt;
565 if (timing->isEnabled())
566 {
567 auto timing_clone = timing->clone();
568 if (lookup.start_time > 0.0)
569 {
570 const auto skip_count = static_cast<std::size_t>(std::llround(lookup.start_time / lookup.dt));
571 timing_clone->skipSamples(skip_count);
572 }
573 // TODO: Replace whole-simulation CW lookup generation with chunked/streaming generation.
575 {
576 LOG(Level::WARNING,
577 "CW phase-noise lookup for timing '{}' allocates {} bytes; large scenarios need chunked "
578 "streaming.",
579 timing->getName(), bytes_per_buffer);
580 }
581 buffer.samples.resize(sample_count);
582 std::ranges::generate(buffer.samples, [&] { return timing_clone->getNextSample(); });
583 }
584
585 lookup.buffers.emplace(timing->getId(), std::move(buffer));
586 }
587
588 return lookup;
589 }
590
591 RealType CwPhaseNoiseLookup::sample(const timing::Timing* const timing, const RealType time) const noexcept
592 {
593 if (timing == nullptr)
594 {
595 return 0.0;
596 }
597 const auto it = buffers.find(timing->getId());
598 if (it == buffers.end())
599 {
600 return 0.0;
601 }
602 return it->second.sampleAt(time);
603 }
604
606 const timing::Timing* const tx_timing,
607 const RealType tx_time) const noexcept
608 {
609 return sample(tx_timing, tx_time) - sample(rx_timing, rx_time);
610 }
611
612 void solveRe(const Transmitter* trans, const Receiver* recv, const Target* targ,
613 const std::chrono::duration<RealType>& time, const RadarSignal* wave, ReResults& results)
614 {
615 // Note: RangeError log messages are handled by the original catch block in calculateResponse
616 // or explicitly here if strict adherence to original logging is required.
617 // Using the helper logic which throws RangeError on epsilon check.
618
619 const RealType t_val = time.count();
620 const auto p_tx = trans->getPosition(t_val);
621 const auto p_rx = recv->getPosition(t_val);
622 const auto p_tgt = targ->getPosition(t_val);
623
624 // Link 1: Tx -> Target
625 LinkGeometry link_tx_tgt;
626 // Link 2: Target -> Rx (Note: Vector for calculation is Tgt->Rx)
627 LinkGeometry link_tgt_rx;
628
629 try
630 {
632 link_tgt_rx = computeLink(p_tgt, p_rx); // Vector Tgt -> Rx
633 }
634 catch (const RangeError&)
635 {
636 LOG(Level::INFO, "Transmitter or Receiver too close to Target for accurate simulation");
637 throw;
638 }
639
640 results.delay = (link_tx_tgt.dist + link_tgt_rx.dist) / params::c();
641
642 // Calculate RCS
643 // Note: getRcs expects (InAngle, OutAngle).
644 // InAngle: Tx -> Tgt (link_tx_tgt.u_vec)
645 // OutAngle: Rx -> Tgt (Opposite of Tgt->Rx, so -link_tgt_rx.u_vec)
646 // This matches existing logic.
649 const auto rcs = targ->getRcs(in_angle, out_angle, t_val);
650
651 const auto wavelength = params::c() / wave->getCarrier();
652
653 // Tx Gain: Direction Tx -> Tgt
655 // Rx Gain: Direction Rx -> Tgt (Opposite of Tgt->Rx).
656 // Time is time + delay.
657 const auto rx_gain = computeAntennaGain(recv, -link_tgt_rx.u_vec, results.delay + t_val, wavelength);
658
659 const bool no_loss = recv->checkFlag(Receiver::RecvFlag::FLAG_NOPROPLOSS);
660 results.power =
662
663 results.phase = -results.delay * 2 * PI * wave->getCarrier();
664 }
665
666 void solveReDirect(const Transmitter* trans, const Receiver* recv, const std::chrono::duration<RealType>& time,
668 {
669 const RealType t_val = time.count();
670 const auto p_tx = trans->getPosition(t_val);
671 const auto p_rx = recv->getPosition(t_val);
672
673 LinkGeometry link;
674 try
675 {
676 link = computeLink(p_tx, p_rx); // Vector Tx -> Rx
677 }
678 catch (const RangeError&)
679 {
680 LOG(Level::INFO, "Transmitter or Receiver too close for accurate simulation");
681 throw;
682 }
683
684 results.delay = link.dist / params::c();
685 const RealType wavelength = params::c() / wave->getCarrier();
686
687 // Discrepancy Fix: Original code used (Rx - Tx) for Receiver Gain but (Tx - Rx) logic for Transmitter gain
688 // was ambiguous/incorrect (using `tpos - rpos` which is Rx->Tx).
689 // Per `calculateDirectPathContribution` preference:
690 // Tx Gain uses Vector Tx -> Rx.
691 // Rx Gain uses Vector Rx -> Tx.
692
693 const auto tx_gain = computeAntennaGain(trans, link.u_vec, t_val, wavelength);
694 const auto rx_gain = computeAntennaGain(recv, -link.u_vec, t_val + results.delay, wavelength);
695
696 const bool no_loss = recv->checkFlag(Receiver::RecvFlag::FLAG_NOPROPLOSS);
698
699 results.phase = -results.delay * 2 * PI * wave->getCarrier();
700 }
701
703 const CwPhaseNoiseLookup* const phase_noise_lookup)
704 {
706 phase_noise_lookup);
707 }
708
710 const Receiver* recv, const RealType timeK,
711 const CwPhaseNoiseLookup* const phase_noise_lookup,
714 {
715 const auto* const trans = source.transmitter;
716 if (trans == nullptr)
717 {
718 return {0.0, 0.0};
719 }
720 // Check for co-location to prevent singularities.
721 // If they share the same platform, we assume they are isolated (no leakage) or explicit
722 // monostatic handling is required (which is not modeled via the far-field path).
723 if (trans->getPlatform() == recv->getPlatform())
724 {
725 return {0.0, 0.0};
726 }
727
728 const auto p_tx = trans->getPlatform()->getPosition(timeK);
729 const auto p_rx = recv->getPlatform()->getPosition(timeK);
730
731 LinkGeometry link;
732 try
733 {
735 }
736 catch (const RangeError&)
737 {
738 return {0.0, 0.0};
739 }
740
741 const RealType tau = link.dist / params::c();
742 if (source.carrier_freq <= 0.0)
743 {
744 return {0.0, 0.0};
745 }
746 const RealType lambda = params::c() / source.carrier_freq;
747
748 // Tx Gain: Direction Tx -> Rx
750 // Rx Gain: Direction Rx -> Tx (-u_vec)
752
753 const bool no_loss = recv->checkFlag(Receiver::RecvFlag::FLAG_NOPROPLOSS);
755
756 // Include Signal Power
757 const RealType amplitude = source.amplitude * std::sqrt(scaling_factor);
758
759 // Carrier Phase
760 RealType phase = 0.0;
761 if (!computeStreamingPhase(source, timeK, tau, chirp_tracker, phase))
762 {
763 return {0.0, 0.0};
764 }
765 ComplexType contribution = std::polar(amplitude, phase);
766
767 // Non-coherent Local Oscillator Effects
769 computeTimingPhase(trans, recv, timeK, timeK - tau, phase_noise_lookup, timing_phase_mode);
770 contribution *= std::polar(1.0, non_coherent_phase);
771
772 return contribution;
773 }
774
780
782 const RealType timeK,
783 const CwPhaseNoiseLookup* const phase_noise_lookup)
784 {
786 phase_noise_lookup);
787 }
788
790 const Receiver* recv, const Target* targ,
791 const RealType timeK,
792 const CwPhaseNoiseLookup* const phase_noise_lookup,
795 {
796 const auto* const trans = source.transmitter;
797 if (trans == nullptr)
798 {
799 return {0.0, 0.0};
800 }
801 // Check for co-location involving the target.
802 // We do not model a platform tracking itself (R=0) or illuminating itself (R=0).
803 if (trans->getPlatform() == targ->getPlatform() || recv->getPlatform() == targ->getPlatform())
804 {
805 return {0.0, 0.0};
806 }
807
808 const auto p_tx = trans->getPlatform()->getPosition(timeK);
809 const auto p_rx = recv->getPlatform()->getPosition(timeK);
810 const auto p_tgt = targ->getPlatform()->getPosition(timeK);
811
812 LinkGeometry link_tx_tgt;
813 LinkGeometry link_tgt_rx;
814
815 try
816 {
819 }
820 catch (const RangeError&)
821 {
822 return {0.0, 0.0};
823 }
824
825 const RealType tau = (link_tx_tgt.dist + link_tgt_rx.dist) / params::c();
826 if (source.carrier_freq <= 0.0)
827 {
828 return {0.0, 0.0};
829 }
830 const RealType lambda = params::c() / source.carrier_freq;
831
832 // RCS Lookups: In (Tx->Tgt), Out (Rx->Tgt = - (Tgt->Rx))
835 const RealType rcs = targ->getRcs(in_angle, out_angle, timeK);
836
837 // Tx Gain: Direction Tx -> Tgt
839 // Rx Gain: Direction Rx -> Tgt (- (Tgt->Rx)). Time: timeK + tau.
841
842 const bool no_loss = recv->checkFlag(Receiver::RecvFlag::FLAG_NOPROPLOSS);
845
846 // Include Signal Power
847 const RealType amplitude = source.amplitude * std::sqrt(scaling_factor);
848
849 RealType phase = 0.0;
850 if (!computeStreamingPhase(source, timeK, tau, chirp_tracker, phase))
851 {
852 return {0.0, 0.0};
853 }
854 ComplexType contribution = std::polar(amplitude, phase);
855
856 // Non-coherent Local Oscillator Effects
858 computeTimingPhase(trans, recv, timeK, timeK - tau, phase_noise_lookup, timing_phase_mode);
859 contribution *= std::polar(1.0, non_coherent_phase);
860
861 return contribution;
862 }
863
864 std::unique_ptr<serial::Response> calculateResponse(const Transmitter* trans, const Receiver* recv,
865 const RadarSignal* signal, const RealType startTime,
866 const Target* targ)
867 {
868 // If calculating direct path (no target) and components are co-located:
869 // 1. If explicitly attached (monostatic), skip (internal leakage handled elsewhere).
870 // 2. If independent but on the same platform, distance is 0. Far-field logic (1/R^2)
871 // diverges. We skip calculation to avoid RangeError crashes, assuming
872 // no direct coupling/interference for co-located far-field antennas.
873 if (targ == nullptr && (trans->getAttached() == recv || trans->getPlatform() == recv->getPlatform()))
874 {
875 return nullptr;
876 }
877
878 // If calculating reflected path and target is co-located with either Tx or Rx:
879 // Skip to avoid singularity. Simulating a radar tracking its own platform
880 // requires near-field clutter models, not point-target RCS models.
881 if (targ != nullptr &&
882 (targ->getPlatform() == trans->getPlatform() || targ->getPlatform() == recv->getPlatform()))
883 {
884 LOG(Level::TRACE,
885 "Skipping reflected path calculation for Target {} co-located with Transmitter {} or Receiver {}",
886 targ->getName(), trans->getName(), recv->getName());
887 return nullptr;
888 }
889
890 const auto start_time_chrono = std::chrono::duration<RealType>(startTime);
891 const auto end_time_chrono = start_time_chrono + std::chrono::duration<RealType>(signal->getLength());
892 const auto sample_time_chrono = std::chrono::duration<RealType>(1.0 / params::simSamplingRate());
893 const int point_count = static_cast<int>(std::ceil(signal->getLength() / sample_time_chrono.count()));
894
895 if ((targ != nullptr) && point_count == 0)
896 {
897 LOG(Level::FATAL, "No time points are available for execution!");
898 throw std::runtime_error("No time points are available for execution!");
899 }
900
901 auto response = std::make_unique<serial::Response>(signal, trans);
902
903 try
904 {
905 for (int i = 0; i <= point_count; ++i)
906 {
907 const auto current_time =
909
911 if (targ != nullptr)
912 {
914 }
915 else
916 {
918 }
919
920 interp::InterpPoint const point{.power = results.power,
921 .time = current_time.count() + results.delay,
922 .delay = results.delay,
923 .phase = results.phase};
924 response->addInterpPoint(point);
925 }
926 }
927 catch (const RangeError&)
928 {
929 LOG(Level::INFO, "Receiver or Transmitter too close for accurate simulation");
930 throw; // Re-throw to be caught by the runner
931 }
932
933 return response;
934 }
935
936 namespace
937 {
938 struct PreviewTransmitterContext
939 {
944 };
945
946 struct PreviewReceiverContext
947 {
951 };
952
953 PreviewTransmitterContext makePreviewTransmitterContext(const Transmitter& transmitter, const RealType time)
954 {
955 const auto* waveform = transmitter.getSignal();
956 return PreviewTransmitterContext{.transmitter = transmitter,
957 .position = transmitter.getPosition(time),
958 .radiated_power = previewRadiatedPower(waveform),
959 .lambda =
960 (waveform != nullptr) ? (params::c() / waveform->getCarrier()) : 0.3};
961 }
962
963 PreviewReceiverContext makePreviewReceiverContext(const Receiver& receiver, const RealType time)
964 {
965 return PreviewReceiverContext{.receiver = receiver,
966 .position = receiver.getPosition(time),
967 .no_loss = receiver.checkFlag(Receiver::RecvFlag::FLAG_NOPROPLOSS)};
968 }
969
970 void addIlluminatorLinks(std::vector<PreviewLink>& links, const PreviewTransmitterContext& tx_ctx,
971 const core::World& world, const RealType time)
972 {
973 for (const auto& target : world.getTargets())
974 {
975 const auto target_position = target->getPosition(time);
976 const Vec3 vec_tx_tgt = target_position - tx_ctx.position;
978 if (range <= EPSILON)
979 {
980 continue;
981 }
982
983 const Vec3 u_tx_tgt = vec_tx_tgt / range;
984 const RealType gain = computeAntennaGain(&tx_ctx.transmitter, u_tx_tgt, time, tx_ctx.lambda);
985 const RealType power_density = (tx_ctx.radiated_power * gain) / (4.0 * PI * range * range);
986 links.push_back({.type = LinkType::BistaticTxTgt,
987 .quality = LinkQuality::Strong,
989 .display_value = wattsToDb(power_density),
990 .source_id = tx_ctx.transmitter.getId(),
991 .dest_id = target->getId(),
992 .origin_id = tx_ctx.transmitter.getId()});
993 }
994 }
995
996 void addMonostaticLinks(std::vector<PreviewLink>& links, const PreviewTransmitterContext& tx_ctx,
997 const PreviewReceiverContext& rx_ctx, const core::World& world, const RealType time)
998 {
999 for (const auto& target : world.getTargets())
1000 {
1001 const auto target_position = target->getPosition(time);
1002 const Vec3 vec_tx_tgt = target_position - tx_ctx.position;
1003 const RealType range = vec_tx_tgt.length();
1004 if (range <= EPSILON)
1005 {
1006 continue;
1007 }
1008
1009 const Vec3 u_tx_tgt = vec_tx_tgt / range;
1010 const RealType gt = computeAntennaGain(&tx_ctx.transmitter, u_tx_tgt, time, tx_ctx.lambda);
1011 const RealType gr = computeAntennaGain(&rx_ctx.receiver, u_tx_tgt, time, tx_ctx.lambda);
1014 const RealType rcs = target->getRcs(in_angle, out_angle, time);
1015 const RealType power_ratio =
1016 computeReflectedPathPower(gt, gr, rcs, tx_ctx.lambda, range, range, rx_ctx.no_loss);
1017 const RealType pr_watts = tx_ctx.radiated_power * power_ratio;
1018 const RealType pr_unit_watts = tx_ctx.radiated_power *
1019 computeReflectedPathPower(gt, gr, 1.0, tx_ctx.lambda, range, range, rx_ctx.no_loss);
1020
1021 links.push_back({.type = LinkType::Monostatic,
1022 .quality = isSignalStrong(pr_unit_watts, rx_ctx.receiver.getNoiseTemperature())
1026 .display_value = wattsToDbm(pr_unit_watts),
1027 .source_id = tx_ctx.transmitter.getId(),
1028 .dest_id = target->getId(),
1029 .origin_id = tx_ctx.transmitter.getId(),
1030 .rcs = rcs,
1031 .actual_power_dbm = wattsToDbm(pr_watts)});
1032 }
1033 }
1034
1035 void addDirectLink(std::vector<PreviewLink>& links, const PreviewTransmitterContext& tx_ctx,
1036 const PreviewReceiverContext& rx_ctx, const RealType time)
1037 {
1038 if (rx_ctx.receiver.checkFlag(Receiver::RecvFlag::FLAG_NODIRECT))
1039 {
1040 return;
1041 }
1042
1043 const Vec3 vec_direct = rx_ctx.position - tx_ctx.position;
1044 const RealType range = vec_direct.length();
1045 if (range <= EPSILON)
1046 {
1047 return;
1048 }
1049
1050 const Vec3 u_tx_rx = vec_direct / range;
1051 const RealType gt = computeAntennaGain(&tx_ctx.transmitter, u_tx_rx, time, tx_ctx.lambda);
1052 const RealType gr = computeAntennaGain(&rx_ctx.receiver, -u_tx_rx, time, tx_ctx.lambda);
1053 const RealType power_ratio = computeDirectPathPower(gt, gr, tx_ctx.lambda, range, rx_ctx.no_loss);
1054 const RealType pr_watts = tx_ctx.radiated_power * power_ratio;
1055
1056 links.push_back({.type = LinkType::DirectTxRx,
1057 .quality = LinkQuality::Strong,
1058 .label = formatPreviewDbmLabel(pr_watts, "Direct: "),
1059 .display_value = wattsToDbm(pr_watts),
1060 .source_id = tx_ctx.transmitter.getId(),
1061 .dest_id = rx_ctx.receiver.getId(),
1062 .origin_id = tx_ctx.transmitter.getId()});
1063 }
1064
1065 void addBistaticTargetReceiverLinks(std::vector<PreviewLink>& links, const PreviewTransmitterContext& tx_ctx,
1066 const PreviewReceiverContext& rx_ctx, const core::World& world,
1067 const RealType time)
1068 {
1069 for (const auto& target : world.getTargets())
1070 {
1071 const auto target_position = target->getPosition(time);
1072 const Vec3 vec_tx_tgt = target_position - tx_ctx.position;
1073 const Vec3 vec_tgt_rx = rx_ctx.position - target_position;
1074 const RealType r1 = vec_tx_tgt.length();
1075 const RealType r2 = vec_tgt_rx.length();
1076 if (r1 <= EPSILON || r2 <= EPSILON)
1077 {
1078 continue;
1079 }
1080
1081 const Vec3 u_tx_tgt = vec_tx_tgt / r1;
1082 const Vec3 u_tgt_rx = vec_tgt_rx / r2;
1083 const RealType gt = computeAntennaGain(&tx_ctx.transmitter, u_tx_tgt, time, tx_ctx.lambda);
1084 const RealType gr = computeAntennaGain(&rx_ctx.receiver, -u_tgt_rx, time, tx_ctx.lambda);
1087 const RealType rcs = target->getRcs(in_angle, out_angle, time);
1088 const RealType power_ratio =
1089 computeReflectedPathPower(gt, gr, rcs, tx_ctx.lambda, r1, r2, rx_ctx.no_loss);
1090 const RealType pr_watts = tx_ctx.radiated_power * power_ratio;
1091 const RealType pr_unit_watts = tx_ctx.radiated_power *
1092 computeReflectedPathPower(gt, gr, 1.0, tx_ctx.lambda, r1, r2, rx_ctx.no_loss);
1093
1094 links.push_back({.type = LinkType::BistaticTgtRx,
1095 .quality = isSignalStrong(pr_unit_watts, rx_ctx.receiver.getNoiseTemperature())
1099 .display_value = wattsToDbm(pr_unit_watts),
1100 .source_id = target->getId(),
1101 .dest_id = rx_ctx.receiver.getId(),
1102 .origin_id = tx_ctx.transmitter.getId(),
1103 .rcs = rcs,
1104 .actual_power_dbm = wattsToDbm(pr_watts)});
1105 }
1106 }
1107
1108 void addReceiverLinks(std::vector<PreviewLink>& links, const PreviewTransmitterContext& tx_ctx,
1109 const PreviewReceiverContext& rx_ctx, const core::World& world, const RealType time)
1110 {
1111 if (tx_ctx.transmitter.getAttached() == &rx_ctx.receiver)
1112 {
1113 addMonostaticLinks(links, tx_ctx, rx_ctx, world, time);
1114 return;
1115 }
1116
1117 addDirectLink(links, tx_ctx, rx_ctx, time);
1118 addBistaticTargetReceiverLinks(links, tx_ctx, rx_ctx, world, time);
1119 }
1120 }
1121
1122 std::vector<PreviewLink> calculatePreviewLinks(const core::World& world, const RealType time)
1123 {
1124 std::vector<PreviewLink> links;
1125
1126 for (const auto& tx : world.getTransmitters())
1127 {
1128 if (!isComponentActive(tx->getSchedule(), time))
1129 {
1130 continue;
1131 }
1132
1133 const auto tx_ctx = makePreviewTransmitterContext(*tx, time);
1134 addIlluminatorLinks(links, tx_ctx, world, time);
1135
1136 for (const auto& rx : world.getReceivers())
1137 {
1138 if (!isComponentActive(rx->getSchedule(), time))
1139 {
1140 continue;
1141 }
1142 const auto rx_ctx = makePreviewReceiverContext(*rx, time);
1143 addReceiverLinks(links, tx_ctx, rx_ctx, world, time);
1144 }
1145 }
1146 return links;
1147 }
1148}
const Transmitter & transmitter
const Receiver & receiver
Vec3 position
RealType lambda
RealType radiated_power
bool no_loss
Header for radar channel propagation and interaction models.
The World class manages the simulator environment.
Definition world.h:39
const std::vector< std::unique_ptr< radar::Transmitter > > & getTransmitters() const noexcept
Retrieves the list of radar transmitters.
Definition world.h:246
const std::vector< std::unique_ptr< radar::Receiver > > & getReceivers() const noexcept
Retrieves the list of radar receivers.
Definition world.h:236
Class representing a radar signal with associated properties.
bool isFmcwTriangle() const noexcept
Returns true when this signal is an FMCW triangular modulation signal.
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.
A class representing a vector in spherical coordinates.
A class representing a vector in rectangular coordinates.
RealType length() const noexcept
Calculates the length (magnitude) of the vector.
math::Vec3 getPosition(const RealType time) const
Retrieves the position of the object.
Definition object.h:50
Represents a radar system on a platform.
Definition radar_obj.h:50
Manages radar signal reception and response processing.
Definition receiver.h:47
bool checkFlag(RecvFlag flag) const noexcept
Checks if a specific flag is set.
Definition receiver.h:137
Base class for radar targets.
Definition target.h:118
Represents a radar transmitter system.
Definition transmitter.h:34
fers_signal::RadarSignal * getSignal() const noexcept
Retrieves the radar signal currently being transmitted.
Definition transmitter.h:72
Exception thrown when a range calculation fails, typically due to objects being too close.
Represents a timing source for simulation.
Definition timing.h:36
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
constexpr RealType PI
Mathematical constant π (pi).
Definition config.h:43
Classes and operations for 3D geometry.
Defines a structure to store interpolation point data for signal processing.
Header file for the logging system.
#define LOG(level,...)
Definition logging.h:19
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.
RealType simSamplingRate() noexcept
Get the simulation sampling rate.
Definition parameters.h:115
RealType rate() noexcept
Get the rendering sample rate.
Definition parameters.h:121
RealType startTime() noexcept
Get the start time for the simulation.
Definition parameters.h:103
RealType boltzmannK() noexcept
Get the Boltzmann constant.
Definition parameters.h:97
unsigned oversampleRatio() noexcept
Get the oversampling ratio.
Definition parameters.h:151
RealType c() noexcept
Get the speed of light.
Definition parameters.h:91
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.
@ DirectTxRx
Interference path.
@ Monostatic
Combined Tx/Rx path.
@ BistaticTgtRx
Scattered path.
@ BistaticTxTgt
Illuminator path.
ComplexType calculateReflectedPathContribution(const Transmitter *trans, const Receiver *recv, const Target *targ, const RealType timeK, const CwPhaseNoiseLookup *const phase_noise_lookup)
Calculates the complex envelope contribution for a reflected path (Tx -> Tgt -> Rx) at a specific tim...
@ Weak
SNR < 0 dB (Geometric line of sight, but below noise floor)
void solveReDirect(const Transmitter *trans, const Receiver *recv, const std::chrono::duration< RealType > &time, const RadarSignal *wave, ReResults &results)
Solves the radar equation for a direct path (Tx -> Rx).
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.
StreamingTimingPhaseMode
Selects how timing phase noise is applied to streaming channel contributions.
@ TransmitterOnly
Incoming RF/baseband signal before receiver LO subtraction.
@ None
Ignore timing phase noise entirely.
void solveRe(const Transmitter *trans, const Receiver *recv, const Target *targ, const std::chrono::duration< RealType > &time, const RadarSignal *wave, ReResults &results)
Solves the bistatic radar equation for a reflected path (Tx -> Tgt -> Rx).
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.
ComplexType calculateDirectPathContribution(const Transmitter *trans, const Receiver *recv, const RealType timeK, const CwPhaseNoiseLookup *const phase_noise_lookup)
Calculates the complex envelope contribution for a direct propagation path (Tx -> Rx) at a specific t...
std::vector< PreviewLink > calculatePreviewLinks(const core::World &world, const RealType time)
Calculates all visual links for the current world state at a specific time.
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.
Defines the Radar class and associated functionality.
Classes for handling radar waveforms and signals.
Radar Receiver class for managing signal reception and response handling.
Classes for managing radar signal responses.
math::Vec3 max
Cached description of an active streaming transmitter segment.
RealType s_pi_alpha
Cached signed pi-scaled FMCW chirp-rate factor.
RealType triangle_period
Cached full triangle period in seconds.
RealType amplitude
Cached emitted signal amplitude.
RealType carrier_freq
Cached carrier frequency in hertz.
RealType two_pi_f0
Cached two-pi carrier angular frequency factor.
RealType neg_pi_alpha
Triangle down-leg quadratic coefficient.
RealType two_pi_f0_plus_B
Triangle down-leg linear coefficient.
RealType mod_phi_tri
Triangle period phase increment modulo 2*pi.
RealType mod_phi_up
Triangle leg phase increment modulo 2*pi.
RealType segment_start
Segment start time in seconds.
RealType pi_alpha
Triangle up-leg quadratic coefficient.
const radar::Transmitter * transmitter
Transmitter active during this segment.
RealType chirp_duration
Cached FMCW chirp duration in seconds.
RealType chirp_period
Cached FMCW chirp period in seconds.
StreamingWaveformKind kind
Cached streaming waveform shape.
std::optional< std::size_t > chirp_count
Optional finite chirp count for the segment.
std::optional< std::size_t > triangle_count
Optional finite triangle count for the segment.
RealType segment_end
Segment end time in seconds.
Tracks the current FMCW chirp boundary for a streaming path.
Stores data for an interpolation point.
Sampled phase-noise buffer for one timing source.
RealType sampleAt(RealType time) const noexcept
Returns the interpolated phase-noise sample at the specified time.
Lookup table for CW phase noise across timing sources.
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.
RealType end_time
Lookup end time in seconds.
RealType start_time
Lookup start time in seconds.
RealType sample(const timing::Timing *timing, RealType time) const noexcept
Samples phase noise for one timing source at the specified time.
RealType phaseDifference(const timing::Timing *rx_timing, RealType rx_time, const timing::Timing *tx_timing, RealType tx_time) const noexcept
Computes receiver-minus-transmitter phase noise at two propagation times.
Stores the intermediate results of a radar equation calculation for a single time point.
Defines classes for radar targets and their Radar Cross-Section (RCS) models.
Timing source for simulation objects.
Header file for the Transmitter class in the radar namespace.
Header file for the World class in the simulator.