FERS 0.1.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
xml_parser_utils.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-only
2//
3// Copyright (c) 2026-present FERS Contributors (see AUTHORS.md).
4//
5// See the GNU GPLv2 LICENSE file in the FERS project root for more information.
6
7#include "xml_parser_utils.h"
8
9#include <GeographicLib/UTMUPS.hpp>
10#include <algorithm>
11#include <cmath>
12#include <exception>
13#include <filesystem>
14#include <format>
15#include <limits>
16#include <optional>
17#include <string_view>
18
20#include "core/config.h"
21#include "core/logging.h"
22#include "core/world.h"
23#include "fers_xml_dtd.h"
24#include "fers_xml_xsd.h"
25#include "math/coord.h"
26#include "math/geometry_ops.h"
27#include "math/path.h"
28#include "math/rotation_path.h"
29#include "radar/platform.h"
30#include "radar/radar_obj.h"
31#include "radar/receiver.h"
32#include "radar/target.h"
33#include "radar/transmitter.h"
38#include "signal/radar_signal.h"
40#include "timing/timing.h"
41
42namespace fs = std::filesystem;
43
45{
46 namespace
47 {
48 /// Parses the mutually exclusive radar operation mode elements.
50 {
51 std::optional<radar::OperationMode> selected_mode;
52 const auto select_mode = [&](const char* const element_name, const radar::OperationMode mode)
53 {
54 if (!parent.childElement(element_name, 0).isValid())
55 {
56 return;
57 }
58 if (selected_mode.has_value())
59 {
60 throw XmlException(owner +
61 " must specify exactly one radar mode (<pulsed_mode>, <cw_mode>, "
62 "<fmcw_mode>, or <sfcw_mode>).");
63 }
64 selected_mode = mode;
65 };
66
71 if (!selected_mode.has_value())
72 {
73 throw XmlException(owner +
74 " must specify exactly one radar mode (<pulsed_mode>, <cw_mode>, "
75 "<fmcw_mode>, or <sfcw_mode>).");
76 }
77 return *selected_mode;
78 }
79
80 void reject_non_empty_sfcw_mode(const XmlElement& parent, const std::string& owner)
81 {
82 const XmlElement sfcw_mode = parent.childElement("sfcw_mode", 0);
83 if (sfcw_mode.isValid() && sfcw_mode.childElement("", 0).isValid())
84 {
85 throw XmlException(owner + " sfcw_mode must be empty.");
86 }
87 }
88
89 /// Returns true when an FMCW mode block carries receiver-side FMCW fields.
91 {
92 return XmlElement::getOptionalAttribute(fmcw_mode, "dechirp_mode").has_value() ||
93 fmcw_mode.childElement("dechirp_reference", 0).isValid() ||
94 fmcw_mode.childElement("if_sample_rate", 0).isValid() ||
95 fmcw_mode.childElement("if_filter_bandwidth", 0).isValid() ||
96 fmcw_mode.childElement("if_filter_transition_width", 0).isValid();
97 }
98
99 /// Parses an optional positive scalar child under <fmcw_mode>.
100 std::optional<RealType> parse_optional_fmcw_if_child(const XmlElement& fmcw_mode, const std::string& child_name,
101 const std::string& owner)
102 {
103 const XmlElement element = fmcw_mode.childElement(child_name, 0);
104 if (!element.isValid())
105 {
106 return std::nullopt;
107 }
108 if (fmcw_mode.childElement(child_name, 1).isValid())
109 {
110 throw XmlException(owner + " must declare at most one <" + child_name + ">.");
111 }
112 const std::string text = element.getText();
113 if (text.empty())
114 {
115 throw XmlException("Element " + child_name + " is empty!");
116 }
117 const RealType value = std::stod(text);
118 if (value <= 0.0 || !std::isfinite(value))
119 {
120 throw XmlException(owner + " <" + child_name + "> must be a finite positive value.");
121 }
122 return value;
123 }
124
126 {
127 return if_chain.sample_rate_hz.has_value() || if_chain.filter_bandwidth_hz.has_value() ||
128 if_chain.filter_transition_width_hz.has_value();
129 }
130
133 const std::string& owner)
134 {
135 if (ref_element.isValid())
136 {
137 throw XmlException(owner + " declares <dechirp_reference> while dechirp_mode is 'none'.");
138 }
140 {
141 throw XmlException(owner + " declares IF-chain fields while dechirp_mode is 'none'.");
142 }
143 }
144
146 {
147 if ((if_chain.filter_bandwidth_hz.has_value() || if_chain.filter_transition_width_hz.has_value()) &&
148 !if_chain.sample_rate_hz.has_value())
149 {
150 throw XmlException(owner + " IF filter fields require <if_sample_rate>.");
151 }
152 if (if_chain.sample_rate_hz.has_value())
153 {
155 if (*if_chain.sample_rate_hz > sim_rate)
156 {
157 throw XmlException(owner + " <if_sample_rate> must not exceed the simulation sample rate.");
158 }
159 }
160 if (if_chain.sample_rate_hz.has_value() && if_chain.filter_bandwidth_hz.has_value() &&
161 *if_chain.filter_bandwidth_hz >= *if_chain.sample_rate_hz / 2.0)
162 {
163 throw XmlException(owner + " <if_filter_bandwidth> must be less than half <if_sample_rate>.");
164 }
165 }
166
168 parse_dechirp_reference(const XmlElement& fmcw_mode, const XmlElement& ref_element, const std::string& owner)
169 {
170 if (!ref_element.isValid())
171 {
172 throw XmlException(owner + " enables dechirping but does not declare <dechirp_reference>.");
173 }
174 if (fmcw_mode.childElement("dechirp_reference", 1).isValid())
175 {
176 throw XmlException(owner + " must declare at most one <dechirp_reference>.");
177 }
178
180 try
181 {
182 reference.source =
184 }
185 catch (const std::exception& e)
186 {
187 throw XmlException(owner + " has invalid dechirp_reference. " + e.what());
188 }
189
190 const auto transmitter_name = XmlElement::getOptionalAttribute(ref_element, "transmitter_name");
191 const auto waveform_name = XmlElement::getOptionalAttribute(ref_element, "waveform_name");
192 switch (reference.source)
193 {
195 if (transmitter_name.has_value() || waveform_name.has_value())
196 {
197 throw XmlException(owner +
198 " attached dechirp_reference must not set transmitter_name or "
199 "waveform_name.");
200 }
201 break;
203 if (!transmitter_name.has_value() || transmitter_name->empty() || waveform_name.has_value())
204 {
205 throw XmlException(owner + " transmitter dechirp_reference requires transmitter_name only.");
206 }
207 reference.name = *transmitter_name;
208 break;
210 if (!waveform_name.has_value() || waveform_name->empty() || transmitter_name.has_value())
211 {
212 throw XmlException(owner + " custom dechirp_reference requires waveform_name only.");
213 }
214 reference.name = *waveform_name;
215 break;
217 throw XmlException(owner + " dechirp_reference source must be attached, transmitter, or custom.");
218 }
219
220 return reference;
221 }
222
223 /// Parses receiver-side dechirp settings from an FMCW mode block.
225 const std::string& owner)
226 {
228 {
230 return;
231 }
232
233 const XmlElement fmcw_mode = parent.childElement("fmcw_mode", 0);
234 if (!fmcw_mode.isValid())
235 {
237 return;
238 }
239
241 if (const auto mode_attr = XmlElement::getOptionalAttribute(fmcw_mode, "dechirp_mode"))
242 {
243 try
244 {
246 }
247 catch (const std::exception& e)
248 {
249 throw XmlException(owner + " has invalid dechirp_mode. " + e.what());
250 }
251 }
252
253 const XmlElement ref_element = fmcw_mode.childElement("dechirp_reference", 0);
255 .sample_rate_hz = parse_optional_fmcw_if_child(fmcw_mode, "if_sample_rate", owner),
256 .filter_bandwidth_hz = parse_optional_fmcw_if_child(fmcw_mode, "if_filter_bandwidth", owner),
257 .filter_transition_width_hz =
258 parse_optional_fmcw_if_child(fmcw_mode, "if_filter_transition_width", owner)};
260 {
262 receiver.setDechirpMode(mode);
263 return;
264 }
265
268
269 receiver.setDechirpMode(mode);
270 receiver.setDechirpReference(std::move(reference));
271 receiver.setFmcwIfChainRequest(if_chain);
272 }
273
274 /// Throws an XML validation exception with the provided message.
275 void throw_xml_validation_error(const std::string& message) { throw XmlException(message); }
276
277 /// Validates an FMCW waveform while adapting validation errors to XmlException.
278 void validate_fmcw_waveform(const fers_signal::RadarSignal& wave, const std::string& owner)
279 {
281 }
282
283 /// Validates waveform/mode compatibility while adapting validation errors to XmlException.
285 const std::string& owner)
286 {
288 }
289
290 /// Validates an FMCW schedule while adapting validation errors to XmlException.
291 void validate_fmcw_schedule(const std::vector<radar::SchedulePeriod>& schedule,
292 const fers_signal::RadarSignal& wave, const std::string& owner)
293 {
295 }
296
297 /// Draws the next unsigned seed from the master random generator.
298 [[nodiscard]] unsigned next_seed(std::mt19937& master_seeder)
299 {
300 static_assert(std::mt19937::max() <= std::numeric_limits<unsigned>::max(),
301 "std::mt19937 output must fit into unsigned seeds.");
302 return static_cast<unsigned>(master_seeder());
303 }
304
305 /// Resolves or instantiates a shared timing instance by prototype SimId.
306 std::shared_ptr<timing::Timing> resolve_timing_instance(const SimId timing_id, ParserContext& ctx,
307 const std::string& owner)
308 {
309 if (const auto it = ctx.timing_instances.find(timing_id); it != ctx.timing_instances.end())
310 {
311 return it->second;
312 }
313
314 const timing::PrototypeTiming* proto = ctx.world->findTiming(timing_id);
315 if (proto == nullptr)
316 {
317 throw XmlException("Timing ID '" + std::to_string(timing_id) + "' not found for " + owner + "'");
318 }
319
320 auto timing_obj =
321 std::make_shared<timing::Timing>(proto->getName(), next_seed(*ctx.master_seeder), proto->getId());
322 timing_obj->initializeModel(proto);
323 ctx.timing_instances.emplace(timing_id, timing_obj);
324 return timing_obj;
325 }
326 }
327
329 {
330 const std::string text = element.childElement(elementName, 0).getText();
331 if (text.empty())
332 {
333 throw XmlException("Element " + elementName + " is empty!");
334 }
335 return std::stod(text);
336 }
337
338 bool get_attribute_bool(const XmlElement& element, const std::string& attributeName, const bool defaultVal)
339 {
341 if (!attr_value.has_value())
342 {
343 LOG(logging::Level::DEBUG, "Attribute '{}' not specified. Defaulting to {}.", attributeName, defaultVal);
344 return defaultVal;
345 }
346 if (*attr_value == "true")
347 {
348 return true;
349 }
350 if (*attr_value == "false")
351 {
352 return false;
353 }
354
355 LOG(logging::Level::WARNING, "Invalid boolean value '{}' for attribute '{}'. Defaulting to {}.", *attr_value,
357 return defaultVal;
358 }
359
361 {
362 const SimId id = SimIdGenerator::instance().generateId(type);
363 LOG(logging::Level::TRACE, "Assigned ID {} to {} (generated)", id, owner);
364 return id;
365 }
366
367 SimId resolve_reference_id(const XmlElement& element, const std::string& attributeName, const std::string& owner,
368 const std::unordered_map<std::string, SimId>& name_map)
369 {
370 const std::string value = XmlElement::getSafeAttribute(element, attributeName);
371 if (value.empty())
372 {
373 throw XmlException("Missing " + attributeName + " for " + owner + ".");
374 }
375 const auto it = name_map.find(value);
376 if (it != name_map.end())
377 {
378 return it->second;
379 }
380 throw XmlException("Unknown " + attributeName + " '" + value + "' for " + owner + ".");
381 }
382
383 std::vector<radar::SchedulePeriod> parseSchedule(const XmlElement& parent, const std::string& parentName,
384 const bool isPulsed, const RealType pri)
385 {
386 std::vector<radar::SchedulePeriod> raw_periods;
387 if (const XmlElement schedule_element = parent.childElement("schedule", 0); schedule_element.isValid())
388 {
389 unsigned p_idx = 0;
390 while (true)
391 {
392 XmlElement const period_element = schedule_element.childElement("period", p_idx++);
393 if (!period_element.isValid())
394 {
395 break;
396 }
397 try
398 {
399 const RealType start = std::stod(XmlElement::getSafeAttribute(period_element, "start"));
400 const RealType end = std::stod(XmlElement::getSafeAttribute(period_element, "end"));
401 raw_periods.push_back({start, end});
402 }
403 catch (const std::exception& e)
404 {
405 LOG(logging::Level::WARNING, "Failed to parse schedule period for '{}': {}", parentName, e.what());
406 }
407 }
408 }
410 }
411
412 unsigned parseUnsignedParameter(const std::string_view param_name, const RealType raw_value)
413 {
414 if (!std::isfinite(raw_value))
415 {
416 throw XmlException(std::format("Parameter '{}' must be finite.", param_name));
417 }
418 if (raw_value < 0.0)
419 {
420 throw XmlException(std::format("Parameter '{}' must be non-negative.", param_name));
421 }
422
423 const RealType floored_value = std::floor(raw_value);
424 if (floored_value > static_cast<RealType>(std::numeric_limits<unsigned>::max()))
425 {
426 throw XmlException(std::format("Parameter '{}' exceeds the supported unsigned range.", param_name));
427 }
428
429 return static_cast<unsigned>(floored_value);
430 }
431
432 template <typename Setter>
433 void setOptionalRealParameter(const XmlElement& parameters, const std::string& param_name,
435 {
436 if (!parameters.childElement(param_name, 0).isValid())
437 {
438 LOG(logging::Level::DEBUG, "Parameter '{}' not specified. Using default value {}.", param_name,
440 return;
441 }
442
444 }
445
446 template <typename Setter>
447 void setOptionalUnsignedParameter(const XmlElement& parameters, const std::string& param_name,
448 const unsigned default_value, Setter setter)
449 {
450 if (!parameters.childElement(param_name, 0).isValid())
451 {
452 LOG(logging::Level::DEBUG, "Parameter '{}' not specified. Using default value {}.", param_name,
454 return;
455 }
456
458 }
459
461 {
463 [&](const RealType value)
464 {
465 params_out.c = value;
466 LOG(logging::Level::INFO, "Propagation speed (c) set to: {:.5f}", value);
467 });
468
469 setOptionalRealParameter(parameters, "simSamplingRate", 1000.0,
470 [&](const RealType value)
471 {
472 params_out.sim_sampling_rate = value;
473 LOG(logging::Level::DEBUG, "Simulation sampling rate set to: {:.5f} Hz", value);
474 });
475
476 if (parameters.childElement("randomseed", 0).isValid())
477 {
478 const auto seed = parseUnsignedParameter("randomseed", get_child_real_type(parameters, "randomseed"));
479 params_out.random_seed = seed;
480 LOG(logging::Level::DEBUG, "Random seed set to: {}", seed);
481 }
482
483 setOptionalUnsignedParameter(parameters, "adc_bits", 0,
484 [&](const unsigned value)
485 {
486 params_out.adc_bits = value;
487 LOG(logging::Level::DEBUG, "ADC quantization bits set to: {}", value);
488 });
489
490 setOptionalUnsignedParameter(parameters, "oversample", 1,
491 [&](const unsigned value)
492 {
494 params_out.oversample_ratio = value;
495 LOG(logging::Level::DEBUG, "Oversampling enabled with ratio: {}", value);
496 });
497 }
498
500 {
501 try
502 {
503 const auto unit_token = parameters.childElement("rotationangleunit", 0).getText();
504 if (!unit_token.empty())
505 {
506 if (const auto unit = params::rotationAngleUnitFromToken(unit_token))
507 {
508 params_out.rotation_angle_unit = *unit;
509 }
510 else
511 {
512 throw XmlException("Unsupported rotation angle unit '" + unit_token + "'.");
513 }
514 }
515 }
516 catch (const XmlException&)
517 {
518 }
519 }
520
522 {
523 bool origin_set = false;
524 if (const XmlElement origin_element = parameters.childElement("origin", 0); origin_element.isValid())
525 {
526 try
527 {
528 params_out.origin_latitude = std::stod(XmlElement::getSafeAttribute(origin_element, "latitude"));
529 params_out.origin_longitude = std::stod(XmlElement::getSafeAttribute(origin_element, "longitude"));
530 if (const auto altitude = XmlElement::getOptionalAttribute(origin_element, "altitude"))
531 {
532 params_out.origin_altitude = std::stod(*altitude);
533 }
534 else
535 {
536 params_out.origin_altitude = 0.0;
537 LOG(logging::Level::DEBUG, "KML origin altitude not specified. Defaulting to 0.");
538 }
539 origin_set = true;
540 LOG(logging::Level::INFO, "KML origin set to lat: {}, lon: {}, alt: {}", params_out.origin_latitude,
541 params_out.origin_longitude, params_out.origin_altitude);
542 }
543 catch (const std::exception& e)
544 {
545 LOG(logging::Level::WARNING, "Could not parse KML origin from XML, using defaults. Error: {}",
546 e.what());
547 }
548 }
549 return origin_set;
550 }
551
553 {
554 params_out.coordinate_frame = params::CoordinateFrame::UTM;
555 params_out.utm_zone = std::stoi(XmlElement::getSafeAttribute(cs_element, "zone"));
556 const std::string hem_str = XmlElement::getSafeAttribute(cs_element, "hemisphere");
557
558 if (params_out.utm_zone < GeographicLib::UTMUPS::MINUTMZONE ||
559 params_out.utm_zone > GeographicLib::UTMUPS::MAXUTMZONE)
560 {
561 throw XmlException("KML UTM zone " + std::to_string(params_out.utm_zone) +
562 " is invalid; must be in [1, 60].");
563 }
564 if (hem_str == "N" || hem_str == "n")
565 {
566 params_out.utm_north_hemisphere = true;
567 }
568 else if (hem_str == "S" || hem_str == "s")
569 {
570 params_out.utm_north_hemisphere = false;
571 }
572 else
573 {
574 throw XmlException("KML UTM hemisphere '" + hem_str + "' is invalid; must be 'N' or 'S'.");
575 }
576 LOG(logging::Level::INFO, "KML coordinate system set to UTM, zone {}{}", params_out.utm_zone,
577 params_out.utm_north_hemisphere ? 'N' : 'S');
578 }
579
581 const bool origin_set)
582 {
583 if (const XmlElement cs_element = parameters.childElement("coordinatesystem", 0); cs_element.isValid())
584 {
585 try
586 {
587 const std::string frame_str = XmlElement::getSafeAttribute(cs_element, "frame");
588 if (frame_str == "UTM")
589 {
591 }
592 else if (frame_str == "ECEF")
593 {
594 params_out.coordinate_frame = params::CoordinateFrame::ECEF;
595 LOG(logging::Level::INFO, "KML coordinate system set to ECEF.");
596 }
597 else if (frame_str == "ENU")
598 {
599 params_out.coordinate_frame = params::CoordinateFrame::ENU;
600 if (!origin_set)
601 {
603 "ENU KML frame specified but no <origin> tag found. Using default KML origin at UCT.");
604 }
605 LOG(logging::Level::INFO, "KML coordinate system set to ENU local tangent plane.");
606 }
607 else
608 {
609 throw XmlException("Unsupported KML coordinate frame: " + frame_str);
610 }
611 }
612 catch (const std::exception& e)
613 {
615 "Could not parse KML <coordinatesystem> from XML: {}. Defaulting KML export to ENU.", e.what());
616 params_out.coordinate_frame = params::CoordinateFrame::ENU;
617 params_out.utm_zone = 0;
618 params_out.utm_north_hemisphere = true;
619 }
620 }
621 }
622
624 {
625 params_out.start = get_child_real_type(parameters, "starttime");
626 params_out.end = get_child_real_type(parameters, "endtime");
627 LOG(logging::Level::INFO, "Simulation time set from {:.5f} to {:.5f} seconds", params_out.start,
628 params_out.end);
629
630 params_out.rate = get_child_real_type(parameters, "rate");
631 if (params_out.rate <= 0)
632 {
633 throw std::runtime_error("Sampling rate must be > 0");
634 }
635 LOG(logging::Level::DEBUG, "Sample rate set to: {:.5f}", params_out.rate);
636
639 const bool origin_set = parseOriginParameter(parameters, params_out);
641 }
642
644 {
645 const std::string name = XmlElement::getSafeAttribute(waveform, "name");
646 const SimId id = assign_id_from_attribute("waveform '" + name + "'", ObjectType::Waveform);
647
648 const auto power = get_child_real_type(waveform, "power");
649 const auto carrier = get_child_real_type(waveform, "carrier_frequency");
650
651 if (const XmlElement pulsed_file = waveform.childElement("pulsed_from_file", 0); pulsed_file.isValid())
652 {
653 const std::string filename_str = XmlElement::getSafeAttribute(pulsed_file, "filename");
654 fs::path pulse_path(filename_str);
655
656 if (!fs::exists(pulse_path))
657 {
658 pulse_path = ctx.base_dir / filename_str;
659 }
660
661 // Defer to dependency-injected file loader
662 auto wave = ctx.loaders.loadWaveform(name, pulse_path, power, carrier, id);
663 ctx.world->add(std::move(wave));
664 }
665 else if (waveform.childElement("cw", 0).isValid())
666 {
667 auto cw_signal = std::make_unique<fers_signal::CwSignal>();
668 auto wave = std::make_unique<fers_signal::RadarSignal>(
669 name, power, carrier, ctx.parameters.end - ctx.parameters.start, std::move(cw_signal), id);
670 ctx.world->add(std::move(wave));
671 }
672 else if (const XmlElement sfcw_element = waveform.childElement("stepped_frequency", 0); sfcw_element.isValid())
673 {
674 const RealType start_frequency_offset = get_child_real_type(sfcw_element, "start_frequency_offset");
675 const RealType step_size = get_child_real_type(sfcw_element, "step_size");
677 if (raw_step_count <= 0.0 || std::floor(raw_step_count) != raw_step_count)
678 {
679 throw XmlException("Waveform '" + name + "' has an invalid step_count.");
680 }
681 const RealType dwell_time = get_child_real_type(sfcw_element, "dwell_time");
682 const RealType step_period = get_child_real_type(sfcw_element, "step_period");
683
684 std::optional<std::size_t> sweep_count;
685 if (const auto sweep_count_element = sfcw_element.childElement("sweep_count", 0);
686 sweep_count_element.isValid())
687 {
688 const RealType raw_count = get_child_real_type(sfcw_element, "sweep_count");
689 if (raw_count <= 0.0 || std::floor(raw_count) != raw_count)
690 {
691 throw XmlException("Waveform '" + name + "' has an invalid sweep_count.");
692 }
693 sweep_count = static_cast<std::size_t>(raw_count);
694 }
695
696 auto sfcw_signal = std::make_unique<fers_signal::SteppedFrequencySignal>(
697 start_frequency_offset, step_size, static_cast<std::size_t>(raw_step_count), dwell_time, step_period,
698 sweep_count);
699 auto wave = std::make_unique<fers_signal::RadarSignal>(name, power, carrier, sfcw_signal->getDwellTime(),
700 std::move(sfcw_signal), id);
701 validate_fmcw_waveform(*wave, "Waveform '" + name + "'");
702 ctx.world->add(std::move(wave));
703 }
704 else if (const XmlElement fmcw_element = waveform.childElement("fmcw_linear_chirp", 0); fmcw_element.isValid())
705 {
706 const auto direction =
708 const RealType chirp_bandwidth = get_child_real_type(fmcw_element, "chirp_bandwidth");
709 const RealType chirp_duration = get_child_real_type(fmcw_element, "chirp_duration");
710 const RealType chirp_period = get_child_real_type(fmcw_element, "chirp_period");
711
712 RealType start_frequency_offset = 0.0;
713 if (const auto start_offset = fmcw_element.childElement("start_frequency_offset", 0);
714 start_offset.isValid())
715 {
716 start_frequency_offset = get_child_real_type(fmcw_element, "start_frequency_offset");
717 }
718
719 std::optional<std::size_t> chirp_count;
720 if (const auto chirp_count_element = fmcw_element.childElement("chirp_count", 0);
721 chirp_count_element.isValid())
722 {
723 const RealType raw_count = get_child_real_type(fmcw_element, "chirp_count");
724 if (raw_count <= 0.0 || std::floor(raw_count) != raw_count)
725 {
726 throw XmlException("Waveform '" + name + "' has an invalid chirp_count.");
727 }
728 chirp_count = static_cast<std::size_t>(raw_count);
729 }
730
731 auto fmcw_signal = std::make_unique<fers_signal::FmcwChirpSignal>(
732 chirp_bandwidth, chirp_duration, chirp_period, start_frequency_offset, chirp_count, direction);
733 // RadarSignal length is the active chirp duration, not T_rep. The repeat period only spaces chirps.
734 auto wave = std::make_unique<fers_signal::RadarSignal>(name, power, carrier, chirp_duration,
735 std::move(fmcw_signal), id);
736 validate_fmcw_waveform(*wave, "Waveform '" + name + "'");
737 ctx.world->add(std::move(wave));
738 }
739 else if (const XmlElement fmcw_triangle_element = waveform.childElement("fmcw_triangle", 0);
740 fmcw_triangle_element.isValid())
741 {
742 const RealType chirp_bandwidth = get_child_real_type(fmcw_triangle_element, "chirp_bandwidth");
743 const RealType chirp_duration = get_child_real_type(fmcw_triangle_element, "chirp_duration");
744
745 RealType start_frequency_offset = 0.0;
746 if (const auto start_offset = fmcw_triangle_element.childElement("start_frequency_offset", 0);
747 start_offset.isValid())
748 {
749 start_frequency_offset = get_child_real_type(fmcw_triangle_element, "start_frequency_offset");
750 }
751
752 std::optional<std::size_t> triangle_count;
753 if (const auto triangle_count_element = fmcw_triangle_element.childElement("triangle_count", 0);
754 triangle_count_element.isValid())
755 {
757 if (raw_count <= 0.0 || std::floor(raw_count) != raw_count)
758 {
759 throw XmlException("Waveform '" + name + "' has an invalid triangle_count.");
760 }
761 triangle_count = static_cast<std::size_t>(raw_count);
762 }
763
764 auto fmcw_signal = std::make_unique<fers_signal::FmcwTriangleSignal>(
765 chirp_bandwidth, chirp_duration, start_frequency_offset, triangle_count);
766 auto wave = std::make_unique<fers_signal::RadarSignal>(
767 name, power, carrier, fmcw_signal->getTrianglePeriod(), std::move(fmcw_signal), id);
768 validate_fmcw_waveform(*wave, "Waveform '" + name + "'");
769 ctx.world->add(std::move(wave));
770 }
771 else
772 {
773 LOG(logging::Level::FATAL, "Unsupported waveform type for '{}'", name);
774 throw XmlException("Unsupported waveform type for '" + name + "'");
775 }
776 }
777
779 {
780 const std::string name = XmlElement::getSafeAttribute(timing, "name");
781 const SimId id = assign_id_from_attribute("timing '" + name + "'", ObjectType::Timing);
782 const RealType freq = get_child_real_type(timing, "frequency");
783 auto timing_obj = std::make_unique<timing::PrototypeTiming>(name, id);
784
785 timing_obj->setFrequency(freq);
786
787 unsigned noise_index = 0;
788 while (true)
789 {
790 XmlElement const noise_element = timing.childElement("noise_entry", noise_index++);
791 if (!noise_element.isValid())
792 {
793 break;
794 }
795
796 timing_obj->setAlpha(get_child_real_type(noise_element, "alpha"),
798 }
799
801 [&](const std::string& element_name, const std::string& description, auto setter)
802 {
803 if (!timing.childElement(element_name, 0).isValid())
804 {
805 LOG(logging::Level::DEBUG, "Clock section '{}' does not specify {}.", name, description);
806 return;
807 }
808 try
809 {
811 }
812 catch (const XmlException&)
813 {
814 LOG(logging::Level::WARNING, "Clock section '{}' has an empty {}. Using default.", name, description);
815 }
816 };
817
818 set_optional_timing_parameter("freq_offset", "frequency offset",
819 [&](const RealType value) { timing_obj->setFreqOffset(value); });
820 set_optional_timing_parameter("random_freq_offset_stdev", "random frequency offset",
821 [&](const RealType value) { timing_obj->setRandomFreqOffsetStdev(value); });
822 set_optional_timing_parameter("phase_offset", "phase offset",
823 [&](const RealType value) { timing_obj->setPhaseOffset(value); });
824 set_optional_timing_parameter("random_phase_offset_stdev", "random phase offset",
825 [&](const RealType value) { timing_obj->setRandomPhaseOffsetStdev(value); });
826
827 if (get_attribute_bool(timing, "synconpulse", false))
828 {
829 timing_obj->setSyncOnPulse();
830 }
831
832 ctx.world->add(std::move(timing_obj));
833 }
834
836 {
837 const std::string name = XmlElement::getSafeAttribute(antenna, "name");
838 const SimId id = assign_id_from_attribute("antenna '" + name + "'", ObjectType::Antenna);
839 const std::string pattern = XmlElement::getSafeAttribute(antenna, "pattern");
840
841 std::unique_ptr<antenna::Antenna> ant;
842
843 LOG(logging::Level::DEBUG, "Adding antenna '{}' with pattern '{}'", name, pattern);
844 if (pattern == "isotropic")
845 {
846 ant = std::make_unique<antenna::Isotropic>(name, id);
847 }
848 else if (pattern == "sinc")
849 {
850 ant = std::make_unique<antenna::Sinc>(name, get_child_real_type(antenna, "alpha"),
852 get_child_real_type(antenna, "gamma"), id);
853 }
854 else if (pattern == "gaussian")
855 {
856 ant = std::make_unique<antenna::Gaussian>(name, get_child_real_type(antenna, "azscale"),
857 get_child_real_type(antenna, "elscale"), id);
858 }
859 else if (pattern == "squarehorn")
860 {
861 ant = std::make_unique<antenna::SquareHorn>(name, get_child_real_type(antenna, "diameter"), id);
862 }
863 else if (pattern == "parabolic")
864 {
865 ant = std::make_unique<antenna::Parabolic>(name, get_child_real_type(antenna, "diameter"), id);
866 }
867 else if (pattern == "xml")
868 {
869 ant = ctx.loaders.loadXmlAntenna(name, XmlElement::getSafeAttribute(antenna, "filename"), id);
870 }
871 else if (pattern == "file")
872 {
873 ant = ctx.loaders.loadH5Antenna(name, XmlElement::getSafeAttribute(antenna, "filename"), id);
874 }
875 else
876 {
877 LOG(logging::Level::FATAL, "Unsupported antenna pattern: {}", pattern);
878 throw XmlException("Unsupported antenna pattern: " + pattern);
879 }
880
881 if (!antenna.childElement("efficiency", 0).isValid())
882 {
883 LOG(logging::Level::DEBUG, "Antenna '{}' does not specify efficiency, assuming unity.", name);
884 }
885 else
886 {
887 try
888 {
889 ant->setEfficiencyFactor(get_child_real_type(antenna, "efficiency"));
890 }
891 catch (const XmlException&)
892 {
893 LOG(logging::Level::WARNING, "Antenna '{}' has an empty efficiency, assuming unity.", name);
894 }
895 }
896
897 ctx.world->add(std::move(ant));
898 }
899
901 {
902 math::Path* path = platform->getMotionPath();
903 if (const auto interp_value = XmlElement::getOptionalAttribute(motionPath, "interpolation"))
904 {
905 if (*interp_value == "linear")
906 {
908 }
909 else if (*interp_value == "cubic")
910 {
912 }
913 else if (*interp_value == "static")
914 {
916 }
917 else
918 {
919 LOG(logging::Level::ERROR, "Unsupported interpolation type: {} for platform {}. Defaulting to static",
920 *interp_value, platform->getName());
922 }
923 }
924 else
925 {
927 "MotionPath interpolation type for platform {} not specified. Defaulting to static.",
928 platform->getName());
930 }
931
932 unsigned waypoint_index = 0;
933 while (true)
934 {
935 XmlElement const waypoint = motionPath.childElement("positionwaypoint", waypoint_index);
936 if (!waypoint.isValid())
937 {
938 break;
939 }
940
941 try
942 {
946 get_child_real_type(waypoint, "altitude"));
947 path->addCoord(coord);
948 LOG(logging::Level::TRACE, "Added waypoint {} to motion path for platform {}.", waypoint_index,
949 platform->getName());
950 }
951 catch (const XmlException& e)
952 {
953 LOG(logging::Level::ERROR, "Failed to add waypoint to motion path. Discarding waypoint. {}", e.what());
954 }
955
957 }
958 path->finalize();
959 }
960
962 {
963 math::RotationPath* path = platform->getRotationPath();
964 try
965 {
966 if (const std::string interp = XmlElement::getSafeAttribute(rotation, "interpolation"); interp == "linear")
967 {
969 }
970 else if (interp == "cubic")
971 {
973 }
974 else if (interp == "static")
975 {
977 }
978 else
979 {
980 throw XmlException("Unsupported interpolation type: " + interp);
981 }
982 }
983 catch (XmlException&)
984 {
986 "Failed to set RotationPath interpolation type for platform {}. Defaulting to static",
987 platform->getName());
989 }
990
991 unsigned waypoint_index = 0;
992 while (true)
993 {
994 XmlElement const waypoint = rotation.childElement("rotationwaypoint", waypoint_index);
995 if (!waypoint.isValid())
996 {
997 break;
998 }
999
1000 try
1001 {
1002 LOG(logging::Level::TRACE, "Adding waypoint {} to rotation path for platform {}.", waypoint_index,
1003 platform->getName());
1004
1005 const RealType az_deg = get_child_real_type(waypoint, "azimuth");
1006 const RealType el_deg = get_child_real_type(waypoint, "elevation");
1007 const RealType time = get_child_real_type(waypoint, "time");
1008 const std::string owner =
1009 std::format("platform '{}' rotation waypoint {}", platform->getName(), waypoint_index);
1010
1012 az_deg, unit, rotation_warning_utils::ValueKind::Angle, "XML", owner, "azimuth");
1014 el_deg, unit, rotation_warning_utils::ValueKind::Angle, "XML", owner, "elevation");
1015
1017 }
1018 catch (const XmlException& e)
1019 {
1020 LOG(logging::Level::ERROR, "Failed to add waypoint to rotation path. Discarding waypoint. {}",
1021 e.what());
1022 }
1024 }
1025 path->finalize();
1026 }
1027
1029 {
1030 math::RotationPath* path = platform->getRotationPath();
1031 try
1032 {
1033 const RealType start_az_deg = get_child_real_type(rotation, "startazimuth");
1034 const RealType start_el_deg = get_child_real_type(rotation, "startelevation");
1035 const RealType rate_az_deg_s = get_child_real_type(rotation, "azimuthrate");
1036 const RealType rate_el_deg_s = get_child_real_type(rotation, "elevationrate");
1037 const std::string owner = std::format("platform '{}' fixedrotation", platform->getName());
1038
1040 start_az_deg, unit, rotation_warning_utils::ValueKind::Angle, "XML", owner, "startazimuth");
1042 start_el_deg, unit, rotation_warning_utils::ValueKind::Angle, "XML", owner, "startelevation");
1044 rate_az_deg_s, unit, rotation_warning_utils::ValueKind::Rate, "XML", owner, "azimuthrate");
1046 rate_el_deg_s, unit, rotation_warning_utils::ValueKind::Rate, "XML", owner, "elevationrate");
1047 const math::RotationCoord start =
1049 const math::RotationCoord rate =
1051
1052 path->setConstantRate(start, rate);
1053 LOG(logging::Level::DEBUG, "Added fixed rotation to platform {}", platform->getName());
1054 }
1055 catch (XmlException& e)
1056 {
1057 LOG(logging::Level::FATAL, "Failed to set fixed rotation for platform {}. {}", platform->getName(),
1058 e.what());
1059 throw XmlException("Failed to set fixed rotation for platform " + platform->getName());
1060 }
1061 }
1062
1063 /// Parses a transmitter after its operation mode has already been determined.
1066 const radar::OperationMode mode)
1067 {
1068 const std::string name = XmlElement::getSafeAttribute(transmitter, "name");
1069 const SimId id = assign_id_from_attribute("transmitter '" + name + "'", ObjectType::Transmitter);
1070 const XmlElement pulsed_mode_element = transmitter.childElement("pulsed_mode", 0);
1071 const bool is_pulsed = mode == radar::OperationMode::PULSED_MODE;
1072
1073 auto transmitter_obj = std::make_unique<radar::Transmitter>(platform, name, mode, id);
1074
1075 const SimId waveform_id =
1076 resolve_reference_id(transmitter, "waveform", "transmitter '" + name + "'", *refs.waveforms);
1077 fers_signal::RadarSignal* wave = ctx.world->findWaveform(waveform_id);
1078 if (wave == nullptr)
1079 {
1080 throw XmlException("Waveform ID '" + std::to_string(waveform_id) + "' not found for transmitter '" + name +
1081 "'");
1082 }
1083 validate_fmcw_waveform(*wave, "Waveform '" + wave->getName() + "'");
1084 validate_waveform_mode_match(*wave, mode, "Transmitter '" + name + "'");
1085 transmitter_obj->setWave(wave);
1086
1087 if (is_pulsed)
1088 {
1090 }
1091
1092 const SimId antenna_id =
1093 resolve_reference_id(transmitter, "antenna", "transmitter '" + name + "'", *refs.antennas);
1094 const antenna::Antenna* ant = ctx.world->findAntenna(antenna_id);
1095 if (ant == nullptr)
1096 {
1097 throw XmlException("Antenna ID '" + std::to_string(antenna_id) + "' not found for transmitter '" + name +
1098 "'");
1099 }
1100 transmitter_obj->setAntenna(ant);
1101
1102 const SimId timing_id =
1103 resolve_reference_id(transmitter, "timing", "transmitter '" + name + "'", *refs.timings);
1104 transmitter_obj->setTiming(resolve_timing_instance(timing_id, ctx, "transmitter '" + name + "'"));
1105
1106 RealType const pri = is_pulsed ? (1.0 / transmitter_obj->getPrf()) : 0.0;
1108 if (wave->isFmcwFamily() || wave->isSteppedFrequency())
1109 {
1110 validate_fmcw_schedule(schedule, *wave, "Transmitter '" + name + "'");
1111 }
1112 if (!schedule.empty())
1113 {
1114 transmitter_obj->setSchedule(std::move(schedule));
1115 }
1116
1117 ctx.world->add(std::move(transmitter_obj));
1118 return ctx.world->getTransmitters().back().get();
1119 }
1120
1122 const ReferenceLookup& refs)
1123 {
1124 const std::string name = XmlElement::getSafeAttribute(transmitter, "name");
1125 const radar::OperationMode mode = parse_mode_elements(transmitter, "Transmitter '" + name + "'");
1126 reject_non_empty_sfcw_mode(transmitter, "Transmitter '" + name + "'");
1127 if (const XmlElement fmcw_mode = transmitter.childElement("fmcw_mode", 0);
1129 {
1130 throw XmlException("Transmitter '" + name + "' fmcw_mode must not contain dechirp configuration.");
1131 }
1133 }
1134
1135 /// Parses a receiver after its operation mode has already been determined.
1138 const radar::OperationMode mode)
1139 {
1140 const std::string name = XmlElement::getSafeAttribute(receiver, "name");
1141 const SimId id = assign_id_from_attribute("receiver '" + name + "'", ObjectType::Receiver);
1142 const XmlElement pulsed_mode_element = receiver.childElement("pulsed_mode", 0);
1143 const bool is_pulsed = mode == radar::OperationMode::PULSED_MODE;
1144
1145 auto receiver_obj = std::make_unique<radar::Receiver>(platform, name, next_seed(*ctx.master_seeder), mode, id);
1146
1147 const SimId ant_id = resolve_reference_id(receiver, "antenna", "receiver '" + name + "'", *refs.antennas);
1148 const antenna::Antenna* antenna = ctx.world->findAntenna(ant_id);
1149 if (antenna == nullptr)
1150 {
1151 throw XmlException("Antenna ID '" + std::to_string(ant_id) + "' not found for receiver '" + name + "'");
1152 }
1153 receiver_obj->setAntenna(antenna);
1154
1155 if (!receiver.childElement("noise_temp", 0).isValid())
1156 {
1157 LOG(logging::Level::DEBUG, "Receiver '{}' does not specify noise temperature",
1158 receiver_obj->getName().c_str());
1159 }
1160 else
1161 {
1162 try
1163 {
1164 receiver_obj->setNoiseTemperature(get_child_real_type(receiver, "noise_temp"));
1165 }
1166 catch (const XmlException&)
1167 {
1168 LOG(logging::Level::WARNING, "Receiver '{}' has an empty noise temperature; using default.",
1169 receiver_obj->getName().c_str());
1170 }
1171 }
1172
1173 if (is_pulsed)
1174 {
1175 const RealType window_length = get_child_real_type(pulsed_mode_element, "window_length");
1176 if (window_length <= 0)
1177 {
1178 throw XmlException("<window_length> must be positive for receiver '" + name + "'");
1179 }
1180
1182 if (prf <= 0)
1183 {
1184 throw XmlException("<prf> must be positive for receiver '" + name + "'");
1185 }
1186
1187 const RealType window_skip = get_child_real_type(pulsed_mode_element, "window_skip");
1188 if (window_skip < 0)
1189 {
1190 throw XmlException("<window_skip> must not be negative for receiver '" + name + "'");
1191 }
1192 receiver_obj->setWindowProperties(window_length, prf, window_skip);
1193 }
1194 const SimId timing_id = resolve_reference_id(receiver, "timing", "receiver '" + name + "'", *refs.timings);
1195 receiver_obj->setTiming(resolve_timing_instance(timing_id, ctx, "receiver '" + name + "'"));
1196
1197 if (get_attribute_bool(receiver, "nodirect", false))
1198 {
1200 LOG(logging::Level::DEBUG, "Ignoring direct signals for receiver '{}'", receiver_obj->getName().c_str());
1201 }
1202
1203 if (get_attribute_bool(receiver, "nopropagationloss", false))
1204 {
1206 LOG(logging::Level::DEBUG, "Ignoring propagation losses for receiver '{}'",
1207 receiver_obj->getName().c_str());
1208 }
1209
1210 RealType const pri = is_pulsed ? (1.0 / receiver_obj->getWindowPrf()) : 0.0;
1212 if (!schedule.empty())
1213 {
1214 receiver_obj->setSchedule(std::move(schedule));
1215 }
1216
1217 parse_receiver_dechirp_config(receiver, *receiver_obj, "Receiver '" + name + "'");
1218
1219 ctx.world->add(std::move(receiver_obj));
1220 return ctx.world->getReceivers().back().get();
1221 }
1222
1224 const ReferenceLookup& refs)
1225 {
1226 const std::string name = XmlElement::getSafeAttribute(receiver, "name");
1227 const radar::OperationMode mode = parse_mode_elements(receiver, "Receiver '" + name + "'");
1228 reject_non_empty_sfcw_mode(receiver, "Receiver '" + name + "'");
1230 }
1231
1233 const ReferenceLookup& refs)
1234 {
1235 const std::string name = XmlElement::getSafeAttribute(monostatic, "name");
1236 const radar::OperationMode monostatic_mode = parse_mode_elements(monostatic, "Monostatic '" + name + "'");
1237 reject_non_empty_sfcw_mode(monostatic, "Monostatic '" + name + "'");
1240 if (trans->getMode() != monostatic_mode || recv->getMode() != monostatic_mode)
1241 {
1242 throw XmlException("Monostatic '" + name + "' parsed inconsistent transmitter/receiver modes.");
1243 }
1244 if (trans->getSignal() != nullptr)
1245 {
1246 validate_waveform_mode_match(*trans->getSignal(), trans->getMode(),
1247 "Monostatic '" + trans->getName() + "'");
1248 }
1249 trans->setAttached(recv);
1250 recv->setAttached(trans);
1251 }
1252
1254 {
1255 const std::string name = XmlElement::getSafeAttribute(target, "name");
1256 const SimId id = assign_id_from_attribute("target '" + name + "'", ObjectType::Target);
1257
1258 const XmlElement rcs_element = target.childElement("rcs", 0);
1259 if (!rcs_element.isValid())
1260 {
1261 throw XmlException("<rcs> element is required in <target>!");
1262 }
1263
1264 const std::string rcs_type = XmlElement::getSafeAttribute(rcs_element, "type");
1265 std::unique_ptr<radar::Target> target_obj;
1266 const unsigned seed = next_seed(*ctx.master_seeder);
1267
1268 if (rcs_type == "isotropic")
1269 {
1271 }
1272 else if (rcs_type == "file")
1273 {
1274 // Defer to dependency-injected file loader
1275 target_obj = ctx.loaders.loadFileTarget(platform, name,
1277 }
1278 else
1279 {
1280 throw XmlException("Unsupported RCS type: " + rcs_type);
1281 }
1282
1283 if (const XmlElement model = target.childElement("model", 0); model.isValid())
1284 {
1285 if (const std::string model_type = XmlElement::getSafeAttribute(model, "type"); model_type == "constant")
1286 {
1287 target_obj->setFluctuationModel(std::make_unique<radar::RcsConst>());
1288 }
1289 else if (model_type == "chisquare" || model_type == "gamma")
1290 {
1291 target_obj->setFluctuationModel(
1292 std::make_unique<radar::RcsChiSquare>(target_obj->getRngEngine(), get_child_real_type(model, "k")));
1293 }
1294 else
1295 {
1296 throw XmlException("Unsupported model type: " + model_type);
1297 }
1298 }
1299
1300 LOG(logging::Level::DEBUG, "Added target {} with RCS type {} to platform {}", name, rcs_type,
1301 platform->getName());
1302 ctx.world->add(std::move(target_obj));
1303 }
1304
1306 const std::function<void(const XmlElement&, std::string_view)>& register_name,
1307 const ReferenceLookup& refs)
1308 {
1309 auto parseChildrenWithRefs = [&](const std::string& elementName, auto parseFunc)
1310 {
1311 unsigned index = 0;
1312 while (true)
1313 {
1314 const XmlElement element = platform.childElement(elementName, index++);
1315 if (!element.isValid())
1316 break;
1319 }
1320 };
1321
1322 auto parseChildrenWithoutRefs = [&](const std::string& elementName, auto parseFunc)
1323 {
1324 unsigned index = 0;
1325 while (true)
1326 {
1327 const XmlElement element = platform.childElement(elementName, index++);
1328 if (!element.isValid())
1329 break;
1332 }
1333 };
1334
1339 }
1340
1342 const std::function<void(const XmlElement&, std::string_view)>& register_name,
1343 const ReferenceLookup& refs)
1344 {
1345 std::string const name = XmlElement::getSafeAttribute(platform, "name");
1346 const SimId id = assign_id_from_attribute("platform '" + name + "'", ObjectType::Platform);
1347 auto plat = std::make_unique<radar::Platform>(name, id);
1348
1350
1351 if (const XmlElement motion_path = platform.childElement("motionpath", 0); motion_path.isValid())
1352 {
1354 }
1355
1356 const XmlElement rot_path = platform.childElement("rotationpath", 0);
1357 if (const XmlElement fixed_rot = platform.childElement("fixedrotation", 0);
1358 rot_path.isValid() && fixed_rot.isValid())
1359 {
1361 "Both <rotationpath> and <fixedrotation> are declared for platform {}. Only <rotationpath> will be "
1362 "used.",
1363 plat->getName());
1364 parseRotationPath(rot_path, plat.get(), ctx.parameters.rotation_angle_unit);
1365 }
1366 else if (rot_path.isValid())
1367 {
1368 parseRotationPath(rot_path, plat.get(), ctx.parameters.rotation_angle_unit);
1369 }
1370 else if (fixed_rot.isValid())
1371 {
1372 parseFixedRotation(fixed_rot, plat.get(), ctx.parameters.rotation_angle_unit);
1373 }
1374
1375 ctx.world->add(std::move(plat));
1376 }
1377
1378 void collectIncludeElements(const XmlDocument& doc, const fs::path& currentDir, std::vector<fs::path>& includePaths)
1379 {
1380 unsigned index = 0;
1381 while (true)
1382 {
1383 XmlElement const include_element = doc.getRootElement().childElement("include", index++);
1384 if (!include_element.isValid())
1385 break;
1386
1387 std::string const include_filename = include_element.getText();
1388 if (include_filename.empty())
1389 {
1390 LOG(logging::Level::ERROR, "<include> element is missing the filename!");
1391 continue;
1392 }
1393
1394 fs::path const include_path = currentDir / include_filename;
1395 includePaths.push_back(include_path);
1396
1398 if (!included_doc.loadFile(include_path.string()))
1399 {
1400 LOG(logging::Level::ERROR, "Failed to load included XML file: {}", include_path.string());
1401 continue;
1402 }
1403
1405 }
1406 }
1407
1409 {
1410 std::vector<fs::path> include_paths;
1412 bool did_combine = false;
1413
1414 for (const auto& include_path : include_paths)
1415 {
1417 if (!included_doc.loadFile(include_path.string()))
1418 {
1419 throw XmlException("Failed to load included XML file: " + include_path.string());
1420 }
1421
1423 did_combine = true;
1424 }
1425
1427 return did_combine;
1428 }
1429
1431 {
1432 LOG(logging::Level::DEBUG, "Validating the{}XML file...", didCombine ? " combined " : " ");
1433 if (!mainDoc.validateWithDtd(fers_xml_dtd))
1434 {
1435 LOG(logging::Level::FATAL, "{} XML file failed DTD validation!", didCombine ? "Combined" : "Main");
1436 throw XmlException("XML file failed DTD validation!");
1437 }
1438 LOG(logging::Level::DEBUG, "{} XML file passed DTD validation.", didCombine ? "Combined" : "Main");
1439
1440 if (!mainDoc.validateWithXsd(fers_xml_xsd))
1441 {
1442 LOG(logging::Level::FATAL, "{} XML file failed XSD validation!", didCombine ? "Combined" : "Main");
1443 throw XmlException("XML file failed XSD validation!");
1444 }
1445 LOG(logging::Level::DEBUG, "{} XML file passed XSD validation.", didCombine ? "Combined" : "Main");
1446 }
1447
1449 {
1450 const XmlElement root = doc.getRootElement();
1451 if (root.name() != "simulation")
1452 {
1453 throw XmlException("Root element is not <simulation>!");
1454 }
1455
1456 std::unordered_map<std::string, std::string> name_registry;
1457 name_registry.reserve(64); // TODO: reserve 64?
1458 const auto register_name = [&](const XmlElement& element, const std::string_view kind)
1459 {
1460 const std::string name = XmlElement::getSafeAttribute(element, "name");
1461 const auto [iter, inserted] = name_registry.emplace(name, std::string(kind));
1462 if (!inserted)
1463 {
1464 throw XmlException("Duplicate name '" + name + "' found for " + std::string(kind) +
1465 "; previously used by " + iter->second + ".");
1466 }
1467 };
1468
1469 try
1470 {
1471 ctx.parameters.simulation_name = XmlElement::getSafeAttribute(root, "name");
1472 if (!ctx.parameters.simulation_name.empty())
1473 {
1474 LOG(logging::Level::INFO, "Simulation name set to: {}", ctx.parameters.simulation_name);
1475 }
1476 }
1477 catch (const XmlException&)
1478 {
1479 LOG(logging::Level::WARNING, "No 'name' attribute found in <simulation> tag. KML name will default.");
1480 }
1481
1482 parseParameters(root.childElement("parameters", 0), ctx.parameters);
1483
1484 params::params = ctx.parameters;
1485
1486 auto parseElements =
1487 [](const XmlElement& parent, const std::string& elementName, ParserContext& parser_ctx, auto parseFunction)
1488 {
1489 unsigned index = 0;
1490 while (true)
1491 {
1492 XmlElement const element = parent.childElement(elementName, index++);
1493 if (!element.isValid())
1494 break;
1496 }
1497 };
1498
1499 parseElements(root, "waveform", ctx,
1500 [&](const XmlElement& p, ParserContext& c)
1501 {
1502 register_name(p, "waveform");
1503 parseWaveform(p, c);
1504 });
1505
1506 parseElements(root, "timing", ctx,
1507 [&](const XmlElement& p, ParserContext& c)
1508 {
1509 register_name(p, "timing");
1510 parseTiming(p, c);
1511 });
1512
1513 parseElements(root, "antenna", ctx,
1514 [&](const XmlElement& p, ParserContext& c)
1515 {
1516 register_name(p, "antenna");
1517 parseAntenna(p, c);
1518 });
1519
1520 std::unordered_map<std::string, SimId> waveform_refs;
1521 std::unordered_map<std::string, SimId> antenna_refs;
1522 std::unordered_map<std::string, SimId> timing_refs;
1523 waveform_refs.reserve(ctx.world->getWaveforms().size());
1524 antenna_refs.reserve(ctx.world->getAntennas().size());
1525 timing_refs.reserve(ctx.world->getTimings().size());
1526
1527 for (const auto& [id, waveform] : ctx.world->getWaveforms())
1528 waveform_refs.emplace(waveform->getName(), id);
1529 for (const auto& [id, antenna] : ctx.world->getAntennas())
1530 antenna_refs.emplace(antenna->getName(), id);
1531 for (const auto& [id, timing] : ctx.world->getTimings())
1532 timing_refs.emplace(timing->getName(), id);
1533
1535
1536 parseElements(root, "platform", ctx,
1537 [&](const XmlElement& p, ParserContext& c)
1538 {
1539 register_name(p, "platform");
1541 });
1542
1543 ctx.world->resolveReceiverDechirpReferences();
1544
1545 ctx.world->scheduleInitialEvents();
1546
1547 LOG(logging::Level::DEBUG, "Initial Event Queue State:\n{}", ctx.world->dumpEventQueue());
1548 }
1549
1551 {
1552 return {.loadWaveform = [](const std::string& name, const fs::path& pulse_path, RealType power,
1554 { return serial::loadWaveformFromFile(name, pulse_path.string(), power, carrierFreq, id); },
1555 .loadXmlAntenna = [](const std::string& name, const std::string& filename, SimId id)
1556 { return std::make_unique<antenna::XmlAntenna>(name, filename, id); },
1557 .loadH5Antenna = [](const std::string& name, const std::string& filename, SimId id)
1558 { return std::make_unique<antenna::H5Antenna>(name, filename, id); },
1559 .loadFileTarget = [](radar::Platform* platform, const std::string& name, const std::string& filename,
1560 unsigned seed, SimId id)
1561 { return radar::createFileTarget(platform, name, filename, seed, id); }};
1562 }
1563}
Header file defining various types of antennas and their gain patterns.
const Transmitter & transmitter
const Receiver & receiver
SimId generateId(ObjectType type)
Generate a unique SimId for a given object type.
Definition sim_id.h:60
static SimIdGenerator & instance()
Get the singleton instance of SimIdGenerator.
Definition sim_id.h:48
Class for managing XML documents.
Class representing a node in an XML document.
XmlElement childElement(const std::string_view name="", const unsigned index=0) const noexcept
Retrieve a child element by name and index.
static std::optional< std::string > getOptionalAttribute(const XmlElement &element, const std::string_view name)
Get the value of an optional attribute.
static std::string getSafeAttribute(const XmlElement &element, const std::string_view name)
Get the value of an attribute safely.
bool isValid() const noexcept
Check if the XML element is valid.
std::string getText() const
Get the text content of the XML element.
Exception class for handling XML-related errors.
Abstract base class representing an antenna.
Class representing a radar signal with associated properties.
Represents a path with coordinates and allows for various interpolation methods.
Definition path.h:31
@ INTERP_STATIC
Hold the first coordinate for all query times.
@ INTERP_LINEAR
Linearly interpolate between neighboring coordinates.
@ INTERP_CUBIC
Cubically interpolate between neighboring coordinates.
void setInterp(InterpType settype) noexcept
Changes the interpolation type.
Definition path.cpp:164
void addCoord(const Coord &coord) noexcept
Adds a coordinate to the path.
Definition path.cpp:27
void finalize()
Finalizes the path, preparing it for interpolation.
Definition path.cpp:147
Manages rotational paths with different interpolation techniques.
void finalize()
Finalizes the rotation path for interpolation.
void setConstantRate(const RotationCoord &setstart, const RotationCoord &setrate) noexcept
Sets constant rate interpolation.
void setInterp(InterpType setinterp) noexcept
Sets the interpolation type for the path.
void addCoord(const RotationCoord &coord) noexcept
Adds a rotation coordinate to the path.
@ INTERP_STATIC
Hold the first rotation for all query times.
@ INTERP_LINEAR
Linearly interpolate between neighboring rotations.
@ INTERP_CUBIC
Cubically interpolate between neighboring rotations.
A class representing a vector in rectangular coordinates.
Represents a simulation platform with motion and rotation paths.
Definition platform.h:32
Manages radar signal reception and response processing.
Definition receiver.h:47
@ Transmitter
Use a named transmitter.
@ Attached
Use the attached transmitter.
@ None
No reference configured.
@ Custom
Use a named top-level waveform with the receiver schedule.
DechirpMode
Receiver-side FMCW dechirping mode.
Definition receiver.h:63
@ None
Output raw pre-mix streaming IQ.
@ FLAG_NODIRECT
Disable direct-path reception.
@ FLAG_NOPROPLOSS
Disable propagation-loss scaling.
Represents a radar transmitter system.
Definition transmitter.h:34
Manages timing properties such as frequency, offsets, and synchronization.
Global configuration file for the project.
double RealType
Type for real numbers.
Definition config.h:27
Coordinate and rotation structure operations.
Classes and operations for 3D geometry.
void mergeXmlDocuments(const XmlDocument &mainDoc, const XmlDocument &includedDoc)
Merge two XML documents.
void removeIncludeElements(const XmlDocument &doc)
Remove "include" elements from the XML document.
Header file for the logging system.
#define LOG(level,...)
Definition logging.h:19
FmcwChirpDirection parseFmcwChirpDirection(const std::string_view direction)
Parses a schema chirp direction token.
@ WARNING
Warning level for potentially harmful situations.
@ FATAL
Fatal level for severe error events.
@ TRACE
Trace level for detailed debugging information.
@ INFO
Info level for informational messages.
@ ERROR
Error level for error events.
@ DEBUG
Debug level for general debugging information.
RealType rate() noexcept
Get the rendering sample rate.
Definition parameters.h:121
unsigned oversampleRatio() noexcept
Get the oversampling ratio.
Definition parameters.h:151
@ UTM
Universal Transverse Mercator.
@ ENU
East-North-Up local tangent plane (default)
@ ECEF
Earth-Centered, Earth-Fixed.
std::optional< RotationAngleUnit > rotationAngleUnitFromToken(const std::string_view token) noexcept
Parses a rotation angle unit from an XML token.
Definition parameters.h:362
void validateOversampleRatio(const unsigned ratio)
Validates that an oversampling ratio is supported.
Definition parameters.h:164
RotationAngleUnit
Defines the units used at external rotation-path boundaries.
Definition parameters.h:42
Parameters params
Global simulation parameter state.
Definition parameters.h:85
std::unique_ptr< Target > createIsoTarget(Platform *platform, std::string name, RealType rcs, unsigned seed, const SimId id=0)
Creates an isotropic target.
Definition target.h:282
OperationMode
Defines the operational mode of a radar component.
Definition radar_obj.h:39
@ SFCW_MODE
The component operates in a stepped-frequency CW streaming mode.
@ PULSED_MODE
The component operates in a pulsed mode.
@ CW_MODE
The component operates in a continuous-wave mode.
@ FMCW_MODE
The component operates in an FMCW streaming mode.
Receiver::DechirpReferenceSource parseDechirpReferenceSourceToken(const std::string_view token)
Parses a dechirp reference source scenario token.
Definition receiver.cpp:92
std::unique_ptr< Target > createFileTarget(Platform *platform, std::string name, const std::string &filename, unsigned seed, const SimId id=0)
Creates a file-based target.
Definition target.h:297
Receiver::DechirpMode parseDechirpModeToken(const std::string_view token)
Parses a dechirp mode scenario token.
Definition receiver.cpp:59
std::vector< SchedulePeriod > processRawSchedule(const std::vector< SchedulePeriod > &periods, const std::string &ownerName, const bool isPulsed, const RealType pri)
Processes a raw list of schedule periods.
void validateWaveform(const fers_signal::RadarSignal &wave, const std::string &owner, const Thrower &throw_error)
Validates that a waveform is compatible with FMCW streaming constraints.
void validateSchedule(const std::vector< radar::SchedulePeriod > &schedule, const fers_signal::FmcwChirpSignal &fmcw, const std::string &owner, const Thrower &throw_error)
Validates that an FMCW waveform schedule can emit complete chirps.
void validateWaveformModeMatch(const fers_signal::RadarSignal &wave, const radar::OperationMode mode, const std::string &owner, const Thrower &throw_error)
Validates that a waveform and radar operation mode are compatible.
math::RotationCoord external_rotation_to_internal(const RealType azimuth, const RealType elevation, const RealType time, const params::RotationAngleUnit unit) noexcept
Converts external compass azimuth/elevation into internal rotation coordinates.
math::RotationCoord external_rotation_rate_to_internal(const RealType azimuth_rate, const RealType elevation_rate, const RealType time, const params::RotationAngleUnit unit) noexcept
Converts external compass azimuth/elevation rates into internal rotation rates.
void maybe_warn_about_rotation_value(const RealType value, const params::RotationAngleUnit declared_unit, const ValueKind kind, const std::string_view source, const std::string_view owner, const std::string_view field)
Emits or captures a warning when a rotation value likely uses the wrong unit.
SimId assign_id_from_attribute(const std::string &owner, ObjectType type)
Generates a unique SimId based on the requested object type.
void setOptionalUnsignedParameter(const XmlElement &parameters, const std::string &param_name, const unsigned default_value, Setter setter)
void parseUtmCoordinateSystem(const XmlElement &cs_element, params::Parameters &params_out)
void collectIncludeElements(const XmlDocument &doc, const fs::path &currentDir, std::vector< fs::path > &includePaths)
static radar::Transmitter * parseTransmitterWithMode(const XmlElement &transmitter, radar::Platform *platform, ParserContext &ctx, const ReferenceLookup &refs, const radar::OperationMode mode)
Parses a transmitter after its operation mode has already been determined.
void parseAntenna(const XmlElement &antenna, ParserContext &ctx)
Parses an <antenna> block and adds it to the World.
void parseWaveform(const XmlElement &waveform, ParserContext &ctx)
Parses a <waveform> block and adds it to the World.
bool addIncludeFilesToMainDocument(const XmlDocument &mainDoc, const fs::path &currentDir)
void processParsedDocument(const XmlDocument &doc, ParserContext &ctx)
Coordinates the full parsing of a validated XML document tree.
std::vector< radar::SchedulePeriod > parseSchedule(const XmlElement &parent, const std::string &parentName, const bool isPulsed, const RealType pri)
Parses a schedule (active periods) for a transmitter or receiver.
SimId resolve_reference_id(const XmlElement &element, const std::string &attributeName, const std::string &owner, const std::unordered_map< std::string, SimId > &name_map)
Resolves an XML string reference into an internal SimId.
void parseFixedRotation(const XmlElement &rotation, radar::Platform *platform, const params::RotationAngleUnit unit)
Parses a <fixedrotation> block and attaches it to a Platform.
void setOptionalRealParameter(const XmlElement &parameters, const std::string &param_name, const RealType default_value, Setter setter)
void parseRotationPath(const XmlElement &rotation, radar::Platform *platform, const params::RotationAngleUnit unit)
Parses a <rotationpath> block and attaches it to a Platform.
radar::Transmitter * parseTransmitter(const XmlElement &transmitter, radar::Platform *platform, ParserContext &ctx, const ReferenceLookup &refs)
Parses a <transmitter> block, resolves its dependencies, and adds it to the World.
void parsePlatformElements(const XmlElement &platform, ParserContext &ctx, radar::Platform *plat, const std::function< void(const XmlElement &, std::string_view)> &register_name, const ReferenceLookup &refs)
Iterates and parses all children elements (radars, targets) of a platform.
void parseTiming(const XmlElement &timing, ParserContext &ctx)
Parses a <timing> block and adds the prototype timing to the World.
void parseRotationAngleUnit(const XmlElement &parameters, params::Parameters &params_out)
void parseParameters(const XmlElement &parameters, params::Parameters &params_out)
Parses the <parameters> block into the isolated context parameters.
void parseCoordinateSystemParameter(const XmlElement &parameters, params::Parameters &params_out, const bool origin_set)
void parsePlatform(const XmlElement &platform, ParserContext &ctx, const std::function< void(const XmlElement &, std::string_view)> &register_name, const ReferenceLookup &refs)
Parses a complete <platform> block, including its motion paths and sub-elements.
void parseTarget(const XmlElement &target, radar::Platform *platform, ParserContext &ctx)
Parses a <target> block and adds it to the World.
RealType get_child_real_type(const XmlElement &element, const std::string &elementName)
Extracts a floating-point (RealType) value from a named child element.
radar::Receiver * parseReceiver(const XmlElement &receiver, radar::Platform *platform, ParserContext &ctx, const ReferenceLookup &refs)
Parses a <receiver> block, resolves its dependencies, and adds it to the World.
static radar::Receiver * parseReceiverWithMode(const XmlElement &receiver, radar::Platform *platform, ParserContext &ctx, const ReferenceLookup &refs, const radar::OperationMode mode)
Parses a receiver after its operation mode has already been determined.
bool get_attribute_bool(const XmlElement &element, const std::string &attributeName, const bool defaultVal)
Extracts a boolean value from a named attribute.
unsigned parseUnsignedParameter(const std::string_view param_name, const RealType raw_value)
void validateXml(const bool didCombine, const XmlDocument &mainDoc)
Validates an XML document against the embedded DTD and XSD schemas.
void parseMotionPath(const XmlElement &motionPath, radar::Platform *platform)
Parses a <motionpath> block and attaches it to a Platform.
bool parseOriginParameter(const XmlElement &parameters, params::Parameters &params_out)
void parseOptionalNumericParameters(const XmlElement &parameters, params::Parameters &params_out)
AssetLoaders createDefaultAssetLoaders()
Creates an AssetLoaders struct populated with standard file-I/O implementations.
void parseMonostatic(const XmlElement &monostatic, radar::Platform *platform, ParserContext &ctx, const ReferenceLookup &refs)
Parses a <monostatic> block, creating a linked transmitter and receiver pair.
std::unique_ptr< RadarSignal > loadWaveformFromFile(const std::string &name, const std::string &filename, const RealType power, const RealType carrierFreq, const SimId id)
Loads a radar waveform from a file and returns a RadarSignal object.
Provides the definition and functionality of the Path class for handling coordinate-based paths with ...
Defines the Platform class used in radar simulation.
Header file for the PrototypeTiming class.
Defines the Radar class and associated functionality.
Classes for handling radar waveforms and signals.
Radar Receiver class for managing signal reception and response handling.
Defines the RotationPath class for handling rotational paths with different interpolation types.
uint64_t SimId
64-bit Unique Simulation ID.
Definition sim_id.h:18
ObjectType
Categorizes objects for ID generation.
Definition sim_id.h:25
math::Vec3 max
RealType c
Represents a position in 3D space with an associated time.
Definition coord.h:24
Represents a rotation in terms of azimuth, elevation, and time.
Definition coord.h:72
Struct to hold simulation parameters.
Definition parameters.h:52
static constexpr RealType DEFAULT_C
Speed of light (m/s)
Definition parameters.h:53
Parsed and resolved dechirp reference details.
Definition receiver.h:80
Receiver-local FMCW IF-chain request parsed from scenario input.
Definition receiver.h:91
Container for functions that load external file-backed assets.
std::function< std::unique_ptr< fers_signal::RadarSignal >(const std::string &name, const std::filesystem::path &pulse_path, RealType power, RealType carrierFreq, SimId id)> loadWaveform
Hook to load a pulsed waveform from an external file.
Encapsulates the state required during the XML parsing process.
Holds maps to resolve string names to internal SimId references during XML parsing.
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.
Interface for loading waveform data into RadarSignal objects.
Header file for the World class in the simulator.
Core utility layer for parsing FERS XML scenario files.