FERS 0.1.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
json_serializer.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright (c) 2025-present FERS Contributors (see AUTHORS.md).
3
4/**
5 * @file json_serializer.cpp
6 * @brief Implements JSON serialization and deserialization for FERS objects.
7 *
8 * This file leverages the `nlohmann/json` library's support for automatic
9 * serialization via `to_json` and `from_json` free functions. By placing these
10 * functions within the namespaces of the objects they serialize, we enable
11 * Argument-Dependent Lookup (ADL). This design choice allows the library to
12 * automatically find the correct conversion functions, keeping the serialization
13 * logic decoupled from the core object definitions and improving modularity.
14 */
15
17
18#include <algorithm>
19#include <cmath>
20#include <format>
21#include <initializer_list>
22#include <nlohmann/json.hpp>
23#include <optional>
24#include <random>
25#include <stdexcept>
26#include <string_view>
27#include <unordered_map>
28
30#include "core/parameters.h"
31#include "core/sim_id.h"
32#include "core/world.h"
33#include "math/coord.h"
34#include "math/path.h"
35#include "math/rotation_path.h"
36#include "radar/platform.h"
37#include "radar/receiver.h"
38#include "radar/target.h"
39#include "radar/transmitter.h"
43#include "signal/radar_signal.h"
45#include "timing/timing.h"
46#include "waveform_factory.h"
47
48// TODO: Add file path validation and error handling as needed.
49
50namespace
51{
52 /// Map from timing prototype SimId to shared runtime timing instance.
53 using TimingInstanceMap = std::unordered_map<SimId, std::shared_ptr<timing::Timing>>;
54
55 /// Serializes a SimId as a JSON string.
56 nlohmann::json sim_id_to_json(const SimId id) { return std::to_string(id); }
57
58 /// Parses a required SimId from a JSON object.
59 SimId parse_json_id(const nlohmann::json& j, const std::string& key, const std::string& owner)
60 {
61 if (!j.contains(key))
62 {
63 throw std::runtime_error("Missing required '" + key + "' for " + owner + ".");
64 }
65 try
66 {
67 if (j.at(key).is_number_unsigned())
68 {
69 return j.at(key).get<SimId>();
70 }
71 if (j.at(key).is_number_integer())
72 {
73 const auto value = j.at(key).get<long long>();
74 if (value < 0)
75 {
76 throw std::runtime_error("negative id");
77 }
78 return static_cast<SimId>(value);
79 }
80 if (j.at(key).is_string())
81 {
82 const auto str = j.at(key).get<std::string>();
83 size_t idx = 0;
84 const unsigned long long parsed = std::stoull(str, &idx, 10);
85 if (idx != str.size())
86 {
87 throw std::runtime_error("trailing characters");
88 }
89 return static_cast<SimId>(parsed);
90 }
91 }
92 catch (const std::exception& e)
93 {
94 throw std::runtime_error("Invalid '" + key + "' for " + owner + ": " + e.what());
95 }
96 throw std::runtime_error("Invalid '" + key + "' type for " + owner + ".");
97 }
98
99 /// Resolves or instantiates a shared timing instance by prototype SimId.
100 std::shared_ptr<timing::Timing> resolve_timing_instance(core::World& world, std::mt19937& masterSeeder,
101 TimingInstanceMap& timing_instances, const SimId timing_id)
102 {
103 if (const auto it = timing_instances.find(timing_id); it != timing_instances.end())
104 {
105 return it->second;
106 }
107
108 auto* const timing_proto = world.findTiming(timing_id);
109 if (timing_proto == nullptr)
110 {
111 return nullptr;
112 }
113
114 auto timing = std::make_shared<timing::Timing>(timing_proto->getName(), static_cast<unsigned>(masterSeeder()),
115 timing_proto->getId());
116 timing->initializeModel(timing_proto);
117 timing_instances.emplace(timing_id, timing);
118 return timing;
119 }
120
121 /// Formats a JSON field for warnings without assuming the field type.
122 std::string json_field_for_log(const nlohmann::json& j, const char* key)
123 {
124 if (!j.contains(key) || j.at(key).is_null())
125 {
126 return "";
127 }
128 if (j.at(key).is_string())
129 {
130 return j.at(key).get<std::string>();
131 }
132 return j.at(key).dump();
133 }
134
135 /// Throws a JSON validation error with the provided message.
136 void throw_json_validation_error(const std::string& message) { throw std::runtime_error(message); }
137
138 /// Validates an FMCW waveform while adapting validation errors to std::runtime_error.
139 void validate_fmcw_waveform(const fers_signal::RadarSignal& wave, const std::string& owner)
140 {
142 }
143
144 /// Validates waveform/mode compatibility while adapting validation errors to std::runtime_error.
146 const std::string& owner)
147 {
149 }
150
151 /// Validates an FMCW schedule while adapting validation errors to std::runtime_error.
152 void validate_fmcw_schedule(const std::vector<radar::SchedulePeriod>& schedule,
153 const fers_signal::RadarSignal& wave, const std::string& owner)
154 {
156 }
157
158 /// Throws if a JSON object contains keys outside a strict whitelist.
159 void reject_unknown_keys(const nlohmann::json& object, const std::string& owner, const std::string_view object_name,
160 const std::initializer_list<std::string_view> allowed_keys)
161 {
162 for (const auto& [key, value] : object.items())
163 {
164 (void)value;
165 bool allowed = false;
166 for (const auto allowed_key : allowed_keys)
167 {
168 if (key == allowed_key)
169 {
170 allowed = true;
171 break;
172 }
173 }
174 if (!allowed)
175 {
176 std::string message = owner;
177 message += ' ';
179 message += " contains unsupported key '";
180 message += key;
181 message += "'.";
182 throw std::runtime_error(message);
183 }
184 }
185 }
186
187 /// Returns true when a JSON FMCW mode object carries receiver-side FMCW fields.
188 bool has_dechirp_fields(const nlohmann::json& mode_json)
189 {
190 return mode_json.contains("dechirp_mode") || mode_json.contains("dechirp_reference") ||
191 mode_json.contains("if_sample_rate") || mode_json.contains("if_filter_bandwidth") ||
192 mode_json.contains("if_filter_transition_width");
193 }
194
195 std::optional<RealType> get_optional_positive_real(const nlohmann::json& object, const std::string_view key,
196 const std::string& owner)
197 {
198 const std::string key_string(key);
199 if (!object.contains(key_string))
200 {
201 return std::nullopt;
202 }
203 const RealType value = object.at(key_string).get<RealType>();
204 if (value <= 0.0 || !std::isfinite(value))
205 {
206 throw std::runtime_error(owner + " " + key_string + " must be a finite positive value.");
207 }
208 return value;
209 }
210
212 {
213 return if_chain.sample_rate_hz.has_value() || if_chain.filter_bandwidth_hz.has_value() ||
214 if_chain.filter_transition_width_hz.has_value();
215 }
216
217 void reject_disabled_json_dechirp_fields(const nlohmann::json& mode_json,
219 const std::string& owner)
220 {
221 if (mode_json.contains("dechirp_reference"))
222 {
223 throw std::runtime_error(owner + " declares dechirp_reference while dechirp_mode is 'none'.");
224 }
226 {
227 throw std::runtime_error(owner + " declares IF-chain fields while dechirp_mode is 'none'.");
228 }
229 }
230
232 {
233 if ((if_chain.filter_bandwidth_hz.has_value() || if_chain.filter_transition_width_hz.has_value()) &&
234 !if_chain.sample_rate_hz.has_value())
235 {
236 throw std::runtime_error(owner + " IF filter fields require if_sample_rate.");
237 }
238 if (if_chain.sample_rate_hz.has_value())
239 {
241 if (*if_chain.sample_rate_hz > sim_rate)
242 {
243 throw std::runtime_error(owner + " if_sample_rate must not exceed the simulation sample rate.");
244 }
245 }
246 if (if_chain.sample_rate_hz.has_value() && if_chain.filter_bandwidth_hz.has_value() &&
247 *if_chain.filter_bandwidth_hz >= *if_chain.sample_rate_hz / 2.0)
248 {
249 throw std::runtime_error(owner + " if_filter_bandwidth must be less than half if_sample_rate.");
250 }
251 }
252
253 std::string required_non_empty_json_string(const nlohmann::json& object, const std::string_view key,
254 const std::string& owner, const std::string_view context)
255 {
256 const std::string key_string(key);
257 auto value = object.at(key_string).get<std::string>();
258 if (value.empty())
259 {
260 throw std::runtime_error(owner + " " + std::string(context) + " has an empty " + key_string + ".");
261 }
262 return value;
263 }
264
266 const std::string& owner)
267 {
268 if (!mode_json.contains("dechirp_reference") || !mode_json.at("dechirp_reference").is_object())
269 {
270 throw std::runtime_error(owner + " enables dechirping but does not declare dechirp_reference.");
271 }
272
273 const auto& ref_json = mode_json.at("dechirp_reference");
274 reject_unknown_keys(ref_json, owner, "dechirp_reference", {"source", "transmitter_name", "waveform_name"});
275 if (!ref_json.contains("source"))
276 {
277 throw std::runtime_error(owner + " dechirp_reference requires source.");
278 }
279
281 reference.source = radar::parseDechirpReferenceSourceToken(ref_json.at("source").get<std::string>());
282 const bool has_transmitter_name = ref_json.contains("transmitter_name");
283 const bool has_waveform_name = ref_json.contains("waveform_name");
284
285 switch (reference.source)
286 {
289 {
290 throw std::runtime_error(owner +
291 " attached dechirp_reference must not set transmitter_name or waveform_name.");
292 }
293 break;
296 {
297 throw std::runtime_error(owner + " transmitter dechirp_reference requires transmitter_name only.");
298 }
299 reference.name =
300 required_non_empty_json_string(ref_json, "transmitter_name", owner, "transmitter dechirp_reference");
301 break;
304 {
305 throw std::runtime_error(owner + " custom dechirp_reference requires waveform_name only.");
306 }
307 reference.name =
308 required_non_empty_json_string(ref_json, "waveform_name", owner, "custom dechirp_reference");
309 break;
311 throw std::runtime_error(owner + " dechirp_reference source must be attached, transmitter, or custom.");
312 }
313
314 return reference;
315 }
316
317 /// Parses receiver-side dechirp settings from a JSON component.
319 const std::string& owner)
320 {
322 {
324 return;
325 }
326
327 const auto& mode_json = comp_json.contains("fmcw_mode") ? comp_json.at("fmcw_mode") : nlohmann::json::object();
328 if (!mode_json.is_object())
329 {
330 throw std::runtime_error(owner + " fmcw_mode must be an object.");
331 }
332 reject_unknown_keys(mode_json, owner, "fmcw_mode",
333 {"dechirp_mode", "dechirp_reference", "if_sample_rate", "if_filter_bandwidth",
334 "if_filter_transition_width"});
335
337 if (mode_json.contains("dechirp_mode"))
338 {
339 mode = radar::parseDechirpModeToken(mode_json.at("dechirp_mode").get<std::string>());
340 }
341
343 .sample_rate_hz = get_optional_positive_real(mode_json, "if_sample_rate", owner),
344 .filter_bandwidth_hz = get_optional_positive_real(mode_json, "if_filter_bandwidth", owner),
345 .filter_transition_width_hz = get_optional_positive_real(mode_json, "if_filter_transition_width", owner)};
346
348 {
350 receiver.setDechirpMode(mode);
351 return;
352 }
353
356
357 receiver.setDechirpMode(mode);
358 receiver.setDechirpReference(std::move(reference));
359 receiver.setFmcwIfChainRequest(if_chain);
360 }
361
362 /// Serializes receiver-side FMCW mode settings.
364 {
365 nlohmann::json mode_json = nlohmann::json::object();
366 if (!receiver.isDechirpEnabled())
367 {
368 return mode_json;
369 }
370
371 const auto& reference = receiver.getDechirpReference();
372 mode_json["dechirp_mode"] = std::string(radar::dechirpModeToken(receiver.getDechirpMode()));
373 const auto& if_chain = receiver.getFmcwIfChainRequest();
374 if (if_chain.sample_rate_hz.has_value())
375 {
376 mode_json["if_sample_rate"] = *if_chain.sample_rate_hz;
377 }
378 if (if_chain.filter_bandwidth_hz.has_value())
379 {
380 mode_json["if_filter_bandwidth"] = *if_chain.filter_bandwidth_hz;
381 }
382 if (if_chain.filter_transition_width_hz.has_value())
383 {
384 mode_json["if_filter_transition_width"] = *if_chain.filter_transition_width_hz;
385 }
386 nlohmann::json ref_json = {{"source", std::string(radar::dechirpReferenceSourceToken(reference.source))}};
388 {
389 ref_json["transmitter_name"] =
390 !reference.transmitter_name.empty() ? reference.transmitter_name : reference.name;
391 }
393 {
394 ref_json["waveform_name"] = !reference.waveform_name.empty() ? reference.waveform_name : reference.name;
395 }
396 mode_json["dechirp_reference"] = std::move(ref_json);
397 return mode_json;
398 }
399}
400
401namespace math
402{
403 void to_json(nlohmann::json& j, const Vec3& v) // NOLINT(*-use-internal-linkage)
404 {
405 j = {{"x", v.x}, {"y", v.y}, {"z", v.z}};
406 } // NOLINT(*-use-internal-linkage)
407
408 void from_json(const nlohmann::json& j, Vec3& v) // NOLINT(*-use-internal-linkage)
409 {
410 j.at("x").get_to(v.x);
411 j.at("y").get_to(v.y);
412 j.at("z").get_to(v.z);
413 }
414
415 void to_json(nlohmann::json& j, const Coord& c) // NOLINT(*-use-internal-linkage)
416 {
417 j = {{"time", c.t}, {"x", c.pos.x}, {"y", c.pos.y}, {"altitude", c.pos.z}};
418 }
419
420 void from_json(const nlohmann::json& j, Coord& c) // NOLINT(*-use-internal-linkage)
421 {
422 j.at("time").get_to(c.t);
423 j.at("x").get_to(c.pos.x);
424 j.at("y").get_to(c.pos.y);
425 j.at("altitude").get_to(c.pos.z);
426 }
427
428 void to_json(nlohmann::json& j, const RotationCoord& rc) // NOLINT(*-use-internal-linkage)
429 {
430 const auto unit = params::rotationAngleUnit();
431 j = {{"time", rc.t},
434 }
435
436 void from_json(const nlohmann::json& j, RotationCoord& rc) // NOLINT(*-use-internal-linkage)
437 {
438 j.at("time").get_to(rc.t);
440 j.at("azimuth").get<RealType>(), j.at("elevation").get<RealType>(), rc.t, params::rotationAngleUnit());
441 rc.azimuth = external.azimuth;
442 rc.elevation = external.elevation;
443 }
444
449
450 void to_json(nlohmann::json& j, const Path& p) // NOLINT(*-use-internal-linkage)
451 {
452 j = {{"interpolation", p.getType()}, {"positionwaypoints", p.getCoords()}};
453 }
454
455 void from_json(const nlohmann::json& j, Path& p) // NOLINT(*-use-internal-linkage)
456 {
457 p.setInterp(j.at("interpolation").get<Path::InterpType>());
458 for (const auto waypoints = j.at("positionwaypoints").get<std::vector<Coord>>(); const auto& wp : waypoints)
459 {
460 p.addCoord(wp);
461 }
462 p.finalize();
463 }
464
468 "constant"}, // Not used in xml_parser or UI yet, but for completeness
471
472 void to_json(nlohmann::json& j, const RotationPath& p) // NOLINT(*-use-internal-linkage)
473 {
474 j["interpolation"] = p.getType();
475 // This logic exists to map the two different rotation definitions from the
476 // XML schema (<fixedrotation> and <rotationpath>) into a unified JSON
477 // structure that the frontend can more easily handle.
479 {
480 // A constant-rate rotation path corresponds to the <fixedrotation> XML element.
481 // The start and rate values are converted to compass degrees per second.
482 // No normalization is applied to preserve negative start angles.
483 const auto unit = params::rotationAngleUnit();
484 j["startazimuth"] = serial::rotation_angle_utils::internal_azimuth_to_external(p.getStart().azimuth, unit);
485 j["startelevation"] =
487 j["azimuthrate"] =
489 j["elevationrate"] =
491 }
492 else
493 {
494 j["rotationwaypoints"] = p.getCoords();
495 }
496 }
497
498 void from_json(const nlohmann::json& j, RotationPath& p) // NOLINT(*-use-internal-linkage)
499 {
500 p.setInterp(j.at("interpolation").get<RotationPath::InterpType>());
501 for (const auto waypoints = j.at("rotationwaypoints").get<std::vector<RotationCoord>>();
502 const auto& wp : waypoints)
503 {
504 p.addCoord(wp);
505 }
506 p.finalize();
507 }
508
509}
510
511namespace timing
512{
513 void to_json(nlohmann::json& j, const PrototypeTiming& pt) // NOLINT(*-use-internal-linkage)
514 {
515 j = nlohmann::json{{"id", sim_id_to_json(pt.getId())},
516 {"name", pt.getName()},
517 {"frequency", pt.getFrequency()},
518 {"synconpulse", pt.getSyncOnPulse()}};
519
520 if (pt.getFreqOffset().has_value())
521 {
522 j["freq_offset"] = pt.getFreqOffset().value();
523 }
524 if (pt.getRandomFreqOffsetStdev().has_value())
525 {
526 j["random_freq_offset_stdev"] = pt.getRandomFreqOffsetStdev().value();
527 }
528 if (pt.getPhaseOffset().has_value())
529 {
530 j["phase_offset"] = pt.getPhaseOffset().value();
531 }
532 if (pt.getRandomPhaseOffsetStdev().has_value())
533 {
534 j["random_phase_offset_stdev"] = pt.getRandomPhaseOffsetStdev().value();
535 }
536
537 std::vector<RealType> alphas;
538 std::vector<RealType> weights;
539 pt.copyAlphas(alphas, weights);
540 if (!alphas.empty())
541 {
542 nlohmann::json noise_entries = nlohmann::json::array();
543 for (size_t i = 0; i < alphas.size(); ++i)
544 {
545 noise_entries.push_back({{"alpha", alphas[i]}, {"weight", weights[i]}});
546 }
547 j["noise_entries"] = noise_entries;
548 }
549 }
550
551 void from_json(const nlohmann::json& j, PrototypeTiming& pt) // NOLINT(*-use-internal-linkage)
552 {
553 pt.setFrequency(j.at("frequency").get<RealType>());
554 if (j.value("synconpulse", false))
555 {
556 pt.setSyncOnPulse();
557 }
558 else
559 {
560 pt.clearSyncOnPulse();
561 }
562
563 if (j.contains("freq_offset"))
564 {
565 pt.setFreqOffset(j.at("freq_offset").get<RealType>());
566 }
567 else
568 pt.clearFreqOffset();
569 if (j.contains("random_freq_offset_stdev"))
570 {
571 pt.setRandomFreqOffsetStdev(j.at("random_freq_offset_stdev").get<RealType>());
572 }
573 else
574 pt.clearRandomFreqOffsetStdev();
575 if (j.contains("phase_offset"))
576 {
577 pt.setPhaseOffset(j.at("phase_offset").get<RealType>());
578 }
579 else
580 pt.clearPhaseOffset();
581 if (j.contains("random_phase_offset_stdev"))
582 {
583 pt.setRandomPhaseOffsetStdev(j.at("random_phase_offset_stdev").get<RealType>());
584 }
585 else
586 pt.clearRandomPhaseOffsetStdev();
587
588 pt.clearNoiseEntries();
589 if (j.contains("noise_entries"))
590 {
591 for (const auto& entry : j.at("noise_entries"))
592 {
593 pt.setAlpha(entry.at("alpha").get<RealType>(), entry.at("weight").get<RealType>());
594 }
595 }
596 }
597}
598
599namespace fers_signal
600{
601 void to_json(nlohmann::json& j, const RadarSignal& rs) // NOLINT(*-use-internal-linkage)
602 {
603 j = nlohmann::json{{"id", sim_id_to_json(rs.getId())},
604 {"name", rs.getName()},
605 {"power", rs.getPower()},
606 {"carrier_frequency", rs.getCarrier()}};
607 if (dynamic_cast<const CwSignal*>(rs.getSignal()) != nullptr)
608 {
609 j["cw"] = nlohmann::json::object();
610 }
611 else if (const auto* fmcw = rs.getFmcwChirpSignal(); fmcw != nullptr)
612 {
613 j["fmcw_linear_chirp"] = {{"direction", std::string(fmcwChirpDirectionToken(fmcw->getDirection()))},
614 {"chirp_bandwidth", fmcw->getChirpBandwidth()},
615 {"chirp_duration", fmcw->getChirpDuration()},
616 {"chirp_period", fmcw->getChirpPeriod()}};
617 if (std::abs(fmcw->getStartFrequencyOffset()) > EPSILON)
618 {
619 j["fmcw_linear_chirp"]["start_frequency_offset"] = fmcw->getStartFrequencyOffset();
620 }
621 if (fmcw->getChirpCount().has_value())
622 {
623 j["fmcw_linear_chirp"]["chirp_count"] = *fmcw->getChirpCount();
624 }
625 }
626 else if (const auto* triangle = rs.getFmcwTriangleSignal(); triangle != nullptr)
627 {
628 j["fmcw_triangle"] = {{"chirp_bandwidth", triangle->getChirpBandwidth()},
629 {"chirp_duration", triangle->getChirpDuration()}};
630 if (std::abs(triangle->getStartFrequencyOffset()) > EPSILON)
631 {
632 j["fmcw_triangle"]["start_frequency_offset"] = triangle->getStartFrequencyOffset();
633 }
634 if (triangle->getTriangleCount().has_value())
635 {
636 j["fmcw_triangle"]["triangle_count"] = *triangle->getTriangleCount();
637 }
638 }
639 else
640 {
641 if (const auto& filename = rs.getFilename(); filename.has_value())
642 {
643 j["pulsed_from_file"] = {{"filename", *filename}};
644 }
645 else
646 {
647 throw std::logic_error("Attempted to serialize a file-based waveform named '" + rs.getName() +
648 "' without a source filename.");
649 }
650 }
651 }
652
653 void from_json(const nlohmann::json& j, std::unique_ptr<RadarSignal>& rs) // NOLINT(*-use-internal-linkage)
654 {
655 const auto name = j.at("name").get<std::string>();
656 const auto id = parse_json_id(j, "id", "waveform");
657 const auto power = j.at("power").get<RealType>();
658 const auto carrier = j.at("carrier_frequency").get<RealType>();
659
660 if (j.contains("cw"))
661 {
662 auto cw_signal = std::make_unique<CwSignal>();
663 rs = std::make_unique<RadarSignal>(name, power, carrier, params::endTime() - params::startTime(),
664 std::move(cw_signal), id);
665 }
666 else if (j.contains("fmcw_linear_chirp"))
667 {
668 const auto& fmcw_json = j.at("fmcw_linear_chirp");
669 const auto direction = parseFmcwChirpDirection(fmcw_json.at("direction").get<std::string>());
670 std::optional<std::size_t> chirp_count;
671 if (fmcw_json.contains("chirp_count"))
672 {
673 const auto parsed_count = fmcw_json.at("chirp_count").get<long long>();
674 if (parsed_count <= 0)
675 {
676 throw std::runtime_error("Waveform '" + name + "' has an invalid chirp_count.");
677 }
678 chirp_count = static_cast<std::size_t>(parsed_count);
679 }
680
681 auto fmcw_signal = std::make_unique<FmcwChirpSignal>(
682 fmcw_json.at("chirp_bandwidth").get<RealType>(), fmcw_json.at("chirp_duration").get<RealType>(),
683 fmcw_json.at("chirp_period").get<RealType>(), fmcw_json.value("start_frequency_offset", 0.0),
684 chirp_count, direction);
685 rs = std::make_unique<RadarSignal>(name, power, carrier, fmcw_signal->getChirpDuration(),
686 std::move(fmcw_signal), id);
687 validate_fmcw_waveform(*rs, "Waveform '" + name + "'");
688 }
689 else if (j.contains("fmcw_triangle"))
690 {
691 const auto& fmcw_json = j.at("fmcw_triangle");
692 std::optional<std::size_t> triangle_count;
693 if (fmcw_json.contains("triangle_count"))
694 {
695 const auto& count_json = fmcw_json.at("triangle_count");
696 if (!count_json.is_number_integer() && !count_json.is_number_unsigned())
697 {
698 throw std::runtime_error("Waveform '" + name + "' has an invalid triangle_count.");
699 }
700 const auto parsed_count = count_json.get<long long>();
701 if (parsed_count <= 0)
702 {
703 throw std::runtime_error("Waveform '" + name + "' has an invalid triangle_count.");
704 }
705 triangle_count = static_cast<std::size_t>(parsed_count);
706 }
707
708 auto fmcw_signal = std::make_unique<FmcwTriangleSignal>(
709 fmcw_json.at("chirp_bandwidth").get<RealType>(), fmcw_json.at("chirp_duration").get<RealType>(),
710 fmcw_json.value("start_frequency_offset", 0.0), triangle_count);
711 rs = std::make_unique<RadarSignal>(name, power, carrier, fmcw_signal->getTrianglePeriod(),
712 std::move(fmcw_signal), id);
713 validate_fmcw_waveform(*rs, "Waveform '" + name + "'");
714 }
715 else if (j.contains("pulsed_from_file"))
716 {
717 const auto& pulsed_file = j.at("pulsed_from_file");
718 const auto filename = pulsed_file.value("filename", "");
719 if (filename.empty())
720 {
721 LOG(logging::Level::WARNING, "Skipping load of file-based waveform '{}': filename is empty.", name);
722 return; // rs remains nullptr
723 }
724 rs = serial::loadWaveformFromFile(name, filename, power, carrier, id);
725 }
726 else
727 {
728 throw std::runtime_error("Unsupported waveform type in from_json for '" + name + "'");
729 }
730 }
731}
732
733namespace antenna
734{
735 void to_json(nlohmann::json& j, const Antenna& a) // NOLINT(*-use-internal-linkage)
736 {
737 j = {{"id", sim_id_to_json(a.getId())}, {"name", a.getName()}, {"efficiency", a.getEfficiencyFactor()}};
738
739 if (const auto* sinc = dynamic_cast<const Sinc*>(&a))
740 {
741 j["pattern"] = "sinc";
742 j["alpha"] = sinc->getAlpha();
743 j["beta"] = sinc->getBeta();
744 j["gamma"] = sinc->getGamma();
745 }
746 else if (const auto* gaussian = dynamic_cast<const Gaussian*>(&a))
747 {
748 j["pattern"] = "gaussian";
749 j["azscale"] = gaussian->getAzimuthScale();
750 j["elscale"] = gaussian->getElevationScale();
751 }
752 else if (const auto* sh = dynamic_cast<const SquareHorn*>(&a))
753 {
754 j["pattern"] = "squarehorn";
755 j["diameter"] = sh->getDimension();
756 }
757 else if (const auto* parabolic = dynamic_cast<const Parabolic*>(&a))
758 {
759 j["pattern"] = "parabolic";
760 j["diameter"] = parabolic->getDiameter();
761 }
762 else if (const auto* xml = dynamic_cast<const XmlAntenna*>(&a))
763 {
764 j["pattern"] = "xml";
765 j["filename"] = xml->getFilename();
766 }
767 else if (const auto* h5 = dynamic_cast<const H5Antenna*>(&a))
768 {
769 j["pattern"] = "file";
770 j["filename"] = h5->getFilename();
771 }
772 else
773 {
774 j["pattern"] = "isotropic";
775 }
776 }
777
778 void from_json(const nlohmann::json& j, std::unique_ptr<Antenna>& ant) // NOLINT(*-use-internal-linkage)
779 {
780 const auto name = j.at("name").get<std::string>();
781 const auto id = parse_json_id(j, "id", "Antenna");
782 const auto pattern = j.value("pattern", "isotropic");
783
784 if (pattern == "isotropic")
785 {
786 ant = std::make_unique<Isotropic>(name, id);
787 }
788 else if (pattern == "sinc")
789 {
790 ant = std::make_unique<Sinc>(name, j.at("alpha").get<RealType>(), j.at("beta").get<RealType>(),
791 j.at("gamma").get<RealType>(), id);
792 }
793 else if (pattern == "gaussian")
794 {
795 ant =
796 std::make_unique<Gaussian>(name, j.at("azscale").get<RealType>(), j.at("elscale").get<RealType>(), id);
797 }
798 else if (pattern == "squarehorn")
799 {
800 ant = std::make_unique<SquareHorn>(name, j.at("diameter").get<RealType>(), id);
801 }
802 else if (pattern == "parabolic")
803 {
804 ant = std::make_unique<Parabolic>(name, j.at("diameter").get<RealType>(), id);
805 }
806 else if (pattern == "xml")
807 {
808 const auto filename = j.value("filename", "");
809 if (filename.empty())
810 {
811 LOG(logging::Level::WARNING, "Skipping load of XML antenna '{}': filename is empty.", name);
812 return; // ant remains nullptr
813 }
814 ant = std::make_unique<XmlAntenna>(name, filename, id);
815 }
816 else if (pattern == "file")
817 {
818 const auto filename = j.value("filename", "");
819 if (filename.empty())
820 {
821 LOG(logging::Level::WARNING, "Skipping load of H5 antenna '{}': filename is empty.", name);
822 return; // ant remains nullptr
823 }
824 ant = std::make_unique<H5Antenna>(name, filename, id);
825 }
826 else
827 {
828 throw std::runtime_error("Unsupported antenna pattern in from_json: " + pattern);
829 }
830
831 ant->setEfficiencyFactor(j.value("efficiency", 1.0));
832 }
833}
834
835namespace radar
836{
837 void to_json(nlohmann::json& j, const SchedulePeriod& p) // NOLINT(*-use-internal-linkage)
838 {
839 j = {{"start", p.start}, {"end", p.end}};
840 } // NOLINT(*-use-internal-linkage)
841
842 void from_json(const nlohmann::json& j, SchedulePeriod& p) // NOLINT(*-use-internal-linkage)
843 {
844 j.at("start").get_to(p.start);
845 j.at("end").get_to(p.end);
846 }
847
848 void to_json(nlohmann::json& j, const Transmitter& t) // NOLINT(*-use-internal-linkage)
849 {
850 j = nlohmann::json{{"id", sim_id_to_json(t.getId())},
851 {"name", t.getName()},
852 {"waveform", sim_id_to_json((t.getSignal() != nullptr) ? t.getSignal()->getId() : 0)},
853 {"antenna", sim_id_to_json((t.getAntenna() != nullptr) ? t.getAntenna()->getId() : 0)},
854 {"timing", sim_id_to_json(t.getTiming() ? t.getTiming()->getId() : 0)}};
855
857 {
858 j["pulsed_mode"] = {{"prf", t.getPrf()}};
859 }
860 else if (t.getMode() == OperationMode::FMCW_MODE)
861 {
862 j["fmcw_mode"] = nlohmann::json::object();
863 }
864 else
865 {
866 j["cw_mode"] = nlohmann::json::object();
867 }
868 if (!t.getSchedule().empty())
869 {
870 j["schedule"] = t.getSchedule();
871 }
872 }
873
874 void to_json(nlohmann::json& j, const Receiver& r) // NOLINT(*-use-internal-linkage)
875 {
876 j = nlohmann::json{{"id", sim_id_to_json(r.getId())},
877 {"name", r.getName()},
878 {"noise_temp", r.getNoiseTemperature()},
879 {"antenna", sim_id_to_json((r.getAntenna() != nullptr) ? r.getAntenna()->getId() : 0)},
880 {"timing", sim_id_to_json(r.getTiming() ? r.getTiming()->getId() : 0)},
881 {"nodirect", r.checkFlag(Receiver::RecvFlag::FLAG_NODIRECT)},
882 {"nopropagationloss", r.checkFlag(Receiver::RecvFlag::FLAG_NOPROPLOSS)}};
883
884 if (r.getMode() == OperationMode::PULSED_MODE)
885 {
886 j["pulsed_mode"] = {
887 {"prf", r.getWindowPrf()}, {"window_skip", r.getWindowSkip()}, {"window_length", r.getWindowLength()}};
888 }
889 else if (r.getMode() == OperationMode::FMCW_MODE)
890 {
891 j["fmcw_mode"] = receiver_fmcw_mode_to_json(r);
892 }
893 else
894 {
895 j["cw_mode"] = nlohmann::json::object();
896 }
897 if (!r.getSchedule().empty())
898 {
899 j["schedule"] = r.getSchedule();
900 }
901 }
902
903 void to_json(nlohmann::json& j, const Target& t) // NOLINT(*-use-internal-linkage)
904 {
905 j["id"] = sim_id_to_json(t.getId());
906 j["name"] = t.getName();
907 nlohmann::json rcs_json;
908 if (const auto* iso = dynamic_cast<const IsoTarget*>(&t))
909 {
910 rcs_json["type"] = "isotropic";
911 rcs_json["value"] = iso->getConstRcs();
912 }
913 else if (const auto* file = dynamic_cast<const FileTarget*>(&t))
914 {
915 rcs_json["type"] = "file";
916 rcs_json["filename"] = file->getFilename();
917 }
918 j["rcs"] = rcs_json;
919
920 // Serialize the fluctuation model if it exists.
921 if (const auto* model_base = t.getFluctuationModel())
922 {
923 nlohmann::json model_json;
924 if (const auto* chi_model = dynamic_cast<const RcsChiSquare*>(model_base))
925 {
926 model_json["type"] = "chisquare";
927 model_json["k"] = chi_model->getK();
928 }
929 else // Default to constant if it's not a recognized type (e.g., RcsConst)
930 {
931 model_json["type"] = "constant";
932 }
933 j["model"] = model_json;
934 }
935 }
936
937 void to_json(nlohmann::json& j, const Platform& p) // NOLINT(*-use-internal-linkage)
938 {
939 j = {{"id", sim_id_to_json(p.getId())}, {"name", p.getName()}, {"motionpath", *p.getMotionPath()}};
940
941 if (p.getRotationPath()->getType() == math::RotationPath::InterpType::INTERP_CONSTANT)
942 {
943 j["fixedrotation"] = *p.getRotationPath();
944 }
945 else
946 {
947 j["rotationpath"] = *p.getRotationPath();
948 }
949 }
950
951}
952
953namespace params
954{
956 {{CoordinateFrame::ENU, "ENU"},
957 {CoordinateFrame::UTM, "UTM"},
958 {CoordinateFrame::ECEF, "ECEF"}})
961
962 void to_json(nlohmann::json& j, const Parameters& p) // NOLINT(*-use-internal-linkage)
963 {
964 j = nlohmann::json{{"starttime", p.start},
965 {"endtime", p.end},
966 {"rate", p.rate},
967 {"c", p.c},
968 {"simSamplingRate", p.sim_sampling_rate},
969 {"adc_bits", p.adc_bits},
970 {"oversample", p.oversample_ratio},
971 {"rotationangleunit", p.rotation_angle_unit}};
972
973 if (p.random_seed.has_value())
974 {
975 j["randomseed"] = p.random_seed.value();
976 }
977
978 j["origin"] = {
979 {"latitude", p.origin_latitude}, {"longitude", p.origin_longitude}, {"altitude", p.origin_altitude}};
980
981 j["coordinatesystem"] = {{"frame", p.coordinate_frame}};
982 if (p.coordinate_frame == CoordinateFrame::UTM)
983 {
984 j["coordinatesystem"]["zone"] = p.utm_zone;
985 j["coordinatesystem"]["hemisphere"] = p.utm_north_hemisphere ? "N" : "S";
986 }
987 }
988
989 void from_json(const nlohmann::json& j, Parameters& p) // NOLINT(*-use-internal-linkage)
990 {
991 p.start = j.at("starttime").get<RealType>();
992 p.end = j.at("endtime").get<RealType>();
993 p.rate = j.at("rate").get<RealType>();
994 p.c = j.value("c", Parameters::DEFAULT_C);
995 p.sim_sampling_rate = j.value("simSamplingRate", 1000.0);
996 p.adc_bits = j.value("adc_bits", 0u);
997 p.oversample_ratio = j.value("oversample", 1u);
998 params::validateOversampleRatio(p.oversample_ratio);
999 p.rotation_angle_unit = j.value("rotationangleunit", RotationAngleUnit::Degrees);
1000 p.random_seed = j.value<std::optional<unsigned>>("randomseed", std::nullopt);
1001
1002 const auto& origin = j.at("origin");
1003 p.origin_latitude = origin.at("latitude").get<double>();
1004 p.origin_longitude = origin.at("longitude").get<double>();
1005 p.origin_altitude = origin.at("altitude").get<double>();
1006
1007 const auto& cs = j.at("coordinatesystem");
1008 p.coordinate_frame = cs.at("frame").get<CoordinateFrame>();
1009 if (p.coordinate_frame == CoordinateFrame::UTM)
1010 {
1011 p.utm_zone = cs.at("zone").get<int>();
1012 p.utm_north_hemisphere = cs.at("hemisphere").get<std::string>() == "N";
1013 }
1014 }
1015}
1016
1017namespace
1018{
1021 {
1022 monostatic_comp["noise_temp"] = receiver.getNoiseTemperature();
1025
1026 if (!transmitter.getSchedule().empty())
1027 {
1028 monostatic_comp["schedule"] = transmitter.getSchedule();
1029 }
1030
1032 {
1033 monostatic_comp["pulsed_mode"] = {{"prf", transmitter.getPrf()},
1034 {"window_skip", receiver.getWindowSkip()},
1035 {"window_length", receiver.getWindowLength()}};
1036 }
1037 else if (transmitter.getMode() == radar::OperationMode::FMCW_MODE)
1038 {
1040 }
1041 else
1042 {
1043 monostatic_comp["cw_mode"] = nlohmann::json::object();
1044 }
1045 }
1046
1048 {
1049 const auto* attached = transmitter.getAttached();
1050 nlohmann::json monostatic_comp;
1051 monostatic_comp["name"] = transmitter.getName();
1052 monostatic_comp["tx_id"] = sim_id_to_json(transmitter.getId());
1053 monostatic_comp["rx_id"] = sim_id_to_json(attached->getId());
1054 monostatic_comp["waveform"] =
1055 sim_id_to_json((transmitter.getSignal() != nullptr) ? transmitter.getSignal()->getId() : 0);
1056 monostatic_comp["antenna"] =
1057 sim_id_to_json((transmitter.getAntenna() != nullptr) ? transmitter.getAntenna()->getId() : 0);
1058 monostatic_comp["timing"] = sim_id_to_json(transmitter.getTiming() ? transmitter.getTiming()->getId() : 0);
1059
1060 if (const auto* receiver = dynamic_cast<const radar::Receiver*>(attached))
1061 {
1063 }
1064 return nlohmann::json{{"monostatic", monostatic_comp}};
1065 }
1066
1068 const core::World& world)
1069 {
1070 for (const auto& transmitter : world.getTransmitters())
1071 {
1072 if (transmitter->getPlatform() != platform)
1073 {
1074 continue;
1075 }
1076 if (transmitter->getAttached() != nullptr)
1077 {
1079 }
1080 else
1081 {
1082 components.push_back(nlohmann::json{{"transmitter", *transmitter}});
1083 }
1084 }
1085 }
1086
1087 void appendReceiverComponents(nlohmann::json& components, const radar::Platform* platform, const core::World& world)
1088 {
1089 for (const auto& receiver : world.getReceivers())
1090 {
1091 if (receiver->getPlatform() == platform && receiver->getAttached() == nullptr)
1092 {
1093 components.push_back(nlohmann::json{{"receiver", *receiver}});
1094 }
1095 }
1096 }
1097
1098 void appendTargetComponents(nlohmann::json& components, const radar::Platform* platform, const core::World& world)
1099 {
1100 for (const auto& target : world.getTargets())
1101 {
1102 if (target->getPlatform() == platform)
1103 {
1104 components.push_back(nlohmann::json{{"target", *target}});
1105 }
1106 }
1107 }
1108
1109 /// Serializes a platform and its attached components to JSON.
1110 nlohmann::json serialize_platform(const radar::Platform* p, const core::World& world)
1111 {
1112 nlohmann::json plat_json = *p;
1113 plat_json["components"] = nlohmann::json::array();
1114 auto& components = plat_json["components"];
1115
1119
1120 return plat_json;
1121 }
1122
1123 /// Parses simulation parameters from JSON and updates the master seeder.
1124 void parse_parameters(const nlohmann::json& sim, std::mt19937& masterSeeder)
1125 {
1126 auto new_params = sim.at("parameters").get<params::Parameters>();
1127
1128 // If a random seed is present in the incoming JSON, it is used to re-seed
1129 // the master generator. This is crucial for allowing the UI to control
1130 // simulation reproducibility.
1131 if (sim.at("parameters").contains("randomseed"))
1132 {
1134 if (params::params.random_seed)
1135 {
1136 LOG(logging::Level::INFO, "Master seed updated from JSON to: {}", *params::params.random_seed);
1137 masterSeeder.seed(*params::params.random_seed);
1138 }
1139 }
1140
1143 params::params.simulation_name = sim.value("name", "");
1144 }
1145
1146 /// Parses top-level reusable assets from JSON into the world.
1147 void parse_assets(const nlohmann::json& sim, core::World& world)
1148 {
1149 if (sim.contains("waveforms"))
1150 {
1151 for (auto waveforms = sim.at("waveforms").get<std::vector<std::unique_ptr<fers_signal::RadarSignal>>>();
1152 auto& waveform : waveforms)
1153 {
1154 // Only add valid waveforms. If filename was empty, waveform is nullptr.
1155 if (waveform)
1156 {
1157 world.add(std::move(waveform));
1158 }
1159 }
1160 }
1161
1162 if (sim.contains("antennas"))
1163 {
1164 for (auto antennas = sim.at("antennas").get<std::vector<std::unique_ptr<antenna::Antenna>>>();
1165 auto& antenna : antennas)
1166 {
1167 // Only add valid antennas.
1168 if (antenna)
1169 {
1170 world.add(std::move(antenna));
1171 }
1172 }
1173 }
1174
1175 if (sim.contains("timings"))
1176 {
1177 for (const auto& timing_json : sim.at("timings"))
1178 {
1179 auto name = timing_json.at("name").get<std::string>();
1180 const auto timing_id = parse_json_id(timing_json, "id", "Timing");
1181 auto timing_obj = std::make_unique<timing::PrototypeTiming>(name, timing_id);
1182 timing_json.get_to(*timing_obj);
1183 world.add(std::move(timing_obj));
1184 }
1185 }
1186 }
1187
1188 using JsonNameRegistry = std::unordered_map<std::string, std::string>;
1189
1190 void register_json_name(JsonNameRegistry& name_registry, const nlohmann::json& element, const std::string_view kind)
1191 {
1192 if (!element.is_object() || !element.contains("name"))
1193 {
1194 return;
1195 }
1196
1197 const auto name = element.at("name").get<std::string>();
1198 const auto [iter, inserted] = name_registry.emplace(name, std::string(kind));
1199 if (!inserted)
1200 {
1201 throw std::runtime_error("Duplicate name '" + name + "' found for " + std::string(kind) +
1202 "; previously used by " + iter->second + ".");
1203 }
1204 }
1205
1206 void register_json_name_array(JsonNameRegistry& name_registry, const nlohmann::json& sim,
1207 const std::string_view key, const std::string_view kind)
1208 {
1209 const std::string key_string(key);
1210 if (!sim.contains(key_string))
1211 {
1212 return;
1213 }
1214 for (const auto& element : sim.at(key_string))
1215 {
1217 }
1218 }
1219
1221 {
1222 if (!platform.contains("components") || !platform.at("components").is_array())
1223 {
1224 return;
1225 }
1226
1227 for (const auto& component_wrapper : platform.at("components"))
1228 {
1229 if (!component_wrapper.is_object())
1230 {
1231 continue;
1232 }
1233 for (const auto& [kind, component] : component_wrapper.items())
1234 {
1236 }
1237 }
1238 }
1239
1240 void register_platform_names(JsonNameRegistry& name_registry, const nlohmann::json& sim)
1241 {
1242 if (!sim.contains("platforms"))
1243 {
1244 return;
1245 }
1246
1247 for (const auto& platform : sim.at("platforms"))
1248 {
1251 }
1252 }
1253
1254 void validate_unique_names(const nlohmann::json& sim)
1255 {
1257 name_registry.reserve(64);
1258 register_json_name_array(name_registry, sim, "waveforms", "waveform");
1259 register_json_name_array(name_registry, sim, "timings", "timing");
1260 register_json_name_array(name_registry, sim, "antennas", "antenna");
1262 }
1263
1264 /// Counts operation mode blocks on a component.
1265 std::size_t mode_block_count(const nlohmann::json& comp_json)
1266 {
1267 return static_cast<std::size_t>(comp_json.contains("pulsed_mode")) +
1268 static_cast<std::size_t>(comp_json.contains("fmcw_mode")) +
1269 static_cast<std::size_t>(comp_json.contains("cw_mode"));
1270 }
1271
1272 /// Throws when a partial update or full component declares conflicting modes.
1273 void reject_conflicting_mode_blocks(const nlohmann::json& comp_json, const std::string& error_context)
1274 {
1275 if (mode_block_count(comp_json) > 1)
1276 {
1277 throw std::runtime_error(error_context +
1278 " must have at most one of 'pulsed_mode', 'cw_mode', or 'fmcw_mode'.");
1279 }
1280 }
1281
1282 /// Parses the mutually exclusive operation mode block for a component.
1283 radar::OperationMode parse_mode(const nlohmann::json& comp_json, const std::string& error_context)
1284 {
1286 if (comp_json.contains("pulsed_mode"))
1287 {
1289 }
1290 if (comp_json.contains("fmcw_mode"))
1291 {
1293 }
1294 if (comp_json.contains("cw_mode"))
1295 {
1297 }
1298 throw std::runtime_error(error_context + " must have a 'pulsed_mode', 'cw_mode', or 'fmcw_mode' block.");
1299 }
1300
1301 /// Parses a transmitter component from JSON into the world.
1302 void parse_transmitter(const nlohmann::json& comp_json, radar::Platform* plat, core::World& world,
1303 std::mt19937& masterSeeder, TimingInstanceMap& timing_instances)
1304 {
1305 // --- Dependency Check ---
1306 // Validate Waveform and Timing existence before creation to prevent core crashes.
1307 const auto wave_id = parse_json_id(comp_json, "waveform", "Transmitter");
1308 const auto timing_id = parse_json_id(comp_json, "timing", "Transmitter");
1309 const auto antenna_id = parse_json_id(comp_json, "antenna", "Transmitter");
1310
1311 if (world.findWaveform(wave_id) == nullptr)
1312 {
1313 LOG(logging::Level::WARNING, "Skipping Transmitter '{}': Missing or invalid waveform '{}'.",
1314 comp_json.value("name", "Unnamed"), json_field_for_log(comp_json, "waveform"));
1315 return;
1316 }
1317 if (world.findTiming(timing_id) == nullptr)
1318 {
1319 LOG(logging::Level::WARNING, "Skipping Transmitter '{}': Missing or invalid timing source '{}'.",
1320 comp_json.value("name", "Unnamed"), json_field_for_log(comp_json, "timing"));
1321 return;
1322 }
1323 if (world.findAntenna(antenna_id) == nullptr)
1324 {
1325 LOG(logging::Level::WARNING, "Skipping Transmitter '{}': Missing or invalid antenna '{}'.",
1326 comp_json.value("name", "Unnamed"), json_field_for_log(comp_json, "antenna"));
1327 return;
1328 }
1329
1330 radar::OperationMode const mode =
1331 parse_mode(comp_json, "Transmitter component '" + comp_json.value("name", "Unnamed") + "'");
1332 if (mode == radar::OperationMode::FMCW_MODE && comp_json.contains("fmcw_mode") &&
1333 has_dechirp_fields(comp_json.at("fmcw_mode")))
1334 {
1335 throw std::runtime_error("Transmitter component '" + comp_json.value("name", "Unnamed") +
1336 "' fmcw_mode must not contain dechirp configuration.");
1337 }
1338
1339 const auto trans_id = parse_json_id(comp_json, "id", "Transmitter");
1340 auto trans = std::make_unique<radar::Transmitter>(plat, comp_json.value("name", "Unnamed"), mode, trans_id);
1341 if (mode == radar::OperationMode::PULSED_MODE && comp_json.contains("pulsed_mode"))
1342 {
1343 trans->setPrf(comp_json.at("pulsed_mode").value("prf", 0.0));
1344 }
1345
1346 auto* const waveform = world.findWaveform(wave_id);
1347 validate_fmcw_waveform(*waveform, "Waveform '" + waveform->getName() + "'");
1348 validate_waveform_mode_match(*waveform, mode,
1349 "Transmitter component '" + comp_json.value("name", "Unnamed") + "'");
1350 trans->setWave(waveform);
1351 trans->setAntenna(world.findAntenna(antenna_id));
1352
1353 if (const auto timing = resolve_timing_instance(world, masterSeeder, timing_instances, timing_id))
1354 {
1355 trans->setTiming(timing);
1356 }
1357
1358 if (comp_json.contains("schedule"))
1359 {
1360 auto raw = comp_json.at("schedule").get<std::vector<radar::SchedulePeriod>>();
1361 RealType pri = 0.0;
1363 {
1364 pri = 1.0 / trans->getPrf();
1365 }
1366 auto schedule =
1368 if (waveform->isFmcwFamily())
1369 {
1370 validate_fmcw_schedule(schedule, *waveform, "Transmitter component '" + trans->getName() + "'");
1371 }
1372 trans->setSchedule(std::move(schedule));
1373 }
1374 else if (waveform->isFmcwFamily())
1375 {
1376 validate_fmcw_schedule(trans->getSchedule(), *waveform, "Transmitter component '" + trans->getName() + "'");
1377 }
1378
1379 world.add(std::move(trans));
1380 }
1381
1382 /// Parses a receiver component from JSON into the world.
1383 void parse_receiver(const nlohmann::json& comp_json, radar::Platform* plat, core::World& world,
1384 std::mt19937& masterSeeder, TimingInstanceMap& timing_instances)
1385 {
1386 // --- Dependency Check ---
1387 // Receiver strictly requires a Timing source.
1388 const auto timing_id = parse_json_id(comp_json, "timing", "Receiver");
1389 const auto antenna_id = parse_json_id(comp_json, "antenna", "Receiver");
1390
1391 if (world.findTiming(timing_id) == nullptr)
1392 {
1393 LOG(logging::Level::WARNING, "Skipping Receiver '{}': Missing or invalid timing source '{}'.",
1394 comp_json.value("name", "Unnamed"), json_field_for_log(comp_json, "timing"));
1395 return;
1396 }
1397
1398 if (world.findAntenna(antenna_id) == nullptr)
1399 {
1400 LOG(logging::Level::WARNING, "Skipping Receiver '{}': Missing or invalid antenna '{}'.",
1401 comp_json.value("name", "Unnamed"), json_field_for_log(comp_json, "antenna"));
1402 return;
1403 }
1404
1405 radar::OperationMode const mode =
1406 parse_mode(comp_json, "Receiver component '" + comp_json.value("name", "Unnamed") + "'");
1407
1408 const auto recv_id = parse_json_id(comp_json, "id", "Receiver");
1409 auto recv =
1410 std::make_unique<radar::Receiver>(plat, comp_json.value("name", "Unnamed"), masterSeeder(), mode, recv_id);
1411 if (mode == radar::OperationMode::PULSED_MODE && comp_json.contains("pulsed_mode"))
1412 {
1413 const auto& mode_json = comp_json.at("pulsed_mode");
1414 recv->setWindowProperties(mode_json.value("window_length", 0.0), mode_json.value("prf", 0.0),
1415 mode_json.value("window_skip", 0.0));
1416 }
1417
1418 recv->setNoiseTemperature(comp_json.value("noise_temp", 0.0));
1419
1420 recv->setAntenna(world.findAntenna(antenna_id));
1421
1422 if (const auto timing = resolve_timing_instance(world, masterSeeder, timing_instances, timing_id))
1423 {
1424 recv->setTiming(timing);
1425 }
1426
1427 if (comp_json.value("nodirect", false))
1428 {
1430 }
1431 if (comp_json.value("nopropagationloss", false))
1432 {
1434 }
1435
1436 if (comp_json.contains("schedule"))
1437 {
1438 auto raw = comp_json.at("schedule").get<std::vector<radar::SchedulePeriod>>();
1439 RealType pri = 0.0;
1441 {
1442 pri = 1.0 / recv->getWindowPrf();
1443 }
1444 recv->setSchedule(
1446 }
1447
1449 "Receiver component '" + comp_json.value("name", "Unnamed") + "'");
1450 world.add(std::move(recv));
1451 }
1452
1453 /// Parses a target component from JSON into the world.
1454 void parse_target(const nlohmann::json& comp_json, radar::Platform* plat, core::World& world,
1455 std::mt19937& masterSeeder)
1456 {
1457 const auto& rcs_json = comp_json.at("rcs");
1458 const auto rcs_type = rcs_json.at("type").get<std::string>();
1459 std::unique_ptr<radar::Target> target_obj;
1460
1461 if (rcs_type == "isotropic")
1462 {
1463 const auto target_id = parse_json_id(comp_json, "id", "Target");
1464 target_obj = radar::createIsoTarget(plat, comp_json.at("name").get<std::string>(),
1465 rcs_json.at("value").get<RealType>(),
1466 static_cast<unsigned>(masterSeeder()), target_id);
1467 }
1468 else if (rcs_type == "file")
1469 {
1470 const auto filename = rcs_json.value("filename", "");
1471 if (filename.empty())
1472 {
1473 LOG(logging::Level::WARNING, "Skipping load of file target '{}': RCS filename is empty.",
1474 comp_json.value("name", "Unknown"));
1475 return;
1476 }
1477 const auto target_id = parse_json_id(comp_json, "id", "Target");
1478 target_obj = radar::createFileTarget(plat, comp_json.at("name").get<std::string>(), filename,
1479 static_cast<unsigned>(masterSeeder()), target_id);
1480 }
1481 else
1482 {
1483 throw std::runtime_error("Unsupported target RCS type: " + rcs_type);
1484 }
1485 world.add(std::move(target_obj));
1486
1487 // After creating the target, check for and apply the fluctuation model.
1488 if (comp_json.contains("model"))
1489 {
1490 const auto& model_json = comp_json.at("model");
1491 const auto model_type = model_json.at("type").get<std::string>();
1492
1493 if (model_type == "chisquare" || model_type == "gamma")
1494 {
1495 auto model = std::make_unique<radar::RcsChiSquare>(world.getTargets().back()->getRngEngine(),
1496 model_json.at("k").get<RealType>());
1497 world.getTargets().back()->setFluctuationModel(std::move(model));
1498 }
1499 else if (model_type == "constant")
1500 {
1501 world.getTargets().back()->setFluctuationModel(std::make_unique<radar::RcsConst>());
1502 }
1503 else
1504 {
1505 throw std::runtime_error("Unsupported fluctuation model type: " + model_type);
1506 }
1507 }
1508 }
1509
1510 /// Parses a monostatic component into linked transmitter and receiver objects.
1511 void parse_monostatic(const nlohmann::json& comp_json, radar::Platform* plat, core::World& world,
1512 std::mt19937& masterSeeder, TimingInstanceMap& timing_instances)
1513 {
1514 // This block reconstructs the internal C++ representation of a
1515 // monostatic radar (a linked Transmitter and Receiver) from the
1516 // single 'monostatic' component in the JSON.
1517 // --- Dependency Check ---
1518 const auto wave_id = parse_json_id(comp_json, "waveform", "Monostatic");
1519 const auto timing_id = parse_json_id(comp_json, "timing", "Monostatic");
1520 const auto antenna_id = parse_json_id(comp_json, "antenna", "Monostatic");
1521
1522 if (world.findWaveform(wave_id) == nullptr)
1523 {
1524 LOG(logging::Level::WARNING, "Skipping Monostatic '{}': Missing or invalid waveform '{}'.",
1525 comp_json.value("name", "Unnamed"), json_field_for_log(comp_json, "waveform"));
1526 return;
1527 }
1528 if (world.findTiming(timing_id) == nullptr)
1529 {
1530 LOG(logging::Level::WARNING, "Skipping Monostatic '{}': Missing or invalid timing source '{}'.",
1531 comp_json.value("name", "Unnamed"), json_field_for_log(comp_json, "timing"));
1532 return;
1533 }
1534 if (world.findAntenna(antenna_id) == nullptr)
1535 {
1536 LOG(logging::Level::WARNING, "Skipping Monostatic '{}': Missing or invalid antenna '{}'.",
1537 comp_json.value("name", "Unnamed"), json_field_for_log(comp_json, "antenna"));
1538 return;
1539 }
1540
1541 radar::OperationMode const mode =
1542 parse_mode(comp_json, "Monostatic component '" + comp_json.value("name", "Unnamed") + "'");
1543
1544 // Transmitter part
1545 const auto tx_id = parse_json_id(comp_json, "tx_id", "Monostatic");
1546 auto trans = std::make_unique<radar::Transmitter>(plat, comp_json.value("name", "Unnamed"), mode, tx_id);
1547 if (mode == radar::OperationMode::PULSED_MODE && comp_json.contains("pulsed_mode"))
1548 {
1549 trans->setPrf(comp_json.at("pulsed_mode").value("prf", 0.0));
1550 }
1551
1552 auto* const waveform = world.findWaveform(wave_id);
1553 validate_fmcw_waveform(*waveform, "Waveform '" + waveform->getName() + "'");
1554 validate_waveform_mode_match(*waveform, mode,
1555 "Monostatic component '" + comp_json.value("name", "Unnamed") + "'");
1556 trans->setWave(waveform);
1557 trans->setAntenna(world.findAntenna(antenna_id));
1558 if (const auto shared_timing = resolve_timing_instance(world, masterSeeder, timing_instances, timing_id))
1559 {
1560 trans->setTiming(shared_timing);
1561 }
1562
1563 // Receiver part
1564 const auto rx_id = parse_json_id(comp_json, "rx_id", "Monostatic");
1565 auto recv =
1566 std::make_unique<radar::Receiver>(plat, comp_json.value("name", "Unnamed"), masterSeeder(), mode, rx_id);
1567 if (mode == radar::OperationMode::PULSED_MODE && comp_json.contains("pulsed_mode"))
1568 {
1569 const auto& mode_json = comp_json.at("pulsed_mode");
1570 recv->setWindowProperties(mode_json.value("window_length", 0.0),
1571 trans->getPrf(), // Use transmitter's PRF
1572 mode_json.value("window_skip", 0.0));
1573 }
1574 recv->setNoiseTemperature(comp_json.value("noise_temp", 0.0));
1575
1576 recv->setAntenna(world.findAntenna(antenna_id));
1577 if (const auto shared_timing = resolve_timing_instance(world, masterSeeder, timing_instances, timing_id))
1578 {
1579 recv->setTiming(shared_timing);
1580 }
1581
1582 if (comp_json.value("nodirect", false))
1583 {
1585 }
1586 if (comp_json.value("nopropagationloss", false))
1587 {
1589 }
1590 if (comp_json.contains("schedule"))
1591 {
1592 auto raw = comp_json.at("schedule").get<std::vector<radar::SchedulePeriod>>();
1593 RealType pri = 0.0;
1595 {
1596 pri = 1.0 / trans->getPrf();
1597 }
1598
1599 // Process once, apply to both
1600 auto processed_schedule =
1602 if (waveform->isFmcwFamily())
1603 {
1605 "Monostatic component '" + comp_json.value("name", "Unnamed") + "'");
1606 }
1607
1608 trans->setSchedule(processed_schedule);
1609 recv->setSchedule(processed_schedule);
1610 }
1611 else if (waveform->isFmcwFamily())
1612 {
1613 validate_fmcw_schedule(trans->getSchedule(), *waveform,
1614 "Monostatic component '" + comp_json.value("name", "Unnamed") + "'");
1615 }
1616
1617 // Link them and add to world
1618 trans->setAttached(recv.get());
1619 recv->setAttached(trans.get());
1621 "Monostatic component '" + comp_json.value("name", "Unnamed") + "'");
1622 world.add(std::move(trans));
1623 world.add(std::move(recv));
1624 }
1625
1626 /// Parses a platform and its component list from JSON into the world.
1627 void parse_platform(const nlohmann::json& plat_json, core::World& world, std::mt19937& masterSeeder,
1628 TimingInstanceMap& timing_instances)
1629 {
1630 auto name = plat_json.at("name").get<std::string>();
1631 const auto platform_id = parse_json_id(plat_json, "id", "Platform");
1632 auto plat = std::make_unique<radar::Platform>(name, platform_id);
1633
1635
1636 // Components - Strict array format
1637 if (plat_json.contains("components"))
1638 {
1639 for (const auto& comp_json_outer : plat_json.at("components"))
1640 {
1641 if (comp_json_outer.contains("transmitter"))
1642 {
1643 parse_transmitter(comp_json_outer.at("transmitter"), plat.get(), world, masterSeeder,
1644 timing_instances);
1645 }
1646 else if (comp_json_outer.contains("receiver"))
1647 {
1648 parse_receiver(comp_json_outer.at("receiver"), plat.get(), world, masterSeeder, timing_instances);
1649 }
1650 else if (comp_json_outer.contains("target"))
1651 {
1652 parse_target(comp_json_outer.at("target"), plat.get(), world, masterSeeder);
1653 }
1654 else if (comp_json_outer.contains("monostatic"))
1655 {
1656 parse_monostatic(comp_json_outer.at("monostatic"), plat.get(), world, masterSeeder,
1657 timing_instances);
1658 }
1659 }
1660 }
1661
1662 world.add(std::move(plat));
1663 }
1664
1665 void populate_world_from_json(const nlohmann::json& j, core::World& world, std::mt19937& masterSeeder)
1666 {
1667 world.clear();
1668
1669 const auto& sim = j.at("simulation");
1671
1673 parse_assets(sim, world);
1674
1675 if (sim.contains("platforms"))
1676 {
1677 TimingInstanceMap timing_instances;
1678 for (const auto& plat_json : sim.at("platforms"))
1679 {
1680 parse_platform(plat_json, world, masterSeeder, timing_instances);
1681 }
1682 }
1684
1685 world.scheduleInitialEvents();
1686 }
1687}
1688
1689namespace serial
1690{
1691 std::unique_ptr<antenna::Antenna> parse_antenna_from_json(const nlohmann::json& j)
1692 {
1693 std::unique_ptr<antenna::Antenna> ant;
1695 return ant;
1696 }
1697
1698 std::unique_ptr<fers_signal::RadarSignal> parse_waveform_from_json(const nlohmann::json& j)
1699 {
1700 std::unique_ptr<fers_signal::RadarSignal> wf;
1702 return wf;
1703 }
1704
1705 std::unique_ptr<timing::PrototypeTiming> parse_timing_from_json(const nlohmann::json& j, const SimId id)
1706 {
1707 auto timing = std::make_unique<timing::PrototypeTiming>(j.at("name").get<std::string>(), id);
1708 j.get_to(*timing);
1709 return timing;
1710 }
1711
1712 void update_parameters_from_json(const nlohmann::json& j, std::mt19937& masterSeeder)
1713 {
1714 nlohmann::json sim;
1715 sim["parameters"] = j;
1717 }
1718
1719 std::unique_ptr<antenna::Antenna> parse_required_update_antenna(const nlohmann::json& j)
1720 {
1722 if (parsed == nullptr)
1723 {
1724 const auto name = j.value("name", std::string{});
1725 const auto pattern = j.value("pattern", "isotropic");
1726 throw std::runtime_error("Cannot update antenna '" + name + "' to pattern '" + pattern +
1727 "' without a filename.");
1728 }
1729 return parsed;
1730 }
1731
1732 bool antenna_pattern_requires_replacement(const std::string_view pattern, const antenna::Antenna* ant) noexcept
1733 {
1734 if (pattern == "isotropic")
1735 {
1736 return dynamic_cast<const antenna::Isotropic*>(ant) == nullptr;
1737 }
1738 if (pattern == "sinc")
1739 {
1740 return dynamic_cast<const antenna::Sinc*>(ant) == nullptr;
1741 }
1742 if (pattern == "gaussian")
1743 {
1744 return dynamic_cast<const antenna::Gaussian*>(ant) == nullptr;
1745 }
1746 if (pattern == "squarehorn")
1747 {
1748 return dynamic_cast<const antenna::SquareHorn*>(ant) == nullptr;
1749 }
1750 if (pattern == "parabolic")
1751 {
1752 return dynamic_cast<const antenna::Parabolic*>(ant) == nullptr;
1753 }
1754 if (pattern == "xml")
1755 {
1756 return dynamic_cast<const antenna::XmlAntenna*>(ant) == nullptr;
1757 }
1758 if (pattern == "file")
1759 {
1760 return dynamic_cast<const antenna::H5Antenna*>(ant) == nullptr;
1761 }
1762 return false;
1763 }
1764
1766 {
1767 if (auto* sinc = dynamic_cast<antenna::Sinc*>(ant))
1768 {
1769 sinc->setAlpha(j.value("alpha", 1.0));
1770 sinc->setBeta(j.value("beta", 1.0));
1771 sinc->setGamma(j.value("gamma", 2.0));
1772 }
1773 else if (auto* gauss = dynamic_cast<antenna::Gaussian*>(ant))
1774 {
1775 gauss->setAzimuthScale(j.value("azscale", 1.0));
1776 gauss->setElevationScale(j.value("elscale", 1.0));
1777 }
1778 else if (auto* horn = dynamic_cast<antenna::SquareHorn*>(ant))
1779 {
1780 horn->setDimension(j.value("diameter", 0.5));
1781 }
1782 else if (auto* para = dynamic_cast<antenna::Parabolic*>(ant))
1783 {
1784 para->setDiameter(j.value("diameter", 0.5));
1785 }
1786 else if (auto* xml = dynamic_cast<antenna::XmlAntenna*>(ant))
1787 {
1788 if (xml->getFilename() != j.value("filename", ""))
1789 {
1791 }
1792 }
1793 else if (auto* h5 = dynamic_cast<antenna::H5Antenna*>(ant))
1794 {
1795 if (h5->getFilename() != j.value("filename", ""))
1796 {
1798 }
1799 }
1800 }
1801
1802 void update_antenna_from_json(const nlohmann::json& j, antenna::Antenna* ant, core::World& world)
1803 {
1804 const auto new_pattern = j.value("pattern", "isotropic");
1806 {
1808 return;
1809 }
1810
1811 ant->setName(j.at("name").get<std::string>());
1812 ant->setEfficiencyFactor(j.value("efficiency", 1.0));
1814 }
1815
1817 {
1818 if (j.contains("motionpath"))
1819 {
1820 auto path = std::make_unique<math::Path>();
1821 j.at("motionpath").get_to(*path);
1822 plat->setMotionPath(std::move(path));
1823 }
1824 if (j.contains("rotationpath"))
1825 {
1826 auto rot_path = std::make_unique<math::RotationPath>();
1827 const auto& rotation_json = j.at("rotationpath");
1828 rot_path->setInterp(rotation_json.at("interpolation").get<math::RotationPath::InterpType>());
1829 unsigned waypoint_index = 0;
1830 for (const auto& waypoint_json : rotation_json.at("rotationwaypoints"))
1831 {
1832 const RealType azimuth = waypoint_json.at("azimuth").get<RealType>();
1833 const RealType elevation = waypoint_json.at("elevation").get<RealType>();
1834 const RealType time = waypoint_json.at("time").get<RealType>();
1835 const std::string owner =
1836 std::format("platform '{}' rotation waypoint {}", plat->getName(), waypoint_index);
1837
1840 "JSON", owner, "azimuth");
1843 "JSON", owner, "elevation");
1844
1845 rot_path->addCoord(rotation_angle_utils::external_rotation_to_internal(azimuth, elevation, time,
1848 }
1849 rot_path->finalize();
1850 plat->setRotationPath(std::move(rot_path));
1851 }
1852 else if (j.contains("fixedrotation"))
1853 {
1854 auto rot_path = std::make_unique<math::RotationPath>();
1855 const auto& fixed_json = j.at("fixedrotation");
1856 const RealType start_az_deg = fixed_json.at("startazimuth").get<RealType>();
1857 const RealType start_el_deg = fixed_json.at("startelevation").get<RealType>();
1858 const RealType rate_az_deg_s = fixed_json.at("azimuthrate").get<RealType>();
1859 const RealType rate_el_deg_s = fixed_json.at("elevationrate").get<RealType>();
1860 const std::string owner = std::format("platform '{}' fixedrotation", plat->getName());
1861
1864 owner, "startazimuth");
1867 owner, "startelevation");
1870 owner, "azimuthrate");
1873 owner, "elevationrate");
1874
1879 rot_path->setConstantRate(start, rate);
1880 rot_path->finalize();
1881 plat->setRotationPath(std::move(rot_path));
1882 }
1883 }
1884
1886 {
1887 reject_conflicting_mode_blocks(j, "Transmitter '" + tx.getName() + "'");
1888 if (j.contains("pulsed_mode"))
1889 {
1891 tx.setPrf(j.at("pulsed_mode").value("prf", 0.0));
1892 }
1893 else if (j.contains("fmcw_mode"))
1894 {
1895 if (has_dechirp_fields(j.at("fmcw_mode")))
1896 {
1897 throw std::runtime_error("Transmitter '" + tx.getName() +
1898 "' fmcw_mode must not contain dechirp configuration.");
1899 }
1901 }
1902 else if (j.contains("cw_mode"))
1903 {
1905 }
1906 }
1907
1909 {
1910 if (!j.contains("waveform"))
1911 {
1912 return;
1913 }
1914 auto id = parse_json_id(j, "waveform", "Transmitter");
1915 auto* wf = world.findWaveform(id);
1916 if (wf == nullptr)
1917 {
1918 throw std::runtime_error("Waveform ID " + std::to_string(id) + " not found.");
1919 }
1920 validate_fmcw_waveform(*wf, "Waveform '" + wf->getName() + "'");
1921 validate_waveform_mode_match(*wf, tx.getMode(), "Transmitter '" + tx.getName() + "'");
1922 tx.setWave(wf);
1923 }
1924
1926 {
1927 if (!j.contains("antenna"))
1928 {
1929 return;
1930 }
1931 auto id = parse_json_id(j, "antenna", "Transmitter");
1932 auto* ant = world.findAntenna(id);
1933 if (ant == nullptr)
1934 {
1935 throw std::runtime_error("Antenna ID " + std::to_string(id) + " not found.");
1936 }
1937 tx.setAntenna(ant);
1938 }
1939
1941 {
1942 if (!j.contains("timing"))
1943 {
1944 return;
1945 }
1946 auto timing_id = parse_json_id(j, "timing", "Transmitter");
1947 auto* const timing_proto = world.findTiming(timing_id);
1948 if (timing_proto == nullptr)
1949 {
1950 throw std::runtime_error("Timing ID " + std::to_string(timing_id) + " not found.");
1951 }
1952 unsigned const seed = tx.getTiming() ? tx.getTiming()->getSeed() : 0;
1953 auto timing = std::make_shared<timing::Timing>(timing_proto->getName(), seed, timing_proto->getId());
1954 timing->initializeModel(timing_proto);
1955 tx.setTiming(timing);
1956 }
1957
1959 {
1960 if (tx.getSignal() == nullptr)
1961 {
1962 return;
1963 }
1964 validate_fmcw_waveform(*tx.getSignal(), "Waveform '" + tx.getSignal()->getName() + "'");
1965 validate_waveform_mode_match(*tx.getSignal(), tx.getMode(), owner);
1966 if (tx.getSignal()->isFmcwFamily())
1967 {
1968 validate_fmcw_schedule(tx.getSchedule(), *tx.getSignal(), owner);
1969 }
1970 }
1971
1973 const std::string& owner)
1974 {
1975 if (!j.contains("schedule"))
1976 {
1977 return;
1978 }
1979 auto raw = j.at("schedule").get<std::vector<radar::SchedulePeriod>>();
1980 const bool pulsed = tx.getMode() == radar::OperationMode::PULSED_MODE;
1981 const RealType pri = pulsed ? 1.0 / tx.getPrf() : 0.0;
1982 auto schedule = radar::processRawSchedule(raw, tx.getName(), pulsed, pri);
1983 if (tx.getSignal() != nullptr && tx.getSignal()->isFmcwFamily())
1984 {
1985 validate_fmcw_schedule(schedule, *tx.getSignal(), owner);
1986 }
1987 tx.setSchedule(std::move(schedule));
1988 }
1989
1991 std::mt19937& /*masterSeeder*/)
1992 {
1993 if (j.contains("name"))
1994 tx->setName(j.at("name").get<std::string>());
1995
1996 const std::string owner = "Transmitter '" + tx->getName() + "'";
2003 }
2004
2006 {
2007 reject_conflicting_mode_blocks(j, "Receiver '" + rx.getName() + "'");
2008 if (j.contains("pulsed_mode"))
2009 {
2011 const auto& mode_json = j.at("pulsed_mode");
2012 rx.setWindowProperties(mode_json.value("window_length", 0.0), mode_json.value("prf", 0.0),
2013 mode_json.value("window_skip", 0.0));
2014 }
2015 else if (j.contains("fmcw_mode"))
2016 {
2018 }
2019 else if (j.contains("cw_mode"))
2020 {
2022 }
2023 }
2024
2026 {
2027 if (j.contains("noise_temp"))
2028 rx.setNoiseTemperature(j.value("noise_temp", 0.0));
2029
2030 if (j.contains("nodirect"))
2031 {
2032 if (j.value("nodirect", false))
2034 else
2036 }
2037 if (j.contains("nopropagationloss"))
2038 {
2039 if (j.value("nopropagationloss", false))
2041 else
2043 }
2044 }
2045
2047 {
2048 if (!j.contains("antenna"))
2049 {
2050 return;
2051 }
2052 auto id = parse_json_id(j, "antenna", "Receiver");
2053 auto* ant = world.findAntenna(id);
2054 if (ant == nullptr)
2055 {
2056 throw std::runtime_error("Antenna ID " + std::to_string(id) + " not found.");
2057 }
2058 rx.setAntenna(ant);
2059 }
2060
2062 {
2063 if (!j.contains("timing"))
2064 {
2065 return;
2066 }
2067 auto timing_id = parse_json_id(j, "timing", "Receiver");
2068 auto* const timing_proto = world.findTiming(timing_id);
2069 if (timing_proto == nullptr)
2070 {
2071 throw std::runtime_error("Timing ID " + std::to_string(timing_id) + " not found.");
2072 }
2073 unsigned const seed = rx.getTiming() ? rx.getTiming()->getSeed() : 0;
2074 auto timing = std::make_shared<timing::Timing>(timing_proto->getName(), seed, timing_proto->getId());
2075 timing->initializeModel(timing_proto);
2076 rx.setTiming(timing);
2077 }
2078
2080 {
2081 if (!j.contains("schedule"))
2082 {
2083 return;
2084 }
2085 auto raw = j.at("schedule").get<std::vector<radar::SchedulePeriod>>();
2086 const bool pulsed = rx.getMode() == radar::OperationMode::PULSED_MODE;
2087 const RealType pri = pulsed ? 1.0 / rx.getWindowPrf() : 0.0;
2088 rx.setSchedule(radar::processRawSchedule(raw, rx.getName(), pulsed, pri));
2089 }
2090
2091 void update_receiver_from_json(const nlohmann::json& j, radar::Receiver* rx, core::World& world,
2092 std::mt19937& /*masterSeeder*/)
2093 {
2094 if (j.contains("name"))
2095 rx->setName(j.at("name").get<std::string>());
2096
2102 if (j.contains("fmcw_mode"))
2103 {
2104 parse_receiver_dechirp_config(j, *rx, "Receiver '" + rx->getName() + "'");
2105 }
2107 }
2108
2109 nlohmann::json monostatic_transmitter_json(const nlohmann::json& j)
2110 {
2111 auto transmitter_json = j;
2112 if (transmitter_json.contains("fmcw_mode") && transmitter_json.at("fmcw_mode").is_object())
2113 {
2114 transmitter_json["fmcw_mode"].erase("dechirp_mode");
2115 transmitter_json["fmcw_mode"].erase("dechirp_reference");
2116 transmitter_json["fmcw_mode"].erase("if_sample_rate");
2117 transmitter_json["fmcw_mode"].erase("if_filter_bandwidth");
2118 transmitter_json["fmcw_mode"].erase("if_filter_transition_width");
2119 }
2120 return transmitter_json;
2121 }
2122
2124 core::World& world)
2125 {
2126 if (j.contains("name"))
2127 rx.setName(j.at("name").get<std::string>());
2128 rx.setMode(tx.getMode());
2129 if (rx.getMode() == radar::OperationMode::PULSED_MODE && j.contains("pulsed_mode"))
2130 {
2131 const auto& mode_json = j.at("pulsed_mode");
2132 rx.setWindowProperties(mode_json.value("window_length", 0.0), tx.getPrf(),
2133 mode_json.value("window_skip", 0.0));
2134 }
2136 if (j.contains("antenna"))
2137 {
2138 rx.setAntenna(world.findAntenna(parse_json_id(j, "antenna", "Monostatic")));
2139 }
2140 }
2141
2143 core::World& world)
2144 {
2145 if (!j.contains("timing"))
2146 {
2147 return;
2148 }
2149 auto timing_id = parse_json_id(j, "timing", "Monostatic");
2150 auto* const timing_proto = world.findTiming(timing_id);
2151 if (timing_proto == nullptr)
2152 {
2153 throw std::runtime_error("Timing ID " + std::to_string(timing_id) + " not found.");
2154 }
2155 unsigned const seed = rx.getTiming() ? rx.getTiming()->getSeed() : 0;
2156 auto shared_timing = std::make_shared<timing::Timing>(timing_proto->getName(), seed, timing_proto->getId());
2157 shared_timing->initializeModel(timing_proto);
2158 tx.setTiming(shared_timing);
2159 rx.setTiming(shared_timing);
2160 }
2161
2163 {
2164 if (!j.contains("schedule"))
2165 {
2166 return;
2167 }
2168 auto raw = j.at("schedule").get<std::vector<radar::SchedulePeriod>>();
2169 const bool pulsed = tx.getMode() == radar::OperationMode::PULSED_MODE;
2170 const RealType pri = pulsed ? 1.0 / tx.getPrf() : 0.0;
2171 auto processed_schedule = radar::processRawSchedule(raw, tx.getName(), pulsed, pri);
2172 if (tx.getSignal() != nullptr && tx.getSignal()->isFmcwFamily())
2173 {
2174 validate_fmcw_schedule(processed_schedule, *tx.getSignal(), "Monostatic '" + tx.getName() + "'");
2175 }
2176 tx.setSchedule(processed_schedule);
2177 rx.setSchedule(processed_schedule);
2178 }
2179
2181 core::World& world, std::mt19937& masterSeeder)
2182 {
2185
2189 validate_transmitter_signal_state(*tx, "Monostatic '" + tx->getName() + "'");
2190 if (j.contains("fmcw_mode"))
2191 {
2192 parse_receiver_dechirp_config(j, *rx, "Monostatic '" + tx->getName() + "'");
2193 }
2195 }
2196
2198 std::mt19937& /*masterSeeder*/)
2199 {
2200 auto* plat = existing_tgt->getPlatform();
2201 const auto& rcs_json = j.at("rcs");
2202 const auto rcs_type = rcs_json.at("type").get<std::string>();
2203 std::unique_ptr<radar::Target> target_obj;
2204
2205 const auto target_id = existing_tgt->getId();
2206 const auto name = j.value("name", existing_tgt->getName());
2207 unsigned const seed = existing_tgt->getSeed();
2208
2209 if (rcs_type == "isotropic")
2210 {
2211 target_obj = radar::createIsoTarget(plat, name, rcs_json.value("value", 1.0), seed, target_id);
2212 }
2213 else if (rcs_type == "file")
2214 {
2215 const auto filename = rcs_json.value("filename", "");
2217 }
2218 else
2219 {
2220 throw std::runtime_error("Unsupported target RCS type: " + rcs_type);
2221 }
2222
2223 if (j.contains("model"))
2224 {
2225 const auto& model_json = j.at("model");
2226 const auto model_type = model_json.at("type").get<std::string>();
2227 if (model_type == "chisquare" || model_type == "gamma")
2228 {
2229 auto model =
2230 std::make_unique<radar::RcsChiSquare>(target_obj->getRngEngine(), model_json.value("k", 1.0));
2231 target_obj->setFluctuationModel(std::move(model));
2232 }
2233 else if (model_type == "constant")
2234 {
2235 target_obj->setFluctuationModel(std::make_unique<radar::RcsConst>());
2236 }
2237 }
2238
2239 world.replace(std::move(target_obj));
2240 }
2241
2242 void update_timing_from_json(const nlohmann::json& j, core::World& world, const SimId id)
2243 {
2244 auto* existing = world.findTiming(id);
2245 if (existing == nullptr)
2246 {
2247 throw std::runtime_error("Timing ID " + std::to_string(id) + " not found.");
2248 }
2249
2250 auto patched = j;
2251 if (!patched.contains("name"))
2252 {
2253 patched["name"] = existing->getName();
2254 }
2255
2257 }
2258
2259 nlohmann::json world_to_json(const core::World& world)
2260 {
2261 nlohmann::json sim_json;
2262
2264 sim_json["parameters"] = params::params;
2265
2266 sim_json["waveforms"] = nlohmann::json::array();
2267 for (const auto& waveform : world.getWaveforms() | std::views::values)
2268 {
2269 sim_json["waveforms"].push_back(*waveform);
2270 }
2271
2272 sim_json["antennas"] = nlohmann::json::array();
2273 for (const auto& antenna : world.getAntennas() | std::views::values)
2274 {
2275 sim_json["antennas"].push_back(*antenna);
2276 }
2277
2278 sim_json["timings"] = nlohmann::json::array();
2279 for (const auto& timing : world.getTimings() | std::views::values)
2280 {
2281 sim_json["timings"].push_back(*timing);
2282 }
2283
2284 sim_json["platforms"] = nlohmann::json::array();
2285 for (const auto& p : world.getPlatforms())
2286 {
2287 sim_json["platforms"].push_back(serialize_platform(p.get(), world));
2288 }
2289
2290 return {{"simulation", sim_json}};
2291 }
2292
2293 void json_to_world(const nlohmann::json& j, core::World& world, std::mt19937& masterSeeder)
2294 {
2297 world.swap(parsed_world);
2298 }
2299}
Header file defining various types of antennas and their gain patterns.
const Transmitter & transmitter
const Receiver & receiver
Abstract base class representing an antenna.
SimId getId() const noexcept
Retrieves the unique ID of the antenna.
Represents a Gaussian-shaped antenna gain pattern.
Represents an antenna whose gain pattern is loaded from a HDF5 file.
Represents an isotropic antenna with uniform gain in all directions.
Represents a parabolic reflector antenna.
Represents a sinc function-based antenna gain pattern.
Represents a square horn antenna.
Represents an antenna whose gain pattern is defined by an XML file.
The World class manages the simulator environment.
Definition world.h:39
void scheduleInitialEvents()
Populates the event queue with the initial events for the simulation.
Definition world.cpp:439
void add(std::unique_ptr< radar::Platform > plat) noexcept
Adds a radar platform to the simulation world.
Definition world.cpp:110
void replace(std::unique_ptr< radar::Target > target)
Replaces an existing target, updating internal pointers.
Definition world.cpp:281
fers_signal::RadarSignal * findWaveform(const SimId id)
Finds a radar signal by ID.
Definition world.cpp:153
const std::vector< std::unique_ptr< radar::Target > > & getTargets() const noexcept
Retrieves the list of radar targets.
Definition world.h:226
const std::unordered_map< SimId, std::unique_ptr< antenna::Antenna > > & getAntennas() const noexcept
Retrieves the map of antennas.
Definition world.h:265
void clear() noexcept
Clears all objects and assets from the simulation world.
Definition world.cpp:407
void resolveReceiverDechirpReferences()
Resolves and validates receiver FMCW dechirp references after all components are loaded.
Definition world.cpp:552
const std::unordered_map< SimId, std::unique_ptr< fers_signal::RadarSignal > > & getWaveforms() const noexcept
Retrieves the map of radar signals (waveforms).
Definition world.h:256
timing::PrototypeTiming * findTiming(const SimId id)
Finds a timing source by ID.
Definition world.cpp:165
antenna::Antenna * findAntenna(const SimId id)
Finds an antenna by ID.
Definition world.cpp:159
const std::unordered_map< SimId, std::unique_ptr< timing::PrototypeTiming > > & getTimings() const noexcept
Retrieves the map of timing prototypes.
Definition world.h:275
void swap(World &other) noexcept
Exchanges all owned world state with another world.
Definition world.cpp:422
const std::vector< std::unique_ptr< radar::Platform > > & getPlatforms() const noexcept
Retrieves the list of platforms.
Definition world.h:216
Continuous-wave signal implementation.
Class representing a radar signal with associated properties.
SimId getId() const noexcept
Gets the unique ID of the radar signal.
Represents a path with coordinates and allows for various interpolation methods.
Definition path.h:31
InterpType
Types of interpolation supported by the Path class.
Definition path.h:37
@ INTERP_STATIC
Hold the first coordinate for all query times.
@ INTERP_LINEAR
Linearly interpolate between neighboring coordinates.
@ INTERP_CUBIC
Cubically interpolate between neighboring coordinates.
Manages rotational paths with different interpolation techniques.
InterpType
Enumeration for types of interpolation.
@ INTERP_STATIC
Hold the first rotation for all query times.
@ INTERP_LINEAR
Linearly interpolate between neighboring rotations.
@ INTERP_CONSTANT
Hold the most recent rotation sample.
@ INTERP_CUBIC
Cubically interpolate between neighboring rotations.
A class representing a vector in rectangular coordinates.
RealType x
The x component of the vector.
RealType z
The z component of the vector.
RealType y
The y component of the vector.
File-based radar target.
Definition target.h:226
Isotropic radar target.
Definition target.h:188
const std::string & getName() const noexcept
Retrieves the name of the object.
Definition object.h:79
Represents a simulation platform with motion and rotation paths.
Definition platform.h:32
const antenna::Antenna * getAntenna() const noexcept
Gets the antenna associated with this radar.
Definition radar_obj.h:93
std::shared_ptr< timing::Timing > getTiming() const
Retrieves the timing source for the radar.
Definition radar_obj.cpp:66
Chi-square distributed RCS model.
Definition target.h:82
Manages radar signal reception and response processing.
Definition receiver.h:47
@ Transmitter
Use a named transmitter.
@ Attached
Use the attached transmitter.
@ None
No reference configured.
@ Custom
Use a named top-level waveform with the receiver schedule.
DechirpMode
Receiver-side FMCW dechirping mode.
Definition receiver.h:63
@ None
Output raw pre-mix streaming IQ.
@ FLAG_NODIRECT
Disable direct-path reception.
@ FLAG_NOPROPLOSS
Disable propagation-loss scaling.
Base class for radar targets.
Definition target.h:118
SimId getId() const noexcept
Gets the unique ID of the target.
Definition target.h:153
const RcsModel * getFluctuationModel() const
Gets the RCS fluctuation model.
Definition target.h:167
Represents a radar transmitter system.
Definition transmitter.h:34
SimId getId() const noexcept
Retrieves the unique ID of the transmitter.
Definition transmitter.h:88
RealType getPrf() const noexcept
Retrieves the pulse repetition frequency (PRF).
Definition transmitter.h:65
fers_signal::RadarSignal * getSignal() const noexcept
Retrieves the radar signal currently being transmitted.
Definition transmitter.h:72
const std::vector< SchedulePeriod > & getSchedule() const noexcept
Retrieves the list of active transmission periods.
OperationMode getMode() const noexcept
Gets the operational mode of the transmitter.
Definition transmitter.h:95
Manages timing properties such as frequency, offsets, and synchronization.
double RealType
Type for real numbers.
Definition config.h:27
constexpr RealType EPSILON
Machine epsilon for real numbers.
Definition config.h:51
Coordinate and rotation structure operations.
Provides functions to serialize and deserialize the simulation world to/from JSON.
#define LOG(level,...)
Definition logging.h:19
void to_json(nlohmann::json &j, const Antenna &a)
void from_json(const nlohmann::json &j, std::unique_ptr< Antenna > &ant)
FmcwChirpDirection parseFmcwChirpDirection(const std::string_view direction)
Parses a schema chirp direction token.
void from_json(const nlohmann::json &j, std::unique_ptr< RadarSignal > &rs)
void to_json(nlohmann::json &j, const RadarSignal &rs)
std::string_view fmcwChirpDirectionToken(const FmcwChirpDirection direction) noexcept
Converts a chirp direction to the schema token.
@ WARNING
Warning level for potentially harmful situations.
@ INFO
Info level for informational messages.
Definition coord.h:18
NLOHMANN_JSON_SERIALIZE_ENUM(Path::InterpType, {{Path::InterpType::INTERP_STATIC, "static"}, {Path::InterpType::INTERP_LINEAR, "linear"}, {Path::InterpType::INTERP_CUBIC, "cubic"}}) void to_json(nlohmann
void to_json(nlohmann::json &j, const Vec3 &v)
void from_json(const nlohmann::json &j, Vec3 &v)
RealType endTime() noexcept
Get the end time for the simulation.
Definition parameters.h:109
RealType rate() noexcept
Get the rendering sample rate.
Definition parameters.h:121
RealType startTime() noexcept
Get the start time for the simulation.
Definition parameters.h:103
unsigned oversampleRatio() noexcept
Get the oversampling ratio.
Definition parameters.h:151
NLOHMANN_JSON_SERIALIZE_ENUM(CoordinateFrame, {{CoordinateFrame::ENU, "ENU"}, {CoordinateFrame::UTM, "UTM"}, {CoordinateFrame::ECEF, "ECEF"}}) NLOHMANN_JSON_SERIALIZE_ENUM(RotationAngleUnit
CoordinateFrame
Defines the coordinate systems supported for KML/geospatial export.
Definition parameters.h:31
@ UTM
Universal Transverse Mercator.
@ ENU
East-North-Up local tangent plane (default)
@ ECEF
Earth-Centered, Earth-Fixed.
RotationAngleUnit rotationAngleUnit() noexcept
Gets the external rotation angle unit.
Definition parameters.h:327
void validateOversampleRatio(const unsigned ratio)
Validates that an oversampling ratio is supported.
Definition parameters.h:164
RotationAngleUnit
Defines the units used at external rotation-path boundaries.
Definition parameters.h:42
@ Radians
Compass azimuth and elevation expressed in radians.
@ Degrees
Compass azimuth and elevation expressed in degrees.
Parameters params
Global simulation parameter state.
Definition parameters.h:85
std::string_view dechirpReferenceSourceToken(const Receiver::DechirpReferenceSource source) noexcept
Converts a dechirp reference source to its scenario token.
Definition receiver.cpp:76
std::unique_ptr< Target > createIsoTarget(Platform *platform, std::string name, RealType rcs, unsigned seed, const SimId id=0)
Creates an isotropic target.
Definition target.h:282
OperationMode
Defines the operational mode of a radar component.
Definition radar_obj.h:39
@ PULSED_MODE
The component operates in a pulsed mode.
@ CW_MODE
The component operates in a continuous-wave mode.
@ FMCW_MODE
The component operates in an FMCW streaming mode.
void to_json(nlohmann::json &j, const SchedulePeriod &p)
Receiver::DechirpReferenceSource parseDechirpReferenceSourceToken(const std::string_view token)
Parses a dechirp reference source scenario token.
Definition receiver.cpp:92
std::string_view dechirpModeToken(const Receiver::DechirpMode mode) noexcept
Converts a dechirp mode to its scenario token.
Definition receiver.cpp:45
void from_json(const nlohmann::json &j, SchedulePeriod &p)
std::unique_ptr< Target > createFileTarget(Platform *platform, std::string name, const std::string &filename, unsigned seed, const SimId id=0)
Creates a file-based target.
Definition target.h:297
Receiver::DechirpMode parseDechirpModeToken(const std::string_view token)
Parses a dechirp mode scenario token.
Definition receiver.cpp:59
std::vector< SchedulePeriod > processRawSchedule(const std::vector< SchedulePeriod > &periods, const std::string &ownerName, const bool isPulsed, const RealType pri)
Processes a raw list of schedule periods.
void validateWaveform(const fers_signal::RadarSignal &wave, const std::string &owner, const Thrower &throw_error)
Validates that a waveform is compatible with FMCW streaming constraints.
void validateSchedule(const std::vector< radar::SchedulePeriod > &schedule, const fers_signal::FmcwChirpSignal &fmcw, const std::string &owner, const Thrower &throw_error)
Validates that an FMCW waveform schedule can emit complete chirps.
void validateWaveformModeMatch(const fers_signal::RadarSignal &wave, const radar::OperationMode mode, const std::string &owner, const Thrower &throw_error)
Validates that a waveform and radar operation mode are compatible.
RealType internal_elevation_to_external(const RealType elevation, const params::RotationAngleUnit unit) noexcept
Converts an internal elevation angle to the external unit.
RealType internal_azimuth_rate_to_external(const RealType azimuth_rate, const params::RotationAngleUnit unit) noexcept
Converts an internal azimuth rate to the external compass convention.
math::RotationCoord external_rotation_to_internal(const RealType azimuth, const RealType elevation, const RealType time, const params::RotationAngleUnit unit) noexcept
Converts external compass azimuth/elevation into internal rotation coordinates.
RealType internal_elevation_rate_to_external(const RealType elevation_rate, const params::RotationAngleUnit unit) noexcept
Converts an internal elevation rate to the external unit.
RealType internal_azimuth_to_external(const RealType azimuth, const params::RotationAngleUnit unit) noexcept
Converts an internal azimuth angle to the external compass convention.
math::RotationCoord external_rotation_rate_to_internal(const RealType azimuth_rate, const RealType elevation_rate, const RealType time, const params::RotationAngleUnit unit) noexcept
Converts external compass azimuth/elevation rates into internal rotation rates.
void maybe_warn_about_rotation_value(const RealType value, const params::RotationAngleUnit declared_unit, const ValueKind kind, const std::string_view source, const std::string_view owner, const std::string_view field)
Emits or captures a warning when a rotation value likely uses the wrong unit.
void update_platform_paths_from_json(const nlohmann::json &j, radar::Platform *plat)
Updates a platform's motion and rotation paths from JSON.
void update_parameters_from_json(const nlohmann::json &j, std::mt19937 &masterSeeder)
Updates global simulation parameters from JSON.
void update_existing_antenna_pattern_fields(const nlohmann::json &j, antenna::Antenna *ant, core::World &world)
void update_transmitter_waveform_from_json(const nlohmann::json &j, radar::Transmitter &tx, core::World &world)
std::unique_ptr< antenna::Antenna > parse_required_update_antenna(const nlohmann::json &j)
void update_receiver_mode_from_json(const nlohmann::json &j, radar::Receiver &rx)
void json_to_world(const nlohmann::json &j, core::World &world, std::mt19937 &masterSeeder)
Deserializes a nlohmann::json object and reconstructs the simulation world.
void update_receiver_from_json(const nlohmann::json &j, radar::Receiver *rx, core::World &world, std::mt19937 &)
Updates a receiver from JSON without full context recreation.
void update_receiver_schedule_from_json(const nlohmann::json &j, radar::Receiver &rx)
bool antenna_pattern_requires_replacement(const std::string_view pattern, const antenna::Antenna *ant) noexcept
void update_timing_from_json(const nlohmann::json &j, core::World &world, const SimId id)
Updates a timing source from JSON without full context recreation.
void update_transmitter_schedule_from_json(const nlohmann::json &j, radar::Transmitter &tx, const std::string &owner)
std::unique_ptr< RadarSignal > loadWaveformFromFile(const std::string &name, const std::string &filename, const RealType power, const RealType carrierFreq, const SimId id)
Loads a radar waveform from a file and returns a RadarSignal object.
void update_monostatic_from_json(const nlohmann::json &j, radar::Transmitter *tx, radar::Receiver *rx, core::World &world, std::mt19937 &masterSeeder)
Updates a monostatic radar from JSON without full context recreation.
void update_monostatic_receiver_basics(const nlohmann::json &j, const radar::Transmitter &tx, radar::Receiver &rx, core::World &world)
void update_transmitter_timing_from_json(const nlohmann::json &j, radar::Transmitter &tx, core::World &world)
void update_monostatic_schedule_from_json(const nlohmann::json &j, radar::Transmitter &tx, radar::Receiver &rx)
void update_receiver_noise_and_flags_from_json(const nlohmann::json &j, radar::Receiver &rx)
void update_transmitter_from_json(const nlohmann::json &j, radar::Transmitter *tx, core::World &world, std::mt19937 &)
Updates a transmitter from JSON without full context recreation.
void update_antenna_from_json(const nlohmann::json &j, antenna::Antenna *ant, core::World &world)
Updates an antenna from JSON without full context recreation.
std::unique_ptr< antenna::Antenna > parse_antenna_from_json(const nlohmann::json &j)
Parses an Antenna from JSON.
void update_target_from_json(const nlohmann::json &j, radar::Target *existing_tgt, core::World &world, std::mt19937 &)
Updates a target from JSON without full context recreation.
nlohmann::json world_to_json(const core::World &world)
Serializes the entire simulation world into a nlohmann::json object.
void update_receiver_timing_from_json(const nlohmann::json &j, radar::Receiver &rx, core::World &world)
void update_monostatic_timing_from_json(const nlohmann::json &j, radar::Transmitter &tx, radar::Receiver &rx, core::World &world)
std::unique_ptr< timing::PrototypeTiming > parse_timing_from_json(const nlohmann::json &j, const SimId id)
Parses a timing prototype from JSON.
void validate_transmitter_signal_state(const radar::Transmitter &tx, const std::string &owner)
void update_receiver_antenna_from_json(const nlohmann::json &j, radar::Receiver &rx, core::World &world)
nlohmann::json monostatic_transmitter_json(const nlohmann::json &j)
void update_transmitter_mode_from_json(const nlohmann::json &j, radar::Transmitter &tx)
void update_transmitter_antenna_from_json(const nlohmann::json &j, radar::Transmitter &tx, core::World &world)
std::unique_ptr< fers_signal::RadarSignal > parse_waveform_from_json(const nlohmann::json &j)
Parses a Waveform from JSON.
void from_json(const nlohmann::json &j, PrototypeTiming &pt)
void to_json(nlohmann::json &j, const PrototypeTiming &pt)
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.
Classes for handling radar waveforms and signals.
Radar Receiver class for managing signal reception and response handling.
Defines the RotationPath class for handling rotational paths with different interpolation types.
uint64_t SimId
64-bit Unique Simulation ID.
Definition sim_id.h:18
math::Vec3 max
RealType c
RealType a
Represents a position in 3D space with an associated time.
Definition coord.h:24
Represents a rotation in terms of azimuth, elevation, and time.
Definition coord.h:72
Struct to hold simulation parameters.
Definition parameters.h:52
std::optional< unsigned > random_seed
Random seed for simulation.
Definition parameters.h:70
std::string simulation_name
The name of the simulation, from the XML.
Definition parameters.h:74
static constexpr RealType DEFAULT_C
Speed of light (m/s)
Definition parameters.h:53
Parsed and resolved dechirp reference details.
Definition receiver.h:80
Receiver-local FMCW IF-chain request parsed from scenario input.
Definition receiver.h:91
Represents a time period during which the transmitter is active.
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.