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>, or "
62 "<fmcw_mode>).");
63 }
64 selected_mode = mode;
65 };
66
70 if (!selected_mode.has_value())
71 {
72 throw XmlException(owner +
73 " must specify exactly one radar mode (<pulsed_mode>, <cw_mode>, or <fmcw_mode>).");
74 }
75 return *selected_mode;
76 }
77
78 /// Returns true when an FMCW mode block carries receiver-side FMCW fields.
80 {
81 return XmlElement::getOptionalAttribute(fmcw_mode, "dechirp_mode").has_value() ||
82 fmcw_mode.childElement("dechirp_reference", 0).isValid() ||
83 fmcw_mode.childElement("if_sample_rate", 0).isValid() ||
84 fmcw_mode.childElement("if_filter_bandwidth", 0).isValid() ||
85 fmcw_mode.childElement("if_filter_transition_width", 0).isValid();
86 }
87
88 /// Parses an optional positive scalar child under <fmcw_mode>.
89 std::optional<RealType> parse_optional_fmcw_if_child(const XmlElement& fmcw_mode, const std::string& child_name,
90 const std::string& owner)
91 {
92 const XmlElement element = fmcw_mode.childElement(child_name, 0);
93 if (!element.isValid())
94 {
95 return std::nullopt;
96 }
97 if (fmcw_mode.childElement(child_name, 1).isValid())
98 {
99 throw XmlException(owner + " must declare at most one <" + child_name + ">.");
100 }
101 const std::string text = element.getText();
102 if (text.empty())
103 {
104 throw XmlException("Element " + child_name + " is empty!");
105 }
106 const RealType value = std::stod(text);
107 if (value <= 0.0 || !std::isfinite(value))
108 {
109 throw XmlException(owner + " <" + child_name + "> must be a finite positive value.");
110 }
111 return value;
112 }
113
115 {
116 return if_chain.sample_rate_hz.has_value() || if_chain.filter_bandwidth_hz.has_value() ||
117 if_chain.filter_transition_width_hz.has_value();
118 }
119
122 const std::string& owner)
123 {
124 if (ref_element.isValid())
125 {
126 throw XmlException(owner + " declares <dechirp_reference> while dechirp_mode is 'none'.");
127 }
129 {
130 throw XmlException(owner + " declares IF-chain fields while dechirp_mode is 'none'.");
131 }
132 }
133
135 {
136 if ((if_chain.filter_bandwidth_hz.has_value() || if_chain.filter_transition_width_hz.has_value()) &&
137 !if_chain.sample_rate_hz.has_value())
138 {
139 throw XmlException(owner + " IF filter fields require <if_sample_rate>.");
140 }
141 if (if_chain.sample_rate_hz.has_value())
142 {
144 if (*if_chain.sample_rate_hz > sim_rate)
145 {
146 throw XmlException(owner + " <if_sample_rate> must not exceed the simulation sample rate.");
147 }
148 }
149 if (if_chain.sample_rate_hz.has_value() && if_chain.filter_bandwidth_hz.has_value() &&
150 *if_chain.filter_bandwidth_hz >= *if_chain.sample_rate_hz / 2.0)
151 {
152 throw XmlException(owner + " <if_filter_bandwidth> must be less than half <if_sample_rate>.");
153 }
154 }
155
157 parse_dechirp_reference(const XmlElement& fmcw_mode, const XmlElement& ref_element, const std::string& owner)
158 {
159 if (!ref_element.isValid())
160 {
161 throw XmlException(owner + " enables dechirping but does not declare <dechirp_reference>.");
162 }
163 if (fmcw_mode.childElement("dechirp_reference", 1).isValid())
164 {
165 throw XmlException(owner + " must declare at most one <dechirp_reference>.");
166 }
167
169 try
170 {
171 reference.source =
173 }
174 catch (const std::exception& e)
175 {
176 throw XmlException(owner + " has invalid dechirp_reference. " + e.what());
177 }
178
179 const auto transmitter_name = XmlElement::getOptionalAttribute(ref_element, "transmitter_name");
180 const auto waveform_name = XmlElement::getOptionalAttribute(ref_element, "waveform_name");
181 switch (reference.source)
182 {
184 if (transmitter_name.has_value() || waveform_name.has_value())
185 {
186 throw XmlException(owner +
187 " attached dechirp_reference must not set transmitter_name or "
188 "waveform_name.");
189 }
190 break;
192 if (!transmitter_name.has_value() || transmitter_name->empty() || waveform_name.has_value())
193 {
194 throw XmlException(owner + " transmitter dechirp_reference requires transmitter_name only.");
195 }
196 reference.name = *transmitter_name;
197 break;
199 if (!waveform_name.has_value() || waveform_name->empty() || transmitter_name.has_value())
200 {
201 throw XmlException(owner + " custom dechirp_reference requires waveform_name only.");
202 }
203 reference.name = *waveform_name;
204 break;
206 throw XmlException(owner + " dechirp_reference source must be attached, transmitter, or custom.");
207 }
208
209 return reference;
210 }
211
212 /// Parses receiver-side dechirp settings from an FMCW mode block.
214 const std::string& owner)
215 {
217 {
219 return;
220 }
221
222 const XmlElement fmcw_mode = parent.childElement("fmcw_mode", 0);
223 if (!fmcw_mode.isValid())
224 {
226 return;
227 }
228
230 if (const auto mode_attr = XmlElement::getOptionalAttribute(fmcw_mode, "dechirp_mode"))
231 {
232 try
233 {
235 }
236 catch (const std::exception& e)
237 {
238 throw XmlException(owner + " has invalid dechirp_mode. " + e.what());
239 }
240 }
241
242 const XmlElement ref_element = fmcw_mode.childElement("dechirp_reference", 0);
244 .sample_rate_hz = parse_optional_fmcw_if_child(fmcw_mode, "if_sample_rate", owner),
245 .filter_bandwidth_hz = parse_optional_fmcw_if_child(fmcw_mode, "if_filter_bandwidth", owner),
246 .filter_transition_width_hz =
247 parse_optional_fmcw_if_child(fmcw_mode, "if_filter_transition_width", owner)};
249 {
251 receiver.setDechirpMode(mode);
252 return;
253 }
254
257
258 receiver.setDechirpMode(mode);
259 receiver.setDechirpReference(std::move(reference));
260 receiver.setFmcwIfChainRequest(if_chain);
261 }
262
263 /// Throws an XML validation exception with the provided message.
264 void throw_xml_validation_error(const std::string& message) { throw XmlException(message); }
265
266 /// Validates an FMCW waveform while adapting validation errors to XmlException.
267 void validate_fmcw_waveform(const fers_signal::RadarSignal& wave, const std::string& owner)
268 {
270 }
271
272 /// Validates waveform/mode compatibility while adapting validation errors to XmlException.
274 const std::string& owner)
275 {
277 }
278
279 /// Validates an FMCW schedule while adapting validation errors to XmlException.
280 void validate_fmcw_schedule(const std::vector<radar::SchedulePeriod>& schedule,
281 const fers_signal::RadarSignal& wave, const std::string& owner)
282 {
284 }
285
286 /// Draws the next unsigned seed from the master random generator.
287 [[nodiscard]] unsigned next_seed(std::mt19937& master_seeder)
288 {
289 static_assert(std::mt19937::max() <= std::numeric_limits<unsigned>::max(),
290 "std::mt19937 output must fit into unsigned seeds.");
291 return static_cast<unsigned>(master_seeder());
292 }
293
294 /// Resolves or instantiates a shared timing instance by prototype SimId.
295 std::shared_ptr<timing::Timing> resolve_timing_instance(const SimId timing_id, ParserContext& ctx,
296 const std::string& owner)
297 {
298 if (const auto it = ctx.timing_instances.find(timing_id); it != ctx.timing_instances.end())
299 {
300 return it->second;
301 }
302
303 const timing::PrototypeTiming* proto = ctx.world->findTiming(timing_id);
304 if (proto == nullptr)
305 {
306 throw XmlException("Timing ID '" + std::to_string(timing_id) + "' not found for " + owner + "'");
307 }
308
309 auto timing_obj =
310 std::make_shared<timing::Timing>(proto->getName(), next_seed(*ctx.master_seeder), proto->getId());
311 timing_obj->initializeModel(proto);
312 ctx.timing_instances.emplace(timing_id, timing_obj);
313 return timing_obj;
314 }
315 }
316
318 {
319 const std::string text = element.childElement(elementName, 0).getText();
320 if (text.empty())
321 {
322 throw XmlException("Element " + elementName + " is empty!");
323 }
324 return std::stod(text);
325 }
326
327 bool get_attribute_bool(const XmlElement& element, const std::string& attributeName, const bool defaultVal)
328 {
330 if (!attr_value.has_value())
331 {
332 LOG(logging::Level::DEBUG, "Attribute '{}' not specified. Defaulting to {}.", attributeName, defaultVal);
333 return defaultVal;
334 }
335 if (*attr_value == "true")
336 {
337 return true;
338 }
339 if (*attr_value == "false")
340 {
341 return false;
342 }
343
344 LOG(logging::Level::WARNING, "Invalid boolean value '{}' for attribute '{}'. Defaulting to {}.", *attr_value,
346 return defaultVal;
347 }
348
350 {
351 const SimId id = SimIdGenerator::instance().generateId(type);
352 LOG(logging::Level::TRACE, "Assigned ID {} to {} (generated)", id, owner);
353 return id;
354 }
355
356 SimId resolve_reference_id(const XmlElement& element, const std::string& attributeName, const std::string& owner,
357 const std::unordered_map<std::string, SimId>& name_map)
358 {
359 const std::string value = XmlElement::getSafeAttribute(element, attributeName);
360 if (value.empty())
361 {
362 throw XmlException("Missing " + attributeName + " for " + owner + ".");
363 }
364 const auto it = name_map.find(value);
365 if (it != name_map.end())
366 {
367 return it->second;
368 }
369 throw XmlException("Unknown " + attributeName + " '" + value + "' for " + owner + ".");
370 }
371
372 std::vector<radar::SchedulePeriod> parseSchedule(const XmlElement& parent, const std::string& parentName,
373 const bool isPulsed, const RealType pri)
374 {
375 std::vector<radar::SchedulePeriod> raw_periods;
376 if (const XmlElement schedule_element = parent.childElement("schedule", 0); schedule_element.isValid())
377 {
378 unsigned p_idx = 0;
379 while (true)
380 {
381 XmlElement const period_element = schedule_element.childElement("period", p_idx++);
382 if (!period_element.isValid())
383 {
384 break;
385 }
386 try
387 {
388 const RealType start = std::stod(XmlElement::getSafeAttribute(period_element, "start"));
389 const RealType end = std::stod(XmlElement::getSafeAttribute(period_element, "end"));
390 raw_periods.push_back({start, end});
391 }
392 catch (const std::exception& e)
393 {
394 LOG(logging::Level::WARNING, "Failed to parse schedule period for '{}': {}", parentName, e.what());
395 }
396 }
397 }
399 }
400
401 unsigned parseUnsignedParameter(const std::string_view param_name, const RealType raw_value)
402 {
403 if (!std::isfinite(raw_value))
404 {
405 throw XmlException(std::format("Parameter '{}' must be finite.", param_name));
406 }
407 if (raw_value < 0.0)
408 {
409 throw XmlException(std::format("Parameter '{}' must be non-negative.", param_name));
410 }
411
412 const RealType floored_value = std::floor(raw_value);
413 if (floored_value > static_cast<RealType>(std::numeric_limits<unsigned>::max()))
414 {
415 throw XmlException(std::format("Parameter '{}' exceeds the supported unsigned range.", param_name));
416 }
417
418 return static_cast<unsigned>(floored_value);
419 }
420
421 template <typename Setter>
422 void setOptionalRealParameter(const XmlElement& parameters, const std::string& param_name,
424 {
425 if (!parameters.childElement(param_name, 0).isValid())
426 {
427 LOG(logging::Level::DEBUG, "Parameter '{}' not specified. Using default value {}.", param_name,
429 return;
430 }
431
433 }
434
435 template <typename Setter>
436 void setOptionalUnsignedParameter(const XmlElement& parameters, const std::string& param_name,
437 const unsigned default_value, Setter setter)
438 {
439 if (!parameters.childElement(param_name, 0).isValid())
440 {
441 LOG(logging::Level::DEBUG, "Parameter '{}' not specified. Using default value {}.", param_name,
443 return;
444 }
445
447 }
448
450 {
452 [&](const RealType value)
453 {
454 params_out.c = value;
455 LOG(logging::Level::INFO, "Propagation speed (c) set to: {:.5f}", value);
456 });
457
458 setOptionalRealParameter(parameters, "simSamplingRate", 1000.0,
459 [&](const RealType value)
460 {
461 params_out.sim_sampling_rate = value;
462 LOG(logging::Level::DEBUG, "Simulation sampling rate set to: {:.5f} Hz", value);
463 });
464
465 if (parameters.childElement("randomseed", 0).isValid())
466 {
467 const auto seed = parseUnsignedParameter("randomseed", get_child_real_type(parameters, "randomseed"));
468 params_out.random_seed = seed;
469 LOG(logging::Level::DEBUG, "Random seed set to: {}", seed);
470 }
471
472 setOptionalUnsignedParameter(parameters, "adc_bits", 0,
473 [&](const unsigned value)
474 {
475 params_out.adc_bits = value;
476 LOG(logging::Level::DEBUG, "ADC quantization bits set to: {}", value);
477 });
478
479 setOptionalUnsignedParameter(parameters, "oversample", 1,
480 [&](const unsigned value)
481 {
483 params_out.oversample_ratio = value;
484 LOG(logging::Level::DEBUG, "Oversampling enabled with ratio: {}", value);
485 });
486 }
487
489 {
490 try
491 {
492 const auto unit_token = parameters.childElement("rotationangleunit", 0).getText();
493 if (!unit_token.empty())
494 {
495 if (const auto unit = params::rotationAngleUnitFromToken(unit_token))
496 {
497 params_out.rotation_angle_unit = *unit;
498 }
499 else
500 {
501 throw XmlException("Unsupported rotation angle unit '" + unit_token + "'.");
502 }
503 }
504 }
505 catch (const XmlException&)
506 {
507 }
508 }
509
511 {
512 bool origin_set = false;
513 if (const XmlElement origin_element = parameters.childElement("origin", 0); origin_element.isValid())
514 {
515 try
516 {
517 params_out.origin_latitude = std::stod(XmlElement::getSafeAttribute(origin_element, "latitude"));
518 params_out.origin_longitude = std::stod(XmlElement::getSafeAttribute(origin_element, "longitude"));
519 if (const auto altitude = XmlElement::getOptionalAttribute(origin_element, "altitude"))
520 {
521 params_out.origin_altitude = std::stod(*altitude);
522 }
523 else
524 {
525 params_out.origin_altitude = 0.0;
526 LOG(logging::Level::DEBUG, "KML origin altitude not specified. Defaulting to 0.");
527 }
528 origin_set = true;
529 LOG(logging::Level::INFO, "KML origin set to lat: {}, lon: {}, alt: {}", params_out.origin_latitude,
530 params_out.origin_longitude, params_out.origin_altitude);
531 }
532 catch (const std::exception& e)
533 {
534 LOG(logging::Level::WARNING, "Could not parse KML origin from XML, using defaults. Error: {}",
535 e.what());
536 }
537 }
538 return origin_set;
539 }
540
542 {
543 params_out.coordinate_frame = params::CoordinateFrame::UTM;
544 params_out.utm_zone = std::stoi(XmlElement::getSafeAttribute(cs_element, "zone"));
545 const std::string hem_str = XmlElement::getSafeAttribute(cs_element, "hemisphere");
546
547 if (params_out.utm_zone < GeographicLib::UTMUPS::MINUTMZONE ||
548 params_out.utm_zone > GeographicLib::UTMUPS::MAXUTMZONE)
549 {
550 throw XmlException("KML UTM zone " + std::to_string(params_out.utm_zone) +
551 " is invalid; must be in [1, 60].");
552 }
553 if (hem_str == "N" || hem_str == "n")
554 {
555 params_out.utm_north_hemisphere = true;
556 }
557 else if (hem_str == "S" || hem_str == "s")
558 {
559 params_out.utm_north_hemisphere = false;
560 }
561 else
562 {
563 throw XmlException("KML UTM hemisphere '" + hem_str + "' is invalid; must be 'N' or 'S'.");
564 }
565 LOG(logging::Level::INFO, "KML coordinate system set to UTM, zone {}{}", params_out.utm_zone,
566 params_out.utm_north_hemisphere ? 'N' : 'S');
567 }
568
570 const bool origin_set)
571 {
572 if (const XmlElement cs_element = parameters.childElement("coordinatesystem", 0); cs_element.isValid())
573 {
574 try
575 {
576 const std::string frame_str = XmlElement::getSafeAttribute(cs_element, "frame");
577 if (frame_str == "UTM")
578 {
580 }
581 else if (frame_str == "ECEF")
582 {
583 params_out.coordinate_frame = params::CoordinateFrame::ECEF;
584 LOG(logging::Level::INFO, "KML coordinate system set to ECEF.");
585 }
586 else if (frame_str == "ENU")
587 {
588 params_out.coordinate_frame = params::CoordinateFrame::ENU;
589 if (!origin_set)
590 {
592 "ENU KML frame specified but no <origin> tag found. Using default KML origin at UCT.");
593 }
594 LOG(logging::Level::INFO, "KML coordinate system set to ENU local tangent plane.");
595 }
596 else
597 {
598 throw XmlException("Unsupported KML coordinate frame: " + frame_str);
599 }
600 }
601 catch (const std::exception& e)
602 {
604 "Could not parse KML <coordinatesystem> from XML: {}. Defaulting KML export to ENU.", e.what());
605 params_out.coordinate_frame = params::CoordinateFrame::ENU;
606 params_out.utm_zone = 0;
607 params_out.utm_north_hemisphere = true;
608 }
609 }
610 }
611
613 {
614 params_out.start = get_child_real_type(parameters, "starttime");
615 params_out.end = get_child_real_type(parameters, "endtime");
616 LOG(logging::Level::INFO, "Simulation time set from {:.5f} to {:.5f} seconds", params_out.start,
617 params_out.end);
618
619 params_out.rate = get_child_real_type(parameters, "rate");
620 if (params_out.rate <= 0)
621 {
622 throw std::runtime_error("Sampling rate must be > 0");
623 }
624 LOG(logging::Level::DEBUG, "Sample rate set to: {:.5f}", params_out.rate);
625
628 const bool origin_set = parseOriginParameter(parameters, params_out);
630 }
631
633 {
634 const std::string name = XmlElement::getSafeAttribute(waveform, "name");
635 const SimId id = assign_id_from_attribute("waveform '" + name + "'", ObjectType::Waveform);
636
637 const auto power = get_child_real_type(waveform, "power");
638 const auto carrier = get_child_real_type(waveform, "carrier_frequency");
639
640 if (const XmlElement pulsed_file = waveform.childElement("pulsed_from_file", 0); pulsed_file.isValid())
641 {
642 const std::string filename_str = XmlElement::getSafeAttribute(pulsed_file, "filename");
643 fs::path pulse_path(filename_str);
644
645 if (!fs::exists(pulse_path))
646 {
647 pulse_path = ctx.base_dir / filename_str;
648 }
649
650 // Defer to dependency-injected file loader
651 auto wave = ctx.loaders.loadWaveform(name, pulse_path, power, carrier, id);
652 ctx.world->add(std::move(wave));
653 }
654 else if (waveform.childElement("cw", 0).isValid())
655 {
656 auto cw_signal = std::make_unique<fers_signal::CwSignal>();
657 auto wave = std::make_unique<fers_signal::RadarSignal>(
658 name, power, carrier, ctx.parameters.end - ctx.parameters.start, std::move(cw_signal), id);
659 ctx.world->add(std::move(wave));
660 }
661 else if (const XmlElement fmcw_element = waveform.childElement("fmcw_linear_chirp", 0); fmcw_element.isValid())
662 {
663 const auto direction =
665 const RealType chirp_bandwidth = get_child_real_type(fmcw_element, "chirp_bandwidth");
666 const RealType chirp_duration = get_child_real_type(fmcw_element, "chirp_duration");
667 const RealType chirp_period = get_child_real_type(fmcw_element, "chirp_period");
668
669 RealType start_frequency_offset = 0.0;
670 if (const auto start_offset = fmcw_element.childElement("start_frequency_offset", 0);
671 start_offset.isValid())
672 {
673 start_frequency_offset = get_child_real_type(fmcw_element, "start_frequency_offset");
674 }
675
676 std::optional<std::size_t> chirp_count;
677 if (const auto chirp_count_element = fmcw_element.childElement("chirp_count", 0);
678 chirp_count_element.isValid())
679 {
680 const RealType raw_count = get_child_real_type(fmcw_element, "chirp_count");
681 if (raw_count <= 0.0 || std::floor(raw_count) != raw_count)
682 {
683 throw XmlException("Waveform '" + name + "' has an invalid chirp_count.");
684 }
685 chirp_count = static_cast<std::size_t>(raw_count);
686 }
687
688 auto fmcw_signal = std::make_unique<fers_signal::FmcwChirpSignal>(
689 chirp_bandwidth, chirp_duration, chirp_period, start_frequency_offset, chirp_count, direction);
690 // RadarSignal length is the active chirp duration, not T_rep. The repeat period only spaces chirps.
691 auto wave = std::make_unique<fers_signal::RadarSignal>(name, power, carrier, chirp_duration,
692 std::move(fmcw_signal), id);
693 validate_fmcw_waveform(*wave, "Waveform '" + name + "'");
694 ctx.world->add(std::move(wave));
695 }
696 else if (const XmlElement fmcw_triangle_element = waveform.childElement("fmcw_triangle", 0);
697 fmcw_triangle_element.isValid())
698 {
699 const RealType chirp_bandwidth = get_child_real_type(fmcw_triangle_element, "chirp_bandwidth");
700 const RealType chirp_duration = get_child_real_type(fmcw_triangle_element, "chirp_duration");
701
702 RealType start_frequency_offset = 0.0;
703 if (const auto start_offset = fmcw_triangle_element.childElement("start_frequency_offset", 0);
704 start_offset.isValid())
705 {
706 start_frequency_offset = get_child_real_type(fmcw_triangle_element, "start_frequency_offset");
707 }
708
709 std::optional<std::size_t> triangle_count;
710 if (const auto triangle_count_element = fmcw_triangle_element.childElement("triangle_count", 0);
711 triangle_count_element.isValid())
712 {
714 if (raw_count <= 0.0 || std::floor(raw_count) != raw_count)
715 {
716 throw XmlException("Waveform '" + name + "' has an invalid triangle_count.");
717 }
718 triangle_count = static_cast<std::size_t>(raw_count);
719 }
720
721 auto fmcw_signal = std::make_unique<fers_signal::FmcwTriangleSignal>(
722 chirp_bandwidth, chirp_duration, start_frequency_offset, triangle_count);
723 auto wave = std::make_unique<fers_signal::RadarSignal>(
724 name, power, carrier, fmcw_signal->getTrianglePeriod(), std::move(fmcw_signal), id);
725 validate_fmcw_waveform(*wave, "Waveform '" + name + "'");
726 ctx.world->add(std::move(wave));
727 }
728 else
729 {
730 LOG(logging::Level::FATAL, "Unsupported waveform type for '{}'", name);
731 throw XmlException("Unsupported waveform type for '" + name + "'");
732 }
733 }
734
736 {
737 const std::string name = XmlElement::getSafeAttribute(timing, "name");
738 const SimId id = assign_id_from_attribute("timing '" + name + "'", ObjectType::Timing);
739 const RealType freq = get_child_real_type(timing, "frequency");
740 auto timing_obj = std::make_unique<timing::PrototypeTiming>(name, id);
741
742 timing_obj->setFrequency(freq);
743
744 unsigned noise_index = 0;
745 while (true)
746 {
747 XmlElement const noise_element = timing.childElement("noise_entry", noise_index++);
748 if (!noise_element.isValid())
749 {
750 break;
751 }
752
753 timing_obj->setAlpha(get_child_real_type(noise_element, "alpha"),
755 }
756
758 [&](const std::string& element_name, const std::string& description, auto setter)
759 {
760 if (!timing.childElement(element_name, 0).isValid())
761 {
762 LOG(logging::Level::DEBUG, "Clock section '{}' does not specify {}.", name, description);
763 return;
764 }
765 try
766 {
768 }
769 catch (const XmlException&)
770 {
771 LOG(logging::Level::WARNING, "Clock section '{}' has an empty {}. Using default.", name, description);
772 }
773 };
774
775 set_optional_timing_parameter("freq_offset", "frequency offset",
776 [&](const RealType value) { timing_obj->setFreqOffset(value); });
777 set_optional_timing_parameter("random_freq_offset_stdev", "random frequency offset",
778 [&](const RealType value) { timing_obj->setRandomFreqOffsetStdev(value); });
779 set_optional_timing_parameter("phase_offset", "phase offset",
780 [&](const RealType value) { timing_obj->setPhaseOffset(value); });
781 set_optional_timing_parameter("random_phase_offset_stdev", "random phase offset",
782 [&](const RealType value) { timing_obj->setRandomPhaseOffsetStdev(value); });
783
784 if (get_attribute_bool(timing, "synconpulse", false))
785 {
786 timing_obj->setSyncOnPulse();
787 }
788
789 ctx.world->add(std::move(timing_obj));
790 }
791
793 {
794 const std::string name = XmlElement::getSafeAttribute(antenna, "name");
795 const SimId id = assign_id_from_attribute("antenna '" + name + "'", ObjectType::Antenna);
796 const std::string pattern = XmlElement::getSafeAttribute(antenna, "pattern");
797
798 std::unique_ptr<antenna::Antenna> ant;
799
800 LOG(logging::Level::DEBUG, "Adding antenna '{}' with pattern '{}'", name, pattern);
801 if (pattern == "isotropic")
802 {
803 ant = std::make_unique<antenna::Isotropic>(name, id);
804 }
805 else if (pattern == "sinc")
806 {
807 ant = std::make_unique<antenna::Sinc>(name, get_child_real_type(antenna, "alpha"),
809 get_child_real_type(antenna, "gamma"), id);
810 }
811 else if (pattern == "gaussian")
812 {
813 ant = std::make_unique<antenna::Gaussian>(name, get_child_real_type(antenna, "azscale"),
814 get_child_real_type(antenna, "elscale"), id);
815 }
816 else if (pattern == "squarehorn")
817 {
818 ant = std::make_unique<antenna::SquareHorn>(name, get_child_real_type(antenna, "diameter"), id);
819 }
820 else if (pattern == "parabolic")
821 {
822 ant = std::make_unique<antenna::Parabolic>(name, get_child_real_type(antenna, "diameter"), id);
823 }
824 else if (pattern == "xml")
825 {
826 ant = ctx.loaders.loadXmlAntenna(name, XmlElement::getSafeAttribute(antenna, "filename"), id);
827 }
828 else if (pattern == "file")
829 {
830 ant = ctx.loaders.loadH5Antenna(name, XmlElement::getSafeAttribute(antenna, "filename"), id);
831 }
832 else
833 {
834 LOG(logging::Level::FATAL, "Unsupported antenna pattern: {}", pattern);
835 throw XmlException("Unsupported antenna pattern: " + pattern);
836 }
837
838 if (!antenna.childElement("efficiency", 0).isValid())
839 {
840 LOG(logging::Level::DEBUG, "Antenna '{}' does not specify efficiency, assuming unity.", name);
841 }
842 else
843 {
844 try
845 {
846 ant->setEfficiencyFactor(get_child_real_type(antenna, "efficiency"));
847 }
848 catch (const XmlException&)
849 {
850 LOG(logging::Level::WARNING, "Antenna '{}' has an empty efficiency, assuming unity.", name);
851 }
852 }
853
854 ctx.world->add(std::move(ant));
855 }
856
858 {
859 math::Path* path = platform->getMotionPath();
860 if (const auto interp_value = XmlElement::getOptionalAttribute(motionPath, "interpolation"))
861 {
862 if (*interp_value == "linear")
863 {
865 }
866 else if (*interp_value == "cubic")
867 {
869 }
870 else if (*interp_value == "static")
871 {
873 }
874 else
875 {
876 LOG(logging::Level::ERROR, "Unsupported interpolation type: {} for platform {}. Defaulting to static",
877 *interp_value, platform->getName());
879 }
880 }
881 else
882 {
884 "MotionPath interpolation type for platform {} not specified. Defaulting to static.",
885 platform->getName());
887 }
888
889 unsigned waypoint_index = 0;
890 while (true)
891 {
892 XmlElement const waypoint = motionPath.childElement("positionwaypoint", waypoint_index);
893 if (!waypoint.isValid())
894 {
895 break;
896 }
897
898 try
899 {
903 get_child_real_type(waypoint, "altitude"));
904 path->addCoord(coord);
905 LOG(logging::Level::TRACE, "Added waypoint {} to motion path for platform {}.", waypoint_index,
906 platform->getName());
907 }
908 catch (const XmlException& e)
909 {
910 LOG(logging::Level::ERROR, "Failed to add waypoint to motion path. Discarding waypoint. {}", e.what());
911 }
912
914 }
915 path->finalize();
916 }
917
919 {
920 math::RotationPath* path = platform->getRotationPath();
921 try
922 {
923 if (const std::string interp = XmlElement::getSafeAttribute(rotation, "interpolation"); interp == "linear")
924 {
926 }
927 else if (interp == "cubic")
928 {
930 }
931 else if (interp == "static")
932 {
934 }
935 else
936 {
937 throw XmlException("Unsupported interpolation type: " + interp);
938 }
939 }
940 catch (XmlException&)
941 {
943 "Failed to set RotationPath interpolation type for platform {}. Defaulting to static",
944 platform->getName());
946 }
947
948 unsigned waypoint_index = 0;
949 while (true)
950 {
951 XmlElement const waypoint = rotation.childElement("rotationwaypoint", waypoint_index);
952 if (!waypoint.isValid())
953 {
954 break;
955 }
956
957 try
958 {
959 LOG(logging::Level::TRACE, "Adding waypoint {} to rotation path for platform {}.", waypoint_index,
960 platform->getName());
961
962 const RealType az_deg = get_child_real_type(waypoint, "azimuth");
963 const RealType el_deg = get_child_real_type(waypoint, "elevation");
964 const RealType time = get_child_real_type(waypoint, "time");
965 const std::string owner =
966 std::format("platform '{}' rotation waypoint {}", platform->getName(), waypoint_index);
967
969 az_deg, unit, rotation_warning_utils::ValueKind::Angle, "XML", owner, "azimuth");
971 el_deg, unit, rotation_warning_utils::ValueKind::Angle, "XML", owner, "elevation");
972
974 }
975 catch (const XmlException& e)
976 {
977 LOG(logging::Level::ERROR, "Failed to add waypoint to rotation path. Discarding waypoint. {}",
978 e.what());
979 }
981 }
982 path->finalize();
983 }
984
986 {
987 math::RotationPath* path = platform->getRotationPath();
988 try
989 {
990 const RealType start_az_deg = get_child_real_type(rotation, "startazimuth");
991 const RealType start_el_deg = get_child_real_type(rotation, "startelevation");
992 const RealType rate_az_deg_s = get_child_real_type(rotation, "azimuthrate");
993 const RealType rate_el_deg_s = get_child_real_type(rotation, "elevationrate");
994 const std::string owner = std::format("platform '{}' fixedrotation", platform->getName());
995
997 start_az_deg, unit, rotation_warning_utils::ValueKind::Angle, "XML", owner, "startazimuth");
999 start_el_deg, unit, rotation_warning_utils::ValueKind::Angle, "XML", owner, "startelevation");
1001 rate_az_deg_s, unit, rotation_warning_utils::ValueKind::Rate, "XML", owner, "azimuthrate");
1003 rate_el_deg_s, unit, rotation_warning_utils::ValueKind::Rate, "XML", owner, "elevationrate");
1004 const math::RotationCoord start =
1006 const math::RotationCoord rate =
1008
1009 path->setConstantRate(start, rate);
1010 LOG(logging::Level::DEBUG, "Added fixed rotation to platform {}", platform->getName());
1011 }
1012 catch (XmlException& e)
1013 {
1014 LOG(logging::Level::FATAL, "Failed to set fixed rotation for platform {}. {}", platform->getName(),
1015 e.what());
1016 throw XmlException("Failed to set fixed rotation for platform " + platform->getName());
1017 }
1018 }
1019
1020 /// Parses a transmitter after its operation mode has already been determined.
1023 const radar::OperationMode mode)
1024 {
1025 const std::string name = XmlElement::getSafeAttribute(transmitter, "name");
1026 const SimId id = assign_id_from_attribute("transmitter '" + name + "'", ObjectType::Transmitter);
1027 const XmlElement pulsed_mode_element = transmitter.childElement("pulsed_mode", 0);
1028 const bool is_pulsed = mode == radar::OperationMode::PULSED_MODE;
1029
1030 auto transmitter_obj = std::make_unique<radar::Transmitter>(platform, name, mode, id);
1031
1032 const SimId waveform_id =
1033 resolve_reference_id(transmitter, "waveform", "transmitter '" + name + "'", *refs.waveforms);
1034 fers_signal::RadarSignal* wave = ctx.world->findWaveform(waveform_id);
1035 if (wave == nullptr)
1036 {
1037 throw XmlException("Waveform ID '" + std::to_string(waveform_id) + "' not found for transmitter '" + name +
1038 "'");
1039 }
1040 validate_fmcw_waveform(*wave, "Waveform '" + wave->getName() + "'");
1041 validate_waveform_mode_match(*wave, mode, "Transmitter '" + name + "'");
1042 transmitter_obj->setWave(wave);
1043
1044 if (is_pulsed)
1045 {
1047 }
1048
1049 const SimId antenna_id =
1050 resolve_reference_id(transmitter, "antenna", "transmitter '" + name + "'", *refs.antennas);
1051 const antenna::Antenna* ant = ctx.world->findAntenna(antenna_id);
1052 if (ant == nullptr)
1053 {
1054 throw XmlException("Antenna ID '" + std::to_string(antenna_id) + "' not found for transmitter '" + name +
1055 "'");
1056 }
1057 transmitter_obj->setAntenna(ant);
1058
1059 const SimId timing_id =
1060 resolve_reference_id(transmitter, "timing", "transmitter '" + name + "'", *refs.timings);
1061 transmitter_obj->setTiming(resolve_timing_instance(timing_id, ctx, "transmitter '" + name + "'"));
1062
1063 RealType const pri = is_pulsed ? (1.0 / transmitter_obj->getPrf()) : 0.0;
1065 if (wave->isFmcwFamily())
1066 {
1067 validate_fmcw_schedule(schedule, *wave, "Transmitter '" + name + "'");
1068 }
1069 if (!schedule.empty())
1070 {
1071 transmitter_obj->setSchedule(std::move(schedule));
1072 }
1073
1074 ctx.world->add(std::move(transmitter_obj));
1075 return ctx.world->getTransmitters().back().get();
1076 }
1077
1079 const ReferenceLookup& refs)
1080 {
1081 const std::string name = XmlElement::getSafeAttribute(transmitter, "name");
1082 const radar::OperationMode mode = parse_mode_elements(transmitter, "Transmitter '" + name + "'");
1083 if (const XmlElement fmcw_mode = transmitter.childElement("fmcw_mode", 0);
1085 {
1086 throw XmlException("Transmitter '" + name + "' fmcw_mode must not contain dechirp configuration.");
1087 }
1089 }
1090
1091 /// Parses a receiver after its operation mode has already been determined.
1094 const radar::OperationMode mode)
1095 {
1096 const std::string name = XmlElement::getSafeAttribute(receiver, "name");
1097 const SimId id = assign_id_from_attribute("receiver '" + name + "'", ObjectType::Receiver);
1098 const XmlElement pulsed_mode_element = receiver.childElement("pulsed_mode", 0);
1099 const bool is_pulsed = mode == radar::OperationMode::PULSED_MODE;
1100
1101 auto receiver_obj = std::make_unique<radar::Receiver>(platform, name, next_seed(*ctx.master_seeder), mode, id);
1102
1103 const SimId ant_id = resolve_reference_id(receiver, "antenna", "receiver '" + name + "'", *refs.antennas);
1104 const antenna::Antenna* antenna = ctx.world->findAntenna(ant_id);
1105 if (antenna == nullptr)
1106 {
1107 throw XmlException("Antenna ID '" + std::to_string(ant_id) + "' not found for receiver '" + name + "'");
1108 }
1109 receiver_obj->setAntenna(antenna);
1110
1111 if (!receiver.childElement("noise_temp", 0).isValid())
1112 {
1113 LOG(logging::Level::DEBUG, "Receiver '{}' does not specify noise temperature",
1114 receiver_obj->getName().c_str());
1115 }
1116 else
1117 {
1118 try
1119 {
1120 receiver_obj->setNoiseTemperature(get_child_real_type(receiver, "noise_temp"));
1121 }
1122 catch (const XmlException&)
1123 {
1124 LOG(logging::Level::WARNING, "Receiver '{}' has an empty noise temperature; using default.",
1125 receiver_obj->getName().c_str());
1126 }
1127 }
1128
1129 if (is_pulsed)
1130 {
1131 const RealType window_length = get_child_real_type(pulsed_mode_element, "window_length");
1132 if (window_length <= 0)
1133 {
1134 throw XmlException("<window_length> must be positive for receiver '" + name + "'");
1135 }
1136
1138 if (prf <= 0)
1139 {
1140 throw XmlException("<prf> must be positive for receiver '" + name + "'");
1141 }
1142
1143 const RealType window_skip = get_child_real_type(pulsed_mode_element, "window_skip");
1144 if (window_skip < 0)
1145 {
1146 throw XmlException("<window_skip> must not be negative for receiver '" + name + "'");
1147 }
1148 receiver_obj->setWindowProperties(window_length, prf, window_skip);
1149 }
1150 const SimId timing_id = resolve_reference_id(receiver, "timing", "receiver '" + name + "'", *refs.timings);
1151 receiver_obj->setTiming(resolve_timing_instance(timing_id, ctx, "receiver '" + name + "'"));
1152
1153 if (get_attribute_bool(receiver, "nodirect", false))
1154 {
1156 LOG(logging::Level::DEBUG, "Ignoring direct signals for receiver '{}'", receiver_obj->getName().c_str());
1157 }
1158
1159 if (get_attribute_bool(receiver, "nopropagationloss", false))
1160 {
1162 LOG(logging::Level::DEBUG, "Ignoring propagation losses for receiver '{}'",
1163 receiver_obj->getName().c_str());
1164 }
1165
1166 RealType const pri = is_pulsed ? (1.0 / receiver_obj->getWindowPrf()) : 0.0;
1168 if (!schedule.empty())
1169 {
1170 receiver_obj->setSchedule(std::move(schedule));
1171 }
1172
1173 parse_receiver_dechirp_config(receiver, *receiver_obj, "Receiver '" + name + "'");
1174
1175 ctx.world->add(std::move(receiver_obj));
1176 return ctx.world->getReceivers().back().get();
1177 }
1178
1180 const ReferenceLookup& refs)
1181 {
1182 const std::string name = XmlElement::getSafeAttribute(receiver, "name");
1183 const radar::OperationMode mode = parse_mode_elements(receiver, "Receiver '" + name + "'");
1185 }
1186
1188 const ReferenceLookup& refs)
1189 {
1190 const std::string name = XmlElement::getSafeAttribute(monostatic, "name");
1191 const radar::OperationMode monostatic_mode = parse_mode_elements(monostatic, "Monostatic '" + name + "'");
1194 if (trans->getMode() != monostatic_mode || recv->getMode() != monostatic_mode)
1195 {
1196 throw XmlException("Monostatic '" + name + "' parsed inconsistent transmitter/receiver modes.");
1197 }
1198 if (trans->getSignal() != nullptr)
1199 {
1200 validate_waveform_mode_match(*trans->getSignal(), trans->getMode(),
1201 "Monostatic '" + trans->getName() + "'");
1202 }
1203 trans->setAttached(recv);
1204 recv->setAttached(trans);
1205 }
1206
1208 {
1209 const std::string name = XmlElement::getSafeAttribute(target, "name");
1210 const SimId id = assign_id_from_attribute("target '" + name + "'", ObjectType::Target);
1211
1212 const XmlElement rcs_element = target.childElement("rcs", 0);
1213 if (!rcs_element.isValid())
1214 {
1215 throw XmlException("<rcs> element is required in <target>!");
1216 }
1217
1218 const std::string rcs_type = XmlElement::getSafeAttribute(rcs_element, "type");
1219 std::unique_ptr<radar::Target> target_obj;
1220 const unsigned seed = next_seed(*ctx.master_seeder);
1221
1222 if (rcs_type == "isotropic")
1223 {
1225 }
1226 else if (rcs_type == "file")
1227 {
1228 // Defer to dependency-injected file loader
1229 target_obj = ctx.loaders.loadFileTarget(platform, name,
1231 }
1232 else
1233 {
1234 throw XmlException("Unsupported RCS type: " + rcs_type);
1235 }
1236
1237 if (const XmlElement model = target.childElement("model", 0); model.isValid())
1238 {
1239 if (const std::string model_type = XmlElement::getSafeAttribute(model, "type"); model_type == "constant")
1240 {
1241 target_obj->setFluctuationModel(std::make_unique<radar::RcsConst>());
1242 }
1243 else if (model_type == "chisquare" || model_type == "gamma")
1244 {
1245 target_obj->setFluctuationModel(
1246 std::make_unique<radar::RcsChiSquare>(target_obj->getRngEngine(), get_child_real_type(model, "k")));
1247 }
1248 else
1249 {
1250 throw XmlException("Unsupported model type: " + model_type);
1251 }
1252 }
1253
1254 LOG(logging::Level::DEBUG, "Added target {} with RCS type {} to platform {}", name, rcs_type,
1255 platform->getName());
1256 ctx.world->add(std::move(target_obj));
1257 }
1258
1260 const std::function<void(const XmlElement&, std::string_view)>& register_name,
1261 const ReferenceLookup& refs)
1262 {
1263 auto parseChildrenWithRefs = [&](const std::string& elementName, auto parseFunc)
1264 {
1265 unsigned index = 0;
1266 while (true)
1267 {
1268 const XmlElement element = platform.childElement(elementName, index++);
1269 if (!element.isValid())
1270 break;
1273 }
1274 };
1275
1276 auto parseChildrenWithoutRefs = [&](const std::string& elementName, auto parseFunc)
1277 {
1278 unsigned index = 0;
1279 while (true)
1280 {
1281 const XmlElement element = platform.childElement(elementName, index++);
1282 if (!element.isValid())
1283 break;
1286 }
1287 };
1288
1293 }
1294
1296 const std::function<void(const XmlElement&, std::string_view)>& register_name,
1297 const ReferenceLookup& refs)
1298 {
1299 std::string const name = XmlElement::getSafeAttribute(platform, "name");
1300 const SimId id = assign_id_from_attribute("platform '" + name + "'", ObjectType::Platform);
1301 auto plat = std::make_unique<radar::Platform>(name, id);
1302
1304
1305 if (const XmlElement motion_path = platform.childElement("motionpath", 0); motion_path.isValid())
1306 {
1308 }
1309
1310 const XmlElement rot_path = platform.childElement("rotationpath", 0);
1311 if (const XmlElement fixed_rot = platform.childElement("fixedrotation", 0);
1312 rot_path.isValid() && fixed_rot.isValid())
1313 {
1315 "Both <rotationpath> and <fixedrotation> are declared for platform {}. Only <rotationpath> will be "
1316 "used.",
1317 plat->getName());
1318 parseRotationPath(rot_path, plat.get(), ctx.parameters.rotation_angle_unit);
1319 }
1320 else if (rot_path.isValid())
1321 {
1322 parseRotationPath(rot_path, plat.get(), ctx.parameters.rotation_angle_unit);
1323 }
1324 else if (fixed_rot.isValid())
1325 {
1326 parseFixedRotation(fixed_rot, plat.get(), ctx.parameters.rotation_angle_unit);
1327 }
1328
1329 ctx.world->add(std::move(plat));
1330 }
1331
1332 void collectIncludeElements(const XmlDocument& doc, const fs::path& currentDir, std::vector<fs::path>& includePaths)
1333 {
1334 unsigned index = 0;
1335 while (true)
1336 {
1337 XmlElement const include_element = doc.getRootElement().childElement("include", index++);
1338 if (!include_element.isValid())
1339 break;
1340
1341 std::string const include_filename = include_element.getText();
1342 if (include_filename.empty())
1343 {
1344 LOG(logging::Level::ERROR, "<include> element is missing the filename!");
1345 continue;
1346 }
1347
1348 fs::path const include_path = currentDir / include_filename;
1349 includePaths.push_back(include_path);
1350
1352 if (!included_doc.loadFile(include_path.string()))
1353 {
1354 LOG(logging::Level::ERROR, "Failed to load included XML file: {}", include_path.string());
1355 continue;
1356 }
1357
1359 }
1360 }
1361
1363 {
1364 std::vector<fs::path> include_paths;
1366 bool did_combine = false;
1367
1368 for (const auto& include_path : include_paths)
1369 {
1371 if (!included_doc.loadFile(include_path.string()))
1372 {
1373 throw XmlException("Failed to load included XML file: " + include_path.string());
1374 }
1375
1377 did_combine = true;
1378 }
1379
1381 return did_combine;
1382 }
1383
1385 {
1386 LOG(logging::Level::DEBUG, "Validating the{}XML file...", didCombine ? " combined " : " ");
1387 if (!mainDoc.validateWithDtd(fers_xml_dtd))
1388 {
1389 LOG(logging::Level::FATAL, "{} XML file failed DTD validation!", didCombine ? "Combined" : "Main");
1390 throw XmlException("XML file failed DTD validation!");
1391 }
1392 LOG(logging::Level::DEBUG, "{} XML file passed DTD validation.", didCombine ? "Combined" : "Main");
1393
1394 if (!mainDoc.validateWithXsd(fers_xml_xsd))
1395 {
1396 LOG(logging::Level::FATAL, "{} XML file failed XSD validation!", didCombine ? "Combined" : "Main");
1397 throw XmlException("XML file failed XSD validation!");
1398 }
1399 LOG(logging::Level::DEBUG, "{} XML file passed XSD validation.", didCombine ? "Combined" : "Main");
1400 }
1401
1403 {
1404 const XmlElement root = doc.getRootElement();
1405 if (root.name() != "simulation")
1406 {
1407 throw XmlException("Root element is not <simulation>!");
1408 }
1409
1410 std::unordered_map<std::string, std::string> name_registry;
1411 name_registry.reserve(64); // TODO: reserve 64?
1412 const auto register_name = [&](const XmlElement& element, const std::string_view kind)
1413 {
1414 const std::string name = XmlElement::getSafeAttribute(element, "name");
1415 const auto [iter, inserted] = name_registry.emplace(name, std::string(kind));
1416 if (!inserted)
1417 {
1418 throw XmlException("Duplicate name '" + name + "' found for " + std::string(kind) +
1419 "; previously used by " + iter->second + ".");
1420 }
1421 };
1422
1423 try
1424 {
1425 ctx.parameters.simulation_name = XmlElement::getSafeAttribute(root, "name");
1426 if (!ctx.parameters.simulation_name.empty())
1427 {
1428 LOG(logging::Level::INFO, "Simulation name set to: {}", ctx.parameters.simulation_name);
1429 }
1430 }
1431 catch (const XmlException&)
1432 {
1433 LOG(logging::Level::WARNING, "No 'name' attribute found in <simulation> tag. KML name will default.");
1434 }
1435
1436 parseParameters(root.childElement("parameters", 0), ctx.parameters);
1437
1438 params::params = ctx.parameters;
1439
1440 auto parseElements =
1441 [](const XmlElement& parent, const std::string& elementName, ParserContext& parser_ctx, auto parseFunction)
1442 {
1443 unsigned index = 0;
1444 while (true)
1445 {
1446 XmlElement const element = parent.childElement(elementName, index++);
1447 if (!element.isValid())
1448 break;
1450 }
1451 };
1452
1453 parseElements(root, "waveform", ctx,
1454 [&](const XmlElement& p, ParserContext& c)
1455 {
1456 register_name(p, "waveform");
1457 parseWaveform(p, c);
1458 });
1459
1460 parseElements(root, "timing", ctx,
1461 [&](const XmlElement& p, ParserContext& c)
1462 {
1463 register_name(p, "timing");
1464 parseTiming(p, c);
1465 });
1466
1467 parseElements(root, "antenna", ctx,
1468 [&](const XmlElement& p, ParserContext& c)
1469 {
1470 register_name(p, "antenna");
1471 parseAntenna(p, c);
1472 });
1473
1474 std::unordered_map<std::string, SimId> waveform_refs;
1475 std::unordered_map<std::string, SimId> antenna_refs;
1476 std::unordered_map<std::string, SimId> timing_refs;
1477 waveform_refs.reserve(ctx.world->getWaveforms().size());
1478 antenna_refs.reserve(ctx.world->getAntennas().size());
1479 timing_refs.reserve(ctx.world->getTimings().size());
1480
1481 for (const auto& [id, waveform] : ctx.world->getWaveforms())
1482 waveform_refs.emplace(waveform->getName(), id);
1483 for (const auto& [id, antenna] : ctx.world->getAntennas())
1484 antenna_refs.emplace(antenna->getName(), id);
1485 for (const auto& [id, timing] : ctx.world->getTimings())
1486 timing_refs.emplace(timing->getName(), id);
1487
1489
1490 parseElements(root, "platform", ctx,
1491 [&](const XmlElement& p, ParserContext& c)
1492 {
1493 register_name(p, "platform");
1495 });
1496
1497 ctx.world->resolveReceiverDechirpReferences();
1498
1499 ctx.world->scheduleInitialEvents();
1500
1501 LOG(logging::Level::DEBUG, "Initial Event Queue State:\n{}", ctx.world->dumpEventQueue());
1502 }
1503
1505 {
1506 return {.loadWaveform = [](const std::string& name, const fs::path& pulse_path, RealType power,
1508 { return serial::loadWaveformFromFile(name, pulse_path.string(), power, carrierFreq, id); },
1509 .loadXmlAntenna = [](const std::string& name, const std::string& filename, SimId id)
1510 { return std::make_unique<antenna::XmlAntenna>(name, filename, id); },
1511 .loadH5Antenna = [](const std::string& name, const std::string& filename, SimId id)
1512 { return std::make_unique<antenna::H5Antenna>(name, filename, id); },
1513 .loadFileTarget = [](radar::Platform* platform, const std::string& name, const std::string& filename,
1514 unsigned seed, SimId id)
1515 { return radar::createFileTarget(platform, name, filename, seed, id); }};
1516 }
1517}
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
@ 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.