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
652 {
653 const std::string filename_str = XmlElement::getSafeAttribute(file_element, "filename");
654 fs::path waveform_path(filename_str);
655
656 if (!fs::exists(waveform_path))
657 {
658 waveform_path = ctx.base_dir / filename_str;
659 }
660
661 auto wave = ctx.loaders.loadWaveform(name, waveform_path, power, carrier, id, kind);
662 ctx.world->add(std::move(wave));
663 };
664
665 if (const XmlElement pulsed_file = waveform.childElement("pulsed_from_file", 0); pulsed_file.isValid())
666 {
668 }
669 else if (const XmlElement cw_file = waveform.childElement("cw_from_file", 0); cw_file.isValid())
670 {
672 }
673 else if (const XmlElement fmcw_file = waveform.childElement("fmcw_from_file", 0); fmcw_file.isValid())
674 {
676 }
677 else if (waveform.childElement("cw", 0).isValid())
678 {
679 auto cw_signal = std::make_unique<fers_signal::CwSignal>();
680 auto wave = std::make_unique<fers_signal::RadarSignal>(
681 name, power, carrier, ctx.parameters.end - ctx.parameters.start, std::move(cw_signal), id);
682 ctx.world->add(std::move(wave));
683 }
684 else if (const XmlElement sfcw_element = waveform.childElement("stepped_frequency", 0); sfcw_element.isValid())
685 {
686 const RealType start_frequency_offset = get_child_real_type(sfcw_element, "start_frequency_offset");
687 const RealType step_size = get_child_real_type(sfcw_element, "step_size");
689 if (raw_step_count <= 0.0 || std::floor(raw_step_count) != raw_step_count)
690 {
691 throw XmlException("Waveform '" + name + "' has an invalid step_count.");
692 }
693 const RealType dwell_time = get_child_real_type(sfcw_element, "dwell_time");
694 const RealType step_period = get_child_real_type(sfcw_element, "step_period");
695
696 std::optional<std::size_t> sweep_count;
697 if (const auto sweep_count_element = sfcw_element.childElement("sweep_count", 0);
698 sweep_count_element.isValid())
699 {
700 const RealType raw_count = get_child_real_type(sfcw_element, "sweep_count");
701 if (raw_count <= 0.0 || std::floor(raw_count) != raw_count)
702 {
703 throw XmlException("Waveform '" + name + "' has an invalid sweep_count.");
704 }
705 sweep_count = static_cast<std::size_t>(raw_count);
706 }
707
708 auto sfcw_signal = std::make_unique<fers_signal::SteppedFrequencySignal>(
709 start_frequency_offset, step_size, static_cast<std::size_t>(raw_step_count), dwell_time, step_period,
710 sweep_count);
711 auto wave = std::make_unique<fers_signal::RadarSignal>(name, power, carrier, sfcw_signal->getDwellTime(),
712 std::move(sfcw_signal), id);
713 validate_fmcw_waveform(*wave, "Waveform '" + name + "'");
714 ctx.world->add(std::move(wave));
715 }
716 else if (const XmlElement fmcw_element = waveform.childElement("fmcw_linear_chirp", 0); fmcw_element.isValid())
717 {
718 const auto direction =
720 const RealType chirp_bandwidth = get_child_real_type(fmcw_element, "chirp_bandwidth");
721 const RealType chirp_duration = get_child_real_type(fmcw_element, "chirp_duration");
722 const RealType chirp_period = get_child_real_type(fmcw_element, "chirp_period");
723
724 RealType start_frequency_offset = 0.0;
725 if (const auto start_offset = fmcw_element.childElement("start_frequency_offset", 0);
726 start_offset.isValid())
727 {
728 start_frequency_offset = get_child_real_type(fmcw_element, "start_frequency_offset");
729 }
730
731 std::optional<std::size_t> chirp_count;
732 if (const auto chirp_count_element = fmcw_element.childElement("chirp_count", 0);
733 chirp_count_element.isValid())
734 {
735 const RealType raw_count = get_child_real_type(fmcw_element, "chirp_count");
736 if (raw_count <= 0.0 || std::floor(raw_count) != raw_count)
737 {
738 throw XmlException("Waveform '" + name + "' has an invalid chirp_count.");
739 }
740 chirp_count = static_cast<std::size_t>(raw_count);
741 }
742
743 auto fmcw_signal = std::make_unique<fers_signal::FmcwChirpSignal>(
744 chirp_bandwidth, chirp_duration, chirp_period, start_frequency_offset, chirp_count, direction);
745 // RadarSignal length is the active chirp duration, not T_rep. The repeat period only spaces chirps.
746 auto wave = std::make_unique<fers_signal::RadarSignal>(name, power, carrier, chirp_duration,
747 std::move(fmcw_signal), id);
748 validate_fmcw_waveform(*wave, "Waveform '" + name + "'");
749 ctx.world->add(std::move(wave));
750 }
751 else if (const XmlElement fmcw_triangle_element = waveform.childElement("fmcw_triangle", 0);
752 fmcw_triangle_element.isValid())
753 {
754 const RealType chirp_bandwidth = get_child_real_type(fmcw_triangle_element, "chirp_bandwidth");
755 const RealType chirp_duration = get_child_real_type(fmcw_triangle_element, "chirp_duration");
756
757 RealType start_frequency_offset = 0.0;
758 if (const auto start_offset = fmcw_triangle_element.childElement("start_frequency_offset", 0);
759 start_offset.isValid())
760 {
761 start_frequency_offset = get_child_real_type(fmcw_triangle_element, "start_frequency_offset");
762 }
763
764 std::optional<std::size_t> triangle_count;
765 if (const auto triangle_count_element = fmcw_triangle_element.childElement("triangle_count", 0);
766 triangle_count_element.isValid())
767 {
769 if (raw_count <= 0.0 || std::floor(raw_count) != raw_count)
770 {
771 throw XmlException("Waveform '" + name + "' has an invalid triangle_count.");
772 }
773 triangle_count = static_cast<std::size_t>(raw_count);
774 }
775
776 auto fmcw_signal = std::make_unique<fers_signal::FmcwTriangleSignal>(
777 chirp_bandwidth, chirp_duration, start_frequency_offset, triangle_count);
778 auto wave = std::make_unique<fers_signal::RadarSignal>(
779 name, power, carrier, fmcw_signal->getTrianglePeriod(), std::move(fmcw_signal), id);
780 validate_fmcw_waveform(*wave, "Waveform '" + name + "'");
781 ctx.world->add(std::move(wave));
782 }
783 else
784 {
785 LOG(logging::Level::FATAL, "Unsupported waveform type for '{}'", name);
786 throw XmlException("Unsupported waveform type for '" + name + "'");
787 }
788 }
789
791 {
792 const std::string name = XmlElement::getSafeAttribute(timing, "name");
793 const SimId id = assign_id_from_attribute("timing '" + name + "'", ObjectType::Timing);
794 const RealType freq = get_child_real_type(timing, "frequency");
795 auto timing_obj = std::make_unique<timing::PrototypeTiming>(name, id);
796
797 timing_obj->setFrequency(freq);
798
799 unsigned noise_index = 0;
800 while (true)
801 {
802 XmlElement const noise_element = timing.childElement("noise_entry", noise_index++);
803 if (!noise_element.isValid())
804 {
805 break;
806 }
807
808 timing_obj->setAlpha(get_child_real_type(noise_element, "alpha"),
810 }
811
813 [&](const std::string& element_name, const std::string& description, auto setter)
814 {
815 if (!timing.childElement(element_name, 0).isValid())
816 {
817 LOG(logging::Level::DEBUG, "Clock section '{}' does not specify {}.", name, description);
818 return;
819 }
820 try
821 {
823 }
824 catch (const XmlException&)
825 {
826 LOG(logging::Level::WARNING, "Clock section '{}' has an empty {}. Using default.", name, description);
827 }
828 };
829
830 set_optional_timing_parameter("freq_offset", "frequency offset",
831 [&](const RealType value) { timing_obj->setFreqOffset(value); });
832 set_optional_timing_parameter("random_freq_offset_stdev", "random frequency offset",
833 [&](const RealType value) { timing_obj->setRandomFreqOffsetStdev(value); });
834 set_optional_timing_parameter("phase_offset", "phase offset",
835 [&](const RealType value) { timing_obj->setPhaseOffset(value); });
836 set_optional_timing_parameter("random_phase_offset_stdev", "random phase offset",
837 [&](const RealType value) { timing_obj->setRandomPhaseOffsetStdev(value); });
838
839 if (get_attribute_bool(timing, "synconpulse", false))
840 {
841 timing_obj->setSyncOnPulse();
842 }
843
844 ctx.world->add(std::move(timing_obj));
845 }
846
848 {
849 const std::string name = XmlElement::getSafeAttribute(antenna, "name");
850 const SimId id = assign_id_from_attribute("antenna '" + name + "'", ObjectType::Antenna);
851 const std::string pattern = XmlElement::getSafeAttribute(antenna, "pattern");
852
853 std::unique_ptr<antenna::Antenna> ant;
854
855 LOG(logging::Level::DEBUG, "Adding antenna '{}' with pattern '{}'", name, pattern);
856 if (pattern == "isotropic")
857 {
858 ant = std::make_unique<antenna::Isotropic>(name, id);
859 }
860 else if (pattern == "sinc")
861 {
862 ant = std::make_unique<antenna::Sinc>(name, get_child_real_type(antenna, "alpha"),
864 get_child_real_type(antenna, "gamma"), id);
865 }
866 else if (pattern == "gaussian")
867 {
868 ant = std::make_unique<antenna::Gaussian>(name, get_child_real_type(antenna, "azscale"),
869 get_child_real_type(antenna, "elscale"), id);
870 }
871 else if (pattern == "squarehorn")
872 {
873 ant = std::make_unique<antenna::SquareHorn>(name, get_child_real_type(antenna, "diameter"), id);
874 }
875 else if (pattern == "parabolic")
876 {
877 ant = std::make_unique<antenna::Parabolic>(name, get_child_real_type(antenna, "diameter"), id);
878 }
879 else if (pattern == "xml")
880 {
881 ant = ctx.loaders.loadXmlAntenna(name, XmlElement::getSafeAttribute(antenna, "filename"), id);
882 }
883 else if (pattern == "file")
884 {
885 ant = ctx.loaders.loadH5Antenna(name, XmlElement::getSafeAttribute(antenna, "filename"), id);
886 }
887 else
888 {
889 LOG(logging::Level::FATAL, "Unsupported antenna pattern: {}", pattern);
890 throw XmlException("Unsupported antenna pattern: " + pattern);
891 }
892
893 if (!antenna.childElement("efficiency", 0).isValid())
894 {
895 LOG(logging::Level::DEBUG, "Antenna '{}' does not specify efficiency, assuming unity.", name);
896 }
897 else
898 {
899 try
900 {
901 ant->setEfficiencyFactor(get_child_real_type(antenna, "efficiency"));
902 }
903 catch (const XmlException&)
904 {
905 LOG(logging::Level::WARNING, "Antenna '{}' has an empty efficiency, assuming unity.", name);
906 }
907 }
908
909 ctx.world->add(std::move(ant));
910 }
911
913 {
914 math::Path* path = platform->getMotionPath();
915 if (const auto interp_value = XmlElement::getOptionalAttribute(motionPath, "interpolation"))
916 {
917 if (*interp_value == "linear")
918 {
920 }
921 else if (*interp_value == "cubic")
922 {
924 }
925 else if (*interp_value == "static")
926 {
928 }
929 else
930 {
931 LOG(logging::Level::ERROR, "Unsupported interpolation type: {} for platform {}. Defaulting to static",
932 *interp_value, platform->getName());
934 }
935 }
936 else
937 {
939 "MotionPath interpolation type for platform {} not specified. Defaulting to static.",
940 platform->getName());
942 }
943
944 unsigned waypoint_index = 0;
945 while (true)
946 {
947 XmlElement const waypoint = motionPath.childElement("positionwaypoint", waypoint_index);
948 if (!waypoint.isValid())
949 {
950 break;
951 }
952
953 try
954 {
958 get_child_real_type(waypoint, "altitude"));
959 path->addCoord(coord);
960 LOG(logging::Level::TRACE, "Added waypoint {} to motion path for platform {}.", waypoint_index,
961 platform->getName());
962 }
963 catch (const XmlException& e)
964 {
965 LOG(logging::Level::ERROR, "Failed to add waypoint to motion path. Discarding waypoint. {}", e.what());
966 }
967
969 }
970 path->finalize();
971 }
972
974 {
975 math::RotationPath* path = platform->getRotationPath();
976 try
977 {
978 if (const std::string interp = XmlElement::getSafeAttribute(rotation, "interpolation"); interp == "linear")
979 {
981 }
982 else if (interp == "cubic")
983 {
985 }
986 else if (interp == "static")
987 {
989 }
990 else
991 {
992 throw XmlException("Unsupported interpolation type: " + interp);
993 }
994 }
995 catch (XmlException&)
996 {
998 "Failed to set RotationPath interpolation type for platform {}. Defaulting to static",
999 platform->getName());
1001 }
1002
1003 unsigned waypoint_index = 0;
1004 while (true)
1005 {
1006 XmlElement const waypoint = rotation.childElement("rotationwaypoint", waypoint_index);
1007 if (!waypoint.isValid())
1008 {
1009 break;
1010 }
1011
1012 try
1013 {
1014 LOG(logging::Level::TRACE, "Adding waypoint {} to rotation path for platform {}.", waypoint_index,
1015 platform->getName());
1016
1017 const RealType az_deg = get_child_real_type(waypoint, "azimuth");
1018 const RealType el_deg = get_child_real_type(waypoint, "elevation");
1019 const RealType time = get_child_real_type(waypoint, "time");
1020 const std::string owner =
1021 std::format("platform '{}' rotation waypoint {}", platform->getName(), waypoint_index);
1022
1024 az_deg, unit, rotation_warning_utils::ValueKind::Angle, "XML", owner, "azimuth");
1026 el_deg, unit, rotation_warning_utils::ValueKind::Angle, "XML", owner, "elevation");
1027
1029 }
1030 catch (const XmlException& e)
1031 {
1032 LOG(logging::Level::ERROR, "Failed to add waypoint to rotation path. Discarding waypoint. {}",
1033 e.what());
1034 }
1036 }
1037 path->finalize();
1038 }
1039
1041 {
1042 math::RotationPath* path = platform->getRotationPath();
1043 try
1044 {
1045 const RealType start_az_deg = get_child_real_type(rotation, "startazimuth");
1046 const RealType start_el_deg = get_child_real_type(rotation, "startelevation");
1047 const RealType rate_az_deg_s = get_child_real_type(rotation, "azimuthrate");
1048 const RealType rate_el_deg_s = get_child_real_type(rotation, "elevationrate");
1049 const std::string owner = std::format("platform '{}' fixedrotation", platform->getName());
1050
1052 start_az_deg, unit, rotation_warning_utils::ValueKind::Angle, "XML", owner, "startazimuth");
1054 start_el_deg, unit, rotation_warning_utils::ValueKind::Angle, "XML", owner, "startelevation");
1056 rate_az_deg_s, unit, rotation_warning_utils::ValueKind::Rate, "XML", owner, "azimuthrate");
1058 rate_el_deg_s, unit, rotation_warning_utils::ValueKind::Rate, "XML", owner, "elevationrate");
1059 const math::RotationCoord start =
1061 const math::RotationCoord rate =
1063
1064 path->setConstantRate(start, rate);
1065 LOG(logging::Level::DEBUG, "Added fixed rotation to platform {}", platform->getName());
1066 }
1067 catch (XmlException& e)
1068 {
1069 LOG(logging::Level::FATAL, "Failed to set fixed rotation for platform {}. {}", platform->getName(),
1070 e.what());
1071 throw XmlException("Failed to set fixed rotation for platform " + platform->getName());
1072 }
1073 }
1074
1075 /// Parses a transmitter after its operation mode has already been determined.
1078 const radar::OperationMode mode)
1079 {
1080 const std::string name = XmlElement::getSafeAttribute(transmitter, "name");
1081 const SimId id = assign_id_from_attribute("transmitter '" + name + "'", ObjectType::Transmitter);
1082 const XmlElement pulsed_mode_element = transmitter.childElement("pulsed_mode", 0);
1083 const bool is_pulsed = mode == radar::OperationMode::PULSED_MODE;
1084
1085 auto transmitter_obj = std::make_unique<radar::Transmitter>(platform, name, mode, id);
1086
1087 const SimId waveform_id =
1088 resolve_reference_id(transmitter, "waveform", "transmitter '" + name + "'", *refs.waveforms);
1089 fers_signal::RadarSignal* wave = ctx.world->findWaveform(waveform_id);
1090 if (wave == nullptr)
1091 {
1092 throw XmlException("Waveform ID '" + std::to_string(waveform_id) + "' not found for transmitter '" + name +
1093 "'");
1094 }
1095 validate_fmcw_waveform(*wave, "Waveform '" + wave->getName() + "'");
1096 validate_waveform_mode_match(*wave, mode, "Transmitter '" + name + "'");
1097 transmitter_obj->setWave(wave);
1098
1099 if (is_pulsed)
1100 {
1102 }
1103
1104 const SimId antenna_id =
1105 resolve_reference_id(transmitter, "antenna", "transmitter '" + name + "'", *refs.antennas);
1106 const antenna::Antenna* ant = ctx.world->findAntenna(antenna_id);
1107 if (ant == nullptr)
1108 {
1109 throw XmlException("Antenna ID '" + std::to_string(antenna_id) + "' not found for transmitter '" + name +
1110 "'");
1111 }
1112 transmitter_obj->setAntenna(ant);
1113
1114 const SimId timing_id =
1115 resolve_reference_id(transmitter, "timing", "transmitter '" + name + "'", *refs.timings);
1116 transmitter_obj->setTiming(resolve_timing_instance(timing_id, ctx, "transmitter '" + name + "'"));
1117
1118 RealType const pri = is_pulsed ? (1.0 / transmitter_obj->getPrf()) : 0.0;
1120 if (wave->isFmcwFamily() || wave->isSteppedFrequency())
1121 {
1122 validate_fmcw_schedule(schedule, *wave, "Transmitter '" + name + "'");
1123 }
1124 if (!schedule.empty())
1125 {
1126 transmitter_obj->setSchedule(std::move(schedule));
1127 }
1128
1129 ctx.world->add(std::move(transmitter_obj));
1130 return ctx.world->getTransmitters().back().get();
1131 }
1132
1134 const ReferenceLookup& refs)
1135 {
1136 const std::string name = XmlElement::getSafeAttribute(transmitter, "name");
1137 const radar::OperationMode mode = parse_mode_elements(transmitter, "Transmitter '" + name + "'");
1138 reject_non_empty_sfcw_mode(transmitter, "Transmitter '" + name + "'");
1139 if (const XmlElement fmcw_mode = transmitter.childElement("fmcw_mode", 0);
1141 {
1142 throw XmlException("Transmitter '" + name + "' fmcw_mode must not contain dechirp configuration.");
1143 }
1145 }
1146
1147 /// Parses a receiver after its operation mode has already been determined.
1150 const radar::OperationMode mode)
1151 {
1152 const std::string name = XmlElement::getSafeAttribute(receiver, "name");
1153 const SimId id = assign_id_from_attribute("receiver '" + name + "'", ObjectType::Receiver);
1154 const XmlElement pulsed_mode_element = receiver.childElement("pulsed_mode", 0);
1155 const bool is_pulsed = mode == radar::OperationMode::PULSED_MODE;
1156
1157 auto receiver_obj = std::make_unique<radar::Receiver>(platform, name, next_seed(*ctx.master_seeder), mode, id);
1158
1159 const SimId ant_id = resolve_reference_id(receiver, "antenna", "receiver '" + name + "'", *refs.antennas);
1160 const antenna::Antenna* antenna = ctx.world->findAntenna(ant_id);
1161 if (antenna == nullptr)
1162 {
1163 throw XmlException("Antenna ID '" + std::to_string(ant_id) + "' not found for receiver '" + name + "'");
1164 }
1165 receiver_obj->setAntenna(antenna);
1166
1167 if (!receiver.childElement("noise_temp", 0).isValid())
1168 {
1169 LOG(logging::Level::DEBUG, "Receiver '{}' does not specify noise temperature",
1170 receiver_obj->getName().c_str());
1171 }
1172 else
1173 {
1174 try
1175 {
1176 receiver_obj->setNoiseTemperature(get_child_real_type(receiver, "noise_temp"));
1177 }
1178 catch (const XmlException&)
1179 {
1180 LOG(logging::Level::WARNING, "Receiver '{}' has an empty noise temperature; using default.",
1181 receiver_obj->getName().c_str());
1182 }
1183 }
1184
1185 if (is_pulsed)
1186 {
1187 const RealType window_length = get_child_real_type(pulsed_mode_element, "window_length");
1188 if (window_length <= 0)
1189 {
1190 throw XmlException("<window_length> must be positive for receiver '" + name + "'");
1191 }
1192
1194 if (prf <= 0)
1195 {
1196 throw XmlException("<prf> must be positive for receiver '" + name + "'");
1197 }
1198
1199 const RealType window_skip = get_child_real_type(pulsed_mode_element, "window_skip");
1200 if (window_skip < 0)
1201 {
1202 throw XmlException("<window_skip> must not be negative for receiver '" + name + "'");
1203 }
1204 receiver_obj->setWindowProperties(window_length, prf, window_skip);
1205 }
1206 const SimId timing_id = resolve_reference_id(receiver, "timing", "receiver '" + name + "'", *refs.timings);
1207 receiver_obj->setTiming(resolve_timing_instance(timing_id, ctx, "receiver '" + name + "'"));
1208
1209 if (get_attribute_bool(receiver, "nodirect", false))
1210 {
1212 LOG(logging::Level::DEBUG, "Ignoring direct signals for receiver '{}'", receiver_obj->getName().c_str());
1213 }
1214
1215 if (get_attribute_bool(receiver, "nopropagationloss", false))
1216 {
1218 LOG(logging::Level::DEBUG, "Ignoring propagation losses for receiver '{}'",
1219 receiver_obj->getName().c_str());
1220 }
1221
1222 RealType const pri = is_pulsed ? (1.0 / receiver_obj->getWindowPrf()) : 0.0;
1224 if (!schedule.empty())
1225 {
1226 receiver_obj->setSchedule(std::move(schedule));
1227 }
1228
1229 parse_receiver_dechirp_config(receiver, *receiver_obj, "Receiver '" + name + "'");
1230
1231 ctx.world->add(std::move(receiver_obj));
1232 return ctx.world->getReceivers().back().get();
1233 }
1234
1236 const ReferenceLookup& refs)
1237 {
1238 const std::string name = XmlElement::getSafeAttribute(receiver, "name");
1239 const radar::OperationMode mode = parse_mode_elements(receiver, "Receiver '" + name + "'");
1240 reject_non_empty_sfcw_mode(receiver, "Receiver '" + name + "'");
1242 }
1243
1245 const ReferenceLookup& refs)
1246 {
1247 const std::string name = XmlElement::getSafeAttribute(monostatic, "name");
1248 const radar::OperationMode monostatic_mode = parse_mode_elements(monostatic, "Monostatic '" + name + "'");
1249 reject_non_empty_sfcw_mode(monostatic, "Monostatic '" + name + "'");
1252 if (trans->getMode() != monostatic_mode || recv->getMode() != monostatic_mode)
1253 {
1254 throw XmlException("Monostatic '" + name + "' parsed inconsistent transmitter/receiver modes.");
1255 }
1256 if (trans->getSignal() != nullptr)
1257 {
1258 validate_waveform_mode_match(*trans->getSignal(), trans->getMode(),
1259 "Monostatic '" + trans->getName() + "'");
1260 }
1261 trans->setAttached(recv);
1262 recv->setAttached(trans);
1263 }
1264
1266 {
1267 const std::string name = XmlElement::getSafeAttribute(target, "name");
1268 const SimId id = assign_id_from_attribute("target '" + name + "'", ObjectType::Target);
1269
1270 const XmlElement rcs_element = target.childElement("rcs", 0);
1271 if (!rcs_element.isValid())
1272 {
1273 throw XmlException("<rcs> element is required in <target>!");
1274 }
1275
1276 const std::string rcs_type = XmlElement::getSafeAttribute(rcs_element, "type");
1277 std::unique_ptr<radar::Target> target_obj;
1278 const unsigned seed = next_seed(*ctx.master_seeder);
1279
1280 if (rcs_type == "isotropic")
1281 {
1283 }
1284 else if (rcs_type == "file")
1285 {
1286 // Defer to dependency-injected file loader
1287 target_obj = ctx.loaders.loadFileTarget(platform, name,
1289 }
1290 else
1291 {
1292 throw XmlException("Unsupported RCS type: " + rcs_type);
1293 }
1294
1295 if (const XmlElement model = target.childElement("model", 0); model.isValid())
1296 {
1297 if (const std::string model_type = XmlElement::getSafeAttribute(model, "type"); model_type == "constant")
1298 {
1299 target_obj->setFluctuationModel(std::make_unique<radar::RcsConst>());
1300 }
1301 else if (model_type == "chisquare" || model_type == "gamma")
1302 {
1303 target_obj->setFluctuationModel(
1304 std::make_unique<radar::RcsChiSquare>(target_obj->getRngEngine(), get_child_real_type(model, "k")));
1305 }
1306 else
1307 {
1308 throw XmlException("Unsupported model type: " + model_type);
1309 }
1310 }
1311
1312 LOG(logging::Level::DEBUG, "Added target {} with RCS type {} to platform {}", name, rcs_type,
1313 platform->getName());
1314 ctx.world->add(std::move(target_obj));
1315 }
1316
1318 const std::function<void(const XmlElement&, std::string_view)>& register_name,
1319 const ReferenceLookup& refs)
1320 {
1321 auto parseChildrenWithRefs = [&](const std::string& elementName, auto parseFunc)
1322 {
1323 unsigned index = 0;
1324 while (true)
1325 {
1326 const XmlElement element = platform.childElement(elementName, index++);
1327 if (!element.isValid())
1328 break;
1331 }
1332 };
1333
1334 auto parseChildrenWithoutRefs = [&](const std::string& elementName, auto parseFunc)
1335 {
1336 unsigned index = 0;
1337 while (true)
1338 {
1339 const XmlElement element = platform.childElement(elementName, index++);
1340 if (!element.isValid())
1341 break;
1344 }
1345 };
1346
1351 }
1352
1354 const std::function<void(const XmlElement&, std::string_view)>& register_name,
1355 const ReferenceLookup& refs)
1356 {
1357 std::string const name = XmlElement::getSafeAttribute(platform, "name");
1358 const SimId id = assign_id_from_attribute("platform '" + name + "'", ObjectType::Platform);
1359 auto plat = std::make_unique<radar::Platform>(name, id);
1360
1362
1363 if (const XmlElement motion_path = platform.childElement("motionpath", 0); motion_path.isValid())
1364 {
1366 }
1367
1368 const XmlElement rot_path = platform.childElement("rotationpath", 0);
1369 if (const XmlElement fixed_rot = platform.childElement("fixedrotation", 0);
1370 rot_path.isValid() && fixed_rot.isValid())
1371 {
1373 "Both <rotationpath> and <fixedrotation> are declared for platform {}. Only <rotationpath> will be "
1374 "used.",
1375 plat->getName());
1376 parseRotationPath(rot_path, plat.get(), ctx.parameters.rotation_angle_unit);
1377 }
1378 else if (rot_path.isValid())
1379 {
1380 parseRotationPath(rot_path, plat.get(), ctx.parameters.rotation_angle_unit);
1381 }
1382 else if (fixed_rot.isValid())
1383 {
1384 parseFixedRotation(fixed_rot, plat.get(), ctx.parameters.rotation_angle_unit);
1385 }
1386
1387 ctx.world->add(std::move(plat));
1388 }
1389
1390 void collectIncludeElements(const XmlDocument& doc, const fs::path& currentDir, std::vector<fs::path>& includePaths)
1391 {
1392 unsigned index = 0;
1393 while (true)
1394 {
1395 XmlElement const include_element = doc.getRootElement().childElement("include", index++);
1396 if (!include_element.isValid())
1397 break;
1398
1399 std::string const include_filename = include_element.getText();
1400 if (include_filename.empty())
1401 {
1402 LOG(logging::Level::ERROR, "<include> element is missing the filename!");
1403 continue;
1404 }
1405
1406 fs::path const include_path = currentDir / include_filename;
1407 includePaths.push_back(include_path);
1408
1410 if (!included_doc.loadFile(include_path.string()))
1411 {
1412 LOG(logging::Level::ERROR, "Failed to load included XML file: {}", include_path.string());
1413 continue;
1414 }
1415
1417 }
1418 }
1419
1421 {
1422 std::vector<fs::path> include_paths;
1424 bool did_combine = false;
1425
1426 for (const auto& include_path : include_paths)
1427 {
1429 if (!included_doc.loadFile(include_path.string()))
1430 {
1431 throw XmlException("Failed to load included XML file: " + include_path.string());
1432 }
1433
1435 did_combine = true;
1436 }
1437
1439 return did_combine;
1440 }
1441
1443 {
1444 LOG(logging::Level::DEBUG, "Validating the{}XML file...", didCombine ? " combined " : " ");
1445 if (!mainDoc.validateWithDtd(fers_xml_dtd))
1446 {
1447 LOG(logging::Level::FATAL, "{} XML file failed DTD validation!", didCombine ? "Combined" : "Main");
1448 throw XmlException("XML file failed DTD validation!");
1449 }
1450 LOG(logging::Level::DEBUG, "{} XML file passed DTD validation.", didCombine ? "Combined" : "Main");
1451
1452 if (!mainDoc.validateWithXsd(fers_xml_xsd))
1453 {
1454 LOG(logging::Level::FATAL, "{} XML file failed XSD validation!", didCombine ? "Combined" : "Main");
1455 throw XmlException("XML file failed XSD validation!");
1456 }
1457 LOG(logging::Level::DEBUG, "{} XML file passed XSD validation.", didCombine ? "Combined" : "Main");
1458 }
1459
1461 {
1462 const XmlElement root = doc.getRootElement();
1463 if (root.name() != "simulation")
1464 {
1465 throw XmlException("Root element is not <simulation>!");
1466 }
1467
1468 std::unordered_map<std::string, std::string> name_registry;
1469 name_registry.reserve(64); // TODO: reserve 64?
1470 const auto register_name = [&](const XmlElement& element, const std::string_view kind)
1471 {
1472 const std::string name = XmlElement::getSafeAttribute(element, "name");
1473 const auto [iter, inserted] = name_registry.emplace(name, std::string(kind));
1474 if (!inserted)
1475 {
1476 throw XmlException("Duplicate name '" + name + "' found for " + std::string(kind) +
1477 "; previously used by " + iter->second + ".");
1478 }
1479 };
1480
1481 try
1482 {
1483 ctx.parameters.simulation_name = XmlElement::getSafeAttribute(root, "name");
1484 if (!ctx.parameters.simulation_name.empty())
1485 {
1486 LOG(logging::Level::INFO, "Simulation name set to: {}", ctx.parameters.simulation_name);
1487 }
1488 }
1489 catch (const XmlException&)
1490 {
1491 LOG(logging::Level::WARNING, "No 'name' attribute found in <simulation> tag. KML name will default.");
1492 }
1493
1494 parseParameters(root.childElement("parameters", 0), ctx.parameters);
1495
1496 params::params = ctx.parameters;
1497
1498 auto parseElements =
1499 [](const XmlElement& parent, const std::string& elementName, ParserContext& parser_ctx, auto parseFunction)
1500 {
1501 unsigned index = 0;
1502 while (true)
1503 {
1504 XmlElement const element = parent.childElement(elementName, index++);
1505 if (!element.isValid())
1506 break;
1508 }
1509 };
1510
1511 parseElements(root, "waveform", ctx,
1512 [&](const XmlElement& p, ParserContext& c)
1513 {
1514 register_name(p, "waveform");
1515 parseWaveform(p, c);
1516 });
1517
1518 parseElements(root, "timing", ctx,
1519 [&](const XmlElement& p, ParserContext& c)
1520 {
1521 register_name(p, "timing");
1522 parseTiming(p, c);
1523 });
1524
1525 parseElements(root, "antenna", ctx,
1526 [&](const XmlElement& p, ParserContext& c)
1527 {
1528 register_name(p, "antenna");
1529 parseAntenna(p, c);
1530 });
1531
1532 std::unordered_map<std::string, SimId> waveform_refs;
1533 std::unordered_map<std::string, SimId> antenna_refs;
1534 std::unordered_map<std::string, SimId> timing_refs;
1535 waveform_refs.reserve(ctx.world->getWaveforms().size());
1536 antenna_refs.reserve(ctx.world->getAntennas().size());
1537 timing_refs.reserve(ctx.world->getTimings().size());
1538
1539 for (const auto& [id, waveform] : ctx.world->getWaveforms())
1540 waveform_refs.emplace(waveform->getName(), id);
1541 for (const auto& [id, antenna] : ctx.world->getAntennas())
1542 antenna_refs.emplace(antenna->getName(), id);
1543 for (const auto& [id, timing] : ctx.world->getTimings())
1544 timing_refs.emplace(timing->getName(), id);
1545
1547
1548 parseElements(root, "platform", ctx,
1549 [&](const XmlElement& p, ParserContext& c)
1550 {
1551 register_name(p, "platform");
1553 });
1554
1555 ctx.world->resolveReceiverDechirpReferences();
1556
1557 ctx.world->scheduleInitialEvents();
1558
1559 LOG(logging::Level::DEBUG, "Initial Event Queue State:\n{}", ctx.world->dumpEventQueue());
1560 }
1561
1563 {
1564 return {.loadWaveform = [](const std::string& name, const fs::path& waveform_path, RealType power,
1566 { return serial::loadWaveformFromFile(name, waveform_path.string(), power, carrierFreq, id, kind); },
1567 .loadXmlAntenna = [](const std::string& name, const std::string& filename, SimId id)
1568 { return std::make_unique<antenna::XmlAntenna>(name, filename, id); },
1569 .loadH5Antenna = [](const std::string& name, const std::string& filename, SimId id)
1570 { return std::make_unique<antenna::H5Antenna>(name, filename, id); },
1571 .loadFileTarget = [](radar::Platform* platform, const std::string& name, const std::string& filename,
1572 unsigned seed, SimId id)
1573 { return radar::createFileTarget(platform, name, filename, seed, id); }};
1574 }
1575}
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.
FileWaveformKind
Simulation mode assigned to samples loaded from a waveform file.
@ 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, const FileWaveformKind kind)
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 &waveform_path, RealType power, RealType carrierFreq, SimId id, fers_signal::FileWaveformKind kind)> loadWaveform
Hook to load a mode-tagged 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.