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 struct StreamingWaveformEvaluation
112 {
113 RealType phase = 0.0;
114 RealType rf_frequency = 0.0;
115 };
116
117 /**
118 * @brief Computes the geometry (distance and direction) between two points.
119 * @param p_from Starting position.
120 * @param p_to Ending position.
121 * @return LinkGeometry containing distance and unit vector.
122 * @throws RangeError if the distance is too small (<= EPSILON).
123 */
124 LinkGeometry computeLink(const Vec3& p_from, const Vec3& p_to)
125 {
126 const Vec3 vec = p_to - p_from;
127 const RealType dist = vec.length();
128
129 if (dist <= EPSILON)
130 {
131 // LOG(Level::FATAL) is handled by the caller or generic exception handler if needed,
132 // but for RangeError strictly we just throw here to keep it pure.
133 // However, existing code logged FATAL before throwing.
134 // We'll throw RangeError, and let callers decide if they want to log or return 0.
136 }
137
138 return {vec / dist, dist};
139 }
140
141 /**
142 * @brief Calculates the antenna gain for a specific direction and time.
143 * @param radar The radar object (Transmitter or Receiver).
144 * @param direction_vec The unit vector pointing AWAY from the antenna towards the target/receiver.
145 * @param time The simulation time for rotation lookup.
146 * @param lambda The signal wavelength.
147 * @return The linear gain value.
148 */
150 {
151 return radar->getGain(SVec3(direction_vec), radar->getRotation(time), lambda);
152 }
153
154 /**
155 * @brief Computes the power scaling factor for a direct path (Friis Transmission Equation).
156 * @param tx_gain Transmitter gain (linear).
157 * @param rx_gain Receiver gain (linear).
158 * @param lambda Wavelength (meters).
159 * @param dist Distance (meters).
160 * @param no_prop_loss If true, distance-based attenuation is ignored.
161 * @return The power scaling factor (Pr / Pt).
162 */
164 bool no_prop_loss)
165 {
166 const RealType numerator = tx_gain * rx_gain * lambda * lambda;
167 RealType denominator = 16.0 * PI * PI; // (4 * PI)^2
168
169 if (!no_prop_loss)
170 {
171 denominator *= dist * dist;
172 }
173
174 return numerator / denominator;
175 }
176
177 /**
178 * @brief Computes the power scaling factor for a reflected path (Bistatic Radar Range Equation).
179 * @param tx_gain Transmitter gain (linear).
180 * @param rx_gain Receiver gain (linear).
181 * @param rcs Target Radar Cross Section (m^2).
182 * @param lambda Wavelength (meters).
183 * @param r_tx Distance from Transmitter to Target.
184 * @param r_rx Distance from Target to Receiver.
185 * @param no_prop_loss If true, distance-based attenuation is ignored.
186 * @return The power scaling factor (Pr / Pt).
187 */
190 {
191 const RealType numerator = tx_gain * rx_gain * rcs * lambda * lambda;
192 RealType denominator = 64.0 * PI * PI * PI; // (4 * PI)^3
193
194 if (!no_prop_loss)
195 {
196 denominator *= r_tx * r_tx * r_rx * r_rx;
197 }
198
199 return numerator / denominator;
200 }
201
202 /**
203 * @brief Computes the non-coherent phase shift due to timing offsets.
204 *
205 * Used for CW simulation where LO effects are modeled analytically.
206 *
207 * @param tx The transmitter.
208 * @param rx The receiver.
209 * @param time The current simulation time.
210 * @return The phase shift in radians.
211 */
213 {
214 const auto tx_timing = tx->getTiming();
215 const auto rx_timing = rx->getTiming();
216 const RealType delta_f = tx_timing->getFreqOffset() - rx_timing->getFreqOffset();
217 const RealType delta_phi = tx_timing->getPhaseOffset() - rx_timing->getPhaseOffset();
218 return 2 * PI * delta_f * time + delta_phi;
219 }
220
221 /// Computes deterministic timing phase for one timing source when no lookup is available.
223 {
224 if (timing == nullptr)
225 {
226 return 0.0;
227 }
228 return 2.0 * PI * timing->getFreqOffset() * time + timing->getPhaseOffset();
229 }
230
231 /// Computes timing phase with an optional lookup and streaming phase-application mode.
233 const RealType tx_time, const simulation::CwPhaseNoiseLookup* phase_noise_lookup,
235 {
237 {
238 return 0.0;
239 }
241 {
242 if (phase_noise_lookup == nullptr)
243 {
244 return computeSingleTimingPhase(tx->getTiming().get(), tx_time);
245 }
246 return phase_noise_lookup->sample(tx->getTiming().get(), tx_time);
247 }
248 if (phase_noise_lookup == nullptr)
249 {
251 }
252 return phase_noise_lookup->phaseDifference(rx->getTiming().get(), rx_time, tx->getTiming().get(), tx_time);
253 }
254
255 /// Checks whether received power is above the thermal noise floor.
257 {
258 // Use configured rate or default to 1Hz if unconfigured to prevent divide-by-zero or silly values
259 const RealType bw = params::rate() > 0 ? params::rate() : 1.0;
260 const RealType noise_floor = params::boltzmannK() * (temp_kelvin > 0 ? temp_kelvin : 290.0) * bw;
261 return power_watts > noise_floor;
262 }
263
264 /**
265 * @brief Converts power in watts to decibels milliwatts (dBm).
266 *
267 * @param watts Power in watts.
268 * @return Power in dBm. Returns -999.0 dBm for non-positive input.
269 */
271 {
272 if (watts <= 0)
273 {
274 return -999.0;
275 }
276 return 10.0 * std::log10(watts * 1000.0);
277 }
278
279 /**
280 * @brief Converts power in watts to decibels (dB).
281 *
282 * @param watts Power in watts.
283 * @return Power in decibels (dB). Returns -999.0 dB for non-positive input.
284 */
286 {
287 if (watts <= 0)
288 {
289 return -999.0;
290 }
291 return 10.0 * std::log10(watts);
292 }
293
294 /**
295 * @brief Checks if a component is active at the given time based on its schedule.
296 *
297 * @param schedule The component's operating schedule.
298 * @param time The current simulation time.
299 * @return true If the schedule is empty (implied always on) or if time is within a period.
300 * @return false If the schedule is populated but the time is outside all periods.
301 */
302 bool isComponentActive(const std::vector<radar::SchedulePeriod>& schedule, RealType time)
303 {
304 if (schedule.empty())
305 {
306 return true;
307 }
308 for (const auto& period : schedule)
309 {
310 if (time >= period.start && time <= period.end)
311 {
312 return true;
313 }
314 }
315 return false;
316 }
317
318 /// Builds a compatibility streaming-source cache for classic CW paths.
320 {
321 auto source = core::makeActiveSource(trans, params::startTime(), std::numeric_limits<RealType>::max());
323 {
324 source.segment_start = std::numeric_limits<RealType>::lowest();
325 }
326 return source;
327 }
328
331 {
333 const auto chirp_index = static_cast<std::size_t>(std::floor(time_since_segment_start / source.chirp_period));
334 if (source.chirp_count.has_value() && chirp_index >= *source.chirp_count)
335 {
336 return false;
337 }
340 {
341 return false;
342 }
343 phase_out = -2.0 * PI * source.carrier_freq * tau + source.two_pi_f0 * chirp_time +
345 return true;
346 }
347
351 {
352 if (!chirp_tracker.initialized)
353 {
355 }
356
357 while (t_ret >= chirp_tracker.t_n + source.chirp_period)
358 {
359 chirp_tracker.t_n += source.chirp_period;
360 ++chirp_tracker.n_current;
361 }
362
363 if (source.chirp_count.has_value() && chirp_tracker.n_current >= *source.chirp_count)
364 {
365 return false;
366 }
367
368 const RealType u_ret = t_ret - chirp_tracker.t_n;
370 {
371 return false;
372 }
373
374 phase_out =
375 -2.0 * PI * source.carrier_freq * tau + source.two_pi_f0 * u_ret + source.s_pi_alpha * u_ret * u_ret;
376 return true;
377 }
378
381 {
382 const RealType delta = t_ret - source.segment_start;
383 const auto triangle_index = static_cast<std::size_t>(std::floor(delta / source.triangle_period));
384 if (source.triangle_count.has_value() && triangle_index >= *source.triangle_count)
385 {
386 return false;
387 }
388 const RealType local_triangle_time = delta - static_cast<RealType>(triangle_index) * source.triangle_period;
389 const bool down_leg = local_triangle_time >= source.chirp_duration;
392 {
393 return false;
394 }
395 const RealType phi_base = std::fmod(static_cast<RealType>(triangle_index) * source.mod_phi_tri, 2.0 * PI) +
396 (down_leg ? source.mod_phi_up : 0.0);
397 const RealType modular_phi_base = phi_base >= 2.0 * PI ? phi_base - 2.0 * PI : phi_base;
398 const RealType linear_coeff = down_leg ? source.two_pi_f0_plus_B : source.two_pi_f0;
399 const RealType quad_coeff = down_leg ? source.neg_pi_alpha : source.pi_alpha;
402 return true;
403 }
404
407 {
408 while (t_ret >= chirp_tracker.triangle_t_leg + source.chirp_duration)
409 {
410 chirp_tracker.triangle_t_leg += source.chirp_duration;
411 chirp_tracker.triangle_leg = 1U - chirp_tracker.triangle_leg;
412 if (chirp_tracker.triangle_leg == 0U)
413 {
414 ++chirp_tracker.triangle_index;
415 }
416 chirp_tracker.triangle_phi_base += source.mod_phi_up;
417 if (chirp_tracker.triangle_phi_base >= 2.0 * PI)
418 {
419 chirp_tracker.triangle_phi_base -= 2.0 * PI;
420 }
421 }
422 }
423
427 {
428 if (!chirp_tracker.triangle_initialized)
429 {
431 }
432
434 if (source.triangle_count.has_value() && chirp_tracker.triangle_index >= *source.triangle_count)
435 {
436 return false;
437 }
438
439 const RealType u_ret = t_ret - chirp_tracker.triangle_t_leg;
441 {
442 return false;
443 }
444
445 const bool down_leg = chirp_tracker.triangle_leg == 1U;
446 const RealType linear_coeff = down_leg ? source.two_pi_f0_plus_B : source.two_pi_f0;
447 const RealType quad_coeff = down_leg ? source.neg_pi_alpha : source.pi_alpha;
448 phase_out = -2.0 * PI * source.carrier_freq * tau + chirp_tracker.triangle_phi_base + linear_coeff * u_ret +
450 return true;
451 }
452
453 /// Computes streaming waveform phase and active RF at a receiver time.
456 StreamingWaveformEvaluation& eval)
457 {
458 if (source.carrier_freq <= 0.0)
459 {
460 return false;
461 }
462
463 const RealType t_ret = rx_time - tau;
465 {
466 return false;
467 }
468
470 {
471 eval.rf_frequency = source.carrier_freq;
472 if (chirp_tracker == nullptr)
473 {
474 return computeLinearFmcwPhaseWithoutTracker(source, t_ret, tau, eval.phase);
475 }
477 }
478
480 {
481 eval.rf_frequency = source.carrier_freq;
482 if (chirp_tracker == nullptr)
483 {
485 }
487 }
488
490 {
491 if (source.sfcw == nullptr)
492 {
493 return false;
494 }
495 const auto step = source.sfcw->activeStepAt(t_ret - source.segment_start, source.carrier_freq);
496 if (!step.has_value() || step->rf_frequency <= 0.0)
497 {
498 return false;
499 }
500 eval.rf_frequency = step->rf_frequency;
501 eval.phase = -2.0 * PI * step->rf_frequency * tau;
502 return true;
503 }
504
505 eval.rf_frequency = source.carrier_freq;
506 eval.phase = -2.0 * PI * source.carrier_freq * tau;
507 return true;
508 }
509
510 /// Returns average radiated power for preview visualization.
512 {
513 if (waveform == nullptr)
514 {
515 return 0.0;
516 }
517 if (const auto* fmcw = waveform->getFmcwChirpSignal(); fmcw != nullptr)
518 {
519 return waveform->getPower() * (fmcw->getChirpDuration() / fmcw->getChirpPeriod());
520 }
521 if (waveform->isFmcwTriangle())
522 {
523 return waveform->getPower();
524 }
525 return waveform->getPower();
526 }
527
528 /// Formats received or radiated power as a dBm preview label.
529 std::string formatPreviewDbmLabel(const RealType watts, const std::string_view prefix = {})
530 {
531 return std::format("{}{:.1f} dBm", prefix, wattsToDbm(watts));
532 }
533
534 /// Formats target illumination density as a dBW-per-square-meter preview label.
536 {
537 return std::format("{:.1f} dBW/m\u00B2", wattsToDb(watts));
538 }
539}
540
541namespace simulation
542{
544 {
545 if (samples.empty())
546 {
547 return 0.0;
548 }
549 if ((time <= start_time) || (samples.size() == 1) || (dt <= 0.0))
550 {
551 return samples.front();
552 }
553
554 const RealType position = (time - start_time) / dt;
555 const auto last_index = static_cast<RealType>(samples.size() - 1);
556 if (position >= last_index)
557 {
558 return samples.back();
559 }
560
561 const auto lower_index = static_cast<std::size_t>(position);
562 const RealType fraction = position - static_cast<RealType>(lower_index);
563 return samples[lower_index] + fraction * (samples[lower_index + 1] - samples[lower_index]);
564 }
565
566 CwPhaseNoiseLookup CwPhaseNoiseLookup::build(const std::span<const std::shared_ptr<timing::Timing>> timings,
567 const RealType start_time, const RealType end_time)
568 {
570 lookup.start_time = start_time;
571 lookup.end_time = std::max(start_time, end_time);
572
573 const RealType sample_rate = params::rate() * params::oversampleRatio();
574 lookup.dt = sample_rate > 0.0 ? (1.0 / sample_rate) : 1.0;
575
576 const auto sample_count =
577 static_cast<std::size_t>(std::ceil((lookup.end_time - lookup.start_time) / lookup.dt) + 1.0);
578 constexpr std::size_t phase_noise_warning_threshold_bytes = 500ULL * 1024ULL * 1024ULL;
579 const auto bytes_per_buffer = sample_count * sizeof(RealType);
580
581 for (const auto& timing : timings)
582 {
583 if (!timing || lookup.buffers.contains(timing->getId()))
584 {
585 continue;
586 }
587
589 buffer.start_time = lookup.start_time;
590 buffer.dt = lookup.dt;
591 if (timing->isEnabled())
592 {
593 auto timing_clone = timing->clone();
594 if (lookup.start_time > 0.0)
595 {
596 const auto skip_count = static_cast<std::size_t>(std::llround(lookup.start_time / lookup.dt));
597 timing_clone->skipSamples(skip_count);
598 }
599 // TODO: Replace whole-simulation CW lookup generation with chunked/streaming generation.
601 {
602 LOG(Level::WARNING,
603 "CW phase-noise lookup for timing '{}' allocates {} bytes; large scenarios need chunked "
604 "streaming.",
605 timing->getName(), bytes_per_buffer);
606 }
607 buffer.samples.resize(sample_count);
608 std::ranges::generate(buffer.samples, [&] { return timing_clone->getNextSample(); });
609 }
610
611 lookup.buffers.emplace(timing->getId(), std::move(buffer));
612 }
613
614 return lookup;
615 }
616
617 RealType CwPhaseNoiseLookup::sample(const timing::Timing* const timing, const RealType time) const noexcept
618 {
619 if (timing == nullptr)
620 {
621 return 0.0;
622 }
623 const auto it = buffers.find(timing->getId());
624 if (it == buffers.end())
625 {
626 return 0.0;
627 }
628 return it->second.sampleAt(time);
629 }
630
632 const timing::Timing* const tx_timing,
633 const RealType tx_time) const noexcept
634 {
635 return sample(tx_timing, tx_time) - sample(rx_timing, rx_time);
636 }
637
638 void solveRe(const Transmitter* trans, const Receiver* recv, const Target* targ,
639 const std::chrono::duration<RealType>& time, const RadarSignal* wave, ReResults& results)
640 {
641 // Note: RangeError log messages are handled by the original catch block in calculateResponse
642 // or explicitly here if strict adherence to original logging is required.
643 // Using the helper logic which throws RangeError on epsilon check.
644
645 const RealType t_val = time.count();
646 const auto p_tx = trans->getPosition(t_val);
647 const auto p_rx = recv->getPosition(t_val);
648 const auto p_tgt = targ->getPosition(t_val);
649
650 // Link 1: Tx -> Target
651 LinkGeometry link_tx_tgt;
652 // Link 2: Target -> Rx (Note: Vector for calculation is Tgt->Rx)
653 LinkGeometry link_tgt_rx;
654
655 try
656 {
658 link_tgt_rx = computeLink(p_tgt, p_rx); // Vector Tgt -> Rx
659 }
660 catch (const RangeError&)
661 {
662 LOG(Level::INFO, "Transmitter or Receiver too close to Target for accurate simulation");
663 throw;
664 }
665
666 results.delay = (link_tx_tgt.dist + link_tgt_rx.dist) / params::c();
667
668 // Calculate RCS
669 // Note: getRcs expects (InAngle, OutAngle).
670 // InAngle: Tx -> Tgt (link_tx_tgt.u_vec)
671 // OutAngle: Rx -> Tgt (Opposite of Tgt->Rx, so -link_tgt_rx.u_vec)
672 // This matches existing logic.
675 const auto rcs = targ->getRcs(in_angle, out_angle, t_val);
676
677 const auto wavelength = params::c() / wave->getCarrier();
678
679 // Tx Gain: Direction Tx -> Tgt
681 // Rx Gain: Direction Rx -> Tgt (Opposite of Tgt->Rx).
682 // Time is time + delay.
683 const auto rx_gain = computeAntennaGain(recv, -link_tgt_rx.u_vec, results.delay + t_val, wavelength);
684
685 const bool no_loss = recv->checkFlag(Receiver::RecvFlag::FLAG_NOPROPLOSS);
686 results.power =
688
689 results.phase = -results.delay * 2 * PI * wave->getCarrier();
690 }
691
692 void solveReDirect(const Transmitter* trans, const Receiver* recv, const std::chrono::duration<RealType>& time,
694 {
695 const RealType t_val = time.count();
696 const auto p_tx = trans->getPosition(t_val);
697 const auto p_rx = recv->getPosition(t_val);
698
699 LinkGeometry link;
700 try
701 {
702 link = computeLink(p_tx, p_rx); // Vector Tx -> Rx
703 }
704 catch (const RangeError&)
705 {
706 LOG(Level::INFO, "Transmitter or Receiver too close for accurate simulation");
707 throw;
708 }
709
710 results.delay = link.dist / params::c();
711 const RealType wavelength = params::c() / wave->getCarrier();
712
713 // Discrepancy Fix: Original code used (Rx - Tx) for Receiver Gain but (Tx - Rx) logic for Transmitter gain
714 // was ambiguous/incorrect (using `tpos - rpos` which is Rx->Tx).
715 // Per `calculateDirectPathContribution` preference:
716 // Tx Gain uses Vector Tx -> Rx.
717 // Rx Gain uses Vector Rx -> Tx.
718
719 const auto tx_gain = computeAntennaGain(trans, link.u_vec, t_val, wavelength);
720 const auto rx_gain = computeAntennaGain(recv, -link.u_vec, t_val + results.delay, wavelength);
721
722 const bool no_loss = recv->checkFlag(Receiver::RecvFlag::FLAG_NOPROPLOSS);
724
725 results.phase = -results.delay * 2 * PI * wave->getCarrier();
726 }
727
729 const CwPhaseNoiseLookup* const phase_noise_lookup)
730 {
732 phase_noise_lookup);
733 }
734
736 const Receiver* recv, const RealType timeK,
737 const CwPhaseNoiseLookup* const phase_noise_lookup,
740 {
741 const auto* const trans = source.transmitter;
742 if (trans == nullptr)
743 {
744 return {0.0, 0.0};
745 }
746 // Check for co-location to prevent singularities.
747 // If they share the same platform, we assume they are isolated (no leakage) or explicit
748 // monostatic handling is required (which is not modeled via the far-field path).
749 if (trans->getPlatform() == recv->getPlatform())
750 {
751 return {0.0, 0.0};
752 }
753
754 const auto p_tx = trans->getPlatform()->getPosition(timeK);
755 const auto p_rx = recv->getPlatform()->getPosition(timeK);
756
757 LinkGeometry link;
758 try
759 {
761 }
762 catch (const RangeError&)
763 {
764 return {0.0, 0.0};
765 }
766
767 const RealType tau = link.dist / params::c();
768 StreamingWaveformEvaluation eval;
770 {
771 return {0.0, 0.0};
772 }
773 const RealType lambda = params::c() / eval.rf_frequency;
774
775 // Tx Gain: Direction Tx -> Rx
777 // Rx Gain: Direction Rx -> Tx (-u_vec)
779
780 const bool no_loss = recv->checkFlag(Receiver::RecvFlag::FLAG_NOPROPLOSS);
782
783 // Include Signal Power
784 const RealType amplitude = source.amplitude * std::sqrt(scaling_factor);
785
786 // Carrier Phase
787 ComplexType contribution = std::polar(amplitude, eval.phase);
788
789 // Non-coherent Local Oscillator Effects
791 computeTimingPhase(trans, recv, timeK, timeK - tau, phase_noise_lookup, timing_phase_mode);
792 contribution *= std::polar(1.0, non_coherent_phase);
793
794 return contribution;
795 }
796
799 {
800 StreamingWaveformEvaluation eval;
802 {
803 return false;
804 }
805 phase_out = eval.phase;
806 return true;
807 }
808
810 const RealType timeK,
811 const CwPhaseNoiseLookup* const phase_noise_lookup)
812 {
814 phase_noise_lookup);
815 }
816
818 const Receiver* recv, const Target* targ,
819 const RealType timeK,
820 const CwPhaseNoiseLookup* const phase_noise_lookup,
823 {
824 const auto* const trans = source.transmitter;
825 if (trans == nullptr)
826 {
827 return {0.0, 0.0};
828 }
829 // Check for co-location involving the target.
830 // We do not model a platform tracking itself (R=0) or illuminating itself (R=0).
831 if (trans->getPlatform() == targ->getPlatform() || recv->getPlatform() == targ->getPlatform())
832 {
833 return {0.0, 0.0};
834 }
835
836 const auto p_tx = trans->getPlatform()->getPosition(timeK);
837 const auto p_rx = recv->getPlatform()->getPosition(timeK);
838 const auto p_tgt = targ->getPlatform()->getPosition(timeK);
839
840 LinkGeometry link_tx_tgt;
841 LinkGeometry link_tgt_rx;
842
843 try
844 {
847 }
848 catch (const RangeError&)
849 {
850 return {0.0, 0.0};
851 }
852
853 const RealType tau = (link_tx_tgt.dist + link_tgt_rx.dist) / params::c();
854 StreamingWaveformEvaluation eval;
856 {
857 return {0.0, 0.0};
858 }
859 const RealType lambda = params::c() / eval.rf_frequency;
860
861 // RCS Lookups: In (Tx->Tgt), Out (Rx->Tgt = - (Tgt->Rx))
864 const RealType rcs = targ->getRcs(in_angle, out_angle, timeK);
865
866 // Tx Gain: Direction Tx -> Tgt
868 // Rx Gain: Direction Rx -> Tgt (- (Tgt->Rx)). Time: timeK + tau.
870
871 const bool no_loss = recv->checkFlag(Receiver::RecvFlag::FLAG_NOPROPLOSS);
874
875 // Include Signal Power
876 const RealType amplitude = source.amplitude * std::sqrt(scaling_factor);
877
878 ComplexType contribution = std::polar(amplitude, eval.phase);
879
880 // Non-coherent Local Oscillator Effects
882 computeTimingPhase(trans, recv, timeK, timeK - tau, phase_noise_lookup, timing_phase_mode);
883 contribution *= std::polar(1.0, non_coherent_phase);
884
885 return contribution;
886 }
887
888 std::unique_ptr<serial::Response> calculateResponse(const Transmitter* trans, const Receiver* recv,
889 const RadarSignal* signal, const RealType startTime,
890 const Target* targ)
891 {
892 // If calculating direct path (no target) and components are co-located:
893 // 1. If explicitly attached (monostatic), skip (internal leakage handled elsewhere).
894 // 2. If independent but on the same platform, distance is 0. Far-field logic (1/R^2)
895 // diverges. We skip calculation to avoid RangeError crashes, assuming
896 // no direct coupling/interference for co-located far-field antennas.
897 if (targ == nullptr && (trans->getAttached() == recv || trans->getPlatform() == recv->getPlatform()))
898 {
899 return nullptr;
900 }
901
902 // If calculating reflected path and target is co-located with either Tx or Rx:
903 // Skip to avoid singularity. Simulating a radar tracking its own platform
904 // requires near-field clutter models, not point-target RCS models.
905 if (targ != nullptr &&
906 (targ->getPlatform() == trans->getPlatform() || targ->getPlatform() == recv->getPlatform()))
907 {
908 LOG(Level::TRACE,
909 "Skipping reflected path calculation for Target {} co-located with Transmitter {} or Receiver {}",
910 targ->getName(), trans->getName(), recv->getName());
911 return nullptr;
912 }
913
914 const auto start_time_chrono = std::chrono::duration<RealType>(startTime);
915 const auto end_time_chrono = start_time_chrono + std::chrono::duration<RealType>(signal->getLength());
916 const auto sample_time_chrono = std::chrono::duration<RealType>(1.0 / params::simSamplingRate());
917 const int point_count = static_cast<int>(std::ceil(signal->getLength() / sample_time_chrono.count()));
918
919 if ((targ != nullptr) && point_count == 0)
920 {
921 LOG(Level::FATAL, "No time points are available for execution!");
922 throw std::runtime_error("No time points are available for execution!");
923 }
924
925 auto response = std::make_unique<serial::Response>(signal, trans);
926
927 try
928 {
929 for (int i = 0; i <= point_count; ++i)
930 {
931 const auto current_time =
933
935 if (targ != nullptr)
936 {
938 }
939 else
940 {
942 }
943
944 interp::InterpPoint const point{.power = results.power,
945 .time = current_time.count() + results.delay,
946 .delay = results.delay,
947 .phase = results.phase};
948 response->addInterpPoint(point);
949 }
950 }
951 catch (const RangeError&)
952 {
953 LOG(Level::INFO, "Receiver or Transmitter too close for accurate simulation");
954 throw; // Re-throw to be caught by the runner
955 }
956
957 return response;
958 }
959
960 namespace
961 {
962 struct PreviewTransmitterContext
963 {
968 };
969
970 struct PreviewReceiverContext
971 {
975 };
976
977 PreviewTransmitterContext makePreviewTransmitterContext(const Transmitter& transmitter, const RealType time)
978 {
979 const auto* waveform = transmitter.getSignal();
980 return PreviewTransmitterContext{.transmitter = transmitter,
981 .position = transmitter.getPosition(time),
982 .radiated_power = previewRadiatedPower(waveform),
983 .lambda =
984 (waveform != nullptr) ? (params::c() / waveform->getCarrier()) : 0.3};
985 }
986
987 PreviewReceiverContext makePreviewReceiverContext(const Receiver& receiver, const RealType time)
988 {
989 return PreviewReceiverContext{.receiver = receiver,
990 .position = receiver.getPosition(time),
991 .no_loss = receiver.checkFlag(Receiver::RecvFlag::FLAG_NOPROPLOSS)};
992 }
993
994 void addIlluminatorLinks(std::vector<PreviewLink>& links, const PreviewTransmitterContext& tx_ctx,
995 const core::World& world, const RealType time)
996 {
997 for (const auto& target : world.getTargets())
998 {
999 const auto target_position = target->getPosition(time);
1000 const Vec3 vec_tx_tgt = target_position - tx_ctx.position;
1001 const RealType range = vec_tx_tgt.length();
1002 if (range <= EPSILON)
1003 {
1004 continue;
1005 }
1006
1007 const Vec3 u_tx_tgt = vec_tx_tgt / range;
1008 const RealType gain = computeAntennaGain(&tx_ctx.transmitter, u_tx_tgt, time, tx_ctx.lambda);
1009 const RealType power_density = (tx_ctx.radiated_power * gain) / (4.0 * PI * range * range);
1010 links.push_back({.type = LinkType::BistaticTxTgt,
1011 .quality = LinkQuality::Strong,
1013 .display_value = wattsToDb(power_density),
1014 .source_id = tx_ctx.transmitter.getId(),
1015 .dest_id = target->getId(),
1016 .origin_id = tx_ctx.transmitter.getId()});
1017 }
1018 }
1019
1020 void addMonostaticLinks(std::vector<PreviewLink>& links, const PreviewTransmitterContext& tx_ctx,
1021 const PreviewReceiverContext& rx_ctx, const core::World& world, const RealType time)
1022 {
1023 for (const auto& target : world.getTargets())
1024 {
1025 const auto target_position = target->getPosition(time);
1026 const Vec3 vec_tx_tgt = target_position - tx_ctx.position;
1027 const RealType range = vec_tx_tgt.length();
1028 if (range <= EPSILON)
1029 {
1030 continue;
1031 }
1032
1033 const Vec3 u_tx_tgt = vec_tx_tgt / range;
1034 const RealType gt = computeAntennaGain(&tx_ctx.transmitter, u_tx_tgt, time, tx_ctx.lambda);
1035 const RealType gr = computeAntennaGain(&rx_ctx.receiver, u_tx_tgt, time, tx_ctx.lambda);
1038 const RealType rcs = target->getRcs(in_angle, out_angle, time);
1039 const RealType power_ratio =
1040 computeReflectedPathPower(gt, gr, rcs, tx_ctx.lambda, range, range, rx_ctx.no_loss);
1041 const RealType pr_watts = tx_ctx.radiated_power * power_ratio;
1042 const RealType pr_unit_watts = tx_ctx.radiated_power *
1043 computeReflectedPathPower(gt, gr, 1.0, tx_ctx.lambda, range, range, rx_ctx.no_loss);
1044
1045 links.push_back({.type = LinkType::Monostatic,
1046 .quality = isSignalStrong(pr_unit_watts, rx_ctx.receiver.getNoiseTemperature())
1050 .display_value = wattsToDbm(pr_unit_watts),
1051 .source_id = tx_ctx.transmitter.getId(),
1052 .dest_id = target->getId(),
1053 .origin_id = tx_ctx.transmitter.getId(),
1054 .rcs = rcs,
1055 .actual_power_dbm = wattsToDbm(pr_watts)});
1056 }
1057 }
1058
1059 void addDirectLink(std::vector<PreviewLink>& links, const PreviewTransmitterContext& tx_ctx,
1060 const PreviewReceiverContext& rx_ctx, const RealType time)
1061 {
1062 if (rx_ctx.receiver.checkFlag(Receiver::RecvFlag::FLAG_NODIRECT))
1063 {
1064 return;
1065 }
1066
1067 const Vec3 vec_direct = rx_ctx.position - tx_ctx.position;
1068 const RealType range = vec_direct.length();
1069 if (range <= EPSILON)
1070 {
1071 return;
1072 }
1073
1074 const Vec3 u_tx_rx = vec_direct / range;
1075 const RealType gt = computeAntennaGain(&tx_ctx.transmitter, u_tx_rx, time, tx_ctx.lambda);
1076 const RealType gr = computeAntennaGain(&rx_ctx.receiver, -u_tx_rx, time, tx_ctx.lambda);
1077 const RealType power_ratio = computeDirectPathPower(gt, gr, tx_ctx.lambda, range, rx_ctx.no_loss);
1078 const RealType pr_watts = tx_ctx.radiated_power * power_ratio;
1079
1080 links.push_back({.type = LinkType::DirectTxRx,
1081 .quality = LinkQuality::Strong,
1082 .label = formatPreviewDbmLabel(pr_watts, "Direct: "),
1083 .display_value = wattsToDbm(pr_watts),
1084 .source_id = tx_ctx.transmitter.getId(),
1085 .dest_id = rx_ctx.receiver.getId(),
1086 .origin_id = tx_ctx.transmitter.getId()});
1087 }
1088
1089 void addBistaticTargetReceiverLinks(std::vector<PreviewLink>& links, const PreviewTransmitterContext& tx_ctx,
1090 const PreviewReceiverContext& rx_ctx, const core::World& world,
1091 const RealType time)
1092 {
1093 for (const auto& target : world.getTargets())
1094 {
1095 const auto target_position = target->getPosition(time);
1096 const Vec3 vec_tx_tgt = target_position - tx_ctx.position;
1097 const Vec3 vec_tgt_rx = rx_ctx.position - target_position;
1098 const RealType r1 = vec_tx_tgt.length();
1099 const RealType r2 = vec_tgt_rx.length();
1100 if (r1 <= EPSILON || r2 <= EPSILON)
1101 {
1102 continue;
1103 }
1104
1105 const Vec3 u_tx_tgt = vec_tx_tgt / r1;
1106 const Vec3 u_tgt_rx = vec_tgt_rx / r2;
1107 const RealType gt = computeAntennaGain(&tx_ctx.transmitter, u_tx_tgt, time, tx_ctx.lambda);
1108 const RealType gr = computeAntennaGain(&rx_ctx.receiver, -u_tgt_rx, time, tx_ctx.lambda);
1111 const RealType rcs = target->getRcs(in_angle, out_angle, time);
1112 const RealType power_ratio =
1113 computeReflectedPathPower(gt, gr, rcs, tx_ctx.lambda, r1, r2, rx_ctx.no_loss);
1114 const RealType pr_watts = tx_ctx.radiated_power * power_ratio;
1115 const RealType pr_unit_watts = tx_ctx.radiated_power *
1116 computeReflectedPathPower(gt, gr, 1.0, tx_ctx.lambda, r1, r2, rx_ctx.no_loss);
1117
1118 links.push_back({.type = LinkType::BistaticTgtRx,
1119 .quality = isSignalStrong(pr_unit_watts, rx_ctx.receiver.getNoiseTemperature())
1123 .display_value = wattsToDbm(pr_unit_watts),
1124 .source_id = target->getId(),
1125 .dest_id = rx_ctx.receiver.getId(),
1126 .origin_id = tx_ctx.transmitter.getId(),
1127 .rcs = rcs,
1128 .actual_power_dbm = wattsToDbm(pr_watts)});
1129 }
1130 }
1131
1132 void addReceiverLinks(std::vector<PreviewLink>& links, const PreviewTransmitterContext& tx_ctx,
1133 const PreviewReceiverContext& rx_ctx, const core::World& world, const RealType time)
1134 {
1135 if (tx_ctx.transmitter.getAttached() == &rx_ctx.receiver)
1136 {
1137 addMonostaticLinks(links, tx_ctx, rx_ctx, world, time);
1138 return;
1139 }
1140
1141 addDirectLink(links, tx_ctx, rx_ctx, time);
1142 addBistaticTargetReceiverLinks(links, tx_ctx, rx_ctx, world, time);
1143 }
1144 }
1145
1146 std::vector<PreviewLink> calculatePreviewLinks(const core::World& world, const RealType time)
1147 {
1148 std::vector<PreviewLink> links;
1149
1150 for (const auto& tx : world.getTransmitters())
1151 {
1152 if (!isComponentActive(tx->getSchedule(), time))
1153 {
1154 continue;
1155 }
1156
1157 const auto tx_ctx = makePreviewTransmitterContext(*tx, time);
1158 addIlluminatorLinks(links, tx_ctx, world, time);
1159
1160 for (const auto& rx : world.getReceivers())
1161 {
1162 if (!isComponentActive(rx->getSchedule(), time))
1163 {
1164 continue;
1165 }
1166 const auto rx_ctx = makePreviewReceiverContext(*rx, time);
1167 addReceiverLinks(links, tx_ctx, rx_ctx, world, time);
1168 }
1169 }
1170 return links;
1171 }
1172}
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.
std::optional< StepState > activeStepAt(RealType time_since_segment_start, RealType carrier_frequency) const noexcept
Returns the active step for a time since the segment start.
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:51
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.
const fers_signal::SteppedFrequencySignal * sfcw
Stable pointer to the stepped-frequency waveform, if any.
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.