FERS 1.0.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
xml_parser.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-only
2//
3// Copyright (c) 2006-2008 Marc Brooker and Michael Inggs
4// Copyright (c) 2008-present FERS Contributors (see AUTHORS.md).
5//
6// See the GNU GPLv2 LICENSE file in the FERS project root for more information.
7
8/**
9 * @file xml_parser.cpp
10 * @brief Implementation file for parsing XML configuration files for simulation.
11 */
12
13#include "xml_parser.h"
14
15#include <GeographicLib/UTMUPS.hpp>
16#include <cmath>
17#include <filesystem>
18#include <functional>
19#include <memory>
20#include <random>
21#include <span>
22#include <string_view>
23#include <utility>
24#include <vector>
25
26// Generated headers
28#include "core/config.h"
29#include "core/logging.h"
30#include "core/parameters.h"
31#include "core/world.h"
32#include "fers_xml_dtd.h"
33#include "fers_xml_xsd.h"
34#include "libxml_wrapper.h"
35#include "math/coord.h"
36#include "math/geometry_ops.h"
37#include "math/path.h"
38#include "math/rotation_path.h"
39#include "radar/platform.h"
40#include "radar/radar_obj.h"
41#include "radar/receiver.h"
42#include "radar/target.h"
43#include "radar/transmitter.h"
45#include "timing/timing.h"
46#include "waveform_factory.h"
47
48namespace fs = std::filesystem;
49
51using core::World;
53using logging::Level;
54using math::Path;
57using radar::Platform;
58using radar::Receiver;
59using radar::Target;
62using timing::Timing;
63
64/**
65 * @brief Parses elements with child iteration (e.g., waveforms, timings, antennas).
66 *
67 * @tparam T The type of the parsing function.
68 * @param root The root XmlElement to parse.
69 * @param elementName The name of the child elements to iterate over.
70 * @param world A pointer to the World object where parsed data is added.
71 * @param parseFunction The parsing function to call for each child element.
72 */
73template <typename T>
74void parseElements(const XmlElement& root, const std::string& elementName, World* world, T parseFunction)
75{
76 unsigned index = 0;
77 while (true)
78 {
79 XmlElement element = root.childElement(elementName, index++);
80 if (!element.isValid())
81 {
82 break;
83 }
84 parseFunction(element, world);
85 }
86}
87
88/**
89 * @brief Helper function to extract a RealType value from an element.
90 *
91 * @param element The XmlElement to extract the value from.
92 * @param elementName The name of the child element to extract the value from.
93 * @return The extracted RealType value.
94 * @throws XmlException if the element is empty or cannot be parsed.
95 */
96auto get_child_real_type = [](const XmlElement& element, const std::string& elementName) -> RealType
97{
98 const std::string text = element.childElement(elementName, 0).getText();
99 if (text.empty())
100 {
101 throw XmlException("Element " + elementName + " is empty!");
102 }
103 return std::stod(text);
104};
105
106/**
107 * @brief Helper function to extract a boolean value from an attribute.
108 *
109 * @param element The XmlElement to extract the value from.
110 * @param attributeName The name of the attribute to extract the value from.
111 * @param defaultVal The default value to return if the attribute is empty or cannot be parsed.
112 * @return The extracted boolean value or the default value.
113 */
114auto get_attribute_bool = [](const XmlElement& element, const std::string& attributeName, const bool defaultVal) -> bool
115{
116 try
117 {
118 return XmlElement::getSafeAttribute(element, attributeName) == "true";
119 }
120 catch (const XmlException&)
121 {
122 LOG(Level::WARNING, "Failed to get boolean value for attribute '{}'. Defaulting to {}.", attributeName,
123 defaultVal);
124 return defaultVal;
125 }
126};
127
128namespace
129{
130 std::vector<radar::SchedulePeriod> parseSchedule(const XmlElement& parent, const std::string& parentName,
131 const bool isPulsed, const RealType pri = 0.0)
132 {
133 std::vector<radar::SchedulePeriod> raw_periods;
134 if (const XmlElement schedule_element = parent.childElement("schedule", 0); schedule_element.isValid())
135 {
136 unsigned p_idx = 0;
137 while (true)
138 {
139 XmlElement period_element = schedule_element.childElement("period", p_idx++);
140 if (!period_element.isValid())
141 {
142 break;
143 }
144 try
145 {
146 const RealType start = std::stod(XmlElement::getSafeAttribute(period_element, "start"));
147 const RealType end = std::stod(XmlElement::getSafeAttribute(period_element, "end"));
148 raw_periods.push_back({start, end});
149 }
150 catch (const std::exception& e)
151 {
152 LOG(Level::WARNING, "Failed to parse schedule period for '{}': {}", parentName, e.what());
153 }
154 }
155 }
156
157 // Delegate processing (sorting, merging, validation) to the shared utility
158 return radar::processRawSchedule(std::move(raw_periods), parentName, isPulsed, pri);
159 }
160
161 /**
162 * @brief Parses the <parameters> element of the XML document.
163 *
164 * @param parameters The <parameters> XmlElement to parse.
165 */
166 void parseParameters(const XmlElement& parameters)
167 {
168 params::setTime(get_child_real_type(parameters, "starttime"), get_child_real_type(parameters, "endtime"));
169
170 params::setRate(get_child_real_type(parameters, "rate"));
171
172 auto set_param_with_exception_handling = [](const XmlElement& element, const std::string& paramName,
173 const RealType defaultValue,
174 const std::function<void(RealType)>& setter)
175 {
176 try
177 {
178 if (paramName == "adc_bits" || paramName == "oversample")
179 {
180 setter(static_cast<unsigned>(std::floor(get_child_real_type(element, paramName))));
181 }
182 else
183 {
184 setter(get_child_real_type(element, paramName));
185 }
186 }
187 catch (const XmlException&)
188 {
189 LOG(Level::WARNING, "Failed to set parameter {}. Using default value. {}", paramName, defaultValue);
190 }
191 };
192
193 set_param_with_exception_handling(parameters, "c", params::c(), params::setC);
194 set_param_with_exception_handling(parameters, "simSamplingRate", params::simSamplingRate(),
196
197 try
198 {
199 const auto seed = static_cast<unsigned>(std::floor(get_child_real_type(parameters, "randomseed")));
201 }
202 catch (const XmlException&)
203 {
204 // Do nothing, optional remains empty
205 }
206
207 set_param_with_exception_handling(parameters, "adc_bits", params::adcBits(), params::setAdcBits);
208 set_param_with_exception_handling(parameters, "oversample", params::oversampleRatio(),
210
211 // Parse the origin element for the KML generator
212 bool origin_set = false;
213 if (const XmlElement origin_element = parameters.childElement("origin", 0); origin_element.isValid())
214 {
215 try
216 {
217 const double latitude = std::stod(XmlElement::getSafeAttribute(origin_element, "latitude"));
218 const double longitude = std::stod(XmlElement::getSafeAttribute(origin_element, "longitude"));
219 const double altitude = std::stod(XmlElement::getSafeAttribute(origin_element, "altitude"));
220 params::setOrigin(latitude, longitude, altitude);
221 origin_set = true;
222 }
223 catch (const std::exception& e)
224 {
225 LOG(Level::WARNING, "Could not parse origin from XML, using defaults. Error: {}", e.what());
226 }
227 }
228
229 // Parse coordinate system, defaulting to ENU
230 if (const XmlElement cs_element = parameters.childElement("coordinatesystem", 0); cs_element.isValid())
231 {
232 try
233 {
234 const std::string frame_str = XmlElement::getSafeAttribute(cs_element, "frame");
236 int zone = 0;
237 bool north = true;
238
239 if (frame_str == "UTM")
240 {
242 zone = std::stoi(XmlElement::getSafeAttribute(cs_element, "zone"));
243 const std::string hem_str = XmlElement::getSafeAttribute(cs_element, "hemisphere");
244
245 if (zone < GeographicLib::UTMUPS::MINUTMZONE || zone > GeographicLib::UTMUPS::MAXUTMZONE)
246 {
247 throw XmlException("UTM zone " + std::to_string(zone) + " is invalid; must be in [1, 60].");
248 }
249 if (hem_str == "N" || hem_str == "n")
250 {
251 north = true;
252 }
253 else if (hem_str == "S" || hem_str == "s")
254 {
255 north = false;
256 }
257 else
258 {
259 throw XmlException("UTM hemisphere '" + hem_str + "' is invalid; must be 'N' or 'S'.");
260 }
261 LOG(Level::INFO, "Coordinate system set to UTM, zone {}{}", zone, north ? 'N' : 'S');
262 }
263 else if (frame_str == "ECEF")
264 {
266 LOG(Level::INFO, "Coordinate system set to ECEF.");
267 }
268 else if (frame_str == "ENU")
269 {
271 if (!origin_set)
272 {
273 LOG(Level::WARNING,
274 "ENU frame specified but no <origin> tag found. Using default origin at UCT.");
275 }
276 LOG(Level::INFO, "Coordinate system set to ENU local tangent plane.");
277 }
278 else
279 {
280 throw XmlException("Unsupported coordinate frame: " + frame_str);
281 }
282 params::setCoordinateSystem(frame, zone, north);
283 }
284 catch (const std::exception& e)
285 {
286 LOG(Level::WARNING, "Could not parse <coordinatesystem> from XML: {}. Defaulting to ENU.", e.what());
288 }
289 }
290 }
291
292 /**
293 * @brief Parses the <waveform> element of the XML document.
294 *
295 * @param waveform The <waveform> XmlElement to parse.
296 * @param world A pointer to the World object where the RadarSignal object is added.
297 * @param baseDir The base directory of the main scenario file to resolve relative paths.
298 */
299 void parseWaveform(const XmlElement& waveform, World* world, const fs::path& baseDir)
300 {
301 const std::string name = XmlElement::getSafeAttribute(waveform, "name");
302
303 const auto power = get_child_real_type(waveform, "power");
304 const auto carrier = get_child_real_type(waveform, "carrier_frequency");
305
306 if (const XmlElement pulsed_file = waveform.childElement("pulsed_from_file", 0); pulsed_file.isValid())
307 {
308 const std::string filename_str = XmlElement::getSafeAttribute(pulsed_file, "filename");
309 fs::path pulse_path(filename_str);
310
311 // Check if path exists as is, if not, try relative to the main XML directory
312 if (!fs::exists(pulse_path))
313 {
314 pulse_path = baseDir / filename_str;
315 }
316
317 if (!fs::exists(pulse_path))
318 {
319 throw XmlException("Waveform file not found: " + filename_str);
320 }
321
322 auto wave = serial::loadWaveformFromFile(name, pulse_path.string(), power, carrier);
323 world->add(std::move(wave));
324 }
325 else if (waveform.childElement("cw", 0).isValid())
326 {
327 auto cw_signal = std::make_unique<fers_signal::CwSignal>();
328 auto wave = std::make_unique<RadarSignal>(name, power, carrier, params::endTime() - params::startTime(),
329 std::move(cw_signal));
330 world->add(std::move(wave));
331 }
332 else
333 {
334 LOG(Level::FATAL, "Unsupported waveform type for '{}'", name);
335 throw XmlException("Unsupported waveform type for '" + name + "'");
336 }
337 }
338
339 /**
340 * @brief Parses the <timing> element of the XML document.
341 *
342 * @param timing The <timing> XmlElement to parse.
343 * @param world A pointer to the World object where the PrototypeTiming object is added.
344 */
345 void parseTiming(const XmlElement& timing, World* world)
346 {
347 const std::string name = XmlElement::getSafeAttribute(timing, "name");
348 const RealType freq = get_child_real_type(timing, "frequency");
349 auto timing_obj = std::make_unique<PrototypeTiming>(name);
350
351 timing_obj->setFrequency(freq);
352
353 unsigned noise_index = 0;
354 while (true)
355 {
356 XmlElement noise_element = timing.childElement("noise_entry", noise_index++);
357 if (!noise_element.isValid())
358 {
359 break;
360 }
361
362 timing_obj->setAlpha(get_child_real_type(noise_element, "alpha"),
363 get_child_real_type(noise_element, "weight"));
364 }
365
366 try
367 {
368 timing_obj->setFreqOffset(get_child_real_type(timing, "freq_offset"));
369 }
370 catch (XmlException&)
371 {
372 LOG(Level::WARNING, "Clock section '{}' does not specify frequency offset.", name);
373 }
374
375 try
376 {
377 timing_obj->setRandomFreqOffsetStdev(get_child_real_type(timing, "random_freq_offset_stdev"));
378 }
379 catch (XmlException&)
380 {
381 LOG(Level::WARNING, "Clock section '{}' does not specify random frequency offset.", name);
382 }
383
384 try
385 {
386 timing_obj->setPhaseOffset(get_child_real_type(timing, "phase_offset"));
387 }
388 catch (XmlException&)
389 {
390 LOG(Level::WARNING, "Clock section '{}' does not specify phase offset.", name);
391 }
392
393 try
394 {
395 timing_obj->setRandomPhaseOffsetStdev(get_child_real_type(timing, "random_phase_offset_stdev"));
396 }
397 catch (XmlException&)
398 {
399 LOG(Level::WARNING, "Clock section '{}' does not specify random phase offset.", name);
400 }
401
402 if (get_attribute_bool(timing, "synconpulse", false))
403 {
404 timing_obj->setSyncOnPulse();
405 }
406
407 world->add(std::move(timing_obj));
408 }
409
410 /**
411 * @brief Parses the <antenna> element of the XML document.
412 *
413 * @param antenna The <antenna> XmlElement to parse.
414 * @param world A pointer to the World object where the Antenna object is added.
415 */
416 void parseAntenna(const XmlElement& antenna, World* world)
417 {
418 std::string name = XmlElement::getSafeAttribute(antenna, "name");
419 const std::string pattern = XmlElement::getSafeAttribute(antenna, "pattern");
420
421 std::unique_ptr<Antenna> ant;
422
423 LOG(Level::DEBUG, "Adding antenna '{}' with pattern '{}'", name, pattern);
424 if (pattern == "isotropic")
425 {
426 ant = std::make_unique<antenna::Isotropic>(name);
427 }
428 else if (pattern == "sinc")
429 {
430 ant = std::make_unique<antenna::Sinc>(name, get_child_real_type(antenna, "alpha"),
432 get_child_real_type(antenna, "gamma"));
433 }
434 else if (pattern == "gaussian")
435 {
436 ant = std::make_unique<antenna::Gaussian>(name, get_child_real_type(antenna, "azscale"),
437 get_child_real_type(antenna, "elscale"));
438 }
439 else if (pattern == "squarehorn")
440 {
441 ant = std::make_unique<antenna::SquareHorn>(name, get_child_real_type(antenna, "diameter"));
442 }
443 else if (pattern == "parabolic")
444 {
445 ant = std::make_unique<antenna::Parabolic>(name, get_child_real_type(antenna, "diameter"));
446 }
447 else if (pattern == "xml")
448 {
449 ant = std::make_unique<antenna::XmlAntenna>(name, XmlElement::getSafeAttribute(antenna, "filename"));
450 }
451 else if (pattern == "file")
452 {
453 ant = std::make_unique<antenna::H5Antenna>(name, XmlElement::getSafeAttribute(antenna, "filename"));
454 }
455 else
456 {
457 LOG(Level::FATAL, "Unsupported antenna pattern: {}", pattern);
458 throw XmlException("Unsupported antenna pattern: " + pattern);
459 }
460
461 try
462 {
463 ant->setEfficiencyFactor(get_child_real_type(antenna, "efficiency"));
464 }
465 catch (XmlException&)
466 {
467 LOG(Level::WARNING, "Antenna '{}' does not specify efficiency, assuming unity.", name);
468 }
469
470 world->add(std::move(ant));
471 }
472
473 /**
474 * @brief Parses the <motionpath> element of the XML document.
475 *
476 * @param motionPath The <motionpath> XmlElement to parse.
477 * @param platform A pointer to the Platform object where the motion path is set.
478 */
479 void parseMotionPath(const XmlElement& motionPath, const Platform* platform)
480 {
481 Path* path = platform->getMotionPath();
482 try
483 {
484 if (std::string interp = XmlElement::getSafeAttribute(motionPath, "interpolation"); interp == "linear")
485 {
487 }
488 else if (interp == "cubic")
489 {
491 }
492 else if (interp == "static")
493 {
495 }
496 else
497 {
498 LOG(Level::ERROR, "Unsupported interpolation type: {} for platform {}. Defaulting to static", interp,
499 platform->getName());
501 }
502 }
503 catch (XmlException&)
504 {
505 LOG(Level::ERROR, "Failed to set MotionPath interpolation type for platform {}. Defaulting to static",
506 platform->getName());
508 }
509
510 unsigned waypoint_index = 0;
511 while (true)
512 {
513 XmlElement waypoint = motionPath.childElement("positionwaypoint", waypoint_index);
514 if (!waypoint.isValid())
515 {
516 break;
517 }
518
519 try
520 {
521 math::Coord coord;
522 coord.t = get_child_real_type(waypoint, "time");
523 coord.pos = math::Vec3(get_child_real_type(waypoint, "x"), get_child_real_type(waypoint, "y"),
524 get_child_real_type(waypoint, "altitude"));
525 path->addCoord(coord);
526 LOG(Level::TRACE, "Added waypoint {} to motion path for platform {}.", waypoint_index,
527 platform->getName());
528 }
529 catch (const XmlException& e)
530 {
531 LOG(Level::ERROR, "Failed to add waypoint to motion path. Discarding waypoint. {}", e.what());
532 }
533
534 waypoint_index++;
535 }
536
537 path->finalize();
538 }
539
540 /**
541 * @brief Parses the <rotationpath> element of the XML document.
542 *
543 * @param rotation The <rotationpath> XmlElement to parse.
544 * @param platform A pointer to the Platform object where the rotation path is set.
545 */
546 void parseRotationPath(const XmlElement& rotation, const Platform* platform)
547 {
548 RotationPath* path = platform->getRotationPath();
549
550 try
551 {
552 if (const std::string interp = XmlElement::getSafeAttribute(rotation, "interpolation"); interp == "linear")
553 {
555 }
556 else if (interp == "cubic")
557 {
559 }
560 else if (interp == "static")
561 {
563 }
564 else
565 {
566 throw XmlException("Unsupported interpolation type: " + interp);
567 }
568 }
569 catch (XmlException&)
570 {
571 LOG(Level::ERROR, "Failed to set RotationPath interpolation type for platform {}. Defaulting to static",
572 platform->getName());
574 }
575
576 unsigned waypoint_index = 0;
577 while (true)
578 {
579 XmlElement waypoint = rotation.childElement("rotationwaypoint", waypoint_index);
580 if (!waypoint.isValid())
581 {
582 break;
583 }
584
585 try
586 {
587 LOG(Level::TRACE, "Adding waypoint {} to rotation path for platform {}.", waypoint_index,
588 platform->getName());
589
590 const RealType az_deg = get_child_real_type(waypoint, "azimuth");
591 const RealType el_deg = get_child_real_type(waypoint, "elevation");
592 const RealType time = get_child_real_type(waypoint, "time");
593
594 // Convert from compass heading (degrees, CW from North) to FERS mathematical angle (radians, CCW from
595 // East)
596 const RealType az_rad = (90.0 - az_deg) * (PI / 180.0);
597 // Convert elevation from degrees to radians
598 const RealType el_rad = el_deg * (PI / 180.0);
599
600 path->addCoord({az_rad, el_rad, time});
601 }
602 catch (const XmlException& e)
603 {
604 LOG(Level::ERROR, "Failed to add waypoint to rotation path. Discarding waypoint. {}", e.what());
605 }
606
607 waypoint_index++;
608 }
609
610 path->finalize();
611 }
612
613 /**
614 * @brief Parses the <fixedrotation> element of the XML document.
615 *
616 * @param rotation The <fixedrotation> XmlElement to parse.
617 * @param platform A pointer to the Platform object where the fixed rotation is set.
618 */
619 void parseFixedRotation(const XmlElement& rotation, const Platform* platform)
620 {
621 RotationPath* path = platform->getRotationPath();
622 try
623 {
625 const RealType start_az_deg = get_child_real_type(rotation, "startazimuth");
626 const RealType start_el_deg = get_child_real_type(rotation, "startelevation");
627 const RealType rate_az_deg_s = get_child_real_type(rotation, "azimuthrate");
628 const RealType rate_el_deg_s = get_child_real_type(rotation, "elevationrate");
629
630 // Convert compass azimuth (degrees, CW from North) to FERS mathematical angle (radians, CCW from East)
631 start.azimuth = (90.0 - start_az_deg) * (PI / 180.0);
632 // Convert elevation from degrees to radians
633 start.elevation = start_el_deg * (PI / 180.0);
634
635 // Convert angular rates from deg/s to rad/s.
636 // A positive (CW) azimuth rate in degrees becomes a negative (CCW) rate in radians.
637 rate.azimuth = -rate_az_deg_s * (PI / 180.0);
638 rate.elevation = rate_el_deg_s * (PI / 180.0);
639
640 path->setConstantRate(start, rate);
641 LOG(Level::DEBUG, "Added fixed rotation to platform {}", platform->getName());
642 }
643 catch (XmlException& e)
644 {
645 LOG(Level::FATAL, "Failed to set fixed rotation for platform {}. {}", platform->getName(), e.what());
646 throw XmlException("Failed to set fixed rotation for platform " + platform->getName());
647 }
648 }
649
650 /**
651 * @brief Parses the <transmitter> element of the XML document.
652 *
653 * @param transmitter The <transmitter> XmlElement to parse.
654 * @param platform A pointer to the Platform
655 * @param world A pointer to the World
656 * @param masterSeeder The master random number generator for seeding.
657 * @return A pointer to the created Transmitter object.
658 */
659 Transmitter* parseTransmitter(const XmlElement& transmitter, Platform* platform, World* world,
660 std::mt19937& masterSeeder)
661 {
662 const std::string name = XmlElement::getSafeAttribute(transmitter, "name");
663 const XmlElement pulsed_mode_element = transmitter.childElement("pulsed_mode", 0);
664 const bool is_pulsed = pulsed_mode_element.isValid();
665 const OperationMode mode = is_pulsed ? OperationMode::PULSED_MODE : OperationMode::CW_MODE;
666
667 if (!is_pulsed && !transmitter.childElement("cw_mode", 0).isValid())
668 {
669 throw XmlException("Transmitter '" + name + "' must specify a radar mode (<pulsed_mode> or <cw_mode>).");
670 }
671
672 auto transmitter_obj = std::make_unique<Transmitter>(platform, name, mode);
673
674 const std::string waveform_name = XmlElement::getSafeAttribute(transmitter, "waveform");
675 RadarSignal* wave = world->findWaveform(waveform_name);
676 if (!wave)
677 {
678 throw XmlException("Waveform '" + waveform_name + "' not found for transmitter '" + name + "'");
679 }
680 transmitter_obj->setWave(wave);
681
682 if (is_pulsed)
683 {
684 transmitter_obj->setPrf(get_child_real_type(pulsed_mode_element, "prf"));
685 }
686
687 const std::string antenna_name = XmlElement::getSafeAttribute(transmitter, "antenna");
688 const Antenna* ant = world->findAntenna(antenna_name);
689 if (!ant)
690 {
691 throw XmlException("Antenna '" + antenna_name + "' not found for transmitter '" + name + "'");
692 }
693 transmitter_obj->setAntenna(ant);
694
695 const std::string timing_name = XmlElement::getSafeAttribute(transmitter, "timing");
696 const auto timing = std::make_shared<Timing>(timing_name, masterSeeder());
697 const PrototypeTiming* proto = world->findTiming(timing_name);
698 if (!proto)
699 {
700 throw XmlException("Timing '" + timing_name + "' not found for transmitter '" + name + "'");
701 }
702 timing->initializeModel(proto);
703 transmitter_obj->setTiming(timing);
704
705 // Use shared logic for schedule parsing
706 RealType pri = is_pulsed ? (1.0 / transmitter_obj->getPrf()) : 0.0;
707 auto schedule = parseSchedule(transmitter, name, is_pulsed, pri);
708 if (!schedule.empty())
709 {
710 transmitter_obj->setSchedule(std::move(schedule));
711 }
712
713 world->add(std::move(transmitter_obj));
714 return world->getTransmitters().back().get();
715 }
716
717 /**
718 * @brief Parses the <receiver> element of the XML document.
719 *
720 * @param receiver The <receiver> XmlElement to parse.
721 * @param platform A pointer to the Platform
722 * @param world A pointer to the World
723 * @param masterSeeder The master random number generator for seeding.
724 * @return A pointer to the created Receiver object.
725 */
726 Receiver* parseReceiver(const XmlElement& receiver, Platform* platform, World* world, std::mt19937& masterSeeder)
727 {
728 const std::string name = XmlElement::getSafeAttribute(receiver, "name");
729 const XmlElement pulsed_mode_element = receiver.childElement("pulsed_mode", 0);
730 const bool is_pulsed = pulsed_mode_element.isValid();
731 const OperationMode mode = is_pulsed ? OperationMode::PULSED_MODE : OperationMode::CW_MODE;
732
733 auto receiver_obj = std::make_unique<Receiver>(platform, name, masterSeeder(), mode);
734
735 const std::string ant_name = XmlElement::getSafeAttribute(receiver, "antenna");
736
737 const Antenna* antenna = world->findAntenna(ant_name);
738 if (!antenna)
739 {
740 throw XmlException("Antenna '" + ant_name + "' not found for receiver '" + name + "'");
741 }
742 receiver_obj->setAntenna(antenna);
743
744 try
745 {
746 receiver_obj->setNoiseTemperature(get_child_real_type(receiver, "noise_temp"));
747 }
748 catch (XmlException&)
749 {
750 LOG(Level::INFO, "Receiver '{}' does not specify noise temperature", receiver_obj->getName().c_str());
751 }
752
753 if (is_pulsed)
754 {
755 const RealType window_length = get_child_real_type(pulsed_mode_element, "window_length");
756 if (window_length <= 0)
757 {
758 throw XmlException("<window_length> must be positive for receiver '" + name + "'");
759 }
760
761 const RealType prf = get_child_real_type(pulsed_mode_element, "prf");
762 if (prf <= 0)
763 {
764 throw XmlException("<prf> must be positive for receiver '" + name + "'");
765 }
766
767 const RealType window_skip = get_child_real_type(pulsed_mode_element, "window_skip");
768 if (window_skip < 0)
769 {
770 throw XmlException("<window_skip> must not be negative for receiver '" + name + "'");
771 }
772 receiver_obj->setWindowProperties(window_length, prf, window_skip);
773 }
774 else if (!receiver.childElement("cw_mode", 0).isValid())
775 {
776 throw XmlException("Receiver '" + name + "' must specify a radar mode (<pulsed_mode> or <cw_mode>).");
777 }
778
779 const std::string timing_name = XmlElement::getSafeAttribute(receiver, "timing");
780 const auto timing = std::make_shared<Timing>(timing_name, masterSeeder());
781
782 const PrototypeTiming* proto = world->findTiming(timing_name);
783 if (!proto)
784 {
785 throw XmlException("Timing '" + timing_name + "' not found for receiver '" + name + "'");
786 }
787 timing->initializeModel(proto);
788 receiver_obj->setTiming(timing);
789
790 if (get_attribute_bool(receiver, "nodirect", false))
791 {
792 receiver_obj->setFlag(Receiver::RecvFlag::FLAG_NODIRECT);
793 LOG(Level::DEBUG, "Ignoring direct signals for receiver '{}'", receiver_obj->getName().c_str());
794 }
795
796 if (get_attribute_bool(receiver, "nopropagationloss", false))
797 {
798 receiver_obj->setFlag(Receiver::RecvFlag::FLAG_NOPROPLOSS);
799 LOG(Level::DEBUG, "Ignoring propagation losses for receiver '{}'", receiver_obj->getName().c_str());
800 }
801
802 // Parse schedule for receiver
803 RealType pri = is_pulsed ? (1.0 / receiver_obj->getWindowPrf()) : 0.0;
804 auto schedule = parseSchedule(receiver, name, is_pulsed, pri);
805 if (!schedule.empty())
806 {
807 receiver_obj->setSchedule(std::move(schedule));
808 }
809
810 world->add(std::move(receiver_obj));
811 return world->getReceivers().back().get();
812 }
813
814 /**
815 * @brief Parses the <monostatic> element of the XML document.
816 *
817 * @param monostatic The <monostatic> XmlElement to parse.
818 * @param platform A pointer to the Platform
819 * @param world A pointer to the World
820 * @param masterSeeder The master random number generator for seeding.
821 */
822 void parseMonostatic(const XmlElement& monostatic, Platform* platform, World* world, std::mt19937& masterSeeder)
823 {
824 Transmitter* trans = parseTransmitter(monostatic, platform, world, masterSeeder);
825 Receiver* recv = parseReceiver(monostatic, platform, world, masterSeeder);
826 trans->setAttached(recv);
827 recv->setAttached(trans);
828 }
829
830 /**
831 * @brief Parses the <target> element of the XML document.
832 *
833 * @param target The <target> XmlElement to parse.
834 * @param platform A pointer to the Platform
835 * @param world A pointer to the World
836 * @param masterSeeder The master random number generator for seeding.
837 * @throws XmlException if the target element is missing required attributes or elements.
838 */
839 void parseTarget(const XmlElement& target, Platform* platform, World* world, std::mt19937& masterSeeder)
840 {
841 const std::string name = XmlElement::getSafeAttribute(target, "name");
842
843 const XmlElement rcs_element = target.childElement("rcs", 0);
844 if (!rcs_element.isValid())
845 {
846 throw XmlException("<rcs> element is required in <target>!");
847 }
848
849 const std::string rcs_type = XmlElement::getSafeAttribute(rcs_element, "type");
850
851 std::unique_ptr<Target> target_obj;
852 const unsigned seed = masterSeeder();
853
854 if (rcs_type == "isotropic")
855 {
856 target_obj = createIsoTarget(platform, name, get_child_real_type(rcs_element, "value"), seed);
857 }
858 else if (rcs_type == "file")
859 {
860 target_obj = createFileTarget(platform, name, XmlElement::getSafeAttribute(rcs_element, "filename"), seed);
861 }
862 else
863 {
864 throw XmlException("Unsupported RCS type: " + rcs_type);
865 }
866
867 if (const XmlElement model = target.childElement("model", 0); model.isValid())
868 {
869 if (const std::string model_type = XmlElement::getSafeAttribute(model, "type"); model_type == "constant")
870 {
871 target_obj->setFluctuationModel(std::make_unique<radar::RcsConst>());
872 }
873 else if (model_type == "chisquare" || model_type == "gamma")
874 {
875 target_obj->setFluctuationModel(
876 std::make_unique<radar::RcsChiSquare>(target_obj->getRngEngine(), get_child_real_type(model, "k")));
877 }
878 else
879 {
880 throw XmlException("Unsupported model type: " + model_type);
881 }
882 }
883
884 LOG(Level::DEBUG, "Added target {} with RCS type {} to platform {}", name, rcs_type, platform->getName());
885
886 world->add(std::move(target_obj));
887 }
888
889 void parsePlatformElements(const XmlElement& platform, World* world, Platform* plat, std::mt19937& masterSeeder)
890 {
891 auto parseChildren = [&](const std::string& elementName, auto parseFunc)
892 {
893 unsigned index = 0;
894 while (true)
895 {
896 const XmlElement element = platform.childElement(elementName, index++);
897 if (!element.isValid())
898 {
899 break;
900 }
901 parseFunc(element, plat, world, masterSeeder);
902 }
903 };
904
905 parseChildren("monostatic", parseMonostatic);
906 parseChildren("transmitter", parseTransmitter);
907 parseChildren("receiver", parseReceiver);
908 parseChildren("target", parseTarget);
909 }
910
911 /**
912 * @brief Parses the <platform> element of the XML document.
913 *
914 * @param platform The <platform> XmlElement to parse.
915 * @param world A pointer to the World object where the Platform object is added.
916 * @param masterSeeder The master random number generator for seeding.
917 */
918 void parsePlatform(const XmlElement& platform, World* world, std::mt19937& masterSeeder)
919 {
920 std::string name = XmlElement::getSafeAttribute(platform, "name");
921 auto plat = std::make_unique<Platform>(name);
922
923 parsePlatformElements(platform, world, plat.get(), masterSeeder);
924
925 if (const XmlElement motion_path = platform.childElement("motionpath", 0); motion_path.isValid())
926 {
927 parseMotionPath(motion_path, plat.get());
928 }
929
930 // Parse either <rotationpath> or <fixedrotation>
931 const XmlElement rot_path = platform.childElement("rotationpath", 0);
932
933 if (const XmlElement fixed_rot = platform.childElement("fixedrotation", 0);
934 rot_path.isValid() && fixed_rot.isValid())
935 {
936 LOG(Level::ERROR,
937 "Both <rotationpath> and <fixedrotation> are declared for platform {}. Only <rotationpath> will be "
938 "used.",
939 plat->getName());
940 parseRotationPath(rot_path, plat.get());
941 }
942 else if (rot_path.isValid())
943 {
944 parseRotationPath(rot_path, plat.get());
945 }
946 else if (fixed_rot.isValid())
947 {
948 parseFixedRotation(fixed_rot, plat.get());
949 }
950
951 world->add(std::move(plat));
952 }
953
954 /**
955 * @brief Collects all "include" elements from the XML document and included documents.
956 *
957 * @param doc The XmlDocument to collect "include" elements from.
958 * @param currentDir The current directory of the main XML file.
959 * @param includePaths A vector to store the full paths to the included files.
960 */
961 void collectIncludeElements(const XmlDocument& doc, const fs::path& currentDir, std::vector<fs::path>& includePaths)
962 {
963 unsigned index = 0;
964 while (true)
965 {
966 XmlElement include_element = doc.getRootElement().childElement("include", index++);
967 if (!include_element.isValid())
968 {
969 break;
970 }
971
972 std::string include_filename = include_element.getText();
973 if (include_filename.empty())
974 {
975 LOG(Level::ERROR, "<include> element is missing the filename!");
976 continue;
977 }
978
979 // Construct the full path to the included file
980 fs::path include_path = currentDir / include_filename;
981 includePaths.push_back(include_path);
982
983 XmlDocument included_doc;
984 if (!included_doc.loadFile(include_path.string()))
985 {
986 LOG(Level::ERROR, "Failed to load included XML file: {}", include_path.string());
987 continue;
988 }
989
990 // Recursively collect include elements from the included document
991 collectIncludeElements(included_doc, include_path.parent_path(), includePaths);
992 }
993 }
994
995 /**
996 * @brief Merges the contents of all included documents into the main document.
997 *
998 * @param mainDoc The main XmlDocument to merge the included documents into.
999 * @param currentDir The current directory of the main XML file.
1000 * @return True if any included documents were merged into the main document, false otherwise.
1001 */
1002 bool addIncludeFilesToMainDocument(const XmlDocument& mainDoc, const fs::path& currentDir)
1003 {
1004 std::vector<fs::path> include_paths;
1005 collectIncludeElements(mainDoc, currentDir, include_paths);
1006 bool did_combine = false;
1007
1008 for (const auto& include_path : include_paths)
1009 {
1010 XmlDocument included_doc;
1011 if (!included_doc.loadFile(include_path.string()))
1012 {
1013 throw XmlException("Failed to load included XML file: " + include_path.string());
1014 }
1015
1016 mergeXmlDocuments(mainDoc, included_doc);
1017 did_combine = true;
1018 }
1019
1020 // Remove all include elements from the main document
1021 removeIncludeElements(mainDoc);
1022
1023 return did_combine;
1024 }
1025
1026 /**
1027 * @brief Validates the combined XML document using DTD and XSD schema data.
1028 *
1029 * @param didCombine True if any included documents were merged into the main document, false otherwise.
1030 * @param mainDoc The combined XmlDocument to validate.
1031 */
1032 void validateXml(const bool didCombine, const XmlDocument& mainDoc)
1033 {
1034 LOG(Level::DEBUG, "Validating the{}XML file...", didCombine ? " combined " : " ");
1035 // Validate the combined document using in-memory schema data - DTD validation is less strict than XSD
1036 if (!mainDoc.validateWithDtd(fers_xml_dtd))
1037 {
1038 LOG(Level::FATAL, "{} XML file failed DTD validation!", didCombine ? "Combined" : "Main");
1039 throw XmlException("XML file failed DTD validation!");
1040 }
1041 LOG(Level::DEBUG, "{} XML file passed DTD validation.", didCombine ? "Combined" : "Main");
1042
1043 // Validate the combined document using in-memory schema data - XSD validation is stricter than DTD
1044 if (!mainDoc.validateWithXsd(fers_xml_xsd))
1045 {
1046 LOG(Level::FATAL, "{} XML file failed XSD validation!", didCombine ? "Combined" : "Main");
1047 throw XmlException("XML file failed XSD validation!");
1048 }
1049 LOG(Level::DEBUG, "{} XML file passed XSD validation.", didCombine ? "Combined" : "Main");
1050 }
1051
1052 void processParsedDocument(const XmlDocument& doc, World* world, const fs::path& baseDir,
1053 std::mt19937& masterSeeder)
1054 {
1055 const XmlElement root = doc.getRootElement();
1056 if (root.name() != "simulation")
1057 {
1058 throw XmlException("Root element is not <simulation>!");
1059 }
1060
1061 try
1062 {
1064 if (!params::params.simulation_name.empty())
1065 {
1066 LOG(logging::Level::INFO, "Simulation name set to: {}", params::params.simulation_name);
1067 }
1068 }
1069 catch (const XmlException&)
1070 {
1071 LOG(logging::Level::WARNING, "No 'name' attribute found in <simulation> tag. KML name will default.");
1072 }
1073
1074 parseParameters(root.childElement("parameters", 0));
1075 auto waveform_parser = [&](const XmlElement& p, World* w) { parseWaveform(p, w, baseDir); };
1076 parseElements(root, "waveform", world, waveform_parser);
1077 parseElements(root, "timing", world, parseTiming);
1078 parseElements(root, "antenna", world, parseAntenna);
1079
1080 auto platform_parser = [&](const XmlElement& p, World* w) { parsePlatform(p, w, masterSeeder); };
1081 parseElements(root, "platform", world, platform_parser);
1082
1083 // Prepare CW receiver buffers before starting simulation
1084 const RealType start_time = params::startTime();
1085 const RealType end_time = params::endTime();
1086 const RealType dt_sim = 1.0 / (params::rate() * params::oversampleRatio());
1087 const auto num_samples = static_cast<size_t>(std::ceil((end_time - start_time) / dt_sim));
1088
1089 for (const auto& receiver : world->getReceivers())
1090 {
1091 if (receiver->getMode() == OperationMode::CW_MODE)
1092 {
1093 receiver->prepareCwData(num_samples);
1094 }
1095 }
1096
1097 // Schedule initial events after all objects are loaded
1098 world->scheduleInitialEvents();
1099
1100 LOG(Level::DEBUG, "Initial Event Queue State:\n{}", world->dumpEventQueue());
1101 }
1102}
1103
1104namespace serial
1105{
1106 void parseSimulation(const std::string& filename, World* world, const bool validate, std::mt19937& masterSeeder)
1107 {
1108 world->clear();
1110 XmlDocument main_doc;
1111 if (!main_doc.loadFile(filename))
1112 {
1113 throw XmlException("Failed to load main XML file: " + filename);
1114 }
1115
1116 const fs::path main_dir = fs::path(filename).parent_path();
1117 const bool did_combine = addIncludeFilesToMainDocument(main_doc, main_dir);
1118
1119 if (validate)
1120 {
1121 validateXml(did_combine, main_doc);
1122 }
1123 else
1124 {
1125 LOG(Level::DEBUG, "Skipping XML validation.");
1126 }
1127
1128 processParsedDocument(main_doc, world, main_dir, masterSeeder);
1129 }
1130
1131 void parseSimulationFromString(const std::string& xmlContent, World* world, const bool validate,
1132 std::mt19937& masterSeeder)
1133 {
1134 world->clear();
1136 XmlDocument doc;
1137 if (!doc.loadString(xmlContent))
1138 {
1139 throw XmlException("Failed to parse XML from memory string.");
1140 }
1141
1142 if (validate)
1143 {
1144 // Note: <include> tags are not processed when loading from a string.
1145 validateXml(false, doc);
1146 }
1147 else
1148 {
1149 LOG(Level::DEBUG, "Skipping XML validation.");
1150 }
1151
1152 // When loading from a string, there's no base directory for relative asset paths.
1153 // The UI/caller is responsible for ensuring any paths in the XML are absolute or resolvable.
1154 const fs::path base_dir = ".";
1155
1156 processParsedDocument(doc, world, base_dir, masterSeeder);
1157 }
1158}
Header file defining various types of antennas and their gain patterns.
Class for managing XML documents.
XmlElement getRootElement() const
Get the root element of the document.
bool loadFile(std::string_view filename)
Load an XML file into the document.
bool validateWithDtd(std::span< const unsigned char > dtdData) const
Validate the document using a DTD.
bool validateWithXsd(std::span< const unsigned char > xsdData) const
Validate the document using an XSD schema.
bool loadString(const std::string &content)
Load an XML document from a string in memory.
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::string getSafeAttribute(const XmlElement &element, const std::string_view name)
Get the value of an attribute safely.
std::string_view name() const noexcept
Get the name of the XML element.
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.
The World class manages the simulator environment.
Definition world.h:38
void scheduleInitialEvents()
Populates the event queue with the initial events for the simulation.
Definition world.cpp:98
timing::PrototypeTiming * findTiming(const std::string &name)
Finds a timing source by name.
Definition world.cpp:80
void add(std::unique_ptr< radar::Platform > plat) noexcept
Adds a radar platform to the simulation world.
Definition world.cpp:35
antenna::Antenna * findAntenna(const std::string &name)
Finds an antenna by name.
Definition world.cpp:75
fers_signal::RadarSignal * findWaveform(const std::string &name)
Finds a radar signal by name.
Definition world.cpp:70
void clear() noexcept
Clears all objects and assets from the simulation world.
Definition world.cpp:85
std::string dumpEventQueue() const
Dumps the current state of the event queue to a string for debugging.
Definition world.cpp:180
const std::vector< std::unique_ptr< radar::Transmitter > > & getTransmitters() const noexcept
Retrieves the list of radar transmitters.
Definition world.h:163
const std::vector< std::unique_ptr< radar::Receiver > > & getReceivers() const noexcept
Retrieves the list of radar receivers.
Definition world.h:153
Class representing a radar signal with associated properties.
Represents a path with coordinates and allows for various interpolation methods.
Definition path.h:30
void setInterp(InterpType settype) noexcept
Changes the interpolation type.
Definition path.cpp:158
void addCoord(const Coord &coord) noexcept
Adds a coordinate to the path.
Definition path.cpp:26
void finalize()
Finalizes the path, preparing it for interpolation.
Definition path.cpp:141
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.
A class representing a vector in rectangular coordinates.
Represents a simulation platform with motion and rotation paths.
Definition platform.h:31
math::Path * getMotionPath() const noexcept
Gets the motion path of the platform.
Definition platform.h:59
const std::string & getName() const noexcept
Gets the name of the platform.
Definition platform.h:89
math::RotationPath * getRotationPath() const noexcept
Gets the rotation path of the platform.
Definition platform.h:66
void setAttached(const Radar *obj)
Attaches another radar object to this radar.
Definition radar_obj.cpp:56
Manages radar signal reception and response processing.
Definition receiver.h:36
Base class for radar targets.
Definition target.h:117
Represents a radar transmitter system.
Definition transmitter.h:32
Manages timing properties such as frequency, offsets, and synchronization.
Represents a timing source for simulation.
Definition timing.h:34
Global configuration file for the project.
double RealType
Type for real numbers.
Definition config.h:27
constexpr RealType PI
Mathematical constant π (pi).
Definition config.h:43
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.
Wrapper for managing XML documents and elements using libxml2.
Header file for the logging system.
#define LOG(level,...)
Definition logging.h:19
@ WARNING
Warning level for potentially harmful situations.
@ INFO
Info level for informational messages.
RealType simSamplingRate() noexcept
Get the simulation sampling rate.
Definition parameters.h:103
RealType endTime() noexcept
Get the end time for the simulation.
Definition parameters.h:97
RealType rate() noexcept
Get the rendering sample rate.
Definition parameters.h:109
RealType startTime() noexcept
Get the start time for the simulation.
Definition parameters.h:91
void setTime(const RealType startTime, const RealType endTime) noexcept
Set the start and end times for the simulation.
Definition parameters.h:156
unsigned oversampleRatio() noexcept
Get the oversampling ratio.
Definition parameters.h:139
CoordinateFrame
Defines the coordinate systems supported for scenario definition.
Definition parameters.h:29
@ UTM
Universal Transverse Mercator.
@ ENU
East-North-Up local tangent plane (default)
@ ECEF
Earth-Centered, Earth-Fixed.
void setRate(RealType rateValue)
Set the rendering sample rate.
Definition parameters.h:177
void setOrigin(const double lat, const double lon, const double alt) noexcept
Set the geodetic origin for the KML generator.
Definition parameters.h:228
void setOversampleRatio(unsigned ratio)
Set the oversampling ratio.
Definition parameters.h:212
void setC(RealType cValue) noexcept
Set the speed of light.
Definition parameters.h:145
unsigned adcBits() noexcept
Get the ADC quantization bits.
Definition parameters.h:121
void setSimSamplingRate(const RealType rate) noexcept
Set the simulation sampling rate.
Definition parameters.h:167
void setAdcBits(const unsigned bits) noexcept
Set the ADC quantization bits.
Definition parameters.h:201
void setCoordinateSystem(const CoordinateFrame frame, const int zone, const bool north) noexcept
Set the coordinate system for the scenario.
Definition parameters.h:265
Parameters params
Definition parameters.h:73
RealType c() noexcept
Get the speed of light.
Definition parameters.h:79
std::unique_ptr< Target > createIsoTarget(Platform *platform, std::string name, RealType rcs, unsigned seed)
Creates an isotropic target.
Definition target.h:265
std::vector< SchedulePeriod > processRawSchedule(std::vector< SchedulePeriod > periods, const std::string &ownerName, const bool isPulsed, const RealType pri)
Processes a raw list of schedule periods.
std::unique_ptr< Target > createFileTarget(Platform *platform, std::string name, const std::string &filename, unsigned seed)
Creates a file-based target.
Definition target.h:279
OperationMode
Defines the operational mode of a radar component.
Definition radar_obj.h:36
std::unique_ptr< RadarSignal > loadWaveformFromFile(const std::string &name, const std::string &filename, const RealType power, const RealType carrierFreq)
Loads a radar waveform from a file and returns a RadarSignal object.
void parseSimulationFromString(const std::string &xmlContent, World *world, const bool validate, std::mt19937 &masterSeeder)
void parseSimulation(const std::string &filename, World *world, const bool validate, std::mt19937 &masterSeeder)
Parses a simulation configuration from an XML file.
Defines the Parameters struct and provides methods for managing simulation parameters.
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.
Radar Receiver class for managing signal reception and response handling.
Defines the RotationPath class for handling rotational paths with different interpolation types.
Represents a position in 3D space with an associated time.
Definition coord.h:24
RealType t
Time.
Definition coord.h:26
Vec3 pos
3D position
Definition coord.h:25
Represents a rotation in terms of azimuth, elevation, and time.
Definition coord.h:72
RealType elevation
Elevation angle.
Definition coord.h:74
RealType azimuth
Azimuth angle.
Definition coord.h:73
std::optional< unsigned > random_seed
Random seed for simulation.
Definition parameters.h:58
std::string simulation_name
The name of the simulation, from the XML.
Definition parameters.h:62
void reset() noexcept
Resets the parameters to their default-constructed state.
Definition parameters.h:70
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.
auto get_child_real_type
Helper function to extract a RealType value from an element.
void parseElements(const XmlElement &root, const std::string &elementName, World *world, T parseFunction)
Parses elements with child iteration (e.g., waveforms, timings, antennas).
auto get_attribute_bool
Helper function to extract a boolean value from an attribute.
Header file for parsing XML configuration files for simulation.