FERS 1.0.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
serial Namespace Reference

Classes

class  KmlGenerator
 Generates KML files from FERS simulation scenarios for geographical visualization. More...
 
class  Response
 Manages radar signal responses from a transmitter. More...
 

Functions

std::vector< std::vector< RealType > > readPattern (const std::string &name, const std::string &datasetName)
 Reads a 2D pattern dataset from an HDF5 file.
 
void readPulseData (const std::string &name, std::vector< ComplexType > &data)
 Reads pulse data from an HDF5 file.
 
void addChunkToFile (HighFive::File &file, const std::vector< ComplexType > &data, RealType time, RealType fullscale, unsigned count)
 Adds a chunk of data to an HDF5 file.
 
nlohmann::json world_to_json (const core::World &world)
 Serializes the entire simulation world into a nlohmann::json object.
 
void json_to_world (const nlohmann::json &j, core::World &world, std::mt19937 &masterSeeder)
 Deserializes a nlohmann::json object and reconstructs the simulation world.
 
std::unique_ptr< RadarSignalloadWaveformFromFile (const std::string &name, const std::string &filename, RealType power, RealType carrierFreq)
 Loads a radar waveform from a file and returns a RadarSignal object.
 
void parseSimulation (const std::string &filename, core::World *world, bool validate, std::mt19937 &masterSeeder)
 Parses a simulation configuration from an XML file.
 
void parseSimulationFromString (const std::string &xmlContent, World *world, const bool validate, std::mt19937 &masterSeeder)
 
std::string world_to_xml_string (const core::World &world)
 Serializes the entire simulation world into an XML formatted string.
 

Function Documentation

◆ addChunkToFile()

void serial::addChunkToFile ( HighFive::File &  file,
const std::vector< ComplexType > &  data,
RealType  time,
RealType  fullscale,
unsigned  count 
)

Adds a chunk of data to an HDF5 file.

Parameters
fileThe HDF5 file where the chunk is written.
dataA vector of complex data to be written.
timeThe time attribute associated with the chunk.
fullscaleThe fullscale attribute for the chunk.
countThe sequential count number for chunk naming.
Exceptions
std::runtime_errorIf there is an error writing data or setting attributes.

Definition at line 76 of file hdf5_handler.cpp.

78 {
79 const unsigned size = data.size();
80
81 const std::string base_chunk_name = "chunk_" + std::format("{:06}", count);
82 const std::string i_chunk_name = base_chunk_name + "_I";
83 const std::string q_chunk_name = base_chunk_name + "_Q";
84
85 std::vector<RealType> i(size), q(size);
86 std::ranges::transform(data, i.begin(), [](const ComplexType& c) { return c.real(); });
87 std::ranges::transform(data, q.begin(), [](const ComplexType& c) { return c.imag(); });
88
89 auto write_chunk = [&](const std::string& chunkName, const std::vector<RealType>& chunkData)
90 {
91 try
92 {
93 HighFive::DataSet dataset =
94 file.createDataSet<RealType>(chunkName, HighFive::DataSpace::From(chunkData));
95 dataset.write(chunkData);
96 }
97 catch (const HighFive::Exception& err)
98 {
99 LOG(Level::FATAL, "Error while writing data to HDF5 file: {}", err.what());
100 throw std::runtime_error("Error while writing data to HDF5 file: " + chunkName + " - " + err.what());
101 }
102 };
103
104 auto set_chunk_attributes = [&](const std::string& chunkName)
105 {
106 try
107 {
108 HighFive::DataSet dataset = file.getDataSet(chunkName);
109 dataset.createAttribute("time", time);
110 dataset.createAttribute("rate", params::rate());
111 dataset.createAttribute("fullscale", fullscale);
112 }
113 catch (const HighFive::Exception& err)
114 {
115 LOG(Level::FATAL, "Error while setting attributes on chunk: {}", err.what());
116 throw std::runtime_error("Error while setting attributes on chunk: " + chunkName + " - " + err.what());
117 }
118 };
119
120 write_chunk(i_chunk_name, i);
121 write_chunk(q_chunk_name, q);
122
123 set_chunk_attributes(i_chunk_name);
124 set_chunk_attributes(q_chunk_name);
125 }
double RealType
Type for real numbers.
Definition config.h:27
std::complex< RealType > ComplexType
Type for complex numbers.
Definition config.h:35
#define LOG(level,...)
Definition logging.h:19
RealType rate() noexcept
Get the rendering sample rate.
Definition parameters.h:109

References LOG, and params::rate().

Referenced by processing::runPulsedFinalizer().

+ Here is the call graph for this function:
+ Here is the caller graph for this function:

◆ json_to_world()

void serial::json_to_world ( const nlohmann::json &  j,
core::World world,
std::mt19937 &  masterSeeder 
)

Deserializes a nlohmann::json object and reconstructs the simulation world.

This function is the counterpart to world_to_json. It performs a full state replacement by clearing the existing world and rebuilding it from the provided JSON. This "replace" strategy simplifies state management, guaranteeing that the C++ core is always perfectly synchronized with the state provided by the UI without requiring complex diffing or patching logic. It also handles re-seeding the master random number generator to ensure that loading a state also restores its deterministic behavior.

Parameters
jThe json object to deserialize.
worldThe world object to populate.
masterSeederA reference to the master random number generator, which will be re-seeded.

Definition at line 650 of file json_serializer.cpp.

651 {
652 // 1. Clear the existing world state. This function always performs a full
653 // replacement to ensure the C++ state is a perfect mirror of the UI state.
654 world.clear();
655
656 const auto& sim = j.at("simulation");
657
658 auto new_params = sim.at("parameters").get<params::Parameters>();
659
660 // If a random seed is present in the incoming JSON, it is used to re-seed
661 // the master generator. This is crucial for allowing the UI to control
662 // simulation reproducibility.
663 if (sim.at("parameters").contains("randomseed"))
664 {
665 params::params.random_seed = new_params.random_seed;
666 if (params::params.random_seed)
667 {
668 LOG(logging::Level::INFO, "Master seed updated from JSON to: {}", *params::params.random_seed);
669 masterSeeder.seed(*params::params.random_seed);
670 }
671 }
672
673 new_params.random_seed = params::params.random_seed;
674 params::params = new_params;
675
676 params::params.simulation_name = sim.value("name", "");
677
678 // 2. Restore assets (Waveforms, Antennas, Timings). This order is critical
679 // because platforms, which are restored next, will reference these
680 // assets by name. The assets must exist before they can be linked.
681 if (sim.contains("waveforms"))
682 {
683 for (auto waveforms = sim.at("waveforms").get<std::vector<std::unique_ptr<fers_signal::RadarSignal>>>();
684 auto& waveform : waveforms)
685 {
686 // Only add valid waveforms. If filename was empty, waveform is nullptr.
687 if (waveform)
688 {
689 world.add(std::move(waveform));
690 }
691 }
692 }
693
694 if (sim.contains("antennas"))
695 {
696 for (auto antennas = sim.at("antennas").get<std::vector<std::unique_ptr<antenna::Antenna>>>();
697 auto& antenna : antennas)
698 {
699 // Only add valid antennas.
700 if (antenna)
701 {
702 world.add(std::move(antenna));
703 }
704 }
705 }
706
707 if (sim.contains("timings"))
708 {
709 for (const auto& timing_json : sim.at("timings"))
710 {
711 auto name = timing_json.at("name").get<std::string>();
712 auto timing_obj = std::make_unique<timing::PrototypeTiming>(name);
713 from_json(timing_json, *timing_obj);
714 world.add(std::move(timing_obj));
715 }
716 }
717
718 // 3. Restore platforms and their components.
719 if (sim.contains("platforms"))
720 {
721 for (const auto& plat_json : sim.at("platforms"))
722 {
723 auto name = plat_json.at("name").get<std::string>();
724 auto plat = std::make_unique<radar::Platform>(name);
725
726 // Paths
727 if (plat_json.contains("motionpath"))
728 {
729 auto path = std::make_unique<math::Path>();
730 from_json(plat_json.at("motionpath"), *path);
731 plat->setMotionPath(std::move(path));
732 }
733 if (plat_json.contains("rotationpath"))
734 {
735 auto rot_path = std::make_unique<math::RotationPath>();
736 from_json(plat_json.at("rotationpath"), *rot_path);
737 plat->setRotationPath(std::move(rot_path));
738 }
739 else if (plat_json.contains("fixedrotation"))
740 {
741 // This logic reconstructs a constant-rate rotation path from the
742 // JSON representation that corresponds to the <fixedrotation> XML element.
743 auto rot_path = std::make_unique<math::RotationPath>();
744 const auto& fixed_json = plat_json.at("fixedrotation");
745 const RealType start_az_deg = fixed_json.at("startazimuth").get<RealType>();
746 const RealType start_el_deg = fixed_json.at("startelevation").get<RealType>();
747 const RealType rate_az_deg_s = fixed_json.at("azimuthrate").get<RealType>();
748 const RealType rate_el_deg_s = fixed_json.at("elevationrate").get<RealType>();
749
751 start.azimuth = (90.0 - start_az_deg) * (PI / 180.0);
752 start.elevation = start_el_deg * (PI / 180.0);
753 rate.azimuth = -rate_az_deg_s * (PI / 180.0);
754 rate.elevation = rate_el_deg_s * (PI / 180.0);
755 rot_path->setConstantRate(start, rate);
756 rot_path->finalize();
757 plat->setRotationPath(std::move(rot_path));
758 }
759
760 // Components - Strict array format
761 if (plat_json.contains("components"))
762 {
763 for (const auto& comp_json_outer : plat_json.at("components"))
764 {
765 if (comp_json_outer.contains("transmitter"))
766 {
767 const auto& comp_json = comp_json_outer.at("transmitter");
768
769 // --- Dependency Check ---
770 // Validate Waveform and Timing existence before creation to prevent core crashes.
771 const auto wave_name = comp_json.value("waveform", "");
772 const auto timing_name = comp_json.value("timing", "");
773 const auto antenna_name = comp_json.value("antenna", "");
774
775 if (wave_name.empty() || !world.findWaveform(wave_name))
776 {
778 "Skipping Transmitter '{}': Missing or invalid waveform '{}'.",
779 comp_json.value("name", "Unnamed"), wave_name);
780 continue;
781 }
782 if (timing_name.empty() || !world.findTiming(timing_name))
783 {
785 "Skipping Transmitter '{}': Missing or invalid timing source '{}'.",
786 comp_json.value("name", "Unnamed"), timing_name);
787 continue;
788 }
789 if (antenna_name.empty() || !world.findAntenna(antenna_name))
790 {
792 "Skipping Transmitter '{}': Missing or invalid antenna '{}'.",
793 comp_json.value("name", "Unnamed"), antenna_name);
794 continue;
795 }
796
798 if (comp_json.contains("pulsed_mode"))
799 {
801 }
802 else if (comp_json.contains("cw_mode"))
803 {
805 }
806 else
807 {
808 throw std::runtime_error("Transmitter component '" +
809 comp_json.value("name", "Unnamed") +
810 "' must have a 'pulsed_mode' or 'cw_mode' block.");
811 }
812
813 auto trans = std::make_unique<radar::Transmitter>(plat.get(),
814 comp_json.value("name", "Unnamed"), mode);
815 if (mode == radar::OperationMode::PULSED_MODE && comp_json.contains("pulsed_mode"))
816 {
817 trans->setPrf(comp_json.at("pulsed_mode").value("prf", 0.0));
818 }
819
820 trans->setWave(world.findWaveform(wave_name));
821 trans->setAntenna(world.findAntenna(antenna_name));
822
823 if (const auto timing_proto = world.findTiming(timing_name))
824 {
825 const auto timing = std::make_shared<timing::Timing>(timing_name, masterSeeder());
826 timing->initializeModel(timing_proto);
827 trans->setTiming(timing);
828 }
829
830 if (comp_json.contains("schedule"))
831 {
832 auto raw = comp_json.at("schedule").get<std::vector<radar::SchedulePeriod>>();
833 RealType pri = 0.0;
835 {
836 pri = 1.0 / trans->getPrf();
837 }
838 trans->setSchedule(radar::processRawSchedule(
839 std::move(raw), trans->getName(), mode == radar::OperationMode::PULSED_MODE, pri));
840 }
841
842 world.add(std::move(trans));
843 }
844 else if (comp_json_outer.contains("receiver"))
845 {
846 const auto& comp_json = comp_json_outer.at("receiver");
847
848 // --- Dependency Check ---
849 // Receiver strictly requires a Timing source.
850 const auto timing_name = comp_json.value("timing", "");
851 const auto antenna_name = comp_json.value("antenna", "");
852
853 if (timing_name.empty() || !world.findTiming(timing_name))
854 {
856 "Skipping Receiver '{}': Missing or invalid timing source '{}'.",
857 comp_json.value("name", "Unnamed"), timing_name);
858 continue;
859 }
860
861 if (!antenna_name.empty() && !world.findAntenna(antenna_name))
862 {
863 LOG(logging::Level::WARNING, "Skipping Receiver '{}': Missing or invalid antenna '{}'.",
864 comp_json.value("name", "Unnamed"), antenna_name);
865 continue;
866 }
867
869 if (comp_json.contains("pulsed_mode"))
870 {
872 }
873 else if (comp_json.contains("cw_mode"))
874 {
876 }
877 else
878 {
879 throw std::runtime_error("Receiver component '" + comp_json.value("name", "Unnamed") +
880 "' must have a 'pulsed_mode' or 'cw_mode' block.");
881 }
882
883 auto recv = std::make_unique<radar::Receiver>(
884 plat.get(), comp_json.value("name", "Unnamed"), masterSeeder(), mode);
885 if (mode == radar::OperationMode::PULSED_MODE && comp_json.contains("pulsed_mode"))
886 {
887 const auto& mode_json = comp_json.at("pulsed_mode");
888 recv->setWindowProperties(mode_json.value("window_length", 0.0),
889 mode_json.value("prf", 0.0),
890 mode_json.value("window_skip", 0.0));
891 }
892
893 recv->setNoiseTemperature(comp_json.value("noise_temp", 0.0));
894
895 recv->setAntenna(world.findAntenna(antenna_name));
896
897 if (const auto timing_proto = world.findTiming(timing_name))
898 {
899 const auto timing = std::make_shared<timing::Timing>(timing_name, masterSeeder());
900 timing->initializeModel(timing_proto);
901 recv->setTiming(timing);
902 }
903
904 if (comp_json.value("nodirect", false))
905 {
907 }
908 if (comp_json.value("nopropagationloss", false))
909 {
911 }
912
913 if (comp_json.contains("schedule"))
914 {
915 auto raw = comp_json.at("schedule").get<std::vector<radar::SchedulePeriod>>();
916 RealType pri = 0.0;
918 {
919 pri = 1.0 / recv->getWindowPrf();
920 }
921 recv->setSchedule(radar::processRawSchedule(
922 std::move(raw), recv->getName(), mode == radar::OperationMode::PULSED_MODE, pri));
923 }
924
925 world.add(std::move(recv));
926 }
927 if (comp_json_outer.contains("target"))
928 {
929 const auto& comp_json = comp_json_outer.at("target");
930 const auto& rcs_json = comp_json.at("rcs");
931 const auto rcs_type = rcs_json.at("type").get<std::string>();
932 std::unique_ptr<radar::Target> target_obj;
933
934 if (rcs_type == "isotropic")
935 {
936 target_obj =
937 radar::createIsoTarget(plat.get(), comp_json.at("name").get<std::string>(),
938 rcs_json.at("value").get<RealType>(), masterSeeder());
939 }
940 else if (rcs_type == "file")
941 {
942 const auto filename = rcs_json.value("filename", "");
943 if (filename.empty())
944 {
946 "Skipping load of file target '{}': RCS filename is empty.",
947 comp_json.value("name", "Unknown"));
948 continue;
949 }
950 target_obj = radar::createFileTarget(
951 plat.get(), comp_json.at("name").get<std::string>(), filename, masterSeeder());
952 }
953 else
954 {
955 throw std::runtime_error("Unsupported target RCS type: " + rcs_type);
956 }
957 world.add(std::move(target_obj));
958
959 // After creating the target, check for and apply the fluctuation model.
960 if (comp_json.contains("model"))
961 {
962 const auto& model_json = comp_json.at("model");
963 if (const auto model_type = model_json.at("type").get<std::string>();
964 model_type == "chisquare" || model_type == "gamma")
965 {
966 auto model = std::make_unique<radar::RcsChiSquare>(
967 world.getTargets().back()->getRngEngine(), model_json.at("k").get<RealType>());
968 world.getTargets().back()->setFluctuationModel(std::move(model));
969 }
970 // "constant" is the default, so no action is needed if that's the type.
971 }
972 }
973 else if (comp_json_outer.contains("monostatic"))
974 {
975 // This block reconstructs the internal C++ representation of a
976 // monostatic radar (a linked Transmitter and Receiver) from the
977 // single 'monostatic' component in the JSON.
978 const auto& comp_json = comp_json_outer.at("monostatic");
979
980 // --- Dependency Check ---
981 const auto wave_name = comp_json.value("waveform", "");
982 const auto timing_name = comp_json.value("timing", "");
983 const auto antenna_name = comp_json.value("antenna", "");
984
985 if (wave_name.empty() || !world.findWaveform(wave_name))
986 {
988 "Skipping Monostatic '{}': Missing or invalid waveform '{}'.",
989 comp_json.value("name", "Unnamed"), wave_name);
990 continue;
991 }
992 if (timing_name.empty() || !world.findTiming(timing_name))
993 {
995 "Skipping Monostatic '{}': Missing or invalid timing source '{}'.",
996 comp_json.value("name", "Unnamed"), timing_name);
997 continue;
998 }
999 if (antenna_name.empty() || !world.findAntenna(antenna_name))
1000 {
1002 "Skipping Monostatic '{}': Missing or invalid antenna '{}'.",
1003 comp_json.value("name", "Unnamed"), antenna_name);
1004 continue;
1005 }
1006
1008 if (comp_json.contains("pulsed_mode"))
1009 {
1011 }
1012 else if (comp_json.contains("cw_mode"))
1013 {
1015 }
1016 else
1017 {
1018 throw std::runtime_error("Monostatic component '" + comp_json.value("name", "Unnamed") +
1019 "' must have a 'pulsed_mode' or 'cw_mode' block.");
1020 }
1021
1022 // Transmitter part
1023 auto trans = std::make_unique<radar::Transmitter>(plat.get(),
1024 comp_json.value("name", "Unnamed"), mode);
1025 if (mode == radar::OperationMode::PULSED_MODE && comp_json.contains("pulsed_mode"))
1026 {
1027 trans->setPrf(comp_json.at("pulsed_mode").value("prf", 0.0));
1028 }
1029
1030 trans->setWave(world.findWaveform(wave_name));
1031 trans->setAntenna(world.findAntenna(antenna_name));
1032 const auto tx_timing_proto = world.findTiming(timing_name);
1033 if (tx_timing_proto)
1034 {
1035 const auto tx_timing = std::make_shared<timing::Timing>(timing_name, masterSeeder());
1036 tx_timing->initializeModel(tx_timing_proto);
1037 trans->setTiming(tx_timing);
1038 }
1039
1040 // Receiver part
1041 auto recv = std::make_unique<radar::Receiver>(
1042 plat.get(), comp_json.value("name", "Unnamed"), masterSeeder(), mode);
1043 if (mode == radar::OperationMode::PULSED_MODE && comp_json.contains("pulsed_mode"))
1044 {
1045 const auto& mode_json = comp_json.at("pulsed_mode");
1046 recv->setWindowProperties(mode_json.value("window_length", 0.0),
1047 trans->getPrf(), // Use transmitter's PRF
1048 mode_json.value("window_skip", 0.0));
1049 }
1050 recv->setNoiseTemperature(comp_json.value("noise_temp", 0.0));
1051
1052 recv->setAntenna(world.findAntenna(antenna_name));
1053 const auto rx_timing_proto = world.findTiming(timing_name);
1054 if (rx_timing_proto)
1055 {
1056 const auto rx_timing = std::make_shared<timing::Timing>(timing_name, masterSeeder());
1057 rx_timing->initializeModel(rx_timing_proto);
1058 recv->setTiming(rx_timing);
1059 }
1060
1061 if (comp_json.value("nodirect", false))
1062 {
1064 }
1065 if (comp_json.value("nopropagationloss", false))
1066 {
1068 }
1069 if (comp_json.contains("schedule"))
1070 {
1071 auto raw = comp_json.at("schedule").get<std::vector<radar::SchedulePeriod>>();
1072 RealType pri = 0.0;
1074 {
1075 pri = 1.0 / trans->getPrf();
1076 }
1077
1078 // Process once, apply to both
1079 auto processed_schedule = radar::processRawSchedule(
1080 std::move(raw), trans->getName(), mode == radar::OperationMode::PULSED_MODE, pri);
1081
1082 trans->setSchedule(processed_schedule);
1083 recv->setSchedule(processed_schedule);
1084 }
1085
1086 // Link them and add to world
1087 trans->setAttached(recv.get());
1088 recv->setAttached(trans.get());
1089 world.add(std::move(trans));
1090 world.add(std::move(recv));
1091 }
1092 }
1093 }
1094
1095 world.add(std::move(plat));
1096 }
1097 }
1098
1099 // 4. Finalize world state after all objects are loaded.
1100
1101 // Prepare CW receiver buffers before starting simulation
1102 const RealType start_time = params::startTime();
1103 const RealType end_time = params::endTime();
1104 const RealType dt_sim = 1.0 / (params::rate() * params::oversampleRatio());
1105 const auto num_samples = static_cast<size_t>(std::ceil((end_time - start_time) / dt_sim));
1106
1107 for (const auto& receiver : world.getReceivers())
1108 {
1109 if (receiver->getMode() == radar::OperationMode::CW_MODE)
1110 {
1111 receiver->prepareCwData(num_samples);
1112 }
1113 }
1114
1115 // Schedule initial events after all objects are loaded.
1116 world.scheduleInitialEvents();
1117 }
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
const std::vector< std::unique_ptr< radar::Target > > & getTargets() const noexcept
Retrieves the list of radar targets.
Definition world.h:143
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
constexpr RealType PI
Mathematical constant π (pi).
Definition config.h:43
@ WARNING
Warning level for potentially harmful situations.
@ INFO
Info level for informational messages.
void from_json(const nlohmann::json &j, Vec3 &v)
RealType endTime() noexcept
Get the end time for the simulation.
Definition parameters.h:97
RealType startTime() noexcept
Get the start time for the simulation.
Definition parameters.h:91
unsigned oversampleRatio() noexcept
Get the oversampling ratio.
Definition parameters.h:139
Parameters params
Definition parameters.h:73
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
@ PULSED_MODE
The component operates in a pulsed mode.
@ CW_MODE
The component operates in a continuous-wave mode.
Represents a rotation in terms of azimuth, elevation, and time.
Definition coord.h:72
Struct to hold simulation parameters.
Definition parameters.h:40
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

References core::World::add(), core::World::clear(), radar::createFileTarget(), radar::createIsoTarget(), radar::CW_MODE, params::endTime(), core::World::findAntenna(), core::World::findTiming(), core::World::findWaveform(), radar::Receiver::FLAG_NODIRECT, radar::Receiver::FLAG_NOPROPLOSS, core::World::getReceivers(), core::World::getTargets(), logging::INFO, LOG, params::oversampleRatio(), params::params, PI, radar::processRawSchedule(), radar::PULSED_MODE, params::Parameters::random_seed, params::rate(), core::World::scheduleInitialEvents(), params::Parameters::simulation_name, params::startTime(), and logging::WARNING.

Referenced by fers_update_scenario_from_json().

+ Here is the call graph for this function:
+ Here is the caller graph for this function:

◆ loadWaveformFromFile()

std::unique_ptr< fers_signal::RadarSignal > serial::loadWaveformFromFile ( const std::string &  name,
const std::string &  filename,
RealType  power,
RealType  carrierFreq 
)

Loads a radar waveform from a file and returns a RadarSignal object.

Parameters
nameThe name of the radar signal.
filenameThe path to the file containing the waveform data.
powerThe power of the radar signal in the waveform.
carrierFreqThe carrier frequency of the radar signal.
Returns
A unique pointer to a RadarSignal object loaded with the waveform data.
Exceptions
std::runtime_errorIf the file cannot be opened or the file format is unrecognized.

Definition at line 115 of file waveform_factory.cpp.

117 {
118 const std::filesystem::path filepath = filename;
119 const auto extension = filepath.extension().string();
120
121 if (hasExtension(extension, ".csv"))
122 {
123 auto wave = loadWaveformFromCsvFile(name, filepath, power, carrierFreq);
124 wave->setFilename(filename);
125 return wave;
126 }
127 if (hasExtension(extension, ".h5"))
128 {
129 auto wave = loadWaveformFromHdf5File(name, filepath, power, carrierFreq);
130 wave->setFilename(filename);
131 return wave;
132 }
133
134 LOG(logging::Level::FATAL, "Unrecognized file extension '{}' for file: '{}'", extension, filename);
135 throw std::runtime_error("Unrecognized file extension '" + extension + "' for file: " + filename);
136 }
@ FATAL
Fatal level for severe error events.

References logging::FATAL, and LOG.

Referenced by fers_signal::from_json().

+ Here is the caller graph for this function:

◆ parseSimulation()

void serial::parseSimulation ( const std::string &  filename,
core::World world,
bool  validate,
std::mt19937 &  masterSeeder 
)

Parses a simulation configuration from an XML file.

This function loads an XML file, merges any included files, validates it against the FERS DTD and XSD schemas, and then populates the simulation World object with the parsed parameters and components.

Parameters
filenameThe path to the main XML simulation script.
worldA pointer to the World object to be populated.
validateA boolean indicating whether to perform XML validation.
masterSeederA reference to the master random number generator used for seeding components.
Exceptions
XmlExceptionif the XML is malformed, fails validation, or contains invalid data.
std::runtime_errorfor file I/O errors or other critical issues.

Definition at line 1106 of file xml_parser.cpp.

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 }
Class for managing XML documents.
bool loadFile(std::string_view filename)
Load an XML file into the document.
Exception class for handling XML-related errors.
void reset() noexcept
Resets the parameters to their default-constructed state.
Definition parameters.h:70

References core::World::clear(), XmlDocument::loadFile(), LOG, params::params, and params::Parameters::reset().

Referenced by fers_load_scenario_from_xml_file().

+ Here is the call graph for this function:
+ Here is the caller graph for this function:

◆ parseSimulationFromString()

void serial::parseSimulationFromString ( const std::string &  xmlContent,
World world,
const bool  validate,
std::mt19937 &  masterSeeder 
)

Definition at line 1131 of file xml_parser.cpp.

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 }
bool loadString(const std::string &content)
Load an XML document from a string in memory.

References core::World::clear(), XmlDocument::loadString(), LOG, params::params, and params::Parameters::reset().

Referenced by fers_load_scenario_from_xml_string().

+ Here is the call graph for this function:
+ Here is the caller graph for this function:

◆ readPattern()

std::vector< std::vector< RealType > > serial::readPattern ( const std::string &  name,
const std::string &  datasetName 
)

Reads a 2D pattern dataset from an HDF5 file.

Parameters
nameThe name of the HDF5 file.
datasetNameThe name of the dataset to be read.
Returns
A 2D vector containing the pattern data.
Exceptions
std::runtime_errorIf there is an error handling the file or if the dataset dimensions are invalid.

Definition at line 127 of file hdf5_handler.cpp.

128 {
129 try
130 {
131 LOG(Level::TRACE, "Reading dataset '{}' from file '{}'", datasetName, name);
132 const HighFive::File file(name, HighFive::File::ReadOnly);
133
134 const auto dataset = file.getDataSet(datasetName);
135
136 const auto dataspace = dataset.getSpace();
137 const auto dims = dataspace.getDimensions();
138
139 if (dims.size() != 2)
140 {
141 LOG(Level::FATAL, "Invalid dataset dimensions for '{}' in file '{}'", datasetName, name);
142 throw std::runtime_error(
143 std::format(R"(Invalid dataset dimensions for "{}" in file "{}")", datasetName, name));
144 }
145
146 LOG(Level::TRACE, "Reading dataset with dimensions {}x{}", dims[0], dims[1]);
147
148 std::vector data(dims[0], std::vector<RealType>(dims[1]));
149 dataset.read(data);
150
151 LOG(Level::TRACE, "Read dataset successfully");
152
153 return data;
154 }
155 catch (const HighFive::Exception& err)
156 {
157 LOG(Level::FATAL, "Error handling HDF5 file: {}", err.what());
158 throw std::runtime_error("Error handling HDF5 file: " + std::string(err.what()));
159 }
160 }

References LOG.

◆ readPulseData()

void serial::readPulseData ( const std::string &  name,
std::vector< ComplexType > &  data 
)

Reads pulse data from an HDF5 file.

Parameters
nameThe name of the HDF5 file.
dataA reference to a vector where the complex data will be stored.
Exceptions
std::runtime_errorIf the file does not exist or the datasets "I" and "Q" have mismatched sizes.

Definition at line 29 of file hdf5_handler.cpp.

30 {
31 if (!std::filesystem::exists(name))
32 {
33 LOG(Level::FATAL, "File '{}' not found", name);
34 throw std::runtime_error("File " + name + " not found.");
35 }
36
37 LOG(Level::TRACE, "Opening file '{}'", name);
38 const HighFive::File file(name, HighFive::File::ReadOnly);
39
40 // Helper lambda to open group and read dataset
41 auto read_dataset = [&file](const std::string& groupName, std::vector<double>& buffer) -> size_t
42 {
43 const auto group = file.getGroup("/" + groupName);
44
45 const auto dataset = group.getDataSet("value");
46
47 const auto dimensions = dataset.getSpace().getDimensions();
48 const auto size = dimensions[0];
49
50 buffer.resize(size);
51 dataset.read(buffer);
52
53 return size;
54 };
55
56 LOG(Level::TRACE, "Reading dataset 'I' from file '{}'", name);
57 std::vector<double> buffer_i;
58 const auto size = read_dataset("I", buffer_i);
59
60 std::vector<double> buffer_q;
61 LOG(Level::TRACE, "Reading dataset 'Q' from file '{}'", name);
62 if (read_dataset("Q", buffer_q) != size)
63 {
64 LOG(Level::FATAL, "Dataset 'Q' is not the same size as dataset 'I' in file '{}'", name);
65 throw std::runtime_error(R"(Dataset "Q" is not the same size as dataset "I" in file )" + name);
66 }
67
68 data.resize(size);
69 for (size_t i = 0; i < size; ++i)
70 {
71 data[i] = ComplexType(buffer_i[i], buffer_q[i]);
72 }
73 LOG(Level::TRACE, "Read dataset successfully");
74 }

References LOG.

◆ world_to_json()

nlohmann::json serial::world_to_json ( const core::World world)

Serializes the entire simulation world into a nlohmann::json object.

This function traverses the core::World object model and constructs a JSON representation. It is designed to produce a format that is convenient for the frontend to consume. This involves translating internal data formats (e.g., angles in radians) to a more UI-friendly format (e.g., compass degrees) and restructuring complex object relationships (like monostatic radars) into simpler representations.

Parameters
worldThe world object to serialize.
Returns
A nlohmann::json object representing the world.

Definition at line 544 of file json_serializer.cpp.

545 {
546 nlohmann::json sim_json;
547
548 sim_json["name"] = params::params.simulation_name;
549 sim_json["parameters"] = params::params;
550
551 sim_json["waveforms"] = nlohmann::json::array();
552 for (const auto& waveform : world.getWaveforms() | std::views::values)
553 {
554 sim_json["waveforms"].push_back(*waveform);
555 }
556
557 sim_json["antennas"] = nlohmann::json::array();
558 for (const auto& antenna : world.getAntennas() | std::views::values)
559 {
560 sim_json["antennas"].push_back(*antenna);
561 }
562
563 sim_json["timings"] = nlohmann::json::array();
564 for (const auto& timing : world.getTimings() | std::views::values)
565 {
566 sim_json["timings"].push_back(*timing);
567 }
568
569 sim_json["platforms"] = nlohmann::json::array();
570 for (const auto& p : world.getPlatforms())
571 {
572 nlohmann::json plat_json = *p;
573
574 // Initialize components array to ensure it exists even if empty
575 plat_json["components"] = nlohmann::json::array();
576
577 // Add Transmitters and Monostatic Radars
578 for (const auto& t : world.getTransmitters())
579 {
580 if (t->getPlatform() == p.get())
581 {
582 if (t->getAttached() != nullptr)
583 {
584 nlohmann::json monostatic_comp;
585 monostatic_comp["name"] = t->getName();
586 monostatic_comp["waveform"] = t->getSignal() ? t->getSignal()->getName() : "";
587 monostatic_comp["antenna"] = t->getAntenna() ? t->getAntenna()->getName() : "";
588 monostatic_comp["timing"] = t->getTiming() ? t->getTiming()->getName() : "";
589
590 if (const auto* recv = dynamic_cast<const radar::Receiver*>(t->getAttached()))
591 {
592 monostatic_comp["noise_temp"] = recv->getNoiseTemperature();
593 monostatic_comp["nodirect"] = recv->checkFlag(radar::Receiver::RecvFlag::FLAG_NODIRECT);
594 monostatic_comp["nopropagationloss"] =
596
597 if (!t->getSchedule().empty())
598 {
599 monostatic_comp["schedule"] = t->getSchedule();
600 }
601
602 if (t->getMode() == radar::OperationMode::PULSED_MODE)
603 {
604 monostatic_comp["pulsed_mode"] = {{"prf", t->getPrf()},
605 {"window_skip", recv->getWindowSkip()},
606 {"window_length", recv->getWindowLength()}};
607 }
608 else
609 {
610 monostatic_comp["cw_mode"] = nlohmann::json::object();
611 }
612 }
613 plat_json["components"].push_back({{"monostatic", monostatic_comp}});
614 }
615 else
616 {
617 plat_json["components"].push_back({{"transmitter", *t}});
618 }
619 }
620 }
621
622 // Add Standalone Receivers
623 for (const auto& r : world.getReceivers())
624 {
625 if (r->getPlatform() == p.get())
626 {
627 // This must be a standalone receiver, as monostatic cases were handled above.
628 if (r->getAttached() == nullptr)
629 {
630 plat_json["components"].push_back({{"receiver", *r}});
631 }
632 }
633 }
634
635 // Add Targets
636 for (const auto& target : world.getTargets())
637 {
638 if (target->getPlatform() == p.get())
639 {
640 plat_json["components"].push_back({{"target", *target}});
641 }
642 }
643
644 sim_json["platforms"].push_back(plat_json);
645 }
646
647 return {{"simulation", sim_json}};
648 }
Manages radar signal reception and response processing.
Definition receiver.h:36

References radar::Receiver::FLAG_NODIRECT, radar::Receiver::FLAG_NOPROPLOSS, core::World::getAntennas(), core::World::getPlatforms(), core::World::getReceivers(), core::World::getTargets(), core::World::getTimings(), core::World::getTransmitters(), core::World::getWaveforms(), params::params, radar::PULSED_MODE, and params::Parameters::simulation_name.

Referenced by fers_get_scenario_as_json().

+ Here is the call graph for this function:
+ Here is the caller graph for this function:

◆ world_to_xml_string()

std::string serial::world_to_xml_string ( const core::World world)

Serializes the entire simulation world into an XML formatted string.

This function serves as the reverse of the XML parser. It is essential for allowing users to modify a scenario in a UI and then export their changes back into a valid FERS XML file that can be used by the CLI or shared. It iterates through the in-memory core::World object and reconstructs the corresponding XML structure.

Parameters
worldThe world object to serialize.
Returns
A string containing the XML representation of the world.

Definition at line 489 of file xml_serializer.cpp.

490 {
491 XmlDocument doc;
492 xmlNodePtr sim_node = xmlNewNode(nullptr, reinterpret_cast<const xmlChar*>("simulation"));
493 XmlElement root(sim_node);
494 doc.setRootElement(root);
495
496 if (!params::params.simulation_name.empty())
497 {
498 root.setAttribute("name", params::params.simulation_name);
499 }
500 else
501 {
502 root.setAttribute("name", "FERS Scenario");
503 }
504
505 const XmlElement params_elem = root.addChild("parameters");
506 serializeParameters(params_elem);
507
508 // Assets (waveforms, timings, antennas) are serialized first. This is
509 // necessary because platforms reference these assets by name. By defining
510 // them at the top of the document, we ensure that any XML parser can
511 // resolve these references when it later encounters the platform definitions.
512 for (const auto& waveform : world.getWaveforms() | std::views::values)
513 {
514 XmlElement waveform_elem = root.addChild("waveform");
515 serializeWaveform(*waveform, waveform_elem);
516 }
517 for (const auto& timing : world.getTimings() | std::views::values)
518 {
519 XmlElement timing_elem = root.addChild("timing");
520 serializeTiming(*timing, timing_elem);
521 }
522 for (const auto& antenna : world.getAntennas() | std::views::values)
523 {
524 XmlElement antenna_elem = root.addChild("antenna");
525 serializeAntenna(*antenna, antenna_elem);
526 }
527 for (const auto& platform : world.getPlatforms())
528 {
529 XmlElement plat_elem = root.addChild("platform");
530 serializePlatform(*platform, world, plat_elem);
531 }
532
533 return doc.dumpToString();
534 }
void setRootElement(const XmlElement &root) const
Set the root element of the document.
std::string dumpToString() const
Dumps the document to a string.
Class representing a node in an XML document.
XmlElement addChild(const std::string_view name) const noexcept
Add a child element to the current node.

References XmlElement::addChild(), XmlDocument::dumpToString(), core::World::getAntennas(), core::World::getPlatforms(), core::World::getTimings(), core::World::getWaveforms(), params::params, XmlElement::setAttribute(), and XmlDocument::setRootElement().

Referenced by fers_get_scenario_as_xml().

+ Here is the call graph for this function:
+ Here is the caller graph for this function: