15#include <GeographicLib/UTMUPS.hpp>
32#include "fers_xml_dtd.h"
33#include "fers_xml_xsd.h"
48namespace fs = std::filesystem;
84 parseFunction(element, world);
101 throw XmlException(
"Element " + elementName +
" is empty!");
103 return std::stod(text);
122 LOG(Level::WARNING,
"Failed to get boolean value for attribute '{}'. Defaulting to {}.", attributeName,
130 std::vector<radar::SchedulePeriod> parseSchedule(
const XmlElement& parent,
const std::string& parentName,
131 const bool isPulsed,
const RealType pri = 0.0)
133 std::vector<radar::SchedulePeriod> raw_periods;
148 raw_periods.push_back({start, end});
150 catch (
const std::exception& e)
152 LOG(Level::WARNING,
"Failed to parse schedule period for '{}': {}", parentName, e.what());
166 void parseParameters(
const XmlElement& parameters)
172 auto set_param_with_exception_handling = [](
const XmlElement& element,
const std::string& paramName,
174 const std::function<void(
RealType)>& setter)
178 if (paramName ==
"adc_bits" || paramName ==
"oversample")
189 LOG(Level::WARNING,
"Failed to set parameter {}. Using default value. {}", paramName, defaultValue);
199 const auto seed =
static_cast<unsigned>(std::floor(
get_child_real_type(parameters,
"randomseed")));
212 bool origin_set =
false;
223 catch (
const std::exception& e)
225 LOG(Level::WARNING,
"Could not parse origin from XML, using defaults. Error: {}", e.what());
239 if (frame_str ==
"UTM")
245 if (zone < GeographicLib::UTMUPS::MINUTMZONE || zone > GeographicLib::UTMUPS::MAXUTMZONE)
247 throw XmlException(
"UTM zone " + std::to_string(zone) +
" is invalid; must be in [1, 60].");
249 if (hem_str ==
"N" || hem_str ==
"n")
253 else if (hem_str ==
"S" || hem_str ==
"s")
259 throw XmlException(
"UTM hemisphere '" + hem_str +
"' is invalid; must be 'N' or 'S'.");
261 LOG(Level::INFO,
"Coordinate system set to UTM, zone {}{}", zone, north ?
'N' :
'S');
263 else if (frame_str ==
"ECEF")
266 LOG(Level::INFO,
"Coordinate system set to ECEF.");
268 else if (frame_str ==
"ENU")
274 "ENU frame specified but no <origin> tag found. Using default origin at UCT.");
276 LOG(Level::INFO,
"Coordinate system set to ENU local tangent plane.");
280 throw XmlException(
"Unsupported coordinate frame: " + frame_str);
284 catch (
const std::exception& e)
286 LOG(Level::WARNING,
"Could not parse <coordinatesystem> from XML: {}. Defaulting to ENU.", e.what());
299 void parseWaveform(
const XmlElement& waveform,
World* world,
const fs::path& baseDir)
309 fs::path pulse_path(filename_str);
312 if (!fs::exists(pulse_path))
314 pulse_path = baseDir / filename_str;
317 if (!fs::exists(pulse_path))
319 throw XmlException(
"Waveform file not found: " + filename_str);
323 world->
add(std::move(wave));
327 auto cw_signal = std::make_unique<fers_signal::CwSignal>();
329 std::move(cw_signal));
330 world->
add(std::move(wave));
334 LOG(Level::FATAL,
"Unsupported waveform type for '{}'", name);
335 throw XmlException(
"Unsupported waveform type for '" + name +
"'");
349 auto timing_obj = std::make_unique<PrototypeTiming>(name);
351 timing_obj->setFrequency(freq);
353 unsigned noise_index = 0;
356 XmlElement noise_element =
timing.childElement(
"noise_entry", noise_index++);
372 LOG(Level::WARNING,
"Clock section '{}' does not specify frequency offset.", name);
381 LOG(Level::WARNING,
"Clock section '{}' does not specify random frequency offset.", name);
390 LOG(Level::WARNING,
"Clock section '{}' does not specify phase offset.", name);
399 LOG(Level::WARNING,
"Clock section '{}' does not specify random phase offset.", name);
404 timing_obj->setSyncOnPulse();
407 world->
add(std::move(timing_obj));
421 std::unique_ptr<Antenna> ant;
423 LOG(Level::DEBUG,
"Adding antenna '{}' with pattern '{}'", name, pattern);
424 if (pattern ==
"isotropic")
426 ant = std::make_unique<antenna::Isotropic>(name);
428 else if (pattern ==
"sinc")
434 else if (pattern ==
"gaussian")
439 else if (pattern ==
"squarehorn")
443 else if (pattern ==
"parabolic")
447 else if (pattern ==
"xml")
451 else if (pattern ==
"file")
457 LOG(Level::FATAL,
"Unsupported antenna pattern: {}", pattern);
458 throw XmlException(
"Unsupported antenna pattern: " + pattern);
467 LOG(Level::WARNING,
"Antenna '{}' does not specify efficiency, assuming unity.", name);
470 world->
add(std::move(ant));
488 else if (
interp ==
"cubic")
492 else if (
interp ==
"static")
498 LOG(Level::ERROR,
"Unsupported interpolation type: {} for platform {}. Defaulting to static",
interp,
505 LOG(Level::ERROR,
"Failed to set MotionPath interpolation type for platform {}. Defaulting to static",
510 unsigned waypoint_index = 0;
526 LOG(Level::TRACE,
"Added waypoint {} to motion path for platform {}.", waypoint_index,
531 LOG(Level::ERROR,
"Failed to add waypoint to motion path. Discarding waypoint. {}", e.what());
556 else if (
interp ==
"cubic")
560 else if (
interp ==
"static")
571 LOG(Level::ERROR,
"Failed to set RotationPath interpolation type for platform {}. Defaulting to static",
576 unsigned waypoint_index = 0;
587 LOG(Level::TRACE,
"Adding waypoint {} to rotation path for platform {}.", waypoint_index,
596 const RealType az_rad = (90.0 - az_deg) * (
PI / 180.0);
598 const RealType el_rad = el_deg * (
PI / 180.0);
600 path->
addCoord({az_rad, el_rad, time});
604 LOG(Level::ERROR,
"Failed to add waypoint to rotation path. Discarding waypoint. {}", e.what());
631 start.
azimuth = (90.0 - start_az_deg) * (
PI / 180.0);
637 rate.azimuth = -rate_az_deg_s * (
PI / 180.0);
638 rate.elevation = rate_el_deg_s * (
PI / 180.0);
641 LOG(Level::DEBUG,
"Added fixed rotation to platform {}", platform->
getName());
645 LOG(Level::FATAL,
"Failed to set fixed rotation for platform {}. {}", platform->
getName(), e.what());
660 std::mt19937& masterSeeder)
664 const bool is_pulsed = pulsed_mode_element.
isValid();
665 const OperationMode mode = is_pulsed ? OperationMode::PULSED_MODE : OperationMode::CW_MODE;
669 throw XmlException(
"Transmitter '" + name +
"' must specify a radar mode (<pulsed_mode> or <cw_mode>).");
672 auto transmitter_obj = std::make_unique<Transmitter>(platform, name, mode);
678 throw XmlException(
"Waveform '" + waveform_name +
"' not found for transmitter '" + name +
"'");
680 transmitter_obj->setWave(wave);
691 throw XmlException(
"Antenna '" + antenna_name +
"' not found for transmitter '" + name +
"'");
693 transmitter_obj->setAntenna(ant);
696 const auto timing = std::make_shared<Timing>(timing_name, masterSeeder());
700 throw XmlException(
"Timing '" + timing_name +
"' not found for transmitter '" + name +
"'");
702 timing->initializeModel(proto);
703 transmitter_obj->setTiming(
timing);
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())
710 transmitter_obj->setSchedule(std::move(schedule));
713 world->
add(std::move(transmitter_obj));
730 const bool is_pulsed = pulsed_mode_element.
isValid();
731 const OperationMode mode = is_pulsed ? OperationMode::PULSED_MODE : OperationMode::CW_MODE;
733 auto receiver_obj = std::make_unique<Receiver>(platform, name, masterSeeder(), mode);
740 throw XmlException(
"Antenna '" + ant_name +
"' not found for receiver '" + name +
"'");
742 receiver_obj->setAntenna(
antenna);
750 LOG(Level::INFO,
"Receiver '{}' does not specify noise temperature", receiver_obj->getName().c_str());
756 if (window_length <= 0)
758 throw XmlException(
"<window_length> must be positive for receiver '" + name +
"'");
764 throw XmlException(
"<prf> must be positive for receiver '" + name +
"'");
770 throw XmlException(
"<window_skip> must not be negative for receiver '" + name +
"'");
772 receiver_obj->setWindowProperties(window_length, prf, window_skip);
776 throw XmlException(
"Receiver '" + name +
"' must specify a radar mode (<pulsed_mode> or <cw_mode>).");
780 const auto timing = std::make_shared<Timing>(timing_name, masterSeeder());
785 throw XmlException(
"Timing '" + timing_name +
"' not found for receiver '" + name +
"'");
787 timing->initializeModel(proto);
788 receiver_obj->setTiming(
timing);
793 LOG(Level::DEBUG,
"Ignoring direct signals for receiver '{}'", receiver_obj->getName().c_str());
799 LOG(Level::DEBUG,
"Ignoring propagation losses for receiver '{}'", receiver_obj->getName().c_str());
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())
807 receiver_obj->setSchedule(std::move(schedule));
810 world->
add(std::move(receiver_obj));
822 void parseMonostatic(
const XmlElement& monostatic,
Platform* platform,
World* world, std::mt19937& masterSeeder)
824 Transmitter* trans = parseTransmitter(monostatic, platform, world, masterSeeder);
825 Receiver* recv = parseReceiver(monostatic, platform, world, masterSeeder);
846 throw XmlException(
"<rcs> element is required in <target>!");
851 std::unique_ptr<Target> target_obj;
852 const unsigned seed = masterSeeder();
854 if (rcs_type ==
"isotropic")
858 else if (rcs_type ==
"file")
864 throw XmlException(
"Unsupported RCS type: " + rcs_type);
871 target_obj->setFluctuationModel(std::make_unique<radar::RcsConst>());
873 else if (model_type ==
"chisquare" || model_type ==
"gamma")
875 target_obj->setFluctuationModel(
876 std::make_unique<radar::RcsChiSquare>(target_obj->getRngEngine(),
get_child_real_type(model,
"k")));
880 throw XmlException(
"Unsupported model type: " + model_type);
884 LOG(Level::DEBUG,
"Added target {} with RCS type {} to platform {}", name, rcs_type, platform->
getName());
886 world->
add(std::move(target_obj));
889 void parsePlatformElements(
const XmlElement& platform,
World* world,
Platform* plat, std::mt19937& masterSeeder)
891 auto parseChildren = [&](
const std::string& elementName,
auto parseFunc)
901 parseFunc(element, plat, world, masterSeeder);
905 parseChildren(
"monostatic", parseMonostatic);
906 parseChildren(
"transmitter", parseTransmitter);
907 parseChildren(
"receiver", parseReceiver);
908 parseChildren(
"target", parseTarget);
918 void parsePlatform(
const XmlElement& platform,
World* world, std::mt19937& masterSeeder)
921 auto plat = std::make_unique<Platform>(name);
923 parsePlatformElements(platform, world, plat.get(), masterSeeder);
927 parseMotionPath(motion_path, plat.get());
934 rot_path.
isValid() && fixed_rot.isValid())
937 "Both <rotationpath> and <fixedrotation> are declared for platform {}. Only <rotationpath> will be "
940 parseRotationPath(rot_path, plat.get());
944 parseRotationPath(rot_path, plat.get());
946 else if (fixed_rot.isValid())
948 parseFixedRotation(fixed_rot, plat.get());
951 world->
add(std::move(plat));
961 void collectIncludeElements(
const XmlDocument& doc,
const fs::path& currentDir, std::vector<fs::path>& includePaths)
967 if (!include_element.
isValid())
972 std::string include_filename = include_element.
getText();
973 if (include_filename.empty())
975 LOG(Level::ERROR,
"<include> element is missing the filename!");
980 fs::path include_path = currentDir / include_filename;
981 includePaths.push_back(include_path);
984 if (!included_doc.
loadFile(include_path.string()))
986 LOG(Level::ERROR,
"Failed to load included XML file: {}", include_path.string());
991 collectIncludeElements(included_doc, include_path.parent_path(), includePaths);
1002 bool addIncludeFilesToMainDocument(
const XmlDocument& mainDoc,
const fs::path& currentDir)
1004 std::vector<fs::path> include_paths;
1005 collectIncludeElements(mainDoc, currentDir, include_paths);
1006 bool did_combine =
false;
1008 for (
const auto& include_path : include_paths)
1011 if (!included_doc.
loadFile(include_path.string()))
1013 throw XmlException(
"Failed to load included XML file: " + include_path.string());
1032 void validateXml(
const bool didCombine,
const XmlDocument& mainDoc)
1034 LOG(Level::DEBUG,
"Validating the{}XML file...", didCombine ?
" combined " :
" ");
1038 LOG(Level::FATAL,
"{} XML file failed DTD validation!", didCombine ?
"Combined" :
"Main");
1041 LOG(Level::DEBUG,
"{} XML file passed DTD validation.", didCombine ?
"Combined" :
"Main");
1046 LOG(Level::FATAL,
"{} XML file failed XSD validation!", didCombine ?
"Combined" :
"Main");
1049 LOG(Level::DEBUG,
"{} XML file passed XSD validation.", didCombine ?
"Combined" :
"Main");
1052 void processParsedDocument(
const XmlDocument& doc,
World* world,
const fs::path& baseDir,
1053 std::mt19937& masterSeeder)
1056 if (root.
name() !=
"simulation")
1058 throw XmlException(
"Root element is not <simulation>!");
1075 auto waveform_parser = [&](
const XmlElement& p,
World* w) { parseWaveform(p, w, baseDir); };
1080 auto platform_parser = [&](
const XmlElement& p,
World* w) { parsePlatform(p, w, masterSeeder); };
1087 const auto num_samples =
static_cast<size_t>(std::ceil((end_time - start_time) / dt_sim));
1089 for (
const auto& receiver : world->getReceivers())
1091 if (receiver->getMode() == OperationMode::CW_MODE)
1093 receiver->prepareCwData(num_samples);
1113 throw XmlException(
"Failed to load main XML file: " + filename);
1116 const fs::path main_dir = fs::path(filename).parent_path();
1117 const bool did_combine = addIncludeFilesToMainDocument(main_doc, main_dir);
1121 validateXml(did_combine, main_doc);
1125 LOG(Level::DEBUG,
"Skipping XML validation.");
1128 processParsedDocument(main_doc, world, main_dir, masterSeeder);
1132 std::mt19937& masterSeeder)
1139 throw XmlException(
"Failed to parse XML from memory string.");
1145 validateXml(
false, doc);
1149 LOG(Level::DEBUG,
"Skipping XML validation.");
1154 const fs::path base_dir =
".";
1156 processParsedDocument(doc, world, base_dir, masterSeeder);
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.
void scheduleInitialEvents()
Populates the event queue with the initial events for the simulation.
timing::PrototypeTiming * findTiming(const std::string &name)
Finds a timing source by name.
void add(std::unique_ptr< radar::Platform > plat) noexcept
Adds a radar platform to the simulation world.
antenna::Antenna * findAntenna(const std::string &name)
Finds an antenna by name.
fers_signal::RadarSignal * findWaveform(const std::string &name)
Finds a radar signal by name.
void clear() noexcept
Clears all objects and assets from the simulation world.
std::string dumpEventQueue() const
Dumps the current state of the event queue to a string for debugging.
const std::vector< std::unique_ptr< radar::Transmitter > > & getTransmitters() const noexcept
Retrieves the list of radar transmitters.
const std::vector< std::unique_ptr< radar::Receiver > > & getReceivers() const noexcept
Retrieves the list of radar receivers.
Class representing a radar signal with associated properties.
Represents a path with coordinates and allows for various interpolation methods.
void setInterp(InterpType settype) noexcept
Changes the interpolation type.
void addCoord(const Coord &coord) noexcept
Adds a coordinate to the path.
void finalize()
Finalizes the path, preparing it for interpolation.
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.
void setAttached(const Radar *obj)
Attaches another radar object to this radar.
Manages radar signal reception and response processing.
Base class for radar targets.
Represents a radar transmitter system.
Manages timing properties such as frequency, offsets, and synchronization.
Represents a timing source for simulation.
Global configuration file for the project.
double RealType
Type for real numbers.
constexpr RealType PI
Mathematical constant π (pi).
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.
@ WARNING
Warning level for potentially harmful situations.
@ INFO
Info level for informational messages.
RealType simSamplingRate() noexcept
Get the simulation sampling rate.
RealType endTime() noexcept
Get the end time for the simulation.
RealType rate() noexcept
Get the rendering sample rate.
RealType startTime() noexcept
Get the start time for the simulation.
void setTime(const RealType startTime, const RealType endTime) noexcept
Set the start and end times for the simulation.
unsigned oversampleRatio() noexcept
Get the oversampling ratio.
CoordinateFrame
Defines the coordinate systems supported for scenario definition.
@ 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.
void setOrigin(const double lat, const double lon, const double alt) noexcept
Set the geodetic origin for the KML generator.
void setOversampleRatio(unsigned ratio)
Set the oversampling ratio.
void setC(RealType cValue) noexcept
Set the speed of light.
unsigned adcBits() noexcept
Get the ADC quantization bits.
void setSimSamplingRate(const RealType rate) noexcept
Set the simulation sampling rate.
void setAdcBits(const unsigned bits) noexcept
Set the ADC quantization bits.
void setCoordinateSystem(const CoordinateFrame frame, const int zone, const bool north) noexcept
Set the coordinate system for the scenario.
RealType c() noexcept
Get the speed of light.
std::unique_ptr< Target > createIsoTarget(Platform *platform, std::string name, RealType rcs, unsigned seed)
Creates an isotropic target.
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.
OperationMode
Defines the operational mode of a radar component.
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 ...
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.
Represents a rotation in terms of azimuth, elevation, and time.
RealType elevation
Elevation angle.
RealType azimuth
Azimuth angle.
std::optional< unsigned > random_seed
Random seed for simulation.
std::string simulation_name
The name of the simulation, from the XML.
void reset() noexcept
Resets the parameters to their default-constructed state.
Defines classes for radar targets and their Radar Cross-Section (RCS) models.
Timing source for simulation objects.
Header file for the Transmitter class in the radar namespace.
Header file for the World class in the simulator.
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.